dsh-any-agent 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +194 -1
  2. package/DESIGN.md +2 -2
  3. package/README.md +1 -1
  4. package/README.zh.md +1 -1
  5. package/USAGE-NOTES.en.md +5 -3
  6. package/USAGE-NOTES.md +11 -4
  7. package/bin/dsh-any-agent-workspaces.mjs +12 -5
  8. package/bin/dsh-any-agent.mjs +4 -1
  9. package/bin/restore.mjs +1 -1
  10. package/docs/en/architecture.md +3 -3
  11. package/docs/en/compatibility.md +27 -5
  12. package/docs/en/reference.md +2 -1
  13. package/docs/en/release.md +4 -0
  14. package/docs/en/security.md +2 -0
  15. package/docs/en/tests.md +58 -16
  16. package/docs/en/usage.md +10 -1
  17. package/docs/zh/architecture.md +3 -3
  18. package/docs/zh/compatibility.md +25 -5
  19. package/docs/zh/limits.md +1 -1
  20. package/docs/zh/reference.md +2 -1
  21. package/docs/zh/release.md +3 -0
  22. package/docs/zh/security.md +2 -0
  23. package/docs/zh/tests.md +58 -16
  24. package/docs/zh/usage.md +10 -1
  25. package/package.json +4 -1
  26. package/scripts/detect-dsh.mjs +55 -0
  27. package/scripts/lint.mjs +595 -0
  28. package/scripts/sync-test-counts.mjs +699 -0
  29. package/src/acp-session.mjs +63 -0
  30. package/src/acp.mjs +14 -5
  31. package/src/config.mjs +42 -99
  32. package/src/conversation.mjs +1600 -0
  33. package/src/doctor.mjs +203 -0
  34. package/src/dsh-locate.mjs +560 -0
  35. package/src/git-snapshot.mjs +196 -0
  36. package/src/hub-store.mjs +108 -0
  37. package/src/hub.mjs +26 -1727
  38. package/src/mcp.mjs +21 -72
  39. package/src/residue.mjs +278 -0
  40. package/src/review.mjs +11 -0
  41. package/src/sentinel.mjs +97 -0
  42. package/src/tool-defs.mjs +399 -0
  43. package/src/tool-render.mjs +262 -0
  44. package/src/tools.mjs +27 -690
  45. package/src/workspace-guard.mjs +89 -0
  46. package/src/workspace.mjs +295 -59
