dsh-molbio-tools 0.20.1 → 0.21.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.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,76 @@
10
10
  改为按**包名**引用后模块缓存问题随之消失——没有拷贝,也就没有需要保持同步的版本目录。
11
11
  下文历史条目里出现的 `vN` 目录保持原样,作为当时的记录。
12
12
 
13
+ ## [0.21.0] — 2026-10-23(DSH 0.2.1-rc.2 适配;两条 benchmark 欠账清零)
14
+
15
+ **工具数不变(60),插件行为零改动。** 这是一次"只跟上游走"的版本:0.20.1 之后本机 harness
16
+ 升到 0.2.1-rc.2,`npm test` 立刻红了三处——组合漂移、契约断言、以及一条依赖旧 fixture 的
17
+ 漂移探针。三处都不是插件退化,但每一处都属于**守卫在说谎**,所以按发布阻断处理。
18
+
19
+ ### 1. preset 行清单按 0.2.1-rc.2 逐行重抄(12 处结构差异)
20
+
21
+ - 上游 `standard` **新增** `time-context`、`tool-schedule`、`tool-plugin-market`;
22
+ - **删除** `tool-subagent-codex` / `tool-subagent-claude-code`(两个 `disabled: true` 的可选
23
+ provider)与 `tool-ralph`;
24
+ - **去掉** persona 的 `suffix: Your working directory is {{cwd}}.`,以及 `tool-subagent` /
25
+ `tool-subagent-fork` 的 `backgroundMode: continuable`;
26
+ - plan-mode 的段落文案改为 "until the user approves your plan through exit_plan_mode…"。
27
+
28
+ `agent.cordis.yml` 现在是上游 `config.plugins:` 块的**整体去缩进** + 末尾一行 `tool-molbio`;
29
+ 0.2.1 的上游文件不带行内注释,因此行内注释随之消失,来源与升级规则集中写在文件头部。
30
+ `preset.patch.yml` 由 `build/preset-patch.mjs` 重新生成。
31
+
32
+ ### 2. `test/contract.mjs`:两条断言不再锚在 minified 字面量上
33
+
34
+ - **client 入口**:改为按包自己的 `exports["./client"]`(`browser` 条件)解析。
35
+ 0.2.1-rc.2 把 `@deepseek-ai/dsh-client-connection` 移到 `lib/client.browser.js`,旧断言
36
+ 硬编码 `lib/client.js`,会把一个加载正常的包判死;同时 Web shell 已是打包产物、不再出现
37
+ `require("包名")`,"seeded into the shell" 兜底只剩代码路径。新增两个对照:没有
38
+ `./client` 导出的包必须解析为 `undefined`,以及移动过的入口必须解析到真实文件。
39
+ - **`presentationMeta`**:0.2.1 删除了 `exec.parent === void 0` 闸门(投影移进
40
+ `createSuccessResult`,对每次成功执行都做)。断言改挂到卡片真正消费的东西:投影被
41
+ `snapshotProjection` 记录、并以 `meta` 附到成功结果上。
42
+
43
+ ### 3. `drift-probe`:突变样本换成上游仍存在的行
44
+
45
+ 上游删掉 `tool-ralph` 后,那条"上游 `disabled: true` 被丢掉"的突变在**空转**(`withRow`
46
+ 找不到目标,什么也没改,守卫却"通过")。改用上游仍以 `disabled: true` 发布的
47
+ `tool-plugin-manager`;14 条突变全部仍能被抓住。
48
+
49
+ ### 4. benchmark:profile 重建、两条欠账清零
50
+
51
+ - preset 新增的两行宿主可承载行(`time-context`、`tool-schedule`)进入 headless profile,
52
+ `benchmark/profile.mjs` 重建 `molbio-bench`:**12 行宿主行,仍注册 60 个工具**。
53
+ - `methylation-block` 与 `qpcr-primers` 的冻结响应停在 0.16.0 之前,`benchmark-score` 每次
54
+ 都点名。本版用一次完整真实模型跑重录,并用 `_freeze-trace.mjs --replace` **只替换这两条**,
55
+ 其余 47 条保持原样;`run.amendments` 记录每条替换来自哪一次跑分。fixture 现为
56
+ **49 条记录、0 条 suspended**。
57
+ - 完整跑分(2026-10-23,DSH 0.2.1-rc.2,61 题):**58/61** 通过,tool selection **60/61**、
58
+ arguments **60/61**、final answer **59/61**;11.2M tokens / 2161 s。
59
+ - `protein-props` 的失败是**断言不认千分位逗号**(答案写 `1,490`,断言写 `/1490/`)——
60
+ 与 0.20.0 修过的 `/3935/` 同一类,当时漏了这一条。已改为 `1[,.]?490`,`--replay` 后
61
+ **59/61**。
62
+ - 剩余两条是**真实的模型遗漏**,不是产品缺陷:`dilution-math` 答案正确(50 µL + 950 µL)
63
+ 却从未调用 `molbio_lab_math`;`batch-analysis` 写出了 CSV,但最终回答只说"报告已生成",
64
+ 没有报出指令要求的长度/特征数。
65
+ - `_freeze-trace.mjs` 新增 `--replace id[,id…]`:只重录被修复作废的条目,其余记录与顺序原样
66
+ 保留,并在 `run.amendments` 里写明每条替换的来源跑分——**替换的是真实录制,不是手改**。
67
+
68
+ ### 5. 未做 / 边界
69
+
70
+ - **`png_path` 仍然不做**:`contract` 的"工作区仍不能存二进制文件"断言在 0.2.1-rc.2 上**仍然
71
+ 通过**,说明上游 fs 缝依旧是 text-only;触发条件未到。
72
+ - **面板版本保持 0.1.3**:`lib/client.js` 逐字节未变,`release-preflight` 复核为
73
+ "already published with identical bytes",因此不 bump。
74
+
75
+ ### 6. 版本与验证
76
+
77
+ - 根包 `0.20.1 → 0.21.0`;DSH 基线钉 **0.2.1-rc.2**(`.github/workflows/{test,release}.yml`、
78
+ preset 头注、`docs/workflow.md`、`docs/roadmap.md`、`docs/maintainer.md`)。
79
+ - `npm test` 全绿:contract 16/16、drift-probe 14 条突变、preset-health 30 行全过、release
80
+ 13 项、benchmark profile/score/coverage/changed、`bench --offline` 61 题。
81
+ - 文档:roadmap 状态、history 的 v0.21.0 段(三条教训)、workflow 的升级提示同步。
82
+
13
83
  ## [0.20.1] — 2026-10-18(插件专属图标)
14
84
 
15
85
  **工具数不变(60),运行时零改动;包内只多一个 746 B 的 SVG。**
@@ -159,6 +159,7 @@ benchmark/
159
159
  ├── _probe.mjs record ground truth (original suite)
160
160
  ├── _probe-all.mjs record ground truth (the rest of the catalog)
161
161
  └── _freeze-trace.mjs freeze a run's responses into the scorer's regression fixture
