dsh-baton-mcp 0.6.1 → 0.6.2

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/README.md CHANGED
@@ -50,7 +50,7 @@ Requires DeepSeek Harness ≥ 0.2.1-rc.2 (signed in) and Claude Code or Codex. N
50
50
 
51
51
  Switched on, the plugin registers dsh-baton for Claude Code and Codex, for every folder, and Harness says so.
52
52
 
53
- **2. Codex only, once:** Codex **Settings → Hooks** → **Trust** **dsh-baton: waiting for the Harness run**. Codex is then woken when a run ends instead of checking every 270 s. You trust it once: updates and Harness restarts do not ask again. Skip it and Codex waits with `dsh_wait` as before; the first hand-over reminds you.
53
+ **2. Codex only, once:** Codex **Settings → Hooks** → **Trust** **dsh-baton: waiting for the Harness run**. The hook comes with plugin 0.6.0 and later (0.6.2 and later when you have no Node.js), and shows up in that list only after you restart Harness and then Codex once, after installing or updating the plugin. Node.js is not required: without one it runs on Harness's own runtime, like the server. Codex is then woken when a run ends instead of checking every 270 s. You trust it once: updates and Harness restarts do not ask again. Skip it and Codex waits with `dsh_wait` as before; the first hand-over reminds you.
54
54
 
55
55
  **3. Check:** in a **new** Claude Code or Codex conversation, ask it to call `dsh_list_workspaces`. It lists your Harness workspaces.
56
56
 
@@ -84,43 +84,65 @@ You don't have to take DeepSeek's word for it. Before the run, dsh-baton notes w
84
84
 
85
85
  ## Token usage
86
86
 
87
- **In short: hand over the big, long jobs.** A feature that spans many files, a large refactor, a whole small app: that is where it saves the most. Claude Code's own bill drops by 65–81%, and Codex gets 2 to 5 times as many tasks out of a ChatGPT Plus 5-hour window (5 times on a medium task with GPT-6 Astra).
87
+ **In short: hand over the big, long jobs.** A feature that spans many files, a large refactor, a whole small app: that is where it saves the most. Claude Code spends 51–72% less in total, DeepSeek included. Codex now saves on both test tasks (Sol 40% and Astra 81% less on the web app), and a GPT-6 Astra web-app task takes 3% of a ChatGPT Plus 5-hour window instead of 22%. On a longer job, a three-phase kanban app (Task 4), Astra spends 73% less in total and uses about 16% of the window instead of 86%.
88
88
 
89
- **The honest part:** for small tasks, Codex on GPT-6.1 Sol is cheap enough that doing it alone costs less in total than handing it over, and at medium size it comes out about even. The hand-over pays off when you want Claude or GPT to plan and review, a cheaper model to do the typing, and you'd rather not keep hitting the 5-hour limit.
89
+ **The honest part:** on the small CLI task, Codex on GPT-6.1 Sol is cheap enough that doing it alone costs a little less in total: the hand-over came out 15% more, almost all of it DeepSeek's share. The hand-over pays off when you want Claude or GPT to plan and review, a cheaper model to do the typing, and you'd rather not keep hitting the 5-hour limit.
90
90
 
91
91
  **Who this is for:** most of us pay for Claude or ChatGPT with a plan (Plus, Pro, Max 5x…), while DeepSeek is pay-as-you-go from a balance you top up, so every hand-over is money spent outside your plan. If your plan's limits already cover how you work, you don't need this: keep it simple and keep the money. If you're like me and keep running long, messy jobs with round after round of changes, and keep hitting the limit, a little DeepSeek spend gets you a lot more out of the plan you already pay for.
92
92
 
93
+ **The two test tasks**, asked the way a user would (the requests are in Chinese, linked as written):
94
+ - **Task 1 · CLI tool:** a command-line task manager in Node, no dependencies, split into modules, with tests. [Request](docs/benchmarks/token-usage-2026-10/natural/medium.md) · [acceptance check](docs/benchmarks/token-usage-2026-10/checks/natural-medium.mjs)
95
+ - **Task 2 · Web app:** a task manager with a Node server and a page, REST API, stats, tests and a README. [Request](docs/benchmarks/token-usage-2026-10/natural/large.md) · [acceptance check](docs/benchmarks/token-usage-2026-10/checks/natural-large.mjs)
96
+
93
97
  <p align="center">
94
- <img src="./docs/benchmarks/token-usage-2026-10/charts/natural-combined.svg" width="100%" alt="Cost per task, working alone vs handed over (0.6.0; the large Astra task with 0.6.1): Claude Code (Opus 5.5) and Codex (GPT-6.1 Sol, GPT-6 Astra), each on its own scale">
98
+ <img src="./docs/benchmarks/token-usage-2026-10/charts/natural-combined.svg" width="100%" alt="Cost per task, working alone vs handed over with 0.6.2, Task 1 · CLI tool and Task 2 · Web app: Claude Code (Opus 5.5) and Codex (GPT-6.1 Sol, GPT-6 Astra), each on its own scale">
95
99
  </p>
96
100
 
97
101
  Cost per task at list price, DeepSeek V4.1 Flash 1017 at its peak price:
98
102
 
99
103
  | Client · model | Task | Alone | Handed over, client only | Handed over, with DeepSeek |
100
104
  |---|---|---|---|---|
101
- | Claude Code · Opus 5.5 high | Small | $1.07 | $0.37 (−65%) | $0.55 (−49%) |
102
- | Claude Code · Opus 5.5 high | Medium | $2.13 | $0.41 (−81%) | $0.70 (−67%) |
103
- | Codex · GPT-6.1 Sol high | Small | $0.17 | $0.09 (−49%) | $0.29 (+73%) |
104
- | Codex · GPT-6.1 Sol high | Medium | $0.41 | $0.10 (−75%) | $0.41 (−2%) |
105
- | Codex · GPT-6 Astra high | Small | $0.79 | $0.56 (−29%) | $0.75 (−6%) |
106
- | Codex · GPT-6 Astra high | Medium | $2.27 | $0.48 (−79%) | $0.78 (−65%) |
105
+ | Claude Code · Opus 5.5 high | Task 1 · CLI tool | $1.07 | $0.42 (−61%) | $0.52 (−51%) |
106
+ | Claude Code · Opus 5.5 high | Task 2 · Web app | $2.13 | $0.47 (−78%) | $0.60 (−72%) |
107
+ | Codex · GPT-6.1 Sol high | Task 1 · CLI tool | $0.17 | $0.06 (−63%) | $0.20 (+15%) |
108
+ | Codex · GPT-6.1 Sol high | Task 2 · Web app | $0.41 | $0.06 (−84%) | $0.25 (−40%) |
109
+ | Codex · GPT-6 Astra high | Task 1 · CLI tool | $0.79 | $0.30 (−62%) | $0.42 (−47%) |
110
+ | Codex · GPT-6 Astra high | Task 2 · Web app | $2.27 | $0.30 (−87%) | $0.42 (−81%) |
111
+
112
+ <p align="center">
113
+ <img src="./docs/benchmarks/token-usage-2026-10/charts/natural-quota-combined.svg" width="80%" alt="Share of the ChatGPT Plus 5-hour window per task in Codex, working alone vs handed over with 0.6.2: GPT-6.1 Sol and GPT-6 Astra">
114
+ </p>
115
+
116
+ Share of the ChatGPT Plus 5-hour window per task: Task 1 Sol 1.9% → ≈0.6%, Task 2 Sol 4.3% → ≈0.6%, Task 1 Astra 7% → 3%, Task 2 Astra 22% → 3% (≈ estimated from cost).
117
+
118
+ **Why it got cheaper:** from 0.6.1 Codex is woken by its Stop hook instead of polling with `dsh_wait`, and 0.6.2 cuts the MCP instructions to one sentence, so Codex reads a few thousand to over ten thousand fewer new input tokens per hand-over.
119
+
120
+ **Task 4 · Kanban app (phased):** a kanban board in plain Node and a plain front end, given in three phases in the same conversation: ① the core (REST API, storage, drag and drop); ② labels, due dates, search syntax, archive, undo and redo, activity log, import and export; ③ version conflicts, WIP limits, bulk actions, live sync over SSE, stats. Each phase has acceptance tests written beforehand, the same for both arms, 55 in all. Requests: [phase 1](docs/benchmarks/token-usage-2026-10/xxl/seed/docs/phase1.md) · [phase 2](docs/benchmarks/token-usage-2026-10/xxl/seed/docs/phase2.md) · [phase 3](docs/benchmarks/token-usage-2026-10/xxl/seed/docs/phase3.md) · [acceptance tests](docs/benchmarks/token-usage-2026-10/xxl/seed/tests/acceptance/) · [checker](docs/benchmarks/token-usage-2026-10/xxl/check.mjs) · [how it was run](docs/benchmarks/token-usage-2026-10/xxl/README.md)
107
121
 
