dsh-plugin-subagent-bridge 0.8.1 → 0.9.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.
- package/README.md +24 -0
- package/index.js +59 -7
- package/package.json +1 -1
- package/skills/deepseek-worker/SKILL.md +22 -0
package/README.md
CHANGED
|
@@ -11,6 +11,30 @@
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## 创建项目(workspace)
|
|
15
|
+
|
|
16
|
+
DSH 的侧边栏分组是"项目(workspace)",和会话的工作目录是两回事:新建会话默认落在「未分组」,除非它所在的目录已经是一个项目。
|
|
17
|
+
|
|
18
|
+
`dsh_task` / `dsh_spawn` 的 `workspace` 参数把这一步也接管了:
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
// 目录不是项目 → 用它创建项目(标题取目录名),会话直接归到项目下
|
|
22
|
+
dsh_task({ prompt: "…", cwd: "/repo/clawer", workspace: true })
|
|
23
|
+
|
|
24
|
+
// 用指定标题创建
|
|
25
|
+
dsh_spawn({ task_name: "w1", message: "…", cwd: "/repo/clawer", workspace: "Clawer" })
|
|
26
|
+
|
|
27
|
+
// 不传 → 只复用已有项目;目录没有项目就保持未分组(和以前一样)
|
|
28
|
+
dsh_task({ prompt: "…", cwd: "/repo/clawer" })
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
行为细节:
|
|
32
|
+
|
|
33
|
+
- 目录不存在时先递归创建,再做项目登记(登记要求目录已存在);
|
|
34
|
+
- 目录已经是项目 → **直接复用,不改标题**(底层 `create` 对同一路径返回已有记录);
|
|
35
|
+
- 返回结果里带上 `workspace: { id, title, path }`,方便确认落在哪个项目;
|
|
36
|
+
- 项目操作失败不影响派发本身(分组是外观,不因此让任务失败)。
|
|
37
|
+
|
|
14
38
|
## 指定模型
|
|
15
39
|
|
|
16
40
|
`dsh_task`、`dsh_spawn`、`dsh_followup` 都可以带 `model`,让 worker 跑在指定模型上:
|
package/index.js
CHANGED
|
@@ -99,6 +99,11 @@ const TOOLS = [
|
|
|
99
99
|
description:
|
|
100
100
|
'Model to run this worker on, as "provider/model" (for example "zai-coding-cn/glm-5.3"). A bare model id keeps the current default provider. Omit to use whatever the session already uses.',
|
|
101
101
|
},
|
|
102
|
+
workspace: {
|
|
103
|
+
type: ['boolean', 'string'],
|
|
104
|
+
description:
|
|
105
|
+
'Grouping for the new session. true creates a DSH project (workspace) for `cwd`, titled after its last path segment, when none owns that directory yet; a string uses that title instead. Omit (or false) to only reuse an existing project, leaving the session ungrouped when the directory has none.',
|
|
106
|
+
},
|
|
102
107
|
reasoningEffort: {
|
|
103
108
|
type: 'string',
|
|
104
109
|
description: 'Reasoning level for `model`, if that model offers levels (low, medium, high, max...).',
|
|
@@ -130,6 +135,11 @@ const TOOLS = [
|
|
|
130
135
|
description:
|
|
131
136
|
'Model for this worker, as "provider/model" (for example "opencode-go/deepseek-v4.1-flash"). A bare model id keeps the current default provider.',
|
|
132
137
|
},
|
|
138
|
+
workspace: {
|
|
139
|
+
type: ['boolean', 'string'],
|
|
140
|
+
description:
|
|
141
|
+
'Grouping for the new worker. true creates a DSH project (workspace) for `cwd` when none owns it; a string uses that title. Omit to only reuse an existing project.',
|
|
142
|
+
},
|
|
133
143
|
reasoningEffort: { type: 'string', description: 'Reasoning level for `model`, when that model offers levels.' },
|
|
134
144
|
},
|
|
135
145
|
required: ['task_name', 'message'],
|
|
@@ -512,6 +522,7 @@ async function callTool(id, params, api, signal) {
|
|
|
512
522
|
forkTurns,
|
|
513
523
|
from: args.from,
|
|
514
524
|
cwd: args.cwd,
|
|
525
|
+
workspace: args.workspace === true || (typeof args.workspace === 'string' && args.workspace !== '') ? args.workspace : undefined,
|
|
515
526
|
model: typeof args.model === 'string' && args.model !== '' ? args.model : undefined,
|
|
516
527
|
reasoningEffort:
|
|
517
528
|
typeof args.reasoningEffort === 'string' && args.reasoningEffort !== '' ? args.reasoningEffort : undefined,
|
|
@@ -565,6 +576,7 @@ async function callTool(id, params, api, signal) {
|
|
|
565
576
|
cwd: typeof args.cwd === 'string' && args.cwd !== '' ? args.cwd : undefined,
|
|
566
577
|
sessionId,
|
|
567
578
|
agentPreset: typeof args.agentPreset === 'string' && args.agentPreset !== '' ? args.agentPreset : undefined,
|
|
579
|
+
workspace: args.workspace === true || (typeof args.workspace === 'string' && args.workspace !== '') ? args.workspace : undefined,
|
|
568
580
|
model: typeof args.model === 'string' && args.model !== '' ? args.model : undefined,
|
|
569
581
|
reasoningEffort:
|
|
570
582
|
typeof args.reasoningEffort === 'string' && args.reasoningEffort !== '' ? args.reasoningEffort : undefined,
|
|
@@ -639,7 +651,7 @@ export function createApi(ctx, state, settings) {
|
|
|
639
651
|
})
|
|
640
652
|
},
|
|
641
653
|
|
|
642
|
-
async spawn({ taskName, message, forkTurns, from, cwd, model, reasoningEffort }) {
|
|
654
|
+
async spawn({ taskName, message, forkTurns, from, cwd, model, reasoningEffort, workspace }) {
|
|
643
655
|
const service = controller()
|
|
644
656
|
if (service === undefined || service === null) throw new Error('session controller is unavailable')
|
|
645
657
|
if (state.workers.has(taskName)) throw new Error(`task_name ${JSON.stringify(taskName)} is already in use`)
|
|
@@ -648,6 +660,7 @@ export function createApi(ctx, state, settings) {
|
|
|
648
660
|
takeDispatchSlot(state, settings)
|
|
649
661
|
assertCapacity(state, settings, root)
|
|
650
662
|
|
|
663
|
+
let placed
|
|
651
664
|
let sessionId
|
|
652
665
|
if (forkTurns === 'all') {
|
|
653
666
|
const parent = sessions()?.get?.(from)
|
|
@@ -660,7 +673,9 @@ export function createApi(ctx, state, settings) {
|
|
|
660
673
|
} else {
|
|
661
674
|
const opened = await service.create(cwd === undefined ? {} : { cwd })
|
|
662
675
|
sessionId = opened?.sessionId
|
|
663
|
-
if (typeof sessionId === 'string' && sessionId !== '')
|
|
676
|
+
if (typeof sessionId === 'string' && sessionId !== '') {
|
|
677
|
+
placed = await attachToWorkspace(ctx, sessionId, cwd ?? undefined, workspaceOptions(workspace))
|
|
678
|
+
}
|
|
664
679
|
if (typeof sessionId !== 'string' || sessionId === '') throw new Error('session controller did not return a session id')
|
|
665
680
|
}
|
|
666
681
|
|
|
@@ -689,6 +704,7 @@ export function createApi(ctx, state, settings) {
|
|
|
689
704
|
accepted: accepted?.accepted === true,
|
|
690
705
|
fork_turns: forkTurns,
|
|
691
706
|
...(appliedModel === undefined ? {} : { model: appliedModel }),
|
|
707
|
+
...(placed === undefined ? {} : { workspace: placed }),
|
|
692
708
|
}
|
|
693
709
|
},
|
|
694
710
|
|
|
@@ -741,7 +757,7 @@ export function createApi(ctx, state, settings) {
|
|
|
741
757
|
return rows
|
|
742
758
|
},
|
|
743
759
|
|
|
744
|
-
async task({ prompt, cwd, sessionId, agentPreset, timeoutSeconds, model, reasoningEffort }) {
|
|
760
|
+
async task({ prompt, cwd, sessionId, agentPreset, timeoutSeconds, model, reasoningEffort, workspace }) {
|
|
745
761
|
const service = controller()
|
|
746
762
|
if (service === undefined || service === null) throw new Error('session controller is unavailable')
|
|
747
763
|
const seconds = Math.min(
|
|
@@ -752,12 +768,15 @@ export function createApi(ctx, state, settings) {
|
|
|
752
768
|
takeDispatchSlot(state, settings)
|
|
753
769
|
const started = Date.now()
|
|
754
770
|
|
|
771
|
+
let placed
|
|
755
772
|
let targetId = sessionId
|
|
756
773
|
let created = false
|
|
757
774
|
if (targetId === undefined) {
|
|
758
775
|
const opened = await service.create(cwd === undefined ? {} : { cwd })
|
|
759
776
|
targetId = opened?.sessionId
|
|
760
|
-
if (typeof targetId === 'string' && targetId !== '')
|
|
777
|
+
if (typeof targetId === 'string' && targetId !== '') {
|
|
778
|
+
placed = await attachToWorkspace(ctx, targetId, cwd ?? undefined, workspaceOptions(workspace))
|
|
779
|
+
}
|
|
761
780
|
if (typeof targetId !== 'string' || targetId === '') throw new Error('session controller did not return a session id')
|
|
762
781
|
created = true
|
|
763
782
|
} else if (ctx.get('agents')?.get?.(targetId)?.status === 'running') {
|
|
@@ -808,6 +827,7 @@ export function createApi(ctx, state, settings) {
|
|
|
808
827
|
}
|
|
809
828
|
: {}),
|
|
810
829
|
...(appliedModel === undefined ? {} : { model: appliedModel }),
|
|
830
|
+
...(placed === undefined ? {} : { workspace: placed }),
|
|
811
831
|
text: collected.text,
|
|
812
832
|
truncated: collected.truncated,
|
|
813
833
|
turns: lastTurn(ctx, targetId),
|
|
@@ -1132,6 +1152,20 @@ async function applyModelSpec(ctx, sessionId, spec, reasoningEffort) {
|
|
|
1132
1152
|
}
|
|
1133
1153
|
}
|
|
1134
1154
|
|
|
1155
|
+
/**
|
|
1156
|
+
* Translate the `workspace` argument into attach options.
|
|
1157
|
+
* - absent / `false` → never create, only reuse an existing project for the directory
|
|
1158
|
+
* - `true` → create a project titled after the directory when none owns it
|
|
1159
|
+
* - a string → same, with that title
|
|
1160
|
+
* @param workspace - raw argument from the tool call.
|
|
1161
|
+
* @returns options for {@link attachToWorkspace}.
|
|
1162
|
+
*/
|
|
1163
|
+
function workspaceOptions(workspace) {
|
|
1164
|
+
if (workspace === true) return { create: true }
|
|
1165
|
+
if (typeof workspace === 'string' && workspace.trim() !== '') return { create: true, title: workspace.trim() }
|
|
1166
|
+
return {}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1135
1169
|
/**
|
|
1136
1170
|
* Place a freshly created session in the workspace its directory belongs to.
|
|
1137
1171
|
* `sessionController.create` only records a working directory; the sidebar groups
|
|
@@ -1140,14 +1174,32 @@ async function applyModelSpec(ctx, sessionId, spec, reasoningEffort) {
|
|
|
1140
1174
|
* @param sessionId - session just created.
|
|
1141
1175
|
* @param cwd - directory the session was opened in.
|
|
1142
1176
|
*/
|
|
1143
|
-
async function attachToWorkspace(ctx, sessionId, cwd) {
|
|
1144
|
-
if (typeof cwd !== 'string' || cwd === '') return
|
|
1177
|
+
async function attachToWorkspace(ctx, sessionId, cwd, options = {}) {
|
|
1178
|
+
if (typeof cwd !== 'string' || cwd === '') return undefined
|
|
1145
1179
|
try {
|
|
1146
1180
|
const registry = ctx.get('workspaceRegistry')
|
|
1147
|
-
|
|
1181
|
+
if (registry === undefined || registry === null) return undefined
|
|
1182
|
+
|
|
1183
|
+
/* Only create when the caller asked for it: an existing project is reused either way,
|
|
1184
|
+
* because `create` returns the record that already owns the canonical path. */
|
|
1185
|
+
let workspace
|
|
1186
|
+
if (options.create === true && typeof registry.create === 'function') {
|
|
1187
|
+
/* The registry rejects a directory that does not exist yet, and a brand-new project
|
|
1188
|
+
* is exactly that, so the path is materialised first. */
|
|
1189
|
+
await mkdir(cwd, { recursive: true }).catch(() => {})
|
|
1190
|
+
const title = typeof options.title === 'string' && options.title.trim() !== '' ? options.title.trim() : undefined
|
|
1191
|
+
workspace = await registry.create(cwd, title)
|
|
1192
|
+
} else {
|
|
1193
|
+
workspace = await registry.resolveByPath?.(cwd)
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1148
1196
|
await workspace?.attachSession?.(sessionId)
|
|
1197
|
+
return workspace === undefined || workspace === null
|
|
1198
|
+
? undefined
|
|
1199
|
+
: { id: workspace.id, title: workspace.title, path: workspace.path }
|
|
1149
1200
|
} catch {
|
|
1150
1201
|
/* Grouping is cosmetic: a failure here must not fail the dispatch. */
|
|
1202
|
+
return undefined
|
|
1151
1203
|
}
|
|
1152
1204
|
}
|
|
1153
1205
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-subagent-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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": [
|
|
@@ -148,6 +148,28 @@ DSH 自己的委派深度上限是 1:worker(depth 0)能开子 agent(dept
|
|
|
148
148
|
写错了(`/glm-5.3`、`a/b/c`)桥会在**发 prompt 之前**报错,不会白跑一轮。
|
|
149
149
|
|
|
150
150
|
|
|
151
|
+
## 新建项目:`workspace`
|
|
152
|
+
|
|
153
|
+
DSH 侧边栏的分组叫「项目(workspace)」,和会话的工作目录是两回事。桥默认只**复用**已有项目:`cwd` 恰好是一个项目的目录,会话就归进去;否则落在「未分组」。
|
|
154
|
+
|
|
155
|
+
要在 DSH 里**新建**一个和 Codex 这边对等的项目,加 `workspace`:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{ "prompt": "…", "cwd": "/repo/clawer", "workspace": true }
|
|
159
|
+
{ "prompt": "…", "cwd": "/repo/clawer", "workspace": "Clawer" }
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
- `true` → 用 `cwd` 建项目,标题取目录名;
|
|
163
|
+
- 字符串 → 同上,但用这个标题;
|
|
164
|
+
- 目录已是项目 → **复用,不改标题**,不会重复创建;
|
|
165
|
+
- 目录不存在 → 先建目录再登记项目;
|
|
166
|
+
- 不传 → 只复用,没有项目就未分组(默认行为)。
|
|
167
|
+
|
|
168
|
+
**什么时候传**:用户说「创建一个 xxx 项目」「在 DSH 里也建一个」「别让它落到未分组」时。单纯派活、目录已经是项目时不用传——传了也只是复用。
|
|
169
|
+
|
|
170
|
+
返回里带 `workspace: { id, title, path }`,可以据此确认落在哪个项目下。
|
|
171
|
+
|
|
172
|
+
|
|
151
173
|
## 怎么写 prompt
|
|
152
174
|
|
|
153
175
|
worker **看不到你这段对话**,它只有 `prompt` 和 `cwd`。写成可以独立开工的工单:
|