dsh-agent-board 1.0.0

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/lib/core.mjs ADDED
@@ -0,0 +1,140 @@
1
+ // dsh-agent-board — 纯逻辑核心(lib/core.mjs)
2
+ // 不依赖任何 ctx/服务/IO:任务状态机、依赖校验、管线分类、prompt 构建、输出解析。
3
+ // index.mjs(IO 编排层)从这里 import;单元测试直接跑本文件(node --test)。
4
+
5
+ // ===== 常量 =====
6
+ export const MAX_CLAIMED = 3
7
+ export const CLAIMABLE = ['pending', 'blocked']
8
+ export const PRIO_RANK = { critical: 4, high: 3, medium: 2, low: 1 }
9
+
10
+ // ===== 任务数据工具 =====
11
+ export function ah(t, f, to, ac, n) { if (!Array.isArray(t.history)) t.history = []; t.history.push({ from: f, to: to, timestamp: new Date().toISOString(), actor: ac, note: n || '' }) }
12
+ export function isb(t) { return t.parentId != null }
13
+ export function gsb(p, a) { return a.filter(function (x) { return x.parentId === p }) }
14
+ export function gpt(t, a) { return isb(t) ? a.find(function (x) { return x.id === t.parentId }) : undefined }
15
+ export function vt(d) { return d && typeof d === 'object' && Array.isArray(d.tasks) }
16
+
17
+ // ===== 依赖校验(#18)=====
18
+ // 存在性 + 自引用 + DFS 环检测;返回错误消息或 null
19
+ export function validateDeps(d, taskId, deps) {
20
+ if (!Array.isArray(deps)) return 'dependsOn must be array'
21
+ for (var i = 0; i < deps.length; i++) {
22
+ var dep = deps[i]
23
+ if (dep === taskId) return 'self-dependency: ' + dep
24
+ if (!d.tasks.find(function (x) { return x.id === dep })) return 'dependency not found: ' + dep
25
+ }
26
+ var visited = {}
27
+ function reaches(cur) {
28
+ if (cur === taskId) return true
29
+ if (visited[cur]) return false
30
+ visited[cur] = true
31
+ var ct = d.tasks.find(function (x) { return x.id === cur })
32
+ var cd = (ct && Array.isArray(ct.dependsOn)) ? ct.dependsOn : []
33
+ for (var k = 0; k < cd.length; k++) { if (reaches(cd[k])) return true }
34
+ return false
35
+ }
36
+ for (var j = 0; j < deps.length; j++) { if (reaches(deps[j])) return 'circular dependency via: ' + deps[j] }
37
+ return null
38
+ }
39
+ // 依赖全部满足(resolved/archived 视为满足)
40
+ export function depsSatisfied(d, t) {
41
+ if (!Array.isArray(t.dependsOn) || t.dependsOn.length === 0) return true
42
+ return t.dependsOn.every(function (id) { var x = d.tasks.find(function (y) { return y.id === id }); return x && (x.status === 'resolved' || x.status === 'archived') })
43
+ }
44
+ // 依赖被永久阻断(依赖已 cancelled)
45
+ export function depsCancelled(d, t) {
46
+ if (!Array.isArray(t.dependsOn) || t.dependsOn.length === 0) return false
47
+ return t.dependsOn.some(function (id) { var x = d.tasks.find(function (y) { return y.id === id }); return x && x.status === 'cancelled' })
48
+ }
49
+
50
+ // ===== 管线分类(#19):规则先行,兜底 full(宁严勿漏)=====
51
+ export function classifyPipeline(t) {
52
+ if (t.acceptance && String(t.acceptance).trim()) return 'full'
53
+ var text = ((t.title || '') + ' ' + (t.description || '')).toLowerCase()
54
+ if (/解释|为什么|是什么|区别|对比|说明一下|是什么意思|how to|what is|why /.test(text)) return 'direct'
55
+ if (/文档|调研|整理|总结|报告|指南|白皮书|readme|分析文/.test(text)) return 'work'
56
+ return 'full'
57
+ }
58
+
59
+ // ===== 配置(一次性派发模型:max*=并发上限;min* 字段保留仅为兼容旧看板文件,引擎不使用)=====
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
+
62
+ // ===== 看板文件种子 =====
63
+ export function seed(sid) { return { version: 12, ownerSession: sid, boardMode: 'auto', teamMode: false, minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', tasks: [] } }
64
+
65
+ // ===== 状态流转 =====
66
+ 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 }
67
+ export function claimApply(d, t, sid, note) { var ps = t.status; t.status = 'in-progress'; t.claimedBy = sid; t.claimedAt = new Date().toISOString(); ah(t, ps, 'in-progress', sid, note) }
68
+ export function checkParentAuto(d, t) { if (!isb(t)) return null; var p = gpt(t, d.tasks); if (!p || p.status !== 'in-progress') return null; var s = gsb(p.id, d.tasks); if (s.every(function (x) { return x.status === 'resolved' })) { p.status = 'verifying'; p.resolvedAt = new Date().toISOString(); p.resolution = 'all subtasks resolved'; ah(p, 'in-progress', 'verifying', 'system', 'auto: all subtasks resolved'); return p }; return null }
69
+ export function resolveApply(d, t, sid, status, resolution, note) { var ps = t.status; if (status === 'verifying' && t.pipeline && t.pipeline !== 'full') { status = 'resolved' } t.status = status; t.resolution = resolution || null; t.resolvedAt = new Date().toISOString(); ah(t, ps, status, sid, note); var r = { ok: true, task: t }; if (status === 'verifying' && isb(t)) { var s = gsb(t.parentId, d.tasks); if (s.every(function (x) { return x.status === 'resolved' || x.id === t.id })) { var p = gpt(t, d.tasks); if (p && p.status === 'in-progress') { p.status = 'verifying'; p.resolvedAt = new Date().toISOString(); p.resolution = 'all subtasks done'; ah(p, 'in-progress', 'verifying', 'system', 'auto'); r.parentUpdated = true } } }; return r }
70
+ export function verifyApply(d, t, sid, verdict, comment) { var ps = t.status; if (verdict === 'approved') { t.status = 'resolved'; t.verifiedAt = new Date().toISOString(); t.verifiedBy = sid; ah(t, ps, 'resolved', sid, 'approved' + (comment ? ': ' + comment : '')) } else { t.status = 'in-progress'; t.resolvedAt = null; t.resolution = null; ah(t, ps, 'in-progress', sid, 'rejected' + (comment ? ': ' + comment : '')) }; var r = { ok: true, task: t }; if (verdict === 'approved' && isb(t)) { var p = checkParentAuto(d, t); if (p) { r.parentUpdated = true } }; return r }
71
+
72
+ // ===== 输出解析(文本降级路径)=====
73
+ // parseSections: 解析 ## 分段输出为结构化字段(容错:无分段时返回空对象,调用方降级)
74
+ export function parseSections(text) {
75
+ var out = {}
76
+ if (!text) return out
77
+ var re = /^##\s+(.+?)\s*$/gm, m, matches = []
78
+ while ((m = re.exec(text))) matches.push({ title: m[1], idx: m.index, end: m.index + m[0].length })
79
+ for (var i = 0; i < matches.length; i++) {
80
+ var body = text.slice(matches[i].end, i + 1 < matches.length ? matches[i + 1].idx : text.length).trim()
81
+ var title = matches[i].title
82
+ if (/开发描述/.test(title)) out.summary = body
83
+ else if (/改动/.test(title)) out.changes = body
84
+ else if (/自测/.test(title)) out.selfTest = body
85
+ else if (/测试概要|验证概要|审查概要/.test(title)) out.verifySummary = body
86
+ else if (/核对项|核验项|检查项/.test(title)) out.checks = body
87
+ }
88
+ return out
89
+ }
90
+ // verifier 结论:行首锚定 APPROVED/REJECTED(输出中提到历史驳回字眼不应误判);null = 无法判定
91
+ export function parseVerdict(output) { var m = (output || '').trim().match(/^[ \t>*#\-\s]*(APPROVED|REJECTED)\b/im); return m ? m[1].toUpperCase() : null }
92
+ // worker 歧义标记
93
+ export function isEscalation(output) { return /\[ESCALATE\]/i.test(output || '') }
94
+ // 从 run.result.output(ContentBlock[])提取纯文本
95
+ export function outputText(res) { if (!res || !res.output) return ''; var parts = []; for (var i = 0; i < res.output.length; i++) { var b = res.output[i]; if (b && b.type === 'text' && b.text) parts.push(b.text) } return parts.join('\n') }
96
+
97
+ // ===== 一次性子代理 prompt 构建(上下文由主窗口 agent 写入 description/instructions,系统只追加生命周期记录)=====
98
+ // 过程记录注入:驳回/裁决/干预 history + messages(裁决答案/干预指令/歧义原文)是系统管理的,必须带给子代理
99
+ export function histNotes(t) { return (t.history || []).filter(function (h) { return h.note && (/歧义|裁决|驳回|干预|rejected/i.test(h.note)) }).map(function (h) { return '- [' + h.timestamp + '] ' + String(h.note).slice(0, 300) }).join('\n') }
100
+ export function buildMessages(t) {
101
+ if (!Array.isArray(t.messages) || t.messages.length === 0) return ''
102
+ 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
+ }
104
+ export function buildWorkerPrompt(t) {
105
+ var notes = histNotes(t)
106
+ var msgs = buildMessages(t)
107
+ var p = '你是一个一次性任务执行 Worker。完成下面这个任务,完成后本会话即销毁。\n\ntaskId: ' + t.id + '\n任务: ' + t.title + '\n描述: ' + (t.description || '')
108
+ if (t.context && t.context.instructions) p += '\n指引: ' + t.context.instructions
109
+ if (t.acceptance) p += '\n硬性验收脚本: ' + t.acceptance + '\n(必须实际运行该命令并在自测情况中粘贴真实输出;未通过不得上报完成)'
110
+ if (notes) p += '\n\n该任务的过程记录(歧义上报/主窗口裁决/驳回/干预,请务必遵循最新裁决方向):\n' + notes
111
+ if (msgs) p += '\n\n该任务的详细消息(裁决答案/干预指令/歧义原文等,请务必遵循):\n' + msgs
112
+ 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
+ return p
114
+ }
115
+ export function buildVerifierPrompt(t) {
116
+ var notes = histNotes(t)
117
+ var msgs = buildMessages(t)
118
+ var p = '你是一个一次性任务审核 Verifier。审查下面这个任务的完成质量,给出结论后本会话即销毁。\n\ntaskId: ' + t.id + '\n任务: ' + t.title + '\n描述: ' + (t.description || '').slice(0, 500)
119
+ p += '\n完成说明: ' + (t.resolution || '(无)')
120
+ p += '\n交付物: ' + (t.deliverable ? ('开发描述: ' + (t.deliverable.summary || '') + '\n改动清单: ' + (t.deliverable.changes || '') + '\n自测情况: ' + (t.deliverable.selfTest || '')) : '(无)').slice(0, 1500)
121
+ if (t.context && t.context.instructions) p += '\n指引: ' + t.context.instructions
122
+ if (t.acceptance) p += '\n硬性验收脚本: ' + t.acceptance + '\n(必须独立复跑该命令并把真实输出贴进核对项;脚本失败必须 REJECTED)'
123
+ if (notes) p += '\n\n该任务的过程记录(歧义上报/主窗口裁决/驳回/干预,若有):\n' + notes + '\n注意:若过程记录显示主窗口已裁决改变任务方向,以裁决后的方向为验收标准。'
124
+ if (msgs) p += '\n\n该任务的详细消息(裁决答案/干预指令/歧义原文等):\n' + msgs
125
+ p += '\n\n结论契约(双模,工具优先):\n1. 优先调用 board_verdict 工具(taskId=' + t.id + ', verdict=approved/rejected, summary=测试概要, checks=逐条核对证据含行号)。\n2. 工具不可用则首行 APPROVED: <结论> 或 REJECTED: <结论>,然后 ## 测试概要 / ## 核对项 分段。'
126
+ return p
127
+ }
128
+
129
+ // ===== 派发决策(纯函数版,poolCycle 持锁段调用)=====
130
+ // 返回 { pendings, verifs }:按优先级排序的可派发任务清单
131
+ export function pickDispatch(d, capW, capV, busyTaskIds) {
132
+ var pendings = capW > 0 ? d.tasks.filter(function (t) { return t.status === 'pending' && !t.claimedBy && t.assignMode !== 'manual' && t.pipeline !== 'direct' && depsSatisfied(d, t) && !t.escalation })
133
+ .sort(function (a, b) { var p = (PRIO_RANK[b.priority] || 2) - (PRIO_RANK[a.priority] || 2); return p !== 0 ? p : (a.createdAt || '').localeCompare(b.createdAt || '') }).slice(0, capW) : []
134
+ var verifs = capV > 0 ? d.tasks.filter(function (t) { return t.status === 'verifying' && (!t.pipeline || t.pipeline === 'full') && !t.escalation && !(busyTaskIds && busyTaskIds[t.id]) }).slice(0, capV) : []
135
+ return { pendings: pendings, verifs: verifs }
136
+ }
137
+ // 孤儿回收判定:in-progress 且 claimedBy 非主会话、无活跃 run、无 escalation、超 2 分钟
138
+ export function isOrphan(d, t, runs, now) {
139
+ return t.status === 'in-progress' && t.claimedBy && t.claimedBy !== d.ownerSession && !(runs && runs[t.id]) && !t.escalation && (now - new Date(t.claimedAt || 0).getTime()) > 120000
140
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "dsh-agent-board",
3
+ "description": "DeepSeek Harness 智能看板:任务卡片 + 一次性 Worker/Verifier 派发 + 依赖调度 + 管线分档 + Team 模式(歧义上报主窗口裁决)",
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "scripts": {
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"
10
+ },
11
+ "exports": {
12
+ ".": "./index.mjs",
13
+ "./client": "./lib/client.js",
14
+ "./cordis.patch.yml": "./cordis.patch.yml",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "dsh": {
18
+ "bundle": {
19
+ "patch": "./cordis.patch.yml"
20
+ },
21
+ "client": {
22
+ "inject": [
23
+ "@deepseek-ai/dsh-client-runtime"
24
+ ],
25
+ "platform": "web"
26
+ }
27
+ },
28
+ "files": [
29
+ "index.mjs",
30
+ "lib/core.mjs",
31
+ "lib/client.js",
32
+ "cordis.patch.yml",
33
+ "README.md",
34
+ "test/"
35
+ ],
36
+ "keywords": [
37
+ "dsh",
38
+ "dsh-plugin",
39
+ "deepseek-harness",
40
+ "task-board",
41
+ "kanban",
42
+ "agent",
43
+ "subagent",
44
+ "task-dispatch"
45
+ ],
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/PPawnsir/task-board-plugin.git",
49
+ "directory": "packages/dsh-agent-board"
50
+ },
51
+ "homepage": "https://github.com/PPawnsir/task-board-plugin#readme",
52
+ "bugs": "https://github.com/PPawnsir/task-board-plugin/issues",
53
+ "engines": {
54
+ "node": ">=22"
55
+ },
56
+ "license": "MIT"
57
+ }
@@ -0,0 +1,252 @@
1
+ // dsh-agent-board 纯逻辑单元测试 — node --test packages/dsh-agent-board/test/
2
+ // 覆盖:状态机流转 / 依赖校验与环检测 / 管线分类 / 输出解析 / prompt 构建 / 派发决策 / 孤儿回收
3
+ import { test } from 'node:test'
4
+ import assert from 'node:assert/strict'
5
+ import * as core from '../lib/core.mjs'
6
+
7
+ function mkTask(over) { return Object.assign({ id: 't1', title: 'T', description: '', status: 'pending', priority: 'medium', tags: [], parentId: null, assignMode: 'auto', assignee: null, context: { instructions: '' }, acceptance: '', dependsOn: [], pipeline: 'full', claimedBy: null, claimedAt: null, createdAt: '2026-01-01T00:00:00Z', resolvedAt: null, history: [], messages: [] }, over || {}) }
8
+ function mkBoard(tasks) { return { version: 11, ownerSession: 's1', boardMode: 'auto', tasks: tasks || [] } }
9
+
10
+ // ===== 状态机 =====
11
+ test('claimApply: pending → in-progress,记 history', () => {
12
+ const t = mkTask(); const d = mkBoard([t])
13
+ core.claimApply(d, t, 'actor1', 'test')
14
+ assert.equal(t.status, 'in-progress'); assert.equal(t.claimedBy, 'actor1')
15
+ assert.equal(t.history.length, 1); assert.equal(t.history[0].to, 'in-progress')
16
+ })
17
+
18
+ test('claimCheck: 非可认领状态拒绝', () => {
19
+ const t = mkTask({ status: 'verifying' }); const d = mkBoard([t])
20
+ assert.match(core.claimCheck(d, t, 'a'), /cannot claim/)
21
+ })
22
+
23
+ test('claimCheck: 已被他人认领拒绝(in-progress 首先被 CLAIMABLE 拦住)', () => {
24
+ const t = mkTask({ status: 'in-progress', claimedBy: 'other' }); const d = mkBoard([t])
25
+ assert.match(core.claimCheck(d, t, 'a'), /cannot claim/) // CLAIMABLE 只含 pending/blocked,in-progress 先被拦
26
+ // claimedBy 分支的实际生效场景:blocked 状态被他人认领后重派
27
+ const t2 = mkTask({ status: 'blocked', claimedBy: 'other' }); const d2 = mkBoard([t2])
28
+ assert.equal(core.claimCheck(d2, t2, 'a'), null) // blocked 可重派(claimedBy 分支只在 in-progress 才有意义)
29
+ })
30
+
31
+ test('claimCheck: 手动模式+指定 assignee 拒绝他人', () => {
32
+ const t = mkTask({ assignMode: 'manual', assignee: 'bob' }); const d = mkBoard([t])
33
+ assert.match(core.claimCheck(d, t, 'alice'), /assigned to bob/)
34
+ assert.equal(core.claimCheck(d, t, 'bob'), null)
35
+ })
36
+
37
+ test('claimCheck: 超过 MAX_CLAIMED 拒绝', () => {
38
+ const mine = [1, 2, 3].map(i => mkTask({ id: 'm' + i, status: 'in-progress', claimedBy: 'me' }))
39
+ const t = mkTask({ id: 'new' }); const d = mkBoard(mine.concat([t]))
40
+ assert.match(core.claimCheck(d, t, 'me'), /max 3 active/)
41
+ })
42
+
43
+ test('resolveApply: full 档 → verifying;work/direct 档 → 直接 resolved', () => {
44
+ const tf = mkTask({ pipeline: 'full' }); core.resolveApply(mkBoard([tf]), tf, 'w', 'verifying', 'done')
45
+ assert.equal(tf.status, 'verifying')
46
+ const tw = mkTask({ pipeline: 'work' }); core.resolveApply(mkBoard([tw]), tw, 'w', 'verifying', 'done')
47
+ assert.equal(tw.status, 'resolved')
48
+ })
49
+
50
+ test('verifyApply: approved → resolved;rejected → 回 in-progress', () => {
51
+ const t = mkTask({ status: 'verifying' }); core.verifyApply(mkBoard([t]), t, 'v', 'approved', 'ok')
52
+ assert.equal(t.status, 'resolved'); assert.ok(t.verifiedAt)
53
+ const t2 = mkTask({ status: 'verifying' }); core.verifyApply(mkBoard([t2]), t2, 'v', 'rejected', 'bad')
54
+ assert.equal(t2.status, 'in-progress'); assert.equal(t2.resolution, null)
55
+ })
56
+
57
+ test('子任务全 resolved → 父任务自动 verifying', () => {
58
+ const p = mkTask({ id: 'p', status: 'in-progress' })
59
+ const c1 = mkTask({ id: 'c1', parentId: 'p', status: 'verifying' })
60
+ const c2 = mkTask({ id: 'c2', parentId: 'p', status: 'resolved' })
61
+ const d = mkBoard([p, c1, c2])
62
+ const r = core.verifyApply(d, c1, 'v', 'approved')
63
+ assert.equal(r.parentUpdated, true); assert.equal(p.status, 'verifying')
64
+ })
65
+
66
+ // ===== 依赖 =====
67
+ test('validateDeps: 自引用/不存在/成环 全拒绝', () => {
68
+ const a = mkTask({ id: 'a' }); const b = mkTask({ id: 'b', dependsOn: ['a'] })
69
+ const d = mkBoard([a, b])
70
+ assert.match(core.validateDeps(d, 'a', ['a']), /self-dependency/)
71
+ assert.match(core.validateDeps(d, 'a', ['ghost']), /not found/)
72
+ // a 依赖 b,b 已依赖 a → 成环
73
+ const d2 = mkBoard([mkTask({ id: 'a', dependsOn: ['b'] }), mkTask({ id: 'b', dependsOn: ['a'] })])
74
+ assert.match(core.validateDeps(d2, 'a', ['b']), /circular/)
75
+ })
76
+
77
+ test('depsSatisfied: resolved/archived 算满足,其余不算', () => {
78
+ const dep = mkTask({ id: 'dep', status: 'resolved' })
79
+ const t = mkTask({ dependsOn: ['dep'] }); const d = mkBoard([dep, t])
80
+ assert.equal(core.depsSatisfied(d, t), true)
81
+ dep.status = 'in-progress'
82
+ assert.equal(core.depsSatisfied(d, t), false)
83
+ dep.status = 'archived'
84
+ assert.equal(core.depsSatisfied(d, t), true)
85
+ })
86
+
87
+ test('depsCancelled: 依赖被取消 → true', () => {
88
+ const dep = mkTask({ id: 'dep', status: 'cancelled' })
89
+ const t = mkTask({ dependsOn: ['dep'] })
90
+ assert.equal(core.depsCancelled(mkBoard([dep, t]), t), true)
91
+ })
92
+
93
+ // ===== 管线分类 =====
94
+ test('classifyPipeline: 验收脚本→full;问答→direct;文档→work;兜底 full', () => {
95
+ assert.equal(core.classifyPipeline(mkTask({ acceptance: 'npm test' })), 'full')
96
+ assert.equal(core.classifyPipeline(mkTask({ title: '解释一下这个函数' })), 'direct')
97
+ assert.equal(core.classifyPipeline(mkTask({ title: '整理 API 文档' })), 'work')
98
+ assert.equal(core.classifyPipeline(mkTask({ title: '实现登录功能' })), 'full')
99
+ })
100
+
101
+ // ===== 输出解析 =====
102
+ test('parseSections: 分段解析', () => {
103
+ const out = core.parseSections('前言\n## 开发描述\n做了 A\n## 改动清单\n改了 x.js\n## 自测情况\nnpm test pass')
104
+ assert.equal(out.summary, '做了 A'); assert.equal(out.changes, '改了 x.js'); assert.equal(out.selfTest, 'npm test pass')
105
+ assert.deepEqual(core.parseSections(''), {})
106
+ assert.deepEqual(core.parseSections('没有分段的纯文本'), {})
107
+ })
108
+
109
+ test('parseVerdict: 行首锚定,历史提及不误判', () => {
110
+ assert.equal(core.parseVerdict('APPROVED: 通过'), 'APPROVED')
111
+ assert.equal(core.parseVerdict('REJECTED: 不达标'), 'REJECTED')
112
+ assert.equal(core.parseVerdict('> APPROVED\n引用块里的也算'), 'APPROVED') // 引用块前缀允许
113
+ assert.equal(core.parseVerdict('上次被 REJECTED 了,这次我觉得行'), null) // 非行首锚定 → 不误判
114
+ assert.equal(core.parseVerdict(''), null)
115
+ })
116
+
117
+ test('isEscalation: [ESCALATE] 标记', () => {
118
+ assert.equal(core.isEscalation('[ESCALATE] 缺少需求'), true)
119
+ assert.equal(core.isEscalation('正常完成'), false)
120
+ })
121
+
122
+ test('outputText: ContentBlock[] 提取文本', () => {
123
+ assert.equal(core.outputText({ output: [{ type: 'text', text: 'a' }, { type: 'image' }, { type: 'text', text: 'b' }] }), 'a\nb')
124
+ assert.equal(core.outputText(null), '')
125
+ assert.equal(core.outputText({}), '')
126
+ })
127
+
128
+ // ===== prompt 构建 =====
129
+ test('buildWorkerPrompt: 注入 taskId/描述/验收脚本/过程记录', () => {
130
+ const t = mkTask({ id: 'tx', description: '做个功能', acceptance: 'npm test', history: [{ note: '驳回: 缺测试', timestamp: '2026-01-01' }] })
131
+ const p = core.buildWorkerPrompt(t)
132
+ assert.match(p, /taskId: tx/); assert.match(p, /做个功能/); assert.match(p, /npm test/); assert.match(p, /驳回: 缺测试/); assert.match(p, /board_report/)
133
+ })
134
+
135
+ test('buildWorkerPrompt: 不自动注入 context.files/docs(由主窗口写入 description)', () => {
136
+ const t = mkTask({ id: 'ctx', context: { instructions: '看 README', files: ['src/x.js'], docs: ['设计文档'], relatedTasks: ['task-a'], prerequisites: 'Node 22+' } })
137
+ const p = core.buildWorkerPrompt(t)
138
+ assert.match(p, /看 README/) // instructions 注入(主窗口写的指引)
139
+ assert.doesNotMatch(p, /src\/x\.js/) // files 不自动注入
140
+ assert.doesNotMatch(p, /设计文档/) // docs 不自动注入
141
+ assert.doesNotMatch(p, /task-a/) // relatedTasks 不自动注入
142
+ assert.doesNotMatch(p, /Node 22/) // prerequisites 不自动注入
143
+ })
144
+
145
+ test('buildWorkerPrompt: 注入 messages(裁决答案/干预指令/歧义原文——系统管理的生命周期记录)', () => {
146
+ const t = mkTask({ id: 'msg', messages: [{ kind: 'arbitration', text: '用方案B', at: '2026-01-01', by: 'main' }, { kind: 'intervention', text: '注意边界', at: '2026-01-02', by: 'main' }] })
147
+ const p = core.buildWorkerPrompt(t)
148
+ assert.match(p, /用方案B/); assert.match(p, /注意边界/); assert.match(p, /arbitration/); assert.match(p, /intervention/)
149
+ })
150
+
151
+ test('buildVerifierPrompt: 注入交付物 + 验收脚本 + messages', () => {
152
+ const t = mkTask({ id: 'tx', acceptance: 'npm test', deliverable: { summary: '做完了', changes: 'x.js', selfTest: 'pass' }, messages: [{ kind: 'arbitration', text: '方案B', at: '2026-01-01', by: 'main' }] })
153
+ const p = core.buildVerifierPrompt(t)
154
+ assert.match(p, /做完了/); assert.match(p, /npm test/); assert.match(p, /board_verdict/); assert.match(p, /方案B/)
155
+ })
156
+
157
+ // ===== 派发决策 =====
158
+ test('pickDispatch: 优先级排序 + 并发上限 + 排除项', () => {
159
+ const tasks = [
160
+ mkTask({ id: 'lo', priority: 'low', createdAt: '2026-01-02' }),
161
+ mkTask({ id: 'hi', priority: 'critical', createdAt: '2026-01-03' }),
162
+ mkTask({ id: 'claimed', claimedBy: 'someone' }),
163
+ mkTask({ id: 'manual', assignMode: 'manual' }),
164
+ mkTask({ id: 'direct', pipeline: 'direct' }),
165
+ mkTask({ id: 'esc', escalation: { question: 'q' } }),
166
+ ]
167
+ const d = mkBoard(tasks)
168
+ const r = core.pickDispatch(d, 3, 0, null)
169
+ assert.deepEqual(r.pendings.map(t => t.id), ['hi', 'lo']) // critical 优先,claimed/manual/direct/escalation 全排除
170
+ assert.equal(r.verifs.length, 0) // capV=0
171
+ const r2 = core.pickDispatch(d, 1, 0, null)
172
+ assert.equal(r2.pendings.length, 1) // cap 生效
173
+ })
174
+
175
+ test('pickDispatch: 依赖未满足不派发', () => {
176
+ const dep = mkTask({ id: 'dep', status: 'in-progress' })
177
+ const t = mkTask({ id: 'w8', dependsOn: ['dep'] })
178
+ const r = core.pickDispatch(mkBoard([dep, t]), 5, 0, null)
179
+ assert.equal(r.pendings.length, 0)
180
+ dep.status = 'resolved'
181
+ const r2 = core.pickDispatch(mkBoard([dep, t]), 5, 0, null)
182
+ assert.equal(r2.pendings.length, 1)
183
+ })
184
+
185
+ test('pickDispatch: verifying 只派 full 档,排除 escalation 和忙中', () => {
186
+ const tasks = [
187
+ mkTask({ id: 'vf', status: 'verifying', pipeline: 'full' }),
188
+ mkTask({ id: 'vw', status: 'verifying', pipeline: 'work' }),
189
+ mkTask({ id: 've', status: 'verifying', pipeline: 'full', escalation: { question: 'q' } }),
190
+ mkTask({ id: 'vb', status: 'verifying', pipeline: 'full' }),
191
+ ]
192
+ const r = core.pickDispatch(mkBoard(tasks), 0, 5, { vb: true })
193
+ assert.deepEqual(r.verifs.map(t => t.id), ['vf']) // work 档不派审、escalation 不派、忙中不派
194
+ })
195
+
196
+ // ===== 孤儿回收 =====
197
+ test('isOrphan: 认领者非主会话 + 无活跃 run + 超 2 分钟', () => {
198
+ const old = new Date(Date.now() - 200000).toISOString()
199
+ const t = mkTask({ status: 'in-progress', claimedBy: 'run-123', claimedAt: old })
200
+ const d = mkBoard([t])
201
+ assert.equal(core.isOrphan(d, t, {}, Date.now()), true)
202
+ assert.equal(core.isOrphan(d, t, { t1: {} }, Date.now()), false) // 有活跃 run
203
+ t.claimedBy = 's1' // 主会话自己认领的
204
+ assert.equal(core.isOrphan(d, t, {}, Date.now()), false)
205
+ t.claimedBy = 'run-123'; t.escalation = { question: 'q' }
206
+ assert.equal(core.isOrphan(d, t, {}, Date.now()), false) // 待裁决的不回收
207
+ delete t.escalation; t.claimedAt = new Date().toISOString()
208
+ assert.equal(core.isOrphan(d, t, {}, Date.now()), false) // 刚认领不超 2 分钟
209
+ })
210
+
211
+ // ===== 种子与配置 =====
212
+ test('seed: 初始看板结构', () => {
213
+ const d = core.seed('s1')
214
+ assert.equal(d.ownerSession, 's1'); assert.equal(d.boardMode, 'auto'); assert.deepEqual(d.tasks, [])
215
+ assert.ok(core.vt(d))
216
+ })
217
+
218
+ test('cfg: 边界夹紧', () => {
219
+ const c = core.cfg({ minWorkers: -1, maxWorkers: 99, minVerifiers: 99, maxVerifiers: -1 })
220
+ assert.equal(c.minWorkers, 0); assert.equal(c.maxWorkers, 10); assert.equal(c.minVerifiers, 5); assert.equal(c.maxVerifiers, 0)
221
+ })
222
+
223
+ // ===== 回归:PRIO_RANK 使用一致性 =====
224
+ test('PRIO_RANK: 与 pickDispatch 排序一致', () => {
225
+ // PRIO_RANK 必须有 4 档,且值与 pickDispatch 的 sort 逻辑一致
226
+ assert.equal(core.PRIO_RANK.critical, 4); assert.equal(core.PRIO_RANK.high, 3)
227
+ assert.equal(core.PRIO_RANK.medium, 2); assert.equal(core.PRIO_RANK.low, 1)
228
+ // pickDispatch 优先级排序验证(已在前面测过,这里确认 PRIO_RANK 可用)
229
+ const d = mkBoard([mkTask({ id: 'lo', priority: 'low' }), mkTask({ id: 'hi', priority: 'critical' })])
230
+ const r = core.pickDispatch(d, 5, 0, null)
231
+ assert.equal(r.pendings[0].id, 'hi') // critical 排前面
232
+ })
233
+
234
+ // ===== 回归:outputText 对各种 SubagentResult 形态 =====
235
+ test('outputText: 各种 ContentBlock 形态', () => {
236
+ assert.equal(core.outputText({ output: [{ type: 'text', text: 'hello' }] }), 'hello')
237
+ assert.equal(core.outputText({ output: [] }), '')
238
+ assert.equal(core.outputText({ output: [{ type: 'tool_use' }] }), '') // 无 text block
239
+ assert.equal(core.outputText({ stopReason: 'completed' }), '') // 无 output 字段
240
+ assert.equal(core.outputText(undefined), '')
241
+ assert.equal(core.outputText(null), '')
242
+ })
243
+
244
+ // ===== 回归:isOrphan 边界 =====
245
+ test('isOrphan: cancelled 状态不回收(不在 in-progress)', () => {
246
+ const t = mkTask({ status: 'cancelled', claimedBy: 'run-x', claimedAt: new Date(Date.now() - 999999).toISOString() })
247
+ assert.equal(core.isOrphan(mkBoard([t]), t, {}, Date.now()), false)
248
+ })
249
+ test('isOrphan: 刚 claim 的(<2min)不回收', () => {
250
+ const t = mkTask({ status: 'in-progress', claimedBy: 'run-x', claimedAt: new Date().toISOString() })
251
+ assert.equal(core.isOrphan(mkBoard([t]), t, {}, Date.now()), false)
252
+ })