108
122
  <p align="center">
109
- <img src="./docs/benchmarks/token-usage-2026-10/charts/natural-quota-combined.svg" width="80%" alt="Share of the ChatGPT Plus 5-hour window per task in Codex, working alone vs handed over with 0.6.0: GPT-6.1 Sol and GPT-6 Astra">
123
+ <img src="./docs/benchmarks/token-usage-2026-10/charts/task4-kanban.svg" width="100%" alt="Task 4 · Kanban app (phased), Codex GPT-6 Astra high: $8.69 alone vs $2.36 handed over with 0.6.2 ($1.62 client, −81%); 86% of the 5-hour window vs about 16%">
110
124
  </p>
111
125
 
112
- Share of the ChatGPT Plus 5-hour window per task: Sol small 1.9% → 0.8%, Sol medium 4.3% → 1.0%, Astra small 7% → 3.5%, Astra medium 22% → 4%.
126
+ | Codex · GPT-6 Astra high | Alone | Handed over (0.6.2) |
127
+ |---|---|---|
128
+ | Client | $8.69 | $1.62 (−81%) |
129
+ | DeepSeek | – | $0.74 |
130
+ | Total | $8.69 | $2.36 (−73%) |
131
+ | 5-hour window | 86% | about 16% (−81%) |
132
+ | Acceptance tests | 55/55, first try | 55/55, first try |
133
+ | Time | about 31 min | about 87 min |
134
+ | Lines of code | 1,876 | 9,953 |
135
+
136
+ Task 3, the same kanban app with all three phases in one request, is being measured with Opus and Sol; its results come in the next update.
113
137
 
114
138
  <details>
115
139
  <summary>What was tested, and how</summary>
116
140
 
117
- - **Two tasks, asked the way a user would.** The small one takes Claude Code or Codex about 7–8 minutes on its own; the medium one is a small app with a server and a page. Long jobs beyond that were not measured. The requests are in Chinese, linked as written:
118
- - Small: a command-line task manager in Node, no dependencies, split into modules, with tests. [Request](docs/benchmarks/token-usage-2026-10/natural/medium.md) · [acceptance check](docs/benchmarks/token-usage-2026-10/checks/natural-medium.mjs)
119
- - Medium: a task manager web app, Node server plus page, REST API, stats, tests and a README. [Request](docs/benchmarks/token-usage-2026-10/natural/large.md) · [acceptance check](docs/benchmarks/token-usage-2026-10/checks/natural-large.mjs)
120
- - **The only difference** in the hand-over runs is one line added at the end: 交给 DeepSeek Harness 去做。 ("Hand it to DeepSeek Harness.")
121
- - Same model and effort in both arms, three runs per cell (Astra: one run each, two for the small hand-over), and every run judged by the same acceptance check, which the model never sees. All 12 hand-over runs passed it the first time.
141
+ - **The only difference** in the hand-over runs is one line added at the end of the request: 交给 DeepSeek Harness 去做。 ("Hand it to DeepSeek Harness.")
142
+ - Same model and effort in both arms, and every run judged by the same acceptance check, which the model never sees.
143
+ - Working alone: the earlier mean of three runs per cell (Astra: one run each). Handed over with 0.6.2: one run per cell; all six passed the first time, with no rework and no `dsh_wait` call.
122
144
  - Prices are list prices. DeepSeek is counted at its peak price, the most conservative case; the real DeepSeek bill came to about 58% of that.
123
- - Every run, the raw data and the scripts: [run log](docs/benchmarks/token-usage-2026-10/LOG.md) and [full report](docs/benchmarks/token-usage-2026-10.md).
145
+ - Every run: [run log](docs/benchmarks/token-usage-2026-10/LOG.md) (section "0.6.2 hand-over rerun"); raw data in `docs/benchmarks/token-usage-2026-10/results/*-n062*.jsonl`; method and earlier builds in the [full report](docs/benchmarks/token-usage-2026-10.md).
124
146
 
125
147
  </details>
126
148
 
@@ -137,6 +159,8 @@ Share of the ChatGPT Plus 5-hour window per task: Sol small 1.9% → 0.8%, Sol m
137
159
  <details>
138
160
  <summary>Full tool reference</summary>
139
161
 
162
+ The server's MCP instructions are a single sentence (from 0.6.2); how to hand over, wait and answer is in each tool's own description. Codex repeats the instructions in front of every tool's description, so the catalogue it shows the model is now about 9.7k characters instead of 24.3k.
163
+
140
164
  | Tool | What it does |
141
165
  |---|---|
142
166
  | `dsh_list_workspaces` | Workspaces in the Harness sidebar |
@@ -203,13 +227,13 @@ When the plugin is unreachable, the MCP server falls back to Harness's `dsh head
203
227
 
204
228
  **What the plugin does when switched on.** It copies the MCP server to `~/.dsh-baton/app`, a folder that stays put, and writes the MCP entry into `~/.claude.json` and `~/.codex/config.toml` directly, keeping a `*.bak-dsh-baton` backup of each. It starts the server with your own Node.js ≥ 22.15 when it finds one on `PATH`; without one, with Harness's own runtime (the Harness app in Node mode, `ELECTRON_RUN_AS_NODE=1`), so nothing else has to be installed. Either way it works with only the desktop apps installed, when `claude` / `codex` are not on your `PATH`. It runs again on each Harness start: an entry that is already right, or a working one that starts another copy of dsh-baton, is left alone. In Harness, the source switches to the npm registry ("Official npm registry", 「npm 官方源」) by itself once you type the package name, and no restart is needed. To turn the registration off, set `DSH_BATON_AUTO_REGISTER=0` for Harness.
205
229
 
206
- **The Codex Stop hook.** The plugin adds one `[[hooks.Stop]]` block, marked `# dsh-baton`, to `~/.codex/config.toml`; Codex shows it as **dsh-baton: waiting for the Harness run** and runs it only once you trust it (dsh-baton never trusts it for you). The block only names a small script, `~/.dsh-baton/bin/codex-stop-hook` (`codex-stop-hook.cmd` on Windows), and everything that can move, Node.js and the server, lives in that script. So the block itself never changes: updates, same-version refreshes and Harness restarts leave it as it is, and your trust stays. If the block is ever rewritten, dsh-baton drops the old trust record, and Codex asks you to trust it again. Windows uses a `.cmd` because Codex runs hooks through `PowerShell -Command`, which cannot start a quoted program path. Not added when the server runs on Harness's own runtime. `dsh-baton setup --codex-hook` adds it by hand, `setup --remove` takes it out, `DSH_BATON_CODEX_HOOK=0` for Harness stops the plugin adding it.
230
+ **The Codex Stop hook.** The plugin adds one `[[hooks.Stop]]` block, marked `# dsh-baton`, to `~/.codex/config.toml`; Codex shows it as **dsh-baton: waiting for the Harness run** and runs it only once you trust it (dsh-baton never trusts it for you). The block only names a small script, `~/.dsh-baton/bin/codex-stop-hook` (`codex-stop-hook.cmd` on Windows), and everything that can move, Node.js and the server, lives in that script. So the block itself never changes: updates, same-version refreshes and Harness restarts leave it as it is, and your trust stays. If the block is ever rewritten, dsh-baton drops the old trust record, and Codex asks you to trust it again. Windows uses a `.cmd` because Codex runs hooks through `PowerShell -Command`, which cannot start a quoted program path. Without a Node.js of your own, the script runs Harness's own app in Node mode (`ELECTRON_RUN_AS_NODE=1`), as the server does; the plugin rewrites it on each start, so it follows Harness updates. Not added without Codex, with `DSH_BATON_CODEX_HOOK=0`, or when Codex's dsh-baton entry names no server folder (a bare `npx` command); the first hand-over in a Codex conversation then says why. `dsh-baton setup --codex-hook` adds it by hand, `setup --remove` takes it out, `DSH_BATON_CODEX_HOOK=0` for Harness stops the plugin adding it.
207
231
 
