paseo-req-kit 1.12.0-beta.2 → 1.12.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/bin/req.mjs +4 -0
- package/contracts/cli-json.md +3 -1
- package/contracts/notifier-event.md +17 -1
- package/docs/adr/0031-fewer-interventions.md +8 -0
- package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +2 -0
- 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 +37 -1
- package/engine/src/api.ts +42 -3
- package/engine/src/card-cmd.ts +13 -0
- package/engine/src/card.ts +29 -0
- package/engine/src/config-view.ts +289 -0
- package/engine/src/config.ts +42 -2
- package/engine/src/doctor.ts +14 -0
- package/engine/src/factory.ts +115 -4
- package/engine/src/messages.ts +22 -0
- package/engine/src/notifiers.ts +32 -1
- package/engine/src/notify-test.ts +8 -2
- package/engine/src/paseo-runtime.ts +9 -0
- package/engine/src/pipeline.ts +61 -4
- package/engine/src/reply.ts +59 -0
- package/engine/src/runtime.ts +2 -0
- package/engine/src/state.ts +11 -1
- package/notifiers/im-template.mjs +31 -5
- package/package.json +1 -1
- package/paseo-plugin/client/config.tsx +216 -0
- package/paseo-plugin/client/data.ts +7 -2
- package/paseo-plugin/client/format.ts +1 -0
- package/paseo-plugin/client/inbox.tsx +173 -0
- package/paseo-plugin/client/overview.tsx +7 -3
- package/paseo-plugin/index.client.tsx +10 -0
- package/paseo-plugin/index.server.ts +2 -1
- package/paseo-plugin/package.json +1 -1
- package/paseo-plugin/server/handlers.ts +8 -3
- package/paseo-plugin/shared/contracts.ts +40 -1
- package/skills/req/playbooks/unattended.md +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
每个版本分为"使用者会看到的变化"和"升级须知"。升级步骤见 [docs/guide/安装与升级.md](docs/guide/安装与升级.md)。
|
|
4
4
|
|
|
5
|
+
## 1.12.0-beta.3
|
|
6
|
+
|
|
7
|
+
接单机器能自己发现问题;插件里可以直接处理等你的事、看到全部配置。
|
|
8
|
+
|
|
9
|
+
### 使用者会看到的变化
|
|
10
|
+
|
|
11
|
+
- **插件"等你处理"**:需求首页最上面列出所有等人处理的事(确认方案、回答问题、中断、未完成),等得最久的在前,写明要做什么、等了多久。方案可以直接确认或写修改意见,问题可以直接回答,插件把回复转给需求的对话,和在对话里回答一样(`req reply`)。
|
|
12
|
+
- **插件"配置"页**:首页的"配置"按主题列出 req-kit 的全部设置,每项写明是什么、现在的值、来自本机、团队还是项目的配置文件;可以按项目查看、搜索、只看改过的。只看不改,页尾写明各项怎么改。密钥和个人账号不显示。
|
|
13
|
+
- **接单器自检与告警**:每次检查前确认 Paseo 在线、agent 已登录、额度没用完、需求管理系统的命令可用;不正常时暂停接单,并通过外部通知告警一次(新事件 `alert`),恢复时再说一次。
|
|
14
|
+
- **草稿合并请求**(可选,`push.mergeRequest`):推送分支后用机器上已登录的 `glab` 等命令建草稿合并请求,描述是提测说明和卡片链接,地址写进卡片。合并仍由人做。`req doctor` 检查是否已登录。
|
|
15
|
+
- **小改动自动确认**(可选,`autoConfirm.hours`,默认关闭):快速档、没有待答问题的卡片,AI 区写明到时没有异议就开始。
|
|
16
|
+
- **按人通知**:通知事件带上发给谁(`people`):需求方要处理的事和结果发给卡片的提出人,账号来自配置的 `people`;其余发到群。内部 IM 适配器拿到接口后可以直接用。
|
|
17
|
+
|
|
18
|
+
### 升级须知
|
|
19
|
+
|
|
20
|
+
- 新配置 `push.mergeRequest`、`autoConfirm`、`people`(都只在需要时写)。`people`、`push` 只能写在本机或团队配置中。
|
|
21
|
+
- 外部通知新增事件类型 `alert` 和字段 `people`;只订阅了部分事件的通知模块不受影响。
|
|
22
|
+
- 新命令 `req reply`、`req api config`;`req api list` 的每一项多了 `waiting`。
|
|
23
|
+
|
|
5
24
|
## 1.12.0-beta.2
|
|
6
25
|
|
|
7
26
|
1.12.0-beta.1 的内容(见下),加一处修正:
|
package/bin/req.mjs
CHANGED
|
@@ -30,6 +30,7 @@ const HELP = `req-kit ${version}(${kitRoot})
|
|
|
30
30
|
req digest --schedule "0 9 * * 1"
|
|
31
31
|
让 Paseo 按时间表发送需求总览(例子为每周一 9:00);取消:req digest --cancel <ID>
|
|
32
32
|
req sync <需求> 把需求同步到对接的外部系统(补发失败的调用、补建卡片、更新状态)
|
|
33
|
+
req reply <需求> <回复> 把回答或方案意见转给需求的对话(插件的"等你处理"用它)
|
|
33
34
|
req integration test [需求] 只显示对接外部系统时会执行的命令,不真正调用
|
|
34
35
|
req start --from <系统>:<卡号> 从需求管理系统中的一张卡片开始需求(没有人开着对话也可以)
|
|
35
36
|
req card check [<卡号>|--all] 读取需求方在卡片中的回复,转给需求的会话
|
|
@@ -197,6 +198,9 @@ switch (command) {
|
|
|
197
198
|
case "api":
|
|
198
199
|
code = needTsx() ? node([tsxCli, path.join(kitRoot, "engine", "src", "api.ts"), ...rest]) : 1;
|
|
199
200
|
break;
|
|
201
|
+
case "reply":
|
|
202
|
+
code = needTsx() ? node([tsxCli, path.join(kitRoot, "engine", "src", "reply.ts"), ...rest]) : 1;
|
|
203
|
+
break;
|
|
200
204
|
case "sync":
|
|
201
205
|
code = needTsx() ? node([tsxCli, path.join(kitRoot, "engine", "src", "integration-cmd.ts"), "sync", ...rest]) : 1;
|
|
202
206
|
break;
|
package/contracts/cli-json.md
CHANGED
|
@@ -27,8 +27,9 @@
|
|
|
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> <需求> [--repo <目录>] [选项]` | `{ command, featureDir, ok, exitCode, message }`。在 `--repo`(默认当前目录)中执行与 `req <命令> <需求>` 完全相同的操作;`ok` 为 false 时退出码为 1 |
|
|
30
|
+
| `req api run <approve\|stop\|resume\|verify\|sync\|reply> <需求> [--repo <目录>] [选项]` | `{ command, featureDir, ok, exitCode, message }`。在 `--repo`(默认当前目录)中执行与 `req <命令> <需求>` 完全相同的操作;`ok` 为 false 时退出码为 1 |
|
|
31
31
|
| `req api models` | 1.12 起。`{ current, choices, recommended }`:本机配置中现在的 `agents`;daemon 上能无人值守运行的选择(`{ id, label, provider, profile }`,Paseo profile 在前);按推荐的 `agents`。连不上 daemon 时报错 |
|
|
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` 不看项目配置 |
|
|
32
33
|
| `req api set-agents --json <设置>` | 1.12 起。`{ ok, message }`。`<设置>` 是 `{ builder, reviewer, fallback }`(`fallback` 可以为 null),检查后写入本机配置的 `agents`,原文件备份 |
|
|
33
34
|
|
|
34
35
|
`<需求>` 的写法与 `req status` 相同:功能目录、目录名或名称中能唯一确定的一部分。需求在为它新建的工作区中开发时,`--repo` 写对话所在的目录或那个工作区都可以。
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
| `running` | 后台进程是否在运行 |
|
|
51
52
|
| `phase` | `develop` / `verify` |
|
|
52
53
|
| `attention` | 需要人处理的事:`approval`(确认方案)、`business`(需求方决定)、`interrupted`(中断,可以继续)、`not_completed`(结束但未完成);没有为 null |
|
|
54
|
+
| `waiting` | 1.12 起。`attention` 不为 null 时:`since`(从什么时候开始等)、`questions`(需求方要回答的问题)、`reason`(停止原因)、`card`(`{ system, id, url, requester }`,从卡片开始的需求)、`canReply`(能否用 `req api run reply <需求> <回复>` 把回复转给需求的对话);否则为 null |
|
|
53
55
|
| `current` | 正在运行的步骤 `[{ key, title, agentId }]` |
|
|
54
56
|
| `updatedAt` | 最近一次变化的时间 |
|
|
55
57
|
| `usage` | `{ steps, agentMinutes, inputTokens, outputTokens, costUsd }`;没运行过为 null |
|
|
@@ -33,7 +33,7 @@ export default async function notify(event, options) { /* 发送 */ }
|
|
|
33
33
|
- 每个事件调用一次,可以返回 Promise。超过 15 秒没有完成按失败处理。
|
|
34
34
|
- 抛出的错误和超时只记入流水线日志("外部通知失败……不影响流水线"),不会让流水线停下或重试。
|
|
35
35
|
- 模块在一次流水线运行中只加载一次;修改模块后,对之后启动或继续的流水线生效。
|
|
36
|
-
- 测试:`req notify-test [--type blocked|failed|completed|milestone|permission|phase_done|approval|digest]` 发一条带"【测试】"字样的示例事件,不需要运行流水线。
|
|
36
|
+
- 测试:`req notify-test [--type blocked|failed|completed|milestone|permission|phase_done|approval|digest|alert]` 发一条带"【测试】"字样的示例事件,不需要运行流水线。
|
|
37
37
|
|
|
38
38
|
## 事件格式(schema 1)
|
|
39
39
|
|
|
@@ -55,6 +55,21 @@ export default async function notify(event, options) { /* 发送 */ }
|
|
|
55
55
|
|
|
56
56
|
`text` 是一两句可以直接发出的话;需要自己排版时用 `details`。
|
|
57
57
|
|
|
58
|
+
**发给谁**(1.12 起,`people`):
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
"people": {
|
|
62
|
+
"audience": "requester",
|
|
63
|
+
"requester": "张广祥",
|
|
64
|
+
"card": { "system": "ALM", "id": "CMASTER-BUSINESS#9902", "url": null },
|
|
65
|
+
"recipients": [{ "name": "张广祥", "accounts": { "um": "ZHANGGX001" } }]
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- `audience`:`requester` 表示这件事要需求方处理或是给他的结果(`blocked`、`approval`、`phase_done`、`completed`,而且需求从卡片开始、知道提出人);其余为 `team`,发到群里即可。
|
|
70
|
+
- `requester`、`card`:从卡片开始的需求的提出人和卡片;其他需求为 null。
|
|
71
|
+
- `recipients`:`audience` 为 `requester` 时是需求方本人;`accounts` 来自本机或团队配置的 `people`(姓名 → 各种账号),没有写时为 `{}`,模块可以改发到群。
|
|
72
|
+
|
|
58
73
|
| type | 什么时候 | details |
|
|
59
74
|
| --- | --- | --- |
|
|
60
75
|
| `milestone` | 方案确定、开始验证、开始修复缺陷(配置 `notifyMilestones` 为 true 时,每次尝试各一次) | `milestone`:`design-settled`、`code-reviewed`、`fixing-defects`、`handback-<n>`(1.7.0 起,测试验证第 n 次把缺陷退回研发) |
|
|
@@ -64,6 +79,7 @@ export default async function notify(event, options) { /* 发送 */ }
|
|
|
64
79
|
| `completed` | 流水线结束,报告已生成 | `outcome`(`completed`、`completed_with_issues`、`not_completed`)、`summary`、`reportPath`。"只做研发"时另有 `mode`(`develop-only`)和 `handoffPath`,报告是研发报告,需要人工测试 |
|
|
65
80
|
| `approval` | 1.8.0 起。方案确认点(`designApproval`):方案已通过审核,等待需求方确认"做完后会看到什么",还没有开始写代码 | `previewPath`(要确认的内容)、`approveCommand`(确认后继续的命令)、`resumeCommand`(修改需求后继续的命令),都在发起的对话中执行 |
|
|
66
81
|
| `phase_done` | 1.7.0 起。分阶段执行时研发阶段已完成(代码已通过评审、提测说明已写好),流水线停在闸口等待测试验证。测试退回后再次修完也会发 | `handoffPath`(提测说明)、`mode`(`gated`)、`verifyCommand`(开始测试验证的命令,在发起的对话中执行)、`verifyAt`(预约的开始时间,ISO 格式,没有为 null)、`handbacks`(已退回的次数) |
|
|
82
|
+
| `alert` | 1.12 起。接单器(`req factory`)自检没有通过(Paseo 连不上、agent 没有登录、额度用完、需求管理系统的命令不可用),暂停接新卡片;或者恢复正常。同样的问题只发一次。`feature.title` 为"接单器",`feature.dir`、`repo` 为空 | `source`(`factory`)、`machine`、`problems`(字符串数组,恢复时为空)、`recovered` |
|
|
67
83
|
| `digest` | `req digest --notify`(或由 `req digest --schedule` 创建的 Paseo 定时任务)发送项目的需求总览,1.6.0 起。`feature.title` 为"<项目> 需求总览",`text` 是完整的多行总览 | `features`:每个需求的 `name`、`title`、`stage`、`branch`、`updatedAt`、`usage`;`recurringLessons`(1.8.0 起):在两个以上需求的 `lessons.tsv` 中出现的问题类别,每项 `category`、`features`、`rules` |
|
|
68
84
|
|
|
69
85
|
## 兼容性
|
|
@@ -31,3 +31,11 @@
|
|
|
31
31
|
- 方案修订的结果文件可以带 `business_questions`。
|
|
32
32
|
- 新命令:`req configure --auto`、`req api models`、`req api set-agents`。
|
|
33
33
|
- 自动处理会多用一些额度(多等一轮、重做一次);每次都写进进度日志和使用记录,可以查到。
|
|
34
|
+
|
|
35
|
+
## 补充(1.12.0-beta.3):接单机器的运维和插件中的处理
|
|
36
|
+
|
|
37
|
+
- **接单器自检**:每次检查前确认 Paseo、agent 登录、额度和需求管理系统的命令。不正常就整次跳过(不接新单、不转达),因为这时接下来的需求也跑不起来;告警走外部通知的新事件 `alert`,按"问题的内容"去重,只在出现、变化和恢复时各发一次,避免刷屏。
|
|
38
|
+
- **草稿合并请求**:和对接配置一样,命令写在配置里、参数用占位符,req-kit 不内置 GitLab 接口、不保存令牌;只建草稿、每个仓库只建一次,合并仍由人做。
|
|
39
|
+
- **自动确认**:只对快速档、没有待答问题的卡片,默认关闭;需求方写了任何内容就不自动确认,每次摘要更新重新计时。确认仍然发生在需求会话里,req-kit 只是在到时转达"按自动确认处理"。
|
|
40
|
+
- **按人通知**:事件带上 `people`(发给需求方还是群、需求方的账号),账号来自本机或团队配置的 `people`,内部 IM 的适配器拿到接口后只需要读它。
|
|
41
|
+
- **插件"等你处理"和配置页**:回答和方案意见用 `req reply` 转给需求的对话,由对话按原来的做法更新需求说明并继续,插件不直接改需求文件或状态;配置页只读,展示 `req api config` 的结果,密钥和个人账号不显示。
|
|
@@ -62,6 +62,8 @@ req-kit 帮你把一个需求从"我想要……"做到"可以验收":你在 P
|
|
|
62
62
|
|
|
63
63
|
装了 req-kit 插件的话(见 [安装与升级](安装与升级.md) 第 13 节),在 Paseo 侧边栏点"需求":本机所有项目的需求一页看完,"需要你处理"的排在最前;点开一个需求能看到每一步的进度和用时、读需求说明、方案、确认预览和完成报告,也可以直接确认方案、停止、继续或开始测试验证。网页端、桌面版和手机上都能用。
|
|
64
64
|
|
|
65
|
+
1.12 起,首页最上面是"等你处理":所有等你确认方案、回答问题、中断了的需求,等得最久的在前,每条写明要做什么、等了多久。方案可以直接确认,也可以写修改意见;方案审核的问题可以直接写回答,插件会转给这个需求的对话,效果和在对话里回答一样。首页的"配置"列出 req-kit 的全部设置:每一项是什么、现在的值、来自本机、团队还是项目的配置文件,可以按项目查看、搜索、只看改过的。
|
|
66
|
+
|
|
65
67
|
每个步骤都会出现在这个对话的子 agent 列表里(名字形如"[20261012-sms-login] 6-代码评审 第2轮"),点开可以看到它在做什么。功能目录下的 `progress.md` 按时间记录了每一步的结果。
|
|
66
68
|
|
|
67
69
|
可能收到的通知(团队接入了企业 IM 的话,这些通知也会发到 IM):
|
|
@@ -162,6 +162,12 @@ spec 头部的 `- 类型:` 决定流水线走哪种做法(1.8 起)。缺
|
|
|
162
162
|
|
|
163
163
|
**同时发到企业 IM**:配置 `notifiers` 后,这些通知还会交给你实现的通知模块,例如转发到企业内部 IM。`notifiers` 只能写在本机或团队配置中(项目配置中的会被忽略)。做法见 [安装与升级](安装与升级.md) 第 12 节,事件格式见 `contracts/notifier-event.md`。
|
|
164
164
|
|
|
165
|
+
**按人通知**(1.12 起):从卡片开始的需求,需要需求方处理的事(回答问题、确认方案)和结果(研发完成、流水线结束)在事件中标明发给需求方本人(`people.audience` 为 `requester`)。在本机或团队配置中写上姓名到账号的对应,事件里就带上账号,通知模块可以按人发送;其余事件发到群里:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
"people": { "张广祥": { "um": "ZHANGGX001" } }
|
|
169
|
+
```
|
|
170
|
+
|
|
165
171
|
## 运行时文件
|
|
166
172
|
|
|
167
173
|
运行时的文件都在功能目录下的 `.pipeline/` 中,不会被提交:
|
|
@@ -368,10 +374,28 @@ Paseo 自带的 `/paseo-advisor`、`/paseo-committee` 等编排 skill 也可以
|
|
|
368
374
|
```
|
|
369
375
|
|
|
370
376
|
- 只对从卡片开始的需求,推送到 `<remote>` 的 `req/<卡号简称>` 分支(例如 `req/cmaster-business-9902`),联合需求的几个仓库各推各的。分支名写进卡片的 AI 区和状态文件。
|
|
371
|
-
-
|
|
377
|
+
- 只推这个前缀的分支;从不强推、不合并,合并仍由人做。推送被拒绝(例如远程分支有别人的提交)时记下原因、写进 AI 区,流水线照常结束。
|
|
372
378
|
- 只能写在本机或团队配置中,写在项目配置里会被忽略。推送用这台机器上 git 已配置的账号。
|
|
373
379
|
- 在个人电脑上通过对话发起的需求不受影响,仍然只在本地提交。
|
|
374
380
|
|
|
381
|
+
**草稿合并请求**(1.12 起,可选):推送后用机器上的 GitLab 命令行(或其他工具)建一个草稿合并请求。命令写法与对接配置一样,参数中用占位符:
|
|
382
|
+
|
|
383
|
+
```json
|
|
384
|
+
"push": {
|
|
385
|
+
"enabled": true,
|
|
386
|
+
"mergeRequest": {
|
|
387
|
+
"command": "glab",
|
|
388
|
+
"args": ["mr", "create", "--draft", "--source-branch", "{branch}", "--target-branch", "{target}", "--title", "{title}", "--description", "{description}", "--yes"],
|
|
389
|
+
"check": ["auth", "status"]
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
- 占位符:`{branch}` 推送的分支;`{target}` 目标分支(`target` 没写时用远程的默认分支,读不到时 `main`);`{title}` 为"[卡号] 需求标题";`{description}` 为提测说明加卡片链接,开头注明是草稿、合并前请人工评审;`{card}`、`{cardUrl}`。
|
|
395
|
+
- 在需求的仓库中执行;输出中的第一个网址记为合并请求地址,写进卡片 AI 区和状态文件。每个仓库只建一次,测试退回后再推送时合并请求自动更新。
|
|
396
|
+
- 建失败(例如没有登录)只记下原因,分支照常推送。`req doctor` 用 `check` 中的参数检查是否已登录。
|
|
397
|
+
- 需要机器上提前登录好(例如 `glab auth login`),req-kit 不保存令牌。
|
|
398
|
+
|
|
375
399
|
**使用统计**:从卡片开始的需求,运行记录带上需求方(卡片的提出人)和卡号。`req stats --report` 多一节"从卡片开始的需求(按需求方)",CSV 多 `requester`、`card` 两列。
|
|
376
400
|
|
|
377
401
|
### 接单器(1.11 起,beta)
|
|
@@ -411,6 +435,18 @@ Paseo 自带的 `/paseo-advisor`、`/paseo-committee` 等编排 skill 也可以
|
|
|
411
435
|
- Paseo daemon 要同时运行:接单器通过它创建需求会话。
|
|
412
436
|
- 现在只支持一台机器接单。几台机器同时运行接单器会重复接同一张卡片。
|
|
413
437
|
|
|
438
|
+
**自检与告警**(1.12 起):每次检查前先确认 Paseo 连得上、builder 和 reviewer 用的 agent 已登录、额度没有用完(配置了备用模型时,备用的也用完才算),以及需求管理系统的命令能列出卡片。不正常时这次不接新卡片、不转达回复,原因写进 `req factory status` 和日志,并通过外部通知发一条 `alert` 事件;同样的问题只发一次,恢复正常时再发一次。
|
|
439
|
+
|
|
440
|
+
**续跑中断的需求**(1.12 起):每次检查时,发现本机有流水线标着"运行中"但后台进程已经不在(机器重启、进程被杀),请它的对话检查并继续,每个进程只提醒一次。
|
|
441
|
+
|
|
442
|
+
**小改动自动确认**(1.12 起,默认关闭):
|
|
443
|
+
|
|
444
|
+
```json
|
|
445
|
+
"autoConfirm": { "hours": 4 }
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
快速档、没有待回答问题的卡片,AI 区写明"到某时前没有异议就开始";到时需求方没有在回复区写任何内容,接单器按"确认:同意"处理。需求方随时可以写"同意"提前开始,或写修改意见。每次摘要更新都重新计时。
|
|
449
|
+
|
|
414
450
|
## 团队配置
|
|
415
451
|
|
|
416
452
|
几台机器、多个项目共用的设置(统一的模型、评审小组、超时、要求的 req-kit 版本)可以放在一份团队配置里。把它放进一个团队共用的 git 仓库,各台机器克隆下来,在本机配置 `~/.req-kit/config.json` 中写上它的路径(绝对路径,或相对 `~/.req-kit/` 的路径):
|
package/engine/src/api.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// req api run <approve|stop|resume|verify> <需求> [--repo <dir>] [其他选项]
|
|
13
13
|
// req api models agents the daemon offers, the current and the recommended ones (1.12)
|
|
14
14
|
// req api set-agents --json <设置> writes builder, reviewer and fallback to the local config (1.12)
|
|
15
|
+
// req api config [--repo <dir>] [--machine] every setting, its value in words and which file set it (1.12)
|
|
15
16
|
//
|
|
16
17
|
// Reading never changes anything. `run` performs the same command as `req <command>` in that
|
|
17
18
|
// directory, so the engine stays the only one that changes a requirement's state.
|
|
@@ -21,7 +22,8 @@ import fs from "node:fs";
|
|
|
21
22
|
import path from "node:path";
|
|
22
23
|
import { resultText, stepName } from "./control.js";
|
|
23
24
|
import { FEATURES_DIR, findWorktreeRun, listFeatures, resolveFeatureArg, summarize } from "./features.js";
|
|
24
|
-
import { KIT_ROOT, kitVersion } from "./locations.js";
|
|
25
|
+
import { defaultConfigPath, KIT_ROOT, kitVersion } from "./locations.js";
|
|
26
|
+
import { configView } from "./config-view.js";
|
|
25
27
|
import { pruneIndex, readIndex, registerRequirement, requirementKey } from "./records.js";
|
|
26
28
|
import { isAlive, type PipelineState, readState, statePath, type StepRecord } from "./state.js";
|
|
27
29
|
import { parsePeriod, summarizeRuns } from "./stats.js";
|
|
@@ -120,6 +122,18 @@ export interface ListItem {
|
|
|
120
122
|
usage: ReturnType<typeof usageOf>;
|
|
121
123
|
/** False when the index still lists a requirement whose directory is gone. */
|
|
122
124
|
present: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* 1.12: for a requirement that needs a person (attention): since when, what it asks, why it
|
|
127
|
+
* stopped, and the card it came from. Null otherwise.
|
|
128
|
+
*/
|
|
129
|
+
waiting: {
|
|
130
|
+
since: string;
|
|
131
|
+
questions: string[];
|
|
132
|
+
reason: string | null;
|
|
133
|
+
card: { system: string; id: string; url: string | null; requester: string | null } | null;
|
|
134
|
+
/** Whether a reply can be sent to the requirement's chat (req reply). */
|
|
135
|
+
canReply: boolean;
|
|
136
|
+
} | null;
|
|
123
137
|
}
|
|
124
138
|
|
|
125
139
|
function item(repo: string, branch: string | null, featureDir: string, extra: { sourceRepo: string | null; workspaceId: string | null; project: string }): ListItem | null {
|
|
@@ -148,6 +162,25 @@ function item(repo: string, branch: string | null, featureDir: string, extra: {
|
|
|
148
162
|
updatedAt: summary.updatedAt,
|
|
149
163
|
usage: usageOf(state),
|
|
150
164
|
present: true,
|
|
165
|
+
waiting: waitingOf(repo, featureDir, state),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function waitingOf(repo: string, featureDir: string, state: PipelineState | null): ListItem["waiting"] {
|
|
170
|
+
if (!state || !attentionOf(state)) return null;
|
|
171
|
+
let card: NonNullable<ListItem["waiting"]>["card"] = null;
|
|
172
|
+
try {
|
|
173
|
+
const s = JSON.parse(fs.readFileSync(path.join(repo, featureDir, "source.json"), "utf8")) as { system?: string; id?: string; url?: string | null; author?: string | null };
|
|
174
|
+
if (s.system && s.id) card = { system: s.system, id: s.id, url: s.url ?? null, requester: s.author ?? null };
|
|
175
|
+
} catch {
|
|
176
|
+
card = null;
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
since: state.updatedAt,
|
|
180
|
+
questions: state.pendingQuestions ?? [],
|
|
181
|
+
reason: state.stopReason,
|
|
182
|
+
card,
|
|
183
|
+
canReply: Boolean(state.originAgentId) && (state.status === "waiting_for_business" || state.status === "waiting_for_approval"),
|
|
151
184
|
};
|
|
152
185
|
}
|
|
153
186
|
|
|
@@ -188,6 +221,7 @@ export function apiList(input: { repo: string; all: boolean }): ListItem[] {
|
|
|
188
221
|
updatedAt: e.registeredAt,
|
|
189
222
|
usage: null,
|
|
190
223
|
present: false,
|
|
224
|
+
waiting: null,
|
|
191
225
|
},
|
|
192
226
|
);
|
|
193
227
|
}
|
|
@@ -293,7 +327,7 @@ export function apiIndex(input: { rebuild: string[]; prune: boolean }) {
|
|
|
293
327
|
return { scanned: [...dirs], registered, pruned, total: readIndex().length };
|
|
294
328
|
}
|
|
295
329
|
|
|
296
|
-
const RUNNABLE = ["approve", "stop", "resume", "verify", "sync"] as const;
|
|
330
|
+
const RUNNABLE = ["approve", "stop", "resume", "verify", "sync", "reply"] as const;
|
|
297
331
|
|
|
298
332
|
/** Runs `req <command> <需求>` where people would, and reports what it said. */
|
|
299
333
|
export function apiRun(command: string, repoArg: string, feature: string, extra: string[]) {
|
|
@@ -398,6 +432,11 @@ export function apiMain(argvIn: string[], cwd = process.cwd()): unknown {
|
|
|
398
432
|
const what = need("命令(approve、stop、resume、verify)");
|
|
399
433
|
return apiRun(what, repo, need("需求"), argv.splice(0));
|
|
400
434
|
}
|
|
435
|
+
case "config": {
|
|
436
|
+
// --machine: this machine's settings without any project's.
|
|
437
|
+
const machineOnly = flag(argv, "--machine");
|
|
438
|
+
return configView(defaultConfigPath(), machineOnly ? null : fs.realpathSync(repo));
|
|
439
|
+
}
|
|
401
440
|
case "models": {
|
|
402
441
|
const { current, choices, recommended } = configureJson(["--catalog-json"]);
|
|
403
442
|
return { current, choices, recommended };
|
|
@@ -409,7 +448,7 @@ export function apiMain(argvIn: string[], cwd = process.cwd()): unknown {
|
|
|
409
448
|
return { ok: true, message };
|
|
410
449
|
}
|
|
411
450
|
default:
|
|
412
|
-
throw new ApiError("bad_arguments", `没有 "${command ?? ""}" 这个 api 命令。可用:version、list、status、file、stats、index、run、models、set-agents`);
|
|
451
|
+
throw new ApiError("bad_arguments", `没有 "${command ?? ""}" 这个 api 命令。可用:version、list、status、file、stats、index、run、models、set-agents、config`);
|
|
413
452
|
}
|
|
414
453
|
}
|
|
415
454
|
|
package/engine/src/card-cmd.ts
CHANGED
|
@@ -140,6 +140,10 @@ export interface CardCmdOptions {
|
|
|
140
140
|
env?: NodeJS.ProcessEnv;
|
|
141
141
|
run?: Runner;
|
|
142
142
|
out?: (line: string) => void;
|
|
143
|
+
/** Tests of the intake process without a daemon to check (1.12). */
|
|
144
|
+
skipHealthCheck?: boolean;
|
|
145
|
+
/** The time, for tests (1.12). */
|
|
146
|
+
now?: Date;
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
export interface BeginResult {
|
|
@@ -288,6 +292,7 @@ async function publish(argv: string[], opts: CardCmdOptions): Promise<number> {
|
|
|
288
292
|
const state = fs.existsSync(statePath(target.repo, target.entry.featureDir)) ? readState(statePath(target.repo, target.entry.featureDir)) : null;
|
|
289
293
|
const waitingPlan = state?.status === "waiting_for_approval";
|
|
290
294
|
const result = await publishCard({
|
|
295
|
+
autoConfirmHours: target.pipelineConfig.autoConfirm.hours,
|
|
291
296
|
config: target.config,
|
|
292
297
|
repo: target.repo,
|
|
293
298
|
featureDir: target.entry.featureDir,
|
|
@@ -337,6 +342,14 @@ export async function checkOne(target: Located, opts: CardCmdOptions, notify: (a
|
|
|
337
342
|
} else if (reply.planConfirmText && !reply.planConfirmed) changes.push(`需求方在“方案确认”一行写了:${reply.planConfirmText}`);
|
|
338
343
|
if (changes.length === 0) changes.push("需求方修改了回复区");
|
|
339
344
|
}
|
|
345
|
+
// 1.12 (autoConfirm.hours): nobody objected in time to a small requirement with nothing to answer.
|
|
346
|
+
const replied = Object.keys(reply.answers).length > 0 || Boolean(reply.notes) || Boolean(reply.confirmText) || Boolean(reply.planConfirmText);
|
|
347
|
+
const now = opts.now ?? new Date();
|
|
348
|
+
if (!confirmed && !source.confirmedAt && source.autoConfirmAt && now.getTime() >= new Date(source.autoConfirmAt).getTime() && !replied && changes.length === 0) {
|
|
349
|
+
confirmed = true;
|
|
350
|
+
source.autoConfirmed = true;
|
|
351
|
+
changes.push(`到 ${new Date(source.autoConfirmAt).toLocaleString("zh-CN", { hour12: false })} 需求方没有提出异议,按自动确认处理(autoConfirm):这就是"确认:同意",可以启动开发`);
|
|
352
|
+
}
|
|
340
353
|
if (changes.length === 0) return `${source.id}:没有新内容`;
|
|
341
354
|
if (!source.sessionAgentId) return `${source.id}:有新内容,但没有需求会话(source.json 中没有 sessionAgentId)`;
|
|
342
355
|
await notify(source.sessionAgentId, relayMessage({ id: source.id, changes, replyText: parts.reply }));
|
package/engine/src/card.ts
CHANGED
|
@@ -55,6 +55,10 @@ export interface SourceFile {
|
|
|
55
55
|
/** The progress line at the top of the AI section. */
|
|
56
56
|
progress: string | null;
|
|
57
57
|
publishedAt: string | null;
|
|
58
|
+
/** 1.12 (autoConfirm.hours): when the requirement starts by itself if nobody objects; null when it does not. */
|
|
59
|
+
autoConfirmAt?: string | null;
|
|
60
|
+
/** 1.12: set when the confirmation was the automatic one. */
|
|
61
|
+
autoConfirmed?: boolean;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
export interface CardData {
|
|
@@ -351,10 +355,27 @@ export interface PublishInput {
|
|
|
351
355
|
appendix?: string;
|
|
352
356
|
/** Adds the "方案确认" line to the reply section. */
|
|
353
357
|
planConfirm?: boolean;
|
|
358
|
+
/** 1.12 (autoConfirm.hours): a small requirement with nothing to answer starts this many hours after it is shown, unless someone objects; 0: off. */
|
|
359
|
+
autoConfirmHours?: number;
|
|
354
360
|
run?: Runner;
|
|
355
361
|
now?: Date;
|
|
356
362
|
}
|
|
357
363
|
|
|
364
|
+
/**
|
|
365
|
+
* 1.12: whether a card's requirement may start without a "确认:同意": the quick profile, nothing
|
|
366
|
+
* left to answer, and not confirmed yet. The requester can always object in the reply section.
|
|
367
|
+
*/
|
|
368
|
+
export function autoConfirmEligible(repo: string, featureDir: string, aiText: string, source: SourceFile): boolean {
|
|
369
|
+
if (source.confirmedAt) return false;
|
|
370
|
+
let spec = "";
|
|
371
|
+
try {
|
|
372
|
+
spec = fs.readFileSync(path.join(repo, featureDir, "spec.md"), "utf8");
|
|
373
|
+
} catch {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
return /^\s*-\s*档位\s*[::]\s*快速/m.test(spec) && questionIds(aiText).length === 0;
|
|
377
|
+
}
|
|
378
|
+
|
|
358
379
|
/**
|
|
359
380
|
* Re-reads the card, replaces the AI section (progress line + card-ai.md + appendix), keeps the
|
|
360
381
|
* requester's text and replies, and writes the description back. Updates and saves source.json.
|
|
@@ -374,6 +395,14 @@ export async function publishCard(input: PublishInput): Promise<{ description: s
|
|
|
374
395
|
const progress = input.progress ?? source.progress ?? "正在整理需求说明";
|
|
375
396
|
const blocks = [progressLine(progress, input.now), session || "需求说明正在整理中,稍后会写在这里。"];
|
|
376
397
|
if (input.appendix?.trim()) blocks.push(input.appendix.trim());
|
|
398
|
+
// Each new version of the summary gets the whole window again.
|
|
399
|
+
const hours = input.autoConfirmHours ?? 0;
|
|
400
|
+
if (hours > 0 && session && autoConfirmEligible(repo, featureDir, session, source)) {
|
|
401
|
+
const at = new Date((input.now ?? new Date()).getTime() + hours * 3600_000);
|
|
402
|
+
source.autoConfirmAt = at.toISOString();
|
|
403
|
+
const when = at.toLocaleString("zh-CN", { hour12: false, month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
404
|
+
blocks.push(`**自动确认**:这是一个小改动,没有需要你回答的问题。到 ${when} 前没有异议,就按上面的说明开始开发;想先确认就在"确认:"后写"同意",要改就写在回复区。`);
|
|
405
|
+
} else if (!source.confirmedAt) source.autoConfirmAt = null;
|
|
377
406
|
const ai = blocks.join("\n\n");
|
|
378
407
|
const questions = questionIds(session);
|
|
379
408
|
const reply = replySection(parts.reply, questions, { planConfirm: input.planConfirm });
|