dsh-agent-board 1.0.2 → 1.1.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 +16 -1
- package/index.mjs +88 -12
- package/lib/client.js +6 -1
- package/lib/core.mjs +27 -3
- package/package.json +3 -2
- package/test/core.test.mjs +72 -0
package/README.md
CHANGED
|
@@ -31,4 +31,19 @@ dsh plugin --profile web add dsh-agent-board@latest
|
|
|
31
31
|
|
|
32
32
|
- 仓库:<https://github.com/PPawnsir/task-board-plugin>
|
|
33
33
|
- 本包即源码,直接维护(v68 起拆除了动态→静态转换层,v74 起去池化)
|
|
34
|
-
- 单元测试:`npm test`(node --test,30 例纯逻辑用例,无需重启 dsh)
|
|
34
|
+
- 单元测试:`npm test`(node --test,30+ 例纯逻辑用例,无需重启 dsh)
|
|
35
|
+
|
|
36
|
+
## 开发热循环(免重启 E2E)
|
|
37
|
+
|
|
38
|
+
host 代码(`index.mjs` / `lib/*.mjs`)改动必须进程重启才能加载,但不必动你正在用的主实例——
|
|
39
|
+
仓库提供一个独立 dev 实例 + 文件监听自动重启方案:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
npm run dev # = node scripts/dev-watch.cjs
|
|
43
|
+
# 浏览器打开 http://127.0.0.1:3081(独立 dev profile,不影响主实例)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- 改 host 代码 → 监听器自动重启 dev 实例(约 3-5s)→ 刷新 3081 页面即可验证
|
|
47
|
+
- 改 `lib/client.js` → **不用重启**,浏览器刷新即生效(client 包按请求从磁盘读)
|
|
48
|
+
- dev profile 在 `~/.dsh/profiles/dev`,只挂 base + web-app + 本插件;会话/看板数据与主实例共享 `~/.dsh`,两边互不干扰
|
|
49
|
+
- 官方 `cordis-plugin-hmr` 行在 web 部署里被 `disabled: true`(reload 生命周期未测完),实测对 link: 插件静默无效,故用 watcher 重启替代
|
package/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// {name, description, parameters, output, execute} 普通对象,这里内联等价实现。
|
|
7
7
|
// parameters 已是完整 JSON Schema,原样透传;output 透传 schema+render。
|
|
8
8
|
import * as core from './lib/core.mjs'
|
|
9
|
-
const { ah, isb, gsb, gpt, vt, validateDeps, depsSatisfied, depsCancelled, classifyPipeline, seed, cfg, claimCheck, claimApply, checkParentAuto, resolveApply, verifyApply, parseSections, parseVerdict, isEscalation, outputText, histNotes, buildWorkerPrompt, buildVerifierPrompt, pickDispatch, isOrphan, PRIO_RANK } = core
|
|
9
|
+
const { ah, isb, gsb, gpt, vt, validateDeps, depsSatisfied, depsCancelled, classifyPipeline, seed, normalizeBoard, cfg, claimCheck, claimApply, checkParentAuto, resolveApply, verifyApply, parseSections, parseVerdict, isEscalation, outputText, histNotes, buildContextPackSection, buildWorkerPrompt, buildVerifierPrompt, pickDispatch, isOrphan, PRIO_RANK } = core
|
|
10
10
|
|
|
11
11
|
function defineTool(options) {
|
|
12
12
|
var userExecute = options.execute
|
|
@@ -59,7 +59,7 @@ export function apply(ctx) {
|
|
|
59
59
|
// teamMode 缓存:由 rt() 同步,供 systemPrompt 动态引导段读取(v65)
|
|
60
60
|
var teamModeCache = {}
|
|
61
61
|
function fileFor(sid) { return '.dsh/tasks-' + sid + '.json' }
|
|
62
|
-
async function rt(sid) { try { var t = await fs.resolve(fileFor(sid)); var r = await fs.readText(t); var d = JSON.parse(r); if (vt(d) && d.ownerSession === sid) { teamModeCache[sid] = !!d.teamMode; return d }; return seed(sid) } catch (_) { return seed(sid) } }
|
|
62
|
+
async function rt(sid) { try { var t = await fs.resolve(fileFor(sid)); var r = await fs.readText(t); var d = JSON.parse(r); if (vt(d) && d.ownerSession === sid) { teamModeCache[sid] = !!d.teamMode; return normalizeBoard(d) }; return seed(sid) } catch (_) { return seed(sid) } }
|
|
63
63
|
async function wt(sid, d) { var c = JSON.stringify(d, null, 2); try { var t = await fs.resolve(fileFor(sid)); await fs.writeText(t, c) } catch (e) { console.error('[task-board] write:', String(e)); throw e } }
|
|
64
64
|
// 每会话一条 promise 链,串行化所有 读-改-写,消除并发写竞争
|
|
65
65
|
var fileLocks = {}
|
|
@@ -73,7 +73,10 @@ export function apply(ctx) {
|
|
|
73
73
|
// 按会话找 root agent(静态插件挂 host 层后多会话共存,不能"取第一个"——会把 worker 挂到别的会话上)
|
|
74
74
|
function rootForSession(sid) { var s = ctx.agents; if (!s) return undefined; var r = s.roots(); for (var i = 0; i < r.length; i++) { if (String(r[i].id) === sid) return r[i] } return undefined }
|
|
75
75
|
function makeSignal() { try { return new AbortController().signal } catch (_) { return { aborted: false, addEventListener: function () {}, removeEventListener: function () {} } } }
|
|
76
|
-
|
|
76
|
+
// makeMsg 支持插件来源标记(参考 dsh-notes 派发模式):
|
|
77
|
+
// form 'recall' = 背景回执(召回上下文,非指令);'notice' = 需注意的通知(带一行 summary)
|
|
78
|
+
// 不再用 kind:'user'——插件消息不该冒充用户在说话,模型可据 form 正确理解语义
|
|
79
|
+
function makeMsg(text, form, summary) { var src = { kind: 'plugin', plugin: 'dsh-agent-board' }; if (form) { src.form = form; if (form === 'notice' && summary) src.summary = summary }; return { id: 'm' + Date.now() + Math.random().toString(36).slice(2, 6), role: 'user', content: [{ type: 'text', text: text }], source: src } }
|
|
77
80
|
// 超时保护:run 挂死时走失败重试路径
|
|
78
81
|
function withTimeout(promise, ms, label) { var timer = ctx.timer; if (!timer) return promise; return Promise.race([promise, timer.timeout(ms).then(function () { throw new Error(label + ' timeout ' + ms + 'ms') })]) }
|
|
79
82
|
|
|
@@ -100,6 +103,38 @@ export function apply(ctx) {
|
|
|
100
103
|
_cachedProvider = names[0]; return _cachedProvider
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
// 主窗口预研文件:t.context.files 里的路径由主 agent 选择性指定(它调研时读过哪些文件),
|
|
107
|
+
// host 在派发时从磁盘读最新内容注入 prompt——Worker/Verifier 不用从零重复调研。
|
|
108
|
+
// 上限:单文件 8KB、总计 40KB,超出截断并标注。
|
|
109
|
+
async function readContextPack(t) {
|
|
110
|
+
var paths = (t.context && Array.isArray(t.context.files)) ? t.context.files : []
|
|
111
|
+
var notes = (t.context && typeof t.context.notes === 'string') ? t.context.notes : ''
|
|
112
|
+
if (!paths.length && !notes.trim()) return ''
|
|
113
|
+
var out = [], total = 0
|
|
114
|
+
for (var i = 0; i < paths.length && total < 40960; i++) {
|
|
115
|
+
var p = String(paths[i] || '')
|
|
116
|
+
if (!p) continue
|
|
117
|
+
try {
|
|
118
|
+
var content = await fs.readText(await fs.resolve(p))
|
|
119
|
+
var truncated = false
|
|
120
|
+
if (content.length > 8192) { content = content.slice(0, 8192); truncated = true }
|
|
121
|
+
if (total + content.length > 40960) { content = content.slice(0, 40960 - total); truncated = true }
|
|
122
|
+
out.push({ path: p, content: content, truncated: truncated })
|
|
123
|
+
total += content.length
|
|
124
|
+
} catch (e) {
|
|
125
|
+
out.push({ path: p, content: '[读取失败: ' + String(e).slice(0, 120) + ']', truncated: false })
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return buildContextPackSection(out, notes.slice(0, 8000))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 预研文件注入通道:内容不混进 user prompt,而是通过 systemPrompt.context 以「上下文注入」
|
|
132
|
+
// 区块呈现(与 skill-catalog 等系统注入同形态)。
|
|
133
|
+
// 首轮竞速:子代理的首次 prompt 组装发生在 subagents.start() 返回之前,packByChild 还没写入
|
|
134
|
+
// → spawn 前把 pack 放进 pendingPacks,provider 按父子归属(isOwnedBy 父 agent)即时认领。
|
|
135
|
+
var packByChild = {}
|
|
136
|
+
var pendingPacks = []
|
|
137
|
+
|
|
103
138
|
async function spawnOneShot(sid, t, role) {
|
|
104
139
|
var subagents = ctx.subagents; if (!subagents) return null
|
|
105
140
|
var parent = rootForSession(sid); if (!parent) { console.error('[task-board] no root agent for session ' + sid + ', skip spawn'); return null }
|
|
@@ -107,7 +142,11 @@ export function apply(ctx) {
|
|
|
107
142
|
var modelOverride = ''
|
|
108
143
|
if (role === 'verifier') { var dd = await rt(sid); modelOverride = (typeof dd.verifierModel === 'string' && dd.verifierModel.trim()) ? dd.verifierModel.trim() : ''; if (modelOverride && badModels[modelKey(sid, modelOverride)]) { console.error('[task-board] model ' + modelOverride + ' circuited, using parent model'); modelOverride = '' } }
|
|
109
144
|
else if (role === 'worker') { var dw = await rt(sid); modelOverride = (typeof dw.workerModel === 'string' && dw.workerModel.trim()) ? dw.workerModel.trim() : ''; if (modelOverride && badModels[modelKey(sid, modelOverride)]) { console.error('[task-board] model ' + modelOverride + ' circuited, using parent model'); modelOverride = '' } }
|
|
110
|
-
var
|
|
145
|
+
var pack = ''
|
|
146
|
+
try { pack = await readContextPack(t) } catch (e) { console.error('[task-board] context pack read failed:', String(e)) }
|
|
147
|
+
// user prompt 只留一行指引,内容走上下文注入区块
|
|
148
|
+
var packNote = pack ? '本任务附带主窗口预研文件,已通过「上下文注入」区提供(含文件完整内容),直接基于其内容工作,不要重复读取这些文件。' : ''
|
|
149
|
+
var req = { label: role + ':' + t.id, prompt: [{ type: 'text', text: role === 'worker' ? buildWorkerPrompt(t, packNote) : buildVerifierPrompt(t, packNote) }], parent: parent, signal: makeSignal() }
|
|
111
150
|
if (modelOverride) {
|
|
112
151
|
// list-models 返回的 id 是 "provider/model" 复合格式(如 "cmss/zhanlu/glm-5.2"),
|
|
113
152
|
// 但 AgentOptions 的 provider 和 model 是分开的——整串塞进 model 会报 UNKNOWN_MODEL
|
|
@@ -116,12 +155,17 @@ export function apply(ctx) {
|
|
|
116
155
|
else req.agentOptions = { model: modelOverride }
|
|
117
156
|
}
|
|
118
157
|
var run
|
|
158
|
+
var ppEntry = pack ? { pack: pack, parent: parent, at: Date.now() } : null
|
|
159
|
+
if (ppEntry) pendingPacks.push(ppEntry)
|
|
119
160
|
try { run = await subagents.start(providerName, req) } catch (e) {
|
|
120
161
|
if (modelOverride) { console.error('[task-board] model override failed, fallback to parent model:', String(e)); delete req.agentOptions; try { run = await subagents.start(providerName, req) } catch (e2) { console.error('[task-board] spawn ' + role + ' failed:', String(e2)); return null } }
|
|
121
162
|
else { console.error('[task-board] spawn ' + role + ' failed:', String(e)); return null }
|
|
163
|
+
} finally {
|
|
164
|
+
if (ppEntry) { var ppi = pendingPacks.indexOf(ppEntry); if (ppi >= 0) pendingPacks.splice(ppi, 1) }
|
|
122
165
|
}
|
|
123
166
|
var rec = { run: run, role: role, taskId: t.id, startedAt: Date.now(), model: modelOverride }
|
|
124
167
|
runsFor(sid)[t.id] = rec
|
|
168
|
+
if (pack) packByChild[String(run.id)] = pack
|
|
125
169
|
if (!dispatchedEver[sid]) dispatchedEver[sid] = {}
|
|
126
170
|
dispatchedEver[sid][String(run.id)] = true
|
|
127
171
|
// 30min 硬超时(一次性 run 没有看门狗,挂死不能白占并发位)→ 走失败重试路径
|
|
@@ -133,6 +177,7 @@ export function apply(ctx) {
|
|
|
133
177
|
async function settleRun(sid, rec, res, err) {
|
|
134
178
|
if (runsFor(sid)[rec.taskId] !== rec) return // 已被 terminate 等路径处理
|
|
135
179
|
delete runsFor(sid)[rec.taskId]
|
|
180
|
+
delete packByChild[String(rec.run.id)] // 上下文注入缓存随 run 销毁
|
|
136
181
|
try { await rec.run.dispose() } catch (_) {}
|
|
137
182
|
var output = outputText(res)
|
|
138
183
|
var failed = !!err || (res && res.stopReason && res.stopReason !== 'completed')
|
|
@@ -218,7 +263,7 @@ export function apply(ctx) {
|
|
|
218
263
|
function notifyMainWindow(sid, t, question) {
|
|
219
264
|
var root = rootForSession(sid)
|
|
220
265
|
if (!root) return
|
|
221
|
-
try { root.followup(makeMsg('⚠️ [任务看板] Worker 上报歧义,等待裁决:\n\n任务: ' + t.title + ' (' + t.id + ')\n\n疑问:\n' + question.slice(0, 1500) + '\n\n请在看板详情页裁决,或直接回复指示。裁决后会有新 Worker 带着裁决答案接手。')) } catch (e) { console.error('[task-board] escalate notify failed:', String(e)) }
|
|
266
|
+
try { root.followup(makeMsg('⚠️ [任务看板] Worker 上报歧义,等待裁决:\n\n任务: ' + t.title + ' (' + t.id + ')\n\n疑问:\n' + question.slice(0, 1500) + '\n\n请在看板详情页裁决,或直接回复指示。裁决后会有新 Worker 带着裁决答案接手。', 'notice', '任务待裁决: ' + t.title)) } catch (e) { console.error('[task-board] escalate notify failed:', String(e)) }
|
|
222
267
|
}
|
|
223
268
|
function maybeNotify(sid, task) { if (task && task.escalation) { notifyMainWindow(sid, task, task.escalation.question) } }
|
|
224
269
|
|
|
@@ -268,7 +313,7 @@ export function apply(ctx) {
|
|
|
268
313
|
}
|
|
269
314
|
lines.push('', '可用 task_list 查看全部;阻塞项可在看板拖回待办重新投放。')
|
|
270
315
|
var text = lines.join('\n')
|
|
271
|
-
function send() { try { root.followup(makeMsg(text)) } catch (e) { console.error('[task-board] receipt flush failed:', String(e)) } }
|
|
316
|
+
function send() { try { root.followup(makeMsg(text, 'recall')) } catch (e) { console.error('[task-board] receipt flush failed:', String(e)) } }
|
|
272
317
|
if (typeof root.whenIdle === 'function') {
|
|
273
318
|
var waited = withTimeout(root.whenIdle(), 300000, 'receipt-idle-wait') // 最多等 5 分钟,超时也发(不能丢回执)
|
|
274
319
|
Promise.resolve(waited).then(send).catch(send)
|
|
@@ -344,10 +389,40 @@ export function apply(ctx) {
|
|
|
344
389
|
// 不能走 resolveRoot:该函数在每个 agent 每次 prompt 组装时同步执行,
|
|
345
390
|
// resolveRoot 全表扫 agent 注册表会把宿主事件循环卡死(曾导致全局界面卡顿、用户消息延迟渲染)。
|
|
346
391
|
if (!teamModeCache[String(agent.id)]) return ''
|
|
347
|
-
return '【任务看板 Team 模式已开启】\n本会话的任务看板处于 Team 模式。请遵循以下工作方式:\n1. 涉及代码改动、文件创建、命令执行等实质性工作时,优先用 task_create 提交为看板任务(由一次性 Worker/Verifier 子代理执行与验收),不要自己直接动手实现。\n2. 你仍保有全部工具能力——调研、读代码、讨论方案、回答问题时直接进行,无需提交任务。\n3. 创建任务时,务必在 description
|
|
392
|
+
return '【任务看板 Team 模式已开启】\n本会话的任务看板处于 Team 模式。请遵循以下工作方式:\n1. 涉及代码改动、文件创建、命令执行等实质性工作时,优先用 task_create 提交为看板任务(由一次性 Worker/Verifier 子代理执行与验收),不要自己直接动手实现。\n2. 你仍保有全部工具能力——调研、读代码、讨论方案、回答问题时直接进行,无需提交任务。\n3. 创建任务时,务必在 description 里写清任务目标和约束;调研结论/原始需求/思路用 contextNotes 带上,调研时读过的关键文件用 contextFiles 把路径带上——两者都会通过「上下文注入」通道传给子代理(独立注入区块,不占对话流)。子代理是全新会话、无你的会话记忆,上下文不够它需要从零自行调研,效率大打折扣甚至跑偏方向。\n4. Worker 上报歧义时会通过 task_arbitrate 等待你裁决,请及时响应。驳回重派时同样:新 Worker 没有上一轮的记忆,驳回原因会在 prompt 里,但额外上下文需你在 description 里补上。\n5. 创建多个相互关联的任务时,必须「先草稿后发布」:所有任务先用 task_create draft:true 建为草稿(草稿不会被派发领取),等全部任务创建完成、dependsOn 依赖关系都写好后,再逐个 task_update publish=true 统一发布。严禁直接创建 pending 任务再后补依赖——依赖还没写入,后向任务就会被 Worker 提前领走。'
|
|
348
393
|
},
|
|
349
394
|
})
|
|
350
395
|
ctx.effect(function () { return disposeSection })
|
|
396
|
+
// 预研文件上下文注入:Worker/Verifier 的预研文件内容以「上下文注入」区块呈现
|
|
397
|
+
// (与 skill-catalog 同形态),不混进 user prompt。按子代理会话 id 命中,O(1),
|
|
398
|
+
// 其他 agent 组装时零成本返回空串。
|
|
399
|
+
var disposeCtxPack = sysPrompt.context({
|
|
400
|
+
name: 'task-board:context-pack',
|
|
401
|
+
order: 50,
|
|
402
|
+
text: function (assembleCtx) {
|
|
403
|
+
var agent = assembleCtx && assembleCtx.agent
|
|
404
|
+
if (!agent) return ''
|
|
405
|
+
var aid = String(agent.id)
|
|
406
|
+
console.error('[task-board][ctxpack] assemble agent=' + aid + ' packKeys=' + Object.keys(packByChild).join(',') + ' pending=' + pendingPacks.length)
|
|
407
|
+
var hit = packByChild[aid]
|
|
408
|
+
if (hit) { console.error('[task-board][ctxpack] HIT packByChild for ' + aid); return hit }
|
|
409
|
+
// 首轮竞速自愈:start() 返回前的首次组装按父子归属从 pendingPacks 认领
|
|
410
|
+
var agentsSvc = ctx.agents
|
|
411
|
+
if (!agentsSvc) return ''
|
|
412
|
+
var now = Date.now()
|
|
413
|
+
for (var i = pendingPacks.length - 1; i >= 0; i--) {
|
|
414
|
+
var pp = pendingPacks[i]
|
|
415
|
+
if (now - pp.at > 60000) { pendingPacks.splice(i, 1); continue }
|
|
416
|
+
try {
|
|
417
|
+
var owned = agentsSvc.isOwnedBy(aid, pp.parent)
|
|
418
|
+
console.error('[task-board][ctxpack] isOwnedBy(' + aid + ', parent=' + (pp.parent && pp.parent.id) + ')=' + owned)
|
|
419
|
+
if (owned) { packByChild[aid] = pp.pack; return pp.pack }
|
|
420
|
+
} catch (e) { console.error('[task-board][ctxpack] isOwnedBy threw: ' + String(e)) }
|
|
421
|
+
}
|
|
422
|
+
return ''
|
|
423
|
+
},
|
|
424
|
+
})
|
|
425
|
+
ctx.effect(function () { return disposeCtxPack })
|
|
351
426
|
}
|
|
352
427
|
|
|
353
428
|
// ===== Tools =====
|
|
@@ -357,8 +432,8 @@ export function apply(ctx) {
|
|
|
357
432
|
ctx.tools.register(defineTool({ name: 'task_resolve', description: '提交验证(verifying)或阻塞(blocked)。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, status: { type: 'string', enum: ['verifying', 'blocked'] }, resolution: { type: 'string' } }, required: ['taskId', 'status'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); return mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; if (t.status !== 'in-progress') return { ok: false, error: 'not in-progress' }; if (t.claimedBy !== actor) return { ok: false, error: 'not claimed by you' }; if (args.status === 'verifying' && !args.resolution) return { ok: false, error: 'resolution required' }; return resolveApply(d, t, actor, args.status, args.resolution, args.resolution || args.status) }) } }))
|
|
358
433
|
ctx.tools.register(defineTool({ name: 'task_verify', description: '验收:approved→resolved,rejected→in-progress。子任务全完成父任务自动verifying。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, verdict: { type: 'string', enum: ['approved', 'rejected'] }, comment: { type: 'string' } }, required: ['taskId', 'verdict'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); return mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; if (t.status !== 'verifying') return { ok: false, error: 'not verifying' }; return verifyApply(d, t, actor, args.verdict, args.comment) }) } }))
|
|
359
434
|
ctx.tools.register(defineTool({ name: 'task_archive', description: '归档已解决/已取消任务。', parameters: { type: 'object', properties: { taskId: { type: 'string' } }, required: ['taskId'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); return mutateLocked(sid, function (d) { var a = d.tasks; var t = a.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; if (t.status !== 'resolved' && t.status !== 'cancelled') return { ok: false, error: 'only resolved/cancelled' }; var ps = t.status; t.status = 'archived'; t.archivedAt = new Date().toISOString(); ah(t, ps, 'archived', actor, 'archived'); var ca = 0; gsb(t.id, a).forEach(function (c) { if (c.status !== 'archived') { ah(c, c.status, 'archived', actor, 'cascade'); c.status = 'archived'; c.archivedAt = new Date().toISOString(); ca++ } }); var r = { ok: true, task: t }; if (ca) r.childrenArchived = ca; return r }) } }))
|
|
360
|
-
ctx.tools.register(defineTool({ name: 'task_update', description: '更新任务字段,可选重置为 pending。dependsOn/pipeline 也可更新(环检测会拒绝成环依赖)。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, title: { type: 'string' }, description: { type: 'string' }, priority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] }, assignMode: { type: 'string', enum: ['auto', 'manual'] }, assignee: { type: 'string' }, dependsOn: { type: 'array', items: { type: 'string' } }, pipeline: { type: 'string', enum: ['full', 'work', 'direct'] }, resetToPending: { type: 'boolean' }, publish: { type: 'boolean', description: '发布草稿为 pending(仅 draft 状态有效)' } }, required: ['taskId'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); return mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; if (args.title !== undefined) t.title = args.title; if (args.description !== undefined) t.description = args.description; if (args.priority !== undefined) t.priority = args.priority; if (args.assignMode !== undefined) t.assignMode = args.assignMode; if (args.assignee !== undefined) t.assignee = args.assignee || null; if (args.dependsOn !== undefined) { var derr = validateDeps(d, t.id, args.dependsOn); if (derr) return { ok: false, error: derr }; t.dependsOn = args.dependsOn } if (args.pipeline !== undefined) { t.pipeline = args.pipeline; t.pipelineAuto = false } if (args.publish) { if (t.status !== 'draft') return { ok: false, error: 'not a draft' }; t.status = 'pending'; ah(t, 'draft', 'pending', actor, 'published') } if (args.resetToPending) { var ps = t.status; t.status = 'pending'; t.claimedBy = null; t.claimedAt = null; t.resolvedAt = null; t.resolution = null; ah(t, ps, 'pending', actor, 'reset to pending after edit') }; return { ok: true, task: t } }) } }))
|
|
361
|
-
ctx.tools.register(defineTool({ name: 'task_create', description: '创建新任务到当前会话看板。acceptance 可选:硬性验收脚本命令(如 "node --test src/x.test.js"),Worker 必须实际运行、Verifier 必须独立复跑。dependsOn 可选:依赖任务 id 数组,依赖全部完成后才会被派发。pipeline 可选:full(默认,工作+验证)/work(只做不验)/direct(不进池,主窗口直接处理)。', parameters: { type: 'object', properties: { id: { type: 'string' }, title: { type: 'string' }, description: { type: 'string' }, priority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] }, tags: { type: 'array', items: { type: 'string' } }, parentId: { type: 'string' }, instructions: { type: 'string' }, acceptance: { type: 'string' }, dependsOn: { type: 'array', items: { type: 'string' } }, pipeline: { type: 'string', enum: ['full', 'work', 'direct'] }, draft: { type: 'boolean', description: 'true 创建为草稿(不派发不可领取),补全信息后用 task_update publish=true 发布' } }, required: ['title'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); return mutateLocked(sid, function (d) { if (args.id && d.tasks.find(function (x) { return x.id === args.id })) return { ok: false, error: 'duplicate id: ' + args.id }; if (args.dependsOn && args.dependsOn.length) { var derr = validateDeps(d, args.id || '(pending)', args.dependsOn); if (derr) return { ok: false, error: derr } }; var now = new Date().toISOString(); var t = { id: args.id || ('task-' + Date.now().toString(36)), title: args.title, description: args.description || '', status: args.draft ? 'draft' : 'pending', priority: args.priority || 'medium', tags: args.tags || [], parentId: args.parentId || null, subtaskStrategy: null, assignMode: 'auto', assignee: null, context: { files: [], docs: [], instructions: args.instructions || '', relatedTasks: [], prerequisites: '' }, acceptance: args.acceptance || '', dependsOn: args.dependsOn || [], pipeline: args.pipeline || '', claimedBy: null, claimedAt: null, createdAt: now, resolvedAt: null, verifiedAt: null, verifiedBy: null, archivedAt: null, resolution: null, messages: [], history: [{ from: 'created', to: args.draft ? 'draft' : 'pending', timestamp: now, actor: actor, note: args.draft ? 'created as draft' : 'created' }] }; if (!t.pipeline) t.pipeline = classifyPipeline(t); t.pipelineAuto = !args.pipeline; d.tasks.push(t); return { ok: true, task: t } }) } }))
|
|
435
|
+
ctx.tools.register(defineTool({ name: 'task_update', description: '更新任务字段,可选重置为 pending。dependsOn/pipeline 也可更新(环检测会拒绝成环依赖)。contextFiles 可更新预研文件清单。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, title: { type: 'string' }, description: { type: 'string' }, priority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] }, assignMode: { type: 'string', enum: ['auto', 'manual'] }, assignee: { type: 'string' }, dependsOn: { type: 'array', items: { type: 'string' } }, contextFiles: { type: 'array', items: { type: 'string' }, description: '预研文件路径(替换式更新),派发时经上下文注入传给子代理' }, contextNotes: { type: 'string', description: '预研笔记(替换式更新):调研结论/原始需求/思路' }, pipeline: { type: 'string', enum: ['full', 'work', 'direct'] }, resetToPending: { type: 'boolean' }, publish: { type: 'boolean', description: '发布草稿为 pending(仅 draft 状态有效)' } }, required: ['taskId'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); return mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; if (args.title !== undefined) t.title = args.title; if (args.description !== undefined) t.description = args.description; if (args.priority !== undefined) t.priority = args.priority; if (args.assignMode !== undefined) t.assignMode = args.assignMode; if (args.assignee !== undefined) t.assignee = args.assignee || null; if (args.dependsOn !== undefined) { var derr = validateDeps(d, t.id, args.dependsOn); if (derr) return { ok: false, error: derr }; t.dependsOn = args.dependsOn } if (args.pipeline !== undefined) { t.pipeline = args.pipeline; t.pipelineAuto = false } if (args.contextFiles !== undefined) { if (!t.context) t.context = { files: [], docs: [], instructions: '', notes: '', relatedTasks: [], prerequisites: '' }; t.context.files = Array.isArray(args.contextFiles) ? args.contextFiles.map(String).slice(0, 20) : [] } if (args.contextNotes !== undefined) { if (!t.context) t.context = { files: [], docs: [], instructions: '', notes: '', relatedTasks: [], prerequisites: '' }; t.context.notes = typeof args.contextNotes === 'string' ? args.contextNotes.slice(0, 8000) : '' } if (args.publish) { if (t.status !== 'draft') return { ok: false, error: 'not a draft' }; t.status = 'pending'; ah(t, 'draft', 'pending', actor, 'published') } if (args.resetToPending) { var ps = t.status; t.status = 'pending'; t.claimedBy = null; t.claimedAt = null; t.resolvedAt = null; t.resolution = null; ah(t, ps, 'pending', actor, 'reset to pending after edit') }; return { ok: true, task: t } }) } }))
|
|
436
|
+
ctx.tools.register(defineTool({ name: 'task_create', description: '创建新任务到当前会话看板。acceptance 可选:硬性验收脚本命令(如 "node --test src/x.test.js"),Worker 必须实际运行、Verifier 必须独立复跑。dependsOn 可选:依赖任务 id 数组,依赖全部完成后才会被派发。pipeline 可选:full(默认,工作+验证)/work(只做不验)/direct(不进池,主窗口直接处理)。contextFiles 可选:你在调研中已经读过的关键文件路径数组——派发时 host 会从磁盘读取最新内容,通过「上下文注入」通道传给 Worker/Verifier(呈现为独立注入区块,不占对话流),避免子代理从零重复调研。contextNotes 可选:调研结论/原始需求/思路等非文件类上下文,同样走上下文注入。', parameters: { type: 'object', properties: { id: { type: 'string' }, title: { type: 'string' }, description: { type: 'string' }, priority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] }, tags: { type: 'array', items: { type: 'string' } }, parentId: { type: 'string' }, instructions: { type: 'string' }, acceptance: { type: 'string' }, dependsOn: { type: 'array', items: { type: 'string' } }, contextFiles: { type: 'array', items: { type: 'string' }, description: '预研文件路径(相对 workspace 或绝对路径),内容将在派发时经上下文注入传给子代理' }, contextNotes: { type: 'string', description: '预研笔记:调研结论/原始需求/思路等(≤8000 字符),经上下文注入传给子代理' }, pipeline: { type: 'string', enum: ['full', 'work', 'direct'] }, draft: { type: 'boolean', description: 'true 创建为草稿(不派发不可领取),补全信息后用 task_update publish=true 发布' } }, required: ['title'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); return mutateLocked(sid, function (d) { if (args.id && d.tasks.find(function (x) { return x.id === args.id })) return { ok: false, error: 'duplicate id: ' + args.id }; if (args.dependsOn && args.dependsOn.length) { var derr = validateDeps(d, args.id || '(pending)', args.dependsOn); if (derr) return { ok: false, error: derr } }; var now = new Date().toISOString(); var t = { id: args.id || ('task-' + Date.now().toString(36)), title: args.title, description: args.description || '', status: args.draft ? 'draft' : 'pending', priority: args.priority || 'medium', tags: args.tags || [], parentId: args.parentId || null, subtaskStrategy: null, assignMode: 'auto', assignee: null, context: { files: (Array.isArray(args.contextFiles) ? args.contextFiles.map(String).slice(0, 20) : []), docs: [], instructions: args.instructions || '', notes: (typeof args.contextNotes === 'string' ? args.contextNotes.slice(0, 8000) : ''), relatedTasks: [], prerequisites: '' }, acceptance: args.acceptance || '', dependsOn: args.dependsOn || [], pipeline: args.pipeline || '', claimedBy: null, claimedAt: null, createdAt: now, resolvedAt: null, verifiedAt: null, verifiedBy: null, archivedAt: null, resolution: null, messages: [], history: [{ from: 'created', to: args.draft ? 'draft' : 'pending', timestamp: now, actor: actor, note: args.draft ? 'created as draft' : 'created' }] }; if (!t.pipeline) t.pipeline = classifyPipeline(t); t.pipelineAuto = !args.pipeline; d.tasks.push(t); return { ok: true, task: t } }) } }))
|
|
362
437
|
|
|
363
438
|
// ===== RPC =====
|
|
364
439
|
// get-tasks 是纯读路径(rt 只读文件)——poolCycle 由 15s 心跳 + 写入后 kickCycle 驱动,
|
|
@@ -368,7 +443,7 @@ export function apply(ctx) {
|
|
|
368
443
|
handle('resolve-task', async function (args) { var sid = rpcSessionId(args); var actor = getActorId(); return mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; if (t.status !== 'in-progress') return { ok: false, error: 'not in-progress' }; return resolveApply(d, t, actor, args.status, args.resolution, args.resolution || args.status) }) })
|
|
369
444
|
handle('verify-task', async function (args) { var sid = rpcSessionId(args); var actor = getActorId(); return mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; if (t.status !== 'verifying') return { ok: false, error: 'not verifying' }; delete t.escalation; delete t.verifyRetries; return verifyApply(d, t, actor, args.verdict, args.comment) }) })
|
|
370
445
|
handle('archive-task', async function (args) { var sid = rpcSessionId(args); var actor = getActorId(); return mutateLocked(sid, function (d) { var a = d.tasks; var t = a.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; if (t.status !== 'resolved' && t.status !== 'cancelled') return { ok: false, error: 'cannot archive' }; var ps = t.status; t.status = 'archived'; t.archivedAt = new Date().toISOString(); ah(t, ps, 'archived', actor, 'manual archive'); var ca = 0; gsb(t.id, a).forEach(function (c) { if (c.status !== 'archived') { ah(c, c.status, 'archived', actor, 'cascade'); c.status = 'archived'; c.archivedAt = new Date().toISOString(); ca++ } }); var r = { ok: true, task: t }; if (ca) r.childrenArchived = ca; return r }) })
|
|
371
|
-
handle('update-task', async function (args) { var sid = rpcSessionId(args); var actor = getActorId(); return mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; if (args.title !== undefined) t.title = args.title; if (args.description !== undefined) t.description = args.description; if (args.priority !== undefined) t.priority = args.priority; if (args.assignMode !== undefined) t.assignMode = args.assignMode; if (args.assignee !== undefined) t.assignee = args.assignee || null; if (args.dependsOn !== undefined) { var derr = validateDeps(d, t.id, args.dependsOn); if (derr) return { ok: false, error: derr }; t.dependsOn = args.dependsOn } if (args.pipeline !== undefined) { t.pipeline = args.pipeline; t.pipelineAuto = false } if (args.publish) { if (t.status !== 'draft') return { ok: false, error: 'not a draft' }; t.status = 'pending'; ah(t, 'draft', 'pending', actor, 'published') } if (args.resetToPending) { var ps = t.status; t.status = 'pending'; t.claimedBy = null; t.claimedAt = null; t.resolvedAt = null; t.resolution = null; ah(t, ps, 'pending', actor, 'reset to pending after edit') }; return { ok: true, task: t } }) })
|
|
446
|
+
handle('update-task', async function (args) { var sid = rpcSessionId(args); var actor = getActorId(); return mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; if (args.title !== undefined) t.title = args.title; if (args.description !== undefined) t.description = args.description; if (args.priority !== undefined) t.priority = args.priority; if (args.assignMode !== undefined) t.assignMode = args.assignMode; if (args.assignee !== undefined) t.assignee = args.assignee || null; if (args.dependsOn !== undefined) { var derr = validateDeps(d, t.id, args.dependsOn); if (derr) return { ok: false, error: derr }; t.dependsOn = args.dependsOn } if (args.pipeline !== undefined) { t.pipeline = args.pipeline; t.pipelineAuto = false } if (args.contextFiles !== undefined) { if (!t.context) t.context = { files: [], docs: [], instructions: '', notes: '', relatedTasks: [], prerequisites: '' }; t.context.files = Array.isArray(args.contextFiles) ? args.contextFiles.map(String).slice(0, 20) : [] } if (args.contextNotes !== undefined) { if (!t.context) t.context = { files: [], docs: [], instructions: '', notes: '', relatedTasks: [], prerequisites: '' }; t.context.notes = typeof args.contextNotes === 'string' ? args.contextNotes.slice(0, 8000) : '' } if (args.publish) { if (t.status !== 'draft') return { ok: false, error: 'not a draft' }; t.status = 'pending'; ah(t, 'draft', 'pending', actor, 'published') } if (args.resetToPending) { var ps = t.status; t.status = 'pending'; t.claimedBy = null; t.claimedAt = null; t.resolvedAt = null; t.resolution = null; ah(t, ps, 'pending', actor, 'reset to pending after edit') }; return { ok: true, task: t } }) })
|
|
372
447
|
handle('set-board-mode', async function (args) { var sid = rpcSessionId(args); return mutateLocked(sid, function (d) { d.boardMode = args.mode === 'manual' ? 'manual' : 'auto'; if (d.boardMode === 'manual' && d.teamMode) { d.teamMode = false; teamModeCache[sid] = false }; return { ok: true, boardMode: d.boardMode, teamMode: !!d.teamMode } }) })
|
|
373
448
|
handle('set-team-mode', async function (args) { var sid = rpcSessionId(args); return mutateLocked(sid, function (d) { d.teamMode = !!args.enabled; if (d.teamMode) d.boardMode = 'auto'; teamModeCache[sid] = d.teamMode; return { ok: true, teamMode: d.teamMode, boardMode: d.boardMode } }) })
|
|
374
449
|
// 裁决回流(v74 一次性模型):原 Worker 已结束,答案写入 history 后任务回 pending,
|
|
@@ -394,7 +469,7 @@ export function apply(ctx) {
|
|
|
394
469
|
var rec = runsFor(sid)[taskId]
|
|
395
470
|
var delivered = false
|
|
396
471
|
if (rec && rec.run && rec.run.localAgent) {
|
|
397
|
-
try { rec.run.localAgent.followup(makeMsg('[高优先级干预] 来自主窗口/用户的指令:\n\n' + msg + '\n\n请优先响应此指令,然后继续当前任务。')); delivered = true } catch (_) {}
|
|
472
|
+
try { rec.run.localAgent.followup(makeMsg('[高优先级干预] 来自主窗口/用户的指令:\n\n' + msg + '\n\n请优先响应此指令,然后继续当前任务。', 'notice', '高优干预: ' + taskId)); delivered = true } catch (_) {}
|
|
398
473
|
}
|
|
399
474
|
await mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === taskId }); if (t) { if (!Array.isArray(t.messages)) t.messages = []; t.messages.push({ kind: 'intervention', text: msg, at: new Date().toISOString(), by: actor }); ah(t, t.status, t.status, actor, '高优干预: ' + msg.slice(0, 200) + (delivered ? '' : '(无活跃 run,随下次派发注入)')) }; return t })
|
|
400
475
|
return { ok: true, delivered: delivered }
|
|
@@ -520,7 +595,7 @@ export function apply(ctx) {
|
|
|
520
595
|
return { ok: true, models: out }
|
|
521
596
|
})
|
|
522
597
|
handle('set-board-config', async function (args) { var sid = rpcSessionId(args); return mutateLocked(sid, function (d) { if (args.key === 'maxWorkers') d.maxWorkers = Math.max(1, Math.min(10, args.value || 3)); else if (args.key === 'maxVerifiers') d.maxVerifiers = Math.max(0, Math.min(5, args.value || 0)); else if (args.key === 'workerModel') d.workerModel = typeof args.value === 'string' ? args.value.trim() : ''; else if (args.key === 'verifierModel') d.verifierModel = typeof args.value === 'string' ? args.value.trim() : ''; return { ok: true } }) })
|
|
523
|
-
handle('create-task', async function (args) { var sid = rpcSessionId(args); var actor = getActorId(); return mutateLocked(sid, function (d) { if (args.id && d.tasks.find(function (x) { return x.id === args.id })) return { ok: false, error: 'duplicate id' }; if (args.dependsOn && args.dependsOn.length) { var derr = validateDeps(d, args.id || '(pending)', args.dependsOn); if (derr) return { ok: false, error: derr } }; var now = new Date().toISOString(); var t = { id: args.id || ('task-' + Date.now().toString(36)), title: args.title || 'Untitled', description: args.description || '', status: args.draft ? 'draft' : 'pending', priority: args.priority || 'medium', tags: args.tags || [], parentId: args.parentId || null, subtaskStrategy: null, assignMode: 'auto', assignee: null, context: { files: [], docs: [], instructions: args.instructions || '', relatedTasks: [], prerequisites: '' }, acceptance: args.acceptance || '', dependsOn: args.dependsOn || [], pipeline: args.pipeline || '', claimedBy: null, claimedAt: null, createdAt: now, resolvedAt: null, verifiedAt: null, verifiedBy: null, archivedAt: null, resolution: null, messages: [], history: [{ from: 'created', to: args.draft ? 'draft' : 'pending', timestamp: now, actor: actor, note: args.draft ? 'created as draft' : 'created' }] }; if (!t.pipeline) { t.pipeline = classifyPipeline(t); t.pipelineAuto = true }; d.tasks.push(t); return { ok: true, task: t } }) })
|
|
598
|
+
handle('create-task', async function (args) { var sid = rpcSessionId(args); var actor = getActorId(); return mutateLocked(sid, function (d) { if (args.id && d.tasks.find(function (x) { return x.id === args.id })) return { ok: false, error: 'duplicate id' }; if (args.dependsOn && args.dependsOn.length) { var derr = validateDeps(d, args.id || '(pending)', args.dependsOn); if (derr) return { ok: false, error: derr } }; var now = new Date().toISOString(); var t = { id: args.id || ('task-' + Date.now().toString(36)), title: args.title || 'Untitled', description: args.description || '', status: args.draft ? 'draft' : 'pending', priority: args.priority || 'medium', tags: args.tags || [], parentId: args.parentId || null, subtaskStrategy: null, assignMode: 'auto', assignee: null, context: { files: (Array.isArray(args.contextFiles) ? args.contextFiles.map(String).slice(0, 20) : []), docs: [], instructions: args.instructions || '', notes: (typeof args.contextNotes === 'string' ? args.contextNotes.slice(0, 8000) : ''), relatedTasks: [], prerequisites: '' }, acceptance: args.acceptance || '', dependsOn: args.dependsOn || [], pipeline: args.pipeline || '', claimedBy: null, claimedAt: null, createdAt: now, resolvedAt: null, verifiedAt: null, verifiedBy: null, archivedAt: null, resolution: null, messages: [], history: [{ from: 'created', to: args.draft ? 'draft' : 'pending', timestamp: now, actor: actor, note: args.draft ? 'created as draft' : 'created' }] }; if (!t.pipeline) { t.pipeline = classifyPipeline(t); t.pipelineAuto = true }; d.tasks.push(t); return { ok: true, task: t } }) })
|
|
524
599
|
handle('list-children', async function (args) { var sid = rpcSessionId(args); var subs = ctx.subagents; if (!subs) return { ok: true, children: [] }; try { var list = await subs.listChildren(sid); var children = (list || []).map(function (c) { return { id: String(c.sessionId || c.id || ''), label: String(c.label || c.title || c.mode || '') } }).filter(function (c) { return c.id.length > 0 }); return { ok: true, children: children } } catch (e) { return { ok: true, children: [], error: String(e) } } })
|
|
525
600
|
// ===== #14 批量操作:archive(仅 resolved/cancelled)/ set-priority(全部)=====
|
|
526
601
|
handle('batch-op', async function (args) {
|
|
@@ -595,3 +670,4 @@ export function apply(ctx) {
|
|
|
595
670
|
|
|
596
671
|
console.log('[task-board] v74 loaded (pool removed: one-shot dispatch, context injected per task, dispose on settle)')
|
|
597
672
|
}
|
|
673
|
+
|
package/lib/client.js
CHANGED
|
@@ -10,6 +10,9 @@ window.__ModuleLoader__.load({
|
|
|
10
10
|
const React = require('react')
|
|
11
11
|
|
|
12
12
|
function apply(ctx) {
|
|
13
|
+
// 硬依赖声明(见文件尾 module.exports):新版 dsh(0.1.5-rc.2)启动顺序下,
|
|
14
|
+
// 不声明 inject 时 apply 先于 slots/sessions/timer 服务注册执行,
|
|
15
|
+
// ctx.get 返回 undefined → 静默退出 → 看板按钮消失。声明后 runner 等服务就绪再 apply。
|
|
13
16
|
var slots = ctx.get('slots')
|
|
14
17
|
if (slots === undefined) return
|
|
15
18
|
var sessionsSvc = ctx.get('sessions')
|
|
@@ -516,6 +519,8 @@ function apply(ctx) {
|
|
|
516
519
|
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 3 } }, '任务描述'),
|
|
517
520
|
React.createElement('textarea', { value: editDesc, onChange: function (e) { setEditDesc(e.target.value) }, rows: 3, style: { width: '100%', fontSize: 12, padding: '6px 8px', marginBottom: 6, border: '1px solid ' + C.border, borderRadius: 4, background: C.card, color: C.text, resize: 'vertical', boxSizing: 'border-box', fontFamily: 'inherit' } }),
|
|
518
521
|
task.context && task.context.instructions ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4 } }, '指引: ' + task.context.instructions) : null,
|
|
522
|
+
task.context && Array.isArray(task.context.files) && task.context.files.length > 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4, display: 'flex', alignItems: 'flex-start', gap: 4 } }, ic('file-text', 11), React.createElement('span', null, '预研文件(派发时注入子代理): ' + task.context.files.join('、'))) : null,
|
|
523
|
+
task.context && task.context.notes ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4, whiteSpace: 'pre-wrap', maxHeight: 100, overflowY: 'auto' } }, '调研笔记: ' + task.context.notes) : null,
|
|
519
524
|
React.createElement(DepsSection, { task: task }),
|
|
520
525
|
task.acceptance ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, padding: '5px 8px', background: C.nested, borderRadius: 4, borderLeft: '2px solid ' + C.ok, fontFamily: 'monospace', display: 'flex', alignItems: 'center', gap: 4 } }, ic('flask-conical', 11), '硬性验收: ' + task.acceptance) : null,
|
|
521
526
|
task.resolution ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, padding: '5px 8px', background: C.nested, borderRadius: 4, borderLeft: '2px solid ' + C.warn, maxHeight: 120, overflowY: 'auto', whiteSpace: 'pre-wrap' } }, '📝 ' + task.resolution) : null,
|
|
@@ -589,7 +594,7 @@ function apply(ctx) {
|
|
|
589
594
|
slots.inject('shell.overlay', function () { return slots.register({ name: 'shell.overlay', id: 'task-board-top-panel' }, function () { return React.createElement(TopPanel) }) })
|
|
590
595
|
}
|
|
591
596
|
|
|
592
|
-
module.exports = { name: 'dsh-agent-board', apply: apply }
|
|
597
|
+
module.exports = { name: 'dsh-agent-board', inject: ['slots', 'sessions', 'timer'], apply: apply }
|
|
593
598
|
return module.exports
|
|
594
599
|
}
|
|
595
600
|
})
|
package/lib/core.mjs
CHANGED
|
@@ -60,7 +60,11 @@ export function classifyPipeline(t) {
|
|
|
60
60
|
export function cfg(d) { return { minWorkers: Math.max(0, Math.min(10, d.minWorkers || 1)), maxWorkers: Math.max(1, Math.min(10, d.maxWorkers || 3)), minVerifiers: Math.max(0, Math.min(5, d.minVerifiers || 0)), maxVerifiers: Math.max(0, Math.min(5, d.maxVerifiers || 2)) } }
|
|
61
61
|
|
|
62
62
|
// ===== 看板文件种子 =====
|
|
63
|
-
|
|
63
|
+
// poolStatus 必须始终在种子/归一化里存在:task_list 工具输出 poolStatus: d.poolStatus,
|
|
64
|
+
// 缺字段 = undefined → 工具结果的 lossless-JSON 校验会拒("value is not lossless JSON")
|
|
65
|
+
export function seed(sid) { return { version: 12, ownerSession: sid, boardMode: 'auto', teamMode: false, minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', poolStatus: { workers: [], verifiers: [] }, tasks: [] } }
|
|
66
|
+
// 旧文件缺 poolStatus 的归一化(读路径兜底,保证任何历史文件都满足工具输出契约)
|
|
67
|
+
export function normalizeBoard(d) { if (d && typeof d === 'object') { if (!d.poolStatus || typeof d.poolStatus !== 'object' || !Array.isArray(d.poolStatus.workers) || !Array.isArray(d.poolStatus.verifiers)) d.poolStatus = { workers: [], verifiers: [] } }; return d }
|
|
64
68
|
|
|
65
69
|
// ===== 状态流转 =====
|
|
66
70
|
export function claimCheck(d, t, sid) { if (CLAIMABLE.indexOf(t.status) < 0) return 'cannot claim in ' + t.status; if (t.claimedBy && t.claimedBy !== sid && t.status === 'in-progress') return 'claimed by ' + t.claimedBy; if (d.boardMode === 'manual' || t.assignMode === 'manual') { if (t.assignee && t.assignee !== sid) return 'assigned to ' + t.assignee }; if (isb(t)) { var p = gpt(t, d.tasks); if (!p) return 'parent not found'; if (p.status !== 'in-progress' && p.status !== 'verifying') return 'parent not in-progress' }; var mc = d.tasks.filter(function (x) { return x.claimedBy === sid && (x.status === 'in-progress' || x.status === 'verifying') && !isb(x) }); if (!isb(t) && mc.length >= MAX_CLAIMED) return 'max ' + MAX_CLAIMED + ' active'; return null }
|
|
@@ -101,7 +105,25 @@ export function buildMessages(t) {
|
|
|
101
105
|
if (!Array.isArray(t.messages) || t.messages.length === 0) return ''
|
|
102
106
|
return t.messages.map(function (m) { return '### [' + (m.kind || 'note') + '] (' + (m.at || '') + ' by ' + (m.by || '') + ')\n' + String(m.text || '').slice(0, 2000) }).join('\n\n')
|
|
103
107
|
}
|
|
104
|
-
|
|
108
|
+
// 主窗口预研上下文段:笔记(调研结论/原始需求/思路)+ 文件,由 host 组装后传入(core 保持纯函数不碰 IO)
|
|
109
|
+
// 注意:systemPrompt context 通道做严格 {{var}} 插值,内容里的 {{...}} 会直接抛异常
|
|
110
|
+
// 毁掉子代理的整个 prompt 组装——必须把 { 连写整 run 拆开(肉眼可读,插值器不再触发)。
|
|
111
|
+
// 教训:replace(/\{\{/g,'{ {') 是开变换——三连括号 {{{lo}}}(Python f-string)会得到
|
|
112
|
+
// "{ {{lo}}}",替换结果自己又造出 {{。按"连 { 整 run 拆开"才是封闭变换。
|
|
113
|
+
function sanitizeCtx(s) { return String(s).replace(/\{+/g, function (m) { return m.length === 1 ? m : m.split('').join(' ') }) }
|
|
114
|
+
export function buildContextPackSection(files, notes) {
|
|
115
|
+
var parts = []
|
|
116
|
+
if (notes && String(notes).trim()) parts.push('### 主窗口调研笔记(结论/思路/原始需求,直接采信)\n' + sanitizeCtx(String(notes)))
|
|
117
|
+
if (Array.isArray(files) && files.length) {
|
|
118
|
+
parts.push('主窗口预研文件(主窗口创建任务前已读过以下内容,直接使用,不要重复读取;标"截断"的内容可按需补读):')
|
|
119
|
+
for (var i = 0; i < files.length; i++) {
|
|
120
|
+
var f = files[i]
|
|
121
|
+
parts.push('### ' + sanitizeCtx(f.path) + (f.truncated ? '(截断)' : '') + '\n' + sanitizeCtx(f.content))
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return parts.length ? parts.join('\n\n') : ''
|
|
125
|
+
}
|
|
126
|
+
export function buildWorkerPrompt(t, pack) {
|
|
105
127
|
var notes = histNotes(t)
|
|
106
128
|
var msgs = buildMessages(t)
|
|
107
129
|
var p = '你是一个一次性任务执行 Worker。完成下面这个任务,完成后本会话即销毁。\n\ntaskId: ' + t.id + '\n任务: ' + t.title + '\n描述: ' + (t.description || '')
|
|
@@ -109,10 +131,11 @@ export function buildWorkerPrompt(t) {
|
|
|
109
131
|
if (t.acceptance) p += '\n硬性验收脚本: ' + t.acceptance + '\n(必须实际运行该命令并在自测情况中粘贴真实输出;未通过不得上报完成)'
|
|
110
132
|
if (notes) p += '\n\n该任务的过程记录(歧义上报/主窗口裁决/驳回/干预,请务必遵循最新裁决方向):\n' + notes
|
|
111
133
|
if (msgs) p += '\n\n该任务的详细消息(裁决答案/干预指令/歧义原文等,请务必遵循):\n' + msgs
|
|
134
|
+
if (pack) p += '\n\n' + pack
|
|
112
135
|
p += '\n\n完成契约(双模,工具优先):\n1. 完成时:优先调用 board_report 工具(kind=complete, taskId=' + t.id + ',summary=开发描述/changes=改动清单/selfTest=自测情况);工具不可用则按分段格式输出(## 开发描述 / ## 改动清单 / ## 自测情况)。\n2. 歧义/信息不足/需用户决策时:优先调用 board_report(kind=escalate, taskId=' + t.id + ', question=疑问);工具不可用则输出以 [ESCALATE] 开头的说明。不要猜测。上报歧义后直接结束本轮——裁决后会有新 Worker 带着裁决答案接手。'
|
|
113
136
|
return p
|
|
114
137
|
}
|
|
115
|
-
export function buildVerifierPrompt(t) {
|
|
138
|
+
export function buildVerifierPrompt(t, pack) {
|
|
116
139
|
var notes = histNotes(t)
|
|
117
140
|
var msgs = buildMessages(t)
|
|
118
141
|
var p = '你是一个一次性任务审核 Verifier。审查下面这个任务的完成质量,给出结论后本会话即销毁。\n\ntaskId: ' + t.id + '\n任务: ' + t.title + '\n描述: ' + (t.description || '').slice(0, 500)
|
|
@@ -122,6 +145,7 @@ export function buildVerifierPrompt(t) {
|
|
|
122
145
|
if (t.acceptance) p += '\n硬性验收脚本: ' + t.acceptance + '\n(必须独立复跑该命令并把真实输出贴进核对项;脚本失败必须 REJECTED)'
|
|
123
146
|
if (notes) p += '\n\n该任务的过程记录(歧义上报/主窗口裁决/驳回/干预,若有):\n' + notes + '\n注意:若过程记录显示主窗口已裁决改变任务方向,以裁决后的方向为验收标准。'
|
|
124
147
|
if (msgs) p += '\n\n该任务的详细消息(裁决答案/干预指令/歧义原文等):\n' + msgs
|
|
148
|
+
if (pack) p += '\n\n' + pack
|
|
125
149
|
p += '\n\n结论契约(双模,工具优先):\n1. 优先调用 board_verdict 工具(taskId=' + t.id + ', verdict=approved/rejected, summary=测试概要, checks=逐条核对证据含行号)。\n2. 工具不可用则首行 APPROVED: <结论> 或 REJECTED: <结论>,然后 ## 测试概要 / ## 核对项 分段。'
|
|
126
150
|
return p
|
|
127
151
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agent-board",
|
|
3
3
|
"description": "DeepSeek Harness 智能看板:任务卡片 + 一次性 Worker/Verifier 派发 + 依赖调度 + 管线分档 + Team 模式(歧义上报主窗口裁决)",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "node --test test/core.test.mjs",
|
|
9
|
-
"prepublishOnly": "node --check index.mjs && node --check lib/client.js && node --check lib/core.mjs && npm test"
|
|
9
|
+
"prepublishOnly": "node --check index.mjs && node --check lib/client.js && node --check lib/core.mjs && npm test",
|
|
10
|
+
"dev": "node scripts/dev-watch.cjs"
|
|
10
11
|
},
|
|
11
12
|
"exports": {
|
|
12
13
|
".": "./index.mjs",
|
package/test/core.test.mjs
CHANGED
|
@@ -154,6 +154,64 @@ test('buildVerifierPrompt: 注入交付物 + 验收脚本 + messages', () => {
|
|
|
154
154
|
assert.match(p, /做完了/); assert.match(p, /npm test/); assert.match(p, /board_verdict/); assert.match(p, /方案B/)
|
|
155
155
|
})
|
|
156
156
|
|
|
157
|
+
// ===== 预研文件段(主窗口选择性注入,host 读盘后传入)=====
|
|
158
|
+
test('buildContextPackSection: 空清单返回空串', () => {
|
|
159
|
+
assert.equal(core.buildContextPackSection([]), '')
|
|
160
|
+
assert.equal(core.buildContextPackSection(null), '')
|
|
161
|
+
assert.equal(core.buildContextPackSection(undefined), '')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('buildContextPackSection: 文件内容 + 截断标记', () => {
|
|
165
|
+
const s = core.buildContextPackSection([
|
|
166
|
+
{ path: 'src/a.ts', content: 'const x = 1', truncated: false },
|
|
167
|
+
{ path: 'docs/b.md', content: '说明', truncated: true },
|
|
168
|
+
])
|
|
169
|
+
assert.match(s, /主窗口预研文件/); assert.match(s, /不要重复读取/)
|
|
170
|
+
assert.match(s, /### src\/a\.ts/); assert.match(s, /const x = 1/)
|
|
171
|
+
assert.match(s, /### docs\/b\.md(截断)/); assert.match(s, /说明/)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test('buildWorkerPrompt/buildVerifierPrompt: 注入预研文件段', () => {
|
|
175
|
+
const pack = core.buildContextPackSection([{ path: 'src/x.js', content: 'hello', truncated: false }])
|
|
176
|
+
const t = mkTask({ id: 'pk' })
|
|
177
|
+
assert.match(core.buildWorkerPrompt(t, pack), /### src\/x\.js/)
|
|
178
|
+
assert.match(core.buildWorkerPrompt(t, pack), /hello/)
|
|
179
|
+
assert.match(core.buildVerifierPrompt(t, pack), /### src\/x\.js/)
|
|
180
|
+
// 不传 pack 时不出现该段
|
|
181
|
+
assert.doesNotMatch(core.buildWorkerPrompt(t), /主窗口预研文件/)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('buildContextPackSection: {{ }} 插值净化(context 通道严格插值会抛异常)', () => {
|
|
185
|
+
const s = core.buildContextPackSection([{ path: 'src/tpl.vue', content: '<div>{{ msg }}</div>', truncated: false }])
|
|
186
|
+
assert.doesNotMatch(s, /\{\{/) // 不允许残留插值触发器
|
|
187
|
+
assert.match(s, /\{ \{ msg \}\}/) // 内容可读性保留
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('buildContextPackSection: 三连花括号(Python f-string)封闭净化', () => {
|
|
191
|
+
// 真实事故:f"{{{lo}}}" 经 replace(/\{\{/g,'{ {') 变成 "{ {{lo}}}"——替换结果自己又造出 {{
|
|
192
|
+
const s = core.buildContextPackSection([{ path: 'gen.py', content: 'quant = f"{{{lo}}}" + f"{{{lo},{hi}}}"', truncated: false }])
|
|
193
|
+
assert.doesNotMatch(s, /\{\{/) // 净化必须是封闭变换
|
|
194
|
+
assert.match(s, /\{ \{ \{lo\}\}\}/) // 可读性保留
|
|
195
|
+
// 五连括号 + 单括号混合
|
|
196
|
+
const s2 = core.buildContextPackSection([{ path: 'x', content: 'a{{{{{b}}}}}{c}{{d}}', truncated: false }])
|
|
197
|
+
assert.doesNotMatch(s2, /\{\{/)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('buildContextPackSection: 笔记(思路/原始需求)注入 + 净化', () => {
|
|
201
|
+
const s = core.buildContextPackSection([{ path: 'a.js', content: 'x', truncated: false }], '用户原话:要做成{{可配置}}的')
|
|
202
|
+
assert.match(s, /主窗口调研笔记/)
|
|
203
|
+
assert.match(s, /用户原话:要做成\{ \{可配置\}\}的/) // 笔记里的 {{}} 也被净化
|
|
204
|
+
assert.match(s, /### a\.js/) // 文件段同时存在
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('buildContextPackSection: 仅笔记无文件也可注入', () => {
|
|
208
|
+
const s = core.buildContextPackSection([], '思路:先改 A 再改 B')
|
|
209
|
+
assert.match(s, /主窗口调研笔记/); assert.match(s, /先改 A 再改 B/)
|
|
210
|
+
assert.doesNotMatch(s, /预研文件/)
|
|
211
|
+
assert.equal(core.buildContextPackSection([], ''), '')
|
|
212
|
+
assert.equal(core.buildContextPackSection(null, ' '), '')
|
|
213
|
+
})
|
|
214
|
+
|
|
157
215
|
// ===== 派发决策 =====
|
|
158
216
|
test('pickDispatch: 优先级排序 + 并发上限 + 排除项', () => {
|
|
159
217
|
const tasks = [
|
|
@@ -213,6 +271,20 @@ test('seed: 初始看板结构', () => {
|
|
|
213
271
|
const d = core.seed('s1')
|
|
214
272
|
assert.equal(d.ownerSession, 's1'); assert.equal(d.boardMode, 'auto'); assert.deepEqual(d.tasks, [])
|
|
215
273
|
assert.ok(core.vt(d))
|
|
274
|
+
// poolStatus 必须存在——task_list 工具输出它,undefined 会被 lossless-JSON 校验拒
|
|
275
|
+
assert.deepEqual(d.poolStatus, { workers: [], verifiers: [] })
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
test('normalizeBoard: 旧文件补 poolStatus', () => {
|
|
279
|
+
const legacy = { tasks: [] }
|
|
280
|
+
const d = core.normalizeBoard(legacy)
|
|
281
|
+
assert.deepEqual(d.poolStatus, { workers: [], verifiers: [] })
|
|
282
|
+
// 已有合法 poolStatus 不动
|
|
283
|
+
const ok = { tasks: [], poolStatus: { workers: [{ id: 'w1' }], verifiers: [] } }
|
|
284
|
+
assert.equal(core.normalizeBoard(ok).poolStatus.workers[0].id, 'w1')
|
|
285
|
+
// 残缺的 poolStatus(缺 verifiers 数组)也修复
|
|
286
|
+
const broken = { tasks: [], poolStatus: { workers: [] } }
|
|
287
|
+
assert.deepEqual(core.normalizeBoard(broken).poolStatus, { workers: [], verifiers: [] })
|
|
216
288
|
})
|
|
217
289
|
|
|
218
290
|
test('cfg: 边界夹紧', () => {
|