dsh-agent-board 1.9.0 → 1.9.2

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/notify.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  // makeMsg(纯函数,模块级 export,rpc.mjs 直接 import):插件来源消息构造。
3
3
  // createNotify(ctx, state, deps):歧义上报 25s 去抖通知 / 回执批量聚合(45s 或满 5 条)+ 主窗口空闲门控 /
4
4
  // 系统异常通知队列 / 投递前按看板现状过滤过期项(含 dispatched 派发回执的离场过滤)。共享状态全部经 state 显式注入。
5
- // 回执两类入口共用同一聚合队列:notifyTaskDone(完成/阻塞)+ notifyDispatched(派发即回执)。
5
+ // 回执三类入口共用同一聚合队列:notifyTaskDone(完成/阻塞)+ notifyDispatched(派发即回执)+ notifyDecomposed(拆解官回执)。
6
6
  // 回执开关(板级 notifyDispatch/notifyDone,设置区「通知」小节)不在本模块判定——闸门在调用方
7
7
  // (dispatch.mjs 派发/结算处读 cfg 快照),这样歧义裁决通知(notifyMainWindow)天然不受开关影响。
8
8
 
@@ -99,6 +99,29 @@ export function createNotify(ctx, state, deps) {
99
99
  var stamp = 'dispatched|' + String(role || 'worker') + '|' + (t.claimedAt || t.verifierRunAt || '')
100
100
  pushReceipt(sid, t.id + ':' + stamp, { kind: 'dispatched', role: String(role || 'worker'), title: t.title, id: t.id, summary: '', note: '' })
101
101
  }
102
+ // 拆解官回执(task-mvrqiu6x):拆解官落定(split 物化 / keep 判定)→ 完成型回执,走同一聚合队列,
103
+ // flush 出「🧩 已拆解」分区。源卡按设计停在 draft/pending(提案由用户确认后逐张 publish),
104
+ // 回执 payload 带源卡 id 供投递前过滤——只有源卡已 archived(人已手动归档=已知悉)才丢弃,
105
+ // draft/pending 等其余状态一律保留(这正是原设计绕行系统通道的原因,豁免断言钉死)。
106
+ // 幂等键 = 源卡id|verdict|子卡id 集合连接:同一批提案重复上报(拆解官重跑/结算重放)指纹一致 → 吞掉;
107
+ // 换一批提案(子卡 id 变)/ verdict 变 → 指纹不同 → 正常新回执。
108
+ function notifyDecomposed(sid, payload) {
109
+ if (!payload || !payload.sourceId) return
110
+ var kids = Array.isArray(payload.children) ? payload.children : []
111
+ var kidIds = []
112
+ for (var i = 0; i < kids.length; i++) kidIds.push(String((kids[i] && kids[i].id) == null ? '' : kids[i].id))
113
+ var summary
114
+ if (payload.verdict === 'split') {
115
+ var titles = []
116
+ for (var j = 0; j < kids.length; j++) titles.push(String((kids[j] && kids[j].title) == null ? '' : kids[j].title).trim().slice(0, 60))
117
+ var list = titles.join('、')
118
+ if (list.length > 300) list = list.slice(0, 300) + '…'
119
+ summary = 'split:' + kids.length + ' 张子卡(' + list + ')'
120
+ } else summary = 'keep:无需拆分'
121
+ var note = payload.reason ? String(payload.reason).slice(0, 150) : ''
122
+ var stamp = String(payload.sourceId) + '|decomposed|' + String(payload.verdict || '') + '|' + kidIds.join(',')
123
+ pushReceipt(sid, stamp, { kind: 'decomposed', id: String(payload.sourceId), title: payload.sourceTitle || String(payload.sourceId), summary: summary, note: note })
124
+ }
102
125
  // 系统级异常通知队列(易失,随回执冲刷):模型熔断/spawn 失败/孤儿回收/看门狗标记