162
+ (`--replace id[,id]` re-records only the named entries and keeps the rest)
162
163
  ```
163
164
 
164
165
  A task's expected text lives in `tasks/`; the input that produces it lives in
@@ -12,6 +12,20 @@
12
12
  * Run this only when you WANT to move the golden fixture forward (for example
13
13
  * after deliberately changing a task). Test failures point here when a recorded
14
14
  * response stops satisfying its task.
15
+ *
16
+ * ── amending a subset (`--replace`) ─────────────────────────────────────────
17
+ *
18
+ * Re-recording the one or two entries a fix invalidated used to mean freezing a
19
+ * whole new run and discarding every other recording. `--replace` takes just
20
+ * those responses from the new report and keeps the rest, and records where each
21
+ * amended response came from in `run.amendments` — explicit provenance beats a
22
+ * fixture that pretends to be a single run:
23
+ *
24
+ * node benchmark/_freeze-trace.mjs benchmark/reports/<stamp>-suite.json \
25
+ * --replace methylation-block,qpcr-primers
26
+ *
27
+ * The replacement is a REAL recording from the named run, never a hand edit: the
28
+ * original ids, order and every untouched response survive.
15
29
  */
16
30
 
17
31
  import { readFile, writeFile } from 'node:fs/promises';
@@ -19,7 +33,16 @@ import { resolve } from 'node:path';
19
33
 
20
34
  const source = process.argv[2];
21
35
  if (source === undefined) {
22
- console.error('usage: node benchmark/_freeze-trace.mjs <report.json>');
36
+ console.error('usage: node benchmark/_freeze-trace.mjs <report.json> [--replace id[,id…]]');
37
+ process.exit(2);
38
+ }
39
+
40
+ const replaceAt = process.argv.indexOf('--replace');
41
+ const replaceIds = replaceAt === -1
42
+ ? undefined
43
+ : (process.argv[replaceAt + 1] ?? '').split(',').map((id) => id.trim()).filter((id) => id !== '');
44
+ if (replaceAt !== -1 && (replaceIds === undefined || replaceIds.length === 0)) {
45
+ console.error('--replace needs a comma-separated list of task ids');
23
46
  process.exit(2);
24
47
  }
25
48
 
@@ -29,23 +52,74 @@ if (!Array.isArray(report.results) || report.results.length === 0) {
29
52
  process.exit(1);
30
53
  }
31
54
 
32
- const out = {
33
- $comment:
34
- 'Real responses from one benchmark run, frozen so the scorer can be regression-tested without spending API budget. See test/benchmark-score.mjs for why each defect these cover was invisible to synthetic traces.',
35
- run: {
36
- recorded_at: report.startedAt,
37
- harness_version: report.harnessVersion,
38
- profile: report.profile,
39
- model: report.model,
40
- summary: { total: report.summary.total, passed: report.summary.passed },
41
- responses: report.results.map((result) => ({
42
- id: result.id,
43
- tool_calls: result.toolCalls,
44
- answer: result.answer,
45
- })),
46
- },
47
- };
55
+ const responses = report.results.map((result) => ({
56
+ id: result.id,
57
+ tool_calls: result.toolCalls,
58
+ answer: result.answer,
59
+ }));
48
60
 
49
61
  const target = resolve(import.meta.dirname, '..', 'test', 'fixtures', 'benchmark-traces.json');
62
+
63
+ /** The whole-fixture shape: one run, every response from it. */
64
+ function wholeRun() {
65
+ return {
66
+ $comment:
67
+ 'Real responses from one benchmark run, frozen so the scorer can be regression-tested without spending API budget. See test/benchmark-score.mjs for why each defect these cover was invisible to synthetic traces.',
68
+ run: {
69
+ recorded_at: report.startedAt,
70
+ harness_version: report.harnessVersion,
71
+ profile: report.profile,
72
+ model: report.model,
73
+ summary: { total: report.summary.total, passed: report.summary.passed },
74
+ responses,
75
+ },
76
+ };
77
+ }
78
+
79
+ /** The amended shape: keep the fixture, swap the named entries, say so. */
80
+ async function amendedRun() {
81
+ const existing = JSON.parse(await readFile(target, 'utf8'));
82
+ const byId = new Map(existing.run.responses.map((response) => [response.id, response]));
83
+ // Replacing the same id twice must not pile up amendment entries or repeat
84
+ // the provenance sentence: this command is re-run whenever a report is rebuilt.
85
+ const amendments = (existing.run.amendments ?? []).filter((entry) => !replaceIds.includes(entry.id));
86
+ for (const id of replaceIds) {
87
+ const fresh = responses.find((response) => response.id === id);
88
+ if (fresh === undefined) {
89
+ console.error(`--replace: the report has no result for "${id}" — nothing was written`);
90
+ process.exit(1);
91
+ }
92
+ if (!byId.has(id)) console.log(` note: "${id}" was not in the fixture; appending it`);
93
+ byId.set(id, fresh);
94
+ amendments.push({
95
+ id,
96
+ recorded_at: report.startedAt,
97
+ harness_version: report.harnessVersion,
98
+ model: report.model,
99
+ });
100
+ }
101
+
102
+ const ordered = existing.run.responses.map((response) => byId.get(response.id));
103
+ for (const id of replaceIds) {
104
+ if (!existing.run.responses.some((response) => response.id === id)) ordered.push(byId.get(id));
105
+ }
106
+
107
+ return {
108
+ $comment: existing.$comment.includes('run.amendments names each one')
109
+ ? existing.$comment
110
+ : `${existing.$comment} Some responses were re-recorded later; run.amendments names each one and the run it came from, and every other response is unchanged. A replaced entry loses any suspension marker it carried, because the replacement is a fresh recording.`,
111
+ run: {
112
+ ...existing.run,
113
+ amendments,
114
+ responses: ordered,
115
+ },
116
+ };
117
+ }
118
+
119
+ const out = replaceIds === undefined ? wholeRun() : await amendedRun();
50
120
  await writeFile(target, `${JSON.stringify(out, null, 2)}\n`, 'utf8');
51
- console.log(`froze ${out.run.responses.length} response(s) into ${target}`);
121
+ console.log(
122
+ replaceIds === undefined
123
+ ? `froze ${out.run.responses.length} response(s) into ${target}`
124
+ : `amended ${replaceIds.length} response(s) (${replaceIds.join(', ')}) in ${target}; ${out.run.responses.length} total`,
125
+ );
@@ -44,7 +44,8 @@
44
44
  },
45
45
  {
46
46
  "where": "answer",
47
- "pattern": "1490"
47
+ "pattern": "1[,.]?490",
48
+ "note": "the extinction coefficient; the same thousands-separator tolerance the MW assertion already carries (0.20.0 fixed /3935/, not this one)"
48
49
  }
49
50
  ]
50
51
  },
@@ -201,7 +201,7 @@ export function tagVersion(tag) {
201
201
  * `rc.10 < rc.2`, so a release of `rc.2` after `rc.10` would look like a
202
202
  * DOWNGRADE and the gate would refuse a legitimate publish (or, in the other
203
203
  * direction, wave through a stale one). This repository does cut prereleases
204
- * (the harness baseline itself is `0.2.0-rc.2`), so the rule is implemented
204
+ * (the harness baseline itself is `0.2.1-rc.2`), so the rule is implemented
205
205
  * rather than approximated.
206
206
  *
207
207
  * Semver's rule: split on dots; numeric identifiers compare numerically and rank
package/docs/history.md CHANGED
@@ -16,6 +16,8 @@
16
16
 
17
17
  | 包版本 | preset 版本 | 内容 | 工具数 |
18
18
  | --- | --- | --- | --- |
19
+ | 0.21.0 | — | **DSH 0.2.1-rc.2 适配**:preset 行清单逐行重抄;contract 两条断言重锚到行为;两条 suspended 记录用真实模型重录 | 60 |
20
+ | 0.20.1 | — | 插件专属图标(透明底双螺旋 SVG),运行时零改动 | 60 |
19
21
  | 0.20.0 | — | **Cas 家族几何**(SpCas9/SaCas9/Cas12a/Cas13a/b/d)+ 反向链脱靶定位修复;TaqMan MGB/订购 CSV;multiplex 退火温度/浓度;批量分析 CSV;静默位点突变;并入 0.19.1 硬化 | 60 |
20
22
  | 0.19.0 | — | 环形/扇形的**旋转多行标签**(画面正确性);新增 `molbio_alignment_postprocess`(共识/修剪/同一性矩阵/覆盖度) | 58 |
21
23
  | 0.16.0 | — | **修掉 benchmark 交出的前两条缺陷**:引物朝向、甲基化参考表(各带串联/来源回归) | 57 |
@@ -41,6 +43,36 @@
41
43
 
42
44
  ---
43
45
 
46
+ ## v0.21.0(2026-10-23,包 0.21.0)
47
+
48
+ 纯适配版:DSH 0.2.0-rc.2 → 0.2.1-rc.2。工具行为零变化,但上游动了三样本仓库**以为不会动**的东西,
49
+ 每条教训对应一道守卫。
50
+
51
+ **(1) 上游可以在 patch 级版本里重写 preset 行清单。**
52
+ 0.2.1-rc.2 的 `standard` 比 0.2.0-rc.2 少了 `tool-subagent-codex` / `tool-subagent-claude-code` /
53
+ `tool-ralph`,少了 persona 的 `suffix` 与两处 `backgroundMode`,多了 `time-context` /
54
+ `tool-schedule` / `tool-plugin-market`,还改了 plan-mode 的文案——12 处结构差异。`preset-health`
55
+ 把这些当**发布阻断**,所以第一分钟就红了;但 `drift-probe` 里那条"上游 `disabled` 被丢掉"的
56
+ 突变仍拿 `tool-ralph` 当样本,**守卫自己**先失效(上游已经没有这一行)。教训:**fixture 也要
57
+ 跟着上游走**——突变样本必须取自当前上游仍然存在、且仍为 `disabled` 的行,否则"证明了守卫会失败"
58
+ 会变成空话。
59
+
60
+ **(2) 把 contract 断言锚在 minified 字面量上,会把"上游改了写法"报成"产品坏了"。**
61
+ `dsh-tools` 的 `exec.parent === void 0` 在 0.2.1 消失:`presentationMeta` 的投影移进
62
+ `createSuccessResult`,不再按父调用过滤。面板依赖的东西(`meta` 落到成功结果上)没变,变的
63
+ 是我们测量它的方式。同源的第二条:`@deepseek-ai/dsh-client-connection` 的 client 入口从
64
+ `lib/client.js` 改成条件导出 `lib/client.browser.js`,而 Web shell 变成打包产物、不再有
65
+ `require("包名")` 字样——"seeded into the shell"这条兜底判据因此整体失效。教训:断言要问**行为**
66
+ (入口文件存在、`meta` 附在结果上),不要问上游当前的拼写;文件名与导出形状应当**读包清单**,
67
+ 而不是猜。
68
+
69
+ **(3) 两条欠账不是"记着",是"花一次 token 消掉"。**
70
+ `methylation-block` 与 `qpcr-primers` 的冻结响应停在 0.16.0 之前,`benchmark-score` 每次运行
71
+ 都点名它们。这一版用一次完整真实模型跑(61 题)重录,并以 `_freeze-trace --replace` 只替换这两条
72
+ (其余 47 条不动,`run.amendments` 记下来源),悬挂条目归零;这次跑分同时作为 0.21.0 的发布证据。
73
+ **fs 侧仍然不能写二进制**,所以 `png_path` 依旧不做——`contract` 里那条断言仍然是绿的,
74
+ 这就是"触发条件没到"的证据。
75
+
44
76
  ## v0.20.0(2026-10-17,包 0.20.0)
45
77
 
46
78
  这一版把 0.19.1 的四项硬化并入 0.20.0,并落地 2.3 / 2.5 / 2.7 / 2.8 与 survey #8。**最重要的教训是
@@ -32,7 +32,7 @@
32
32
 
33
33
  - **包**:`dsh-molbio-tools`,零依赖 DSH 插件包,**60 个 `molbio_*` 工具**(其中
34
34
  `molbio_plot` 一个工具承载 8 种图表)。
35
- - **基线**:DSH **0.2.0-rc.2**;`package.json` 版本 0.20.1。
35
+ - **基线**:DSH **0.2.1-rc.2**;`package.json` 版本 0.21.0。
36
36
  - **两条分发渠道**:根包(工具 + 面板)与 `packages/molbio-panel`(只面板),
37
37
  **两个独立的 npm 条目、两条独立的版本线**,必须分别发布。
38
38
  - **工具只在专属模式里出现**(设计如此):bundle 的 `cordis.patch.yml` 是**空列表**,
package/docs/roadmap.md CHANGED
@@ -10,12 +10,12 @@ L = 需要新机制或外部资源)。
10
10
 
11
11
  ---
12
12
 
13
- ## 1. 当前状态(0.20.0)
13
+ ## 1. 当前状态(0.21.0)
14
14
 
15
15
  - 60 个 `molbio_*` 工具,零依赖,随 preset 分发(见 [README](../README.md))。
16
16
  - 离线测试金字塔 + 组合漂移守卫全绿;`benchmark/` 提供**模型使用**维度的评估
17
17
  (见 [benchmark/README.md](../benchmark/README.md))。
18
- - 基线:DSH **0.2.0-rc.2**。
18
+ - 基线:DSH **0.2.1-rc.2**。
19
19
  - **CI 已在位**(`.github/workflows/test.yml`,0.18.2 起):push / PR / 手动都跑全套;harness 装
20
20
  **钉死版本**,并用 `npm prefix -g` 显式导出 `DSH_HARNESS_ROOT`。0.18.4 起**同时跑
21
21
  `ubuntu-latest` 与 `windows-latest`**——3.6 已修,套件不再依赖宿主分隔符。
@@ -27,6 +27,11 @@ L = 需要新机制或外部资源)。
27
27
  `molbio_alignment_postprocess`(共识/修剪/同一性矩阵/覆盖度)。
28
28
  - **2.3 / 2.5 / 2.7 / 2.8 与 survey #8 已在 0.20.0 交付**:批量分析 CSV、Cas12a/Cas13 家族几何
29
29
  (含反向链脱靶定位修复)、multiplex 退火温度/浓度建议、TaqMan MGB/订购 CSV、静默位点突变。
30
+ - **0.21.0 是纯适配版**:基线 0.2.0-rc.2 → 0.2.1-rc.2。preset 行清单按上游**逐行重抄**
31
+ (+`time-context` / `tool-schedule` / `tool-plugin-market`;−两个可选 subagent provider 与
32
+ `tool-ralph`;去掉 persona `suffix` 与两处 `backgroundMode`),`contract` 两条断言从上游
33
+ minified 字面量改挂到**行为**,两条 suspended 的 benchmark 记录用真实模型重录。
34
+ 工具数与插件行为零变化。
30
35
 
31
36
  ---
32
37
 
package/docs/workflow.md CHANGED
@@ -425,7 +425,7 @@ git status --short # lib/client.js 与 packages/molbio-panel/lib/clien
425
425
  `standard`** 逐行对比——0.1.7-alpha.1 起上游基线是
426
426
  `<harness>/node_modules/@deepseek-ai/dsh-web-app/presets/standard.patch.yml`
427
427
  (旧的 `@deepseek-ai/dsh-agent-presets/presets/standard/agent.cordis.yml` 已随该包一并消失)。
428
- 只应剩 `skill-filesystem`、`tool-skill`、`tool-molbio` 这三行差异。
428
+ 只应剩 `tool-molbio` 这一行差异(0.2.1-rc.2 起上游自己也带 `skill-filesystem` 与 `tool-skill`)。
429
429
 
430
430
  客户端半的改动还有两条**专门针对"会弄坏 GUI"**的确认:
431
431
 
@@ -456,15 +456,17 @@ git status --short # lib/client.js 与 packages/molbio-panel/lib/clien
456
456
  ### DSH 升级后:preset 组合的维护(必做)
457
457
 
458
458
  `preset/molbio-lab/agent.cordis.yml` 是官方 `standard` 预设的**行清单**副本 + 末尾一行
459
- `tool-molbio`(当前基线:**dsh 0.2.0-rc.2**)。它不会自动跟随 DSH 升级,因此每次升级
459
+ `tool-molbio`(当前基线:**dsh 0.2.1-rc.2**)。它不会自动跟随 DSH 升级,因此每次升级
460
460
  DSH 后:
461
461
 
462
462
  1. 取新版的 shipped `standard`:
463
463
  `<harness>/node_modules/@deepseek-ai/dsh-web-app/presets/standard.patch.yml`;
464
464
  2. 与 `preset/molbio-lab/agent.cordis.yml` 对比(把上游的 `config.plugins:` 块与我们的行清单
465
465
  对齐看),**逐行吸收上游改动**(新增/删除的行、注释、key 顺序、配置契约变化),只保留
466
- `skill-filesystem` / `tool-skill` / `tool-molbio` 这几处有意差异与头部注释。**照抄上游文本,
467
- 不要手写"看起来等价"的行**——上游在 0.1.6-alpha.1 用 `workflow-worker-thread` 教过一次;
466
+ `tool-molbio` 这一处有意差异与头部注释。**照抄上游文本,不要手写"看起来等价"的行**——
467
+ 上游在 0.1.6-alpha.1 用 `workflow-worker-thread` 教过一次。上游这份文件**不一定带行内
468
+ 注释**(0.2.1-rc.2 就是纯行清单):照抄即可,别把我们自己的解释混进去;`preset-health`
469
+ 只比结构,注释漂移抓不住,但下一次人工 diff 会;
468
470
  3. 跑 `node build/preset-patch.mjs` 重新生成 `preset/molbio-lab/preset.patch.yml`;
469
471
  4. 跑 `node test/preset-health.mjs` 直到 `OK`(它把任何结构差异当**发布阻断**,不只是打印
470
472
  note),再跑 `node test/drift-probe.mjs` 确认守卫仍能抓到漂移;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-molbio-tools",
3
- "version": "0.20.1",
3
+ "version": "0.21.0",
4
4
  "description": "Zero-dependency molecular-biology plugin bundle for the DeepSeek Harness: sequence analysis, primer design (incl. cross-intron qPCR primers, TaqMan hydrolysis/MGB probes with ordering CSV and multiplex compatibility), in-silico PCR, cloning simulation, SnapGene .dna parsing and plasmid maps, Sanger verification, protein tools (incl. helical wheel and hydropathy plots), methylation-aware restriction analysis with double-digest buffer checks, qPCR analysis and plotting, multiple sequence alignment that conserves every input residue, conservation analysis, sequence logos, codon-usage analysis, phylogenetic trees with bootstrap support, Newick I/O and long-name label wrapping, CpG-island/GC-skew composition analysis, read-level FASTQ QC, batch file analysis with CSV export, silent restriction-site mutagenesis, family-aware CRISPR gRNA design (SpCas9/SaCas9/Cas12a/Cas13) with off-target search, an opt-in hand-off that shows a generated plot to the model as an attached image, plus a browser-side right-sidebar panel (plasmid maps and sequence logos drawn in the Web GUI) and a literature/protocol/experiment record system.",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
@@ -41,16 +41,24 @@
41
41
  #
42
42
  # ── How this file is maintained ─────────────────────────────────────────────
43
43
  #
44
- # This row list is a COPY of the DSH-shipped `standard` preset with THREE
45
- # intentional differences: `skill-filesystem`, `tool-skill`, and the trailing
46
- # `tool-molbio` row. Everything besides those — every comment, every row, every
47
- # config key, in upstream's ORDER — is upstream text copied verbatim.
48
- #
49
- # Baseline: the `standard` preset of dsh 0.2.0-rc.2 (harness root
44
+ # This row list is a COPY of the DSH-shipped `standard` preset with ONE
45
+ # intentional difference: the trailing `tool-molbio` row. Everything else —
46
+ # every row, every config key, in upstream's ORDER — is upstream's
47
+ # `config.plugins:` block, de-indented by ten spaces. This release's upstream
48
+ # list carries no comments of its own, so none appear below; provenance and the
49
+ # upgrade rules live in this header.
50
+ #
51
+ # Baseline: the `standard` preset of dsh 0.2.1-rc.2 (harness root
50
52
  # `%APPDATA%\npm\node_modules\@deepseek-ai\dsh`, file
51
53
  # `node_modules/@deepseek-ai/dsh-web-app/presets/standard.patch.yml`, whose
52
- # rows live under the `preset-standard` row's `config.plugins:`). Verified
53
- # identical to 0.1.7-alpha.1 — the 0.1.7-alpha.2 and 0.2.0-rc.2 bumps needed no row edit here.
54
+ # rows live under the `preset-standard` row's `config.plugins:`).
55
+ #
56
+ # 0.2.1-rc.2 is the first bump since 0.1.7-alpha.1 that changed the row list: it
57
+ # added `time-context`, `tool-schedule` and `tool-plugin-market`; dropped the two
58
+ # disabled optional subagent providers (`tool-subagent-codex`,
59
+ # `tool-subagent-claude-code`) and `tool-ralph`; dropped the persona `suffix` and
60
+ # both `backgroundMode: continuable` settings; and reworded the plan-mode
61
+ # section. Absorb such a bump verbatim — see workflow.md §4.
54
62
  #
55
63
  # Keeping the text verbatim is not cosmetic: a preset mounts DSH's OWN
56
64
  # packages, so upstream drift here kills the whole preset at mount (the failure
@@ -66,98 +74,47 @@
66
74
  # for the right reason;
67
75
  # 2. re-run `node build/preset-patch.mjs`, then `node test/preset-health.mjs`,
68
76
  # which validates every row's config against the INSTALLED package's own
69
- # schema and reports drift; only the allowed differences above may remain;
77
+ # schema and reports drift; only the allowed difference above may remain;
70
78
  # 3. `node test/drift-probe.mjs` must still catch its mutations — a guard
71
79
  # nobody has seen fail is not a guard.
72
80
  #
73
81
  # Do NOT "fix" `present` (or any other row) by hand to suit the toolset: if the
74
82
  # upstream row is wrong for this preset, fix it upstream, not here.
75
83
 
76
- # ── identity ────────────────────────────────────────────────────────────────
77
-
78
- # The preset's own persona, shadowing the deployment default for this agent.
79
- # `{{model}}` and `{{cwd}}` resolve from the agent's own route and workspace.
80
84
  - id: persona
81
85
  name: '@deepseek-ai/dsh-persona'
82
86
  config:
83
- suffix: Your working directory is {{cwd}}.
84
- prefix: >-
85
- You are a coding agent powered by the {{model}} model.
86
-
87
+ prefix: You are a coding agent powered by the {{model}} model.
87
88
  - id: agent-instructions
88
89
  name: '@deepseek-ai/dsh-agent-instructions'
89
90
  config:
90
91
  maxBytes: 65536
91
-
92
- # ── shell ───────────────────────────────────────────────────────────────────
93
-
94
- # `shell-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
95
- # publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
96
- # the criterion for host-plane ownership — injection resolves before any session
97
- # exists, so there is no agent to key by. Behind a preset realm those variables
98
- # never reached the model's shell at all. Both shell tools consume the host
99
- # registry from here; their executors (`bash-sandbox`/`pwsh-sandbox`) are
100
- # host-plane too.
92
+ - id: time-context
93
+ name: '@deepseek-ai/dsh-time-context'
101
94
  - id: tool-bash