208
232
  **Updates need no restart.** When a newer version lands in `~/.dsh-baton/app` (a plugin update, `setup`, or a copy by hand), or the same version with changed files, conversations already open in Claude Code and Codex use it from their next `dsh_*` call: the server checks the folder before each call, starts the new version, and says once in that call's result that it was updated. A `dsh_wait` that is already running finishes on the old version first. If the new version cannot start, the old one keeps working and the result says why. A tool added in the new version shows up in Claude Code at once (the server tells the client its tool list changed); Codex keeps the tool list a conversation started with, so it does not show a new tool until the next conversation; from 0.5.9 on it can still use one at once through `dsh_call`, which the update note points to, and the tools it already has run the new code right away. This works from the version after 0.5.8 on; a server started from 0.5.8 or earlier needs one last restart (or `/mcp` reconnect) to pick it up.
209
233
 
210
234
  **`npx -y dsh-baton-mcp setup`** does the same registration from a terminal: for a plugin that could not register, or when you come from an MCP directory. With the plugin already registered it changes nothing, then points you to the plugin if it is missing.
211
235
 
212
- **Updating.** Harness has an update button only for its official plugins, so: Plugins → dsh-baton-host → **Uninstall**, then **Add plugin** → `dsh-baton-host`. For a while after a release, Harness installs the previous version instead (pnpm's `minimumReleaseAge` holds back versions published very recently, and the install dialog says so); to get the new one at once, type the version: `dsh-baton-host@0.6.1`. A pre-release must always be added by its exact version, such as `dsh-baton-host@0.6.0-next.0`: Harness resolves `@next` to a broken placeholder. Plain `dsh-baton-host` gets the latest release. Switched on again, the plugin brings the MCP server to the same version; new conversations use it. If the two are ever out of step, the first tool call that notices it says so, naming the older side and the newer side's exact version to install.
236
+ **Updating.** Harness has an update button only for its official plugins, so: Plugins → dsh-baton-host → **Uninstall**, then **Add plugin** → `dsh-baton-host`. For a while after a release, Harness installs the previous version instead (pnpm's `minimumReleaseAge` holds back versions published very recently, and the install dialog says so); to get the new one at once, type the version: `dsh-baton-host@0.6.2`. A pre-release must always be added by its exact version, such as `dsh-baton-host@0.6.0-next.0`: Harness resolves `@next` to a broken placeholder. Plain `dsh-baton-host` gets the latest release. Switched on again, the plugin brings the MCP server to the same version; new conversations use it. If the two are ever out of step, the first tool call that notices it says so, naming the older side and the newer side's exact version to install.
213
237
 
214
238
  **Removing.** Harness → Plugins → dsh-baton-host → **Uninstall**, then `npx -y dsh-baton-mcp setup --remove`. The plugin cannot tell an uninstall from Harness quitting, so it leaves the MCP entries to that command, which also deletes `~/.dsh-baton/app`; run records stay in `~/.dsh-baton` until you delete it.
215
239
 
package/README.zh-CN.md CHANGED
@@ -50,7 +50,7 @@ Codex 和 Claude 轮流往同一个 Harness 对话发消息,每条都标明是
50
50
 
51
51
  插件启用后会为 Claude Code 和 Codex 注册 dsh-baton(对所有文件夹生效),Harness 里会弹出提示。
52
52
 
53
- **2. 仅 Codex,只需一次**:Codex **设置 → 钩子(Hooks)** → **信任** **dsh-baton: waiting for the Harness run**。之后 Codex 在任务跑完时会被自动唤醒,不用再每 270 秒查一次。只需信任一次:以后升级、热更新、重启 Harness 都不用再信任。不信任也能用,Codex 会照旧用 `dsh_wait` 等,第一次交接时会提醒你。
53
+ **2. 仅 Codex,只需一次**:Codex **设置 → 钩子(Hooks)** → **信任** **dsh-baton: waiting for the Harness run**。钩子要插件 0.6.0 以上才有(没有 Node.js 时要 0.6.2 以上);装好或更新插件后,先重启一次 Harness,再重启一次 Codex,它才会出现在这个列表里。不需要 Node.js:没有的话它和服务器一样用 Harness 自带运行环境。之后 Codex 在任务跑完时会被自动唤醒,不用再每 270 秒查一次。只需信任一次:以后升级、热更新、重启 Harness 都不用再信任。不信任也能用,Codex 会照旧用 `dsh_wait` 等,第一次交接时会提醒你。
54
54
 
55
55
  **3. 检查**:在 Claude Code 或 Codex 里**新开**一个对话,让它调用 `dsh_list_workspaces`,会列出你的 Harness 工作区。
56
56
 
@@ -84,43 +84,65 @@ Codex 和 Claude 轮流往同一个 Harness 对话发消息,每条都标明是
84
84
 
85
85
  ## Token 用量实测
86
86
 
87
- **一句话:大活、长活交给它**。一次性做一个跨很多文件的功能、一次大重构、一整个小应用,这种时候最省:Claude Code 自己的花费少 65–81%,Codex 在 ChatGPT Plus 一个 5 小时额度里能多做 2 到 5 倍的任务(GPT-6 Astra 做中号任务时是 5 倍)。
87
+ **一句话:大活、长活交给它**。一次性做一个跨很多文件的功能、一次大重构、一整个小应用,这种时候最省:Claude Code 连 DeepSeek 一起算,总花费少 51–72%。Codex 现在两个案例都省(案例 2 任务管理网页,Sol 少 40%,Astra 少 81%);GPT-6 Astra 做案例 2,ChatGPT Plus 5 小时额度从占 22% 降到 3%。更长的活,比如分三个阶段的看板应用(案例 4),Astra 总花费少 73%,额度从 86% 降到约 16%。
88
88
 
89
- **说实话**:小任务,Codex 用 GPT-6.1 Sol 自己做,总价反而比转交更便宜;中号任务两者基本持平。转交值得的场景是:你想让 Claude 或 GPT 负责规划和把关,让更便宜的模型去动手写,又不想老是撞上 5 小时额度上限。
89
+ **说实话**:案例 1 这种小的命令行工具,Codex 用 GPT-6.1 Sol 自己做,总价还略便宜一点:转交反而多花 15%,多出来的几乎全是 DeepSeek 那部分。转交值得的场景是:你想让 Claude 或 GPT 负责规划和把关,让更便宜的模型去动手写,又不想老是撞上 5 小时额度上限。
90
90
 
91
91
  **适合谁**:大多数人用 Claude 或 ChatGPT 走的是订阅套餐(Plus、Pro、Max 5x 之类),而 DeepSeek 是充值后按用量扣费,所以每次转交都是套餐之外多花的钱。如果你的套餐额度本来就够用,就不用折腾这套流程,省一点是一点。如果你像我一样,经常跑又长又杂、要反复修改的大任务,老是撞上额度上限,那花一点 DeepSeek 的钱,就能让你已经付费的套餐多干很多活。
92
92
 
93
+ **两个测试案例**,都按普通用户的口吻提需求(原文可以直接点开看):
94
+ - **案例 1 · 命令行工具**:用 Node 写一个命令行任务管理工具,零依赖,分模块,带测试。[需求原文](docs/benchmarks/token-usage-2026-10/natural/medium.md) · [验收脚本](docs/benchmarks/token-usage-2026-10/checks/natural-medium.mjs)
95
+ - **案例 2 · 任务管理网页**:Node 服务器加前端页面,REST 接口、统计、测试和 README。[需求原文](docs/benchmarks/token-usage-2026-10/natural/large.md) · [验收脚本](docs/benchmarks/token-usage-2026-10/checks/natural-large.mjs)
96
+
93
97
  <p align="center">
94
- <img src="./docs/benchmarks/token-usage-2026-10/charts/natural-combined.zh.svg" width="100%" alt="每个任务的花费,自己做与转交(0.6.0;Astra 大号任务为 0.6.1):Claude Code(Opus 5.5)和 Codex(GPT-6.1 Sol、GPT-6 Astra),各用自己的刻度">
98
+ <img src="./docs/benchmarks/token-usage-2026-10/charts/natural-combined.zh.svg" width="100%" alt="每个任务的花费,自己做与用 0.6.2 转交,案例 1 · 命令行工具和案例 2 · 任务管理网页:Claude Code(Opus 5.5)和 Codex(GPT-6.1 Sol、GPT-6 Astra),各用自己的刻度">
95
99
  </p>
96
100
 
97
101
  每个任务的花费,按标价计,DeepSeek V4.1 Flash 1017 按高峰价:
98
102
 
99
- | 客户端 · 模型 | 任务 | 自己做 | 转交,只算客户端 | 转交,含 DeepSeek |
103
+ | 客户端 · 模型 | 案例 | 自己做 | 转交,只算客户端 | 转交,含 DeepSeek |
100
104
  |---|---|---|---|---|
101
- | Claude Code · Opus 5.5 high | 小号 | $1.07 | $0.37(−65%) | $0.55(−49%) |
102
- | Claude Code · Opus 5.5 high | 中号 | $2.13 | $0.41(−81%) | $0.70(−67%) |
103
- | Codex · GPT-6.1 Sol high | 小号 | $0.17 | $0.09(−49%) | $0.29(+73%) |
104
- | Codex · GPT-6.1 Sol high | 中号 | $0.41 | $0.10(−75%) | $0.41(−2%) |
105
- | Codex · GPT-6 Astra high | 小号 | $0.79 | $0.56(−29%) | $0.75(−6%) |
106
- | Codex · GPT-6 Astra high | 中号 | $2.27 | $0.48(−79%) | $0.78(−65%) |
105
+ | Claude Code · Opus 5.5 high | 案例 1 · 命令行工具 | $1.07 | $0.42(−61%) | $0.52(−51%) |
106
+ | Claude Code · Opus 5.5 high | 案例 2 · 任务管理网页 | $2.13 | $0.47(−78%) | $0.60(−72%) |
107
+ | Codex · GPT-6.1 Sol high | 案例 1 · 命令行工具 | $0.17 | $0.06(−63%) | $0.20(+15%) |
108
+ | Codex · GPT-6.1 Sol high | 案例 2 · 任务管理网页 | $0.41 | $0.06(−84%) | $0.25(−40%) |
109
+ | Codex · GPT-6 Astra high | 案例 1 · 命令行工具 | $0.79 | $0.30(−62%) | $0.42(−47%) |
110
+ | Codex · GPT-6 Astra high | 案例 2 · 任务管理网页 | $2.27 | $0.30(−87%) | $0.42(−81%) |
111
+
112
+ <p align="center">
113
+ <img src="./docs/benchmarks/token-usage-2026-10/charts/natural-quota-combined.zh.svg" width="80%" alt="Codex 每个任务占 ChatGPT Plus 5 小时额度,自己做与用 0.6.2 转交:GPT-6.1 Sol 和 GPT-6 Astra">
114
+ </p>
115
+
116
+ 每个任务占 ChatGPT Plus 5 小时额度:案例 1 Sol 1.9% → ≈0.6%,案例 2 Sol 4.3% → ≈0.6%,案例 1 Astra 7% → 3%,案例 2 Astra 22% → 3%(≈ 为按花费估算)。
117
+
118
+ **为什么更省了**:0.6.1 起 Codex 由 Stop 钩子唤醒,不再用 `dsh_wait` 轮询;0.6.2 把 MCP 说明缩成一句话,Codex 每次转交少读几千到一万多个新输入 token。
119
+
120
+ **案例 4 · 看板应用(分阶段)**:只用 Node 内置模块和原生前端的看板应用,分三个阶段在同一个对话里依次给:① 主体(REST 接口、持久化、前端拖拽);② 标签、截止日期、搜索语法、归档、撤销重做、活动日志、导入导出;③ 版本冲突检测、WIP 上限、批量操作、SSE 实时同步、统计。每个阶段都有事先写好的验收测试,两组用同一套,共 55 项。需求原文:[阶段 1](docs/benchmarks/token-usage-2026-10/xxl/seed/docs/phase1.md) · [阶段 2](docs/benchmarks/token-usage-2026-10/xxl/seed/docs/phase2.md) · [阶段 3](docs/benchmarks/token-usage-2026-10/xxl/seed/docs/phase3.md) · [验收测试](docs/benchmarks/token-usage-2026-10/xxl/seed/tests/acceptance/) · [验收脚本](docs/benchmarks/token-usage-2026-10/xxl/check.mjs) · [测试流程](docs/benchmarks/token-usage-2026-10/xxl/README.md)
107
121
 
108
122
  <p align="center">
109
- <img src="./docs/benchmarks/token-usage-2026-10/charts/natural-quota-combined.zh.svg" width="80%" alt="Codex 每个任务占 ChatGPT Plus 5 小时额度,自己做与用 0.6.0 转交:GPT-6.1 Sol 和 GPT-6 Astra">
123
+ <img src="./docs/benchmarks/token-usage-2026-10/charts/task4-kanban.zh.svg" width="100%" alt="案例 4 · 看板应用(分阶段),Codex GPT-6 Astra high:自己做 $8.69,用 0.6.2 转交 $2.36(主 Agent $1.62,−81%);5 小时额度 86% 对约 16%">
110
124
  </p>
111
125
 
112
- 每个任务占 ChatGPT Plus 5 小时额度:Sol 小号 1.9% → 0.8%,Sol 中号 4.3% → 1.0%,Astra 小号 7% → 3.5%,Astra 中号 22% → 4%。
126
+ | Codex · GPT-6 Astra high | 自己做 | 转交(0.6.2) |
127
+ |---|---|---|
128
+ | 主 Agent | $8.69 | $1.62(−81%) |
129
+ | DeepSeek | – | $0.74 |
130
+ | 合计 | $8.69 | $2.36(−73%) |
131
+ | 5 小时额度 | 86% | 约 16%(−81%) |
132
+ | 验收测试 | 55/55,一次通过 | 55/55,一次通过 |
133
+ | 用时 | 约 31 分钟 | 约 87 分钟 |
134
+ | 代码量 | 1,876 行 | 9,953 行 |
135
+
136
+ 案例 3 是同一个看板应用、三个阶段一次给完的版本,Opus 和 Sol 正在测,结果下一次更新补上。
113
137
 
114
138
  <details>
115
139
  <summary>测了什么、怎么测的</summary>
116
140
 
117
- - **两个任务,用普通用户的口吻提**。小号任务 Claude Code 或 Codex 自己做大约 7–8 分钟;中号任务是一个带服务器和页面的小应用。比这更长的大活没有测。原文如下,可以直接点开看:
118
- - 小号:用 Node 写一个命令行任务管理工具,零依赖,分模块,带测试。[需求原文](docs/benchmarks/token-usage-2026-10/natural/medium.md) · [验收脚本](docs/benchmarks/token-usage-2026-10/checks/natural-medium.mjs)
119
- - 中号:一个任务管理网页应用,Node 服务器加前端页面,REST 接口、统计、测试和 README。[需求原文](docs/benchmarks/token-usage-2026-10/natural/large.md) · [验收脚本](docs/benchmarks/token-usage-2026-10/checks/natural-large.mjs)
120
141
  - **转交组唯一的区别**,是在需求末尾多一句:交给 DeepSeek Harness 去做。
121
- - 两组用同一个模型、同样的推理强度,每格跑 3 次(Astra 各 1 次,小号转交跑了 2 次);每次都用同一套验收脚本检查,模型看不到这个脚本。转交组 12 次全部一次通过。
142
+ - 两组用同一个模型、同样的推理强度;每次都用同一套验收脚本检查,模型看不到这个脚本。
143
+ - 自己做:沿用之前每格 3 次的平均值(Astra 各 1 次)。用 0.6.2 转交:每格跑 1 次,6 次全部一次通过,没有返工,也没有调用 `dsh_wait`。
122
144
  - 价格按标价算。DeepSeek 按高峰价,是最保守的算法;实际 DeepSeek 账单大约是这个数的 58%。
123
- - 每一次运行、原始数据和脚本:[运行日志](docs/benchmarks/token-usage-2026-10/LOG.md)、[完整报告](docs/benchmarks/token-usage-2026-10.md)。
145
+ - 每一次运行:[运行日志](docs/benchmarks/token-usage-2026-10/LOG.md)(「0.6.2 hand-over rerun」一节);原始数据在 `docs/benchmarks/token-usage-2026-10/results/*-n062*.jsonl`;方法和更早的版本见[完整报告](docs/benchmarks/token-usage-2026-10.md)。
124
146
 
125
147
  </details>
126
148
 
@@ -137,6 +159,8 @@ Codex 和 Claude 轮流往同一个 Harness 对话发消息,每条都标明是
137
159
  <details>
138
160
  <summary>完整工具说明</summary>
139
161
 
162
+ 服务器的 MCP instructions 只有一句话(0.6.2 起);怎么转交、怎么等、怎么回答问题,都写在各个工具自己的说明里。Codex 会在每个工具的说明前面重复一遍 instructions,所以它给模型看的工具目录从约 2.43 万字符降到约 0.97 万字符。
163
+
140
164
  | 工具 | 作用 |
141
165
  |---|---|
142
166
  | `dsh_list_workspaces` | 列出 Harness 侧边栏里的工作区 |
@@ -205,13 +229,13 @@ dsh-baton rename-workspace ~/code/new-app 新名字
205
229
 
206
230
  **插件启用时做了什么**。它把 MCP 服务器复制到 `~/.dsh-baton/app` 这个固定的文件夹,再直接把 MCP 注册写进 `~/.claude.json` 和 `~/.codex/config.toml`,各留一份 `*.bak-dsh-baton` 备份。电脑上有 Node.js ≥ 22.15(在 `PATH` 里能找到)就用它启动服务器;没有的话,就用 Harness 自带的运行环境(Harness 应用本身以 Node 模式运行,`ELECTRON_RUN_AS_NODE=1`),不用另外安装任何东西。两种情况下,只装了桌面应用、`claude` / `codex` 不在 `PATH` 里也能用。每次 Harness 启动都会再检查一遍:已经正确的注册、或者指向另一份可用 dsh-baton 的注册,都不会改动。在 Harness 里输入包名后,安装源会自动切到「npm 官方源」(英文界面是 "Official npm registry"),不用另外选择,也不用重启。不想让插件自动注册,给 Harness 设置 `DSH_BATON_AUTO_REGISTER=0`。
207
231
 
208
- **Codex 的 Stop 钩子**。插件会在 `~/.codex/config.toml` 里加一段带 `# dsh-baton` 标记的 `[[hooks.Stop]]`,Codex 里显示为 **dsh-baton: waiting for the Harness run**,你信任之后才会运行(dsh-baton 不会替你信任)。这一段只写了一个小脚本的路径 `~/.dsh-baton/bin/codex-stop-hook`(Windows 上是 `codex-stop-hook.cmd`),Node.js 和服务器这些会变的路径都放在脚本里。所以这一段本身永远不变:升级、同版本刷新、重启 Harness 都不会动它,信任一直有效。万一这一段被改写,dsh-baton 会清掉旧的信任记录,Codex 会请你重新信任。Windows 上用 `.cmd`,是因为 Codex 通过 `PowerShell -Command` 运行钩子,启动不了带引号的程序路径。服务器用 Harness 自带运行环境时不加这个钩子。手动添加用 `dsh-baton setup --codex-hook`,`setup --remove` 会删掉它,给 Harness 设置 `DSH_BATON_CODEX_HOOK=0` 则插件不再添加。
232
+ **Codex 的 Stop 钩子**。插件会在 `~/.codex/config.toml` 里加一段带 `# dsh-baton` 标记的 `[[hooks.Stop]]`,Codex 里显示为 **dsh-baton: waiting for the Harness run**,你信任之后才会运行(dsh-baton 不会替你信任)。这一段只写了一个小脚本的路径 `~/.dsh-baton/bin/codex-stop-hook`(Windows 上是 `codex-stop-hook.cmd`),Node.js 和服务器这些会变的路径都放在脚本里。所以这一段本身永远不变:升级、同版本刷新、重启 Harness 都不会动它,信任一直有效。万一这一段被改写,dsh-baton 会清掉旧的信任记录,Codex 会请你重新信任。Windows 上用 `.cmd`,是因为 Codex 通过 `PowerShell -Command` 运行钩子,启动不了带引号的程序路径。没有自己的 Node.js 时,脚本和服务器一样以 Node 模式(`ELECTRON_RUN_AS_NODE=1`)运行 Harness 程序本身;插件每次启动都会重写它,所以 Harness 更新后路径也会跟上。只有没装 Codex、设了 `DSH_BATON_CODEX_HOOK=0`、或 Codex 里的 dsh-baton 条目没指向服务器目录(比如裸的 `npx` 命令)时才不加;这时 Codex 对话里第一次交接会说明原因。手动添加用 `dsh-baton setup --codex-hook`,`setup --remove` 会删掉它,给 Harness 设置 `DSH_BATON_CODEX_HOOK=0` 则插件不再添加。
209
233
 
210
234
  **更新不用重启**。新版本装进 `~/.dsh-baton/app` 后(插件更新、`setup` 或手动复制),或者同一版本但文件有改动时,Claude Code 和 Codex 里已经开着的对话,下一次调用 `dsh_*` 工具时就会用上:服务器每次调用前检查这个文件夹,启动新版本,并在这次调用的结果里提示一次已经更新。正在进行的 `dsh_wait` 会先在旧版本里跑完。新版本启动失败时,旧版本继续工作,结果里会说明原因。新版本新增的工具,Claude Code 里马上就能用(服务器会通知客户端工具列表变了);Codex 一直用对话开始时的工具列表,新工具要到下一个新对话才会显示;从 0.5.9 起,旧对话里也能通过 `dsh_call` 马上用上新功能(更新提示里会写明),已有的工具也会立刻用上新代码。这个功能从 0.5.8 之后的版本开始有效;由 0.5.8 或更早版本启动的服务器,还需要最后重启一次(或在 `/mcp` 里重新连接)。
211
235
 
212
236
  **`npx -y dsh-baton-mcp setup`** 在终端里做同样的注册:用于插件没能注册的情况,或者从 MCP 目录过来的用户。插件已经注册过时它什么都不改;插件没装的话,它会提示去添加。
213
237
 
214
- **更新**。Harness 只给官方插件提供"更新"按钮,所以要:插件 → dsh-baton-host → **卸载**,再**添加插件** → `dsh-baton-host`。新版本刚发布的一段时间里,Harness 会装上一个版本(pnpm 的 `minimumReleaseAge` 会暂缓刚发布的版本,安装弹窗里会说明);想马上用新版,输入带版本号的名字:`dsh-baton-host@0.6.1`。预发布版必须写确切版本号添加,比如 `dsh-baton-host@0.6.0-next.0`:Harness 会把 `@next` 解析成一个坏掉的占位包。只输入 `dsh-baton-host` 装的是最新正式版。重新启用后,插件会把 MCP 服务器也更新到同一版本,新开的对话就会用上。万一两边版本对不上,第一次发现不一致的工具调用会提示,并写明哪一边旧、另一边的确切版本号。
238
+ **更新**。Harness 只给官方插件提供"更新"按钮,所以要:插件 → dsh-baton-host → **卸载**,再**添加插件** → `dsh-baton-host`。新版本刚发布的一段时间里,Harness 会装上一个版本(pnpm 的 `minimumReleaseAge` 会暂缓刚发布的版本,安装弹窗里会说明);想马上用新版,输入带版本号的名字:`dsh-baton-host@0.6.2`。预发布版必须写确切版本号添加,比如 `dsh-baton-host@0.6.0-next.0`:Harness 会把 `@next` 解析成一个坏掉的占位包。只输入 `dsh-baton-host` 装的是最新正式版。重新启用后,插件会把 MCP 服务器也更新到同一版本,新开的对话就会用上。万一两边版本对不上,第一次发现不一致的工具调用会提示,并写明哪一边旧、另一边的确切版本号。
215
239
 
216
240
  **卸载**。Harness → 插件 → dsh-baton-host → **卸载**,再运行 `npx -y dsh-baton-mcp setup --remove`。插件分不清"被卸载"和"Harness 退出",所以 MCP 注册交给这条命令删,它也会删掉 `~/.dsh-baton/app`;运行记录留在 `~/.dsh-baton`,不需要的话自己删掉。
217
241
 
package/host/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-baton-host",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "DeepSeek Harness plugin for dsh-baton: adding it is the whole install. Switched on, it registers the dsh-baton MCP server for Claude Code and Codex, then takes the baton over a local, owner-only control socket, runs it in a specific session through the host's own session controller, and hands the result back when the turn is done.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -20,7 +20,7 @@
20
20
  "LICENSE"
21
21
  ],
22
22
  "dependencies": {
23
- "dsh-baton-mcp": "0.6.1"
23
+ "dsh-baton-mcp": "0.6.2"
24
24
  },
25
25
  "license": "MIT",
26
26
  "dsh": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-baton-mcp",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "mcpName": "io.github.JialaoLiu/dsh-baton",
5
5
  "description": "Pass the baton from Claude Code, Codex or any MCP client to a local DeepSeek Harness: an MCP server that hands off tasks, continues a specific session, shows the run live in Harness and hands the result back when it is done.",
6
6
  "type": "module",
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/JialaoLiu/dsh-baton",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.6.1",
10
+ "version": "0.6.2",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "dsh-baton-mcp",
15
- "version": "0.6.1",
15
+ "version": "0.6.2",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }
@@ -7,7 +7,7 @@
7
7
  import fs from 'node:fs'
