dsh-agent-board 1.7.4 → 1.7.6

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 CHANGED
@@ -102,16 +102,20 @@ dsh plugin --profile web remove dsh-agent-board
102
102
 
103
103
  ### 仪表盘(Token 消耗)
104
104
 
105
- - 仪表盘视图新增「Token 消耗」区:本看板累计**有效 / 缓存读 / 合计**三分量 + 输入 / 输出(缓存写非零时一并展示)拆分、按模型分布条形图、任务消耗 **Top 8**(标题可点击直达该任务详情,title 附「有效 N / 缓存读 M」拆分);大数字为**今日有效消耗**(本地日口径,无日账则显示 0,旁附「含缓存读共 X」小字对照,旧口径日有效值不可知时显示「—」并在 title 说明,**不拿总量冒充有效值**)、旁附「累计(本看板)」三分量小字,下方「近 7 天」迷你条形按日展示近 7 天**有效消耗**(今天高亮,title 补总量对照,旧口径日画矮灰柱并标「旧口径数据(仅总量)」,7 天全为 0 时不渲染);进行中的卡片右上角显示本任务已累计消耗(`⛁ 数字`);区底固定口径 caption:累计 / Top 8 仅看板派发的 run 消耗,主窗口行并列不混入
106
- - 口径要点:**有效消耗 = 输入 + 输出 + 缓存写(不含缓存读)**。实证本板累计 10.3M 里缓存读占 9.7M(94%)、有效消耗仅 605K——总量被缓存读撑高约 17 倍,因此大数字与「近 7 天」一律用有效值,缓存读单列可查(差额一眼可见),避免「仪表盘与真实消耗差太多」
107
- - **主数字口径统一**(`task-muwq9u04`):**按模型分布行与任务消耗 Top 8 行的主数字一律为有效消耗**(含缓存读的合计进悬浮 title),与「今日」「近 7 天」同口径——不再出现「今日有效 3.7M / 单任务 21M」这类同区并排的矛盾数字(模型有效分摊由 host 的 `byModelEff` 给出;老 host 缺该字段或老卡无五分量留账时,主数字退化用合计并在 title 标 `~`)
108
- - **统计范围接通 Token 区**(`task-muwc7hjd`):仪表盘顶部的「统计范围」(快捷:今天 / 近 7 天 / 近 30 天 / 全部 + 自定义起止)现在同时作用于 Token 区的**按模型分布 / 任务消耗 Top 8 / 累计三分量**——范围变化即用新范围重拉 `get-tasks`,由 host 按每个 run 的本地日落点(`t.runs[i].at` → `YYYY-MM-DD`,两端闭区间)过滤:范围外 run 不计入 total/byModel/top,`byDay` 同步裁到范围内;范围激活时 Token 区标题带「范围内: …」标记。**今日大数字与「近 7 天」柱子是自身固定口径,不随范围变化**(区底第二行 caption 与范围筛选浮层均写明)。无范围(两端皆空)时聚合结果与旧版逐字一致(parity);无 run 级留账(老卡 / runs 条目未落 usage)退化为按 `updatedAt` 本地日判定,无 `at` 的老 run 只在无范围时计入(宁可漏不错),run 未记模型名时按入选 run 占比摊任务级模型小计(合计与有效同法摊派,ΣbyModel 与入选总量、ΣbyModelEff 与入选有效各自对齐)。**数据源口径修正(`task-muwq9u04`)**:run 记录数组在**任务级 `t.runs[]`**(`t.usage.runs` 只是结算次数计数)——此前误把计数当数组读,范围过滤静默退化成 `updatedAt` 单日近似、Top 8 与无范围逐字相同;现已逐 run 精确裁切,且范围切换后客户端按 `usageSummary` 的 JSON 串比对补一次 notify(选范围即渲染,不等下一次任意刷新)。**Token 口径三连修(`task-muwsnyqv`)**:① 范围内 `byDay` 按入选 run 的本地日落点逐日重建(`to` 端闭区间,`from=to=今天` 时今天不丢——修「范围含今天时 total 含今天、byDay 却吞今天」);② `usageSummary` 同时返回**未过滤全量 `byDayFull`** 与范围内 `byDay`,「今日」「近 7 天」改读 `byDayFull` 兑现固定口径承诺(老 host 缺字段退化读 `byDay`);③ **旧口径日(`e=null`:老 number 日账 / 存量兜底,只有含缓存读的总量)不拿总量冒充有效值**——今日大数字显示「—」+ title 说明,近 7 天画矮灰柱 + tooltip 标「旧口径数据(仅总量,含缓存读)」,柱高归一只按有效值(此前 154M 含 97% 缓存读的旧日账会被画成巨柱、把其余日子的有效柱压平)
105
+ - 仪表盘视图新增「Token 消耗」区:本看板累计**有效 / 缓存读 / 合计**三分量 + 输入 / 输出(缓存写非零时一并展示)拆分、按模型分布条形图、任务消耗 **Top 8**(标题可点击直达该任务详情,title 附「有效 N / 缓存读 M」拆分);大数字为**今日总量**(含缓存读,本地日口径,无日账则显示 0,旁附「其中有效 X」小字对照,有效消耗详见悬浮 title)、旁附「累计(本看板)」三分量小字,下方「近 7 天」迷你条形按日展示近 7 天**总量**(今天高亮,title 补有效对照,7 天全为 0 时不渲染);进行中的卡片右上角显示本任务已累计消耗(`⛁ 数字`);区底固定口径 caption:累计 / Top 8 仅看板派发的 run 消耗,主窗口行并列不混入
106
+ - 口径要点:**有效消耗 = 输入 + 输出 + 缓存写(不含缓存读)**;总量 = 有效 + 缓存读。**主显口径翻转(`task-muxnqunk`,用户 2026-10-07 裁决「算总的吧」,推翻 `task-muwq9u04` 的有效主显)**:Token 区**主数字均为总量口径(含缓存读)**——今日大数字 / 近 7 天柱 / 按模型分布 / Top 8 / 主窗口行一律显总量,有效消耗退居悬浮 title 或次要小字;旧口径日(`e=null`)的「今日显 — / 矮灰柱」防护顺势退役(总量恒有值,正常画柱,悬浮注明有效不可知)。**不动的**:「模型表现」「质量趋势」区保持有效口径(效率指标语义),报告导出分列口径不动。实证背景:本板累计 10.3M 里缓存读占 9.7M(94%)——总量与有效的差额一眼可在累计三分量与悬浮里对照
107
+ - **主数字口径统一**(`task-muxnqunk` 翻转版,替代 `task-muwq9u04`):**按模型分布行与任务消耗 Top 8 行的主数字一律为总量**(含缓存读;模型取 host 的 `byModel`,任务取 `topTasks[].total`),有效消耗(`byModelEff` 模型有效分摊 / `topTasks[].effective`)进悬浮 title——与「今日」「近 7 天」同口径;老 host 缺 `byModelEff` 或老卡无五分量留账时 title 标「有效不可知」(口径不伪造)
108
+ - **统计范围接通 Token 区**(`task-muwc7hjd`):仪表盘顶部的「统计范围」(快捷:今天 / 近 7 天 / 近 30 天 / 全部 + 自定义起止)现在同时作用于 Token 区的**按模型分布 / 任务消耗 Top 8 / 累计三分量**——范围变化即用新范围重拉 `get-tasks`,由 host 按每个 run 的本地日落点(`t.runs[i].at` → `YYYY-MM-DD`,两端闭区间)过滤:范围外 run 不计入 total/byModel/top,`byDay` 同步裁到范围内;范围激活时 Token 区标题带「范围内: …」标记。**今日大数字与「近 7 天」柱子是自身固定口径,不随范围变化**(区底第二行 caption 与范围筛选浮层均写明)。无范围(两端皆空)时聚合结果与旧版逐字一致(parity);无 run 级留账(老卡 / runs 条目未落 usage)退化为按 `updatedAt` 本地日判定,无 `at` 的老 run 只在无范围时计入(宁可漏不错),run 未记模型名时按入选 run 占比摊任务级模型小计(合计与有效同法摊派,ΣbyModel 与入选总量、ΣbyModelEff 与入选有效各自对齐)。**数据源口径修正(`task-muwq9u04`)**:run 记录数组在**任务级 `t.runs[]`**(`t.usage.runs` 只是结算次数计数)——此前误把计数当数组读,范围过滤静默退化成 `updatedAt` 单日近似、Top 8 与无范围逐字相同;现已逐 run 精确裁切,且范围切换后客户端按 `usageSummary` 的 JSON 串比对补一次 notify(选范围即渲染,不等下一次任意刷新)。**Token 口径三连修(`task-muwsnyqv`)**:① 范围内 `byDay` 按入选 run 的本地日落点逐日重建(`to` 端闭区间,`from=to=今天` 时今天不丢——修「范围含今天时 total 含今天、byDay 却吞今天」);② `usageSummary` 同时返回**未过滤全量 `byDayFull`** 与范围内 `byDay`,「今日」「近 7 天」改读 `byDayFull` 兑现固定口径承诺(老 host 缺字段退化读 `byDay`);③ ~~旧口径日灰柱防护~~ **已随口径翻转退役**(`task-muxnqunk`):总量主显后 `e=null` 的旧口径日就是正常柱(`t` 恒有值,柱高按总量归一),不再画矮灰柱 / 今日显「—」,有效不可知只在悬浮 title 注明
109
109
  - 数据来源:每次 Worker/Verifier run 结算时读该 run 的 v4 会话日志(`~/.dsh/sessions/*/<runId>/session.v4.jsonl.zstd`),把 `assistant/message` 事件的 `usage`(`inputTokens` / `outputTokens` / `cacheReadTokens` / `cacheWriteTokens` / `totalTokens`,字段形状以真实日志为准)按 zstd 帧逐帧累加到任务 `usage`(含按模型小计、`runs` 计数与 `byDay` 日账——本地日 `YYYY-MM-DD`,一次 run 整笔记在结算日;多轮重跑/驳回重做自动累加);**run 级留账**:本次用量同时原样写回 `t.runs` 对应条目的 `usage`(五分量俱全),因此按天 / 按模型 / 按阶段任何维度都能从 runs 精确重建,口径若要再调整不必回头猜