103
126
  // taskId 可选:告警类通知(软超时提醒等)语义只对「任务仍在执行中」成立,
104
127
  // 带上任务 id 后 flush 投递前可重读看板校验,任务已落定的过期告警直接丢弃
@@ -118,13 +141,18 @@ export function createNotify(ctx, state, deps) {
118
141
  function noteOf(status) { return status === 'verifying' ? '验证中' : '进行中' }
119
142
  // 组装投递文本(入参已是过滤后的存活项,避免用已丢弃项的计数)
120
143
  function composeText(items, notes) {
121
- var done = [], blocked = [], dispatched = []
122
- for (var i = 0; i < items.length; i++) { (items[i].kind === 'resolved' ? done : (items[i].kind === 'dispatched' ? dispatched : blocked)).push(items[i]) }
144
+ var done = [], blocked = [], dispatched = [], decomposed = []
145
+ for (var i = 0; i < items.length; i++) { (items[i].kind === 'resolved' ? done : (items[i].kind === 'decomposed' ? decomposed : (items[i].kind === 'dispatched' ? dispatched : blocked))).push(items[i]) }
123
146
  var lines = [items.length ? '📋 [任务看板] 回执摘要(' + items.length + ' 条)' : '📋 [任务看板] 系统通知', '']
124
147
  if (dispatched.length) {
125
148
  lines.push('🚀 已派发 ' + dispatched.length + ' 个:')
126
149
  for (var d = 0; d < dispatched.length && d < 8; d++) lines.push(' · ' + dispatched[d].title + ' (' + dispatched[d].id + ') — ' + (dispatched[d].role === 'verifier' ? 'Verifier 验收中' : 'Worker 执行中'))
127
150
  }
151
+ if (decomposed.length) {
152
+ // 拆解官正式回执分区(task-mvrqiu6x):源卡标题 (源卡id) — split:M 张子卡(标题列表)/ keep:无需拆分
153
+ lines.push('🧩 已拆解 ' + decomposed.length + ' 个(待确认发布):')
154
+ for (var m = 0; m < decomposed.length && m < 8; m++) lines.push(' · ' + decomposed[m].title + ' (' + decomposed[m].id + ')' + (decomposed[m].summary ? ' — ' + decomposed[m].summary.slice(0, 120) : ''))
155
+ }
128
156
  if (done.length) {
129
157
  lines.push('✅ 完成 ' + done.length + ' 个:')
130
158
  for (var j = 0; j < done.length && j < 8; j++) lines.push(' · ' + done[j].title + ' (' + done[j].id + ')' + (done[j].summary ? ' — ' + done[j].summary.slice(0, 120) : ''))
@@ -169,6 +197,11 @@ export function createNotify(ctx, state, deps) {
169
197
  var it = items[j]
170
198
  var tt = findTask(it.id)
171
199
  if (tt && tt.status === 'archived') continue
200
+ // 拆解回执豁免(task-mvrqiu6x):kind='decomposed' 的源卡按设计停在 draft/pending(拆解提案
201
+ // 由用户确认后发布)——不得因「任务非 in-progress」被丢(这正是原设计绕行系统通道的原因,
202
+ // 此豁免由断言钉死,防后人把「非 in-progress 丢弃」改成通用规则误伤);唯一离场规则是
203
+ // 源卡已 archived(上面那条,人已手动归档=已知悉)。rt 读盘失败(tt 为 null)时同样保留。
204
+ if (it.kind === 'decomposed') { keptItems.push(it); continue }
172
205
  if (it.kind === 'dispatched' && tt && tt.status !== 'in-progress') continue
173
206
  keptItems.push(it)
174
207
  }
@@ -217,5 +250,5 @@ export function createNotify(ctx, state, deps) {
217
250
  } catch (e) { console.error('[task-board] late approve notify failed:', String(e)) }
218
251
  }
219
252
 
220
- return { maybeNotify: maybeNotify, notifyTaskDone: notifyTaskDone, notifyDispatched: notifyDispatched, pushSysNote: pushSysNote, notifyBoardCorrupt: notifyBoardCorrupt, notifyLateReject: notifyLateReject, notifyLateApprove: notifyLateApprove }
253
+ return { maybeNotify: maybeNotify, notifyTaskDone: notifyTaskDone, notifyDispatched: notifyDispatched, notifyDecomposed: notifyDecomposed, pushSysNote: pushSysNote, notifyBoardCorrupt: notifyBoardCorrupt, notifyLateReject: notifyLateReject, notifyLateApprove: notifyLateApprove }
221
254
  }
package/lib/policy.mjs CHANGED
@@ -102,7 +102,7 @@ export var DECOMPOSE_SCHEMA = {
102
102
  modelHint: { type: 'string' },
103
103
  touches: { type: 'array', items: { type: 'string' } },
104
104
  acceptance: { type: 'string' },
105
- dependsOnIdx: { type: 'array', items: { type: 'number' } },
105
+ dependsOn: { type: 'array', items: { type: 'string' } },
106
106
  },
107
107
  },