8
8
  import path from 'node:path'
9
9
  import * as core from './core.mjs'
10
- import { codexConfigPath, stableNodePath, codexHookEntry, codexHookStatus, HOOK_TIMEOUT_S } from './register.mjs'
10
+ import { codexConfigPath, stableNodePath, codexHookEntry, codexHookStatus, hookResultFile, HOOK_TIMEOUT_S } from './register.mjs'
11
11
 
12
12
  export { editCodexHook, HOOK_TIMEOUT_S } from './register.mjs'
13
13
 
@@ -68,6 +68,20 @@ export function hookUntrusted() {
68
68
  }
69
69
  }
70
70
 
71
+ // Why the plugin did not add the hook (see addCodexHook), in one sentence, or null: when
72
+ // it was added, when Codex is not installed, or when no plugin from 0.6.2 on has run yet.
73
+ const SKIP_REASONS = {
74
+ disabled: 'it is turned off with DSH_BATON_CODEX_HOOK=0 in the environment Harness runs with',
75
+ 'no-cli': "Codex's dsh-baton entry does not name a server folder with cli.mjs in it (a bare command such as npx, or a folder that is gone)",
76
+ }
77
+ export function hookSkipReason(env = process.env) {
78
+ let r = null
79
+ try { r = JSON.parse(fs.readFileSync(hookResultFile(env), 'utf8')) } catch { return null }
80
+ if (!['off', 'skipped', 'failed'].includes(r?.state) || !r.reason) return null
81
+ const why = SKIP_REASONS[r.reason] ?? `adding it failed (${String(r.reason).slice(0, 200)})`
82
+ return `dsh-baton's Codex Stop hook is not set up because ${why}, so Codex waits with dsh_wait.`
83
+ }
84
+
71
85
  // Each Codex conversation is told about trusting the hook once.
