paseo-req-kit 1.13.0-beta.5 → 1.13.0
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 +25 -0
- package/contracts/state.md +1 -1
- package/docs/guide//347/273/264/346/212/244/350/200/205/346/214/207/345/215/227.md +1 -1
- 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 +12 -2
- package/engine/prompts/00-shared-context.md +2 -0
- package/engine/src/activity.ts +23 -0
- package/engine/src/config-view.ts +5 -0
- package/engine/src/config.ts +12 -0
- package/engine/src/control.ts +8 -1
- package/engine/src/doctor.ts +18 -0
- package/engine/src/paseo-runtime.ts +9 -1
- package/engine/src/pipeline.ts +45 -1
- package/engine/src/review.ts +3 -1
- package/engine/src/runtime.ts +2 -0
- package/engine/src/scan-guard.ts +132 -0
- package/engine/src/state.ts +3 -1
- package/engine/src/stats.ts +1 -0
- package/package.json +1 -1
- package/paseo-plugin/client/review.tsx +11 -1
- package/paseo-plugin/client/web.ts +17 -0
- package/paseo-plugin/package.json +1 -1
- package/site-dist/404.html +2 -2
- package/site-dist/assets/{app.BhERz0tz.js → app.C6QQarr3.js} +1 -1
- package/site-dist/assets/changelog.md.BzAEYWi7.js +1 -0
- package/site-dist/assets/{changelog.md.Cu4SyOh_.lean.js → changelog.md.BzAEYWi7.lean.js} +1 -1
- package/site-dist/assets/chunks/@localSearchIndexroot.BjRGqGQa.js +1 -0
- package/site-dist/assets/chunks/{VPLocalSearchBox.BZONO_Xu.js → VPLocalSearchBox.NZKA1cjk.js} +1 -1
- package/site-dist/assets/chunks/{theme.Cdyvhz0n.js → theme.BP9XiatK.js} +2 -2
- package/site-dist/assets/{guide_getting-started.md.CspCz67n.js → guide_getting-started.md.CA-aS0r_.js} +1 -1
- package/site-dist/assets/{guide_getting-started.md.CspCz67n.lean.js → guide_getting-started.md.CA-aS0r_.lean.js} +1 -1
- package/site-dist/assets/guide_maintainer.md.C1v7EpSY.js +1 -0
- package/site-dist/assets/{guide_project.md.DnD2-Dsd.js → guide_project.md.qQLH2fWy.js} +3 -3
- package/site-dist/assets/{guide_project.md.DnD2-Dsd.lean.js → guide_project.md.qQLH2fWy.lean.js} +3 -3
- package/site-dist/assets/reference_contracts_state.md.EEBd_l2P.js +1 -0
- 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 +5 -5
- package/site-dist/guide/phases.html +4 -4
- package/site-dist/guide/project.html +7 -7
- package/site-dist/guide/user.html +4 -4
- 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 +4 -4
- 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 +4 -4
- package/site-dist/reference/contracts/verify.html +4 -4
- package/site-dist/reference/scope.html +4 -4
- package/site-dist/assets/changelog.md.Cu4SyOh_.js +0 -1
- package/site-dist/assets/chunks/@localSearchIndexroot.C1vUA1BF.js +0 -1
- package/site-dist/assets/guide_maintainer.md.RZIzyCTV.js +0 -1
- package/site-dist/assets/reference_contracts_state.md.TRfzD5bc.js +0 -1
- /package/site-dist/assets/{guide_maintainer.md.RZIzyCTV.lean.js → guide_maintainer.md.C1v7EpSY.lean.js} +0 -0
- /package/site-dist/assets/{reference_contracts_state.md.TRfzD5bc.lean.js → reference_contracts_state.md.EEBd_l2P.lean.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
每个版本分为"使用者会看到的变化"和"升级须知"。升级步骤见 [docs/guide/安装与升级.md](docs/guide/安装与升级.md)。
|
|
4
4
|
|
|
5
|
+
## 1.13.0
|
|
6
|
+
|
|
7
|
+
稳定版:内网能装能用、在 Paseo 网页端审查原型、不靠看图检查界面、看得见 token 花在哪。内容包括 1.13.0-beta.1 至 beta.5(详见下面各条)和下面新增的三项,设计决定见 ADR 0032、0033。
|
|
8
|
+
|
|
9
|
+
### 使用者会看到的变化
|
|
10
|
+
|
|
11
|
+
- **内网安装和文档**:`npx -y [--registry=<镜像地址>] paseo-req-kit@latest install` 一条命令装好,只需要 npm 源;`req docs` 在本机打开文档站点。
|
|
12
|
+
- **在 Paseo 里审查原型**:需求页面和"等你处理"里的主按钮"审查原型并确认方案",在原型上点要改的地方、选改法、用自己的话补一句,提交后整理成清单交给需求会话。
|
|
13
|
+
- **界面检查 `req ui-check`**:用本机的 Edge 或 Chrome,用文字报告显示不全、遮挡、超出屏幕、文字太淡、没名字的控件、报错,并和确认过的原型对文字;功能验证自动使用,不需要模型看图。
|
|
14
|
+
- **token**:`req stats` 和插件的"token 花在哪";可选的每个需求预算(`budget`);开始时和 `req status` 里的"预计还要多久"。
|
|
15
|
+
- **后台进程意外退出时由插件直接继续**,不再每小时叫醒对话;等待批准的权限进"等你处理"。
|
|
16
|
+
- **新增:打断大目录搜索**(`scanGuard`,默认开):步骤 agent 搜索整个磁盘、用户目录、Maven 本地仓库(`~/.m2`)、Gradle 缓存、全局或整个 `node_modules` 时,流水线一分钟后打断这个命令,并告诉 agent 改用什么办法(`git grep`、按坐标直接打开那一个依赖、`mvn dependency:tree`);任何搜索运行超过 5 分钟也打断。构建和测试命令不受影响。各步骤的提示词也写明了只在仓库里搜索。
|
|
17
|
+
- **新增:预算不起作用时会说**:agent 不报告 token 用量时,设了预算的需求在第一步之后提醒一次"预算不会起作用",`req doctor` 也会检查。
|
|
18
|
+
- **新增:从别的电脑打开审查页时说明原因**:审查页默认只在运行 Paseo 的电脑上能打开;从别的电脑打开 Paseo 网页、点"审查原型"时,页面写明为什么看不到、怎么设置。只设 `REQ_KIT_REVIEW_HOST` 时,审查页地址用这台电脑的计算机名。
|
|
19
|
+
|
|
20
|
+
### 升级须知
|
|
21
|
+
|
|
22
|
+
- 新配置都可选:`budget`(默认不限)、`scanGuard`(默认开:`{ "enabled": true, "minutes": 5, "paths": [] }`,关闭写 `"enabled": false`)。
|
|
23
|
+
- 打开 `scanGuard` 时,流水线每分钟查看一次步骤 agent 的状态(以前每 10 分钟),只是读取状态,不占模型 token。
|
|
24
|
+
- 原型按新约定写(`data-ac`、`data-label`、`<section data-state>`、不写脚本),旧原型照常能看,只是审查页认不出区域和状态。
|
|
25
|
+
- 1.12 时预约的"对话定时检查"会留到过期(最多 3 天),装了插件后新需求不再预约,不用手动删。
|
|
26
|
+
- 装过插件的,`req upgrade` 会一并更新插件。新的安装脚本只能安装 1.13 及以后的版本。
|
|
27
|
+
- 状态文件、使用记录和 `req api` 的新增字段见各 beta 的升级须知;`recoveries` 新增 `stop_scan`。
|
|
28
|
+
- 发布前 Linux 和 Windows 的测试都必须通过。
|
|
29
|
+
|
|
5
30
|
## 1.13.0-beta.5
|
|
6
31
|
|
|
7
32
|
### 使用者会看到的变化
|
package/contracts/state.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
| `stopKind` | string \| null | 可选,1.12 起。停下等人的原因代码:必要的决定 `approval`(确认方案)、`phase_gate`(研发完成,等待测试)、`business`(需求方决定)、`reproduce`(缺陷复现不了);中断 `timeout`、`agent_error`、`invalid_result`、`transient`、`stopped`、`process_exit`、`budget`(1.13,用完 token 预算)、`other`。运行中或完成时为 null |
|
|
22
22
|
| `budget` | object | 可选,1.13 起,配置了 `budget.tokens` 时。`{ allowance, warnedAt }`:停下前允许用到的 tokens(预算,每次用完后继续再加一份);已经提醒过的那一档 |
|
|
23
23
|
| `waitingPermission` | object \| null | 可选,1.13 起。步骤 agent 在等权限批准时:`{ agentId, stepTitle, requests, since }`;没有在等时为 null |
|
|
24
|
-
| `recoveries` | array | 可选,1.12 起。流水线自己处理掉的问题(最多保留 200 条):`at`、`key`(步骤键)、`kind`(`retry` 重试 / `reattach` 接着等上一次的 agent / `reuse_result` 沿用上一次 agent 的结果 / `extend_timeout` 超时后多等一轮 / `redo` 新开 agent 重做 / `fallback` 换备用模型 / `auto_resume` 预约自动继续 / `quota_wait` 预约在额度恢复后继续 / `self_answer` 方案审核的问题由 builder
|
|
24
|
+
| `recoveries` | array | 可选,1.12 起。流水线自己处理掉的问题(最多保留 200 条):`at`、`key`(步骤键)、`kind`(`retry` 重试 / `reattach` 接着等上一次的 agent / `reuse_result` 沿用上一次 agent 的结果 / `extend_timeout` 超时后多等一轮 / `redo` 新开 agent 重做 / `fallback` 换备用模型 / `auto_resume` 预约自动继续 / `quota_wait` 预约在额度恢复后继续 / `self_answer` 方案审核的问题由 builder 自答 / `stop_scan` 打断了搜索大目录的命令,1.13 起)、`detail` |
|
|
25
25
|
| `profile` | `quick` \| `standard` | 档位 |
|
|
26
26
|
| `branch` | string \| null | 工作分支,启动时确定 |
|
|
27
27
|
| `milestones` | string[] | 已发送的进展通知,续跑时不重复发送 |
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
3. **写 `CHANGELOG.md`**:新增一节,分"使用者会看到的变化"和"升级须知"两部分。升级须知写清:要不要重跑 `/req-setup`、提示词有哪些变化(影响用 `prompts.local/` 覆盖过的人)、文件格式有没有变化、旧行为的兼容方式。
|
|
40
40
|
4. **验证**:`npm test`、`npm run typecheck`、`npm run eval:self-check`;步骤提示词或引擎有改动时跑 `npm run eval`,把 `summary.md` 复制到 `evals/baselines/<版本>.md`;入口或手册有改动时跑对话评估,记入记分表。主版本发布前,在一个真实项目里试运行一周。
|
|
41
41
|
5. **冒烟测试**:在一台干净的机器(或临时的 `REQ_KIT_HOME` 和 HOME)上按 [安装与升级](安装与升级.md) 安装,跑冒烟测试第 1–4 项;至少有一台是 Windows。
|
|
42
|
-
6. **合并到 `main`,标签自动打上**:`main` 收到 `package.json` 版本号还没有标签的提交时,`.github/workflows/release.yml`
|
|
42
|
+
6. **合并到 `main`,标签自动打上**:`main` 收到 `package.json` 版本号还没有标签的提交时,`.github/workflows/release.yml` 先在这个提交上跑测试(Linux 和 Windows 各一遍,1.13 起)、类型检查和评估自检,通过后才打上同名标签(不用自己打,也不需要推送标签的权限),接着在同一次运行里发布。测试没通过时不会留下标签,把修复合到 `main` 就会用同一个版本号重新发布(1.12.1 起;1.13 起任何推到 `main` 的提交都会检查,不必再改 `package.json`)。也可以照旧自己打:在 GitHub 上建 Release 并新建标签 `<版本>`,或推送 `git tag -a <版本>`,同样会触发发布。发布流程:跑测试、类型检查和评估自检,核对标签与 `package.json` 版本一致,然后发布到 npm(预发布版用 `beta` 标签,正式版用 `latest`),正式版再设置 `paseo-<小版本>` 标签,把 `npm pack` 的安装包附在 Release 上,Release 正文取自 CHANGELOG 的这一节。标签同时也是 git 方式安装的版本列表,所以改版本号的提交要在验证完成后再合到 `main`(自动打标签的时机就是它);打错了尽快删除标签,npm 上发布错的版本用 `npm deprecate` 标注(npm 不允许重新发布同一版本号)。
|
|
43
43
|
7. **通知**:把"使用者会看到的变化"发给使用者,把"升级须知"发给各台机器的安装者;团队配置的 `requiredVersion` 需要提高时,在团队配置仓库中修改。
|
|
44
44
|
|
|
45
45
|
**npm 发布的一次性设置**:可信发布(trusted publisher)要在 npm 上已有的包的设置页里配置,所以第一个版本在本机手工发布一次,不需要生成任何口令:
|
|
@@ -47,7 +47,7 @@ spec 头部的 `- 类型:` 决定流水线走哪种做法(1.8 起)。缺
|
|
|
47
47
|
|
|
48
48
|
为了让审查页能用,方案设计按约定写原型:每个改动区域标 `data-ac`(对应的 AC)和 `data-label`(用需求方的话说是什么),各种状态包在 `<section data-state="状态名">` 中,按钮、标题、字段名写最终的文字。原型里的脚本不会运行。
|
|
49
49
|
|
|
50
|
-
审查页由一个本机服务提供:用到时在后台启动,空闲 4 小时后自己退出,只监听 `127.0.0.1`,地址中带随机令牌(插件设为只读时给只读的地址)。审查页允许被本机地址上的页面(Paseo 网页端)嵌入;要限定或放开,设 `REQ_KIT_REVIEW_FRAME_ANCESTORS`(CSP `frame-ancestors` 的值)。需求方在别的电脑上用浏览器访问 Paseo 网页端时,在运行 Paseo daemon 的机器上设置环境变量 `REQ_KIT_REVIEW_HOST`(例如 `0.0.0.0`)、`REQ_KIT_REVIEW_PUBLIC_URL`(别人访问这台机器用的地址,例如 `http://10.1.2.3:6781`),需要时用 `REQ_KIT_REVIEW_PORT` 换端口(默认 6781),然后重启 Paseo daemon
|
|
50
|
+
审查页由一个本机服务提供:用到时在后台启动,空闲 4 小时后自己退出,只监听 `127.0.0.1`,地址中带随机令牌(插件设为只读时给只读的地址)。审查页允许被本机地址上的页面(Paseo 网页端)嵌入;要限定或放开,设 `REQ_KIT_REVIEW_FRAME_ANCESTORS`(CSP `frame-ancestors` 的值)。需求方在别的电脑上用浏览器访问 Paseo 网页端时,在运行 Paseo daemon 的机器上设置环境变量 `REQ_KIT_REVIEW_HOST`(例如 `0.0.0.0`)、`REQ_KIT_REVIEW_PUBLIC_URL`(别人访问这台机器用的地址,例如 `http://10.1.2.3:6781`),需要时用 `REQ_KIT_REVIEW_PORT` 换端口(默认 6781),然后重启 Paseo daemon 让插件拿到新的环境变量(只设 `REQ_KIT_REVIEW_HOST` 时,地址用这台电脑的计算机名)。没有设置时从别的电脑打开 Paseo 网页、点"审查原型",页面会说明为什么打不开、该怎么设置(1.13)。
|
|
51
51
|
|
|
52
52
|
## 代码评审分两个方向
|
|
53
53
|
|
|
@@ -267,6 +267,8 @@ spec 头部的 `- 类型:` 决定流水线走哪种做法(1.8 起)。缺
|
|
|
267
267
|
|
|
268
268
|
写在本机、团队或项目配置中都可以,例如 `"budget": { "tokens": 3000000 }`。还没有数据时,先不设预算跑几个需求,用 `req stats` 的"token 花在哪"看每个需求的中位数,再按它的两三倍设。
|
|
269
269
|
|
|
270
|
+
agent 不报告 token 用量时(有些模型或接入方式不报告),预算不会起作用:设了预算的需求在第一个步骤结束后会在对话里说明一次,流水线照常继续;`req doctor` 也会检查本机最近的需求有没有用量数据。
|
|
271
|
+
|
|
270
272
|
用时估算不用配置:本机已完成的同档位需求有 3 个以上时,开始开发的回复里写出"agent 合计用时和 token 的中位数"作为参考;`req status` 和插件的需求页面显示"预计还要约多久"(按中位数减去已用的 agent 时间,不含等人的时间),超过中位数时如实说"已超过通常的用时"。
|
|
271
273
|
|
|
272
274
|
## 并行编排:确认前审阅、方案提案、浏览器验证
|
|
@@ -526,7 +528,7 @@ Paseo 自带的 `/paseo-advisor`、`/paseo-committee` 等编排 skill 也可以
|
|
|
526
528
|
|
|
527
529
|
**结果文件**:每步必须写一个 JSON 结果文件,流水线只看它,不看 agent 的回复文字。文件缺失或格式不对时,会在同一个会话里提醒 agent 补写,最多 2 次,仍不对则判定该步骤失败。
|
|
528
530
|
|
|
529
|
-
**等待与超时**:每步有总时限(`stepTimeoutMinutes`)。到时 agent 还在做时,先再给它同样长的时间(`selfHeal`,见下);仍未完成才判定失败,但不停止 agent。继续(`--resume`、自动重试)时先看这一步上一次的 agent:已经做完、结果文件有效就直接用它的结果;还在做就接着等它一轮(这一轮不再额外延长);第二次仍超时才停止它、另起一个 agent。需求改过、开始了新一次尝试时,上一次留下的 agent 直接停掉,不接手(1.12.1)。同一步不会有两个 agent 同时在做(它们会改同样的文件、在同一个工作区提交、写同一个结果文件)。连续读不到 agent 状态时同样处理。等待被拆成每段 10
|
|
531
|
+
**等待与超时**:每步有总时限(`stepTimeoutMinutes`)。到时 agent 还在做时,先再给它同样长的时间(`selfHeal`,见下);仍未完成才判定失败,但不停止 agent。继续(`--resume`、自动重试)时先看这一步上一次的 agent:已经做完、结果文件有效就直接用它的结果;还在做就接着等它一轮(这一轮不再额外延长);第二次仍超时才停止它、另起一个 agent。需求改过、开始了新一次尝试时,上一次留下的 agent 直接停掉,不接手(1.12.1)。同一步不会有两个 agent 同时在做(它们会改同样的文件、在同一个工作区提交、写同一个结果文件)。连续读不到 agent 状态时同样处理。等待被拆成每段 10 分钟(打开 `scanGuard` 时每段 1 分钟,以便查看正在运行的命令),连接断开时自动重试。agent 等待权限批准时,同一个请求只通知一次,然后继续等待。
|
|
530
532
|
|
|
531
533
|
**临时错误自动重试**:步骤因限流(429)、服务暂时不可用(502/503/504/529、overloaded)、网络中断等临时错误失败时,等待后用新的 agent 重做这一步,并告诉它工作区里可能留有上一次没做完的改动。默认最多重试 2 次,分别等 1 分钟和 5 分钟:
|
|
532
534
|
|
|
@@ -554,6 +556,14 @@ Paseo 自带的 `/paseo-advisor`、`/paseo-committee` 等编排 skill 也可以
|
|
|
554
556
|
|
|
555
557
|
自动处理的每一次都记在进度日志、状态文件的 `recoveries` 和使用记录中,`req stats` 的"人工介入"一节汇总:停下等人的原因、自动处理了多少次、到达提测的需求中没有非必要介入的比例。
|
|
556
558
|
|
|
559
|
+
**打断大目录搜索**(1.13,`scanGuard`,默认开):步骤 agent 有时为了找一个类或一个包,去搜索整个磁盘、用户目录、Maven 本地仓库(`~/.m2`)、Gradle 缓存、全局 `node_modules` 或整个 `node_modules`,一次就是几十分钟,这一步迟迟做不完。步骤运行时,流水线大约每分钟看一次 agent 正在运行的命令:搜索这类大目录的命令运行超过 1 分钟、或者任何搜索命令运行超过 `minutes` 分钟,就打断它,并告诉 agent 改用什么办法(在仓库里用 `git grep`;按 `pom.xml` 里的坐标直接打开那一个依赖目录、用 `unzip -l` 看那一个 jar;`mvn dependency:tree`;找不到就写进"假设"继续)。构建和测试命令(`mvn`、`gradle`、`npm test` 等)不打断;只搜具体的子目录(如 `~/.m2/repository/com/acme/foo`)或排除了 `node_modules` 的搜索也不算。同一个命令只打断一次,每轮最多打断 5 次;每次都记在进度日志和 `recoveries`(`stop_scan`)中。各步骤的提示词里也写明了只在仓库里搜索。
|
|
560
|
+
|
|
561
|
+
```json
|
|
562
|
+
"scanGuard": { "enabled": true, "minutes": 5, "paths": ["D:/share"] }
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
`paths`:另外不允许整个搜索的目录(例如共享盘);`enabled: false` 关闭。
|
|
566
|
+
|
|
557
567
|
**同一个目录一次只运行一条流水线**,因为各步骤都在这个目录中修改和提交。需要并行时,为每个需求使用单独的 worktree 工作区。
|
|
558
568
|
|
|
559
569
|
## 注意事项
|
|
@@ -65,6 +65,8 @@ time step ref decision alternatives reason kind needs_human
|
|
|
65
65
|
|
|
66
66
|
**遵循项目已有的做法。** 开始前阅读 `CLAUDE.md`、`AGENTS.md`、`README`、`CONTRIBUTING`(存在哪个读哪个),以及 `GLOSSARY.md`(业务与领域术语;较早的项目可能叫 `CONTEXT.md`)和 `docs/adr/`(已有决定)。命名、目录结构、测试写法、提交信息格式都跟随项目现状。
|
|
67
67
|
|
|
68
|
+
**只在仓库里搜索,不要扫大目录。** 不要搜索整个磁盘、用户目录、依赖缓存(Maven 的 `~/.m2`、Gradle 的 `~/.gradle`、npm 缓存、全局 `node_modules`)或整个 `node_modules`,这类搜索要几十分钟,流水线会在一分钟后打断它。在仓库里找用 `git grep -n` 或 `git ls-files`。要看某个依赖:先从 `pom.xml`、`build.gradle`、`package.json` 确定它的坐标和版本,再直接打开那一个目录(如 `~/.m2/repository/<groupId 的点换成 />/<artifactId>/<版本>/`、`node_modules/<包名>/`),用 `unzip -l`、`jar tf` 看那一个 jar。不知道类在哪个依赖里时,`mvn dependency:tree` 比搜索快。
|
|
69
|
+
|
|
68
70
|
**只做这个需求。** 不顺手重构无关代码,不升级无关依赖,不改无关的格式。看到无关的问题可以在产物里提一句,留给人来决定。
|
|
69
71
|
|
|
70
72
|
**多轮的步骤。** `<pipeline-context>` 写明"第 2 轮"或之后时,说明这一步之前已经执行过:
|
package/engine/src/activity.ts
CHANGED
|
@@ -85,3 +85,26 @@ export function describeActivity(items: ActivityItem[], now = new Date()): strin
|
|
|
85
85
|
return ` ${when ? `${when}:` : ""}${a.text}`;
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 1.13: the tool call still running at the end of the timeline (for the scan guard): its
|
|
91
|
+
* command or search pattern and when it started (the first entry seen for that call).
|
|
92
|
+
*/
|
|
93
|
+
export function runningToolOf(entries: TimelineEntry[]): import("./scan-guard.js").RunningTool | null {
|
|
94
|
+
const calls = new Map<string, { first: string; last: Record<string, unknown> }>();
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
const it = entry.item as Record<string, unknown> | null;
|
|
97
|
+
if (!it || typeof it !== "object" || it.type !== "tool_call" || typeof it.callId !== "string") continue;
|
|
98
|
+
const seen = calls.get(it.callId);
|
|
99
|
+
if (seen) seen.last = it;
|
|
100
|
+
else calls.set(it.callId, { first: entry.timestamp, last: it });
|
|
101
|
+
}
|
|
102
|
+
const running = [...calls.entries()].filter(([, c]) => c.last.status === "running");
|
|
103
|
+
if (running.length === 0) return null;
|
|
104
|
+
const [callId, call] = running[running.length - 1];
|
|
105
|
+
const detail = (call.last.detail ?? {}) as Record<string, unknown>;
|
|
106
|
+
const kind = String(detail.type ?? "unknown");
|
|
107
|
+
const raw = kind === "shell" ? detail.command : kind === "search" ? detail.query : (detail.command ?? "");
|
|
108
|
+
const text = Array.isArray(raw) ? raw.join(" ") : String(raw ?? "");
|
|
109
|
+
return { kind, text, startedAt: call.first, callId };
|
|
110
|
+
}
|
|
@@ -122,6 +122,11 @@ const CATALOG: Array<{ id: string; title: string; description: string; keys: Rec
|
|
|
122
122
|
description: "一个需求所有步骤合计(输入 + 输出)最多用多少 tokens;用到一定比例时提醒,用完后做完当前步骤就停下,继续开发会再给一份预算。",
|
|
123
123
|
display: (v) => { const b = v as PipelineConfig["budget"]; return b.tokens === null ? "不限" : `${b.tokens >= 10_000 ? `${Math.round(b.tokens / 1000) / 10} 万` : b.tokens},用到 ${b.warnPercent}% 时提醒`; },
|
|
124
124
|
},
|
|
125
|
+
scanGuard: {
|
|
126
|
+
label: "打断大目录搜索",
|
|
127
|
+
description: "步骤 agent 搜索整个磁盘、用户目录、Maven 仓库(.m2)、全局 node_modules 等很大的目录时,一分钟后打断它并告诉它该怎么查;任何搜索运行太久也打断。",
|
|
128
|
+
display: (v) => { const g = v as PipelineConfig["scanGuard"]; return g.enabled ? `开,搜索超过 ${g.minutes} 分钟也打断${g.paths.length ? `;另外不许整个搜索:${g.paths.join("、")}` : ""}` : "关"; },
|
|
129
|
+
},
|
|
125
130
|
},
|
|
126
131
|
},
|
|
127
132
|
{
|
package/engine/src/config.ts
CHANGED
|
@@ -245,6 +245,12 @@ export interface PipelineConfig {
|
|
|
245
245
|
* pipeline stops to ask; null: no limit. A notice at warnPercent; continuing gives one more budget.
|
|
246
246
|
*/
|
|
247
247
|
budget: { tokens: number | null; warnPercent: number };
|
|
248
|
+
/**
|
|
249
|
+
* 1.13: interrupt a step agent's search of a very large directory (the whole disk, the home
|
|
250
|
+
* directory, ~/.m2, the global node_modules, …) after a minute, and any search after `minutes`,
|
|
251
|
+
* and tell it how to find what it needs instead. `paths`: more directories never to search whole.
|
|
252
|
+
*/
|
|
253
|
+
scanGuard: import("./scan-guard.js").ScanGuardSettings;
|
|
248
254
|
/** Automatic retries of a step after a transient error (rate limit, dropped connection). */
|
|
249
255
|
retry: { attempts: number; backoffSeconds: number[] };
|
|
250
256
|
/**
|
|
@@ -355,6 +361,7 @@ export const DEFAULT_CONFIG: PipelineConfig = {
|
|
|
355
361
|
factory: { pollMinutes: 5, titleTags: ["[AI]", "【AI】"], maxNewPerPoll: 3 },
|
|
356
362
|
autoConfirm: { hours: 0 },
|
|
357
363
|
budget: { tokens: null, warnPercent: 80 },
|
|
364
|
+
scanGuard: { enabled: true, minutes: 5, paths: [] },
|
|
358
365
|
retry: { attempts: 2, backoffSeconds: [60, 300] },
|
|
359
366
|
paseoServices: "auto",
|
|
360
367
|
serviceReadySeconds: 120,
|
|
@@ -424,6 +431,7 @@ function merge(base: PipelineConfig, raw: Record<string, unknown>): PipelineConf
|
|
|
424
431
|
merged.factory = { ...base.factory, ...(isRecord(raw.factory) ? raw.factory : {}) } as PipelineConfig["factory"];
|
|
425
432
|
merged.autoConfirm = { ...base.autoConfirm, ...(isRecord(raw.autoConfirm) ? raw.autoConfirm : {}) } as PipelineConfig["autoConfirm"];
|
|
426
433
|
merged.budget = { ...base.budget, ...(isRecord(raw.budget) ? raw.budget : {}) } as PipelineConfig["budget"];
|
|
434
|
+
merged.scanGuard = { ...base.scanGuard, ...(isRecord(raw.scanGuard) ? raw.scanGuard : {}) } as PipelineConfig["scanGuard"];
|
|
427
435
|
if (isRecord(raw.cardSource)) {
|
|
428
436
|
const c = raw.cardSource;
|
|
429
437
|
merged.cardSource = {
|
|
@@ -645,6 +653,10 @@ function validate(config: PipelineConfig): void {
|
|
|
645
653
|
if (typeof config.budget.warnPercent !== "number" || config.budget.warnPercent <= 0 || config.budget.warnPercent >= 100) {
|
|
646
654
|
throw new ConfigError("budget.warnPercent 必须在 1 到 99 之间(用到预算的百分之几时提醒)");
|
|
647
655
|
}
|
|
656
|
+
const sg = config.scanGuard;
|
|
657
|
+
if (typeof sg.enabled !== "boolean") throw new ConfigError("scanGuard.enabled 必须是 true 或 false");
|
|
658
|
+
if (typeof sg.minutes !== "number" || !(sg.minutes >= 1)) throw new ConfigError("scanGuard.minutes 必须是至少 1 的分钟数(搜索运行多久后打断)");
|
|
659
|
+
if (!Array.isArray(sg.paths) || !sg.paths.every((p) => typeof p === "string" && p.trim())) throw new ConfigError('scanGuard.paths 必须是目录路径的数组,例如 ["D:/share"]');
|
|
648
660
|
const f = config.factory;
|
|
649
661
|
if (typeof f.pollMinutes !== "number" || !(f.pollMinutes >= 1)) throw new ConfigError("factory.pollMinutes 必须是至少 1 的分钟数");
|
|
650
662
|
if (!Number.isInteger(f.maxNewPerPoll) || f.maxNewPerPoll < 1) throw new ConfigError("factory.maxNewPerPoll 必须是正整数");
|
package/engine/src/control.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { describeActivity } from "./activity.js";
|
|
|
5
5
|
import { describeExternal } from "./integrations.js";
|
|
6
6
|
import { PHASE_MODE_LABELS, PROFILE_LABELS, type PipelineConfig, REQUIREMENT_TYPE_LABELS } from "./config.js";
|
|
7
7
|
import type { AgentRuntime, RuntimeFactory } from "./runtime.js";
|
|
8
|
-
import { isAlive, type PipelineState, readState, type StepRecord, writeState } from "./state.js";
|
|
8
|
+
import { currentLockHolder, isAlive, type PipelineState, readState, type StepRecord, writeState } from "./state.js";
|
|
9
9
|
import { stepTitle } from "./playbooks.js";
|
|
10
10
|
import { describeTotals, totals } from "./usage.js";
|
|
11
11
|
|
|
@@ -195,6 +195,13 @@ export async function stopPipeline(input: StopInput): Promise<{ stopped: boolean
|
|
|
195
195
|
latest.stopReason = wasAlive ? "已被手动停止" : "后台进程已意外退出";
|
|
196
196
|
latest.stopKind = wasAlive ? "stopped" : "process_exit";
|
|
197
197
|
writeState(input.statePath, latest);
|
|
198
|
+
// On Windows a stopped process gets no chance to remove the repository lock itself; this
|
|
199
|
+
// removes a lock whose holder is gone (anywhere, harmless when it was already removed).
|
|
200
|
+
try {
|
|
201
|
+
currentLockHolder(latest.repo);
|
|
202
|
+
} catch {
|
|
203
|
+
// The next start checks the lock the same way.
|
|
204
|
+
}
|
|
198
205
|
|
|
199
206
|
// The step agent keeps working unless told otherwise; stop it so it changes nothing more.
|
|
200
207
|
if (interrupted.some((r) => r.agentId)) {
|
package/engine/src/doctor.ts
CHANGED
|
@@ -18,6 +18,8 @@ import { defaultConfigPath, findDependency, KIT_ROOT, kitVersion } from "./locat
|
|
|
18
18
|
import { PROJECT_DIR } from "./prompts.js";
|
|
19
19
|
import { describeAgentProfile, ProfileResolutionError, profilesFromDaemonConfig, resolveConfigProfiles, usesPaseoProfiles } from "./paseo-profiles.js";
|
|
20
20
|
import { isAlive, STATE_VERSION } from "./state.js";
|
|
21
|
+
import { readRuns, runsFile } from "./records.js";
|
|
22
|
+
import { byRequirement, dedupe } from "./stats.js";
|
|
21
23
|
import { resolveCommand } from "./integrations.js";
|
|
22
24
|
|
|
23
25
|
const counts = { fail: 0, warn: 0 };
|
|
@@ -160,6 +162,7 @@ function configuration(configPath: string, repo: string | null): PipelineConfig
|
|
|
160
162
|
if (satisfies(kitVersion(), required)) report("ok", `版本要求 ${required}:当前 ${kitVersion()} 满足`);
|
|
161
163
|
else report("warn", `团队或项目要求 req-kit ${required},当前为 ${kitVersion()}`, "按 docs/guide/安装与升级.md 升级到符合要求的版本");
|
|
162
164
|
}
|
|
165
|
+
budgetUsage(loaded.config);
|
|
163
166
|
return loaded.config;
|
|
164
167
|
} catch (error) {
|
|
165
168
|
if (!(error instanceof ConfigError)) throw error;
|
|
@@ -168,6 +171,21 @@ function configuration(configPath: string, repo: string | null): PipelineConfig
|
|
|
168
171
|
}
|
|
169
172
|
}
|
|
170
173
|
|
|
174
|
+
/** 1.13: a token budget only works when the agents report usage; this machine's recent requirements say whether they do. */
|
|
175
|
+
function budgetUsage(config: PipelineConfig): void {
|
|
176
|
+
if (config.budget.tokens === null) return;
|
|
177
|
+
let recent: ReturnType<typeof byRequirement> = [];
|
|
178
|
+
try {
|
|
179
|
+
recent = byRequirement(dedupe(readRuns(runsFile()))).slice(-3);
|
|
180
|
+
} catch {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (recent.length === 0) return;
|
|
184
|
+
if (recent.every((q) => q.inputTokens === null && q.outputTokens === null)) {
|
|
185
|
+
report("warn", `设了 token 预算(每个需求 ${config.budget.tokens}),但本机最近 ${recent.length} 个需求的 agent 都没有报告 token 用量,预算不会起作用`, "换用会报告用量的 agent;不需要预算时把 budget.tokens 设为 null");
|
|
186
|
+
} else report("ok", "token 预算:agent 会报告用量");
|
|
187
|
+
}
|
|
188
|
+
|
|
171
189
|
/** Daemon address from the raw file, so the daemon can be checked even when the agents are not filled in yet. */
|
|
172
190
|
function daemonSettings(configPath: string): PipelineConfig {
|
|
173
191
|
try {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PaseoAgentHandle, PaseoClient } from "@getpaseo/client";
|
|
2
|
-
import { summarizeActivity } from "./activity.js";
|
|
2
|
+
import { runningToolOf, summarizeActivity } from "./activity.js";
|
|
3
3
|
import { type AgentProfile, isResolved, type PipelineConfig } from "./config.js";
|
|
4
4
|
import { profilesFromDaemonConfig } from "./paseo-profiles.js";
|
|
5
5
|
import { cronAt, localTimeZone } from "./schedule-time.js";
|
|
@@ -288,6 +288,14 @@ export function createRuntimeWithClient(
|
|
|
288
288
|
return null;
|
|
289
289
|
}
|
|
290
290
|
},
|
|
291
|
+
async runningTool(agentId) {
|
|
292
|
+
try {
|
|
293
|
+
const page = await client.agents.ref(agentId).timeline.refetch({ direction: "tail", limit: 40 });
|
|
294
|
+
return runningToolOf(page.entries);
|
|
295
|
+
} catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
},
|
|
291
299
|
async createWorktree(request) {
|
|
292
300
|
if (!client.workspaces) throw new Error("这个 Paseo 版本不支持新建工作区");
|
|
293
301
|
const workspace = await client.workspaces.create({
|
package/engine/src/pipeline.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { judge as judgeScan } from "./scan-guard.js";
|
|
1
2
|
import { execFileSync } from "node:child_process";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
@@ -200,6 +201,13 @@ async function checkBudget(ctx: PipelineContext, stepTitle: string): Promise<voi
|
|
|
200
201
|
const budget = (ctx.state.budget ??= { allowance: limit, warnedAt: null });
|
|
201
202
|
const used = tokensUsed(ctx.state);
|
|
202
203
|
if (used >= budget.allowance) throw new BudgetReached(used, budget.allowance);
|
|
204
|
+
// 1.13: agents that report no usage make the budget do nothing; say so once instead of keeping quiet.
|
|
205
|
+
const finished = ctx.state.history.filter((h) => h.endedAt);
|
|
206
|
+
if (used === 0 && finished.length > 0 && !budget.noUsageNoticed && finished.every((h) => !h.usage || (h.usage.inputTokens === undefined && h.usage.outputTokens === undefined))) {
|
|
207
|
+
budget.noUsageNoticed = true;
|
|
208
|
+
save(ctx);
|
|
209
|
+
await notifyOrigin(ctx, noUsageMessage(shownFeatureDir(ctx), limit));
|
|
210
|
+
}
|
|
203
211
|
if (used >= (budget.allowance * ctx.config.budget.warnPercent) / 100 && budget.warnedAt !== budget.allowance) {
|
|
204
212
|
budget.warnedAt = budget.allowance;
|
|
205
213
|
save(ctx);
|
|
@@ -209,6 +217,10 @@ async function checkBudget(ctx: PipelineContext, stepTitle: string): Promise<voi
|
|
|
209
217
|
}
|
|
210
218
|
}
|
|
211
219
|
|
|
220
|
+
export function noUsageMessage(featureDir: string, limit: number): string {
|
|
221
|
+
return `【需求开发流水线通知】${featureDir} 设了每个需求 ${tokenText(limit)} tokens 的预算,但已完成的步骤里 agent 都没有报告 token 用量,所以预算不会起作用,token 统计和预计也没有数据。流水线照常继续。要让预算生效,请换用会报告用量的 agent(Paseo 里能看到用量的那种);不需要预算时可以在配置里把 budget.tokens 设为 null。这条消息只是告知,不需要处理。`;
|
|
222
|
+
}
|
|
223
|
+
|
|
212
224
|
class BusinessDecisionNeeded extends Error {
|
|
213
225
|
constructor(
|
|
214
226
|
public readonly questions: string[],
|
|
@@ -621,8 +633,37 @@ interface Deadline {
|
|
|
621
633
|
record?: StepRecord;
|
|
622
634
|
}
|
|
623
635
|
|
|
636
|
+
/** How often the scan guard looks at the running tool call. */
|
|
637
|
+
const SCAN_CHECK_SECONDS = 60;
|
|
638
|
+
/** At most this many interruptions per turn; after that the step's time limit takes over. */
|
|
639
|
+
const SCAN_MAX_INTERRUPTS = 5;
|
|
640
|
+
|
|
641
|
+
interface ScanGuardMemory {
|
|
642
|
+
interrupted: Set<string>;
|
|
643
|
+
count: number;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** 1.13: interrupts a search of a very large directory (or a search that runs too long), once per tool call. */
|
|
647
|
+
async function guardScan(ctx: PipelineContext, agent: StepAgent, def: StepDef, key: string, memory: ScanGuardMemory): Promise<void> {
|
|
648
|
+
if (!ctx.config.scanGuard.enabled || memory.count >= SCAN_MAX_INTERRUPTS) return;
|
|
649
|
+
const tool = await ctx.runtime.runningTool(agent.id).catch(() => null);
|
|
650
|
+
if (!tool || memory.interrupted.has(tool.callId)) return;
|
|
651
|
+
const verdict = judgeScan(tool, ctx.config.scanGuard);
|
|
652
|
+
if (!verdict) return;
|
|
653
|
+
memory.interrupted.add(tool.callId);
|
|
654
|
+
memory.count += 1;
|
|
655
|
+
try {
|
|
656
|
+
await ctx.runtime.interruptAgent(agent.id, verdict.message);
|
|
657
|
+
} catch (error) {
|
|
658
|
+
ctx.log(`打断「${def.title}」的搜索失败:${(error as Error).message}`);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
recover(ctx, key, "stop_scan", `「${def.title}」的命令${verdict.reason},已打断并告诉 agent 改用别的办法:${tool.text.replace(/\s+/g, " ").slice(0, 120)}`);
|
|
662
|
+
}
|
|
663
|
+
|
|
624
664
|
async function waitForTurn(ctx: PipelineContext, agent: StepAgent, def: StepDef, agentTitle: string, deadline: Deadline): Promise<AgentUsage | null> {
|
|
625
665
|
const notifiedPermissions = new Set<string>();
|
|
666
|
+
const guard: ScanGuardMemory = { interrupted: new Set(), count: 0 };
|
|
626
667
|
let consecutiveErrors = 0;
|
|
627
668
|
for (;;) {
|
|
628
669
|
let remaining = deadline.at - Date.now();
|
|
@@ -644,7 +685,9 @@ async function waitForTurn(ctx: PipelineContext, agent: StepAgent, def: StepDef,
|
|
|
644
685
|
}
|
|
645
686
|
let outcome;
|
|
646
687
|
try {
|
|
647
|
-
|
|
688
|
+
// 1.13: with the scan guard on, look at the running tool call about once a minute.
|
|
689
|
+
const chunk = ctx.config.scanGuard.enabled ? Math.min(ctx.config.waitChunkSeconds, SCAN_CHECK_SECONDS) : ctx.config.waitChunkSeconds;
|
|
690
|
+
outcome = await agent.wait(Math.min(chunk * 1000, remaining));
|
|
648
691
|
consecutiveErrors = 0;
|
|
649
692
|
} catch (error) {
|
|
650
693
|
consecutiveErrors += 1;
|
|
@@ -665,6 +708,7 @@ async function waitForTurn(ctx: PipelineContext, agent: StepAgent, def: StepDef,
|
|
|
665
708
|
case "idle":
|
|
666
709
|
return outcome.usage ?? null;
|
|
667
710
|
case "timeout":
|
|
711
|
+
await guardScan(ctx, agent, def, deadline.key, guard);
|
|
668
712
|
continue;
|
|
669
713
|
case "error":
|
|
670
714
|
throw new AgentErrored(def.title, `agent 报告错误:${outcome.error ?? "未知错误"}`);
|
package/engine/src/review.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { spawn, spawnSync } from "node:child_process";
|
|
12
12
|
import crypto from "node:crypto";
|
|
13
13
|
import fs from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
14
15
|
import http from "node:http";
|
|
15
16
|
import path from "node:path";
|
|
16
17
|
import { ENGINE_DIR, KIT_ROOT, kitHome, kitVersion } from "./locations.js";
|
|
@@ -510,7 +511,8 @@ async function serve(env: NodeJS.ProcessEnv): Promise<void> {
|
|
|
510
511
|
token,
|
|
511
512
|
readToken,
|
|
512
513
|
version: kitVersion(),
|
|
513
|
-
|
|
514
|
+
// Listening for other computers without a public address: this computer's name is the best guess.
|
|
515
|
+
publicUrl: env.REQ_KIT_REVIEW_PUBLIC_URL?.trim() || `http://${/^(127\.|localhost$|::1$)/.test(host) ? "127.0.0.1" : os.hostname()}:${port}`,
|
|
514
516
|
startedAt: new Date().toISOString(),
|
|
515
517
|
};
|
|
516
518
|
fs.mkdirSync(kitHome(env), { recursive: true });
|
package/engine/src/runtime.ts
CHANGED
|
@@ -141,6 +141,8 @@ export interface AgentRuntime {
|
|
|
141
141
|
providerQuota(providers: string[]): Promise<ProviderQuota[] | null>;
|
|
142
142
|
/** The agent's latest actions, oldest first; null when they cannot be read. */
|
|
143
143
|
recentActivity(agentId: string, limit: number): Promise<ActivityItem[] | null>;
|
|
144
|
+
/** 1.13: the tool call the agent is running now (the latest one still running), or null. */
|
|
145
|
+
runningTool(agentId: string): Promise<import("./scan-guard.js").RunningTool | null>;
|
|
144
146
|
/** Which Paseo tools step agents get (Settings → Agents); null when unknown. */
|
|
145
147
|
capabilities(): Promise<{ paseoTools: boolean; browser: boolean } | null>;
|
|
146
148
|
/** 1.12: whether each agent provider is installed and logged in ("ready"); null when the daemon does not say. */
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Stopping commands that search very large directories (1.13): a step agent that looks for a
|
|
2
|
+
// class or a package by searching the whole home directory, the Maven repository (~/.m2), the
|
|
3
|
+
// global node_modules or a drive can sit for many minutes and never finish the step. While a step
|
|
4
|
+
// runs, the pipeline looks at the agent's running tool call about once a minute; a search of such
|
|
5
|
+
// a directory (after a minute) or any search that runs too long is interrupted, and the agent is
|
|
6
|
+
// told how to find what it needs without the scan. A plain check of the command text, no model.
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
|
|
9
|
+
export interface RunningTool {
|
|
10
|
+
/** Paseo's tool kind: "shell", "search", … */
|
|
11
|
+
kind: string;
|
|
12
|
+
/** The command (shell) or the pattern (search). */
|
|
13
|
+
text: string;
|
|
14
|
+
/** When the call started (ISO time). */
|
|
15
|
+
startedAt: string;
|
|
16
|
+
/** The tool call's ID, so the same call is interrupted at most once. */
|
|
17
|
+
callId: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ScanGuardSettings {
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
/** A search that runs this long is interrupted wherever it searches. */
|
|
23
|
+
minutes: number;
|
|
24
|
+
/** More directories never to be searched as a whole (e.g. a shared drive). */
|
|
25
|
+
paths: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A search of a large directory gets this long before it is interrupted (most are quick to fail or finish). */
|
|
29
|
+
export const BIG_DIR_GRACE_MS = 60_000;
|
|
30
|
+
|
|
31
|
+
/** Commands that walk a directory tree. */
|
|
32
|
+
const SCAN_COMMAND =
|
|
33
|
+
/(^|[\s;&|("'`])(find|fd|locate|mlocate|tree|du|rg|ag|ack|grep\s+(-\w*[rR]\w*|--recursive)|egrep\s+-\w*[rR]|ls\s+(-\w*R\w*)|Get-ChildItem\b[^|;]*-r(ecurse)?\b|gci\b[^|;]*-r(ecurse)?\b|dir\b[^|;]*(\/s\b|-r(ecurse)?\b)|findstr\b[^|;]*\/s\b|where\s+\/r|Select-String\b[^|;]*-Path\b[^|;]*\*\*)(?=$|[\s;&|)"'`])/i;
|
|
34
|
+
|
|
35
|
+
/** Option values that keep a directory out of a search: not what it searches. */
|
|
36
|
+
const EXCLUSIONS = [
|
|
37
|
+
/--exclude(-dir)?(=|\s+)("[^"]*"|'[^']*'|\S+)/gi,
|
|
38
|
+
/(-g|--glob|--iglob)(=|\s+)("![^"]*"|'![^']*'|!\S+)/gi,
|
|
39
|
+
/(-not|!)\s+-(i?path|i?name|wholename)\s+("[^"]*"|'[^']*'|\S+)/gi,
|
|
40
|
+
/-(i?path|i?name|wholename)\s+("[^"]*"|'[^']*'|\S+)\s+-prune/gi,
|
|
41
|
+
/--(ignore-dir|ignore-directory|exclude-from)(=|\s+)("[^"]*"|'[^']*'|\S+)/gi,
|
|
42
|
+
/-Exclude\s+("[^"]*"|'[^']*'|\S+)/gi,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const END = String.raw`(?=$|[\s"'*;|&)\]` + "`" + `])`;
|
|
46
|
+
const START = String.raw`(?:^|[\s"'=(\[` + "`" + `])`;
|
|
47
|
+
|
|
48
|
+
const escape = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
49
|
+
const norm = (s: string) => s.replace(/\\/g, "/");
|
|
50
|
+
|
|
51
|
+
interface BigDir {
|
|
52
|
+
re: RegExp;
|
|
53
|
+
label: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function bigDirs(home: string, extra: string[]): BigDir[] {
|
|
57
|
+
const h = escape(norm(home).replace(/\/+$/, ""));
|
|
58
|
+
const homes = String.raw`(?:~|\$HOME|\$\{HOME\}|%USERPROFILE%|\$env:USERPROFILE|\$env:HOME${h ? `|${h}` : ""})`;
|
|
59
|
+
const sub = (p: string) => new RegExp(`${START}(?:${homes}/|[^\\s"']*/)?${p}/?${END}`, "i");
|
|
60
|
+
const list: BigDir[] = [
|
|
61
|
+
{ re: new RegExp(`${START}/${END}`), label: "整个磁盘(/)" },
|
|
62
|
+
{ re: new RegExp(`${START}[a-z]:/?${END}`, "i"), label: "整个磁盘" },
|
|
63
|
+
{ re: new RegExp(`${START}${homes}/?${END}`, "i"), label: "用户目录" },
|
|
64
|
+
{ re: sub(String.raw`\.m2(?:/repository)?`), label: "Maven 本地仓库(.m2)" },
|
|
65
|
+
{ re: sub(String.raw`\.gradle(?:/caches)?`), label: "Gradle 缓存(.gradle)" },
|
|
66
|
+
{ re: sub(String.raw`\.ivy2`), label: "Ivy 缓存(.ivy2)" },
|
|
67
|
+
{ re: sub(String.raw`(?:\.npm|\.pnpm-store|\.yarn|AppData/Local/(?:npm-cache|pnpm|Yarn))`), label: "npm/pnpm/yarn 缓存" },
|
|
68
|
+
{ re: sub(String.raw`(?:\.nvm|\.volta|\.fnm|AppData/Roaming/nvm)`), label: "Node.js 版本目录" },
|
|
69
|
+
{ re: sub(String.raw`\.cache`), label: "缓存目录(.cache)" },
|
|
70
|
+
{ re: sub(String.raw`(?:\.cargo|go/pkg/mod|\.rustup)`), label: "Rust/Go 依赖缓存" },
|
|
71
|
+
{ re: sub(String.raw`AppData(?:/Roaming|/Local)?`), label: "AppData" },
|
|
72
|
+
{ re: sub(String.raw`(?:usr/(?:local/)?lib/node_modules|AppData/Roaming/npm(?:/node_modules)?)`), label: "全局 node_modules" },
|
|
73
|
+
{ re: /npm\s+root\s+-g|pnpm\s+root\s+-g/i, label: "全局 node_modules" },
|
|
74
|
+
{ re: sub("node_modules"), label: "node_modules" },
|
|
75
|
+
{ re: new RegExp(`${START}(?:[a-z]:)?/(?:usr(?:/lib|/share|/local)?|opt|var|Library|Program Files(?: \\(x86\\))?|Windows)/?${END}`, "i"), label: "系统目录" },
|
|
76
|
+
{ re: sub(String.raw`site-packages`), label: "Python 依赖目录(site-packages)" },
|
|
77
|
+
];
|
|
78
|
+
for (const p of extra) {
|
|
79
|
+
const n = escape(norm(p).replace(/\/+$/, ""));
|
|
80
|
+
if (n) list.push({ re: new RegExp(`${START}${n}/?${END}`, "i"), label: p });
|
|
81
|
+
}
|
|
82
|
+
return list;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The large directory a command searches as a whole, or null. */
|
|
86
|
+
export function bigDirOf(command: string, options: { home?: string; paths?: string[] } = {}): string | null {
|
|
87
|
+
if (!SCAN_COMMAND.test(command)) return null;
|
|
88
|
+
let text = command;
|
|
89
|
+
for (const re of EXCLUSIONS) text = text.replace(re, " ");
|
|
90
|
+
text = norm(text);
|
|
91
|
+
for (const d of bigDirs(options.home ?? os.homedir(), options.paths ?? [])) if (d.re.test(text)) return d.label;
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function isScan(tool: Pick<RunningTool, "kind" | "text">): boolean {
|
|
96
|
+
return tool.kind === "search" || (tool.kind === "shell" && SCAN_COMMAND.test(tool.text));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ScanVerdict {
|
|
100
|
+
/** Why: the large directory, or how long the search has run. */
|
|
101
|
+
reason: string;
|
|
102
|
+
/** What the agent is told (the interrupt message). */
|
|
103
|
+
message: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const clip = (s: string, n = 160) => {
|
|
107
|
+
const one = s.replace(/\s+/g, " ").trim();
|
|
108
|
+
return one.length > n ? `${one.slice(0, n - 1)}…` : one;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** Whether the running tool call should be interrupted, and what to tell the agent. */
|
|
112
|
+
export function judge(tool: RunningTool | null, settings: ScanGuardSettings, now = new Date(), home = os.homedir()): ScanVerdict | null {
|
|
113
|
+
if (!tool || !settings.enabled) return null;
|
|
114
|
+
const ran = now.getTime() - new Date(tool.startedAt).getTime();
|
|
115
|
+
if (!(ran >= 0)) return null;
|
|
116
|
+
const big = tool.kind === "shell" ? bigDirOf(tool.text, { home, paths: settings.paths }) : null;
|
|
117
|
+
let reason: string | null = null;
|
|
118
|
+
if (big && ran >= BIG_DIR_GRACE_MS) reason = `在搜索${big}`;
|
|
119
|
+
else if (isScan(tool) && ran >= settings.minutes * 60_000) reason = `搜索已经运行了 ${Math.round(ran / 60_000)} 分钟`;
|
|
120
|
+
if (!reason) return null;
|
|
121
|
+
return { reason, message: scanMessage(tool.text, reason) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function scanMessage(command: string, reason: string): string {
|
|
125
|
+
return [
|
|
126
|
+
`【流水线提示】已停止你刚才的${command ? `命令「${clip(command)}」` : "搜索"}:它${reason},这类搜索要很久,会让这一步一直做不完。请继续完成本步骤的任务,但不要再搜索整个磁盘、用户目录或依赖缓存,改用下面的办法:`,
|
|
127
|
+
"- 在仓库里找:用 `git grep -n <内容>` 或 `git ls-files | grep <文件名>`;用 rg 时只在仓库目录里搜,它会跳过 .gitignore 里的目录。",
|
|
128
|
+
"- 找某个 Maven/Gradle 依赖里的类:先在 pom.xml / build.gradle 里确定 groupId、artifactId 和版本,直接打开 `~/.m2/repository/<groupId 的点换成 />/<artifactId>/<版本>/` 这一个目录,用 `unzip -l <jar> | grep <类名>` 或 `jar tf` 看那一个 jar;要源码时用 `mvn dependency:sources -DincludeArtifactIds=<artifactId>`。不知道是哪个依赖时,`mvn dependency:tree` 比搜索快。",
|
|
129
|
+
"- 找某个 npm 包:直接看仓库里的 `node_modules/<包名>/package.json`,以及其中 main、types 指向的文件;不要搜索整个 node_modules 或全局 node_modules。",
|
|
130
|
+
"- 实在找不到就不要再找了:按项目代码和文档里能确认的信息继续,把不确定的地方写进本步骤产物的\"假设\"一节。",
|
|
131
|
+
].join("\n");
|
|
132
|
+
}
|
package/engine/src/state.ts
CHANGED
|
@@ -50,7 +50,7 @@ export type StopKind =
|
|
|
50
50
|
export interface Recovery {
|
|
51
51
|
at: string;
|
|
52
52
|
key: string;
|
|
53
|
-
kind: "retry" | "reattach" | "reuse_result" | "extend_timeout" | "redo" | "fallback" | "auto_resume" | "quota_wait" | "self_answer";
|
|
53
|
+
kind: "retry" | "reattach" | "reuse_result" | "extend_timeout" | "redo" | "fallback" | "auto_resume" | "quota_wait" | "self_answer" | "stop_scan";
|
|
54
54
|
detail: string;
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -89,6 +89,8 @@ export interface BudgetState {
|
|
|
89
89
|
allowance: number;
|
|
90
90
|
/** The allowance a warning was given for, so each level warns once. */
|
|
91
91
|
warnedAt: number | null;
|
|
92
|
+
/** 1.13: the person was told the agents report no token usage (so the budget cannot work). */
|
|
93
|
+
noUsageNoticed?: boolean;
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
export interface PipelineState {
|
package/engine/src/stats.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paseo-req-kit",
|
|
3
|
-
"version": "1.13.0
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "req-kit: a requirement-development workflow for Paseo. /req aligns a requirement in chat; the engine runs design, implementation, review and verification as Paseo agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -6,7 +6,7 @@ import { Text, View } from "react-native";
|
|
|
6
6
|
import { useReviewUrl } from "./data";
|
|
7
7
|
import { openReview } from "./nav";
|
|
8
8
|
import { Button, Status, useStyles } from "./ui";
|
|
9
|
-
import { canEmbedPages, embeddedPage } from "./web";
|
|
9
|
+
import { canEmbedPages, embeddedPage, reviewUnreachable } from "./web";
|
|
10
10
|
|
|
11
11
|
type Theme = PluginHostProps["theme"];
|
|
12
12
|
|
|
@@ -28,6 +28,16 @@ export function ReviewScreen({ theme, layout, params }: PluginScreenProps) {
|
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
30
|
const { url, readOnly } = review.data;
|
|
31
|
+
if (reviewUnreachable(url)) {
|
|
32
|
+
return (
|
|
33
|
+
<View style={[s.screen, { padding: 12 }]}>
|
|
34
|
+
<View style={[s.card, { gap: 8 }]}>
|
|
35
|
+
<Text style={s.text}>原型审查页只在运行 Paseo 的那台电脑上能打开(地址是 {new URL(url).host}),你现在是从另一台电脑打开的 Paseo 网页,所以看不到。</Text>
|
|
36
|
+
<Text style={s.muted}>{`办法:到运行 Paseo 的那台电脑上打开 Paseo 网页再点"审查原型";或者请管理员在那台电脑上设置环境变量 REQ_KIT_REVIEW_HOST=0.0.0.0 和 REQ_KIT_REVIEW_PUBLIC_URL=http://<那台电脑的地址>:6781,再重启 Paseo daemon,审查页就能从别的电脑打开(能连上那台电脑的人都能看到审查页,提交意见要用页面地址里的口令)。也可以直接看功能目录里的 preview.md 和 prototype/。`}</Text>
|
|
37
|
+
</View>
|
|
38
|
+
</View>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
31
41
|
const frame = canEmbedPages ? embeddedPage(url, `原型审查:${params.title ?? featureDir}`) : null;
|
|
32
42
|
return (
|
|
33
43
|
<View style={[s.screen, { gap: 6 }]}>
|
|
@@ -15,3 +15,20 @@ export function embeddedPage(url: string, title: string): ReactElement | null {
|
|
|
15
15
|
style: { border: 0, width: "100%", height: "calc(100vh - 120px)", minHeight: 560, display: "block", background: "transparent" },
|
|
16
16
|
} as Record<string, unknown>);
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
const LOOPBACK = /^(127\.\d+\.\d+\.\d+|localhost|\[::1\]|::1)$/i;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Whether the review page cannot load in this browser: the review server listens only on the
|
|
23
|
+
* daemon's machine (127.0.0.1) and this Paseo web page was opened from another computer.
|
|
24
|
+
*/
|
|
25
|
+
export function reviewUnreachable(url: string): boolean {
|
|
26
|
+
if (Platform.OS !== "web") return false;
|
|
27
|
+
try {
|
|
28
|
+
const page = (globalThis as { location?: { hostname?: string } }).location?.hostname ?? "";
|
|
29
|
+
const target = new URL(url).hostname;
|
|
30
|
+
return LOOPBACK.test(target) && !!page && !LOOPBACK.test(page);
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|