package/CHANGELOG.md CHANGED
@@ -7,6 +7,194 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0] — 2026-11-07
11
+
12
+ ### Added
13
+
14
+ - **Zero-config DSH discovery, including the desktop app.** The server now finds
15
+ the DeepSeek Harness desktop app on its own (Windows uninstall registry via
16
+ `reg export`, plus default install paths on every platform), reads the CLI's
17
+ version and `engines` straight out of `resources/app.asar` with plain Node,
18
+ and launches it on the app's bundled Electron runtime in Node mode
19
+ (`ELECTRON_RUN_AS_NODE=1`). Previously only npx caches and npm global installs
20
+ were searched, so with the desktop app on 0.2.2-rc.1 the server silently ran a
21
+ stale 0.2.1-rc.5 from an npx cache. Candidates are ranked runnable-first →
22
+ newest → desktop > npm global > npx. `DSH_BIN` stays as an explicit override
23
+ (it may point inside an `.asar`); new `DSH_RUNTIME` overrides the launcher,
24
+ both with `DSH_BIN` and on its own for auto-discovered npm/npx installs (whose
25
+ `engines.node` is then re-checked against it); the desktop app always keeps its
26
+ bundled runtime.
27
+ - `dsh_doctor`: `dsh_bin` shows the source and runtime; new `dsh_runtime`
28
+ (launcher's Node vs DSH's `engines.node` — 0.2.2 needs Node ≥ 24.9),
29
+ `dsh_desktop` (warns when the GUI and this server run different DSH versions
30
+ over the shared `~/.dsh`), and `dsh_candidates`.
31
+ - Background receipts on Windows carry `sentinel_read_powershell`, a reader that
32
+ passes `-Encoding UTF8` (Windows PowerShell 5.1 otherwise misreads the BOM-less
33
+ sentinel and `ConvertFrom-Json` fails on CJK text).
34
+ - `npm run lint` (`scripts/lint.mjs`, no npm dependencies): `node --check` on every
35
+ `.mjs`, relative import paths that resolve with exact case, named imports that
36
+ are real exports (including `await import()` destructuring and `node:`
37
+ builtins), and unused named imports. Runs first in every CI offline cell and at
38
+ the Node 22.15 floor; `test/lint.mjs` is its regression suite. Its first run
39
+ removed nine unused imports.
40
+ - **Test counts in the docs are generated, not hand-edited.** `test/run.mjs`
41
+ writes a machine-readable report (`.state/test-report.json` by default,
42
+ `--report <path>`; per suite: passed/failed/skipped/duration, plus platform,
43
+ Node, DSH and client-fixture context), every suite prints a standard summary
44
+ line, and `npm run docs:counts` rewrites the per-suite counts, totals and the
45
+ CI scope comment from it; `npm run docs:counts:check` fails on any mismatch.
46
+ The CI offline job runs the check on the platform-independent suites, and the
47
+ release checklist includes it. Several hand-maintained numbers were wrong and
48
+ are corrected (e.g. failure-terminal 10, not 11).
49
+
50
+ ### Changed
51
+
52
+ - **Internal: `src/hub.mjs` and `src/tools.mjs` are split into cohesive modules**
53
+ with no behaviour change. From `hub.mjs`: `conversation.mjs` (the `Conversation`
54
+ class), `git-snapshot.mjs`, `hub-store.mjs` (meta sidecar and forget
55
+ tombstones), `sentinel.mjs`, `workspace-guard.mjs`, `acp-session.mjs`. From
56
+ `tools.mjs`: `tool-defs.mjs`, `tool-render.mjs`, `doctor.mjs`. Both files
57
+ re-export every name they exported before, so existing import paths still work.
58
+ - `model-suites` workflow finds DSH through the project's own `resolveDsh()`
59
+ (`scripts/detect-dsh.mjs`) instead of `command -v dsh`, so a self-hosted runner
60
+ with only the desktop app installed works without secrets; `DSH_BIN` stays an
61
+ optional override.
62
+ - **`dsh-any-agent-workspaces --purge-test-sessions --ids …` now refuses
63
+ sessions whose cwd is not a test directory**, even when named explicitly, and
64
+ reports them as skipped. The purge also removes every artifact of a test
65
+ session (projection cache, registry entry, sentinels, reviews, locks), not
66
+ just its session directory.
67
+ - Compatibility matrix: DSH 0.2.1-rc.5 and 0.2.2-rc.1 are **verified** (full
68
+ suites, 2026-11-06); 0.2.3+ is unverified.
69
+ - Server `instructions` cut from ~4.4k to ~1.5k characters with the hard rules
70
+ first. Clients truncate long instructions (Claude Code dropped the old tail,
71
+ including the "don't" rules); `smoke` now enforces a 2400-character cap.
72
+ - `node test/run.mjs` runs **every** suite and lists all failures at the end;
73
+ `--bail` restores stop-at-first-failure. One stale assertion used to hide the
74
+ eleven model-backed suites after it.
75
+
76
+ ### Fixed
77
+
78
+ - **Test runs left residue in the real `~/.dsh`.** Purging a test session removed
79
+ only its session directory; the DSH projection cache
80
+ (`storages/session_projcache/sessions/<id>.json`) was never removed, so about
81
+ 400 orphan files piled up, and the project's own `runs/`, `reviews/`, `meta/`,
82
+ `locks/` and registry entries leaked too (eight suites kept their state loose
83
+ under `%TEMP%`, where nothing cleaned it). Every model-backed suite now purges
84
+ exactly the sessions it created (plus DSH-spawned subagent sessions) with every
85
+ artifact in its own `finally`; `test/cleanup.mjs` is only the backstop and also
86
+ sweeps old orphans. The purge refuses any session whose cwd is not a test
87
+ directory, even when named by id, and no longer re-registers every temp session
88
+ on disk into the GUI workspace list as a side effect. New offline suite
89
+ `residue-purge` proves both halves in a sandboxed `DSH_HOME`. With
90
+ `DSH_MCP_KEEP_EVIDENCE=1`, `cleanup.mjs` now skips the whole sweep, so the
91
+ sessions and state directories that `acceptance` / `workspace-effect` keep for
92
+ inspection survive the end-of-run cleanup.
93
+ - **`dsh_read` merged different tool calls into one entry.** Consecutive tool
94
+ updates in a turn were concatenated (`read [in_progress]read [completed]pwsh …`)
95
+ under the first call's `tool_call_id`. Each tool call now has its own entry and
96
+ a status update replaces it in place; only streamed text is still merged, so an
97
+ empty turn's start/end markers also stay separate. Because an in-place update
98
+ can give an earlier entry a higher revision, `dsh_read` now pages in revision
99
+ order (entries in a page are still returned in time order) so a small `limit`
100
+ can never skip an unread entry, and `gap` is computed from entries actually
101
+ evicted from the ring buffer instead of assuming revisions grow along it.
102
+ - **`answer` ran message segments together.** Text DSH sends before and after a
103
+ tool call is two messages, but the collected answer joined them with no
104
+ separator (`…where possible.1) package.json…`). A blank line now separates a
105
+ segment that follows a tool call (none is added when the previous segment
106
+ already ends with a newline, or before the first text); `dsh_read` entries are
107
+ unchanged.
108
+ - **A clean repository is a 0-change snapshot, not `null`.** `null` now only means
109
+ "no snapshot" (not a git repo, git unavailable, disabled). A turn that removed
110
+ the last change used to report `workspace_changed=true` with `diff_stat=null`.
111
+ - **A cold `session/resume` that DSH never answers is retried once.** A live
112
+ `test/multi.mjs` run once hit `ACP request timed out: session/resume` (60 s of
113
+ silence from a freshly spawned DSH); a rerun passed and it did not reproduce in
114
+ ~70 controlled resume cycles, 40 of them under concurrent load. The write lock is
115
+ ruled out (DSH takes it with a zero-timeout wait and refuses instantly), so it
116
+ looks like a rare stall inside the DSH process. `resume` is idempotent and the
117
+ failure path already kills the process and waits for it to exit, so `ensureAlive`
118
+ now rebuilds the process and retries exactly once — only for a timeout, never for
119
+ a write-lock refusal or invalid params, and not on a shared host that still carries
120
+ other conversations. `AcpProcess` timeout errors now carry `code=ACP_TIMEOUT` and
121
+ `method`, and the DSH stderr tail at the moment of a timeout goes to the opt-in
122
+ `DSH_MCP_LOG_STDERR` log. New offline suite `resume-retry` (fake ACP server).
123
+ - Tests: `timeout` asserts that a negative `timeout_ms` is rejected (the 0.4.0
124
+ behaviour) instead of the old "wait forever" semantics, and no longer passes on
125
+ an error result; `list` asserts caching via `probe_cached` instead of a 500 ms
126
+ wall-clock threshold; `integration`'s queue check uses a code token instead of
127
+ requiring the model to echo a phrase verbatim; `capability` prints the tool
128
+ error text when a call fails instead of an empty `err=`.
129
+
130
+ ## [0.5.2] — 2026-11-05
131
+
132
+ ### Fixed
133
+
134
+ - **`workspace_changed` no longer misses real edits.** The turn snapshot now
135
+ fingerprints every `git status` entry (status line + per-path size/mtime,
136
+ with untracked directories expanded recursively), not just the first 40
137
+ status lines. A file that was already dirty and changed again, or any change
138
+ past the 40th entry, is detected; `changed_files` and `entries` keep their
139
+ old meaning (`entries` is still display-truncated to 40).
140
+ - **`gitStatus` is asynchronous.** Turn start/end snapshots now use `execFile`
141
+ instead of `spawnSync`, so a large repository's `git status` (up to 8 s) no
142
+ longer blocks the shared MCP event loop, ACP messages, approvals or other
143
+ conversations. `busy` is still set before any await; turn finalization keeps
144
+ releasing `busy`, waking interject waiters and scheduling the drain before
145
+ waiting for git, and only writes the sentinel after the result (including
146
+ `workspace_changed` and the review ticket) is complete.
147
+ - `--no-optional-locks` is passed as a **global** git option (before the
148
+ subcommand); previously `git status ... --no-optional-locks` exited 129, so
149
+ every real snapshot was `null` and `workspace_changed` stayed false.
150
+ - **`dsh_get(max_chars)` now bounds the whole response.** The budget used to
151
+ apply only to the rendered text while `structured.run.result` kept every
152
+ tool's full input, so a 77-tool turn came back as ~75K characters and was
153
+ rejected by Claude Code's per-call limit even with `max_chars=8000`. With
154
+ `max_chars > 0`, structured is compacted too: the answer is truncated and
155
+ `tools_used[]` keeps `id/title/name/status/kind` plus `input_chars`
156
+ (`run.result_compacted: true`). `max_chars=0` (default) is unchanged; the
157
+ full result is always in the sentinel file.
158
+ - **`dsh_doctor` flags unverified DSH versions and fragile entry points.** A new
159
+ `dsh_version` check grades the DSH version against the compatibility matrix
160
+ (`verified` 0.1.7 ≤ v < 0.2.0 → ok; `partial`, `unsupported`, `unverified`
161
+ such as 0.2.x, or unreadable → warn). `dsh_bin` warns when DSH resolves from
162
+ an npx cache (`_npx/<hash>/`), which disappears on cache cleanup.
163
+
164
+ ### Added
165
+
166
+ - **Auto-approved approvals are visible.** Under `on_approval=auto-allow`, every
167
+ turn result now carries `auto_approvals_total`, `auto_escalations_total`
168
+ (requests with `sandbox_permissions`, e.g. `danger-full-access`) and up to 20
169
+ redacted `auto_approvals` summaries (tool, escalation, justification, command
170
+ preview). The review ticket's evidence and checklist call them out, so a
171
+ `workspace-write` session that silently escalated no longer looks like it
172
+ stayed in bounds. Defaults are unchanged; use `on_approval=ask` to gate each.
173
+ - **PowerShell sentinel waiter.** On Windows the background receipt includes a
174
+ ready-made, quote-escaped `sentinel_wait_powershell` command (also in the
175
+ text), and the server instructions document it, for hosts where Git Bash is
176
+ missing or hangs.
177
+
178
+ ### Website
179
+
180
+ - Playground: step backward, stateful connection-line status, sentinel receipt
181
+ pulse, and a "try interject" cross-scenario action on dispatch frames.
182
+ - Code snippets now use one-pass syntax highlighting (quoted keys, values,
183
+ comments and arrows) without nested spans; the Cursor config tab and copy
184
+ success icon were added; `/en/` HTML is explicitly revalidated.
185
+ - Navigation now surfaces the FAQ; English and Chinese nav order match.
186
+
187
+ ### Documentation
188
+
189
+ - Fix the profile-creation command in `USAGE-NOTES.md` / `.en.md`: it needs
190
+ `dsh --profile dsh-mcp --from-default-profile acp`, and 0.5.0+ normally
191
+ bootstraps the profile automatically.
192
+ - Correct the team-mode profile default in `docs/zh/limits.md` and the
193
+ architecture diagrams: `dsh-mcp` is default; `dsh-team` is used only when
194
+ team mode is on.
195
+ - Clarify the website README's Node.js floor (website build ≥ 22; the MCP
196
+ server itself requires ≥ 22.15).
197
+
10
198
  ## [0.5.1] — 2026-11-04
