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 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 !== '') await attachToWorkspace(ctx, sessionId, cwd ?? undefined)
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 !== '') await attachToWorkspace(ctx, targetId, cwd ?? undefined)
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
- const workspace = await registry?.resolveByPath?.(cwd)
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.8.1",
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`。写成可以独立开工的工单: