dsh-immune-system 0.9.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +187 -16
  2. package/dsh-immune-system/client/client.js +375 -159
  3. package/dsh-immune-system/dist/core/distiller.js +78 -0
  4. package/dsh-immune-system/dist/core/pairing.js +178 -0
  5. package/dsh-immune-system/dist/core/prompts.js +88 -2
  6. package/dsh-immune-system/dist/core/similarity.js +51 -0
  7. package/dsh-immune-system/dist/core/store/antibodies.js +37 -8
  8. package/dsh-immune-system/dist/core/store/episodes.js +19 -5
  9. package/dsh-immune-system/dist/core/store/index.js +58 -35
  10. package/dsh-immune-system/dist/core/store/jobs.js +17 -0
  11. package/dsh-immune-system/dist/core/store/open.js +1 -0
  12. package/dsh-immune-system/dist/core/store/rerank.js +57 -0
  13. package/dsh-immune-system/dist/core/store/schema.js +9 -0
  14. package/dsh-immune-system/dist/core/store/turn-state.js +52 -0
  15. package/dsh-immune-system/dist/core/ui/page.js +481 -132
  16. package/dsh-immune-system/dist/core/ui/query.js +12 -2
  17. package/dsh-immune-system/dist/core/ui/server.js +2 -2
  18. package/dsh-immune-system/dist/dsh/src/dsh-adapter.js +66 -17
  19. package/dsh-immune-system/dist/dsh/src/index.js +143 -9
  20. package/dsh-immune-system/dist/dsh/src/ui-route.js +7 -3
  21. package/dsh-immune-system/dist/zcode/src/doctor.js +51 -3
  22. package/dsh-immune-system/dist/zcode/src/drain.js +73 -20
  23. package/dsh-immune-system/dist/zcode/src/hooks/post-tool-use-failure.js +3 -1
  24. package/dsh-immune-system/dist/zcode/src/hooks/post-tool-use.js +4 -2
  25. package/dsh-immune-system/dist/zcode/src/hooks/session-start.js +6 -0
  26. package/dsh-immune-system/dist/zcode/src/hooks/user-prompt-submit.js +3 -0
  27. package/dsh-immune-system/package.json +7 -7
  28. package/package.json +4 -3
package/README.md CHANGED
@@ -55,8 +55,8 @@ dsh plugin --profile <profile> add dsh-immune-system
55
55
  **tarball / git**(等价,无需 npm 账号):
56
56
 
57
57
  ```bash
58
- cd dsh-immune-system && npm pack # → dsh-immune-system-0.9.0.tgz
59
- dsh plugin --profile <profile> add ./dsh-immune-system-0.9.0.tgz
58
+ cd dsh-immune-system && npm pack # → dsh-immune-system-0.9.8.tgz
59
+ dsh plugin --profile <profile> add ./dsh-immune-system-0.9.8.tgz
60
60
  dsh plugin --profile <profile> add github:meat-daemon-404/dsh-plugin-immune
61
61
  ```
62
62
 
@@ -90,7 +90,7 @@ node "<zcode.cjs 路径>" plugins install dsh-immune-system@dev-dsh-plugin-immun
90
90
 
91
91
  | 命令 | 宿主 | 作用 |
92
92
  | --- | --- | --- |
93
- | `/immune` | DSH | 列出所有抗体(`/immune --all` 含已退役) |
93
+ | `/immune` | DSH | 列出所有抗体(`/immune --all` 含已退役);`/immune --budget` 查看蒸馏预算,`--budget <ms>` 本次会话内改(内存态,重启回到环境变量) |
94
94
  | `/immune-export` | DSH | 把经过验证的抗体导出成 `SKILL.md` |
95
95
  | `/immune-forget <id>` | DSH | 删除指定抗体 |
96
96
  | `/immune-forget --last` | DSH | 撤销上一回合刚学的那条 |
@@ -99,9 +99,11 @@ node "<zcode.cjs 路径>" plugins install dsh-immune-system@dev-dsh-plugin-immun
99
99
  | `/dsh-immune-system:immune` | ZCode | 同上(加 `--all`) |