102
95
  name: '@deepseek-ai/dsh-tool-bash'
103
96
  disabled: !!js process.platform === 'win32'
104
-
105
97
  - id: tool-pwsh
106
98
  name: '@deepseek-ai/dsh-tool-pwsh'
107
99
  disabled: !!js process.platform !== 'win32'
108
-
109
- # ── filesystem ──────────────────────────────────────────────────────────────
110
-
111
- # Both register into the host `tools` registry and provide nothing, so
112
- # they need no realm. The `fs` service and its policy stay in the host.
113
100
  - id: tool-fs
114
101
  name: '@deepseek-ai/dsh-tool-fs'
115
-
116
102
  - id: tool-fs-search
117
103
  name: '@deepseek-ai/dsh-tool-fs-search'
118
104
  config:
119
105
  sampleOverCapGlobResults: false
120
-
121
- # ── background jobs ────────────────────────────────────────────────────────
122
-
123
- # Only the model-facing controls. The task REGISTRY stays on the host plane:
124
- # its producers sit outside any realm this file could put it in — `tool-bash`
125
- # above resolves it with `ctx.get`, and an entry-local realm here is invisible
126
- # to every sibling row, so `run_in_background` would answer "background jobs
127
- # unavailable" while these controls sat in the catalog. The registry is keyed by
128
- # owning agent anyway, so one host instance serves every session. What a preset
129
- # chooses is whether its agent can collect and stop background work at all.
130
106
  - id: tool-jobs
