paseo-req-kit 1.9.0-beta.2 → 1.9.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,42 @@
2
2
 
3
3
  每个版本分为"使用者会看到的变化"和"升级须知"。升级步骤见 [docs/guide/安装与升级.md](docs/guide/安装与升级.md)。
4
4
 
5
+ ## 1.9.1
6
+
7
+ 1.9 的正式版(1.9.0 的发布任务因一个测试的时序问题失败,没有发布到 npm,内容相同,以 1.9.1 发布)。汇总 1.9.0-beta.1 到 beta.3 的全部内容(各节见下):本机的需求索引与使用统计、Paseo 插件、外部系统对接。
8
+
9
+ ### 使用者会看到的变化
10
+
11
+ - **使用统计**:`req stats` 看本机的需求数、交付周期、返工、等待时长、用时和用量;`--report` 生成给管理方的汇总和明细,`--export` / `--merge` 汇总多台机器。
12
+ - **Paseo 插件**(`req plugin install`):在 Paseo 的网页端、桌面版和手机上查看本机所有需求的进度、文档(按 Markdown 显示)和统计,确认方案、停止、继续、开始测试验证、同步到外部系统。可以设为只读。
13
+ - **外部系统对接**:配置 `integrations` 后,需求在团队的需求管理系统中自动建卡、跟着阶段改状态;卡片字段在确认需求说明时整理。`req integration test` 预演命令,`req sync` 补发。
14
+ - **`req api`**:给程序用的 JSON 接口,插件通过它工作。
15
+
16
+ ### 升级须知
17
+
18
+ - 从 1.8.0 升级:依次看下面 1.9.0-beta.1 到 beta.3 的升级须知。三项新能力都是可选的;升级后在项目目录运行一次 `req index --rebuild`,让之前运行过的需求出现在索引和插件中。
19
+ - 装过插件的,`req upgrade` 会自动把插件换成 1.9.1。
20
+ - 从 1.9.0-beta.3 升级:功能没有变化,只改了版本号和文档中的版本示例。
21
+
22
+ ## 1.9.0-beta.3
23
+
24
+ 外部系统对接:需求在团队的需求管理系统中自动建卡、跟着阶段改状态;插件中的文档按 Markdown 显示。
25
+
26
+ ### 使用者会看到的变化
27
+
28
+ - **外部系统对接**:配置 `integrations` 后,流水线开始时在需求管理系统中建一张卡,之后每到一个阶段改卡片的状态:开始开发、等待确认、研发完成、开始测试验证、完成、中断,对应哪个状态可以配置。卡片编号和链接显示在进度、完成报告和 Paseo 插件里。
29
+ - 对接的是本机已有的命令行工具,命令的写法写在团队配置里,换系统或系统改参数都不用升级 req-kit。
30
+ - 卡片的业务字段(优先级、需求类型等)由 AI 在确认需求说明时从需求说明中整理,和需求方一起确认后写进 `card.json`。
31
+ - 外部系统出错不影响流水线:失败的调用会自动补发,也可以运行 `req sync <需求>`,插件中有"同步到外部系统"。
32
+ - `req integration test [需求]` 显示每个事件会执行的完整命令,但不真正调用,配置时先用它检查。
33
+ - **插件**:文档按 Markdown 显示(标题、列表、表格、代码、链接),`decisions.tsv` 等显示为表格;需求详情中显示外部系统的卡片和链接。
34
+
35
+ ### 升级须知
36
+
37
+ - 对接是可选的,写法见《项目定制与运行规则》的"外部系统对接"。`integrations` 只能写在本机或团队配置中;项目可以在自己的配置中用 `integrationDefaults` 提供项目编号等取值。
38
+ - 状态文件新增可选字段 `external`,功能目录新增 `card.json`,格式版本仍为 1。
39
+ - 自定义了 `prompts.local/` 的不受影响;`/req` 的新需求手册在确认前多一步"卡片字段",没有配置对接时自动跳过。设计决定见 ADR 0028。
40
+
5
41
  ## 1.9.0-beta.2
6
42
 
7
43
  Paseo 插件:在 Paseo 的界面里查看和管理需求。
package/bin/req.mjs CHANGED
@@ -29,6 +29,8 @@ const HELP = `req-kit ${version}(${kitRoot})
29
29
  req digest [--notify] 项目的需求总览;--notify 同时发到外部通知(如企业 IM)
30
30
  req digest --schedule "0 9 * * 1"
31
31
  让 Paseo 按时间表发送需求总览(例子为每周一 9:00);取消:req digest --cancel <ID>
32
+ req sync <需求> 把需求同步到对接的外部系统(补发失败的调用、补建卡片、更新状态)
33
+ req integration test [需求] 只显示对接外部系统时会执行的命令,不真正调用
32
34
 
33
35
  使用情况(可以在任何目录运行):
34
36
  req stats [--since 30d] [--month 2026-10]
@@ -180,6 +182,12 @@ switch (command) {
180
182
  case "api":
181
183
  code = needTsx() ? node([tsxCli, path.join(kitRoot, "engine", "src", "api.ts"), ...rest]) : 1;
182
184
  break;
185
+ case "sync":
186
+ code = needTsx() ? node([tsxCli, path.join(kitRoot, "engine", "src", "integration-cmd.ts"), "sync", ...rest]) : 1;
187
+ break;
188
+ case "integration":
189
+ code = needTsx() ? node([tsxCli, path.join(kitRoot, "engine", "src", "integration-cmd.ts"), ...rest]) : 1;
190
+ break;
183
191
  case "plugin":
184
192
  code = node([path.join(kitRoot, "bin", "req-plugin.mjs"), ...rest]);
185
193
  break;
@@ -27,7 +27,7 @@
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> <需求> [--repo <目录>] [选项]` | `{ command, featureDir, ok, exitCode, message }`。在 `--repo`(默认当前目录)中执行与 `req <命令> <需求>` 完全相同的操作;`ok` 为 false 时退出码为 1 |
30
+ | `req api run <approve\|stop\|resume\|verify\|sync> <需求> [--repo <目录>] [选项]` | `{ command, featureDir, ok, exitCode, message }`。在 `--repo`(默认当前目录)中执行与 `req <命令> <需求>` 完全相同的操作;`ok` 为 false 时退出码为 1 |
31
31
 
