English · 简体中文
Harness 负责执行,Reins 负责编排。
用一句话告诉 Reins 你要什么:它和你一起规划,跨 Task、会话、审批、CI、故障和重启持续推进,
并且只有它亲自运行的检查真正通过才算完成。
安装 ·
快速上手 ·
命令 ·
配置 ·
架构 ·
安全模型 ·
测试 ·
开发
---
## 为什么需要 Reins
一个 Agent 会话擅长“现在就把这件事做完”,但真实的工程目标往往要跨越很长时间:改代码、跑测试、**等人审批**推送、**等 CI**、修 CI 失败、**等 Review**……中间还可能遇到进程崩溃、机器重启。
如果让 Agent 循环一直空转等待,既浪费资源,又容易把“模型说完成了”误当成“真的完成了”。Reins 把这些事交给一个**持久的编排层**:
- 用自然语言说目标,**当前对话的模型和你一起规划**,遇到真正需要你决定的地方会先问你。
- 等待是**持久状态**,不是空转的 Agent 循环;事件到达后用**新的 Attempt** 继续。
- 敏感操作**无人审批即拒绝**,批准后只授权**那一次精确的调用**。
- Task 完成必须通过**验收门**:Goal 完成、Attempt 成功都不算数,测试由 **Reins 亲自运行**,不采信 Agent 的说法。
- 所有状态都在 SQLite 里,崩溃重启后**保守对账**,不确定的状态永远不会被当成成功。
## 工作方式
```text
/reins 给笔记工具加个删除功能
```
1. **你输入这句话的那个对话**里的模型先看项目。
2. 需求里有真正需要你决定的地方时,它会问你(DSH 的选项卡片);它自己能查清楚的事不会问。
3. 现有测试覆盖不到这次改动时,它会**先写好检查**,并确认这个检查现在会失败。
4. 它调用 `reins_create_objective`,提交 Task、执行顺序和每个 Task 的检查命令。Reins **按哈希钉住检查文件**:做 Task 的 Agent 改不了自己的考卷。
5. Reins 自己的 Agent 在后台、在独立的会话里干活。需要审批时,`/reins-todo` 会列出来,`/reins-approve <编号>` 之后 Task 从停下的地方继续。
6. Reins 亲自运行检查,**退出码为 0 是唯一的“完成”**。
这套流程在 DSH Web 中用真实的 DeepSeek 模型验证过:要求“给笔记工具加个删除功能”,模型先问“按下标删除还是按内容删除”,写好 `check-delete.mjs` 并确认它会失败,然后创建 Objective;Reins 的 Agent 实现了按下标删除,Reins 运行被钉住的检查,退出码 0,验收通过。
## 核心特性
| | 能力 | 说明 |
|---|---|---|
| 💬 | **自然语言规划** | `/reins <你想要什么>`:当前对话的模型和你一起规划,有真正的分歧就问你,先写检查,再创建 Objective |
| 🧭 | **Task DAG** | 一个 Objective 包含多个有依赖的 Task;只有上游**通过验收**,下游才会 ready |
| 🔁 | **Attempt 边界** | 每次执行都是一个不可变的 Attempt;终态 Attempt 永不原地复活,恢复总是创建新 Attempt |
| ✋ | **持久审批** | 敏感工具无人审批时拒绝并 park;批准后生成一次性授权,绑定 Task / Attempt / Session / Goal / 工具 / 参数 / 策略版本 / 有效期,原子消费一次 |
| ⏳ | **持久等待与事件** | `awaiting_approval` / `awaiting_ci` / `awaiting_timer` / `awaiting_external_event`;不可变事件、幂等消费、重复事件抑制 |
| 📡 | **轮询 Watcher** | GitHub Checks、PR Review、本地/远程进程;退避、超时、限流、离线、陈旧结果拒绝;凭据不落库、不进日志 |
| 🧩 | **安全重规划** | graph revision;只在安全点修改 DAG,旧 revision 上的 Attempt 在数据库层面无法被验收 |
| 🌿 | **并行 worktree** | 并行 Task 各用独立 git worktree;孤儿对账、分支冲突规避、合并与冲突处理、验收绑定工件指纹 |
| 🤖 | **Subagent / Session fork** | 按 Task 路由到普通 Session、只读 Subagent 或 Session fork;工具可见性 ≠ 授权 |
| 🧪 | **验收门** | 冻结的验收准则 + 确定性校验器(文件类、`reins.*` 持久事实类,以及由 Reins **亲自运行**测试命令的 `reins.command_succeeds`),写入 pass 收据才能完成 |
| 📉 | **无进展检测** | 确定性失败指纹;连续相同失败、验收停滞、重复拒绝、重复工具错误都会挂起 Task 等人处理 |
| 💥 | **崩溃恢复** | 租约 + epoch 围栏;必须有绑定 owner / objective / epoch / execution 的退出证明才能接管;同一数据库只允许一个存活的 Reins 进程 |
## 安装
### 前置条件
- **Node.js ≥ 22.13**(需要内置的 `node:sqlite`)
- **DeepSeek Harness `@deepseek-ai/dsh@0.2.0-rc.1`**(Reins 的 peer 依赖固定到这个版本)
```bash
npm install -g @deepseek-ai/dsh@0.2.0-rc.1
```
下文用 `` 表示 Harness 的数据目录(环境变量 `DSH_HOME`,未设置时为 `~/.dsh`),用 `web` 表示要安装到的 profile。
### 1. 准备一份固定版本的插件目录
推荐从一个**固定的快照目录**安装,这样之后继续修改源码不会影响已安装的版本:
```bash
git clone --branch v0.4.0 https://github.com/dreamfarer-space/dsh-reins.git reins-0.4.0
```
### 2. 安装
Reins 是一个 DSH **插件包(bundle)**,两种装法任选其一:
- **在网页里装**:DSH Web 侧边栏 **插件** → **添加插件**,填入快照目录的绝对路径(例如 `D:/plugins/reins-0.4.0`),安装完成后点 **立即启用**。
- **用命令行装**:
```bash
dsh plugin --profile web add file:/absolute/path/to/reins-0.4.0
```
装好后,Reins 出现在 **插件 → 已安装** 里,带图标和说明,右侧开关就是 **启动 / 关闭**。不需要手动编辑 `cordis.patch.yml`。
### 3. 在插件页里设置
点开 **Reins 任务编排**,设置直接写在这个页面里,保存后立即生效(不用重启):
| 设置 | 说明 |
|---|---|
| **审批策略** | **严格**:所有工具都要批准;**推荐**(默认):读写编辑文件无需批准,运行命令、联网、子 Agent、未知工具需要批准;**自定义**:在推荐基础上勾选额外信任的工具(`pwsh`、`bash`、`web_fetch`…),并可列出**始终需要批准**的工具;**全部信任**:除“始终需要批准”的工具外全部放行(只在可回滚的目录里用) |
| **自动继续** | 事件到达、重试或上一个 Task 完成后不等 `/reins-tick` 就继续(Objective 也要允许:在 `/reins` 里说明,或 `/reins-goal --auto`) |
| **网页审批等待时间** | 审批在对话里等多久后释放 Agent,转为 `/reins-todo` 里的审批请求 |
| **默认工作目录** | 没有指定 `workspace` 的 Objective 在这里工作 |
| **桌面通知 / 对话中提醒** | 需要你处理或任务完成时怎么提醒你 |
这些设置保存在 profile 的 `cordis.patch.yml` 里(`- id: reins` 下的 `config:`),也可以手动编辑;其余配置项见[配置](#配置)。
### 4. 确认
在对话里输入 `/reins`,返回的用法说明会列出所有命令;输入 `/reins` 时 DSH 的命令菜单里也能看到它们。Reins 只约束**它自己发起的** Attempt;你平时直接和 Agent 的对话不受影响。
### 更新、回滚与卸载
- **更新**:准备一份新的快照目录,再执行一次 `dsh plugin --profile web add file:<新目录>`,或在插件页重新添加。
- **回滚**:安装前先备份 `/profiles/web` 下的 `package.json`、`pnpm-lock.yaml`、`cordis.patch.yml`;需要回滚时拷回这些文件,再执行 `dsh plugin --profile web install`。
- **关闭 / 卸载**:在插件页关掉开关即停止 Reins(数据库和设置保留);**卸载**按钮会移除插件包。
- **从 0.3.0 之前的版本升级**:旧版本需要在 `cordis.patch.yml` 里手写 `- insert: - id: reins ...`。现在由插件包自己声明这个条目,请把旧的 `insert` 块改成只写设置的覆盖:`- id: reins` 加 `config:`,否则会出现重复的 `reins` 条目。
- **从 0.3.x 升级**:每个 `/reins <子命令>` 都变成了独立命令(`/reins todo` → `/reins-todo`,`/reins dag ` → `/reins-status `……)。旧写法仍然可用,并会提示新名字;`/reins` 后面跟其它内容时,现在按自然语言需求处理。
## 快速上手
### 直接说你要什么
```text
/reins 登录接口有空指针异常,修一下
/reins 给报表页加一个 CSV 导出,保留现在的 JSON 导出
```
当前对话的模型会按[工作方式](#工作方式)里的流程和你一起规划。在任何对话里都能用,包括新对话的第一条消息。如果项目不在当前对话的目录里,在需求里写上目录。
### 一句话 + 自己指定检查
已经知道用哪条命令证明做完了,就跳过规划:
```text
/reins-goal 修复 clamp 的边界情况
/reins-goal 让 lint 通过 --test "npm run lint" --workspace D:/work/app
/reins-goal 修复 clamp --dry-run # 只预览,不创建
/reins-goal 修复 clamp --auto # 允许事件 / 重试后自动继续(还需配置 unattendedEnabled)
```
不加 `--test` 时,Reins 自动识别测试命令(`package.json` 的 `test` 脚本、Node 测试文件、Cargo、Go、pytest),识别不到会提示你用 `--test` 指定。
### 之后只需要两条命令
```text
/reins-todo # 所有等你处理的事:待审批、被挂起的 Task(附最后的错误)、等待 tick 的 Task
/reins-approve 3f9c1a2b # 用 todo 里显示的短编号批准
```
批准后 Task 会**在原来的会话里**继续(Agent 记得之前做到哪),不需要再执行 `/reins-tick`。读写编辑文件默认信任;运行命令(`pwsh` / `bash`)、联网、子 Agent 仍需审批(见[配置](#配置)中的 `trustPreset`)。
### 在 DSH Web 里
- **Reins 的会话就在侧边栏**:每个 Task 的 Agent 会话出现在侧边栏的“未分组”里(以 Task 内容命名),可以点进去看 Agent 做了什么。
- **审批两种方式都行**:需要审批时,那个会话会标记为“待审批”,点进去直接点“允许一次 / 拒绝”即可;如果你没去看,等待 `interactiveApprovalTimeoutMs`(默认 5 分钟)后它会释放 Agent,转成 `/reins-todo` 里带短编号的审批请求。DSH 自身的审批(例如 `pwsh` 请求放宽沙箱)也按同样规则处理。
- **主动提醒**:有事需要你时(待审批、Task 被挂起、Objective 完成或失败),Windows 会弹出系统通知;你下一次和任意 DSH 对话说话时,Agent 也会先简短告诉你,并附上要执行的命令。
- **强制关闭 DSH 后**:没有任务在执行时,重启后 Reins 自动接管。如果关闭时某个 Objective 正有 Attempt 在执行,Reins 无法证明它已停止,就**只把这个 Objective 挂起**(不接管、不改动它),其余照常运行;`/reins-todo`、对话提醒和系统通知都会提示。确认旧的 DSH 已经关闭后执行 `/reins-recover --confirm`:未完成的 Attempt 记为 interrupted,结果不确定的副作用交给你确认,然后继续。
### 手写多 Task:让 PR 达到可合并状态
需要完全掌控 Task、依赖和验收准则时,用 `/reins-objective` 手写 Task 依赖图:先实现修复,再跑测试,最后等 CI 通过。
```text
/reins-objective {"id":"pr-7","summary":"让 PR #7 达到可合并状态","workspace":"D:/work/app","tasks":[
{"id":"fix","title":"实现修复","criteria":[{"kind":"file_contains","spec":{"path":"src/fix.js","substring":"FIXED"}}]},
{"id":"tests","title":"运行测试","dependsOn":["fix"],"criteria":[{"kind":"reins.command_succeeds","spec":{"argv":["node","--test"],"timeoutMs":300000}}]},
{"id":"ci","title":"等待 CI 通过","dependsOn":["tests"],"criteria":[{"kind":"reins.wait_satisfied","spec":{"waitKind":"ci","eventType":"ci.passed"}}]}
]}
```
接下来会发生:
1. Reins 为 `fix` 创建 Attempt。Agent 收到一份**结构化交接**(目标、当前 Task、验收准则、上次失败原因、预算……),完成后调用 `task_complete`。
2. 验收门检查准则:通过才把 `fix` 置为 completed,`tests` 随之 ready。`tests` 的验收由 Reins 自己运行 `node --test`,只看退出码,不采信 Agent 写的任何测试报告。
3. 在 `ci` 中,Agent 推送后调用 `reins_wait`(`kind: "ci"`)。当前 Attempt 结束、Agent 停止,Watcher 开始轮询。
4. 推送是敏感操作:无人在场时会被拒绝并 park。用 `/reins-todo` 查看待审批请求,用 `/reins-approve <短编号>` 批准;Task 随即在原会话中继续。
5. CI 失败时,新 Attempt 会带着精简的失败上下文(失败的检查项、失败指纹)继续修复;CI 通过后完成验收。
随时查看全貌:
```text
/reins-status pr-7
```
> [!IMPORTANT]
> **无人值守执行默认关闭。** 由事件、计时器、自动重试或“上一个 Task 验收后开始下一个 Task”触发的 Attempt 属于无人值守执行,默认不会自动启动;`/reins-todo` 会列出它们,执行 `/reins-tick ` 推进。**你批准过的审批不在此列**:批准即授权,Task 会自动继续。若希望 Reins 全程自动继续,请同时设置配置 `unattendedEnabled: true` 与 Objective 字段 `"unattendedOptIn": true`(`/reins-goal` 用 `--auto`;用 `/reins` 时在需求里说明)。
## 命令
每个操作都是独立的顶层命令,DSH 的命令菜单可以直接列出和补全。`approve`、`deny`、`resolve` 可以使用 `/reins-todo` 显示的短编号(唯一前缀,至少 4 位,有歧义时拒绝)。
| 命令 | 作用 |
|---|---|
| `/reins <你想要什么>` | 自然语言需求:当前对话的模型和你一起规划并创建 Objective。不带内容时显示用法 |
| `/reins-goal <一句话> [--test "<命令>"] [--workspace <目录>] [--id ] [--auto] [--dry-run]` | 一句话创建单 Task Objective,测试命令退出码为 0 才算完成 |
| `/reins-todo` | 所有等你处理的事:待审批(带短编号)、正在 DSH 对话里等你回答的审批、结果不确定的副作用、被挂起的 Task 及最后的错误、等待 tick 的 Task、强制关闭后被挂起的 Objective |
| `/reins-status [objectiveId]` | 不带 id:全部 Objective,未完成的在前;带 id:Objective、Task DAG、当前 Attempt、验收、等待、审批、Watcher、预算、租约、最近事件 |
| `/reins-approve [理由]` | 批准一个持久审批请求 |
| `/reins-deny [理由]` | 拒绝,并把原因告诉 Agent |
| `/reins-pause ` | 暂停:正在执行的 Attempt 停下,状态保留 |
| `/reins-resume ` | 恢复暂停的 Objective;也会解除进行中 Objective 里被挂起的 Task(无进展、连续审批、等待超时),并重新开始计算无进展 |
| `/reins-cancel ` | 取消;未使用的授权先作废 |
| `/reins-tick [objectiveId]` | 立即执行一次调度(视为有人在场) |
| `/reins-recover [objectiveId] [--confirm]` | 强制关闭 DSH 后:列出被旧进程占住的 Objective;`--confirm` 在确认旧进程已不存在后释放它们(Reins 没能启动时也可用) |
| `/reins-objective ` | 用手写的 Task 依赖图创建 Objective(字段见下表) |
| `/reins-replan ` | 安全点重规划:`{"add":[…],"remove":[…],"modify":[…],"reopen":[…],"reason":"…"}` |
| `/reins-event ` | 注入一个外部持久事件 |
| `/reins-resolve done\|not_done` | 人工确认一个结果不确定(ambiguous)的副作用是否发生 |
| `/reins-merge ` | 把已验收 Task 的 worktree 合并回当前分支 |
Objective 与 Task 字段
| 字段 | 说明 |
|---|---|
| `id` | Objective id(可选,默认随机) |
| `summary` | 目标描述 |
| `workspace` | 该 Objective 的工作目录(绝对路径,必须存在);Agent 在此工作,文件类准则和验收命令也在此执行。默认取配置 `evidenceRoot` / `repoRoot` |
| `tasks[]` | Task 列表,至少一个 |
| `tasks[].id` / `title` / `description` | Task 标识、标题、详细说明 |
| `tasks[].dependsOn` | 依赖的 Task id 列表(不允许成环) |
| `tasks[].required` | 是否为完成 Objective 所必需,默认 `true` |
| `tasks[].criteria[]` | 验收准则:`{"kind","spec","required"}` |
| `tasks[].capability` | 执行路由,见下方“能力路由” |
| `tasks[].maxAttempts` / `maxWallTime` | Task 级预算 |
| `maxAttempts` / `maxWallTime` | Objective 级预算;`maxWallTime` 默认 7 天,**包含**等待时间 |
| `maxActiveMs` | Objective 实际执行时间上限,**不含**等待时间 |
| `maxParallel` | 同一 Objective 的最大并行 Attempt 数(>1 需要 worktree 隔离) |
| `noProgressLimit` | 无进展判定阈值(默认取配置 `noProgressAttemptLimit`) |
| `unattendedOptIn` | 是否允许无人值守继续 |
能力路由 capability
| 字段 | 说明 |
|---|---|
| `route` | `session`(默认,普通 Agent + Goal)、`subagent`(进程内子 Agent)、`fork`(从上一次 Attempt 的会话派生) |
| `readOnly` | `subagent` / `fork` 默认 `true`:敏感工具一律拒绝 |
| `isolation` | `none` 或 `worktree`;会写代码的 Subagent 必须使用 `worktree` |
| `provider` | Subagent 提供方名称(默认 `spawn`,fork 为 `fork`) |
| `model` / `modelProvider` | 指定模型与模型提供方 |
| `tools.allow` / `tools.deny` | 限制可见工具。**可见不等于授权**,敏感调用仍需审批 |
| `maxGoalRounds` | 每个 Attempt 的 Goal 轮数上限,默认 3 |
验收准则类型
| kind | spec | 说明 |
|---|---|---|
| `file_exists` / `file_absent` | `{"path"}` | 文件存在 / 不存在 |
| `file_contains` | `{"path","substring"}` | 文件包含文本 |
| `file_sha256_equals` | `{"path","sha256"}` | 文件哈希相等 |
| `json_field_equals` | `{"path","pointer","expected"}` | JSON 字段相等,`pointer` 为点分路径(如 `a.b`) |
| `marker_count_equals` | `{"path","marker","count"}` | 标记出现次数 |
| `reins.wait_satisfied` | `{"waitKind","eventType"}` | 该 Task 最近一次等待已被指定类型的持久事件满足,且当前 Attempt 晚于该事件 |
| `reins.output_contains` | `{"substring"}` | Subagent 的输出包含指定文本 |
| `reins.no_ambiguous_side_effects` | `{}` | 该 Task 不存在结果不确定的副作用 |
| `reins.command_succeeds` | `{"argv","cwd","timeoutMs","expectExitCode","pins","env"}` | 验收时由 Reins 亲自运行命令,退出码等于 `expectExitCode`(默认 0)才通过 |
文件路径一律相对于 Attempt 的工作目录(Objective 的 `workspace`、`evidenceRoot` 或 worktree),禁止绝对路径和 `..`。
> [!WARNING]
> 文件类准则检查的是工作目录里的文件,**Agent 自己也能写这些文件**。真模型测试中出现过:测试失败后,模型把 `test-report.json` 改写成 `{"passed":true}`,`json_field_equals` 因此通过。凡是“测试通过”“构建成功”这类结论,请用 `reins.command_succeeds`,不要用报告文件。
`reins.command_succeeds` 的规则:
- `argv` 是参数数组,不经过 shell;`argv[0]` 为 `node` 时使用运行 Harness 的同一个 Node。Windows 上的 `.cmd` / `.bat` 不能直接运行,请写成 `["node", "<脚本路径>"]`。(`/reins` 和 `/reins-goal` 会把你给的命令行自动包成 `cmd /d /s /c` 或 `sh -c`。)
- `cwd` 为工作目录内的相对路径(默认 `.`);`timeoutMs` 默认 10 分钟、最长 1 小时,超时会结束整个进程树并判为失败。
- 子进程只拿到最小环境变量(`PATH`、系统目录、临时目录等)加上 `env` 中显式声明的值,不继承其它环境变量,没有标准输入。
- 命令会执行工作目录里的代码,而 Agent 可能改过这些代码。用 `pins`(`[{"path","sha256"}]`)钉住测试文件:哈希不符时直接判失败,**命令根本不会运行**。`/reins` 会钉住规划时写下的检查文件。
- `task_complete` 预校验时也会运行一次,失败输出的末尾会反馈给 Agent;最终结论仍以验收门重新运行的结果为准。
### Agent 侧工具
| 工具 | 谁用 | 说明 |
|---|---|---|
| `reins_create_objective` | 你输入 `/reins` 的那个对话 | 创建规划好的 Objective。每次 `/reins` 只能用一次,且只在那个对话里可用,Reins 自己的会话不能用;校验 Task、顺序、检查命令,并钉住 `checkFiles`。`dryRun` 只展示计划、不创建 |
| `task_complete` | Reins 的 Agent | 声明 Task 已完成;Reins 先做预校验,最终由验收门决定 |
| `reins_wait` | Reins 的 Agent | 结束当前 Attempt 并持久等待,事件到达后以新 Attempt 继续 |
`reins_wait` 的 `kind`:
| kind | 参数 | 恢复事件 |
|---|---|---|
| `ci` | `repo`(`owner/name`)、`sha`、可选 `requiredChecks` | `ci.passed` / `ci.failed` |
| `pr_review` | `repo`、`number` | `review.approved` / `review.changes_requested` / `review.commented` / `pr.merged` / `pr.closed` |
| `process` | `exitFile` 或 `pid`、可选 `name` | `process.succeeded` / `process.failed` / `process.lost` |
| `remote` | `url`、可选 `name`(主机必须在配置 `remoteWatcherHosts` 中) | `remote.succeeded` / `remote.failed` / `remote.cancelled` |
| `timer` | `delayMs` | `timer.fired` |
| `external_event` | `subject`、`eventTypes` | 通过 `/reins-event` 注入的事件 |
所有等待都可以带 `timeoutMs`(默认 24 小时);超时后 Task 挂起等人处理,不会盲目重试。
外部事件示例(`subject` 需加 `ext:` 前缀,与 `reins_wait` 的 `subject` 对应):
```text
/reins-event {"source":"deployer","sourceEventId":"deploy-42","type":"deploy.done","subject":"ext:deploy-42","objective":"pr-7","payload":{"env":"staging"}}
```
`source + sourceEventId` 是事件的稳定身份:重复投递会被抑制;同一身份但内容不同会被标记为冲突,不会覆盖原事件。
单 Task 命令(Phase 2/3,供脚本使用)
这些命令只能以 `/reins <子命令>` 的形式使用:`create | `、`criteria-add `、`criteria-list [revision]`、`criteria-revise `、`accept [attemptId]`、`receipts `。`/reins-status`、`-pause`、`-resume`、`-cancel` 对这类 Objective 同样有效。新工作请用 `/reins` 或 `/reins-goal`。
## 配置
所有配置都经过严格校验:未知键、非法数值、云端 URL、UNC 网络路径都会被拒绝。
| 配置项 | 默认值 | 说明 |
|---|---|---|
| `databasePath` | `~/.dsh/dsh-reins/reins.sqlite` | 本地 SQLite 路径(目录不存在时自动创建)。同一数据库只允许一个存活的 Reins 进程 |
| `evidenceRoot` | `null` | 未指定 `workspace` 的 Objective 的默认工作目录;与 `repoRoot` 都未设置时,编排型 Objective 必须自带 `workspace` |
| `approvalRequiredTools` | `[]` | 始终需要审批的工具 |
| `trustPreset` | `workspace` | 内置信任集合。`workspace`:`read`、`glob`、`grep`、`write`、`edit`、`str_replace_editor`、`todo_write`、`get_goal`、`update_goal` 以及 Reins 的 `task_complete`、`reins_wait`;运行命令、联网、子 Agent 和未知工具仍需审批。`none`:只信任 `trustedTools` |
| `trustedTools` | `[]` | 在预设之外额外信任的工具(例如 `["pwsh"]`) |
| `trustUnlistedTools` | `false` | 是否信任未列出的工具。默认**不信任**:未知副作用一律需要审批 |
| `unattendedEnabled` | `false` | 是否允许无人值守执行(还需 Objective 同时 `unattendedOptIn`) |
| `defaultMaxAttempts` | `5` | 单个 Task 默认最大 Attempt 数 |
| `defaultMaxWallTimeMs` | `300000` | 单 Task Objective 的默认墙钟预算 |
| `orchestratedDefaultWallTimeMs` | 7 天 | 编排型 Objective 的默认墙钟预算(包含等待) |
| `noProgressAttemptLimit` | `3` | 无进展判定阈值 |
| `maxConcurrentAttempts` | `1` | 全局并发 Attempt 上限(最大 16;>1 需要 worktree 隔离) |
| `repoRoot` / `worktreeBaseDir` | `null` | git 仓库根目录与 worktree 存放目录;设置 `repoRoot` 后才启用并行 worktree |
| `approvalRequestTtlMs` | 24 小时 | 待审批请求的有效期 |
| `allowanceTtlMs` | 15 分钟 | 批准后一次性授权的有效期 |
| `subagentWallTimeMs` | `600000` | 单个 Subagent Attempt 的时间上限 |
| `watcherCredentials` | `{}` | 凭据引用名 → **环境变量名**,例如 `{"github": "GITHUB_TOKEN"}`。只写变量名,绝不写密钥 |
| `githubApiBase` | `https://api.github.com` | GitHub API 地址(可指向 GitHub Enterprise) |
| `notifyDesktop` | `true` | 需要你时弹出系统通知(Windows;其他系统需配置 `notifyCommand`) |
| `notifyInConversation` | `true` | 你下一次和 DSH 对话时,由 Agent 简短提醒 |
| `notifyCommand` | `[]` | 自定义通知命令(参数数组),通过环境变量 `REINS_TITLE` / `REINS_BODY` 接收内容 |
| `interactiveApprovalTimeoutMs` | 5 分钟 | DSH 对话里的审批等待多久后释放 Agent、转为 `/reins-approve` 请求(0 表示一直等) |
| `minTimerWaitMs` | 60 秒 | Agent 用 `reins_wait` 等待计时器的最短时间;更短的会被拒绝,Agent 继续工作 |
| `remoteWatcherHosts` | `[]` | `remote` 等待允许访问的主机(`"host"` 或 `"host:port"`)。为空时禁用 `remote` 等待 |
| `orchestratorTickMs` / `watcherPollMs` / `watcherTimeoutMs` | `2000` / `1000` / `15000` | 调度间隔、轮询间隔、单次查询超时 |
| `leaseDurationMs` / `heartbeatIntervalMs` | `10000` / `3000` | 租约时长与心跳间隔(每个租约期至少续约 3 次) |
**预算口径**:`maxWallTime` 是墙钟时间,包含等待 CI、审批的时间;`maxActiveMs` 只统计 Attempt 的实际执行时间;每个 Attempt 的轮数由 `maxGoalRounds` 约束。没有可靠的 token 遥测,因此不提供 token / 费用预算。
## 架构
Reins 是一个 **Cordis 插件**,只使用 DeepSeek Harness 的公开 API,不修改 Harness 核心:
- **命令与规划**(`src/commands/`):`/reins` 与 `/reins-*` 命令;`/reins <需求>` 通过 `agent.followup` 把需求交给当前对话的模型,`reins_create_objective` 把它的计划变成 Objective。
- **编排层**(`src/orchestrator/`):调度 Ready Task,维护 DAG 与 graph revision,管理持久等待与事件、Watcher、持久审批、worktree、能力路由、失败指纹与交接。
- **执行运行时**(`src/orchestration/runtime.js`):原子准入、租约围栏、外层结算(disarm → cancel → idle → flush → 终态收据)、工具与步骤守卫。每个 Attempt 内,**只有 Harness 的 goal-round-driver 负责续跑**。
- **验收门**(`src/acceptance/`):唯一能让 Task 进入 `completed` 的路径;数据库触发器保证没有 pass 收据就无法完成。
- **存储**(`src/store/` + `migrations/`):SQLite(`node:sqlite`),迁移 001–015,只增审计,关键不变量由触发器强制。
### 一次恢复是怎么发生的
CI、定时器、PR Review、外部事件都走同一条路径:**等待 → 持久事件 → 幂等消费 → 新 Attempt**。
## 安全模型
Reins 的每一项能力都服从下面这些不变量,测试和真实 Harness 验证都围绕它们展开:
- **Harness 负责执行,Reins 负责编排。** 只用公开 API,不导入私有模块,不修改 Harness 核心。
- **一个 Attempt 内只有 goal-round-driver 负责续跑**;一个 Objective 同时只有一个有效写者(租约 + epoch 围栏)。
- **终态 Attempt 永不原地复活**;被 park 的 Attempt 恢复时总是创建新 Attempt。
- **敏感操作无人审批即拒绝**;拒绝之后还必须完成 park / cancel 与静止证明。
- **审批不等于验收,Goal 完成不等于 Task 完成。** 审批不能绕过沙箱、守卫、租约、预算或验收门。
- **等待状态不保留 Agent 循环。**
- **结果不确定的副作用不自动重放**;授权一旦消费,即使工具失败或进程崩溃也不会自动恢复。
- **SQLite 与 Harness 会话日志是两个独立的一致性域**:只有拿到 flush 证明才算静止;重启后不会把任何不确定状态当成成功。
- **崩溃后接管需要退出证明**:证明必须绑定 owner / objective / epoch / execution;没有证明时只挂起该 Objective(`LEASE_BUSY`),绝不改动它。
- **一个数据库只对应一个存活的 Reins 进程**:另一个存活进程持有数据库时以 `DATABASE_IN_USE` 拒绝启动;被接管的进程立即停止调度和轮询。
- **“测试通过”由 Reins 亲自验证**:`reins.command_succeeds` 在验收时运行冻结的命令,被钉住的文件改动过就不运行。
- **规划出的 Objective 只能来自你**:`reins_create_objective` 只在你于该对话输入 `/reins` 之后可用、只能用一次,Reins 自己的会话不能用;规划时写下的检查文件在任何 Agent 开始之前就被钉住。
- **强制关闭后保守接管**:强制关闭留下的租约,只有在“旧进程已记录于本机且确认不存在、且没有执行中的工作”时自动接管;有执行中的 Attempt 时必须由你通过 `/reins-recover --confirm` 确认,确认记录只增不改。
- **审批后在原会话中继续**:被审批 / 等待 park 的 Attempt,下一个 Attempt 重新打开同一个 Harness 会话(仍是新 Attempt、新 Goal,授权仍只放行那一次精确调用);失败、崩溃、重规划后使用新会话。连续 10 次审批 park 仍未完成的 Task 会被挂起,交给你决定。
- **Agent 指定的 URL 只能访问白名单主机**:`remote` 等待在注册时和每次轮询前都检查 `remoteWatcherHosts`,拒绝重定向,从不携带凭据。
## 测试与验证
```bash
npm ci # 安装依赖
npm run typecheck # 公开 Harness 接口的类型检查
npm test # 主回归(约 70 秒)
npm run test:runner # 场景驱动与验证器回归(含验证器反例)
npm run test:live # 真实 DeepSeek Harness 场景(约 4 分钟)
npm run test:live:phase3 # Phase 3 单 Task 真实 Harness 场景(约 2 分钟)
node scripts/build-diagrams.mjs # 重新生成中英文 README 示意图
```
当前结果:
| 套件 | 结果 |
|---|---|
| `npm test` | 499 / 499 |
| `npm run test:runner` | 48 / 48(含 Phase 4 验证器反例 28 个) |
| `npm run test:live` | 7 / 7 |
| `npm run test:live:phase3` | 11 / 11 项门禁 |
| `npm run typecheck` | 通过 |
### 真实 Harness 场景(`npm run test:live`)
在隔离的 `DSH_HOME` 中启动真实的 `dsh --profile` 进程:真实的 Cordis、Agent、agent loop、goal-round-driver、会话持久化、工具管线和无头审批,只有模型换成确定性 fixture。用户操作走真实注册的 `/reins-*` 命令;GitHub 由一个在进程重启之间保持状态的本地替身提供。
| 用例 | 验证内容 |
|---|---|
| `e2e` | “让 PR 可合并”五个 Task:无人审批 → park → 批准 → 新 Attempt 精确推送一次;CI 失败 → 修复 → 再次审批推送 → CI 通过;处理 Review;全部验收后 Objective 完成 |
| `crash-running` | Attempt 运行中被 `SIGKILL`,带退出证明重启:对账为 interrupted,重试并完成 |
| `crash-consumed` | 授权已消费、推送已发生但结果未回报时被杀:副作用标记 ambiguous,不重放,人工确认后完成 |
| `crash-awaiting-ci` | 等待 CI 时被杀:Watcher 恢复轮询并驱动后续 Attempt 完成 |
| `no-proof` | 被杀后没有退出证明:只挂起该 Objective(`LEASE_BUSY`),不改动它,Reins 其余部分照常运行 |
| `second-process` | 第一个进程运行 `e2e` 期间,在同一数据库上启动第二个进程:第二个进程以 `DATABASE_IN_USE` 拒绝启动、不执行任何命令,第一个进程照常完成 |
| `subagent` | 真实 Subagent 与 Session fork:只读子 Agent 的敏感调用被识别并拒绝、零物理副作用;子会话的 `parentSession` 等于 Reins 锚点会话;fork 重试确实从上一次会话派生 |
独立验证器 `scripts/phase4-harness-verify.mjs` 只读取 SQLite(只读打开)、物理副作用日志、监督进程记录和真实会话日志,每个用例做 6–59 项检查,不采信场景插件自己的结论;`tests/runner/phase4-verifier-negative.test.js` 用真实运行快照逐项篡改,证明验证器会判 FAIL。
> [!NOTE]
> 上述真实 Harness 场景使用确定性模型;GitHub 由本地替身提供。真实大模型(DeepSeek)另外在 DSH Web 中验证过:`/reins` 规划(先看项目、就真正的分歧提问、写好并钉住检查、创建;随后 Reins 的 Agent 完成工作,被钉住的检查通过);`/reins-goal`;正常修复、被拒后纠正、审批、无法完成、默认工具策略、伪造测试报告,以及用 `reins.command_succeeds` 验收的场景(伪造的报告文件存在时,Reins 自己运行测试得到 exit=1,Task 不会完成)。
## 项目结构
```text
reins/
├── src/
│ ├── plugin.js # 插件入口(Cordis apply)
│ ├── config.js # 配置 schema 与严格校验
│ ├── live-config.js # 插件页保存的设置无需重载即生效
│ ├── commands/ # /reins 与 /reins-* 命令,/reins 规划(plan.js)
│ ├── orchestrator/ # 编排层:DAG、等待/事件、Watcher、审批、worktree、路由、指纹、交接
│ ├── orchestration/ # 执行运行时:准入、预算、无进展、外层结算
│ ├── acceptance/ # 验收门、准则冻结、校验器、收据完整性
│ ├── approval/ # 关系型审批(Phase 2)
│ ├── store/ # SQLite 存储、事务、迁移执行、实例锁
│ ├── reconcile/ # 启动对账
│ ├── harness/ # Harness 公开 API 适配
│ └── tools/ # task_complete 工具
├── client/client.js # DSH Web 插件页上的设置页
├── migrations/ # 001–015,只新增、不改写
├── scripts/ # 真实 Harness 场景驱动、独立验证器、示意图生成
├── tests/ # 按功能分组的回归测试(见 tests/README.md)
├── docs/assets/ # README 图片(中英文)
└── smoke/public-api.ts # 公开 API 类型检查样例
```
## 开发
### 开发规则
1. 新功能或缺陷修复先写能复现问题的测试,再修改实现。
2. 不删除或放宽安全断言来让测试变绿。身份绑定、租约围栏、审批与验收默认拒绝的语义必须保留。
3. Task / Objective 完成必须走验收门,不能只凭模型说“完成”或直接改数据库。
4. 不把 API Key、真实用户配置、真实数据库、日志或运行证据提交到仓库。
5. 每次改动后运行 `typecheck`、`test`、`test:runner`;涉及框架生命周期、恢复或工具调用时,还要运行 `test:live`。
### 约定
- 数据库结构变更只能新增迁移文件,不能改写已有的 001–015。
- 新的用户操作就是新的顶层命令 `/reins-<操作>`(`src/commands/register.js` 中 `COMMANDS` 的一行);真实 Cordis 卸载测试固定了完整的命令集合。`listValidatorKinds()` 固定为 6 种文件校验器;新的验收类型走独立的 `reins.*` 校验器注册表。
- 测试临时文件统一使用 `tests/fixtures/scratch.js`,不要写死本机路径。
- 文本文件统一使用 LF(由 `.gitattributes` 强制)。
- 中英文示意图由 `scripts/build-diagrams.mjs` 从同一份描述生成。
### 发布内容
`package.json` 的 `files` 只包含插件本身(`src`、`migrations`、`client`、`locale`、插件包 patch、图标、许可证与 README);测试、脚本和开发依赖不进入发布包。
## 已知限制
- 文件类验收准则信任工作目录中的文件,而 Agent 能写这些文件;涉及“测试 / 构建通过”的结论请使用 `reins.command_succeeds`。
- `reins.command_succeeds` 会运行工作目录中的代码;没有被 `pins` 钉住的文件,Agent 都可能改过。它不在沙箱中运行。
- `/reins` 的规划依赖当前对话模型的判断:它可能自己决定了你本想被问到的事,或者选了一个比你期望更弱的检查。请看它的总结;`/reins-status ` 会显示每个 Task 的具体检查。`/reins-goal` 不会钉住测试文件;需要更强保证时,用 `/reins`(会钉住它写的检查)或带 `pins` 的 `/reins-objective`。
- 真实大模型覆盖了 `/reins` 规划、`/reins-goal` 和单 Task 场景;CI / PR Watcher、崩溃恢复、并行 worktree、Subagent / fork、重规划只用确定性模型在真实 Harness 中验证过。
- 只在 Windows 上测试过;Linux / macOS 未验证。
- 系统通知目前只在 Windows 上内置;其他系统需自行配置 `notifyCommand`。
- DSH 的沙箱会阻止 `node --test` 这类需要启动子进程的命令,Agent 可能为此请求放宽沙箱;验收由 Reins 在沙箱外自己运行测试,不受影响。
- 单 Task Objective(`/reins create`)同一时间只能执行一个,且未设置 `evidenceRoot` 时在 DSH 进程的启动目录中工作;编排型 Objective 通过独立的执行槽位并行。
## 许可证
[MIT](LICENSE)。