131
107
  name: '@deepseek-ai/dsh-tool-jobs'
132
-
133
- # ── skills ──────────────────────────────────────────────────────────────────
134
-
135
- # The skill REGISTRY lives in the host composition and is layered per scope:
136
- # these rows register into THIS preset's layer of it, so they need no realm.
137
- # `skill-filesystem` contributes local-root discovery for agents on this preset, and
138
- # `tool-skill` gives them the catalog and loader; the merged catalog also
139
- # carries whatever the deployment registered globally (repository plugins).
108
+ - id: tool-schedule
109
+ name: '@deepseek-ai/dsh-tool-schedule'
140
110
  - id: skill-filesystem
141
111
  name: '@deepseek-ai/dsh-skill-filesystem'
142
-
143
112
  - id: tool-skill
144
113
  name: '@deepseek-ai/dsh-tool-skill'
145
-
146
- # ── goals ───────────────────────────────────────────────────────────────────
147
-
148
- # The goal service and session driver stay on the host plane, where the Gateway
149
- # can resolve them. The human command and model-facing tool register into this
150
- # preset's scoped layers.
151
114
  - id: command-goal
152
115
  name: '@deepseek-ai/dsh-command-goal'
153
-
154
116
  - id: tool-goal
155
117
  name: '@deepseek-ai/dsh-tool-goal'
156
-
157
- # ── plan mode ───────────────────────────────────────────────────────────────
158
-
159
- # Plan state is per-agent by nature, so an entry-local realm is not a
160
- # workaround here — it is the correct lifetime.
161
118
  - id: planning
162
119
  name: cordis:group
163
120
  group: true
@@ -168,29 +125,17 @@
168
125
  name: '@deepseek-ai/dsh-plan-mode'
169
126
  config:
170
127
  section: |
171
- You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
172
-
173
- Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
128
+ You are in plan mode. Stay in plan mode until the user approves your plan through exit_plan_mode or switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
174
129
 
175
- The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
130
+ Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
176
131
 
177
- Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
132
+ The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
178
133
 
179
- Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
134
+ Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
180
135
 
181
- When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
136
+ Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
182
137
 
183
- # ── compaction ──────────────────────────────────────────────────────────────
184
-
185
- # `compaction-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
186
- # share this realm rather than sit outside it.
187
- #
188
- # `tokenMeter` is deliberately NOT in this realm: the meter stays on the HOST
189
- # plane, and the rows here resolve that one instance. It takes no configuration,
190
- # keys every fold by Session, and owns the context-meter projection units the
191
- # browser reads for every session — behind a realm those units would come and go
192
- # with whichever presets happen to be mounted. What a preset chooses is whether
193
- # its agent compacts at all, which is `compaction-basic` below.
138
+ When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
194
139
  - id: compaction
195
140
  name: cordis:group
196
141
  group: true
@@ -200,28 +145,14 @@
200
145
  config:
201
146
  - id: compaction-basic
202
147
  name: '@deepseek-ai/dsh-compaction-basic'
203
-
204
148
  - id: command-compact
205
149
  name: '@deepseek-ai/dsh-command-compact'
206
-
207
150
  - id: tool-result-pruner
208
151
  name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
209
152
  config:
210
153
  thresholdChars: 8192
211
154
  headChars: 4096
212
155
  tailChars: 1024
213
-
214
- # ── delegation and workflows ────────────────────────────────────────────────
215
-
216
- # The `subagents` registry and its spawn/fork backends live in the HOST
217
- # composition: the registry is a process singleton whose cross-session queries
218
- # the api-proxy serves to the browser, and a provider name may only be
219
- # registered once. This preset contributes the delegation TOOLS, which resolve
220
- # that host registry.
221
- #
222
- # `workflows` is different — nothing outside an agent reads it — so every row
223
- # that reaches it shares one entry-local realm here, and a consumer left
224
- # outside would resolve a host registry this preset does not populate.
225
156
  - id: delegation
226
157
  name: cordis:group
227
158
  group: true
@@ -230,101 +161,44 @@
230
161
  config:
231
162
  - id: tool-subagent-control
232
163
  name: '@deepseek-ai/dsh-tool-subagent-control'
233
-
234
164
  - id: tool-subagent-list-agents
235
165
  name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
236
-
237
166
  - id: tool-subagent
238
167
  name: '@deepseek-ai/dsh-tool-subagent'
239
168
  config:
240
169
  provider: spawn
241
170
  toolName: subagent
242
171
  modelSelectionSettings: true
243
- backgroundMode: continuable
244
-
245
- # Fork omits model selection so provider/model stay equal to the parent and
246
- # the inherited history remains eligible for KV Cache reuse. This preset
247
- # keeps fork continuable; parent and child inherit the same messaging tool,
248
- # while the parent id and return guidance follow the inherited history.
249
172
  - id: tool-subagent-fork
250
173
  name: '@deepseek-ai/dsh-tool-subagent'
251
174
  config:
252
175
  provider: fork
253
176
  toolName: subagent_fork
254
- backgroundMode: continuable
255
-
256
- # Production dsh does not install these optional providers. Install the
257
- # matching Bundle in this Profile and restart the Host, then copy this
258
- # preset and remove `disabled` from the matching tool row. Host availability
259
- # alone grants no tool.
260
- - id: tool-subagent-codex
261
- name: '@deepseek-ai/dsh-tool-subagent'
262
- disabled: true
263
- config:
264
- provider: codex
265
- toolName: subagent_codex
266
- backgroundMode: one-shot
267
- maxDepth: provider-managed
268
-
269
- - id: tool-subagent-claude-code
270
- name: '@deepseek-ai/dsh-tool-subagent'
271
- disabled: true
272
- config:
273
- provider: claude-code
274
- toolName: subagent_claude_code
275
- backgroundMode: one-shot
276
- maxDepth: provider-managed
277
-
278
177
  - id: workflow-ptc
279
178
  name: '@deepseek-ai/dsh-workflow-ptc'
280
179
  config:
281
180
  provider: spawn
282
-
283
181
  - id: tool-workflow
284
182
  name: '@deepseek-ai/dsh-tool-workflow'
