dsh-agent-board 1.8.4 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -6
- package/index.mjs +6 -1
- package/lib/client/board-list.js +51 -7
- package/lib/client/dashboard.js +316 -26
- package/lib/client/kernel.js +41 -6
- package/lib/client/task-detail.js +16 -9
- package/lib/client.js +424 -48
- package/lib/core.mjs +303 -21
- package/lib/dispatch.mjs +1045 -25
- package/lib/notify.mjs +37 -4
- package/lib/policy.mjs +187 -7
- package/lib/rpc.mjs +374 -24
- package/lib/store.mjs +37 -0
- package/lib/usage.mjs +34 -4
- package/package.json +1 -1
- package/test/core.test.mjs +681 -44
- package/test/decompose.test.mjs +648 -0
- package/test/fault-e2e-circuit.test.mjs +187 -0
- package/test/fault-e2e-fallback-chain.test.mjs +249 -0
- package/test/fault-e2e-hung.test.mjs +504 -0
- package/test/fault-e2e-recovery.test.mjs +255 -0
- package/test/fault-e2e-softgate.test.mjs +206 -0
- package/test/fault-e2e-stale-dir.test.mjs +226 -0
- package/test/fault-injection-docs.test.mjs +114 -0
- package/test/fault-provider.test.mjs +458 -0
- package/test/helpers/fault-provider.mjs +475 -0
- package/test/helpers/mock-ctx.mjs +15 -1
- package/test/hung-frame-signal.test.mjs +630 -0
- package/test/integration.test.mjs +557 -0
- package/test/lane-gui-bysize.test.mjs +835 -0
- package/test/lane-models.test.mjs +728 -0
- package/test/onboarding-banner.test.mjs +88 -0
- package/test/ops-log.test.mjs +138 -0
- package/test/popover-clamp.test.mjs +92 -0
- package/test/rate-limit-cooldown.test.mjs +308 -0
- package/test/readme-decompose-docs.test.mjs +112 -0
- package/test/settle-stop-interrupt.test.mjs +510 -0
- package/test/size-hint.test.mjs +192 -0
- package/test/spawn-activation.test.mjs +211 -0
- package/test/store-data-safety.test.mjs +102 -0
- package/test/ui-zoom-client.test.mjs +9 -4
- package/test/verify-timing.test.mjs +94 -0
package/README.md
CHANGED
|
@@ -41,6 +41,8 @@
|
|
|
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.1** 已在宿主 **0.2.1-rc.3** 全量实测:723 断言全绿 + 拆解官/力度路由/健康嗅探/帧活性挂死检测真机 E2E 全链。
|
|
45
|
+
|
|
44
46
|
```sh
|
|
45
47
|
dsh plugin --profile web add dsh-agent-board@latest # 0.1.7+/0.2.x 宿主(推荐)
|
|
46
48
|
dsh plugin --profile web add dsh-agent-board@1.2.1 # 0.1.5/0.1.6 宿主
|
|
@@ -113,10 +115,12 @@ dsh plugin --profile web remove dsh-agent-board
|
|
|
113
115
|
|
|
114
116
|
## 功能总览
|
|
115
117
|
|
|
116
|
-
**核心能力一页速览**(`★` 为 v1.7.5–1.8.4 新栈):
|
|
118
|
+
**核心能力一页速览**(`★` 为 v1.7.5–1.8.4 新栈;`☆` 为 v1.9.0 新栈):
|
|
117
119
|
|
|
118
120
|
- **看板**:六列状态流、拖拽 / 多选批量 / 文本·优先级·标签筛选、归档留档、会话隔离
|
|
119
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
|
+
- **智能调度** `☆`:**拆解官工作流**(自然语言需求卡 → `task_decompose` 拆解官先调研后提案(outputSchema 强箍,子卡必带档位/touches/验收命令 + **dependsOn 序号接线**:提案内以 1 起序号互相引用,物化时映射真实任务 id,前排约束天然无环、坏序号丢弃留痕)→ 主窗口呈现、你确认调档 → 逐张 publish;拆解官在池条「🧩 拆解中」与源卡徽标可见;**落定走正式回执**:「🧩 已拆解 N 个(待确认发布)」聚合分区——split 列子卡清单 / keep 列理由,源卡停 draft 也必送达(投递过滤豁免,archived 才丢),失败仍走 ⚠️ 系统异常)+ **力度路由**(轻/标/重三档 × Worker/Verifier 六条有序链,四级取模链:卡级特例→档位链→板级→父模型,每跳过健康闸)+ **备用模型链** `fallbackModels`(额度到顶/故障先跨供应商同档切换,链尽才降档)+ **冷却与熔断**(限流带重置时间解析、连接/网关故障 10 分钟短窗自恢复)+ **模型健康嗅探**(`llm.stream` 极简探测定时扫已配置模型,统一 modelHealth 视图——冷却/熔断/近 5 次 run 聚合,弹层链芯片带 🟢🟡🔴⚪ 健康点,事前调度可见;**探测错误码分治**:额度>凭据>连接信号才判失败,纯空响应(供应商怪癖)判 🟡 degraded 存活——不喂冷却、路由仍可选用;**探测预算独立**:每次尝试各 20s(慢模型重试不再被共享窗口饿死),重试预算耗尽沿用首探判定不判死、超时不覆盖既有观测)
|
|
123
|
+
- **执行纪律面** `☆`:**计划闸口**(Worker 动笔前报「计划改动」清单,越界+撞他卡锁双查警示,点名持锁卡)+ **仲裁扩权入锁**(`task_arbitrate` 的 `expandTouches` 把放行文件并入有效锁集+冲突预检+hold 撞锁自动等锁复工)+ **续跑健康闸**(病模型续跑自动改 fresh spawn、宿主秒拆喂熔断、死会话哨兵 5s 即重派)+ **sizeHint 动态调档**(Worker 执行中自报档位修正,下次 spawn 生效)+ **opsLog 人工留痕**(GUI/脚本写集落盘截尾 50 条 + Team 注入「最近人工操作」摘要)
|
|
120
124
|
- **验收治理** `★`:硬性验收脚本独立复跑、驳回全量带回重派、Verifier 自测指南(userTest)、风险队列「待你过目」、验收时序防护(Verifier 在场时主窗口抢批需显式 `force:true`;有活跃 run 的卡禁止归档;Verifier 迟到结论落 `lateVerdict` 留痕+直投告警)、**迟到 verdict 三保险**(①入参校验:`verdict` 非 approved/rejected 拒收并返回重调指引——漏带字段不再被当驳回落账;②通知分态:approved 迟到=「✅ 迟到复核·全过追认」notice 级,🚨「迟到驳回」只留给真 rejected;③指纹去重:同 taskId+by+verdict 只投一次,重复落账零重复通知)
|
|
121
125
|
- **度量** `★`:Token 消耗(总量 / 有效 / 缓存读,按模型·按任务·按日)+ 记分卡(模型表现 / 质量趋势 / 驳回聚类)
|
|
122
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 条;模型下拉两段式选中≠保存)
|
|
@@ -128,7 +132,7 @@ dsh plugin --profile web remove dsh-agent-board
|
|
|
128
132
|
|
|
129
133
|
- 会话标题栏「智能看板」按钮 → 顶部抽屉面板(看板 / 团队 / 仪表盘三视图)
|
|
130
134
|
- 面板右上角「+ 新建任务」:表单弹层填标题/描述/优先级/管线/touches/依赖多选/验收脚本,Team 托管默认存为草稿(提交走 create-task RPC,成功即刷新、失败表单内联报错)
|
|
131
|
-
- 新建任务弹层为视口级 overlay(fixed 全屏遮罩 + 限高 80vh + 字段区内部滚动 +
|
|
135
|
+
- 新建任务弹层为视口级 overlay(fixed 全屏遮罩 + 限高 80vh + 字段区内部滚动 + 按钮区吸底常驻,不再被看板抽屉裁切);**设置/模型路由弹层带边界钳制**(左探出抽屉自动改左锚、右溢钳回,界面缩放档位下坐标自动还原);执行池条窄抽屉下自动折行;卡片「当前动作」行摘要化为「工具名 + 关键参数」(40 字符内有意义断点截断,悬停 title 看 host 原文),不再展示裸 JSON
|
|
132
136
|
- 六列状态流:草稿 → 待办 → 进行中 → 验证中 → 已完成 → 阻塞(空板也统一渲染六列骨架 + 引导 CTA:右上角「+ 新建任务」或对话一句 task_create 建第一张卡)
|
|
133
137
|
- 读路径 error 防线:get-tasks 返回 {error} 或网络抖动时保留现有卡片与设置(不再静默清空全板),面板头部下方出非阻断红色错误条,下次刷新成功自动消失
|
|
134
138
|
- 耗时口径三分离:待办/草稿卡显「⏳ 等待 N」(创建→被领取),执行后显「⏱ 执行 N」(被领取→完成,tooltip 写清口径)——排队时间不再混进"耗时"
|
|
@@ -152,6 +156,7 @@ dsh plugin --profile web remove dsh-agent-board
|
|
|
152
156
|
- **主窗口(本会话对话)消耗单列**(`task-muwsol23`):累计行下方新增一行「**主窗口(本会话):今日 X · 累计 Y**」(**总量口径含缓存读**——`task-muxnqunk` 翻转后缓存读不再单列、已含在 total 里,有效值见悬浮 title),按模型分布尾部追加一条「**主窗口(对话)**」(琥珀色与 run 模型区分,总量为 0 不渲染,固定尾部不参与排序);数据源是该看板所属主会话自己的 v4 日志,host 侧**增量尾读**聚合(缓存键=会话 id:文件不变零读、变大只读新增字节段、变小/轮换全量重读一次,尾部半帧不结算下轮补读——3s 轮询稳态零全量重读),五分量与有效口径(e=输入+输出+缓存写)同 Worker 完全一致;**隔离红线:不进 Top 8、不进「本看板累计」、不进架构健康/学习飞轮基数**,只并列展示;随统计范围裁剪(按 `assistant/message` 事件的本地日落点);日志不存在/读取失败/无 usage 一律**静默降级**(不渲染该行,不炸轮询)
|
|
153
157
|
- 统计口径边界:本区累计 / Top 8 / 架构健康与学习飞轮基数只统计**看板派发的 Worker/Verifier run**,**不含主窗口对话**;主窗口对话消耗按上条单列一行 + 模型分布尾部一条并列展示(不越界混入,UI caption 已明示)
|
|
154
158
|
- **「模型表现」区**(记分卡卡3,`task-muxhtgi2`):Token 区下方新增**模型 × 场景八指标对比表**——每行一个 模型 × 角色(worker/verifier)× 规模段(小 <1M / 中 1~10M / 大 >10M,按任务有效 token 分桶)桶;八列口径一句话版:**有效均值** = 桶内 run 有效消耗(输入+输出+缓存写,不含缓存读)的均值、**耗时中位** = run 耗时(endedAt−at)中位数(P90 在行 title)、**驳回率** = 归因到本桶的验收驳回 / 桶内 runs、**一次通过率** = 桶内 resolved 任务中零驳回占比、**超时率** = 超时落定 run / 桶内 runs、**失败率** = 跑崩落定(incomplete/error/failed)run / 桶内 runs(terminated 人工终止不计入,`task-mv73ym6i`)、**续跑率** = resume 续跑记录 / 桶内 runs、**缓存命中率** = cacheRead/(input+cacheRead)(⚠️ 跨 provider 对比先核 input 是否含缓存读);表头列名均带 title 口径说明;表头下两个**本地筛选器**(角色 全部/worker/verifier + 规模段 全部/小/中/大)只在已返回的 scoreboard 上过滤、**不重拉**;桶内 runs<5 整行**灰显** + title「样本 <5,仅供参考」(不参与任何「最优」强调);无数据显「暂无足够 run 数据」;Token 区 caption 新增「**→ 模型表现**」锚链接一键滚动到位。数据源 = `get-tasks` 响应里的 `usageSummary.scoreboard`(卡2 host 聚合底座 `buildScoreboard`,与 Token 区同一统计范围口径裁剪)
|
|
159
|
+
- **「按档位」小表**(力度路由记分维度,`task-mvneq4mw`):模型表现区八指标表下方新增**力度三档 × runs/驳回/异常/token** 小表——行 = 轻/标/重三档(归桶键 = run.size 派发留档,**无 size 老 run 归标档**、脏值收敛标档,零迁移;只计 Worker/Verifier run,hook 恒父模型不计入);列口径:**runs** = 该档已落定 run 数、**驳回** = 归因到该档 worker run 的验收驳回、**异常** = 超时落定 + 跑崩落定(行 title 拆分,terminated 人工终止不计入)、**token** = 有效口径合计(行 title 附总量含缓存读);数据源 `usageSummary.scoreboard.bySize`(host 装配层现算,与八指标同一 range 裁剪;全部档位零 runs 时整块不渲染)
|
|
155
160
|
- **「质量趋势」区**(记分卡卡4,`task-muxhtgil`):模型表现区下方新增全局趋势四块(P1 质量 KPI 并入)——**一次通过率**(近 14 天柱形:当日落定 resolved 任务中零驳回占比,今日柱高亮 + 块头标今日值,无落定日画灰基线不假装 0%)、**卡时长分布**(四桶横条 <10m / 10-30m / 30-60m / >60m,创建→落定墙钟含排队/验收全程)、**超时率走势**(近 14 天迷你柱行:当日超时落定 run / 当日 runs)、**续跑成功率**(resume 续跑记录中 completed 占比,范围总量口径、host 出参无逐日故不伪造 byDay);全部读 `usageSummary.scoreboard.trends`(卡2 已按统计范围裁剪,client 只渲染不重拉),范围激活时区头带「范围内: …」徽章;近 14 天为固定窗口(范围落在窗口外时对应日柱为空,caption 写明);整区/单块空态均灰显不炸
|
|
156
161
|
- **「驳回聚类」区**(记分卡卡5,`task-muxhu1zv`):质量趋势区下方新增**驳回原因 Top3**——host 对验收驳回文本做规则法关键词聚类(不上 embedding:驳回文本结构模板化;首命中归类,占比分母=驳回总数,未匹配进「其他」兜底桶;按驳回消息自身本地日落点随统计范围裁剪),每行 TopN 类目 + 占比 + 代表原文(行 title 悬浮,各截 80 字)+ **「建议沉淀为约定」结构化文本**(形如「驳回 Top1『验收脚本未实跑』占 42%——建议在派发约定/Worker prompt 补一条:…」,一键复制按钮,沉淀动作人工);挂 `usageSummary.scoreboard.rejectionClusters`,老 host 缺字段整块不渲染,无驳回显空态。同一通道的**「按表现荐模型」hint**(卡5③):某模型在中小卡 worker 桶合并后 runs≥10 且一次通过率≥90% 且有效均值较其他每个合格模型严格低 >30% → 「架构健康」区亮黄条「模型 X 在中小卡表现最优(通过率 92% · 有效均值 0.8M,较其他合格模型低 35%),建议设为默认 workerModel」;无足够数据/无对比对象不亮
|
|
157
162
|
- 「架构健康」区(架构自省 L1):`get-tasks` 顺带对**近 50 张卡**现算四信号(纯函数零存储:touches 声明热度 ≥8 次且占比 ≥40% / 带 touches 任务滞留中位数 >2 倍 / 任务**执行**时长 p90 >45min(claimedAt→resolvedAt 纯干活口径,不含排队)/ 同路径驳回 ≥2 次),命中才在仪表盘渲染提示条(⚠️/ℹ️ 两级,最多 3 条)——让运行数据主动提示"该优化架构了"(如某文件反复成为锁热点=该拆),信号只建议不裁判;**运行时健康自检**(`task-muxhrkbg`)复用同一 hint 通道——host 侧在 poolCycle 成功轮 / settle 结算 / 幽灵回收三处记内存心跳(`state.poolHealth`,不落盘):有可派卡+有空位却 >5min 无成功派发轮 → 亮「🔴 派发循环疑似冻结」,有在跑卡却 >30min 无任何结算 → 黄级「结算通道长时间无活动」,幽灵表项回收 >0 时留一条一次性 info 记录(90s 保鲜、读到即灭)——运行事故不再只靠 e2e 翻车与肉眼发现;**质量异动告警**(异常驱动审视②):`usageSummary.scoreboard.trends` 环比——近 7 天一次通过率较再前 7 天跌 >10 个百分点(两侧样本各 ≥5 防小样本误报)→ 黄条「质量异动:一次通过率本周 X%(上周 Y%),建议抽查近期验收」,超时率 / 驳回率突变各加一条同构规则(阈值常量置顶,恰好 10pp 不亮、10.1pp 亮)
|
|
@@ -203,7 +208,7 @@ draft → pending → in-progress → verifying → resolved → archived
|
|
|
203
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` 留一行日志便于溯源
|
|
204
209
|
- **任务粒度建议**:单任务 **10~30 分钟**可独立完成为甜区;预计超过 30 分钟的大任务先建一张 **epic 父卡**(`pipeline: direct`,不进池派发),再挂若干 10~30 分钟的子任务(`task_create` 传 `parentId=父卡 id`,有先后顺序用 `dependsOn` 串联),子任务全部完成后父卡自动流转(`checkParentAuto`)——`task_create` 工具描述与 Team 模式提示词都写了这条契约;**容器卡结构性不派发**:凡有子任务的卡(epic)永不进派发候选(与 `pipeline` 字段无关,不再只靠 `direct` 口头约定——手滑建 `full` 档 epic 也不会被派出占 worker 槽),子任务全了结转 verifying 后派 Verifier 验收不受影响
|
|
205
210
|
- **suggestSplit 软提示**:`task_create` / `create-task` 发现描述超 500 字符、或标题/描述命中「全量 / 整体 / 系统级 / 全面 / 重构 / 所有模块 / 整个」等史诗特征词时,返回体附带一行 `suggestSplit` 建议文案(**只提示,不阻断创建与派发**;未命中则不出现该字段,老调用方无感)
|
|
206
|
-
- **开关 `epicSplit`(史诗拆分总开关)**(⚙️ 入池配置弹层「**功能**」小节,默认**开**):关掉只停**引导**——Team 提示词第 6
|
|
211
|
+
- **开关 `epicSplit`(史诗拆分总开关)**(⚙️ 入池配置弹层「**功能**」小节,默认**开**):关掉只停**引导**——Team 提示词第 6~8 条拆分 doctrine 整条不注入、`create-task`/`task_create` 响应不再附 `suggestSplit` 软提示;**机制不禁**(显式传 `parentId` 建子卡、史诗自动收口与 hooks 状态机照常工作,明确要拆时不受阻);老看板文件没有该字段 → 读路径自动补 `true`(与升级前行为一致)。工具描述里的粒度契约是静态工具定义(随 request header 快照),不在开关范围
|
|
207
212
|
- **模型字段写保护**:`set-board-config` 对 `workerModel`/`verifierModel` 的非空值校验当前网关可用清单(`list-models` 同款枚举)——手写非法 id(如把熔断通知里的显示名抄成 id)直接拒写并返回合法清单前 8 个,验收 spawn 不再 UNKNOWN_MODEL 秒死;空串=清除放行;宿主无 llm 服务时降级跳过校验不炸。配套熔断通知分态:`UNKNOWN_MODEL` 类失败报「配置非法(不在可用清单)」,与「验收连续失败」区分——mistake 归 mistake,quality 归 quality
|
|
208
213
|
|
|
209
214
|
### 一次性派发(v74 去池化)
|
|
@@ -214,8 +219,9 @@ draft → pending → in-progress → verifying → resolved → archived
|
|
|
214
219
|
- **按需自取代替 host 预切段**:派发侧零读盘(不再切行段、不再附结构索引块)——子代理自己 `read(path, offset, limit)` 取需要的那段,清单里给出的行号就是起点;`task_preview_context` / `preview-context` 返回的也是这份瘦身清单(不是文件正文)
|
|
215
220
|
- **派发调研门禁(warning 族,软提示不阻断)**:`task_create`/`create-task` 响应附 `warning` 字段——①描述为空「Worker 只能凭标题猜需求」②full/work + touches 非空而未附调研上下文 ③touches 含整树 glob 建议精确到文件级(可多条合并);GUI 表单内黄色展示不关窗。epic 发布(publish)时自动轻量预检全部子任务注入情况,缺材料则 pushSysNote 汇总提醒主窗口(全有不打扰);派发时清单组装失败落任务「最近失败」行,不再静默。另存 `lintWarnings[]` 起草 lint(四规则:touches 整树 glob「粒度过粗,几乎锁整仓」/ 相对路径双仓库歧义「建议加仓库前缀」/ full 无验收「建议带验收命令」/ 标题缺动词或描述空),落卡透出、详情页 ⚠️ 行展示,不阻断创建
|
|
216
221
|
- **Worker/Verifier 均可配置异构模型**(⚙️ 弹出层下拉选择,空 = 继承父级),避免同源盲点;模型故障自动熔断回退父级模型
|
|
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`)
|
|
217
223
|
- 孤儿回收:子代理 run 结束/丢失超 2 分钟 → 任务自动回待办重派
|
|
218
|
-
-
|
|
224
|
+
- 看门狗:**帧活性双层判定**——运行中 assistant 帧静默 ≥120s → 标「🟡 疑似卡顿」(仅标卡、不杀,裁决权交主窗口/用户);帧静默 ≥600s → 判死按模型面故障结算重派并计入熔断连击;无事件订阅能力/零帧观测的宿主退回日志零增长兜底(连续 40 轮零增长 × ≥600s 墙钟)
|
|
219
225
|
- 歧义上报:Worker 遇到歧义不猜测,上报等主窗口裁决(任何模式下都通知);裁决后新 Worker 携带裁决答案接手
|
|
220
226
|
- 驳回详情全量带回:三条驳回路径(`board_verdict` 工具 / Verifier 文本结算 / 手动 `task_verify`·`verify-task`)统一往 `t.messages` 落一条 `kind: "rejection"` 完整驳回包(summary + checks 逐条核对证据;手动路径补写 `t.verification`),经 `buildMessages` 全量注入重派 Worker prompt——新 Worker 据此返工,不再只看到 300 字截断的 history 记录
|
|
221
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` 收末尾并标注「无用户可感知面」)
|
|
@@ -224,6 +230,25 @@ draft → pending → in-progress → verifying → resolved → archived
|
|
|
224
230
|
- 手动派发:详情页「派发 / 派发验收」按钮可随时手动触发单任务派发(auto 模式补派、manual 模式主通道)
|
|
225
231
|
- 会话隔离:看板按会话分桶,多会话互不干扰
|
|
226
232
|
|
|
233
|
+
#### 力度路由使用指南(三档 × 双角色 / 取模链四级)
|
|
234
|
+
|
|
235
|
+
一句话:卡级 `size` 不是标签,是**路由键**——派发时按它查板级 `laneModels` 表定模,不用每张卡手填模型。
|
|
236
|
+
|
|
237
|
+
- **怎么调档**:对话里 `task_update taskId=... size='heavy'`(`light` / `standard` / `heavy`;空串 `''` = 清除、回标档语义;非法值拒写)照常可用,且 GUI 三处可达:新建表单档位下拉、详情页档位下拉直调、卡列表「轻/标/重」徽标(未设档显示灰「标」);**Team 托管板建卡/发布必须显式设档**(缺档拒建拒发,错误文案带三档语义),非 Team 板零约束;拆解出的子卡每张必带档(拆解官 schema 硬约束)。
|
|
238
|
+
- **怎么配表**:「模型路由」弹层(工具栏 cpu 图标)「**力度路由**」小节=三档 × Worker/Verifier **共 6 条有序链编辑器**(下拉添加+芯片 ×/↑ 排序,改动即写);空链 =「继承板级」(继承上方 `workerModel` / `verifierModel` 行)。写通道 `set-board-config key='laneModels'`,**部分合并**——只合显式给出的槽位,非法模型整体拒写不留半截,每次写入落 configLog 审计行。**档位槽支持有序数组=同档优先序**:槽位值允许字符串(单模)或有序字符串数组(第一位=首选,派发按序取首个健康模型——首选冷却/熔断则次选赢,槽内全病照旧走备用链→降档→父模型);GUI 为链式编辑器(`task-mvokikl3`:下拉添加+芯片 × 移除/↑ 前移,读写整个有序数组,备用模型链同款)。
|
|
239
|
+
- **怎么读结果**:取模链四级,自上而下 ① **卡级 `model` 特例**(最高)→ ② **档位 lane 槽** → ③ **板级 `workerModel` / `verifierModel`** → ④ **父模型**;每跳照常过熔断,命中熔断降下一跳(限流冷却期同样按 heavy→standard→light 降档取模)。实际命中跳随 run 落 `modelSource` 留档(审计「这张卡为什么用了这个模型」),档位随 run 落 `size`。
|
|
240
|
+
- **怎么用来看账**:记分卡「**按档位**」小表=三档 × runs/驳回/异常/token(归桶键 = `run.size`;无 size 老 run 归标档、脏值收敛标档,零迁移),用来回答「轻/标/重哪档性价比最高、该给哪档配更强模型」。
|
|
241
|
+
|
|
242
|
+
#### 拆解官工作流(task_decompose:需求卡 → 实现侧子卡)
|
|
243
|
+
|
|
244
|
+
适用场景:用户用自然语言丢来一个大需求(跨模块、边界不清、一口气干不完)——先建 **draft 需求卡**保留需求原文,再用 `task_decompose` 派**一次性拆解官**出实现侧子卡提案;确认后逐张发布,Worker 拿到的就是 10~30 分钟可独立验收的小卡。Team 模式第 9 条需求卡工作流(`TEAM_DECOMPOSE_FLOW`)即这条链路;`task_decompose` 工具常驻,**不受 `epicSplit` 开关影响**。
|
|
245
|
+
|
|
246
|
+
- **门禁**:源卡须 `draft`/`pending`(`in-progress`/`verifying`/已完成卡拒拆:「源卡须为 draft/pending」);同一源卡已有活跃拆解 run → 拒并返回 `runId`(等落定再重拆);仅主窗口可用(子代理调用被拒)。
|
|
247
|
+
- **提案契约**:拆解官**零建卡工具**(权限口子根本不用开),唯一交付物是 `outputSchema` 强箍的 `{verdict, children[], rationale}`——`verdict` 只 `split`/`keep`;每个 child 必填 `title`/`description`/`size`/`touches`/`acceptance`,可选 `modelHint`/`dependsOn`;`children` 上限 **8 张**(`DECOMPOSE_MAX_CHILDREN`,超出截断);`dependsOn` 以 1 起序号引用本批**前排**子卡(如第 2 张依赖第 1 张填 `["1"]`),物化时翻译成真实任务 id,按构造天然无环(坏序号丢弃+卡面留痕,task-mvr44rm3)。拆解纪律写死在 prompt:先 `read` 调研清单里的文件、按文件/模块边界从实现侧拆,每张卡 touches 精确到文件、acceptance 是可执行命令。
|
|
248
|
+
- **物化**:run 落定后 host 读 `result.structured` 自动物化为 **draft 子卡**(`parentId`=源卡、`proposalOf`=源卡 id、size/model/touches/acceptance/dependsOn 全部落位,id 派生 `源卡id-N`),源卡追加「🧩 拆解提案已就位」消息并通知主窗口;**源卡状态不变**(仍 draft/pending),确认或调档后逐张 `task_update publish=true` 发布(也可 `batch-op publish` 批量)。`verdict=keep` = 判定不用拆:rationale 落源卡消息、零子卡,源卡按原样推进。
|
|
249
|
+
- **幂等重拆**:新提案落地时清掉上次**未发布**的提案子卡(`proposalOf` 指向该源卡且仍 `draft` 的整批移除);已 publish/已落定的子卡绝不碰;提案畸形或 run 失败时旧批**不清**(失败不毁已有资产),失败原因落源卡消息,可原样重试。
|
|
250
|
+
- **并发位**:拆解官 run 计入 **Verifier 并发位**(`activeV`),**不占 Worker 位**;活跃拆解期间源卡**跳过派发**(不让 Worker 抢在拆解结论前整坨执行);宿主不支持 `outputSchema` 时自动剥外挂参数重试,仍拿不到 `structured` 走失败臂(源卡零改动)。
|
|
251
|
+
|
|
227
252
|
### 可续跑 Worker(continuable,卡1~卡3 已落地,默认开)
|
|
228
253
|
|
|
229
254
|
**一句话**:Worker 从「一次性 run」变成**持久子会话**——超时不再丢现场,重派时原会话**冷复活**接着干;Verifier 与 hook run 仍是一次性。
|
|
@@ -267,16 +292,20 @@ Team 托管档独有(调度员体验):
|
|
|
267
292
|
> 兼容:旧的 `set-board-mode` / `set-team-mode` 两个 RPC 原样保留(旧客户端与脚本不受影响),
|
|
268
293
|
> 内部仍以 `boardMode` + `teamMode` 两个字段落盘,老看板文件无损;`get-tasks` 额外返回派生字段 `workMode` 供 UI 单点读取。
|
|
269
294
|
|
|
270
|
-
##
|
|
295
|
+
## 16 个 Agent 工具
|
|
271
296
|
|
|
272
297
|
| 类别 | 工具 |
|
|
273
298
|
|---|---|
|
|
274
299
|
| 任务管理 | `task_create` / `task_list` / `task_context` / `task_update` / `task_claim` / `task_resolve` / `task_verify` / `task_archive` / `task_cancel` |
|
|
300
|
+
| 需求拆解 | `task_decompose` |
|
|
301
|
+
| 调研上下文 | `task_preview_context` |
|
|
275
302
|
| 池治理 | `task_terminate` / `task_intervene` / `task_arbitrate` |
|
|
276
303
|
| 子代理上报 | `board_report` / `board_verdict` |
|
|
277
304
|
|
|
278
305
|
> 管理工具仅主窗口可用(子代理调用会被拒绝);`board_report`/`board_verdict` 是子代理的专用上报通道。
|
|
279
306
|
> `board_report` 的 `kind` 三档:`complete`(交付完成)/ `escalate`(歧义上报等裁决)/ `progress`(里程碑进展,静默可见、不通知)。
|
|
307
|
+
> `task_decompose` 只 spawn 一次性拆解官 run(计 Verifier 并发位、不占 Worker 位),提案由 host 物化为 draft 子卡——工具本身不建卡,用法见「拆解官工作流」。
|
|
308
|
+
> `task_preview_context` 只预演调研注入清单(笔记全文 + 文件清单;不建卡、不派发),开工前用来确认材料够不够。
|
|
280
309
|
|
|
281
310
|
## 仓库结构
|
|
282
311
|
|
|
@@ -312,7 +341,7 @@ v1.6.0 起 host 端从单体 index.mjs(1487 行)拆为薄壳 + 7 个领域
|
|
|
312
341
|
| `store.mjs` | 持久化 | boardPath / rt / wt 原子落盘 / 跨重启继承 / fileLocks 串行化 / mutateLocked |
|
|
313
342
|
| `notify.mjs` | 通知 | makeMsg / 歧义 25s 去抖 / 回执聚合 + 空闲门控 / 投递前过滤 |
|
|
314
343
|
| `dispatch.mjs` | 派发引擎 | poolCycle / spawnOneShot / settleRun / 两级超时 / 孤儿回收 / 可续跑 Worker(continuable + 重启 reconcile + usage 水位线) |
|
|
315
|
-
| `rpc.mjs` | 接口层 | RPC 路由 +
|
|
344
|
+
| `rpc.mjs` | 接口层 | RPC 路由 + 16 个 Agent 工具注册 |
|
|
316
345
|
|
|
317
346
|
(store→dispatch 的循环依赖由 `deps.poolCycle` 晚绑定解开;index.mjs 对外 re-export 契约不变。)
|
|
318
347
|
|
package/index.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// 共享闭包状态全部收进 apply 内的显式 state 对象,逐模块经参数注入(禁止跨模块隐式引用)。
|
|
15
15
|
//
|
|
16
16
|
// 兼容 re-export:对外导出契约不变(单测与历史引用直接 import 自本文件)。
|
|
17
|
-
export { TASK_SIZE_CONTRACT, SUGGEST_SPLIT_TEXT, TEAM_SPLIT_RULE, suggestSplitOf, withSplitHint, splitRuleOf } from './lib/policy.mjs'
|
|
17
|
+
export { TASK_SIZE_CONTRACT, SUGGEST_SPLIT_TEXT, TEAM_SPLIT_RULE, TEAM_DECISION_TREE, TEAM_CONTEXT_PACKING, TEAM_DECOMPOSE_FLOW, suggestSplitOf, withSplitHint, splitRuleOf } from './lib/policy.mjs'
|
|
18
18
|
export { findRunLog, readRunUsage, aggregateUsageSummary, effectiveTokens, taskEffectiveTokens, readMainWindowUsage, sizeBucketOf, runRoleOf, auditRunEntry, auditRunsCompleteness, attributeRejection, buildScoreboard, SCOREBOARD_MIN_SAMPLE, SIZE_BUCKET_SMALL_MAX, SIZE_BUCKET_MEDIUM_MAX, clusterRejections, REJECTION_CLUSTER_RULES, modelPerfHint, MODEL_HINT_MIN_RUNS, MODEL_HINT_MIN_PASS, MODEL_HINT_EFF_GAP, qualityChangeHints, QUALITY_CHANGE_PP, QUALITY_MIN_SAMPLE, QUALITY_WINDOW_DAYS } from './lib/usage.mjs'
|
|
19
19
|
import { createSession } from './lib/session.mjs'
|
|
20
20
|
import { createStore } from './lib/store.mjs'
|
|
@@ -33,6 +33,8 @@ export function apply(ctx) {
|
|
|
33
33
|
teamModeCache: {}, // rt()/set-*-mode 同步,systemPrompt 引导段读取
|
|
34
34
|
feedbackCache: {}, // 学习飞轮开关缓存(rt()/set-board-config 同步)
|
|
35
35
|
epicSplitCache: {}, // 史诗拆分总开关缓存(rt()/set-board-config 同步,Team 引导段读取)
|
|
36
|
+
opsCache: {}, // 人工/外部操作留痕缓存(task-mvnf3gly):rt() 同步 d.opsLog 尾部 5 条,
|
|
37
|
+
// Team 注入段「最近人工/外部操作」小节读取(同步函数不能读文件,与上三缓存同纪律)
|
|
36
38
|
fileLocks: {}, // 每会话一条 promise 链,串行化所有 读-改-写
|
|
37
39
|
cyclePending: {}, // kickCycle 50ms 去抖
|
|
38
40
|
activeRuns: {}, // sid -> { taskId: { id, run, role, taskId, startedAt, model, settled, continuable?, childId?, ran? } }
|
|
@@ -41,6 +43,8 @@ export function apply(ctx) {
|
|
|
41
43
|
dispatchedEver: {}, // sid -> { runId: true }(回执判定:区分派发执行 vs 主窗口手动)
|
|
42
44
|
reconcileDone: {}, // sid -> true(可续跑 Worker 重启 reconcile 的 per-host 一次性标记,见 dispatch.mjs)
|
|
43
45
|
badModels: {}, // 模型熔断坏名单(sid|model → true)
|
|
46
|
+
modelCooldown: {}, // RATE_LIMIT 额度冷却表(task-mvneq4kk):modelId → until 时间戳;纯内存不持久化,
|
|
47
|
+
// 与 badModels 同生命周期(重启即清,额度重置窗口本就短);熔断=连续失败、冷却=额度窗口,两表分治
|
|
44
48
|
escNotifyTimers: {}, // 歧义通知 25s 去抖(同任务新调度顶替旧调度)
|
|
45
49
|
receiptBuf: {}, // 回执 45s/满 5 条聚合窗口
|
|
46
50
|
receiptedKeys: {}, // 回执幂等去重(完成事件指纹,1h TTL)
|
|
@@ -71,6 +75,7 @@ export function apply(ctx) {
|
|
|
71
75
|
withTimeout: session.withTimeout, runsFor: session.runsFor, feedbackOn: session.feedbackOn, epicSplitOn: session.epicSplitOn,
|
|
72
76
|
pushSysNote: notify.pushSysNote, maybeNotify: notify.maybeNotify, notifyTaskDone: notify.notifyTaskDone,
|
|
73
77
|
notifyDispatched: notify.notifyDispatched, notifyLateReject: notify.notifyLateReject,
|
|
78
|
+
notifyDecomposed: notify.notifyDecomposed, // 拆解官回执(task-mvrqiu6x):「🧩 已拆解」聚合分区接线
|
|
74
79
|
notifyLateApprove: notify.notifyLateApprove, // 迟到复核分态通知(三连根修②,task-mvf7nve8)
|
|
75
80
|
})
|
|
76
81
|
storeDeps.poolCycle = dispatch.poolCycle // 晚绑定收口
|
package/lib/client/board-list.js
CHANGED
|
@@ -157,16 +157,51 @@
|
|
|
157
157
|
return React.createElement('span', { title: b.tip, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + b.color + ' 14%, transparent)', color: b.color, fontWeight: 600, whiteSpace: 'nowrap' } }, b.txt)
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
// ===== 拆解官状态徽标(task-mvomad6c ②,源卡列表行「🧩 拆解中 / 已提案 ×N」)=====
|
|
161
|
+
// 纯函数现算(不入 state、不落库、不引用 C/ago——自包含,可直接被单测提取执行),
|
|
162
|
+
// 返回 { txt, tone('brand'|'warn'), tip },色板映射留给渲染层 decomposePhaseBadge。
|
|
163
|
+
// 数据源 = t.runs 留档(role='decomposer')+ 同板子卡提案批:
|
|
164
|
+
// 在跑 → 末条 decomposer 留档 outcome='running' 且无 endedAt;**超龄守卫**:at 距今
|
|
165
|
+
// ≥ hardMin(留档自带,缺省 120)分钟视为 host 重启残留(sweepZombieRuns 只扫
|
|
166
|
+
// 落定卡,draft/pending 源卡的 running 留档不会被关账)——不亮「拆解中」,继续下判;
|
|
167
|
+
// 已提案 → 同板存在 proposalOf===本卡 id 且 status='draft' 的子卡(拆解官物化的待确认提案批;
|
|
168
|
+
// 逐张 publish 后 draft 消失、徽标同步熄灭——「待确认」语义精准跟随);
|
|
169
|
+
// 其余(从未拆解 / 判定不拆 / 失败 / 提案已全发布)→ null 不渲染,老卡零 DOM 变化。
|
|
170
|
+
function decomposeBadgeOf(t, allTasks) {
|
|
171
|
+
if (!t || !Array.isArray(t.runs)) return null
|
|
172
|
+
var last = null
|
|
173
|
+
for (var i = t.runs.length - 1; i >= 0; i--) { if (t.runs[i] && t.runs[i].role === 'decomposer') { last = t.runs[i]; break } }
|
|
174
|
+
if (last && last.outcome === 'running' && !last.endedAt) {
|
|
175
|
+
var ageMs = last.at ? (Date.now() - new Date(last.at).getTime()) : Infinity
|
|
176
|
+
if (isFinite(ageMs) && ageMs < (last.hardMin || 120) * 60000) {
|
|
177
|
+
return { txt: '🧩 拆解中', tone: 'brand', tip: '拆解官正在读本卡并产出子任务提案(run ' + last.id + ');拆解期间本卡不派 Worker,落定后转「🧩 已提案」' }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
var kidsN = 0
|
|
181
|
+
var list = Array.isArray(allTasks) ? allTasks : []
|
|
182
|
+
for (var k = 0; k < list.length; k++) { var x = list[k]; if (x && x.proposalOf === t.id && x.status === 'draft') kidsN++ }
|
|
183
|
+
if (kidsN) return { txt: '🧩 已提案 ×' + kidsN, tone: 'warn', tip: '拆解官已产出 ' + kidsN + ' 张草稿子卡:确认后逐张发布(详情页「发布」或 task_update publish=true),全发布后本徽标熄灭' }
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
function decomposePhaseBadge(t) {
|
|
187
|
+
var b = decomposeBadgeOf(t, state.tasks)
|
|
188
|
+
if (!b) return null
|
|
189
|
+
var color = b.tone === 'warn' ? C.warn : C.brand
|
|
190
|
+
return React.createElement('span', { title: b.tip, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + color + ' 14%, transparent)', color: color, fontWeight: 600, whiteSpace: 'nowrap' } }, b.txt)
|
|
191
|
+
}
|
|
192
|
+
|
|
160
193
|
function Card(props) { var t = props.task; var pc = prioColor[t.priority] || prioColor.low; var dragging = state.dragTask === t.id; var preview = (t.deliverable && t.deliverable.summary) || t.resolution; var critGlow = t.priority === 'critical' && !t.escalation; var sel = !!state.selected[t.id]; var pm = pipeOf(t); var depBlock = t.status === 'pending' && depsBlocked(t); var delOk = !state.selectMode && canDelete(t); var cs = (state.childStats && state.childStats[t.id]) || null; var csDone = cs ? (typeof cs.settled === 'number' ? cs.settled : cs.resolved) : 0; var epic = !!(cs && cs.total > 0); var parentT = t.parentId ? getTask(t.parentId) : null; var depWaitTitle = ''; var depWaitN = 0; if (depBlock) { t.dependsOn.forEach(function (id) { var d = getTask(id); if (!d || (d.status !== 'resolved' && d.status !== 'archived')) { depWaitN++; if (!depWaitTitle) depWaitTitle = d && d.title ? d.title : id } }) } var durB = cardDur(t);
|
|
161
194
|
// 无障碍(反馈 n-mut9rzpyc7p1):卡片根以 button 角色进 Tab 序(aria-label=标题+状态),
|
|
162
195
|
// Enter/Space 触发与点击相同的激活行为(多选=切换选中,否则开详情);
|
|
163
196
|
// focus 态用主题色 C.brand 2px outline(不用浏览器默认蓝框,保持主题一致)
|
|
164
197
|
function activate() { if (state.selectMode) { if (state.selected[t.id]) delete state.selected[t.id]; else state.selected[t.id] = true; notify() } else { state.detailId = t.id; notify() } }
|
|
165
|
-
return React.createElement('div', { role: 'button', tabIndex: 0, 'aria-label': t.title + '(' + (statusLabels[t.status] || t.status) + ')', draggable: !state.selectMode, onDragStart: function (e) { onDragStart(e, t) }, onDragEnd: onDragEnd, onMouseEnter: function () { if (delOk) setHover(t.id, 'del') }, onMouseLeave: function () { if (delOk) setHover('', '') }, onFocus: function () { setCardFocus(t.id) }, onBlur: function () { setCardFocus('') }, onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activate() } }, onClick: activate, style: { outline: isCardFocus(t.id) ? '2px solid ' + C.brand : 'none', outlineOffset: 2, border: '1px solid ' + (sel ? C.brand : (t.escalation ? C.err : (critGlow ? C.err : C.border))), borderRadius: 6, padding: '6px 8px', marginBottom: 6, background: sel ? C.nested : C.card, borderLeft: '3px solid ' + (t.escalation ? C.err : pc), cursor: state.selectMode ? 'pointer' : 'grab', fontSize: 12, opacity: dragging ? 0.4 : (depBlock ? 0.65 : 1), transition: 'opacity .15s', animation: critGlow ? 'tskb-crit 2s ease-in-out infinite' : 'none' } }, React.createElement('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 4 } }, state.selectMode ? React.createElement('span', { style: { color: sel ? C.brand : C.text2, flexShrink: 0, marginTop: 1, display: 'inline-flex' } }, ic(sel ? 'square-check-big' : 'square', 12)) : null, React.createElement('div', { style: { fontWeight: 600, color: C.text, marginBottom: 2, wordBreak: 'break-word', flex: 1 } }, t.title), React.createElement('span', { style: { flexShrink: 0, marginTop: 1, display: 'inline-flex', color: C.text2 }, title: pm.label }, ic(pm.icon, 10)), React.createElement('span', { style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + pc + ' 20%, transparent)', color: pc, flexShrink: 0, marginTop: 1 } }, prioLabel[t.priority] || '中'), reviewPending(t) ? React.createElement('span', { title: '👁 建议过目(命中风险信号):\n' + ((t.reviewHint && t.reviewHint.reasons) || []).join('\n'), style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.warn + ' 22%, transparent)', color: C.warn, fontWeight: 600, whiteSpace: 'nowrap', flexShrink: 0, marginTop: 1 } }, '👁 建议过目') : null, noResearch(t) ? React.createElement('span', { title: '本任务未附调研上下文,Worker 需自行定位——建议补 contextFiles/contextNotes', style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + C.warn + ' 18%, transparent)', color: C.warn, fontWeight: 600, flexShrink: 0, marginTop: 1, whiteSpace: 'nowrap' } }, '⚠️ 无调研') : null, (t.usage && t.usage.total) ?React.createElement('span', { style: { fontSize: 9, color: C.text2, flexShrink: 0, marginTop: 1 }, title: '本任务累计 token:' + String(t.usage.total) + '(' + (t.usage.runs || 0) + ' 次 run)' }, '⛁ ' + fmtTokens(t.usage.total)) : null), t.parentId ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: parentT ? '父任务:' + parentT.title + ' (' + t.parentId + ')' : t.parentId }, '↳ ' + (parentT && parentT.title ? parentT.title : shortId(t.parentId))) : null, t.escalation ? React.createElement('div', { style: { fontSize: 10, color: C.err, fontWeight: 600, marginBottom: 2, display: 'flex', alignItems: 'center', gap: 3 } }, ic('alert-triangle', 10), '待裁决 — 点击查看疑问') : null, t.stuckSince ? React.createElement('div', { style: { fontSize: 10, color: C.warn, fontWeight: 600, marginBottom: 2, animation: 'tskb-pulse 1.5s ease-in-out infinite' } }, '⏱
|
|
198
|
+
return React.createElement('div', { role: 'button', tabIndex: 0, 'aria-label': t.title + '(' + (statusLabels[t.status] || t.status) + ')', draggable: !state.selectMode, onDragStart: function (e) { onDragStart(e, t) }, onDragEnd: onDragEnd, onMouseEnter: function () { if (delOk) setHover(t.id, 'del') }, onMouseLeave: function () { if (delOk) setHover('', '') }, onFocus: function () { setCardFocus(t.id) }, onBlur: function () { setCardFocus('') }, onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activate() } }, onClick: activate, style: { outline: isCardFocus(t.id) ? '2px solid ' + C.brand : 'none', outlineOffset: 2, border: '1px solid ' + (sel ? C.brand : (t.escalation ? C.err : (critGlow ? C.err : C.border))), borderRadius: 6, padding: '6px 8px', marginBottom: 6, background: sel ? C.nested : C.card, borderLeft: '3px solid ' + (t.escalation ? C.err : pc), cursor: state.selectMode ? 'pointer' : 'grab', fontSize: 12, opacity: dragging ? 0.4 : (depBlock ? 0.65 : 1), transition: 'opacity .15s', animation: critGlow ? 'tskb-crit 2s ease-in-out infinite' : 'none' } }, React.createElement('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 4 } }, state.selectMode ? React.createElement('span', { style: { color: sel ? C.brand : C.text2, flexShrink: 0, marginTop: 1, display: 'inline-flex' } }, ic(sel ? 'square-check-big' : 'square', 12)) : null, React.createElement('div', { style: { fontWeight: 600, color: C.text, marginBottom: 2, wordBreak: 'break-word', flex: 1 } }, t.title), React.createElement('span', { style: { flexShrink: 0, marginTop: 1, display: 'inline-flex', color: C.text2 }, title: pm.label }, ic(pm.icon, 10)), React.createElement('span', { style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + pc + ' 20%, transparent)', color: pc, flexShrink: 0, marginTop: 1 } }, prioLabel[t.priority] || '中'), React.createElement('span', { style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: t.size ? 'color-mix(in srgb, ' + C.brand + ' 18%, transparent)' : C.nested, color: t.size ? C.brand : C.text2, fontWeight: 600, flexShrink: 0, marginTop: 1 }, title: t.size ? '力度档位 size=' + t.size + '(路由板级 laneModels 三档表定模;详情页/对话 task_update 可调)' : '力度档位未显式设置——缺省=标档语义(路由 laneModels.standard 槽)' }, ({ light: '轻', standard: '标', heavy: '重' })[t.size] || '标'), reviewPending(t) ? React.createElement('span', { title: '👁 建议过目(命中风险信号):\n' + ((t.reviewHint && t.reviewHint.reasons) || []).join('\n'), style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.warn + ' 22%, transparent)', color: C.warn, fontWeight: 600, whiteSpace: 'nowrap', flexShrink: 0, marginTop: 1 } }, '👁 建议过目') : null, noResearch(t) ? React.createElement('span', { title: '本任务未附调研上下文,Worker 需自行定位——建议补 contextFiles/contextNotes', style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + C.warn + ' 18%, transparent)', color: C.warn, fontWeight: 600, flexShrink: 0, marginTop: 1, whiteSpace: 'nowrap' } }, '⚠️ 无调研') : null, (t.usage && t.usage.total) ?React.createElement('span', { style: { fontSize: 9, color: C.text2, flexShrink: 0, marginTop: 1 }, title: '本任务累计 token:' + String(t.usage.total) + '(' + (t.usage.runs || 0) + ' 次 run)' }, '⛁ ' + fmtTokens(t.usage.total)) : null), t.parentId ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: parentT ? '父任务:' + parentT.title + ' (' + t.parentId + ')' : t.parentId }, '↳ ' + (parentT && parentT.title ? parentT.title : shortId(t.parentId))) : null, t.escalation ? React.createElement('div', { style: { fontSize: 10, color: C.err, fontWeight: 600, marginBottom: 2, display: 'flex', alignItems: 'center', gap: 3 } }, ic('alert-triangle', 10), '待裁决 — 点击查看疑问') : null, t.stuckSince ? React.createElement('div', { title: 'assistant 帧静默 ≥2 分钟(120s):仅标卡不终止,裁决权交主窗口/用户;静默 ≥10 分钟(600s)由看门狗判死并按模型面故障结算重派', style: { fontSize: 10, color: C.warn, fontWeight: 600, marginBottom: 2, animation: 'tskb-pulse 1.5s ease-in-out infinite' } }, '⏱ 疑似卡顿 · ' + ago(t.stuckSince) + ' — 点击处理') : null, React.createElement('div', { style: { fontSize: 10, color: C.text2, display: 'flex', alignItems: 'center', gap: 5, flexWrap: 'wrap' } },
|
|
166
199
|
epic ? React.createElement('span', { title: '史诗父卡:' + csDone + '/' + cs.total + ' 个子任务已了结(完成/取消/归档)', style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.brand + ' 14%, transparent)', color: C.brand, fontWeight: 600 } }, '📦 史诗 · ' + csDone + '/' + cs.total) : null,
|
|
167
200
|
// hook 相位徽章:与 📦 徽章**并存**(相位是 epic 的附加过程态,不是替代进度)
|
|
168
201
|
epic ? hookPhaseBadge(t) : null,
|
|
169
202
|
t.frozen ? React.createElement('span', { title: '已冻结:不参与自动派发(详情页可「解除冻结」)', style: { color: C.brand, fontWeight: 600 } }, '❄ 冻结') : null,
|
|
203
|
+
// 拆解官徽标(task-mvomad6c ②):源卡「🧩 拆解中 / 已提案 ×N」,非拆解卡零渲染
|
|
204
|
+
decomposePhaseBadge(t),
|
|
170
205
|
(Array.isArray(t.waitingForTouches) && t.waitingForTouches.length) ? React.createElement('span', { title: '等文件锁释放:' + t.waitingForTouches.join('、') + '(touches 冲突,详情页可 force 越权派发)', style: { color: C.warn, fontWeight: 600 } }, '🔒 等文件释放') : null,
|
|
171
206
|
React.createElement('span', { title: pm.label, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: C.nested, border: '1px solid ' + C.border } }, pm.short),
|
|
172
207
|
(t.retryCount || 0) + (t.rejectCount || 0) > 0 ? React.createElement('span', { title: '重试 ' + (t.retryCount || 0) + ' 次 / 驳回 ' + (t.rejectCount || 0) + ' 次', style: { color: C.warn, fontWeight: 600 } }, '⟳' + ((t.retryCount || 0) + (t.rejectCount || 0))) : null,
|
|
@@ -238,15 +273,18 @@
|
|
|
238
273
|
(function () {
|
|
239
274
|
var runs = historyRuns(t)
|
|
240
275
|
if (!runs.length) return React.createElement('span', { style: { fontSize: 9, color: C.text2, opacity: 0.6, padding: '2px 0' } }, '无历史会话')
|
|
241
|
-
|
|
276
|
+
// 拆解官可见性同族修正(task-mvomad6c ③):runs 行此前 W/V 二元标签,decomposer 被误标 W——
|
|
277
|
+
// 加 🧩 标签与独立序数(W/V 序号口径逐字不变)
|
|
278
|
+
var wN = 0, vN = 0, dN = 0
|
|
242
279
|
return React.createElement('span', { style: { display: 'inline-flex', gap: 5, flexWrap: 'wrap' } }, runs.map(function (r, i) {
|
|
243
|
-
var
|
|
280
|
+
var isD = r.role === 'decomposer'
|
|
281
|
+
var seq = r.role === 'verifier' ? (++vN) : (isD ? (++dN) : (++wN))
|
|
244
282
|
var isV = r.role === 'verifier'
|
|
245
283
|
return React.createElement('button', {
|
|
246
284
|
key: i, onClick: function (e) { e.stopPropagation(); if (uiWorkspaceSvc) uiWorkspaceSvc.openSession(r.id) },
|
|
247
|
-
title: (isV ? 'Verifier' : 'Worker') + ' 第 ' + seq + ' 次' + (r.at ? ' · ' + ago(r.at) : '') + (r.model ? ' · ' + r.model : '') + '(' + r.id + ')',
|
|
285
|
+
title: (isV ? 'Verifier' : (isD ? '拆解官' : 'Worker')) + ' 第 ' + seq + ' 次' + (r.at ? ' · ' + ago(r.at) : '') + (r.model ? ' · ' + r.model : '') + '(' + r.id + ')',
|
|
248
286
|
style: { fontSize: 9, padding: '2px 7px', border: '1px solid ' + (isV ? C.warn : C.brand), borderRadius: 3, background: 'transparent', color: isV ? C.warn : C.brand, cursor: 'pointer' }
|
|
249
|
-
}, '→ ' + (isV ? 'V' : 'W') + '#' + seq)
|
|
287
|
+
}, '→ ' + (isV ? 'V' : (isD ? '🧩' : 'W')) + '#' + seq)
|
|
250
288
|
}))
|
|
251
289
|
})()))
|
|
252
290
|
}))
|
|
@@ -261,6 +299,8 @@
|
|
|
261
299
|
var _d = useState(''), desc = _d[0], setDesc = _d[1]
|
|
262
300
|
var _p = useState('medium'), prio = _p[0], setPrio = _p[1]
|
|
263
301
|
var _pl = useState('full'), pipe = _pl[0], setPipe = _pl[1]
|
|
302
|
+
// 档位(task-mvomad2c size 普适化):Team 板缺省标档(无继承项,host 硬闸必填);非 Team 缺省「继承」=不落 size 字段(标档语义)
|
|
303
|
+
var _sz = useState(wm === 'team' ? 'standard' : ''), sizeSel = _sz[0], setSizeSel = _sz[1]
|
|
264
304
|
var _tc = useState(''), touchesRaw = _tc[0], setTouchesRaw = _tc[1]
|
|
265
305
|
var _dp = useState({}), depSel = _dp[0], setDepSel = _dp[1]
|
|
266
306
|
var _ac = useState(''), acc = _ac[0], setAcc = _ac[1]
|
|
@@ -282,7 +322,9 @@
|
|
|
282
322
|
var tags = tagsRaw.split(/[,,]/).map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 })
|
|
283
323
|
var deps = cands.filter(function (x) { return depSel[x.id] }).map(function (x) { return x.id })
|
|
284
324
|
setBusy(true); setErr(''); setOkMsg(''); setWarn('')
|
|
285
|
-
|
|
325
|
+
var payload = { title: t, description: desc, priority: prio, pipeline: pipe, touches: touches, tags: tags, dependsOn: deps, acceptance: acc.trim(), draft: asDraft }
|
|
326
|
+
if (sizeSel) payload.size = sizeSel // 档位:继承(空值)不落字段;Team 板缺省标档显式写(host 硬闸缺档拒建)
|
|
327
|
+
rpc('create-task', payload).then(function (r) {
|
|
286
328
|
setBusy(false)
|
|
287
329
|
if (r && r.ok === false) { setErr('⚠️ ' + (r.error || '创建失败')); return }
|
|
288
330
|
fetchTasks()
|
|
@@ -317,7 +359,9 @@
|
|
|
317
359
|
React.createElement('div', { style: { flex: '1 1 0', minWidth: 0 } }, field('管线', React.createElement('select', { value: pipe, onChange: function (e) { setPipe(e.target.value) }, style: inp },
|
|
318
360
|
React.createElement('option', { value: 'full' }, '全流程(执行+验证)'),
|
|
319
361
|
React.createElement('option', { value: 'work' }, '免验证(只做不验)'),
|
|
320
|
-
React.createElement('option', { value: 'direct' }, '主窗口处理')), '管线 = 任务走哪种流程:全流程 = Worker 做 + Verifier 验;免验证 = 只做不验;主窗口处理 = 你在对话里直接办'))
|
|
362
|
+
React.createElement('option', { value: 'direct' }, '主窗口处理')), '管线 = 任务走哪种流程:全流程 = Worker 做 + Verifier 验;免验证 = 只做不验;主窗口处理 = 你在对话里直接办')),
|
|
363
|
+
React.createElement('div', { style: { flex: '1 1 0', minWidth: 0 } }, field('档位', React.createElement('select', { value: sizeSel, onChange: function (e) { setSizeSel(e.target.value) }, style: inp },
|
|
364
|
+
(wm === 'team' ? [] : [React.createElement('option', { key: '', value: '' }, '继承(标档)')]).concat([['light', '轻(light)'], ['standard', '标(standard)'], ['heavy', '重(heavy)']].map(function (s) { return React.createElement('option', { key: s[0], value: s[0] }, s[1]) }))), wm === 'team' ? '力度档位(Team 板必填硬闸):轻=单点小改,标=常规改动,重=跨模块/高风险' : '力度档位 = 路由板级 laneModels 三档表定模;继承 = 不显式设档(标档语义)'))),
|
|
321
365
|
field('标签(逗号分隔,可空)', React.createElement('input', { value: tagsRaw, onChange: function (e) { setTagsRaw(e.target.value) }, placeholder: '如 bug, epic, 待验收', style: inp }), '标签 = 用于筛选的逗号分隔关键字(自动 trim + 去空)'),
|
|
322
366
|
field('touches(文件/glob,逗号或换行分隔,可空)', React.createElement('input', { value: touchesRaw, onChange: function (e) { setTouchesRaw(e.target.value) }, placeholder: 'src/a.mjs, src/**', style: inp }), 'touches = 本卡要改动的文件(glob 匹配);声明后并行任务间防互踩(文件锁)'),
|
|
323
367
|
field('依赖(全部完成后才派发,可空)', cands.length === 0
|