100
100
  | `/dsh-immune-system:immune-export` | ZCode | 同上 |
101
101
  | `/dsh-immune-system:immune-forget` | ZCode | 同上(`<id>` / `--last` / `--prune` / `--purge`) |
102
+ | `/dsh-immune-system:immune-learn` | ZCode | 手动教一条规则 |
102
103
  | `/dsh-immune-system:immune-ui` | ZCode | 启动本地仪表盘,给出地址 |
103
104
  | `cli.js ui [--port N]` | 两者 | 同上(前台运行,默认 7788,端口占用自动顺延) |
104
105
  | `cli.js learn <trigger> [symptom] <prescription>` | 两者 | 手动教一条规则(借鉴 pro-workflow 的 `/learn-rule`) |
106
+ | `cli.js search <query>` | 两者 | 只读检索——与仪表盘/MCP 同一条不计数通道 |
105
107
  | `cli.js export-json <path>` / `import-json <path>` | 两者 | 库快照导出/导入——跨机器同步、git 提交、团队共享 |
106
108
  | `cli.js doctor` | 两者 | 安装健康自检:node 版本、FTS5、数据库、marker、MCP 脚本 |
107
109
 
@@ -113,8 +115,9 @@ node "<zcode.cjs 路径>" plugins install dsh-immune-system@dev-dsh-plugin-immun
113
115
  `/dsh-immune-system:immune-ui`(或 `cli.js ui`)会起一个本地页面:
114
116
 
115
117
  - 搜索触发词/症状/处方,实时过滤;**浏览不计入命中数**,所以晋升和淘汰的统计不会被点鼠标污染
116
- - 每条可**退役 / 恢复 / 删除**,分「项目库」「共享库」两个页签
117
- - 顶部显示生效数、退役数、累计注入、共享库条数
118
+ - 每条可**退役 / 恢复 / 删除**,分「项目库」「共享库」两个页签,可按「最近更新 / 命中最多 / 失效最多」排序
119
+ - 顶部 5 张 KPI:生效数、退役数、累计注入、共享库条数,外加**当前蒸馏预算**
120
+ - 跟随系统深浅色、窄屏自动把表格降级成卡片、骨架屏与空态都是有设计的那种
118
121
 
119
122
  安全边界是刻意收窄的:只绑 `127.0.0.1`、每次启动生成随机 token 且所有 `/api` 都要带它
120
123
  (跨站简单 POST 不需要预检,只靠 Origin 判断不够)、严格 CSP、页面不加载任何外部资源。