285
-
286
- # Off by default with the base row it mirrors: the tool description
287
- # restricts `ralph` to runs the human explicitly asked for, and completion
288
- # is a worker self-report, not an independent evaluation. Preset files take
289
- # no patches, so a session that wants it duplicates this preset under a new
290
- # id and drops `disabled`; the shipped root shadows a copy that reuses this
291
- # id.
292
- - id: tool-ralph
293
- name: '@deepseek-ai/dsh-tool-ralph'
294
- disabled: true
295
- config:
296
- subagentProvider: spawn
297
- maxRounds: 64
298
-
299
- # ── remaining model-facing rows ─────────────────────────────────────────────
300
-
301
183
  - id: tool-ask-user
302
184
  name: '@deepseek-ai/dsh-tool-ask-user'
303
-
304
185
  - id: tool-todo
305
186
  name: '@deepseek-ai/dsh-tool-todo'
306
187
  config:
307
188
  allowParallelInProgress: true
308
-
309
- # The `web` service and its search provider stay in the host composition; only
310
- # the model-facing tool is per-session.
311
189
  - id: tool-web
312
190
  name: '@deepseek-ai/dsh-tool-web'
313
191
  config:
314
192
  fetch: true
315
193
  searchTimeoutMs: 60000
316
-
317
- # Explicit workspace file delivery. Carried by every shipped preset that is not
318
- # the fixed two-tool `minimal` training configuration, so this preset keeps the
319
- # row: without it the agent cannot declare its generated maps/plots/gels as
320
- # deliverables, which is most of what this toolset produces. It consumes only
321
- # host-plane services (fs/session/tools) and publishes none.
322
194
  - id: present
323
195
  name: '@deepseek-ai/dsh-tool-present'
324
-
325
196
  - id: tool-plugin-manager
326
197
  name: '@deepseek-ai/dsh-plugin-manager/tools'
327
198
  disabled: true
199
+ - id: tool-plugin-market
200
+ name: '@deepseek-ai/dsh-plugin-market/tools'
201
+ disabled: true
328
202
 
329
203
  # dsh-molbio-tools: molecular-biology toolset (60 tools across sequence
330
204
  # analysis, primers (incl. TaqMan probes and multiplex compatibility),
@@ -26,84 +26,37 @@
26
26
  - id: persona
27
27
  name: '@deepseek-ai/dsh-persona'
28
28
  config:
29
- suffix: Your working directory is {{cwd}}.
30
- prefix: >-
31
- You are a coding agent powered by the {{model}} model.
32
-
29
+ prefix: You are a coding agent powered by the {{model}} model.
33
30
  - id: agent-instructions
34
31
  name: '@deepseek-ai/dsh-agent-instructions'
35
32
  config:
36
33
  maxBytes: 65536
37
-
38
- # ── shell ───────────────────────────────────────────────────────────────────
39
-
40
- # `shell-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
41
- # publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
42
- # the criterion for host-plane ownership — injection resolves before any session
43
- # exists, so there is no agent to key by. Behind a preset realm those variables
44
- # never reached the model's shell at all. Both shell tools consume the host
45
- # registry from here; their executors (`bash-sandbox`/`pwsh-sandbox`) are
46
- # host-plane too.
34
+ - id: time-context
35
+ name: '@deepseek-ai/dsh-time-context'
47
36
  - id: tool-bash
48
37
  name: '@deepseek-ai/dsh-tool-bash'
49
38
  disabled: !!js process.platform === 'win32'
50
-
51
39
  - id: tool-pwsh
52
40
  name: '@deepseek-ai/dsh-tool-pwsh'
53
41
  disabled: !!js process.platform !== 'win32'
54
-
55
- # ── filesystem ──────────────────────────────────────────────────────────────
56
-
57
- # Both register into the host `tools` registry and provide nothing, so
58
- # they need no realm. The `fs` service and its policy stay in the host.
59
42
  - id: tool-fs
60
43
  name: '@deepseek-ai/dsh-tool-fs'
61
-
62
44
  - id: tool-fs-search
63
45
  name: '@deepseek-ai/dsh-tool-fs-search'
64
46
  config:
65
47
  sampleOverCapGlobResults: false
66
-
67
- # ── background jobs ────────────────────────────────────────────────────────
68
-
69
- # Only the model-facing controls. The task REGISTRY stays on the host plane:
70
- # its producers sit outside any realm this file could put it in — `tool-bash`
71
- # above resolves it with `ctx.get`, and an entry-local realm here is invisible
72
- # to every sibling row, so `run_in_background` would answer "background jobs
73
- # unavailable" while these controls sat in the catalog. The registry is keyed by
74
- # owning agent anyway, so one host instance serves every session. What a preset
75
- # chooses is whether its agent can collect and stop background work at all.
76
48
  - id: tool-jobs
77
49
  name: '@deepseek-ai/dsh-tool-jobs'
78
-
79
- # ── skills ──────────────────────────────────────────────────────────────────
80
-
81
- # The skill REGISTRY lives in the host composition and is layered per scope:
82
- # these rows register into THIS preset's layer of it, so they need no realm.
83
- # `skill-filesystem` contributes local-root discovery for agents on this preset, and
84
- # `tool-skill` gives them the catalog and loader; the merged catalog also
85
- # carries whatever the deployment registered globally (repository plugins).
50
+ - id: tool-schedule
51
+ name: '@deepseek-ai/dsh-tool-schedule'
86
52
  - id: skill-filesystem
87
53
  name: '@deepseek-ai/dsh-skill-filesystem'
88
-
89
54
  - id: tool-skill
90
55
  name: '@deepseek-ai/dsh-tool-skill'
91
-
92
- # ── goals ───────────────────────────────────────────────────────────────────
93
-
94
- # The goal service and session driver stay on the host plane, where the Gateway
95
- # can resolve them. The human command and model-facing tool register into this
96
- # preset's scoped layers.
97
56
  - id: command-goal
98
57
  name: '@deepseek-ai/dsh-command-goal'
99
-
100
58
  - id: tool-goal
101
59
  name: '@deepseek-ai/dsh-tool-goal'
102
-
103
- # ── plan mode ───────────────────────────────────────────────────────────────
104
-
105
- # Plan state is per-agent by nature, so an entry-local realm is not a
106
- # workaround here — it is the correct lifetime.
107
60
  - id: planning
108
61
  name: cordis:group
109
62
  group: true
@@ -114,29 +67,17 @@
114
67
  name: '@deepseek-ai/dsh-plan-mode'
115
68
  config:
116
69
  section: |
117
- You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
118
-
119
- Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
70
+ You are in plan mode. Stay in plan mode until the user approves your plan through exit_plan_mode or switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
120
71
 
121
- The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
72
+ Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
122
73
 
123
- Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
74
+ The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
124
75
 
125
- Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
76
+ Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
126
77
 
127
- When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
78
+ Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
128
79
 
129
- # ── compaction ──────────────────────────────────────────────────────────────
130
-
131
- # `compaction-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
132
- # share this realm rather than sit outside it.
133
- #
134
- # `tokenMeter` is deliberately NOT in this realm: the meter stays on the HOST
135
- # plane, and the rows here resolve that one instance. It takes no configuration,
136
- # keys every fold by Session, and owns the context-meter projection units the
137
- # browser reads for every session — behind a realm those units would come and go
138
- # with whichever presets happen to be mounted. What a preset chooses is whether
139
- # its agent compacts at all, which is `compaction-basic` below.
80
+ When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
140
81
  - id: compaction
141
82
  name: cordis:group
142
83
  group: true
@@ -146,28 +87,14 @@
146
87
  config:
147
88
  - id: compaction-basic
148
89
  name: '@deepseek-ai/dsh-compaction-basic'
149
-
150
90
  - id: command-compact
151
91
  name: '@deepseek-ai/dsh-command-compact'
152
-
153
92
  - id: tool-result-pruner
154
93
  name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
155
94
  config:
156
95
  thresholdChars: 8192
157
96
  headChars: 4096
158
97
  tailChars: 1024
159
-
160
- # ── delegation and workflows ────────────────────────────────────────────────
161
-
162
- # The `subagents` registry and its spawn/fork backends live in the HOST
163
- # composition: the registry is a process singleton whose cross-session queries
164
- # the api-proxy serves to the browser, and a provider name may only be
165
- # registered once. This preset contributes the delegation TOOLS, which resolve
166
- # that host registry.
167
- #
168
- # `workflows` is different — nothing outside an agent reads it — so every row
169
- # that reaches it shares one entry-local realm here, and a consumer left
170
- # outside would resolve a host registry this preset does not populate.
171
98
  - id: delegation
172
99
  name: cordis:group
173
100
  group: true
@@ -176,101 +103,44 @@
176
103
  config:
177
104
  - id: tool-subagent-control
178
105
  name: '@deepseek-ai/dsh-tool-subagent-control'
179
-
180
106
  - id: tool-subagent-list-agents
181
107
  name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
182
-
183
108
  - id: tool-subagent
184
109
  name: '@deepseek-ai/dsh-tool-subagent'
185
110
  config:
186
111
  provider: spawn
187
112
  toolName: subagent
188
113
  modelSelectionSettings: true
189
- backgroundMode: continuable
190
-
191
- # Fork omits model selection so provider/model stay equal to the parent and
192
- # the inherited history remains eligible for KV Cache reuse. This preset
193
- # keeps fork continuable; parent and child inherit the same messaging tool,
194
- # while the parent id and return guidance follow the inherited history.
195
114
  - id: tool-subagent-fork
196
115
  name: '@deepseek-ai/dsh-tool-subagent'
197
116
  config:
198
117
  provider: fork
199
118
  toolName: subagent_fork