72
86
  const notedFile = () => path.join(dir(), 'trust-noted.json')
73
87
  export function noteOnce(thread) {
@@ -94,6 +108,16 @@ function reasonFor(results, running) {
94
108
  return `dsh-baton: ${what}.${still} ${one ? 'This is its result' : 'These are their results'}, as dsh_wait would return ${one ? 'it' : 'them'}; act on ${one ? 'it' : 'them'} now (follow the hints):\n${body}`
95
109
  }
96
110
 
111
+ // What Codex shows the user for the wake (systemMessage), instead of the JSON in reason:
112
+ // a short line per run with its id and state. The model reads reason only.
113
+ export function summaryFor(results, running = []) {
114
+ const one = (r) => r.needs_input ? `${r.run_id} 在等回答` : `${r.run_id}(${r.state})`
115
+ const head = results.length === 1
116
+ ? (results[0].needs_input ? `dsh-baton:Harness 任务 ${one(results[0])},问题已交给 Codex` : `dsh-baton:Harness 任务 ${one(results[0])}已结束,结果已交回 Codex`)
117
+ : `dsh-baton:${results.length} 个 Harness 任务已交回 Codex:${results.map(one).join(',')}`
118
+ return running.length ? `${head};还在运行 ${running.length} 个` : head
119
+ }
120
+
97
121
  // The JSON Codex writes to the hook's stdin, or null. A leading BOM is dropped (seen on
98
122
  // Windows). Input that does not parse is kept (its first 4 KB) in last-bad-stdin, so a
99
123
  // hook that ran and gave up can be told from one Codex never ran.
@@ -153,7 +177,8 @@ export async function runHook(input, { shape, wait = core.api.wait, now = Date.n
153
177
  }
154
178
  if (ended.size && (urgent || !running.length)) {
155
179
  for (const [runId, st] of ended) delivered(runId, st)
156
- return { decision: 'block', reason: reasonFor([...ended].map(([run_id, st]) => ({ run_id, ...shape(st) })), running) }
180
+ const results = [...ended].map(([run_id, st]) => ({ run_id, ...shape(st) }))
181
+ return { decision: 'block', reason: reasonFor(results, running), systemMessage: summaryFor(results, running) }
157
182
  }
158
183
  if (!running.length) return null
159
184
  if (now() > deadline) return null
@@ -161,6 +186,6 @@ export async function runHook(input, { shape, wait = core.api.wait, now = Date.n
161
186
  }
162
187
 
163
188
  // The hook entry for Codex's config.toml (see register.mjs, which also writes it).
164
- export function hookCommand(root = core.ROOT, node = stableNodePath(), { platform } = {}) {
165
- return codexHookEntry(node, path.join(root, 'src', 'server.mjs'), { platform, home: core.config.batonHome })
189
+ export function hookCommand(root = core.ROOT, node = stableNodePath(), { platform, harness = Boolean(process.versions.electron) } = {}) {
190
+ return codexHookEntry(node, path.join(root, 'src', 'server.mjs'), { platform, home: core.config.batonHome, harness })
166
191
  }
package/src/mcp.mjs CHANGED
@@ -5,7 +5,7 @@ import path from 'node:path'
5
5
  import * as core from './core.mjs'
6
6
  import { stableNodePath } from './setup.mjs'
7
7
  import { conversationFolder, within } from './folder.mjs'
8
- import { threadOf, remember, delivered, hookActive, hookUntrusted, noteOnce } from './codex-hook.mjs'
8
+ import { threadOf, remember, delivered, hookActive, hookUntrusted, hookSkipReason, noteOnce } from './codex-hook.mjs'
9
9
 
10
10
  const SERVER = { name: 'dsh-baton', version: core.VERSION }
11
11
  const PROTOCOLS = ['2025-06-18', '2025-03-26', '2024-11-05']
@@ -36,6 +36,7 @@ const codeModeYield = () => {
36
36
  // while dsh-baton's Stop hook is waiting to be trusted.
37
37
  const TRUST_NOTE = 'One step left: in Codex, open Settings → Hooks and trust the Stop hook "dsh-baton: waiting for the Harness run". After that, Codex no longer uses your quota while it waits for DeepSeek Harness.'
38
38
  const NO_POLL = 'While waiting, make no other calls (no dsh_status, dsh_session_activity or dsh_read_session) and do not list or read the target folder to check progress.'
39
+ const INSTRUCTIONS = 'Hands tasks to a local DeepSeek Harness and brings the results back: start with dsh_send.'
39
40
  const ARCHIVED = 'Sessions not in dsh_list_sessions are archived or gone: do not look for them on disk.'
40
41
  // A shell command that blocks until a run ends and prints its reply. Run in the
41
42
  // background by a client that wakes the model when the command exits (Claude Code),
@@ -125,8 +126,8 @@ const TOOLS = [
125
126
  {
126
127
  name: 'dsh_send',
127
128
  annotations: annotations('Send message', hints(false, false, false)),
128
- description: 'Send a message to a Harness session and return at once with a run_id; then wait for the reply with dsh_wait (or the wait_command it returns, where offered). session_id continues that session; without it a new session starts in cwd or workspace_id, or, with neither, in the folder this conversation works in (a new cwd is registered first). If the session is busy, mode followup queues the message and steer adds it to the running turn; leave mode out to follow the Harness setting. The message arrives verbatim as a user message (with checks, a short request for a sub-agent review table is appended): make it self-contained (Harness does not see this conversation), in the user\'s language. For a large task, first turn the request into a concrete spec (goal, modules and the files each owns, level of detail, acceptance criteria) instead of passing the user\'s words on; send independent parts to separate sessions in parallel, each owning its own files. Review once all parallel parts are back: check their screenshots or test results together, then send back specific fixes for what falls short.',
129
- inputSchema: { type: 'object', required: ['message'], properties: { message: str('The task or reply to send'), session_id: str('Existing session to continue'), cwd: str('Workspace folder for a new session; leave out to use the folder this conversation works in'), workspace_id: str('Harness workspace for a new session (from dsh_list_workspaces)'), mode: { type: 'string', enum: ['followup', 'steer'], description: "followup queues after the current turn; steer injects into a running turn. Omit to follow Harness's busy-send setting" }, model: str('Only when the user names a model for this run (e.g. "DeepSeek-V4 Pro", "v4-pro"); leave out otherwise and the session uses its own model, or Harness\'s default model for a new session. Matched against Harness\'s model list ignoring case, spaces, hyphens and a leading DeepSeek; a name that matches no model or several fails the send with the list (provider/model picks one). Sets this session only; Harness\'s default model is left as it was'), reasoning_effort: str('Only when the user names a reasoning effort, e.g. high; one of the efforts dsh_list_models shows for the model'), checks: { type: 'array', items: { type: 'string' }, description: 'The project\'s own test commands, e.g. npm test (ask Harness in the message to write tests for each requirement). Do not write check scripts yourself. dsh-baton runs them in the folder when the run ends and returns exit codes and the last output lines as evidence.' } } },
129
+ description: 'Hand a task to DeepSeek Harness, or continue a session: returns at once with a run_id and a hint saying how to wait for the reply; follow it. Call it directly, no need to list anything first. A new session starts in the folder this conversation works in, or in cwd or workspace_id (a new cwd is registered first); session_id continues that session. message arrives verbatim as a user message, and Harness does not see this conversation: make it self-contained, in the user\'s language, the full, explicit spec, asking for tests that cover each requirement. For a large task, write a concrete spec (goal, modules and the files each owns, level of detail, acceptance criteria) rather than passing the user\'s words on, and send independent parts to separate sessions in parallel, each owning its own files; review once all are back, then send specific fixes. With checks, a short request for a sub-agent review table is appended. To a busy session, mode followup queues the message and steer adds it to the running turn; left out, the Harness setting decides.',
130
+ inputSchema: { type: 'object', required: ['message'], properties: { message: str('The task or reply to send'), session_id: str('Existing session to continue'), cwd: str('Workspace folder for a new session; leave out to use the folder this conversation works in'), workspace_id: str('Harness workspace for a new session (from dsh_list_workspaces)'), mode: { type: 'string', enum: ['followup', 'steer'], description: "followup queues after the current turn; steer injects into a running turn. Omit to follow Harness's busy-send setting" }, model: str('Only when the user names a model for this run (e.g. "DeepSeek-V4 Pro", "v4-pro"); leave out otherwise and the session uses its own model, or Harness\'s default model for a new session. Matched against Harness\'s model list ignoring case, spaces, hyphens and a leading DeepSeek; a name that matches no model or several fails the send with the list (provider/model picks one). Sets this session only; Harness\'s default model is left as it was'), reasoning_effort: str('Only when the user names a reasoning effort, e.g. high; one of the efforts dsh_list_models shows for the model'), checks: { type: 'array', items: { type: 'string' }, description: 'The project\'s own test commands, e.g. npm test; never check scripts you write yourself. dsh-baton runs them in the folder when the run ends and returns exit codes and the last output lines as evidence.' } } },
130
131
  run: async (a, { meta } = {}) => {
131
132
  // A new session goes to this conversation's folder unless the call names another one;
132
133
  // a different folder is allowed but said back, so a wrong guess does not go unnoticed.
@@ -147,9 +148,13 @@ const TOOLS = [
147
148
  return { ...st, hint: `Run started. End your turn now: reply to the user in one short line (in their language) that the task is with DeepSeek Harness. Do not call dsh_wait, dsh_status or any other tool, and do not read the target folder. When all the runs you started end (or one stops to ask something or fails), dsh-baton continues this conversation by itself with their results together (as dsh_wait returns them), so you review them in one go. If the conversation is ever resumed without it, call dsh_wait with this run_id.` }
148
149
  }
149
150
  // Not trusted yet: the user is told once per conversation, until they trust it.
150
- const note = thread && hookUntrusted() && noteOnce(thread) ? { note_for_user: TRUST_NOTE } : null
151
+ const untrusted = thread && hookUntrusted()
152
+ const note = untrusted && noteOnce(thread) ? { note_for_user: TRUST_NOTE } : null
151
153
  const relay = note ? 'First, start your reply to the user with note_for_user, in their language, on a line of its own. ' : ''
152
- return { ...st, ...note, hint: `${relay}Run started. Call dsh_wait with this run_id; it returns when the run ends or stops to ask the user something. ${NO_POLL} ${codeModeYield()}` }
154
+ // Not added at all: the reason, once per conversation.
155
+ const reason = thread && !untrusted ? hookSkipReason() : null
156
+ const why = reason && noteOnce(thread) ? `${reason} Tell the user so in one line, in their language. ` : ''
157
+ return { ...st, ...note, hint: `${relay}${why}Run started. Call dsh_wait with this run_id; it returns when the run ends or stops to ask the user something. ${NO_POLL} ${codeModeYield()}` }
153
158
  }
154
159
  return {
155
160
  ...st,
@@ -168,7 +173,7 @@ const TOOLS = [
168
173
  {
169
174
  name: 'dsh_wait',
170
175
  annotations: annotations('Wait for run'),
171
- description: () => `Wait for a run sent with dsh_send to end and return its state and final reply.${isCodex() ? '' : ' Prefer the background wait_command from dsh_send if your client wakes you when a background command exits: it costs no turns.'} It also returns at once when the run stops to ask a person: needs_input answer (question and relay; reply with dsh_answer) or harness-ui (an approval only a person can give in Harness). Each call waits up to ${waitSeconds()} s; if it returns timed_out: true, call dsh_wait again with the same run_id, without a progress note.${isCodex() ? ` ${NO_POLL} ${codeModeYield()}` : ''}`,
176
+ description: () => `Wait for a run sent with dsh_send to end and return its state and final reply.${!isCodex() ? ' Prefer the background wait_command from dsh_send if your client wakes you when a background command exits: it costs no turns.' : hookActive() ? ' Call it only when a hint says so: when dsh_send\'s hint says to end your turn, end it, and the result comes back by itself.' : ''} It also returns at once when the run stops to ask a person: needs_input answer (question and relay; reply with dsh_answer) or harness-ui (an approval only a person can give in Harness). Each call waits up to ${waitSeconds()} s; if it returns timed_out: true, call dsh_wait again with the same run_id, without a progress note.${isCodex() ? ` ${NO_POLL} ${codeModeYield()}` : ''}`,
172
177
  inputSchema: { type: 'object', required: ['run_id'], properties: { run_id: str('Run id from dsh_send') } },
173
178
  run: async (a, { progress }) => {
174
179
  const result = await waitInChunks(a, progress)
@@ -180,7 +185,7 @@ const TOOLS = [
180
185
  {
181
186
  name: 'dsh_answer',
182
187
  annotations: annotations('Answer question', hints(false, false, false)),
183
- description: 'Answer the questions a waiting run asked (question in dsh_wait or dsh_status). Per question: option numbers or labels in choose, text for a free answer, or skip. Pass reason when the user let you decide. The answer reaches Harness once every question has one (submitted: true; remaining lists the rest); then wait for the run again. If the user already answered in Harness, it reports that there is no open question.',
188
+ description: `Answer the questions a waiting run asked (needs_input: answer in dsh_wait or dsh_status). ${core.ASKED} Per question: option numbers or labels in choose, text for a free answer, or skip. The answer reaches Harness once every question has one (submitted: true; remaining lists the rest); then wait for the run again. If the user already answered in Harness, it reports that there is no open question.`,
184
189
  inputSchema: {
185
190
  type: 'object', required: ['run_id', 'answers'], properties: {
186
191
  run_id: str('Run id that is waiting (needs_input: answer)'),
@@ -298,7 +303,10 @@ async function handle(req) {
298
303
  protocolVersion: PROTOCOLS.includes(asked) ? asked : PROTOCOLS[0],
299
304
  capabilities: { tools: {} },
300
305
  serverInfo: SERVER,
301
- instructions: `Hands tasks to a local DeepSeek Harness and passes the results back. To hand over a task, call dsh_send {message, checks} directly (no need to list anything first; the new session starts in the folder this conversation works in, or pass cwd for another folder): message is the full, explicit spec (ask for tests that cover each requirement), checks are the project's own test commands such as npm test; do not write check scripts yourself. To continue a session, dsh_send {session_id, message}. Then wait for the reply: ${isCodex() ? 'call dsh_wait with the run_id until it returns the final state (not dsh_status)' : 'run the wait_command it returns as a background shell command if your client wakes you when one exits, otherwise call dsh_wait'}. A run that stops to ask the user something ends the wait with needs_input and the questions; answer with dsh_answer. ${core.ASKED} ${ARCHIVED} If a note says dsh-baton was updated and names a tool you do not have, call it through dsh_call {name, arguments}.`,
306
+ // One sentence: Codex repeats it in front of every tool's description, while a client
307
+ // that lists MCP tools by name only (Claude Code's tool search) still learns what the
308
+ // server is for. The rules live in the tools' own descriptions.
309
+ instructions: INSTRUCTIONS,
302
310
  }
303
311
  }
304
312
  case 'ping':
package/src/register.mjs CHANGED
@@ -130,13 +130,17 @@ export const hookWrapperPath = (home = batonHome(), platform = process.platform)
130
130
  // A path with spaces (a user name with a space): on Windows its 8.3 short form, else
131
131
  // PowerShell's call form of the whole line (PowerShell being Codex's shell on Windows);
132
132
  // elsewhere the path in double quotes, for the shell Codex runs the command with.
133
- export function codexHookEntry(node, script, { platform = process.platform, home = batonHome(), shortPath = winShortPath } = {}) {
133
+ //
134
+ // harness: node is Harness's own binary (no Node.js of the user's own, see harnessRuntime),
135
+ // which runs cli.mjs only in Node mode, so the wrapper sets ELECTRON_RUN_AS_NODE=1 first.
136
+ // The entry stays the same either way: only the script changes.
137
+ export function codexHookEntry(node, script, { platform = process.platform, home = batonHome(), shortPath = winShortPath, harness = false } = {}) {
134
138
  const line = codexHookCommand(node, script)
135
139
  const file = hookWrapperPath(home, platform)
136
140
  const sh = (p) => `'${p.replace(/'/g, `'\\''`)}'`
137
141
  const body = platform === 'win32'
138
- ? `@${line.replace(/%/g, '%%')}\r\n`
139
- : `#!/bin/sh\nexec ${sh(node)} ${sh(path.join(path.dirname(script), 'cli.mjs'))} codex-stop-hook "$@"\n`
142
+ ? `${harness ? '@set ELECTRON_RUN_AS_NODE=1\r\n' : ''}@${line.replace(/%/g, '%%')}\r\n`
143
+ : `#!/bin/sh\n${harness ? 'ELECTRON_RUN_AS_NODE=1 ' : ''}exec ${sh(node)} ${sh(path.join(path.dirname(script), 'cli.mjs'))} codex-stop-hook "$@"\n`
140
144
  fs.mkdirSync(path.dirname(file), { recursive: true })
141
145
  let old = null
142
146
  try { old = fs.readFileSync(file, 'utf8') } catch { /* first time */ }
@@ -145,7 +149,7 @@ export function codexHookEntry(node, script, { platform = process.platform, home
145
149
  if (!/\s/.test(file)) return file
146
150
  if (platform !== 'win32') return JSON.stringify(file)
147
151
  const at = shortPath(file)
148
- return at && !/\s/.test(at) ? at : `& ${line}`
152
+ return at && !/\s/.test(at) ? at : `${harness ? "$env:ELECTRON_RUN_AS_NODE='1'; " : ''}& ${line}`
149
153
  }
150
154
 
151
155
  // The 8.3 short form of an existing path, or null (8.3 names can be off for a drive).
@@ -443,24 +447,39 @@ export function autoRegister({ root, home = os.homedir(), env = process.env, nod
443
447
 
444
448
  // The Codex Stop hook goes in with the server, so Codex waits for runs without model
445
449
  // requests (see codex-hook.mjs). It runs cli.mjs beside the server Codex is set up with,
446
- // with the same node. Codex runs a new hook only once the user trusts it (Settings →
447
- // Hooks); until then dsh-baton keeps to dsh_wait. Not added when the server runs on
448
- // Harness's own runtime or a bare command (no node to name), or with DSH_BATON_CODEX_HOOK=0.
450
+ // with the same runtime: the user's Node.js, or Harness's own in Node mode. Codex runs a
451
+ // new hook only once the user trusts it (Settings → Hooks); until then dsh-baton keeps to
452
+ // dsh_wait. Not added without Codex, with DSH_BATON_CODEX_HOOK=0, or when the entry has
453
+ // no cli.mjs to run (a bare command such as npx, or a server folder that is gone).
454
+ // What happened, and why, is noted in hookResultFile for dsh_send to pass on.
449
455
  function addCodexHook(codex, env, fallback, home) {
456
+ const done = (state, reason = null, command = null) => {
457
+ try {
458
+ const file = hookResultFile(env, home)
459
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 })
460
+ fs.writeFileSync(file, JSON.stringify({ state, reason, command, at: Date.now() }))
461
+ } catch { /* nowhere to note it */ }
462
+ return state
463
+ }
450
464
  try {
451
- if (!codex?.present || env.DSH_BATON_CODEX_HOOK === '0') return 'off'
465
+ if (!codex?.present) return 'off'
466
+ if (env.DSH_BATON_CODEX_HOOK === '0') return done('off', 'disabled')
452
467
  const e = readCodexEntry(fs.existsSync(codex.file) ? fs.readFileSync(codex.file, 'utf8') : '')
453
468
  const script = e?.args?.[0]
454
- const usable = e && /[\\/]/.test(e.command) && !e.env?.ELECTRON_RUN_AS_NODE &&
455
- (!fallback || path.resolve(e.command) !== path.resolve(fallback.command)) &&
456
- script && fs.existsSync(path.join(path.dirname(script), 'cli.mjs'))
457
- if (!usable) return 'skipped'
458
- return rewrite(codex.file, (t) => editCodexHook(t, codexHookEntry(e.command, script, { home: batonHome(env, home) }))) === 'updated' ? 'added' : 'unchanged'
459
- } catch {
460
- return 'failed'
469
+ if (!e || !/[\\/]/.test(e.command) || !script || !fs.existsSync(path.join(path.dirname(script), 'cli.mjs'))) return done('skipped', 'no-cli')
470
+ const harness = e.env?.ELECTRON_RUN_AS_NODE === '1' || Boolean(fallback && path.resolve(e.command) === path.resolve(fallback.command))
471
+ const entry = codexHookEntry(e.command, script, { home: batonHome(env, home), harness })
472
+ return done(rewrite(codex.file, (t) => editCodexHook(t, entry)) === 'updated' ? 'added' : 'unchanged', null, entry)
473
+ } catch (err) {
474
+ return done('failed', err.message)
461
475
  }
462
476
  }
463
477
 
478
+ // What the plugin last did about the hook ({ state, reason, command, at }), see addCodexHook.
479
+ // command is the hook command as written to config.toml (the wrapper path, its 8.3 form,
480
+ // or PowerShell's call form), so a check need not parse TOML.
481
+ export const hookResultFile = (env = process.env, home = os.homedir()) => path.join(batonHome(env, home), 'codex-hook', 'registered.json')
482
+
464
483
  // Where the Stop hook notes each run (codex-hook.mjs): when, and the node and cli.mjs it
465
484
  // ran as, so a hook rewritten since can be told from the one Codex trusted.
466
485
  export const hookAliveFile = (env = process.env, home = os.homedir()) => path.join(batonHome(env, home), 'codex-hook', 'alive')