Reins — DeepSeek Harness 的持久 Objective 编排层

English · 简体中文

DeepSeek Harness 0.2.0-rc.1 Node.js >= 22.13 SQLite 499 tests passing live harness 7/7 MIT license

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 架构总览

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,只增审计,关键不变量由触发器强制。 ### 一次恢复是怎么发生的

一次审批恢复:从 park 到验收

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)。