dsh-plugin-subagent-bridge 0.14.2 → 0.15.1

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 (3) hide show
  1. package/README.md +200 -258
  2. package/index.js +30 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -5,271 +5,220 @@
5
5
  <!-- M8ven ownership proof — keep this line in place; it verifies the publisher. -->
6
6
  [![M8ven Score](https://m8ven.ai/badge/mcp/dogtiti-dsh-plugin-subagent-bridge-8a3t8k?v=f15f3e56c3fa22e8aafb88cf5428501f)](https://m8ven.ai/mcp/dogtiti-dsh-plugin-subagent-bridge-8a3t8k?s=readme)
7
7
 
8
- 在 DeepSeek Harness 宿主内部提供**本机回环 MCP 端点**,让另一个智能体(如 Codex)把任务派给 DSH 会话并取回结果。
8
+ 在 DeepSeek Harness 宿主内部提供**本机回环 MCP 端点**,让另一个智能体把任务派给 DSH 会话并取回结果;反过来,DSH 里的 agent 也能把任务派给 Codex 或 Claude Code。
9
9
 
10
- 插件运行在宿主进程内,直接使用公开的 `sessionController` 服务 —— 不需要读取 `~/.dsh/.credentials.yaml`,也不需要伪造 cookie。
10
+ 两个方向共用同一个插件,各自遵守对方产品的 `maxDepth: 1`:**能派活,不能再派回来**。
11
11
 
12
12
  ---
13
13
 
14
- ## 让 DSH 派活给 Claude
14
+ ## 能力一览
15
15
 
16
- 反向那一侧以前只有 Codex。现在多了 `claude_dispatch`:DSH 里的 agent 可以把任务交给 Claude Code,拿回最终回答。
17
-
18
- ```js
19
- claude_dispatch({ prompt: "审一下这个 PR 的风险", cwd: "/repo", resume: "a4925654-…", model: "sonnet" })
20
- ```
21
-
22
- | 参数 | 说明 |
23
- | --- | --- |
24
- | `prompt` | 必填,任务正文 |
25
- | `cwd` | 工作目录;默认用当前会话目录 |
26
- | `resume` | Claude Code 的 session id,续接同一段对话 |
27
- | `model` | 模型别名或 id;不传则用配置,再退回 Claude 自己的设置 |
28
-
29
- 返回 `{ ok, text, sessionId, durationMs }`,`sessionId` 就是下次 `resume` 用的那个。
30
-
31
- 和 `codex_dispatch` 的差别只在底层:Claude Code 的 `-p` 模式**一次输出一个 JSON 对象**(`{type:"result", result, session_id, is_error}`),不像 Codex 是事件流。所以解析器只看 stdout 的 JSON,**stderr 的警告不算失败**——比如模型名带 `[1M]` 后缀时 CLI 会警告,但仍然正常返回。
32
-
33
- 配置项:`claudeBin`(默认 `claude`)、`claudeModel`(默认空,即用 CLI 自己的设置)、`claudeTimeoutMs`(默认 300 秒,上限 240 秒夹取)。
34
-
35
- ## 创建项目(workspace)
36
-
37
- DSH 的侧边栏分组是"项目(workspace)",和会话的工作目录是两回事:新建会话默认落在「未分组」,除非它所在的目录已经是一个项目。
16
+ | 方向 | 入口 | 一次调用做什么 |
17
+ | --- | --- | --- |
18
+ | Codex / 任意 MCP 客户端 → DSH | `dsh_task` | 开会话、投递、阻塞到这一轮结束,只回最终答案 |
19
+ | 同上,并行 | `dsh_spawn` + `dsh_wait` | 立刻返回句柄,多个 worker 同时跑,再批量等 |
20
+ | 同上,续派 | `dsh_followup` | 复用 worker 的上下文再派一轮 |
21
+ | DSH → Codex | `codex_dispatch` | 跑一次 `codex exec`,回最终回答与可续接的线程 id |
22
+ | DSH → Claude Code | `claude_dispatch` | 跑一次 `claude -p`,回最终回答与可续接的 session id |
38
23
 
39
- `dsh_task` / `dsh_spawn` 的 `workspace` 参数把这一步也接管了:
24
+ 插件运行在宿主进程内,直接用公开服务(`sessionController`、`sessions`、`workspaceRegistry`、`userQuestions`),不需要读凭据文件,也不需要伪造 cookie。
40
25
 
41
- ```js
42
- // 目录不是项目 → 用它创建项目(标题取目录名),会话直接归到项目下
43
- dsh_task({ prompt: "…", cwd: "/repo/clawer", workspace: true })
26
+ ---
44
27
 
45
- // 用指定标题创建
46
- dsh_spawn({ task_name: "w1", message: "…", cwd: "/repo/clawer", workspace: "Clawer" })
28
+ ## 安装
47
29
 
48
- // 不传 → 只复用已有项目;目录没有项目就保持未分组(和以前一样)
49
- dsh_task({ prompt: "…", cwd: "/repo/clawer" })
30
+ ```bash
31
+ git clone https://github.com/Dogtiti/dsh-plugin-subagent-bridge
50
32
  ```
51
33
 
52
- 行为细节:
53
-
54
- - 目录不存在时先递归创建,再做项目登记(登记要求目录已存在);
55
- - 目录已经是项目 → **直接复用,不改标题**(底层 `create` 对同一路径返回已有记录);
56
- - 返回结果里带上 `workspace: { id, title, path }`,方便确认落在哪个项目;
57
- - 项目操作失败不影响派发本身(分组是外观,不因此让任务失败)。
58
-
59
- ## 回答 worker 的问题(`dsh_answer`)
60
-
61
- worker 需要用户拍板时会调用 `ask_user_question`,然后**卡在那里等**。如果人不在,任务就停住了。
34
+ Plugins → 添加 → 选该目录。
62
35
 
63
- 桥把这种情况暴露出来:
36
+ 启动后在 `127.0.0.1:47821` 监听,并把端点写到 `~/.dsh/bridge/endpoint.json`(目录 0700、文件 0600):
64
37
 
65
- ```jsonc
66
- // dsh_wait 的返回
38
+ ```json
67
39
  {
68
- "status": { "w1": "running" },
69
- "timedOut": true,
70
- "pendingQuestion": {
71
- "callId": "call_abc",
72
- "questions": [
73
- {
74
- "id": "mode",
75
- "question": "用哪种模式?",
76
- "header": "Choose Mode",
77
- "options": [{ "label": "快速", "description": "只跑单测" }, { "label": "完整" }]
78
- }
79
- ]
80
- },
81
- "next": "blocked on a question: answer it with dsh_answer, or call dsh_wait again"
40
+ "url": "http://127.0.0.1:47821/mcp/<token>",
41
+ "port": 47821,
42
+ "token": "<token>",
43
+ "pid": 12345,
44
+ "startedAt": "2026-10-22T12:30:00.000Z"
82
45
  }
83
46
  ```
84
47
 
85
- 调用方(Codex / Claude)看到 `pendingQuestion` 就可以代用户回答:
48
+ ## 接到 Codex
86
49
 
87
- ```jsonc
88
- dsh_answer({
89
- "worker": "w1",
90
- "answers": [{ "id": "mode", "selected": ["快速"] }] // custom 可用于自由文本
91
- })
50
+ ```bash
51
+ codex mcp add dsh-bridge --url "$(node -p "require(process.env.HOME+'/.dsh/bridge/endpoint.json').url")"
52
+ codex mcp list # 验证
53
+ codex mcp remove dsh-bridge # 移除
92
54
  ```
93
55
 
94
- 细节:
56
+ 桌面 App 与 CLI 共用 `~/.codex/config.toml`,两端都会看到这条。
95
57
 
96
- - 一个问题的 id 必须**恰好出现一次**,否则底层会报 `BAD_ANSWER`;
97
- - `selected` 是选项 label 数组,多选时给多个;跳过一个问题就给空数组;
98
- - 回答是插进正在等待的那个回合的(底层用 `steer`),worker 立刻继续;
99
- - 只有真卡在 `ask_user_question` 上才算「等待中」——工具一旦返回结果就不再报。
58
+ > token 放在 URL 路径里(能力 URL),所以 Codex 侧不需要配环境变量——桌面 App 从 Finder 启动时拿不到自定义环境变量。也支持 `Authorization: Bearer <token>`,需要时可用 `--bearer-token-env-var`。
100
59
 
101
- ## 排队还是插话(`mode`)
60
+ Claude Code 同理:
102
61
 
103
- DSH 的「繁忙时的发送行为」只有两种,桥把它们原样交给调用方:
104
-
105
- | `mode` | DSH 里的名字 | 行为 |
106
- | --- | --- | --- |
107
- | `queue`(默认) | 排队发送 | 等当前回合跑完,再作为一个新回合处理 |
108
- | `steer` | 插话发送 | 并入当前回合,模型在下一步就能看到 |
109
-
110
- `dsh_task`、`dsh_spawn`、`dsh_followup` 都支持:
111
-
112
- ```js
113
- dsh_followup({ worker: "w1", message: "接下来只改 src 下的文件", mode: "steer" })
114
- dsh_task({ sessionId: "session-…", prompt: "停一下,改成只跑单测", mode: "steer" })
62
+ ```bash
63
+ claude mcp add --transport http dsh-bridge "<上面那个 url>" --scope user
115
64
  ```
116
65
 
117
- 两点实测结论:
118
-
119
- 1. **`steer` 不打断正在执行的工具**。实测 worker 正在跑 `sleep 90`,`steer` 的消息投递进去了,但要等那次工具调用返回、模型走到下一步才被看到——整整 70 秒。真想立刻停,是 DSH 界面上另一个动作(取消),不是投递模式。
120
- 2. **`steer` 不新开回合**(turn 编号不变),所以等待条件必须是「当前回合结束」。早期实现等的是「下一个回合」,答案都回来了、调用还在等,最后 120 秒超时。
121
-
122
- `dsh_task` 对正在运行的会话默认会拒绝,错误信息里会提示改用 `steer`。
66
+ ---
123
67
 
124
- 第三个细节是返回值:插话的回复和原任务在**同一个回合**里,所以不能简单取"这一轮最后一条 assistant 文本"——那可能是原任务后面的步骤。桥会在插话后定位到**自己那条 user/message**,只取它之后的 assistant 文本作为回答。
68
+ ## 正向:Codex → DSH
125
69
 
126
- ## 创建项目(workspace)
70
+ ### 工具
127
71
 
128
- DSH 的侧边栏分组是"项目(workspace)",和会话的工作目录是两回事:新建会话默认落在「未分组」,除非它所在的目录已经是一个项目。
72
+ | 工具 | 作用 |
73
+ | --- | --- |
74
+ | `dsh_task` | **同步**派活:开会话、投递、阻塞到这一轮结束,只返回**最终回答** |
75
+ | `dsh_spawn` | **异步**派活:立刻返回 `task_name` + `sessionId`,可并行开多个 |
76
+ | `dsh_followup` | 给已有 worker 再派一轮(复用上下文) |
77
+ | `dsh_wait` | 一次等多批 worker,任一到达终态即返回 |
78
+ | `dsh_close` | 关闭 worker,释放并发名额(不删除会话) |
79
+ | `dsh_answer` | 代用户回答 worker 提出的问题(见「问题上报」) |
80
+ | `dsh_list_workers` | 列出本桥开的 worker 及实时状态 |
81
+ | `dsh_dispatch` | 向指定会话投递一条用户消息并开始一轮;返回 `requestId` 与 `seqAtDispatch` |
82
+ | `dsh_read_result` | **长轮询**读该会话在 `seqAtDispatch` 之后的全部助手文本(看过程用;要结果用 `dsh_wait`) |
83
+ | `dsh_list_sessions` | 列出会话**元数据**(id、标题、工作目录、是否运行、最后活动时间) |
129
84
 
130
- `dsh_task` / `dsh_spawn` 的 `workspace` 参数把这一步也接管了:
85
+ ### 基本用法
131
86
 
132
87
  ```js
133
- // 目录不是项目 → 用它创建项目(标题取目录名),会话直接归到项目下
134
- dsh_task({ prompt: "…", cwd: "/repo/clawer", workspace: true })
88
+ // 同步:等它跑完,直接拿最终答案
89
+ dsh_task({ prompt: "审一下 src/bridge.ts 的错误处理", cwd: "/repo", timeoutSeconds: 300 })
135
90
 
136
- // 用指定标题创建
137
- dsh_spawn({ task_name: "w1", message: "…", cwd: "/repo/clawer", workspace: "Clawer" })
91
+ // 并行:先拿句柄,再批量等
92
+ dsh_spawn({ task_name: "audit_api", message: "…", cwd: "/repo" })
93
+ dsh_spawn({ task_name: "audit_ui", message: "…", cwd: "/repo" })
94
+ dsh_wait({ targets: ["audit_api", "audit_ui"], timeoutMs: 240000 })
138
95
 
139
- // 不传 → 只复用已有项目;目录没有项目就保持未分组(和以前一样)
140
- dsh_task({ prompt: "…", cwd: "/repo/clawer" })
96
+ // 续派:复用 worker 的上下文
97
+ dsh_followup({ worker: "audit_api", message: "把第 3 条展开,给出补丁" })
141
98
  ```
142
99
 
143
- 行为细节:
144
-
145
- - 目录不存在时先递归创建,再做项目登记(登记要求目录已存在);
146
- - 目录已经是项目 → **直接复用,不改标题**(底层 `create` 对同一路径返回已有记录);
147
- - 返回结果里带上 `workspace: { id, title, path }`,方便确认落在哪个项目;
148
- - 项目操作失败不影响派发本身(分组是外观,不因此让任务失败)。
149
-
150
- ## 排队还是立刻打断(`mode`)
100
+ 边界:
151
101
 
152
- DSH 的 prompt 有两种投递模式,桥把它们交给调用方选:
102
+ - `dsh_list_sessions` **只返回元数据**,不含任何消息内容;
103
+ - `dsh_read_result` 只对**本桥派发过**的会话生效,从未派发过的会话一律拒绝。
153
104
 
154
- | `mode` | 行为 |
155
- | --- | --- |
156
- | `queue`(默认) | 等当前回合跑完再处理这条消息 |
157
- | `steer` | 把消息**插进当前回合**(当前那一步仍会跑完) |
158
- | `interrupt` | **先取消当前回合**,再发送,真正立刻停下 |
105
+ ### 指定模型
159
106
 
160
- `dsh_task`、`dsh_spawn`、`dsh_followup` 都支持:
107
+ `dsh_task`、`dsh_spawn`、`dsh_followup` 都可以带 `model`:
161
108
 
162
109
  ```js
163
- dsh_followup({ worker: "w1", message: "别改了,先把现在的结果汇报一下", mode: "steer" })
164
- dsh_task({ sessionId: "session-…", prompt: "停一下,改成只跑单测", mode: "steer" })
110
+ dsh_task({ prompt: "…", model: "zai-coding-cn/glm-5.3", reasoningEffort: "max" })
111
+ dsh_spawn({ task_name: "review", message: "…", model: "opencode-go/deepseek-v4.1-flash" })
165
112
  ```
166
113
 
167
- 和 `dsh_dispatch` 的关系:`dsh_dispatch` 一直有 `mode`,但它只投递、不等待;`dsh_task` 会阻塞到回合结束,所以以前对正在运行的会话直接拒绝。现在传 `mode: "steer"` 就允许插进运行中的会话——**它会先打断当前回合**,然后等这一轮结束才返回。
168
-
169
- 两个实测出来的细节:
114
+ - 写法是 `provider/model`;只写模型名时沿用当前默认 provider;
115
+ - `reasoningEffort` 可选,取值随模型;
116
+ - 不传就完全不动会话;
117
+ - 实测:请求确实切到指定模型,且**不会改写 profile 的默认模型**。
170
118
 
171
- 1. **`steer` 不会打断正在执行的工具**。实测:worker 正在跑 `sleep 90`,`steer` 进去后消息被**并入当前回合**,但要等那一步工具调用结束才会被模型看到——整整等了 70 秒。原因是 `steer` 只是投递,`cancel` 才是中止。
172
- 2. **`steer` 不新开回合**,所以等待条件不能是"下一个回合结束"。早期实现那样写,答案已经回来了、调用还在等,最后 120 秒超时。现在 steer 等的是「当前回合结束」。
119
+ 实现上是在建好会话、发第一个 prompt **之前**调一次 `selectModel`,所以 worker 第一轮就在新模型上跑。
173
120
 
174
- 所以真要"按下就停",用 `interrupt`:它先 `cancel` 掉当前回合(最多等 15 秒确认),再以普通排队模式发消息,开启新回合。
121
+ ### 项目归属(`workspace`)
175
122
 
176
- 返回里带 `mode` 字段,方便确认这次是排队还是打断。
177
-
178
- ## 指定模型
179
-
180
- `dsh_task`、`dsh_spawn`、`dsh_followup` 都可以带 `model`,让 worker 跑在指定模型上:
123
+ DSH 的侧边栏分组(项目 / workspace)和工作目录是两回事:新建会话默认落在「未分组」,除非所在目录已经是项目。
181
124
 
182
125
  ```js
183
- dsh_task({ prompt: "…", model: "zai-coding-cn/glm-5.3", reasoningEffort: "max" })
184
- dsh_spawn({ task_name: "review", message: "…", model: "opencode-go/deepseek-v4.1-flash" })
185
- dsh_followup({ worker: "review", message: "…", model: "deepseek-account/deepseek-flash" })
126
+ dsh_task({ prompt: "…", cwd: "/repo/clawer", workspace: true }) // 用目录名建项目
127
+ dsh_spawn({ task_name: "w1", message: "…", cwd: "/repo/clawer", workspace: "Clawer" })
128
+ dsh_task({ prompt: "…", cwd: "/repo/clawer" }) // 只复用,不创建
186
129
  ```
187
130
 
188
- - 写法是 `provider/model`;**只写模型名**时沿用当前默认 provider(`model: "glm-5.3"`);
189
- - `reasoningEffort` 可选,取值随模型(`low` / `medium` / `high` / `max` 等);
190
- - 不传 `model` 就完全不动会话,行为和以前一致;
191
- - 实测(Host 侧 `sessionController.selectModel`):请求头里的 provider/model 确实变成指定的那组,且**不会改写 profile 的默认模型**。
192
-
193
- 实现上就是在建好会话、发第一个 prompt **之前**调一次 `selectModel`,所以 worker 的第一轮就已经在新模型上跑。
194
-
195
- ## 工具
196
-
197
- | 工具 | 作用 |
198
- | --- | --- |
199
- | `dsh_task` | **同步**派活:开会话、投递、阻塞到这一轮结束,只返回**最终回答**(最后一条 assistant 消息) |
200
- | `dsh_spawn` | **异步**派活:立刻返回 `task_name` + `sessionId`,可同时开多个 worker 并行跑 |
201
- | `dsh_followup` | 给已有 worker 再派一轮(复用它的上下文) |
202
- | `dsh_close` | 关闭 worker,释放并发名额(不删除会话) |
203
- | `dsh_list_workers` | 列出本桥开的 worker 及其实时状态 |
204
- | `dsh_wait` | 一次等多批 worker:`targets` + `timeoutMs`,任一到达终态即返回 `{completed}` / `{errored}` / `"running"` / `"not_found"` |
205
- | `dsh_dispatch` | 向指定会话投递一条用户消息并开始一轮;返回 `requestId` 与 `seqAtDispatch` |
206
- | `dsh_read_result` | **长轮询**读进度:返回该会话在 `seqAtDispatch` 之后的**全部**助手文本(用于看过程;要结果用 `dsh_wait`) |
207
- | `dsh_list_sessions` | 列出会话**元数据**(id、标题、工作目录、是否运行、最后活动时间) |
131
+ - 目录不存在时先递归创建,再做项目登记(登记要求目录已存在);
132
+ - 目录已经是项目 → **复用,不改标题**;
133
+ - 返回里带 `workspace: { id, title, path }`;
134
+ - 项目操作失败不影响派发本身。
208
135
 
209
- ## 反向:DSH → Codex
136
+ ### 排队还是插话(`mode`)
210
137
 
211
- 插件同时给本部署的 Agent 注册了一个工具:
138
+ DSH 的「繁忙时的发送行为」只有两种,桥原样交给调用方:
212
139
 
213
- | 工具 | 参数 | 返回 |
140
+ | `mode` | DSH 里的名字 | 行为 |
214
141
  | --- | --- | --- |
215
- | `codex_dispatch` | `prompt`(必填)、`cwd`、`resume`(线程 id)、`sandbox` | `{ ok, text, threadId, exitCode, durationMs, error? }` |
142
+ | `queue`(默认) | 排队发送 | 等当前回合跑完,作为新回合处理 |
143
+ | `steer` | 插话发送 | 并入当前回合,模型下一步就能看到 |
216
144
 
217
- 它执行 `codex exec [resume <id>] --json -o <临时文件>`,从事件流取 `thread.started`、从 `-o` 文件取最终消息;工具结果里会带 `[codex thread <id> · 12s]`,把那个 id 传给 `resume` 就能继续同一个 Codex 线程(实测能记住上一轮的回复)。
145
+ ```js
146
+ dsh_followup({ worker: "w1", message: "接下来只改 src 下的文件", mode: "steer" })
147
+ ```
218
148
 
219
- 注意:**`resume` 时 `sandbox` 与 `cwd` 不生效** —— `codex exec resume` 子命令本身不接受 `-s` / `-C`,线程沿用它原来的设置。
149
+ 两个实测结论:
220
150
 
221
- 边界(v0 硬约束):
151
+ 1. **`steer` 不打断正在执行的工具**——worker 若正卡在 `sleep 90` 这类调用里,消息要等它返回才被看到(实测 70 秒)。真想立刻停下是界面上的「取消」,不是投递模式。
152
+ 2. **`steer` 不新开回合**(turn 编号不变),所以桥等的是「当前回合结束」;等「下一回合」会一直等到超时。
222
153
 
223
- - `dsh_list_sessions` **只返回元数据**,不含任何消息内容。
224
- - `dsh_read_result` 只对**本桥派发过**的会话生效;从未派发过的会话一律拒绝。
154
+ `dsh_task` 对正在运行的会话默认拒绝,错误信息会提示改用 `steer`。插话的回复和原任务在同一回合里,桥会定位到**自己那条 user/message** 之后再取回答,避免混进原任务后续步骤的输出。
225
155
 
226
- ---
156
+ ### 无人值守(`assumeDefaults`)
227
157
 
228
- ## 安装
158
+ worker 卡在 `ask_user_question` 上时需要有人在界面上点一下,但调用方是另一个 agent 时那个人往往不在。因此桥在派发时**自动追加一条常驻指令**:
229
159
 
230
- ```bash
231
- # 本地目录安装
232
- git clone https://github.com/Dogtiti/dsh-plugin-subagent-bridge
160
+ ```
161
+ (如果缺少必要信息或需要在多个方案里选择,直接采用最合理的默认做法继续做完,
162
+ 不要把选择权抛回给用户、不要调用 ask_user_question 等回答;
163
+ 在结果里用一行说明你假设了什么。)
233
164
  ```
234
165
 
235
- Plugins → 添加 → 选该目录。
166
+ - 默认开启,`assumeDefaults: false` 可关;
167
+ - `dsh_task`、`dsh_spawn`、`dsh_followup` 三条路径都会加;
168
+ - prompt 里本来就提到 `ask_user_question` 时不追加。
236
169
 
237
- 启动后插件在 `127.0.0.1:47821` 监听,并写入端点文件(权限 0700/0600):
170
+ 效果:worker 不会停下来等人,而是选一个合理默认做法做完并交代假设,调用方据此复核。
238
171
 
239
- ```json
172
+ ### 问题上报(`pendingQuestion`)
173
+
174
+ worker 真的问了问题时,`dsh_wait` 会把问题原样带出来:
175
+
176
+ ```jsonc
240
177
  {
241
- "url": "http://127.0.0.1:47821/mcp/<token>",
242
- "port": 47821,
243
- "token": "<token>",
244
- "pid": 12345,
245
- "startedAt": "2026-10-22T12:30:00.000Z"
178
+ "status": { "w1": "running" },
179
+ "timedOut": true,
180
+ "pendingQuestion": {
181
+ "callId": "call_abc",
182
+ "questions": [{
183
+ "id": "mode",
184
+ "question": "用哪种模式?",
185
+ "header": "Choose Mode",
186
+ "options": [{ "label": "快速", "description": "只跑单测" }, { "label": "完整" }]
187
+ }]
188
+ },
189
+ "next": "blocked on a question: answer it with dsh_answer, or call dsh_wait again"
246
190
  }
247
191
  ```
248
192
 
249
- 路径:`~/.dsh/bridge/endpoint.json`
193
+ `dsh_answer({ worker, answers: [{ id, selected: [...] }] })` 用来代答。**已知限制**:DSH 的 `userQuestions.answer()` 只接受已经超时、进入 `continued` 状态的问题;还阻塞在工具调用里的问题是 Web UI 的 answerer 接管的,桥答不了(实测返回 `answered: false`)。所以实际使用中更该依赖上面的 `assumeDefaults`,而不是等 worker 提问。
250
194
 
251
- ## 接到 Codex
195
+ ---
252
196
 
253
- ```bash
254
- codex mcp add dsh-bridge --url "$(node -p "require(process.env.HOME+'/.dsh/bridge/endpoint.json').url")"
255
- ```
197
+ ## 反向:DSH → Codex / Claude Code
256
198
 
257
- 验证:
199
+ 两个工具都会给本部署的 Agent 注册,参数形状一致,差别只在底层 CLI。
258
200
 
259
- ```bash
260
- codex mcp list
261
- codex mcp get dsh-bridge
262
- ```
201
+ ### `codex_dispatch`
202
+
203
+ | 参数 | 说明 |
204
+ | --- | --- |
205
+ | `prompt` | 必填,任务正文 |
206
+ | `cwd` | 工作目录,默认当前会话目录 |
207
+ | `resume` | Codex 线程 id,续接同一线程 |
208
+ | `sandbox` | `read-only` / `workspace-write` / `danger-full-access` |
263
209
 
264
- Codex 的桌面 App 与 CLI 共用 `~/.codex/config.toml`,所以两端都会看到这条。
210
+ 返回 `{ ok, text, threadId, exitCode, durationMs }`。执行 `codex exec [resume <id>] --json -o <临时文件>`,从事件流取 `thread.started`、从 `-o` 取最终消息。**`resume` 时 `sandbox` 与 `cwd` 不生效**——`codex exec resume` 不接受 `-s` / `-C`。
265
211
 
266
- > token 存在 URL 路径里(能力 URL),因此不需要在 Codex 侧配置环境变量 —— 桌面 App 从 Finder 启动时拿不到自定义环境变量。若你更想用 bearer,也可以 `--bearer-token-env-var` 指向环境变量,插件同时支持 `Authorization: Bearer <token>`。
212
+ ### `claude_dispatch`
267
213
 
268
- 移除:
214
+ | 参数 | 说明 |
215
+ | --- | --- |
216
+ | `prompt` | 必填,任务正文 |
217
+ | `cwd` | 工作目录 |
218
+ | `resume` | Claude Code 的 session id |
219
+ | `model` | 模型别名或 id;不传则用配置,再退回 CLI 自己的设置 |
269
220
 
270
- ```bash
271
- codex mcp remove dsh-bridge
272
- ```
221
+ 返回 `{ ok, text, sessionId, durationMs }`。执行 `claude -p --output-format json`,只读 stdout 的单个 JSON 对象;**stderr 的警告不算失败**(模型名带 `[1M]` 后缀时 CLI 会警告,但结果正常)。
273
222
 
274
223
  ---
275
224
 
@@ -286,81 +235,76 @@ codex mcp remove dsh-bridge
286
235
  | 键 | 默认 | 说明 |
287
236
  | --- | --- | --- |
288
237
  | `enabled` | `true` | 总开关;关闭后不监听、不写端点文件 |
289
- | `port` | `47821` | 回环监听端口;固定端口是为了让 Codex 侧那条 URL 在重启后依然有效 |
290
- | `endpointFile` | `~/.dsh/bridge/endpoint.json` | 端点与令牌的落盘位置 |
291
- | `maxResultChars` | `20000` | 单次 `dsh_read_result` 返回的最大字符数(超出保留尾部) |
292
- | `taskTimeoutSeconds` | `300` | `dsh_task` 等待一轮结束的默认上限(客户端可用 `timeoutSeconds` 覆盖,最大 600) |
293
- | `workersFile` | `~/.dsh/bridge/workers.json` | worker 注册表与重基线的落盘位置(0600) |
294
- | `maxHops` | `1` | 跨工具委派层数上限(1 = 只允许一层,和 Codex / DSH 的默认 `maxDepth: 1` 一致) |
295
- | `dispatchPerMinute` | `10` | 滚动窗口内允许的派发次数(spawn / task / dispatch / followup / codex_dispatch 合计) |
238
+ | `port` | `47821` | 回环端口;固定端口让客户端那条 URL 重启后依然有效 |
239
+ | `endpointFile` | `~/.dsh/bridge/endpoint.json` | 端点与令牌落盘位置 |
240
+ | `maxResultChars` | `20000` | 单次结果返回的最大字符数(超出保留尾部) |
241
+ | `taskTimeoutSeconds` | `300` | `dsh_task` 等待上限(客户端可覆盖,最大 240) |
242
+ | `assumeDefaults` | `true` | 派发时追加「自行决策」的常驻指令 |
243
+ | `workersFile` | `~/.dsh/bridge/workers.json` | worker 注册表(0600) |
244
+ | `maxHops` | `1` | 跨工具委派层数上限 |
245
+ | `dispatchPerMinute` | `10` | 滚动窗口内的派发次数 |
296
246
  | `dispatchWindowMs` | `60000` | 上述窗口长度 |
297
- | `maxWorkersPerRoot` | `4` | **每个工作目录**的 worker 上限(`cwd` 自动分桶,调用方不需要传 owner) |
298
- | `maxWorkers` | `12` | 全局兜底上限,防止多个项目一起把机器压满 |
299
- | `workerTtlMs` | `86400000` | 记录超过这个时间没更新就不再占名额(过期自动释放,无需手动清理) |
300
- | `waitTimeoutMs` | `240000` | `dsh_wait` 的默认超时(1s ~ 240s,客户端可用 `timeoutMs` 覆盖) |
301
- | `readWaitSeconds` | `25` | `dsh_read_result` 的默认长轮询时长(上限 55,客户端可用 `waitSeconds` 覆盖) |
302
- | `codexBin` | `codex` | Codex 可执行文件;含 `/` 时按路径严格解析,找不到就报错而不回退 |
303
- | `codexSandbox` | `workspace-write` | `codex_dispatch` 的默认 sandbox(`resume` 时无效) |
304
- | `codexTimeoutMs` | `300000` | 单次 `codex_dispatch` 超时,超时先 SIGTERM 再 SIGKILL |
305
-
306
- 端口被占用时插件不会静默换端口:端到端文件里会写入 `error` 字段,并在宿主日志打印 `subagent-bridge: could not start on port …`。
247
+ | `maxWorkersPerRoot` | `4` | **每个工作目录**的 worker 上限 |
248
+ | `maxWorkers` | `12` | 全局上限 |
249
+ | `workerTtlMs` | `86400000` | 记录过期后不再占名额 |
250
+ | `waitTimeoutMs` | `240000` | `dsh_wait` 默认超时 |
251
+ | `readWaitSeconds` | `25` | `dsh_read_result` 默认长轮询时长 |
252
+ | `codexBin` | `codex` | Codex 可执行文件;含 `/` 时严格按路径解析 |
253
+ | `codexSandbox` | `workspace-write` | `codex_dispatch` 默认 sandbox |
254
+ | `codexTimeoutMs` | `300000` | 单次 `codex_dispatch` 超时 |
255
+ | `claudeBin` | `claude` | Claude Code 可执行文件 |
256
+ | `claudeModel` | 空 | 默认让 Claude CLI 用自己的设置 |
257
+ | `claudeTimeoutMs` | `300000` | 单次 `claude_dispatch` 超时 |
258
+
259
+ 端口被占用时不会静默换端口:端点文件里写入 `error` 字段,宿主日志打印 `subagent-bridge: could not start on port …`。
307
260
 
308
261
  ---
309
262
 
310
- ## 防环护栏
311
-
312
- 两个 agent 都接了同一个桥时,存在真实的环路:DSH worker → `codex_dispatch` → 那次 Codex 运行也能看到 `dsh-bridge` → 又 `dsh_spawn` → …。两边**各自进程内**的深度规则都管不到这条链(Codex 的 `agent_max_depth` 只数自己的 `spawn_agent`,DSH 的 `maxDepth` 只看 `delegationDepth`,而桥建的 worker 是顶层会话,深度恒为 0)。
263
+ ## 护栏与限制
313
264
 
314
- 所以桥自带两道:
265
+ ### 防环
315
266
 
316
- **① 深度(`maxHops`,默认 1)**
267
+ 两个 agent 都接了同一个桥时存在真实环路:DSH worker → `codex_dispatch` → 那次 Codex 也能看到 `dsh-bridge` → 又 `dsh_spawn` → …。双方进程内的深度规则都管不到这条链(桥建的 worker 是顶层会话,深度恒为 0)。
317
268
 
318
- - 每次都计算这次派发会产生的深度:`max(当前在飞的链路深度, 调用方深度) + 1`;
319
- - 调用方是桥建的 worker 时按其深度 1 起算;
320
- - `codex_dispatch` 运行期间会把链路标记为"在飞",嵌套回来的调用继承该深度;
321
- - 超限直接报错,措辞可执行:
322
- `codex_dispatch would create delegation depth 2, but maxHops is 1. Carry the work out directly instead of delegating again.`
269
+ **① 深度(`maxHops`,默认 1)**:每次派发都计算 `max(在飞链路深度, 调用方深度) + 1`,超限直接报错:
323
270
 
324
- 默认 1 的含义:**Codex 可以派给 DSH,但 DSH 不能再用桥派回去** —— 与两个产品各自的 `maxDepth: 1` 语义对齐。
325
-
326
- **② 速率(`dispatchPerMinute`,默认 10 / 60s)**
271
+ ```
272
+ codex_dispatch would create delegation depth 2, but maxHops is 1.
273
+ Carry the work out directly instead of delegating again.
274
+ ```
327
275
 
328
- 拦截广度型失控(一次开很多 worker)和客户端紧循环重试,与提示词无关,是纯兜底:
276
+ **② 速率(`dispatchPerMinute`,默认 10)**:拦截广度型失控与紧循环重试:
329
277
 
330
278
  ```
331
279
  rate limit: 10 dispatches per 60s reached; retry in 23s
332
280
  ```
333
281
 
334
- ## 名额怎么算
335
-
336
- worker 名额**按工作目录分桶**,调用方不用传任何 owner:
282
+ ### 名额
337
283
 
338
- - 每个 `cwd` 默认 **4 个**(`maxWorkersPerRoot`);
339
- - 全局默认 **12 个**(`maxWorkers`),跨项目兜底;
340
- - **完成但没 `dsh_close` 的仍占名额**(与 Codex 一致);
341
- - 记录 **24 小时**没更新就自动不再占名额(`workerTtlMs`),不需要手动清理;
284
+ - 每个 `cwd` 默认 **4 个**(`maxWorkersPerRoot`),全局 **12 个**(`maxWorkers`);
285
+ - **完成但没 `dsh_close` 的仍占名额**;
286
+ - 记录 **24 小时**没更新自动释放;
342
287
  - 阻塞式 `dsh_task` 不计入名额,只登记。
343
288
 
344
- 为什么不是"每个会话 4 个":MCP 端点是共享的,请求里没有会话或线程身份(Codex 的 `CODEX_THREAD_ID` 只存在于 shell 环境)。`cwd` 是唯一由调用方稳定提供、且语义上对应"一个项目"的维度。
289
+ 按 `cwd` 分桶而不是按会话,是因为 MCP 端点共享、请求里没有会话身份;`cwd` 是唯一由调用方稳定提供、且语义上对应「一个项目」的维度。
345
290
 
346
- ## 跨重启
291
+ ### 跨重启
347
292
 
348
- worker 注册表(`task_name` / `sessionId` / 终态结果)和每轮的时间基线都落在 `~/.dsh/bridge/workers.json`:
293
+ worker 注册表和每轮的时间基线落在 `~/.dsh/bridge/workers.json`:
349
294
 
350
- - 派发、完成、关闭都会更新它;
351
- - **重启后 `dsh_list_workers` 仍能看到之前的 worker**;
352
- - 已经结束的 worker,`dsh_wait` 直接返回缓存里的最终回答(不必重新打开会话);
353
- - 重启时还在跑的 worker,会话未打开时状态是 `offline`(结果未知,去 DSH 侧边栏打开那个会话即可查看);
354
- - `dsh_followup` 会给 worker 开新一轮,同时**清掉**缓存的旧回答;
355
- - 文件最多保留 200 条,超出时先淘汰最旧的 **已关闭** worker。
295
+ - 重启后 `dsh_list_workers` 仍能看到之前的 worker;
296
+ - 已结束的 worker,`dsh_wait` 直接返回缓存里的最终回答;
297
+ - 重启时还在跑的,会话未打开时状态为 `offline`;
298
+ - `dsh_followup` 会开新一轮并清掉旧回答;
299
+ - 最多保留 200 条,超出先淘汰最旧的已关闭 worker。
356
300
 
357
- ## 安全模型
301
+ ### 安全模型
358
302
 
359
- - 只监听 `127.0.0.1`,不对外暴露。
360
- - 请求必须携带令牌:路径 `/mcp/<token>`,或 `Authorization: Bearer <token>`;比较使用定长时间比较。
361
- - 令牌首次启动时生成并**持久化**(重启后复用),因此能力 URL 稳定。
362
- - 能力等价于「向指定会话投递消息」:本机任意进程只要能读到端点文件就能派活。文件权限 0600、目录 0700;不要把它复制到共享位置。
363
- - DSH 侧的权限策略不受影响:派发进去的消息仍按会话自身的 sandbox / 审批策略执行。
303
+ - 只监听 `127.0.0.1`;
304
+ - 请求必须带令牌:路径 `/mcp/<token>` 或 `Authorization: Bearer <token>`,定长时间比较;
305
+ - 令牌首次启动生成并持久化,能力 URL 稳定;
306
+ - 能力等价于「向指定会话投递消息」:能读到端点文件的进程就能派活,文件 0600、目录 0700;
307
+ - DSH 侧的权限策略不受影响,派进去的消息仍按会话自身的 sandbox / 审批策略执行。
364
308
 
365
309
  ---
366
310
 
@@ -373,22 +317,22 @@ Codex / 任意 MCP 客户端
373
317
  dsh-plugin-subagent-bridge(宿主进程内)
374
318
  · node:http 监听 127.0.0.1:47821
375
319
  · initialize / tools/list / tools/call / ping
376
- · sessionController.prompt({sessionId, requestId, mode, content})
320
+ · sessionController.prompt({ sessionId, requestId, mode, content })
377
321
  · sessions.get(id).snapshotEvents() 读回助手文本
378
322
  ```
379
323
 
380
- 协议实现为 **streamable HTTP 的无状态子集**:每个 POST 返回一段 `application/json`,不做服务端推送,因此取结果用**长轮询**——`dsh_read_result` 在服务端等,客户端一次调用即可拿到答案;客户端断开时服务端立即停止等待。
324
+ 协议是 **streamable HTTP 的无状态子集**:每个 POST 返回一段 `application/json`,不做服务端推送,因此取结果用**长轮询**——`dsh_read_result` 在服务端等,客户端一次调用即可拿到答案;客户端断开时服务端立即停止等待。
381
325
 
382
326
  ---
383
327
 
384
328
  ## 配套 skill
385
329
 
386
- `skills/` 下有两份可直接安装的 skill,把"什么时候该派活、怎么派、怎么收尾"写成模型能读的规则:
330
+ `skills/` 下有两份可直接安装的 skill,把「什么时候该派活、怎么派、怎么收尾」写成模型能读的规则:
387
331
 
388
332
  | Skill | 装给谁 | 作用 |
389
333
  | --- | --- | --- |
390
334
  | `deepseek-worker` | Codex | 让 Codex 把任务当 subagent 派给 DSH(`dsh_task`),一次调用等结果 |
391
- | `delegate-to-external-agent` | DSH | 让 DSH 的 agent 在需要独立视角/复核时派给外部 agent:Codex(`codex_dispatch`)或 Claude Code(`claude_dispatch`),含两者的差别与续接方式 |
335
+ | `delegate-to-external-agent` | DSH | 让 DSH 的 agent 在需要独立视角/复核时派给 Codex 或 Claude Code,含两者差别与续接方式 |
392
336
 
393
337
  ```bash
394
338
  # Codex 侧
@@ -398,17 +342,15 @@ mkdir -p ~/.codex/skills && cp -R skills/deepseek-worker ~/.codex/skills/
398
342
  mkdir -p ~/.dsh/skills && cp -R skills/delegate-to-external-agent ~/.dsh/skills/
399
343
  ```
400
344
 
401
- 两份都是标准 `SKILL.md`:YAML frontmatter(`name` + `description`)+ 正文。**触发靠 description**——模型平时只看到名字和描述,判断相关了才读正文,所以描述里写满了中英触发语和"不要用"的边界。改完立刻生效,不用重启:Codex 下一次会话、DSH 下一轮就能看到。
345
+ 两份都是标准 `SKILL.md`:YAML frontmatter(`name` + `description`)+ 正文。**触发靠 description**——模型平时只看到名字和描述,判断相关了才读正文,所以描述里写满了中英触发语和「不要用」的边界。改完立刻生效,不用重启。
402
346
 
403
347
  ---
404
348
 
405
349
  ## 兼容性
406
350
 
407
- - DSH `^0.2.1-0`(`engines.dsh`)。
408
- - MCP 协议版本:`2025-06-18`、`2025-03-26`、`2024-11-05`(按客户端请求回显,未知版本回落到最新)。
409
- - 依赖宿主公开服务 `sessionController`(`list` / `prompt`)与 `sessions.get()`;两者不可用时工具会返回明确错误。
410
-
411
- ---
351
+ - DSH `^0.2.1-0`(`engines.dsh`);
352
+ - MCP 协议版本:`2025-06-18`、`2025-03-26`、`2024-11-05`(按客户端请求回显,未知版本回落到最新);
353
+ - 依赖宿主公开服务 `sessionController`、`sessions`、`workspaceRegistry`、`userQuestions`;不可用时工具返回明确错误。
412
354
 
413
355
  ## 开发
414
356
 
@@ -416,8 +358,9 @@ mkdir -p ~/.dsh/skills && cp -R skills/delegate-to-external-agent ~/.dsh/skills/
416
358
  node --test "test/*.test.mjs"
417
359
  ```
418
360
 
419
- 58 项:握手与版本协商、通知处理、工具清单、未知方法与未知工具、参数校验、元数据列表不含消息内容、派发调用形状、拒绝与错误透出、结果读取的时间基线、未派发会话的拒绝、长轮询(文本晚到即返回 / 超时 / 调用方取消)、真实 HTTP 往返(含 404/405/202 与 bearer 分支)、`codex exec` 事件解析、二进制解析、成功/失败/超时三种运行结果、resume 的参数分叉、工具结果的 lossless JSON 约束、**只回最终回答**(多步叙述被丢弃)、`dsh_wait` 的任一完成 / 超时 / 错误 / 未派发四种状态、`dsh_spawn` 立即返回与注册、`fork_turns=all` 走 fork、并发上限与 `dsh_close` 释放名额、`dsh_followup` 的重基线与最终回答切换、注册表落盘与重启后复原、冷会话下的缓存回答与 `offline` 状态、注册表 200 条上限的淘汰顺序、`maxHops` 拒绝回派而放行首层、在飞链路阻塞嵌套派发、速率突发的拒绝与窗口过期、followup 计入同一预算、握手版本号取自 `package.json`(不再硬编码)、新建的 worker 会按 `cwd` 自动归入对应工作区(不再落到「未分组」)、阻塞式 `dsh_task` 不占 spawn 名额(重启后也不占)、按 `cwd` 分桶的每项目预算与全局上限、过期记录到期释放名额、超时返回 `running: true` 与续等指引、阻塞时长被夹到 240 秒以内。
420
- `test/persistence.test.mjs` 单独覆盖持久化:文件权限 0600、缺失/损坏/畸形记录、spawn 返回时文件已落盘、完成即缓存、followup 清缓存、close 后重启仍为 closed、冷会话走缓存或 `offline`、并发写不串。
361
+ 83 项,覆盖握手与版本协商、工具清单与参数校验、派发调用形状、结果读取的时间基线、长轮询、真实 HTTP 往返(含 404/405/202 与 bearer 分支)、`codex exec` 事件解析与二进制解析、成功/失败/超时三种运行结果、resume 的参数分叉、只回最终回答、`dsh_wait` 的四种终态、并发上限与释放、followup 重基线、注册表落盘与重启复原、`maxHops` 与速率限制、按 `cwd` 分桶的预算、工作区归属、`workspace` 创建与复用、`queue` / `steer` 的投递与等待、插话回答的定位、`claude` 输出解析与 resume 参数、`assumeDefaults` 指令注入、`pendingQuestion` 与 `dsh_answer`。
362
+
363
+ `test/persistence.test.mjs` 单独覆盖持久化:文件权限、缺失/损坏记录、并发写、冷会话缓存与 `offline`。
421
364
 
422
365
  ---
423
366
 
@@ -425,12 +368,11 @@ node --test "test/*.test.mjs"
425
368
 
426
369
  | 插件 | 说明 |
427
370
  | --- | --- |
371
+ | **dsh-plugin-subagent-bridge**(本仓库) | 让 Codex 通过本机 MCP 把 DSH 当 subagent 用,反向也能派给 Codex / Claude Code |
428
372
  | [dsh-plugin-composer-predict](https://github.com/Dogtiti/dsh-plugin-composer-predict) | 预测下一条指令并显示为占位文字,`Tab` 采纳 |
429
373
  | [dsh-plugin-composer-history](https://github.com/Dogtiti/dsh-plugin-composer-history) | 输入框中按 `↑` / `↓` 回填历史消息 |
430
- | **dsh-plugin-subagent-bridge**(本仓库) | 让 Codex 通过本机 MCP 把 DSH 当 subagent 用(spawn / wait / followup),反向也能把任务派给 Codex |
431
- | [dsh-plugin-usage-panel](https://github.com/Dogtiti/dsh-plugin-usage-panel) | 在模型选择器旁显示供应商套餐额度余量(目前 GLM Coding Plan),点开是明细浮层 |
432
- | [dsh-plugin-provider-icons](https://github.com/Dogtiti/dsh-plugin-provider-icons) | 在模型名旁显示供应商品牌 logo(24 个厂商,支持前缀匹配) |
433
-
374
+ | [dsh-plugin-usage-panel](https://github.com/Dogtiti/dsh-plugin-usage-panel) | 在模型选择器旁显示供应商套餐额度余量 |
375
+ | [dsh-plugin-provider-icons](https://github.com/Dogtiti/dsh-plugin-provider-icons) | 在模型名旁显示供应商品牌 logo |
434
376
 
435
377
  ## 许可
436
378
 
package/index.js CHANGED
@@ -75,6 +75,7 @@ const DEFAULTS = {
75
75
  codexBin: 'codex',
76
76
  codexSandbox: 'workspace-write',
77
77
  codexTimeoutMs: 300000,
78
+ assumeDefaults: true,
78
79
  claudeBin: 'claude',
79
80
  claudeModel: '',
80
81
  claudeTimeoutMs: 300000,
@@ -309,6 +310,28 @@ const TOOLS = [
309
310
  },
310
311
  ]
311
312
 
313
+ /**
314
+ * Standing instruction appended to a delegated task.
315
+ *
316
+ * A worker that stops to ask a question needs somebody at the UI; when the caller is
317
+ * another agent, nobody is. The caller would rather have a reasonable assumption and a
318
+ * finished result, so the worker is told to make one and report it.
319
+ */
320
+ const ASSUMPTION_POLICY =
321
+ '如果缺少必要信息或需要在多个方案里选择,直接采用最合理的默认做法继续做完,不要把选择权抛回给用户、不要调用 ask_user_question 等回答;在结果里用一行说明你假设了什么。'
322
+
323
+ /**
324
+ * Attach the standing instruction when the deployment wants it.
325
+ * @param settings - resolved settings.
326
+ * @param message - caller-supplied text.
327
+ * @returns the text to deliver.
328
+ */
329
+ function withAssumptionPolicy(settings, message) {
330
+ if (settings.assumeDefaults !== true) return message
331
+ if (message.includes('ask_user_question')) return message
332
+ return `${message}\n\n(${ASSUMPTION_POLICY})`
333
+ }
334
+
312
335
  /**
313
336
  * Take over the questions a worker asks, so the calling agent can answer them.
314
337
  *
@@ -965,7 +988,7 @@ export function createApi(ctx, state, settings) {
965
988
  const before = Number(sessions()?.get?.(sessionId)?.seq ?? 0)
966
989
  const requestId = randomUUID()
967
990
  const accepted = await service.prompt(
968
- { sessionId, requestId, mode: mode === 'steer' ? 'steer' : 'queue', content: [{ type: 'text', text: message }] },
991
+ { sessionId, requestId, mode: mode === 'steer' ? 'steer' : 'queue', content: [{ type: 'text', text: withAssumptionPolicy(settings, message) }] },
969
992
  AbortSignal.timeout(30000),
970
993
  )
971
994
  state.dispatched.add(sessionId)
@@ -1000,7 +1023,7 @@ export function createApi(ctx, state, settings) {
1000
1023
  const before = Number(sessions()?.get?.(target.sessionId)?.seq ?? 0)
1001
1024
  const requestId = randomUUID()
1002
1025
  const accepted = await service.prompt(
1003
- { sessionId: target.sessionId, requestId, mode: mode === 'steer' ? 'steer' : 'queue', content: [{ type: 'text', text: message }] },
1026
+ { sessionId: target.sessionId, requestId, mode: mode === 'steer' ? 'steer' : 'queue', content: [{ type: 'text', text: withAssumptionPolicy(settings, message) }] },
1004
1027
  AbortSignal.timeout(30000),
1005
1028
  )
1006
1029
  state.baseline.set(target.sessionId, before)
@@ -1079,7 +1102,7 @@ export function createApi(ctx, state, settings) {
1079
1102
  const finished = waitForTurnEnd(ctx, targetId, waitAfter, seconds * 1000)
1080
1103
  const requestId = randomUUID()
1081
1104
  const accepted = await service.prompt(
1082
- { sessionId: targetId, requestId, mode: deliveryMode, content: [{ type: 'text', text: prompt }] },
1105
+ { sessionId: targetId, requestId, mode: deliveryMode, content: [{ type: 'text', text: withAssumptionPolicy(settings, prompt) }] },
1083
1106
  AbortSignal.timeout(30000),
1084
1107
  )
1085
1108
  state.dispatched.add(targetId)
@@ -2309,6 +2332,10 @@ function resolveSettings(config) {
2309
2332
  }
2310
2333
  if (key === 'codexBin' && typeof value === 'string' && value !== '') merged.codexBin = value
2311
2334
  if (key === 'codexSandbox' && typeof value === 'string' && value !== '') merged.codexSandbox = value
2335
+ if (key === 'assumeDefaults') {
2336
+ merged.assumeDefaults = value === true || value === 'true'
2337
+ continue
2338
+ }
2312
2339
  if (key === 'claudeBin' && typeof value === 'string' && value !== '') merged.claudeBin = value
2313
2340
  if (key === 'claudeModel' && typeof value === 'string') merged.claudeModel = value
2314
2341
  if (key === 'workersFile' && typeof value === 'string' && value !== '') merged.workersFile = expandHome(value)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-subagent-bridge",
3
- "version": "0.14.2",
3
+ "version": "0.15.1",
4
4
  "type": "module",
5
5
  "description": "Subagent bridge: lets Codex delegate a task to DeepSeek Harness the way it would spawn a subagent (one call, waits for the answer), plus a codex_dispatch tool for the reverse direction.",
6
6
  "keywords": [