dsh-agent-board 1.9.2 → 1.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -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 +5 -2
- package/lib/hung.mjs +11 -0
- package/lib/policy.mjs +34 -3
- package/lib/rpc.mjs +52 -6
- package/package.json +2 -2
- package/test/core.test.mjs +240 -2
- package/test/hung-frame-signal.test.mjs +197 -0
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
| **0.1.7 ~ 0.1.7-x**(含 rc) | **≥ 1.2.2(必须)** | 0.1.7 会话日志升级为 format v4:插件消息 `source.kind` 必须是生产者自有 kind。1.2.1 及更早会在**任务完成/阻塞回执**落盘时抛 `SessionFormatError: format v4 message requires a producer-owned source kind`,并连带使主窗口当前轮次失败(表现为「本轮运行失败」);同时「跳转会话」因宿主移除 `sessions.open` 而失效(控制台 `sessionsSvc.open is not a function`),卡片活动心跳读不到 v4 日志(`session.v4.jsonl.zstd`) |
|
|
42
42
|
| 0.1.5-rc.1 ~ 0.1.6 | ≤ 1.2.1 | 1.2.2 起按 0.1.7 协议编写(v4 source kind、`uiWorkspace.openSession` 跳转),旧宿主未做回归验证,建议停留 1.2.1 |
|
|
43
43
|
|
|
44
|
-
> 当前版本 **v1.9.
|
|
44
|
+
> 当前版本 **v1.9.3** 已在宿主 **0.2.1-rc.5** 全量实测:739 断言全绿 + 拆解官/力度路由/健康嗅探/帧活性挂死检测真机 E2E 全链。
|
|
45
45
|
|
|
46
46
|
```sh
|
|
47
47
|
dsh plugin --profile web add dsh-agent-board@latest # 0.1.7+/0.2.x 宿主(推荐)
|
|
@@ -120,7 +120,7 @@ dsh plugin --profile web remove dsh-agent-board
|
|
|
120
120
|
- **看板**:六列状态流、拖拽 / 多选批量 / 文本·优先级·标签筛选、归档留档、会话隔离
|
|
121
121
|
- **派发**:一次性 Worker / 独立 Verifier、依赖调度(DFS 环检测)、touches 文件级排他、可续跑 Worker、Team 托管、**spawn 落闸锁内重验**(spawnGateOk:claim 占位与落闸决策读同一把锁的同一份快照,terminate/dependsOn 后补 落在 spawn 在途窗口即弃权——竞态与后补依赖双双根治)、**高优干预分态**(intervene 无活跃 run 时区分「续跑窗口,干预随续跑注入」与「真无 run」,返回体带 state 字段)、**建卡派发预警**(将自动派发时 lintWarnings 瞬态提示「约 1s 内被领取,想占位请 draft:true」)、**宿主 spawn API 双兼容**(v0.2.1 宿主把 `subagents.start/startContinuable` 统一重构为 `startActivation(spec)`——能力探测自动选路:新契约按 `delivery` 语义分发(continuable=`parent` 结算留言 / 一次性=`caller` 只回 result),Activation 适配旧 run 形状下游零改动;老宿主逐字走旧双路径)+ **continuable 结算兜底臂**(`activation.result` 在子会话被宿主拆除时落定即接失败结算——rate-limit/模型崩溃的死子不再挂到硬超时)
|
|
122
122
|
- **智能调度** `☆`:**拆解官工作流**(自然语言需求卡 → `task_decompose` 拆解官先调研后提案(outputSchema 强箍,子卡必带档位/touches/验收命令 + **dependsOn 序号接线**:提案内以 1 起序号互相引用,物化时映射真实任务 id,前排约束天然无环、坏序号丢弃留痕)→ 主窗口呈现、你确认调档 → 逐张 publish;拆解官在池条「🧩 拆解中」与源卡徽标可见;**落定走正式回执**:「🧩 已拆解 N 个(待确认发布)」聚合分区——split 列子卡清单 / keep 列理由,源卡停 draft 也必送达(投递过滤豁免,archived 才丢),失败仍走 ⚠️ 系统异常)+ **力度路由**(轻/标/重三档 × Worker/Verifier 六条有序链,四级取模链:卡级特例→档位链→板级→父模型,每跳过健康闸)+ **备用模型链** `fallbackModels`(额度到顶/故障先跨供应商同档切换,链尽才降档)+ **冷却与熔断**(限流带重置时间解析、连接/网关故障 10 分钟短窗自恢复)+ **模型健康嗅探**(`llm.stream` 极简探测定时扫已配置模型,统一 modelHealth 视图——冷却/熔断/近 5 次 run 聚合,弹层链芯片带 🟢🟡🔴⚪ 健康点,事前调度可见;**探测错误码分治**:额度>凭据>连接信号才判失败,纯空响应(供应商怪癖)判 🟡 degraded 存活——不喂冷却、路由仍可选用;**探测预算独立**:每次尝试各 20s(慢模型重试不再被共享窗口饿死),重试预算耗尽沿用首探判定不判死、超时不覆盖既有观测;**探测恢复熔断解除**:已熔断模型连续 2 个探测周期通过即自动翻绿+解除拉黑+连击归零(单次成功不复位,过滤偶然响应——探测管可调性的对称面:能判死就能证活))
|
|
123
|
-
- **执行纪律面** `☆`:**计划闸口**(Worker 动笔前报「计划改动」清单,越界+撞他卡锁双查警示,点名持锁卡;清单解析容错:正则残片/中文散文片不误判为路径)+ **仲裁扩权入锁**(`task_arbitrate` 的 `expandTouches` 把放行文件并入有效锁集——闸口比对即认扩权边界+冲突预检+hold
|
|
123
|
+
- **执行纪律面** `☆`:**计划闸口**(Worker 动笔前报「计划改动」清单,越界+撞他卡锁双查警示,点名持锁卡;清单解析容错:正则残片/中文散文片不误判为路径)+ **仲裁扩权入锁**(`task_arbitrate` 的 `expandTouches` 把放行文件并入有效锁集——闸口比对即认扩权边界+冲突预检+hold 撞锁自动等锁复工)+ **touches 更新通道**(`task_update` 带 `touches` 替换式更新:规划期 draft/pending/blocked/cancelled 与落定后 resolved 主窗口可直改(GUI 详情页亦可编辑);**运行态 in-progress/verifying 拒写**——持锁期边界以 Worker 实际覆盖面申报为准,走 escalate→仲裁扩权;归一化与建卡同款,history 留「N 项→M 项」审计轨)+ **续跑健康闸**(病模型续跑自动改 fresh spawn、宿主秒拆喂熔断、死会话哨兵 5s 即重派)+ **sizeHint 动态调档**(Worker 执行中自报档位修正,下次 spawn 生效)+ **opsLog 人工留痕**(GUI/脚本写集落盘截尾 50 条 + Team 注入「最近人工操作」摘要)
|
|
124
124
|
- **验收治理** `★`:硬性验收脚本独立复跑、驳回全量带回重派、Verifier 自测指南(userTest)、风险队列「待你过目」、验收时序防护(Verifier 在场时主窗口抢批需显式 `force:true`;有活跃 run 的卡禁止归档;Verifier 迟到结论落 `lateVerdict` 留痕+直投告警)、**迟到 verdict 三保险**(①入参校验:`verdict` 非 approved/rejected 拒收并返回重调指引——漏带字段不再被当驳回落账;②通知分态:approved 迟到=「✅ 迟到复核·全过追认」notice 级,🚨「迟到驳回」只留给真 rejected;③指纹去重:同 taskId+by+verdict 只投一次,重复落账零重复通知)
|
|
125
125
|
- **度量** `★`:Token 消耗(总量 / 有效 / 缓存读,按模型·按任务·按日)+ 记分卡(模型表现 / 质量趋势 / 驳回聚类)
|
|
126
126
|
- **可运维性** `★`:运行时健康自检(派发冻结 / 结算停滞告警)、异常驱动审视(风险队列 / 质量异动告警 / 批次摘要)、起草 lint、数据安全(原子写盘 / 腐坏隔离 / 自动 salvage)、**僵尸 run 留档清扫**(host 重启后「落定卡上的 running 无 endedAt 留档」在派发周期自动关账 `stale-closed` + 清认领位,归档门禁不再被幻影 run 永久拦截)、**子会话生命周期收口**(一次性 run 落定/迟到 verdict 落账后主动 interrupt 子会话——「结算≠停止」空转根治;continuable 收紧为「续跑位真在本 child 手上」才豁免,残影 Worker 无处藏身)、**terminate 三保险**(no-rec 也按认领位盲杀补刀 + interrupt 失败 err 级可见化 + `hold:true` 原子「终止+转手动」)、**配置审计**(set-board-config 成功写入落板级 configLog {key,from,to,actor,at} 截尾 50 条;模型下拉两段式选中≠保存)
|
|
@@ -206,7 +206,7 @@ draft → pending → in-progress → verifying → resolved → archived
|
|
|
206
206
|
- 详情父子区块:父卡详情列子任务清单(状态色点 + 标题,**含已归档留档**——归档行灰化 + 行尾「已归档」徽章并沉底,点击直达子卡详情,标题行汇总 settled/total);子卡详情顶行「↳ 史诗:父标题」点击回跳父卡
|
|
207
207
|
- **可选 hooks(史诗可选点位:前置/后置各是一次真实 agent 运行)**:`epic.hooks = { pre: { enabled, prompt, state, runId }, post: 同构 }`——**前置**让子任务具备开跑条件(串行闸门:未完成前该史诗的子任务一张都不派),**后置**把这批活收口(全部子任务了结后跑一遍,完成后史诗才转验证中);prompt 只给薄框架契约,具体动作由 hook agent **自行决策**,**吃不准就走歧义上报**(失败即转阻塞等人裁决:重试/跳过/放弃,不自动重跑)。hook run 记在 `epic.runs[]`(role `hook-pre`/`hook-post`),卡片带相位徽章(`⏳ 前置准备中` / `🧪 收尾中` / `⚠️ hook 失败待裁决`,无 hooks 的老卡零渲染),详情页 hooks 区可编辑 enabled+prompt、查看 state/耗时并跳转 run 会话、失败一键跳裁决区;**hooks 仅主窗口可设**(工具与 RPC 双通道门禁),commit/push 不入任何默认形态
|
|
208
208
|
- **删除通道(真删,无 undo)**:`delete-task` RPC(卡片 hover 垃圾桶按钮 / 详情页「删除」按钮,均先 `confirm('删除不可恢复,确认删除「标题」?')`)+ `batch-op op='delete'`(多选模式底部「批量删除」,同样 confirm)。状态门禁:**草稿/待办/阻塞可删**;进行中/验证中拒绝并提示先用 `terminate-agent` 终止(避免在跑的 run 变孤儿);已完成/取消引导改用归档(`archive-task`,留档可检索);有**未归档子任务**时拒删(防 `parentId` 悬空破坏父任务自动流转);已归档任务幂等返回 ok。是真删(从 `tasks` 数组移除),因此**不产生 `batch-undo` 撤销快照**(批量条对 delete 不显示「↩️ 撤销」),删除操作在 host 端 `console.error` 留一行日志便于溯源
|
|
209
|
-
- **任务粒度建议**:单任务 **10~30 分钟**可独立完成为甜区;预计超过 30 分钟的大任务先建一张 **epic 父卡**(`pipeline: direct`,不进池派发),再挂若干 10~30 分钟的子任务(`task_create` 传 `parentId=父卡 id`,有先后顺序用 `dependsOn` 串联),子任务全部完成后父卡自动流转(`checkParentAuto`)——`task_create` 工具描述与 Team 模式提示词都写了这条契约;**容器卡结构性不派发**:凡有子任务的卡(epic)永不进派发候选(与 `pipeline` 字段无关,不再只靠 `direct` 口头约定——手滑建 `full` 档 epic 也不会被派出占 worker
|
|
209
|
+
- **任务粒度建议**:单任务 **10~30 分钟**可独立完成为甜区;预计超过 30 分钟的大任务先建一张 **epic 父卡**(`pipeline: direct`,不进池派发),再挂若干 10~30 分钟的子任务(`task_create` 传 `parentId=父卡 id`,有先后顺序用 `dependsOn` 串联),子任务全部完成后父卡自动流转(`checkParentAuto`)——`task_create` 工具描述与 Team 模式提示词都写了这条契约;**容器卡结构性不派发**:凡有子任务的卡(epic)永不进派发候选(与 `pipeline` 字段无关,不再只靠 `direct` 口头约定——手滑建 `full` 档 epic 也不会被派出占 worker 槽),子任务全了结后按父卡 `pipeline` 分态收口——`full`/`work` 父卡转 verifying 派 Verifier 验收,`direct` 父卡直接 resolved(`direct` 不进派发池对 verifier 同成立,推进 verifying 只会把它推进 verifier 过滤闸够不到的状态)
|
|
210
210
|
- **suggestSplit 软提示**:`task_create` / `create-task` 发现描述超 500 字符、或标题/描述命中「全量 / 整体 / 系统级 / 全面 / 重构 / 所有模块 / 整个」等史诗特征词时,返回体附带一行 `suggestSplit` 建议文案(**只提示,不阻断创建与派发**;未命中则不出现该字段,老调用方无感)
|
|
211
211
|
- **开关 `epicSplit`(史诗拆分总开关)**(⚙️ 入池配置弹层「**功能**」小节,默认**开**):关掉只停**引导**——Team 提示词第 6~8 条拆分 doctrine 整条不注入、`create-task`/`task_create` 响应不再附 `suggestSplit` 软提示;**机制不禁**(显式传 `parentId` 建子卡、史诗自动收口与 hooks 状态机照常工作,明确要拆时不受阻);老看板文件没有该字段 → 读路径自动补 `true`(与升级前行为一致)。工具描述里的粒度契约是静态工具定义(随 request header 快照),不在开关范围
|
|
212
212
|
- **模型字段写保护**:`set-board-config` 对 `workerModel`/`verifierModel` 的非空值校验当前网关可用清单(`list-models` 同款枚举)——手写非法 id(如把熔断通知里的显示名抄成 id)直接拒写并返回合法清单前 8 个,验收 spawn 不再 UNKNOWN_MODEL 秒死;空串=清除放行;宿主无 llm 服务时降级跳过校验不炸。配套熔断通知分态:`UNKNOWN_MODEL` 类失败报「配置非法(不在可用清单)」,与「验收连续失败」区分——mistake 归 mistake,quality 归 quality
|
|
@@ -221,7 +221,7 @@ draft → pending → in-progress → verifying → resolved → archived
|
|
|
221
221
|
- **Worker/Verifier 均可配置异构模型**(⚙️ 弹出层下拉选择,空 = 继承父级),避免同源盲点;模型故障自动熔断回退父级模型
|
|
222
222
|
- **力度路由(三档 × 双角色模型表,`task-mvnemvf5` 地基 + `task-mvneq4mw` GUI)**:卡级 `size`(light/standard/heavy,对话 `task_update` 调档,缺省=标档语义、存量卡零迁移)查板级 `laneModels` 路由表定模——取模链四级:**卡级 `model` 特例(最高)→ 档位 lane 槽 → 板级 workerModel/verifierModel 行 → 父模型**(每跳照常过熔断,命中熔断降下一跳;实际命中跳随 run 落 `modelSource` 留档审计,档位随 run 落 `size` 留档供记分卡按档位聚合);「模型路由」弹层(工具栏 cpu 图标)「**力度路由**」小节:三档 × Worker/Verifier 共 **6 条有序链编辑器**(下拉添加+芯片 × 移除/↑ 前移,改动即写),空链 =「继承板级」(继承上方 Worker/Verifier 模型行);写通道 = `set-board-config key='laneModels'` **部分合并**(只合显式给出的槽位,非法模型整体拒写不落半截 + configLog 审计行);**档位槽支持有序数组=同档优先序**(`task-mvnxckhq`:槽位 `string[]` 第一位=首选,按序取首个健康模型——首选冷却/熔断则次选赢,槽内全病照旧 fallback→降档→父模型;GUI 为链式编辑器——下拉添加+芯片 ×/↑ 排序,读写整个有序数组,`task-mvokikl3`;老单字符串槽位读入自动归一单元素数组,零迁移);卡面 size/卡级 model 徽章只读展示(GUI 不编辑,调档走对话 `task_update`)
|
|
223
223
|
- 孤儿回收:子代理 run 结束/丢失超 2 分钟 → 任务自动回待办重派
|
|
224
|
-
- 看门狗:**帧活性双层判定**——运行中 assistant 帧静默 ≥120s → 标「🟡 疑似卡顿」(仅标卡、不杀,裁决权交主窗口/用户);帧静默 ≥600s → 判死按模型面故障结算重派并计入熔断连击;无事件订阅能力/零帧观测的宿主退回日志零增长兜底(连续 40 轮零增长 × ≥600s
|
|
224
|
+
- 看门狗:**帧活性双层判定**——运行中 assistant 帧静默 ≥120s → 标「🟡 疑似卡顿」(仅标卡、不杀,裁决权交主窗口/用户);帧静默 ≥600s → 判死按模型面故障结算重派并计入熔断连击;无事件订阅能力/零帧观测的宿主退回日志零增长兜底(连续 40 轮零增长 × ≥600s 墙钟);**「继续观察」有真语义**(点 dismiss 即重置帧静默基线 `frameSilenceSince=now` + 120s 防抖窗 `stuckRearmUntil`——模型需再静默满 120s 才会重新标卡,防抖窗不拦 600s 判死闸;日志兜底路径的 `zeroGrowthSince` 同款重置)
|
|
225
225
|
- 歧义上报:Worker 遇到歧义不猜测,上报等主窗口裁决(任何模式下都通知);裁决后新 Worker 携带裁决答案接手
|
|
226
226
|
- 驳回详情全量带回:三条驳回路径(`board_verdict` 工具 / Verifier 文本结算 / 手动 `task_verify`·`verify-task`)统一往 `t.messages` 落一条 `kind: "rejection"` 完整驳回包(summary + checks 逐条核对证据;手动路径补写 `t.verification`),经 `buildMessages` 全量注入重派 Worker prompt——新 Worker 据此返工,不再只看到 300 字截断的 history 记录
|
|
227
227
|
- **Verifier 自测指南(userTest)**:Verifier prompt 末尾追加「## 自测指南」段契约——四字段 `gist`(一句人话说改了什么)/ `steps[]`(用户操作步骤,每条一步)/ `expect`(预期看到什么)/ `tier`(`ui`=界面可操作 | `metric`=看指标变化 | `internal`=纯内部无用户可感知面;`internal` 时 steps 可空、expect 写「验证靠测试套件」);**诚实护栏写死在 prompt**:只给亲自验过/从 diff 可推导的步骤,不许编没验过的操作,UI 特性给具体路径(哪个区哪个按钮),host-only 改动如实标 `internal`。双模落账 `t.verification.userTest`(工具通道 `board_verdict` 的 `userTest` 参数 / 文本通道收「## 自测指南」段;tier 非法/缺省一律归 `internal` 保守档,缺段/字段全空不挂字段);详情页验证记录区渲染「📋 自测指南」块(gist + 编号步骤 + 预期 + tier 徽章:ui 绿 / metric 蓝 / internal 灰),报告导出聚合「## 本版自测清单」段(按验收通过时间倒序近 10 张已验收卡,`internal` 收末尾并标注「无用户可感知面」)
|
|
@@ -287,6 +287,7 @@ Team 托管档独有(调度员体验):
|
|
|
287
287
|
- 主窗口 system prompt 注入派发引导(提示词层面建议实质性改动走看板,不硬拦截);引导含**上下文书写提示**——子代理是全新会话、无会话记忆,description 写不够会自行调研跑偏
|
|
288
288
|
- **默认草稿护栏**:`task_create` / `create-task` 缺省建为草稿(草稿不派发),先把所有任务的 dependsOn、contextNotes/contextFiles 补齐,再逐个 `task_update publish=true` 统一发布;确实要立即派发的单个任务才显式传 `draft:false`
|
|
289
289
|
- **歧义通知 25s 去抖**:通知延迟 25s 投递,投递前重读看板——歧义已被裁决、或任务已 resolved/archived 就静默跳过(消除主窗口 turn 排队导致的过期回声);同一任务连续多次上报只投最新一条
|
|
290
|
+
- **判档与管线指引**(v1.9.3,修「全落标档」「全落 full」两个提示词盲区):Team 注入带三档具象样例——`light`=单文件小改/文案/配置/断言增补/≤15 分钟(样例:改一句提示词、加一条断言、调一个常量);`standard`=单模块多文件/常规特性/需新断言(样例:加一个开关、修一个判定分支);`heavy`=跨模块/改判定链核心/拆分类/高风险(样例:新机制、重构、动结算链)——配**判档三问**(几个文件?动不动判定核心?要不要全新机制?)与**「拿不准按低一档报」**(低报有 `sizeHint` 执行中自纠通道,高报只会白烧额度);同步 pipeline 场景指引——`work`=回归测试/巡检/文档/纯查询/纯报表(只做不验);`full`=代码改动/修 bug/新特性(默认,要 Verifier 独立复跑验收);`direct`=主窗口亲办不进池;**「用户说做回归/巡检/盘点 → 归 work」显式点名**。`task_create` 工具描述与拆解官判档文案同口径同步
|
|
290
291
|
- 任务完成/阻塞时主窗口收到**批量聚合回执**(45s 窗口或满 5 条聚合,等主窗口空闲再发,不打断对话);⚙️ 设置区「通知」小节有两个回执开关——`notifyDispatch`(⚡ 派发回执:任务被 Worker/Verifier 领走时播报)与 `notifyDone`(✅ 完成回执:完成/阻塞时聚合播报),缺省均**开**(老看板文件缺字段自动补 `true`);**歧义裁决通知不受开关影响**(裁决通道不是回执,任务等人裁决必须提醒)
|
|
291
292
|
|
|
292
293
|
> 兼容:旧的 `set-board-mode` / `set-team-mode` 两个 RPC 原样保留(旧客户端与脚本不受影响),
|
|
@@ -283,6 +283,35 @@
|
|
|
283
283
|
React.createElement('button', { onClick: commit, title: '保存标签(逗号分隔,自动 trim + 去空)', style: { fontSize: 10, padding: '3px 10px', border: 'none', borderRadius: 4, background: C.brand, color: C_INV, cursor: 'pointer', flexShrink: 0 } }, '保存')))
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
// touches 声明编辑(task-mvxw7hn5):文件锁声明主窗口/GUI 可改(生命周期门禁在 host——运行中拒写)。
|
|
287
|
+
// 未持锁态(draft/pending/blocked/cancelled/resolved)渲染 textarea + 保存,提交走 update-task RPC 的
|
|
288
|
+
// touches 通道(替换式更新,空清单=清除声明回「无锁」态);in-progress/verifying 只读展示 + 申报指引
|
|
289
|
+
//(此阶段边界由 Worker 以实际覆盖面申报:escalate→仲裁扩权;落定后可在此修正)。
|
|
290
|
+
// 分隔符兼容逗号/顿号/分号/换行;Ctrl+Enter 快捷提交。
|
|
291
|
+
function TouchesEditor(props) {
|
|
292
|
+
var task = props.task
|
|
293
|
+
var running = task.status === 'in-progress' || task.status === 'verifying'
|
|
294
|
+
var _R = React; var useState = _R.useState
|
|
295
|
+
var _s = useState((task.touches || []).join(', ')), draft = _s[0], setDraft = _s[1]
|
|
296
|
+
var _m = useState(''), msg = _m[0], setMsg = _m[1]
|
|
297
|
+
function commit() {
|
|
298
|
+
var touches = draft.split(/[,,、;;\n]+/).map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 }).slice(0, 20)
|
|
299
|
+
rpc('update-task', { taskId: task.id, touches: touches }).then(function (r) {
|
|
300
|
+
if (r && r.ok === false) setMsg('⛔ ' + ((r && r.error) || '保存失败'))
|
|
301
|
+
else setMsg('✅ touches 已更新(' + touches.length + ' 项' + (touches.length ? '' : '——空清单=清除声明,回「无锁」态') + ')')
|
|
302
|
+
fetchTasks()
|
|
303
|
+
}).catch(function (e) { setMsg('⛔ ' + String(e)) })
|
|
304
|
+
}
|
|
305
|
+
return React.createElement('div', { style: { marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4 } },
|
|
306
|
+
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '🔒 文件级排他(touches · ' + (running ? '运行中只读' : '逗号/换行分隔,空=清除声明') + ')'),
|
|
307
|
+
running ? React.createElement('div', null,
|
|
308
|
+
React.createElement('div', { style: { fontSize: 10, color: C.text2, wordBreak: 'break-all' } }, '🔒 ' + ((Array.isArray(task.touches) && task.touches.length) ? task.touches.join('、') : '(未声明)')),
|
|
309
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2 } }, '运行中任务的 touches 由 Worker 以实际覆盖面申报(escalate→仲裁扩权);落定后可在此修正。')) : React.createElement('div', { style: { display: 'flex', gap: 4, alignItems: 'flex-start' } },
|
|
310
|
+
React.createElement('textarea', { value: draft, onChange: function (e) { setDraft(e.target.value) }, onKeyDown: function (e) { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) commit() }, placeholder: '如 src/x.mjs、src/**(每行一条或逗号分隔;空=清除声明)', rows: 2, style: { flex: 1, minWidth: 0, fontSize: 11, padding: '4px 8px', border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text, fontFamily: 'inherit', boxSizing: 'border-box', resize: 'vertical' } }),
|
|
311
|
+
React.createElement('button', { onClick: commit, title: '保存 touches 声明(替换式更新;空清单=清除声明回「无锁」态)', style: { fontSize: 10, padding: '3px 10px', border: 'none', borderRadius: 4, background: C.brand, color: C_INV, cursor: 'pointer', flexShrink: 0 } }, '保存')),
|
|
312
|
+
msg ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, wordBreak: 'break-all' } }, msg) : null)
|
|
313
|
+
}
|
|
314
|
+
|
|
286
315
|
function DetailView() {
|
|
287
316
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect; var task = getTask(state.detailId)
|
|
288
317
|
var _a = useState(task ? task.title : ''), editTitle = _a[0], setEditTitle = _a[1]; var _b = useState(task ? task.description || '' : ''), editDesc = _b[0], setEditDesc = _b[1]; var _c = useState(false), saving = _c[0], setSaving = _c[1]; var _d = useState(state.boardMode), mode = _d[0], setMode = _d[1]
|
|
@@ -362,7 +391,7 @@
|
|
|
362
391
|
React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 4 } }, '本任务声明的 touches 与以下进行中任务重叠(同一批文件并行改会互踩);它们提交验收/完成后自动派发。'),
|
|
363
392
|
React.createElement('div', { style: { fontSize: 10, display: 'flex', flexWrap: 'wrap', gap: 5 } }, task.waitingForTouches.map(function (id) { var t2 = getTask(id); return React.createElement('span', { key: id, onClick: function () { if (t2) { state.detailId = id; notify() } }, title: t2 ? (t2.title + ' (' + id + ')') : id, style: { padding: '1px 5px', borderRadius: 3, background: C.nested, border: '1px solid ' + C.border, color: t2 ? C.brand : C.text2, cursor: t2 ? 'pointer' : 'default' } }, (t2 ? t2.title.slice(0, 20) : shortId(id)) + ' · ' + shortId(id)) })),
|
|
364
393
|
Array.isArray(task.touches) && task.touches.length ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 4 } }, '本任务 touches: ' + task.touches.join('、')) : null) : null,
|
|
365
|
-
|
|
394
|
+
React.createElement(TouchesEditor, { key: task.id + '|touches', task: task }),
|
|
366
395
|
// 起草 lint(task.lintWarnings[],host 建卡/更新卡现算落盘,get-tasks 透出):
|
|
367
396
|
// 四规则软警告(粒度过粗/双仓库歧义/缺验收/标题缺动词或描述空)随卡展示,缺字段/空数组整块不渲染(老卡零影响)。
|
|
368
397
|
Array.isArray(task.lintWarnings) && task.lintWarnings.length ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, padding: '5px 8px', border: '1px solid ' + C.warn, borderRadius: 6, background: 'color-mix(in srgb, ' + C.warn + ' 8%, transparent)' } },
|
package/lib/client.js
CHANGED
|
@@ -1392,6 +1392,35 @@ function apply(ctx) {
|
|
|
1392
1392
|
React.createElement('button', { onClick: commit, title: '保存标签(逗号分隔,自动 trim + 去空)', style: { fontSize: 10, padding: '3px 10px', border: 'none', borderRadius: 4, background: C.brand, color: C_INV, cursor: 'pointer', flexShrink: 0 } }, '保存')))
|
|
1393
1393
|
}
|
|
1394
1394
|
|
|
1395
|
+
// touches 声明编辑(task-mvxw7hn5):文件锁声明主窗口/GUI 可改(生命周期门禁在 host——运行中拒写)。
|
|
1396
|
+
// 未持锁态(draft/pending/blocked/cancelled/resolved)渲染 textarea + 保存,提交走 update-task RPC 的
|
|
1397
|
+
// touches 通道(替换式更新,空清单=清除声明回「无锁」态);in-progress/verifying 只读展示 + 申报指引
|
|
1398
|
+
//(此阶段边界由 Worker 以实际覆盖面申报:escalate→仲裁扩权;落定后可在此修正)。
|
|
1399
|
+
// 分隔符兼容逗号/顿号/分号/换行;Ctrl+Enter 快捷提交。
|
|
1400
|
+
function TouchesEditor(props) {
|
|
1401
|
+
var task = props.task
|
|
1402
|
+
var running = task.status === 'in-progress' || task.status === 'verifying'
|
|
1403
|
+
var _R = React; var useState = _R.useState
|
|
1404
|
+
var _s = useState((task.touches || []).join(', ')), draft = _s[0], setDraft = _s[1]
|
|
1405
|
+
var _m = useState(''), msg = _m[0], setMsg = _m[1]
|
|
1406
|
+
function commit() {
|
|
1407
|
+
var touches = draft.split(/[,,、;;\n]+/).map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 }).slice(0, 20)
|
|
1408
|
+
rpc('update-task', { taskId: task.id, touches: touches }).then(function (r) {
|
|
1409
|
+
if (r && r.ok === false) setMsg('⛔ ' + ((r && r.error) || '保存失败'))
|
|
1410
|
+
else setMsg('✅ touches 已更新(' + touches.length + ' 项' + (touches.length ? '' : '——空清单=清除声明,回「无锁」态') + ')')
|
|
1411
|
+
fetchTasks()
|
|
1412
|
+
}).catch(function (e) { setMsg('⛔ ' + String(e)) })
|
|
1413
|
+
}
|
|
1414
|
+
return React.createElement('div', { style: { marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4 } },
|
|
1415
|
+
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '🔒 文件级排他(touches · ' + (running ? '运行中只读' : '逗号/换行分隔,空=清除声明') + ')'),
|
|
1416
|
+
running ? React.createElement('div', null,
|
|
1417
|
+
React.createElement('div', { style: { fontSize: 10, color: C.text2, wordBreak: 'break-all' } }, '🔒 ' + ((Array.isArray(task.touches) && task.touches.length) ? task.touches.join('、') : '(未声明)')),
|
|
1418
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2 } }, '运行中任务的 touches 由 Worker 以实际覆盖面申报(escalate→仲裁扩权);落定后可在此修正。')) : React.createElement('div', { style: { display: 'flex', gap: 4, alignItems: 'flex-start' } },
|
|
1419
|
+
React.createElement('textarea', { value: draft, onChange: function (e) { setDraft(e.target.value) }, onKeyDown: function (e) { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) commit() }, placeholder: '如 src/x.mjs、src/**(每行一条或逗号分隔;空=清除声明)', rows: 2, style: { flex: 1, minWidth: 0, fontSize: 11, padding: '4px 8px', border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text, fontFamily: 'inherit', boxSizing: 'border-box', resize: 'vertical' } }),
|
|
1420
|
+
React.createElement('button', { onClick: commit, title: '保存 touches 声明(替换式更新;空清单=清除声明回「无锁」态)', style: { fontSize: 10, padding: '3px 10px', border: 'none', borderRadius: 4, background: C.brand, color: C_INV, cursor: 'pointer', flexShrink: 0 } }, '保存')),
|
|
1421
|
+
msg ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, wordBreak: 'break-all' } }, msg) : null)
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1395
1424
|
function DetailView() {
|
|
1396
1425
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect; var task = getTask(state.detailId)
|
|
1397
1426
|
var _a = useState(task ? task.title : ''), editTitle = _a[0], setEditTitle = _a[1]; var _b = useState(task ? task.description || '' : ''), editDesc = _b[0], setEditDesc = _b[1]; var _c = useState(false), saving = _c[0], setSaving = _c[1]; var _d = useState(state.boardMode), mode = _d[0], setMode = _d[1]
|
|
@@ -1471,7 +1500,7 @@ function apply(ctx) {
|
|
|
1471
1500
|
React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 4 } }, '本任务声明的 touches 与以下进行中任务重叠(同一批文件并行改会互踩);它们提交验收/完成后自动派发。'),
|
|
1472
1501
|
React.createElement('div', { style: { fontSize: 10, display: 'flex', flexWrap: 'wrap', gap: 5 } }, task.waitingForTouches.map(function (id) { var t2 = getTask(id); return React.createElement('span', { key: id, onClick: function () { if (t2) { state.detailId = id; notify() } }, title: t2 ? (t2.title + ' (' + id + ')') : id, style: { padding: '1px 5px', borderRadius: 3, background: C.nested, border: '1px solid ' + C.border, color: t2 ? C.brand : C.text2, cursor: t2 ? 'pointer' : 'default' } }, (t2 ? t2.title.slice(0, 20) : shortId(id)) + ' · ' + shortId(id)) })),
|
|
1473
1502
|
Array.isArray(task.touches) && task.touches.length ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 4 } }, '本任务 touches: ' + task.touches.join('、')) : null) : null,
|
|
1474
|
-
|
|
1503
|
+
React.createElement(TouchesEditor, { key: task.id + '|touches', task: task }),
|
|
1475
1504
|
// 起草 lint(task.lintWarnings[],host 建卡/更新卡现算落盘,get-tasks 透出):
|
|
1476
1505
|
// 四规则软警告(粒度过粗/双仓库歧义/缺验收/标题缺动词或描述空)随卡展示,缺字段/空数组整块不渲染(老卡零影响)。
|
|
1477
1506
|
Array.isArray(task.lintWarnings) && task.lintWarnings.length ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, padding: '5px 8px', border: '1px solid ' + C.warn, borderRadius: 6, background: 'color-mix(in srgb, ' + C.warn + ' 8%, transparent)' } },
|
package/lib/core.mjs
CHANGED
|
@@ -463,7 +463,9 @@ export function claimApply(d, t, sid, note) { var ps = t.status; t.status = 'in-
|
|
|
463
463
|
// 状态闸门即拦,级联不会反向误推动父卡。
|
|
464
464
|
export function isChildSettled(t) { return !!t && (t.status === 'resolved' || t.status === 'cancelled' || t.status === 'archived') }
|
|
465
465
|
// 史诗父卡自动收口(共享 helper,唯一判定口径):父卡存在、父卡 in-progress、且全部子任务 ∈ 终态完成集
|
|
466
|
-
// →
|
|
466
|
+
// → 按父卡 pipeline 分态收口:full/work 转 verifying(交人/Verifier 验收);direct 直接 resolved
|
|
467
|
+
// (direct 不进派发池对 verifier 同成立,推进 verifying 等于推进它够不到的状态)。
|
|
468
|
+
// 幂等:父卡已 verifying/resolved/archived 一律返回 null,重复调用无副作用。
|
|
467
469
|
export function maybeAutoCloseParent(d, childTask) {
|
|
468
470
|
if (!d || !childTask || !isb(childTask)) return null
|
|
469
471
|
var p = gpt(childTask, d.tasks)
|
|
@@ -489,6 +491,21 @@ export function maybeAutoCloseParent(d, childTask) {
|
|
|
489
491
|
}
|
|
490
492
|
}
|
|
491
493
|
if (p.hooks && p.hooks.post && p.hooks.post.pending) delete p.hooks.post.pending
|
|
494
|
+
// ===== 收口分态(task-mvxmxl00):direct 父卡直接 resolved,full/work 维持推进 verifying =====
|
|
495
|
+
// 设计矛盾实证(反馈 n-mvxmue8zggw0,本体板三张 direct 史诗 task-mvwk48c2/48rr/49fw):旧实现
|
|
496
|
+
// 一律推进 verifying,但 pickDispatch 的 verifier 候选过滤是 `(!t.pipeline || t.pipeline === 'full')`
|
|
497
|
+
// ——direct 卡永远不进 verifs,卡尸停在 verifying(派 Verifier 永不可能),把人锁死在这个状态。
|
|
498
|
+
// 口径修正:direct 语义「不进派发池」对 verifier 同样成立,故 direct 父卡在子任务全了结后直接
|
|
499
|
+
// resolved(子任务每张都已独立走完 worker+verifier 验收,父卡验收是形式收口;父卡本就是人建的容器)。
|
|
500
|
+
// full/work 零变化:照旧推进 verifying 走验收(work 卡在 resolveApply 侧本就落 resolved,不经此分支)。
|
|
501
|
+
// 边界:只修推进侧不再把 direct 卡推进它够不到的状态;派发侧过滤闸(pickDispatch)一字不动。
|
|
502
|
+
if (p.pipeline === 'direct') {
|
|
503
|
+
p.status = 'resolved'
|
|
504
|
+
p.resolvedAt = new Date().toISOString()
|
|
505
|
+
p.resolution = 'all subtasks resolved'
|
|
506
|
+
ah(p, 'in-progress', 'resolved', 'system', 'auto: all subtasks resolved(direct 父卡直接收口)')
|
|
507
|
+
return p
|
|
508
|
+
}
|
|
492
509
|
p.status = 'verifying'
|
|
493
510
|
p.resolvedAt = new Date().toISOString()
|
|
494
511
|
p.resolution = 'all subtasks resolved'
|
package/lib/dispatch.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { statSync } from 'node:fs'
|
|
11
11
|
import * as core from './core.mjs'
|
|
12
12
|
import { findRunLog } from './usage.mjs'
|
|
13
|
-
import { splitRuleOf, DECOMPOSE_SCHEMA, buildDecomposerPrompt, TEAM_DECOMPOSE_FLOW } from './policy.mjs'
|
|
13
|
+
import { splitRuleOf, DECOMPOSE_SCHEMA, buildDecomposerPrompt, TEAM_DECOMPOSE_FLOW, TEAM_SIZE_PIPELINE_GUIDE } from './policy.mjs'
|
|
14
14
|
import { createHung } from './hung.mjs'
|
|
15
15
|
import { createSettle } from './settle.mjs'
|
|
16
16
|
// 模型健康域(拆分第 2 刀 task-mvt8c1yx-2 + 第 3 刀 task-mvt8c1yx-3 + 第 4 刀 task-mvt8c1yx-4):冷却嗅探纯函数
|
|
@@ -1333,13 +1333,16 @@ export function createDispatch(ctx, state, deps) {
|
|
|
1333
1333
|
// 显式 parentId 建子卡 / 史诗自动收口 / hooks 状态机全部照常(机制不禁)。
|
|
1334
1334
|
// 第 9 条(需求卡→拆解官→确认发布工作流,task-mvneoy1u)**不受 epicSplit 门禁**——
|
|
1335
1335
|
// 它是流程引导不是劝拆,恒拼接在 splitRuleOf(...) 之后(teamMode 总闸仍由上面 teamModeCache 把守)。
|
|
1336
|
+
// 第 10/11 条(判档指引 + pipeline 场景,task-mvxpzcr9)同族纪律:**同样不受 epicSplit 门禁**——
|
|
1337
|
+
// 档位怎么判 / pipeline 怎么选与「要不要拆」正交,关掉拆分引导不能让主窗口重新失去选档标准
|
|
1338
|
+
//(「全落标档」「全落 full」两个提示词盲区的根修点就在这里)。
|
|
1336
1339
|
// ===== 「最近人工/外部操作」小节(task-mvnf3gly):opsCache 尾部 5 条单行,LESSON_RECALL_HINT 之前 =====
|
|
1337
1340
|
// 无操作时不出现该节(零噪音);section text() 同步纪律——只读 opsCache 缓存(rt() 同步点与 teamModeCache 同点),
|
|
1338
1341
|
// 每轮 prompt 组装现算现拼,不扫注册表、不读文件。
|
|
1339
1342
|
var __ops = opsCache[String(agent.id)]
|
|
1340
1343
|
var __opsLines = (Array.isArray(__ops) ? __ops : []).map(opsLogLine).filter(function (s) { return s.length > 0 })
|
|
1341
1344
|
var __opsSec = __opsLines.length ? '\n【最近人工/外部操作】HTTP 通道写集留痕(GUI/脚本等「非 agent 工具通道」的盘面改动)——人动了什么,规划下一步前先对齐:\n' + __opsLines.join('\n') : ''
|
|
1342
|
-
return '【任务看板 Team 模式已开启】\n本会话的任务看板处于 Team 模式。请遵循以下工作方式:\n1. 涉及代码改动、文件创建、命令执行等实质性工作时,优先用 task_create 提交为看板任务(由一次性 Worker/Verifier 子代理执行与验收),不要自己直接动手实现。\n2. 你仍保有全部工具能力——调研、读代码、讨论方案、回答问题时直接进行,无需提交任务。\n3. 创建任务时,务必在 description 里写清任务目标和约束;调研结论/原始需求/思路用 contextNotes 带上,调研时读过的关键文件用 contextFiles 把路径带上(可写「路径:L1-L2 — 一句用途」,只给行号不给正文)——两者都会随子代理的首条 prompt 一次性注入(调研笔记全文 + 文件清单),文件内容由子代理按需用 read 工具按行号范围自取。子代理是全新会话、无你的会话记忆,上下文不够它需要从零自行调研,效率大打折扣甚至跑偏方向——开发类任务(代码改动/修复/特性)务必带文件调研,实测可省 Worker 10~15 分钟自行 grep 定位;未带调研上下文的开发类任务返回会附 warning。\n4. Worker 上报歧义时会通过 task_arbitrate 等待你裁决,请及时响应。驳回重派时同样:新 Worker 没有上一轮的记忆,驳回原因会在 prompt 里,但额外上下文需你在 description 里补上。\n5. Team 模式下 task_create 默认建为草稿(草稿不会被派发领取)。把所有任务的 dependsOn 依赖关系、contextNotes/contextFiles 都补完后,再逐个 task_update publish=true 统一发布。确实需要立即派发的单个任务才显式传 draft:false。' + splitRuleOf(epicSplitOn(String(agent.id))) + '\n' + TEAM_DECOMPOSE_FLOW + __opsSec + (feedbackOn(String(agent.id)) ? '\n' + LESSON_RECALL_HINT + '把检索到的相关历史教训写进任务的 contextNotes,让子代理少踩重复的坑。' : '')
|
|
1345
|
+
return '【任务看板 Team 模式已开启】\n本会话的任务看板处于 Team 模式。请遵循以下工作方式:\n1. 涉及代码改动、文件创建、命令执行等实质性工作时,优先用 task_create 提交为看板任务(由一次性 Worker/Verifier 子代理执行与验收),不要自己直接动手实现。\n2. 你仍保有全部工具能力——调研、读代码、讨论方案、回答问题时直接进行,无需提交任务。\n3. 创建任务时,务必在 description 里写清任务目标和约束;调研结论/原始需求/思路用 contextNotes 带上,调研时读过的关键文件用 contextFiles 把路径带上(可写「路径:L1-L2 — 一句用途」,只给行号不给正文)——两者都会随子代理的首条 prompt 一次性注入(调研笔记全文 + 文件清单),文件内容由子代理按需用 read 工具按行号范围自取。子代理是全新会话、无你的会话记忆,上下文不够它需要从零自行调研,效率大打折扣甚至跑偏方向——开发类任务(代码改动/修复/特性)务必带文件调研,实测可省 Worker 10~15 分钟自行 grep 定位;未带调研上下文的开发类任务返回会附 warning。\n4. Worker 上报歧义时会通过 task_arbitrate 等待你裁决,请及时响应。驳回重派时同样:新 Worker 没有上一轮的记忆,驳回原因会在 prompt 里,但额外上下文需你在 description 里补上。\n5. Team 模式下 task_create 默认建为草稿(草稿不会被派发领取)。把所有任务的 dependsOn 依赖关系、contextNotes/contextFiles 都补完后,再逐个 task_update publish=true 统一发布。确实需要立即派发的单个任务才显式传 draft:false。' + splitRuleOf(epicSplitOn(String(agent.id))) + '\n' + TEAM_DECOMPOSE_FLOW + '\n' + TEAM_SIZE_PIPELINE_GUIDE + __opsSec + (feedbackOn(String(agent.id)) ? '\n' + LESSON_RECALL_HINT + '把检索到的相关历史教训写进任务的 contextNotes,让子代理少踩重复的坑。' : '')
|
|
1343
1346
|
},
|
|
1344
1347
|
})
|
|
1345
1348
|
ctx.effect(function () { return disposeSection })
|
package/lib/hung.mjs
CHANGED
|
@@ -29,6 +29,10 @@ export var HUNG_ZERO_GROWTH_ROUNDS = 40 // ≈ HUNG_WALL_FLOOR_MS / 15s 心跳
|
|
|
29
29
|
// 120s 只标疑似不杀;600s 才走既有判死结算。帧时间戳只在内存刷新,持久化仅承担 sweep 基线与状态转换。
|
|
30
30
|
export var HUNG_FRAME_SUSPECT_MS = 120000
|
|
31
31
|
export var HUNG_FRAME_KILL_MS = 600000
|
|
32
|
+
// 「继续观察」豁免防抖窗(task-mvxmxkq1):dismiss 重置帧静默基线后,sweep 可能已在飞(持锁前读到旧基线),
|
|
33
|
+
// 下轮仍可能按旧基线重标。窗口期内本卡跳过重标疑似——与疑似阈值同值 120s(「观察一轮」本义:模型要再静默
|
|
34
|
+
// 满 120s 才可能重标)。一次性:重标成功或帧恢复即清除;判死 600s 闸不受本窗影响(豁免不永续)。
|
|
35
|
+
export var HUNG_SUSPECT_REARM_MS = 120000
|
|
32
36
|
export var HUNG_KILL_PREFIX = 'run 挂死:' // 判死失败文本前缀(进 isModelKillFailure 白名单 = 计入熔断连击)
|
|
33
37
|
export var HUNG_CIRCUIT_WHY = '挂死零产出(会话日志字节持续不涨),已熔断' // 挂死触发的熔断文案分态(挂死≠秒拆)
|
|
34
38
|
// 挂死型模型面失败识别:判死结算的错误文本(HUNG_KILL_PREFIX 开头)与宿主秒拆/activation 异常
|
|
@@ -133,6 +137,8 @@ export function createHung(opts) {
|
|
|
133
137
|
if (hadFrame) {
|
|
134
138
|
var recoverySilence = frameSince ? nowFrame - frameSince : 0
|
|
135
139
|
if (frameSince) { delete row.frameSilenceSince; any = true }
|
|
140
|
+
// 帧恢复即「活体证据」:dismiss 留下的防抖窗一并清除(同帧恢复同款一次性语义,task-mvxmxkq1)
|
|
141
|
+
if (typeof t.stuckRearmUntil === 'number') { delete t.stuckRearmUntil; any = true }
|
|
136
142
|
if (t.stuckSince && recoverySilence < HUNG_FRAME_KILL_MS) {
|
|
137
143
|
delete t.stuckSince
|
|
138
144
|
ah(t, t.status, t.status, String(rec.id), 'assistant 帧恢复(静默不足 600s),清除疑似卡顿标记')
|
|
@@ -154,7 +160,12 @@ export function createHung(opts) {
|
|
|
154
160
|
return
|
|
155
161
|
}
|
|
156
162
|
if (frameSilentMs >= HUNG_FRAME_SUSPECT_MS && !t.stuckSince) {
|
|
163
|
+
// 继续观察防抖窗(task-mvxmxkq1):窗内跳过重标——dismiss 已重置基线,此处兜 sweep 在飞读到
|
|
164
|
+
// 旧基线的竞态边缘;窗过期后按重置后的新基线正常判定(豁免不永续)。
|
|
165
|
+
var rearmUntil = typeof t.stuckRearmUntil === 'number' ? t.stuckRearmUntil : 0
|
|
166
|
+
if (rearmUntil && nowFrame < rearmUntil) return
|
|
157
167
|
t.stuckSince = nowFrame
|
|
168
|
+
if (rearmUntil) { delete t.stuckRearmUntil; any = true } // 一次性窗口:重标成功即消耗
|
|
158
169
|
ah(t, t.status, t.status, String(rec.id), '🟡 疑似卡顿:assistant 帧静默 ' + Math.round(HUNG_FRAME_SUSPECT_MS / 1000) + 's(仅标卡,不终止)')
|
|
159
170
|
any = true
|
|
160
171
|
}
|
package/lib/policy.mjs
CHANGED
|
@@ -18,7 +18,22 @@ const { cfg, lessonText, pushLesson, ah, classifyPipeline } = core
|
|
|
18
18
|
// 关的是「主动劝你拆」,不是「不许你拆」。
|
|
19
19
|
// 第 1 条(TASK_SIZE_CONTRACT)留在 task_create 的**静态**工具描述里:工具定义会快照进 request header
|
|
20
20
|
// (dsh-session 校验 description 必须是 string),没有按板动态能力,故不在开关范围内。
|
|
21
|
-
|
|
21
|
+
// ===== 判档 / pipeline 引导文案(task-mvxpzcr9,用户 2026-11-01 两连报的提示词层根修)=====
|
|
22
|
+
// 实证病灶(都是「文案只有定义、没有场景」的产物,零机制问题):
|
|
23
|
+
// ① 「Team 模式实测大部分任务都标记为标档」——判档语义原先只写在拆解官 prompt 一处,且原文是
|
|
24
|
+
// 「standard=常规模块改动(缺省)」=引力井:主窗口 Team 注入里完全没有判档引导,agent 建卡时
|
|
25
|
+
// 没见过判档标准,拿不准自然落缺省 → 全落标档;
|
|
26
|
+
// ② 「agent 发起的任务都是 full,即便我明确说做回归」——pipeline 说明原先只有一句定义
|
|
27
|
+
// (full(默认)/work(只做不验)/direct(不进池)),从没说过「回归/巡检类归 work」,default=full 自然一律 full。
|
|
28
|
+
// 修法:同一份文案常量三处同源,消灭「(缺省)」引力井表述:
|
|
29
|
+
// ① Team 注入(dispatch.mjs,第 10/11 条;与 epicSplit 正交,关拆分引导也照样注入——档位与拆分是两件事);
|
|
30
|
+
// ② task_create 工具描述(静态契约层:TASK_SIZE_CONTRACT 扩样例 + PIPELINE_SCENE_CONTRACT);
|
|
31
|
+
// ③ 拆解官 prompt(buildDecomposerPrompt 第 3 条,与注入同口径)。
|
|
32
|
+
// 边界:纯文案,判档机制(laneModels/healthyModel/sizeHint)、pipeline 分类(classifyPipeline)、
|
|
33
|
+
// 拆解官 schema 一律不动——只让 agent 看得见「怎么选」。
|
|
34
|
+
export var SIZE_TIER_SAMPLES = 'light=单文件小改/文案/配置/断言增补/≤15 分钟(样例:改一句提示文案、加一条断言、调一个常量);standard=单模块多文件/常规特性/需新断言(样例:加一个开关、修一个判定分支);heavy=跨模块/改判定链核心/拆分类/高风险(样例:新机制、重构、动结算链)'
|
|
35
|
+
export var SIZE_PICK_QUESTIONS = '判档三问:几个文件?动不动判定核心?要不要全新机制?——拿不准按低一档报(宁小勿大:档位可在执行/验收中用 board_report 的 sizeHint 自修正,当前 run 模型不变、对下一次派发生效;低报有执行中纠错通道,高报只会白烧额度)。'
|
|
36
|
+
export var TASK_SIZE_CONTRACT = '建议粒度:单任务 10~30 分钟可独立完成。超出此范围的大任务请先拆分——建一张 epic 卡(parentId 体系)再挂子任务,别整坨塞进来。力度档位 size 三档样例:' + SIZE_TIER_SAMPLES + '。' + SIZE_PICK_QUESTIONS
|
|
22
37
|
export var SUGGEST_SPLIT_TEXT = '任务看起来偏大(建议单任务 10~30 分钟):考虑拆分子任务(parentId)或收窄边界'
|
|
23
38
|
// Team 模式提示词第 6 条:大任务的 epic 拆分流程(父卡 pipeline=direct 不派发,子任务全 resolved 后父卡由 checkParentAuto 自动转 verifying)
|
|
24
39
|
export var TEAM_SPLIT_RULE = '6. 大任务必须拆分:预计超过 30 分钟的任务,先建一张 epic 父卡(pipeline 传 direct,不派发),再拆成若干 10~30 分钟的子任务(task_create 传 parentId=父卡 id,有先后顺序的用 dependsOn 串联)。子任务全部完成后父卡会自动标记完成(checkParentAuto)。'
|
|
@@ -32,6 +47,21 @@ export var TEAM_CONTEXT_PACKING = '8. 拆解后的上下文装配:每张子卡
|
|
|
32
47
|
// ⚠️ 本条**不受 epicSplit 门禁**:它是「确认流怎么用」的流程引导,不是「主动劝你拆」——
|
|
33
48
|
// 拆解官机制本身也不看 epicSplit(task_decompose 工具常驻)。dispatch 注入段在 splitRuleOf(...) 之后恒拼接本条。
|
|
34
49
|
export var TEAM_DECOMPOSE_FLOW = '9. 需求卡工作流:用户用自然语言下达的大需求,先建成 draft 需求卡(保留需求原文),再用 task_decompose 派拆解官出实现侧子卡提案;提案以草稿子卡落位后,向用户呈现一览(标题/档位/模型/touches),用户确认或调档(task_update 改 size/model)后统一 publish。拆解官提案不是终稿——你的呈现与用户的那一眼确认是语义质量的最后防线。命中拆分树信号的需求,即使主窗口已在讨论中形成设计稿,也一律走拆解官——设计稿写进需求卡 contextNotes 作为参考方案;拆解官读它当起点,职责是验证/修正设计而非从零重来。禁止以『我已经设计好了』为由整卡直派。'
|
|
50
|
+
// Team 提示词第 10 条:力度档位判档指引(样例化口径,与 TASK_SIZE_CONTRACT / 拆解官 prompt 同源)。
|
|
51
|
+
// ⚠️ 与第 9 条同款纪律:**不受 epicSplit 门禁**——档位怎么判与「要不要拆」正交,关掉拆分引导
|
|
52
|
+
// 也不能让主窗口重新失去判档标准(那正是「全落标档」的成因)。dispatch 恒拼接。
|
|
53
|
+
export var TEAM_SIZE_GUIDE = '10. 力度档位(size)判档——按样例选档,别一律落到标档:' + SIZE_TIER_SAMPLES + '。' + SIZE_PICK_QUESTIONS
|
|
54
|
+
// pipeline 三场景(工具描述与 Team 注入共用同一份文案,口径唯一)
|
|
55
|
+
export var PIPELINE_SCENES = 'work=回归测试/巡检/文档/纯查询/纯报表(自己跑通即可,不需 Verifier 复跑验收);full=代码改动/修 bug/新特性(默认,要 Verifier 独立复跑验收);direct=主窗口亲办、不进派发池'
|
|
56
|
+
export var PIPELINE_NAMED_CASES = '显式点名:用户说做回归/巡检/盘点 → 归 work(别默认 full);纯查询/纯报表同理。'
|
|
57
|
+
// Team 提示词第 11 条:pipeline 场景指引(同上,不受 epicSplit 门禁)
|
|
58
|
+
export var TEAM_PIPELINE_GUIDE = '11. pipeline 场景——按「要不要 Verifier 复核」选,别一律 full:' + PIPELINE_SCENES + '。' + PIPELINE_NAMED_CASES
|
|
59
|
+
// Team 注入出口:第 10/11 条合成一段(dispatch 在 TEAM_DECOMPOSE_FLOW 之后恒拼接)
|
|
60
|
+
export var TEAM_SIZE_PIPELINE_GUIDE = '\n' + TEAM_SIZE_GUIDE + '\n' + TEAM_PIPELINE_GUIDE
|
|
61
|
+
// task_create 静态描述尾部:pipeline 场景指引(紧跟 TASK_SIZE_CONTRACT 拼接;工具描述会快照进 request header,
|
|
62
|
+
// 没有按板动态能力,故与粒度契约同处静态层)
|
|
63
|
+
export var PIPELINE_SCENE_CONTRACT = ' pipeline 场景:' + PIPELINE_SCENES + '。' + PIPELINE_NAMED_CASES
|
|
64
|
+
|
|
35
65
|
// Team 提示词第 6~8 条拆分 doctrine 的注入出口(每条含前导换行,调用方直接字符串相加):
|
|
36
66
|
// epicSplit 缺省/true → 第 6 条拆分流程 + 第 7 条决策树 + 第 8 条上下文装配;
|
|
37
67
|
// epicSplit === false → ''(第 6~8 条整条不出现,编号 1~5 连续不受影响)。
|
|
@@ -112,7 +142,8 @@ export var DECOMPOSE_SCHEMA = {
|
|
|
112
142
|
// 拆解官 prompt 契约(写死纪律):
|
|
113
143
|
// ① 第一步必须 read contextFiles 清单里的文件再动手拆(实现侧拆解,禁止需求侧转述);
|
|
114
144
|
// ② 每张子卡 10~30 分钟可独立验收、touches 精确到文件、acceptance 是可执行命令;
|
|
115
|
-
// ③ size
|
|
145
|
+
// ③ size 判档=样例化同源(SIZE_TIER_SAMPLES + SIZE_PICK_QUESTIONS,与 Team 注入 / task_create 描述逐字同口径;
|
|
146
|
+
// 旧文案「standard=常规模块改动(缺省)」是「全落标档」的引力井,已消灭);
|
|
116
147
|
// modelHint 仅在有特殊理由时填(如超长上下文);
|
|
117
148
|
// ④ children 上限 DECOMPOSE_MAX_CHILDREN 张;verdict=keep 表示源卡不用拆;
|
|
118
149
|
// ⑤ dependsOn 只许引用本批**前排**子卡的序号('1' 起,string)——物化侧翻译成本批真实 id,按构造天然无环;无法解析的序号丢弃并在该子卡 messages 留痕。
|
|
@@ -128,7 +159,7 @@ export function buildDecomposerPrompt(t, pack) {
|
|
|
128
159
|
p += '\n拆解纪律(硬约束):\n'
|
|
129
160
|
p += '1. 第一步必须先用 read 工具读上面文件清单里的文件(按所给行号范围),从**实现侧**拆(按模块/文件边界切分);禁止不读代码就拍脑袋拆,禁止只做需求侧转述。\n'
|
|
130
161
|
p += '2. 每张子卡必须 10~30 分钟可独立完成并验收:touches 精确到文件(文件级排他锁,同批子卡 touches 不得互相重叠),acceptance 必须是可执行命令(如 "node --test test/xxx.test.mjs")。\n'
|
|
131
|
-
p += '3. size
|
|
162
|
+
p += '3. size 判档(按样例落档,别一律标档;与主窗口 Team 注入同口径):' + SIZE_TIER_SAMPLES + '。' + SIZE_PICK_QUESTIONS + 'modelHint 仅在有特殊理由时填(如某子卡需超长上下文模型),否则留空。\n'
|
|
132
163
|
p += '4. children 上限 ' + DECOMPOSE_MAX_CHILDREN + ' 张;有先后顺序时用 dependsOn 引用同批序号(只许引用本批**前排**子卡,序号 1 起计:如第 2 张依赖第 1 张填 ["1"])。\n'
|
|
133
164
|
p += '5. 源卡本身不大、不必拆时:verdict=keep,并在 rationale 写清理由。\n'
|
|
134
165
|
p += '\n权限边界:你没有任何建卡工具——不要尝试创建/修改看板任务。提案由宿主读结构化结果后自动物化为 draft 子卡(源卡状态不变,由主窗口确认后逐张 publish)。\n'
|
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/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.3",
|
|
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
|
}
|
package/test/core.test.mjs
CHANGED
|
@@ -15,6 +15,9 @@ import * as core from '../lib/core.mjs'
|
|
|
15
15
|
import { suggestSplitOf, withSplitHint, SUGGEST_SPLIT_TEXT, TASK_SIZE_CONTRACT, TEAM_SPLIT_RULE, TEAM_DECISION_TREE, TEAM_CONTEXT_PACKING, TEAM_DECOMPOSE_FLOW, splitRuleOf } from '../index.mjs'
|
|
16
16
|
import { aggregateUsageSummary, readRunUsage, findRunLog, effectiveTokens, taskEffectiveTokens } from '../index.mjs'
|
|
17
17
|
import { createRpc } from '../lib/rpc.mjs'
|
|
18
|
+
// 拆解官 prompt 契约 + 判档/pipeline 文案常量(task-mvxpzcr9):直接读 policy 层(契约同源头;本卡 touches
|
|
19
|
+
// 不含 index.mjs 薄壳,故新常量不新增 re-export,测试按 policy 层直引——与 decompose.test 同款导入面)
|
|
20
|
+
import { buildDecomposerPrompt, TEAM_SIZE_GUIDE, TEAM_PIPELINE_GUIDE, TEAM_SIZE_PIPELINE_GUIDE, SIZE_TIER_SAMPLES, SIZE_PICK_QUESTIONS, PIPELINE_SCENE_CONTRACT } from '../lib/policy.mjs'
|
|
18
21
|
// no-root 刷屏根治(task-muuf0o7a)专项:会话层幻影板防线 + 派发层 root 闸门直调断言
|
|
19
22
|
import { createSession, isFullSessionId } from '../lib/session.mjs'
|
|
20
23
|
import { createDispatch } from '../lib/dispatch.mjs'
|
|
@@ -159,6 +162,83 @@ test('史诗收口⑥:checkParentAuto 兼容名委托共享 helper(单一判
|
|
|
159
162
|
assert.equal(p.resolution, 'all subtasks resolved') // 口径文案统一(旧 resolveApply 分支的 'all subtasks done' 已退役)
|
|
160
163
|
})
|
|
161
164
|
|
|
165
|
+
// ===== direct 父卡收口分态(task-mvxmxl00,反馈 n-mvxmue8zggw0,2026-11-01 本体板实证)=====
|
|
166
|
+
// 设计矛盾:旧 checkParentAuto 一律把收口的父卡推进 verifying,而 pickDispatch 的 verifier 候选过滤是
|
|
167
|
+
// `(!t.pipeline || t.pipeline === 'full')`——direct 父卡被推进 verifying 后永远不进 verifs,
|
|
168
|
+
// verifier 永不 spawn,卡尸停在「验证中」(实证:task-mvwk48c2/48rr/49fw 三张 direct 史诗,只能人工收口)。
|
|
169
|
+
// 修法(方案乙):按父卡 pipeline 分态收口——direct 直接 resolved(direct「不进派发池」对 verifier
|
|
170
|
+
// 同样成立),full/work 维持推进 verifying;派发侧过滤闸一字不动(单一口径留在派发侧)。
|
|
171
|
+
test('史诗收口⑪:direct 父卡子任务全了结 → 直接 resolved,不进 verifying(无 verifier 记录)', () => {
|
|
172
|
+
const p = mkTask({ id: 'epic', status: 'in-progress', pipeline: 'direct' })
|
|
173
|
+
const c = mkTask({ id: 'c1', parentId: 'epic', status: 'in-progress', pipeline: 'work' })
|
|
174
|
+
const d = mkBoard([p, c])
|
|
175
|
+
const r = core.resolveApply(d, c, 'w', 'verifying', 'done')
|
|
176
|
+
assert.equal(c.status, 'resolved')
|
|
177
|
+
assert.equal(r.parentUpdated, true)
|
|
178
|
+
assert.equal(p.status, 'resolved') // 旧实现此处为 verifying → verifier 永不 spawn 的卡死态
|
|
179
|
+
assert.equal(p.resolution, 'all subtasks resolved')
|
|
180
|
+
assert.ok(p.resolvedAt)
|
|
181
|
+
assert.equal((p.runs || []).some((x) => x && x.role === 'verifier'), false) // 父卡不走 verifier run
|
|
182
|
+
assert.equal(p.verifiedBy, undefined) // 无验收戳:形式收口(子任务各自已验收)
|
|
183
|
+
assert.deepEqual(core.pickDispatch(d, 1, 4, {}).verifs, []) // 派发侧过滤闸未动:direct 恒不进 verifier 候选
|
|
184
|
+
// verifyApply 路径同样分态(子任务 full 档走完验收 → direct 父卡直接收口)
|
|
185
|
+
const p2 = mkTask({ id: 'epic2', status: 'in-progress', pipeline: 'direct' })
|
|
186
|
+
const c2 = mkTask({ id: 'c2', parentId: 'epic2', status: 'verifying', pipeline: 'full' })
|
|
187
|
+
const d2 = mkBoard([p2, c2])
|
|
188
|
+
assert.equal(core.verifyApply(d2, c2, 'v', 'approved', 'ok').parentUpdated, true)
|
|
189
|
+
assert.equal(p2.status, 'resolved')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('史诗收口⑫:full 父卡 → 仍推进 verifying 并进 verifier 候选(回归锁:照派 verifier)', () => {
|
|
193
|
+
const p = mkTask({ id: 'epic', status: 'in-progress', pipeline: 'full' })
|
|
194
|
+
const c = mkTask({ id: 'c1', parentId: 'epic', status: 'verifying', pipeline: 'full' })
|
|
195
|
+
const d = mkBoard([p, c])
|
|
196
|
+
const r = core.verifyApply(d, c, 'v', 'approved', 'ok')
|
|
197
|
+
assert.equal(r.parentUpdated, true)
|
|
198
|
+
assert.equal(p.status, 'verifying') // 与修改前逐字一致
|
|
199
|
+
assert.deepEqual(core.pickDispatch(d, 1, 4, {}).verifs.map((t) => t.id), ['epic']) // 照派 Verifier
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('史诗收口⑬:work 父卡与 full 同态 → 推进 verifying(分态只切 direct,work 零变化)', () => {
|
|
203
|
+
const p = mkTask({ id: 'epic', status: 'in-progress', pipeline: 'work' })
|
|
204
|
+
const c = mkTask({ id: 'c1', parentId: 'epic', status: 'in-progress', pipeline: 'work' })
|
|
205
|
+
const d = mkBoard([p, c])
|
|
206
|
+
core.resolveApply(d, c, 'w', 'verifying', 'done')
|
|
207
|
+
assert.equal(p.status, 'verifying') // 不被 direct 直收口分支吃掉
|
|
208
|
+
assert.equal(p.resolution, 'all subtasks resolved')
|
|
209
|
+
assert.equal(core.pickDispatch(d, 1, 4, {}).verifs.length, 0) // verifier 过滤闸保持现状(work 不含在内,本卡不改)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('史诗收口⑭:流转文案分态——direct 带「direct 父卡直接收口」标注 / full·work 原文案逐字不变', () => {
|
|
213
|
+
const pd = mkTask({ id: 'epic', status: 'in-progress', pipeline: 'direct' })
|
|
214
|
+
const cd = mkTask({ id: 'c1', parentId: 'epic', status: 'resolved' })
|
|
215
|
+
core.checkParentAuto(mkBoard([pd, cd]), cd)
|
|
216
|
+
const hd = pd.history[pd.history.length - 1]
|
|
217
|
+
assert.equal(hd.from, 'in-progress')
|
|
218
|
+
assert.equal(hd.to, 'resolved')
|
|
219
|
+
assert.equal(hd.actor, 'system')
|
|
220
|
+
assert.equal(hd.note, 'auto: all subtasks resolved(direct 父卡直接收口)')
|
|
221
|
+
const pf = mkTask({ id: 'epic', status: 'in-progress', pipeline: 'full' })
|
|
222
|
+
const cf = mkTask({ id: 'c1', parentId: 'epic', status: 'resolved' })
|
|
223
|
+
core.checkParentAuto(mkBoard([pf, cf]), cf)
|
|
224
|
+
const hf = pf.history[pf.history.length - 1]
|
|
225
|
+
assert.equal(hf.to, 'verifying')
|
|
226
|
+
assert.equal(hf.note, 'auto: all subtasks resolved') // full 文案不变
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
test('README 双份同步记录 epic 收口分态契约(direct 直接 resolved / full·work 转 verifying)', () => {
|
|
230
|
+
const pkg = readFileSync(new URL('../README.md', import.meta.url), 'utf8')
|
|
231
|
+
const root = readFileSync(new URL('../../../README.md', import.meta.url), 'utf8')
|
|
232
|
+
assert.equal(pkg, root) // 两份 README 必须字节一致(npm run sync-readme 的约束)
|
|
233
|
+
for (const s of [
|
|
234
|
+
'按父卡 `pipeline` 分态收口',
|
|
235
|
+
'`full`/`work` 父卡转 verifying 派 Verifier 验收',
|
|
236
|
+
'`direct` 父卡直接 resolved',
|
|
237
|
+
'`direct` 不进派发池对 verifier 同成立',
|
|
238
|
+
]) assert.ok(pkg.includes(s), 'README 应记录 epic 收口分态口径:' + s)
|
|
239
|
+
assert.ok(!pkg.includes('子任务全了结转 verifying 后派 Verifier 验收不受影响'), 'README 不应再保留「一律转 verifying」旧口径')
|
|
240
|
+
})
|
|
241
|
+
|
|
162
242
|
// ===== 史诗自动收口漏触发根修(task-mux3uvx3,反馈 n-muw706h1uymy,2026-10-06 实证)=====
|
|
163
243
|
// 实证根因(repro 脚本逐条复现,嫌疑点逐个排除):
|
|
164
244
|
// ① isChildSettled 旧口径不含 archived——「resolve 一张归档一张」的交错序列下,末子卡 resolve
|
|
@@ -3534,6 +3614,94 @@ test('hooks 只许主窗口设置:create-task/update-task RPC 与 task_create/
|
|
|
3534
3614
|
assert.equal(b4.tasks[0].hooks.pre.state, 'idle') // 状态机不被越权写入扰动
|
|
3535
3615
|
})
|
|
3536
3616
|
|
|
3617
|
+
// ===== touches 更新通道(task-mvxw7hn5):主窗口/GUI 替换式更新锁声明,Worker 禁 + 生命周期门禁 =====
|
|
3618
|
+
// 断言族(与卡上口径同步·主窗口 2026-11-01 生命周期修正版):
|
|
3619
|
+
// ① 工具 schema 含 touches 参数(顶层,与 hooks 同级)
|
|
3620
|
+
// ② draft 卡主窗口可改:归一化(非字符串过滤+trim,与 create-task normTouches 同款)+ history 留痕含原值摘要
|
|
3621
|
+
// ③ Worker actor 双通道拒(RPC update-task / task_update 工具,文案「touches 更新仅主窗口可用」)
|
|
3622
|
+
// ④ 运行中(in-progress/verifying)拒写——主窗口也不可直改,边界由 Worker 以实际覆盖面申报
|
|
3623
|
+
// (board_report escalate → task_arbitrate expandTouches);拒写不改卡
|
|
3624
|
+
// ⑤ 落定后(resolved/cancelled)可写(锁已释放,修正权回主窗口)
|
|
3625
|
+
// ⑥ 空数组=清除声明(t.touches=[] 且 effectiveTouches 回 [])
|
|
3626
|
+
// ⑦ 上限 20 条(与 create-task 同款)⑧ 工具通道直改 pending 成功 ⑨ GUI TouchesEditor 走 update-task
|
|
3627
|
+
test('task-mvxw7hn5 touches 更新通道:schema/角色门禁/生命周期门禁/归一化留痕/GUI 编辑', async () => {
|
|
3628
|
+
const child = { resolveRoot: () => 'root-other' } // 子代理身份(resolveRoot 指向别处)
|
|
3629
|
+
const h = mkRpcHandlers(mkBoard([]))
|
|
3630
|
+
// ① task_update 工具 schema 含 touches 参数(顶层属性)
|
|
3631
|
+
const tool = h.__tools['task_update']
|
|
3632
|
+
assert.ok(tool, 'task_update 工具已注册')
|
|
3633
|
+
assert.ok(tool.parameters.properties.touches, 'task_update schema 含 touches 参数')
|
|
3634
|
+
assert.equal(tool.parameters.properties.touches.type, 'array')
|
|
3635
|
+
assert.equal(tool.parameters.properties.touches.items.type, 'string')
|
|
3636
|
+
assert.match(tool.description, /touches 可替换式更新/)
|
|
3637
|
+
assert.match(tool.parameters.properties.touches.description, /仅主窗口/)
|
|
3638
|
+
assert.match(tool.parameters.properties.touches.description, /in-progress\/verifying(运行中持锁)拒写/)
|
|
3639
|
+
// ② draft 卡主窗口可改:非字符串过滤 + trim + history 留痕(原值进摘要)
|
|
3640
|
+
const b2 = mkBoard([mkTask({ id: 'd1', status: 'draft', touches: ['old/x.mjs'] })])
|
|
3641
|
+
const r2 = await mkRpcHandlers(b2)['update-task']({ taskId: 'd1', touches: ['lib/a.mjs ', 42, '', 'lib/b.mjs'] })
|
|
3642
|
+
assert.equal(r2.ok, true)
|
|
3643
|
+
assert.deepEqual(b2.tasks[0].touches, ['lib/a.mjs', 'lib/b.mjs'])
|
|
3644
|
+
const note2 = b2.tasks[0].history[b2.tasks[0].history.length - 1].note
|
|
3645
|
+
assert.match(note2, /touches 更新:1 项 → 2 项(原:old\/x\.mjs)/)
|
|
3646
|
+
// ③ Worker actor 双通道拒:角色门禁文案,且不改卡
|
|
3647
|
+
const b3 = mkBoard([mkTask({ id: 'd2', status: 'draft', touches: [] })])
|
|
3648
|
+
const r3 = await mkRpcHandlers(b3, child)['update-task']({ taskId: 'd2', touches: ['x.mjs'] })
|
|
3649
|
+
assert.equal(r3.ok, false); assert.match(r3.error, /touches 更新仅主窗口可用/)
|
|
3650
|
+
assert.deepEqual(b3.tasks[0].touches, [])
|
|
3651
|
+
const r3b = await mkRpcHandlers(b3, child).__tools['task_update'].execute({ taskId: 'd2', touches: ['x.mjs'] })
|
|
3652
|
+
assert.equal(r3b.ok, false); assert.match(r3b.error, /touches 更新仅主窗口可用/)
|
|
3653
|
+
assert.deepEqual(b3.tasks[0].touches, [])
|
|
3654
|
+
// ④ 运行中拒写(生命周期门禁):in-progress(持锁)与 verifying——主窗口也不可直改;拒写不改卡
|
|
3655
|
+
const b4 = mkBoard([
|
|
3656
|
+
mkTask({ id: 'w1', status: 'in-progress', claimedBy: 'agent-1', claimedAt: '2026-01-01T00:00:00Z', touches: ['keep/x.mjs'] }),
|
|
3657
|
+
mkTask({ id: 'v1', status: 'verifying', touches: ['keep/y.mjs'] }),
|
|
3658
|
+
])
|
|
3659
|
+
const r4 = await mkRpcHandlers(b4)['update-task']({ taskId: 'w1', touches: ['other/z.mjs'] })
|
|
3660
|
+
assert.equal(r4.ok, false)
|
|
3661
|
+
assert.match(r4.error, /运行中的任务 touches 由 Worker 以实际覆盖面申报——请 escalate 或由主窗口仲裁扩权;待任务落定后再修正/)
|
|
3662
|
+
const r4b = await mkRpcHandlers(b4)['update-task']({ taskId: 'v1', touches: [] })
|
|
3663
|
+
assert.equal(r4b.ok, false); assert.match(r4b.error, /待任务落定后再修正/)
|
|
3664
|
+
assert.deepEqual(b4.tasks[0].touches, ['keep/x.mjs'])
|
|
3665
|
+
assert.deepEqual(b4.tasks[1].touches, ['keep/y.mjs'])
|
|
3666
|
+
const r4c = await mkRpcHandlers(mkBoard([mkTask({ id: 'w2', status: 'in-progress', claimedBy: 'agent-2', touches: ['a.mjs'] })])).__tools['task_update'].execute({ taskId: 'w2', touches: ['b.mjs'] })
|
|
3667
|
+
assert.equal(r4c.ok, false); assert.match(r4c.error, /运行中的任务 touches/)
|
|
3668
|
+
// ⑤ 落定后可写:resolved / cancelled(锁已释放,修正权回主窗口)
|
|
3669
|
+
const b5 = mkBoard([
|
|
3670
|
+
mkTask({ id: 'rs', status: 'resolved', touches: ['old.mjs'] }),
|
|
3671
|
+
mkTask({ id: 'cc', status: 'cancelled', touches: [] }),
|
|
3672
|
+
])
|
|
3673
|
+
const r5 = await mkRpcHandlers(b5)['update-task']({ taskId: 'rs', touches: ['new.mjs'] })
|
|
3674
|
+
assert.equal(r5.ok, true); assert.deepEqual(b5.tasks[0].touches, ['new.mjs'])
|
|
3675
|
+
const r5b = await mkRpcHandlers(b5)['update-task']({ taskId: 'cc', touches: ['c.mjs'] })
|
|
3676
|
+
assert.equal(r5b.ok, true); assert.deepEqual(b5.tasks[1].touches, ['c.mjs'])
|
|
3677
|
+
// ⑥ 空数组=清除声明:t.touches=[] 且 effectiveTouches 回 [](无锁态)
|
|
3678
|
+
const b6 = mkBoard([mkTask({ id: 'd3', status: 'draft', touches: ['x.mjs'] })])
|
|
3679
|
+
const r6 = await mkRpcHandlers(b6)['update-task']({ taskId: 'd3', touches: [] })
|
|
3680
|
+
assert.equal(r6.ok, true); assert.deepEqual(b6.tasks[0].touches, [])
|
|
3681
|
+
assert.deepEqual(core.effectiveTouches(b6.tasks[0]), [])
|
|
3682
|
+
assert.match(b6.tasks[0].history[b6.tasks[0].history.length - 1].note, /touches 更新:1 项 → 0 项(原:x\.mjs)/)
|
|
3683
|
+
// ⑦ 上限 20 条(与 create-task normTouches 同款:slice(0, 20))
|
|
3684
|
+
const many = []; for (let i = 0; i < 25; i++) many.push('f' + i + '.mjs')
|
|
3685
|
+
const b7 = mkBoard([mkTask({ id: 'd4', status: 'draft' })])
|
|
3686
|
+
const r7 = await mkRpcHandlers(b7)['update-task']({ taskId: 'd4', touches: many })
|
|
3687
|
+
assert.equal(r7.ok, true); assert.equal(b7.tasks[0].touches.length, 20)
|
|
3688
|
+
// ⑧ 工具通道直改 pending 成功(主窗口 tester 身份;留痕「0 项 → 1 项(原:无)」)
|
|
3689
|
+
const b8 = mkBoard([mkTask({ id: 'd5', status: 'pending', touches: [] })])
|
|
3690
|
+
const r8 = await mkRpcHandlers(b8).__tools['task_update'].execute({ taskId: 'd5', touches: ['t.mjs'] })
|
|
3691
|
+
assert.equal(r8.ok, true); assert.deepEqual(b8.tasks[0].touches, ['t.mjs'])
|
|
3692
|
+
assert.match(b8.tasks[0].history[b8.tasks[0].history.length - 1].note, /touches 更新:0 项 → 1 项(原:无)/)
|
|
3693
|
+
// ⑨ GUI TouchesEditor:源码 + 组装产物都走 update-task touches 通道;运行态只读指引在
|
|
3694
|
+
const src = readFileSync(new URL('../lib/client/task-detail.js', import.meta.url), 'utf8')
|
|
3695
|
+
assert.match(src, /function TouchesEditor\(props\)/)
|
|
3696
|
+
assert.match(src, /rpc\('update-task', \{ taskId: task\.id, touches: touches \}\)/)
|
|
3697
|
+
assert.match(src, /running = task\.status === 'in-progress' \|\| task\.status === 'verifying'/)
|
|
3698
|
+
assert.match(src, /运行中任务的 touches 由 Worker 以实际覆盖面申报(escalate→仲裁扩权);落定后可在此修正/)
|
|
3699
|
+
const built = readFileSync(new URL('../lib/client.js', import.meta.url), 'utf8')
|
|
3700
|
+
assert.match(built, /function TouchesEditor\(props\)/)
|
|
3701
|
+
assert.match(built, /rpc\('update-task', \{ taskId: task\.id, touches: touches \}\)/)
|
|
3702
|
+
assert.match(built, /运行中任务的 touches 由 Worker 以实际覆盖面申报/)
|
|
3703
|
+
})
|
|
3704
|
+
|
|
3537
3705
|
// ===== hooks 接线(host 侧):pre 闸门 spawn / post 补 spawn / hook run 结算分派(直调 + 源码级)=====
|
|
3538
3706
|
// poolCycle 直调 harness(带 subagents):spawnOneShot 走真实路径,捕获 provider/label/prompt;
|
|
3539
3707
|
// run.result 用永不落定的 Promise(本用例只验证「谁被 spawn 了、prompt 对不对」,不触发结算)。
|
|
@@ -4253,8 +4421,8 @@ test('hooks 主窗口限定(源码级):RPC 与工具双通道门禁 + 浅
|
|
|
4253
4421
|
// create-task RPC:锁外角色门禁 + 锁内形状校验(与建卡同处一次持锁段)
|
|
4254
4422
|
assert.match(rpc, /if \(args\.hooks !== undefined && hooksDenied\(actor\)\) return \{ ok: false, error: HOOKS_MAIN_ONLY \}/)
|
|
4255
4423
|
assert.match(rpc, /var nHooks = null; if \(args\.hooks !== undefined\) \{ var hn = normalizeHooks\(args\.hooks\)/)
|
|
4256
|
-
// update-task RPC:同一门禁 +
|
|
4257
|
-
assert.match(rpc, /if \(args\.hooks !== undefined && hooksDenied\(actor\)\) return \{ ok: false, error: HOOKS_MAIN_ONLY \}; var existsFn = existsInSession\(sid\)/)
|
|
4424
|
+
// update-task RPC:同一门禁 + 浅合并;touches 门禁(task-mvxw7hn5)随其后插在同一锁外位置,同族角色门禁
|
|
4425
|
+
assert.match(rpc, /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\)/)
|
|
4258
4426
|
assert.match(rpc, /if \(args\.hooks !== undefined\) \{ var __he = applyHooks\(t, args\.hooks\)/)
|
|
4259
4427
|
// 工具通道:两个工具的 hooks 专属门禁(在通用门禁之后,子代理拿到更准确的错误文案)
|
|
4260
4428
|
assert.equal((rpc.match(/if \(args\.hooks !== undefined && resolveRoot\(__ra\) !== __ra\) return \{ ok: false, error: 'hooks 仅主窗口可设(子代理无 hooks 权限)' \}/g) || []).length, 2)
|
|
@@ -5398,6 +5566,13 @@ test('epicSplit 行为①:Team 提示词第 6~8 条随门禁注入/消失(
|
|
|
5398
5566
|
// 第 9 条需求卡工作流(task-mvneoy1u):随注入段出现,且注入位置在 splitRuleOf 出口内容(第 8 条)之后
|
|
5399
5567
|
assert.ok(on.includes(TEAM_DECOMPOSE_FLOW)); assert.match(on, /需求卡工作流/)
|
|
5400
5568
|
assert.ok(on.indexOf(TEAM_DECOMPOSE_FLOW) > on.indexOf(TEAM_CONTEXT_PACKING), '第 9 条注入位置在 splitRuleOf 之后')
|
|
5569
|
+
// 第 10/11 条(判档指引 + pipeline 场景,task-mvxpzcr9):恒拼接在第 9 条之后(同样不受 epicSplit 门禁)
|
|
5570
|
+
assert.ok(on.includes(TEAM_SIZE_PIPELINE_GUIDE), 'Team 注入含判档/pipeline 引导段')
|
|
5571
|
+
assert.ok(on.indexOf(TEAM_SIZE_GUIDE) > on.indexOf(TEAM_DECOMPOSE_FLOW), '判档段接在第 9 条之后')
|
|
5572
|
+
assert.match(on, /判档三问:几个文件?动不动判定核心?要不要全新机制?/)
|
|
5573
|
+
assert.match(on, /拿不准按低一档报/)
|
|
5574
|
+
assert.match(on, /work=回归测试\/巡检\/文档\/纯查询\/纯报表/)
|
|
5575
|
+
assert.match(on, /用户说做回归\/巡检\/盘点 → 归 work/)
|
|
5401
5576
|
const off = textOf(false)
|
|
5402
5577
|
// 第 6~8 条整条消失(连 parentId/checkParentAuto 都不再出现),基础 1~5 条引导一字不少
|
|
5403
5578
|
assert.doesNotMatch(off, /大任务必须拆分|拆解决策树|上下文装配/)
|
|
@@ -5406,9 +5581,72 @@ test('epicSplit 行为①:Team 提示词第 6~8 条随门禁注入/消失(
|
|
|
5406
5581
|
// 第 9 条**不受 epicSplit 门禁**(流程引导不是劝拆):6~8 条整条消失时它仍在
|
|
5407
5582
|
assert.ok(off.includes(TEAM_DECOMPOSE_FLOW), 'epicSplit=false 时第 9 条仍注入')
|
|
5408
5583
|
assert.match(off, /确认或调档(task_update 改 size\/model)后统一 publish/)
|
|
5584
|
+
// 档位/pipeline 与拆分正交:关掉拆分引导时第 10/11 条照样注入(否则「全落标档」盲区原样复发)
|
|
5585
|
+
assert.ok(off.includes(TEAM_SIZE_PIPELINE_GUIDE), 'epicSplit=false 时判档/pipeline 段仍注入')
|
|
5586
|
+
assert.match(off, /按样例选档,别一律落到标档/)
|
|
5587
|
+
assert.match(off, /拿不准按低一档报/)
|
|
5588
|
+
assert.match(off, /用户说做回归\/巡检\/盘点 → 归 work/)
|
|
5409
5589
|
assert.equal(on.replace(splitRuleOf(true), ''), off.replace(splitRuleOf(false), '')) // 除第 6~8 条外逐字相同
|
|
5410
5590
|
})
|
|
5411
5591
|
|
|
5592
|
+
// ===== Team 提示词判档/pipeline 指引(task-mvxpzcr9):三档具象样例 + 判档三问 + pipeline 场景 =====
|
|
5593
|
+
// 病灶(用户 2026-11-01 两连报):① 判档文案原只写在拆解官一处、且原文「standard=常规模块改动(缺省)」
|
|
5594
|
+
// = 引力井 → 主窗口建卡全落标档;② pipeline 只有定义没场景(「回归/巡检类归 work」从未写过)→ 一律 full。
|
|
5595
|
+
// 本族断言把「样例化文案 + 三处同源(Team 注入 / task_create 静态描述 / 拆解官 prompt)」钉死。
|
|
5596
|
+
test('判档/pipeline 指引文案:三档具象样例 + 判档三问 + 拿不准报低档 + pipeline 三场景(回归点名)', () => {
|
|
5597
|
+
const g = TEAM_SIZE_PIPELINE_GUIDE
|
|
5598
|
+
// 三档样例逐字(每档 = 判据 + 具象例子),且与静态契约/SIZE_TIER_SAMPLES 同源
|
|
5599
|
+
for (const s of [
|
|
5600
|
+
'light=单文件小改/文案/配置/断言增补/≤15 分钟(样例:改一句提示文案、加一条断言、调一个常量)',
|
|
5601
|
+
'standard=单模块多文件/常规特性/需新断言(样例:加一个开关、修一个判定分支)',
|
|
5602
|
+
'heavy=跨模块/改判定链核心/拆分类/高风险(样例:新机制、重构、动结算链)',
|
|
5603
|
+
]) {
|
|
5604
|
+
assert.ok(g.includes(s), '判档样例缺:' + s)
|
|
5605
|
+
assert.ok(SIZE_TIER_SAMPLES.includes(s), '样例常量应逐字同源:' + s)
|
|
5606
|
+
assert.ok(TASK_SIZE_CONTRACT.includes(s), 'task_create 静态契约应同款样例:' + s)
|
|
5607
|
+
}
|
|
5608
|
+
// 判档三问 + 拿不准报低档(带 sizeHint 自修正出口,别把「低报」说成死路)
|
|
5609
|
+
assert.match(TEAM_SIZE_GUIDE, /判档三问:几个文件?动不动判定核心?要不要全新机制?/)
|
|
5610
|
+
assert.match(TEAM_SIZE_GUIDE, /拿不准按低一档报/)
|
|
5611
|
+
assert.match(TEAM_SIZE_GUIDE, /sizeHint/)
|
|
5612
|
+
assert.ok(SIZE_PICK_QUESTIONS.length > 0 && TEAM_SIZE_GUIDE.includes(SIZE_PICK_QUESTIONS))
|
|
5613
|
+
// 编号接第 9 条(6~9 已在用);旧「(缺省)」引力井措辞必须清零
|
|
5614
|
+
assert.match(TEAM_SIZE_GUIDE, /^10\. /)
|
|
5615
|
+
assert.doesNotMatch(TEAM_SIZE_GUIDE, /(缺省)/)
|
|
5616
|
+
assert.doesNotMatch(TASK_SIZE_CONTRACT, /(缺省)/)
|
|
5617
|
+
// pipeline 三场景 + 回归/巡检显式点名(work 不再靠猜)
|
|
5618
|
+
assert.match(TEAM_PIPELINE_GUIDE, /^11\. /)
|
|
5619
|
+
for (const s of ['work=回归测试/巡检/文档/纯查询/纯报表', 'full=代码改动/修 bug/新特性', 'direct=主窗口亲办、不进派发池']) {
|
|
5620
|
+
assert.ok(TEAM_PIPELINE_GUIDE.includes(s), 'pipeline 场景缺:' + s)
|
|
5621
|
+
assert.ok(PIPELINE_SCENE_CONTRACT.includes(s), 'task_create 静态契约应同款 pipeline 场景:' + s)
|
|
5622
|
+
}
|
|
5623
|
+
assert.match(TEAM_PIPELINE_GUIDE, /用户说做回归\/巡检\/盘点 → 归 work/)
|
|
5624
|
+
assert.match(PIPELINE_SCENE_CONTRACT, /用户说做回归\/巡检\/盘点 → 归 work/)
|
|
5625
|
+
// 注入出口 = 两条合成(每条含前导换行,调用方直接字符串相加)
|
|
5626
|
+
assert.equal(TEAM_SIZE_PIPELINE_GUIDE, '\n' + TEAM_SIZE_GUIDE + '\n' + TEAM_PIPELINE_GUIDE)
|
|
5627
|
+
})
|
|
5628
|
+
|
|
5629
|
+
test('判档/pipeline 同源落三处:Team 注入位 + task_create 工具描述 + 拆解官 prompt(静态契约层)', () => {
|
|
5630
|
+
const src = hostSrc()
|
|
5631
|
+
// dispatch 注入:接在第 9 条之后恒拼接(不进 epicSplit 门禁出口——档位与拆分正交)
|
|
5632
|
+
assert.ok(src.includes("+ '\\n' + TEAM_DECOMPOSE_FLOW + '\\n' + TEAM_SIZE_PIPELINE_GUIDE"))
|
|
5633
|
+
assert.ok(!splitRuleOf(true).includes(TEAM_SIZE_PIPELINE_GUIDE), '判档段不进 epicSplit 门禁出口')
|
|
5634
|
+
// task_create 工具描述(真注册出来的 description,不是源码拼串)
|
|
5635
|
+
const tc = mkRpcHandlers(mkBoard([])).__tools['task_create']
|
|
5636
|
+
assert.ok(tc.description.includes(SIZE_TIER_SAMPLES), 'task_create 描述含三档样例')
|
|
5637
|
+
assert.ok(tc.description.includes(PIPELINE_SCENE_CONTRACT), 'task_create 描述含 pipeline 场景指引')
|
|
5638
|
+
assert.match(tc.description, /回归\/巡检\/盘点 → 归 work/)
|
|
5639
|
+
// 参数级描述同步(模型按参数描述选档/pipeline 的那一面)
|
|
5640
|
+
assert.ok(tc.parameters.properties.size.description.includes('加一个开关'), 'size 参数描述同款具象样例')
|
|
5641
|
+
assert.match(tc.parameters.properties.pipeline.description, /work=回归测试\/巡检/)
|
|
5642
|
+
// 拆解官 prompt 第 3 条:与注入同口径(样例 + 三问),旧「(缺省)」引力井消失
|
|
5643
|
+
const p = buildDecomposerPrompt(mkTask({ id: 'pc1', title: 'P' }), '')
|
|
5644
|
+
for (const s of ['加一个开关', '修一个判定分支', '新机制', '动结算链', '判档三问', '拿不准按低一档报']) {
|
|
5645
|
+
assert.ok(p.includes(s), '拆解官判档文案缺:' + s)
|
|
5646
|
+
}
|
|
5647
|
+
assert.ok(!p.includes('常规模块改动(缺省)'), '拆解官旧「缺省」引力井必须消灭')
|
|
5648
|
+
})
|
|
5649
|
+
|
|
5412
5650
|
test('withSplitHint: epicSplit=false 不附 suggestSplit(与未命中同形),缺省/true 零变化', () => {
|
|
5413
5651
|
const big = mkTask({ title: '全量重写派发引擎' })
|
|
5414
5652
|
const off = withSplitHint({ ok: true, task: {} }, big, false)
|
|
@@ -628,3 +628,200 @@ test('帧⑦:verifier 在跑 run 帧路径覆盖——零帧 ≥600s 判死走
|
|
|
628
628
|
await env.cleanup()
|
|
629
629
|
}
|
|
630
630
|
})
|
|
631
|
+
|
|
632
|
+
// ===== 帧⑧:「继续观察」(dismiss-suspect)重置判定基线 + 120s 防抖窗(task-mvxmxkq1)=====
|
|
633
|
+
// 旧病(用户 2026-11-01 实测):dismiss 只清 t.stuckSince,帧静默基线 frameSilenceSince 未动——模型仍静默
|
|
634
|
+
// (≥120s)时下一 sweep 立刻重标,「继续观察」等于没点。新语义两层:①基线置 now(需再静默满 120s 才可能
|
|
635
|
+
// 重标,「观察一轮」本义)②卡上 stuckRearmUntil 防抖窗(兜 sweep 在飞读到旧基线的竞态边缘),一次性
|
|
636
|
+
// (重标成功/帧恢复清除)。本组锁:窗内不重标、窗过期正常重标、字段一次性。
|
|
637
|
+
function dismissSuspect(env, taskId) {
|
|
638
|
+
return env.rpc('dismiss-suspect', { sessionId: env.sid, taskId: taskId })
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
test('帧⑧:「继续观察」重置帧静默基线 + 120s 防抖窗——窗内不重标 / 窗过期重标(字段一次性)', async () => {
|
|
642
|
+
var env = createMockCtx()
|
|
643
|
+
var boot = null
|
|
644
|
+
try {
|
|
645
|
+
boot = await bootWorkerBoard(env, 'task-frame-dismiss')
|
|
646
|
+
var child = boot.child
|
|
647
|
+
await primeFrameBaseline(env, 'task-frame-dismiss', child.childId)
|
|
648
|
+
await pushBackBaseline(env, 'task-frame-dismiss', child.childId, 'frameSilenceSince', 125000)
|
|
649
|
+
await env.clock.tick()
|
|
650
|
+
await env.waitFor(function () {
|
|
651
|
+
var x = env.task('task-frame-dismiss')
|
|
652
|
+
return x && typeof x.stuckSince === 'number' ? x : null
|
|
653
|
+
}, '疑似落卡(前置状态)', W)
|
|
654
|
+
// ---- dismiss:清标记 + 基线重置 + 防抖窗落卡 ----
|
|
655
|
+
var tBefore = Date.now()
|
|
656
|
+
var dr = await dismissSuspect(env, 'task-frame-dismiss')
|
|
657
|
+
assert.equal(dr.ok, true, 'dismiss-suspect 成功')
|
|
658
|
+
var t1 = env.task('task-frame-dismiss')
|
|
659
|
+
assert.equal(t1.stuckSince, undefined, 'dismiss 清除疑似标记')
|
|
660
|
+
assert.ok(typeof t1.stuckRearmUntil === 'number' && t1.stuckRearmUntil >= tBefore + 120000, 'stuckRearmUntil 落卡(now + 120s)')
|
|
661
|
+
var row1 = runningRow(env, 'task-frame-dismiss', child.childId)()
|
|
662
|
+
assert.ok(row1 && row1.frameSilenceSince >= tBefore, '帧静默基线重置为 now(不再按旧基线秒重标)')
|
|
663
|
+
var notes1 = (t1.history || []).filter(function (h) { return /继续观察/.test(h.note || '') })
|
|
664
|
+
assert.equal(notes1.length, 1, 'dismiss 留史一条')
|
|
665
|
+
assert.match(notes1[0].note, /基线已重置/, 'dismiss 留史文案带「基线已重置」')
|
|
666
|
+
// ---- ①基线重置生效:正常一轮 sweep(基线=now)不重标 ----
|
|
667
|
+
await env.clock.tick()
|
|
668
|
+
await new Promise(function (r) { setTimeout(r, 300) })
|
|
669
|
+
assert.equal(env.task('task-frame-dismiss').stuckSince, undefined, '基线重置:正常 sweep 不重标')
|
|
670
|
+
// ---- ②竞态边缘:sweep 在飞读到旧基线(倒推回 125s)→ 防抖窗兜住,不重标 ----
|
|
671
|
+
await pushBackBaseline(env, 'task-frame-dismiss', child.childId, 'frameSilenceSince', 125000)
|
|
672
|
+
await env.clock.tick()
|
|
673
|
+
await new Promise(function (r) { setTimeout(r, 300) })
|
|
674
|
+
var t3 = env.task('task-frame-dismiss')
|
|
675
|
+
assert.equal(t3.stuckSince, undefined, '120s 窗内:旧基线也不重标(防抖窗生效)')
|
|
676
|
+
assert.ok(typeof t3.stuckRearmUntil === 'number', '窗未过期:字段保留(一次性语义未消耗)')
|
|
677
|
+
// ---- ③窗过期:仍静默 → 正常重标(豁免不永续);重标即消耗字段 ----
|
|
678
|
+
env.patchBoard(function (d) {
|
|
679
|
+
var x = d.tasks.find(function (y) { return y.id === 'task-frame-dismiss' })
|
|
680
|
+
x.stuckRearmUntil = Date.now() - 1
|
|
681
|
+
})
|
|
682
|
+
await env.clock.tick()
|
|
683
|
+
var t4 = await env.waitFor(function () {
|
|
684
|
+
var x = env.task('task-frame-dismiss')
|
|
685
|
+
return x && typeof x.stuckSince === 'number' ? x : null
|
|
686
|
+
}, '窗过期后正常重标疑似', W)
|
|
687
|
+
assert.equal(t4.stuckRearmUntil, undefined, '重标即消耗防抖窗(一次性)')
|
|
688
|
+
assert.equal((t4.history || []).filter(function (h) { return /疑似卡顿/.test(h.note || '') }).length, 2, '第二次疑似留史(重标正常发生)')
|
|
689
|
+
} finally {
|
|
690
|
+
if (boot) boot.unpin()
|
|
691
|
+
await env.cleanup()
|
|
692
|
+
}
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
// ===== 帧⑨:判死 600s 闸不受防抖窗影响(豁免不永续、不拦判死)=====
|
|
696
|
+
// 防抖窗只挂在疑似判定前(hung.mjs 疑似分支内部),kill 分支先于它且零守卫:窗内静默真到 600s 仍判死。
|
|
697
|
+
test('帧⑨:防抖窗不拦判死——dismiss 后静默真到 600s 仍正常判死重派', async () => {
|
|
698
|
+
var env = createMockCtx()
|
|
699
|
+
var boot = null
|
|
700
|
+
try {
|
|
701
|
+
boot = await bootWorkerBoard(env, 'task-frame-dismiss-kill', function (d) { d.workerModel = 'prov/hung' })
|
|
702
|
+
var child = boot.child
|
|
703
|
+
await primeFrameBaseline(env, 'task-frame-dismiss-kill', child.childId)
|
|
704
|
+
await pushBackBaseline(env, 'task-frame-dismiss-kill', child.childId, 'frameSilenceSince', 125000)
|
|
705
|
+
await env.clock.tick()
|
|
706
|
+
await env.waitFor(function () {
|
|
707
|
+
var x = env.task('task-frame-dismiss-kill')
|
|
708
|
+
return x && typeof x.stuckSince === 'number' ? x : null
|
|
709
|
+
}, '疑似落卡(前置状态)', W)
|
|
710
|
+
await dismissSuspect(env, 'task-frame-dismiss-kill')
|
|
711
|
+
assert.ok(typeof env.task('task-frame-dismiss-kill').stuckRearmUntil === 'number', 'dismiss 后防抖窗在位(判死前置)')
|
|
712
|
+
// 窗内把静默推过 600s → 判死照常(防抖窗不在 kill 分支上)
|
|
713
|
+
await pushBackBaseline(env, 'task-frame-dismiss-kill', child.childId, 'frameSilenceSince', 601000)
|
|
714
|
+
await env.clock.tick()
|
|
715
|
+
var t = await env.waitFor(function () {
|
|
716
|
+
var x = env.task('task-frame-dismiss-kill')
|
|
717
|
+
var rows = (x && x.runs) || []
|
|
718
|
+
for (var i = rows.length - 1; i >= 0; i--) {
|
|
719
|
+
if (rows[i] && String(rows[i].id) === child.childId && rows[i].outcome === 'timeout/error') return x
|
|
720
|
+
}
|
|
721
|
+
return null
|
|
722
|
+
}, '窗内静默 ≥600s 判死关账', W)
|
|
723
|
+
assert.match(String(t.lastError || ''), /帧静默 600s/, '帧路径判死文案')
|
|
724
|
+
assert.equal(String(t.lastError || '').indexOf('run 挂死:'), 0, 'HUNG_KILL_PREFIX 前缀锚定(判死链语义不变)')
|
|
725
|
+
} finally {
|
|
726
|
+
if (boot) boot.unpin()
|
|
727
|
+
await env.cleanup()
|
|
728
|
+
}
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
// ===== 帧⑩:dismiss 后帧恢复 → 防抖窗清除 + 基线删档;再静默按新基线计时(③)=====
|
|
732
|
+
// 「继续观察」是豁免不是免死:帧恢复(活体证据)即清防抖窗(与 stuckSince 同款一次性);再静默按新基线
|
|
733
|
+
// 重新计 120s 才重标。
|
|
734
|
+
test('帧⑩:dismiss 后帧恢复清防抖窗 + 删基线 → 再静默按新基线重标', async () => {
|
|
735
|
+
var env = createMockCtx()
|
|
736
|
+
var boot = null
|
|
737
|
+
try {
|
|
738
|
+
boot = await bootWorkerBoard(env, 'task-frame-dismiss-recover')
|
|
739
|
+
var child = boot.child
|
|
740
|
+
await primeFrameBaseline(env, 'task-frame-dismiss-recover', child.childId)
|
|
741
|
+
await pushBackBaseline(env, 'task-frame-dismiss-recover', child.childId, 'frameSilenceSince', 125000)
|
|
742
|
+
await env.clock.tick()
|
|
743
|
+
await env.waitFor(function () {
|
|
744
|
+
var x = env.task('task-frame-dismiss-recover')
|
|
745
|
+
return x && typeof x.stuckSince === 'number' ? x : null
|
|
746
|
+
}, '疑似落卡(前置状态)', W)
|
|
747
|
+
var tBefore = Date.now()
|
|
748
|
+
await dismissSuspect(env, 'task-frame-dismiss-recover')
|
|
749
|
+
// 帧恢复:emitFrame 置 dirty → 下一轮 sweep 走 recovery:删基线 + 清防抖窗(stuckSince 已无)
|
|
750
|
+
env.emitFrame(child.childId)
|
|
751
|
+
await env.clock.tick()
|
|
752
|
+
await new Promise(function (r) { setTimeout(r, 300) })
|
|
753
|
+
var t1 = env.task('task-frame-dismiss-recover')
|
|
754
|
+
assert.equal(t1.stuckRearmUntil, undefined, '帧恢复清除防抖窗(一次性语义)')
|
|
755
|
+
assert.equal(t1.stuckSince, undefined, '帧恢复不标疑似')
|
|
756
|
+
assert.equal(runningRow(env, 'task-frame-dismiss-recover', child.childId)().frameSilenceSince, undefined, '帧恢复删持久基线')
|
|
757
|
+
// 再静默:下一轮零帧 sweep 重建基线(= lastFrameAt)→ 倒推 125s → 按新基线重标
|
|
758
|
+
await env.clock.tick()
|
|
759
|
+
var row2 = await env.waitFor(function () {
|
|
760
|
+
var r = runningRow(env, 'task-frame-dismiss-recover', child.childId)()
|
|
761
|
+
return r && typeof r.frameSilenceSince === 'number' ? r : null
|
|
762
|
+
}, '帧恢复后重建帧基线', W)
|
|
763
|
+
assert.ok(row2.frameSilenceSince >= tBefore, '新基线取帧恢复时刻(非 dismiss 前旧值)')
|
|
764
|
+
await pushBackBaseline(env, 'task-frame-dismiss-recover', child.childId, 'frameSilenceSince', 125000)
|
|
765
|
+
await env.clock.tick()
|
|
766
|
+
var t2 = await env.waitFor(function () {
|
|
767
|
+
var x = env.task('task-frame-dismiss-recover')
|
|
768
|
+
return x && typeof x.stuckSince === 'number' ? x : null
|
|
769
|
+
}, '再静默 ≥120s 按新基线重标', W)
|
|
770
|
+
assert.ok(t2.stuckSince >= tBefore, '重标时间戳按新基线计时落卡')
|
|
771
|
+
} finally {
|
|
772
|
+
if (boot) boot.unpin()
|
|
773
|
+
await env.cleanup()
|
|
774
|
+
}
|
|
775
|
+
})
|
|
776
|
+
|
|
777
|
+
// ===== 帧⑪:日志兜底路径(无帧能力宿主)dismiss 同款重置 zeroGrowthSince——600s 墙钟重新起算 =====
|
|
778
|
+
// ⑥ 语义:无帧能力宿主(ctx.on 缺席 → frameStreamSeen 恒 false)走原日志双闸;dismiss 同步重置
|
|
779
|
+
// zeroGrowthSince(同款两闸的另一半),轮数到线但墙钟未到不判死;重置后重新满 600s 判死照常。
|
|
780
|
+
test('帧⑪:日志兜底路径 dismiss 重置 zeroGrowthSince——墙钟重新起算,600s 后判死照常', async () => {
|
|
781
|
+
var env = createMockCtx()
|
|
782
|
+
var unpin = pinHomedir(env)
|
|
783
|
+
try {
|
|
784
|
+
delete env.ctx.on // 老宿主:帧订阅静默跳过 → 日志双闸
|
|
785
|
+
plugin.apply(env.ctx)
|
|
786
|
+
await createTask(env, 'task-frame-dismiss-log', { pipeline: 'full', acceptance: 'npm test' })
|
|
787
|
+
env.patchBoard(function (d) { d.workerModel = 'prov/hung' })
|
|
788
|
+
await env.clock.advance(50)
|
|
789
|
+
await env.waitFor(function () { return env.continuables.length >= 1 }, 'worker spawn 发生(无帧能力宿主)', W)
|
|
790
|
+
var child = env.continuables[0]
|
|
791
|
+
await env.waitFor(runningRow(env, 'task-frame-dismiss-log', child.childId), '落卡 in-progress 且 runs[0] running', W)
|
|
792
|
+
await env.clock.tick()
|
|
793
|
+
await env.waitFor(function () {
|
|
794
|
+
var r = runningRow(env, 'task-frame-dismiss-log', child.childId)()
|
|
795
|
+
return r && typeof r.zeroGrowthSince === 'number' ? r : null
|
|
796
|
+
}, '日志双闸首见记基线', W)
|
|
797
|
+
// 推到墙钟线附近 + 39 轮零增长(差一轮判死)
|
|
798
|
+
await pushBackBaseline(env, 'task-frame-dismiss-log', child.childId, 'zeroGrowthSince', 601000)
|
|
799
|
+
await driveZeroGrowth(env, 'task-frame-dismiss-log', child.childId, 39)
|
|
800
|
+
var tBefore = Date.now()
|
|
801
|
+
var dr = await dismissSuspect(env, 'task-frame-dismiss-log')
|
|
802
|
+
assert.equal(dr.ok, true, 'dismiss-suspect 成功(日志路径)')
|
|
803
|
+
var row1 = runningRow(env, 'task-frame-dismiss-log', child.childId)()
|
|
804
|
+
assert.ok(row1.zeroGrowthSince >= tBefore, 'zeroGrowthSince 重置为 now(600s 墙钟重新起算)')
|
|
805
|
+
assert.ok(typeof env.task('task-frame-dismiss-log').stuckRearmUntil === 'number', '防抖窗同样落卡(与帧路径语义一键对齐)')
|
|
806
|
+
// 下一轮:轮数到 40,但墙钟距新基线 < 600s → 不判死
|
|
807
|
+
await env.clock.tick()
|
|
808
|
+
await env.waitFor(function () {
|
|
809
|
+
var r = runningRow(env, 'task-frame-dismiss-log', child.childId)()
|
|
810
|
+
return r && r.zeroGrowthRounds === 40 ? r : null
|
|
811
|
+
}, '第 40 轮零增长计数落盘', W)
|
|
812
|
+
var t1 = env.task('task-frame-dismiss-log')
|
|
813
|
+
assert.equal(t1.runs[0].outcome, 'running', '墙钟未到:不判死(基线重置生效)')
|
|
814
|
+
assert.ok(!t1.lastError, '零 lastError')
|
|
815
|
+
// 再推过 600s → 判死照常(豁免不永续、日志闸不失效)
|
|
816
|
+
await pushBackBaseline(env, 'task-frame-dismiss-log', child.childId, 'zeroGrowthSince', 601000)
|
|
817
|
+
await env.clock.tick()
|
|
818
|
+
await env.waitFor(function () {
|
|
819
|
+
var x = env.task('task-frame-dismiss-log')
|
|
820
|
+
return x && x.runs[0] && x.runs[0].outcome === 'timeout/error' ? x : null
|
|
821
|
+
}, '重置后重新满 600s 判死关账', W)
|
|
822
|
+
assert.match(String(env.task('task-frame-dismiss-log').lastError || ''), /连续 40 轮会话日志零增长/, '日志双闸文案照旧')
|
|
823
|
+
} finally {
|
|
824
|
+
unpin()
|
|
825
|
+
await env.cleanup()
|
|
826
|
+
}
|
|
827
|
+
})
|