@@ -125,20 +128,31 @@ UI 是自建的而非宿主插件 UI——ZCode 插件没有 UI 插槽(只有
125
128
  #### the agent harness 原生 UI(设置页里的 tab)
126
129
 
127
130
  the agent harness 有 web GUI 和客户端插槽,所以那边**不用另开页面**:
128
- `设置 → 插件` 里会出现一个「免疫 / Immune」tab。
131
+ `设置 → 插件` 里会出现一个「免疫 / Immune」tab。它与独立仪表盘共用同一套设计:
132
+ KPI(含预算)、搜索与清除、页签计数、排序、状态圆点、窄屏卡片、骨架屏与空态。
129
133
 
130
134
  - 客户端是 `dsh-immune-system/client/client.js`,按模块装载器契约手写
131
135
  (`window.__ModuleLoader__.load({ id, factory })`),**不需要打包器、不写 JSX**
136
+ - 样式是注入一段静态 `<style>`(选择器全部 scope 在 `.imm` 下),所以 hover、媒体查询、
137
+ 跟随宿主的 `--dsh-*` 深浅色变量都能用,而不是几十个 inline style
132
138
  - 数据走宿主路由 `/plugins/dsh-immune-system/api`,用
133
139
  `ctx.inject(['webServer'], …)` 注册——这是可选依赖,CLI/桌面 profile 没有 web 服务时
134
140
  插件照常工作,只是没有这个 tab
141
+ - **manifest 必须同时声明** `dsh.client: { platform: "web" }` 与字符串
142
+ `exports["./client"]`。宿主的 `client-modules` 只按这两条把包认成「浏览器半」,
143
+ 再把文件原样挂到 `/plugins/<id>/client.js?rev=<hash>`;少一条,文件在磁盘上存在
144
+ 但路由 404、tab 永不出现(而且判定在本进程内缓存,必须重启才生效)。
145
+ `test/manifests.test.mjs` 与 `scripts/verify-installed.mjs` 把声明、导出目标与
146
+ 文件内容三件事都钉死了
135
147
  - 变更类请求要求 `content-type: application/json` 且同源:JSON 会强制 CORS 预检,
136
148
  跨站页面过不去
137
149
 
138
150
  > 诚实说明:客户端与路由的**契约**有测试(模拟 `__ModuleLoader__` 装载、假 ctx 验证路由、
139
- > 跨源拒绝、无 web 服务时不注册),但**没有在真实 web 应用里端到端跑通**——
140
- > 本机 web profile 装了十几个第三方插件和 MCP 服务,启动日志淹没在它们的输出里,
141
- > 我没能把启动稳定下来。tab 是否如期出现,需要你重启 DSH 后看一眼。
151
+ > 跨源拒绝、无 web 服务时不注册、预算透传),布局则用真实 React 18 做过静态渲染自检
152
+ > (含深色模式、空态、窄屏)。**0.9.8 之前**它还从未被宿主真正当成浏览器半——根因是
153
+ > manifest 漏了 `dsh.client` 声明(实测:`/plugins/dsh-immune-system/api` 返回 200,
154
+ > 说明宿主半在跑;而 `/plugins/dsh-immune-system/client.js` 返回 404)。0.9.8 起已补齐,
155
+ > 但需要**重启 DSH** 后到 `设置 → 插件` 确认 tab 出现。
142
156
 
143
157
  ### 同步与体检
144
158
 
@@ -168,8 +182,13 @@ node "<plugin>/dist/zcode/src/cli.js" doctor # 安装自检
168
182
  - DSH:`result.isError` **只覆盖基础设施失败**(spawn 错误、abort、监听器抛错),
169
183
  非零退出被明确设计为「reported, not errored」,模型自己决定怎么办。
170
184
 
171
- 所以两版都自己判结果。DSH 侧的判据顺序是
172
- `isError` → `value.exitCode` → 行尾 `[exit code: N]` / `[killed by signal: X]` / `[timed out after Nms]` / sandbox 拒绝标记 → 窄口径失败标记正则。
185
+ 所以两版都自己判结果。DSH 侧的判据按权威度排序:
186
+ `isError` → sandbox 拒绝标记(判失败但**不入库学习**)→ kill/timeout(含 `value.timedOut`)→
187
+ 结构化 `value.exitCode`(**有则权威:0 成功、非 0 失败,不再看文本**)→
188
+ 有结构化 `value` 但无退出状态(**载荷即结论,正文是数据,不扫**)→ 行尾 `[exit code: N]` →
189
+ 窄口径失败标记正则(**仅当无任何结构化状态**)。后面两步刻意让位给结构化信号:一条退出 0
190
+ 的命令在 `grep` 输出里顺带印出 `command not found` 是常事,而 `read` 一个含 `Error:` 的日志
191
+ 文件更是日常——都不该被记成失败。
173
192
 
174
193
  **② 抗体失效会被淘汰(Anti-Rot)**
175
194
 
@@ -230,6 +249,32 @@ node "<plugin>/dist/zcode/src/cli.js" doctor # 安装自检
230
249
  所以它挂在 `agent/turn-stopping`(`@mode serial`,回合边界提交前 await)。
231
250
  未清空的队列还会在下次 `session-start` 由 `requeueStuckJobs()` 兜底。
232
251
 
