dsh-any-agent 0.4.0 โ†’ 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,38 +15,12 @@
15
15
 
16
16
  </div>
17
17
 
18
- ๐Ÿงช 843 checks green on 0.3.9 on this machine (879 when all nine client fixtures exist); 524 of them are in the client-registration suites (560 with all fixtures) ยท ๐Ÿ”Œ MCP over stdio ยท ๐Ÿ“ฆ Zero dependencies ยท ๐Ÿ“œ MIT
18
+ ๐Ÿงช 0.4.0 offline suite green (memory controls, team mode and the dispatch surface are all covered) ยท ๐Ÿ”Œ 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
 
22
22
  **Claude Code ยท Claude Desktop ยท Codex ยท Cursor ยท Windsurf ยท Zed ยท Gemini CLI ยท opencode ยท VS Code** โ€” or any client that speaks MCP over stdio.
23
23
 
24
- ## Quick start
25
-
26
- Requires **Node.js โ‰ฅ 22.15** and **DSH installed with a working model route and credentials**. See [Requirements](#-requirements) for details.
27
-
28
- ```bash
29
- npm install -g dsh-any-agent
30
- dsh-any-agent-setup
31
- dsh-any-agent-setup --yes
32
- ```
33
-
34
- The second command previews the changes; `--yes` applies them. Reconnect your client's MCP server, then ask your agent to start a DSH conversation in your project, dispatch a task, and collect the result. The [dispatch workflow](#-workflow-dispatch-then-collect-later) shows the tool sequence.
35
-
36
- > **Before your first task:** read the [Usage Notes](USAGE-NOTES.en.md), especially the model-config and external-MCP permission boundaries. External MCP tools bypass DSH's sandbox. For a fresh machine, follow the [migration guide](USAGE-MIGRATION.en.md).
37
-
38
- <details>
39
- <summary>Full contents</summary>
40
-
41
- - **Quick start** โ€” [Install and connect](#quick-start) ยท [Changelog](CHANGELOG.md)
42
- - **Start here** โ€” [The problem](#-the-problem-this-solves) ยท [Why it's different](#-why-its-different) ยท [Why not just wrap the DSH CLI](#-why-not-just-wrap-the-dsh-cli) ยท [Requirements](#-requirements) ยท [Setup](#-setup)
43
- - **Using it** โ€” [Tools](#-tools) ยท [Dispatch then collect](#-workflow-dispatch-then-collect-later) ยท [Completion notifications](#-workflow-get-notified-on-completion-no-polling) ยท [Many conversations](#-workflow-many-conversations-at-once)
44
- - **Behavior** โ€” [Permissions](#-permissions) ยท [Reasoning](#-reasoning-hidden-by-default) ยท [Logging and privacy](#-logging-and-privacy) ยท [Workspace & GUI](#-workspace-visibility-in-the-dsh-gui)
45
- - **Hard parts** โ€” [The write lock](#-the-write-lock-one-writer-per-conversation-at-a-time)
46
- - **Reference** โ€” [Environment](#-environment-variables) ยท [Tests](#-tests) ยท [Known limitations](#-known-limitations) ยท [Why the old profile name?](#-why-is-the-profile-still-called-dsh-mcp)
47
-
48
- </details>
49
-
50
24
  ## ๐ŸŽฏ The problem this solves
51
25
 
52
26
  You already have a coding agent in your editor. What you do **not** have is one that can:
@@ -94,13 +68,13 @@ does not need to know DSH's tool list. The DSH agent picks what it needs โ€” inc
94
68
  capabilities added after this server was built (for example the experimental Agent
95
69
  Teams). This server does not grow one MCP tool per DSH feature.
96
70
 
97
- **Team mode (on by default since 0.4.0)**: the default profile is `dsh-team`
98
- (auto-created on first start with the experimental Agent Teams packages), and
99
- `dsh_send` injects a short "form a team if this needs parallel or multi-role work"
100
- line before the caller's prompt; simple tasks are not forced into a team. Disable it
101
- globally with `DSH_MCP_TEAM=0` (back to `dsh-mcp`, no injection) or per call with
102
- `dsh_send(team=false)`. The team profile disables the legacy `subagent` tools and
103
- replaces them with nine team tools; teammates share one workspace.
71
+ **Team mode (opt-in since 0.5.0)**: off by default. Run `dsh-any-agent team on`
72
+ and reconnect; the profile switches to `dsh-team` (the experimental Agent Teams
73
+ packages), and `dsh_send` then adds a short "form a team if this needs parallel or
74
+ multi-role work" line; simple tasks are not forced into a team. Turn it off with
75
+ `dsh-any-agent team off`, or for one call with `dsh_send(team=false)`. The team
76
+ profile disables the legacy `subagent` tools and replaces them with nine team
77
+ tools; teammates share one workspace.
104
78
 
105
79
  **Only dispatch semantics become tools**: start, send, read, get, interject,
106
80
  interrupt, approve, queue, inbox, status, release, takeover, doctor. No file I/O, no
@@ -111,324 +85,32 @@ shell, no editor, no task board.
111
85
  outer worktree isolation. For collaboration, prefer letting DSH run the team itself
112
86
  (mount the experimental team packages into the profile; teammates share one workspace).
113
87
 
114
- ## ๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘ Team mode (on by default since 0.4.0)
115
-
116
- The default profile is `dsh-team`: created on first start, it mounts the
117
- experimental Agent Teams packages and disables the legacy `subagent` tools.
118
- `dsh_send` injects a short "form a team when this needs parallel or multi-role
119
- work" line before the caller's prompt; simple tasks are not forced into a team.
120
-
121
- | What you want | How |
122
- |---|---|
123
- | Keep the default (on) | Nothing to set |
124
- | **Turn it off globally** | `DSH_MCP_TEAM=0` (back to the `dsh-mcp` profile, no team prompt) |
125
- | **Turn it off for one call** | `dsh_send(..., team=false)` |
126
- | Use a custom profile | `DSH_MCP_PROFILE=<name>`; the server checks whether that profile actually contains the team package and suppresses injection if it does not |
127
- | Check the current state | The `dsh_start` reply, or `dsh_doctor`'s `config` check (`team=on/off`) |
128
-
129
- Notes:
130
-
131
- - Team mode requires DSH `>= 0.1.7-0`; an older runtime falls back to `dsh-mcp`.
132
- - The team profile disables the legacy `subagent` tools and replaces them with
133
- nine team tools; teammates share one workspace.
134
- - Agent Teams is **experimental** and carries no stability promise.
135
-
136
- ## โšก Why not just wrap the DSH CLI?
137
-
138
- DSH ships two programmable entry points. The `sdk` profile looks simpler, but its JSON-RPC surface has only **3 requests and 4 notifications** โ€” and it lacks three things that make an agent unusable in practice. The `acp` profile (Agent Client Protocol) has all of them. Everything below was verified against DSH `0.2.1-rc.5`.
139
-
140
- | Capability | `sdk` profile | `acp` profile (**this project**) |
141
- |---|---|---|
142
- | **Resume a conversation** | โŒ no resume method; reusing an id fails with `already exists` | โœ… `session/resume` โ€” verified **across processes** |
143
- | **Interrupt a running turn** | โŒ only by killing the process | โœ… `session/cancel` โ€” the same path as the user's own stop button |
144
- | **Answer approval prompts** | โŒ no channel; operations silently fail closed | โœ… `session/request_permission` round-trip |
145
- | List / close sessions | โŒ none | โœ… `session/list`, `session/close` |
146
- | Gray vs. black streaming | one message, split by a `type` field | โœ… two separate channels: `agent_thought_chunk` / `agent_message_chunk` |
147
- | Per-session MCP servers | โŒ process-wide | โœ… `mcpServers` accepted by `session/new` and `session/resume` |
148
-
149
- **Core idea: the process is a cache, not the identity.** A conversation's identity is DSH's `sessionId`, persisted under `~/.dsh/sessions`. If the process is reaped, crashes, or the whole MCP server restarts, the next call transparently `session/resume`s it โ€” history intact.
150
-
151
- <details>
152
- <summary>How the clients, server and DSH connect</summary>
153
-
154
- ```
155
- any MCP client โ† Claude Code, Codex, Cursor, Zed, Cline, your ownโ€ฆ
156
- (Claude/Codex/Cursor/โ€ฆ) โ”‚
157
- โ”‚ MCP over stdio โ”‚ newline-delimited JSON-RPC (ACP)
158
- โ–ผ โ–ผ
159
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
160
- โ”‚dsh-any-agentโ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ dsh --profile dsh-mcp โ”‚ one long-lived process
161
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ per conversation
162
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
163
- โ”‚
164
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
165
- โ”‚ DSH's own tools โ”‚ files, shell, search,
166
- โ”‚ โ”‚ skills, subagents, workflows
167
- โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
168
- โ”‚ dsh-mcp-client โ”‚ โ†’ your other MCP servers
169
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
170
- ```
171
-
172
- **MCP compatibility.** The server uses only `initialize`, `ping`, `tools/list` and `tools/call`. It has also been verified with a bare-bones client that negotiates an older protocol version and never sends `notifications/initialized`.
173
-
174
- </details>
175
-
176
- ## ๐Ÿ“ฆ Requirements
177
-
178
- - **Node.js โ‰ฅ 22.15** โ€” the server uses `zstdDecompressSync` from `node:zlib` to read DSH session headers, and that API does not exist before 22.15
179
- - **DSH installed** (`@deepseek-ai/dsh`), with working credentials under `~/.dsh`
180
- - A working provider/model route in your local DSH configuration. The server prepares its `dsh-mcp` ACP profile automatically from that route; no provider config or credentials are bundled.
181
-
182
- ## ๐Ÿš€ Setup
183
-
184
- ### 1. Prepare the DSH profile
185
-
186
- On first launch, the server prepares a `dsh-mcp` profile from the shipped ACP template and copies the model route from your existing DSH configuration. Credentials stay with DSH. Use `DSH_MCP_ROUTE_SOURCE` to choose a source profile, or `DSH_MCP_BOOTSTRAP=0` to manage the profile yourself. See [Environment variables](#-environment-variables).
187
-
188
- <details>
189
- <summary>Manual profile setup and ACP configuration details</summary>
190
-
191
- ```bash
192
- dsh --profile dsh-mcp --from-default-profile acp
193
- ```
194
-
195
- Then edit `~/.dsh/profiles/dsh-mcp/cordis.patch.yml`, remove its `# dsh-mcp:managed` marker if present, and declare your provider and model. Start from [`profile-example/cordis.patch.yml`](profile-example/cordis.patch.yml).
196
-
197
- > โš ๏ธ **Two traps here** (both learned the hard way):
198
- >
199
- > 1. **ACP's model does NOT come from `agent-default-model`.** It comes from the `dsh-acp` plugin's own `config.provider` / `config.model`, which the `acp` bundle hard-codes to `deepseek-official`. If you don't override the `acp` row, your sessions silently run on the wrong provider and fail with `no API key for provider route "deepseek-official"`.
200
- > 2. **ACP's `reasoning_effort` defaults to empty** (= "Provider default"), not to the maximum. This server explicitly sets it to `max` on every `session/new` and after every `session/resume` โ€” because a fresh process does not remember the previous choice.
201
-
202
- </details>
203
-
204
- ### 2. Register the server
205
-
206
- Run the three commands in [Quick start](#quick-start), then reconnect your client. For a manual configuration, expand the client examples below.
207
-
208
- `setup` finds the MCP clients on your machine โ€” **Claude Code, Claude Desktop, Codex, Cursor, Windsurf, Zed, Gemini CLI, opencode, VS Code** โ€” and writes the entry into each one it finds, skipping the rest. It is safe by construction:
209
-
210
- - **dry-run by default**; `--yes` is required to write anything, and a preview starts **no subprocess at all** โ€” it only reads the files it would touch
211
- - every file is backed up to `<file>.bak-dsh-<timestamp>` first, holding the exact **pre-write bytes**
212
- - the config is parsed before and after writing, so a broken config never lands; malformed JSON and a read-only file are refused rather than forced, while a missing directory is created safely
213
- - **text-level writes**: only the bytes of setup's own entry are replaced, so indentation (2-space, 4-space, tabs), key order, CRLF endings, a UTF-8 BOM and a missing trailing newline all survive untouched
214
- - **idempotent** โ€” re-running only changes what differs, and a second `--yes` writes nothing and adds no `.bak`
88
+ ## Quick start
215
89
 
216
- **Measured impact scope (2026-11-02, plan mode on this machine):** 5 of the 9 clients were detected (Claude Code, Codex, Cursor, Zed, VS Code) and 4 were skipped; `--json` reported `mode=plan, changed=5, skipped=4, failed=0`. The SHA-256, size and mtime of every existing client config were unchanged. A real `--yes` is additive: only the single `dsh-any-agent` entry is written, every other byte is preserved, and a `.bak-dsh-<timestamp>` copy is taken first; `--remove` deletes only that entry.
90
+ Requires **Node.js โ‰ฅ 22.15** and **DSH installed with a working model route and credentials**. See [Requirements](#-requirements) for details.
217
91
 
218
92
  ```bash
219
- dsh-any-agent-setup --yes --only cursor,zed # just these
220
- dsh-any-agent-setup --yes --permission read-only
221
- dsh-any-agent-setup --remove --yes # take it back out; only its own entry is removed
222
- ```
223
-
224
- <details>
225
- <summary>Advanced flags and client configuration paths</summary>
226
-
227
- **Three new flags**
228
-
229
- | Flag | What it does |
230
- |---|---|
231
- | `--json` | **stdout carries one parseable JSON object and nothing else** โ€” every human-readable line goes to stderr. Shape: `{ ok, mode, clients: [{ id, label, path, status, summary, error? }], changed, skipped, failed }` with `mode` being `plan`, `apply` or `remove`. This is a contract, not a convenience: a script can pipe stdout straight into a JSON parser |
232
- | `--home <dir>` | use this directory instead of the real `HOME` / `USERPROFILE` / `APPDATA`, so a register-then-remove cycle can be rehearsed without touching a live config |
233
- | `--force` | only two things: (a) create a directory or file that does not exist yet (it does that by default too โ€” the flag makes it reliable for automation, including when the client itself was not detected); (b) overwrite a same-named entry that exists but is not the shape this program expects. It never deletes somebody else's entry and never skips validation |
234
-
235
- **Which file each client actually gets**
236
-
237
- A `.bak-dsh-<timestamp>` backup lands beside every file before it is written. Paths are generic examples; "its own entry" means the single entry/section named `dsh-any-agent`.
238
-
239
- | Client (`--only` id) | Config file | Container key | Install / update | `--remove` | Undo |
240
- |---|---|---|---|---|---|
241
- | Claude Code (`claude-code`) | `~/.claude.json` | `mcpServers` | its own entry only, with `type: "stdio"` | deletes its own entry only | copy the backup back |
242
- | Claude Desktop (`claude-desktop`) | Windows `%APPDATA%\Claude\claude_desktop_config.json`; macOS `~/Library/Application Support/Claude/claude_desktop_config.json`; Linux `~/.config/Claude/claude_desktop_config.json` | `mcpServers` | adds or rewrites its own entry only | deletes its own entry only | copy the backup back |
243
- | Codex (`codex`) | `~/.codex/config.toml` | `[mcp_servers.dsh-any-agent]` | appends its two TOML sections line-wise | deletes those two sections and nothing else | copy the backup back |
244
- | Cursor (`cursor`) | `~/.cursor/mcp.json` | `mcpServers` | its own entry only | its own entry only | copy the backup back |
245
- | Windsurf (`windsurf`) | `~/.codeium/windsurf/mcp_config.json` | `mcpServers` | its own entry only | its own entry only | copy the backup back |
246
- | Zed (`zed`) | Windows `%APPDATA%\Zed\settings.json`; elsewhere `~/.config/zed/settings.json` | `context_servers` | its own entry only | its own entry only | copy the backup back |
247
- | Gemini CLI (`gemini`) | `~/.gemini/settings.json` | `mcpServers` | its own entry only | its own entry only | copy the backup back |
248
- | opencode (`opencode`) | `~/.config/opencode/opencode.json` | `mcp` | its own entry only, written in opencode's shape: `{ type: "local", command: [exe, ...args], enabled: true }` โ€” one array, not separate `command`/`args` | its own entry only | copy the backup back |
249
- | VS Code (`vscode`) | Windows `%APPDATA%\Code\User\mcp.json`; elsewhere the same `mcp.json` under your VS Code user directory | `servers` โ€” not `mcpServers` | its own entry only | its own entry only | copy the backup back |
250
-
251
- For the exact path on your machine, read the `path` field for each client in `--json` output โ€” that is the file this run would write, on your OS, which beats any table in a README.
252
-
253
- Three shapes worth spelling out, because getting them wrong is silent. **VS Code** stores MCP servers under `servers`, not `mcpServers`; writing the usual key there produces a config the editor ignores. **opencode** takes a single `command` array (`{ type: "local", command: [exe, ...args], enabled: true }`) rather than separate `command`/`args` fields. **Claude Code** is the reason this release does not use a JSON library at all: `~/.claude.json` really does hold a `projects` map with keys that differ only by letter case (`.../Rust` next to `.../rust`), so a naive `JSON.parse` fails on it and any whole-file rewrite would destroy the user's project list. A byte-range scanner that never builds a value object handles it, and an earlier design that shelled out to `claude mcp add` was dropped for the same reason โ€” that command rewrites the file in its own formatting with no backup you could restore.
254
-
255
- </details>
256
-
257
- <details>
258
- <summary>Registration checks and round-trip verification</summary>
259
-
260
- **What `setup` will and will not touch**
261
-
262
- It is additive, and it only ever touches its own entry. Verified against a real machine holding existing MCP servers (Cursor with four, Codex with two, Claude Code with five, Zed with 33 settings keys): every pre-existing entry survived **byte for byte**, key order included, and the only diff was one new entry per client.
263
-
264
- ```
265
- Cursor 4 -> 5 entries existing entries byte-identical
266
- Codex 2 -> 3 sections node_repl and rea bodies unchanged
267
- Claude Code 5 -> 6 servers all four originals still listed
268
- Zed 33 -> 33 keys only context_servers gained a child
269
- ```
270
-
271
- What changed since 0.1.x: JSON configs used to be **re-serialised whole** (values preserved, layout lost), which is why the docs said whitespace was not guaranteed. Writes are text-level now โ€” apart from its own entry, the file comes back byte-identical, whether it was indented with 4 spaces, 2 spaces or tabs, laid out as one long line, missing its trailing newline, using CRLF, or carrying a UTF-8 BOM.
272
-
273
- Codex's `config.toml` gets the same treatment line-wise: your other `[tables]` and their comments, the top-level keys (`model`, `approval_policy`, `sandbox_mode`, โ€ฆ) and CRLF endings come back exactly as they were โ€” `--remove` deletes only the two `[mcp_servers.dsh-any-agent]` sections and leaves the rest intact.
274
-
275
- The coverage for each of those claims is in [`test/`](test/) (`test/config-safety.mjs` asserts them one by one, 407 checks).
276
-
277
- **Checked against a real home, read-only.** Running `setup --json` (plan mode) against an actual developer profile reported all nine clients and left every existing config untouched โ€” identical size, mtime and SHA-256, no `.bak-dsh-*` created, no new file created:
278
-
279
- ```
280
- ~/.claude.json 182693 B unchanged
281
- ~/.cursor/mcp.json 1396 B unchanged
282
- ~/.codex/config.toml 6647 B unchanged
283
- ~/.gemini/settings.json 465 B unchanged
284
- ~/.config/opencode/opencode.json 441 B unchanged
285
- %APPDATA%/Zed/settings.json 5647 B unchanged
286
- %APPDATA%/Claude/claude_desktop_config.json 247 B unchanged
287
- ```
288
-
289
- The same run with `--home <tmp>` put **every** reported path inside that directory, `APPDATA` included โ€” which is what makes the register/remove cycle safe to rehearse.
290
-
291
- **A full register -> remove round trip on the real home, not just a preview.** All seven configs were backed up, `--yes` was run for real, then `--remove --yes`, and everything was verified byte by byte:
292
-
293
- | Client | Before | After register | After remove | Other MCP servers |
294
- |---|---|---|---|---|
295
- | ~/.claude.json | 182693 B | 182693 B (already current) | byte-identical to baseline | untouched; 67 top-level keys, 60 projects entries and both case-only duplicate keys still there |
296
- | ~/.cursor/mcp.json | 1396 B | 1389 B | 1009 B | stitch / obsidian-mcp-server / codegraph / rea identical field by field |
297
- | ~/.codex/config.toml | 6647 B | 6647 B (already current) | 6239 B | every other table, comment and top-level key kept |
298
- | %APPDATA%/Zed/settings.json | 5647 B | 5647 B (already current) | 5240 B | all 32 top-level settings keys kept |
299
- | ~/.gemini/settings.json | 465 B | 845 B | 465 B | restored byte for byte |
300
- | ~/.config/opencode/opencode.json | 441 B | 869 B | 441 B | `$schema` and the pencil entry kept |
301
- | %APPDATA%/Claude/claude_desktop_config.json | 247 B | 627 B | 247 B | restored byte for byte |
302
-
303
- Removal is not accepted because it *looks* right: the check is that **taking our one entry out of the backup with the same editing engine reproduces the file now on disk, byte for byte**. All seven files pass. That proves removal is the strict inverse of registration and cannot quietly take anything else with it.
304
-
305
- Afterwards every JSON config still parses and `codex mcp list` still reads the file; no client needs hand repair.
306
-
307
- </details>
308
-
309
- **`cwd` is optional: it defaults to the project the client has open**
310
-
311
- The client already knows which folder the user is working in (MCP roots). `dsh_start` without `cwd` now sends one `roots/list` request and converts the returned `file://` URI into a path, so "DSH works where the client is looking" is the default - which also removes the usual way to end up on the home directory by building the path by hand. A client that does not advertise roots, or does not answer in time, gets an actionable error instead of a guessed directory.
312
-
313
- **Do not use the home directory as `cwd`**
314
-
315
- DSH's own ACL sandbox requires its temp directory to be **outside** the workspace. On Windows `%TEMP%` is `C:/Users/<you>/AppData/Local/Temp`, inside the home directory, so `cwd` set to home makes the first shell command fail with `Windows ACL: temp root must be outside the workspace`. `dsh_start` now rejects that combination and names both escapes: a narrower project directory, or `permission=read-only` / `danger-full-access`.
316
-
317
- **Backups and rollback**
318
-
319
- Both registering and removing snapshot the file first: `<config>.bak-dsh-<timestamp>`, holding the exact pre-operation bytes. Both directions, because the state you want back differs โ€” the pre-register backup undoes a bad install, the pre-remove backup reinstalls after a bad removal.
320
-
321
- After every real write, each client entry in `--json` carries a `backup` field, and the human-readable output prints `backup: <path>` under each written entry.
322
-
323
- The copies live next to each client's config, so picking the right one by hand is error-prone. There is a companion command:
324
-
325
- | What | Command |
326
- |---|---|
327
- | List backups, newest first | `dsh-any-agent-restore --list` |
328
- | Restore the newest backup per file | `dsh-any-agent-restore --latest` |
329
- | Restore one batch | `dsh-any-agent-restore --stamp 2026-10-30T01-35` |
330
- | Restore one specific copy | `dsh-any-agent-restore --file <backup path>` |
331
- | See what would happen | add `--dry-run` |
332
-
333
- A restore is itself destructive, so it copies the **current** file aside as `.bak-dsh-pre-restore-<timestamp>` before overwriting โ€” a restore can be undone.
334
-
335
- **Install / update / remove**
336
-
337
- | Task | Command |
338
- |---|---|
339
- | Preview what would change | `dsh-any-agent-setup` |
340
- | Register with every client found | `dsh-any-agent-setup --yes` |
341
- | Register with just some | `dsh-any-agent-setup --yes --only cursor,zed` |
342
- | Change the permission tier in place | `dsh-any-agent-setup --yes --permission read-only` |
343
- | Update after upgrading the package | `dsh-any-agent-setup --yes` (idempotent; only differing entries are rewritten) |
344
- | Unregister from every client | `dsh-any-agent-setup --remove --yes` |
345
- | Unregister from just some | `dsh-any-agent-setup --remove --yes --only codex` |
346
- | Undo one bad write | copy `.bak-dsh-<stamp>` back over the config |
347
-
348
- <details>
349
- <summary>What removal changes: verification details</summary>
350
-
351
- **What `--remove` actually does โ€” measured**
352
-
353
- Unregistering touches exactly one key per client and leaves everything else alone. Measured on a real machine, before โ†’ after:
354
-
355
- ```
356
- Cursor { keep, dsh-any-agent } -> { keep }
357
- the hand-written entry survived untouched
358
- Claude Code 6 servers -> 5 servers
359
- all five originals still listed
360
- Zed 33 top-level keys -> 33 keys
361
- only context_servers lost its child, now empty
362
- Codex our two TOML blocks -> removed
363
- model, approval_policy, sandbox_mode, node_repl and rea all intact
93
+ npm install -g dsh-any-agent
94
+ dsh-any-agent-setup
95
+ dsh-any-agent-setup --yes
364
96
  ```
365
97
 
366
- Nothing outside those entries is read, rewritten or deleted. Your credentials, other MCP servers and unrelated settings are not touched. The package itself is *not* uninstalled by `--remove` โ€” that is `npm uninstall -g dsh-any-agent`, and the two are independent: you can keep the package installed with no clients registered, or unregister without removing it.
367
-
368
- To verify by hand afterwards: `claude mcp list`, `codex mcp list`, and inspect `~/.cursor/mcp.json` / the `context_servers` key of Zed's settings.
369
-
370
- </details>
371
-
372
- **The recorded path is absolute**
98
+ The second command previews the changes; `--yes` applies them. Reconnect your client's MCP server, then ask your agent to start a DSH conversation in your project, dispatch a task, and collect the result. The [dispatch workflow](#-workflow-dispatch-then-collect-later) shows the tool sequence.
373
99
 
374
- `setup` writes the absolute path of the installation it ran from. **If you move or delete that directory, every client will fail with `CONNECTION_CLOSED`** until you either re-run `setup` from the new location or `--remove` the stale entries. This bites when you install into a temporary folder to try things out, then clean it up: the configs still point into the void.
100
+ > **Before your first task:** read the [Usage Notes](USAGE-NOTES.en.md), especially the model-config and external-MCP permission boundaries. External MCP tools bypass DSH's sandbox. For a fresh machine, follow the [migration guide](USAGE-MIGRATION.en.md).
375
101
 
376
102
  <details>
377
- <summary>Manual client configuration: Claude Code, Codex and generic MCP</summary>
378
-
379
- **From a local checkout**
380
-
381
- Clone or unzip anywhere, then point `command` at `node` and `args` at `<checkout>/bin/dsh-any-agent.mjs` (shown below).
382
-
383
- ---
384
-
385
- **Claude Code**
386
-
387
- ```bash
388
- # global install
389
- claude mcp add dsh-any-agent -- dsh-any-agent
390
-
391
- # local checkout
392
- claude mcp add dsh-any-agent -- node /absolute/path/to/server/bin/dsh-any-agent.mjs
393
- ```
394
-
395
- **Codex** (`~/.codex/config.toml`)
396
-
397
- ```toml
398
- [mcp_servers.dsh-any-agent]
399
- command = "dsh-any-agent" # or "node" + path to bin/dsh-any-agent.mjs
400
- args = []
401
- startup_timeout_sec = 20.0
402
- tool_timeout_sec = 120.0
403
- ```
404
-
405
- **Generic MCP client** โ€” anything that speaks MCP over stdio:
406
-
407
- ```json
408
- {
409
- "mcpServers": {
410
- "dsh-any-agent": {
411
- "command": "dsh-any-agent",
412
- "args": []
413
- }
414
- }
415
- }
416
- ```
417
-
418
- For a local checkout, use `"command": "node"` and `"args": ["/absolute/path/to/server/bin/dsh-any-agent.mjs"]`.
103
+ <summary>Full contents</summary>
419
104
 
420
- > Cursor, Windsurf, Cline, Continue and most other clients use this same `mcpServers` shape โ€” if it can launch a stdio MCP server, it works. Zed is the exception: it uses `context_servers` (shown above).
105
+ - **Quick start** โ€” [Install and connect](#quick-start) ยท [Changelog](CHANGELOG.md)
106
+ - **Start here** โ€” [The problem](#-the-problem-this-solves) ยท [Why it's different](#-why-its-different) ยท [Why not just wrap the DSH CLI](#-why-not-just-wrap-the-dsh-cli) ยท [Requirements](#-requirements) ยท [Setup](#-setup)
107
+ - **Using it** โ€” [Tools](#-tools) ยท [Dispatch then collect](#-workflow-dispatch-then-collect-later) ยท [Completion notifications](#-workflow-get-notified-on-completion-no-polling) ยท [Many conversations](#-workflow-many-conversations-at-once)
108
+ - **Behavior** โ€” [Permissions](#-permissions) ยท [Reasoning](#-reasoning-hidden-by-default) ยท [Logging and privacy](#-logging-and-privacy) ยท [Workspace & GUI](#-workspace-visibility-in-the-dsh-gui)
109
+ - **Hard parts** โ€” [The write lock](#-the-write-lock-one-writer-per-conversation-at-a-time)
110
+ - **Reference** โ€” [Environment](#-environment-variables) ยท [Tests](#-tests) ยท [Known limitations](#-known-limitations) ยท [Why the old profile name?](#-why-is-the-profile-still-called-dsh-mcp)
421
111
 
422
112
  </details>
423
113
 
424
- ### 3. Try it
425
-
426
- ```bash
427
- dsh-any-agent --version # prints the version, starts nothing
428
- dsh-any-agent --help # flags, key env vars, docs link
429
- node test/smoke.mjs # no LLM calls, verifies the whole plumbing
430
- ```
431
-
432
114
  ## ๐Ÿงฐ Tools
433
115
 
434
116
  | Tool | What it does |
@@ -448,342 +130,31 @@ node test/smoke.mjs # no LLM calls, verifies the whole plumbing
448
130
  | `dsh_queue` | Inspect/modify the session queue: `list` / `remove` / `update` / `promote` (created by `dsh_send(queue=true)`, memory only) |
449
131
  | `dsh_doctor` | Read-only health check: DSH version, profile, state/dir writability, orphan locks, sessions and RSS, effective config |
450
132
 
451
- ## ๐Ÿ“‹ Workflow: dispatch, then collect later
452
-
453
- MCP tool calls **block**, so `dsh_send` **does not block by default** (`wait=false`): you get a receipt immediately, the turn runs in the background, and the sentinel file wakes you when it finishes. The whole flow:
454
-
455
- ```text
456
- 1. dsh_send(conversation_id, prompt, wait=false)
457
- โ†’ { run_id: "run-abc123", background: true } # returns without waiting for the turn
458
-
459
- 2. โ€ฆkeep working on something else (verified: another conversation ran a full turn meanwhile)โ€ฆ
460
-
461
- 3. dsh_status(conversation_id)
462
- โ†’ state=running | elapsed=3.5s | current_tool=pwsh | out=37 chars
463
-
464
- 4. dsh_read(conversation_id, cursor) # peek mid-flight
465
- โ†’ [me] โ€ฆ | [turn] turn 1 started | [tool] pwsh [in_progress]
466
-
467
- 5. dsh_get(conversation_id, run_id) # collect the result
468
- โ†’ status=done, elapsed=20.3s, full answer
469
- ```
470
-
471
- `run_id` is the receipt. Run records are **memory-only, last 20 kept**; under many concurrent runs old ones get evicted and `dsh_get` will fail โ€” which is exactly why the completion sentinel below carries the result.
472
-
473
- ## ๐Ÿ”” Workflow: get notified on completion (no polling)
474
-
475
- Besides `run_id`, `wait=false` returns a **`sentinel_file`** path plus **`sentinel_file_posix`** (the Bash/MSYS form โ€” use it directly; hand-converting Windows paths is the easiest way to make your waiter hang until timeout). When the turn ends โ€” **whether it succeeds, fails, or is cancelled** โ€” the service **atomically writes** a small file there containing the final `status` and `result`. So you neither poll nor block: **wait for that file with a background task in your own host (Claude/Codex); the moment it appears, you're woken up.**
476
-
477
- ```bash
478
- # sent="$sentinel_file_posix" โ† copy it straight from the tool result
479
- sent='/c/Users/you/.dsh/.dsh-any-agent/runs/<conversation_id>/<run_id>.json'
480
- dl=$(( $(date +%s) + 2100 )) # 35min safety net so a dead service can't hang you forever
481
- until [ -f "$sent" ]; do
482
- [ "$(date +%s)" -ge "$dl" ] && { echo "TIMEOUT"; exit 1; }
483
- sleep 2
484
- done
485
- echo "DONE" # task exits โ†’ host wakes you โ†’ read the file to collect
486
- ```
487
-
488
- Once the file exists: **read it** for `status` and `result` (preferred โ€” immune to the in-memory window and survives restarts), or use `dsh_get(conversation_id, run_id)`.
489
-
490
- Design notes (especially under **concurrent multi-session** use):
491
-
492
- - **No cross-talk**: the path is `<state dir>/runs/<conversation_id>/<run_id>.json`, namespaced by conversation. `run_id` sequence numbers are counted per-conversation, so two conversations *can* mint the same `run_id` in the same millisecond โ€” the per-conversation directory keeps them apart. Arm one background task per run; each completes and wakes you independently.
493
- - **Latch semantics**: the file is written once and kept. Even if the run finishes *before* you arm the waiter, `[ -f ]` is immediately true โ€” you never miss it.
494
- - **Atomic write**: `tmp` + `rename`; if the file exists its contents are complete. Each run uses its own tmp, so concurrent writes never clobber.
495
- - **No server push**: the service only answers requests over MCP (it sends no notifications). "Completion notification" is entirely the sentinel file plus your background waiter.
496
- - **โ˜… No reasoning in the sentinel**: the payload carries the answer, thinking *statistics*, usage and (credential-redacted) tool inputs, but never reasoning text โ€” `result.thinking` is stripped and replaced by a boolean `thinking_omitted` (`true` when reasoning was removed, `false` when there was none), because this server's rule is that reasoning never reaches its own disk artifacts. `dsh_get(run_id)` still returns it from memory while the record lives. `DSH_MCP_SENTINEL_INCLUDE_REASONING=1` breaks that guarantee deliberately. Note that **DSH's own session log does contain reasoning** โ€” see [Logging and privacy](#-logging-and-privacy).
497
-
498
- > Cleanup: sentinels contain full answers, so the service **prunes sentinels older than 7 days at startup** (tune with `DSH_MCP_SENTINEL_TTL_MS`; `0` disables). Your waiter should still `rm` the file after consuming it.
499
-
500
- ## ๐Ÿงต Workflow: many conversations at once
501
-
502
- ```text
503
- 1. dsh_list(only_running=true)
504
- - โ€ฆ | id=448b239bโ€ฆ | ๐Ÿ”ต running (9s, tool=pwsh, 40 chars this turn) | cwd=โ€ฆ
505
- - โ€ฆ | id=33427096โ€ฆ | ๐Ÿ”ต running (8s, 34 chars this turn) | cwd=โ€ฆ
506
- 2. dsh_read(id, cursor=0) # read the first, note the cursor
507
- 3. dsh_read(id2, cursor=0) # switch to the second
508
- 4. dsh_read(id, cursor=<prev>) # switch back; entries that grew since <prev> reappear (match by seq)
509
- ```
510
-
511
- ## ๐Ÿคซ Reasoning: hidden by default
512
-
513
- DSH separates reasoning from answer text at the model layer, and ACP exposes them as **two independent streams**. This server hides reasoning by default and returns only **statistics** (characters, duration, chunk count) โ€” so the caller knows thinking happened without paying for it in context.
514
-
515
- - `reasoning=hide` (default) โ€” content dropped, stats returned
516
- - `reasoning=marker` / `summary` / `full` โ€” progressively more content (mind your context budget)
517
- - `dsh_read(include_reasoning=true)` โ€” live reasoning of the *currently running* turn only; it is never buffered and is discarded when the turn ends
518
-
519
- > โš ๏ธ This server never writes reasoning to its own logs, state file or sentinels โ€” but **DSH's own session log (`~/.dsh/sessions/**/session.v4.jsonl.zstd`) does contain the full reasoning text**, even when `reasoning=hide`. That is DSH's storage (it is what makes resume possible) and is outside this server's control; account for it when analyzing sensitive data.
520
-
521
- ## ๐Ÿ” Permissions
522
-
523
- | Tier | Approval default | Use for |
524
- |---|---|---|
525
- | `danger-full-access` (raw server default) | `auto-allow` | Zero friction; `setup` writes `workspace-write` by default |
526
- | `workspace-write` | `auto-allow` | Writes are limited to the workspace by DSH's sandbox; out-of-workspace requests are auto-allowed unless you pass `on_approval=ask` |
527
- | `read-only` | `auto-deny` | Analyzing untrusted samples (malware, unknown dumps) |
528
-
529
- `on_approval` can be `auto-allow`, `auto-deny`, or `ask` (which exposes a pending approval for `dsh_approval_decide` to adjudicate).
530
-
531
- **`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).
532
-
533
- ## ๐ŸชŸ Workspace visibility in the DSH GUI
534
-
535
- Two different things โ€” don't confuse them:
536
-
537
- | | Depends on | Status |
538
- |---|---|---|
539
- | **Workspace effectiveness** | the conversation's `cwd` | โœ… Always correct. Process working directory, file placement and sandbox write root all follow it |
540
- | **GUI grouping** | `sessionIds` in `~/.dsh/storages/workspace.json` | โš ๏ธ This server registers it, but **the running DSH server caches the registry in memory** โ€” external file edits only take effect after a DSH restart |
541
-
542
- Verified experimentally ([`test/workspace-effect.mjs`](test/workspace-effect.mjs)): two conversations in two desktop folders, given **no path at all**, each wrote its file into its own workspace, self-reported the correct working directory, and leaked nothing into the server's cwd / home / temp / the other workspace (24/24 checks).
543
-
544
- Maintenance CLI:
545
-
546
- ```bash
547
- node bin/dsh-any-agent-workspaces.mjs --list # inspect the registry
548
- node bin/dsh-any-agent-workspaces.mjs --backfill # register every session found on disk
549
- node bin/dsh-any-agent-workspaces.mjs --prune-temp # drop temp-directory entries
550
- node bin/dsh-any-agent-workspaces.mjs --prune-empty # drop empty workspaces whose path is gone
551
- node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions # delete test sessions (strict rules)
552
- ```
553
-
554
- > โš ๏ธ Before restarting DSH, **don't touch workspaces in the GUI** โ€” the server will write its in-memory state back over the file.
555
-
556
- ## ๐Ÿ•ถ๏ธ Logging and privacy
133
+ ## ๐Ÿ“š Documentation map
557
134
 
558
- Silent by default โ€” **not one byte is written**.
559
-
560
- 1. MCP's **stdout is the protocol channel**; anything mixed in corrupts the stream.
561
- 2. This server is launched by Claude/Codex, so its stderr lands in *their* logs.
562
- 3. DSH's own stderr may carry model output **including reasoning**, so it is never forwarded by default.
563
-
564
- | Level | Behavior |
565
- |---|---|
566
- | `silent` (default) | nothing at all |
567
- | `info` | this server's own lifecycle events only โ€” **never any DSH output** |
568
- | `debug` | more of our own events; still no DSH output |
569
- | `DSH_MCP_LOG_STDERR=1` | additionally forward DSH's raw stderr (**may contain reasoning**) |
570
-
571
- **This server never writes reasoning to disk** โ€” with exactly one deliberate exception:
572
-
573
- - Not in logs (silent by default). The state file and the per-conversation metadata sidecars under `<state dir>/meta/` hold metadata only (id, cwd, permissions, counters, title hint) โ€” a smoke-test assertion guards the registry.
574
- - **The async completion sentinel persists the answer, usage and tool inputs** (it must, so a caller can collect offline) but **strips `result.thinking`** and always reports `thinking_omitted` (true when reasoning was removed, false when there was none). Tool inputs are passed through the same credential redaction as approvals, but the sentinel still contains commands and file contents โ€” treat it as sensitive. A test asserts that even when `reasoning=full` is requested, no reasoning text appears in the sentinel โ€” while `dsh_get(run_id)` still returns it from memory. Set `DSH_MCP_SENTINEL_INCLUDE_REASONING=1` to break this on purpose.
575
- - Sentinels are pruned after 7 days by default (`DSH_MCP_SENTINEL_TTL_MS`; `0` keeps them forever).
576
-
577
- > โš ๏ธ **DSH writes its own session log under `~/.dsh/sessions` and that log contains the full reasoning text** (even with `reasoning=hide`). It is what makes resume possible and is outside this server's control. The absolute "reasoning never reaches disk" claim applies to *this server's* artifacts only.
578
-
579
- ## โš™๏ธ Environment variables
580
-
581
- | Variable | Default | Purpose |
582
- |---|---|---|
583
- | `DSH_BIN` | auto-detected | Path to `@deepseek-ai/dsh/lib/bin.js` |
584
- | `DSH_HOME` | `~/.dsh` | DSH home shared with the GUI (credentials, sessions, workspace registry). Point it elsewhere only for isolation/multi-account |
585
- | `DSH_MCP_PROFILE` | `dsh-team` (team on) / `dsh-mcp` (team off) | DSH profile to drive; an explicit value wins |
586
- | `DSH_MCP_TEAM` | `1` | Team mode. On by default: uses the `dsh-team` profile and injects a "form a team when needed" line before `dsh_send`. Set `0` to disable (back to `dsh-mcp`, no injection); per call use `dsh_send(team=false)` |
587
- | `DSH_MCP_BOOTSTRAP` | on | Whether this server may create/sync the DSH profile on first start. `0` makes it drive an existing profile only and never write a file |
588
- | `DSH_MCP_ROUTE_SOURCE` | unset | Pin where model routes are copied from during bootstrap: a profile name (`desktop`, `web`, โ€ฆ) or `__home__` for `$DSH_HOME/cordis.patch.yml`. Unset = try home โ†’ desktop โ†’ web โ†’ headless โ†’ acp |
589
- | `DSH_MCP_STATE` | `$DSH_HOME/.dsh-any-agent/conversations.json` | Conversation registry, survives restarts. The old `<repo>/.state` location is migrated once on startup |
590
- | `DSH_MCP_APPROVAL` | `auto-allow` (non-read-only) | Default approval policy. `ask` makes out-of-workspace requests surface as pending approvals instead of being auto-allowed |
591
- | `DSH_MCP_APPROVAL_TIMEOUT_MS` | `300000` | How long a pending `ask` approval waits before being cancelled |
592
- | `DSH_MCP_FORGOTTEN_DIR` | `$DSH_HOME/.dsh-any-agent-forgotten` | Tombstones written by `dsh_release(forget=true)` |
593
- | `DSH_MCP_RUNS_DIR` | `<state file dir>/runs` | Root for async completion sentinels (`<this>/<conversation_id>/<run_id>.json`) |
594
- | `DSH_MCP_LOCKS_DIR` | `<state file dir>/locks` | Holder registrations for the write lock. โš ๏ธ **All instances must agree on this value**, or they cannot see each other's registrations and will misread each other as an unidentifiable holder (a GUI) and refuse to preempt |
595
- | `DSH_MCP_LOCK_STALE_MS` | `90000` (3ร— `REAP_INTERVAL_MS`, floor 30 s) | How long without a heartbeat before a holder counts as lost (and becomes auto-preemptible by `dsh_takeover`); changing the reap interval changes the derived default |
596
- | `DSH_MCP_IDLE_TTL_MS` | `600000` (10 minutes) | How long a conversation may sit idle before its DSH process is reaped. **Default 10 minutes**: the memory is released and the next call resumes the conversation transparently; the cost is that the write lock goes free and the GUI may take it. Set `0` to never reap (hold the lock forever; ~300โ€“480 MB per conversation), or use `dsh_start(keep_alive=true)` for the specific sessions that must stay attached |
597
- | `DSH_MCP_MAX_LIVE` | `4` (โ‰ˆ1.2โ€“1.9 GB worst case) | โ˜… **How many conversation processes to keep alive at once** (`0` = unlimited). Going over the cap reaps the **least recently used** one (a running turn is never reaped). `keep_alive=true` sessions are exempt. To free idle conversations at once: `dsh_release(all=true)` (busy turns are skipped unless `force=true`) |
598
- | `DSH_MCP_MAX_RSS_MB` | `0` (off) | Global child-process memory budget in MB. When the measured RSS exceeds it, idle conversations are reaped least-recently-used first; `keep_alive` sessions are reaped last |
599
- | `DSH_MCP_CHILD_MAX_OLD_SPACE_MB` | `0` (unset) | Appends `--max-old-space-size` to each DSH child's `NODE_OPTIONS`. A backstop for runaway contexts; too small will OOM long conversations |
600
- | `DSH_MCP_SHARED_PROCESS` | `0` (off) | **Experimental**: sessions with the same `(cwd, permission)` share one ACP process. Large memory and cold-start win (measured: second `session/new` โ‰ˆ300 ms vs โ‰ˆ4 s standalone). Trade-off: a process crash takes down the whole group (error sentinels are written; each session resumes on the next call) |
601
- | `DSH_MCP_AUDIT_FILE` | empty (off) | Metadata audit JSONL path. Records events, conversation/run ids, decisions and counts; **never** prompts, answers, thinking or tool input. Rotates at 10 MB |
602
- | `DSH_MCP_LIST_PROBE_TTL_MS` | `10000` | Cache lifetime for `dsh_list`'s on-disk probe (the probe spawns a DSH process and takes ~1 s) |
603
- | `DSH_MCP_SENTINEL_TTL_MS` | `604800000` (7 days) | Startup pruning age for sentinels; `0` disables pruning |
604
- | `DSH_MCP_SENTINEL_INCLUDE_REASONING` | unset | `1` writes `result.thinking` into the sentinel (**breaks the never-on-disk guarantee**); answer/error/title/tool inputs are still credential-redacted |
605
- | `DSH_MCP_PERMISSION` | `danger-full-access` | Default tier for `dsh_start` |
606
- | `DSH_MCP_REASONING_EFFORT` | `max` | Default reasoning effort |
607
- | `DSH_MCP_PROMPT_TIMEOUT_MS` | `0` (no timeout) | Wait bound for **`wait=true` only**; on expiry the turn merely moves to the background |
608
- | `DSH_MCP_REGISTER_WORKSPACE` | `project` | `project` / `all` / `0` |
609
- | `DSH_MCP_GIT_STATUS` | on | `0` disables the per-turn `git status` summary. Useful for untrusted repos (git can run repo-configured fsmonitor/filters outside DSH's sandbox) |
610
- | `DSH_MCP_BACKUP_KEEP` | `20` | How many `.bak-dsh-*` copies to keep per client config (rotation is best-effort) |
611
- | `DSH_MCP_REQUEST_TIMEOUT_MS` | `60000` | Per-request timeout on the ACP channel (initialize, session/new, โ€ฆ) |
612
- | `DSH_MCP_REAP_INTERVAL_MS` | `30000` | How often the idle reaper checks for conversations to reclaim. Values below the safe floor fall back to the default |
613
- | `DSH_MCP_LOG` | `silent` | `silent` / `info` / `debug` |
614
- | `DSH_MCP_LOG_STDERR` | unset | `1` forwards DSH's raw stderr |
615
-
616
- ## ๐Ÿงช Tests
617
-
618
- ```bash
619
- npm test # offline suites; smoke skips DSH sections if no DSH
620
- npm run test:all # offline + model-backed suites (needs DSH/provider)
621
- node test/cleanup.mjs # suite teardown on its own (test-created dirs only; --dry-run to preview)
622
- ```
623
-
624
- Model-backed suites are not part of the hosted CI matrix (they need a real DSH
625
- install and provider credentials). Run them locally, or dispatch the
626
- `model-suites` workflow with a self-hosted runner that has DSH configured.
627
-
628
- The suite **cleans up after itself**: `run.mjs` always ends with `cleanup.mjs`, which removes sessions and workspace registrations whose cwd is **a test-created directory** (`tmpdir()/dsh-any-agent-*` or `Desktop/dsh-any-agent-test-*`). Real project sessions โ€” including projects under `%TEMP%` โ€” and your live state directory are never touched. To remove `Desktop/dsh-any-agent-test-*` artifacts explicitly, run `node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions`.
629
-
630
- | Suite | Checks | Covers |
631
- |---|---|---|
632
- | `smoke` | 29 | handshake, tool table, conversation creation, config, registry purity |
633
- | `boundary` | 46 | protocol edges (double initialize, malformed lines, unknown method), argument validation (including read-only + permissive approval rejection, unknown provider/model, and no stale write-lock marker on a failed create), unknown ids, cursor edges, lifecycle idempotence, unicode |
634
- | `integration` | 30 | resume-with-memory across processes, interrupt, both interject modes |
635
- | `async` | 16 | fire-and-forget + later collection |
636
- | `sentinel` | 36 | completion sentinel: atomicity, latch semantics, per-conversation namespacing under concurrency, cancelled runs still land it, and **reasoning never reaching the file** |
637
- | `prune` | 11 | **sentinel retention**: prunes over-age and crash-leftover files, keeps fresh ones, removes empty shells, `TTL=0` disables pruning, and never touches the conversation registry beside it (no tokens, runs every time) |
638
- | `guards` | 36 | **environment-variable guards**: odd numbers always land on the safe side โ€” `IDLE_TTL_MS=-1` means "never reap", garbage/empty falls back to the 10-minute default, a too-small reap interval falls back to the default (no busy loop), and the **lost-holder threshold is floored at 30 s** (so another instance cannot "legitimately" preempt and kill our turn) (no tokens, runs every time) |
639
- | `sandbox-guard` | 21 | **the test sandbox itself**: a "throwaway HOME" that resolves to the real user profile, to the filesystem root, or to anywhere outside the OS temp dir is refused with an error instead of being written to and deleted. Every other suite builds its HOME through `test/sandbox-home.mjs` and can only destroy what it created there (no tokens, runs every time) |
640
- | `approval-details` | 27 | **approval review context**: the pending-approval snapshot carries the command, reason, cwd and original approval ID โ€” not just a bare ID โ€” so a caller can actually adjudicate; missing `rawInput` is correlated from the earlier `tool_call` update by ID; credential-shaped fields and common inline literals are redacted; closing a conversation cancels pending approvals. Verified end-to-end over real stdio MCP |
641
- | `failure-terminal` | 11 | **terminal state on failure**: an `ensureAlive` failure wakes `dsh_interject` waiters, writes an `error` sentinel, and attaches `runId` to the error; an approval with no matching option reports `effective=cancelled` instead of a fake allow; `dsh_get` reads a run back from its sentinel (no tokens) |
642
- | `shared-host` | 15 | **shared process host**: notifications and permission requests route by `sessionId`, unknown-session approvals are cancelled, a process exit fans out to every attached conversation, the child stops only when the last conversation detaches; RSS helper (no tokens) |
643
- | `shared-process` | 8 (skips without DSH) | **real-DSH shared process**: two conversations with the same `(cwd, permission)` share one PID, closing one leaves the other alive, closing both clears the host and exits the PID (needs a local DSH; runs with `--all`) |
644
- | `dispatch-tools` | 31 | **dispatch surface**: `dsh_inbox` aggregates pending approvals across conversations, `client_run_id` idempotency hits and evicts with its run, queue list/update/promote/remove (remove writes a cancelled sentinel), `dsh_get(max_chars)` truncates only the text while structured keeps the full answer, audit records metadata only, `dsh_doctor` essential checks (no tokens) |
645
- | `line-decoder` | 4 | **UTF-8 line framing**: multi-byte characters split across MCP/ACP stream chunks are preserved (no U+FFFD), including CRLF and EOF-without-newline (no tokens) |
646
- | `read-cursor` | 10 | **`dsh_read` cursor semantics**: text appended to an already-read entry is delivered again (revision cursor, match by `seq`), and `limit` is clamped to 1..500 so `0`/negative cannot spin or drop the tail (no tokens) |
647
- | `sentinel-redaction` | 19 | **sentinel disk hygiene**: `result.thinking` is stripped, `thinking_omitted` is always a boolean, and `answer`/`error`/`title`/`tools_used[].input` all pass the same credential redaction as approvals (no tokens) |
648
- | `forget-tombstone` | 10 | **`forget` is terminal**: `release(forget=true)` writes a tombstone, by-id lookups fail with a recovery path, lists exclude it, and deleting the tombstone restores the normal unknown-session path (no tokens) |
649
- | `bootstrap-profile` | 24 | **first-start profile works in the published zero-dependency form**: copies routing with the built-in YAML subset parser, strips `apiKey`/`headers`/literal secrets, never writes `defaultPreset`, is idempotent, and leaves a user-maintained profile byte-identical (no tokens) |
650
- | `workspace-safety` | 23 | **data-loss boundaries**: `isTempPath` is segment-aware (`tmpdir()+'Suffix'` and `/tmpfoo` are *not* temp), a corrupt `workspace.json` is never overwritten, an unreadable session root never wipes `sessionIds`, and automatic cleanup deletes only test-created sessions (no tokens) |
651
- | `permission-adopt` | 17 | **no silent privilege escalation**: adopting an external session with no permission record fails closed to `read-only`; a recorded `read-only` cannot be promoted by the sidecar; the runtime read-back rejects a recorded/requested mismatch (no tokens) |
652
- | `state-safety` | 26 | **state/sentinel/lock storage**: defaults live under `$DSH_HOME/.dsh-any-agent`, an unwritable directory fails startup loudly, sentinel write failures return `null` + `sentinel_error`, and bad numeric env values land on the safe side (no tokens) |
653
- | `setup` | 45 | **the registration CLI**: preview writes nothing, `--remove` takes out only its own entry, an absent client is skipped rather than failed, the Codex TOML block round-trips, and the command recorded in a config is actually spawned and must answer `initialize` (no tokens). It never invokes `claude mcp get` โ€” that CLI rewrites `~/.claude.json` |
654
- | `removal-scope` | 24 | **unregistering touches only its own entry**: configs holding unrelated MCP servers, top-level Codex settings and sibling Zed settings are registered and then removed, asserting every other value is byte-identical; removing something never written is a no-op with no write (no tokens) |
655
- | `config-safety` | 407 | **the property the 0.3 line is about**, per client: preview leaves files byte-identical with no `.bak`; register-then-remove restores every other entry byte-for-byte, keeps key order and drops no top-level key; 4-space / 2-space indentation, single-line JSON, missing trailing newline, CRLF and a UTF-8 BOM all survive a write; malformed JSON and read-only files are refused without damage; the second `--yes` is a no-op with no new backup; every `.bak-dsh-<stamp>` appears once and equals the pre-write bytes; a `claude` on PATH is never invoked (no tokens) |
656
- | `real-configs` | 84 (66 with 7/9, 48 with 5/9 fixtures) | **round-trip on the real thing**: copies this machine's actual client configs (an 182693-byte `~/.claude.json` holding hundreds of `projects` entries and case-colliding keys, a 238-line Codex TOML, Zed's full `settings.json`) into a temp dir, registers, then removes, and requires every file back **byte for byte**; entry-presence is checked by key (not a bare substring), and missing fixtures are reported as skipped rather than green (no tokens) |
657
- | `commit-script` | 26 | commit messages go through stdin, never a temp file that `git add -A` could capture (no tokens) |
658
- | `roots` | 12 | `dsh_start` without `cwd` asks the client for MCP roots; a client without roots gets an actionable error (no tokens) |
659
- | `workspace-titles` | 15 | home-as-workspace is rejected before DSH starts; `title_hint` is returned separately from DSH's automatic title (no tokens) |
660
- | `package-check` | 14 | `files` whitelist, zero runtime dependencies, Node floor, and bin shebangs (no tokens) |
661
- | `timeout` | 38 | **timeout semantics**: `wait=false` is unaffected by `timeout_ms`; a `wait=true` expiry merely downgrades to background (turn not cancelled, result not lost, `busy` never lies); `timeout_ms<=0` waits forever; a failed resume invalidates the process instead of wedging the conversation; an empty prompt yields a clear error |
662
- | `lock` | 79 | **write lock and preemption**: four holder classifications, `writeMarker` never overwriting a live holder, malformed/missing registration edge cases; **end-to-end** with two real MCP instances fighting over one conversation โ†’ clear error โ†’ refusal โ†’ `force` takeover; **a crash releases the lock automatically**, **a wedged holder is preempted without `force`**; plus the "one turn, one sentinel" invariant (inline turns and idle interjects included) |
663
- | `cycle` | 36 | **the async dispatch lifecycle**: dispatch โ†’ collect the sentinel โ†’ idle reap โ†’ dispatch again, three rounds with no lock error; re-dispatch right on the reap boundary (widening the race); an immediate re-dispatch after `dsh_release`; and an assertion that reaping leaves no unattributable lock (this suite caught the reaper collecting a freshly spawned process as if it were idle) |
664
- | `concurrency` | 31 | three simultaneous conversations + live incremental reads |
665
- | `capability` | 22 | writing code, running scripts, **spawning its own subagents** (verified on disk via child session headers) |
666
- | `workspace-effect` | 24 | workspace actually effective when no path is given |
667
- | `acceptance` | 36 | two folders ร— two conversations doing a read-only IDA Pro analysis |
668
- | `multi` | 19 | **several instances and non-ASCII paths**: reproduces "two instances share the registry and the later save drops the earlier instance's conversation", then proves **that conversation is recoverable by id alone from the session store and actually usable** (registry loss, not an explicit `forget` โ€” forget is a terminal tombstone); **recovery never silently escalates privileges** (a read-only conversation stays read-only); no `.tmp` residue; and **CJK / emoji workspace paths** create sessions, do real work, and place files in the right directory |
669
- | `permission` | 11 | โ˜… **whether the permission tiers actually take effect** (a safety property): it ignores our own return values (the very thing that used to lie) and reads **the session's own record** (`permissions.preset` / `sandboxMode` in the projection cache), checking all three tiers and that their recorded values differ. **This suite caught a silent privilege escalation**: a `defaultPreset` in the profile overrides `DSH_PERMISSION_MODE` at session creation |
670
- | `list` | 13 | **the cost of `dsh_list`** (measured): a default call with the on-disk probe takes ~1 s (it spawns a DSH process) while **repeated calls hit a cache and drop to single-digit milliseconds**; `only_running=true` and `include_closed=false` **skip the probe** with equivalent semantics (unopened on-disk sessions are simply excluded) |
671
- | `reasoning-check` | 8 | **reasoning visibility**: `reasoning=hide` returns no thinking body but non-zero stats; `reasoning=full` returns the text and the rendered section |
672
-
673
- **`node test/run.mjs` runs 843 checks on a machine with five client fixtures (879 with all nine)** โ€” smoke 29 + 814 offline checks (850 with nine). Every suite exited 0 on 0.3.9; none needs a provider (smoke skips its session-creation sections when DSH is absent).
674
-
675
- That is **524 assertions in the client-registration suites alone** (`setup` 45, `removal-scope` 24, `config-safety` 407, `real-configs` 48; 560 with all nine fixtures), which is where the 0.3 work actually is.
676
-
677
- The model-backed suites (`smoke` 29, `boundary` 46, `integration` 30, `async` 16, `sentinel` 36, `timeout` 38, `lock` 79, `cycle` 36, `concurrency` 31, `capability` 22, `workspace-effect` 24, `acceptance` 36, `multi` 19, `permission` 11, `list` 13, `reasoning-check` 8) total 474 and were last measured in full on 0.3.9 โ€” they need a working provider, so re-run them yourself before trusting a release: `node test/run.mjs --all`. `acceptance` now **skips explicitly** when the DSH profile has no IDA MCP (`mcp__ida__*`).
678
-
679
- > Three different totals are intentional: `node test/run.mjs` (843/879), the CI offline job (nominal 814/850, no smoke; hosted runners without client fixtures skip real-configs' 48 checks), and the model-backed set (474 nominal). Counts are the numbers each suite prints itself; don't use one number for a different runner.
680
-
681
- ### ๐Ÿงฏ Why `sandbox-guard` exists
682
-
683
- On 2026-10-29 a throwaway verification script stored its sandbox path in a **PowerShell** variable named `$home`. `$home` is a read-only automatic variable, so the assignment failed silently (the script ran with `$ErrorActionPreference = 'Continue'`), `$env:USERPROFILE` was pointed at the sandbox name anyway, and the cleanup line โ€” `Remove-Item $home -Recurse -Force` โ€” resolved to the **real user profile**. The run timed out at 120 s mid-delete, taking part of `~/.codex`, `~/.cursor` and `~/.claude` with it.
684
-
685
- Nothing in the test suites was ever green-but-wrong about the *writer*; what was missing was any assertion about the *sandbox*. `sandbox-guard` is that assertion: it refuses the real home, its subdirectories, case variants, the filesystem root, and any path outside `os.tmpdir()`. `test/sandbox-home.mjs` is now the only place a sandbox is created or destroyed, and `setup`/`removal-scope` no longer call `mkdtempSync` or `rmSync` themselves โ€” the suite fails if they go back to doing so.
686
-
687
- ### ๐Ÿ“ Numbers in this README must be reproducible
688
-
689
- Every figure in this document is either **printed by a test** or **measured with a command you can re-run**. If you change a number, change it to a measurement โ€” not to an estimate, and never to a plausible-looking value.
690
-
691
- This section exists because that rule was violated. An early draft claimed `dsh_send(wait=false)` returned "in ~1 ms" (measured: 81 ms), that interruption converged in "~18 ms" (no test ever produced that number โ€” `integration.mjs` only asserts `waited_ms` is between 0 and 25 s; four manual samples gave 91/102/105/119 ms), and that a conversation costs "~120 MB" (measured: 295โ€“476 MB) โ€” the last one contradicting itself in a single sentence, quoting both "120 MB each" and "three = 428 MB".
692
-
693
- **Three tiers, and how to state each:**
694
-
695
- | Tier | What it means | How to write it |
696
- |---|---|---|
697
- | **Tested** | A suite asserts it | Give the number; the suite is the authority |
698
- | **Measured** | You ran a command | Give the number **and the method**, so a reader can reproduce it |
699
- | **Estimated** | Derived or bounded | Say so ("roughly", "worst case", "โ‰ˆ") and show the arithmetic |
700
-
701
- **Recording a measurement inline.** The memory figures model the pattern โ€” they name the tool, the before/after, and both samples:
702
-
703
- ```text
704
- measured 300-480 MB per conversation
705
- (Get-Process node: WorkingSet64 summed before vs. after one active
706
- conversation reached end_turn; two cold starts gave 476 MB and 295 MB)
707
- ```
708
-
709
- **When yours disagrees, say so.** Figures depend on machine, DSH version and load. A measurement outside a documented range is worth an issue โ€” the range is a claim, not decoration.
710
-
711
- ## ๐Ÿ”’ The write lock: one writer per conversation at a time
712
-
713
- DSH conversations carry a **cross-process write lock**: **it never expires while the holder lives, and no API can take it from a live holder** โ€” so **whoever gets there first decides everything**. This server turns that into "**you can look, we can write**":
714
-
715
- | Situation | Outcome |
135
+ | Document | Contents |
716
136
  |---|---|
717
- | **This server holds the lock** (recently used, or `keep_alive=true`) | โœ… Opening it in the GUI is **read-only**: you still **see the content**, but you **cannot take the lock**. Since 0.4.0 the process is reaped only after **10 minutes** idle; `keep_alive=true` sessions are never reaped for idleness |
718
- | **You open it in the GUI while the lock is free** | โŒ **The GUI keeps it permanently** (navigating away, waiting and archiving all fail to release it) and this server **can never reconnect** โ€” only restarting `dsh web` helps |
719
- | **Recommended** | Watch progress with **`dsh_read`** (cursor-based incremental reads) or the **sentinel file**; if you must use the GUI, `dsh_release` first (that interrupts a running turn, so it fits the gaps between tasks) |
720
-
721
- **Who holds it** is reported by `dsh_status.lock_holder`; `dsh_takeover` handles the four cases:
722
-
723
- | `lock_holder` | Meaning | Action |
724
- |---|---|---|
725
- | `self` | this process holds it | nothing to preempt |
726
- | `stale-mcp` | the holder is **alive but silent** (no heartbeat for 90 s โ€” typically that MCP is wedged) | โœ… `dsh_takeover` **kills and takes over** |
727
- | `live-mcp` | another **live** dsh-any-agent instance is using it | โš ๏ธ refused by default; `force=true` takes it |
728
- | `none` | no registration โ†’ most likely **your own DSH GUI** | โŒ **never killed** (GUI conversations live inside the single `dsh web` process, so killing it takes down your whole UI and every GUI conversation with it), reported only |
729
-
730
- > **Two measured footnotes.** โ‘  **A crash leaves no orphan lock** โ€” `SIGKILL` the MCP and its children exit with it (their stdio pipe closes), releasing the lock automatically โœ“; so `stale-mcp` really means "**alive but wedged**". โ‘ก **Several instances**: the registry is last-writer-wins (a later save drops conversations it never saw), but the **session store is authoritative** โ€” any session on disk is recoverable **by id alone**; if you give instances different `DSH_MCP_STATE` files, **point `DSH_MCP_LOCKS_DIR` at one shared directory**, or they cannot see each other's holder registrations.
731
- >
732
- > The full measurements (the A/B probe method, the evidence that the GUI opens read-only, why even archiving does not release the lock, and crash-vs-wedge) are in [Usage Notes ยง3.5โ€“3.12](USAGE-NOTES.en.md).
733
-
734
- ## โš ๏ธ Known limitations
735
-
736
- 1. **No token-level streaming into model context** โ€” an MCP limitation, not DSH's. Callers get per-step results; humans can follow progress via stderr logs.
737
- 2. **True mid-turn steering is impossible** โ€” ACP rejects concurrent prompts (`a prompt is already in flight for this session`). `dsh_interject` is the practical equivalent: cancel, then immediately start a new turn, history preserved.
738
- 3. **Image prompts are unsupported** โ€” ACP advertises `promptCapabilities: {image: false}`.
739
- 4. **Restarting the MCP server takes its children with it** โ€” when this service is restarted or killed, the DSH children it spawned exit too (their stdio is a pipe to us): **in-flight turns are interrupted and no sentinel lands** (judge by inspecting the workspace, don't just wait for the file). The good news: the **write lock is released automatically**, so a restart always gets it back. The service also pushes no MCP notifications; "completion notification" is the sentinel file plus the caller's background waiter.
740
- 5. **A conversation that dies before its first successful turn may never have materialized on disk** โ€” resume then fails with a clear error. Safe after the first message.
741
- 6. **No renaming** โ€” DSH's title subsystem has no external rename API (`SessionTitleService.rename` requires a live in-process session). Titles are auto-generated from the first message.
742
- 7. **`session/list` returns only `{sessionId, cwd}` and excludes already-open sessions** โ€” titles are filled in by this server from DSH's projection cache.
743
- 8. **Memory (measured 300โ€“480 MB per active conversation)** โ€” since 0.4.0 the defaults are **10-minute idle reaping + `MAX_LIVE=4`**, so the worst case is โ‰ˆ1.2โ€“1.9 GB; `keep_alive=true` sessions are exempt from idle reaping (at the cost of staying resident). You can also: โ‘  cap by measured RSS with `DSH_MCP_MAX_RSS_MB`; โ‘ก cap the child heap with `DSH_MCP_CHILD_MAX_OLD_SPACE_MB`; โ‘ข set `DSH_MCP_SHARED_PROCESS=1` to share one ACP process across sessions with the same `(cwd, permission)` (experimental: saves memory and cold start, but a crash takes down the group). To drop to zero right after a batch of work: **`dsh_release(all=true)`** โœ“ (releases idle conversations; busy turns are skipped and keep running โ€” pass `force=true` to interrupt them too. Everything resumes on demand).
744
-
745
- ## โ“ Why is the profile still called `dsh-mcp`?
746
-
747
- Because renaming it would break things that already exist on disk.
748
-
749
- When you first start this server it creates a DSH profile under `~/.dsh/profiles/dsh-mcp/`. That directory name is referenced by three other persisted things:
750
-
751
- | Persisted identifier | Location | What breaks if renamed |
752
- |---|---|---|
753
- | profile directory | `~/.dsh/profiles/dsh-mcp/` | existing sessions can no longer resolve their provider (`no adapter registered for provider "โ€ฆ"`) |
754
- | `DSH_MCP_*` environment variables | your client configs | every deployment must change at once |
755
- | `# dsh-mcp:managed` marker line | first line of the generated `cordis.patch.yml` | bootstrap no longer recognizes the file as its own and stops syncing it |
756
-
757
- So the **product name** is `dsh-any-agent`, while the **on-disk identifiers** deliberately keep the old name. This is a compatibility decision, not an oversight. For a fresh deployment that wants a different profile name, set `DSH_MCP_PROFILE` โ€” the server will use the name you give it.
758
-
759
- ## ๐Ÿ“ฆ Release process
760
-
761
- The order matters; two of these steps exist because a bad release shipped without them.
762
-
763
- ```bash
764
- # 1. Bump the version. SERVER_VERSION reads package.json, nothing else.
765
- npm version patch --no-git-tag-version
766
- # Then edit CHANGELOG.md: turn [Unreleased] into the new version.
767
-
768
- # 2. Run the offline suites locally (same set as CI, no tokens, no DSH needed for most).
769
- node test/run.mjs
770
- node test/package-check.mjs
771
-
772
- # 3. Verify the tarball itself: builds, installs, starts.
773
- npm pack --dry-run
774
- # Then in a temp dir: npm install <tgz> && node_modules/.bin/dsh-any-agent --version
775
-
776
- # 4. Tag and push (wait for CI before publishing).
777
- git add -A && git commit -m "chore: release x.y.z"
778
- git tag vx.y.z && git push origin main --tags
779
-
780
- # 5. Publish to npm and verify.
781
- npm publish --access public
782
- npm view dsh-any-agent version
783
- # A 404 right after publish is usually propagation, not failure: npm returns
784
- # 202 and the registry can lag briefly. Confirm the local npm log shows the
785
- # version before retrying.
786
- ```
137
+ | [Setup](docs/en/install.md) | Requirements, profile bootstrap, 9 MCP clients |
138
+ | [Usage](docs/en/usage.md) | Dispatch/collect, sentinels, many conversations, reasoning |
139
+ | [Team mode](docs/en/team.md) | Default behavior, how to disable (per client), verify and revert |
140
+ | [Permissions & privacy](docs/en/security.md) | Tiers, approvals, GUI visibility, logging |
141
+ | [Write lock](docs/en/locking.md) | Four holder classes, takeover, GUI contention |
142
+ | [Environment variables](docs/en/reference.md) | Every switch and its default |
143
+ | [Tests](docs/en/tests.md) | Offline/integration suites and coverage |
144
+ | [Known limitations](docs/en/limits.md) | Limits, profile naming |
145
+ | [Compatibility](docs/en/compatibility.md) | DSH version matrix, team-package requirements, upgrade checks |
146
+ | [Architecture](docs/en/architecture.md) | Why ACP, sdk/plugin comparison |
147
+ | [Release process](docs/en/release.md) | Release checklist |
148
+
149
+ ## ๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘ Team mode (opt-in)
150
+
151
+ Off by default. Run `dsh-any-agent team on` and reconnect; the profile switches
152
+ to `dsh-team` (the experimental Agent Teams packages) and `dsh_send` adds a
153
+ short "form a team when this needs parallel or multi-role work" line. Simple
154
+ tasks are not forced into a team. Turn it off with `dsh-any-agent team off`, or
155
+ for one call with `dsh_send(team=false)`.
156
+
157
+ > Full steps (commands, verification, rollback) are in [Team mode](docs/en/team.md).
787
158
 
788
159
  ## Star History
789
160