200
- backgroundMode: continuable
201
-
202
- # Production dsh does not install these optional providers. Install the
203
- # matching Bundle in this Profile and restart the Host, then copy this
204
- # preset and remove `disabled` from the matching tool row. Host availability
205
- # alone grants no tool.
206
- - id: tool-subagent-codex
207
- name: '@deepseek-ai/dsh-tool-subagent'
208
- disabled: true
209
- config:
210
- provider: codex
211
- toolName: subagent_codex
212
- backgroundMode: one-shot
213
- maxDepth: provider-managed
214
-
215
- - id: tool-subagent-claude-code
216
- name: '@deepseek-ai/dsh-tool-subagent'
217
- disabled: true
218
- config:
219
- provider: claude-code
220
- toolName: subagent_claude_code
221
- backgroundMode: one-shot
222
- maxDepth: provider-managed
223
-
224
119
  - id: workflow-ptc
225
120
  name: '@deepseek-ai/dsh-workflow-ptc'
226
121
  config:
227
122
  provider: spawn
228
-
229
123
  - id: tool-workflow
230
124
  name: '@deepseek-ai/dsh-tool-workflow'
231
-
232
- # Off by default with the base row it mirrors: the tool description
233
- # restricts `ralph` to runs the human explicitly asked for, and completion
234
- # is a worker self-report, not an independent evaluation. Preset files take
235
- # no patches, so a session that wants it duplicates this preset under a new
236
- # id and drops `disabled`; the shipped root shadows a copy that reuses this
237
- # id.
238
- - id: tool-ralph
239
- name: '@deepseek-ai/dsh-tool-ralph'
240
- disabled: true
241
- config:
242
- subagentProvider: spawn
243
- maxRounds: 64
244
-
245
- # ── remaining model-facing rows ─────────────────────────────────────────────
246
-
247
125
  - id: tool-ask-user
248
126
  name: '@deepseek-ai/dsh-tool-ask-user'
249
-
250
127
  - id: tool-todo
251
128
  name: '@deepseek-ai/dsh-tool-todo'
252
129
  config:
253
130
  allowParallelInProgress: true
254
-
255
- # The `web` service and its search provider stay in the host composition; only
256
- # the model-facing tool is per-session.
257
131
  - id: tool-web
258
132
  name: '@deepseek-ai/dsh-tool-web'
259
133
  config:
260
134
  fetch: true
261
135
  searchTimeoutMs: 60000
262
-
263
- # Explicit workspace file delivery. Carried by every shipped preset that is not
264
- # the fixed two-tool `minimal` training configuration, so this preset keeps the
265
- # row: without it the agent cannot declare its generated maps/plots/gels as
266
- # deliverables, which is most of what this toolset produces. It consumes only
267
- # host-plane services (fs/session/tools) and publishes none.
268
136
  - id: present
269
137
  name: '@deepseek-ai/dsh-tool-present'
270
-
271
138
  - id: tool-plugin-manager
272
139
  name: '@deepseek-ai/dsh-plugin-manager/tools'
273
140
  disabled: true
141
+ - id: tool-plugin-market
142
+ name: '@deepseek-ai/dsh-plugin-market/tools'
143
+ disabled: true
274
144
 
275
145
  # dsh-molbio-tools: molecular-biology toolset (60 tools across sequence
276
146
  # analysis, primers (incl. TaqMan probes and multiplex compatibility),
package/test/contract.mjs CHANGED
@@ -67,6 +67,29 @@ function packageDir(pkg) {
67
67
  return join(harnessRoot, 'node_modules', ...pkg.split('/'));
68
68
  }
69
69
 
70
+ /**
71
+ * Resolve a package's client entry the way the boot scan does: read its OWN
72
+ * `exports["./client"]` and take the `browser` condition (the shell is the
73
+ * browser), then `default`.
74
+ *
75
+ * The path is NOT assumed to be `lib/client.js`. 0.2.1-rc.2 moved
76
+ * `@deepseek-ai/dsh-client-connection` to `lib/client.browser.js`; a hardcoded
77
+ * filename reported a package the scanner loads fine, which is a test defect
78
+ * wearing a contract failure's clothes.
79
+ *
80
+ * @param {string} dir the installed package directory.
81
+ * @returns {string|undefined} an absolute entry path, or undefined when the
82
+ * package declares no `./client` export (it is not a client package).
83
+ */
84
+ function clientEntryPath(dir) {
85
+ const manifestPath = join(dir, 'package.json');
86
+ if (!existsSync(manifestPath)) return undefined;
87
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
88
+ const entry = manifest.exports?.['./client'];
89
+ const rel = typeof entry === 'string' ? entry : entry?.browser ?? entry?.default;
90
+ return typeof rel === 'string' ? join(dir, rel) : undefined;
91
+ }
92
+
70
93
  /** Read one installed client bundle, or undefined when the package is absent. */
