dsh-agent-board 1.9.2 → 1.9.4
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 +6 -4
- package/lib/client/task-detail.js +30 -1
- package/lib/client.js +30 -1
- package/lib/core.mjs +18 -1
- package/lib/dispatch.mjs +13 -2
- package/lib/hung.mjs +11 -0
- package/lib/policy.mjs +34 -3
- package/lib/rpc.mjs +52 -6
- package/lib/settle.mjs +136 -1
- package/package.json +2 -2
- package/test/core.test.mjs +240 -2
- package/test/hung-frame-signal.test.mjs +197 -0
- package/test/settle-stop-interrupt.test.mjs +172 -0
package/lib/rpc.mjs
CHANGED
|
@@ -9,11 +9,12 @@ import * as core from './core.mjs'
|
|
|
9
9
|
import path from 'node:path'
|
|
10
10
|
import fsNode from 'node:fs'
|
|
11
11
|
import { findRunLog, readLogBytes, readLogFrames, aggregateUsageSummary, readMainWindowUsage, buildScoreboard, modelPerfHint, qualityChangeHints } from './usage.mjs'
|
|
12
|
-
import { TASK_SIZE_CONTRACT, withSplitHint, pushRejectLesson, pushArbitrationLesson } from './policy.mjs'
|
|
12
|
+
import { TASK_SIZE_CONTRACT, PIPELINE_SCENE_CONTRACT, SIZE_TIER_SAMPLES, withSplitHint, pushRejectLesson, pushArbitrationLesson } from './policy.mjs'
|
|
13
13
|
import { makeMsg } from './notify.mjs'
|
|
14
14
|
import { computeHealthHints, computeRuntimeHealthHints } from './health.mjs'
|
|
15
15
|
import { isFullSessionId } from './session.mjs' // 幻影板防线口径(纯函数,与 policy.mjs 直引同例)
|
|
16
16
|
import { previewCleanup, runCleanup, SKIP_REASON_TEXT } from './cleanup.mjs' // 已结算会话清理(预览+执行)
|
|
17
|
+
import { HUNG_SUSPECT_REARM_MS } from './hung.mjs' // 「继续观察」豁免防抖窗(与疑似阈值同值,task-mvxmxkq1)
|
|
17
18
|
const { ah, isb, gsb, gpt, vt, validateDeps, classifyPipeline, cfg, claimCheck, claimApply, resolveApply, verifyApply, lateVerdictApply, verdictErr, lateNoticeFp, archiveApply, cancelApply, maybeAutoCloseParent, PRIO_RANK, touchesConflict, holdsFiles, boardHome, aggregateChildStats, createTaskWarnings, epicPrecheck, epicPrecheckNote, attachContextSuggestions, REJECT_REDISPATCH_HINT, tasksHash, normalizeHooks, mergeHooks, pushRejection, normalizeUserTest, draftLint, appendDispatchWarning, TASK_SIZES, opsLogLine } = core
|
|
18
19
|
|
|
19
20
|
function defineTool(options) {
|
|
@@ -234,6 +235,28 @@ export function createRpc(ctx, state, deps) {
|
|
|
234
235
|
return { ok: true }
|
|
235
236
|
}
|
|
236
237
|
|
|
238
|
+
// ===== touches 更新通道(task-mvxw7hn5):锁声明替换式更新,主窗口/GUI 专享·生命周期保护 =====
|
|
239
|
+
// 归一化口径与 create-task 同款(normTouches:非字符串过滤 + trim + 上限 20;空数组=清除声明
|
|
240
|
+
// 回「无锁」态)。生命周期门禁(主窗口 2026-11-01 拍板):
|
|
241
|
+
// · draft/pending/blocked/cancelled/resolved(未持锁态:规划期纠正权 + 落定后修正权)→ 可直改;
|
|
242
|
+
// · in-progress/verifying(持锁运行态)→ **主窗口也不可**直改——此阶段边界由 Worker 以实际
|
|
243
|
+
// 覆盖面申报为准(board_report escalate → task_arbitrate expandTouches 既有通道);
|
|
244
|
+
// verifying 态过窄/过宽由 Verifier 在验收意见里点名,主窗口落定后再修正或按验收放行。
|
|
245
|
+
// history 留审计轨(原值进留痕,超长截断)。Worker/子代理禁(hooksDenied 同款闸,双通道接线在
|
|
246
|
+
// update-task RPC 与 task_update 工具);extraTouches 语义不动(它仍是「扩权留原值」的独立轨)。
|
|
247
|
+
var TOUCHES_MAIN_ONLY = 'touches 更新仅主窗口可用(子代理无改锁权限)'
|
|
248
|
+
var TOUCHES_RUNNING_ERR = '运行中的任务 touches 由 Worker 以实际覆盖面申报——请 escalate 或由主窗口仲裁扩权;待任务落定后再修正'
|
|
249
|
+
function applyTouchesUpdate(t, actor, raw) {
|
|
250
|
+
if (t.status === 'in-progress' || t.status === 'verifying') return { error: TOUCHES_RUNNING_ERR }
|
|
251
|
+
var old = Array.isArray(t.touches) ? t.touches : []
|
|
252
|
+
var next = normTouches(raw)
|
|
253
|
+
var oldText = old.length ? old.join('、') : '无'
|
|
254
|
+
if (oldText.length > 120) oldText = oldText.slice(0, 120) + '…'
|
|
255
|
+
t.touches = next
|
|
256
|
+
ah(t, t.status, t.status, actor, 'touches 更新:' + old.length + ' 项 → ' + next.length + ' 项(原:' + oldText + ')')
|
|
257
|
+
return {}
|
|
258
|
+
}
|
|
259
|
+
|
|
237
260
|
// ===== Tools =====
|
|
238
261
|
ctx.tools.register(defineTool({ name: 'task_list', description: '列出当前会话任务。返回体 recentOps=最近人工/外部操作尾部 5 条单行摘要(HTTP 通道写集留痕,Team 模式下对齐「人动了什么」)。', parameters: { type: 'object', properties: { status: { type: 'string', enum: ['pending', 'in-progress', 'verifying', 'resolved', 'blocked', 'cancelled'] }, priority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] }, tag: { type: 'string' }, parentId: { type: 'string' }, includeArchived: { type: 'boolean' }, limit: { type: 'number' } }, required: [] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var d = await rt(sid); var a = d.tasks; var ts = a; if (!args.includeArchived) ts = ts.filter(function (x) { return x.status !== 'archived' }); if (args.status) ts = ts.filter(function (x) { return x.status === args.status }); if (args.priority) ts = ts.filter(function (x) { return x.priority === args.priority }); if (args.tag) ts = ts.filter(function (x) { return (x.tags || []).indexOf(args.tag) >= 0 }); if (args.parentId === 'null') ts = ts.filter(function (x) { return !isb(x) }); else if (args.parentId) ts = ts.filter(function (x) { return x.parentId === args.parentId }); var po = PRIO_RANK; ts.sort(function (a, b) { var dd = (po[b.priority] || 0) - (po[a.priority] || 0); return dd !== 0 ? dd : (a.createdAt || '').localeCompare(b.createdAt || '') }); var lim = Math.min(args.limit || 20, 100); var res = ts.slice(0, lim).map(function (x) { var e = Object.assign({}, x); if (isb(x)) { var p = gpt(x, a); if (p) e.parentSummary = { id: p.id, title: p.title, status: p.status } }; var ch = gsb(x.id, a); if (ch.length) { e.subtaskCount = ch.length; e.subtaskResolved = ch.filter(function (y) { return y.status === 'resolved' }).length }; return e }); var out = { tasks: res, total: ts.length, session: sid, actor: getActorId(), boardMode: d.boardMode || 'auto', teamMode: !!d.teamMode, poolStatus: d.poolStatus, recentOps: (Array.isArray(d.opsLog) ? d.opsLog : []).slice(-5).map(opsLogLine).filter(function (s) { return s.length > 0 }) }; if (d.teamMode) out.teamHint = 'Team 模式已开启:实质性改动请优先 task_create 提交看板由池执行;调研/读取/讨论可直接进行;Worker 歧义会上报等你裁决。'; return out } }))
|
|
239
262
|
ctx.tools.register(defineTool({ name: 'task_context', description: '获取任务完整上下文。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, expandFiles: { type: 'boolean' }, includeParent: { type: 'boolean' }, includeSubtasks: { type: 'boolean' } }, required: ['taskId'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var d = await rt(sid); var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found: ' + args.taskId }; return { ok: true, context: { task: t, inheritedContext: t.context || {} } } } }))
|
|
@@ -247,8 +270,8 @@ export function createRpc(ctx, state, deps) {
|
|
|
247
270
|
ctx.tools.register(defineTool({ name: 'task_verify', description: '验收:approved→resolved,rejected→in-progress。子任务全完成父任务自动verifying。Verifier 仍在跑(无 endedAt/run outcome 仍 running)时默认拦截并返回提示;确认提前裁决请加 force:true。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, verdict: { type: 'string', enum: ['approved', 'rejected'] }, comment: { type: 'string' }, force: { type: 'boolean' } }, 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' }; var __act = activeRunOf(sid, t); if (__act && __act.role === 'verifier' && args.force !== true) return { ok: false, error: 'Verifier 还在跑(run ' + __act.id + '),提前裁决将丢弃它的独立结论。确认提前裁决请加 force:true', verifierActive: __act.id }; var __vr = verifyApply(d, t, actor, args.verdict, args.comment); if (args.verdict === 'rejected') { t.verification = { verdict: 'rejected', summary: args.comment || '', checks: '', at: new Date().toISOString(), by: actor }; pushRejection(t, args.comment, '', t.verification.at, actor); __vr.hint = REJECT_REDISPATCH_HINT } return __vr }) } }))
|
|
248
271
|
ctx.tools.register(defineTool({ name: 'task_archive', description: '归档已解决/已取消任务(无活跃 run 的 in-progress 僵尸卡也可归档,用于 parentKick 僵尸态出清)。', 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 t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; var __ae = archiveErr(sid, t); if (__ae) return { ok: false, error: __ae }; return archiveApply(d, t, actor, 'archived') }) } }))
|
|
249
272
|
ctx.tools.register(defineTool({ name: 'task_cancel', description: '取消任务:draft/pending/blocked 态可取消(取消后不再派发,可在归档 tab 恢复待办)。in-progress/verifying 需先 terminate-agent,resolved 请用归档。reason 可选,落流转轨迹。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, reason: { type: 'string', description: '取消理由(可选,随流转轨迹留档)' } }, required: ['taskId'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; return doCancel(toolSessionId(), getActorId(), args.taskId, args.reason) } }))
|
|
250
|
-
ctx.tools.register(defineTool({ name: 'task_update', description: '更新任务字段,可选重置为 pending。dependsOn/pipeline 也可更新(环检测会拒绝成环依赖)。contextFiles 可更新预研文件清单。unfreeze:true 解除裁决挂起冻结(frozen)并触发派发。', 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: '预研文件路径(替换式更新),派发时以文件清单(路径:L行号 — 一句用途)随首条 prompt 注入,内容由子代理按需 read 自取' }, contextNotes: { type: 'string', description: '预研笔记(替换式更新):调研结论/原始需求/思路' }, pipeline: { type: 'string', enum: ['full', 'work', 'direct'] }, resetToPending: { type: 'boolean' }, unfreeze: { type: 'boolean', description: '解除冻结(frozen)并立即触发派发,用于 hold 裁决补完上下文后重新入池' }, publish: { type: 'boolean', description: '发布草稿为 pending(仅 draft 状态有效)' }, size: { type: 'string', enum: ['light', 'standard', 'heavy', ''], description: '力度档位(查板级 laneModels 路由表):light/standard/heavy;空串=清除(回标档语义),非法值拒写' }, model: { type: 'string', description: '卡级模型特例(取模链最高优先,provider/model 复合格式):空串=清除;非空走合法清单校验,非法拒写' }, hooks: { type: 'object', additionalProperties: true, description: '史诗 hook 点位(仅主窗口可设):{ pre?: { enabled?, prompt }, post?: { enabled?, prompt } } —— hook 点=一次真实 agent 运行(不是声明式命令):pre 在子任务派发前跑一遍前置准备(未完成前该 epic 的子任务一张都不派),post 在全部子任务了结后跑一遍收口(完成后 epic 才转 verifying)。prompt 是薄框架契约(做什么由 hook agent 判断,吃不准会歧义上报)。传 null 清除,传 { pre: null } 只撤该点位。' } }, required: ['taskId'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (args.hooks !== undefined && resolveRoot(__ra) !== __ra) return { ok: false, error: 'hooks 仅主窗口可设(子代理无 hooks 权限)' }; if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); var existsFn = existsInSession(sid); /* 力度路由卡级字段(task-mvnemvf5):size 枚举校验 + model 合法清单校验(空串=清除);model 校验要异步拉网关清单,必须在锁外完成 */ if (args.size !== undefined) { var __szV = typeof args.size === 'string' ? args.size.trim() : ''; if (__szV !== '' && TASK_SIZES.indexOf(__szV) < 0) return { ok: false, error: '非法 size: ' + String(args.size) + '(合法值: light/standard/heavy,空串=清除)' } } if (args.model !== undefined) { var __cmv = typeof args.model === 'string' ? args.model.trim() : ''; if (__cmv) { var __cme = await cardModelErr(__cmv); if (__cme) return { ok: false, error: __cme } } } var __res = await mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; var __pre = null; var __depWarn = null; /* dependsOn 后补软提示(task-mv9yupm4②):在飞卡写依赖时挂返回体 warning 字段 */ 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; /* 力度路由卡级字段落卡(task-mvnemvf5):合法性已在锁外验过;空串=删字段清除(缺字段=标档/无特例语义,存量卡零迁移) */ if (args.size !== undefined) { var __sv = typeof args.size === 'string' ? args.size.trim() : ''; if (__sv) t.size = __sv; else delete t.size } if (args.model !== undefined) { var __mv2 = typeof args.model === 'string' ? args.model.trim() : ''; if (__mv2) t.model = __mv2; else delete t.model } if (args.dependsOn !== undefined) { var derr = validateDeps(d, t.id, args.dependsOn); if (derr) return { ok: false, error: derr }; t.dependsOn = args.dependsOn; /* task-mv9yupm4②:在飞卡(in-progress 含 spawn-pending 占位 / verifying)写依赖只软提示——本次 run 的 prompt 已注入、不受新依赖影响(在途 spawn 由落闸锁内重验 depsSatisfied 兜底否决);不拒写不写历史 */ if (t.status === 'in-progress' || t.status === 'verifying') __depWarn = '卡已在执行中,依赖变更不影响本次 run' } 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' }; /* Team 硬闸(task-mvomad2c):缺档草稿拒发布(同调用先赋 size 者已在上方生效,可一把过) */ if (d.teamMode && TASK_SIZES.indexOf(t.size) < 0) return { ok: false, error: core.TEAM_SIZE_REQUIRED_ERR }; t.status = 'pending'; ah(t, 'draft', 'pending', actor, 'published'); /* 调研门禁③:发布的是 epic(有子任务)时轻量预检子任务调研注入,缺失挂返回值由锁外 pushSysNote 汇总 */ __pre = epicPrecheck(d.tasks, t.id, existsFn) } if (args.unfreeze && t.frozen) { delete t.frozen; delete t.frozenAt; delete t.frozenBy; delete t.waitingLock; ah(t, t.status, t.status, actor, '解除冻结,重新进入派发池') } if (args.resetToPending) { var ps = t.status; t.status = 'pending'; t.claimedBy = null; t.claimedAt = null; t.resolvedAt = null; t.resolution = null; delete t.retryCount; delete t.stuckSince; delete t.cancelledAt; ah(t, ps, 'pending', actor, 'reset to pending after edit') }; if (args.hooks !== undefined) { var __he = applyHooks(t, args.hooks); if (__he.error) return { ok: false, error: __he.error } }; t.lintWarnings = draftLint(t, draftLintIo(sid, d.ownerCwd)); var __out = { ok: true, task: t }; if (__depWarn) __out.warning = __depWarn; if (__pre && __pre.missing.length) __out.epicPrecheck = __pre; return __out }); afterPublishPrecheck(sid, args.taskId, __res); return __res } }))
|
|
251
|
-
ctx.tools.register(defineTool({ name: 'task_create', description: '创建新任务到当前会话看板。acceptance 可选:硬性验收脚本命令(如 "node --test src/x.test.js"),Worker 必须实际运行、Verifier 必须独立复跑。dependsOn 可选:依赖任务 id 数组,依赖全部完成后才会被派发。pipeline
|
|
273
|
+
ctx.tools.register(defineTool({ name: 'task_update', description: '更新任务字段,可选重置为 pending。dependsOn/pipeline 也可更新(环检测会拒绝成环依赖)。contextFiles 可更新预研文件清单。touches 可替换式更新(仅主窗口:空数组=清除声明回「无锁」态;in-progress/verifying 运行态拒写——边界由 Worker 以实际覆盖面申报,落定后再修正)。unfreeze:true 解除裁决挂起冻结(frozen)并触发派发。', 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: '预研文件路径(替换式更新),派发时以文件清单(路径:L行号 — 一句用途)随首条 prompt 注入,内容由子代理按需 read 自取' }, contextNotes: { type: 'string', description: '预研笔记(替换式更新):调研结论/原始需求/思路' }, touches: { type: 'array', items: { type: 'string' }, description: '本任务将要改动的文件路径/glob(替换式更新,仅主窗口可用;归一化与 task_create 同款:非字符串过滤+上限 20,空数组=清除声明回「无锁」态)。生命周期门禁:draft/pending/blocked/cancelled/resolved(锁未生效/已释放)可直改;in-progress/verifying(运行中持锁)拒写——此阶段边界由 Worker 以实际覆盖面申报(board_report escalate → task_arbitrate expandTouches),落定后再修正。Worker/子代理禁' }, pipeline: { type: 'string', enum: ['full', 'work', 'direct'] }, resetToPending: { type: 'boolean' }, unfreeze: { type: 'boolean', description: '解除冻结(frozen)并立即触发派发,用于 hold 裁决补完上下文后重新入池' }, publish: { type: 'boolean', description: '发布草稿为 pending(仅 draft 状态有效)' }, size: { type: 'string', enum: ['light', 'standard', 'heavy', ''], description: '力度档位(查板级 laneModels 路由表):light/standard/heavy;空串=清除(回标档语义),非法值拒写' }, model: { type: 'string', description: '卡级模型特例(取模链最高优先,provider/model 复合格式):空串=清除;非空走合法清单校验,非法拒写' }, hooks: { type: 'object', additionalProperties: true, description: '史诗 hook 点位(仅主窗口可设):{ pre?: { enabled?, prompt }, post?: { enabled?, prompt } } —— hook 点=一次真实 agent 运行(不是声明式命令):pre 在子任务派发前跑一遍前置准备(未完成前该 epic 的子任务一张都不派),post 在全部子任务了结后跑一遍收口(完成后 epic 才转 verifying)。prompt 是薄框架契约(做什么由 hook agent 判断,吃不准会歧义上报)。传 null 清除,传 { pre: null } 只撤该点位。' } }, required: ['taskId'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (args.hooks !== undefined && resolveRoot(__ra) !== __ra) return { ok: false, error: 'hooks 仅主窗口可设(子代理无 hooks 权限)' }; if (args.touches !== undefined && resolveRoot(__ra) !== __ra) return { ok: false, error: TOUCHES_MAIN_ONLY }; if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); var existsFn = existsInSession(sid); /* 力度路由卡级字段(task-mvnemvf5):size 枚举校验 + model 合法清单校验(空串=清除);model 校验要异步拉网关清单,必须在锁外完成 */ if (args.size !== undefined) { var __szV = typeof args.size === 'string' ? args.size.trim() : ''; if (__szV !== '' && TASK_SIZES.indexOf(__szV) < 0) return { ok: false, error: '非法 size: ' + String(args.size) + '(合法值: light/standard/heavy,空串=清除)' } } if (args.model !== undefined) { var __cmv = typeof args.model === 'string' ? args.model.trim() : ''; if (__cmv) { var __cme = await cardModelErr(__cmv); if (__cme) return { ok: false, error: __cme } } } var __res = await mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; var __pre = null; var __depWarn = null; /* dependsOn 后补软提示(task-mv9yupm4②):在飞卡写依赖时挂返回体 warning 字段 */ if (args.title !== undefined) t.title = args.title; if (args.description !== undefined) t.description = args.description; /* touches 更新通道(task-mvxw7hn5):主窗口替换式更新锁声明(生命周期门禁+归一化+留痕单一口径在 applyTouchesUpdate;运行态拒写原样返回错误) */ if (args.touches !== undefined) { var __txe = applyTouchesUpdate(t, actor, args.touches); if (__txe.error) return { ok: false, error: __txe.error } } 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; /* 力度路由卡级字段落卡(task-mvnemvf5):合法性已在锁外验过;空串=删字段清除(缺字段=标档/无特例语义,存量卡零迁移) */ if (args.size !== undefined) { var __sv = typeof args.size === 'string' ? args.size.trim() : ''; if (__sv) t.size = __sv; else delete t.size } if (args.model !== undefined) { var __mv2 = typeof args.model === 'string' ? args.model.trim() : ''; if (__mv2) t.model = __mv2; else delete t.model } if (args.dependsOn !== undefined) { var derr = validateDeps(d, t.id, args.dependsOn); if (derr) return { ok: false, error: derr }; t.dependsOn = args.dependsOn; /* task-mv9yupm4②:在飞卡(in-progress 含 spawn-pending 占位 / verifying)写依赖只软提示——本次 run 的 prompt 已注入、不受新依赖影响(在途 spawn 由落闸锁内重验 depsSatisfied 兜底否决);不拒写不写历史 */ if (t.status === 'in-progress' || t.status === 'verifying') __depWarn = '卡已在执行中,依赖变更不影响本次 run' } 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' }; /* Team 硬闸(task-mvomad2c):缺档草稿拒发布(同调用先赋 size 者已在上方生效,可一把过) */ if (d.teamMode && TASK_SIZES.indexOf(t.size) < 0) return { ok: false, error: core.TEAM_SIZE_REQUIRED_ERR }; t.status = 'pending'; ah(t, 'draft', 'pending', actor, 'published'); /* 调研门禁③:发布的是 epic(有子任务)时轻量预检子任务调研注入,缺失挂返回值由锁外 pushSysNote 汇总 */ __pre = epicPrecheck(d.tasks, t.id, existsFn) } if (args.unfreeze && t.frozen) { delete t.frozen; delete t.frozenAt; delete t.frozenBy; delete t.waitingLock; ah(t, t.status, t.status, actor, '解除冻结,重新进入派发池') } if (args.resetToPending) { var ps = t.status; t.status = 'pending'; t.claimedBy = null; t.claimedAt = null; t.resolvedAt = null; t.resolution = null; delete t.retryCount; delete t.stuckSince; delete t.cancelledAt; ah(t, ps, 'pending', actor, 'reset to pending after edit') }; if (args.hooks !== undefined) { var __he = applyHooks(t, args.hooks); if (__he.error) return { ok: false, error: __he.error } }; t.lintWarnings = draftLint(t, draftLintIo(sid, d.ownerCwd)); var __out = { ok: true, task: t }; if (__depWarn) __out.warning = __depWarn; if (__pre && __pre.missing.length) __out.epicPrecheck = __pre; return __out }); afterPublishPrecheck(sid, args.taskId, __res); return __res } }))
|
|
274
|
+
ctx.tools.register(defineTool({ name: 'task_create', description: '创建新任务到当前会话看板。acceptance 可选:硬性验收脚本命令(如 "node --test src/x.test.js"),Worker 必须实际运行、Verifier 必须独立复跑。dependsOn 可选:依赖任务 id 数组,依赖全部完成后才会被派发。pipeline 可选(按场景选,别一律 full):work=回归测试/巡检/文档/纯查询/纯报表(只做不验);full=代码改动/修 bug/新特性(默认,要 Verifier 独立复跑验收);direct=不进池、主窗口直接处理(epic 父卡用这个)。用户说做回归/巡检/盘点 → 归 work。contextFiles 可选:你在调研中已经读过的关键文件路径数组——派发时以「文件清单」(每行「路径:L起-L止 — 一句用途」)随首条 prompt 一次性注入,只给路径与行号、不含内容本体(子代理用 read 工具按行号范围按需自取:执行时盘面更新鲜,也免了大文件正文撑爆 prompt 与随快照每轮重发),避免子代理从零重复调研。contextNotes 可选:调研结论/原始需求/思路等非文件类上下文,全文随首条 prompt 一次性注入(≤8000 字符)。【开发类任务(涉及代码改动/修复/特性)务必带文件调研】:把调研中读过的关键文件路径放进 contextFiles、结论思路放进 contextNotes——实测可让 Worker 省去 10~15 分钟从零 grep 定位的时间,且方向感天壤之别(无调研的 Worker 只能靠标题猜需求、极易跑偏)。调研门禁:pipeline=full/work 且 touches 非空而未带调研上下文时,返回将附 warning 提示。touches 可选:本任务将要改动的文件路径/glob 数组(如 "src/x.mjs"、"src/**")——派发器在同一时刻只派发 touches 不冲突的任务,避免并行 Worker 改同一批文件互踩;任务持有文件锁(in-progress/verifying 持有,状态流转到 resolved/cancelled/archived 即释放——锁只护正在写的阶段,验收后-提交前的窗口期由主窗口提交纪律 + 史诗 post-hook 承接)。' + TASK_SIZE_CONTRACT + PIPELINE_SCENE_CONTRACT, 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 或绝对路径;可写「路径:L1-L2 — 一句用途」),派发时以文件清单随首条 prompt 注入(只给路径行号,内容由子代理按需 read 自取)' }, contextNotes: { type: 'string', description: '预研笔记:调研结论/原始需求/思路等(≤8000 字符),全文随首条 prompt 一次性注入' }, pipeline: { type: 'string', enum: ['full', 'work', 'direct'], description: '管线(按场景选,别一律 full):work=回归测试/巡检/文档/纯查询/纯报表(只做不验,不需 Verifier 复跑);full=代码改动/修 bug/新特性(默认,要 Verifier 独立复跑验收);direct=不进池、主窗口亲办。用户说做回归/巡检/盘点 → 归 work' }, size: { type: 'string', enum: ['light', 'standard', 'heavy'], description: '力度档位(查板级 laneModels 路由表):light=单文件小改/文案/配置/断言增补/≤15 分钟,standard=单模块多文件/常规特性/需新断言,heavy=跨模块/改判定链核心/拆分类/高风险(三档具象样例:' + SIZE_TIER_SAMPLES + ');Team 模式板为必填硬闸(缺 size 拒建拒发布),非 Team 板可省(缺省=标档语义,不落字段)' }, touches: { type: 'array', items: { type: 'string' }, description: '本任务将改动的文件路径/glob(文件级排他锁):与活动任务 touches 冲突时不派发,等锁释放;支持 "src/**" 目录、"./a/b.mjs"、裸文件名等写法' }, draft: { type: 'boolean', description: '创建为草稿(不派发)。Team 模式下缺省即为 true——补全 dependsOn/上下文后用 task_update publish=true 统一发布;非 Team 模式缺省 false(建卡即立即派发:约 1s 内被 Worker 领取烧 token),显式 draft:false 可跳过草稿——不派发的占位/测试卡请显式 draft:true' }, hooks: { type: 'object', additionalProperties: true, description: '史诗 hook 点位(仅主窗口可设):{ pre?: { enabled?, prompt }, post?: { enabled?, prompt } } —— hook 点=一次真实 agent 运行(不是声明式命令):pre 在子任务派发前跑一遍前置准备(未完成前该 epic 的子任务一张都不派),post 在全部子任务了结后跑一遍收口(完成后 epic 才转 verifying)。prompt 是薄框架契约(做什么由 hook agent 判断,吃不准会歧义上报)。传 null 清除,传 { pre: null } 只撤该点位。' } }, required: ['title'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (args.hooks !== undefined && resolveRoot(__ra) !== __ra) return { ok: false, error: 'hooks 仅主窗口可设(子代理无 hooks 权限)' }; 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(); /* Team 模式护栏:draft 缺省跟随 teamMode(先补齐依赖/上下文再统一 publish);显式 draft:false 保留为立即派发的逃生门 */ var asDraft = args.draft === undefined ? !!d.teamMode : !!args.draft; /* size 普适化(task-mvomad2c):白名单校验(双模式)+ Team 硬闸(缺档拒建,非 Team 零约束零提示) */ var __sz = typeof args.size === 'string' ? args.size.trim() : ''; if (__sz && TASK_SIZES.indexOf(__sz) < 0) return { ok: false, error: '非法 size: ' + String(args.size) + '(合法值: light/standard/heavy)' }; if (d.teamMode && !__sz) return { ok: false, error: core.TEAM_SIZE_REQUIRED_ERR }; var t = { id: args.id || ('task-' + Date.now().toString(36)), title: args.title, description: args.description || '', status: asDraft ? '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 || [], touches: normTouches(args.touches), pipeline: args.pipeline || '', claimedBy: null, claimedAt: null, createdAt: now, resolvedAt: null, verifiedAt: null, verifiedBy: null, archivedAt: null, resolution: null, waitingForTouches: null, messages: [], history: [{ from: 'created', to: asDraft ? 'draft' : 'pending', timestamp: now, actor: actor, note: asDraft ? 'created as draft' : 'created' }] }; if (__sz) t.size = __sz; if (args.hooks !== undefined) { var __hn = normalizeHooks(args.hooks); if (__hn.error) return { ok: false, error: __hn.error }; if (__hn.hooks && Object.keys(__hn.hooks).length) t.hooks = __hn.hooks } if (!t.pipeline) t.pipeline = classifyPipeline(t); t.pipelineAuto = !args.pipeline; d.tasks.push(t); t.lintWarnings = draftLint(t, draftLintIo(sid, d.ownerCwd)); /* 调研门禁 warning 族(与 create-task RPC 同口径,core 纯函数):空描述/无调研上下文/整树 glob 三类软提示合并为一条(;分隔),不拦截创建 */ var __out = withSplitHint({ ok: true, task: t }, t, cfg(d).epicSplit); appendDispatchWarning(__out, d, t); /* 派发预警(task-mvbealtf):将自动派发形态的建卡返回体 lintWarnings 追加一行瞬态提示(不落卡) */ var __warns = createTaskWarnings(t); attachContextSuggestions(__out, __warns, t.touches, existsInSession(sid)); if (__warns.length) __out.warning = __warns.join(';'); return __out }) } }))
|
|
252
275
|
// ===== 拆解官(task-mvnenrwk):task_decompose 工具(仅主窗口门禁同 task_create)=====
|
|
253
276
|
// 设计(需求定稿):拆解官=一次性 run,outputSchema 强箍结构化提案;拆解官**零建卡工具**——
|
|
254
277
|
// 提案由 host 读 result.structured 后物化成 draft 子卡(权限口子=根本不需要开)。
|
|
@@ -574,7 +597,7 @@ export function createRpc(ctx, state, deps) {
|
|
|
574
597
|
})
|
|
575
598
|
}
|
|
576
599
|
handle('cancel-task', async function (args) { return doCancel(rpcSessionId(args), rpcActor(args), args.taskId, args.reason) })
|
|
577
|
-
handle('update-task', async function (args) { var sid = rpcSessionId(args); var actor = rpcActor(args); if (args.hooks !== undefined && hooksDenied(actor)) return { ok: false, error: HOOKS_MAIN_ONLY }; var existsFn = existsInSession(sid); /* 力度路由卡级字段(task-mvnemvf5):size 枚举校验 + model 合法清单校验(空串=清除);model 校验要异步拉网关清单,必须在锁外完成 */ if (args.size !== undefined) { var __szV = typeof args.size === 'string' ? args.size.trim() : ''; if (__szV !== '' && TASK_SIZES.indexOf(__szV) < 0) return { ok: false, error: '非法 size: ' + String(args.size) + '(合法值: light/standard/heavy,空串=清除)' } } if (args.model !== undefined) { var __cmv = typeof args.model === 'string' ? args.model.trim() : ''; if (__cmv) { var __cme = await cardModelErr(__cmv); if (__cme) return { ok: false, error: __cme } } } var __res = await mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; var __pre = null; var __depWarn = null; /* dependsOn 后补软提示(task-mv9yupm4②):在飞卡写依赖时挂返回体 warning 字段 */ 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.tags !== undefined) t.tags = Array.isArray(args.tags) ? args.tags.map(function (s) { return String(s).trim() }).filter(function (s) { return s.length > 0 }) : []; if (args.assignMode !== undefined) t.assignMode = args.assignMode; if (args.assignee !== undefined) t.assignee = args.assignee || null; /* 力度路由卡级字段落卡(task-mvnemvf5):合法性已在锁外验过;空串=删字段清除(缺字段=标档/无特例语义,存量卡零迁移) */ if (args.size !== undefined) { var __sv = typeof args.size === 'string' ? args.size.trim() : ''; if (__sv) t.size = __sv; else delete t.size } if (args.model !== undefined) { var __mv2 = typeof args.model === 'string' ? args.model.trim() : ''; if (__mv2) t.model = __mv2; else delete t.model } if (args.dependsOn !== undefined) { var derr = validateDeps(d, t.id, args.dependsOn); if (derr) return { ok: false, error: derr }; t.dependsOn = args.dependsOn; /* task-mv9yupm4②:在飞卡(in-progress 含 spawn-pending 占位 / verifying)写依赖只软提示——本次 run 的 prompt 已注入、不受新依赖影响(在途 spawn 由落闸锁内重验 depsSatisfied 兜底否决);不拒写不写历史 */ if (t.status === 'in-progress' || t.status === 'verifying') __depWarn = '卡已在执行中,依赖变更不影响本次 run' } 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' }; /* Team 硬闸(task-mvomad2c):缺档草稿拒发布(同调用先赋 size 者已在上方生效,可一把过) */ if (d.teamMode && TASK_SIZES.indexOf(t.size) < 0) return { ok: false, error: core.TEAM_SIZE_REQUIRED_ERR }; t.status = 'pending'; ah(t, 'draft', 'pending', actor, 'published'); /* 调研门禁③:发布的是 epic(有子任务)时轻量预检子任务调研注入,缺失挂返回值由锁外 pushSysNote 汇总 */ __pre = epicPrecheck(d.tasks, t.id, existsFn) } if (args.unfreeze && t.frozen) { delete t.frozen; delete t.frozenAt; delete t.frozenBy; delete t.waitingLock; ah(t, t.status, t.status, actor, '解除冻结,重新进入派发池') } if (args.resetToPending) { var ps = t.status; t.status = 'pending'; t.claimedBy = null; t.claimedAt = null; t.resolvedAt = null; t.resolution = null; delete t.retryCount; delete t.stuckSince; delete t.cancelledAt; ah(t, ps, 'pending', actor, 'reset to pending after edit') }; if (args.hooks !== undefined) { var __he = applyHooks(t, args.hooks); if (__he.error) return { ok: false, error: __he.error } }; t.lintWarnings = draftLint(t, draftLintIo(sid, d.ownerCwd)); var __out = { ok: true, task: t }; if (__depWarn) __out.warning = __depWarn; if (__pre && __pre.missing.length) __out.epicPrecheck = __pre; return __out }); afterPublishPrecheck(sid, args.taskId, __res); return __res })
|
|
600
|
+
handle('update-task', async function (args) { var sid = rpcSessionId(args); var actor = rpcActor(args); if (args.hooks !== undefined && hooksDenied(actor)) return { ok: false, error: HOOKS_MAIN_ONLY }; if (args.touches !== undefined && hooksDenied(actor)) return { ok: false, error: TOUCHES_MAIN_ONLY }; var existsFn = existsInSession(sid); /* 力度路由卡级字段(task-mvnemvf5):size 枚举校验 + model 合法清单校验(空串=清除);model 校验要异步拉网关清单,必须在锁外完成 */ if (args.size !== undefined) { var __szV = typeof args.size === 'string' ? args.size.trim() : ''; if (__szV !== '' && TASK_SIZES.indexOf(__szV) < 0) return { ok: false, error: '非法 size: ' + String(args.size) + '(合法值: light/standard/heavy,空串=清除)' } } if (args.model !== undefined) { var __cmv = typeof args.model === 'string' ? args.model.trim() : ''; if (__cmv) { var __cme = await cardModelErr(__cmv); if (__cme) return { ok: false, error: __cme } } } var __res = await mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; var __pre = null; var __depWarn = null; /* dependsOn 后补软提示(task-mv9yupm4②):在飞卡写依赖时挂返回体 warning 字段 */ if (args.title !== undefined) t.title = args.title; if (args.description !== undefined) t.description = args.description; /* touches 更新通道(task-mvxw7hn5):主窗口/GUI 替换式更新锁声明(生命周期门禁+归一化+留痕单一口径在 applyTouchesUpdate;运行态拒写原样返回错误) */ if (args.touches !== undefined) { var __txe = applyTouchesUpdate(t, actor, args.touches); if (__txe.error) return { ok: false, error: __txe.error } } if (args.priority !== undefined) t.priority = args.priority; if (args.tags !== undefined) t.tags = Array.isArray(args.tags) ? args.tags.map(function (s) { return String(s).trim() }).filter(function (s) { return s.length > 0 }) : []; if (args.assignMode !== undefined) t.assignMode = args.assignMode; if (args.assignee !== undefined) t.assignee = args.assignee || null; /* 力度路由卡级字段落卡(task-mvnemvf5):合法性已在锁外验过;空串=删字段清除(缺字段=标档/无特例语义,存量卡零迁移) */ if (args.size !== undefined) { var __sv = typeof args.size === 'string' ? args.size.trim() : ''; if (__sv) t.size = __sv; else delete t.size } if (args.model !== undefined) { var __mv2 = typeof args.model === 'string' ? args.model.trim() : ''; if (__mv2) t.model = __mv2; else delete t.model } if (args.dependsOn !== undefined) { var derr = validateDeps(d, t.id, args.dependsOn); if (derr) return { ok: false, error: derr }; t.dependsOn = args.dependsOn; /* task-mv9yupm4②:在飞卡(in-progress 含 spawn-pending 占位 / verifying)写依赖只软提示——本次 run 的 prompt 已注入、不受新依赖影响(在途 spawn 由落闸锁内重验 depsSatisfied 兜底否决);不拒写不写历史 */ if (t.status === 'in-progress' || t.status === 'verifying') __depWarn = '卡已在执行中,依赖变更不影响本次 run' } 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' }; /* Team 硬闸(task-mvomad2c):缺档草稿拒发布(同调用先赋 size 者已在上方生效,可一把过) */ if (d.teamMode && TASK_SIZES.indexOf(t.size) < 0) return { ok: false, error: core.TEAM_SIZE_REQUIRED_ERR }; t.status = 'pending'; ah(t, 'draft', 'pending', actor, 'published'); /* 调研门禁③:发布的是 epic(有子任务)时轻量预检子任务调研注入,缺失挂返回值由锁外 pushSysNote 汇总 */ __pre = epicPrecheck(d.tasks, t.id, existsFn) } if (args.unfreeze && t.frozen) { delete t.frozen; delete t.frozenAt; delete t.frozenBy; delete t.waitingLock; ah(t, t.status, t.status, actor, '解除冻结,重新进入派发池') } if (args.resetToPending) { var ps = t.status; t.status = 'pending'; t.claimedBy = null; t.claimedAt = null; t.resolvedAt = null; t.resolution = null; delete t.retryCount; delete t.stuckSince; delete t.cancelledAt; ah(t, ps, 'pending', actor, 'reset to pending after edit') }; if (args.hooks !== undefined) { var __he = applyHooks(t, args.hooks); if (__he.error) return { ok: false, error: __he.error } }; t.lintWarnings = draftLint(t, draftLintIo(sid, d.ownerCwd)); var __out = { ok: true, task: t }; if (__depWarn) __out.warning = __depWarn; if (__pre && __pre.missing.length) __out.epicPrecheck = __pre; return __out }); afterPublishPrecheck(sid, args.taskId, __res); return __res })
|
|
578
601
|
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, workMode: deriveWorkMode(d) } }) })
|
|
579
602
|
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, workMode: deriveWorkMode(d) } }) })
|
|
580
603
|
// 工作模式三档单入口(v75 UI 收敛):一次写入 boardMode+teamMode 两个字段,
|
|
@@ -912,6 +935,8 @@ export function createRpc(ctx, state, deps) {
|
|
|
912
935
|
var t = d.tasks.find(function (x) { return x.id === taskId })
|
|
913
936
|
if (!t) return null
|
|
914
937
|
delete t.stuckSince
|
|
938
|
+
// 豁免防抖窗与卡死标记同生命周期清理(task-mvxmxkq1):终止后重排队的新 run 不应受上一轮窗口压制
|
|
939
|
+
delete t.stuckRearmUntil
|
|
915
940
|
// 认领位清理与状态无关(task-mv1pqy27 死结窗口根修):凡 terminate 命中该卡的 run(rec 在内存表),
|
|
916
941
|
// 按 rec.role 清对应认领位——覆盖 resolved/archived/cancelled 态(卡已落定但 run 尚在结算窗口)的
|
|
917
942
|
// 死结:否则 verifierRun/claimedBy 仍指向已终止 run 的 running 留档 → activeRunOf 卡面兜底误判活跃 → archive 永久拒。
|
|
@@ -938,11 +963,32 @@ export function createRpc(ctx, state, deps) {
|
|
|
938
963
|
if (killFail) ret.killUnconfirmed = killFail // 调用方可直接感知「终止未确认生效」
|
|
939
964
|
return ret
|
|
940
965
|
}
|
|
941
|
-
//
|
|
966
|
+
// 继续观察:清除卡死标记 + 重置判定基线(task-mvxmxkq1)
|
|
967
|
+
// 根因:旧实现只清 t.stuckSince,判定输入(run 级 frameSilenceSince 帧静默基线)未重置——模型仍在静默
|
|
968
|
+
// (≥120s)时下一 sweep 立刻重标,「继续观察」等于没点。修法两层:
|
|
969
|
+
// ① 基线重置:该卡最新在跑留档(worker/verifier)的帧静默基线置 now——模型需再静默满 120s 才可能重标;
|
|
970
|
+
// 日志兜底路径(无帧能力宿主)同款重置 zeroGrowthSince,600s 墙钟重新起算(同款两闸,语义一键对齐)。
|
|
971
|
+
// ② 防抖窗:t.stuckRearmUntil=now+120s,窗内判定跳过重标(兜 sweep 在飞时读到旧基线的竞态边缘);
|
|
972
|
+
// 一次性,重标成功/帧恢复即清(见 hung.mjs)。判死 600s 闸不拦——豁免不永续。
|
|
942
973
|
async function doDismiss(sid, actor, taskId) {
|
|
943
974
|
await mutateLocked(sid, function (d) {
|
|
944
975
|
var t = d.tasks.find(function (x) { return x.id === taskId })
|
|
945
|
-
if (t)
|
|
976
|
+
if (!t) return null
|
|
977
|
+
delete t.stuckSince
|
|
978
|
+
var nowMs = Date.now()
|
|
979
|
+
// ① 基线重置:倒序取最新一条在跑留档(同 sweep 的 row 选取口径);role 缺省按 worker(存量行无 role)
|
|
980
|
+
var trs = Array.isArray(t.runs) ? t.runs : []
|
|
981
|
+
for (var i = trs.length - 1; i >= 0; i--) {
|
|
982
|
+
var row = trs[i]
|
|
983
|
+
if (!row || row.outcome !== 'running') continue
|
|
984
|
+
if (row.role && row.role !== 'worker' && row.role !== 'verifier') continue
|
|
985
|
+
row.frameSilenceSince = nowMs // 帧主路径基线(缺失也先立,杜绝按 lastFrameAt/startedAt 旧值秒重标)
|
|
986
|
+
row.zeroGrowthSince = nowMs // 日志双闸兜底基线(无帧能力宿主:600s 墙钟重新起算)
|
|
987
|
+
break
|
|
988
|
+
}
|
|
989
|
+
// ② 防抖窗:与疑似阈值同值 120s(「观察一轮」本义)
|
|
990
|
+
t.stuckRearmUntil = nowMs + HUNG_SUSPECT_REARM_MS
|
|
991
|
+
ah(t, t.status, t.status, actor, '清除卡死标记,继续观察(基线已重置)')
|
|
946
992
|
return t
|
|
947
993
|
})
|
|
948
994
|
return { ok: true }
|
package/lib/settle.mjs
CHANGED
|
@@ -867,6 +867,140 @@ export function createSettle(opts) {
|
|
|
867
867
|
return swept.length
|
|
868
868
|
}
|
|
869
869
|
|
|
870
|
+
// ===== 野跑看门狗(task-mvyzshi7):非落定卡 running 留档超 hardMin 墙钟即结算 =====
|
|
871
|
+
// 事故(本体板 2026-11-02 实证,task-mvyyxs5u):host 重启把 armTimeouts 的两级超时臂(内存 setTimeout)
|
|
872
|
+
// 清零,而 reconcileRoot 只重建 continuable Worker 的观测与超时臂(三道门禁自证「只对 role=worker +
|
|
873
|
+
// continuable」)——一次性 verifier run 的超时臂**永不恢复**,hardMin 硬超时彻底失效,run 无任何上限
|
|
874
|
+
// 可无限野跑(实证:verifier run 跑 8 小时 2 分仍在活跃写盘;日志持续增长,600s 零增长挂死闸也抓不到)。
|
|
875
|
+
// 修法(墙钟兜底,不依赖内存计时器):每轮 poolCycle 扫所有**非落定卡**上 outcome==='running' 且无
|
|
876
|
+
// endedAt 的留档,年龄 now-at 超过该留档的 hardMin(缺省 360 分钟,留档自带值优先)→ 按失败结算收口:
|
|
877
|
+
// · 关账:outcome='timeout/error' + endedAt + note(settleRun 超时臂同款结局,字段与 closeRunHistory
|
|
878
|
+
// 一致、并在同一持锁事务里连同卡面推进一次写完);
|
|
879
|
+
// · 落卡:lastError 野跑文案 + 喂熔断连击(feedModelFailure 三连拉黑——与在飞臂的 verifier 即时
|
|
880
|
+
// badModels 熔断不同:重启残影不欺负模型,走连击更保守);
|
|
881
|
+
// · worker(认领位曾指向它):settleWorker 失败臂同款——retryCount++ → pending 重排 / x3 blocked;
|
|
882
|
+
// · verifier(幂等位曾指向它):settleVerifier 失败臂同款——verifyRetries++ → 重排审查 / x3 转人工;
|
|
883
|
+
// · hook-post:幂等占用位已清 → 重挂 hooks.post.pending 待跑标记(releaseSpawnPlaceholder 同款,
|
|
884
|
+
// 下轮 cycle 重新 spawn 收口);hook-pre 的状态机由 poolCycle 既有 2min 死占位回收自愈,不动;
|
|
885
|
+
// · 其余(占位未指向本档/卡不在对应跑态/escalation/frozen):只关账+留痕,绝不碰卡面状态——
|
|
886
|
+
// 占位易主说明卡面已有新 run 接手(孤儿回收后重派的形状),清卡面会误杀新 run;
|
|
887
|
+
// · 补停:锁外对野跑子会话补一枪 interrupt(stopSettledOneShot 同款 authority 形状)——不补停则旧
|
|
888
|
+
// 会话残影继续空烧(事故原形 8h);continuable 补停失败 → noResume 落档(重派回退 fresh spawn)。
|
|
889
|
+
// 保守双闸(防误杀在飞 run):留档有活跃 rec 接管且 rec 年龄未超线 → 跳过——rec 年轻(reconcile 重建/
|
|
890
|
+
// 续跑刚接手)说明超时臂在位、run 有主;留档 at 是重启前的时间戳、年龄大不算野跑。rec 年龄也超线才动手
|
|
891
|
+
// (settleRun 有 settled 守卫,双闸再防一层「墙钟 vs 在飞臂」的双结算重派)。
|
|
892
|
+
// 与 sweepZombieRuns 分工:那个管**落定卡**(resolved/cancelled/archived)的 running 无 endedAt 留档
|
|
893
|
+
// 关账(stale-closed,无年龄闸);本函数管**非落定卡**的超龄 running 留档真超时结算——状态域互斥不重叠。
|
|
894
|
+
// 边界(只加墙钟兜底):armTimeouts 两级超时臂语义、reconcileRoot 门禁、sweepZombieRuns 口径均不动。
|
|
895
|
+
var WILD_ERR = 'run 超出硬超时上限(重启后超时臂丢失的野跑),按模型面故障结算重派'
|
|
896
|
+
var WILD_RUN_NOTE = '野跑看门狗墙钟结算(重启后超时臂丢失)'
|
|
897
|
+
var WILD_CIRCUIT_WHY = '野跑超硬超时墙钟(重启后超时臂丢失),已熔断'
|
|
898
|
+
function wildHardMinOf(r) { var hm = Number(r && r.hardMin); return hm > 0 ? hm : 360 }
|
|
899
|
+
// 候选闸(预扫/锁内共用同一判定,两处口径永不漂移):非落定卡 + running 无 endedAt + 年龄超线 + 双闸放行
|
|
900
|
+
function wildGate(t, r, runs) {
|
|
901
|
+
if (!t || !r || r.outcome !== 'running' || r.endedAt) return false
|
|
902
|
+
var atT = new Date(r.at).getTime()
|
|
903
|
+
if (!isFinite(atT) || atT <= 0) return false // 无/畸形起始时间:年龄不可知,宁少勿多不动
|
|
904
|
+
var hardMs = wildHardMinOf(r) * 60000
|
|
905
|
+
if (Date.now() - atT < hardMs) return false // 年龄闸:墙钟直比(now - at >= hardMin×60s)
|
|
906
|
+
var rec = runs[t.id]
|
|
907
|
+
if (rec && !rec.settled && String(rec.id) === String(r.id)) {
|
|
908
|
+
var recStart = Number(rec.startedAt)
|
|
909
|
+
if (!(recStart > 0) || Date.now() - recStart < hardMs) return false // 在飞 rec 年轻:超时臂在位,留给臂结算
|
|
910
|
+
}
|
|
911
|
+
return true
|
|
912
|
+
}
|
|
913
|
+
async function sweepWildRuns(sid, snap) {
|
|
914
|
+
if (!snap || snap.__noPersist || !Array.isArray(snap.tasks)) return 0 // 瞬时读盘失败/空板:跳过,不写盘
|
|
915
|
+
var runs = runsFor(sid)
|
|
916
|
+
// 预扫快照(零写盘纪律同 sweepZombieRuns):无候选不进锁——安静板不因看门狗多出一次锁调用
|
|
917
|
+
var hasCandidate = false
|
|
918
|
+
for (var pi = 0; pi < snap.tasks.length && !hasCandidate; pi++) {
|
|
919
|
+
var pt = snap.tasks[pi]
|
|
920
|
+
if (!pt || pt.status === 'resolved' || pt.status === 'cancelled' || pt.status === 'archived') continue
|
|
921
|
+
var prs = Array.isArray(pt.runs) ? pt.runs : []
|
|
922
|
+
for (var pj = 0; pj < prs.length; pj++) { if (wildGate(pt, prs[pj], runs)) { hasCandidate = true; break } }
|
|
923
|
+
}
|
|
924
|
+
if (!hasCandidate) return 0
|
|
925
|
+
var settled = [] // 锁内结算清单 {taskId,rid,role,model,continuable}——锁外喂熔断/补停消费
|
|
926
|
+
var blockedResumes = [] // continuable 补停失败者:锁外二次小事务落 noResume(续跑资格作废)
|
|
927
|
+
await mutateLocked(sid, function (d) {
|
|
928
|
+
if (!d || !Array.isArray(d.tasks)) return null
|
|
929
|
+
var any = false
|
|
930
|
+
d.tasks.forEach(function (t) {
|
|
931
|
+
if (!t || t.status === 'resolved' || t.status === 'cancelled' || t.status === 'archived') return // 落定卡归 sweepZombieRuns
|
|
932
|
+
if (!Array.isArray(t.runs)) return
|
|
933
|
+
t.runs.forEach(function (r) {
|
|
934
|
+
if (!wildGate(t, r, runs)) return // 锁内重判(含双闸):预扫到持锁的窗口里卡面可能已变
|
|
935
|
+
var rid = String(r.id)
|
|
936
|
+
var role = r.role || '?'
|
|
937
|
+
var wasClaim = t.claimedBy === rid // worker 认领位曾指向本档(清占位前取值——清了就判不了)
|
|
938
|
+
var wasVerif = t.verifierRun === rid // verifier/hook-post 幂等位曾指向本档
|
|
939
|
+
r.outcome = 'timeout/error' // settleRun 超时臂同款结局(closeRunHistory 同字段直写,单事务原子)
|
|
940
|
+
r.endedAt = new Date().toISOString()
|
|
941
|
+
r.note = WILD_RUN_NOTE
|
|
942
|
+
if (wasClaim) { t.claimedBy = null; t.claimedAt = null }
|
|
943
|
+
if (wasVerif) { t.verifierRun = null; delete t.verifierRunAt }
|
|
944
|
+
t.lastError = String(WILD_ERR).slice(0, 300) // 失败原因落卡(settleWorker/settleVerifier 同款截断)
|
|
945
|
+
if (role === 'worker' && wasClaim && t.status === 'in-progress' && !t.escalation && !t.frozen) {
|
|
946
|
+
t.retryCount = (t.retryCount || 0) + 1
|
|
947
|
+
if (t.retryCount >= 3) { t.status = 'blocked'; ah(t, 'in-progress', 'blocked', 'system', 'worker 失败 x' + t.retryCount + '(' + WILD_ERR.slice(0, 120) + '),待人工介入') }
|
|
948
|
+
else { t.status = 'pending'; ah(t, 'in-progress', 'pending', 'system', 'worker 失败(' + WILD_ERR.slice(0, 80) + '),重新排队 (' + t.retryCount + '/3)') }
|
|
949
|
+
} else if (role === 'verifier' && wasVerif && t.status === 'verifying' && !t.escalation && !t.frozen) {
|
|
950
|
+
t.verifyRetries = (t.verifyRetries || 0) + 1
|
|
951
|
+
if (t.verifyRetries >= 3) {
|
|
952
|
+
t.escalation = { question: 'Verifier 连续 ' + t.verifyRetries + ' 次超硬超时野跑(重启后超时臂丢失)。交付物已完成,请人工验收:看板详情页直接通过/驳回,或 task_verify 裁决。', at: new Date().toISOString(), by: 'system' }
|
|
953
|
+
ah(t, 'verifying', 'verifying', 'system', 'verifier 野跑超时 x' + t.verifyRetries + ',转人工验收')
|
|
954
|
+
} else ah(t, 'verifying', 'verifying', 'system', 'verifier 野跑超硬超时,重新排队审查 (' + t.verifyRetries + '/3)')
|
|
955
|
+
} else if (role === 'hook-post' && wasVerif && t.hooks && t.hooks.post && t.hooks.post.state !== 'done') {
|
|
956
|
+
t.hooks.post.pending = true // 幂等占用位已清 → 重挂待跑标记,下轮 cycle 重新 spawn 收口
|
|
957
|
+
} else {
|
|
958
|
+
ah(t, t.status, t.status, 'system', '野跑 run 留档墙钟超时关账(角色 ' + role + ')')
|
|
959
|
+
}
|
|
960
|
+
settled.push({ taskId: t.id, rid: rid, role: role, model: r.model || '', continuable: r.continuable === true })
|
|
961
|
+
any = true
|
|
962
|
+
})
|
|
963
|
+
})
|
|
964
|
+
return any ? d : null // 无改动返回 null → 不写盘
|
|
965
|
+
}, true) // skipKick:本轮已是 poolCycle,重派由本轮持锁段按新鲜 d 自然承接
|
|
966
|
+
if (!settled.length) return 0
|
|
967
|
+
// ===== 锁外收尾:喂熔断连击 + 野跑子会话补停 =====
|
|
968
|
+
// 熔断/补停只盖 worker/verifier(hungZeroOutputSweep / stopSettledOneShot 同款边界——hook 与
|
|
969
|
+
// decomposer 的模型不进连击面、子会话不由看门狗停)。
|
|
970
|
+
var subagents = ctx.subagents
|
|
971
|
+
var parent = rootForSession(sid)
|
|
972
|
+
for (var wi = 0; wi < settled.length; wi++) {
|
|
973
|
+
var sw = settled[wi]
|
|
974
|
+
if (sw.model && (sw.role === 'worker' || sw.role === 'verifier')) feedModelFailure(sid, sw.model, WILD_CIRCUIT_WHY)
|
|
975
|
+
if (!subagents || typeof subagents.interrupt !== 'function') continue // 老宿主/老桩无此能力:跳过补停(关账不受影响)
|
|
976
|
+
if (sw.role !== 'worker' && sw.role !== 'verifier') continue
|
|
977
|
+
if (!parent) { if (sw.role === 'worker' && sw.continuable) blockedResumes.push(sw); continue } // 无活 root 可授权(endContinuable resumeBlocked 同款)
|
|
978
|
+
try { subagents.interrupt(sw.rid, { kind: 'ancestor', agent: parent }) }
|
|
979
|
+
catch (e) {
|
|
980
|
+
console.error('[task-board] 野跑子会话补停失败(会话可能仍在空转,只影响 token)task ' + sw.taskId + ' child ' + sw.rid + ':', String(e))
|
|
981
|
+
if (sw.role === 'worker' && sw.continuable) blockedResumes.push(sw)
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
// continuable 补停失败 → noResume 落档:该会话不可信(interrupt 打不到),重派直接 fresh spawn 不空唤醒
|
|
985
|
+
// (endContinuable → closeRunHistory(flags.noResume) 同款语义;留档已关账故走二次小事务直写)。
|
|
986
|
+
if (blockedResumes.length) {
|
|
987
|
+
try {
|
|
988
|
+
await mutateLocked(sid, function (d) {
|
|
989
|
+
if (!d || !Array.isArray(d.tasks)) return null
|
|
990
|
+
var anyR = false
|
|
991
|
+
blockedResumes.forEach(function (s2) {
|
|
992
|
+
var t2 = d.tasks.find(function (x) { return x.id === s2.taskId })
|
|
993
|
+
if (!t2 || !Array.isArray(t2.runs)) return
|
|
994
|
+
t2.runs.forEach(function (r2) { if (r2 && String(r2.id) === s2.rid && r2.noResume !== true) { r2.noResume = true; anyR = true } })
|
|
995
|
+
})
|
|
996
|
+
return anyR ? d : null
|
|
997
|
+
}, true)
|
|
998
|
+
} catch (e) { console.error('[task-board] 野跑 noResume 落档失败:', String(e)) }
|
|
999
|
+
}
|
|
1000
|
+
console.log('[task-board] 野跑看门狗结算 ' + settled.length + ' 条:' + settled.map(function (x) { return x.taskId + '/' + x.rid + '/' + x.role }).join(', '))
|
|
1001
|
+
return settled.length
|
|
1002
|
+
}
|
|
1003
|
+
|
|
870
1004
|
// ===== 幽灵占位回收(池冻结根修,task-muwlepg6)=====
|
|
871
1005
|
// 病根(活体实证):活跃表项(rec)的**唯一**摘除点是各结算通道(事件通道 settleRun /
|
|
872
1006
|
// 上报通道 settleReportedRun / 手动终止)。任何一条漏走(worker 经 board_report 完成但当时
|
|
@@ -950,5 +1084,6 @@ export function createSettle(opts) {
|
|
|
950
1084
|
// 重指本实例——同一 deps 接线(mutateLocked/health/通知类/kickCycle 同源),语义与搬移前一致。
|
|
951
1085
|
// 拆分第 7 刀:reconcile 域四函数(accumulateRunUsage/reconcileRoot/sweepZombieRuns/reapGhostRecs)
|
|
952
1086
|
// 从返回集导出——dispatch.mjs 的 poolCycleBody 三挂点与 return API(rpc 侧手动终止关账消费)重指本实例。
|
|
953
|
-
|
|
1087
|
+
// 野跑看门狗(task-mvyzshi7)随返回集导出:dispatch.mjs 的 poolCycleBody 挂点消费(sweepZombieRuns 之后)。
|
|
1088
|
+
return { settleRun: settleRun, settleRunRecord: settleRunRecord, settleReportedRun: settleReportedRun, onAgentStatus: onAgentStatus, settleWorker: settleWorker, accumulateRunUsage: accumulateRunUsage, reconcileRoot: reconcileRoot, sweepZombieRuns: sweepZombieRuns, sweepWildRuns: sweepWildRuns, reapGhostRecs: reapGhostRecs }
|
|
954
1089
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agent-board",
|
|
3
3
|
"description": "DeepSeek Harness 智能看板:任务卡片 + 一次性 Worker/Verifier 派发 + 依赖调度 + 管线分档 + Team 模式(歧义上报主窗口裁决)",
|
|
4
|
-
"version": "1.9.
|
|
4
|
+
"version": "1.9.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
7
7
|
"scripts": {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
65
|
"react": ">=18",
|
|
66
|
-
"@deepseek-ai/dsh": "^0.1.7 || 0.2.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.3 || ^0.2.0"
|
|
66
|
+
"@deepseek-ai/dsh": "^0.1.7 || 0.2.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5 || ^0.2.0"
|
|
67
67
|
},
|
|
68
68
|
"license": "MIT"
|
|
69
69
|
}
|