32
32
  `<需求>` 的写法与 `req status` 相同:功能目录、目录名或名称中能唯一确定的一部分。需求在为它新建的工作区中开发时,`--repo` 写对话所在的目录或那个工作区都可以。
33
33
 
@@ -64,3 +64,4 @@
64
64
  | `steps` | 每个步骤一项:`{ key, stepId, title, phase, attempt, round, agentId, startedAt, endedAt, minutes, outcome, result, error, usage }`;`title` 是给人看的名称(如"代码评审(第 2 轮)"),`result` 是结论的文字(如"需要修改") |
65
65
  | `gate` | 质量门:状态文件中的 `gate` |
66
66
  | `joint` | 联合需求的其他仓库:`[{ name, slug, repo, sourceRepo, branch, workspaceId, gate }]` |
67
+ | `external` | 外部系统中的卡片(1.9.0 起):`[{ name, id, url, status, syncedAt, pending, lastError }]`;`pending` 是等待补发的调用数 |
@@ -67,6 +67,16 @@ spec.md 头部的"版本"与最新一节一致。
67
67
 
68
68
  1.8.0 起。缺陷类型的"复现与定位"写 `diagnosis.md`:现象与复现(复现测试或脚本的位置、命令、修复前的失败输出)、根因(带代码位置)、排除的其他原因、影响面、修复思路。评估类型的"调研评估"写 `assessment.md`(代替 `design.md`):结论摘要、现状、可选方案(含工作量的大小和主要工作项)、推荐、风险与未知、如果要做(需求草稿)。
69
69
 
70
+ ## card.json(外部系统的卡片字段)
71
+
72
+ 1.9.0 起。团队配置了外部系统对接(`integrations`)、且其中写了 `cardFields` 时,由 `/req` 在需求说明确认时和需求方一起确认后写入,与 `spec.md` 一起提交:
73
+
74
+ ```json
75
+ { "title": "<卡片标题,可省略,默认用需求说明的标题>", "body": "<卡片描述,可省略,默认用 spec.md 全文>", "fields": { "<字段>": "<值>" } }
76
+ ```
77
+
78
+ `fields` 的名称来自 `cardFields`,在命令参数中写作 `{card.<字段>}`。没有写的字段用 `defaults` / `integrationDefaults` 中同名的值,都没有时这个参数和它前面的选项一起省略。
79
+
70
80
  ## contract.md(联合需求的接口约定)
71
81
 
72
82
  1.8.0 起,联合需求(spec 的 `- 联合开发:`)的方案设计在主仓库写入:每个接口一节,路径和方法、请求字段(类型、是否必填、取值范围)、响应字段、错误码和提示、一两组真实的请求和响应示例。方案审核通过后冻结,各仓库按它同时开发:提供接口的一边用示例写接口测试,调用的一边用示例做测试替身。主仓库的需求说明、访谈记录、方案、接口约定和预览在实现前同步到其他仓库工作区的同名功能目录并提交;其他仓库的实现、评审和缺陷处理记录写在它们自己的功能目录中。
@@ -34,6 +34,7 @@
34
34
  | `codeBase` | object \| null | 可选,1.8.0 起。`attempt`、`commit`:这次尝试在改动任何代码之前的提交,质量门的基线和范围检查从这里比较。尝试开始时就已经写过代码的旧记录为 `commit: null` |
35
35
  | `joint` | array \| null | 可选,1.8.0 起。联合需求(spec 的 `- 联合开发:`)中其他仓库的工作区:`name`(`relatedRepos` 中的名称)、`slug`(步骤键的后缀,如 `implement@backend`)、`sourceRepo`(切出工作区的那份检出)、`repo`(工作区目录)、`workspaceId`、`branch`、`specHash`(最近一次同步过去的 spec),以及这个仓库自己的 `codeBase` 和 `gate`(格式同顶层)。见 ADR 0025 |
36
36
  | `approval` | object \| null | 可选,1.8.0 起。方案确认点:`attempt`(哪一次尝试的方案)、`status`(`waiting` / `approved`)、`at`。需求变更后的新尝试需要重新确认 |