110
- - `byDay` 为**双指标**形态 `{ t, e }`(`t` = 总量含缓存读,`e` = 有效消耗不含缓存读;一次 run 不跨日拆分)。聚合端兼容老数据:值是裸 number 的历史日账按 `{ t: n, e: null }` 处理——总量照常保留(「近 7 天」不会整段消失),有效值不可知就置 `null`;**旧口径日(`e=null`)UI 不拿总量冒充有效值**(`task-muwsnyqv`):今日大数字显「—」、近 7 天画矮灰柱 + tooltip 标「旧口径数据(仅总量)」,总量仅在「含缓存读共 X」小字与悬浮 title 里单列对照
110
+ - `byDay` 为**双指标**形态 `{ t, e }`(`t` = 总量含缓存读,`e` = 有效消耗不含缓存读;一次 run 不跨日拆分)。聚合端兼容老数据:值是裸 number 的历史日账按 `{ t: n, e: null }` 处理——总量照常保留(「近 7 天」不会整段消失),有效值不可知就置 `null`;**口径翻转后(`task-muxnqunk`)旧口径日按总量正常展示**(今日大数字显总量、近 7 天按总量画柱),有效不可知只在悬浮 title 注明——`task-muwsnyqv` 的「显 — / 矮灰柱」防护已随总量主显退役
111
111
  - `get-tasks` 再现算 board 级 `usageSummary`(总量 / 有效合计 / 按模型(合计 `byModel` + 有效分摊 `byModelEff`,两者同源累加)/ Top8(带有效与缓存读拆分)/ 日账双份(范围内 `byDay` + 未过滤全量 `byDayFull`,后者供「今日」「近 7 天」固定口径读取),不落盘额外表;老任务无 `byDay` 时整笔近似归到 `updatedAt` 的本地日,无 `updatedAt` 则不计入任何日)——**只做展示、不做计费断言**,日志读不到或没有 usage 时一律显示「暂无数据」;第二参 `range`(可选 `{ from, to }` 本地日)给定即只聚合该范围内的 run(口径见上条)
112
- - **主窗口(本会话对话)消耗单列**(`task-muwsol23`):累计行下方新增一行「**主窗口(本会话):今日有效 X · 累计 Y(缓存读 Z)**」,按模型分布尾部追加一条「**主窗口(对话)**」(琥珀色与 run 模型区分,有效为 0 不渲染,固定尾部不参与排序);数据源是该看板所属主会话自己的 v4 日志,host 侧**增量尾读**聚合(缓存键=会话 id:文件不变零读、变大只读新增字节段、变小/轮换全量重读一次,尾部半帧不结算下轮补读——3s 轮询稳态零全量重读),五分量与有效口径(e=输入+输出+缓存写)同 Worker 完全一致;**隔离红线:不进 Top 8、不进「本看板累计」、不进架构健康/学习飞轮基数**,只并列展示;随统计范围裁剪(按 `assistant/message` 事件的本地日落点);日志不存在/读取失败/无 usage 一律**静默降级**(不渲染该行,不炸轮询)
112
+ - **主窗口(本会话对话)消耗单列**(`task-muwsol23`):累计行下方新增一行「**主窗口(本会话):今日 X · 累计 Y**」(**总量口径含缓存读**——`task-muxnqunk` 翻转后缓存读不再单列、已含在 total 里,有效值见悬浮 title),按模型分布尾部追加一条「**主窗口(对话)**」(琥珀色与 run 模型区分,总量为 0 不渲染,固定尾部不参与排序);数据源是该看板所属主会话自己的 v4 日志,host 侧**增量尾读**聚合(缓存键=会话 id:文件不变零读、变大只读新增字节段、变小/轮换全量重读一次,尾部半帧不结算下轮补读——3s 轮询稳态零全量重读),五分量与有效口径(e=输入+输出+缓存写)同 Worker 完全一致;**隔离红线:不进 Top 8、不进「本看板累计」、不进架构健康/学习飞轮基数**,只并列展示;随统计范围裁剪(按 `assistant/message` 事件的本地日落点);日志不存在/读取失败/无 usage 一律**静默降级**(不渲染该行,不炸轮询)
113
113
  - 统计口径边界:本区累计 / Top 8 / 架构健康与学习飞轮基数只统计**看板派发的 Worker/Verifier run**,**不含主窗口对话**;主窗口对话消耗按上条单列一行 + 模型分布尾部一条并列展示(不越界混入,UI caption 已明示)
