← 返回列表
未验证
一个 DeepSeek Harness dsh…
尚未跑自动兼容性验证,可查看页面内的依赖与入口分析。 · 最近上游提交 2026/9/21 · 已提供中文文档
让长时间运行的智能体回合始终对用户的真实请求负责:一个常驻的维护者文档提醒、两个执行前门禁、一个引用你最新指令的客观锚点,以及一个在阻断之前先纠正的提醒阶段。
综合分
30.4
GitHub 分
30.4
用户评分
—
★ Stars
1
周下载量
—
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add huxin7735-collab/dsh-maintainer-doc-guard该插件未发布到 npm,走 GitHub 源安装(pnpm 若拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑)
信任档位:已验证本站已于 0 天前真实安装成功(L4 · 真实安装)
- 是什么
- dsh 原生插件 · chat
- 装得上吗
- 本站已真实安装成功(L4 · 真实安装,非静态推断)
- 安全吗
- 本站尚未对该插件做风险分级(暂未覆盖,不等同于无风险)
- 还在维护吗
- 活跃:最近一次提交在 4 天前
档位由下列信号合成:本站实装验证(真实安装,当前最高到 L4)· 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。下方各区块是它的证据明细。 验证判据与等级说明 →
🟢实装验证通过· 2026/9/25
由本站实装验证器在真实 dsh 环境安装成功,非静态推断。
数据截至 2026/9/25(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动
README
由 DeepSeek 最新模型翻译生成dsh-maintainer-doc-guard
一个 DeepSeek Harness (dsh) bundle,它将长期项目记忆保留在上下文窗口之外——并让一个长回合始终对发起它的请求负责。
为什么
一个“努力思考”的小参数模型会构建很长的上下文;harness 会压缩它;压缩后的摘要会悄悄丢掉项目的约定、计划、技术栈决策和工作状态——于是模型开始漂移。
经过验证的修复方法(在 DeepSeek V4 Flash 上测得)是把这些记忆外化为维护者文档,并让模型每一回合都读取它们,这样记忆就会从文件重新水合,而不是从有损摘要中回忆:
.dsh-maintainer-doc-guard/
/ # 每个对话一组隔离的文档
plan.md conventions.md stack.md state.md maintainer/README.md
这五个文档位于 .dsh-maintainer-doc-guard/ 下的按会话隔离的子文件夹中(可用 docsDir 旋钮覆盖父目录)。每个对话都有自己的文档集,因此同一工作区中的两个会话永远不会共享或混用同一个文件。插件在首次打开面板时会创建会话文件夹以及每个文档的空文件,因此无需手动设置,读取也是即时的。
这个插件做什么
它有五个部分,外加 0.5.3 中新增的四个提示性通知(见下文提示性通知)。
1. 常驻提醒(一个系统提示词区段)。 它注册一个系统提示词区段(maintainer-doc-guard),其中列出工作区中存在的维护者文档,并指示模型在每次操作或思考轮次之前读取相关文档。
为什么用提示词区段而不是前置步骤消息:前置步骤消息会被持久化到会话日志中(instruction-hint 正是这样对其提示去重的),因此每一步都注入一条会淹没历史记录。区段会在每次提示词组装时——每一回合——重新求值,并且从不累积。这正是这里所需的“常驻的、每回合的指令”。
2. 写入前先例门禁(一个 tools/pre-execute 守卫)。 harness 已经要求模型在覆盖文件之前先读取它(dsh-fs-observation-policy → FS_NOT_OBSERVED)。这覆盖了它要替换的文件,但没有覆盖它必须遵守的契约——因此凭记忆编写一个全新的基础设施文件(插件、预设、技能)正是虚构 API 进入运行中 harness 的方式。
该门禁补上了另一半。一个 write/edit 操作,如果其目标位于受保护的基础设施区域,就会被拒绝,直到该会话至少读取过一个已经可用的同名先例。拒绝信息对模型可见,并列出具体的候选文件供其读取:
Blocked by maintainer-doc-guard: you are about to write "", which lives
in a guarded infrastructure area, but this session has not read any working
"index.js" precedent that shows the real contract.
Read one of these first, then retry:
- /…/node_modules/@deepseek-ai/pkg-alpha/lib/index.js
原因:凭记忆编写的基础设施,正是虚构的 API 进入运行中的测试框架的方式。如果文件已经存在,就读取它;如果你正在创建它,就读取一个可用的同类文件——你复制的那个对应物,正是让新文件与真实 API 保持一致的东西。
它遵循的设计规则:
- 单调。 监听器先向下游委托,然后才应用自己的拒绝,因此它永远无法强行允许另一个守卫所拒绝的内容。
- 有界。 在针对某个目标达到 gate.maxDeniesPerTarget 次拒绝后,它会让调用通过并记录日志——一个固执的模型无法让自己的回合陷入死锁。
- 永不致命。 每条路径都被包裹;内部故障会降级为“无决策”并发出警告。守卫的缺陷绝不能破坏工具分发,正如某个部分的缺陷绝不能破坏提示词组装一样。
- 按会话、在内存中。 观察到的读取以不透明的 agent.session 身份为键,且从不持久化,因此恢复的会话会重新读取其先例——这与 dsh-fs-observation-policy 使用的语义相同。
- 有意粗粒度。 “相同基名”是“此类文件的一个可用对应物”的代理,而非证明。这使误拒很少发生,而对于一个其价值在于迫使进行一次有意识查看的减速带来说,这是正确的取舍。
候选文件是通过向上遍历目录链并在每一层询问同级包是否带有相同的相对路径来找到的——对于 …/node_modules//lib/index.js,第一个命中是 …/node_modules//lib/index.js,这正是一位细心的作者会首先读取的文件。
3. 意图门(第二个 tools/pre-execute 守卫)。 先例门监管的是变更如何编写;它对变更是否值得做只字不提。意图门监管这一点,而且它是更廉价的一半:一个尚未说明其步骤目的的回合,其第一个产生副作用的调用会被拒绝一次。 一次往返换来对目的的明确陈述——而且,当目的无法陈述时,诚实的答案是停下来询问,而不是运行命令去查明。
它的设计规则与先例门的设计规则有一处不同:它是按回合作用域的,而不是按目标作用域的,因为它衡量的是一个回合推理的属性,而不是某个文件的属性。它也是这里唯一一个在传感器缺失时退让的守卫——参见意图门下关于故障开放(fail-open)的说明。
4. 目标锚点(第二个系统提示词部分)。 上述两个门都监管一个步骤是如何采取的。两者都没有注意到目标被替换了——而这正是“它把整个回合浪费在没人要求的事情上”背后的实际机制。一个长回合会漂移,因为模型自己的最后一段成了它的新前提:用户的指令退入被压缩的历史,而一个自我发现的线索取而代之,此后每一步在局部都显得合理。
锚点将用户的最新指令逐字引用保留在系统
prompt,并带有一条明确的优先级规则:
当前目标——用户的最新指令
修改一下估算的,这是任务1,任务2是……
这是当前工作所要对之负责的指令。它引自用户最后一条消息,因此其优先级高于你此后推断出的任何内容。
当这些内容不一致时的优先级:
1. 用户的最新指令,即上文所引
2. 你在自己上一轮中陈述的计划
3. 你自己发现的线索、想法或支线任务
第一项未覆盖的工作属于你自己的额外探索,而非任务本身。在为此再花费一步之前,先用一行说明这一点;若其代价超过这一步,则先询问。
有两个值得点明的设计选择:
- 它是一个独立小节,位于较后的顺序(anchor.order,默认 10150),而不是追加到文档提醒中的文本。 未变化的提示词可以保留前缀(KV)复用,而变化后的提示词会从第一个变化的 token 起失去复用——而这一节恰恰是用户一开口就会变化的那一节。它位于 Web surface(10100)之后、部署 persona 后缀(10200)之前,可将对组装结果的影响限制在最小的尾部范围内。
- 只有用户实际撰写的消息才会成为锚点。 guard 自身的通知和 loop 的运行时上下文快照也是 user/message 事件;source.kind === 'user' 是 loop 自身使用的判别条件,因此锚点绝不会最终把 guard 的话引用给它自己。
在委派的子代理内部
锚点在子代理中默认关闭,原因不是性能——而是正确性。被委派的子代理(dsh 的 subagent 工具)是由委派消息启动的,而不是由人类输入的内容启动的,因此父级的“最新用户指令”不是子代理的任务。在那里渲染它,会颠倒锚点本应执行的优先级规则,并诱使子代理去追逐原始请求,而不是它被交付的任务。
当你确实想要它时,设置 anchor.inSubagents: true,该小节就会为该读者切换身份:
你的委派任务——你被派到这里要做的事
把解析器改成允许尾逗号,周五前。
这是委派给你的任务。它引自启动你会话的那条消息。其优先级高于你此后推断出的任何内容,并且不会因为有趣而扩大范围。
当这些内容不一致时的优先级:
1. 委派任务,即上文所引
2. 你在自己上一轮中陈述的计划
3. 你自己发现的线索、想法或支线任务
有两项机制使这一点得以成立:
- 委派是按谱系、按位置采纳的。 子代理的起始消息通过普通的用户消息路径传递,因此 source.kind 无法将其与人类的消息区分开来。取而代之的是,子代理会采纳它看到的第一条非空 user-role 消息并保留它;之后真正的发送不会悄悄替换交给它的任务。在顶层则没有任何变化——最新的用户消息仍然胜出,插件/工具通知仍然被忽略。
- 每个贡献一个开关。 文档提醒和锚点是可分别开关的(inSubagents / anchor.inSubagents),因为它们在这里想要相反的答案:文档通过父级的 cwd 继承,值得阅读;父级的目标不是子级的任务。true 会让某个贡献进入子级和孙级;默认值、false 或任何无法识别的值都会将其保留在顶层,因此拼写错误会静默失败。
这两个开关都仅用于组合——参见 cordis.patch.yml,而不是设置卡片,后者恰好包含十个字段。
锚点会渲染为空字符串——因此会消失,不消耗任何 token——直到会话看到过一条用户消息。由于循环在认领已接受的用户批次的步骤中追加该批次,下一次组装已经携带了新的目标;该轮自身的第一个步骤是用户消息仍然是请求中最后内容的唯一位置,因此此时还没有任何需要锚点消歧的内容。
5. 一个提醒阶段(先纠正,再阻止)。 只有在整整一轮已经消耗之后才触发的拦截是一张收据,而不是一次纠正。因此意图判断是分阶段的,并且先进行成本较低的阶段:
| # | 会发生什么 | 成本 |
|---|---|---|
| 第 1 次无法解释的操作 | 注入一条提醒(agent.inject),并且该调用运行 | 0 次往返 |
| 第 2 次 | 拒绝,并要求说明该步骤的目的是什么 | 1 次往返 |
| 第 3 次 | 再次拒绝,直到 intent.maxBlocksPerTurn 用完 | 每次拒绝 1 次 |
提醒会在模型的下一个步骤边界到达模型,因此纠正会落在需要它的那一轮内,而不是下一轮。存在两种提醒:
- 步骤目的提醒,代替第一次拒绝被注入;
- 漂移检查,当一轮已经运行了 nudge.afterSteps 个步骤而用户没有再次发言时注入一次——这是唯一不需要对模型正在做什么进行语义判断的漂移信号,因为步骤数是一个事实,而这个事实正是“把一轮消耗在支线任务上”从外部看起来的样子。即使某一轮完美地叙述了自己,它也会触发,因为叙述良好的一轮也可能漂移得一样远。
该通道特意使用 agent.inject,而不是执行后的 additionalContexts 字段:additionalContexts 只存在于 POST 分发决策上,而 PRE 分发的 PreToolDecision 是 allow/deny/ask,不能携带上下文——因此必须在某个操作之前到达的提醒必须通过 agent 传递。steer 是同一对中的唤醒部分,但未被使用,因为守卫没有理由在空闲驱动器上启动一轮。投递失败会降级为普通允许,并计入 nudgeFailed;分阶段路径绝不会意外变成阻塞路径。
设置 nudge.grace: 0 可恢复 0.5 之前的行为(拒绝第一次违规),设置 nudge.enabled: false 可完全移除该阶段,或设置 nudge.afterSteps: 0 以
保留提示,去掉漂移检查。
提示性通知(0.5.3)
在五个部分之上新增了四个通知。四个通知默认全部开启,全部只观察、从不阻止,并且都可以从右侧边栏标签页切换(进程级,与严格模式开关一样)——它们刻意不是设置卡片的新字段,因为那张卡片有十个字段的硬上限。
按需账本命中。 lessons.md 与操作者的文档并列列出,但不会被强制注入:只有当某一行的 触发 字段与实时调用匹配时,该行才会被投递——同一工具,外加本次调用中至少存在一个路径/命令片段(子串匹配; 将一个片段拆分为若干 AND 连接的部分)。每行每会话最多命中一次。跨对话复用来自工作区级的 /.dsh-maintainer-doc-guard/lessons.md,而不是每一步都重新陈述。
被删除数字的落地检查。 对于 edit,old_string 与 new_string 之间数字段落的净减少,是所观察到故障的客观痕迹——删除了承载某个数字或结论的句子。该通知要求在删除成立之前进行落地位置检查。
完成标准,每个产物一次。 在成功写入代码产物(.mjs/.cjs/.js/.jsx/.ts/.tsx/.ps1/.sh/.py/.json/.yml)之后,下一次调用会被提醒一次,要求说明它是如何被验证的以及实际读数——而不是将其标记为完成。
自动归纳。 对受管工具的一次失败调用,随后对同一目标的一次成功调用,会向账本追加一行:以失败自身的错误文本作为原因,以及哪些参数键发生了变化作为修复。只记录观察到的事实;shell 调用被排除(命令字符串没有稳定的“同一目标”);每会话上限三行;按整行去重。
子代理:通知是不对称的
账本命中和自动归纳在委托的子会话中默认关闭:isChildSession(session) && !subagentOptIn(config.inSubagents) 返回“无通知 / 无记录”,与文档提醒采用相同的策略(通过 inSubagents 让子会话选择加入)。数字落地检查和完成提醒完全没有子会话检查,因此它们确实会在子会话中运行——而且由于这四个通知共享一个进程级开关组,父会话无法独立地让其子会话安静下来。
成本与权衡(0.5.3)
它带来了什么。 模型不再丢失线索:它保持切题,并且不再用一条毫无进展的宏大绕路来“解决”一个长任务。在实践中,这是一种明显比 4.0 更舒适的长程体验。
它的代价——诚实的清单。
1. 它会触及上下文,因此提示缓存命中率大约在 70-80%(操作者的测量)。 强制注入的文档和追加的锚点每一轮都会重写提示的一部分,因此前缀的其余部分必须重新计算。这就是“保持切题”的主要账单。
2. 重新加载后通知会重复出现。 热重载或重启插件会
清除按会话记录的簿记信息(已读集合、通知版本、归纳计数),因此
“你尚未阅读这些文档”可能会在你刚读完后再次触发。
3. 子代理行为是不对称的(见上文小节)。
4. 片段匹配基于子字符串。 写成
edit + index.js 的触发器会为任何包含 index.js 的路径触发,包括对
无关文件的编辑。
5. 自动归纳在构造上就是不完整的。 只有“同一目标:先失败,然后
成功”,排除 shell,每个会话最多三行——而且当重试没有改变任何参数键时,
记录的那一行说明不了什么。
6. 通知也会消耗上下文,每轮共享一份预算,而且第二条
文档提醒会嵌入文档本身。
7. 设置卡片最多只能有十个字段,因此较新的开关放在
侧边栏中:进程级、重启后丢失,只能通过
cordis.patch.yml 持久化。
8. 通知只是建议,不是保证。 只有先例门控和意图门控
会真正拒绝,而且两者在有限次数的尝试后都会放行调用。
配置
行配置(全部可选):
| 键 | 默认值 | 含义 |
|---|---|---|
| enabled | true | 在不移除该行的情况下关闭该节 |
| docs | ["plan.md","conventions.md","stack.md","state.md","maintainer/README.md"] | 要探测的文档名称 |
| docsDir | ".dsh-maintainer-doc-guard" | 存放文档的文件夹(相对于每个工作区);首次访问时创建 |
| onlyWhenPresent | false | 当为 true 时,除非至少存在一个文档,否则保持静默 |
| inSubagents | false | 也将文档提醒注入委托的子代理(true 表示选择启用) |
| order | 100 | 节排序顺序(在位于 0 的人格前缀之后) |
| walkUp | 6 | 向上查找项目根目录时遍历的祖先层级数 |
| projectMarkers | [".git"] | 用于向上查找的根标记 |
| title / intro | 内置 | 覆盖提醒的标题 / 正文文本 |
门控配置位于 gate: 下,并且也全部可选:
| 键 | 默认值 | 含义 |
|---|---|---|
| gate.enabled | true | 是否挂载 tools/pre-execute 防护 |
| gate.dryRun | false | 仅观察:统计它将会拒绝的内容,从不阻止 |
| gate.tools | ["write","edit"] | 门控检查的工具名称 |
| gate.guardedGlobs | [".dsh/profiles/", ".dsh/skills/", ".dsh/.agent-presets/", "node_modules/@deepseek-ai/"] | 解析后目标路径中使其受防护的 POSIX 子字符串 |
| gate.exemptGlobs | [] | 即使目标受防护也将其豁免的 POSIX 子字符串 |
| gate.maxDeniesPerTarget | 2 | 对同一目标拒绝多少次后门控放弃并放行 |
| gate.maxCandidates | 3 | 一次拒绝中列出的先例数量 |
| gate.scanLevels | 4 | 为查找同级先例而扫描的祖先层级数 |
| gate.entriesPerLevel | 400 | 每层检查的目录条目数(限制 node_modules 扫描) |
示例——先观察再强制执行:
yaml
- id: maintainer-doc-guard
config:
gate:
dryRun: true
该门禁对 guardedGlobs 之外的任何内容均关闭,因此普通项目文件
(plan.md、源代码、笔记、草稿脚本)永远不会被它触及。
每份文档都会先在会话工作目录中探测,然后在项目
根目录中探测,因此即使从嵌套的当前工作目录出发,也能找到保存在仓库根目录的文档。
此插件从自身之外复制的所有内容——引用的用户
指令、文档名称、title / intro 覆盖项——在到达提示词之前,都会针对
运行框架的 {{variable}} 插值进行拆解(两字符的开头和结尾标记会被拆开),
因为渲染在遇到未注册的引用时会抛出异常。因此,用户输入 {{x}} 不会破坏
会话其余部分的提示词组装。
锚点配置位于 anchor: 下,且同样全部可选:
| 键 | 默认值 | 含义 |
|---|---|---|
| anchor.enabled | true | 是否渲染常驻目标部分 |
| anchor.inSubagents | false | 是否也在委派的子代理内渲染,以子代理特定措辞引用子代理自身的委派(true 表示启用) |
| anchor.order | 10150 | 部分排序顺序——故意靠后,这样每次用户消息都会变化的部分会使尽可能少的前缀失效 |
| anchor.maxChars | 800 | 截断引用的指令,并用 … (truncated) 标记截断处 |
| anchor.title / anchor.intro | 内置 | 覆盖标题 / 框架句 |
| anchor.priorities | 三步规则 | 优先级列表,在引用之后以编号列表形式渲染 |
| anchor.childTitle / anchor.childIntro / anchor.childPriorities | 内置 | 为子代理阅读覆盖全部三项——每一项默认措辞都表示“你的委派任务”而非“用户的最新指令” |
提示配置位于 nudge: 下,且同样全部可选:
| 键 | 默认值 | 含义 |
|---|---|---|
| nudge.enabled | true | 是否启用意图判断;false 会像 0.4 那样拒绝首次违规 |
| nudge.dryRun | false | 仅观察:统计它会注入的内容,但不注入任何内容 |
| nudge.grace | 1 | 在任何拒绝之前,允许通过并附带提醒的未解释操作数 |
| nudge.maxPerTurn | 3 | 每轮在停止注入前的提醒次数(拒绝预算单独计算) |
| nudge.afterSteps | 12 | 触发每轮一次漂移检查的步数;0 表示禁用 |
设置页面——十个实时旋钮
两个门禁最常调整的开关也可以在运行时编辑,位于
运行框架自身的设置 UI 中,路径为 设置 → 插件 → 插件配置(“Plugin
configuration”),无需重启:
| 设置字段 | 映射到 | 默认值 |
|---|---|---|
| gateEnabled | gate.enabled | true |
| gateDryRun | gate.dryRun | false |
| gateMaxDeniesPerTarget | gate.maxDeniesPerTarget | 2 |
| intentEnabled | intent.enabled | true |
| intentDryRun | intent.dryRun | false |
| intentMaxBlocksPerTurn | intent.maxBlocksPerTurn | 1 |
| intentMinChars | intent.minChars | 24 |
| anchorEnabled | anchor.enabled | true |
| nudgeGrace | nudge.grace | 1 |
| nudgeAfterSteps | nudge.afterSteps | 12 |
该卡片按守卫顺序列出了这十个字段——先例门、意图门、目标锚点,然后是漂移纠正——每个字段都有自己的标签和提示,因此某个开关属于哪个守卫永远不会含糊不清。
它的工作方式,以及它刻意不做的事:
- 宿主侧通过 ctx.settings.installSection(...) 注册一个设置命名空间(maintainer-doc-guard)——由 ctx.inject(['settings'], …) 保护,因此没有设置提供者的部署会继续使用组合配置。这里没有任何东西是门正常工作所必需的。
- 值会持久化到 harness 设置文档($DSH_HOME/settings.yaml),该文档会被热重载;如果提供者分离,installSection 会回退到组合条目,因此覆盖层永远不会过期。
- 门在每次工具调用时读取其配置,而不是在挂载时——这正是翻转开关能立即生效的原因。仅在 gate.enabled 为 true 时挂载监听器会把开关冻结在加载时。锚点出于同样的原因,在每次组装时重新读取其配置。
- 只暴露这十个旋钮。gate.tools、gate.guardedGlobs、gate.exemptGlobs、gate.scanLevels、intent.tools、anchor.order、anchor.maxChars、anchor.priorities、inSubagents、anchor.inSubagents 以及类似项是部署决策,而不是偏好设置,它们留在组合条目中——关于提醒本身的一切也是如此。尤其是 anchor.order 在注册时固定:更改它意味着编辑组合条目并重启。十个字段的上限是硬性的:提供者接受一个共享命名空间卡片,最多十个旋钮,因此第十一个会导致整个卡片挂载失败。
- 卡片由命名空间键分派,因此只有当宿主实际提供该命名空间后,该部分才会渲染它。当没有挂载提供者时,卡片会说明这一点并保持只读,而不是假装写入已生效。
gate.dryRun、intent.dryRun 和 nudge.dryRun 都是忠实的模拟:一次本应发生的拒绝仍会消耗其预算中的一个名额,因此一天的观察能准确预测强制执行会做什么(包括它会在何时放弃)。
intentMinChars 是在判定时读取的,而不是在记录时。一个回合会记录它已累积了多少字符的解释,而阈值会在工具调用即将分派时应用——因此拖动滑块会重新调整当前回合,而不仅仅是下一个回合。
意图门
第二道门回答的问题与前例门不同。前例门问的是“你在这里写之前读过前例了吗?”;这道门问的是“你在运行这一步之前说明它的目的了吗?”它之所以存在,是因为代价高昂的失败模式不是糟糕的写入——而是在检查该行动是否值得采取之前就行动,这会把 token 烧在没人要求的工作上。
它如何判定:
- 宿主订阅 session/event(一个非否决的观察者接缝),并按轮次记录该轮已产生多少助手散文,以及已采取多少步骤。
- 在 tools/pre-execute——唯一能在派发前阻止调用的接缝——对 intent.tools 之一的调用会遇到上文 5. 一个提醒阶段 中描述的分阶段判定:先提醒,只有在提醒被忽视后才拒绝,且拒绝次数绝不超过 intent.maxBlocksPerTurn。
yaml
- id: maintainer-doc-guard
config:
intent:
enabled: true
dryRun: false
tools: ["write", "edit", "bash", "pwsh"]
maxBlocksPerTurn: 1
minChars: 24
| 键 | 默认值 | 含义 |
|---|---|---|
| intent.enabled | true | 是否挂载意图门 |
| intent.dryRun | false | 仅观察:统计它会拒绝什么,但绝不阻止 |
| intent.tools | ["write","edit","bash","pwsh"] | 仅限有副作用的工具——对廉价探索设门毫无收益 |
| intent.maxBlocksPerTurn | 1 | 每轮在放弃并让该轮运行之前的拒绝次数 |
| intent.minChars | 24 | 被视为“说明了这一步的目的”的助手散文字符数 |
拒绝是一道提示,不是一堵墙。它对下一次尝试提出三项要求:这一步达成什么、为什么必须现在发生,以及预期结果是什么——并明确说明,如果无法陈述这一步的价值,就不应运行这一步,而应询问用户他们实际想要什么。它注入的同类提示在调用仍被允许通过时表达同样的意思,因此接受提醒的模型根本不会撞上那堵墙。
有三项安全属性值得点名:
- 通道缺失时故障开放。 如果某次工具调用已经在派发,而插件本轮观察到零条助手消息,那么观察通道没有到达它(事件形状改变、harness 重新排序)。它会在该轮自行解除武装并计入 intentDisarmed——在卡片中显示为 ⚠未观测到通道(已自停 N)——而不是盲目拒绝。一个在传感器损坏时进行阻止的门比没有门更糟。
- 独立预算。 意图门最先运行,但下游的拒绝(前例门,或 harness 自身的策略)不会消耗意图预算。因此一轮可以吸收一次意图拒绝和一次前例拒绝,而不会让第二次被吞掉。提醒预算是又一个独立的计数器。
- 有界。 intent.maxBlocksPerTurn(默认 1)正是让一个固执的
模型无法死锁自己的回合:一旦额度耗尽,该回合无论如何都会运行,放弃会被计为 intentGaveUp。调高它会以更长的停滞换取更坚定的要求——3 是一个合理的“坚持”设置;按设计,没有任何设置能无限期地阻止一个回合。
对于 intent.tools 之外的任何工具,这道闸门都是关闭的,因此 read、glob、grep 以及所有只读探测永远不会被它触及。
UI——右侧边栏标签页
该 bundle 还附带一个浏览器端部分(./client.js),它在右侧边栏中添加一个维护者文档标签页:
- 一条横跨所配置 docs 的标签条(默认:上述五个);
- 每个文档一个编辑器——在文本区域中编辑原始 markdown,点击即可保存;
- 尚不存在的文档会标记为 ·未建,因此该面板也可兼作仍需撰写内容的清单;
- 头部显示面板所解析出的工作目录——即打开会话的 cwd,若宿主无法提供,则回退到上次提示词组装时记录的 cwd。
它是手写的且无需构建,并且只依赖平台种子模块 react——这是有意为之,这样它就不会触发 require(...) missed the module table 失败,而 rc. 时代的客户端 bundle 在 0.1.5 上曾遇到该失败。
数据平面(宿主路由,仅在宿主拥有 Web 服务器时注册):
| 方法 | 路径 | 载荷 |
|---|---|---|
| GET | /dsh-maintainer-doc-guard/docs | 查询参数 cwd=(可选)。还会返回一个 gate 对象——见下文——这样你可以确认这些守卫确实在触发。 |
| PUT | /dsh-maintainer-doc-guard/docs | 请求体 {cwd, name, content} |
| GET | /dsh-maintainer-doc-guard/settings | 设置卡片的读取模型:served、namespace、value、overridden(用户层中每个字段是否存在)、revision、fields,以及相同的 gate 计数器 |
| PUT | /dsh-maintainer-doc-guard/settings | 编辑时请求体为 {patch:{…}},将某个字段恢复为组合值时请求体为 {unset:["gateDryRun", …]}。未知字段和错误类型会被丢弃;没有提供者时,它会以 YAML 回退内容应答 409,而不是假装成功 |
gate 对象携带每个守卫的计数器:
| 计数器 | 含义 |
|---|---|
| seen | 闸门流水线检查过的工具调用 |
| denied | 已发出的先例拒绝 |
| gaveUp | 先例闸门对某个目标耗尽了 maxDeniesPerTarget |
| wouldDeny | 仅 gate.dryRun:本会发生的拒绝 |
| reads | 被记录为先例的读取 |
| lastDenied | 最近一次先例拒绝的 {tool, target} |
| intentBlocked | 已发出的意图拒绝 |
| intentWouldBlock | 仅 intent.dryRun:本会发生的拒绝 |
| intentGaveUp | 意图闸门对某个回合耗尽了 maxBlocksPerTurn |
| intentDisarmed | 观察通道缺失而它选择退让的回合 |
| lastIntentBlocked | 最近一次意图拒绝的 {tool, turn, step} |
| nudged | 实际传递给模型的提醒 |
| nudgeWouldSend | 仅 nudge.dryRun:本应被送达的提醒 |
| nudgeCapped | 轮次触达 nudge.maxPerTurn,因此有一条提醒被扣留 |
| nudgeFailed | 送达失败(没有 agent.inject)——调用通过了,未被纠正 |
| lastNudge | 最近一条提醒的 {tool, tag};tag 为 intent 或 drift |
调优时真正重要的三个计数器是 nudged(廉价阶段是否在起作用?)、intentBlocked(昂贵阶段是否仍然需要?),以及
nudgeFailed (is the reminder channel actually wired in this deployment — a
non-zero value there means the staged path has silently degraded to "allow").
name must be one of the configured docs; writes are confined to cwd
(path traversal is rejected) and parent directories are created on demand.
Open the tab from the right sidebar's "new tab" menu. Restart dsh after changing
this plugin — bundles are not hot-loaded.
Install
Requires dsh 0.1.5-rc. and Node >= 20. The plugin is build-free: lib/ is
the shipped source, there is no compile step, and it depends on nothing but the
harness itself.
Quick install (recommended)
dsh plugin --profile web add github:huxin7735-collab/dsh-maintainer-doc-guard
Then restart dsh (dsh web) and refresh the page. This is the command the plugin
marketplaces expect in a README; substitute your own profile name for web if
you run a different one.
The marketplace's one-click install uses this same spec. Nothing else needs to
be written by hand.
Manual install (if you prefer to vendor the source)
1. Get the code anywhere on disk.
git clone https://github.com/huxin7735-collab/dsh-maintainer-doc-guard.git
2. Declare it in the profile — $DSH_HOME/profiles/web/package.json:
jsonc
{
"dependencies": {
"dsh-maintainer-doc-guard": "link:/abs/path/to/dsh-maintainer-doc-guard"
},
"dsh": {
"profile": {
"bundles": [
// …the bundles already listed…
"dsh-maintainer-doc-guard"
]
}
}
}
Both halves matter: dependencies makes it resolvable, and dsh.profile.bundles
is what actually loads it. A package that resolves but is not in the bundle list
mounts nothing.
Use link: with an absolute path, never file:. With nodeLinker: hoisted,
pnpm resolves a file: directory to a symlink into* the source tree instead of a
self-contained package — that either breaks resolution or silently drops the
bundle from the graph.
3. Install and restart.
cd "$DSH_HOME/profiles/web" && pnpm install
Then restart dsh: bundles are not hot-loaded. A window close-reopen is often
not a restart either — if the shell log shows adopted orphan service, it
reused the old process and you are still on the old config. Kill the service PID
and start again.
4. Verify. dsh web --dump-config should list the maintainer-doc-guard row
exactly once, and 设置 → 插件 → 插件配置 should show the card with ten
knobs. If the card renders read-only and says no provider is mounted, the harness
has no settings service — every knob then falls back to this composition entry
and the plugin still works, just without live tuning.
To confirm the anchor half is live without waiting for a drift to happen: send a
message, then read GET /dsh-maintainer-doc-guard/settings and check that
gate.nudged moves when you next take an unexplained action. gate.nudgeFailed
staying at 0 is what tells you the reminder channel is genuinely wired in.
Upgrading
git pull into the same directory and restart dsh. Nothing is written into your
profile beyond those two declarations, so there is no migration step.
Uninstalling
Drop the dsh.profile.bundles entry (that is what unmounts it), then the
dependencies line, then pnpm install. The documents it manages —
plan.md, conventions.md, stack.md, state.md, maintainer/README.md and
lessons.md (the ledger the model itself appends to) — are ordinary workspace files and are left untouched.
Note on this repository
This plugin was developed against a specific deployment's cordis.patch.yml, so
the composition example above is the generic form. The guarded areas, the
document names and the tuning all live in that one row's config: block, which
ships in cordis.patch.yml here — read it before wiring the plugin into a
harness that guards different directories.
Release notes
See CHANGELOG.md for what changed in each version, including the
design notes behind the objective anchor and the nudge stage, the reason a
hard-stop-the-turn escalation was deliberately not implemented, and what remains
unverified.