252
+ ### 失败怎么被判出来(四层判定 × 三类失败)
253
+
254
+ `judgeOutcome` 不是「看一个 boolean」,而是按权威顺序逐层走。三类常见失败在四层里的落点并不相同:
255
+
256
+ | 层 | 判据 | bash 非零退出 | 超时 | 沙箱拒绝 |
257
+ | --- | --- | --- | --- | --- |
258
+ | 1 | `result.isError`(宿主**只**对基础设施失败置真) | ✗ 返回 `isError:false` | 可能 ✗ | 偶尔 ✓(file 工具抛异常) |
259
+ | 2 | 沙箱标记 `[sandbox: file access denied under … mode]` | ✗ | ✗ | ✓ 命中 → 打 `sandboxDenied`,**不记录、不惩罚、不蒸馏** |
260
+ | 3 | kill / timed-out 标记 + `value.timedOut` | ✗ | ✓ 命中(即便 `exitCode=0`) | ✗ |
261
+ | 4 | 结构化 `value.exitCode`(shell 工具的权威判据) | ✓ 非 0 即失败 | — | — |
262
+ | 5 | 尾标记 `[exit code: N]` / 特征词(**仅**当没有任何结构化状态) | 兜底 | 兜底 | — |
263
+
264
+ ```
265
+ 一次工具结果
266
+ ├─ isError ──────────────────────────────→ 失败(error.message 原样记入 trace)
267
+ ├─ 沙箱拒绝标记 ─────────────────────────→ 拒绝:不记录 / 不惩罚 / 不蒸馏
268
+ ├─ killed / timed out ───────────────────→ 失败
269
+ ├─ value.exitCode 存在 ──┬─ 0 ───────────→ 成功(此后不再扫文本)
270
+ │ └─ ≠0 ──────────→ 失败
271
+ ├─ value 为非空对象(无退出码)──────────→ 成功(载荷即结论)
272
+ ├─ [exit code: N] / 特征词 ──────────────→ 失败
273
+ └─ 其余 ─────────────────────────────────→ 成功
274
+ ```
275
+
276
+ 要点:**结构化 exitCode 一旦存在就压过文本**——`grep` 命中内容、测试打印 `Error:` 都不会被误判成功/失败;而 bash 的非零退出正是靠第 4 层抓到的(宿主本身不会把它置成 `isError`)。这也解释了为什么「读一份含 `Error:` 的日志」不会凭空长出一条抗体。
277
+
233
278
  ### 存储
234
279
 
235
280
  抗体库是 SQLite 单文件(含 FTS5 trigram 索引,中文按 3 字窗口匹配)。
@@ -253,12 +298,30 @@ node "<plugin>/dist/zcode/src/cli.js" doctor # 安装自检
253
298
  | `IMMUNE_LLM_BASE_URL` | OpenAI 兼容端点,如 `https://api.example.com/v1` |
254
299
  | `IMMUNE_LLM_MODEL` | 模型名,与上面配对使用 |
255
300
  | `IMMUNE_LLM_API_KEY` | Bearer 令牌 |
301
+ | `IMMUNE_DISTILL_BUDGET_MS` | 单次回合末蒸馏的时间预算,默认 `12000`(两版同名同默认,钳制在 1–60 s);超预算的队列留到下次边界 |
256
302
  | `IMMUNE_LOCALE` | `zh-CN` / `en` 强制单语;**默认中英双语** |
257
303
  | `IMMUNE_ALLOW_PRIVATE_ENDPOINT` | 设为 `1` 才允许环回/私有端点。**默认拒绝**,防 SSRF |
258
304
  | `IMMUNE_GLOBAL_LIBRARY` | 设为 `0` 关闭全局共享库。默认开启 |
259
305
  | `IMMUNE_GUARD` | `off`(默认)/ `warn` / `deny` —— 高危错招事前检查,见下 |
260
306
  | `IMMUNE_DEBUG_LOG` | 指定文件则记录执行细节,用于排障 |
261
307
 
