### v1.1.3 · 更新日志改成「简短三句」 用户要求:「更新日志的信息改为**简短三句交代更新了什么**就行」。 ### 起因:v1.1.2 的面板摘要里没有一句说「改了什么」 摘要从 v1.0.1 起取的是**Release 正文的前 3 行**。到 v1.1.2 上就露馅了—— 那一版正文的头三行是用户原话与「先量,再改」的铺垫,于是面板上显示的是: ``` 用户原话:「他现在还是一直跟个复读机一样说相似的话,很烦」, 并在追问里点名 「两个都烦」(插件注入的行 + 模型自己的播报)。 我没有凭感觉调,而是把会话日志解出来数了一遍。这一步本身有个坑: ``` 三句里没有一句交代这版改了什么。**位置(前 3 行)跟内容(改了什么)不是一回事**, 所以这一版把摘要改成显式区块,而不是继续靠「正文开头正好是摘要」这个巧合。 ### 改法 | 改哪 | 改成什么 | 为什么 | | --- | --- | --- | | `lib/updater.js` | 新增 `summarySectionOf`:只取正文里 `## 摘要` 区块 | 区块**之前**的铺垫与**之后**的验收判据都不进摘要 | | `lib/updater.js` | `releaseListOf` 加 `summaryMissing` 字段 | 「没写摘要」与「摘要为空」在面板上是两句话,得能区分 | | `lib/client.js` | 没写摘要 → 显示「(这版没写摘要)」 | 不再含糊成「这一版没有单独的说明」(那会让人以为这版真没改什么) | | `tools/make-release-notes.mjs` | 正文只留「标题 + `## 摘要` + 附件校验」 | 用户裁定;详细过程留在本文件(CHANGELOG)与测试里 | | `UI.md` | 「每版最多 3 行」一节重写成「简短三句」 | 原来那节的理由(v1.0.0 铺全文 6194 字符)仍然成立,但摘要来源变了 | 标题必须**独占一行**才算摘要区块:`## 摘要表` 不是(否则会把后面的正文误吞进来)。 ### 摘要上限仍是三行 `RELEASE_SUMMARY_LINES = 3` 不变。用户这次给的是**上限**口径(「最多三句,一句也行」), 所以三句不标截断、四句以上才标「…还有 N 行」。 ### 老版本:最近三版的 Release 正文已改短 v1.1.2 / v1.1.1 / v1.1.0 的 Release 正文已按新格式重写(标题 + `## 摘要` + 附件校验), 免得往下翻时新旧两种摘要在同一屏里混着。**GitHub 上的正文是 PATCH 过的**, 所以那三版的正文 sha256 与本地 `CHANGELOG.md` 的小节不再逐字对应——这是预期的。 ### 验证 - `npm test` 全绿:16 组(`130-updater` 从 32 项加到 **35 项**,含 `summarySectionOf` 的 4 条边界); - **变异验证 31 项全红**(新增 2 项:「摘要退回取正文前 3 行」「没写摘要时也编一段兜底」)。 ### 验收判据 1. **摘要只认 `## 摘要` 区块**:正文里有 → 显示区块里的三句(区块之前的铺垫与之后的验收判据都不进摘要); 没有 → 摘要为空,面板显示「(这版没写摘要)」。判定:`node test/130-updater.test.mjs`。 2. **最多三句**:`summarizeNotes` 把区块文本压到 3 行;正好三句时不标截断。判定:同上。 3. **面板把「没写摘要」与「有摘要」分开说**:夹具里 `summaryMissing: true` 的那条要渲染出 「(这版没写摘要)」。判定:`node test/20-client.test.mjs`。 4. **变异验证**:把 `summarySectionOf(body)` 换成 `body`(退回取前 3 行)→ 红; 把「没有区块返回空串」改成返回兜底文案 → 红。 5. **全量**:`npm test` 全绿 + `npm run test:mutate` 31 项按预期变红。 ### v1.1.2 · 治「复读机」:注入降频 + 催短 + 加那句 用户原话:**「他现在还是一直跟个复读机一样说相似的话,很烦」**,追问里点名 **「两个都烦」** (插件注入的行 + 模型自己的播报)。 **先量,再改。** 一个 4 天的会话(8815 行日志)里,插件注入了 **688 条**,全部被 Web GUI 渲染成独立消息行:工作流触发 **510**(同一条流程被原样塞 **159 次**完整 11 步)、 催促 147、说话纠正 31。平均**每 1.8 条助手消息就被插一条**。 | 改哪 | 改成什么 | 为什么 | | --- | --- | --- | | 工作流触发 | **首次全文,之后一行指路** | 重复的体积压到约 1/10;但不彻底闭嘴——压缩后模型看不到那 11 步 | | 催促 | 节拍 6 → **12** 步,文案压成**一行** | 它是**确定会重复**的那条;条数减半 + 每条降到约 1/4 | | 催促 | **撤掉「一句话报进度」** | 那句话被模型照做了,每到一个节拍就多一句播报——正是「复读机」的来源 | | 催促 | 加上**「你怎么这么慢,快点做啊」** | 用户指定原文,三档都带,一眼认得出 | | 提示段 | **工具调用之间不写「我刚发现了什么」** | 逐字比对抓不到「同一种动作重复」,只能用行为规则治 | 解日志本身还有个坑:日志是**多帧 zstd**,`zstdDecompressSync` 只解第一帧—— 8964937 字节的文件解出来只有 195 字节,看起来像「日志是空的」。按魔数切帧(3996 帧)才对。 **测试**:`npm test` 15 组全绿 + 跨插件握手 18/0;**变异验证 29 项全部按预期变红** (新增 5 项,其中一项第一版是**假绿**——断言写成 `includes(NUDGE_PUSH)`, 常量改成空串后 `includes('')` 恒真,被变异验证抓出来,已改成比对字面量)。 ### v1.1.1 · 修「一键更新装不上」(本机 github.com 不通) v1.1.0 发完当场真机自更新,**失败了**——但这次失败很有价值:**回退机制干净地生效了** (三处全部还原、插件照旧可用),而日志把真原因写清楚了。 **两次尝试、两次不同的死因**: | 规格 | 报错 | | --- | --- | | `github:owner/repo#tag` | `git+ssh://` → `Host key verification failed` | | `git+https://github.com/…#tag` | `Failed to connect to github.com:443 after 21119 ms` | **根因**:本机可达性实测(2026-10-09): | 域名 | 443 | | --- | --- | | `api.github.com` | 通 | | `codeload.github.com` | 通 | | **`github.com`** | **不通** | 而 pnpm 装 git 依赖要**两步**:① 在 `github.com` 上 `git ls-remote` 解析 tag → SHA; ② 按 SHA 从 codeload 下 tarball。**第一步就死了**——所以任何要碰 `github.com` 的规格都装不上,换协议解决不了。 **解法**:把第一步挪进插件——插件走可达的 `api.github.com` 解析出 SHA, 再把 **SHA 版的 codeload URL** 交给 pnpm,那一步不必碰 `github.com`。 这不是权宜之计:pnpm 自己写进 `pnpm-lock.yaml` 的就是这个形式 (实测本机锁文件:`version: https://codeload.github.com/…/tar.gz/`), 所以装完 `package.json` / 锁文件的形状与原来一致,回退与后续升级都不受影响。 用 SHA 而不是 tag 还有个额外好处:**tag 可以被移动**(本仓发版时确实 `git tag -f` 过),SHA 不能——这才是「装的东西可复现」的前提。 ### 顺带修掉三个由此暴露的缺陷 1. **自检把成功判成失败**:判据一是「依赖声明里含 tag」,而 SHA 版 URL 里没有 tag → 一次**已经装成功**的安装被拦下并回退(实测)。判据的本意是「指向这次要装的那一版」, 而 SHA 更精确地表达了这件事,所以改成「有 SHA 就认 SHA,没给才退回认 tag」。 2. **失败信息没有信息量**:第一版只写「pnpm add 退出码 1」,而真正的死因 (`Host key verification failed`)躺在 4000 字符的日志里。现在会把关键行 摘出来放进 `error`——**失败信息的第一职责是让人知道下一步该干什么**。 3. **`main()` 是 async 却没有 `.catch`**:未处理的 rejection 会让 Node 打一行警告后以**退出码 0** 结束,于是面板看到的「进程结束」是成功, 而结果文件什么都没写 → **面板永远转圈**。现在兜底写结果并退出码 1。 ### 新增一个机械检查 `tools/check-install-spec.mjs`(11 项):面板显示的命令与安装器实际执行的 spec 分处两个文件(后者不能 import 前者),靠人记着同步必然漂移。 它**剥掉注释再查代码**——第一版直接查全文,把「解释为什么不用它」的注释也判成违规, 而那种检查会逼着人删掉解释,下一个人就会把旧写法加回来。 `npm test` 全绿:60 + 47 + 7 + 51 + 17 + 36 + 12 + 9 + 21 + 23 + 21 + 32 + 22 + 12 + 22; 跨插件握手 18 / 0。**变异验证 25 项全部按预期变红。** ### v1.1.0 · 集成 dsh-cot-guard:文件观察守卫 + 注入闸门 用户要求把 `dsh-cot-guard` 的**功能集成进拼图插件**(不是另装一个插件)。 **为什么它治的不是「思维链」**:那个插件的原需求是「思维链修正」,但它动手前把 69 个会话、514 次工具错误翻了一遍,结论是**思维链本身不是病因**——原报告七条判据里 四条过不了零模型对照(决定下得太早:标记随机撒是 75.4%,比实测 54.8% 还高; 重开多与步长 r²=0.777,是长步本来就更长;主题词重合 52.5% vs 零模型 50.0%)。 真正拦路的是**文件观察状态**:`FS_STALE_VERSION` 51.6% + `FS_NOT_OBSERVED` 18.9% + `FS_IO_ERROR` 8.2% = **78.7% 的工具错误**,与推理无关。而它有一条**有对照组**的修法: | 重试方式 | 例数 | 成功 | | --- | --- | --- | | 先 `read` 再重试 | 181 | **98.3%** | | 不 read 直接重试 | 15 | **0.0%** | **四个触发点**(全部按需,不写进每步提示):写被拒 → 提示「先 read 再重试」; 文件距上次观察 ≥5 步 → 提前提醒(该档报错率 32.0%,基线 14.8%); `FS_NOT_OBSERVED` + `edit` → 把机制错误升格为行为纠正;单步决定宣告 ≥8 次 → 叫停。 **只提醒,不拦截**——提醒错了只是多读一次,拦错了是任务卡死。 ### 集成时**顺手发现了拼图插件自己的一个真 bug** 核对 `exec.arguments` 的形状时(cot-guard 说它是字符串,拼图插件当对象用), 用会话日志逐条数了一遍: ``` tool/call 共 248 条:arguments 是字符串 248 条,对象 0 条 ``` **五处**按对象读,于是全部**静默失效**: | 受影响的功能 | 症状 | | --- | --- | | **只拼不写拦截** | 认不出任何一次真实调用——**那条防线一直没生效** | | 工作流触发 | 触发词里的文件路径类关键词一条都命中不了 | | 重复思考熔断 | 动作签名恒为空串(无参调用不计入连击) | | 催促(分段读) | 永远拿不到 `file_path` | | 文档改写判定 | 恒为 `null` | 它们**都有测试、都全绿**——因为测试用的是对象(照着类型想象写的)。 这正是本仓记过的老教训:**测试夹具的形状必须从真实运行时抄**。 修法:新增 `lib/toolargs.js` 统一解析,**字符串与对象都认** (防宿主将来改形状时这五处再次静默失效)。 ### 七条注入共用一道闸门(用户裁定「一起降低注入频次」) 加上守卫的四个触发点,一共有**七条**会往同一轮上下文里塞话。每条单独看都克制, 但它们互不知情——同一轮里可能同时来三四条。所以新增 `lib/injectgate.js`: **一轮最多 2 条 + 同类 6 步冷却**,各类独立计数。 两处例外,各有硬理由: - **「先 read 再重试」永远放行**:它是唯一有对照组的动作(98.3% vs 0%), 被别的话挤掉就等于没有它; - **熔断与催促豁免同类冷却**:它们自带节流(3 步升级 / 6 步节拍), 而闸门冷却是 6 步——会把熔断的「第 2 次、第 3 次」整个吃掉。 **这不是「少说一句」,是把一条已验证的机制弄失效**(实测:接上闸门后 `60-loopguard` 那条「不改动作时必须重复注入」当场变红)。 还有一条更根本的取舍:**信息不足时闸门不拦**。`turn` 拿不到时无法判断 「这是同一轮的第三条还是新轮的第一条」,而判错的不对称性很明显—— **多注入是噪音,误拦是功能失效**。所以拿不到轮信息就放行,把节制交给各条自带的节流。 ### 验证 - `npm test` 全绿:15 组(新增 `160-toolargs-fsguard` 22 项); - **变异验证 25 项全红**,其中「入参只认对象」那条正是那个真 bug 的复现; - 测试用**真实形状**驱动(字符串入参 + `error.info.code`),不再用想象的类型。 `npm test` 全绿:60 + 47 + 7 + 51 + 17 + 36 + 12 + 9 + 21 + 23 + 21 + 32 + 22 + 12 + 22; 跨插件握手 18 / 0。 ### v1.0.3 · 修「打开更新面板就消失」 用户报的**真机崩溃**,一句话症状:**打开更新页,整个面板不见了。** #### 根因 更新页的渲染代码里用了 `sessionId`,而 `updatePage()` 里**没有这个变量** (更新是全局功能,本来就不需要会话)→ `ReferenceError` → React 整棵子树卸载 → 面板一打开就没了。 同一个错我在这版之前的 `devSection` 里犯过一次(`installed is not defined`)。 两个函数挨着,但**局部变量不共享**——这是最容易被「它俩挨着」骗过去的一类错。 修法:`releaseList()` 根本不需要 `sessionId`(它收了参数却从没用过), 调用处不传即可,函数签名也去掉那个参数。 #### 为什么测试当时是全绿的(这条比 bug 本身更重要) `20-client` 的更新页断言**确实存在**,但它验的是「切换主视图、只有一层面板」—— 而夹具对 `method:'update'` 回的是 `{ ok: true }`(**没有 result**)。 于是更新页只渲染「点右上角『重新检查』」那一行,**那条会崩的分支根本没走到**。 一个从没被执行的渲染路径,等于没测。 修法(三处,缺一不可): 1. **夹具回真实数据**:`check` / `releases` / `staged` 三条 action 的真实形状都造出来—— 有版本号、有摘要、有安装结果、有备份清单; 2. **点开之后要 `await flush()`**:点开更新页会同时发三个异步请求, 不等它们回来就读树,看到的是初始空态(第一版漏了这行,断言假红); 3. **断言「有数据时必须渲染出内容」**:版本号、3 行摘要、「…还有 N 行」、「当前」标记、 安装结论、备份区块——全部逐条断言。 **变异验证**:把 `sessionId` 那个 bug 改回去 → 测试**立刻红** (`ReferenceError: sessionId is not defined`,栈顶直指 `updatePage`); 还原 → 绿。这条变异已加进 `mutate-check`(20 → 21 项)。 #### 顺带新增一个发版脚本 `tools/add-readme-section.mjs`:往 README 插入版本小节,并机械地只留最近 3 个 (本仓发版约定),超出的自动删并补一句「更早的版本见 CHANGELOG」。 起因是这一轮我第三次在 PowerShell 里拼含反引号与引号的长字符串而失败—— 写成脚本 + 从文件读正文就没有那一层。 `npm test` 全绿:60 + 47 + 7 + 51 + 17 + 36 + 12 + 9 + 21 + 23 + 21 + 32 + 22 + 12; 跨插件握手 18 / 0。**变异验证 21 项全部按预期变红。** ### v1.0.2 · 修「本机版本读错来源」(假报「已是最新」) v1.0.1 发完当场实测发现的**真缺陷**——它不报错,只是**安静地说错话**。 #### 症状 发完 v1.0.1 后用插件自己的更新功能检查本机版本,结果是: \\n线上最新 = 1.0.1 | 本机 = 1.0.1 | 状态 = latest 没有新版本,跳过 \\n **但本机 profile 里装的明明是 1.0.0。** 面板会说「已经是最新版本」,用户不会去升级, 而实际装的是旧版。 #### 根因 pluginVersion() 读的是**当前正在执行的这份代码**所在的 package.json。 开发机上这常常是**源码仓库**(版本已是 1.0.1),而 profile 里装着的还是 1.0.0。 于是「本机版本」读成了「源码版本」。 #### 修法 新增 localVersion() 作为「本机版本」的**唯一入口**: 1. 先读 **profile 里装的那份**(/node_modules/dsh-puzzle-mode/package.json); 2. 读不到才退回当前代码的版本(插件不是从 profile 加载的场景); 3. **回传 ersionSource**(profile / unning)——面板要能说清版本号是从哪读来的。 三处(check / download / staged)全部改走它。**三处各写一遍就会漂移**, 而漂移的后果正是这次这个 bug。 #### 为什么值得单独发一版 「假报已是最新」比**报错**更糟:报错会让人去查,而假报会让人以为没事。 这类缺陷只有**真跑一次**才看得见——单元测试里 profile 与源码版本通常一致, 怎么测都是绿的。 **新增断言**(X-updater 22 → 32 项):造一个假 profile,里面装着 .7.7-from-profile,断言 installedVersionOf 读出的是**那个**版本, 而不是当前跑着的代码的版本。 ### v1.0.1 · 各版本日志 + 一键更新(失败自动回退) 两条需求,都是对 v1.0.0 那个更新页的返工: > 1. 我要的是有个地方看各个版本的更新日志,而且要**洁简**,不要一大堆字 > 2. 更新改成**更新失败就回退** #### ① 各版本更新日志(洁简) v1.0.0 的更新页只显示**最新那一版**,而且把整篇 Release 正文原样铺出来 (实测 6194 字符)——一屏放不下一个版本,那不叫看日志,那叫读文档。 现在从 **Release 列表 API**(`/releases?per_page=20`,带 gh-proxy 镜像回退)拉各版本, **每版最多 3 行摘要**。压缩规则与理由: | 规则 | 为什么 | | --- | --- | | 丢掉标题行 | 列表里已有版本号与标题,重复一次纯占地方 | | 丢掉图片行、分隔线、空行 | 纯文本摘要里它们只是噪音 | | **表格行压平保留**(`\| a \| b \|` → `a · b`) | 发版正文的关键信息常在表里,整行丢掉会漏掉「改了什么」 | | 剥掉 `**粗体**` 与 `` `代码` `` 记号 | 摘要不渲染 markdown,记号只会碍眼 | | 列表符号统一成 `·` | `-` / `*` / `1.` 混着出现视觉很乱 | | 超出行数**明说**「…还有 N 行」 | 静默截断会让人以为这版就这么点内容 | 截断在**宿主半**做(`summarizeNotes`),不在面板做——这样那条规则能被测试直接钉住 (断言行数与省略标记),而不必真机点开才验得到。 当前装的那版打「当前」标记;预发布版单独标记;草稿直接剔掉。 #### ② 一键更新,失败自动回退 v1.0.0 不敢做一键,理由是「装坏了插件加载不了、面板跟着消失、只剩手改 `package.json`」。 那个顾虑是对的,但**解法不是不让装**,而是让**回退逻辑不依赖插件活着**: ``` 插件(宿主半) 独立进程(tools/dsh-puzzle-update.mjs) │ │ │ ① 复制脚本到 $DSH_HOME/puzzle-mode-updates/ 再启动 ├───────────────────────►│ ② 备份三处:node_modules / package.json / 锁文件 │ │ ③ pnpm add │ │ ④ 自检:依赖声明 · 版本号 · 入口能否解析 │ │ ⑤ 任何一步不过 → 整体回退三处 │ 轮询 last-run.json ◄───┤ ⑥ 写结论(成功 / 失败 / 已回退) ``` **脚本先被复制到 `$DSH_HOME` 下再执行**——那个位置不属于任何包,所以 `node_modules` 被装坏也不影响它跑完回退。这是整个设计的支点。 三处一起恢复(用户裁定):只恢复 `node_modules` 不够——`package.json` 还指着新版, 下次任何一次 `pnpm install` 都会把坏版本装回来,那种「修好了又自己坏掉」最难查。 **「装成功」的三条判据**(缺一不可):依赖声明已指向新 tag · 装上的版本号正确 · 入口文件能被 `node --check` 解析。第三条最关键:前两条都过、但入口语法坏了, 插件照样加载不了——而那正是「更新失败」最典型的形态。 #### ③ 实测抓到的三个真缺陷 这一版的三个缺陷**都是真跑一次才暴露的**,读代码看不出来: 1. **结果文件只在结束时写** → 面板分不清「正在装」与「根本没起来」。 实测就是这样:`spawn` 出去了、进程在跑,面板什么都看不到,用户不知道该等还是该重来。 修法:干重活**之前**先写一条 `running: true`(带 pid 与阶段),面板据此说 「正在装(pid …)」。面板的轮询上限也从 90 秒放宽到 5 分钟——pnpm 解析 git tag 要联网问 GitHub,实测一次要几秒到一两分钟,上限太短会把「正常在装」判成卡住。 2. **`--expect-version` 为空时是空判据**:第一版写 `expectVersion === '' ? version !== '' : version === expectVersion`, 只要装上了一个能读出版本号的包就算通过——哪怕装错版本。而「装完版本没变」 正是更新失败最常见的形态(pnpm 静默复用旧缓存),那一条恰恰要拦住。 3. **变异打偏了位置**:`rollback` 有两个调用点(pnpm 失败 / 自检失败), 只改一处另一处仍会回退,测试自然不红;`writeRunning` 同理有两处调用。 变异要改**函数体**,不是某一个调用点——第一版白跑一轮。 #### ④ 变异验证 20 项全红,其中 3 项逼我改掉了「结构断言」 v1.0.0 是 12 项,这一版加到 20 项。新增的 8 项里有 **3 项第一轮全绿**, 原因不是守卫失效,而是我的断言写成了**结构断言**: ```js // ❌ 这样守不住:把 writeRunning(...) 换成 void writeRunning,字符串还在,断言照样绿 assert.ok(readFileSync(script, 'utf8').includes('writeRunning(resultPath')) ``` 那 3 条已改成**真跑一次验行为**:在安装器运行期间轮询结果文件, 必须**观察到** `running: true` 与 `phase: 'install'`;失败路径则断言 「三处真的被恢复过(`skipped: false`)」而不是「代码里有 rollback 这个词」。 `150-installer` 新增 12 项,全部**跑真脚本**(造一个假 profile,依赖指向不存在的 tag, 几秒内走完「失败 → 回退」整条路径,真 profile 一个字节都不碰)。 `npm test` 全绿:60 + 47 + 7 + 51 + 17 + 36 + 12 + 9 + 21 + 23 + 21 + 31 + 22 + 12; 跨插件握手 18 / 0。 ### v1.0.0 · 自动更新 / 主题开发分享 / 工作流去重 / 液态玻璃 用户一次点了五件事。这一版**新增三个纯逻辑模块**(`workflowguard` / `updater` / `themedev`), 宿主半与面板各接线一次。 #### ① 自动更新:只做到「暂存」,不自己装进 profile(用户裁定) 备选是让宿主直接跑 `pnpm add`。那是真的一键,但它有一个**不可接受**的失败模式: **装坏了插件本身就加载不了,面板随之消失**——用户只剩手改 `package.json` 这一条路, 而那时他连界面都没有。所以边界划在:插件只写自己的缓存目录 (`$DSH_HOME/puzzle-mode-updates/`),装不装由用户执行那一条命令决定。 代价是多一次复制粘贴,换来「更新失败 = 面板照旧能用」。 按钮因此叫「**下载到暂存区(不自动安装)**」而不叫「更新」——叫「更新」会让人 以为点完就好了,然后发现版本没变,那是骗人。 **通道按实测可达性排序**(2026-10-08 实测,不是猜的): | 通道 | 结果 | | --- | --- | | `api.github.com` | **通**(615ms)——版本号、正文、附件地址一次到手 | | `cdn.jsdelivr.net` | **通**(1430ms)——退路:读仓库里的 `package.json` / `CHANGELOG.md` | | `gh-proxy.com` | **通**(690ms) | | `raw.githubusercontent.com` | **不通**(DNS 解析失败) | API 失败时退到 jsDelivr——够回答「有没有新版、改了啥」,但**拿不到附件地址**, 所以界面如实说「去 Release 页下载」,不给一个点了必然失败的按钮。 **校验验什么**:网上流传的「下载完比一下 sha256」在这里有个陷阱——**哈希本身从哪来**? 若哈希也从同一个可能被劫持的通道取,那只是把信任问题挪了个位置。做法是: 发版时把 tgz 的 sha256 写进 **Release 正文**(正文与附件是两条不同的下载路径, 且正文同时进 git 历史)。更新器从正文解析出这个锚点,再比对下载到的字节。 正文里没有锚点(老版本)时退化成**结构校验**(gzip 解得开、包里是 `dsh-puzzle-mode`、 版本对得上),并**如实标成「结构校验,正文里没有哈希锚点」**——假装验过了比不验更糟。 **解包是这条路径上唯一的攻击面**(解压一个从网上下来的归档),所以逐条拦: `..` 穿越、绝对路径、Windows 盘符、反斜杠穿越(只查 `/` 会在 Windows 上漏掉)、 tar 里的符号链接与硬链接条目。并且**先全量校验路径再落盘**——先解一半再发现有问题 会留下半个目录,本仓对「装了一半」的态度一贯是宁可不写。 自己解 tar 而不调 `tar` 命令:零运行时依赖是硬约束,且 `tar` 在 Windows 上要 Windows 10 1803+ 才自带——少一个平台假设就少一类「在我机器上能用」。 #### ② 主题开发与分享(用户澄清:「上传是让别人能够分享自己做的主题风格」) 两条分享路,因为「让别人用上」有两种场合: - **推到主题仓库**:别人刷新主题列表就能看到、一键安装。插件只做 `git add / commit / push`——**插件代码里没有任何令牌路径**,凭据由 git 自己的 凭据链(credential helper / SSH agent)取。这比「读 `~/.dsh/.github-token` 调 Contents API」安全一个量级:后者一旦被插件拿到,就等于把「写你所有仓库」的能力交出去了。 - **导出单文件主题包**:发给对方粘贴导入,不依赖 git 与网络源。包里带 sha256, 导入端验伪;同 id 已装时**先问一句**再替换,不替用户决定。 上传前用**插件自己的校验器**量一遍——推上去的坏主题会让**所有**用户装不上, 比本地报错严重得多。再重算 `index.json`:sha256 必须现算,手写就意味着 「改完 CSS 忘了更新 hash」,而那个后果不是报错,是所有用户都装不上。 「一键开发」建出脚手架并把一份开发提示词**填进输入框**(不自动发送, 与其它按钮同一条规矩:面板只填提示词,发不发由用户决定)。 **实测抓到的真缺陷**:脚手架模板的注释里写着「不许 `@import` / `javascript:`」, 而校验器对这几个关键字是**文本包含**匹配——**模板自己撞了自己的规则**, 作者拿到脚手架第一步就装不上,报错还指着一行注释。修法是把规则只写在开发提示词里 (那是给模型读的,不经过校验器),CSS 里一个字都不提。 #### ③ 默认皮肤正名为液态玻璃并做实 **事实澄清**(动手前先核对过):默认皮肤**本来就有**玻璃材质—— `@supports` 那条 `backdrop-filter:blur(18px) saturate(1.2)` 一直在。 但它同时挂着科幻 HUD 的装饰(青蓝主色 + 网格 + 顶部扫描线),而面板上写的是 「默认皮肤(随宿主明暗)」——**没人知道那叫液态玻璃**。所以做两件事: - **正名**:面板文案改成「液态玻璃(内置默认 · 随宿主明暗)」; - **做实**:网格归零(`--dshpz-grid:transparent`)、blur 18 → **24px**、 顶部彩色扫描线换成**白色高光边**(玻璃的边是光打出来的,不是彩色的)。 `THEME_HUD` 随之改名 `THEME_GLASS`——它已经不是 HUD 了,留着旧名字会让下一个读代码的人 以为里面还是那套科幻装饰。 #### ④ 工作流注入去重 改之前**每一次**工具调用都会重新注入一遍命中的流程——连着改同一类文件, 同一份 11 步流程被原样塞进上下文十遍。代价不只是浪费(每一步的上下文都要重发): 同一段规则刷屏之后,模型会开始**忽略**它。 判据是一句话:**「这个命中集合,在最近 `WORKFLOW_MIN_INTERVAL`(6)步内注入过吗?」** 注入过就跳过,没注入过就注入。它同时给出用户要的两个效果: - **命中集合没变 → 不重复注入**(治刷屏); - **换了阶段(集合变了)→ 立刻重新注入**(不漏关键时刻)。 按**整个命中集合**(而不是按单条)记签名:一次调用可能同时命中几条流程, 按单条记的话 A+B 之后只命中 A 会被判成「没变」而跳过——但注入内容其实变了。 另外挡掉「来回横跳」:只按「签名变没变」判的话,A、B、A、B 交替命中时每一步都算「变了」, 于是每一步都注入——**比去重之前还吵**。所以判据统一成「这个签名在最近 N 步内注入过没有」, 横跳回一个刚注入过的签名同样被挡住,而真正的新阶段仍然立刻注入。 #### ⑤ 测试:变异验证抓出两个真漏洞 新增三组共 65 项(`120-workflowguard` 21 / `130-updater` 22 / `140-themedev` 22), `20-client` 新增 4 项面板断言(更新页、互斥、液态玻璃的可判定判据、开发区)。 **`tools/mutate-check.mjs` 跑 12 项变异,全部按预期变红**——并因此抓出两个真漏洞: 1. **去重的「注入次数」没有任何断言看过**:把 `injected + 1` 挪进「没命中」那条分支, 原先的断言**全部照绿**。那个字段是面板用来告诉用户「这条规则生效了几次」的, 虚高就等于报告了一件没发生的事。 2. **更新器只测了纯函数,从没测过下载校验链**:把 sha256 锚点比对改成恒真, 上面那些纯函数断言**一条都不红**。补上一个**真实本地 HTTP 服务**驱动的完整下载路径 (`listen(0)` 随机端口,跑完立刻关)——校验链里好几处只在「真的读到字节」时才走到, mock 掉 `fetch` 就等于把这条链整段跳过。 测试全绿:60 + 47 + 7 + 51 + 17 + 36 + 12 + 9 + 21 + 23 + 21 + 22 + 22;跨插件握手 18 / 0。 #### ⑥ 主题仓库:新增「新拟物」官方主题 与仓库里其它几套**做法不同**:别的主题靠颜色与发光区分层次,新拟物不用描边、不用发光、 几乎不用颜色,只靠一明一暗两道投影把元素从同色底面上「挤」出来(凸起)或「按」下去(凹陷)。 所以取值有一条硬纪律:**投影必须比颜色重要**——把 `--dshpz-shadow` 改淡,这套皮肤立刻 退化成「一片灰」,辨识度全在投影上。清单重算后 5 套主题,两道自检通过。 #### ⑦ 其它 - RPC 正文上限 16KB → **1MB**:面板要能把一份单文件主题包贴进来导入, 而主题包上限是 512KB。原先那个 16KB 是给「几个字段的表单」定的, 导入功能一上就会被它静默拒掉——而且拒绝发生在读正文那一步,面板只会看到 「请求过大」,完全联想不到是上限太小。 - `sha256FromNotes` 的正则补 `i` 标志:GitHub 附件字段给的是大写十六进制, 发版脚本手写时关键字也可能大写。只认小写会让锚点解析成空串—— 那会**静默退化**成「未校验哈希」,看起来一切正常,实际少了一道校验。 - 新增 `tools/bump-readme.mjs`:发版时同步 README 三处版本号 + UI.md 头部 + PUBLISH 第 0 节。 上一轮在 PowerShell 里做字符串替换,反引号与 `$` 反复把替换搞成**静默不匹配** (变异没生效却报「通过」)——所以改成 Node 脚本,替换没命中就明确报错退出。 ### v0.30.0 · 自动审查只报漏洞 用户裁定:「自动审查就不要再报巨函数问题了,这些留到手动审查再说, 自动审查只找代码漏洞和修复漏洞」。 **为什么这个收窄是对的**:结构类是**重构建议**,不是缺陷——巨函数不会让代码跑错, 只会让它难读。而自动审查每次都要给人一份「点单清单」,混进几十条「这个函数太长」 会**淹掉真正的漏洞**:用户看到第一屏全是行数,就不会往下翻。 **改法**:结构类(巨函数 / 大文件 / 目录不分层 / 死导出)**不再进 `fixPlan`**。 `findings` 里 `source:*` 一条没少(含文件与行号),**手动审查**读它; 源码体检照常算(五维真实值依赖它),只是不再翻成清单条目。 **唯一保留的 structure 条目**是「没查到源码」那条告警——它不是重构建议, 而是「这一轮没有实测漏洞」的警告。砍掉它,用户会以为「没报漏洞 = 没漏洞」。 三处口径同步改口(提示段 / `AUDIT_PROMPT` / 工具描述),`AUDIT_PROMPT` 新增「〇」节 把这条裁定写在最前面;面板的「审查(交给 AI)」模板改成指向漏洞而不是拆函数。 **测试**:`90-audit-tool` 5 → **9 项**。判据是**两头**:① 结构类一条都不许进 `fixPlan`; ② 但它们**必须仍在 findings 里**——只验①会把「干脆不算了」也判成通过。 **变异验证**:把结构类重新塞回 `fixPlan` → **当场变红**。 `npm test` 全绿:60 + 47 + 7 + 51 + 17 + 36 + 12 + 9 + 21 + 23;跨插件握手 18 / 0。 ### v0.29.0 · 说话纠正:说英文 / 复读输入时当场纠正 用户第二次提同一件事:「强化防复读效果,就说输出陈述时不要复读输入的内容」 「陈述中文化强化搞一个最硬的约束,现在还是会说英文」。 **关键事实:v0.28.0 起提示段里就写着「一律用中文」,用户实测仍然说英文。** 所以这次不只改措辞——**加了检测**。 **① 提示段升级成「最硬约束」**:该节开头直接写「【最硬约束 · 中文】所有陈述必须是中文」, 并点明「优先于本提示段的其它一切风格要求」;英文只允许出现在四种位置 (代码 / 标识符 / 文件路径 / 命令与报错原文)。不复读同样列为最硬约束。 **② 新增 `lib/speech.js` + 两个钩子**: - `session/event`(只读观察):记下输入(用户消息 / 工具输出)与陈述; - `agent/pre-step`(注入):陈述违规 → 下一步进入模型前注入纠正。 **检测口径**: - **英文**:先剥代码块与行内代码,再看纯英文行——≥ 5 拉丁词且无汉字, 全篇累计 ≥ 12 词判违规;另有**更严的标题规则**(≥ 2 词、≥ 8 字母、词首大写), 因为实测漏过 `## Next Steps`。 - **复读**:压平空白后按 4 字符步进取 24 字符窗口比对,逐字命中即判。 **误判防线**:代码块 / 行内代码全剥;引用报错原文放行;`## op:read` 与 `## git status` 放行(**大小写是命令标题与英文标题的分界**); 工具输出里的英文不算模型说英文。 **测试**:新增 `test/110-speech.test.mjs`(23 项)。**变异验证三处均红**: 去掉标题规则 → 红;标题规则去掉大小写判据 → 红;不剥代码块 → 红。 `npm test` 全绿:60 + 47 + 7 + 51 + 17 + 36 + 12 + 5 + 21 + 23;跨插件握手 18 / 0。 ### v0.28.2 · 催促节拍改为每 6 步 用户改口:**「改为 6 步」**(v0.28.1 定的是 4 步)。 判据不变——**纯节拍**,不分读写;只改 `NUDGE_EVERY`:`4` → **`6`**。 节奏落在第 **6 / 12 / 18 / 24** 步,等距。「节拍固定、语气看表现」也不变。 测试侧顺手做了件事:节拍断言与驱动数据改为**跟随常量** (`Array.from({length: NUDGE_EVERY}, …)` 交错 read / write / pwsh),不再写死 4 项数组—— 下次调数字只改一个常量,测试自动跟上。**变异验证**:节拍退回 4 → 当场变红。 `npm test` 全绿:60 + 47 + 7 + 51 + 17 + 36 + 12 + 5 + 21;跨插件握手 18 / 0。 ### v0.28.1 · 催促改成纯节拍:每 4 步催一次 用户原话:**「我的意思是干什么都四步催一次」**。 v0.28.0 把催促做成了「连续 6 次**只读**调用才催」——用户当场纠正:要的是**节拍**, 不是「只读」这个特征。改成纯计数:**每 4 次工具调用就催一次,不分读写**。 好处:不再依赖「什么算只读」那张白名单。白名单一改(增删工具),触发点就漂移; 纯计数是确定的。 **节拍固定,语气看表现**: | 这几步的情况 | 档位 | | --- | --- | | 中间有产出(写过文件 / 跑过命令) | 停在第 1 档:一句话报进度 + 直接做下一步 | | 一直只读、一行产出都没有 | 逐次加硬:第 2 档禁掉只读工具,第 3 档只留「写结论 + 执行下一步」 | 工具名不再决定**要不要催**,只决定**催得多凶**——既不漏催,也不会把正在写代码的人 当磨蹭的人骂。节奏落在第 4 / 8 / 12 / 16 步,等距。同一文件读第二次仍额外点名「分段读」。 `NUDGE_READ_STREAK` / `NUDGE_ESCALATE_EVERY` 退役,换成 `NUDGE_EVERY = 4`; `NUDGE_REASON_STREAK` → `NUDGE_REASON_CADENCE`;`noteCall` 返回值 `streak` → `count`。 **测试**:`test/100-nudge.test.mjs` 重写(21 项)。最关键的一条是 「读写混合走到第 4 步也必须催」——防退回「只读才算」那个口径的守卫。 **变异验证三处均红**:退回「只读才算」→ 红;节拍改成 5 → 红;每步都催 → 红。 `npm test` 全绿:60 + 47 + 7 + 51 + 17 + 36 + 12 + 5 + 21;跨插件握手 18 / 0。 ### v0.28.0 · 说话方式 + 读文件一次读全 + 催促 用户一次点了三件事:「模型在几度陈述时会复读类似的意思,而且陈述用英语」 「读文件老是分段读,不如直接一次读全部要的地方」 「干活都磨磨唧唧的,加一个自动注入语音犀利的催促的功能」。 **① 说话方式**(提示段新增 `### 说话与干活` 一节):**一律用中文**(英文只留给标识符、 路径、命令与报错原文,不许整段英文陈述);**同一件事只说一遍**(每轮开头不复述上一轮, 直接给新结论或新动作)。 **② 读文件一次读全**:`read` 把 `limit` 给足,不要一段一段读同一个文件; 要看好几个文件就同一步里并行发多个 `read`。 **③ 催促(新功能 `lib/nudge.js`)**:只看不动时注入语气犀利的催促。 两条触发:**连续 6 次只读调用**(read / grep / glob)无产出;**同一个文件读第 2 次** (正是「分段读」)。档位加码方式是**撤出口**(与熔断同一手法):第 1 档还允许 「说清你在找什么」,第 2 档**禁掉只读工具**,第 3 档只留「写结论 + 执行下一步」; 到顶后仍按间隔继续催——沉默比重复更贵。 **误报防线**:只读白名单只有 read / grep / glob,其余一切算产出并**清空**连击; 阈值取 6(连着读 5 个文件是正常调研);工具失败(block)时不插话。 **测试**:新增 `test/100-nudge.test.mjs`(**21 项**),纯函数 + 真钩子接线, 含 4 条守卫自证。**变异验证三处均红**:白名单混入 `write` → 红;退回「只报一次」→ 红; 关掉分段读判定 → 红。 **顺带修掉一个既有红**:`test/40-pre-execute.test.mjs` 没隔离 `DSH_HOME`, 会去读用户真实设置——用户关掉固定收尾问后那条断言立刻变红而代码没错(本机实测)。 已给 4 个漏掉的测试文件补 `helpers/isolate-home.mjs`。 `npm test` 全绿:60 + 47 + 7 + 51 + 17 + 36 + 12 + 5 + 21;跨插件握手 18 / 0。 ### v0.27.1 · 删掉面板遮罩;主题页改为主界面切换 两条观感问题,用户直接点名:「打开面板的遮罩删掉」「换成主题页改为直接切换原本的主界面」。 同一类毛病:屏幕上不该同时出现两层「面板」。 **① 遮罩**:`.dshpz-backdrop` 去掉 `rgba(0,0,0,.45)`,只保留「全屏居中 + 点外部关闭」。 面板自己的玻璃底已经保证可读性。**内联兜底里那句 `background` 也一起删**—— 只改样式表、兜底又写回来是假修,测试专门钉这一点。 **② 主题页**:原先压出一层 `.dshpz-tlayer` 全屏覆盖层(带 `clip-path` 生长动画), 同时存在两套玻璃框、两个关闭按钮、两级 Esc。现在复用 `.dshpz-panel`、替换三栏主体, 与主界面共用同一层 backdrop。随之删掉 `.dshpz-tlayer` / `.dshpz-tpanel` / `@keyframes dshpz-tgrow` 与 `themeOrigin`。顶栏「关闭主题」改成 **`← 返回`** (主题页是主界面的一个视图,关整个面板是右上角 `✕` 的事)。 **测试**:`20-client` 新增 2 条断言,判据是**数层数**(`dshpz-panel` 恰好 1 个) 与**查底色**(样式表与内联都不许有 `background`)——这毛病「多一层看不出来」, 只有数层数抓得住。**变异验证均红**:加回 `rgba` → 底色红;外面再包一层 `panel` → 层数红。 `npm test` 全绿:60 + 47 + 7 + 51 + 17 + 36 + 12 + 5;跨插件握手 18 / 0。 ### v0.27.0 · 熔断之后继续推:报过不再闭嘴,第二次起撤掉「转人工」 用户原话:「模型复读了之后它会熔断……熔断了之后他就没有继续……还得人工手动。」 **这不是检测问题,是熔断之后的处置问题。** 旧实现是「同一段连击**只报一次**」: `fired` 一置位,签名不变就**永久闭嘴**。于是模型被点名后若没换动作,第 4、5、6…次 全部**静默放行**——链条原地空转;或模型挑「停下来说清」把问题原样抛回用户, **最后还是人工推**。当时防噪音防过了头:一次提醒不是熔断,**持续推动才是**。 **改动**:新增 `LOOP_ESCALATE_EVERY = 3`(到阈值报第 1 次,之后每 3 次再报一次); `noteAction` 多返回 `level`;`loopBreakText` 按 `level` 加码;`LOOP_MAX_LEVEL = 3` (文案到顶不再加码,但**仍然继续按间隔重报**——沉默比重复更贵)。 **第 2 档起撤掉「停下来说清」**,改成硬指令:① 先捋事实(把已经拿到的信息列成结论); ② 再基于事实下结论 / 动手,**不要再调一次工具去确认**;③ 只有连最小下一步都做不了时 才允许说卡住,且必须说清**卡在哪一步、缺哪一条具体信息**。 为什么必须撤:那条出路原本是给「真缺权限 / 工具坏了」准备的,实测里它成了**最省力的 逃逸口**——一被点名就挑它,熔断于是变成「转人工」。第一次提醒留着它合理; **第二次还在原地重复**,就说明不是真卡住,而是**没在推进**。 提示段同步补「它不是只报一次」,避免行为与提示两说并存。 **测试**:`60-loopguard` 13 → **17 项**。新增 4 项正对本次回归:间隔内不刷屏; 没换动作时**每 3 次必须再报**;`level` 封顶后仍持续重报;**接线层**验钩子真的重复注入 且第二次是硬指令。**变异验证**:`shouldFire` 退回 `firstTime` → 在「必须再报一次」 那条当场变红,还原复绿。 `npm test` 全绿:60 + 47 + 7 + 51 + 17 + 36 + 12 + 5;跨插件握手 18 / 0。 ### v0.26.1 · 修 op:audit 带源码体检必失败 **症状**:`puzzle_mode{op:'audit'}` 只要带源码体检(也就是它的主用法),一律报 `tool "puzzle_mode" returned invalid output: value is not lossless JSON`; 只有显式 `source:false` 能跑。文档与代码都没问题,纯粹是**返回体的形状**踩了宿主的校验。 **根因**:宿主对工具返回做「无损 JSON」校验——必须能 `JSON.parse(JSON.stringify(x))` **原样往返**,而**显式 `undefined`** 过不了这一关 (`{file: undefined}` → stringify → `{}` → 不等于原值)。`lib/index.js` 把源码发现 并进 `findings` 时无条件写了 `file: item.file`,而 `source_flat`(「N 个文件全在同一层目录」) 与「另有 N 个函数超长」这两条是**项目级**发现、本来就没有单个文件。 **修法**:字段**要么是字符串、要么根本不存在**—— `...(typeof item.file === 'string' && item.file !== '' ? { file: item.file } : {})`。 带 file 的发现照旧带 file,项目级的那两条不再带。 **新增回归测试** `test/90-audit-tool.test.mjs`(5 项)。它不钉某个具体字段,而是断言 **整个返回**能无损往返——这条判据不管以后哪个字段被写成 `undefined` 都会红。 为了让 `source_flat` 真的出现,测试会造出「7 个源码文件、只有一层目录」的形状。 > 诊断过程记一笔:`inspectSource` 与 `auditOf` **单独跑都可序列化**,所以问题只能在 > 「组装工具返回」那一段——**只有真调一次工具才覆盖得到**。 > 这也是为什么加的是工具级测试,而不是给 `lib/source.js` 加单测。 `npm test` 全绿:60 + 47 + 7 + 51 + 13 + 36 + 12 + 5;跨插件握手 18 / 0。 ### v0.26.0 · 固定收尾问可全局关闭 用户原话:「提问到最后还要选继续还是停下?的功能加一个全局关闭功能。」 面板左栏新增 `固定收尾问:开 · 点此关闭`。关掉后:提示段换成「已全局关闭、不要再问」的 另一份正文;工具返回 `askPause:false` 且**不再下发** `pauseQuestion` / `pauseOptions` (留着文案模型就会继续问);拒绝理由里也不复述收尾问;面板那句「每次提问最后都会问…」 如实改成「已全局关闭」。 **默认开,且缺字段算开**——语义是「默认行为」不是「新特性」,反过来会让老用户静默变行为。 开关是**全局**的(`$DSH_HOME/.dsh-puzzle-mode.json`),与「关掉本会话的拼图模式」 是两个开关、共用一个文件但互不覆盖。 顺手修掉一处测试脆弱性:`10-puzzle` / `30-rpc` 会读运行者**真实的** `DSH_HOME`, 用户关掉收尾问就会让 `npm test` 变红而代码没错;现统一走 `test/helpers/isolate-home.mjs` (必须在其它 import 之前——ESM import 提升,写文件中间赋值环境变量太晚了)。 实测把真实设置改成关闭,全套测试仍绿。 新增 `test/80-ask-pause.test.mjs`(12 项)、`50-contract` 补 2 条契约锚、 `tools/verify-ask-pause.mjs`(7 项,走真实 RPC 验三处接线)。 `npm test` 全绿:60 + 47 + 7 + 51 + 13 + 36 + 12;跨插件握手 18 / 0。 ### v0.25.0 · 换主题:主题不在包里,点一下从主题仓库下 面板右上角一颗主题图标 → 全屏主题管理页(卡片墙 + 下载并应用 + 卸载 + 顶部常驻「恢复默认」)。 主题**不随插件打包**:`package.json` 的 `files` 里没有任何主题目录,`lib/` 下也没有 `.css`, 皮肤全部从 `liancha22/dsh-puzzle-themes` 现取。 **下载链路**:先拉几 KB 的 `index.json`(列表 + 预览色 + `sha256`),点应用才下该主题 CSS; 镜像 jsDelivr → gh-proxy → raw 顺序回退(本机实测 GitHub 直连超时,raw 也不通, 只有前两条可用,所以单镜像方案不可接受);下完比对 `sha256`,不符拒绝安装且**不覆盖** 已装主题;落盘 `$DSH_HOME/puzzle-mode-themes/.css`;清单拉不到就用上次缓存并明示「当前离线」。 **只允许 CSS**(用户先选「允许带 JS」后改口「不要 JS,只 CSS」,以后者为准): 插件里没有任何 JS 求值路径。下载的 CSS 逐条过白名单——`@import` / 外部 `url()` / `javascript:` / `expression(` / `/{manifest.json,theme.css}` + `tools/build-index.mjs` 现算清单(hash 手写必漂移)。首发 4 套:樱花 / 墨黑 / 终端绿 / 深海。 插件侧新增 `tools/verify-themes.mjs`:拿**插件自己的校验器**去验**主题仓库的实际产物** (本地模式 + `--remote` 走真实下载路径验镜像),补上「两个仓库各自测自己那一半」的缺口。 **测试**:新增 `test/70-themes.test.mjs`(36 项)。关键一组是拿同一批恶意样本**同时喂给 宿主半与浏览器半**——浏览器半是手写 module-loader 包、拿不到 ESM 导出,所以重复实现了 一遍硬规则;重复实现必然漂移,除非有测试同时钉两边。另加两条「主题不在打包里」的双向 守卫(`files` 不含主题目录 **且** 下载通路必须在包里——只测前者的话删掉功能也算通过)。 `test/30-rpc.test.mjs` 补 6 条 theme RPC 用例(全部在**未绑项目**的会话上跑,钉住 「主题与项目无关」)。顺手修掉 10-puzzle 里一处不可移植断言(写死 `/tmp`,Windows 上恒红)。 `npm test` 全绿:60 + 47 + 7 + 49 + 13 + 36;跨插件握手 18 / 0。 ### v0.24.2 · 审查器没跟上「项目规模」——大档项目被全线误报 用户原话:「审查器没有同步规模改动 / 看看提示词有没有旧的无规模的限制提示词」。 两处都成立,审查器那边是**真 bug**。 **① 审查器三处写死了中档上限**(写入放行、审查报警): | 位置 | 原先(写死中档) | 改成 | | --- | --- | --- | | 条数上限 | `ENTRY_CAPS` = 4 / 10 | `capsOfSize(档位)` | | 模块条目字数 | 中档 20 字 | 按档位(大档 40 字) | | 主文档条目字数 | 中档 50 字 | 按档位(大档 80 字) | 写入侧一直按档位放行,审查侧却拿中档去量——**大档项目每一条合法内容都被报成违规**。 实测对照(大档写 31 字要点 + 12 条已定): ``` 修前:报「要点超长(limit 20)」+「已定 12 条超过上限 10 条」 修后:两条都归零 ``` 这是本仓记过的「两把尺子」:**同一份规格必须两侧同源**。 **② 提示词四处旧上限**:文档锁理由(**每次拦 write/edit 都回给模型**)、提示段「文档分工」、 「条目级发现优先」、工具 schema 的 `content` 说明——原先都写死中档值。 而提示段**自己两说并存**:一行说死数字、下一行说别信死数字。现在统一改成 「随项目规模变,以 `op:read` / `op:size` 返回的 `limits` 为准」。 工具 schema 保持静态(提示缓存);工作流的名字 / 步数上限仍写死——那两个数与规模无关。 新增 1 条断言,**双向**钉住:大档合法内容不许被报 **+** 中档已有的超长条目仍须被报 (只测前者的话,把上限全改成 `Infinity` 也能绿)。两处变异验证均红。 `npm test` 全绿:60 + 41 + 7 + 49 + 13;跨插件握手 18 / 0。 ### v0.24.1 · 两处「看着烦」的收拾:接续不翻代码 + 空态删占位 两处都是用户直接点名的**观感/轮数**问题,没有新能力。 **① 接续会话不再自己翻代码推进度**(用户原话:「把接续会话里的分析代码删了吧, 这种事交给专门的审查就行了,不然拆来拆去看得我烦」)。 模板第 5 步原先是「结合代码与文档的**当前状态**判断进度(哪些写完了、哪些还是 空壳 / TODO)」,删掉,换成「从文档里读到的断点直接接着做」。理由与 v0.21.0 删 「报五维最弱项」完全一致:接续会话的读者是**干活的人**,不是评审;自己翻代码推进度 也是「先做一轮评估」,而且**更贵**——它要真的把源码读进来。进度该由 `op:audit` 的客观发现给。 > **刻意保留**第 4 步「按源码索引跳、**不要全仓搜**」:那是**防乱翻的护栏**, > 删了反而更容易满仓找。断言里专门有一条反向保护钉着它。 **② 空态中栏删掉虚线占位卡**(用户原话:「把未绑定态面版中间的白色大块占位删了,没用」)。 那块卡(图标 + 标题「本会话还没绑定拼图项目」+ 一行灰字)与面板顶部副标题 **重复**,又占着中栏最值钱的位置,把三个真按钮往下推。 但那行灰字**不能一起删**——它是回答「我的项目怎么不见了」的信息(新会话不会自动 占用上一个会话的项目),而且**有一条断言钉着它**(动手时当场报红)。 所以处理成**删占位、留信息**:去掉虚线框与图标,那行字降级成一行普通提示。 随之成为孤儿的 `inbox` 图标一并清掉。 新增 8 条断言(接续 5 条 + 空态 3 条),**变异验证均红**。 `npm test` 全绿:59 + 41 + 7 + 49 + 13;跨插件握手 18 / 0。 ### v0.24.0 · 重复思考熔断:模型绕圈时把它推出去 长任务里模型会陷进「同一步反复做同一件事」——反复读同一个文件、反复跑同一条命令。 每一轮都要把**整个上下文重发一遍**,空转的每一轮都是实打实的成本;而模型自己往往 出不来,因为它**看不到「我刚做过」**。本版给宿主加了一道熔断:**同一个工具连续 3 次 用同样的参数调用**,就注入一条提示,把模型推出去。 - **判据是工具调用签名**(工具名 + **键排序后**的参数),不是推理文本。抓思维链要读 reasoning,形状随模型/版本变、拿不稳;动作层的指纹稳定且可纯函数测试。 键排序是必须的:不排序时 `{a,b}` 与 `{b,a}` 会算出**两个签名**,连击永远数不到 3, 熔断就成了**永不触发的死代码**。 - **挂在 `tools/post-execute`**,不是 `agent/pre-step`——后者 `decision.messages` 的契约是 `UserMessage[]`,**读不到 tool-call**(本仓在 40-pre-execute 的开头已用假绿换过这个教训)。 `post-execute` 的 `additionalContexts` 是唯一能「不拦、不打断、只提醒一句」的通道。 - **三条误报防线**(宁可漏报,不可误伤):无参调用一律不计(`job_list` / 截图这类轮询 反复调是正常行为);阈值 3(连续 2 次极常见——改完再跑一次测试);同一段连击只报一次 (报过要等签名变了才重新武装,否则第 4、5 次各报一条,比不报更烦)。 - **提示给三条出路**:换输入 / 换动作 / 停下来说清卡点。语气是陈述事实 + 给出口, 不是训斥——模型不是不听话,是它看不到自己刚做过。只说「别重复」等于没给信息。 - 提示段补第二道兜底(hook 判定 + 提示段),与「首轮自动判定」同一条设计。 新增 13 条断言,**两处变异验证都真的红了**(拆掉钩子 / 去掉键排序)。 `npm test` 全绿:59 + 41 + 7 + 49 + 13;跨插件握手 18 / 0。 ### v0.23.3 · 修「项目规模」这条线上的三个 bug(其中一个是静默删数据) **① 重写 front-matter 漏透传 `规模:` → 静默删除条目**(严重,会丢数据) `op:main`(写坑 / 写工作流)、`writeWorkflowDoc`(写归档)、`writeSessionList`(改会话绑定) **三条路径重写 front-matter 时都漏了透传 `规模:`**。于是「追加一条坑」这种**与档位无关**的写入 会把 `规模: 大` 抹掉 → 下次读按**中档**算 → 中档的 `pit` 上限更严 → **下一次写入静默删掉超出的条目**。 **这不是理论推断,是本插件在自己的文档上真的发生了**:主文档「坑」从 **149 条被砍到 60 条**。 被删条目只出现在返回值 `droppedEntries` 里,界面上**没有任何提示**。 (`setMainFields` 本来就有这一行、注释里也写明了这个后果;另外三条路当初漏了。三处已补齐。) **② 大档的「坑」比中档更严——切档反而收紧** `中.pit = null`(不限)而 `大.pit = 60`。实测: | 档位 | 写 70 条坑 | 结果 | | --- | --- | --- | | 中 | 落盘 71 条 | 全留 | | 大 | 落盘 60 条 | **被砍掉 10 条** | 这恰好违反分档的初衷(大项目更不该丢内容),而「坑」是**只增不减的踩坑记录**。 改成 `大.pit = null`,恢复单调性(小 10 ≤ 中不限 = 大不限)。 客户端 `FALLBACK_LIMITS` 同步——契约测试「与宿主逐字段相等」当场抓到这一处。 **③ 切项目时面板沿用上一个项目的模式** 用户报「执行模式继承到下一个打开的面板」。**不是数据写串了**(宿主侧两个项目各写各的,验过), 而是切项目时只改了「当前是哪个」的标记、**没换 `mode`**,而重读是异步的: ``` ① 当前 A(只拼不写) 高亮 = 只拼不写 ② 在 A 点「边拼边写」 高亮 = 边拼边写 ③ 切到 B(回包未到) 高亮 = 边拼边写 ← 这是 A 的模式 ④ 回包到达 高亮 = 写后再拼 ← 最终才对 ``` 改成从胶囊**就地**取目标项目的 `mode` / `health`(胶囊本来就带这些字段,不需额外请求)。 **新增 3 条断言,逐条做过变异验证(去掉修复即红)**,其中一条是**7 条写入路径逐个过一遍** (写坑 / 写工作流 / 写模块 / 改模式 / 改源码根 / 写会话 / 写归档),而不是只测踩到的那条。 > ⚠️ 顺带记一笔:本轮写断言时**又踩到一次假绿**——切项目那条最初用 `await setTimeout(0)`, > 但本地更新是在**微任务**(`.then` 回调)里跑的,而 `setTimeout` 是宏任务: > 回调还没跑就读树,看到的是「点了没反应」那一态,**断言与修复无关**。 > 改成排空微任务才咬住。这已是连续第三个假绿变体(宏任务越过回包 / 假 React 的 > `useEffect` 每渲染都跑 / 微任务未排空),三种成因都写进了注释。 `npm test` 全绿(59+41+7+49),跨插件握手 18/0。 ### v0.23.2 · 修两个面板 bug:胶囊模式「弹回去」+ 规模切换「假态/延时」 两个都是**真机报出来的**,都不是界面崩掉,而是**显示的值不对**——所以三个断言都写成 「断言渲染出来的文本 / 高亮」,而不是断言内部字段(读错字段时界面照常渲染,不报错)。 **① 写操作返回缺绑定组 → 胶囊上的模式「点了又自己弹回去」** `mode` / `size` / `current` / `workflow` 的返回原先只有 `summarize(readState(...))`, 里面**没有 `bindings`**;而客户端 `mergePanelData` 的规则是「新值没给这个键就沿用旧的」。 于是它防得住「胶囊塌掉」,防不住**值过期**——胶囊的悬停提示直接渲染 `item.mode` (健康性同理由 `%` 渲染),点完「边拼边写」胶囊仍显示改动前那个模式。 修法:抽 `bindingPanel(...)` **一处**算绑定组,`state` 与四个写操作共用同一份。 **② 项目规模切换「假态」+「延时切换」** - **假态**:那行「当前上限:悬而未决 ≤x · 已定 ≤y …」原先读 `limits.sizeCaps`—— 那是「小/中/大**三档的整张表**」,不是当前这一档。`caps.pending` 恒为 `undefined`, 靠兜底**永远显示中档数字**:切到「大」显示「已定 ≤10」,切到「小」也显示「已定 ≤10」。 真值在 `limits.entryCaps`。 - **延时**:按钮只在**回包后**才变(写盘 + 重读 + 回包)。改成乐观更新,**按下即翻档**; 同一处纪律也补给了**执行模式**按钮。两处的失败分支都改成**回宿主重读**—— 乐观更新已经把界面翻过去了,不拉回来就会停在一个没写进文档的假态上。 **新增 4 条断言,逐条做过变异验证(把修复去掉就红)**。其中一条踩过的坑写进了注释: 这个测试用的假 React 的 `useEffect` 是**每次渲染都跑**的,而面板 effect 里有 `load()`, 所以只要 `await` 一个宏任务,那次 `state` 拉取就会把乐观更新冲掉—— **断言看到的其实是回包后的状态,把修复整个删掉照样绿**(第一版就是这么假绿的)。 改成「扣住回包 + 同步读树」才真正咬住。 `npm test` 全绿(58+41+7+49),跨插件握手 18/0。 ### v0.23.0 · 项目规模(小 / 中 / 大)+ 删掉面板那颗审查按钮 **① 项目规模三档**——固定上限对两头都不合适:小项目的「已定 10 条」写不满, 大项目的「悬而未决 4 条」一上午就顶满、**最旧的决策被静默挤掉**。 | 档位 | 悬而未决 | 已定 | 工作流 | 坑 | 字数上限 | | --- | --- | --- | --- | --- | --- | | 小 | 4 | 6 | 3 | 10 | 同中档 | | **中**(默认) | 4 | 10 | 5 | 不限 | 同升级前 | | 大 | 12 | 30 | 12 | 60 | 放宽(要点 40 字等) | 存主文档 front-matter 的 `规模:`(跟着项目走);**中档 = 升级前的全部值**, 所以没写过这一行的存量文档一个都不用改。面板五维下面有三颗按钮,或 `op:size`。 「照现有项目搭文档」也加了一步:让 AI 按真读到的行数/模块数**自判档位**。 **② 删掉「审查 / 真实值」按钮**(用户裁定「看项目的审查没什么用」):连它配套的 「真实值(声明 → 实测)」显示区一起删(没按钮拉数据,那块永远是空的)。 **审查能力没动**:`op:audit` 还在,「让 AI 动手」区的「审查(交给 AI)」仍可用。 **③ 缓存检查修掉一个真问题**:提示段原先用中档固定值告诉模型「悬而未决 ≤4 条」, 而大档实际能写 12 条——**两边数字不一致**。修法是提示段只说「上限随规模变,看返回里的 `limits`」(保住常量与缓存),真实值由 `receipt` 按当前档下发。 ### v0.22.0 · 与「无限五代」同装时按契约共存 两个插件都会往系统提示里插常驻规则,而且都曾**自认「最后一段」**(本插件写死 `order = 10500`,无限五代的末位锚点是 `10150`)——同装时那句话必然有一边是假的。 本版把跨插件共存并回主线: - **段序可协商**:默认 `10120`(小于对方末位锚点 `10150`,于是它的末位声明成立), 可用 `PUZZLE_SECTION_ORDER=10500 dsh ...` 覆盖回旧行为;导出 `SECTION_ORDER_VALUE` 供外部核对。 取 `10120` 而非 `10100` 是有意的——**内置段序表里已有 `WEB_SURFACE: 10100`**, 同号时段序相同时按段名比较,位置就不再由协商决定。 - **六条分工条款写进提示段**:域划分 / 提问额度按轮 / 批量题不打断 / 工具形态 / 停下不回退 / 末位让位。 两边的规则落在同一份提示里,不靠外部约定。 - **把「会不会漂」变成能自动发现**:`compat.json` 契约 + `npm run verify:cross` (已挂在 `npm test` 链尾,装了对方就双向互校)+ CI 门禁 + 2 条钉字面量的契约断言。 对方不在场时走「互校跳过」分支并标注,**不判失败**——CI 上没有那个仓库。 详见 [COMPAT.md](COMPAT.md)。 ### v0.21.0 · 多绑定真正打通:扫描建多个 + 迁移/接续作用于全局 三条需求,都落在「一个会话绑了多个项目」这件事上——v0.20.0 只把绑定做成了多对多, **围绕它的动作还是单项目的**。这版把它们补齐。 **① 扫工作区 · 一次建多个** 空态新增一颗按钮:**扫工作区 · 一次建多个**。宿主扫工作区顶层目录,列出 **还没有拼图文档**的那些,每条带上目录里的文件名当证据;勾选后一次建齐, **全部追加绑定**(最后一个成为当前项目)。 - 为什么由宿主扫而不是让模型读工作区:目录清单是**客观事实**,让模型去数, 不同轮次答案会不一样;它看不到真实文件树时只能编。 - 判据**宽**(纯数据目录、脚本集合也算项目,漏掉比多列糟),但噪音目录挡掉: `node_modules` / `.git` / `.cache` / 隐藏目录等(本项目工作区实测 10 个目录、 9 个已有文档,扫出 1 个真候选)。 - 为什么不让它「扫到就自动全建」:顶层常混着实验目录,全自动会建出一堆没人要的文档。 **插件摆事实,勾选留给用户**。 **② 迁移 / 仅迁移格式 · 接续会话 → 作用于全局** - `op:rebuild` 与 RPC `rebuild` 新增 `all:true`:遍历本会话**全部绑定**,逐项目回报改动数, 预览**逐项目分组**(不是只渲染当前那个的 `files`——那会显示「已经是当前格式」这种**假话**)。 - 面板在多绑定时自动带上 `all`:迁移是「把这份文档升到当前格式」,与「我现在在看哪个」无关, 只迁当前那个会留下几个格式不一致的文档。 - 接续会话提示词列出**全部绑定项目**,并说明接续是全局动作。 **③ 接续会话提示词:直接接着上一个会话的活干** - 主旨从「怎么读文档」挪到「**接着上一个会话没干完的活继续干**」,并明确「不要重新问我需求」; - **删掉**要求汇报「当前最弱的一维是哪一维」那段——接续会话的读者是**干活的人**,不是评审, 让它先报五维最弱项等于把「继续做」变成「先做一轮评估」; - 保留「按需读、不通读」(全量读一遍实测 8888 字符,而本轮真正用得到的通常只有一两个模块)—— 这是**手段**,不是目的。 **途中挖出一个真 bug(做 ② 时暴露,已修)** v6→v7 迁移的判据原本是「有 `会话:` 但没有 `当前会话:`」——可这个判据**只看单个项目**, 而「当前是哪个」是**工作区级**事实:一个会话绑三个项目时,只有当前那一个该带 `当前会话:`, 另外两个**本来就不带**。于是全局迁移给三个都补上,**三个同时自称当前**。 后果不是显示错乱(排序按扫描顺序,看着还正常),而是**解绑当前项目时静默把当前身份 送给一个从没被选过的项目**(实测:解绑 beta 后 gamma 自动当上当前)。 **修法**:判据带上版本——新增 `CURRENT_SESSION_VERSION = 7`,只有 `docVersion < 7` 才补。v7 文档不带这一行是**合法状态**(绑着但不是当前),不是「缺字段」。 > 这个 bug 单项目路径下**到不了**(只会给当前项目补),是「作用于全局」把它从 > 潜伏变成了**一键可触发**。加了两条**方向相反**的断言:v7 非当前不许补、v6 必须补且幂等。 **顺手加固一处同形隐患** `data.bindings` 在宿主侧有**两种形状**:`state` RPC 给对象数组(`{project, health…}`), 而 `current` / `unbind` 给**字符串数组**。按字段取值的写法(`.map(b => b.project)`) 遇到字符串数组会得到一串 `undefined`——`length` 看着还对,`all` 却被静默判成 `false` (迁移又只作用于当前项目,正是这版要根治的行为)。 这个坑在本仓出现过一次(v0.20.0:`...extra` 把对象数组盖成 `[undefined, undefined]`), 所以抽了 `boundNamesOf` **两种形状都认**,凡按字段取 `data.bindings` 的地方一律走它 (3 处:迁移的 `all` 判断、接续会话的绑定列表、切换条渲染)。 **验证**:新增 9 条断言,**逐条验过「把 bug 改回去就红」**。 `npm test` **145 项全绿**(58+37+7+43)。 ### v0.20.5 · 迁移/重构提示词与实现脱节(v0.20.0 改了行为没改提示词) **「迁移/重构」本身就是一段提示词**(`refactorTemplate`,面板按钮把它填进输入框)。 而 **v0.20.0 的 diff 从 `client.js` 第 343 行才开始——模板区 60–290 行一行没碰**。 于是这段提示词自己与现行规格直接冲突: | 提示词写的 | 现行规格 | 后果 | | --- | --- | --- | | 「每条必须带(源码: 文件:行)」 | `## 工作流` **不在** `MAIN_ENTRY_SPEC` 里,不要求出处 | 让模型给工作流步骤**编行号**——同一段里还写着「不许编行号」 | | 「坑 20 字、其余主文档条目 50 字」 | 工作流是 `### 名字 ≤20` + 步骤 `≤80` | 模型把**合法**步骤砍到 50 以内 | 第二行正是 v0.20.2 那个假发现的**镜像**:那次是**插件**拿 50 的尺子量 80 的步骤, 这次是**提示词让模型自己砍**。 **同一批还清了 5 处**(都是「实现改了、说法没改」): 1. **模板区数字全部硬编码**——`var ASK_MAX_QUESTIONS = 10` 与正文里的 20/50/4/10/12/80/5。改 `constants.js` 时模板不跟随,**提示词把过期数字交给模型、 模型照写、写入被拒**,违反本项目自己那条「只改一处即可」。 现在宿主每次 `state` 都下发 `limits`(`PANEL_LIMITS`),模板一律引用; 客户端那份 `FALLBACK_LIMITS` 只是「首次数据到达前」的兜底,由契约测试钉住与宿主相等。 2. **提示段自相矛盾**——`### 项目` 开头写「一个会话只绑一个项目」, 同一份提示段末尾的 `### 会话与多绑定(v7)` 又写「可以同时绑多个项目」。 模型每轮都读到两条**相反**的规则。 3. **`op:init` 说法过期**——提示段还写「建完自动绑定本会话,并从别的项目上解绑」, 而 v7 起 `op:init` 走 `addBinding`(**只追加、不解绑**)。 4. **面板上用户可见的假话**——单绑定「切换条不显示」,实际 `bindingSwitcher` 的注释与实现都明确「不做这个退化」(用户当场抓到过一次)。两处按钮提示已改。 5. **「项目下拉」残留**——面板两处提示还让用户去点原生下拉, 而它在 v0.20.0 就换成了绑定切换条。 **同一形状还有第三处**:`op:main` 的 `content` 参数描述里写着「工作流 50 字」—— 而工作流真正的尺子是 `### 名字 ≤20` + 步骤 `≤80`(写入侧走 `normalizeWorkflowEntries`, 不是条目规格)。`ENTRY_LIMITS.workflow = 50` 只是个**占位值** (`entries.js` 里写明「无实际校验作用」),任何地方都不该拿它当上限。 这是同一个「两把尺子」bug 的第三处:v0.20.2 修审查侧、本轮修面板提示词侧、本次修工具描述侧。 **v7 形式补齐**:`interviewTemplate` 补了「op:init 是追加、`rebound:false` 要补 `op:bind`」, `bindTemplate` 补了「op:bind 是替换全部、想追加走 +」。 **验证**:新增 6 条回归断言,**逐条验过「把 bug 改回去就红」**—— 模板写死数字 / `FALLBACK_LIMITS` 漂移 / 提示段两说并存 / 面板声称不显示切换条 / 迁移提示词缺工作流例外 / 残留 v6 绑定口径 / 工具描述拿 50 当工作流尺子。共 **139 项全绿**(56+35+7+41)。 ### v0.20.4 · 切换项目「切着切着胶囊没了」 **根因**:面板把**写操作的返回**整体当成新 `data`,而 `mode` / `workflow` / `bind` / `current` 的返回都是 `summarize(readState(...))`——**一个都不带 `bindings`** (只有 `method:'state'` 带)。于是每做一次写操作,`data.bindings` 就变成 `undefined`, 切换条塌成「只有当前项目」的兜底,要等下一次轮询(最多 8 秒)才恢复。 实测复现:**点一下执行模式按钮,胶囊就从 `["aaa","bbb"]` 变成 `["aaa"]`**。 切项目只是最容易撞见的那个入口。 **修法**:客户端加一层 `mergePanelData`——写操作的返回**没给**某个字段就沿用旧的 (给了就用新的,包括空数组)。一次修好 `mode` / `workflow` / `bind` / `current` 全部。 > 另一种修法是让宿主每个写操作都带上 `bindings`,但那要给每个绑定项目都算一遍健康性 > (`readState`),与「返回体积 ∝ 本次改动」的纪律相反——而且**绑定组本来就没变**, > 没有新信息可言。 **过程中的一处诚实交代**:我同时加了 `markCurrentLocal`(切当前后本地把当前项改掉), 但**没能稳定构造出它单独失效的场景**——试了扣住 `state`、按名字/位置读高亮等几种测法, 去掉它之后断言仍然绿。所以它**没有**配套回归断言,价值未经独立验证, 代码注释里写明了这一点。真正防「胶囊塌掉」的是 `mergePanelData`(那条有断言,去掉就红)。 ### v0.20.3 · 多绑定模式点「绑定」闪红框:守卫把两种载荷搞混了 用户原话:**「我发现选中以后点绑定不行会闪出红框」**。 **根因**:多绑定模式勾选后点「绑定选中的 N 个」,客户端发的是 `projects: [...]`、 **没有 `project` 字段**。而 `bind` 的入参守卫写成「没有 `project` 就 400」, 且它**排在 `projects` 分支之前**——于是这条请求永远被挡在门外,面板弹「缺少 project」。 **修法**:守卫挪到 `projects` 之后,语义改成「**两个都没给**才算缺参数」。 两处都补了断言:服务端(`30-rpc`:多选请求必须 200 且两个都绑上、最后一个是当前) 与界面(`20-client`:勾选 → 点按钮发出的载荷**只带 `projects`**)。 > 这条 bug 的形状值得记一笔:**同一个接口的两种载荷(单选 / 多选)被同一个守卫挡住**。 > 守卫只想着「主流那一种」,另一种就被判成「参数缺失」——而错误信息还挺像回事 > (「缺少 project」),排查时容易往客户端找,其实客户端发得没错。 ### v0.20.2 · 审查把 `## 工作流` 当条目量,长期挂着 7 条假发现 **这个 bug 也是「做完上一版顺手跑审查」发现的**——本项目的审查里一直有 `## 工作流` 那一栏「7 条超长」,而那些步骤**全都是合法的**。 **根因**:`MAIN_ENTRY_SPEC` 由 `SECTION_ORDER` 直接生成,把 `## 工作流` 也当成 **条目小节**,于是拿 `ENTRY_LIMITS.workflow = 50` 去量**每一行**。 可步骤的合法上限是 `WORKFLOW_STEP_LIMIT = 80`——**两把尺子不一样**。 51–80 字的步骤被判超长,而写入侧(`checkWorkflowBlocks`)明明收得下。 **修法**:把 `workflow` 从条目规格里**排除**——它本来就不是「一句话条目」, 而是一条流水线块;它的形状校验有自己的那把尺子(名字限长 / 步数 / 每步 ≤80)。 加契约测试钉住:断言 `MAIN_ENTRY_SPEC.workflow === undefined`,并端到端写一条 60 字的步骤确认审查不报它。**把 bug 改回去,测试立刻红。** > 顺带清掉了本项目文档里最后一处真发现:「写操作回吐整份状态浪费35倍」—— > 这是 v0.19.9 的 ② 机制(数字须带测法)报出来的,改成「实测:回吐7026字而回执1192」。 ### v0.20.1 · 修一个「老文档头永远与正文矛盾」的迁移 bug **这个 bug 是本次做完 v0.20.0、顺手给项目自己的文档跑迁移时发现的**—— 也就是说,它一直挂在**本项目自己的主文档**上: 文件头写着 ``` > 只有四节:模块索引 / 源码索引 / 工具索引 / 坑。决策与轮汇报在 `模块/` 下。 ``` 正文却早就是**五节**(v4 加了 `## 工作流`)。跑多少次「迁移/重构」都修不掉。 **根因**:迁移判据是「整行**完全一致**才替换」,而 `OLD_PREAMBLE_LINES`(旧说明行表) 里只列了 v4 之后的「只有五节:…」。v3 时代写的是「只有四节:…」, 认不出来就**原样留在文件头**——读文档的人先看到的正是那句错的。 **修法**:把 v3 的四节说明行补进旧表,并加契约测试钉住——断言里直接用那一行原文, 再跑一遍真实迁移确认它被换掉、且第二次幂等。**漏哪一版,测试就红在哪一版。** > 这条坑的教训比修法本身重要:**旧写法表是「靠列举」的,列举不全就等于没修**。 > 所以判据不能只靠读代码看着对,得有一条断言直接喂那一版的原文。 ### v0.20.0 · 一个会话同时操作几个项目 需求原话:**「添加一个新会话可以绑定多个拼图文档的功能,并且将绑定后切换拼图文档那个条的范围缩小为选择绑定的那几个并画个 UI(现在用的是默认的)」**。 v6 及更早有一条不变量:**一个会话只绑一个项目**——`bindSession` 靠「先解绑全部再绑目标」 维持它。这一版把它放开成一对多,连带三处语义都变了: | 变化 | v6 及更早 | v0.20.0 | | --- | --- | --- | | 绑定 | 一个会话绑一个项目 | **一个会话可绑多个**,主文档新增 `当前会话:` 标记「当前是哪个」 | | 建项目 / `op:bind` | 都走「替换全部」 | `op:init` = **追加**(建项目不该抹掉别的绑定);`op:bind` = 替换全部 | | 拦截 | 只看当前项目 | **按全部绑定里最严的那个**算——任一项目是只拼不写就拦 | | 工作流触发 | 只看当前项目 | **按命中的项目各自注入**,并点名属于哪个项目 | | 面板切换 | 原生下拉,列工作区**全部**项目,选中即改绑 | **绑定切换条**,只列**绑定的那几个**,选中即换当前 | **切换条画成什么样**:绑定 2–4 个时是一排**胶囊**(当前项高亮、带健康性百分比、 悬停出 `×` 只解这一个),≥5 个换成**卡片列表**(多显示模式、模块数与健康性进度条); 末尾 `+` 展开工作区里还没绑的项目,点一个即追加。空态那栏新增**单绑定 / 多绑定**模式选择。 **「当前项目」为什么要落盘**:它是**状态**,不是列表顺序。你从面板切到 B, 下一次工具调用就该落在 B——靠列表顺序或 mtime 都会漂(「我刚往 A 写了一行」会把当前拽回 A), 那种漂移没有可见起因,用户只会觉得插件不听话。所以主文档多一行 `当前会话:`, 不变量是 **`当前会话:` ⊆ `会话:`**(写盘时收口)。 **文档格式 v6 → v7**:v6 及更早「唯一那个绑定就是当前」,所以迁移把 `会话:` 的第一个 补成 `当前会话:`——**无信息损失**,补出来的值与原行为完全一致。老文档跑一次「迁移/重构」即可。 **途中抓到的两个真 bug**(都是「看起来写完了、其实不生效」那一类): - `createProject` 仍用替换全部的 `bindSession` → 建第三个项目会**静默抹掉前两个绑定**; - 我的 `located()` 返回里也有个 `bindings` 字段(名字数组),和 `op:bindings` 的 **对象数组**同名,`...extra` 展开顺序一错就把对象盖成 `[undefined, undefined]`。 **还有一个被用户当场抓到的**:我做过一个「只绑一个就退化成一行纯文字」的优化—— 单绑定是绝大多数情况,等于**切换条平时根本不存在**。已删掉,并把 「绑几个就画几个胶囊」写成了回归断言。 ### v0.19.9 · 三条通用约束:别靠「记住」,靠「撞见就红」 用户原话:**「你说你现在的自觉性还不强,有什么通用建议给拼图插件增强约束」**。 这一轮暴露的规律很清楚: | 规则 | 写在哪 | 结果 | | --- | --- | --- | | 「description 别堆版本历史」 | `PUBLISH.md` **早就写着** | ✗ 我还是违反了——改 `package.json` 时**撞不见它** | | 「测试要跑」 | 先只写文档 | ✗ 没用;加了 `release.sh` 门禁 → ✓ 真管住了 | | 「条目要带出处」 | `checkEntry` **写入时校验** | ✓ 从没漏过——写的时候就红 | **同一条规则,「写在文档里」和「在动作那一刻报错」是两个物种。** 三条约束都朝这个方向做。 | # | 约束 | 关键设计 | | --- | --- | --- | | ① | 工作流可写 `触发: 关键词`,命中该动作时自动注入 | 落在 `tools/post-execute`(`additionalContexts`):**不拦、不弹窗**,工具照常成功 | | ② | 数字须带测法,否则 audit 报 warn | 只认 `ms/µs/KB/MB/倍` 这类**换个机器就会变**的度量;`≤4 条`「五节」是规定,不报 | | ③ | 写「已定」时回显现有条目 | **不做自动判冲突**——实测字面算法拿不到可用阈值(真冲突只 1 分) | **① 为什么不用 `pre-execute`**:它只有 `allow`/`deny`/`ask`——`deny` 会拦下工具(改个 `package.json` 就报错),`ask` 会弹审批框(比不提醒更烦),而 `allow` **不能附带任何信息**。 `post-execute` 的 `additionalContexts` 才是「非阻塞提醒」的唯一通道。 **③ 为什么只回显不判断**:按 2-gram 打分时,真冲突「不写测试不跑测试」vs「要跑测试」 **只拿 1 分**(只共享「测试」两字)——阈值 2 会漏、降到 1 会误报。根因是 **「矛盾」是语义关系不是字面关系**。所以只做确定能做对的那一半:把旧条目摊出来让模型自己判。 **三条守卫都验证过真能抓到破坏**:让 ① 不写回 `触发:` ✓ 红、把 ② 改成拒绝 ✓ 红、 关掉 ③ 的接线 ✓ 红(这条**最初漏过**——只测纯函数时接线断了照样全绿,已改成走真实写入路径)。 **边界**:这三条**都不是**「保证模型不犯错」,只是把**违反变得可见**。 真正硬的是「条目必须带出处」那种**写入时直接拒绝**的规则;②③ 受限于语义判断做不准,只能做到可见。 ### v0.19.8 · 放开本项目的测试约定:测试曾经整片脱节,现在有门禁 用户裁定:**「这个项目可以放开约定,记得写入文档,开始做并作为 v0.19.8」**。 放开不是"测试突然有用了",而是**不守的代价被实测到了**:文档格式 v4 → v6 改造后, 断言停在旧格式(六节主文档、`related` 合体小节、项目级健康性、裸条目),实现早改了, 于是 `npm test` 长期**三红一绿**——**红成了常态就没人再看它**, **v0.19.7 的 tag 是在测试全红的状态下打出来的**——是 [@SunsetRNE](https://github.com/SunsetRNE) 在 [PR #2](https://github.com/liancha22/dsh-puzzle-mode/pull/2) 里指出并修好的:他把 3 个测试文件(+201 / −180)对齐到现行实现,`npm test` 由三红一绿转为全绿。 本版的契约测试与发版门禁是接着他的方向做的。 > ⚠️ 全局约定「不写测试、不跑测试」**只对这一个仓放开**,已写进 `~/.dsh/AGENTS.md` 第 0.1 节。 三条防复发: | # | 措施 | 关键点 | | --- | --- | --- | | ① | 断言**引常量** | 不再写死「5 问 / 六节 / `related`」,改引 `lib/constants.js` | | ② | 新增**格式契约测试** `test/50-contract.test.mjs` | **故意钉字面量**——格式一变必须红 | | ③ | `release.sh` **发版门禁** | 测试红 → 拒绝建 Release;`--skip-tests` 必须显式写且留痕 | **②与①方向相反是刻意的**:常量与模板同源,改常量时两边一起动、测试照样绿。 实测漏过——把 `SECTION_HEADINGS.pit` 改成「## 踩过的坑」,只引常量的版本 **15 条全绿**; 加了「契约锚」字面量断言后才拦得住。这个文件还留了一条**「守卫的守卫」**, 防止 `extraSectionsIn` 哪天退化成恒返回 `[]`、让上面两条变成永远通过的空断言。 守卫能力**实测过**(不是写完就算):模板多插一节 ✓ 红(这条最初漏过)、漏掉一节 ✓ 红、 常量改小节文案 ✓ 红、条目超长/缺出处 ✓ 红。 ``` npm test → 10-puzzle 55 项 / 20-client / 30-rpc 31 项 / 40-pre-execute 7 项 / 50-contract 19 项 → 退出码 0 ``` **边界**:这次放开的是**地板**不是**天花板**——测试全绿 ≠ 功能可用, 真机验收判据仍写在每个版本的 Release 正文里。 ### v0.19.7 · 更正 v0.19.6 的错数字 + 每次工具调用压到 1~2 次 stat **先说更正。** 用户追问「装了插件会不会让写入文件变慢」,我复查时发现上一版发布里 有个**错误数字**:v0.19.6 写的「127ms → **1.3µs**(~100000×)」是错的,真实是 **0.9ms(~140×)**。错因是 `tools/pre-execute` 是 `async` 函数,我按同步计时—— `hrtime` 只量到**第一个 `await` 之前**。优化是真的,倍数写错了约 500 倍。 **再回答那个问题**,分两层: | 问题 | 答案 | | --- | --- | | 写文件这个动作本身会变慢吗? | **不会**。插件没包裹 `writeFileSync`、没有文件监听、没有 `post-execute`,也没改写 DSH 的 `write`/`edit` 工具 | | 调用 write 工具那一次呢? | **会加一笔固定开销**——`tools/pre-execute` 挂在每次工具调用上 | 那笔开销的**来源**用确定性计数(数 fs 调用,不看耗时,避开 FUSE 抖动)查清了: | 每次工具调用 | v0.19.5 | v0.19.6 | **v0.19.7** | | --- | --- | --- | --- | | write(已绑定) | 34 stat + 24 read | 3 stat | **2 stat** | | write(未绑定) | 23 stat + 10 read + 1 readdir | 14 stat + 1 readdir | **1 stat** | **规律**:v0.19.6 里 stat 次数 ≈ **当前工作目录的条目数**——绑定查询会遍历 cwd 下**每一个** 条目去试 `<条目>/拼图/主文档.md`,`.git`、`.tgz` 都 stat 一遍,**工作区越乱越慢**。 对照:一次 20KB 文件写实测 **0.27ms**,而未绑定路径 14 次 stat 约 **10ms**—— **插件收的税比它守护的那次写盘还贵 30 倍**。 **改法**:绑定结果缓到内存,两条失效—— ① **文档纪元**:`atomicWrite` 每次写盘 +1,记忆存「在哪个纪元算的」,对不上就重算 (所以 `op:bind`/`op:unbind`/切模式后**当场**看到新值); ② **短 TTL**(1s / 未绑定 5s)兜住外部改动(手工编辑文档不会 +1)。 过期后不立刻全扫,先用**一次** stat 验证,通过就续期。 **代价(写出来,不藏着)**:手工编辑文档搬绑定,可见性从「立刻」变成「最多 1 秒」。 | 场景 | v0.19.5 | v0.19.6 | **v0.19.7** | | --- | --- | --- | --- | | write/edit/bash(已绑定) | ~107 ms | ~0.9 ms | **0.65 ms** | | write(未绑定,"装了但没用") | ~39 ms | ~8 ms | **0.25 ms** | **行为逐字节不变**:原始 v0.19.5 与本版跑同一串操作(init→写坑→写健康性→切模式→ 解绑→重绑→再解绑→再重绑→再写),状态 JSON **逐字节一致**。 ### 更早的版本 v0.19.6 及更早(一直到 v0.9.0)的说明已挪到 **[CHANGELOG.md](CHANGELOG.md)**; 每个版本的完整正文与验收判据见 [Releases](https://github.com/liancha22/dsh-puzzle-mode/releases)。 --- # 更新日志 按版本**倒序**排列。最新版本的功能说明见 [README](README.md) 的「最新版本」一节; 每个版本的完整正文与**验收判据**见 [Releases](https://github.com/liancha22/dsh-puzzle-mode/releases)。 --- ## 历史版本 ### v0.19.6 · 性能:每次工具调用同步阻塞 127ms → 0.9ms 用户反馈:**「装了插件速度变慢了不少」**。量下来是真的,而且根因不在某条业务逻辑, 而在**钩子挂的位置**: `tools/pre-execute` 挂在**每一次工具调用**上,而它一路走到 `readState`——把工作区里 **9 份主文档 + 全部模块文档**同步读一遍,再算健康性 / 条目合规 / 工作流 / 引用源码。 **而这一整条链只为了回答一个问题**:「本会话绑定的项目是不是『只拼不写』模式」。 算完的 99% 当场丢掉。实测 **127ms/次**(同步阻塞事件循环),20 次调用的回合 ≈ **2.5 秒** 纯等待;**不绑项目的用户也在付这笔钱**(86ms/次)。 四处改动: | # | 改动 | 为什么 | | --- | --- | --- | | 1 | 热路径只读 `readProjectMode()`(一份主文档的 front-matter) | 只回答「什么模式」,不碰健康性/模块/条目 | | 2 | `readTextCached()`,指纹 `mtimeMs + size` | 加 `size` 是因为 `mtime` 在部分文件系统上只有秒级粒度 | | 3 | 绑定记忆:**没有时间窗口**,命中时重新验证 | TTL 会留下「刚改绑读到旧的」窗口;验证则当场正确 | | 4 | 提示段与设置常量化 | `policyText` 每步重拼 6042 字符(2.1ms);`readSettings` 每步读盘解析 | **失效点只有一处**:全仓仅 `docfs.atomicWrite` 有 `writeFileSync`,11 个写路径全部收口, 所以不存在「某条路径忘了失效」。 | 指标 | 改前 | 改后 | | --- | --- | --- | | 每次工具调用 | 127.3 ms | **0.9 ms** | | 每 step 提示段 | 2.143 ms | 0.603 ms | | 20 次调用的回合 | ≈ 2.59 s | ≈ 0.02 s | **行为逐字节不变**:拿 `git HEAD` 的原始代码与改后代码跑同一串操作(设置禁用/恢复 + init→写坑→写健康性→切模式→解绑→重绑),状态序列化后 JSON **逐字节一致**。 **关键一条:没有缓存用户源码。** `source.js` 读项目源码仍走无缓存 `readText`—— 源码被 `edit` / `bash` 改,绕过 `atomicWrite`,缓存它会让审查报出**过期的行数**。 只有拼图文档(写入全部收口)进缓存。 ### v0.19.5 · 把提问写深:每题 3–6 个真岔路,每个配一句取舍 v0.19.4 只做了「**放开上限**」。用户看过后提了下一个要求: **「最好写得有广度和有深度」**——因为**上限管的是数量,管不了质量**: | v0.19.4 写了什么 | 模型实际会怎么做 | | --- | --- | | 「**选项别只给两三个**」 | 照字面凑到 3–4 个,但**没说清一个选项该长什么样** | | (没有任何范例) | 于是很容易凑出「改 / 不改 / 再看看 / 都行」——**四个选项、零信息量** | 这一版补质量,把「一句形容词」换成**可照抄的骨架**: - **每题给 3–6 个真岔路,每个配一句取舍**(`options[].description`); - **找岔路的五个维度**(广度):**做法** / **范围** / **时机** / **代价** / **取舍**—— 给的是找岔路的方法,不是模板;一个决策通常问得出 2–3 个维度; - **反例**:`要不要改 A?` ① 改 ② 不改 —— 没问出「怎么改、改多大」; - **正例**(深度):`A 的改法走哪条?` ① 就地改(推荐):改动最小,但旧行为消失; ② 加开关并存:两套都留,代价是多一个配置项与两条路径;③ 抽新模块替换:A 退役, 但要改 5 处调用点;④ 先不改,只把问题写进文档:零风险,但问题留着。 **为什么用「反例 + 正例」**:模型对能照抄的骨架的遵循度明显高于形容词。正例四个选项 正好覆盖「最小改动 / 兼容并存 / 彻底重构 / 不做」,对应最常用的三个维度。 **成本**(提示段每 step 注入,长度直接等于每轮成本):5702 → 6057 字符,**+355(+6%)**。 刻意没给每个维度都配例子——那会让成本翻倍,而一个正例已经能定住形状。 ### v0.19.4 · 提问放开:一轮最多 10 问、每题最多 10 个选项 用户原话:**「只拼不写下模型的提问感觉不够全面,不然把问题放开至 10 个,选项放开至 10 个」**。 看代码,问题有两层,**第二层才是根因**: | 层 | 现状 | 后果 | | --- | --- | --- | | 数字太小 | 提示段写「一轮 **最多 5 问**」 | 问不全 | | **压根没提选项** | 全文只有「能用选项就用选项」,**没有任何选项数上限** | 模型每题只给 2–3 个选项,用户能点的面太窄 | 第二层是「不够全面」的直接来源:**问题数放开到 10、但每题还是两个选项,等于没放开**—— 用户照样只能在「是 / 否」里点。所以这一版**同时**给出选项上限,并把「别只给两三个」写进规则。 规则现在长这样(提示段 `### 提问`): > 一轮 **最多 10 问**、**每题最多 10 个选项**。能用选项就用选项,问的是真正卡住决定的点。 > **选项别只给两三个**:把真正有取舍的岔路都列出来(推荐项放第一并加「(推荐)」), > 用户点得到才叫问全了;同一件事只有一种做法时别硬凑选项,直接写进正文。 最后半句是**防凑数**:放开上限不等于每题都要凑满 10 个——只有一种做法时硬凑, 用户反而要在假选项里挑。 **两个数收成一处定义**(`lib/constants.js` 的 `ASK_MAX_QUESTIONS` / `ASK_MAX_OPTIONS`), 原先「5 问」散在多处各写一份,改一次就得满仓库找。现在提示段、首轮注入、`op:init` 回执、 面板三处模板与底部提示**全部引用它**;浏览器半 `client.js` 拿不到 ESM 导出,另存一份同值常量。 **刻意不加校验**:`ask_user_question` 在 DSH 核心(`dsh-tool-ask-user` / `dsh-user-questions`) 里**没有任何数量校验**,UI 走 `options.map(...)` 全渲染、卡片自带滚动。所以这两个数 **只是提示给模型的软上限**——插件侧拦了反而会在核心调默认值时变成误报。 ### v0.19.3 · 返回体积改为 ∝ 本次改变了什么 作者本人用这个插件做了一轮真实工作,一轮下来光工具返回烧掉约 **193KB**, 而每步真正需要的确认信息不到 200 字符。回头一量,浪费是**结构性**的: | 缺陷 | 实测 | | --- | --- | | 写操作回吐**整份状态** | 7026 字符(`modules` 占 4936),实际需 ~200 → **浪费 35×** | | 同一句证据**重复 39 次** | 114 次出现、仅 12 条唯一;10 个模块的 `reasons` 11406 字符 → 去重后 585(**19.5×**) | | `inflation[].because` 是纯副本 | 6712 字符,逐条比对确认 100% 重复 | | 审查指令每轮重发 | `AUDIT_PROMPT` 2628 字符,而提示段里**已有一份同样的规则** | **根因是一条设计缺陷,不是某个字段写错**:返回体积 ∝ **项目规模**, 而不是 ∝ **本次改变了什么**。拼图模式的项目就是靠模块数增长的——等于「越用越贵」。 **修法**:写操作只回**回执**(改了什么 + 全局几个数);证据抽成 `evidenceTable` 去重表,各处只留下标;`inflation[].because` 删除;审查指令默认不回吐(`verbose:true` 可取回)。 | 场景 | 改前 | 改后 | 省 | | --- | --- | --- | --- | | 一次写操作 | 7026 字符 | 1192 字符 | **83%** | | 一次 `op:audit`(实测响应) | 43KB | 28KB | **35%** | 并把判据写进提示段防止复发:**返回体积应当 ∝ 本次改变了什么,而不是 ∝ 项目有多大**。 ### v0.19.2 · 面板教程配图(照着 UI.md 渲染,可复现) README 顶部与 [UI.md](UI.md) 各节现在**带图**了:10 张 PNG,全部由 `npm run render-tutorial` **照着 `UI.md` 渲染**出来——图与正文同源, 改完正文重跑一次即可,不存在「图与文字对不上」的中间态。 > 为什么强调「可复现」:工作区里原先那批教程图是 **v0.16.4** 时期手工渲染的, > 而 `UI.md` 已经改过 4 处(模式名、文档格式 v5→v6、工作流改成图块式交互、 > 归档加了「永久删除」)。**图与正文对不上,比没有图更糟**—— > 所以这次不只是补图,而是补了一条能重跑的路。 脚本踩过三个坑,都写进 [PUBLISH.md](PUBLISH.md) 了:页面宽必须按 **830px** (GitHub 正文宽)而不是 1400 渲染,否则进页面被缩到 0.59x、字糊;渲染前要 **剥掉正文里的 `![](...)`**,否则自引用会渲染出一排破图图标;表格要 `table-layout:fixed`,否则窄宽下长句把表格撑出画布。 ### v0.19.1 · 修 v0.19.0 首轮判定的两个真缺陷 + 三处审查发现 **没有新功能**——只修 v0.19.0 自己的问题。两个是发版前核对时抓出的真缺陷, 三个来自一轮 `op:audit`(漏洞 1 + 冗余 2)。 - **`step === 1` 不是「新会话」**:`AgentLoop` **每个 turn 都把 `step` 归零**, 于是冷恢复的历史会话下一条消息就命中「首步」,被当成第一条需求开始采访。 判据改成 `turn === 1 && step === 1`(`turn` 来自 `turnBoundary` 会话投影, 由持久化日志重建,跨进程成立)。 - **子代理被误注入**:hook 注册在**根级 ctx**,对每个 agent 都生效; 子代理的首条 prompt 同样是 `role:'user'` + `source.kind:'user'`,但**它没有用户可问**。 新增 `isDelegatedSession(agent)` 按会话头部(`origin` / `parentSession` / `delegationDepth`)识别并跳过。 - **`markFired` 落闸太早**(漏洞):先落闸再造上下文,中间任一步抛错就被 `catch` 吞掉, **闸门已关、提示没注入,`fired` 却报 true**。改成先造上下文、最后落闸。 - **`resetFired` 是死导出**(冗余):导出 + 导入但全仓库无调用点,注释里的 「面板重新判定」按钮并不存在。删掉。 - **触发条件文案漂移**(冗余):条件原先 4 处各写一份,`summary.js` 的 `note` 在判据改过之后**没跟上**,面板显示的规则与真实判据互相矛盾。 收成 `FIRST_RUN_CONDITIONS` 单一来源。 ### v0.19.0 · 新会话直接发需求,自动走「采访后再建」 **以前**:「采访后再建」只是面板上的一颗按钮——把提示词填进输入框,**要你自己点、自己发**。 新开会话直接把需求打出来时,这条路根本不会触发。 **现在**:新会话里把需求打出来就行。宿主半注册了 `agent/pre-step`, 在每步进入模型之前**确定性判定**,五个条件同时满足才触发: | 条件 | 为什么 | | --- | --- | | **本会话第一轮**(`turn === 1 && step === 1`) | 「新会话」的确定性判据。**只看 `step === 1` 是错的**:`AgentLoop` 每个 turn 都会把 `step` 归零,恢复的历史会话下一条消息就会命中 | | **不是子代理**(`origin !== 'subagent'`) | 子代理的首条 prompt 同样是 `role:'user'` + `source.kind:'user'`,但它**没有用户可问**——注入「去采访用户」纯属污染 | | 用户发的**非空文本** | 插件自己注入的上下文不算(按 `source.kind` 认来源) | | 本会话**未绑项目** | 已绑的会话是「继续做项目」,不该被打断 | | **不是寒暄**、用户也**没说「别采访」** | 「你好」不值得起流程;说跳过就跳过 | 判定通过后在**用户需求之后**插一条上下文(`source.kind: 'puzzle-first-run'`)。 它是**追加**,从不 reject、从不 deny——出错就按原样放行,**绝不因为自身问题挡住你的需求**。 **先查再问**:工作区里已经有拼图项目时,第一步不是采访,而是先问 「**绑定已有项目还是新建**」。这一步有原因:v0.16.4 踩过——不先查就 `op:init`, 会把已存在的项目又建一遍。 **能跳**:说「别采访 / 直接建 / 不用问」就跳过采访。跳过识别**只在句首匹配** (配短句 ≤20 字的句中出现也算)——实测反例:「这个功能别采访用户,要保留提问流程…」 是一条**需求**,出现在句中且是长句,不会被误判。 **提示段是第二道**:同一套规则也写进了注入的提示段。hook 管确定性,提示段管兜底 (会话恢复、宿主版本差异等 hook 没走到的路径)。 **可观测**:`op:read` 每次返回都带 `firstRun: { fired, note }`—— 本会话注入了没,一眼看得见,不是「悄悄注入了一条上下文」。 **发版前修掉两个缺陷**(初版实现踩的,判据与修法见 Release 正文): - **`step === 1` 不是「新会话」**:`AgentLoop` **每个 turn 都把 `step` 归零**, 于是冷恢复的历史会话下一条消息就命中「首步」,被当成第一条需求开始采访。 判据改成 `turn === 1 && step === 1`(`turn` 来自 `turnBoundary` 会话投影, 由持久化日志重建,跨进程成立)。 - **子代理被误注入**:本 hook 注册在**根级 ctx**,对每个 agent 都生效; 子代理(`subagent` / teammate)的首条 prompt 同样是 `role:'user'` + `source.kind:'user'`,但**它没有用户可问**。新增 `isDelegatedSession(agent)` 按会话头部(`origin` / `parentSession` / `delegationDepth`)识别并跳过。 > **不引入新的运行时依赖**:注入的消息按 `dsh-llm` 的 `createMessage` 形状自己造 > (`{id, role, content, source}`,冻结),实测与官方 `createUserMessage` 产物 > **字段与内容完全一致**。原因是 `@deepseek-ai/dsh-llm` **不在本插件的 peerDependencies 里**—— > 静态 import 会让「宿主没装它」变成加载期硬失败,把整个插件拖垮。 ### v0.18.0 · 审查的判据从「文件多大」换成「函数形状」 有人拿一个真实项目反推出一份缺陷报告,十条缺陷里最狠的一条不是某个正则写错了, 而是**判据本身选错了量**:审查用「文件多大」代理「代码乱不乱」。 工程化是一棵树:**树干粗不是病**。核心调度、状态机、协议编解码天然内聚, 硬拆只会让调用链横跨十个文件,读一处逻辑要跳十次,维护成本反而上升。 老实现恰好反过来,两个方向都错: | 样本 | 行数 | 函数形状 | 按行数判(旧) | 按函数判(新) | | --- | --- | --- | --- | --- | | A:1200 行 / 30 个 28 行小函数 | 大 | **健康** | ⚠️ 超 800 报警 | `info`:承重模块 | | B:300 行 / 1 个 260 行函数 | 小 | **有病** | ok | ❌ 巨函数 | **A 被冤枉,B 被放过。** 修完这一版:A 不再扣分,B 再也跑不掉。 **具体改了什么** | # | 老毛病 | 现在 | | --- | --- | --- | | D0 | 文件大就报警 | `source_big_file` 恒为 `info`、**不扣分**;大但函数小 → 「承重模块,函数粒度健康,**不必拆文件**」 | | D1 | `class X {` / `if (a) {` / `try {` / `const cfg = {` 全被当成函数起点,报出的是**类体总长** | 起点必须**有函数特征**(`function` / `名字(参数) {` / `get`·`set`·`async` / `=> {`),并排除控制流与类型关键字;**类内方法各自成为候选起点** | | D2 | 只对**最大的那个文件**做巨函数检测,其余文件的巨函数永不发现 | 对**每个文件**跑,`source_long_function` 带 `file`,能正确归因到模块 | | D3 | `(源码: 待建 src/x.ts)` 也算「测过」→ 没开工的模块被抬成源码侧满分 | 剔除「待建 / 计划 / TODO」前缀;`measured` 要求**至少一个引用文件真实存在** | | D4 | 源码引用只留 basename,`src/a/index.ts` 与 `src/b/index.ts` 互相污染 | 保留**相对路径**,模块级过滤用路径段后缀比较(老文档写 `x.js` 也仍对得上) | | D5 | `` `src/gi/pool.ts:30` `` 被反引号带偏,整条**静默丢弃** | 认反引号与空格包裹 | | D6 | 同一份返回里 `weakest_dimension` / `ranking` / `trueDimensions` **三个数字讲三个故事** | 最弱一维按**真实值**算;与手写值不一致时另出 `ranking_mismatch` | | D7 | 插件自己的源码过不了自己的检测 | 修完 D1/D2 重跑:`client.js` 识别为**打包器外壳**单独标出,`project.js` 读成「30 个函数、平均 28 行」的健康树干;剩下的真巨函数照实报、照实留 | | D8 | prompt 让模型补漏洞 / 冗余,但**它们进不了 `fixPlan`**,复测也无从核对 | `op:audit` 收 `additions` 回传(校验:`kind` + `target` 带行号 + fact/fix/expect 四段,缺一**整条退回并说明原因**);每条清单项带 `key`,重跑带 `previousKeys` 看 `recheck.resolved` / `remaining` | | D9 | `lib/` 下 10 个文件、36 处相邻重复的 JSDoc(拆 `puzzle.js` 的遗留) | 批量删除,共 **306 行**;`lib/` 现在 0 处重复 | | D10 | prompt 暗示「找死代码」,而体检**完全没有这一项** | prompt 明写「能测什么 / 不能测什么」;另加轻量 `source_dead_export`(导出符号全项目文本计数 ≤1,`info`、**不扣分**、诚实标注可能被 barrel 漏掉) | **重构方向因此变了**:以前看到「`project.js` 1187 行超阈值」会去把这棵健康的树干劈成碎块 (制造跨文件调用链 = **把好代码改坏**);现在报的是「问题不是行数,是第 519 行那个 118 行的函数」, 该拆的是**函数**。 **唯一的行为变化**:`fixPlanOf` 的返回值从**数组**变成 `{plan, additions, rejected}` (调用方是插件自身,已同步)。 ### v0.17.0 · 工作流重定义为「标准化流水线」(文档格式 v6) **工作流以前是「扁平约束清单」**:`## 工作流` 下面是 `- 一句话` 的条目,≤5 条、每条 ≤50 字, 内容是「在什么操作下别做别的事」。**现在它是「标准化流水线」**(用户原话): > 工作流其实就是工作流程,比如说为完成特定任务,把重复步骤、工具、规则和人 / AI > 按顺序串成的「标准化流水线」。 这个差别不只是措辞——旧的形状**装不下一条流程**:50 字写不下有序步骤,而 「超了删最旧」还会鼓励碎片化(想扩展就新加一条,而不是改那一条)。所以文档格式升 **v6**。 **新形状**:一条工作流 = 一个 `### 名字` 块,块内逐行是**有序**步骤。 ```markdown ## 工作流 ### 发布新版本 1. 先改 `package.json` 的 version 与 `.github/release-vX.Y.Z.md` 2. 跑 `node --check lib/*.js` 确认语法 3. `git commit` 并打 tag、推送 4. 建 Release 并**单独上传** tgz 附件 ``` 四条硬规则(都写进了提示段,模型每步都读得到): | 规则 | 为什么 | | --- | --- | | **一条 = 一个 `### 名字` 块**,步骤有序 | 序号由文档自动重排成 `1. 2. 3.`,不靠模型自觉写对 | | **每条是并列的一条独立的路,两条之间不应有依赖** | 若两条其实是一条就合并,若要分叉就是两条——避免把一条流程拆成互为依据的碎条 | | **要改一条就改那一块**,不要新加一条 | 这条直接治「碎片化」:扩展靠编辑原块,不靠追加 | | **超 5 条删最旧(整条);超 12 步是报错,不删步骤** | 两条工作流本来就并列,顶掉最旧不会让剩下的路断掉;但**步骤是一条有序的路,少一步就断了**——静默删步比拒绝写入危险得多 | **上限**:名字 ≤20 字、单步 ≤80 字(一步要写清「谁 + 用什么工具 + 做什么 + 产出什么」)、 每条 ≤12 步、最多 5 条。 **面板改成图块式**(用户裁定:改成像模块点击出细节的模式):列表只显示「序号 + 名字 + 几步」, **点名字才铺开有序步骤**,再点收起——5 条流水线不会把面板撑长,也一眼看得出每条有多长。 **归档不可展开**(它是「删过什么」的账,不是当前流程),只显示名字与步数, 每条给两个出口:`恢复` / `永久删除`(后者危险色)。 **文档格式 v5 → v6 迁移**(`op:rebuild`,默认 dry-run): - 旧的扁平条目被搬进**一个** `### 迁移自 v5` 块,每行成为一步; - front-matter 的 `工作流归档:` 从字符串数组升级成 `{name, steps}` 对象数组(旧项按 「只有名字、没有步骤」收下,**不丢**); - 其余正文**一字不动**,迁移是幂等的(跑第二次不再改)。 > 为什么不逐条转成一个个块:v5 的每一条约束**不是一个流程**(没有顺序、没有步骤), > 逐条转块会造出 N 条「只有一步的流水线」——形状对了,语义是假的。合成一个块至少诚实。 ### v0.17.0 · 工作流重定义为「标准化流水线」(文档格式 v6) `## 工作流` 从「扁平约束清单」重定义为**标准化流水线**:一条 = 一个 `### 名字` 块 + 有序步骤(≤5 条、每条 ≤12 步、每步 ≤80 字),超步数**报错不删步**(一条有序的路少一步就断了)。 面板改图块式(点名字才铺开步骤),归档不可展开但可恢复 / 永久删除。 文档格式升 **v6**,含 v5→v6 迁移(旧扁平条目合成一个 `### 迁移自 v5` 块,其余正文一字不动)。 完整说明见 [README](README.md) 的「最新版本」一节。 ### v0.16.5 · 修「样式自检」自己报假警(两处代码 bug) 有人的面板**看起来是好的**,却在顶部显示「⚠ 面板样式没有生效」, 并给出 `applied=false inset=false color-mix=false backdrop=false min()=false`。 UA 是 **Chrome 152** —— 一个四项全不支持的浏览器并不存在。**是自检自己坏了**: **bug ① · 探针自己写了 `position:absolute`。** 探针是 `.dshpz-backdrop` 的一个 div,用 `getComputedStyle().position === 'fixed'` 判定 样式表有没有生效。但它同时写了行内 `position:absolute`,而**行内样式的优先级高于样式表** —— 于是 `position` 恒为 `absolute`,`applied` 在**任何**浏览器上都是 `false`。 修法:探针只负责挪出视口(`left:-9999px`),`position` 交给样式表判定。 **bug ② · `CSS.supports` 里的 `CSS` 被本文件的样式表字符串遮蔽了。** 本文件有 `var CSS = [...]`(样式表本身)。原先写 `CSS.supports(…)`, 取到的是**字符串的** `undefined` 属性 → `CSS.supports !== undefined` 为假 → `&&` 短路 → `inset` / `color-mix` / `backdrop` / `min()` **四项一律返回 `false`**。 这正是那行报告里四项全 `false` 的来源。 修法:改走 `window.CSS.supports(…)`,不再用裸 `CSS`。 **顺带把诊断行改成可信的**:原先 `applied=false` 是**写死在字面量里**的, 于是「自检误报」与「真的没生效」看起来一模一样。现在 `applied` 取真实值, 并新增一项 `rules=`(`