108
108
  },
@@ -115,7 +115,7 @@ export var DECOMPOSE_SCHEMA = {
115
115
  // ③ size 判档语义:light=单点小改/文档/配置,standard=常规模块改动(缺省),heavy=跨模块/高风险/需强推理;
116
116
  // modelHint 仅在有特殊理由时填(如超长上下文);
117
117
  // ④ children 上限 DECOMPOSE_MAX_CHILDREN 张;verdict=keep 表示源卡不用拆;
118
- // ⑤ dependsOnIdx 只许引用本批**前排**子卡下标(0 起)——物化侧据此翻译成本批真实 id,按构造天然无环。
118
+ // ⑤ dependsOn 只许引用本批**前排**子卡的序号('1' 起,string)——物化侧翻译成本批真实 id,按构造天然无环;无法解析的序号丢弃并在该子卡 messages 留痕。
119
119
  export function buildDecomposerPrompt(t, pack) {
120
120
  var p = '你是任务看板的拆解官(一次性 run)。你的唯一交付物是**结构化拆解提案**(由 outputSchema 强箍:verdict/children/rationale),提案落定后本会话即销毁。\n\n'
121
121
  p += '源卡:' + t.id + ' · ' + String(t.title || '') + '\n'
@@ -129,7 +129,7 @@ export function buildDecomposerPrompt(t, pack) {
129
129
  p += '1. 第一步必须先用 read 工具读上面文件清单里的文件(按所给行号范围),从**实现侧**拆(按模块/文件边界切分);禁止不读代码就拍脑袋拆,禁止只做需求侧转述。\n'
130
130
  p += '2. 每张子卡必须 10~30 分钟可独立完成并验收:touches 精确到文件(文件级排他锁,同批子卡 touches 不得互相重叠),acceptance 必须是可执行命令(如 "node --test test/xxx.test.mjs")。\n'
131
131
  p += '3. size 判档:light=单点小改/文档/配置;standard=常规模块改动(缺省);heavy=跨模块/高风险/需强推理。modelHint 仅在有特殊理由时填(如某子卡需超长上下文模型),否则留空。\n'
132
- p += '4. children 上限 ' + DECOMPOSE_MAX_CHILDREN + ' 张;有先后顺序的用 dependsOnIdx 标注(只许引用本批**前排**子卡的下标,0 起)。\n'
132
+ p += '4. children 上限 ' + DECOMPOSE_MAX_CHILDREN + ' 张;有先后顺序时用 dependsOn 引用同批序号(只许引用本批**前排**子卡,序号 1 起计:如第 2 张依赖第 1 张填 ["1"])。\n'
133
133
  p += '5. 源卡本身不大、不必拆时:verdict=keep,并在 rationale 写清理由。\n'
134
134
  p += '\n权限边界:你没有任何建卡工具——不要尝试创建/修改看板任务。提案由宿主读结构化结果后自动物化为 draft 子卡(源卡状态不变,由主窗口确认后逐张 publish)。\n'
135
135
  return p
@@ -146,7 +146,7 @@ function decomposeChildId(d, parentId, n) {
146
146
  // · 校验先于清理——提案畸形/run 失败时**绝不清**旧批提案(失败不毁已有资产);
147
147
  // · 幂等:提案有效才清旧批(proposalOf===源卡 id 且仍 draft 的提案子卡整批移除;已 publish/落定的绝不碰);
148
148
  // · 子卡落位:status=draft、parentId=源卡、size(合法才落)、model=modelHint(空不落)、touches/acceptance/
149
- // description 落位、dependsOnIdx → 本批真实 id(只收前排下标,天然无环)、上限 DECOMPOSE_MAX_CHILDREN 截断;
149
+ // description 落位、dependsOn 序号 → 本批真实 id(只收前排序号,天然无环;坏序号丢弃+该子卡 messages 留痕)、上限 DECOMPOSE_MAX_CHILDREN 截断;
150
150
  // · 源卡自身状态不变(仍 draft/pending,由用户处置),只追加 messages 提案摘要行 + history 审计行。
151
151
  // 返回 { ok, verdict, children?:[{id,title}], error? }——非空返回值驱动 mutateLocked 写盘。
152
152
  export function applyDecomposeProposal(d, t, proposal, now) {
@@ -180,17 +180,26 @@ export function applyDecomposeProposal(d, t, proposal, now) {
180
180
  ah(t, t.status, t.status, 'system', '拆解官判定无需拆分(rationale 落源卡消息)')
181
181
  return { ok: true, verdict: 'keep', children: [] }
182
182
  }
183
- // 物化:先分配整批 id(dependsOnIdx → 真实 id 翻译要求全员 id 已知),再逐张落 draft 子卡。
183
+ // 物化:先分配整批 id(dependsOn 序号 → 真实 id 翻译要求全员 id 已知),再逐张落 draft 子卡。
184
184
  var ids = []
185
185
  for (var k = 0; k < kids.length; k++) ids.push(decomposeChildId(d, t.id, k + 1))
186
186
  var created = []
187
187
  for (var k2 = 0; k2 < kids.length; k2++) {
188
188
  var c = kids[k2]
189
+ // dependsOn 序号引用 → 真实任务 id(task-mvr44rm3):序号 '1' 起计,指本批第 n 张提案;先全批建卡
190
+ // 拿到真实 id 再映射。只认**前排**(序号 1..k2,当前位 k2+1)——按构造天然无环。无法解析的序号
191
+ //(非数字/越界/自引用/后排)一律丢弃,并在该子卡 messages 留一条「依赖序号无法解析」:
192
+ // 发布前审阅子卡详情即可见,主窗口可 publish 后用 task_update dependsOn 手工补挂。
189
193
  var deps = []
190
- var di = Array.isArray(c.dependsOnIdx) ? c.dependsOnIdx : []
191
- for (var dj = 0; dj < di.length; dj++) {
192
- var idx = di[dj]
193
- if (typeof idx === 'number' && isFinite(idx) && idx >= 0 && idx < k2 && deps.indexOf(ids[idx]) < 0) deps.push(ids[idx])
194
+ var unres = []
195
+ var dr = Array.isArray(c.dependsOn) ? c.dependsOn : []
196
+ for (var dj = 0; dj < dr.length; dj++) {
197
+ var ref = String(dr[dj] == null ? '' : dr[dj]).trim()
198
+ if (!ref) continue
199
+ var n = /^\d+$/.test(ref) ? parseInt(ref, 10) : NaN
200
+ if (!(n >= 1 && n <= k2)) { if (unres.indexOf(ref) < 0) unres.push(ref); continue }
201
+ var rid = ids[n - 1]
202
+ if (deps.indexOf(rid) < 0) deps.push(rid)
194
203
  }
195
204
  var child = {
196
205
  id: ids[k2],
@@ -225,6 +234,7 @@ export function applyDecomposeProposal(d, t, proposal, now) {
225
234
  if (sz === 'light' || sz === 'standard' || sz === 'heavy') child.size = sz
226
235
  var mh = typeof c.modelHint === 'string' ? c.modelHint.trim() : ''
227
236
  if (mh) child.model = mh
237
+ if (unres.length) child.messages.push({ kind: 'decompose', text: '🧩 依赖序号无法解析(' + unres.slice(0, 10).map(function (u) { return String(u).slice(0, 20) }).join('、') + ')——已丢弃;只认同批**前排**子卡序号(1 起计),如需依赖可 publish 后用 task_update dependsOn 手工补挂', at: stamp, by: 'decomposer' })
228
238
  d.tasks.push(child)
229
239
  created.push(child)
230
240
  }
package/lib/rpc.mjs CHANGED
@@ -833,8 +833,9 @@ export function createRpc(ctx, state, deps) {
833
833
  // (retryCount+1 / lastError=子会话被宿主拆除 / history 落「worker 失败…重新排队」,
834
834
  // retryCount=2 的卡直接被打成 blocked,与「hold=停派」语义正面冲突)。
835
835
  // 终止的账由本函数自己结(accumulateRunUsage + closeRunHistory + mutateLocked),立旗后
836
- // 兜底臂/超时臂/idle 事件通道对这条 rec 的迟到落定一律空转——与落闸弃权臂(dispatch 646)
837
- // 同一纪律,且补上「离开活跃表的 rec 必然已立 settled 旗」不变式的最后一个缺口。
836
+ // 兜底臂/超时臂/idle 事件通道对这条 rec 的迟到落定一律空转——与落闸弃权臂
837
+ // (lib/dispatch.mjs 的 spawn 落闸处置)同一纪律,且补上「离开活跃表的 rec 必然已立
838
+ // settled 旗」不变式的最后一个缺口。
838
839
  rec.settled = true
839
840
  // 清理:一次性 run 走 dispose(既有语义逐字不变);continuable 的 rec 没有 run(持久子会话),
840
841
  // 卡2 起改走 **interrupt 留存**(task-muw5h2ps):只打断当前 turn、不销毁子会话——否则「手动终止」
@@ -1019,7 +1020,10 @@ export function createRpc(ctx, state, deps) {
1019
1020
  // 双查第二查「撞他卡有效锁」要用它现算他卡持锁(core.planLockConflicts)。
1020
1021
  function planGateApply(t, planFiles, actor, pat, d) {
1021
1022
  // 有效边界 = touches ∪ extraTouches(task-mvonytz3:仲裁扩权登记的文件即本卡合法边界,
1022
- // 不再误报越界;touches 审计原值不动——边界展示带「含仲裁扩权」标注以资区别)
1023
+ // 不再误报越界;touches 审计原值不动——边界展示带「含仲裁扩权」标注以资区别)。
1024
+ // 含 extraTouches(task-mvtz2jjq 勘误):此处自 task-mvonytz3 起就按有效锁集比对,此前
1025
+ // 「闸口不认 extraTouches」的诊断系解析伪影误判——真因在 core.parsePlanFiles 把「调用点/
1026
+ // 接线断言仍锁」这类中文散文片与 `/if \(...` 正则残片误判成路径,残片永远不在边界内。
1023
1027
  var boundary = core.effectiveTouches(t)
1024
1028
  var hasExtra = Array.isArray(t.extraTouches) && t.extraTouches.length > 0
1025
1029
  // 卡无有效边界 → 无越界可比;但计划仍可能撞他卡锁(无边界卡的静默漂移同样现形)