37
+ | `external` | object \| null | 可选,1.9.0 起。外部系统中的卡片,按集成名称:`id`(卡片编号)、`url`、`status`(最近设置的状态)、`syncedAt`、`pending`(没有成功、等待补发的调用:`action` 为 `create` / `status`、`event`、`status`、`at`、`attempts`、`error`)。见 ADR 0028 |
37
38
  | `gate` | object \| null | 可选,1.8.0 起。质量门:`baseline`(`attempt`、`commit`、`at`、`results`:每项检查改动前 `pass` / `fail`),`runs`(每次运行:`key` 步骤键、`attempt`、`at`、`checks`(`name`、`result`:`pass` / `fail` / `skipped`、`failedBefore`)、`newFailures`、`fixRounds`、`passed`)。各次运行的完整输出在 `.pipeline/checks/<步骤键>/` |
38
39
 
39
40
  `schedules[].kind` 在 1.7.0 起还可能是 `verify-start`(`--verify-at` 预约的测试验证开始),1.8.0 起还可能是 `approval-continue`(方案确认超时后按方案继续,`designApproval.autoContinueHours`)。`history` 中每条步骤记录在 1.7.0 起有可选的 `phase`(`develop` / `verify`),用于按阶段统计用时与用量;没有时按研发阶段统计。
@@ -0,0 +1,22 @@
1
+ # 0028 外部系统对接:命令写法在配置里,内容由 AI 整理,调用是确定的
2
+
3
+ - 状态:已采纳(与 [0014](0014-notifier-slot.md) 的通知槽并存:通知只发消息,集成会回写)
4
+
5
+ ## 背景
6
+
7
+ 团队希望需求同时出现在自己的需求管理系统中:需求开始时建卡,阶段变化时改卡片状态,卡片编号能在需求、报告和插件里看到。对接的系统各不相同,通常提供本机命令行工具(例如需求管理系统的 CLI),参数会随系统升级变化。
8
+
9
+ ## 决定
10
+
11
+ - **命令写法写在本机或团队配置里,不写进 req-kit 的代码**:`integrations` 中每项写明工具、建卡和改状态的参数(带占位符)、从输出取卡片编号和链接的正则、各个流水线事件对应的卡片状态。换系统或系统改参数只改配置。项目配置不能写 `integrations`(它会运行命令),只能用 `integrationDefaults` 提供自己的取值,例如项目编号。
12
+ - **AI 整理内容,不决定怎么调用**:卡片的业务字段(`cardFields`,如优先级、需求类型、子系统)由 `/req` 在需求说明确认时从需求说明中整理,和需求方一起确认后写进 `card.json`。运行时引擎按配置把这些值填进参数并执行,每次行为一致、可以补发、不会重复建卡;不在每次调用时让 AI 读帮助、拼命令,避免结果不确定、在真实系统上做错操作和难以复现。配置本身可以请 AI 读工具的帮助起草,由人确认。
13
+ - **不经过 shell**:参数数组直接传给进程,标题和 Markdown 描述中的空格、换行、引号都原样传递。Windows 上 npm 安装的工具是 `.cmd` 包装,只有 cmd.exe 能运行,而 cmd.exe 会破坏这类参数,所以改用 node 直接运行包装里指向的脚本。
14
+ - **外部系统出错不影响流水线**:失败或超时的调用记在状态文件的 `external.<名称>.pending`,下一个事件时先按顺序补发;卡片没建好时状态排在后面;等待中的状态只留最新的一个。流水线没在运行时可以 `req sync`。
15
+ - **先试再用**:`req integration test` 打印每个事件会执行的完整命令而不调用。
16
+ - 录入测试用例暂不做,等对接系统提供能力后,作为新的动作和事件加入(例如功能验证结束时把验收标准和结果录入)。
17
+
18
+ ## 影响
19
+
20
+ - 新的状态文件字段 `external`(可选,格式版本仍为 1)、功能目录文件 `card.json`、`req api status` 的 `external`。
21
+ - `/req` 在确认需求说明前运行"卡片字段"命令;没有配置对接时它输出"跳过"。
22
+ - 事件:started、approval、blocked、phase_done、verify、completed(可按结论细分)、failed。
@@ -29,3 +29,4 @@ req-kit 自身的设计决定。修改其中任何一条,请新增一条 ADR
29
29
  - [0025 联合需求:一个需求同时改几个仓库](0025-joint-requirements.md)
30
30
  - [0026 本机的需求索引、使用记录和 req api](0026-machine-records-and-api.md)
31
31
  - [0027 Paseo 插件:随 req-kit 一起发布,只通过 req api 工作](0027-paseo-plugin.md)
32
+ - [0028 外部系统对接:命令写法在配置里,内容由 AI 整理,调用是确定的](0028-external-integrations.md)
@@ -86,19 +86,19 @@ req doctor --no-project # 体检
86
86
  **第一步:取得程序。** 把要安装的版本放进它自己的版本目录:
87
87
 
88
88
  ```bash
89
- git clone --branch 1.8.0 --depth 1 https://github.com/yiyisf/paseo-req-kit ~/tools/req-kit/versions/1.8.0
89
+ git clone --branch 1.9.1 --depth 1 https://github.com/yiyisf/paseo-req-kit ~/tools/req-kit/versions/1.9.1
90
90
  ```
