# @wweir/dsh-advisor [English](README.md) | 简体中文 DeepSeek Harness 会话的自动第二意见。 按可配置的每轮节奏,一个**受限子 Agent** 用只读工具重读 Executor 近期的工作。具体 缺陷被记录并投递进 Executor 的收件箱;没有可指出的问题时审查方保持沉默——沉默是 正常结果,不是失败。 本插件从不运行自己的模型循环:审查是一个真实的子会话,宿主的 turn/step 状态机、 工具管线、流式输出、取消与持久化原样适用于它。 ## 它能帮你什么 - **抓住幻觉。** 审查方亲自读文件(只读):声称改了但没改的、不存在的 API、从未写 入的值。备注在工作还新鲜时指出具体不符;投递是建议性的,只在配置的程度上打断。 - **让会话不走偏。** 每 N 个完成的轮次最多审查一次(默认 3),绝不并发——审查 期间到来的轮次合入单次补审。跑偏在运行中的 turn 内被报告;空闲会话可被严重问题 唤醒(有预算、按严重度门控)。沉默被报告为结果,而不是错误。 - **成本有界。** 审查方的上下文是按行编译、以 32K tokens 为默认上限(≈ **128K 窗口的 25%**)的投影,绝不是 Executor 上下文的副本。稳定的指令前缀让 provider 的上下文缓存跨次命中(tab 显示命中缓存的 token,按缓存价计费)。输出仅一条建 议,最多 2K 字符。 ## 为什么是这个形态 大多数 advisor 插件把 transcript 交给一次额外的 LLM 调用,然后把返回内容直接注入。 本插件把审查方做成**一个自带只读工具的真实子会话**,这带来四样具体的东西: - **它靠读来核实,而不是猜。** 审查方打开 Executor 实际动过的文件,因此每条备 注指向的是它亲自查看过的缺陷——而不是从 transcript 摘要里推断出的模式,那只会 复读 transcript 里已有的内容。 - **宿主的规则对审查本身生效。** turn/step 记账、工具管线、流式输出、取消、持久化 以及真实的审计轨迹:审查就是一个可以像其它会话一样检查的会话,它的成本是上报的 用量,不是估算。 - **隔离是被强制执行的,不是承诺。** 每个进程一份审查者认领、只读工具面、比上游 advisor 有意更严的受保护路径守卫(工作区根的私钥和 `.envrc` 同样在禁列),且与 Executor 不共享任何上下文。 - **投递是有记账的。** 备注只有在宿主证明它被处理过之后(followup turn 的模型回 复)才计入已投递;未确认的备注会重新入队,绝不悄悄丢弃。重复按 finding 身份加 相似度抑制,支持按 hash mute。 ## 安装 本包是 Cordis 插件(`dsh.bundle.patch` → `cordis.patch.yml`),已发布到 npm(每次发布带 provenance 证明),安装方式与任何 profile bundle 相同: ```bash dsh plugin --profile add @wweir/dsh-advisor ``` 也可以用本地 checkout 的路径 `dsh plugin … add /path/to/checkout` 以 `link:` 方式安装; 其部署方式见 `docs/OPERATIONS.md`。 然后给它一条模型路由。未配置模型前它**处于禁用状态**,且绝不回退到 Executor 的模型: ```yaml - id: advisor name: '@wweir/dsh-advisor' config: enabled: true model: provider: opencode-go model: glm-5.3-flash reasoningEffort: low instructions: '' # 追加到审查提示词末尾的额外指令 review: everyTurns: 3 # 两次审查之间最少间隔的完整轮数 timeoutMs: 180000 # 单次审查的墙钟上限 maxContextTokens: 32000 # 发送给审查方的材料上限 maxAdviceChars: 2000 # 单条建议正文上限 maxPending: 32 # 排队中、尚未投递的备注上限 delivery: activeIdleSeverities: [concern, blocker] # 允许唤醒空闲 Executor 的严重度 security: additionalProtectedPaths: [] # 审查方永远不许读取的额外路径 ``` patch 条目按行 id **整体替换、不做合并**:一个只设了追加指令的 overlay 会丢掉模型 路由,该行随之停止加载——以校验错误响亮报错。 ## 值得了解的行为 - **触发**——一次审查以*轮*为单位:Executor 的最终回答,或对于没有最终回答就结 束的 turn,以正常完成、触顶或报错的顶层 turn 结束。以轮为单位是因为读者可以在 一个进行中的 turn 里持续 steering:该 turn 永远不结束,基于 turn 的触发器就永远 不触发,尽管每轮回答都是完整的。子 Agent 会话(包括审查子会话本身)永远不会被审查; 本插件自己打开的 turn 同样不会被审查。 - **投递**——Executor 运行中时,备注被交进下一个 step 边界;空闲时,备注可以开一个 独占 turn,但只有允许唤醒空闲会话的严重度才可以(默认 concern 与 blocker),每个 会话最多 5 次;其余情况等待后续 flush。交进运行中 turn 的备注在宿主接受后即记为 已投递。开了独占 turn 的备注**只在那个 turn 产生模型回复之后才记账**:交接本身不 能证明备注被读到,因此一个没有回复就关闭的 turn(重启、打断、空 turn)会把备注放 回队列,标记为已尝试过——它不会再开第二个 follow-up turn,但下一个运行中的 turn 仍可收到它。确认会立即持久化;加载时在 sidecar 里发现未确认的声明,也按同样方式 重新入队。已投递的 finding 按身份抑制,换措辞重复不会被重复投递。 - **失败暂停**——连续三次*真实*失败(模型错误、超时、工具面错误)暂停自动审查。 取消单独计数:有记录但永不触发暂停。暂停在插件加载期间持续;**插件重载(配置行变更 或进程重启)会清除 tripwire** 并重新武装审查。在页面表单里保存配置不算重载,暂停 状态保持不动。 - **状态**——每个 Executor 会话一个 JSON sidecar,位于 `$DSH_HOME/advisor/`(默认 `~/.dsh/advisor/`)。其中保存审查游标、去重表、待投 递队列、mute 列表,以及作为诊断的失败记账。无法解析的文件永不被覆盖,并会在宿 主日志里报告。 - **Mutes**——向 sidecar 的 `mutes` 列表添加 `{ "hash": "<64-十六进制 finding 的 hash>", "label": "..." }`。身份以完整 hash 为准,没有前缀匹配。 - **页面**——web 客户端在 Chat/Trajectory/Host 旁显示 **Advisor** tab。它通过 与 GUI 本身相同的认证通道读取与 `/advisor status` 相同的状态(浏览器会话 cookie 与 Host/Origin 围栏同样生效),每 几秒刷新一次,无需输入命令即可看到暂停的 advisor 以及上次沉默的原因。展示层只有一 条注入样式表、基于 DSH 主题 token,因此明暗主题、焦点、悬停与触控尺寸都跟随宿主; 区块是带标签区域里的标题,轮询状态同时镜像到 polite live region 供屏幕阅读器使用。 它的队列区块就是投递面,按「读者要动手的顺序」排:先是在途状态——待投递备注连同队首 备注被**挂起**的原因(executor 运行中、严重度不允许唤醒空闲会话、followup 预算耗尽、 上一次尝试未被处理、更新的指令到达),以及 followup 交接后**等待确认**的备注——然后 是一行弱化的累计量:只显示本会话真正计数过的东西(已投递、已 mute、花掉过的空闲 followup,旁边是宿主上报的预算;从未用过的预算不算值得一画的数字)。队列为空时用一句话说明,而不是画一排 0。它还承载日志里没有的每会话审查记账,同样是一行一 件事:审查方读到的提示词总量及其缓存命中占比、输出量、审查次数与平均耗时、只列真正 发生过的结果,以及有界的、最新在前的审查与投递历史。它也编辑常用配置——开关、模 型路由、审查节奏与各项预算、空闲投递严重度和额外审查指令——保存即应用到运行中的 reviewer。配置行仍是默认值;页面编辑作为 override 层持久化,校验用的是比加载 schema 更严的页面上界(后者只限下界);受保护路径设置归配置行所有,行里的值若超出 页面上界会拒绝保存,而不是写出一份下次加载会被整体丢弃的 override 层。Provider、模型与推理等级是由宿主实时模型注册表 驱动的**联动下拉框**(GUI 模型选择器读的是同一来源):选了 provider 就把模型指向它 的第一项,档位列表是所选模型广告出来的档位——「默认」始终可用,注册表未广告但已 配置的值也保持可表示,使表单可以往返;若某个模型广告的档位本插件一个都提供不了, 就只显示「默认」而不再回退静态列表,而注册表里完全没有档位信息的模型仍回退到它。档位归属路由而非独立存在:未选 provider/ model 时其控件不可用并显示「默认」,因为不带模型路由的配置会丢掉它。模型注册表 无法列出的宿主会让表单退回之前的纯文本 provider/model 输入。无论哪种情况,表单都 拒绝会撒谎的保存:半填的路由,「清空路由」(页面无法表达后者,因为不带模型路由的 配置会保留行的路由),以及**在没有路由时启用审查**——宿主对合并后的配置校验这条 不变式,否则页面会报告保存成功,而这份配置什么都不审。数字字段也按同样的上下界在 本地校验,空框或超上界的值会报在它自己的字段上,而不是以宿主字段路径的形式回来。 - **编译后的审查上下文**——审查模型读的不是 Executor 自己的上下文,而是按行投影、 默认以 32K tokens 为界的版本。因此「会话」区块只报两个事实:这份提示词有多大、是否 不得不省略更早的内容。页面不再提供提示词查看器——原先那段分段预览对这个很少翻看的 诊断面来说只是交互噪音——但 `GET /api/advisor/context?sessionId=…` 仍按需返回完整 提示词。 - **状态面**——`/advisor [status]` 报告调用者自己会话的状态:生效的路由、当前 turn、 是否有审查在进行、最近审查过的 turn 以及**它为什么没有产出备注**(沉默、被抑制 为 muted 或已投递、失败或取消)、待投递队列及其严重度和队首挂起原因、等待 followup 确认的备注、followup 预算、失败记账。沉默被报告为结果而非错误,否则 「审查方找不到能指出的东西」和「插件没在跑」这两种情况无法区分。这里只有 status 一个问题;从旧版本带过来的 on/off/model/config 动词会被明确拒绝而不是应答,以免 旧习惯被误读为成功。 ## 投递备注的呈现 审查备注以一条归属 advisor 的聊天通知投递。Executor 运行中投递时,它渲染为已带 一行摘要(`关注 · 仍适用 · …`)的折叠上下文行。Executor 空闲时投递会唤醒一个 turn, 于是聊天画出它的 **turn-trigger 卡片**——而上游这张卡片没有 advisor 分支:它会退化 为通用的 "Execution requested" 标题且不显示摘要,备注在被点开前不可见。 本地客户端补丁 `~/.wweir/bin/dsh-patch-advisor-note`(由 `~/.wweir/bin/dsh-install` 重新应用)给 vendored 的聊天 bundle 补上缺失的分支:卡片显示「二审提醒」/ "Advisor review",折叠行显示记录的严重度与摘要,展开时渲染备注正文本身。它是对 vendored 文 件的修改,因此 DSH 升级会将其丢掉,直到再次运行 `dsh-install`。 ## 验证 ```bash pnpm run verify # typecheck(src + tests)、测试套件、宿主构建 pnpm run build # tsc: src/ -> lib/,被 link 的 profile 加载的就是它 ``` 本包遵循 DSH 插件形态:宿主入口是 `lib/index.js`(从 `src/index.ts` 构建),web 半部是 `client/index.js` 的手写 bundle,`dsh.bundle.patch` 指向 `cordis.patch.yml`。link 的 checkout 如何部署(含免重启热重载与验证标记)见 `docs/OPERATIONS.md`。 测试覆盖纯 policy 层,以及通过 fake port 驱动的运行时(17 个文件);完整链路需要一个 **长驻** app——部署方式与这一约束见 `docs/OPERATIONS.md`。 ## 已知限制 - token 数字是审查子会话聚合 step 的 provider 上报用量。建议记录即被中止的那个 step 可能不会上报用量,因此一次审查的成本可能被略微低估。 - 受保护路径守卫判定的是读工具寻址的路径。以子目录为根的搜索工具仍可能返回该子目录 内受保护文件的内容(例如仓库里的 `.env`);搜索根禁令覆盖工作目录本身,结果脱敏是 第二道防线。 - 页面把它的配置持久化为页面自有的完整投影,因此重启后 tab 显示的内容可复现。该集合 之外的字段(队列容量、受保护路径)仍归配置行所有,无法从页面修改。 - 日志重写/compaction 时的延迟建议失效未实现;审查游标有记录,但尚未用于决策。 - 审查方通过 Executor 自己的 agent-preset 组合读文件,因此远程资产文件工具不在它的 能力面上:经由远程命名空间完成的工作无法通过读文件来核实,这类审查保持沉默,而不 是对一个不同的文件系统下判断。受保护路径守卫经由文件系统工具所用的同一宿主服务 解析路径。 - 宿主接受但没有变成 turn 的 followup(例如 agent 已在关闭中)会让备注保持*等待确认* 的可见状态,而不是谎报已投递;下一次插件加载时它会重新入队。交进运行中 turn 的备 注没有这种证据——交接不返回任何确认——因此在下一个 step 之前关闭的 agent 仍可能 吞掉它。 - 审查子会话是真实会话(审查内容审计轨迹),但 web 会话列表会隐藏以子代理身份创建的 会话,因此审查永远不会以游离行的形式出现在那里。 运行与部署见 `docs/OPERATIONS.md` · 关键决策见 `docs/DECISIONS.md`。