308
+ ### 什么时候值得开 LLM 蒸馏(成本 / 收益)
309
+
310
+ 不配 `IMMUNE_LLM_*` 时走离线规则蒸馏器:零成本、结果确定,只从失败/成功的调用串里抽关键词拼出 trigger / prescription。配上模型后,同一段「失败→成功」会被改写成一句更像人话的处方(跨项目复用、导出成 `SKILL.md` 都更好),代价是**每条蒸馏一次模型往返**。
311
+
312
+ 实测(`scripts/bench-tool-latency.mjs`,mock 端点 1500 ms/次):
313
+
314
+ | 队列深度 | `agent/turn-stopping` 耗时 | 蒸馏条数 |
315
+ | --- | --- | --- |
316
+ | 1 | ~1.5 s | 1 |
317
+ | 2 | ~3.0 s | 2 |
318
+ | 3 | ~4.6 s | 3 |
319
+ | 5 | ~7.5 s | 5 |
320
+
321
+ - 成本只落在**回合边界**一次(`agent/turn-stopping`),不落在工具调用上——`tools/post-execute` 中位稳定在 ~1 ms。
322
+ - 预算默认 12 s(`IMMUNE_DISTILL_BUDGET_MS`)。队列超过预算时,多出的任务留到下一次边界:实测 3000 ms 预算 + 6 个任务只蒸馏 2 个,其余 4 个顺延。
323
+ - 经验阈值:**每回合最多攒几条**「失败→成功」时才开模型。如果一个回合常常攒十几条,先跑离线规则蒸馏,或把 `IMMUNE_DISTILL_BUDGET_MS` 调小、让它跨多个回合摊平。
324
+
262
325
  ### 安全
263
326
 
264
327
  - **脱敏**:写入磁盘前擦除 Bearer 令牌、`*_KEY=` 类赋值、URL 内嵌凭证、JWT。
@@ -272,11 +335,12 @@ node "<plugin>/dist/zcode/src/cli.js" doctor # 安装自检
272
335
  | 检查 | 结果 |
273
336
  | --- | --- |
274
337
  | `npm run build` | exit 0 |
275
- | `npm test` | **66 条全绿**(41 条 core + ZCode,22 条 DSH 单元,3 条原生 UI 契约) |
338
+ | `npm test` | **90 条全绿**(34 条 core、7 条 ZCode hook、29 条 DSH 单元、4 条原生 UI(tab 契约)、3 条 MCP、3 条 LLM、5 条 UI 查询层、5 条 manifest 一致性(含 `dsh.client` 浏览器半契约与 shadow root 隔离)) |
276
339
  | `scripts/verify-step1.mjs` | 15 断言(真实载荷判失败,含 bash 非零退出) |
277
340
  | `scripts/verify-steps23.mjs` | 22 断言(失败→成功在回合结束时产出抗体;新会话注入 `<immune_memory>`) |
278
341
  | `scripts/verify-live-host.mjs` | 20 断言(**真 Cordis 宿主**:真事件总线 + 真 SQLite + 跨会话注入 + 冷启动注入) |
279
- | `scripts/verify-installed.mjs` | 16 断言(本机 profile 安装校验:bundles 登记、裸包名解析、patch 形状) |
342
+ | `scripts/verify-installed.mjs` | desktop / web 各 22 断言(bundles 登记、裸包名解析、patch 形状,以及浏览器半的 `dsh.client` 声明 + `exports["./client"]` + 装载器注册面) |
343
+ | `scripts/bench-tool-latency.mjs` | PASS,19 断言(慢速 mock 模型 1500 ms/次:`tools/post-execute` 中位 ~1 ms;`agent/turn-stopping` 按队列线性 1.5/3.0/4.6/7.5 s(1/2/3/5 条);3000 ms 预算下 6 条只蒸馏 2 条、其余顺延) |
280
344
 
281
345
  ### 已知限制
282
346
 
@@ -312,7 +376,7 @@ dsh plugin --profile <profile> add dsh-immune-system
312
376
 
313
377
  ```bash
314
378
  cd dsh-immune-system && npm pack
315
- dsh plugin --profile <profile> add ./dsh-immune-system-0.9.0.tgz
379
+ dsh plugin --profile <profile> add ./dsh-immune-system-0.9.8.tgz
316
380
  dsh plugin --profile <profile> add github:meat-daemon-404/dsh-plugin-immune
317
381
  ```