91
91
 
92
92
  Windows PowerShell:
93
93
 
94
94
  ```powershell
95
- git clone --branch 1.8.0 --depth 1 https://github.com/yiyisf/paseo-req-kit "$env:USERPROFILE\tools\req-kit\versions\1.8.0"
95
+ git clone --branch 1.9.1 --depth 1 https://github.com/yiyisf/paseo-req-kit "$env:USERPROFILE\tools\req-kit\versions\1.9.1"
96
96
  ```
97
97
 
98
98
  **第二步:安装依赖并启用。**
99
99
 
100
100
  ```bash
101
- cd ~/tools/req-kit/versions/1.8.0
101
+ cd ~/tools/req-kit/versions/1.9.1
102
102
  npm install
103
103
  node install.mjs
104
104
  ```
@@ -126,6 +126,7 @@ node install.mjs --configure
126
126
  | `req notify-test` | 给外部通知发测试消息 |
127
127
  | `req digest` | 项目的需求总览;`--notify` 发到外部通知,`--schedule "0 9 * * 1"` 每周一 9 点自动发送 |
128
128
  | `req stats` | 本机的使用统计;`--report --month 2026-10` 生成汇报用的文件,`--export` / `--merge` 汇总多台机器 |
129
+ | `req sync <需求>` / `req integration test [需求]` | 外部系统对接:补发和试运行(见 [项目定制与运行规则](项目定制与运行规则.md) 的"外部系统对接") |
129
130
  | `req index --rebuild` | 把 1.9 之前运行过的需求登记进本机的需求索引(Paseo 插件据此列出所有需求) |
130
131
 
131
132
  `<需求>` 可以只写名称中能唯一确定的一部分,如 `sms-login`。`req help` 查看全部。不加 PATH 也可以用完整路径 `~/.req-kit/bin/req`(Windows 为 `%USERPROFILE%\.req-kit\bin\req.cmd`)。
@@ -239,12 +240,12 @@ node bin/req-doctor.mjs --no-project
239
240
  ```bash
240
241
  req upgrade --check # 只看有没有新版本
241
242
  req upgrade # 升级到最新版本(能支持本机 Paseo 的)
242
- req upgrade 1.8.0 # 升级到指定版本
243
+ req upgrade 1.9.1 # 升级到指定版本
243
244
  req upgrade --beta # 当前是正式版时,也考虑 beta 版
244
245
  req versions # 已安装的版本和 npm 上的版本
245
246
  ```
246
247
 
247
- - **用 npm 安装的**(1.7.0 起):从 npm 仓库读取已发布的版本,先读本机 Paseo daemon 的版本,跳过不支持它的 req-kit(每个版本都声明了支持的 Paseo 范围),装进 `versions/<版本>` 后切换。被跳过的版本会说明原因,例如"1.9.0 需要 Paseo >=0.12.0 <0.13.0,本机是 0.11.2"。读不到 daemon 版本时不筛选,升级后请运行 `req doctor`。
248
+ - **用 npm 安装的**(1.7.0 起):从 npm 仓库读取已发布的版本,先读本机 Paseo daemon 的版本,跳过不支持它的 req-kit(每个版本都声明了支持的 Paseo 范围),装进 `versions/<版本>` 后切换。被跳过的版本会说明原因,例如"1.10.0 需要 Paseo >=0.12.0 <0.13.0,本机是 0.11.2"。读不到 daemon 版本时不筛选,升级后请运行 `req doctor`。
248
249
  - **用 git 安装的**:读取 git 标签,下载到 `versions/<版本>`,执行 `npm install`,再切换。公司网络只能访问内网镜像时,用 `--from <镜像地址>` 或环境变量 `REQ_KIT_REPO_URL` 指定。想改用 npm 升级时加 `--from npm`。
249
250
  - 当前是正式版时只升级到正式版;当前是 beta 时也会升级到更新的 beta。任何一步失败,当前版本不受影响。
250
251
  - 也可以按 Paseo 版本直接安装管理器:`npm i -g paseo-req-kit@paseo-0.11`(每个支持的 Paseo 小版本有一个这样的标签)。
@@ -285,6 +285,38 @@ Paseo 自带的 `/paseo-advisor`、`/paseo-committee` 等编排 skill 也可以
285
285
 
286
286
  本机的需求索引 `~/.req-kit/index/` 记录每个运行过的需求在哪个目录,Paseo 插件据此在一个页面列出本机所有项目的需求。1.9 之前运行过的需求不在索引里,在项目目录运行一次 `req index --rebuild` 补登;目录删掉后用 `req index --prune` 清理。
287
287
 
