# AGENTS.md — 给 AI 协作者的项目手册 > 这是 `dsh-plugin-debug-mode` 的项目手册。动手前请先读本文件——**这是本插件唯一的 > 权威入口;契约以代码与本文为准**。判据、阈值、被推翻过的机制都写在本文里, > 不留任何"另一份设计稿里才有答案"的空位。 --- ## 1. 这个项目是什么 一个 DSH(DeepSeek Harness)插件,灵感来自 **Cursor Agent 的 Debug 模式**。 一句话:**让 Agent 别猜,自己在代码里埋探针,把现场取回来;用户复现后二选一。** 完整流程: ``` 用户:现象描述 └─ Agent 分析代码 → 埋探针(带可摘除标记) ← 不运行任何命令(§1.1 第 14 条) └─ 探针把记录交给宿主,落进 <用户临时目录>/dsh-debug-mode/tasks//.log (**首选**:POST 宿主的 HTTP 入口——语言无关,写日志只有宿主一份实现; 只在热路径 / 需要"崩溃前落盘" / 没有网络栈时才用文件桩; 临时根不可写时回退 /.dsh/debug-mode/,回退必须出声——见 §1.1 第 15 条) └─ Agent 通过「读取日志信号」拿到摘要(不自己去翻文件) └─ 用户复现 → 弹出面板: [ 问题已解决 ] → 删日志 + 删掉本次任务的全部探针代码 [ 问题复现,需要进一步修复 ] → 日志**保留**,Agent 读完再开下一轮 ``` 包名:`dsh-plugin-debug-mode`(npm 与 DSH bundle 同名,不带 scope)。 对外标识:`/debug` 命令、`dsh_debug_*` 工具、`dsh-debug:probe ` 探针块标记。 用户侧的进入方式**有且只有一个**:`/debug <现象描述>`。 ### 1.1 核心不变量(**改代码前必须确认不破坏这几条**) 1. **入口唯一:只有 `/debug` 命令能进入调试模式。** 不做对话意图识别、不做按钮。 `dsh_debug_start` **必须持票**(`/debug` 签发的会话级一次性票据)才放行; 无票调用一律拒绝。这是**宿主侧硬校验**,不是提示词约定——工具对所有会话可见, 模型永远能调用它,只有返回值拦得住。 2. **一个任务一个哈希。** 首次进入调试模式时生成,之后无论多少轮都不变。 哈希用 CSPRNG 生成 8 位小写十六进制,且**只由宿主生成**,从不接受外部传入。 3. **清空日志只有一个时机:`dsh_debug_continue`。** (truncate 为 0 字节,不删文件、 不换目录。)**用户点「问题复现」不清空任何东西**——那一刻 Agent 还没读, 清空就是把用户刚复现出来的现场当场扔掉。清空发生在「Agent 已读过现场、正要开始 新一轮埋点」= **下一次用户复现之前**;`continue` 还有**闸门 C**: 日志里还有 `> task.lastRead.bytes` 的未读内容时拒绝清空(三振降级)。 4. **用户信号不直接删日志。** 「已解决」只唤醒 Agent;删日志与删探针代码由 `dsh_debug_end` 一并完成——两者必须同生共死。 5. **探针代码必须可机器摘除。** 每处探针都被 `dsh-debug:probe start/end` 夹住;`dsh_debug_end` 要能凭标记把代码库恢复干净,并给出对账清单。 6. **Agent 不直接读日志文件。** 日志内容一律经 `dsh_debug_read` 输出; 摘要优先、异常全量、省略量显式声明。 7. **调试准则只在调试模式下注入。** `systemPrompt.section` 的 `text` 必须是 thunk, 无活跃任务时返回空串;否则没进调试模式的会话也会收到「去埋点」的指令, 等于把入口约束从提示词层面又打开一次。 8. **协议冻结。** 日志行格式与字段序的唯一真相是 `lib/protocol.js` (`formatRecord` / `parseRecord` / `parseLog`);只能新增**可选**字段, 且必须同步 `assets/probe.template.mjs` 与测试。冻结项要改就得走 §4.5 的三步、 并按 §4.6 升版本号——"悄悄改一个字段序"是这份文件里最贵的一类事故。 9. **输入框始终可用。** 调试模式下用户随时可以用原生输入框描述现象——按钮只是 快捷方式,不是唯一出口。因此:① 面板**不得**抢走或挡住输入框; ② 输入框有未发送内容时**禁用**面板按钮(否则同一条描述会被送两遍); ③ 宿主观察到 `awaiting-user` 状态下出现新的用户消息时,自动迁移到 `active`。 10. **探针账与修复账分开记,收尾只撤探针。** 探针用 `dsh-debug:probe ` 包裹、 代码改动用 `dsh-debug:change ` 标记;`probeFiles` 与 `changedFiles` 分别登记。 收尾**只删探针、绝不回滚用户的修复**;用 `git checkout` / `git stash` 整体回滚是 **明令禁止**的收尾方式(会连带 revert 掉用户要的修复)。 11. **Agent 可以不进调试。** `/debug` 是用户发起的,不是契约:一眼能看出根因时, Agent 应当**不调用** `dsh_debug_start`,说明根因并直接改代码(票据自己会过期, 无需任何清理动作)。 12. **闸门不能造成死锁,也不能 fail-open 而不告知。** 任何硬闸门**连续拒绝同一原因 3 次** 后必须降级为 warn-only 并把降级写进欠账;闸门依赖的扫描能力不可用时(只读沙箱、 路径在仓库外等),必须在面板显示「残留检测未生效」,**绝不假装保护着**。 13. **残留处理由「可判定性」决定,不由风险偏好决定。** `probe`/`temp` 必删、 `fix` 保留、**hash 不属于任何已知任务或格式残缺的一律进隔离区**(逐条让用户三选一), 完全无标记的只提示不删。**不允许静默保留**——漏删的临时改动没有任何下游拦截, 它不报错、不告警,只是慢慢烂掉,那正是「屎山」的定义。为什么不能反过来把 未标记的改动默认当「要保留」,见 §7。 14. **Agent 在调试流程里不运行任何命令。** 它的全部动作只有两种:给调试服务发信号 (`dsh_debug_*` 工具)+ 用编辑工具在源代码里埋点/改代码。**复现是被调试程序的 使用者的事。** 这条是实测事故驱动的:某次调试里 `D:\` 归属 `SYSTEM`,当前账号缺 WRITE_DAC,于是 **所有 shell 命令**在授权阶段就被拒(`SetNamedSecurityInfoW` `Win32 5`),而同一时刻 `edit`/`write` 完全正常——**能挡住整个流程的,恰恰是 流程本来就不需要的那一步**。连带约束:任何**必需**的通道都不许依赖命令 (页面类目标走 HTTP 摄入端点,而不是"起个本地服务器")。 15. **产物不进工作区。** 调试产物落**用户临时目录**:`join(tempBase, 'dsh-debug-mode')`, `tempBase` 依次取工具参数 `tempDir` → 环境变量 `DSH_DEBUG_MODE_TMP` → `os.tmpdir()`。 临时根不可写时**回退**到 `/.dsh/debug-mode/`,且回退必须**出声**—— 同一句说明要同时出现在任务的 `notes`、待办清单 `debts` 与 `dsh_debug_start` 的返回正文里; 两个根都不可写时 `start` 直接拒绝。任务里用 `artifactRoot`(绝对路径)与 `artifactRootKind`(`'temp'` / `'workspace'`)记住这次落在哪儿,缺省按 `workspace` 解释 = 旧版本留下的任务。理由:产物一旦落进工作区,它就会跟着 `git status`、 被编辑器索引、被残留扫描器当成项目文件;而一次静默回退只会让下一个人看到"日志不见了"。 --- ## 2. 当前状态 **M0 – M6 + 防堆屎山机制 R1–R6 + HTTP 统一埋点入口全部落地,面板已重做为「输入框上方的可折叠卡片」,`/debug` 会直接开一轮、注入段自带本任务地址: 全部测试全绿(`node --test test/*.test.mjs`)。** 剩下的一件事是**人在真实 GUI 里点一次面板**(M4 的验收判据)—— 代码路径已被产物测试覆盖,但那一步只能由人做。 已落地: | 文件 | 内容 | | --- | --- | | [CHANGELOG.md](./CHANGELOG.md) | 只追加的变更记录(**版本号纪律见 §4.6**):最新版本 v0.20,行序新→旧、版本号连续无缺口 | | [package.json](./package.json) | 包清单:`dsh.bundle.patch` + `dsh.client` 双挂载声明 | | [cordis.patch.yml](./cordis.patch.yml) | bundle 挂载声明(插入 `debug-mode` 行) | | [lib/protocol.js](./lib/protocol.js) | 日志行协议的唯一真相:`formatRecord` / `parseRecord` / `parseLog` | | [lib/task.js](./lib/task.js) | 任务哈希、产物根与相对路径形状、状态机与迁移白名单 | | [lib/probe-template.js](./lib/probe-template.js) | 由模板生成三种产物(Node 桩 / 浏览器桩 / 多语言 HTTP 片段),锚点校验,缺锚点即抛错 | | [lib/debug-service.js](./lib/debug-service.js) | **DebugService**:票据闸门、任务表、日志管道、**产物根解析与回退(临时目录优先)**、**HTTP 摄入与通道自检、浏览器桩由宿主路由伺服、端点来源**、唤醒通道、收尾对账、闸门降级、欠账看板 | | [lib/scan.js](./lib/scan.js) | **防堆屎山的两台机器**:R1 工作区索引 + 窗口 diff、R2/R3 残留扫描与栈式配对、语法自检;桩引用扫描认**两形态**(临时根的 `dsh-debug-mode/tasks//` 与回退根的 `.dsh/debug-mode/tasks//`) | | [lib/summary.js](./lib/summary.js) | `dsh_debug_read` 的摘要渲染(异常全量、计数先行、省略显式) | | [lib/prompt.js](./lib/prompt.js) | 埋点指南渲染 + 调试准则正文(含"不运行任何命令")+ `/debug` 指令正文 | | [lib/command.js](./lib/command.js) | `/debug` 命令(签票 + **开一轮对话**把指令交给 Agent + 一句给人看的结果说明) | | [lib/tools.js](./lib/tools.js) | 5 个 `dsh_debug_*` 工具定义与 agent 作用域注册 | | [lib/routes.js](./lib/routes.js) | 面板的 `state` / `board` / `probe` / `resolve` / `reproduce` 同源路由 + 浏览器桩的 `GET /dsh-debug-mode/stub.js?h=`(即时渲染 `runtime/probe.browser.js`,`Content-Type: application/javascript`) | | [lib/index.js](./lib/index.js) | 宿主半入口:装配命令、工具、提示词段落、路由与会话观察 | | [assets/probe.template.mjs](./assets/probe.template.mjs) | 零依赖 Node 探针桩模板(生成到任务产物根的 `runtime/probe.mjs`;烘死 TASK/LOG;16 MiB 上限) | | [assets/probe.browser.js](./assets/probe.browser.js) | 零依赖**浏览器**探针桩模板(经典脚本,`dshProbe` → HTTP 摄入;批量 + 卸载冲刷);页面用 `