paseo-req-kit 1.13.0-beta.2 → 1.13.0-beta.4
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 +28 -0
- package/contracts/cli-json.md +3 -1
- package/contracts/state.md +3 -1
- package/contracts/usage-record.md +2 -0
- package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +5 -2
- 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 -4
- package/engine/src/api.ts +29 -5
- package/engine/src/card-cmd.ts +2 -0
- package/engine/src/config-view.ts +5 -0
- package/engine/src/config.ts +13 -0
- package/engine/src/estimate.ts +95 -0
- package/engine/src/factory.ts +13 -30
- package/engine/src/launch.ts +5 -0
- package/engine/src/main.ts +5 -1
- package/engine/src/messages.ts +7 -2
- package/engine/src/pipeline.ts +89 -1
- package/engine/src/records.ts +6 -0
- package/engine/src/review.ts +14 -3
- package/engine/src/state.ts +15 -0
- package/engine/src/stats.ts +83 -0
- package/engine/src/watchdog.ts +105 -0
- package/package.json +1 -1
- package/paseo-plugin/client/detail.tsx +14 -6
- package/paseo-plugin/client/format.ts +1 -0
- package/paseo-plugin/client/inbox.tsx +11 -7
- package/paseo-plugin/client/nav.ts +12 -0
- package/paseo-plugin/client/overview.tsx +1 -1
- package/paseo-plugin/client/review.tsx +46 -0
- package/paseo-plugin/client/stats.tsx +42 -0
- package/paseo-plugin/client/web.ts +17 -0
- package/paseo-plugin/index.client.tsx +4 -0
- package/paseo-plugin/index.server.ts +16 -0
- package/paseo-plugin/package.json +1 -1
- package/paseo-plugin/shared/contracts.ts +23 -1
- package/site-dist/404.html +2 -2
- package/site-dist/assets/{app.DWPPPhFm.js → app.n_bkEV-X.js} +1 -1
- package/site-dist/assets/changelog.md.RqcFias_.js +1 -0
- package/site-dist/assets/{changelog.md.CoelFFCL.lean.js → changelog.md.RqcFias_.lean.js} +1 -1
- package/site-dist/assets/chunks/@localSearchIndexroot.DQEikTmr.js +1 -0
- package/site-dist/assets/chunks/{VPLocalSearchBox.D3_9dRGY.js → VPLocalSearchBox.B54kK3Ge.js} +1 -1
- package/site-dist/assets/chunks/{theme.Bm8aLiLN.js → theme.BH3XHrL3.js} +2 -2
- package/site-dist/assets/{guide_getting-started.md.B4GJeoPz.js → guide_getting-started.md.Di4587Bu.js} +1 -1
- package/site-dist/assets/{guide_project.md.nGlTCo4D.js → guide_project.md.DnD2-Dsd.js} +3 -3
- package/site-dist/assets/{guide_project.md.nGlTCo4D.lean.js → guide_project.md.DnD2-Dsd.lean.js} +3 -3
- package/site-dist/assets/{guide_user.md.tiHFxIsQ.js → guide_user.md.Cenm9vlN.js} +1 -1
- package/site-dist/assets/{guide_user.md.tiHFxIsQ.lean.js → guide_user.md.Cenm9vlN.lean.js} +1 -1
- package/site-dist/assets/reference_contracts_cli-json.md.B7o8deSY.js +1 -0
- package/site-dist/assets/reference_contracts_cli-json.md.B7o8deSY.lean.js +1 -0
- package/site-dist/assets/reference_contracts_state.md.TRfzD5bc.js +1 -0
- package/site-dist/assets/{reference_contracts_state.md.5bNTC3ow.lean.js → reference_contracts_state.md.TRfzD5bc.lean.js} +1 -1
- package/site-dist/assets/{reference_contracts_usage-record.md._C0OXWeq.js → reference_contracts_usage-record.md.DBMNY4OP.js} +2 -2
- package/site-dist/assets/{reference_contracts_usage-record.md._C0OXWeq.lean.js → reference_contracts_usage-record.md.DBMNY4OP.lean.js} +2 -2
- package/site-dist/changelog.html +5 -5
- package/site-dist/guide/getting-started.html +5 -5
- package/site-dist/guide/how-it-works.html +4 -4
- package/site-dist/guide/index.html +4 -4
- package/site-dist/guide/install.html +4 -4
- package/site-dist/guide/maintainer.html +4 -4
- package/site-dist/guide/phases.html +4 -4
- package/site-dist/guide/project.html +6 -6
- package/site-dist/guide/user.html +5 -5
- package/site-dist/hashmap.json +1 -1
- package/site-dist/index.html +4 -4
- package/site-dist/reference/adr/0001-step-agents-under-origin-chat.html +4 -4
- package/site-dist/reference/adr/0002-no-directory-switch-in-chat.html +4 -4
- package/site-dist/reference/adr/0003-script-engine-not-hub.html +4 -4
- package/site-dist/reference/adr/0004-result-files.html +4 -4
- package/site-dist/reference/adr/0005-no-default-branch-local-commits-only.html +4 -4
- package/site-dist/reference/adr/0006-skill-install-targets.html +4 -4
- package/site-dist/reference/adr/0007-own-interview-skill.html +4 -4
- package/site-dist/reference/adr/0008-entries-do-not-call-entries.html +4 -4
- package/site-dist/reference/adr/0009-versioned-install-layout.html +4 -4
- package/site-dist/reference/adr/0010-generated-feature-overview.html +4 -4
- package/site-dist/reference/adr/0011-resume-after-completion.html +4 -4
- package/site-dist/reference/adr/0012-review-panel.html +4 -4
- package/site-dist/reference/adr/0013-no-release-tooling.html +4 -4
- package/site-dist/reference/adr/0014-notifier-slot.html +4 -4
- package/site-dist/reference/adr/0015-incremental-continue.html +4 -4
- package/site-dist/reference/adr/0016-upgrade-from-tags.html +4 -4
- package/site-dist/reference/adr/0017-use-paseo-capabilities.html +4 -4
- package/site-dist/reference/adr/0018-own-worktree-option.html +4 -4
- package/site-dist/reference/adr/0019-parallel-orchestration.html +4 -4
- package/site-dist/reference/adr/0020-scheduling-through-the-chat.html +4 -4
- package/site-dist/reference/adr/0021-npm-version-manager.html +4 -4
- package/site-dist/reference/adr/0022-develop-and-verify-phases.html +4 -4
- package/site-dist/reference/adr/0023-types-gate-slices.html +4 -4
- package/site-dist/reference/adr/0024-confirm-early-review-axes.html +4 -4
- package/site-dist/reference/adr/0025-joint-requirements.html +4 -4
- package/site-dist/reference/adr/0026-machine-records-and-api.html +4 -4
- package/site-dist/reference/adr/0027-paseo-plugin.html +4 -4
- package/site-dist/reference/adr/0028-external-integrations.html +4 -4
- package/site-dist/reference/adr/0029-requirements-from-cards.html +4 -4
- package/site-dist/reference/adr/0030-card-intake-process.html +4 -4
- package/site-dist/reference/adr/0031-fewer-interventions.html +4 -4
- package/site-dist/reference/adr/0032-npm-only-install.html +4 -4
- package/site-dist/reference/adr/0033-prototype-review-and-ui-check.html +4 -4
- package/site-dist/reference/adr/index.html +4 -4
- package/site-dist/reference/commands.html +4 -4
- package/site-dist/reference/config.html +4 -4
- package/site-dist/reference/contracts/cli-json.html +5 -5
- package/site-dist/reference/contracts/feature-files.html +4 -4
- package/site-dist/reference/contracts/index.html +4 -4
- package/site-dist/reference/contracts/notifier-event.html +4 -4
- package/site-dist/reference/contracts/result-file.html +4 -4
- package/site-dist/reference/contracts/spec.html +4 -4
- package/site-dist/reference/contracts/state.html +5 -5
- package/site-dist/reference/contracts/usage-record.html +6 -6
- package/site-dist/reference/contracts/verify.html +4 -4
- package/site-dist/reference/scope.html +4 -4
- package/paseo-plugin/client/review-link.tsx +0 -29
- package/site-dist/assets/changelog.md.CoelFFCL.js +0 -1
- package/site-dist/assets/chunks/@localSearchIndexroot.Hdq0-4L3.js +0 -1
- package/site-dist/assets/reference_contracts_cli-json.md.N-v2HNUd.js +0 -1
- package/site-dist/assets/reference_contracts_cli-json.md.N-v2HNUd.lean.js +0 -1
- package/site-dist/assets/reference_contracts_state.md.5bNTC3ow.js +0 -1
- /package/site-dist/assets/{guide_getting-started.md.B4GJeoPz.lean.js → guide_getting-started.md.Di4587Bu.lean.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
每个版本分为"使用者会看到的变化"和"升级须知"。升级步骤见 [docs/guide/安装与升级.md](docs/guide/安装与升级.md)。
|
|
4
4
|
|
|
5
|
+
## 1.13.0-beta.4
|
|
6
|
+
|
|
7
|
+
### 使用者会看到的变化
|
|
8
|
+
|
|
9
|
+
- **token 花在哪**:`req stats` 和插件的"使用统计"页新增一节,按步骤、按模型列出 token(输入 + 输出)和占比,单独算出返工(方案修订、评审问题修复、缺陷修复、复验,以及第 2 轮起的审核和评审)占了多少、输入中多少来自提示缓存、每个完成需求的中位数。每一步用的模型从这个版本开始记录。
|
|
10
|
+
- **token 预算**(可选,`budget.tokens`):用到 `budget.warnPercent`(默认 80%)时在对话里告知一次;用完时已完成的步骤保留,在下一步开始前停下,问需求方是否继续,继续会再给一份预算。
|
|
11
|
+
- **还要多久**:本机已完成 3 个以上同档位需求后,开始开发时给出这类需求通常的 agent 用时和 token;`req status` 和插件的需求页面显示"预计还要约多久"。
|
|
12
|
+
- **后台进程意外退出时直接继续,不再叫醒对话**:装了插件时,插件每 5 分钟在 daemon 里检查一次,发现"运行中但后台进程已不在"的需求就直接继续(`req resume`),不经过模型;这时流水线不再预约对话每小时一次的定时检查,省下这部分 token。接单器也改为直接继续。
|
|
13
|
+
- **等待批准权限进"等你处理"**:步骤 agent 在等你允许一项操作时,插件的"等你处理"列出要批准的操作,点"打开这个 agent"去处理。
|
|
14
|
+
|
|
15
|
+
### 升级须知
|
|
16
|
+
|
|
17
|
+
- 新配置 `budget`(默认不限)。用 `req stats` 看过"每个完成需求的中位数"后,可以按它的两三倍设置。
|
|
18
|
+
- 停下原因新增 `budget`;状态文件新增 `budget`、`waitingPermission`;使用记录的步骤新增 `cachedInputTokens`、`agent`;`req api` 新增 `watchdog`,`status` 新增 `estimate`,`attention` 新增 `permission`。
|
|
19
|
+
- 插件设为只读时不做自动继续。
|
|
20
|
+
- CI 新增 Windows 任务(暂时只报告结果,不阻断发布)。
|
|
21
|
+
|
|
22
|
+
## 1.13.0-beta.3
|
|
23
|
+
|
|
24
|
+
### 使用者会看到的变化
|
|
25
|
+
|
|
26
|
+
- **原型审查页在 Paseo 里打开**:网页端和桌面端不再跳到单独的浏览器窗口,审查页嵌在插件的"审查原型"页面中;手机上仍在浏览器中打开,页面右上角也可以"在新窗口打开"。
|
|
27
|
+
- **按钮更醒目**:"等你处理"和需求页面最上面,有原型的方案确认显示为主按钮"审查原型并确认方案"("确认方案"变为次要按钮);没在等确认时也有"审查原型"按钮。请你确认方案的对话消息也会告诉你去点这个按钮。
|
|
28
|
+
|
|
29
|
+
### 升级须知
|
|
30
|
+
|
|
31
|
+
- 审查页现在允许被本机地址上的页面嵌入(`frame-ancestors`);从别的电脑访问时(设了 `REQ_KIT_REVIEW_HOST`)允许任何页面嵌入,地址中的令牌仍是访问的唯一凭证。可以用 `REQ_KIT_REVIEW_FRAME_ANCESTORS` 指定。
|
|
32
|
+
|
|
5
33
|
## 1.13.0-beta.2
|
|
6
34
|
|
|
7
35
|
在 Paseo 网页端审查原型,界面验证不再依赖桌面端和模型的视觉能力。见 [ADR 0033](docs/adr/0033-prototype-review-and-ui-check.md)。
|
package/contracts/cli-json.md
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
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`,原文件备份 |
|
|
34
|
+
| `req api watchdog [--by plugin\|factory] [--dry-run]` | 1.13 起。`{ checked, resumed, failed, skipped }`:把本机"运行中但后台进程已不在"的需求直接继续(`req resume`)。每个进程只继续一次,每个需求 24 小时内最多 3 次;`--by plugin` 同时记下插件在检查(流水线据此不再预约对话的定时检查) |
|
|
34
35
|
| `req api review-url <需求> [--repo <目录>] [--read-only]` | 1.13 起。`{ url, readOnly }`:这个需求的原型审查页地址(需要时在后台启动审查页的服务)。`--read-only` 的地址只能看,不能提交意见或确认方案。需求不在本机的需求索引中或还没有 `prototype/` 时报 `not_found` |
|
|
35
36
|
|
|
36
37
|
`<需求>` 的写法与 `req status` 相同:功能目录、目录名或名称中能唯一确定的一部分。需求在为它新建的工作区中开发时,`--repo` 写对话所在的目录或那个工作区都可以。
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
| `status` | 状态文件的 `status`;还没运行过为 null |
|
|
52
53
|
| `running` | 后台进程是否在运行 |
|
|
53
54
|
| `phase` | `develop` / `verify` |
|
|
54
|
-
| `attention` | 需要人处理的事:`approval`(确认方案)、`business`(需求方决定)、`interrupted`(中断,可以继续)、`not_completed`(结束但未完成);没有为 null |
|
|
55
|
+
| `attention` | 需要人处理的事:`approval`(确认方案)、`business`(需求方决定)、`permission`(1.13,步骤在等权限批准;`waiting.questions` 是要批准的操作,`waiting.agentId` 是那个 agent)、`interrupted`(中断,可以继续)、`not_completed`(结束但未完成);没有为 null |
|
|
55
56
|
| `waiting` | 1.12 起。`attention` 不为 null 时:`since`(从什么时候开始等)、`questions`(需求方要回答的问题)、`reason`(停止原因)、`card`(`{ system, id, url, requester }`,从卡片开始的需求)、`canReply`(能否用 `req api run reply <需求> <回复>` 把回复转给需求的对话);否则为 null |
|
|
56
57
|
| `current` | 正在运行的步骤 `[{ key, title, agentId }]` |
|
|
57
58
|
| `updatedAt` | 最近一次变化的时间 |
|
|
@@ -69,5 +70,6 @@
|
|
|
69
70
|
| `run` | `{ attempt, startedAt, updatedAt, pid, phaseMode, stopReason, outcome, queuedSince, verifyAt, approval, originAgentId }`;没运行过为 null |
|
|
70
71
|
| `steps` | 每个步骤一项:`{ key, stepId, title, phase, attempt, round, agentId, startedAt, endedAt, minutes, outcome, result, error, usage }`;`title` 是给人看的名称(如"代码评审(第 2 轮)"),`result` 是结论的文字(如"需要修改") |
|
|
71
72
|
| `gate` | 质量门:状态文件中的 `gate` |
|
|
73
|
+
| `estimate` | 1.13 起。`{ basis, profile, typicalAgentMinutes, typicalTokens, usedAgentMinutes, usedTokens, leftMinutes, over }`:按本机已完成的同档位需求(至少 3 个)的中位数估算;数据不够或已完成时为 null |
|
|
72
74
|
| `joint` | 联合需求的其他仓库:`[{ name, slug, repo, sourceRepo, branch, workspaceId, gate }]` |
|
|
73
75
|
| `external` | 外部系统中的卡片(1.9.0 起):`[{ name, id, url, status, syncedAt, pending, lastError }]`;`pending` 是等待补发的调用数 |
|
package/contracts/state.md
CHANGED
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
| `results` | object | 已完成步骤的结果,键为步骤键(如 `code_review-r2`) |
|
|
19
19
|
| `history` | array | 每次步骤执行的记录:agent、起止时间、结果、错误;`usage`(可选,1.3.0 起)为 agent 报告的用量:`inputTokens`、`cachedInputTokens`、`outputTokens`、`totalCostUsd`,agent 没有报告时为 null 或缺少;`leftRunning`(可选,1.11 起)为 true 表示失败时 agent 可能还在运行,下一次运行这一步时先看它;`reattached`(可选,1.11 起)为接着等上一次 agent 的次数 |
|
|
20
20
|
| `stopReason` / `outcome` | string \| null | 停止原因;最终结论 |
|
|
21
|
-
| `stopKind` | string \| null | 可选,1.12 起。停下等人的原因代码:必要的决定 `approval`(确认方案)、`phase_gate`(研发完成,等待测试)、`business`(需求方决定)、`reproduce`(缺陷复现不了);中断 `timeout`、`agent_error`、`invalid_result`、`transient`、`stopped`、`process_exit`、`other`。运行中或完成时为 null |
|
|
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
|
+
| `budget` | object | 可选,1.13 起,配置了 `budget.tokens` 时。`{ allowance, warnedAt }`:停下前允许用到的 tokens(预算,每次用完后继续再加一份);已经提醒过的那一档 |
|
|
23
|
+
| `waitingPermission` | object \| null | 可选,1.13 起。步骤 agent 在等权限批准时:`{ agentId, stepTitle, requests, since }`;没有在等时为 null |
|
|
22
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` |
|
|
23
25
|
| `profile` | `quick` \| `standard` | 档位 |
|
|
24
26
|
| `branch` | string \| null | 工作分支,启动时确定 |
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
| `schema` / `kind` | `1` / `"run"` |
|
|
31
31
|
| `machine` / `kitVersion` | 主机名、req-kit 版本 |
|
|
32
32
|
| `requirement` | `{ key, project, name, title, type, profile }`;从卡片开始的需求(1.10 起)另有 `requester`(卡片的提出人)和 `source`(`{ system, id }`) |
|
|
33
|
+
| `steps[].cachedInputTokens` / `steps[].agent` | 1.13 起。输入中来自提示缓存的 tokens(agent 报告时);这一步用的 agent(Paseo profile 名,或 "provider/model")。更早的记录没有 |
|
|
33
34
|
| `run` | `{ startedAt, endedAt, attempt, phase, phaseMode, status, outcome, stopReason, stopKind, recoveries }`;`status` 是运行结束时的状态,`outcome` 是完成时报告的结论;`stopKind`(1.12 起)是停下等人的原因代码(见 [state.md](state.md)),`recoveries`(1.12 起)是这次运行中自动处理的次数,按种类,如 `{ "retry": 2 }`。更早的记录没有这两项 |
|
|
34
35
|
| `agents` | `{ builder, reviewer }`:当时配置的 agent(如 `claude/opus(bypassPermissions)`) |
|
|
35
36
|
| `steps` | 这次运行中开始的步骤:`{ key, stepId, phase, attempt, round, startedAt, endedAt, minutes, outcome, inputTokens, outputTokens, costUsd }`;agent 没有报告的用量为 null |
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
- 交付周期:需求第一次开始到完成(最后一次运行以"已完成"结束)的小时数,用这个需求的全部运行,算在完成的那段时间。
|
|
44
45
|
- 代码评审轮数、缺陷修复轮数:不同的(尝试、轮次、仓库)个数;评审分方向或评审小组的几个成员算一轮。
|
|
45
46
|
- 等待:相邻两次运行之间的间隔,按前一次结束时的状态归类:等待确认方案、等待需求方决定、研发完成到开始测试验证、中断到继续。
|
|
47
|
+
- token 花在哪(1.13 起,`tokens`):这段时间的步骤按 `stepId`、按 `agent` 合计 token(输入 + 输出)和占比;返工(`design_revise`、`review_fix`、`defect_fix`、`reverify`,以及 `design_review`、`code_review` 第 2 轮起)的合计和占比;报告了 `cachedInputTokens` 的,算出输入中来自提示缓存的比例;已完成需求的 token 中位数(按档位)。
|
|
46
48
|
- 停下等人的原因(1.12 起):这段时间结束的运行按 `stopKind` 计数;更早的记录按结束时的状态推断。确认方案、研发完成等待测试是必要的决定。
|
|
47
49
|
- 无人工到提测的比例(1.12 起):这段时间有进展、已经研发完成或完成的需求中,所有运行都只停在必要决定处的比例。
|
|
48
50
|
- 自动处理的比例(1.12 起):自动处理的次数 ÷(自动处理的次数 + 以中断结束的运行数);需求方决定、缺陷复现不了不算中断。
|
|
@@ -64,7 +64,7 @@ req-kit 帮你把一个需求从"我想要……"做到"可以验收":你在 P
|
|
|
64
64
|
|
|
65
65
|
1.12 起,首页最上面是"等你处理":所有等你确认方案、回答问题、中断了的需求,等得最久的在前,每条写明要做什么、等了多久。方案可以直接确认,也可以写修改意见;方案审核的问题可以直接写回答,插件会转给这个需求的对话,效果和在对话里回答一样。首页的"配置"列出 req-kit 的全部设置:每一项是什么、现在的值、来自本机、团队还是项目的配置文件,可以按项目查看、搜索、只看改过的。
|
|
66
66
|
|
|
67
|
-
**在原型上提意见(1.13 起)。**
|
|
67
|
+
**在原型上提意见(1.13 起)。** 界面改动的需求,在请你确认方案时会有一份界面原型。在插件首页的"等你处理"或需求页面最上面,点**"审查原型并确认方案"**按钮(终端里是 `req review <需求>`),原型审查页就在 Paseo 里打开(手机上会在浏览器中打开;嫌窗口小时点右上角"在新窗口打开"):
|
|
68
68
|
|
|
69
69
|
- 标着 ①②③ 的是这次要改的地方,右边写着各自对应的验收标准,顺着看就不会漏。
|
|
70
70
|
- 想改哪里就**点哪里**,选一种改法(文字改成…、大一点、换颜色、换位置、删掉、这里加东西……),再用自己的话补一句。不用想专业词汇,你点的是哪个按钮、原来是什么颜色和大小,页面会自动记下来交给 AI。
|
|
@@ -72,13 +72,16 @@ req-kit 帮你把一个需求从"我想要……"做到"可以验收":你在 P
|
|
|
72
72
|
- 正常、出错、空数据这些状态,以及电脑、手机宽度,在页面上方切换。
|
|
73
73
|
- 写的意见会自动保存。看完后点"提交修改意见",需求会话会据此改需求说明,改好后请你再确认;改过的原型再打开时,会用绿色框标出这一版改了哪里。没有要改的,点"同意开始写代码"。
|
|
74
74
|
|
|
75
|
+
**还要多久(1.13 起)。** 本机做完过 3 个以上同档位的需求后,开始开发时会告诉你这类需求通常要多久、用多少 token;问"做到哪了"或在插件的需求页面里,会看到"预计还要约多久"(不含等人确认的时间)。
|
|
76
|
+
|
|
75
77
|
每个步骤都会出现在这个对话的子 agent 列表里(名字形如"[20261012-sms-login] 6-代码评审 第2轮"),点开可以看到它在做什么。功能目录下的 `progress.md` 按时间记录了每一步的结果。
|
|
76
78
|
|
|
77
79
|
可能收到的通知(团队接入了企业 IM 的话,这些通知也会发到 IM):
|
|
78
80
|
|
|
79
81
|
- **需要你决定**:方案审核发现只有你能回答的问题。AI 会逐个问你,然后继续。一天没有回答的话,会在对话里提醒一次。
|
|
80
82
|
- **研发完成,可以提测**(分阶段执行时):代码已写完并通过评审,提测说明在 `handoff.md`。一天没有开始测试验证的话,会在对话里提醒一次。
|
|
81
|
-
- **等待批准**:某一步在等权限批准,按提示打开对应的 agent 处理,或请开发同事处理。
|
|
83
|
+
- **等待批准**:某一步在等权限批准,按提示打开对应的 agent 处理,或请开发同事处理。1.13 起插件的"等你处理"里也会显示,列出要批准的操作,点"打开这个 agent"直接去处理。
|
|
84
|
+
- **token 用量提醒**(团队设了 token 预算时):用到预算的一定比例时告知一次,开发照常进行;用完时做完当前步骤就停下,AI 会告诉你用了多少、问你是否继续。继续会再给一份预算。
|
|
82
85
|
- **中断**:限流、网络中断这类临时问题会先自动等待重试,仍然不行才中断,并预约半小时后自动继续(说"先停一下"可以取消)。AI 会说明原因和日志位置。环境类问题(例如测试服务没启动)你可以自己处理;涉及配置或代码的请开发同事处理。处理后说"继续开发"。
|
|
83
86
|
|
|
84
87
|
## 验收
|
|
@@ -43,11 +43,11 @@ spec 头部的 `- 类型:` 决定流水线走哪种做法(1.8 起)。缺
|
|
|
43
43
|
|
|
44
44
|
等待确认时进度显示"等待需求方确认方案",需求总览归入"需要处理",外部通知收到 `approval` 事件。评估类需求不写代码,不经过确认点。
|
|
45
45
|
|
|
46
|
-
**原型审查页(1.13 起)。** 有 `prototype/` 的需求,插件的需求页面和"等你处理"里有"
|
|
46
|
+
**原型审查页(1.13 起)。** 有 `prototype/` 的需求,插件的需求页面和"等你处理"里有"审查原型"按钮(等待确认方案时是最醒目的"审查原型并确认方案"),审查页嵌在 Paseo 的网页端和桌面端里打开,手机上在浏览器中打开;终端里是 `req review <需求>`。需求方在原型上点选要改的地方(页面记下元素、文字、颜色、大小和位置)、双击直接改文字,切换状态和电脑、手机宽度,然后提交:意见整理成带位置信息的清单转给需求会话(与插件中写修改意见的效果相同),或者直接确认方案。草稿和提交记录在功能目录的 `.pipeline/review/` 中,每一版原型也存在那里,原型改过后页面会标出这一版改了哪里。
|
|
47
47
|
|
|
48
48
|
为了让审查页能用,方案设计按约定写原型:每个改动区域标 `data-ac`(对应的 AC)和 `data-label`(用需求方的话说是什么),各种状态包在 `<section data-state="状态名">` 中,按钮、标题、字段名写最终的文字。原型里的脚本不会运行。
|
|
49
49
|
|
|
50
|
-
审查页由一个本机服务提供:用到时在后台启动,空闲 4 小时后自己退出,只监听 `127.0.0.1
|
|
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 让插件拿到新的环境变量。
|
|
51
51
|
|
|
52
52
|
## 代码评审分两个方向
|
|
53
53
|
|
|
@@ -256,6 +256,19 @@ spec 头部的 `- 类型:` 决定流水线走哪种做法(1.8 起)。缺
|
|
|
256
256
|
|
|
257
257
|
开始前,流水线读取 Paseo 的用量信息(需要 Paseo 能读到对应 agent 的订阅用量)。所用 agent 的某个额度窗口用到 `quotaWarnPercent`(默认 85)以上时,在对话里提醒一次,流水线照常进行;用完时遇到限流会自动等待重试。设为 0 关闭提醒。
|
|
258
258
|
|
|
259
|
+
## token 预算和用时估算(1.13 起)
|
|
260
|
+
|
|
261
|
+
内网模型通常不报金额,所以按 token(输入 + 输出)计:
|
|
262
|
+
|
|
263
|
+
| 配置 | 默认 | 说明 |
|
|
264
|
+
| --- | --- | --- |
|
|
265
|
+
| `budget.tokens` | `null`(不限) | 一个需求所有步骤、所有尝试合计最多用多少 tokens,例如 `3000000`。每个新步骤开始前检查:用完时,已完成的步骤都保留,流水线在下一步开始前停下(状态"已中断",停下原因 `budget`),请对话问需求方是否继续;继续(`req resume` 或在对话里说"继续开发")会再给一份同样多的预算 |
|
|
266
|
+
| `budget.warnPercent` | 80 | 用到预算的这个比例时在对话里告知一次(不停),同时发 `milestone` 外部通知 |
|
|
267
|
+
|
|
268
|
+
写在本机、团队或项目配置中都可以,例如 `"budget": { "tokens": 3000000 }`。还没有数据时,先不设预算跑几个需求,用 `req stats` 的"token 花在哪"看每个需求的中位数,再按它的两三倍设。
|
|
269
|
+
|
|
270
|
+
用时估算不用配置:本机已完成的同档位需求有 3 个以上时,开始开发的回复里写出"agent 合计用时和 token 的中位数"作为参考;`req status` 和插件的需求页面显示"预计还要约多久"(按中位数减去已用的 agent 时间,不含等人的时间),超过中位数时如实说"已超过通常的用时"。
|
|
271
|
+
|
|
259
272
|
## 并行编排:确认前审阅、方案提案、浏览器验证
|
|
260
273
|
|
|
261
274
|
| 配置 | 默认 | 作用 |
|
|
@@ -280,7 +293,7 @@ Paseo 自带的 `/paseo-advisor`、`/paseo-committee` 等编排 skill 也可以
|
|
|
280
293
|
| `waitingReminderHours` | 24 | 等待需求方回答、或研发完成后等待测试验证超过这么久,在对话里提醒一次;0 关闭 |
|
|
281
294
|
| (启动参数 `--verify-at`) | — | 分阶段时,研发完成后在这个时间自动开始测试验证(写法同 `--at`;隐含分阶段)。研发完成时时间已过就直接继续 |
|
|
282
295
|
| `autoResumeMinutes` / `autoResumeMax` | 30 / 2 | 临时错误(重试用完)中断后,这么久之后在对话里自动继续;每次尝试最多几次;0 关闭 |
|
|
283
|
-
| `watchdogMinutes` | 60(1.12 起默认打开) | 运行期间对话定时检查,后台进程意外退出时自动继续。每次检查对话 agent 会运行一个回合;0
|
|
296
|
+
| `watchdogMinutes` | 60(1.12 起默认打开) | 运行期间对话定时检查,后台进程意外退出时自动继续。每次检查对话 agent 会运行一个回合;0 关闭。1.13 起:装了 req-kit 插件时,插件每 5 分钟在 daemon 里直接检查本机所有需求,发现"运行中但后台进程已不在"的就执行 `req resume`(不经过模型,每个进程只继续一次,每个需求 24 小时内最多 3 次),这时不再预约对话的定时检查;插件设为只读时不做。接单器(`req factory`)每次检查也同样直接继续 |
|
|
284
297
|
| `maxConcurrentPipelines` | 0(不限) | 本机同时运行的流水线上限,满了排队。只能写在本机配置中才有意义 |
|
|
285
298
|
|
|
286
299
|
## 需求总览
|
|
@@ -291,7 +304,7 @@ Paseo 自带的 `/paseo-advisor`、`/paseo-committee` 等编排 skill 也可以
|
|
|
291
304
|
|
|
292
305
|
1.9.0 起,每次流水线运行结束时,引擎在本机的 `~/.req-kit/usage/runs.jsonl` 追加一条记录:各步骤的用时和用量、质量门、评审和缺陷轮数、结束时的状态(格式见 [使用记录](../../contracts/usage-record.md))。只记在本机,不记需求内容和代码,也不会自动发往任何地方。
|
|
293
306
|
|
|
294
|
-
- `req stats`:最近 30 天的统计(`--since 2w`、`--since 2026-10-01`、`--month 2026-10`):有进展和完成的需求数、交付周期、返工(评审轮数、需要缺陷修复的占比、质量门失败)、各种等待的时长、各步骤用时、用量,以及不同 builder
|
|
307
|
+
- `req stats`:最近 30 天的统计(`--since 2w`、`--since 2026-10-01`、`--month 2026-10`):有进展和完成的需求数、交付周期、返工(评审轮数、需要缺陷修复的占比、质量门失败)、各种等待的时长、各步骤用时、用量,以及不同 builder 的比较。1.13 起有"token 花在哪"一节:按步骤和按模型的 token 及占比、返工(方案修订、评审问题修复、缺陷修复、复验,以及第 2 轮起的审核和评审)的占比、输入中来自提示缓存的比例、每个完成需求的中位数;插件的"使用统计"页用条形图显示同样的内容。`--json`、`--csv` 输出机器可读的格式。
|
|
295
308
|
- `req stats --report --month 2026-10 [--out 目录]`:生成给管理方汇报用的两份文件:`req-kit-usage-2026-10.md`(汇总)和 `.csv`(每个需求一行,可以用表格软件打开)。
|
|
296
309
|
- 几台机器一起汇报:每台机器运行 `req stats --month 2026-10 --export`,得到 `req-kit-usage-<主机名>-<日期>.jsonl`;把这些文件放到一起,运行 `req stats --merge <文件…> --month 2026-10 --report`。
|
|
297
310
|
|
package/engine/src/api.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// Reading never changes anything. `run` performs the same command as `req <command>` in that
|
|
18
18
|
// directory, so the engine stays the only one that changes a requirement's state.
|
|
19
19
|
|
|
20
|
+
import { progressOf, typical } from "./estimate.js";
|
|
20
21
|
import { spawnSync } from "node:child_process";
|
|
21
22
|
import fs from "node:fs";
|
|
22
23
|
import path from "node:path";
|
|
@@ -67,11 +68,12 @@ export const READABLE_FILES = [
|
|
|
67
68
|
|
|
68
69
|
const MAX_FILE_BYTES = 512 * 1024;
|
|
69
70
|
|
|
70
|
-
export type Attention = "approval" | "business" | "interrupted" | "not_completed" | null;
|
|
71
|
+
export type Attention = "approval" | "business" | "permission" | "interrupted" | "not_completed" | null;
|
|
71
72
|
|
|
72
73
|
/** What a person needs to do about a requirement, if anything. */
|
|
73
|
-
export function attentionOf(state: Pick<PipelineState, "status" | "pid" | "outcome"> | null): Attention {
|
|
74
|
+
export function attentionOf(state: Pick<PipelineState, "status" | "pid" | "outcome" | "waitingPermission"> | null): Attention {
|
|
74
75
|
if (!state) return null;
|
|
76
|
+
if (state.status === "running" && state.waitingPermission && isAlive(state.pid)) return "permission";
|
|
75
77
|
if (state.status === "waiting_for_approval") return "approval";
|
|
76
78
|
if (state.status === "waiting_for_business") return "business";
|
|
77
79
|
if (state.status === "failed" || (state.status === "running" && !isAlive(state.pid))) return "interrupted";
|
|
@@ -135,6 +137,8 @@ export interface ListItem {
|
|
|
135
137
|
card: { system: string; id: string; url: string | null; requester: string | null } | null;
|
|
136
138
|
/** Whether a reply can be sent to the requirement's chat (req reply). */
|
|
137
139
|
canReply: boolean;
|
|
140
|
+
/** 1.13, attention "permission": the step agent that waits for an approval. */
|
|
141
|
+
agentId?: string;
|
|
138
142
|
} | null;
|
|
139
143
|
}
|
|
140
144
|
|
|
@@ -178,10 +182,12 @@ function waitingOf(repo: string, featureDir: string, state: PipelineState | null
|
|
|
178
182
|
} catch {
|
|
179
183
|
card = null;
|
|
180
184
|
}
|
|
185
|
+
const permission = attentionOf(state) === "permission" ? state.waitingPermission : null;
|
|
181
186
|
return {
|
|
182
|
-
since: state.updatedAt,
|
|
183
|
-
questions: state.pendingQuestions ?? [],
|
|
184
|
-
reason: state.stopReason,
|
|
187
|
+
since: permission?.since ?? state.updatedAt,
|
|
188
|
+
questions: permission ? permission.requests : (state.pendingQuestions ?? []),
|
|
189
|
+
reason: permission ? `「${permission.stepTitle}」步骤在等待权限批准,批准前会一直停在这里` : state.stopReason,
|
|
190
|
+
...(permission ? { agentId: permission.agentId } : {}),
|
|
185
191
|
card,
|
|
186
192
|
canReply: Boolean(state.originAgentId) && (state.status === "waiting_for_business" || state.status === "waiting_for_approval"),
|
|
187
193
|
};
|
|
@@ -294,12 +300,22 @@ export function apiStatus(repoArg: string, feature: string) {
|
|
|
294
300
|
}
|
|
295
301
|
: null,
|
|
296
302
|
steps: state ? state.history.map((h) => stepJson(h, state)) : [],
|
|
303
|
+
// 1.13: what to expect, from this machine's completed requirements of the same profile.
|
|
304
|
+
estimate: estimateOf(state),
|
|
297
305
|
gate: state?.gate ?? null,
|
|
298
306
|
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 })),
|
|
299
307
|
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 })),
|
|
300
308
|
};
|
|
301
309
|
}
|
|
302
310
|
|
|
311
|
+
function estimateOf(state: PipelineState | null) {
|
|
312
|
+
if (!state || state.status === "completed") return null;
|
|
313
|
+
const t = typical(state.profile ?? "standard");
|
|
314
|
+
if (!t) return null;
|
|
315
|
+
const p = progressOf(state, t);
|
|
316
|
+
return { basis: t.basis, profile: t.profile, typicalAgentMinutes: t.agentMinutes, typicalTokens: t.tokens, ...p };
|
|
317
|
+
}
|
|
318
|
+
|
|
303
319
|
export function apiFile(repoArg: string, feature: string, file: string) {
|
|
304
320
|
if (!(READABLE_FILES as readonly string[]).includes(file)) {
|
|
305
321
|
throw new ApiError("not_allowed", `只能读取这些文件:${READABLE_FILES.join("、")}`);
|
|
@@ -450,6 +466,14 @@ export function apiMain(argvIn: string[], cwd = process.cwd()): unknown {
|
|
|
450
466
|
const { message } = configureJson(["--set", value]);
|
|
451
467
|
return { ok: true, message };
|
|
452
468
|
}
|
|
469
|
+
case "watchdog": {
|
|
470
|
+
// 1.13: continue pipelines whose process died (the plugin runs this every few minutes).
|
|
471
|
+
const by = (option(argv, "--by") ?? "manual") as "plugin" | "factory" | "manual";
|
|
472
|
+
const dryRun = flag(argv, "--dry-run");
|
|
473
|
+
return import("./watchdog.js").then(({ watchdogPass }) =>
|
|
474
|
+
watchdogPass({ by: ["plugin", "factory"].includes(by) ? by : "manual", dryRun, resume: (r, f) => apiRun("resume", r, f, []) }),
|
|
475
|
+
);
|
|
476
|
+
}
|
|
453
477
|
case "review-url": {
|
|
454
478
|
// 1.13: the prototype review page of a requirement (starts the page's server when needed).
|
|
455
479
|
const feature = need("需求");
|
package/engine/src/card-cmd.ts
CHANGED
|
@@ -147,6 +147,8 @@ export interface CardCmdOptions {
|
|
|
147
147
|
skipHealthCheck?: boolean;
|
|
148
148
|
/** The time, for tests (1.12). */
|
|
149
149
|
now?: Date;
|
|
150
|
+
/** Continues a requirement (`req resume`); tests replace it (1.13, factory watchdog). */
|
|
151
|
+
resume?: (repo: string, featureDir: string) => { ok: boolean; message: string };
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
export interface BeginResult {
|
|
@@ -117,6 +117,11 @@ const CATALOG: Array<{ id: string; title: string; description: string; keys: Rec
|
|
|
117
117
|
browserVerify: { label: "检查网页界面", description: "功能验证时用界面检查命令(不靠看图)检查网页;Paseo 有内置浏览器时也用它操作。", display: (v) => ({ auto: "自动", off: "关" })[String(v)] ?? String(v) },
|
|
118
118
|
paseoServices: { label: "由 Paseo 启动服务", description: "用 paseo.json 中的服务脚本启动和重启服务。", display: (v) => ({ auto: "自动", off: "关" })[String(v)] ?? String(v) },
|
|
119
119
|
serviceReadySeconds: { label: "等服务就绪", description: "启动服务后最多等多久。", display: (v) => `${v} 秒` },
|
|
120
|
+
budget: {
|
|
121
|
+
label: "每个需求的 token 预算",
|
|
122
|
+
description: "一个需求所有步骤合计(输入 + 输出)最多用多少 tokens;用到一定比例时提醒,用完后做完当前步骤就停下,继续开发会再给一份预算。",
|
|
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
|
+
},
|
|
120
125
|
},
|
|
121
126
|
},
|
|
122
127
|
{
|
package/engine/src/config.ts
CHANGED
|
@@ -240,6 +240,11 @@ export interface PipelineConfig {
|
|
|
240
240
|
* starts this many hours after its summary is on the card, unless the requester objects; 0: off.
|
|
241
241
|
*/
|
|
242
242
|
autoConfirm: { hours: number };
|
|
243
|
+
/**
|
|
244
|
+
* 1.13: tokens (input + output, all steps and attempts) one requirement may use before the
|
|
245
|
+
* pipeline stops to ask; null: no limit. A notice at warnPercent; continuing gives one more budget.
|
|
246
|
+
*/
|
|
247
|
+
budget: { tokens: number | null; warnPercent: number };
|
|
243
248
|
/** Automatic retries of a step after a transient error (rate limit, dropped connection). */
|
|
244
249
|
retry: { attempts: number; backoffSeconds: number[] };
|
|
245
250
|
/**
|
|
@@ -349,6 +354,7 @@ export const DEFAULT_CONFIG: PipelineConfig = {
|
|
|
349
354
|
cardSource: null,
|
|
350
355
|
factory: { pollMinutes: 5, titleTags: ["[AI]", "【AI】"], maxNewPerPoll: 3 },
|
|
351
356
|
autoConfirm: { hours: 0 },
|
|
357
|
+
budget: { tokens: null, warnPercent: 80 },
|
|
352
358
|
retry: { attempts: 2, backoffSeconds: [60, 300] },
|
|
353
359
|
paseoServices: "auto",
|
|
354
360
|
serviceReadySeconds: 120,
|
|
@@ -417,6 +423,7 @@ function merge(base: PipelineConfig, raw: Record<string, unknown>): PipelineConf
|
|
|
417
423
|
merged.push = { ...base.push, ...(isRecord(raw.push) ? raw.push : {}) } as PipelineConfig["push"];
|
|
418
424
|
merged.factory = { ...base.factory, ...(isRecord(raw.factory) ? raw.factory : {}) } as PipelineConfig["factory"];
|
|
419
425
|
merged.autoConfirm = { ...base.autoConfirm, ...(isRecord(raw.autoConfirm) ? raw.autoConfirm : {}) } as PipelineConfig["autoConfirm"];
|
|
426
|
+
merged.budget = { ...base.budget, ...(isRecord(raw.budget) ? raw.budget : {}) } as PipelineConfig["budget"];
|
|
420
427
|
if (isRecord(raw.cardSource)) {
|
|
421
428
|
const c = raw.cardSource;
|
|
422
429
|
merged.cardSource = {
|
|
@@ -632,6 +639,12 @@ function validate(config: PipelineConfig): void {
|
|
|
632
639
|
throw new ConfigError('people 必须写成 { "<姓名>": { "<账号类型>": "<账号>" } },例如 { "张广祥": { "um": "ZHANGGX001" } }');
|
|
633
640
|
}
|
|
634
641
|
if (typeof config.autoConfirm.hours !== "number" || config.autoConfirm.hours < 0) throw new ConfigError("autoConfirm.hours 必须是不小于 0 的小时数(0 表示关闭)");
|
|
642
|
+
if (config.budget.tokens !== null && (typeof config.budget.tokens !== "number" || !Number.isFinite(config.budget.tokens) || config.budget.tokens <= 0)) {
|
|
643
|
+
throw new ConfigError("budget.tokens 必须是正数(每个需求最多用多少 tokens,例如 3000000),或 null 表示不限");
|
|
644
|
+
}
|
|
645
|
+
if (typeof config.budget.warnPercent !== "number" || config.budget.warnPercent <= 0 || config.budget.warnPercent >= 100) {
|
|
646
|
+
throw new ConfigError("budget.warnPercent 必须在 1 到 99 之间(用到预算的百分之几时提醒)");
|
|
647
|
+
}
|
|
635
648
|
const f = config.factory;
|
|
636
649
|
if (typeof f.pollMinutes !== "number" || !(f.pollMinutes >= 1)) throw new ConfigError("factory.pollMinutes 必须是至少 1 的分钟数");
|
|
637
650
|
if (!Number.isInteger(f.maxNewPerPoll) || f.maxNewPerPoll < 1) throw new ConfigError("factory.maxNewPerPoll 必须是正整数");
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// How long a requirement usually takes and how many tokens it uses (1.13), from this machine's
|
|
2
|
+
// run records (~/.req-kit/usage/runs.jsonl): the medians of completed requirements of the same
|
|
3
|
+
// profile. Shown when a run starts and in the status, so people know roughly what to expect.
|
|
4
|
+
import type { RunRecord } from "./records.js";
|
|
5
|
+
import { readRuns, runsFile } from "./records.js";
|
|
6
|
+
import type { PipelineState } from "./state.js";
|
|
7
|
+
import { byRequirement, dedupe } from "./stats.js";
|
|
8
|
+
|
|
9
|
+
/** Fewer completed requirements than this, and the numbers say little: no estimate. */
|
|
10
|
+
export const MIN_BASIS = 3;
|
|
11
|
+
|
|
12
|
+
export interface Typical {
|
|
13
|
+
profile: string;
|
|
14
|
+
/** How many completed requirements the medians are from. */
|
|
15
|
+
basis: number;
|
|
16
|
+
/** Median of the agents' working time per requirement (sum of step durations). */
|
|
17
|
+
agentMinutes: number;
|
|
18
|
+
/** Median tokens (input + output) per requirement; null when agents did not report usage. */
|
|
19
|
+
tokens: number | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function median(values: number[]): number | null {
|
|
23
|
+
if (values.length === 0) return null;
|
|
24
|
+
const s = [...values].sort((a, b) => a - b);
|
|
25
|
+
const m = Math.floor(s.length / 2);
|
|
26
|
+
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function typical(profile: string, runs: RunRecord[] | null = null, env: NodeJS.ProcessEnv = process.env): Typical | null {
|
|
30
|
+
let records = runs;
|
|
31
|
+
if (records === null) {
|
|
32
|
+
try {
|
|
33
|
+
records = readRuns(runsFile(env));
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const done = byRequirement(dedupe(records)).filter((q) => q.status === "completed" && q.profile === profile);
|
|
39
|
+
if (done.length < MIN_BASIS) return null;
|
|
40
|
+
const tokens = done.flatMap((q) => (q.inputTokens === null && q.outputTokens === null ? [] : [(q.inputTokens ?? 0) + (q.outputTokens ?? 0)]));
|
|
41
|
+
return {
|
|
42
|
+
profile,
|
|
43
|
+
basis: done.length,
|
|
44
|
+
agentMinutes: median(done.map((q) => q.agentMinutes)) ?? 0,
|
|
45
|
+
tokens: tokens.length >= MIN_BASIS ? median(tokens) : null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function minutesText(m: number): string {
|
|
50
|
+
const total = Math.max(0, Math.round(m));
|
|
51
|
+
if (total < 60) return `${total} 分钟`;
|
|
52
|
+
const h = Math.floor(total / 60);
|
|
53
|
+
const rest = total % 60;
|
|
54
|
+
return rest ? `${h} 小时 ${rest} 分` : `${h} 小时`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function tokenText(n: number): string {
|
|
58
|
+
return n >= 10_000 ? `${Math.round(n / 1000) / 10} 万` : String(Math.round(n));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const PROFILE_NAMES: Record<string, string> = { quick: "快速档", standard: "标准档" };
|
|
62
|
+
|
|
63
|
+
/** One line for a run that is starting. */
|
|
64
|
+
export function typicalLine(t: Typical | null): string | null {
|
|
65
|
+
if (!t) return null;
|
|
66
|
+
return `参考:本机最近 ${t.basis} 个${PROFILE_NAMES[t.profile] ?? t.profile}需求,agent 合计用时中位数约 ${minutesText(t.agentMinutes)}${t.tokens !== null ? `,约 ${tokenText(t.tokens)} tokens` : ""}(不含等人确认的时间)。`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface Progress {
|
|
70
|
+
usedAgentMinutes: number;
|
|
71
|
+
usedTokens: number | null;
|
|
72
|
+
/** Minutes left by the median; 0 when already past it. */
|
|
73
|
+
leftMinutes: number;
|
|
74
|
+
over: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function progressOf(state: Pick<PipelineState, "history">, t: Typical, now = new Date()): Progress {
|
|
78
|
+
let minutes = 0;
|
|
79
|
+
let tokens: number | null = null;
|
|
80
|
+
for (const h of state.history) {
|
|
81
|
+
const end = h.endedAt ? new Date(h.endedAt) : now;
|
|
82
|
+
minutes += Math.max(0, (end.getTime() - new Date(h.startedAt).getTime()) / 60_000);
|
|
83
|
+
if (h.usage && (h.usage.inputTokens !== undefined || h.usage.outputTokens !== undefined)) tokens = (tokens ?? 0) + (h.usage.inputTokens ?? 0) + (h.usage.outputTokens ?? 0);
|
|
84
|
+
}
|
|
85
|
+
return { usedAgentMinutes: minutes, usedTokens: tokens, leftMinutes: Math.max(0, t.agentMinutes - minutes), over: minutes > t.agentMinutes };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** "预计还要…" for a requirement that is not finished; null without enough history. */
|
|
89
|
+
export function remainingLine(state: Pick<PipelineState, "history" | "status" | "profile">, t: Typical | null, now = new Date()): string | null {
|
|
90
|
+
if (!t || state.status === "completed") return null;
|
|
91
|
+
const p = progressOf(state, t, now);
|
|
92
|
+
const used = `已用 ${minutesText(p.usedAgentMinutes)}${p.usedTokens !== null ? `、${tokenText(p.usedTokens)} tokens` : ""}`;
|
|
93
|
+
const basis = `本机最近 ${t.basis} 个${PROFILE_NAMES[t.profile] ?? t.profile}需求的中位数 ${minutesText(t.agentMinutes)}${t.tokens !== null ? `、${tokenText(t.tokens)} tokens` : ""}`;
|
|
94
|
+
return p.over ? `预计:已超过通常的用时(${used};${basis}),复杂的需求会更久。` : `预计还要:约 ${minutesText(Math.max(5, p.leftMinutes))}(${used};${basis};不含等人的时间)`;
|
|
95
|
+
}
|
package/engine/src/factory.ts
CHANGED
|
@@ -18,14 +18,11 @@ import path from "node:path";
|
|
|
18
18
|
import { parseArgs } from "node:util";
|
|
19
19
|
import { cardRoutes, findCardEntry, hashText, parseJsonOutput } from "./card.js";
|
|
20
20
|
import { beginFromCard, type CardCmdOptions, cardIntegrations, checkCards, withRuntime } from "./card-cmd.js";
|
|
21
|
-
import { resumeCommand, statusCommand } from "./cli-args.js";
|
|
22
21
|
import { loadConfig, type PipelineConfig } from "./config.js";
|
|
23
|
-
import { watchdogPrompt } from "./messages.js";
|
|
24
22
|
import { dispatchNotifiers } from "./notifiers.js";
|
|
25
|
-
import { readIndex } from "./records.js";
|
|
26
23
|
import { type IntegrationConfig, renderArgs, runCommand, type Runner } from "./integrations.js";
|
|
27
24
|
import { defaultConfigPath, ENGINE_DIR, KIT_ROOT, kitHome, kitVersion } from "./locations.js";
|
|
28
|
-
import { isAlive
|
|
25
|
+
import { isAlive } from "./state.js";
|
|
29
26
|
|
|
30
27
|
const USAGE = `用法:
|
|
31
28
|
req factory once 检查一次:为新卡片开始需求,把需求方的新回复转给需求会话
|
|
@@ -231,7 +228,7 @@ export async function pollOnce(configPath: string, opts: CardCmdOptions = {}, no
|
|
|
231
228
|
}
|
|
232
229
|
|
|
233
230
|
try {
|
|
234
|
-
await
|
|
231
|
+
await continueInterrupted(env, opts, result, now);
|
|
235
232
|
} catch (error) {
|
|
236
233
|
result.errors.push(`续跑中断的需求:${(error as Error).message}`);
|
|
237
234
|
}
|
|
@@ -351,31 +348,17 @@ function appendLog(dir: string, text: string, now: Date): void {
|
|
|
351
348
|
* restarted, the process was killed): its chat gets the same message as the watchdog's, so
|
|
352
349
|
* the chat checks and continues it. Once per process; a chat that cannot be reached is noted.
|
|
353
350
|
*/
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
if (due.length === 0) return;
|
|
366
|
-
await withRuntime(config, async (runtime) => {
|
|
367
|
-
for (const d of due) {
|
|
368
|
-
const commands = { repo: d.repo, featureDir: d.featureDir, configPath: null };
|
|
369
|
-
try {
|
|
370
|
-
await runtime.notifyAgent(d.origin, watchdogPrompt({ featureDir: d.featureDir, statusCommand: statusCommand(commands), resumeCommand: resumeCommand(commands) }));
|
|
371
|
-
nudged[d.key] = d.pid;
|
|
372
|
-
result.resumed.push(`${path.join(d.repo, d.featureDir)}:后台进程已退出,已请它的对话检查并继续`);
|
|
373
|
-
} catch (error) {
|
|
374
|
-
result.errors.push(`${path.join(d.repo, d.featureDir)} 的后台进程已退出,但没能通知它的对话:${(error as Error).message}`);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
});
|
|
378
|
-
writeJson(file, nudged);
|
|
351
|
+
/**
|
|
352
|
+
* 1.13: pipelines whose background process died are continued directly (`req resume`), not by
|
|
353
|
+
* asking their chat: no model turn, and it works when the chat is archived or rate-limited.
|
|
354
|
+
*/
|
|
355
|
+
async function continueInterrupted(env: NodeJS.ProcessEnv, opts: CardCmdOptions, result: PollResult, now: Date): Promise<void> {
|
|
356
|
+
const { watchdogPass } = await import("./watchdog.js");
|
|
357
|
+
const { apiRun } = await import("./api.js");
|
|
358
|
+
const pass = watchdogPass({ env, by: "factory", now, resume: opts.resume ?? ((repo, featureDir) => apiRun("resume", repo, featureDir, [])) });
|
|
359
|
+
for (const r of pass.resumed) result.resumed.push(`${path.join(r.repo, r.featureDir)}:后台进程已退出,已自动继续`);
|
|
360
|
+
for (const r of pass.failed) result.errors.push(`${path.join(r.repo, r.featureDir)} 的后台进程已退出,自动继续失败:${r.message}`);
|
|
361
|
+
for (const r of pass.skipped) result.problems.push(`${r.featureDir}:${r.reason}`);
|
|
379
362
|
}
|
|
380
363
|
|
|
381
364
|
function describe(result: PollResult, now = new Date()): string[] {
|
package/engine/src/launch.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { describeFeatures, featuresWithRuns, findWorktreeRun, listFeatures, reso
|
|
|
20
20
|
import { checkChatCwd, normalizeFeatureDir, preflight, type PreflightResult, specProfile, verifyPhaseProblem } from "./preflight.js";
|
|
21
21
|
import { loadRuntimeFactory } from "./runtime-loader.js";
|
|
22
22
|
import type { AgentRuntime } from "./runtime.js";
|
|
23
|
+
import { remainingLine, typical, typicalLine } from "./estimate.js";
|
|
23
24
|
import { ensurePipelineDir, hashFile, isAlive, pipelineDir, readState, statePath } from "./state.js";
|
|
24
25
|
|
|
25
26
|
function fail(message: string): never {
|
|
@@ -159,6 +160,9 @@ if (args.status || args.stop) {
|
|
|
159
160
|
process.exit(0);
|
|
160
161
|
}
|
|
161
162
|
console.log(describeStatus(state, featureDir));
|
|
163
|
+
// 1.13: what to expect, from this machine's completed requirements of the same profile.
|
|
164
|
+
const estimate = state ? remainingLine(state, typical(state.profile ?? "standard")) : null;
|
|
165
|
+
if (estimate) console.log(estimate);
|
|
162
166
|
if (state && reservation) console.log(`已预约:${describeTime(new Date(reservation.at))} 开始(Paseo 定时任务,到时由对话执行;执行暂停命令可以取消)`);
|
|
163
167
|
if (args.sourceRepo) console.log(`位置:为这个需求新建的工作区 ${repo}(需求文件在 ${path.join(repo, featureDir)})`);
|
|
164
168
|
// Live detail from Paseo, best effort: the plain status above never depends on the daemon.
|
|
@@ -405,6 +409,7 @@ console.log(
|
|
|
405
409
|
? `这个需求在为它新建的工作区中开发:${checked.repo}`
|
|
406
410
|
: null,
|
|
407
411
|
`功能目录:${checked.featureDir}`,
|
|
412
|
+
checked.mode.kind === "fresh" && !checked.entersVerify ? typicalLine(typical(profile)) : null,
|
|
408
413
|
checked.branch ? `工作分支:${checked.branch}` : "不是 git 仓库:步骤不会提交代码。",
|
|
409
414
|
loaded.sources.length > 1 ? `使用了项目配置:${loaded.sources[1]}` : null,
|
|
410
415
|
`运行日志:${logPath}`,
|
package/engine/src/main.ts
CHANGED
|
@@ -308,7 +308,11 @@ async function main(): Promise<number> {
|
|
|
308
308
|
|
|
309
309
|
// Optional watchdog: the chat checks on the run now and then and continues it if the process died.
|
|
310
310
|
let watchdog: string | null = null;
|
|
311
|
-
|
|
311
|
+
// 1.13: when the req-kit plugin is checking (it continues dead runs itself, no model needed),
|
|
312
|
+
// the chat is not woken up for it.
|
|
313
|
+
const pluginWatching = (await import("./watchdog.js")).watchdogActive();
|
|
314
|
+
if (pluginWatching && config.watchdogMinutes > 0) log("req-kit 插件在检查后台进程,不再预约对话的定时检查");
|
|
315
|
+
if (config.watchdogMinutes > 0 && originAgentId && !pluginWatching) {
|
|
312
316
|
watchdog = await runtime
|
|
313
317
|
.scheduleMessage({
|
|
314
318
|
agentId: originAgentId,
|
package/engine/src/messages.ts
CHANGED
|
@@ -352,10 +352,15 @@ export function approvalMessage(input: {
|
|
|
352
352
|
`功能目录:${input.featureDir}`,
|
|
353
353
|
"情况:方案已经通过审核,还没有开始写代码。开发前请需求方确认\"做完之后会看到什么\",这时调整的成本最低。",
|
|
354
354
|
`- 做完后你会看到什么:${input.previewPath}`,
|
|
355
|
-
...(input.prototypePath
|
|
355
|
+
...(input.prototypePath
|
|
356
|
+
? [
|
|
357
|
+
`- 界面原型:${input.prototypePath}`,
|
|
358
|
+
`- 原型审查页:在 Paseo 侧边栏的"需求"中打开这个需求,点"审查原型"("等你处理"里也有),可以直接在原型上点选要改的地方、双击改文字,提交意见或确认方案;终端里是 req review ${input.featureDir.split("/").pop()}`,
|
|
359
|
+
]
|
|
360
|
+
: []),
|
|
356
361
|
"",
|
|
357
362
|
"请这样处理:",
|
|
358
|
-
"1. 读 preview.md
|
|
363
|
+
"1. 读 preview.md,用几句话把要点讲给用户:会看到的变化、主要的假设(尤其是影响界面、数据、权限的)。有原型时,请用户点 Paseo 侧边栏\"需求\"里这个需求的\"审查原型\"按钮去看:在原型上点选提意见比用文字描述准确,提交的意见会转回这个对话。",
|
|
359
364
|
"2. 问用户:按这个做,还是有要改的地方?",
|
|
360
365
|
`3. 用户确认后,执行下面的命令继续开发:\n\n${input.approveCommand}`,
|
|
361
366
|
"4. 用户要改的:按需求对齐时的方式问清楚,把结论记入 alignment.md,更新 spec.md(顶部\"版本\"加 1,在 changes.md 中记一节,原因写\"方案确认时的修改\"),请用户再次确认需求说明,然后执行下面的命令,流水线会从方案设计开始按修改后的需求调整方案,再请用户确认一次:",
|