288
+ ## 外部系统对接(需求管理等)
289
+
290
+ 1.9.0 起,流水线可以把需求同步到团队的需求管理系统:需求开始时建一张卡片,之后每到一个阶段改卡片的状态,卡片编号和链接写回需求,显示在进度、完成报告和 Paseo 插件里。对接的是本机已有的命令行工具(例如需求管理系统的 CLI),写在本机或团队配置中(它会运行命令,项目配置中的会被忽略):
291
+
292
+ ```json
293
+ "integrations": [
294
+ {
295
+ "name": "ALM",
296
+ "command": "alm",
297
+ "create": {
298
+ "args": ["card", "create", "--title", "{title}", "--type", "{field.issueType}", "--project", "{field.projectId}", "--priority", "{card.priority}", "--body", "{body}"],
299
+ "id": "(?<id>[A-Z][A-Z0-9-]*#\\d+)",
300
+ "url": "(https?://\\S+)"
301
+ },
302
+ "status": { "args": ["card", "edit", "{id}", "--state", "{status}"] },
303
+ "statusMap": { "started": "开发中", "phase_done": "待测试", "verify": "测试中", "completed": "已完成", "not_completed": "已挂起" },
304
+ "cardFields": { "priority": "优先级:高 / 中 / 低" },
305
+ "defaults": { "issueType": "用户故事" }
306
+ }
307
+ ],
308
+ "integrationDefaults": { "ALM": { "projectId": "<团队的项目编号>" } }
309
+ ```
310
+
311
+ - **命令**:`args` 是参数数组,引擎不经过 shell 运行,标题或描述里有空格、换行、引号都不会出问题。Windows 上 npm 安装的命令(`.cmd`)会改用 node 直接运行它的脚本。
312
+ - **占位符**:`{title}`(`card.json` 的标题,否则需求说明的标题)、`{body}`(`card.json` 的描述,否则 `spec.md` 全文)、`{card.<字段>}`(`card.json` 中需求方确认过的字段)、`{field.<名称>}`(`defaults` 和项目配置的 `integrationDefaults`,项目可以只写自己的项目编号)、`{id}`、`{url}`、`{status}`、`{feature}`、`{branch}`、`{type}`、`{outcome}`。一个参数只是占位符且没有值时,它和前面的选项一起省略。
313
+ - **卡片编号**:`create.id` 是从建卡命令输出中取编号的正则表达式(取名为 `id` 的组或第一个组),`url` 同理,可以不写。
314
+ - **状态**:`statusMap` 的键是流水线事件:`started`(开始开发)、`approval`(等待确认方案)、`blocked`(等待需求方决定)、`phase_done`(研发完成,待测试)、`verify`(开始测试验证)、`completed`(完成;可以用 `completed_with_issues`、`not_completed` 细分)、`failed`(中断)。没写的事件不改状态。
315
+ - **卡片字段由 AI 整理、需求方确认**:写了 `cardFields` 时,`/req` 在请需求方确认需求说明时,从需求说明中整理出这些字段的取值一起确认,写进功能目录的 `card.json`。命令的写法固定在配置里,运行时不让 AI 决定怎么调用。
316
+ - **失败不影响流水线**:调用失败或超时(`timeoutSeconds`,默认 60)记在状态文件里,下一个事件时先按顺序补发;等待中的状态只留最新的一个,不会重复建卡。流水线没在运行时,`req sync <需求>` 立即补发(插件中也有"同步到外部系统")。
317
+ - **先试再用**:`req integration test [需求]` 显示每个事件会执行的完整命令(用这个需求的需求说明和 `card.json` 填好),不真正调用;`req integration fields` 显示需求方要确认的字段。
318
+ - 认证由工具自己负责(例如工具已登录本机账号)。不要把口令写进配置;工具需要时放在环境变量里。
319
+
288
320
  ## 团队配置
289
321
 
290
322
  几台机器、多个项目共用的设置(统一的模型、评审小组、超时、要求的 req-kit 版本)可以放在一份团队配置里。把它放进一个团队共用的 git 仓库,各台机器克隆下来,在本机配置 `~/.req-kit/config.json` 中写上它的路径(绝对路径,或相对 `~/.req-kit/` 的路径):
package/engine/src/api.ts CHANGED
@@ -256,6 +256,7 @@ export function apiStatus(repoArg: string, feature: string) {
256
256
  : null,
257
257
  steps: state ? state.history.map((h) => stepJson(h, state)) : [],
258
258
  gate: state?.gate ?? null,
259
+ external: Object.entries(state?.external ?? {}).map(([name, r]) => ({ name, id: r.id, url: r.url, status: r.status, syncedAt: r.syncedAt, pending: r.pending.length, lastError: r.pending.at(-1)?.error ?? null })),
259
260
  joint: (state?.joint ?? []).map((j) => ({ name: j.name, slug: j.slug, repo: j.repo, sourceRepo: j.sourceRepo, branch: j.branch, workspaceId: j.workspaceId, gate: j.gate ?? null })),
260
261
  };
261
262
  }
@@ -290,7 +291,7 @@ export function apiIndex(input: { rebuild: string[]; prune: boolean }) {
290
291
  return { scanned: [...dirs], registered, pruned, total: readIndex().length };
291
292
  }
292
293
 
293
- const RUNNABLE = ["approve", "stop", "resume", "verify"] as const;
294
+ const RUNNABLE = ["approve", "stop", "resume", "verify", "sync"] as const;
294
295
 
295
296
  /** Runs `req <command> <需求>` where people would, and reports what it said. */