71
94
  function clientBundle(pkg) {
72
95
  const path = join(packageDir(pkg), 'lib', 'client.js');
@@ -263,21 +286,35 @@ check('the package manifest keeps the client declaration the scan keys on', () =
263
286
  assert.equal(clientRel, './lib/client.js', 'exports["./client"] points at the artifact');
264
287
  assert.ok(existsSync(join(packageRoot, clientRel)), 'the artifact exists');
265
288
 
266
- // An injected client package must be a boot-graph row — either mounted with
267
- // its own bundle on disk, or seeded into the shell (sidebar-right and slots
268
- // ship inside the Web frontend, so they have no `lib/client.js` to find here).
289
+ // An injected client package must be a boot-graph row. Ask the package where
290
+ // its client entry is (exports["./client"], browser condition) instead of
291
+ // assuming `lib/client.js`: the scan resolves the manifest, and 0.2.1-rc.2
292
+ // moved `dsh-client-connection` to `lib/client.browser.js`.
269
293
  const shellDir = join(packagesDir, 'dsh-web-frontend', 'dist');
270
294
  const shellText = existsSync(shellDir)
271
295
  ? filesUnder(shellDir).filter((path) => path.endsWith('.js')).map((path) => readFileSync(path, 'utf8')).join('\n')
272
296
  : '';
273
297
  for (const dependency of manifest.dsh.client.inject) {
274
- const dir = packageDir(dependency);
275
- const mounted = existsSync(join(dir, 'lib', 'client.js'));
298
+ const entry = clientEntryPath(packageDir(dependency));
299
+ const mounted = entry !== undefined && existsSync(entry);
300
+ // A package genuinely seeded into the shell has no file of its own. That
301
+ // fallback is kept, but it is only evidence when the shell actually names
302
+ // the package — the current shell is a minified bundle that names none.
276
303
  const seeded = shellText.includes(`require("${dependency}")`) || shellText.includes(`require('${dependency}')`);
277
- assert.ok(mounted || seeded, `${dependency} is either a mounted client package or seeded into the shell`);
278
- assert.ok(existsSync(join(dir, 'package.json')) || seeded, `${dependency} resolves as a package or comes from the shell`);
304
+ assert.ok(mounted || seeded, `${dependency} resolves a ./client entry (declared: ${entry ?? 'none'})`);
279
305
  }
280
306
 
307
+ // The resolver must be able to say NO, and must find the entry of the package
308
+ // that moved — otherwise the loop above could pass on a directory that merely
309
+ // exists.
310
+ assert.equal(
311
+ clientEntryPath(packageDir('@deepseek-ai/dsh-tools')),
312
+ undefined,
313
+ 'a package with no exports["./client"] resolves to no client entry',
314
+ );
315
+ const movedEntry = clientEntryPath(packageDir('@deepseek-ai/dsh-client-connection'));
316
+ assert.ok(movedEntry !== undefined && existsSync(movedEntry), 'the conditional client entry resolves to a file on disk');
317
+
281
318
  // The panel-only package must carry the same declaration: it is the channel a
282
319
  // shared profile installs.
283
320
  const panelManifestPath = join(packageRoot, 'packages', 'molbio-panel', 'package.json');
@@ -328,13 +365,21 @@ check('the published tarball would carry the client half and the panel package',
328
365
  });
329
366
 
330
367
  check('the toolview card contract still holds (meta path + keyed seat)', () => {
331
- // The card reads the tool-result block's `meta`, which the tool layer fills
332
- // from a tool's `output.presentationMeta` — for a ROOT call only.
368
+ // The card reads the tool-result block's `meta`, filled from a tool's
369
+ // `output.presentationMeta`. 0.2.1-rc.2 removed the `exec.parent === void 0`
370
+ // gate this check used to pin: the projection now runs for every successful
371
+ // execution, and the root call the card reads is a subset of that. So pin the
372
+ // path that still carries the value — consulted, snapshotted, attached as
373
+ // `meta` — rather than a literal upstream is free to drop (the panel does not
374
+ // depend on nested calls being excluded, only on the root call carrying it).
333
375
  const tools = hostHalf('@deepseek-ai/dsh-tools');
334
376
  assert.ok(tools !== undefined, 'dsh-tools is installed');
335
377
  assert.ok(/output\.presentationMeta\s*!==\s*void 0/.test(tools.text), 'the tool layer still consults output.presentationMeta');
336
- assert.ok(tools.text.includes('exec.parent === void 0'), 'and calls it for root calls');
337
378
  assert.ok(tools.text.includes('snapshotProjection(tool.name, "presentationMeta"'), 'and records the projection on the result');
379
+ assert.ok(
380
+ /meta !== void 0 \? \{ meta \} : \{\}/.test(tools.text),
381
+ 'and attaches the snapshot to the success result as `meta`, which is what the card reads',
382
+ );
338
383
  assert.ok(/output \{ schema, render, presentationMeta\? \}/.test(tools.text), 'register() accepts presentationMeta on a raw output');
339
384
 
340
385
  // The browser side: the seat exists and is keyed by tool name.
@@ -13,8 +13,10 @@
13
13
  * no released DSH installs. The row mounted nothing and the preset died at
14
14
  * load. The id comparison saw a row named `workflow-worker-thread`, noted it
15
15
  * as drift, printed a note, and exited 0.
16
- * - The same composition enabled `tool-ralph`, which upstream `standard`
17
- * ships with `disabled: true`. Same row id, so the guard was blind to it.
16
+ * - The same composition enabled `tool-ralph`, which the then-shipped
17
+ * `standard` preset carried with `disabled: true`. Same row id, so the guard
18
+ * was blind to it. (0.2.1-rc.2 no longer ships that row; the probe mutates
19
+ * `tool-plugin-manager`, which it still ships disabled.)
18
20
  *
19
21
  * So this file mutates a copy of the composition and asserts the guard reports
20
22
  * the mutation, then asserts a clean composition reports NOTHING (a guard that
@@ -125,9 +127,16 @@ check('a phantom provider package is caught (v17: dsh-workflow-worker-thread)',
125
127
  assert.match(text, /extra row "workflow-worker-thread" \(not in shipped standard\)/, text);
126
128
  });
127
129
 
128
- check('an upstream `disabled: true` that was dropped is caught (v17: tool-ralph)', () => {
129
- const text = driftText(withRow('tool-ralph', { disabled: undefined }));
130
- assert.match(text, /row "tool-ralph": upstream ships it disabled and this preset enables it/, text);
130
+ check('an upstream `disabled: true` that was dropped is caught (v17: tool-plugin-manager)', () => {
131
+ // v17 mutates whichever row the shipped standard carries disabled: the
132
+ // mechanism is the same, and naming a row upstream has since dropped would
133
+ // make this probe pass by mutating nothing.
134
+ const text = driftText(withRow('tool-plugin-manager', { disabled: undefined }));
135
+ assert.match(
136
+ text,
137
+ /row "tool-plugin-manager": upstream ships it disabled and this preset enables it/,
138
+ text,
139
+ );
131
140
  });
132
141
 
133
142
  // ── the other ways a copied composition rots ───────────────────────────────
@@ -1,5 +1,5 @@
1
1
  {
2
- "$comment": "Real responses from one benchmark run, frozen so the scorer can be regression-tested without spending API budget. See test/benchmark-score.mjs for why each defect these cover was invisible to synthetic traces. Two recordings are suspended via answer_excluded_from_scoring/answer_excluded_reason: they were made before the 0.16.0 primer-orientation and methylation-table fixes, so they answer the pre-fix tool, and only a real model run can refresh them.",
2
+ "$comment": "Real responses from one benchmark run, frozen so the scorer can be regression-tested without spending API budget. See test/benchmark-score.mjs for why each defect these cover was invisible to synthetic traces. Two responses (methylation-block, qpcr-primers) were recorded before the 0.16.0 primer-orientation and methylation-table fixes and were re-recorded on 2026-10-23 against DSH 0.2.1-rc.2; run.amendments names each one and the run it came from, and every other response is unchanged.",
3
3
  "run": {
4
4
  "recorded_at": "2026-09-23T12:03:15.526Z",
5
5
  "harness_version": "0.1.7-alpha.2",
@@ -38,14 +38,13 @@
38
38
  "molbio_extract_region",
39
39
  "molbio_methylation_check",
40
40
  "molbio_methylation_check",
41
+ "molbio_enzyme_lookup",
41
42
  "molbio_methylation_check",
43
+ "pwsh",
42
44
  "molbio_restriction_sites",
43
- "web_search",
44
- "web_fetch"
45
+ "molbio_restriction_sites"
45
46
  ],
46
- "answer": "## What's going on\n\nYour BamHI site is real — it's the single `GGATCC` in the polylinker at **905–910** (cut at 906), inside the MCS/lacZα region of pUC118 (3162 bp, circular). But that hexamer **contains the Dam target `GATC` in its middle four bases**, so on DNA prepared from a normal *E. coli* host the overlapping adenine is methylated — and the methylation analysis rates BamHI as **impaired**.\n\n## Methylation check (pUC118, circular, 3162 bp)\n\n- **Dam (GATC): 15 sites**\n- **Dcm (CCWGG): 5 sites**\n- 21 enzymes checked → **8 usable, 0 blocked, 2 impaired**\n\n### BamHI\n| | |\n|---|---|\n| Verdict | **impaired (dam)** |\n| Site | `GGATCC` 905–910, single site, cut at 906 |\n| Why | `GGATCC` embeds `GATC` (bases 2–5) — a Dam site overlapping the recognition sequence |\n\n**BstYI** is the other casualty: `impaired (dam)`, 7 sites (905, 1923, 1934, 2020, 2032, 2800, 2817). Nothing was called fully **blocked**.\n\n## Enzymes the tool considers usable\n\n| Enzyme | Cut position(s) | Fragments (circular) |\n|---|---|---|\n| HincII | 896 | 3162 (single cut) |\n| KpnI | 919 | 3162 (single cut) |\n| NaeI | 515 | 3162 (single cut) |\n| PvuII | 785, 1107 | **2840 + 322** (only two-cutter) |\n| SacI | 925 | 3162 (single cut) |\n| ScaI | 2656 | 3162 (single cut) |\n| SmaI | 913 | 3162 (single cut) |\n| XbaI | 900 | 3162 (single cut) |\n\nNote that KpnI, SacI, SmaI and XbaI all sit in the same MCS neighbourhood as BamHI, so they're the natural substitutes for MCS-based cloning/diagnostics. XbaI (`TCTAGA`) is usable here because in `...TCTAGAGGATCC...` it is not followed by `TC`, so no `GATC` overlaps it.\n\n## The consistent story for your failed digest\n\nThe \"barely cut / partial\" phenotype matches the **impaired** (reduced-cleavage) call rather than a hard block: the embedded `GATC` is Dam-methylated in the standard `dam+` host, and BamHI cleaves that overlap poorly. Adding more enzyme or longer incubation won't fully rescue it if the template is already methylated.\n\nTwo practical fixes:\n1. **Re-prep the plasmid from a dam⁻/dcm⁻ host** (e.g., JM110/SCS110) — then the site is unmethylated and BamHI should cut to completion. This is the clean fix if you specifically need BamHI.\n2. **Switch to a usable MCS enzyme** (XbaI, SmaI, SacI, KpnI here) for diagnostic or cloning digests, or use PvuII if you want an internal two-band check (2840 + 322 bp).\n\n## Caveat\n\nThe tool's methylation-sensitivity table is a **hand-transcribed NEB/REBASE-style quick reference**, and it says so explicitly: dam/dcm blocking can vary with enzyme lot and the methylation state of the DNA. You should confirm BamHI's Dam sensitivity against the supplier's current table before the experiment depends on it (see the [NEB DNA methylation & restriction digest application note](https://prd-sccd01.neb.com/en-sg/-/media/nebus/files/application-notes/appnote_dna_methylation_and_restriction_digests.pdf) and the [Promega restriction-enzyme methylation-sensitivity table](https://www.promega.com/resources/guides/nucleic-acid-analysis/restriction-enzyme-resource/restriction-enzyme-resource-tables/restriction-enzyme-methylation-sensitivity/)). Independently of the table, the structural fact driving the call is unambiguous: the pUC118 BamHI site carries a `GATC`, so Dam status of your prep is worth knowing.",
47
- "answer_excluded_from_scoring": true,
48
- "answer_excluded_reason": "recorded before the 0.16.0 methylation-table fix: the model correctly reported the tool's then-verdict (BamHI impaired by dam) and cross-checked the supplier. With the table corrected the tool reports BamHI as cutting, so this answer predates the behaviour it would now be graded against. Re-record with `node benchmark/run.mjs --model --task methylation-block` then `node benchmark/_freeze-trace.mjs benchmark/reports/<run>.json`."
47
+ "answer": "## Bottom line\n\n**Methylation is not your BamHI problem.** pUC118 carries 15 Dam sites and 5 Dcm sites, and the BamHI recognition site itself contains a Dam `GATC` — but BamHI is **not Dam- or Dcm-sensitive**, so the analysis classes it as usable and predicts a clean single cut.\n\n## The marks found (pUC118, 3162 bp circular)\n\n| Mark | Motif | Count | 1-based positions |\n|---|---|---|---|\n| Dam | GATC | 15 | 753, **906**, 1849, 1924, 1935, 1943, 2021, 2033, 2138, 2479, 2497, 2543, 2801, 2818, 2854 |\n| Dcm | CCWGG | 5 | 830, 1021, 1309, 1430, 1443 |\n\n## BamHI specifically\n\n- Site: **GGATCC at 905–910**, cut **G^GATCC at 906** → one fragment of 3162 bp (linearised plasmid).\n- That site *does* contain an embedded Dam site: `GGATCC` = G + **GATC** + C, with the `GATC` at 906–909. So the plasmid is methylated at the very base pair BamHI needs to cut.\n- Nevertheless the tool returns **BamHI: usable** — BamHI is insensitive to Dam methylation (and to Dcm). The embedded `GATC` is a red herring for this enzyme.\n- No enzyme in the 103-enzyme panel is fully **blocked**; the only flagged enzyme is **BstYI (impaired, Dam)**.\n\nThat last point is a useful positive control for the analysis: **BstYI (R^GATCY)** recognises the *same* `GGATCC` at 905–910 (plus six other RGATCC sites, all of which contain `GATC` at 1924, 1935, 2021, 2033, 2801, 2818) and is correctly flagged as Dam-impaired. So the tool is not simply waving everything through — it distinguishes BamHI from a genuinely Dam-sensitive enzyme at an identical site.\n\n## Enzymes the analysis considers safe here\n\n**46 of 103** panel enzymes are usable (0 blocked, 1 impaired, the rest have no site on this plasmid at all). The everyday ones:\n\n**BamHI, EcoRI, HindIII, KpnI, PstI, SacI, SalI, SmaI, XbaI** — all single-cutters in the MCS and all methylation-safe.\n\nFull usable list: AatII, Acc65I, AseI, BamHI, BfuAI, BglI, BsaAI, BsaI, BsmBI, BspQI, BtgZI, BveI, DraI, DraIII, EcoRI, Esp3I, FokI, FspI, HincII, HindIII, KasI, KpnI, LguI, NaeI, NarI, NgoMIV, PciI, PluTI, PsiI, PstI, PvuI, PvuII, SacI, SalI, SapI, SbfI, ScaI, SfoI, SmaI, SphI, SspI, TspMI, XbaI, XmaI, XmnI, ZraI.\n\nTwo caveats on that list: \"usable\" means *methylation-safe*, not *single-cutter* — e.g. **AseI cuts 3×** (1054, 1113, 2348 → 1868/1235/59 bp) and **BglI cuts 2×** (728, 2296 → 1594/1568 bp). And ClaI, BclI, BglII and XhoI are absent from every list because they have **no site** on pUC118 — a different reason for not working, unrelated to methylation.\n\n## Caveats\n\n- The sensitivity table is a curated REBASE/NEB-style quick reference; blocking can depend on the enzyme lot and the actual methylation state, so confirm against the supplier's current table before betting an experiment on it.\n- Dam/Dcm are the marks a standard `dam+`/`dcm+` *E. coli* host adds. If pUC118 was grown in a `dam−/dcm−` host, none of these marks are present.\n- CpG methylation is **not** modelled — relevant only if the DNA came from a mammalian source. BamHI *is* blocked by overlapping CpG methylation, but plasmid DNA from *E. coli* carries none.\n\nSo if the BamHI lane is empty or uncut, look elsewhere: buffer/incubation temperature and time, salt or EDTA carryover, DNA quality/contamination, enzyme lot or repeated freeze–thaw, and gel loading — not Dam/Dcm."
49
48
  },
50
49
  {
51
50
  "id": "qpcr-ddct",
@@ -82,9 +81,7 @@
82
81
  "tool_calls": [
83
82
  "molbio_design_primers"
84
83
  ],
85
- "answer": "**Best candidate primer pair** (Tm model: SantaLucia 1998 nearest‑neighbour, 50 mM Na⁺/1.5 mM Mg²⁺/0.8 mM dNTP/200 nM primer — an estimate, not an instrument measurement).\n\n| | Sequence (5′→3′) | Position on template | Length | Tm* | GC% |\n|---|---|---|---|---|---|\n| Forward | `ATTACTCCTGCTCTTCCCATAC` | 201–222 | 22 nt | 58.4 °C | 45.5 |\n| Reverse | `CACTACTCCTCTGTACGCAC` | 105–124 | 20 nt | 58.9 °C | 55.0 |\n\n**Amplicon:** 105–222 = **118 bp** (within your 80–150 bp window)\n**Tm difference:** 0.5 °C (well matched)\n**Structure:** no hairpins (0 °C); self‑complementarity scores 3/1 (F) and 7/2 (R), both below the Primer3 thresholds (8 overall / 3 at the 3′ end) — no GC clamp or run violations flagged.\n\nNotes: the reverse primer is written 5′→3′ as ordered; its 105–124 span refers to the top‑strand binding site it anneals to (it reads right‑to‑left on the top strand). The designer returned this as the single top‑ranked pair out of the constraints you set, penalty 5.5 (lower is better).",
86
- "answer_excluded_from_scoring": true,
87
- "answer_excluded_reason": "recorded before the 0.16.0 primer-orientation fix: the answer names amplicon 105-222 (118 bp) with the roles as the buggy designer printed them (F 201-222 downstream of R 105-124). The corrected designer reports 105-223 (119 bp) with F upstream, so this answer predates the behaviour it would now be graded against. Re-record with `node benchmark/run.mjs --model --task qpcr-primers` then freeze."
84
+ "answer": "**Best qPCR primer pair** (one candidate satisfied all constraints):\n\n| | Sequence (5'→3') | Position | Length | Tm* | GC |\n|---|---|---|---|---|---|\n| Forward | `GTGCGTACAGAGGAGTAGTG` | 105–124 | 20 nt | 58.9 °C | 55.0% |\n| Reverse | `CGTATGGGAAGAGCAGGAG` | 205–223 | 19 nt | 58.6 °C | 57.9% |\n\n- **Amplicon:** 105–223 = **119 bp** (inside your 80–150 bp window)\n- **Tm Δ:** 0.3 °C; no hairpins detected (0 °C), low self-complementarity (best score 7.0/2.0 for F, 4.0/2.5 for R) — no primer-dimer or hairpin red flags.\n\n*Tm is a SantaLucia-1998 nearest-neighbour estimate (50 mM Na⁺, 1.5 mM Mg²⁺, 0.8 mM dNTP, 200 nM primer), not an instrument calibration — confirm with your own melt curve.\n\nThe pair was ranked best of the candidates found; the search returned only this one pair meeting all four of your hard constraints, so there is no runner-up trade-off to weigh."
88
85
  },
89
86
  {
90
87
  "id": "fastq-qc",
@@ -552,6 +549,20 @@
552
549
  ],
553
550
  "answer": "## Parsed results\n\n| Property | pUC118.gb (GenBank) | pUC118.dna (SnapGene) |\n|---|---|---|\n| Name | **pUC118** | **pUC118** |\n| Accession | **U07649** (VERSION U07649.1) | label carries **U07649** |\n| Length | 3162 bp | 3162 bp |\n| Topology | **circular** (`LOCUS … circular SYN`) | **circular** |\n| AmpR feature | CDS **2102..2962**, `/label=\"AmpR\"`, `/gene=\"bla\"`, `/product=\"beta-lactamase\"` | CDS 2102‑2962, **AmpR** (annotated reverse) |\n\nBoth records also share the same source line (`synthetic construct`). The GenBank file is sparsely annotated (only `source` and the AmpR CDS); the SnapGene file carries 10 features including `ori`, `AmpR promoter`, `MCS`, `lac promoter`, `lac operator`, `M13 fwd/rev`, `lacZα` and the `M13 ori`.\n\n## AmpR coordinates\n\n- Span is identical in both files: **2102–2962, 861 bp**, encoding a 286‑aa beta-lactamase (TEM‑1-type: `MSIQHFRVALIPFFAAFCLPVFAHPETLVKVKDAEDQLGARVGYIELDLNSGKILESFRPEERFPMMSTFKVLLCGAVLSRIDAGQEQLGRRIHYSQNDLVEYSPVTEKHLTDGMTVRELCSAAITMSDNTAANLLLTTIGGPKELTAFLHNMGDHVTRLDRWEPELNEAIPNDERDTTMPVAMATTLRKLLTGELLTLASRQQLIDWMEADKVAGPLLRSALPAGWFIADKSGAGERGSRGIIAALGPDGKPSRIVVIYTTGSQATMDERNRQIAEIGASLIKHW`).\n- **The strand annotation disagrees.** SnapGene marks it reverse (minus) strand. The GenBank record writes a plain `2102..2962` (i.e. forward) with no `complement`. Translating confirms SnapGene is right: the reverse complement is a clean 286‑aa ORF, whereas the forward strand has 12 stops and no ORF ≥30 aa. So the `.gb` file's AmpR strand is a mis-annotation, not a different molecule.\n\n## Do the two files describe the same molecule?\n\n**Yes.** Aligning the full sequences gives **100% identity across all 3162 aligned columns**, with the same length and same circular topology. They are the same pUC118 U07649 molecule.\n\nThe only substantive difference is annotation, not DNA:\n1. The `.dna` file is richly annotated (10 features); the `.gb` file has only `source` + AmpR.\n2. For AmpR the coordinate span matches exactly, but the `.gb` file leaves it on the forward strand — the correct strand is reverse, as the SnapGene file (and the actual beta-lactamase ORF) shows. Worth fixing in the GenBank record if you plan to use it for cloning or maps.\n\nCaveat: the SnapGene parse exposed the label \"pUC118 (U07649)\" but no separate accession field, so the accession U07649 is explicitly present only in the GenBank record."
554
551
  }
552
+ ],
553
+ "amendments": [
554
+ {
555
+ "id": "methylation-block",
556
+ "recorded_at": "2026-10-23T12:35:37.379Z",
557
+ "harness_version": "0.2.1-rc.2",
558
+ "model": "the profile default (deepseek-flash unless the profile or session overrides it)"
559
+ },
560
+ {
561
+ "id": "qpcr-primers",
562
+ "recorded_at": "2026-10-23T12:35:37.379Z",
563
+ "harness_version": "0.2.1-rc.2",
564
+ "model": "the profile default (deepseek-flash unless the profile or session overrides it)"
565
+ }
555
566
  ]
556
567
  }
557
568
  }
@@ -215,7 +215,7 @@ function toolCountDrift(description, registered) {
215
215
  * order. Comparing ids alone (the v15–v17 behaviour) misses exactly the two
216
216
  * defects that reached a release: a provider package the harness does not
217
217
  * install (`workflow-worker-thread`) and an upstream `disabled: true` that had
218
- * been dropped (`tool-ralph`).
218
+ * been dropped (v17's `tool-ralph`; 0.2.1-rc.2 ships `tool-plugin-manager` disabled).
219
219
  */
220
220
  function compositionDrift(standardRows, mineRows) {
221
221
  const drift = [];