114
- - 「架构健康」区(架构自省 L1):`get-tasks` 顺带对**近 50 张卡**现算四信号(纯函数零存储:touches 声明热度 ≥8 次且占比 ≥40% / 带 touches 任务滞留中位数 >2 倍 / 任务**执行**时长 p90 >45min(claimedAt→resolvedAt 纯干活口径,不含排队)/ 同路径驳回 ≥2 次),命中才在仪表盘渲染提示条(⚠️/ℹ️ 两级,最多 3 条)——让运行数据主动提示"该优化架构了"(如某文件反复成为锁热点=该拆),信号只建议不裁判
114
+ - **「模型表现」区**(记分卡卡3,`task-muxhtgi2`):Token 区下方新增**模型 × 场景七指标对比表**——每行一个 模型 × 角色(worker/verifier)× 规模段(小 <1M / 中 1~10M / 大 >10M,按任务有效 token 分桶)桶;七列口径一句话版:**有效均值** = 桶内 run 有效消耗(输入+输出+缓存写,不含缓存读)的均值、**耗时中位** = run 耗时(endedAt−at)中位数(P90 在行 title)、**驳回率** = 归因到本桶的验收驳回 / 桶内 runs、**一次通过率** = 桶内 resolved 任务中零驳回占比、**超时率** = 超时落定 run / 桶内 runs、**续跑率** = 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 区同一统计范围口径裁剪)
115
+ - **「质量趋势」区**(记分卡卡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 写明);整区/单块空态均灰显不炸
116
+ - **「驳回聚类」区**(记分卡卡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」;无足够数据/无对比对象不亮
117
+ - 「架构健康」区(架构自省 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 亮)
118
+ - 报告导出新增「## 批次摘要(近 24 小时)」段(异常驱动审视③):`buildReport` 顶部一页纸——完成 N 张 / 驳回 M 次 / Token 总量(约 X,附有效 Y)/ 异常事件计数(超时 + 幽灵回收,读现有字段与 healthHints)/ 一句话质量趋势;纯聚合现有字段、不新建状态,无数据时优雅空态(一句话「近 24 小时无活动记录」)
115
119
  - 统计区耗时同口径拆分:「平均排队 / 平均执行」双行展示(平均验收单列不变)
116
120
  - 统计区新增「调研 ROI」对比行:resolved/archived 卡按有无调研注入分组现算卡数 / 平均执行时长 / 平均 token(双组总样本 ≥4 才渲染,无调研组明显更慢时数字 warn 色提示)
117
121
 
@@ -136,6 +140,7 @@ draft → pending → in-progress → verifying → resolved → archived
136
140
  - **依赖调度**:`dependsOn` 声明依赖(DFS 环检测),依赖全部完成后才会被派发,串行链路自动编排
137
141
  - **管线分档**:`full`(执行+验证)/ `work`(只做不验)/ `direct`(不进池,主窗口直接处理),创建时按规则自动分类、可手动覆盖
138
142
  - **硬性验收**:`acceptance` 字段写验收脚本命令,Worker 必须实际运行、Verifier 必须独立复跑
143
+ - **git 纪律红线**:Worker prompt 写死禁止还原命令(`git checkout` / `git restore` / `git reset --hard` / `git clean` 等——会冲掉并行 Worker 与你自己的未提交在途编辑);只改自己写的文件、别人改了别的文件与你无关,确实需要干净基线时走歧义上报(`board_report` kind=escalate)由主窗口裁决
139
144
  - **文件级排他**:`touches` 声明本任务要改的文件/glob(如 `["src/**", "README.md"]`);持有文件锁的任务(`in-progress`/`verifying`)与候选 touches 重叠就跳过本轮(卡片显示 `🔒 等文件释放`,详情页列出在等谁),**锁随工作态**:状态流转到已完成(`resolved`)即放锁,`cancelled`/归档同样不再持锁(归档回归纯收纳动作、不再是释放点)——锁只护「正在写」的阶段,「验收后-提交前」的窗口期由主窗口「回执到即提交」纪律 + 史诗 post-hook 承接,不用长持锁把整批串行化。避免并行 Worker 改同一批文件互踩;手动「派发」遇到冲突会列出冲突任务,确认后才以 `force` 越权派发
140
145
  - **里程碑进展通道**:Worker 每完成一个可验证的里程碑,可调用 `board_report`(`kind: "progress"`,`question` 写一行进展摘要 ≤200 字符)上报——进行中的卡片显示「📈 最近进展 · 相对时间」(覆盖式只留最新一条),详情页消息流保留全部 progress 条目
141
146
  - **防表演式汇报**:进展契约只写在 Worker prompt 里、且要求「有实际产物/结论才报」(禁止定时汇报);progress **静默不通知主窗口**(不进回执聚合),也不写 `history` 流转记录,避免刷屏
@@ -157,12 +162,15 @@ draft → pending → in-progress → verifying → resolved → archived
157
162
  - **预研上下文注入(contextFiles/contextNotes,瘦身分离形态)**:主窗口调研时读过的文件与笔记随子代理的**首条 prompt 一次性注入**——调研笔记全文(notes,≤8000 字符)+ **文件清单**(每行「`路径:L起-L止` — 一句用途」);**文件内容本体不进 prompt**,由子代理用 `read` 工具按行号范围按需自取(执行时盘面更新鲜;旧形态「host 读盘取正文注入」既受单文件 8KB/总包 40KB 截断,又随 runtime 快照每轮刷新重发——自治 run 实测 6×48.8K 字符≈白烧 75–100K token);UI 侧调研门禁——full/work 且声明了 touches 却未附调研的卡片亮「⚠️ 无调研」徽章,详情页「调研注入」区列 files 清单 + notes 字数(无则明示)
158
163
  - **锚点行段与用途**:`contextFiles` 条目写法「`path:L2350-L2420` / `path:L2350`」+ 可选「` — 一句用途`」(em dash 两侧空格分隔;缺省只给路径行号)——锚点只认尾部 `:L<行号>`(兼容 Windows 盘符),清单里原样带上行号供子代理直接按行段 read;锚点写错(`:L0` / `:L5-L2`)自动剥掉,不误导子代理去读空段
159
164
  - **按需自取代替 host 预切段**:派发侧零读盘(不再切行段、不再附结构索引块)——子代理自己 `read(path, offset, limit)` 取需要的那段,清单里给出的行号就是起点;`task_preview_context` / `preview-context` 返回的也是这份瘦身清单(不是文件正文)
160
- - **派发调研门禁(warning 族,软提示不阻断)**:`task_create`/`create-task` 响应附 `warning` 字段——①描述为空「Worker 只能凭标题猜需求」②full/work + touches 非空而未附调研上下文 ③touches 含整树 glob 建议精确到文件级(可多条合并);GUI 表单内黄色展示不关窗。epic 发布(publish)时自动轻量预检全部子任务注入情况,缺材料则 pushSysNote 汇总提醒主窗口(全有不打扰);派发时清单组装失败落任务「最近失败」行,不再静默
165
+ - **派发调研门禁(warning 族,软提示不阻断)**:`task_create`/`create-task` 响应附 `warning` 字段——①描述为空「Worker 只能凭标题猜需求」②full/work + touches 非空而未附调研上下文 ③touches 含整树 glob 建议精确到文件级(可多条合并);GUI 表单内黄色展示不关窗。epic 发布(publish)时自动轻量预检全部子任务注入情况,缺材料则 pushSysNote 汇总提醒主窗口(全有不打扰);派发时清单组装失败落任务「最近失败」行,不再静默。另存 `lintWarnings[]` 起草 lint(四规则:touches 整树 glob「粒度过粗,几乎锁整仓」/ 相对路径双仓库歧义「建议加仓库前缀」/ full 无验收「建议带验收命令」/ 标题缺动词或描述空),落卡透出、详情页 ⚠️ 行展示,不阻断创建
161
166
  - **Worker/Verifier 均可配置异构模型**(⚙️ 弹出层下拉选择,空 = 继承父级),避免同源盲点;模型故障自动熔断回退父级模型
162
167
  - 孤儿回收:子代理 run 结束/丢失超 2 分钟 → 任务自动回待办重派
163
168
  - 看门狗:运行超时且事件流停滞 → 标记"疑似卡死"(不自动杀,裁决权交主窗口/用户)
164
169
  - 歧义上报:Worker 遇到歧义不猜测,上报等主窗口裁决(任何模式下都通知);裁决后新 Worker 携带裁决答案接手
165
170
  - 驳回详情全量带回:三条驳回路径(`board_verdict` 工具 / Verifier 文本结算 / 手动 `task_verify`·`verify-task`)统一往 `t.messages` 落一条 `kind: "rejection"` 完整驳回包(summary + checks 逐条核对证据;手动路径补写 `t.verification`),经 `buildMessages` 全量注入重派 Worker prompt——新 Worker 据此返工,不再只看到 300 字截断的 history 记录
171
+ - **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` 收末尾并标注「无用户可感知面」)
172
+ - **开关 `verifyUserGuide`**(⚙️ 设置区「验收」小节——「通知」旁,默认**开**):关掉后 Verifier prompt 不拼指南段(省 token)、验收落账不挂 `userTest`、详情页自测指南块与报告清单段整块不渲染;老看板文件没有该字段 → 读路径自动补 `true`(与升级前行为一致)
173
+ - **Verifier 验收员加餐**(固定人设 + 文件纪律 + 工具收窄):verifier spawn 挂固定 `persona`(宿主 scoped persona-prefix 影子段)——独立判断不轻信 Worker 汇报、以验收脚本实证为准、诚实分级、结论写给主窗口与用户双读者;prompt 内写死**文件纪律**(可写临时/测试文件,工程代码与文档只读,验收结束清理,事后 touches 审计兜底);`toolFilter` 保守收窄只砍联网检索(`web_fetch`/`web_search`),spawn 失败自动剥外挂参数裸请求重试(Worker spawn 路径不受影响)
166
174
  - 手动派发:详情页「派发 / 派发验收」按钮可随时手动触发单任务派发(auto 模式补派、manual 模式主通道)
167
175
  - 会话隔离:看板按会话分桶,多会话互不干扰
168
176
 
@@ -180,6 +188,7 @@ draft → pending → in-progress → verifying → resolved → archived
180
188
  - **结算双通道 + 池韧性(2026-10-06 事故修复)**:continuable 结算有事件通道(`agent/status` 的 running→idle,身份取 `agent.session.id`)与上报通道(`board_report` 落定即收尾)两条入口,任一到达即关账(结局/usage/超时臂三件套,幂等);派发周期自带**幽灵活跃表项 GC**(卡面证据核对回收残留 rec,防残留把派发容量顶到 0 拖死全池)+ 整轮 try/catch 与逐卡隔离(单点异常只作废该卡该轮)+ 去抖 latch 时间戳兜底复位
181
189
  - **续跑指令优先级**:断点续跑指令会带上卡上 messages 原文(仲裁/干预/驳回理由),并声明**最新裁决/干预优先于历史原始契约**(冲突以最新为准)——冷复活子会话的历史里没有仲裁答案,不带原文它无从知晓
182
190
  - **高优干预实时送达 continuable Worker**:`task_intervene` 对 continuable rec 走宿主投递通道(保留插件 source,下一个 step 边界消费),会话不可用降级 `sendMessage` 冷复活投递,再不行回退「记录注入随重派送达」并在 history 注明
191
+ - **运行区子会话 id 归属标注(防误杀)**:卡详情「运行」区显示当前活跃 run 的子会话 id(childId)并标注「对应子代理列表同名条目」,终止 Worker 前先到子代理列表核对该 id 的同名条目,避免误杀别的合法 Worker
183
192
 
184
193
  ### 工作模式(三档)
185
194
 
@@ -196,6 +205,7 @@ draft → pending → in-progress → verifying → resolved → archived
196
205
  - **歧义裁决**:Worker 遇歧义不猜测,一律上报;裁决后新 Worker 携带答案接手(Team 托管档附带 system prompt 派发引导 + 默认草稿护栏)
197
206
  - **Verifier 验收**:`acceptance` 硬性验收脚本命令,Worker 必须实际运行、Verifier 必须独立复跑;跨档一致
198
207
  - **touches 排他**:`touches` 文件级排他锁在活动任务间生效,冲突任务跳过本轮派发,**锁随工作态**(`in-progress`/`verifying` 持有;状态流转到 `resolved` 即释放,`cancelled`/归档同样释放——归档不再承担解锁职责);跨档一致
208
+ - **起草 lint**:`task_create`/`create-task` 与更新入口现算四规则软警告(touches 整树 glob「粒度过粗,几乎锁整仓」/ 相对路径双仓库歧义「建议加仓库前缀」/ full 无验收「建议带验收命令」/ 标题缺动词或描述空),存 `lintWarnings[]` 落卡透出、详情页 ⚠️ 行展示,不阻断;跨档一致
199
209
  - 孤儿回收、看门狗、级联归档、会话隔离同样三档一致
200
210
 
201
211
  Team 托管档独有(调度员体验):
@@ -228,7 +238,8 @@ Team 托管档独有(调度员体验):
228
238
  │ ├── lib/client/ # client 端模块源(按用户感知域拆分,见下节)
229
239
  │ ├── lib/client.js # client 端产物(⚠️ GENERATED:scripts/build-client.cjs 拼装,勿直接编辑)
230
240
  │ ├── scripts/build-client.cjs # 零依赖组装器(模块源 → 产物;--check 校验产物新鲜度)
231
- │ ├── test/core.test.mjs # 单元测试(node --test,84 例)
241
+ │ ├── test/ # 无头集成套件(node --test;*.test.mjs 为测试本体,
242
+ │ │ └── helpers/mock-ctx.mjs # helpers/ 是假宿主基建,不被当测试跑)
232
243
  │ ├── package.json # dsh.bundle.patch + dsh.client 元数据
233
244
  │ └── cordis.patch.yml # bundle 挂载行
234
245
  └── docs/
@@ -267,7 +278,7 @@ dsh web 的 client 运行时不具备模块解析能力(entry 被整体读成
267
278
  | `kernel.js` | 底座(用户不可感知) | 渲染原语(ic/icText/ActorLink)、RPC 封装与轮询族、共享状态(state/listeners/notify)、图标(ICONS)、共享任务工具,以及面板骨架入口(BoardButton/ViewTab/TopPanel/slots.inject) |
268
279
  | `board-list.js` | 看板列表 | 看板列与卡片(Card)、筛选条、多选批量操作条、建卡表单、归档列表 |
269
280
  | `task-detail.js` | 任务详情 | 详情抽屉(编辑/流转/冻结/touches/历史会话/高优介入)、歧义裁决对话与候选教训沉淀、依赖区块 |
270
- | `dashboard.js` | 仪表盘与设置 | 统计图表、Token 消耗、全局总览、报告生成、团队池视图、池/模型/超时设置与工作模式开关 |
281
+ | `dashboard.js` | 仪表盘与设置 | 统计图表、Token 消耗、模型表现(七指标×场景筛选)、质量趋势(通过率/时长分布/超时/续跑)、全局总览、报告生成、团队池视图、池/模型/超时设置与工作模式开关 |
271
282
 
272
283
  四个模块拼进同一个 `apply(ctx)` 函数作用域(`var`/`function` 声明提升使跨模块引用与拼接顺序无关;
273
284
  所有同步执行代码——DOM 监听、轮询注册、布局同步、slots.inject——都在 kernel 域内保持原相对顺序)。
@@ -276,9 +287,22 @@ dsh web 的 client 运行时不具备模块解析能力(entry 被整体读成
276
287
  > v68 起拆除了"动态源码 → 静态包"的转换层(build-pkg.cjs):插件已稳定,
277
288
  > 双形态维护的复杂度大于收益,包内文件即唯一源码,改完重启 dsh 即生效。
278
289
  >
279
- > v74 起去池化(一次性派发)+ 纯逻辑抽到 `lib/core.mjs`,跑
280
- > `node --test packages/dsh-agent-board/test/` 即可验证状态机/依赖/派发决策,
281
- > 不用重启 dsh 人肉回归。
290
+ > v74 起去池化(一次性派发)+ 纯逻辑抽到 `lib/core.mjs`,在包目录跑
291
+ > `npm test`(node --test,无头集成全套件)即可验证状态机/依赖/派发决策,
292
+ > 不用重启 dsh 人肉回归——跑法与口径详见 [docs/DEVTESTING.md](docs/DEVTESTING.md)。
293
+
294
+ ## 开发与测试
295
+
296
+ - **一条命令**:`cd packages/dsh-agent-board && npm test`——`pretest` 自动先拼装前端产物
297
+ (`build-client`),再跑 `node --test test/**/*.test.mjs`:纯逻辑单测 + 无头宿主集成全套件
298
+ (当前 355 例,~6s),无需 dsh 环境。
299
+ - **helpers 约定**:`test/helpers/mock-ctx.mjs` 是无头基建(假 cordis ctx / 临时 HOME / 虚拟时钟 /
300
+ 剧本化 subagents),不是测试;靠 `*.test.mjs` 文件名约定排除,不会被误跑。
301
+ - **CI 零改动继承**:`.github/workflows/test.yml` 在 push/PR 时跑语法检查 + `npm test`,
302
+ 套件随脚本自动生效;`prepublishOnly` 门禁同款。
303
+ - **插件改动生效口径**:host 端代码(`index.mjs` / `lib/*.mjs`)不可热重载(宿主模块表按 URL 缓存),
304
+ 改了必须重启 dsh;管理接口 disable→enable 只能重组组合层(路由/工具卸下再挂上),拉不到新代码。
305
+ 实测证据与探针脚本(`scripts/reload-probe.cjs`)见 [docs/DEVTESTING.md](docs/DEVTESTING.md)。
282
306
 
283
307
  ## 文档
284
308
 
@@ -286,6 +310,7 @@ dsh web 的 client 运行时不具备模块解析能力(entry 被整体读成
286
310
  - [docs/PACKAGING.md](docs/PACKAGING.md) — 正式安装(Bundle 打包)注意事项
287
311
  - [docs/icon-style-guide.md](docs/icon-style-guide.md) — 图标规范
288
312
  - [docs/REGRESSION-v59.md](docs/REGRESSION-v59.md) — 回归测试说明
313
+ - [docs/DEVTESTING.md](docs/DEVTESTING.md) — 开发与测试:无头套件跑法 / CI 继承 / 热重载口径
289
314
 
290
315
  ## 发布新版本(维护者)
291
316
 
@@ -310,7 +335,7 @@ git push --follow-tags # tag 推送触发流水线
310
335
  - **README 单一来源**:本文件(根 README)即唯一来源;发版前在 `packages/dsh-agent-board` 跑一次 `npm run sync-readme` 同步进包(npm 页面展示的是包内 README)
311
336
  - 需在仓库 **Settings → Secrets and variables → Actions** 配置 `NPM_TOKEN`
312
337
  (npm granular access token:bypass 2FA + direct publish)
313
- - 日常 push / PR 有 `test.yml` 跑语法检查 + 60 例单测
338
+ - 日常 push / PR 有 `test.yml` 跑语法检查 + 无头集成全套件(`npm test`)
314
339
  - 本地手动发布仍然可用:`npm publish --registry=https://registry.npmjs.org`(本机默认源是镜像时必须显式指定)
315
340
 
316
341
  ## License
package/index.mjs CHANGED
@@ -15,7 +15,7 @@
15
15
  //
16
16
  // 兼容 re-export:对外导出契约不变(单测与历史引用直接 import 自本文件)。
17
17
  export { TASK_SIZE_CONTRACT, SUGGEST_SPLIT_TEXT, TEAM_SPLIT_RULE, suggestSplitOf, withSplitHint, splitRuleOf } from './lib/policy.mjs'
18
- export { findRunLog, readRunUsage, aggregateUsageSummary, effectiveTokens, taskEffectiveTokens, readMainWindowUsage } from './lib/usage.mjs'
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'
21
21
  import { createNotify } from './lib/notify.mjs'
@@ -47,6 +47,14 @@ export function apply(ctx) {
47
47
  sysNotesBuf: {}, // 系统级异常通知队列(随回执冲刷)
48
48
  mainWindowUsageCache: {}, // 主窗口消耗增量尾读缓存(task-muwsol23):sid → { size, mtimeMs, 聚合五分量+byDay },
49
49
  // 文件不变零读 / 变大只读增量 / 变小全量重读一次;纯内存不落盘,重启自然全量一次
50
+ scoreboardCache: {}, // 记分卡聚合缓存(task-muxhtgh9 卡2):sid → { taskCount, stamp, recs },
51
+ // runs 只增不改——键=(任务数, 最新落定时刻) 命中则提取层零重算,范围裁剪在装配层现算;
52
+ // 纯内存不落盘,重启自然全量一次(与 mainWindowUsageCache 同生命周期纪律)
53
+ poolHealth: {}, // 运行时健康自检(task-muxhrkbg)内存心跳:sid → { bornAt, poolLastOkAt, dispatchOk,
54
+ // lastDispatchAt, settleLastOkAt, reapNote }——dispatch.mjs 在 poolCycle 成功轮 /
55
+ // settleRunRecord 成功结算 / 幽灵回收 >0 / spawn 成功四处打点,rpc get-tasks 现算
56
+ // 运行时 hint(health.computeRuntimeHealthHints)。纯内存不落盘:重启后 bornAt 起算,
57
+ // 心跳阈值的宽限期随之重置(心跳的意义是「host 活着时在不在转」,跨重启无继承价值)
50
58
  }
51
59
  // ===== 模块接线(依赖顺序:session → store → notify → dispatch → rpc)=====
52
60
  var session = createSession(ctx, state)
@@ -7,13 +7,14 @@
7
7
  // #14 批量操作条(#16 带一步撤销:快照操作前的 priority/status;delete 是真删,明确不支持撤销)
8
8
  function BatchBar() {
9
9
  var _R = React; var useState = _R.useState, useEffect = _R.useEffect
10
- var _a = useState(0), cnt = _a[0], setCnt = _a[1]; var _b = useState(false), on = _b[0], setOn = _b[1]; var _c = useState(''), msg = _c[0], setMsg = _c[1]; var _d = useState(null), undo = _d[0], setUndo = _d[1]
10
+ var _a = useState(0), cnt = _a[0], setCnt = _a[1]; var _b = useState(false), on = _b[0], setOn = _b[1]; var _c = useState(''), msg = _c[0], setMsg = _c[1]; var _d = useState(null), undo = _d[0], setUndo = _d[1]; var _e = useState([]), reasons = _e[0], setReasons = _e[1]
11
11
  useEffect(function () { function update() { setCnt(Object.keys(state.selected).length); setOn(state.selectMode); setUndo(state.undoSnapshot) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
12
12
  if (!on) return null
13
13
  var ids = Object.keys(state.selected)
14
14
  function snapshot(op) { return { op: op, at: Date.now(), items: ids.map(function (id) { var t = getTask(id); return t ? { id: id, priority: t.priority, status: t.status } : null }).filter(Boolean) } }
15
- // 跳过原因只回显第一条(如"任务正在执行中,请先…"),避免把整屏门禁文案塞进批量条
16
- function firstReason(r) { var m = (r && r.reasons) || {}; var ks = Object.keys(m); return ks.length ? String(m[ks[0]]) : '' }
15
+ // 跳过原因逐条渲染:host batch-op 返回 reasons 对象(id → 原因),转成「标题:原因」列表
16
+ // (巡检 n-muyj5p755l3c:批量归档跳过此前只显示「跳过 N」无原因)
17
+ function reasonRows(r) { var m = (r && r.reasons) || {}; return Object.keys(m).map(function (id) { var t = getTask(id); return { title: t ? t.title : shortId(id), reason: String(m[id]) } }) }
17
18
  function run(op, value) {
18
19
  // 只有可撤销的 op 才需要快照(delete 是真删,快照里的 status/priority 复活不回整张卡)
19
20
  var snap = op === 'delete' ? null : snapshot(op)
@@ -22,8 +23,8 @@
22
23
  state.selected = {}
23
24
  // 删除无 undo:done>0 也不留撤销快照(否则「↩️ 撤销」点了恢复不了,纯粹误导);其他 op 维持一步撤销
24
25
  state.undoSnapshot = (r && r.done > 0 && op !== 'delete') ? snap : null
25
- var reason = (op === 'delete' && r && r.reasons) ? firstReason(r) : ''
26
- setMsg('✅ 已处理 ' + (r && r.done || 0) + ' 个' + (r && r.skipped && r.skipped.length ? ',跳过 ' + r.skipped.length + (reason ? '(' + reason + ')' : '') : ''))
26
+ setReasons(reasonRows(r))
27
+ setMsg('✅ 已处理 ' + (r && r.done || 0) + ' 个' + (r && r.skipped && r.skipped.length ? ',跳过 ' + r.skipped.length + ' 个' : ''))
27
28
  fetchTasks()
28
29
  }).catch(function (e) { setMsg('⚠️ ' + String(e)) })
29
30
  }
@@ -35,7 +36,7 @@
35
36
  }
36
37
  function doUndo() { if (!undo) return; rpc('batch-undo', { snapshot: undo }).then(function (r) { setMsg('↩️ 已撤销 ' + (r && r.done || 0) + ' 个'); state.undoSnapshot = null; fetchTasks() }).catch(function (e) { setMsg('⚠️ ' + String(e)) }) }
37
38
  var btn = { fontSize: 10, padding: '3px 10px', borderRadius: 4, border: 'none', cursor: 'pointer', fontWeight: 600 }
38
- return React.createElement('div', { style: { position: 'sticky', bottom: 0, display: 'flex', alignItems: 'center', gap: 6, padding: '6px 8px', marginTop: 8, background: C.nested, border: '1px solid ' + C.border, borderRadius: 6 } },
39
+ return React.createElement('div', { style: { position: 'sticky', bottom: 0, display: 'flex', alignItems: 'center', gap: 6, padding: '6px 8px', marginTop: 8, background: C.nested, border: '1px solid ' + C.border, borderRadius: 6, flexWrap: 'wrap' } },
39
40
  React.createElement('span', { style: { fontSize: 11, color: C.text, fontWeight: 600 } }, '已选 ' + cnt + ' 项'),
40
41
  React.createElement('button', { onClick: function () { run('archive') }, disabled: cnt === 0, style: Object.assign({}, btn, { background: C.text2, color: C_INV, display: 'inline-flex', alignItems: 'center', gap: 3 }) }, ic('archive', 11), '批量归档'),
41
42
  ['critical', 'high', 'medium', 'low'].map(function (p) { return React.createElement('button', { key: p, onClick: function () { run('set-priority', p) }, disabled: cnt === 0, style: Object.assign({}, btn, { background: prioColor[p], color: C_INV }) }, prioLabel[p]) }),
@@ -43,21 +44,26 @@
43
44
  React.createElement('button', { onClick: batchDelete, disabled: cnt === 0, title: '删除选中任务(不可恢复;执行中/验证中的需先终止,已落定请用归档)', style: Object.assign({}, btn, { background: C.err, color: C_INV, display: 'inline-flex', alignItems: 'center', gap: 3 }) }, ic('trash-2', 11), '批量删除'),
44
45
  undo ? React.createElement('button', { onClick: doUndo, title: '撤销最近一次批量操作(删除不可撤销)', style: Object.assign({}, btn, { background: C.brand, color: C_INV }) }, '↩️ 撤销') : null,
45
46
  msg ? React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, msg) : null,
46
- React.createElement('button', { onClick: function () { state.selected = {}; notify() }, style: { marginLeft: 'auto', fontSize: 10, padding: '3px 8px', border: 'none', background: 'transparent', color: C.text2, cursor: 'pointer' } }, '清除选择'))
47
+ React.createElement('button', { onClick: function () { state.selected = {}; notify() }, style: { marginLeft: 'auto', fontSize: 10, padding: '3px 8px', border: 'none', background: 'transparent', color: C.text2, cursor: 'pointer' } }, '清除选择'),
48
+ reasons.length > 0 ? React.createElement('div', { style: { flexBasis: '100%', marginTop: 4, padding: '4px 6px', background: C.card, border: '1px solid ' + C.border, borderRadius: 4, maxHeight: 96, overflowY: 'auto' } },
49
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 2 } }, '跳过原因'),
50
+ reasons.map(function (it, i) { return React.createElement('div', { key: i, style: { fontSize: 10, color: C.text2, marginBottom: 1, wordBreak: 'break-word' } }, it.title + ':' + it.reason) })) : null)
47
51
  }
48
52
 
49
53
  function FilterBar() {
50
54
  var _R = React; var useState = _R.useState, useEffect = _R.useEffect
51
- var _a = useState(state.filterQ), q = _a[0], setQ = _a[1]; var _b = useState(state.filterPrio), fp = _b[0], setFp = _b[1]; var _c = useState(state.filterTag), ft = _c[0], setFt = _c[1]
52
- useEffect(function () { function update() { setQ(state.filterQ); setFp(state.filterPrio); setFt(state.filterTag) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
55
+ // 优先级筛选单一事实源:chip 高亮与「清除」显隐统一读全局 state.filterPrio(消灭本地 fp 副本),
56
+ // 曾因本地态与全局 filterPrio 不同帧更新出现「清除按钮消失但高亮残留」的矛盾画面(巡检 n-muyimk1hpdxv)
57
+ var _a = useState(state.filterQ), q = _a[0], setQ = _a[1]; var _c = useState(state.filterTag), ft = _c[0], setFt = _c[1]
58
+ useEffect(function () { function update() { setQ(state.filterQ); setFt(state.filterTag) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
53
59
  function setQ2(v) { state.filterQ = v; notify() }
54
60
  function togglePrio(p) { var i = state.filterPrio.indexOf(p); if (i >= 0) state.filterPrio.splice(i, 1); else state.filterPrio.push(p); notify() }
55
61
  function clearAll() { state.filterQ = ''; state.filterPrio = []; state.filterTag = ''; notify() }
56
- var hasFilter = q.trim() || fp.length > 0 || ft
62
+ var hasFilter = q.trim() || state.filterPrio.length > 0 || ft
57
63
  var chipBase = { fontSize: 10, padding: '2px 8px', borderRadius: 10, border: '1px solid ' + C.border, cursor: 'pointer', background: 'transparent', color: C.text2, transition: 'all .15s' }
58
64
  return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8, flexWrap: 'wrap' } },
59
65
  React.createElement('input', { value: q, onChange: function (e) { setQ2(e.target.value) }, placeholder: '🔍 搜索标题/描述/ID…', style: { flex: '0 1 200px', fontSize: 11, padding: '3px 8px', border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text } }),
60
- ['critical', 'high', 'medium', 'low'].map(function (p) { var on = fp.indexOf(p) >= 0; return React.createElement('button', { key: p, onClick: function () { togglePrio(p) }, style: Object.assign({}, chipBase, on ? { background: prioColor[p], color: C_INV, borderColor: prioColor[p] } : {}) }, prioLabel[p]) }),
66
+ ['critical', 'high', 'medium', 'low'].map(function (p) { var on = state.filterPrio.indexOf(p) >= 0; return React.createElement('button', { key: p, onClick: function () { togglePrio(p) }, style: Object.assign({}, chipBase, on ? { background: prioColor[p], color: C_INV, borderColor: prioColor[p] } : {}) }, prioLabel[p]) }),
61
67
  allTags().length > 0 ? React.createElement('select', { value: ft, onChange: function (e) { state.filterTag = e.target.value; notify() }, style: { fontSize: 10, padding: '2px 4px', border: '1px solid ' + C.border, borderRadius: 4, background: C.card, color: C.text } }, React.createElement('option', { value: '' }, '🏷 全部标签'), allTags().map(function (g) { return React.createElement('option', { key: g, value: g }, g) })) : null,
62
68
  hasFilter ? React.createElement('button', { onClick: clearAll, style: { fontSize: 10, padding: '2px 8px', borderRadius: 10, border: 'none', background: C.nested, color: C.text2, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('x', 9), '清除') : null)
63
69
  }
@@ -162,8 +168,8 @@
162
168
  (Array.isArray(t.waitingForTouches) && t.waitingForTouches.length) ? React.createElement('span', { title: '等文件锁释放:' + t.waitingForTouches.join('、') + '(touches 冲突,详情页可 force 越权派发)', style: { color: C.warn, fontWeight: 600 } }, '🔒 等文件释放') : null,
163
169
  React.createElement('span', { title: pm.label, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: C.nested, border: '1px solid ' + C.border } }, pm.short),
164
170
  (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,
165
- t.status === 'in-progress' && t.claimedBy ? React.createElement('span', null, '⚡ ' + shortId(t.claimedBy)) : null,
166
- t.assignee ? React.createElement('span', null, '👤→' + shortId(t.assignee)) : null,
171
+ t.status === 'in-progress' && t.claimedBy ? React.createElement('span', { title: '执行该任务的子代理会话:' + t.claimedBy }, '⚡ ' + String(t.claimedBy).slice(0, 8) + '…') : null,
172
+ t.assignee ? React.createElement('span', { title: '指派给:' + t.assignee }, '👤→' + String(t.assignee).slice(0, 8) + '…') : null,
167
173
  durB ? React.createElement('span', { title: durB.tip }, durB.txt) : null,
168
174
  // 删除入口(仅草稿/待办/阻塞,多选模式下隐藏以免误触):hover 才由透明转红,平时不抢视觉
169
175
  delOk ? React.createElement('span', {
@@ -182,22 +188,27 @@
182
188
  t.status === 'verifying' && preview ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '📝 ' + preview) : null, depBlock ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: '依赖全部完成后才会派发' }, '⛓ 等待「' + depWaitTitle + '」' + (depWaitN > 1 ? ' 等 ' + depWaitN + ' 个' : '')) : null) }
183
189
 
184
190
  function ArchiveView() {
185
- var _R = React; var useState = _R.useState, useEffect = _R.useEffect
191
+ var _R = React; var useState = _R.useState
186
192
  var _a = useState(state.archQ || ''), q = _a[0], setQ = _a[1]
187
- useEffect(function () { if (!state.archived.length) fetchArchived() }, [])
188
- var list = state.archived.filter(function (t) {
193
+ // 归档数据源口径对齐仪表盘(反馈 n-muyg3e0m9z7i):直接读 state.tasks(get-tasks 全量含归档),
194
+ // 不再依赖独立 state.archived/fetchArchived——旧路径异步填库却未接入 TopPanel 的 notify 更新项,
195
+ // 归档 tab 打开后停在「暂无归档任务」空态,而仪表盘「已归档」计数来自 state.tasks(全量),二者对不上。
196
+ // 全量 + 按归档时间倒序(archivedAt 优先,回退 resolvedAt/createdAt)+ 顶部注明条数。
197
+ var archived = state.tasks.filter(function (t) { return t.status === 'archived' })
198
+ var list = archived.filter(function (t) {
189
199
  if (!q) return true
190
200
  var qq = q.toLowerCase()
191
201
  return (t.title || '').toLowerCase().indexOf(qq) >= 0 || (t.id || '').toLowerCase().indexOf(qq) >= 0 || (t.tags || []).join(' ').toLowerCase().indexOf(qq) >= 0
192
202
  }).sort(function (a, b) {
193
- // 完成时间最近在前:resolvedAt 优先,回退验收时间/归档时间/最后活动时间
194
- function doneTs(t) { return t.resolvedAt || (t.verification && t.verification.at) || t.archivedAt || taskLastTs(t) || '' }
195
- return (doneTs(b) || '').localeCompare(doneTs(a) || '')
203
+ // 归档时间倒序:archivedAt 优先,回退 resolvedAt/createdAt(老卡可能缺 archivedAt)
204
+ function archTs(t) { return t.archivedAt || t.resolvedAt || t.createdAt || '' }
205
+ return (archTs(b) || '').localeCompare(archTs(a) || '')
196
206
  })
197
- function restore(id) { rpc('update-task', { taskId: id, resetToPending: true }).then(function () { fetchArchived(); fetchTasks() }).catch(function () {}) }
207
+ function restore(id) { rpc('update-task', { taskId: id, resetToPending: true }).then(function () { fetchTasks() }).catch(function () {}) }
198
208
  return React.createElement('div', null,
199
209
  React.createElement('input', { value: q, onChange: function (e) { setQ(e.target.value); state.archQ = e.target.value }, placeholder: '检索标题 / ID / 标签…', style: { width: '100%', padding: '5px 8px', fontSize: 11, border: '1px solid ' + C.border2, borderRadius: 5, background: C.card, color: C.text, marginBottom: 8 } }),
200
- state.archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '暂无归档任务') :
210
+ React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, '共 ' + archived.length + ' 条归档(全量,与仪表盘「已归档」同口径)'),
211
+ archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '暂无归档任务') :
201
212
  list.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '无匹配结果') :
202
213
  list.map(function (t) {
203
214
  return React.createElement('div', { key: t.id, style: { padding: '6px 8px', marginBottom: 5, background: C.card, border: '1px solid ' + C.border, borderRadius: 6 } },
@@ -268,7 +279,7 @@
268
279
  }
269
280
  var inp = { width: '100%', boxSizing: 'border-box', fontSize: 11, padding: '4px 6px', border: '1px solid ' + C.border, borderRadius: 4, background: C.card, color: C.text, fontFamily: 'inherit' }
270
281
  var lblStyle = { fontSize: 10, color: C.text2, marginBottom: 3 }
271
- function field(label, node) { return React.createElement('div', { style: { marginBottom: 8 } }, React.createElement('div', { style: lblStyle }, label), node) }
282
+ function field(label, node, hint) { return React.createElement('div', { style: { marginBottom: 8 } }, React.createElement('div', { style: lblStyle }, label), node, hint ? React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2, lineHeight: 1.4 } }, hint) : null) }
272
283
  var btnGhost = { fontSize: 11, padding: '4px 12px', border: '1px solid ' + C.border, borderRadius: 5, background: 'transparent', color: C.text2, cursor: 'pointer' }
273
284
  var btnPrimary = { fontSize: 11, padding: '4px 14px', border: 'none', borderRadius: 5, background: C.brand, color: C_INV, fontWeight: 600, cursor: busy ? 'default' : 'pointer', opacity: busy ? 0.6 : 1 }
274
285
  // 视口级 overlay(修裁切,反馈 n-mut9rzl4mxe4):fixed 全屏遮罩脱离看板抽屉(maxHeight 60vh + overflow hidden)
@@ -288,8 +299,8 @@
288
299
  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 },
289
300
  React.createElement('option', { value: 'full' }, '全流程(执行+验证)'),
290
301
  React.createElement('option', { value: 'work' }, '免验证(只做不验)'),
291
- React.createElement('option', { value: 'direct' }, '主窗口处理'))))),
292
- field('touches(文件/glob,逗号或换行分隔,可空)', React.createElement('input', { value: touchesRaw, onChange: function (e) { setTouchesRaw(e.target.value) }, placeholder: 'src/a.mjs, src/**', style: inp })),
302
+ React.createElement('option', { value: 'direct' }, '主窗口处理')), '管线 = 任务走哪种流程:全流程 = Worker 做 + Verifier 验;免验证 = 只做不验;主窗口处理 = 你在对话里直接办'))),
303
+ field('touches(文件/glob,逗号或换行分隔,可空)', React.createElement('input', { value: touchesRaw, onChange: function (e) { setTouchesRaw(e.target.value) }, placeholder: 'src/a.mjs, src/**', style: inp }), 'touches = 本卡要改动的文件(glob 匹配);声明后并行任务间防互踩(文件锁)'),
293
304
  field('依赖(全部完成后才派发,可空)', cands.length === 0
294
305
  ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '当前无草稿/待办任务可选')
295
306
  : React.createElement('div', { style: { maxHeight: 96, overflowY: 'auto', border: '1px solid ' + C.border, borderRadius: 4, padding: '4px 6px', background: C.card } }, cands.map(function (x) {
@@ -297,7 +308,7 @@
297
308
  React.createElement('input', { type: 'checkbox', checked: !!depSel[x.id], onChange: function () { toggleDep(x.id) } }),
298
309
  React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, (statusLabels[x.status] || x.status) + ' · ' + (x.title || x.id)))
299
310
  }))),
300
- field('验收脚本(命令,可空)', React.createElement('input', { value: acc, onChange: function (e) { setAcc(e.target.value) }, placeholder: '如 node --test test/x.test.js', style: Object.assign({}, inp, { fontFamily: 'monospace' }) })),
311
+ field('验收脚本(命令,可空)', React.createElement('input', { value: acc, onChange: function (e) { setAcc(e.target.value) }, placeholder: '如 node --test test/x.test.js', style: Object.assign({}, inp, { fontFamily: 'monospace' }) }), '验收脚本 = 硬性验收命令:Worker 必须真实运行,Verifier 独立复跑'),
301
312
  React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: C.text, cursor: 'pointer', marginBottom: 6, flexWrap: 'wrap' } },
302
313
  React.createElement('input', { type: 'checkbox', checked: asDraft, onChange: function (e) { setAsDraft(e.target.checked) } }),
303
314
  '存为草稿',
@@ -309,5 +320,5 @@
309
320
  err ? React.createElement('div', { style: { fontSize: 11, color: C.err, marginBottom: 6 } }, err) : null,
310
321
  React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 6 } },
311
322
  React.createElement('button', { onClick: close, style: btnGhost }, '取消'),
312
- React.createElement('button', { onClick: submit, disabled: busy, title: asDraft ? '创建为草稿(不派发)' : '创建并立即进入派发池', style: btnPrimary }, busy ? '创建中…' : (asDraft ? '存为草稿' : '创建并派发'))))))
323
+ React.createElement('button', { onClick: submit, disabled: busy, title: asDraft ? '创建为草稿(不派发)' : '创建并立即交给 Worker 执行', style: btnPrimary }, busy ? '创建中…' : (asDraft ? '存为草稿' : '创建并交给 Worker'))))))
313
324
  }