296
297
  export function apiRun(command: string, repoArg: string, feature: string, extra: string[]) {
@@ -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 --card-fields 团队对接了外部系统时,确认需求说明时要一并确认的卡片字段
14
15
  start-pipeline --feature <功能目录> --spec-review 确认前请另一个模型独立审阅需求说明草稿(后台进行,结果发回对话)
15
16
  start-pipeline --feature <功能目录> --worktree 为这个需求新建 Paseo worktree 工作区并在那里开发(对话目录不变)
16
17
  start-pipeline --feature <功能目录> --at 22:00 预约在指定时间开始(也可以写"明天 8:30""2026-10-14 08:30""+2h")
@@ -52,6 +53,7 @@ export function parseLaunchArgs(argv: string[]): LaunchArgs {
52
53
  status: { type: "boolean", default: false },
53
54
  stop: { type: "boolean", default: false },
54
55
  list: { type: "boolean", default: false },
56
+ "card-fields": { type: "boolean", default: false },
55
57
  "spec-review": { type: "boolean", default: false },
56
58
  worktree: { type: "boolean", default: false },
57
59
  "source-repo": { type: "string" },
@@ -71,7 +73,7 @@ export function parseLaunchArgs(argv: string[]): LaunchArgs {
71
73
  console.log(USAGE);
72
74
  process.exit(0);
73
75
  }
74
- if (!values.feature && !values.list && !values.status) {
76
+ if (!values.feature && !values.list && !values.status && !values["card-fields"]) {
75
77
  throw new Error(`缺少 --feature\n\n${USAGE}`);
76
78
  }
77
79
  const profile = values.profile?.trim().toLowerCase();
@@ -121,6 +123,7 @@ export function parseLaunchArgs(argv: string[]): LaunchArgs {
121
123
  status: values.status ?? false,
122
124
  stop: values.stop ?? false,
123
125
  list: values.list ?? false,
126
+ cardFields: values["card-fields"] ?? false,
124
127
  specReview: values["spec-review"] ?? false,
125
128
  worktree: values.worktree ?? false,
126
129
  sourceRepo: values["source-repo"] ? path.resolve(values["source-repo"]) : null,
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { type IntegrationConfig, validateIntegrations } from "./integrations.js";
4
5
  import { NOTIFIER_EVENT_TYPES, type NotifierConfig } from "./notifiers.js";
5
6
  import type { StepId } from "./steps.js";
6
7
 
@@ -175,6 +176,10 @@ export interface PipelineConfig {
175
176
  reviewPanel: ReviewPanelSettings;
176
177
  /** External notifiers (e.g. an internal IM). Machine or team config only. */
177
178
  notifiers: NotifierConfig[];
179
+ /** External systems kept in step with requirements, e.g. an ALM's CLI (1.9). Machine or team config only: they run commands. */
180
+ integrations: IntegrationConfig[];
181
+ /** Per integration name, values for its {field.*} placeholders; a project's file may set these (e.g. its project id). */
182
+ integrationDefaults: Record<string, Record<string, string>>;
178
183
  /** Automatic retries of a step after a transient error (rate limit, dropped connection). */
179
184
  retry: { attempts: number; backoffSeconds: number[] };
180
185
  /**
@@ -277,6 +282,8 @@ export const DEFAULT_CONFIG: PipelineConfig = {
277
282
  requiredVersion: null,
278
283
  reviewPanel: { enabled: false, steps: ["design_review", "code_review"], profiles: ["standard"], members: [] },
279
284
  notifiers: [],
285
+ integrations: [],
286
+ integrationDefaults: {},
280
287
  retry: { attempts: 2, backoffSeconds: [60, 300] },
281
288
  paseoServices: "auto",
282
289
  serviceReadySeconds: 120,
@@ -332,6 +339,14 @@ function readJson(file: string, label: string): Record<string, unknown> {
332
339
  function merge(base: PipelineConfig, raw: Record<string, unknown>): PipelineConfig {
333
340
  const merged: PipelineConfig = { ...base, ...(raw as Partial<PipelineConfig>) };
334
341
  merged.agents = { ...base.agents, ...(isRecord(raw.agents) ? raw.agents : {}) } as PipelineConfig["agents"];
342
+ // Integration defaults add up by integration and field: the team sets the shared ones, a project its own.
343
+ if (isRecord(raw.integrationDefaults)) {
344
+ const combined: Record<string, Record<string, string>> = { ...base.integrationDefaults };
345
+ for (const [name, fields] of Object.entries(raw.integrationDefaults)) {
346
+ combined[name] = { ...(combined[name] ?? {}), ...(isRecord(fields) ? (fields as Record<string, string>) : {}) };
347
+ }
348
+ merged.integrationDefaults = combined;
349
+ }
335
350
  merged.stepTimeoutMinutes = {
336
351
  ...base.stepTimeoutMinutes,
337
352
  ...(isRecord(raw.stepTimeoutMinutes) ? raw.stepTimeoutMinutes : {}),
@@ -515,6 +530,11 @@ function validate(config: PipelineConfig): void {
515
530
  throw new ConfigError(`notifiers[${i}].events 只能包含 ${NOTIFIER_EVENT_TYPES.join("、")}`);
516
531
  }
517
532
  });
533
+ const integrationProblem = validateIntegrations(config.integrations);
534
+ if (integrationProblem) throw new ConfigError(integrationProblem);
535
+ if (!isRecord(config.integrationDefaults) || !Object.values(config.integrationDefaults).every((m) => isRecord(m) && Object.values(m).every((v) => typeof v === "string"))) {
536
+ throw new ConfigError('integrationDefaults 必须是 { "<集成名称>": { "<字段>": "<值>" } }');
537
+ }
518
538
  if (config.requiredVersion !== null && typeof config.requiredVersion !== "string") {
519
539
  throw new ConfigError('requiredVersion 必须是版本范围字符串,例如 ">=1.2.0 <2"');
520
540
  }
@@ -550,7 +570,7 @@ export interface LoadedConfig {
550
570
  }
551
571
 
552
572
  /** Keys a project file may not set: they run code or reach outside the repository. */
553
- const NOT_IN_PROJECT_KEYS = ["notifiers"] as const;
573
+ const NOT_IN_PROJECT_KEYS = ["notifiers", "integrations"] as const;
554
574
 
555
575
  /** Notifier module paths are relative to the file that names them; "~/" is the home directory. */
556
576
  function resolveNotifiers(raw: Record<string, unknown>, file: string): Record<string, unknown> {
@@ -2,6 +2,7 @@
2
2
  // is it?" or "stop for now". Output is plain language for relaying to the user.
3
3
 
4
4
  import { describeActivity } from "./activity.js";
5
+ import { describeExternal } from "./integrations.js";
5
6
  import { PHASE_MODE_LABELS, PROFILE_LABELS, type PipelineConfig, REQUIREMENT_TYPE_LABELS } from "./config.js";
6
7
  import type { AgentRuntime, RuntimeFactory } from "./runtime.js";
7
8
  import { isAlive, type PipelineState, readState, type StepRecord, writeState } from "./state.js";
@@ -80,6 +81,7 @@ export function describeStatus(state: PipelineState | null, featureDir: string,
80
81
  ];
81
82
  if (state.stopReason && !running) lines.push(`原因:${state.stopReason}`);
82
83
  for (const j of state.joint ?? []) lines.push(`联合开发:${j.name} 在工作区 ${j.repo}(分支 ${j.branch})`);
84
+ for (const l of describeExternal(state.external)) lines.push(`外部系统 ${l}`);
83
85
  const KIND_TEXT = { reminder: "提醒", "auto-resume": "自动继续", watchdog: "定时检查", "verify-start": "开始测试验证", "approval-continue": "未确认时按方案继续" } as const;
84
86
  for (const s of state.schedules ?? []) {
85
87
  if (s.kind === "watchdog" && !running) continue;
@@ -0,0 +1,161 @@
1
+ // `req integration` and `req sync` (1.9, ADR 0028).
2
+ //
3
+ // req integration test [需求] what each configured integration would run, without running it
4
+ // req integration fields the card fields /req confirms with the spec (card.json)
5
+ // req sync <需求> bring the requirement's cards up to date now: send what failed,
6
+ // create a missing card, set the state for where the requirement is
7
+
8
+ import path from "node:path";
9
+ import { locate } from "./api.js";
10
+ import { loadConfig, type PipelineConfig } from "./config.js";
11
+ import {
12
+ cardFieldsText,
13
+ commandAvailable,
14
+ describeExternal,
15
+ INTEGRATION_EVENT_LABELS,
16
+ INTEGRATION_EVENTS,
17
+ type IntegrationConfig,
18
+ type IntegrationEvent,
19
+ placeholderValues,
20
+ renderArgs,
21
+ resolveCommand,
22
+ statusFor,
23
+ syncIntegration,
24
+ } from "./integrations.js";
25
+ import { defaultConfigPath } from "./locations.js";
26
+ import { acquireLock, isAlive, type PipelineState, readState, statePath, writeState } from "./state.js";
27
+
28
+ /** The event that describes where a requirement is now. */
29
+ export function eventForState(state: Pick<PipelineState, "status" | "phase" | "pid">): IntegrationEvent {
30
+ switch (state.status) {
31
+ case "waiting_for_approval":
32
+ return "approval";
33
+ case "waiting_for_business":
34
+ return "blocked";
35
+ case "phase_done":
36
+ return "phase_done";
37
+ case "completed":
38
+ return "completed";
39
+ case "failed":
40
+ return "failed";
41
+ default:
42
+ return state.phase === "verify" ? "verify" : "started";
43
+ }
44
+ }
45
+
46
+ function quoteArg(a: string): string {
47
+ const shown = a.length > 120 ? `${a.slice(0, 117)}…` : a;
48
+ return /^[^\s"'\\$`&|;<>()*?!]+$/.test(shown) ? shown : `"${shown.replace(/\n/g, "\\n").replace(/"/g, '\\"')}"`;
49
+ }
50
+
51
+ function testIntegration(config: IntegrationConfig, project: PipelineConfig, target: { repo: string; featureDir: string; state: PipelineState | null } | null): string[] {
52
+ const lines = [`== ${config.name}(命令 ${config.command})`];
53
+ const { file, prefix } = resolveCommand(config.command);
54
+ lines.push(commandAvailable(config.command) ? `✓ 找到命令:${[file, ...prefix].join(" ")}` : `✗ 找不到命令 ${config.command}:请确认它已安装并在 PATH 中(运行 Paseo daemon 的用户的 PATH)`);
55
+ const ctx = {
56
+ repo: target?.repo ?? process.cwd(),
57
+ featureDir: target?.featureDir ?? "docs/features/20261015-示例需求",
58
+ state: { branch: target?.state?.branch ?? "feature/示例", profile: target?.state?.profile ?? "standard", type: target?.state?.type ?? null, outcome: target?.state?.outcome ?? null, external: target?.state?.external ?? null },
59
+ projectDefaults: project.integrationDefaults[config.name],
60
+ } as Parameters<typeof placeholderValues>[1];
61
+ if (!target) lines.push("(没有指定需求:用示例值;指定一个需求可以看到用它的需求说明和 card.json 填出的命令)");
62
+ const show = (label: string, args: string[], status?: string | null, event?: IntegrationEvent) => {
63
+ try {
64
+ const values = placeholderValues(config, ctx, { status, event });
65
+ if (!values.id) values.id = "<卡片编号>";
66
+ lines.push(`${label}:${config.command} ${renderArgs(args, values).map(quoteArg).join(" ")}`);
67
+ } catch (error) {
68
+ lines.push(`${label}:✗ ${(error as Error).message}`);
69
+ }
70
+ };
71
+ if (config.create) show("建卡", config.create.args);
72
+ for (const event of INTEGRATION_EVENTS) {
73
+ const status = statusFor(config, event, null);
74
+ if (status && config.status) show(`${INTEGRATION_EVENT_LABELS[event]} → 「${status}」`, config.status.args, status, event);
75
+ }
76
+ for (const outcome of ["completed_with_issues", "not_completed"] as const) {
77
+ const status = config.statusMap?.[outcome];
78
+ if (status && config.status) show(`完成(${outcome === "not_completed" ? "未完成" : "有遗留问题"})→ 「${status}」`, config.status.args, status, "completed");
79
+ }
80
+ return lines;
81
+ }
82
+
83
+ async function main(argv: string[]): Promise<number> {
84
+ const [sub, ...rest] = argv;
85
+ const cwd = process.cwd();
86
+ const configPath = defaultConfigPath();
87
+ if (sub === "fields") {
88
+ const config = loadConfig(configPath, cwd).config;
89
+ console.log(cardFieldsText(config.integrations) ?? "没有配置外部系统对接(integrations)。");
90
+ return 0;
91
+ }
92
+ if (sub === "test") {
93
+ let target: { repo: string; featureDir: string; state: PipelineState | null } | null = null;
94
+ if (rest[0]) {
95
+ const found = locate(cwd, rest[0]);
96
+ target = { ...found, state: readState(statePath(found.repo, found.featureDir)) };
97
+ }
98
+ const config = loadConfig(configPath, target?.repo ?? cwd).config;
99
+ if (config.integrations.length === 0) {
100
+ console.log("没有配置外部系统对接(integrations)。写法见 docs/guide/项目定制与运行规则.md 的“外部系统对接”。");
101
+ return 0;
102
+ }
103
+ console.log("只显示将要执行的命令,不会真正调用。\n");
104
+ for (const i of config.integrations) console.log(`${testIntegration(i, config, target).join("\n")}\n`);
105
+ return 0;
106
+ }
107
+ if (sub === "sync") {
108
+ if (!rest[0]) {
109
+ console.error("请写出是哪个需求,例如:req sync sms-login");
110
+ return 2;
111
+ }
112
+ const { repo, featureDir } = locate(cwd, rest[0]);
113
+ const file = statePath(repo, featureDir);
114
+ const state = readState(file);
115
+ if (!state) {
116
+ console.log(`${featureDir} 还没有启动过开发流水线,没有要同步的。`);
117
+ return 0;
118
+ }
119
+ if (state.status === "running" && isAlive(state.pid)) {
120
+ console.log("流水线正在运行:它会在下一个阶段变化时自动补发。现在不需要 req sync。");
121
+ return 0;
122
+ }
123
+ const config = loadConfig(configPath, repo).config;
124
+ if (config.integrations.length === 0) {
125
+ console.log("没有配置外部系统对接(integrations)。");
126
+ return 0;
127
+ }
128
+ const release = acquireLock(repo, featureDir);
129
+ try {
130
+ const event = eventForState(state);
131
+ for (const i of config.integrations) {
132
+ await syncIntegration({
133
+ config: i,
134
+ ctx: { repo, featureDir, state, projectDefaults: config.integrationDefaults[i.name] },
135
+ event,
136
+ log: (m) => console.log(m),
137
+ });
138
+ }
139
+ writeState(file, state);
140
+ } finally {
141
+ release();
142
+ }
143
+ const lines = describeExternal(state.external);
144
+ console.log(lines.length > 0 ? `\n${lines.join("\n")}` : "");
145
+ return Object.values(state.external ?? {}).some((r) => r.pending.length > 0) ? 1 : 0;
146
+ }
147
+ console.error("用法:req integration test [需求] | req integration fields | req sync <需求>");
148
+ return 2;
149
+ }
150
+
151
+ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename)) {
152
+ main(process.argv.slice(2)).then(
153
+ (code) => process.exit(code),
154
+ (error: Error) => {
155
+ console.error(error.message);
156
+ process.exit(2);
157
+ },
158
+ );
159
+ }
160
+
161
+ export { main as integrationMain };