11
199
 
12
200
  ### Security
@@ -948,7 +1136,12 @@ First public release, published to npm as [`dsh-any-agent`](https://www.npmjs.co
948
1136
  profiles, sessions and client configs. The product name changed; the on-disk
949
1137
  contract did not. See the README FAQ.
950
1138
 
951
- [Unreleased]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.9...HEAD
1139
+ [Unreleased]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.6.0...HEAD
1140
+ [0.6.0]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.5.2...v0.6.0
1141
+ [0.5.2]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.5.1...v0.5.2
1142
+ [0.5.1]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.5.0...v0.5.1
1143
+ [0.5.0]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.4.0...v0.5.0
1144
+ [0.4.0]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.9...v0.4.0
952
1145
  [0.3.9]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.8...v0.3.9
953
1146
  [0.3.8]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.7...v0.3.8
954
1147
  [0.3.7]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.6...v0.3.7
package/DESIGN.md CHANGED
@@ -252,7 +252,7 @@ Profile = `~/.dsh/profiles/<name>/`,由 `package.json` 的 `dsh.profile.bundle
252
252
 
253
253
  **B11 设计要点(调用方审核)**
254
254
  - **为什么放进返回载荷**:返回值是"另一个 agent 的自述"。调用方最常见的失败模式是直接转述"已完成/已修好"。审核要求必须和结果出现在同一条消息里(`content.text`、`structuredContent`、哨兵 `result.review`),才可能改变调用方的下一步动作;单独一个"记得审核"的约定会被忽略。
255
- - **审核单形状**:`{review_id, mode, status, run_id, prompt_preview, checklist, evidence, submit:{tool:"dsh_review", arguments}, instructions}`。清单随回合形状变化(有没有改工作区、有没有错误、用了哪些工具);`evidence` 只放核对所需的最小事实(stop_reason、error、changed_files、工具名)。
255
+ - **审核单形状**:`{review_id, mode, status, run_id, prompt_preview, checklist, evidence, submit:{tool:"dsh_review", arguments}, instructions}`。清单随回合形状变化(有没有改工作区、有没有错误、用了哪些工具);`evidence` 只放核对所需的最小事实(stop_reason、error、changed_files、工具名)。`workspace_changed` 必须基于**全量**快照指纹(逐路径 size/mtime,未跟踪目录递归),只比前 40 条状态行会把"同一状态行但内容变了"误报成没落地,审核清单也就失去了证据价值。
256
256
  - **默认 advisory,不阻塞**:这是刻意的产品选择。强制门禁会打断"审核失败 → 派修复回合"的自然流程,也会破坏既有调用方;advisory 只把审核请求显性化。`review=off` 可按会话/单次派活关闭;即使 off,`dsh_review` 仍可对旧 run 显式补审核。
257
257
  - **裁决落盘与回写**:`reviews/<会话id>/<run_id>.review.json` 原子写入(tmp + rename,唯一 tmp 名),含裁决、理由/findings、清单与证据。`submitReview` 依次从内存 run → 哨兵 → 已有审核记录取事实,所以服务重启、run 被挤出内存后仍可审核。提交后 **原子重写哨兵里的 `result.review`**,避免异步调用方下次读文件看到与事实相反的 `pending`。
258
258
  - **隐私**:审核单不含思考正文;理由、findings、任务预览、证据**与每个 checklist 项**在落盘前都走 `redact.sanitizeApprovalContext`(与审批上下文同一套规则)——checklist 会拼接 `result.error`,漏掉它就会成为第二个明文泄漏点。审核记录与哨兵共用 `DSH_MCP_SENTINEL_TTL_MS` 的 7 天清理。
@@ -305,7 +305,7 @@ Profile = `~/.dsh/profiles/<name>/`,由 `package.json` 的 `dsh.profile.bundle
305
305
 
306
306
  | # | 功能 | 说明 |
307
307
  |---|---|---|
308
- | E1 | **返回变更 diff** | 回合结束后返回工作区变更(`workspace/changes` 事件 + `git diff --stat`)。**让调用方 agent 能审阅 DSH 的产出再决定是否接受**——这是"外包工程师"模式的关键 |
308
+ | E1 | **返回工作区变更** | 回合结束后返回 `workspace_changed`(本回合前后 `git status` 的**全量**快照指纹:状态行 + 逐路径 size/mtime,未跟踪目录递归展开)与 `diff_stat`(当前脏文件摘要)。快照走异步子进程,不阻塞共享 MCP 事件循环。**让调用方 agent 能审阅 DSH 的产出再决定是否接受**——这是"外包工程师"模式的关键 |
309
309
  | E2 | 计划模式 | 只出计划不落地(`dsh-plan-mode`),调用方批准后再执行 |
310
310
  | E3 | 审查模式 | 对既有改动做对抗式审查 |
311
311
  | E4 | 接受/回滚 | 会话级 checkpoint(DSH 有 `dsh-session-checkpoint-policy`)配合 git 做回滚 |
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  </div>
17
17
 
18
- 🧪 0.5.1 offline suite green, including caller review and input-boundary hardening · 🔌 MCP over stdio · 📦 Zero dependencies · 📜 MIT
18
+ 🧪 0.5.2 offline suite green, including the workspace-change detection fix and visible auto-approvals · 🔌 MCP over stdio · 📦 Zero dependencies · 📜 MIT
19
19
 
20
20
  An MCP server that drives DeepSeek Harness as a **long-lived coding agent**. Hand it a goal, let it write code and run scripts, steer it mid-flight, and collect the result when you are ready.
21
21
 
package/README.zh.md CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  </div>
17
17
 
18
- 🧪 0.5.1 本机离线套件全绿,含调用方审核与输入边界加固 · 🔌 MCP over stdio · 📦 零依赖 · 📜 MIT
18
+ 🧪 0.5.2 本机离线套件全绿,含工作区变化检测修复、自动放行审批可见 · 🔌 MCP over stdio · 📦 零依赖 · 📜 MIT
19
19
 
20
20
  一个 MCP 服务端,把 DeepSeek Harness 当作**长驻的编码 agent** 来驱动。开个会话、扔个目标,它自己写代码、跑脚本、派子代理;你随时能看、能插话、能回头验收。
21
21
 
package/USAGE-NOTES.en.md CHANGED
@@ -8,7 +8,7 @@ Practical gotchas and trade-offs you *will* hit when letting Claude / Codex driv
8
8
 
9
9
  ## 1. Setup
10
10
 
11
- **1.1 The profile must exist first.** Otherwise every turn fails with `no adapter registered for provider "xxx"`. This server ships no provider config; it drives `~/.dsh/profiles/dsh-mcp/`. Create it with `dsh dsh-mcp --from-default-profile acp` and fill in your own provider per [`profile-example/cordis.patch.yml`](profile-example/cordis.patch.yml).
11
+ **1.1 The profile must exist first.** Otherwise every turn fails with `no adapter registered for provider "xxx"`. This server ships no provider config; it drives `~/.dsh/profiles/dsh-mcp/`. Create it with `dsh --profile dsh-mcp --from-default-profile acp` and fill in your own provider per [`profile-example/cordis.patch.yml`](profile-example/cordis.patch.yml). On 0.5.0+ the server bootstraps this profile on first start, so the manual command is normally only needed when `DSH_MCP_BOOTSTRAP=0` or the profile is broken.
12
12
 
13
13
  **1.2 ★ The most common trap: ACP's model does NOT come from `agent-default-model`.** You configure your provider there, yet turns fail with `no API key for provider route "deepseek-official"`. ACP reads the `dsh-acp` plugin's own `config.provider` / `config.model`, which the `acp` bundle hard-codes. You must override the `acp` row:
14
14
 
@@ -24,7 +24,7 @@ Practical gotchas and trade-offs you *will* hit when letting Claude / Codex driv
24
24
 
25
25
  **1.4 Never register dsh-any-agent into DSH's own MCP list.** DSH is an MCP client, this server is an MCP server; pointing one at the other recurses forever.
26
26
 
27
- **1.5 `DSH CLI entry not found`.** Set `DSH_BIN` explicitly (auto-detection scans the npx cache, npm global, and common paths).
27
+ **1.5 `DSH CLI entry not found`.** Auto-detection finds the DeepSeek Harness desktop app (Windows uninstall registry plus default install paths on every platform; the CLI inside `app.asar` runs on the app's bundled runtime), npm global installs and the npx cache. Check `dsh_candidates` in `dsh_doctor`; for portable or custom locations set `DSH_BIN` explicitly (it may point inside `app.asar`).
28
28
 
29
29
  ## 2. Cost and budget
30
30
 
@@ -94,6 +94,8 @@ Practical gotchas and trade-offs you *will* hit when letting Claude / Codex driv
94
94
 
95
95
  **6.3 Temp directories are skipped by default** (`DSH_MCP_REGISTER_WORKSPACE=project`) so test runs don't flood your GUI. Use `all` to include them.
96
96
 
97
+ **6.4 Running the tests must not leave anything in `~/.dsh`.** Symptom: the session count under `~/.dsh` keeps growing, and `storages/session_projcache/sessions/` holds many orphan files whose session directories are long gone. Cause: model-backed tests use your real `~/.dsh` (the credentials live there, so it cannot be isolated), and the old cleanup removed the session directory but neither the projection cache nor this project's own `runs/reviews/meta/locks`. Now: every model-backed suite removes the sessions it created, with every artifact, in its own `finally`, and `node test/cleanup.mjs` is the global backstop. **Only sessions whose cwd is a test directory are ever deleted** — naming an id does not bypass that. See `docs/en/tests.md`.
98
+
97
99
  ## 7. Privacy
98
100
 
99
101
  **7.1 Silent by default** — not one byte. MCP's stdout is the protocol channel, and stderr lands in Claude/Codex's logs.
@@ -130,7 +132,7 @@ Practical gotchas and trade-offs you *will* hit when letting Claude / Codex driv
130
132
  1. **Give goals, not commands.** DSH is an agent: say "add tests for this module and make them pass", not "run pytest". It picks tools, plans steps, and spawns subagents on its own.
131
133
  2. **One conversation per task** — mixing unrelated work pollutes history and costs more.
132
134
  3. **Split long work into dispatch + collect**: `wait=false` → do something else → `dsh_get(run_id)`.
133
- 4. **Review output via workspace changes** — `workspace_changed` is true when the `git status` entry set changed across the turn; `diff_stat` lists the current dirty files (it is not a per-turn diff).
135
+ 4. **Review output via workspace changes** — `workspace_changed` compares a **full** `git status` snapshot fingerprint taken before and after the turn (status line plus each dirty/untracked path's size and mtime; untracked directories are expanded recursively), so an already-dirty file that changes again, or a change past the 40th entry, is still detected. `diff_stat` lists the current dirty files (it is not a per-turn diff).
134
136
  5. **Review the result, don't just relay it** — every result carries a `review` ticket. Check at least one claim with your own tools (read the changed file, inspect `git diff`, run the relevant test), then record `pass` / `partial` / `fail` with `dsh_review`. Default `advisory` does not block the next send; `fail`/`partial` need a reason, and a failed review is not fed back to DSH automatically.
135
137
  6. **Ask for a plan first** if you want to approve before execution (DSH has plan mode).
136
138
  7. **Three cost knobs**: lower `reasoning_effort`, keep `reasoning=hide`, release and start fresh.
package/USAGE-NOTES.md CHANGED
@@ -11,7 +11,7 @@
11
11
  ### 1.1 profile 必须先建,否则连不上模型
12
12
  - **现象**:所有回合都失败,报 `no adapter registered for provider "xxx"`。
13
13
  - **原因**:本服务不自带任何 provider 配置,它驱动的是 `~/.dsh/profiles/dsh-mcp/` 这个 profile。
14
- - **对策**:`dsh dsh-mcp --from-default-profile acp`,然后照 [`profile-example/cordis.patch.yml`](profile-example/cordis.patch.yml) 填上你自己的 provider。
14
+ - **对策**:`dsh --profile dsh-mcp --from-default-profile acp`,然后照 [`profile-example/cordis.patch.yml`](profile-example/cordis.patch.yml) 填上你自己的 provider。0.5.0+ 首次启动会自动 bootstrap 这个 profile,通常无需手工执行;只有在 `DSH_MCP_BOOTSTRAP=0` 或 profile 损坏时才需要。
15
15
 
16
16
  ### 1.2 ★ 最常见的坑:ACP 的模型不走 `agent-default-model`
17
17
  - **现象**:明明在 `agent-default-model` 里配了自己的 provider,会话却报 `no API key for provider route "deepseek-official"`。
@@ -37,8 +37,10 @@
37
37
 
38
38
  ### 1.5 `DSH_BIN` 探测失败
39
39
  - **现象**:启动即报 `DSH CLI 入口未找到`。
40
- - **原因**:DSH 装在非常规位置(本服务按 npx 缓存 → npm 全局 → 常见路径的顺序通配搜索)。
41
- - **对策**:显式设置 `DSH_BIN=/path/to/node_modules/@deepseek-ai/dsh/lib/bin.js`。
40
+ - **原因**:DSH 装在非常规位置。本服务自动识别 DeepSeek Harness 桌面版(Windows 卸载注册表 +
41
+ 各平台默认安装位置,用桌面版自带运行时跑 asar 里的 CLI)、npm 全局安装和 npx 缓存。
42
+ - **对策**:先跑 `dsh_doctor` 看 `dsh_candidates`;便携版/自定义位置再显式设置
43
+ `DSH_BIN=/path/to/node_modules/@deepseek-ai/dsh/lib/bin.js`(可指进 `app.asar` 内部)。
42
44
 
43
45
  ---
44
46
 
@@ -226,6 +228,11 @@
226
228
  - **原因**:跑测试会在 `%TEMP%` 下建一堆工作区,全登记会把你 GUI 灌满。
227
229
  - **对策**:默认模式 `project` 会跳过临时目录。要连临时目录一起登记就设 `DSH_MCP_REGISTER_WORKSPACE=all`。
228
230
 
231
+ ### 6.4 跑测试不该在 `~/.dsh` 里留东西
232
+ - **现象**:`~/.dsh` 里会话数越攒越多;`storages/session_projcache/sessions/` 里有大量会话目录早已不在的孤儿文件。
233
+ - **原因**:真机测试用的是你真实的 `~/.dsh`(凭据在那里,没法隔离);旧版清理只删会话目录,不删投影缓存,也不清本项目自己的 `runs/reviews/meta/locks`。
234
+ - **现状**:每个真机套件在 `finally` 里清掉自己造的会话及全部产物,全局 `node test/cleanup.mjs` 兜底;**只有 cwd 是测试目录的会话才会被删**,点名 id 也绕不过。详见 `docs/zh/tests.md`。
235
+
229
236
  ---
230
237
 
231
238
  ## 七、隐私与日志
@@ -271,7 +278,7 @@
271
278
  1. **给它目标,不要给命令**。DSH 是 agent:说"把这个模块的测试补上并跑通",别说"执行 pytest"。它自己会选工具、分步骤、必要时派子代理。
272
279
  2. **一个会话一件事**。别往一个会话里塞互不相关的任务,历史会互相污染且越来越贵。
273
280
  3. **长任务拆成"派活 + 验收"**:`wait=false` → 干别的 → `dsh_get(run_id)`。
274
- 4. **要审查产出就用工作区变更**:`workspace_changed` 表示本回合前后 `git status` 的条目集合发生了变化;`diff_stat` 列出当前仍处于改动状态的文件(不是本回合的精确 diff)。
281
+ 4. **要审查产出就用工作区变更**:`workspace_changed` 比较的是本回合前后 `git status` 的**全量**快照指纹(状态行 + 每个脏/未跟踪路径的 size/mtime;未跟踪目录会递归展开),所以"回合前已经脏、回合内又被改内容"或"改动落在第 41 条之后"都能识别。`diff_stat` 列出当前仍处于改动状态的文件(不是本回合的精确 diff)。
275
282
  5. **审核结果,别直接转述**:每个结果都带 `review` 审核单。用自己的工具核验至少一项关键声明(读改动的文件、看 `git diff`、跑相关测试),再用 `dsh_review` 记录 `pass` / `partial` / `fail`。默认 `advisory` 不阻塞下一次派活;`fail`/`partial` 必须给理由,且审核不通过不会自动回灌给 DSH。
276
283
  6. **要它先想清楚再动手**:让它先给方案、你确认后再执行(DSH 有计划模式)。
277
284
  7. **省钱的三个旋钮**:`reasoning_effort` 调低、`reasoning` 保持 `hide`、及时 `dsh_release` 并开新会话。
@@ -15,8 +15,11 @@
15
15
  * node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions [--dry-run] [--ids=a,b]
16
16
  *
17
17
  * 只有 --purge-test-sessions 会动磁盘上的会话文件,且规则很死:只删
18
- * "cwd 在 tmpdir()/dsh-any-agent-* 下" 或 "cwd 在 Desktop\dsh-any-agent-test-*" 的会话,
19
- * 外加 --ids 显式指定的。**不会**删除用户在 %TEMP% 里自己的真实项目会话。
18
+ * "cwd 在 tmpdir()/dsh-any-agent-* 下"、"cwd 在 Desktop\dsh-any-agent-test-A|B" 或本仓库
19
+ * ".state/ws-probe-*" 的会话。**不会**删除用户在 %TEMP% 里自己的真实项目会话。
20
+ * --ids=a,b 只清点名的那几个,但**点名也绕不过护栏**:cwd 不是测试目录的会被拒绝并列出。
21
+ * 每个被删会话的全部产物一并清掉(日志、投影缓存、工作区登记、墓碑,以及本项目的
22
+ * 注册表/元数据/哨兵/审核/锁登记);另外清掉投影缓存里的测试孤儿。
20
23
  */
21
24
 
22
25
  import {
@@ -24,10 +27,10 @@ import {
24
27
  listWorkspaces,
25
28
  pruneEmptyWorkspaces,
26
29
  pruneTempWorkspaces,
27
- purgeTestSessions,
28
30
  WORKSPACE_FILE,
29
31
  SESSIONS_ROOT,
30
32
  } from '../src/workspace.mjs';
33
+ import { purgeTestResidue, sweepTestResidue } from '../src/residue.mjs';
31
34
 
32
35
  const argv = process.argv.slice(2);
33
36
  const has = (f) => argv.includes(f);
@@ -46,14 +49,18 @@ const ids = (val('--ids') ?? '').split(',').map((s) => s.trim()).filter(Boolean)
46
49
 
47
50
  // ── 清理测试会话(唯一会删文件的动作) ──────────────────────
48
51
  if (has('--purge-test-sessions')) {
49
- const res = purgeTestSessions({ dryRun, ids, testOnly: true });
52
+ // 点名(--ids)= 只清这几个;否则全盘扫荡。两条路都会连带清本项目侧的 per-run 数据。
53
+ const full = ids.length ? purgeTestResidue({ ids, dryRun }) : sweepTestResidue({ dryRun });
54
+ const res = full.sessions;
50
55
  console.log(`${flag}清理测试会话:${res.count} 个`);
51
56
  for (const v of res.victims) {
52
57
  console.log(` - ${v.id} [${v.reason}]${v.origin ? ` origin=${v.origin}` : ''} cwd=${v.cwd}`);
53
58
  if (v.error) console.log(` ⚠️ 删除失败: ${v.error}`);
54
59
  }
60
+ for (const s of res.skipped ?? []) console.log(` ✗ 拒绝 ${s.id}:${s.reason} cwd=${s.cwd}`);
55
61
  if (!res.count) console.log(' (没有命中任何会话)');
56
- if (!dryRun) console.log(' 已同步从工作区注册表摘除这些会话 id');
62
+ if (!dryRun) console.log(' 已同步清掉这些会话的投影缓存、工作区登记、墓碑与本项目 per-run 数据');
63
+ if (full.orphans) console.log(`${flag} 投影缓存测试孤儿:${full.orphans.count} 个`);
57
64
  console.log('');
58
65
  }
59
66
 
@@ -3,7 +3,8 @@
3
3
  * dsh-any-agent 入口:以 MCP stdio 服务端方式运行,用 DSH (ACP) 作为后端。
4
4
  *
5
5
  * 环境变量:
6
- * DSH_BIN DSH CLI 入口(默认探测 npx 缓存位置)
6
+ * DSH_BIN DSH CLI 入口(默认自动识别:桌面版 → npm 全局 → npx 缓存)
7
+ * DSH_RUNTIME 拉起 DSH 的可执行文件(默认:桌面版用其自带运行时,其余用本进程 node)
7
8
  * DSH_MCP_PROFILE 驱动用的 DSH profile(默认 dsh-mcp)
8
9
  * DSH_MCP_STATE 会话注册表路径(默认 $DSH_HOME/.dsh-any-agent/conversations.json)
9
10
  * DSH_MCP_PERMISSION 新建会话的默认权限档(默认 danger-full-access)
@@ -45,6 +46,8 @@ if (argv.includes('--help') || argv.includes('-h')) {
45
46
  ' dsh-any-agent-setup --yes apply\n\n' +
46
47
  'Key environment variables (all optional):\n' +
47
48
  ' DSH_BIN path to @deepseek-ai/dsh/lib/bin.js; auto-detected\n' +
49
+ ' (desktop app -> npm global -> npx cache)\n' +
50
+ ' DSH_RUNTIME executable that launches DSH; auto (desktop app\'s own runtime)\n' +
48
51
  ' DSH_MCP_PROFILE DSH profile to drive (default dsh-mcp)\n' +
49
52
  ' DSH_MCP_PERMISSION danger-full-access | workspace-write | read-only\n' +
50
53
  ' DSH_MCP_APPROVAL auto-allow | auto-deny | ask\n' +
package/bin/restore.mjs CHANGED
@@ -19,7 +19,7 @@
19
19
  import { chmodSync, existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
20
20
  import { basename, dirname, join } from 'node:path';
21
21
 
22
- import { atomicWrite, backupFile, clientList, resolveDirs, ENTRY_NAME } from '../src/clients.mjs';
22
+ import { atomicWrite, backupFile, clientList, resolveDirs } from '../src/clients.mjs';
23
23
 
24
24
  const argv = process.argv.slice(2);
25
25
  const has = (f) => argv.includes(f);
@@ -24,9 +24,9 @@ DSH ships two programmable entry points. The `sdk` profile looks simpler, but it
24
24
  │ MCP over stdio │ newline-delimited JSON-RPC (ACP)
25
25
  ▼ ▼
26
26
  ┌─────────────┐ ┌──────────────────────────┐
27
- │dsh-any-agent│───────────▶│ dsh --profile dsh-team │ sessions with the same
28
- └─────────────┘ │ │ (cwd, permission) share
29
- └────────────┬─────────────┘ one process (default since 0.5.0)
27
+ │dsh-any-agent│───────────▶│ dsh --profile dsh-mcp │ sessions with the same
28
+ └─────────────┘ │ (dsh-team when team on) │ (cwd, permission) share
29
+ └────────────┬──────────────┘ one process (default since 0.5.0)
30
30
  │
31
31
  ┌────────────┴─────────────┐
32
32
  │ DSH's own tools │ files, shell, search,
@@ -12,7 +12,8 @@ upgrading DSH, and run `dsh_doctor` afterwards.
12
12
  |---|---|---|---|
13
13
  | 0.5.x | `0.1.7-rc.1` or newer | ✅ Verified | ACP and the experimental Agent Teams packages are available; team mode is opt-in via `dsh-any-agent team on` |
14
14
  | 0.5.x | `0.1.5-rc.1` | ⚠️ Partial | ACP basics work; no team packages, so team mode stays off |
15
- | 0.5.x | `0.2.x` | ⚠️ Unverified | The ACP `session/*` surface may change; team packages must match the DSH version. Run `dsh_doctor` and the offline suite before upgrading |
15
+ | 0.5.x | `0.2.1-rc.5` – `0.2.2-rc.1` | ✅ Verified | Full suites run on 2026-11-06 (0.2.1-rc.5 from npm, 0.2.2-rc.1 on the desktop app's bundled runtime); from 0.2.2 DSH requires Node ≥ 24.9 |
16
+ | 0.5.x | `0.2.3` or newer | ⚠️ Unverified | The ACP `session/*` surface may change; team packages must match the DSH version. Run `dsh_doctor` and the offline suite before upgrading |
16
17
  | 0.4.x | `0.1.7-rc.1` | ✅ Verified | Team mode was on by default at that time |
17
18
  | 0.4.x | `0.1.5-rc.1` | ⚠️ Partial | No team packages |
18
19
 
@@ -22,21 +23,41 @@ upgrading DSH, and run `dsh_doctor` afterwards.
22
23
 
23
24
  ## Runtime requirements
24
25
 
25
- - **Node.js ≥ 22.15** (uses `zstdDecompressSync` from `node:zlib` to read DSH session headers);
26
+ - **Node.js ≥ 22.15** for this server (uses `zstdDecompressSync` from `node:zlib` to read DSH session headers);
26
27
  - DSH installed and `~/.dsh` already has a working provider and credentials (the GUI can chat);
28
+ - **DSH's own Node requirement** (`engines.node`, ≥ 24.9 from 0.2.2) is met by whatever runtime launches DSH:
29
+ the desktop app's bundled runtime when DSH comes from the desktop app (no local Node 24 needed), or this server's node for npm/npx installs;
27
30
  - Team mode also requires DSH `>= 0.1.7-0` and a profile that actually mounts both team packages.
28
31
 
32
+ ## Where DSH comes from (auto-detected, no configuration)
33
+
34
+ The server finds DSH on its own, in this order — **no need to set `DSH_BIN`**:
35
+
36
+ 1. **DeepSeek Harness desktop app**: on Windows the install directory comes from the uninstall
37
+ registry (HKCU/HKLM); default install locations are checked on every platform. The CLI lives
38
+ inside `resources/app.asar`, so the server runs it with the app's bundled Electron in Node mode
39
+ (`ELECTRON_RUN_AS_NODE=1`) — the same DSH and the same runtime the GUI uses;
40
+ 2. **npm global install** (`npm i -g @deepseek-ai/dsh`);
41
+ 3. **npx cache** (can vanish at any time; lowest priority when otherwise equal).
42
+
43
+ When several exist: candidates whose runtime satisfies `engines.node` first → higher version →
44
+ desktop > global > npx. `DSH_BIN` (may point inside an `.asar`) and `DSH_RUNTIME` (the executable
45
+ that launches DSH) are explicit overrides only.
46
+
29
47
  ## How to check
30
48
 
31
49
  ```bash
32
50
  dsh-any-agent --version # this server
33
- node <DSH_BIN> --version # DSH version (set DSH_BIN to pin it)
34
51
  dsh-any-agent team status # team switch and profile
35
52
  ```
36
53
 
37
54
  In an MCP client, call `dsh_doctor` and look at:
38
55
 
39
- - `dsh_bin`: DSH entry point and version;
56
+ - `dsh_bin`: DSH entry point, version, source (desktop / npm global / npx cache / DSH_BIN) and runtime; **warn** when it resolves from an npx cache (`_npx/<hash>/`), which disappears on cache cleanup;
57
+ - `dsh_runtime`: whether the launching runtime's Node satisfies DSH's `engines.node`; **warn** if not;
58
+ - `dsh_desktop`: the desktop app's version; **warn** when it differs from the DSH this server uses (both share the `~/.dsh` session store);
59
+ - `dsh_candidates`: every DSH found and which one was picked;
60
+ - `dsh_version`: the version graded against the table above — `verified` is ok; `partial` / `unsupported` / `unverified` (e.g. 0.2.3+) / unreadable are **warn**;
40
61
  - `profile`: exists, drift, route source;
41
62
  - `team`: `on` / `off` / `requested but unavailable` (version or profile lacks the team package);
42
63
  - `config`: effective permission, idle reaping, shared process, and so on.
@@ -48,4 +69,5 @@ In an MCP client, call `dsh_doctor` and look at:
48
69
  3. If team mode will not turn on, check `dsh-any-agent team status` and `dsh_doctor`'s
49
70
  `team` check, then verify `~/.dsh/profiles/dsh-team/cordis.patch.yml` contains
50
71
  `@deepseek-ai/dsh-experimental-agent-team`;
51
- 4. To pin a DSH version, set `DSH_BIN` explicitly instead of letting the npx cache pick one.
72
+ 4. By default the server follows the desktop app: after the app updates, the next DSH launch picks
73
+ up the new version (discovery is cached for 60 s). Set `DSH_BIN` only to pin a specific version.
@@ -4,7 +4,8 @@
4
4
 
5
5
  | Variable | Default | Purpose |
6
6
  |---|---|---|
7
- | `DSH_BIN` | auto-detected | Path to `@deepseek-ai/dsh/lib/bin.js` |
7
+ | `DSH_BIN` | auto-detected (desktop app → npm global → npx cache) | Explicit path to `@deepseek-ai/dsh/lib/bin.js` (may point inside the desktop app's `app.asar`) |
8
+ | `DSH_RUNTIME` | auto | Explicit executable that launches DSH (e.g. a Node 24). Used with `DSH_BIN`, or on its own for auto-discovered npm global / npx installs (their `engines.node` is re-checked against it); the desktop app always keeps its bundled runtime (only it can read the asar). Unset: the desktop app's bundled runtime, otherwise this server's node |
8
9
  | `DSH_HOME` | `~/.dsh` | DSH home shared with the GUI (credentials, sessions, workspace registry). Point it elsewhere only for isolation/multi-account |
9
10
  | `DSH_MCP_PROFILE` | `dsh-mcp` (`dsh-team` when team is on) | DSH profile to drive; an explicit value wins |
10
11
  | `DSH_MCP_TEAM` | `0` (off) | Team mode, opt-in. `dsh-any-agent team on` writes the settings file; this env var overrides it. When on, uses the `dsh-team` profile and injects a "form a team when needed" line before `dsh_send`; per call use `dsh_send(team=false)` |
@@ -13,6 +13,10 @@ npm version patch --no-git-tag-version
13
13
  node test/run.mjs
14
14
  node test/package-check.mjs
15
15
  npm run check:release # version / CHANGELOG / doc links / package contents agree
16
+ npm run docs:counts:check # check counts in tests.md and the CI comment = the report node test/run.mjs just wrote
17
+ # A mismatch means tests changed without the numbers being refreshed:
18
+ # node test/run.mjs --all --report .state/test-report.json && npm run docs:counts
19
+ # (model-suite numbers only move with a report made using --all; see "After changing tests" in docs/en/tests.md)
16
20
 
17
21
  # 3. Verify the tarball itself: builds, installs, starts.
18
22
  npm pack --dry-run
@@ -10,6 +10,8 @@
10
10
 
11
11
  `on_approval` can be `auto-allow`, `auto-deny`, or `ask` (which exposes a pending approval for `dsh_approval_decide` to adjudicate).
12
12
 
13
+ **Under `auto-allow`, `workspace-write` is a soft boundary.** When DSH's sandbox blocks something (e.g. child-process pipes on Windows), DSH asks for `sandbox_permissions: danger-full-access` and `auto-allow` grants it. The default is unchanged, but the grant is **no longer silent**: every turn result carries `auto_approvals_total`, `auto_escalations_total` and up to 20 redacted `auto_approvals` summaries (tool, escalation tier, justification, command preview), and the review ticket's evidence and checklist call out "N auto-approved this turn, M of them escalations". To gate each one, start a conversation with `on_approval=ask`.
14
+
13
15
  **`read-only` is enforced, not merely a default**: it always runs with `auto-deny`; passing `auto-allow` or `ask` for a read-only conversation is rejected at `dsh_start`, and the approval layer refuses any permission request from a read-only conversation. After create/resume the server reads back the session's recorded tier; for read-only it **fails closed** if the projection cache has no permission record within the DSH flush window, rather than relying on the approval layer alone. Without that, a profile `defaultPreset` plus a permissive approval policy could let a write through (found by the adversarial audit).
14
16
 
15
17
  ## 🧷 Input boundaries