318
382
 
@@ -338,7 +402,10 @@ exiting non-zero is *not* an error result: ZCode's `PostToolUseFailure` only fir
338
402
  when a handler throws, and DSH documents that "non-zero exits are reported, not
339
403
  errored" — `isError` covers infrastructure failures only. Both ports therefore
340
404
  classify the result themselves (exit code, trailing `[exit code: N]` /
341
- `[killed by signal: X]` / timed-out markers, and a narrow marker regex).
405
+ `[killed by signal: X]` / timed-out markers, and a narrow marker regex). A
406
+ structured `value` is authoritative: a non-zero exit code is a failure and a
407
+ zero one is a success even if the output mentions `Error:`; a payload object with
408
+ no exit status is a success and is not text-scanned at all.
342
409
 
343
410
  **Anti-rot, relevance-guarded.** An injected antibody that still did not prevent
344
411
  the failure loses credit and is retired after three such episodes. The penalty is
@@ -350,6 +417,110 @@ blamed for any Bash failure.
350
417
  never raw tool output; brackets are stripped so a line cannot close its wrapper
351
418
  tag; length-capped; labelled advisory.
352
419
 
420
+ **Distillation cost sits at the turn boundary.** Jobs are queued during tool
421
+ use and drained once at turn end under `IMMUNE_DISTILL_BUDGET_MS` (default
422
+ 12 s). Measured with a slow mock model (`scripts/bench-tool-latency.mjs`):
423
+ `tools/post-execute` stays ~1 ms median, while `agent/turn-stopping` pays one
424
+ round trip per queued job (~1.5 s each at 1500 ms/call) and defers whatever the
425
+ budget cannot fit to the next boundary. With a model configured the drain now
426
+ **batches four episodes per request**, so a backlog fits in one budget window
427
+ instead of four.
428
+
429
+ **Failure judgement — four layers.** A tool result is classified in this order,
430
+ and the first layer that yields a verdict wins:
431
+
432
+ | # | Signal | Verdict |
433
+ | --- | --- | --- |
434
+ | 1 | structured `value.exitCode` | non-zero = failure, zero = success — authoritative, output text is not scanned |
435
+ | 2 | `isError` / `error` payload | failure, and its message becomes the symptom |
436
+ | 3 | trailing markers (`[exit code: N]`, `[killed by signal: X]`, timed out) | failure when N≠0 |
437
+ | 4 | narrow marker regex over the body (`ENOENT`, `npm ERR!`, `Cannot find module`, …) | failure; a healthy log that merely mentions "error" does not match |
438
+
439
+ **Repeat suppression.** Asking three questions in a row about the same file
440
+ would otherwise inject the identical block three times and count three hits.
441
+ When the candidate set is unchanged and fewer than four turns have passed, the
442
+ injection is skipped and `hit_count` is left alone — that counter drives
443
+ promotion and anti-rot, so inflating it would corrupt the library's own
444
+ statistics. `resume` / `clear` / `compact` reset the state.
445
+
446
+ **Target-scoped pairing.** A failure only pairs with a success that is the
447
+ *same kind of work*: same file for file tools, same program family for commands
448
+ (`npm`/`npx`/`pnpm` are one family — a build failing under `npm` and passing
449
+ under `npx` is the same task's fix; `npm …` then `git …` is not). Read-only
450
+ probes (`ls`, `pwd`, `cat`, `git status`, `git log`, `git diff`, `which`, …)
451
+ never close an episode: they cannot have fixed anything, and they sit between
452
+ an error and its real fix constantly.
453
+
454
+ **Jaccard near-duplicate merge.** Before inserting, prescriptions within the
455
+ same trigger are compared with character-3-gram Jaccard (blended with a
456
+ character-set Jaccard for short CJK sentences, where inserting one character
457
+ shifts every trigram). At ≥0.70 the rule merges into the existing row and its
458
+ hit count increments; a human correction or a model rewrite then overwrites the
459
+ stored wording, because it outranks whatever was there.
460
+
461
+ **Path-affinity rerank.** Retrieval fetches twice the requested limit, then
462
+ re-ranks in memory: ×1.5 when the trigger names a file or directory the prompt
463
+ mentions, ×0.85 for shared-library rules (project rules always win ties), and a
464
+ time decay of `1 / (1 + 0.02 × days)` so a rule nobody has needed for a year
465
+ does not outrank a fresh one.
466
+
467
+ ### Dashboard
468
+
469
+ `cli.js ui` serves a local page at `http://127.0.0.1:7788/`: search, retire /
470
+ restore / delete, project and shared tabs, live counts. Browsing is a
471
+ read-only path — it never counts as an injection.
472
+
473
+ Boundaries are deliberately narrow: binds `127.0.0.1` only, a random per-run
474
+ token is required on every `/api` call (a cross-origin simple POST needs no
475
+ preflight, so Origin checking alone would not do), strict CSP, and the page
476
+ loads nothing external.
477
+
478
+ ### Native UI (dsh.client)
479
+
480
+ The DSH web app gets a real settings tab instead of a separate page. The client
481
+ is `dsh-immune-system/client/client.js`, hand-written against the module-loader
482
+ contract (`window.__ModuleLoader__.load({ id, factory })`) — no bundler, no JSX,
483
+ no new dependency. Data comes from a host route registered as an *optional*
484
+ `webServer` dependency, so CLI and desktop profiles still work without it.
485
+ Mutations require same-origin JSON, which forces a CORS preflight.
486
+
487
+ ### Sync & Doctor
488
+
489
+ Library snapshots are plain tagged JSON, and merging is conservative:
490
+ prescription is the identity, tallies take the MAX (two machines count
491
+ independently and must not multiply), status is active when either side says
492
+ so, and malformed rows are counted invalid rather than aborting the import.
493
+
494
+ ```bash
495
+ node "<plugin>/dist/zcode/src/cli.js" export-json library.json
496
+ node "<plugin>/dist/zcode/src/cli.js" import-json library.json
497
+ node "<plugin>/dist/zcode/src/cli.js" doctor
498
+ ```
499
+
500
+ `doctor` reports node version, an FTS5 trigram probe, database and shared-library
501
+ readability, the plugin marker, the MCP script, the native-UI contract
502
+ (`dsh.client` + `exports["./client"]` + a loader-registered bundle) and counts.
503
+ Any failure exits non-zero, so it can gate a script.
504
+
505
+ ### Guard (IMMUNE_GUARD)
506
+
507
+ Advisory text can be dropped by a model in a long turn, so a short curated list
508
+ of calls that are wrong in essentially every environment also gets a
509
+ deterministic pre-execution check: force-push to main, `rm -rf /`,
510
+ `git reset --hard`.
511
+
512
+ ```bash
513
+ IMMUNE_GUARD=off # default: no check
514
+ IMMUNE_GUARD=warn # louder advisory (ZCode only)
515
+ IMMUNE_GUARD=deny # refuse the call (both hosts)
516
+ ```
517
+
518
+ Patterns live in code, never in the database: a distilled rule is model output,
519
+ and compiling it into a regex that can block tool calls would hand an
520
+ unreviewed string the power to stop work, plus ReDoS risk. `deny` is never the
521
+ default. `warn` is ZCode-only because DSH's `PreToolDecision` carries no
522
+ context channel.
523
+
353
524
  ### Layout
354
525
 
355
526
  ```
@@ -363,7 +534,7 @@ dsh/ DSH adapter — verified against the real host contract
363
534
  dsh-immune-system/ installable bundle (manifest, patch, commands, dist)
364
535
  zcode/ ZCode adapter (hooks, CLI)
365
536
  docs/ dsh-host-contract.md — every contract claim with file:line
366
- scripts/ the four verification scripts above
537
+ scripts/ the five verification & benchmark scripts above
367
538
  test/ node:test
368
539
  ```
369
540