@whaletalk/dsh-codex-collab 0.1.1 → 0.1.3
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 +34 -5
- package/codex-plugin/plugins/dsh/.codex-plugin/plugin.json +1 -1
- package/codex-plugin/plugins/dsh/skills/dsh-collab/SKILL.md +50 -21
- package/codex-plugin/plugins/dsh/skills/dsh-collab/scripts/task.mjs +68 -15
- package/harness/dsh-bridge.mjs +477 -44
- package/harness/session-target.mjs +208 -0
- package/mcp/dsh-mcp.mjs +47 -6
- package/package.json +1 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话目标的纯决策层。
|
|
3
|
+
*
|
|
4
|
+
* 这里不 import 任何 DSH 模块、不碰 IO,只做判定与文本提取,因此可以在
|
|
5
|
+
* node:test 里直接测。dsh-bridge.mjs 只负责把这里的结论接到真实服务上。
|
|
6
|
+
*
|
|
7
|
+
* 设计要点(对应"找到原对话"与"接通原对话"是两件事):
|
|
8
|
+
* - 目标显式:worker(新建,现有行为)与 session(接已有会话)是两个判别分支;
|
|
9
|
+
* - 无歧义才动手:查询命中 0 条 / 多条都不猜,返回稳定 reason 让调用方决定;
|
|
10
|
+
* - session 目标绝不静默降级为新建;
|
|
11
|
+
* - 所有失败都带稳定 reason,调用方不用解析中文文案。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** 全部稳定失败码。UI/CLI/MCP 都按这些码分支,不解析文案。 */
|
|
15
|
+
export const REASONS = {
|
|
16
|
+
INVALID_TARGET: 'invalid-target',
|
|
17
|
+
CONTROLLER_UNAVAILABLE: 'session-controller-unavailable',
|
|
18
|
+
QUERY_EMPTY: 'session-query-empty',
|
|
19
|
+
AMBIGUOUS: 'session-ambiguous',
|
|
20
|
+
NOT_FOUND: 'session-not-found',
|
|
21
|
+
BUSY: 'session-busy',
|
|
22
|
+
WRITER_HELD: 'session-writer-held',
|
|
23
|
+
ARCHIVED: 'session-archived',
|
|
24
|
+
NOT_ACCEPTED: 'session-not-accepted',
|
|
25
|
+
TIMEOUT: 'session-timeout',
|
|
26
|
+
CANCEL_REFUSED: 'session-cancel-refused',
|
|
27
|
+
BAD_REQUEST: 'session-bad-request',
|
|
28
|
+
GATEWAY_INTERNAL: 'session-gateway-internal',
|
|
29
|
+
DISPATCH_FAILED: 'session-dispatch-failed',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 候选列表回传给调用方时的上限,避免把整个会话库倒出去。 */
|
|
33
|
+
export const MAX_CANDIDATES = 10
|
|
34
|
+
|
|
35
|
+
const nonEmpty = (v) => typeof v === 'string' && v.trim() !== ''
|
|
36
|
+
|
|
37
|
+
/** 把文本块树拍平成纯文本(bridge 的既有语义,抽出来以便复用与单测)。 */
|
|
38
|
+
export const textOfBlocks = (blocks) => {
|
|
39
|
+
if (blocks === undefined || blocks === null) return ''
|
|
40
|
+
let out = ''
|
|
41
|
+
const list = Array.isArray(blocks) ? blocks : [blocks]
|
|
42
|
+
for (const b of list) {
|
|
43
|
+
if (b === undefined || b === null || typeof b !== 'object') continue
|
|
44
|
+
if (b.type === 'text' && typeof b.text === 'string') out += b.text
|
|
45
|
+
if (Array.isArray(b.content)) out += textOfBlocks(b.content)
|
|
46
|
+
}
|
|
47
|
+
return out
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 从一条会话事件里取助手文本;非助手消息返回空串。 */
|
|
51
|
+
export const assistantTextOf = (event) => {
|
|
52
|
+
if (event === undefined || event === null || event.type !== 'assistant/message') return ''
|
|
53
|
+
const data = event.data
|
|
54
|
+
const msg = data && data.message !== undefined ? data.message : data
|
|
55
|
+
const content = msg && msg.content !== undefined ? msg.content : data && data.content
|
|
56
|
+
return textOfBlocks(content)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 某会话的基线:事件条数 + 助手文本总长。派发前取一次,用来切分"这一轮的输出"。 */
|
|
60
|
+
export const baselineOf = (events) => {
|
|
61
|
+
const list = Array.isArray(events) ? events : []
|
|
62
|
+
let textLen = 0
|
|
63
|
+
for (const e of list) textLen += assistantTextOf(e).length
|
|
64
|
+
return { count: list.length, textLen }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 取出基线之后新增的助手文本。
|
|
69
|
+
* 返回新的基线,调用方直接替换即可(与 bridge 里 lastSeen/seenCount 的用法一致)。
|
|
70
|
+
*/
|
|
71
|
+
export const extractNewAssistantText = (events, baseline) => {
|
|
72
|
+
const list = Array.isArray(events) ? events : []
|
|
73
|
+
const base = baseline === undefined || baseline === null ? { count: 0, textLen: 0 } : baseline
|
|
74
|
+
let text = ''
|
|
75
|
+
for (const e of list) text += assistantTextOf(e)
|
|
76
|
+
const fresh = text.length > base.textLen ? text.slice(base.textLen) : ''
|
|
77
|
+
return { text: fresh, total: text, baseline: { count: list.length, textLen: text.length } }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 判定一次派发的目标。
|
|
82
|
+
*
|
|
83
|
+
* 接受两种书写:显式 `target` 对象(HTTP/原生工具),或扁平的 `sessionId`/`sessionQuery`
|
|
84
|
+
* 参数(MCP 工具更好用)。两者同时给出且互相矛盾时按 `invalid-target` 拒绝,绝不猜。
|
|
85
|
+
*
|
|
86
|
+
* @returns {{ok:true, target:object, deliver:'queue'|'steer', notes:string[]}}
|
|
87
|
+
* | {{ok:false, reason:string, message:string}}
|
|
88
|
+
*/
|
|
89
|
+
export const classifyTarget = (input) => {
|
|
90
|
+
const raw = input === undefined || input === null ? {} : input
|
|
91
|
+
const notes = []
|
|
92
|
+
const flatSessionId = nonEmpty(raw.sessionId) ? raw.sessionId.trim() : undefined
|
|
93
|
+
const flatQuery = nonEmpty(raw.sessionQuery) ? raw.sessionQuery.trim() : undefined
|
|
94
|
+
const t = raw.target
|
|
95
|
+
|
|
96
|
+
// ---- 显式 target 对象 ----
|
|
97
|
+
if (t !== undefined && t !== null) {
|
|
98
|
+
if (typeof t !== 'object' || Array.isArray(t)) {
|
|
99
|
+
return { ok: false, reason: REASONS.INVALID_TARGET, message: 'target must be an object' }
|
|
100
|
+
}
|
|
101
|
+
const kind = t.kind
|
|
102
|
+
if (kind === 'worker') {
|
|
103
|
+
if (nonEmpty(t.sessionId) || nonEmpty(t.query)) {
|
|
104
|
+
return { ok: false, reason: REASONS.INVALID_TARGET, message: 'worker target must not carry session fields' }
|
|
105
|
+
}
|
|
106
|
+
return { ok: true, target: { kind: 'worker', cwd: t.cwd, lane: t.lane }, deliver: 'queue', notes }
|
|
107
|
+
}
|
|
108
|
+
if (kind !== 'session') {
|
|
109
|
+
return { ok: false, reason: REASONS.INVALID_TARGET, message: 'target.kind must be worker or session' }
|
|
110
|
+
}
|
|
111
|
+
const id = nonEmpty(t.sessionId) ? t.sessionId.trim() : undefined
|
|
112
|
+
const query = nonEmpty(t.query) ? t.query.trim() : undefined
|
|
113
|
+
if (id !== undefined && query !== undefined) {
|
|
114
|
+
return { ok: false, reason: REASONS.INVALID_TARGET, message: 'session target takes sessionId or query, not both' }
|
|
115
|
+
}
|
|
116
|
+
if (id === undefined && query === undefined) {
|
|
117
|
+
return { ok: false, reason: REASONS.INVALID_TARGET, message: 'session target needs sessionId or query' }
|
|
118
|
+
}
|
|
119
|
+
// session 语义自带工作目录与模型:混用会让人以为还能控制它们。
|
|
120
|
+
for (const [field, value] of [['cwd', raw.cwd], ['lane', raw.lane], ['model', raw.model]]) {
|
|
121
|
+
if (nonEmpty(value)) {
|
|
122
|
+
return { ok: false, reason: REASONS.INVALID_TARGET, message: `${field} is not accepted with a session target` }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (raw.commit === true) {
|
|
126
|
+
return { ok: false, reason: REASONS.INVALID_TARGET, message: 'commit is not accepted with a session target' }
|
|
127
|
+
}
|
|
128
|
+
const deliver = raw.deliver === undefined ? 'queue' : raw.deliver
|
|
129
|
+
if (deliver !== 'queue' && deliver !== 'steer') {
|
|
130
|
+
return { ok: false, reason: REASONS.INVALID_TARGET, message: "deliver must be 'queue' or 'steer'" }
|
|
131
|
+
}
|
|
132
|
+
if (nonEmpty(raw.history) || (Array.isArray(raw.history) && raw.history.length > 0)) {
|
|
133
|
+
notes.push('history ignored for session targets')
|
|
134
|
+
}
|
|
135
|
+
return { ok: true, target: { kind: 'session', sessionId: id, query }, deliver, notes }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---- 扁平写法(MCP 工具的 sessionId/sessionQuery)----
|
|
139
|
+
if (flatSessionId !== undefined || flatQuery !== undefined) {
|
|
140
|
+
if (flatSessionId !== undefined && flatQuery !== undefined) {
|
|
141
|
+
return { ok: false, reason: REASONS.INVALID_TARGET, message: 'pass sessionId or sessionQuery, not both' }
|
|
142
|
+
}
|
|
143
|
+
if (nonEmpty(raw.cwd) || nonEmpty(raw.lane)) {
|
|
144
|
+
return { ok: false, reason: REASONS.INVALID_TARGET, message: 'cwd/lane are not accepted with a session target' }
|
|
145
|
+
}
|
|
146
|
+
const deliver = raw.deliver === undefined ? 'queue' : raw.deliver
|
|
147
|
+
if (deliver !== 'queue' && deliver !== 'steer') {
|
|
148
|
+
return { ok: false, reason: REASONS.INVALID_TARGET, message: "deliver must be 'queue' or 'steer'" }
|
|
149
|
+
}
|
|
150
|
+
if (Array.isArray(raw.history) && raw.history.length > 0) notes.push('history ignored for session targets')
|
|
151
|
+
return { ok: true, target: { kind: 'session', sessionId: flatSessionId, query: flatQuery }, deliver, notes }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---- 默认:worker(与 0.1.1 完全一致)----
|
|
155
|
+
return { ok: true, target: { kind: 'worker', cwd: raw.cwd, lane: raw.lane }, deliver: 'queue', notes }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 从搜索结果里挑出唯一目标。0 条与多条都不猜:多条时把候选回传,由调用方指定 id。
|
|
160
|
+
* 显式 sessionId 优先,完全不查注册表。
|
|
161
|
+
*/
|
|
162
|
+
export const selectSession = (hits, target) => {
|
|
163
|
+
const t = target === undefined || target === null ? {} : target
|
|
164
|
+
if (nonEmpty(t.sessionId)) {
|
|
165
|
+
return { ok: true, sessionId: t.sessionId.trim(), snippet: undefined, candidates: [] }
|
|
166
|
+
}
|
|
167
|
+
const list = Array.isArray(hits) ? hits.filter((h) => h !== null && typeof h === 'object' && nonEmpty(h.sessionId)) : []
|
|
168
|
+
const candidates = list.slice(0, MAX_CANDIDATES).map((h) => ({ sessionId: h.sessionId, snippet: typeof h.snippet === 'string' ? h.snippet.slice(0, 200) : '' }))
|
|
169
|
+
if (list.length === 0) {
|
|
170
|
+
return { ok: false, reason: REASONS.QUERY_EMPTY, message: `no session matched ${JSON.stringify(t.query)}`, candidates: [] }
|
|
171
|
+
}
|
|
172
|
+
if (list.length > 1) {
|
|
173
|
+
return { ok: false, reason: REASONS.AMBIGUOUS, message: `${list.length} sessions matched ${JSON.stringify(t.query)}; pass sessionId`, candidates }
|
|
174
|
+
}
|
|
175
|
+
return { ok: true, sessionId: list[0].sessionId, snippet: candidates[0].snippet, candidates }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** 把 sessionController 抛出的 RemoteError 映射成稳定 reason。 */
|
|
179
|
+
export const mapControllerError = (error) => {
|
|
180
|
+
const code = (error !== null && typeof error === 'object' && typeof error.code === 'string' ? error.code : '')
|
|
181
|
+
|| (error !== null && typeof error === 'object' && error.cause !== null && typeof error.cause === 'object' && typeof error.cause.code === 'string' ? error.cause.code : '')
|
|
182
|
+
switch (code) {
|
|
183
|
+
case 'session/not-found': return REASONS.NOT_FOUND
|
|
184
|
+
case 'session/agent-busy': return REASONS.BUSY
|
|
185
|
+
case 'session/writer-held': return REASONS.WRITER_HELD
|
|
186
|
+
case 'session/invalid-time-zone': return REASONS.INVALID_TARGET
|
|
187
|
+
case 'gateway/bad-request': return REASONS.BAD_REQUEST
|
|
188
|
+
case 'gateway/internal': return REASONS.GATEWAY_INTERNAL
|
|
189
|
+
default: return REASONS.DISPATCH_FAILED
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** 任务在 HTTP 行上的回显:调用方一眼能看出这次是"新建"还是"接进哪个会话"。 */
|
|
194
|
+
export const taskResponse = (task) => {
|
|
195
|
+
const out = { taskId: task.taskId, status: task.status, kind: task.kind }
|
|
196
|
+
const target = task.target !== undefined && task.target !== null
|
|
197
|
+
? task.target
|
|
198
|
+
// 兼容 0.1.1 之前落下的记录(如 review 任务):没有 target 字段时按 worker 回显。
|
|
199
|
+
: { kind: 'worker', cwd: task.cwd, lane: task.lane }
|
|
200
|
+
if (target.kind === 'session') {
|
|
201
|
+
out.target = { kind: 'session', sessionId: target.sessionId, deliver: task.deliver }
|
|
202
|
+
if (target.snippet !== undefined) out.target.snippet = target.snippet
|
|
203
|
+
} else {
|
|
204
|
+
out.target = { kind: 'worker', cwd: target.cwd, lane: target.lane }
|
|
205
|
+
}
|
|
206
|
+
if (Array.isArray(task.notes) && task.notes.length > 0) out.notes = task.notes
|
|
207
|
+
return out
|
|
208
|
+
}
|
package/mcp/dsh-mcp.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// 客户端(Secure MCP Tunnel / cloudflared)桥接给 ChatGPT 等远程产品。
|
|
7
7
|
const GATEWAY = process.env.DSH_BRIDGE_URL || 'http://127.0.0.1:3080'
|
|
8
8
|
const SERVER_NAME = 'dsh-bridge'
|
|
9
|
-
const SERVER_VERSION = '0.1.
|
|
9
|
+
const SERVER_VERSION = '0.1.3'
|
|
10
10
|
const PROTOCOL_VERSION = '2024-11-05'
|
|
11
11
|
|
|
12
12
|
const log = (...args) => console.error('[dsh-mcp]', ...args)
|
|
@@ -15,23 +15,43 @@ const tools = [
|
|
|
15
15
|
{
|
|
16
16
|
name: 'dsh_task',
|
|
17
17
|
description:
|
|
18
|
-
'把编码任务交给本机 DeepSeek Harness
|
|
18
|
+
'把编码任务交给本机 DeepSeek Harness 执行(DeepSeek 写代码,ChatGPT/Codex 验收)。两种目标:默认新建编码子代理在 cwd 里干活;' +
|
|
19
|
+
'给出 sessionId 或 sessionQuery 时改为**投递进那一条已有会话**(保留它自己的上下文,不新建会话/工作区)。' +
|
|
19
20
|
'参数: instruction(必填,完整中文指令:目标/产出目录/验收标准/约束), cwd(工作目录,默认 D:\\Harness,通常传当前项目目录), ' +
|
|
20
21
|
'lane(并行通道名,默认 main), model(fast=deepseek-v4-flash 快而省 / pro=deepseek-v4-pro 质量优先), ' +
|
|
22
|
+
'sessionId(已有会话 id), sessionQuery(按标题/内容搜已有会话,需唯一命中,否则返回候选), deliver(queue=排队(默认) / steer=插入当前回合), ' +
|
|
21
23
|
'wait(默认 true,阻塞至任务完成并返回 DeepSeek 汇报), timeoutSec(默认 600,最长 900)。',
|
|
22
24
|
inputSchema: {
|
|
23
25
|
type: 'object',
|
|
24
26
|
properties: {
|
|
25
27
|
instruction: { type: 'string', description: '完整中文编码指令(目标、产出目录、验收标准、约束)。' },
|
|
26
|
-
cwd: { type: 'string', description: '工作目录(绝对路径)。默认 D:\\Harness
|
|
28
|
+
cwd: { type: 'string', description: '工作目录(绝对路径)。默认 D:\\Harness。与 sessionId/sessionQuery 互斥。' },
|
|
27
29
|
lane: { type: 'string', description: '并行通道名(默认 main)。' },
|
|
28
30
|
model: { type: 'string', enum: ['fast', 'pro'], description: '模型别名:fast=deepseek-v4-flash,pro=deepseek-v4-pro。' },
|
|
31
|
+
sessionId: { type: 'string', description: '已有会话 id:把指令投递进这条会话,而不是新建子代理。' },
|
|
32
|
+
sessionQuery: { type: 'string', description: '按标题或内容搜索已有会话;唯一命中才派发,多条会返回 candidates 让你改用 sessionId。' },
|
|
33
|
+
deliver: { type: 'string', enum: ['queue', 'steer'], description: '投递方式:queue 排队(默认),steer 插入当前回合。' },
|
|
29
34
|
wait: { type: 'boolean', description: '默认 true:阻塞直到任务完成并返回汇报。' },
|
|
30
35
|
timeoutSec: { type: 'number', description: '最长等待秒数(默认 600,最大 900)。' },
|
|
31
36
|
},
|
|
32
37
|
required: ['instruction'],
|
|
33
38
|
},
|
|
34
39
|
},
|
|
40
|
+
{
|
|
41
|
+
name: 'dsh_sessions',
|
|
42
|
+
description:
|
|
43
|
+
'只读查找本机 DSH 的已有会话(不创建、不投递)。用 sessionQuery 按标题/内容搜,或用 sessionId 直接解析某一条;' +
|
|
44
|
+
'拿到 sessionId 后交给 dsh_task 即可把任务投递进那条会话。参数: query, sessionId, limit(默认 10), inspect(默认 false,为 true 时探测前 5 条能否接)。',
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
query: { type: 'string', description: '按标题/内容搜索(与 sessionId 二选一)。' },
|
|
49
|
+
sessionId: { type: 'string', description: '直接解析某条会话(与 query 二选一)。' },
|
|
50
|
+
limit: { type: 'number', description: '最多返回条数(默认 10,最大 50)。' },
|
|
51
|
+
inspect: { type: 'boolean', description: '为 true 时对前 5 条探测 agentAvailable / problem(默认 false)。' },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
35
55
|
{
|
|
36
56
|
name: 'dsh_task_status',
|
|
37
57
|
description: '查询 DeepSeek 协作任务的当前状态与结果。参数: taskId(必填)。',
|
|
@@ -43,10 +63,13 @@ const tools = [
|
|
|
43
63
|
},
|
|
44
64
|
{
|
|
45
65
|
name: 'dsh_task_cancel',
|
|
46
|
-
description: '取消一个运行中的 DeepSeek 协作任务。参数: taskId(必填)。',
|
|
66
|
+
description: '取消一个运行中的 DeepSeek 协作任务。参数: taskId(必填), force(session 目标的任务默认拒绝取消,因为它会打断用户自己的回合;确认要打断时传 true)。',
|
|
47
67
|
inputSchema: {
|
|
48
68
|
type: 'object',
|
|
49
|
-
properties: {
|
|
69
|
+
properties: {
|
|
70
|
+
taskId: { type: 'string', description: '任务 ID。' },
|
|
71
|
+
force: { type: 'boolean', description: '仅对投递进已有会话的任务有意义:true 才真正打断该会话。' },
|
|
72
|
+
},
|
|
50
73
|
required: ['taskId'],
|
|
51
74
|
},
|
|
52
75
|
},
|
|
@@ -112,18 +135,36 @@ async function handleToolCall(name, args) {
|
|
|
112
135
|
cwd: args.cwd,
|
|
113
136
|
lane: args.lane,
|
|
114
137
|
model: args.model,
|
|
138
|
+
sessionId: args.sessionId,
|
|
139
|
+
sessionQuery: args.sessionQuery,
|
|
140
|
+
deliver: args.deliver,
|
|
115
141
|
})
|
|
142
|
+
// 4xx 拒绝(缺目标 / 会话不存在 / 搜索结果不唯一 / 被写锁占用)不带 taskId,绝不静默新建。
|
|
143
|
+
if (task.taskId === undefined) return { ok: false, phase: 'rejected', ...task }
|
|
116
144
|
if (task.status === 'error') return { ok: false, ...task }
|
|
117
145
|
if (args.wait === false) return { ok: true, phase: 'submitted', ...task }
|
|
118
146
|
const done = await waitTask(task.taskId, args.timeoutSec || 600)
|
|
119
147
|
return { ok: done.status === 'done', phase: 'finished', ...done }
|
|
120
148
|
}
|
|
149
|
+
if (name === 'dsh_sessions') {
|
|
150
|
+
if (args.query === undefined && args.sessionId === undefined) {
|
|
151
|
+
return { ok: false, error: 'query or sessionId is required' }
|
|
152
|
+
}
|
|
153
|
+
const found = await callGateway('/api/dsh-bridge/sessions', {
|
|
154
|
+
query: args.query,
|
|
155
|
+
sessionId: args.sessionId,
|
|
156
|
+
limit: args.limit,
|
|
157
|
+
inspect: args.inspect,
|
|
158
|
+
})
|
|
159
|
+
if (Array.isArray(found.items)) return { ok: true, ...found }
|
|
160
|
+
return { ok: false, ...found }
|
|
161
|
+
}
|
|
121
162
|
if (name === 'dsh_task_status') {
|
|
122
163
|
const res = await fetch(GATEWAY + '/api/dsh-bridge/status?taskId=' + encodeURIComponent(args.taskId))
|
|
123
164
|
return res.json()
|
|
124
165
|
}
|
|
125
166
|
if (name === 'dsh_task_cancel') {
|
|
126
|
-
return callGateway('/api/dsh-bridge/cancel', { taskId: args.taskId })
|
|
167
|
+
return callGateway('/api/dsh-bridge/cancel', { taskId: args.taskId, force: args.force === true })
|
|
127
168
|
}
|
|
128
169
|
if (name === 'dsh_review') {
|
|
129
170
|
const task = await callGateway('/api/dsh-bridge/review', { cwd: args.cwd, diff: args.diff, focus: args.focus })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whaletalk/dsh-codex-collab",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Codex × DeepSeek Harness 双向编码协作:Codex 主导派活与验收,DeepSeek Harness 编码子代理在共享工作目录执行并回传,独立评审子代理实际跑通构建/测试后出具报告。提供 DSH 宿主网关插件、零依赖 MCP 服务器与 Codex 技能三件套。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|