@yangdcm/dsh-expert-team 1.3.16 → 1.3.17

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/CHANGELOG.md CHANGED
@@ -732,6 +732,47 @@ DAG 并行扇出、角色 chip、工件脚注全部保留。
732
732
  自学习 / 配额 / 冷启动清算
733
733
  - 零运行时依赖、无构建步骤、无安装钩子
734
734
 
735
+ ## 1.3.17
736
+
737
+ **重活有界化 + 「为谁而做」前置与其主视觉。**
738
+
739
+ ### 性能:把两段最贵的活变成"有界"(不是"更快")
740
+
741
+ 真机基线(30+ 子代理的重会话,用户实测):`?section=people,feed` 单发 **5.4–9.2 s**,分段
742
+ `subs` **2.9–3.7 s**、`roles` **2.4–5.5 s** —— 两段都**没有上限**,请求还压在事件循环上
743
+ (`summary` 与重活并发时从 16 ms 涨到 **772 ms**)。
744
+
745
+ - **`subs`**:枚举整棵 sessions 树是最贵的一步 ⇒ 两次之间至少 N 秒
746
+ (`DSH_EXPERT_TEAM_SUBS_ENUM_MIN_INTERVAL_MS`,默认 30 s),窗口内**零枚举**;另给枚举期限
747
+ (`DSH_EXPERT_TEAM_SUBS_DEADLINE_MS`,默认 800 ms),到点即停。被挡住的 id **不丢成员**:
748
+ 它只是没有 header,按既有口径如实显示"细节不可得"(不假装 0)。
749
+ - **`roles`**:读 MB 级子会话日志很贵且**不紧急** ⇒ 实时路径**默认零读**,未知角色如实显示为
750
+ **待解析**(新字段 `rolesPending`,替代旧名 `rolesDeferred`;客户端**真的渲染**它),解析交给
751
+ 低频后台(每轮 ≤`DSH_EXPERT_TEAM_ROLES_PER_BURST`(默认 1)条、两次间隔
752
+ ≥`DSH_EXPERT_TEAM_ROLES_MIN_INTERVAL_MS`(默认 30 s)),期限
753
+ `DSH_EXPERT_TEAM_ROLES_DEADLINE_MS`(默认 600 ms)。已解析结果永久缓存 ⇒ `rolesPending`
754
+ **单调下降收敛到 0**(旧实现每请求从零重算 ⇒ 实测 16→40 **上涨**)。
755
+ - **噪声纪律**(新):**节流不是失败**,绝不每轮进 `degraded` —— 节流窗口内每次轮询都会命中,
756
+ 否则面板会长期挂一个降级标记,把真告警一起降权。节流的事实由 `subsPending` / `rolesPending`
757
+ 诚实表达;`degraded` 只承载**真截断**(`subs:deadline` / `roles:deadline`)。"缺块 ≠ 空数据"不变。
758
+ - 护栏:新增 `state-bounded.test.mjs`(13 条)钉死"零枚举 / 期限即停 / 零读 / 收敛 / 字段单一真源
759
+ 且被渲染 / 节流不进 degraded"。
760
+ - 顺带修两处**源码级断言与文本强耦合**(加五行代码就假红,属"断言失去判据对象"那一类):
761
+ `run-ownership` 的固定 2600 字符窗口改为**语义锚点切片**;`state-sections` 的正则改为只表达
762
+ 意图(不再绑死参数表)。**断言本身一字未改**。
763
+ - **诚实边界**:端到端墙钟需实机复测(本机装不进运行中的宿主);本版给的是"机制级证据 +
764
+ 单位成本账"。默认值都可用上面的环境变量覆盖 —— 想更激进就调小间隔。
765
+
766
+ ### 文档:「为谁而做」前置 + 主视觉(图 1)
767
+
768
+ - 「为谁而做 / Who it is for」提到 hero + 指标行之后、`速览` 之前(中英同序);
769
+ - 新增 `docs/images/who-is-it-for.svg`(中)与 `.en.svg`(英,同布局同生成器):顶带主张
770
+ 「一句话 → 一支完整技术部」、左带三类对象、主带 **12 个岗位卡 4×3**、
771
+ 「**你 · lead**」橙色胶囊**独占一带**、底带诚实边界;纯矢量、系统字体栈、配色克制;
772
+ - **保留**「岗位 → 角色 → 做什么」对照表(图下方):**图给冲击力、表给检索**(表格是机读文本,
773
+ 比 SVG 里的字更可靠);图号顺延为 图 1…图 8(中英一致、无重复);图用绝对地址 + 关键词 alt;
774
+ - 自检:包围盒估算 → 两两相交 → 带约束 → 越界 → **文字溢出父容器**;首轮抓到英文 chip 超框
775
+ 16.8px 并修掉,最终两语言 无相交/无跨带/无越界/无溢出 + `xmllint` 良构;渲染由父会话浏览器核对。
735
776
  ## 1.3.16
736
777
 
737
778
  **三件收尾:`/state` 重活的最大一段(`subs`)、冷启动、以及 R1 的"bash 绕过"坦白。**
package/README.en.md CHANGED
@@ -22,31 +22,15 @@ A plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
22
22
 
23
23
  > Zero runtime dependencies. Recommended: also install **Hindsight** (cross-project memory) — see [Dependencies and recommended plugins](#dependencies-and-recommended-plugins).
24
24
 
25
- ## At a glance
26
-
27
- | Item | Value |
28
- |---|---|
29
- | Package | `@yangdcm/dsh-expert-team` (public npm package) |
30
- | Repository | <https://github.com/yangdcm/dsh-expert-team> |
31
- | Host | [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) ≥ **0.1.5-rc.1** (`web` profile) |
32
- | Runtime dependencies | **none** (`dependencies: {}`; `lib/` imports only sibling files and Node builtins) |
33
- | Node.js | ≥ 20 |
34
- | License | MIT |
35
- | Install (one line) | `dsh plugin --profile web add @yangdcm/dsh-expert-team` |
36
- | First run (one line) | switch the session to "Expert team mode" → `/team build a payments module with login` |
37
- | Process artifacts | `<your workspace>/team/<run-id>/` (`SPEC.md` · `PLAN.md` · `TASKS.json` · `REVIEW.md` · `TEST.md` · `SUMMARY.md` …) |
38
- | Machine-local data | `$DSH_HOME/expert-team/` (`settings.json` · `LEARNINGS.md` · `session-runs.json`) |
39
-
40
- > A dsh plugin · a DeepSeek Harness multi-agent (agent team) orchestrator: role-based subagents · DAG parallelism · staged gates · quality gates · artifact trail.
41
- >
42
- > Index for LLMs and retrieval: [`llms.txt`](https://github.com/yangdcm/dsh-expert-team/blob/main/llms.txt)
43
-
44
25
  ## Who it is for
45
26
 
46
27
  **A complete engineering department for small teams and solo builders** — no hiring, no assembling a team:
47
28
  one sentence spins up the twelve seats (product, architecture, research, UI/UX, backend, frontend, data,
48
29
  security, review, QA, devops, docs) and delivers through a 9-phase gated pipeline. Implementers edit your
49
30
  codebase directly, and everything is logged as reviewable artifacts.
31
+ ![Who it is for: a complete engineering department for small teams and solo builders — product, architecture, research, UI/UX, backend, frontend, data, security, review, QA, devops and docs woven into a 9-phase gated pipeline](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/who-is-it-for.en.svg)
32
+
33
+ <sub>Figure 1: **who it is for**. Three audiences (internal tools and product iteration in small companies · freelance and outsourced delivery · solo developers shipping a complete project) share one way of working: twelve department seats woven into a 9-phase gated pipeline. Note the amber strip at the bottom — **you (lead) sit on a different layer from the twelve seats**: you decide product-level and scope-level questions, the team drives the rest.</sub>
50
34
 
51
35
  | Department seat | Agent | What it does in the pipeline |
52
36
  |---|---|---|
@@ -70,12 +54,31 @@ solo developers shipping a complete project · any long task where "someone inde
70
54
 
71
55
  ![The expert-team 9-phase gated pipeline: clarify → research → design → spec-review (hard gate) → plan approval → implement (DAG parallel) → review → test → deliver](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/pipeline.svg)
72
56
 
73
- <sub>Figure 1: the 9-phase gated pipeline. `spec-review` is a **hard gate** — if the SPEC.md
57
+ <sub>Figure 2: the 9-phase gated pipeline. `spec-review` is a **hard gate** — if the SPEC.md
74
58
  "boundaries and prohibitions" section is empty, the run does not advance (`lib/interception.js`).
75
59
  `plan-approval` is an approval gate that is **on by default** (`identity.keepPlanGate`, can be turned off in settings).
76
60
  The `implement` phase **fans out** along the dependency DAG: several implementers start at once, each touching only its own files.</sub>
77
61
 
78
62
  ---
63
+ ## At a glance
64
+
65
+ | Item | Value |
66
+ |---|---|
67
+ | Package | `@yangdcm/dsh-expert-team` (public npm package) |
68
+ | Repository | <https://github.com/yangdcm/dsh-expert-team> |
69
+ | Host | [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) ≥ **0.1.5-rc.1** (`web` profile) |
70
+ | Runtime dependencies | **none** (`dependencies: {}`; `lib/` imports only sibling files and Node builtins) |
71
+ | Node.js | ≥ 20 |
72
+ | License | MIT |
73
+ | Install (one line) | `dsh plugin --profile web add @yangdcm/dsh-expert-team` |
74
+ | First run (one line) | switch the session to "Expert team mode" → `/team build a payments module with login` |
75
+ | Process artifacts | `<your workspace>/team/<run-id>/` (`SPEC.md` · `PLAN.md` · `TASKS.json` · `REVIEW.md` · `TEST.md` · `SUMMARY.md` …) |
76
+ | Machine-local data | `$DSH_HOME/expert-team/` (`settings.json` · `LEARNINGS.md` · `session-runs.json`) |
77
+
78
+ > A dsh plugin · a DeepSeek Harness multi-agent (agent team) orchestrator: role-based subagents · DAG parallelism · staged gates · quality gates · artifact trail.
79
+ >
80
+ > Index for LLMs and retrieval: [`llms.txt`](https://github.com/yangdcm/dsh-expert-team/blob/main/llms.txt)
81
+
79
82
 
80
83
  ## In 30 seconds
81
84
 
@@ -118,32 +121,32 @@ answer them:
118
121
 
119
122
  ![Expert-team full-screen canvas: phase bar, progress, and the role-based subagent roster (who is running, on which model)](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/canvas.png)
120
123
 
121
- <sub>Figure 2: **the full-screen canvas**. Look at the phase bar and progress, the roster (who is running, on which model), and the four views (people / tasks / artifacts / board) — one layer above the floating panel.</sub>
124
+ <sub>Figure 3: **the full-screen canvas**. Look at the phase bar and progress, the roster (who is running, on which model), and the four views (people / tasks / artifacts / board) — one layer above the floating panel.</sub>
122
125
 
123
126
  ![Expert-team task dependency graph: tasks running in parallel along the DAG, including the repair/review rework loop](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/canvas-tasks.png)
124
127
 
125
- <sub>Figure 3: **the task dependency graph** — 11 tasks advance in parallel along the dependency DAG; 7 completed, 4 failed. A failure triggers `repair` plus **independent re-verification** (`repair-1 → review-2 → repair-2 → review-3`) until it passes or is honestly marked as needing revision — this is what the "rework does not converge" hard gate looks like in a real run.</sub>
128
+ <sub>Figure 4: **the task dependency graph** — 11 tasks advance in parallel along the dependency DAG; 7 completed, 4 failed. A failure triggers `repair` plus **independent re-verification** (`repair-1 → review-2 → repair-2 → review-3`) until it passes or is honestly marked as needing revision — this is what the "rework does not converge" hard gate looks like in a real run.</sub>
126
129
 
127
130
  ![Expert-team quality-gate violation banner: a missing spec boundary blocked by plugin code, not by a prompt](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/panel-gate.png)
128
131
 
129
- <sub>Figure 4: **gate violations**. Look at the banner at the top — the violation and its refusal reason
132
+ <sub>Figure 5: **gate violations**. Look at the banner at the top — the violation and its refusal reason
130
133
  (e.g. "SPEC.md's boundary section has entered `implement` but still has no 'expected rejection' row")
131
134
  is decided by `lib/interception.js`, hooked onto the host's `tools/post-execute` waterfall, and surfaced
132
135
  immediately. This is code, not a prompt reminder.</sub>
133
136
 
134
137
  ![Expert-team overlay: role members, the model each one uses, task detail and artifact preview](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/panel-live.png)
135
138
 
136
- <sub>Figure 5: **the roster**. Look at the member list — who is running, on which model, and what it is doing;
139
+ <sub>Figure 6: **the roster**. Look at the member list — who is running, on which model, and what it is doing;
137
140
  expand a member for its tasks and artifacts. Models are configurable per role; heterogeneous models are used for cross-checking.</sub>
138
141
 
139
142
  ![Expert-team phase progress: current and completed phases, plus the artifact body written at that phase](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/panel-flow.png)
140
143
 
141
- <sub>Figure 6: **phases and artifacts**. Look at the phase bar and the preview pane — the current phase, the phases
144
+ <sub>Figure 7: **phases and artifacts**. Look at the phase bar and the preview pane — the current phase, the phases
142
145
  already passed, and the actual body of the artifact written in that phase (artifacts are the single source of truth; the overlay is just a view of them).</sub>
143
146
 
144
147
  ![The expert-team section inside the official DeepSeek Harness settings page: 18 settings, Chinese labels, applied on change](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/settings.png)
145
148
 
146
- <sub>Figure 7: **settings**. Look at the official `Settings → Expert team` page — 18 settings, Chinese labels,
149
+ <sub>Figure 8: **settings**. Look at the official `Settings → Expert team` page — 18 settings, Chinese labels,
147
150
  **saved on change and applied immediately** (caps, rounds, the tier gate and the oscillation detector are recomputed
148
151
  in-process). Values live in the host namespace `expert-team`, so they travel with the plugin market's backup/restore.</sub>
149
152
 
package/README.md CHANGED
@@ -20,30 +20,14 @@
20
20
 
21
21
  > 零运行时依赖。推荐同时装 **Hindsight**(跨项目记忆)—— 见[依赖与推荐插件](#依赖与推荐插件--dependencies-and-recommended-plugins)。
22
22
 
23
- ## 速览 / At a glance
24
-
25
- | 项目 | 值 |
26
- |---|---|
27
- | 包名 | `@yangdcm/dsh-expert-team`(npm 公开包) |
28
- | 仓库 | <https://github.com/yangdcm/dsh-expert-team> |
29
- | 宿主 | [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) ≥ **0.1.5-rc.1**(`web` profile) |
30
- | 运行时依赖 | **无**(`dependencies: {}`;`lib/` 只 import 同目录文件与 Node 内建) |
31
- | Node.js | ≥ 20 |
32
- | License | MIT |
33
- | 安装(一行) | `dsh plugin --profile web add @yangdcm/dsh-expert-team` |
34
- | 上手(一行) | 会话切到「专家团模式」→ `/team 做一个带登录的支付模块` |
35
- | 过程产物 | `<你的工作区>/team/<run-id>/`(`SPEC.md` · `PLAN.md` · `TASKS.json` · `REVIEW.md` · `TEST.md` · `SUMMARY.md` …) |
36
- | 本机数据 | `$DSH_HOME/expert-team/`(`settings.json` · `LEARNINGS.md` · `session-runs.json`) |
37
-
38
- > dsh 插件 · DeepSeek Harness 多智能体(multi-agent)编排器:角色化 subagent 团队 · 依赖 DAG 并行 · 阶段门控 · 质量门禁 · 工件留痕。
39
- >
40
- > 给 LLM / 检索用的索引:[`llms.txt`](https://github.com/yangdcm/dsh-expert-team/blob/main/llms.txt)
41
-
42
23
  ## 为谁而做
43
24
 
44
25
  **给中小团队与个人接单者的一支「完整技术部」** —— 不用招人、不用攒团队:一句话拉起产品、架构、调研、
45
26
  UI/UX、前后端、数据、安全、评审、测试、运维、文档这 12 个岗位,按 9 阶段门控流程交付,
46
27
  实现者直接改你的代码库,全程留痕成可复核的工件。
28
+ ![为谁而做:给中小团队与个人接单者的一支完整技术部 —— 产品经理/架构师/技术调研/UI/UX/后端/前端/数据/安全审计/代码评审/测试/运维/技术文档 12 个岗位编织进 9 阶段门控流水线](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/who-is-it-for.svg)
29
+
30
+ <sub>图 1:**为谁而做**。三类对象(中小公司内部工具与产品迭代 · 个人接单 / 外包交付 · 独立开发者做完整项目)共用同一套做法:把 12 个技术部岗位编织进一条 9 阶段门控流水线。注意底部那条橙色带 —— **你(lead)与 12 个岗位不在同一层**:你把关产品级与范围级决策,其余由团队推进。</sub>
47
31
 
48
32
  | 技术部岗位 | 角色 | 在这个流程里做什么 |
49
33
  |---|---|---|
@@ -67,9 +51,28 @@ UI/UX、前后端、数据、安全、评审、测试、运维、文档这 12
67
51
 
68
52
  ![专家团 9 阶段门控流水线:澄清→调研→设计→规格评审(硬门)→方案确认→实现(依赖 DAG 并行)→审查→测试→交付](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/pipeline.svg)
69
53
 
70
- <sub>图 1:9 阶段门控流水线。「规格评审」是**硬门** —— SPEC.md 的「边界与禁止项」没填就不放行(`lib/interception.js`);「方案确认」是默认开启的**确认门**(`identity.keepPlanGate`,可在设置里关掉);「实现」阶段按依赖 DAG **并行扇出**,多个实现者同时开工、各自只改自己那份文件。</sub>
54
+ <sub>图 2:9 阶段门控流水线。「规格评审」是**硬门** —— SPEC.md 的「边界与禁止项」没填就不放行(`lib/interception.js`);「方案确认」是默认开启的**确认门**(`identity.keepPlanGate`,可在设置里关掉);「实现」阶段按依赖 DAG **并行扇出**,多个实现者同时开工、各自只改自己那份文件。</sub>
71
55
 
72
56
  ---
57
+ ## 速览 / At a glance
58
+
59
+ | 项目 | 值 |
60
+ |---|---|
61
+ | 包名 | `@yangdcm/dsh-expert-team`(npm 公开包) |
62
+ | 仓库 | <https://github.com/yangdcm/dsh-expert-team> |
63
+ | 宿主 | [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) ≥ **0.1.5-rc.1**(`web` profile) |
64
+ | 运行时依赖 | **无**(`dependencies: {}`;`lib/` 只 import 同目录文件与 Node 内建) |
65
+ | Node.js | ≥ 20 |
66
+ | License | MIT |
67
+ | 安装(一行) | `dsh plugin --profile web add @yangdcm/dsh-expert-team` |
68
+ | 上手(一行) | 会话切到「专家团模式」→ `/team 做一个带登录的支付模块` |
69
+ | 过程产物 | `<你的工作区>/team/<run-id>/`(`SPEC.md` · `PLAN.md` · `TASKS.json` · `REVIEW.md` · `TEST.md` · `SUMMARY.md` …) |
70
+ | 本机数据 | `$DSH_HOME/expert-team/`(`settings.json` · `LEARNINGS.md` · `session-runs.json`) |
71
+
72
+ > dsh 插件 · DeepSeek Harness 多智能体(multi-agent)编排器:角色化 subagent 团队 · 依赖 DAG 并行 · 阶段门控 · 质量门禁 · 工件留痕。
73
+ >
74
+ > 给 LLM / 检索用的索引:[`llms.txt`](https://github.com/yangdcm/dsh-expert-team/blob/main/llms.txt)
75
+
73
76
 
74
77
  ## 30 秒看懂
75
78
 
@@ -109,27 +112,27 @@ $ /team 做一个带登录的支付模块
109
112
 
110
113
  ![专家团全屏画布:阶段条与进度、角色化 subagent 团队编制(谁在跑、用哪个模型)](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/canvas.png)
111
114
 
112
- <sub>图 2:**全屏画布**。看阶段条与进度、团队编制(谁在跑、用哪个模型)、以及 `人 / 事 / 料 / 盘` 四个视角 —— 比浮层更完整的一层视图。</sub>
115
+ <sub>图 3:**全屏画布**。看阶段条与进度、团队编制(谁在跑、用哪个模型)、以及 `人 / 事 / 料 / 盘` 四个视角 —— 比浮层更完整的一层视图。</sub>
113
116
 
114
117
  ![专家团任务依赖图:任务按依赖 DAG 并行,含 repair 与 review 的返工闭环](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/canvas-tasks.png)
115
118
 
116
- <sub>图 3:**任务依赖图** —— 11 个任务按依赖 DAG 并行推进;7 个完成、4 个失败。失败会触发 `repair` 与**独立复验**(`repair-1 → review-2 → repair-2 → review-3`),直到通过或被如实判为需修订 —— 这就是「返工不收敛」的硬门禁在真实运行里的样子。</sub>
119
+ <sub>图 4:**任务依赖图** —— 11 个任务按依赖 DAG 并行推进;7 个完成、4 个失败。失败会触发 `repair` 与**独立复验**(`repair-1 → review-2 → repair-2 → review-3`),直到通过或被如实判为需修订 —— 这就是「返工不收敛」的硬门禁在真实运行里的样子。</sub>
117
120
 
118
121
  ![专家团质量门禁违规实时横幅:规格边界未填即被插件代码拦下](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/panel-gate.png)
119
122
 
120
- <sub>图 4:**门禁违规**。看顶部那条横幅 —— 违规项与拒绝理由(例如"SPEC.md 的边界章节已进入 `implement` 但仍无任何一行填写")由 `lib/interception.js` 挂在宿主 `tools/post-execute` 上当场判出后推出,不是提示词提醒。</sub>
123
+ <sub>图 5:**门禁违规**。看顶部那条横幅 —— 违规项与拒绝理由(例如"SPEC.md 的边界章节已进入 `implement` 但仍无任何一行填写")由 `lib/interception.js` 挂在宿主 `tools/post-execute` 上当场判出后推出,不是提示词提醒。</sub>
121
124
 
122
125
  ![专家团浮层:角色成员列表、各自使用的模型、任务详情与工件预览](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/panel-live.png)
123
126
 
124
- <sub>图 5:**角色编制**。看成员列表 —— 谁在跑、用哪个模型、当前在做什么;展开任一成员可看它的任务与产物。模型可按角色分别配置,异构模型用于交叉验证。</sub>
127
+ <sub>图 6:**角色编制**。看成员列表 —— 谁在跑、用哪个模型、当前在做什么;展开任一成员可看它的任务与产物。模型可按角色分别配置,异构模型用于交叉验证。</sub>
125
128
 
126
129
  ![专家团阶段推进视图:当前阶段、已过阶段与该阶段的工件正文](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/panel-flow.png)
127
130
 
128
- <sub>图 6:**阶段与工件**。看阶段条与预览区 —— 当前阶段、已过阶段、以及该阶段真正写下的工件正文(工件是唯一真源,浮层只是它的视图)。</sub>
131
+ <sub>图 7:**阶段与工件**。看阶段条与预览区 —— 当前阶段、已过阶段、以及该阶段真正写下的工件正文(工件是唯一真源,浮层只是它的视图)。</sub>
129
132
 
130
133
  ![DeepSeek Harness 官方设置页里的「专家团」分节:18 个设置项、中文标签、改动即时生效](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/settings.png)
131
134
 
132
- <sub>图 7:**设置**。看官方 `设置 →「专家团」` 这一页 —— 18 个设置项、中文标签、**改动即保存并即时生效**(上限/轮次/档位门/振荡检测在进程内重算);值存在宿主命名空间 `expert-team`,随插件市场的备份/恢复一起走。</sub>
135
+ <sub>图 8:**设置**。看官方 `设置 →「专家团」` 这一页 —— 18 个设置项、中文标签、**改动即保存并即时生效**(上限/轮次/档位门/振荡检测在进程内重算);值存在宿主命名空间 `expert-team`,随插件市场的备份/恢复一起走。</sub>
133
136
 
134
137
  **输入框正上方还有一条常驻状态条**(client 槽 `conversation.input.dock`,id `expert-team-subagents`,order 200)—— 有子代理在跑时是琥珀色横幅「N 个子代理运行中」+ 最多 3 个角色名 + 一个跳动圆点,点击它直接打开团队面板;没有在跑时只剩一行暗灰字「无子代理在运行」,会话或状态尚未就绪时则完全不渲染(判据与页头徽章同一条:`/state` 的 `agents[].activity === 'running'`)。
135
138
 
package/client.js CHANGED
@@ -2194,6 +2194,11 @@ window.__ModuleLoader__.load({
2194
2194
  wfRunBlock,
2195
2195
  peopleNoteBlock,
2196
2196
  roster.length ? roster : h('div', { className: 'exp-empty' }, t('(暂无成员)', '(no members)')),
2197
+ // 有界化(2026-09-15):实时路径**不读**大日志解析角色 ⇒ 未知角色是"**待解析**",
2198
+ // 与下面那段"确实解析不出角色"是**两件事**(本仓纪律:两种零必须分得开,别混成一句话)。
2199
+ (data && Number(data.rolesPending) > 0) ? h('div', { className: 'exp-legend', key: 'roles-pending' },
2200
+ t('另有 ' + data.rolesPending + ' 条子代理的角色**待解析**(实时路径不读大日志,后台低频解析中;"待解析"≠"解析不出来")',
2201
+ 'Roles for ' + data.rolesPending + ' subagent(s) are still **pending** (the live path does not read large logs; pending is not the same as unresolvable)')) : null,
2197
2202
  // 真实性提示:活子代理里没被名册认领的分两类,必须分开说 ——
2198
2203
  // ① 同角色的重复派工/历史 leg(**有角色**,只是每个角色只展示 1 个成员)
2199
2204
  // ② 真的解析不出角色的(label 为空、子会话日志不可读、事件流里也没有派工 label)
@@ -2500,10 +2505,25 @@ window.__ModuleLoader__.load({
2500
2505
  if (!sid || typeof fetch === 'undefined') return
2501
2506
  stateHubFetch(liveUrl(sid), liveDeliver)
2502
2507
  }
2508
+ // 会话 id → 角色名(来自 /state 的 agents[],**只用于把 id 翻译成可读名字**)。
2509
+ // 为什么不参与判定:判定必须与页头同源(宿主会话态)。这份映射只是显示层的润色,
2510
+ // 缺了它就回落 id 前 8 位;它来自本来就有的 /state 拉取(页头徽章/面板),**不新增任何请求**。
2511
+ var liveRoles = {}
2512
+ function harvestRoles(d) {
2513
+ try {
2514
+ var arr = (d && Array.isArray(d.agents)) ? d.agents : null
2515
+ if (!arr) return
2516
+ for (var i = 0; i < arr.length; i++) {
2517
+ var a = arr[i]
2518
+ if (a && a.id && a.role) liveRoles[String(a.id)] = String(a.role)
2519
+ }
2520
+ } catch (e) {}
2521
+ }
2503
2522
  function liveDeliver(d) {
2504
2523
  if (!d) return
2505
2524
  liveStore.data = d
2506
2525
  try { harvestActivity(d) } catch (e) {}
2526
+ try { harvestRoles(d) } catch (e) {}
2507
2527
  try { stateHub.busy = teamBusy(d) } catch (e) {}
2508
2528
  liveStore.subs.forEach(function (f) { try { f(d) } catch (e) {} })
2509
2529
  }
@@ -2512,30 +2532,81 @@ window.__ModuleLoader__.load({
2512
2532
  return Object.keys(m).some(function (k) { var v = m[k]; return v && typeof v === 'object' && (v.activity === 'running' || v.shortStatus === 'running') })
2513
2533
  }
2514
2534
  // ── 子代理运行状态条的**唯一**判据(纯函数,无 React、无 DOM:便于单测直接断言)──────
2515
- // 为什么用 /state `agents[]` 而不是别的:它由 host 的 `subagents.listChildren` 产出,
2516
- // `activity === 'running'` 正是页头徽章用的同一个判据(两种指示同源,不会互相打架)。
2517
- // 两种"零"必须分清:`agents` 缺键/非数组(老 host、ok:false、?section=summary)是"未知",
2518
- // 只有拿到数组且里面没有 running 才是"确实没有人在跑"。未知时显示占位符,不谎报"无人在跑"。
2519
- /** 从一份 /state 负载里取出 activity === 'running' 的子代理行(非数组/缺键 ⇒ 空数组)。 */
2520
- function runningAgents(d) {
2521
- var arr = (d && Array.isArray(d.agents)) ? d.agents : []
2522
- return arr.filter(function (a) { return a && String(a.activity || '') === 'running' })
2535
+ // 与页头「N 个子代理」**同源**:宿主会话态(host session store)里 `running` 为真的子代理。
2536
+ // 之前的版本读插件自己的 `/state` → `agents[].activity === 'running'`,那是**错的**:服务端把
2537
+ // 子代理语料的归属会话解析成「当前 run 的 ownerSession」(`lib/command.js` `peopleSid`),
2538
+ // 客户端传的 sessionId 会被忽略 ⇒ 新 run 还没落 `STATE.json` 时视图会落到同工作区的旧 run 上,
2539
+ // 面板拿到别人的(已冷的)子代理,状态条就谎报「无子代理在运行」。
2540
+ // 两种"零"必须分清:拿不到会话态(null 快照 / 无 sid)是"未知"⇒ 不渲染;只有拿到会话态且
2541
+ // 里面没有 running 的子代理后代,才是"确实没有人在跑"。
2542
+
2543
+ /**
2544
+ * 正在运行的子代理 id(宿主会话态的 `running` 字段,**与页头「N 个子代理」同源**)。
2545
+ *
2546
+ * 为什么不再用插件自己的 `/state` → `agents[].activity`:服务端把子代理语料的**归属会话**
2547
+ * 解析成「当前 run 的 ownerSession」(`lib/command.js` 的 `peopleSid`),于是**客户端传的
2548
+ * sessionId 会被忽略**。真机实测的翻车场景:一个刚开跑、还没落 `STATE.json` 的新 run,
2549
+ * 在 `newestRun` 按 `updatedAt` 排序时输给同工作区一个 13 天前的旧 run,于是本会话明明
2550
+ * 有子代理在跑,面板却拿到旧 run 的 82 个**已冷**子代理(`activity:'inactive'`)⇒ 状态条
2551
+ * 谎报「无子代理在运行」。改用宿主会话态后,既与页头同源,又不再经过那条会串会话的解析。
2552
+ *
2553
+ * 两个集合都要:`byId` 给 `running`/`origin`/`parentId`,`items` 给直接子会话
2554
+ * (`parentSessionId` + `origin:'subagent'`)—— 只靠其一都会漏(与页头同一套数据源)。
2555
+ * 计数按**会话是当前会话的子代理后代**判定(任一深度),不是只数直接子级。
2556
+ */
2557
+ function runningSubagentIds(sessions, sid) {
2558
+ var out = []
2559
+ if (!sid || !sessions || typeof sessions !== 'object') return out
2560
+ var byId = sessions.byId || {}
2561
+ var items = Array.isArray(sessions.items) ? sessions.items : []
2562
+ var seen = {}
2563
+ function consider(id, running) {
2564
+ if (!id) return
2565
+ var k = String(id)
2566
+ if (seen[k] || !running) return
2567
+ seen[k] = 1
2568
+ out.push(k)
2569
+ }
2570
+ // ① 直接子会话:列表项自带 parentSessionId + origin
2571
+ items.forEach(function (s) {
2572
+ if (!s) return
2573
+ if (s.origin !== 'subagent') return
2574
+ if (String(s.parentSessionId || '') !== String(sid)) return
2575
+ consider(s.sessionId, s.running === true)
2576
+ })
2577
+ // ② 任一深度的后代:沿 byId 的 parentId 往上走,只有 origin==='subagent' 才继续
2578
+ Object.keys(byId).forEach(function (key) {
2579
+ var e = byId[key]
2580
+ if (!e || e.origin !== 'subagent') return
2581
+ var cur = e
2582
+ var guard = 0
2583
+ while (cur && cur.parentId !== undefined && guard++ < 64) {
2584
+ if (String(cur.parentId) === String(sid)) { consider(key, e.running === true); return }
2585
+ var up = byId[String(cur.parentId)]
2586
+ if (!up || up.origin !== 'subagent') return
2587
+ cur = up
2588
+ }
2589
+ })
2590
+ return out
2523
2591
  }
2592
+
2524
2593
  /**
2525
2594
  * 状态条视图模型(纯函数)。返回值:
2526
- * null → 不渲染(没有会话 / 还没拿到负载 / 拿不到 agents 块)
2595
+ * null → 不渲染(没有会话 / 会话态还没到)
2527
2596
  * { kind: 'idle', text } → 一行灰字:确实没有子代理在跑
2528
2597
  * { kind: 'busy', n, text, names } → 运行中横幅
2529
- * 角色名取不到就回落到 id 前 8 位(如实显示"这是谁",不编造角色名)。
2598
+ * `names` 取不到角色名时回落 id 前 8 位(如实显示"这是谁",不编造角色名)。
2530
2599
  */
2531
- function subagentBarModel(d, hasSession) {
2532
- if (!hasSession) return null
2533
- if (!d || d.ok !== true || !Array.isArray(d.agents)) return null
2534
- var run = runningAgents(d)
2600
+ function subagentBarModel(sessions, sid) {
2601
+ if (!sid || !sessions || typeof sessions !== 'object') return null
2602
+ if (!sessions.byId && !Array.isArray(sessions.items)) return null
2603
+ // `/state` role 名(可选):只用于把 id 翻成更好读的角色名,**不参与任何判定**
2604
+ var roles = (sessions.__roles && typeof sessions.__roles === 'object') ? sessions.__roles : {}
2605
+ var run = runningSubagentIds(sessions, sid)
2535
2606
  if (!run.length) return { kind: 'idle', n: 0, text: t('无子代理在运行', 'No subagents running'), names: [] }
2536
- var names = run.slice(0, 3).map(function (a) {
2537
- var nm = roleLabel(a.role)
2538
- return nm || String(a.id || '').slice(0, 8)
2607
+ var names = run.slice(0, 3).map(function (id) {
2608
+ var nm = roleLabel(roles[id])
2609
+ return nm || String(id).slice(0, 8)
2539
2610
  })
2540
2611
  var more = run.length > 3 ? ' +' + (run.length - 3) : ''
2541
2612
  return {
@@ -2555,6 +2626,7 @@ window.__ModuleLoader__.load({
2555
2626
  // 收到一份只有 summary 的负载就把已知的成员/活动抹掉。
2556
2627
  liveStore.data = Object.assign({}, liveStore.data || {}, d)
2557
2628
  try { harvestActivity(liveStore.data) } catch (e) {}
2629
+ try { harvestRoles(d) } catch (e) {}
2558
2630
  try { stateHub.busy = teamBusy(liveStore.data) } catch (e) {}
2559
2631
  liveStore.subs.forEach(function (f) { try { f(liveStore.data) } catch (e) {} })
2560
2632
  }
@@ -2627,13 +2699,47 @@ window.__ModuleLoader__.load({
2627
2699
  /**
2628
2700
  * 「子代理运行中」状态条(注册进 conversation.input.dock ⇒ 输入框正上方)。
2629
2701
  * 常驻:运行中给醒目横幅,无人运行给一行灰字(用户要求"能一眼区分有没有在跑")。
2702
+ * 判据 = 宿主会话态(与页头「N 个子代理」**同源**),**不再**读插件自己的 `/state`:
2703
+ * 那条路会把子代理语料的归属会话解析成「当前 run 的 ownerSession」,客户端传的 sessionId
2704
+ * 会被忽略 ⇒ 新 run 还没落 `STATE.json` 时视图落到旧 run,状态条谎报「无子代理在运行」。
2705
+ * 取数全程 try/catch:宿主 API 形状一变就退化成"不渲染这一条",绝不把界面炸掉。
2630
2706
  */
2631
2707
  function SubagentBar(props) {
2632
- var sid = useCurrentSessionId()
2633
- var barSessionId = (props && props.sessionId) || sid
2634
- var live = useLiveState(barSessionId)
2708
+ // 会话 id 的解法与之前一致;`useCurrentSessionId()` **无条件**先调(钩子顺序必须稳定,
2709
+ // 不能因为这一帧有没有 props.sessionId 就少调一次)。
2710
+ var fallbackSid = useCurrentSessionId()
2635
2711
  ensureCss()
2636
- var m = subagentBarModel(live, !!barSessionId)
2712
+ var sid = (props && props.sessionId) || fallbackSid
2713
+ var snap = null
2714
+ var useSessions = props && props.useSessions
2715
+ if (typeof useSessions === 'function') {
2716
+ // 标准 prop:宿主把会话 store 的 hook 直接递给槽组件(页头「N 个子代理」用的同一份数据)。
2717
+ try { snap = useSessions(function (s) { return s }) } catch (e) { snap = null }
2718
+ } else {
2719
+ // 降级:宿主没递标准 prop(API 形状变了)时,直接向 `ctx.sessions` 的**同一个**会话
2720
+ // store 要列表快照(`{ ids, byId, current, … }`)。任何一步拿不到或抛错 ⇒ 保持 null
2721
+ // ⇒ subagentBarModel 返回 null ⇒ 不渲染(宁可不显示,也不显示"别人的会话"的子代理数)。
2722
+ try {
2723
+ var svc = ctxRoot && ctxRoot.sessions ? ctxRoot.sessions : null
2724
+ var list = svc && svc.list ? svc.list : null
2725
+ var st = (list && typeof list.getSnapshot === 'function') ? list.getSnapshot() : null
2726
+ if (!st && svc && typeof svc.getSnapshot === 'function') st = svc.getSnapshot()
2727
+ if (st && typeof st === 'object') snap = st
2728
+ } catch (e) { snap = null }
2729
+ }
2730
+ // 角色映射只影响**命名**,不参与判定:把它挂在交给 `subagentBarModel` 的输入上。
2731
+ // ⚠️ **不能写进 `snap`**:那是宿主会话 store 的共享/缓存快照,写进去会污染页头等别的视图。
2732
+ // 因此需要时才做一层浅拷贝(只复制自有键),再挂 `__roles`;任何一步出错都退回原快照 ⇒
2733
+ // 最坏也只是回落 id 前 8 位,绝不把状态条弄没。
2734
+ var modelInput = snap
2735
+ try {
2736
+ if (snap && typeof snap === 'object' && snap.__roles !== liveRoles) {
2737
+ modelInput = {}
2738
+ for (var k in snap) { if (Object.prototype.hasOwnProperty.call(snap, k)) modelInput[k] = snap[k] }
2739
+ modelInput.__roles = liveRoles
2740
+ }
2741
+ } catch (e) { modelInput = snap }
2742
+ var m = subagentBarModel(modelInput, sid)
2637
2743
  if (!m) return null
2638
2744
  if (m.kind === 'idle') return h('div', { className: 'exp-subbusy-idle' }, esc(m.text))
2639
2745
  function openPanel() {
@@ -2782,11 +2888,16 @@ window.__ModuleLoader__.load({
2782
2888
  // suite assert text safety without booting the overlay in a browser.
2783
2889
  exports._live = { esc: esc, tierBadge: tierBadge, TIER_LABELS_ZH: TIER_LABELS_ZH, settingsFormModel: settingsFormModel }
2784
2890
  // 测试钩子(沿用 `_live` 的约定):状态条的纯函数可脱离浏览器直接断言。
2785
- exports._subagentBar = { subagentBarModel: subagentBarModel, runningAgents: runningAgents }
2891
+ exports._subagentBar = { subagentBarModel: subagentBarModel, runningSubagentIds: runningSubagentIds }
2786
2892
  exports.inject = ['slots', 'sessions', 'remote', 'uiSession', 'uiConversation', 'locale']
2787
2893
  var ctxUISession = null
2894
+ // 宿主 ctx 本体:只给状态条**降级**取数用(标准 prop `useSessions` 拿不到时,退到
2895
+ // `ctx.sessions.list` 的同一份会话 store 快照)。声明位置与 `ctxUISession` 同理:
2896
+ // var 提升到工厂作用域,组件在 apply() 之后才渲染,读到的一定是已赋值的引用。
2897
+ var ctxRoot = null
2788
2898
  exports.apply = function (ctx) {
2789
2899
  console.log('[dsh-expert-team] client apply() called')
2900
+ try { ctxRoot = ctx || null } catch (e) { ctxRoot = null }
2790
2901
  try { ctxUISession = ctx && ctx.uiSession ? ctx.uiSession : null } catch (e) { ctxUISession = null }
2791
2902
  try {
2792
2903
  // N2:待拍板 pendingInteraction 发布(composer select 据此接管)
package/lib/command.js CHANGED
@@ -3260,18 +3260,27 @@ function _resetListSessionsCache() { LIST_SESSIONS_CACHE = { at: 0, stamp: null,
3260
3260
  // 旧实现每请求都要枚举 475 个 artifact 才能回答"这些人是何时建的、谁派工的"——
3261
3261
  // 有了备忘,第二个请求起直接查表。
3262
3262
  const SUB_HEADER_MEMO = new Map();
3263
- const SUB_HEADER_STATS = { memoHits: 0, memoWrites: 0, enumCalls: 0 };
3263
+ // `cut`(2026-09-15 有界化)如实记录这一段被截断/节流的原因,供调用方进 `degraded`:
3264
+ // '' = 未截断|'throttled' = 本轮在节流窗口内、未枚举|'deadline' = 枚举到期限被截断
3265
+ const SUB_HEADER_STATS = { memoHits: 0, memoWrites: 0, enumCalls: 0, cut: '' };
3264
3266
  function _resetSubHeaderMemo() {
3265
3267
  SUB_HEADER_MEMO.clear();
3266
3268
  SUB_HEADER_STATS.memoHits = 0;
3267
3269
  SUB_HEADER_STATS.memoWrites = 0;
3268
3270
  SUB_HEADER_STATS.enumCalls = 0;
3271
+ SUB_HEADER_STATS.cut = '';
3269
3272
  }
3270
3273
 
3271
- async function listSubagentStatusBySession(ctx, sid, knownIds) {
3274
+ async function listSubagentStatusBySession(ctx, sid, knownIds, opts) {
3272
3275
  // 只认真 id:调用方若传进角色名(历史 bug,见 `memberAgentIds` 的注释),枚举分支会**永久**
3273
3276
  // 认为"还有人查不到" ⇒ 每请求重枚举。这里再兜一道,防止未来又有人把角色名传进来。
3274
3277
  const ids = (Array.isArray(knownIds) ? knownIds : []).filter(isAgentIdLike);
3278
+ // 有界化(2026-09-15):枚举整棵 sessions 树是最贵的一步(真机实测 2.4–3.6 s,且它是
3279
+ // **全库**扫描)。调用方可以(a)在节流窗口内**禁止枚举**、(b)给一个期限。两者都不命中时
3280
+ // 保持原行为(默认允许 + 无期限)。被挡住/截断时**不丢成员**:那些 id 只是没有 header ⇒
3281
+ // 既有口径已如实显示为"细节不可得"(`hasTimestamp:false`),不假装 0。
3282
+ const allowEnum = !(opts && opts.allowEnum === false);
3283
+ const enumDeadlineAt = (opts && Number.isFinite(opts.enumDeadlineAt)) ? opts.enumDeadlineAt : Infinity;
3275
3284
  const root = sid ? await rootSessionId(ctx, sid) : '';
3276
3285
  let rows = [];
3277
3286
  if (root) {
@@ -3322,12 +3331,15 @@ async function listSubagentStatusBySession(ctx, sid, knownIds) {
3322
3331
  }
3323
3332
  rows = [...memoById.values()];
3324
3333
  if (stillUnknown.length) {
3325
- try {
3334
+ if (!allowEnum) {
3335
+ SUB_HEADER_STATS.cut = SUB_HEADER_STATS.cut || 'throttled';
3336
+ } else try {
3326
3337
  const q = (ctx && typeof ctx.get === 'function') ? ctx.get('sessionQuery') : null;
3327
3338
  if (q && typeof q.listSessions === 'function') {
3328
3339
  const sessions = await cachedListSessions(q, await sessionsRootStamp());
3329
3340
  const byId = new Map(rows.map((r) => [r.id, r]));
3330
3341
  for (const rec of sessions || []) {
3342
+ if (Date.now() > enumDeadlineAt) { SUB_HEADER_STATS.cut = 'deadline'; break; }
3331
3343
  const h = rec?.header || rec;
3332
3344
  const aid = h?.id;
3333
3345
  if (!aid || byId.has(aid) || !ids.includes(aid)) continue;
@@ -3434,6 +3446,28 @@ const ROLE_READ_BUDGET_PER_REQUEST = 4;
3434
3446
  let ROLE_READS_LEFT = ROLE_READ_BUDGET_PER_REQUEST;
3435
3447
  /** 待解析队列(跨请求保留进度):元素是子会话 id。 */
3436
3448
  let ROLE_PENDING = [];
3449
+
3450
+ // ── 有界化策略(2026-09-15 第三轮)─────────────────────────────────────────────
3451
+ // 真机基线(30+ 子代理的重会话):`people,feed` 单发 5.4–9.2 s,其中 subs 2.9–3.7 s、roles 2.4–5.5 s,
3452
+ // **两段都没有上限**。本轮的目标不是"更快",而是"**有界**":
3453
+ // · subs :枚举整棵 sessions 树很贵 ⇒ 两次之间至少 N 秒(默认 30 s),窗口内不枚举;
3454
+ // · roles :读 MB 级子会话日志很贵且**不紧急** ⇒ 实时路径默认**不读**,未知角色如实显示为
3455
+ // "待解析"(`rolesPending`),解析交给低频后台(每轮 ≤N 条、两次间隔 ≥N 秒);
3456
+ // 已解析结果**永久缓存**(已结束会话的角色不可变)⇒ pending 单调下降并收敛到 0。
3457
+ // 纪律不变:截断一律进 `degraded`;"还没解析"(pending)与"解析不出来"(unresolved)仍是两个数。
3458
+ const SUBS_ENUM_MIN_INTERVAL_MS = Math.max(0, Number((typeof process !== 'undefined' && process.env && process.env.DSH_EXPERT_TEAM_SUBS_ENUM_MIN_INTERVAL_MS) || 0) || 30000);
3459
+ const SUBS_ENUM_DEADLINE_MS = Math.max(50, Number((typeof process !== 'undefined' && process.env && process.env.DSH_EXPERT_TEAM_SUBS_DEADLINE_MS) || 0) || 800);
3460
+ const ROLES_READ_MIN_INTERVAL_MS = Math.max(0, Number((typeof process !== 'undefined' && process.env && process.env.DSH_EXPERT_TEAM_ROLES_MIN_INTERVAL_MS) || 0) || 30000);
3461
+ const ROLES_READ_PER_BURST = Math.max(1, Number((typeof process !== 'undefined' && process.env && process.env.DSH_EXPERT_TEAM_ROLES_PER_BURST) || 0) || 1);
3462
+ const ROLES_READ_DEADLINE_MS = Math.max(50, Number((typeof process !== 'undefined' && process.env && process.env.DSH_EXPERT_TEAM_ROLES_DEADLINE_MS) || 0) || 600);
3463
+ let SUBS_ENUM_LAST_AT = 0;
3464
+ let ROLES_READ_LAST_AT = 0;
3465
+ const ROLE_READ_STATS = { cut: '' };
3466
+ /** 本轮允许枚举吗(节流窗口)? */
3467
+ function subsEnumAllowed(now) { return (Number(now) - SUBS_ENUM_LAST_AT) >= SUBS_ENUM_MIN_INTERVAL_MS; }
3468
+ /** 本轮允许读几条角色日志(低频后台;0 = 如实显示"待解析")。 */
3469
+ function roleReadAllowance(now) { return (Number(now) - ROLES_READ_LAST_AT) >= ROLES_READ_MIN_INTERVAL_MS ? ROLES_READ_PER_BURST : 0; }
3470
+ function _resetBoundedState() { SUBS_ENUM_LAST_AT = 0; ROLES_READ_LAST_AT = 0; ROLE_READ_STATS.cut = ''; SUB_HEADER_STATS.cut = ''; }
3437
3471
  /** /state 每次进来复位**本请求的读取额度**(队列进度**不**复位)。 */
3438
3472
  function resetRoleReadBudget() { ROLE_READS_LEFT = ROLE_READ_BUDGET_PER_REQUEST; }
3439
3473
  /**
@@ -3631,7 +3665,11 @@ async function workflowRuns(ctx, sid, subs) {
3631
3665
  * 结果是**恰好反了**——能解析的全部留空,解析不出的那批反而被下一段子会话日志兜底填上。
3632
3666
  * (2026-09-11 真实踩到:30 个子代理里只有 5 个有角色,正是 wfLabels 解析不出的那 5 个。)
3633
3667
  */
3634
- async function resolveSubRoles(ctx, subs, wfLabels) {
3668
+ async function resolveSubRoles(ctx, subs, wfLabels, opts) {
3669
+ // 有界化:调用方决定本轮最多读几条(0 = 只查便宜来源与缓存,缺的如实显示为"待解析")
3670
+ // 以及墙上期限;不传则保持原行为(每请求 ROLE_READ_BUDGET_PER_REQUEST 条、无期限)。
3671
+ const maxReads = (opts && Number.isFinite(opts.maxReads)) ? opts.maxReads : ROLE_READ_BUDGET_PER_REQUEST;
3672
+ const deadlineAt = (opts && Number.isFinite(opts.deadlineAt)) ? opts.deadlineAt : Infinity;
3635
3673
  const byId = new Map();
3636
3674
  for (const s of subs || []) { const id = String((s && s.id) || ''); if (id) byId.set(id, s); }
3637
3675
  // ① 便宜来源(自带 label / 父会话事件流 label)与已缓存结果 —— **不花预算、不读日志**
@@ -3651,14 +3689,18 @@ async function resolveSubRoles(ctx, subs, wfLabels) {
3651
3689
  // 这样 `deferred`(队列长度)只减不增,除非真的又新派了人 —— 旧实现每请求从零重算,
3652
3690
  // 预算永远喂给队首同几条,新派的人永远轮不到(实测 deferred 16 → 40)。
3653
3691
  syncRolePending(subs);
3654
- while (ROLE_READS_LEFT > 0 && ROLE_PENDING.length) {
3692
+ let reads = 0;
3693
+ while (reads < maxReads && ROLE_READS_LEFT > 0 && ROLE_PENDING.length) {
3694
+ if (Date.now() > deadlineAt) { ROLE_READ_STATS.cut = 'deadline'; break; }
3655
3695
  const id = ROLE_PENDING.shift();
3656
3696
  if (SUB_ROLE_LOG_CACHE.has(id)) continue;
3657
3697
  const r = await roleFromChildLog(ctx, id);
3658
3698
  if (!SUB_ROLE_LOG_CACHE.has(id)) { ROLE_PENDING.unshift(id); break; } // 预算耗尽(未写缓存)⇒ 放回队首,下一轮继续
3659
3699
  const s = byId.get(id);
3660
3700
  if (s && r && !s.role) s.role = r;
3701
+ reads += 1;
3661
3702
  }
3703
+ if (reads) ROLES_READ_LAST_AT = Date.now(); // 本轮流过日志 ⇒ 进入节流窗口(下次至少 N 秒后)
3662
3704
  return subs;
3663
3705
  }
3664
3706
  // sub rows → Map<role, sub> (first match wins; unknown labels map to '' and
@@ -5232,7 +5274,7 @@ function warnLeadToolFaceOnce(status, detail) {
5232
5274
  LEAD_TOOLFACE_WARNED.add(status);
5233
5275
  console.warn(`[expert-team] lead 工具面**未**收窄(${status}):${detail}`);
5234
5276
  }
5235
- export const _live = { pushActivity, phaseAccountingViolations, loggedPhases, authorityViolations, WRITE_TRACER, createWriteTracer, formatConflict, summarizeTool, parseLogLine, roleOfSub, mapRoleToSub, membersFromState, memberAgentIds, isAgentIdLike, buildRoleSubMap, resolveSubRoles, childSessionTiming, subHeaderIndex, sessionExists, SUB_HEADER_CACHE, workflowEventIndex, workflowChildLabels, workflowChildMeta, workflowRuns, WF_EVENT_CACHE, rememberSessionRun, sessionRunFor, runOwnerSession, SESSION_RUNS, parseTeamCommand, deriveMemberEntries, schemaViolations, runHealth, RUN_STALL_MS, scaffoldFingerprint, SCAFFOLD_REQUIRED, strandedTasks, settleStranded, IN_FLIGHT_STATUSES, normalizeCoverage, SCHEMA_WARN_SEEN, pushActivityEvent, DEFAULT_LIMITS, LIMITS, resolveLimits, capacityViolations, DEFAULT_ROUND_LIMITS, ROUND_LIMITS, ROUND_LIMIT_ENV, resolveRoundLimits, ROUND_LIMIT_OF_KIND, roundOf, isQualityTask, normTitle, roundLimitViolations, reworkLoopWriteGuard, mutateTasks, readStandingRules, appendStandingRule, rulesRun, scopeOverlapWarnings, applyTaskStatus, waitRun, eventFamily, verdictFromToken, normalizeRoleName, truncateCodepoints, filterRunScopedSubs, runCreatedAtMs, runLogTail, liveFiles, LIVE_FILES_CACHE, DEFAULT_ROLES, resolveTierGate, TIER_GATE_ENV, snapshotRun, parseStateSections, listRunsInWorkspace, runsIndexPath, RUNS_INDEX_STATS, _resetRunsIndex, settingsPath, loadSettingsSync, currentSettings, limitsBaseFromSettings, roundLimitsBaseFromSettings, effectiveTierGate: () => TIER_GATE, ensureSkillInstalled, ensurePresetInstalled, uninstallInstalled, buildSkillRegistration, parseSkillMarkdown, PLUGIN_VERSION, INSTALL_STAMP, runtimeSkillRegistered: () => RUNTIME_SKILL_REGISTERED, agentScopedToolNames, warnLeadToolFaceOnce, resolveLeadToolFace, LEAD_TOOLFACE_ENV, effectiveLeadToolFace: () => LEAD_TOOLFACE, resolveLoopGuard, LOOP_GUARD_ENV, effectiveLoopGuard: () => LOOP_GUARD_ENABLED, resolveRosterDefaults, rosterSettings, createRun, watchScript, canvasPollMs, installHostSettings, hostValues, hostScope, hostSettingsNote, updateHostSettings, pickFileOnly, pickHostExpressible, buildHostSchema, hostBase, hostSchemaPaths, reapplySettingsDerived, currentSettings, mergeSettings, detectOptionalPlugins, OPTIONAL_PLUGINS, scheduleEffortPreflight, _resetEffortPreflight, effortPreflightPlan, declaredEffortsFromPresetSource, hintOptionalPluginsOnce, _resetOptionalHintOnce, scheduleOptionalPluginCheck, recheckOptionalPlugins, OPTIONAL_PROBE_DELAYS_MS, loaderEntryNames, hindsightToolReady, costMeterReady, listSubagentStatusBySession, cachedListSessions, _resetListSessionsCache, LIST_SESSIONS_TTL_MS, SUB_HEADER_MEMO, SUB_HEADER_STATS, _resetSubHeaderMemo, sessionsRootStamp, resetRoleReadBudget, roleReadBudgetSnapshot, syncRolePending, _resetRolePending, ROLE_READ_BUDGET_PER_REQUEST, ROLE_READ_LOG_CACHE: SUB_ROLE_LOG_CACHE };
5277
+ export const _live = { pushActivity, phaseAccountingViolations, loggedPhases, authorityViolations, WRITE_TRACER, createWriteTracer, formatConflict, summarizeTool, parseLogLine, roleOfSub, mapRoleToSub, membersFromState, memberAgentIds, isAgentIdLike, buildRoleSubMap, resolveSubRoles, childSessionTiming, subHeaderIndex, sessionExists, SUB_HEADER_CACHE, workflowEventIndex, workflowChildLabels, workflowChildMeta, workflowRuns, WF_EVENT_CACHE, rememberSessionRun, sessionRunFor, runOwnerSession, SESSION_RUNS, parseTeamCommand, deriveMemberEntries, schemaViolations, runHealth, RUN_STALL_MS, scaffoldFingerprint, SCAFFOLD_REQUIRED, strandedTasks, settleStranded, IN_FLIGHT_STATUSES, normalizeCoverage, SCHEMA_WARN_SEEN, pushActivityEvent, DEFAULT_LIMITS, LIMITS, resolveLimits, capacityViolations, DEFAULT_ROUND_LIMITS, ROUND_LIMITS, ROUND_LIMIT_ENV, resolveRoundLimits, ROUND_LIMIT_OF_KIND, roundOf, isQualityTask, normTitle, roundLimitViolations, reworkLoopWriteGuard, mutateTasks, readStandingRules, appendStandingRule, rulesRun, scopeOverlapWarnings, applyTaskStatus, waitRun, eventFamily, verdictFromToken, normalizeRoleName, truncateCodepoints, filterRunScopedSubs, runCreatedAtMs, runLogTail, liveFiles, LIVE_FILES_CACHE, DEFAULT_ROLES, resolveTierGate, TIER_GATE_ENV, snapshotRun, parseStateSections, listRunsInWorkspace, runsIndexPath, RUNS_INDEX_STATS, _resetRunsIndex, settingsPath, loadSettingsSync, currentSettings, limitsBaseFromSettings, roundLimitsBaseFromSettings, effectiveTierGate: () => TIER_GATE, ensureSkillInstalled, ensurePresetInstalled, uninstallInstalled, buildSkillRegistration, parseSkillMarkdown, PLUGIN_VERSION, INSTALL_STAMP, runtimeSkillRegistered: () => RUNTIME_SKILL_REGISTERED, agentScopedToolNames, warnLeadToolFaceOnce, resolveLeadToolFace, LEAD_TOOLFACE_ENV, effectiveLeadToolFace: () => LEAD_TOOLFACE, resolveLoopGuard, LOOP_GUARD_ENV, effectiveLoopGuard: () => LOOP_GUARD_ENABLED, resolveRosterDefaults, rosterSettings, createRun, watchScript, canvasPollMs, installHostSettings, hostValues, hostScope, hostSettingsNote, updateHostSettings, pickFileOnly, pickHostExpressible, buildHostSchema, hostBase, hostSchemaPaths, reapplySettingsDerived, currentSettings, mergeSettings, detectOptionalPlugins, OPTIONAL_PLUGINS, scheduleEffortPreflight, _resetEffortPreflight, effortPreflightPlan, declaredEffortsFromPresetSource, hintOptionalPluginsOnce, _resetOptionalHintOnce, scheduleOptionalPluginCheck, recheckOptionalPlugins, OPTIONAL_PROBE_DELAYS_MS, loaderEntryNames, hindsightToolReady, costMeterReady, listSubagentStatusBySession, cachedListSessions, _resetListSessionsCache, LIST_SESSIONS_TTL_MS, SUB_HEADER_MEMO, SUB_HEADER_STATS, _resetSubHeaderMemo, sessionsRootStamp, resetRoleReadBudget, roleReadBudgetSnapshot, syncRolePending, _resetRolePending, ROLE_READ_BUDGET_PER_REQUEST, SUBS_ENUM_MIN_INTERVAL_MS, SUBS_ENUM_DEADLINE_MS, ROLES_READ_MIN_INTERVAL_MS, ROLES_READ_PER_BURST, ROLES_READ_DEADLINE_MS, subsEnumAllowed, roleReadAllowance, ROLE_READ_STATS, _resetBoundedState, ROLE_READ_LOG_CACHE: SUB_ROLE_LOG_CACHE };
5236
5278
 
5237
5279
  export function apply(ctx, config) {
5238
5280
  // 留一份 config:设置在运行时改变(官方面板 / 浮层)时要**用同一份 config** 重算上限与档位门,
@@ -5640,12 +5682,27 @@ export function apply(ctx, config) {
5640
5682
  // `feed` 也必须先有 subs(它按子会话遍历活动缓存)。若只想要 feed 却跳过枚举,
5641
5683
  // feed 会**静默变空** —— 那正是本仓最忌讳的"两种零分不清"(缺块 ≠ 空数据)。
5642
5684
  const needSubs = want('people') || want('feed');
5643
- let subs = needSubs ? await listSubagentStatusBySession(ctx, peopleSid, knownIds) : [];
5685
+ // 有界化:本轮是否允许枚举(节流窗口)+ 枚举期限;结果经 SUB_HEADER_STATS.cut 上报。
5686
+ const subsStartedAt = Date.now();
5687
+ const allowEnumNow = subsEnumAllowed(subsStartedAt);
5688
+ SUB_HEADER_STATS.cut = '';
5689
+ const subsOpts = { allowEnum: allowEnumNow, enumDeadlineAt: subsStartedAt + SUBS_ENUM_DEADLINE_MS };
5690
+ let subs = needSubs ? await listSubagentStatusBySession(ctx, peopleSid, knownIds, subsOpts) : [];
5644
5691
  // 归属会话拿不到人时退回请求会话(否则面板整块空掉),但**如实标注**这不是归属会话的人。
5645
5692
  if (needSubs && !subs.length && peopleSid !== sid) {
5646
- const fallback = await listSubagentStatusBySession(ctx, sid, knownIds);
5693
+ const fallback = await listSubagentStatusBySession(ctx, sid, knownIds, subsOpts);
5647
5694
  if (fallback.length) { subs = fallback; peopleSid = sid; }
5648
5695
  }
5696
+ // 有界化上报:本轮流过枚举 ⇒ 进入节流窗口;被节流/被期限截断一律**如实**进 degraded
5697
+ // (缺块 ≠ 空数据:被挡住的 id 只是没有 header,成员本身不丢)。
5698
+ if (needSubs && allowEnumNow) SUBS_ENUM_LAST_AT = Date.now();
5699
+ // ⚠️ 噪声纪律:**节流不是失败**,不能每轮都进 `degraded`(节流窗口内每次轮询都会命中
5700
+ // ⇒ 面板会长期挂一个降级标记,把真告警一起降权)。"某些成员细节不可得"这一事实由
5701
+ // `subsPending` 诚实表达(与 `rolesPending` 对称);只有"开始枚举却被期限截断"算
5702
+ // **部分结果**,才进 `degraded`。
5703
+ const idsReal = (Array.isArray(knownIds) ? knownIds : []).filter(isAgentIdLike);
5704
+ const subsPending = needSubs ? idsReal.filter((id) => !subs.some((r) => r.id === id)).length : 0;
5705
+ if (needSubs && SUB_HEADER_STATS.cut === 'deadline') degraded.push('subs:deadline');
5649
5706
  // 供面板如实提示:resolve=false 时人员名单可能来自错误的会话
5650
5707
  sel.peopleSession = peopleSid;
5651
5708
  sel.peopleSessionIsOwner = peopleSid === (owner.sid || '');
@@ -5670,13 +5727,21 @@ export function apply(ctx, config) {
5670
5727
  // ① 只在 people 分节里做;② 硬上限 `MAX_ROLE_SUBS`(超限只解析前 N 条)。
5671
5728
  // 被上限挡住的条数**如实进 `degraded`**;"还没解析"(rolesDeferred,队列长度)与
5672
5729
  // "解析不出来"(unresolved)仍然分得开(两种零可区分)。
5730
+ // 有界化:角色解析移出实时路径 —— 默认**不读**(缺的如实显示为"待解析"),
5731
+ // 只在节流窗口到点后读 ≤ROLES_READ_PER_BURST 条,并给墙上期限。
5732
+ const rolesAllowance = roleReadAllowance(Date.now());
5733
+ const rolesOpts = { maxReads: rolesAllowance, deadlineAt: Date.now() + ROLES_READ_DEADLINE_MS };
5734
+ ROLE_READ_STATS.cut = '';
5673
5735
  if (want('people')) {
5674
5736
  if (subs.length > MAX_ROLE_SUBS) {
5675
- await resolveSubRoles(ctx, subs.slice(0, MAX_ROLE_SUBS), wfLabels);
5737
+ await resolveSubRoles(ctx, subs.slice(0, MAX_ROLE_SUBS), wfLabels, rolesOpts);
5676
5738
  degraded.push('roles:' + (subs.length - MAX_ROLE_SUBS));
5677
5739
  } else {
5678
- await resolveSubRoles(ctx, subs, wfLabels);
5740
+ await resolveSubRoles(ctx, subs, wfLabels, rolesOpts);
5679
5741
  }
5742
+ // 期限截断如实上报;**节流不报 degraded** —— 它不是失败,而是"待解析"这一诚实
5743
+ // 状态,由 `rolesPending` 表达(否则每请求都刷 degraded,把告警变成噪声)。
5744
+ if (ROLE_READ_STATS.cut === 'deadline') degraded.push('roles:deadline');
5680
5745
  }
5681
5746
  mark('roles');
5682
5747
  // R12 + R13 + R17(F-1 · P0 跨 run 成员串号):**显示与写盘都只吃过滤后的 `subById`**。
@@ -5877,11 +5942,11 @@ export function apply(ctx, config) {
5877
5942
  persistSessionRuns();
5878
5943
  mark('tail');
5879
5944
  // profile 只在 DSH_EXPERT_TEAM_STATE_PROFILE=1 时出现(默认不含该字段,
5880
- // 线上响应体不变);rolesDeferred = 本轮因预算被推迟的子会话日志读取条数,
5945
+ // 线上响应体不变);rolesPending = "**还没解析**"的子会话条数(与本轮是否节流无关),
5881
5946
  // 让调用方能把"还没解析"与"解析不出来"分开(两种零可区分)。
5882
5947
  const budget = roleReadBudgetSnapshot();
5883
5948
  json(200, Object.assign(
5884
- { ok: true, runs, workspaces, cwd, agents: sel.agents, rolesDeferred: budget.deferred },
5949
+ { ok: true, runs, workspaces, cwd, agents: sel.agents, rolesPending: budget.deferred, subsPending },
5885
5950
  sel,
5886
5951
  // 新增字段(只在相关时出现,默认负载与老客户端保持不变):
5887
5952
  // sections —— 显式要了分节时,告诉调用方"这份负载包含哪些块"(缺的块 ≠ 空数据,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yangdcm/dsh-expert-team",
3
- "version": "1.3.16",
3
+ "version": "1.3.17",
4
4
  "description": "dsh「专家团」bundle:一句自然语言自动组建/持久化一支 12 角色多智能体团队,共享工作区协议 + 阶段门控编排 + 结构化交接 + 质量门禁/自动调度,实现者直接改代码并产出持久工件;带 live 团队浮层(质量门禁/覆盖率/工件预览)。 · Role-based multi-agent expert team for DeepSeek Harness: one sentence in, a staged and gated team delivery out.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -88,7 +88,7 @@
88
88
  "check:name": "node scripts/rename-package.mjs --check",
89
89
  "test:regression": "node regression.test.mjs",
90
90
  "test:e2e": "node e2e.test.mjs",
91
- "test:all": "node bootstrap.test.mjs && node host-settings.test.mjs && node smoke.test.mjs && node regression.test.mjs && node e2e.test.mjs && node flow.test.mjs && node plan-decide.test.mjs && node plan-discard.test.mjs && node models-honesty.test.mjs && node evidence-gate.test.mjs && node sync-gate.test.mjs && node artifact-writer.test.mjs && node dag.test.mjs && node metrics.test.mjs && node broken-chain.test.mjs && node schema-warn.test.mjs && node template-copy.test.mjs && node task-binding.test.mjs && node role-merge.test.mjs && node wf-recovery.test.mjs && node panorama.test.mjs && node run-ownership.test.mjs && node role-identity.test.mjs && node command-parse.test.mjs && node member-registry.test.mjs && node schema-check.test.mjs && node run-health.test.mjs && node cordis-fs-port.test.mjs && node scaffold-fingerprint.test.mjs && node preset-lint.test.mjs && node stranded-tasks.test.mjs && node agent-scope-guard.test.mjs && node schema-warn-noise.test.mjs && node tool-card-status.test.mjs && node dag-status.test.mjs && node capacity-limits.test.mjs && node task-cas.test.mjs && node write-lock.test.mjs && node card-stall.test.mjs && node multi-session-and-confirm.test.mjs && node standing-rules.test.mjs && node scope-overlap.test.mjs && node task-cancel-protect.test.mjs && node wait-run.test.mjs && node rework-loop.test.mjs && node client-css-integrity.test.mjs && node interception.test.mjs && node mutation-catalog.test.mjs && node write-bypass-ratchet.test.mjs && node asi-hazards.test.mjs && node dispatch-contract.test.mjs && node closure-ledger.test.mjs && node rework-nature.test.mjs && node loop-guard.test.mjs && node vocab-consistency.test.mjs && node state-no-write.test.mjs && node routes-shared.test.mjs && node metrics-render.test.mjs && node metrics-collect.test.mjs && node first-runnable.test.mjs && node closing-budget.test.mjs && node scan-single-source.test.mjs && node tier.test.mjs && node dispatch-ledger.test.mjs && node tier-gate.test.mjs && node tier-badge.test.mjs && node validate-module.test.mjs && node settings.test.mjs && node settings-page.test.mjs && node policy.test.mjs && node phase-accounting.test.mjs && node log-parse-module.test.mjs && node command-parse-module.test.mjs && node authority.test.mjs && node write-tracer.test.mjs && node token-accounting.test.mjs && node lead-toolface.test.mjs && node settings-consumers.test.mjs && node settings-wiring.test.mjs && node state-perf-guard.test.mjs && node state-sections.test.mjs && node artifact-ownership.test.mjs && node subagent-bar.test.mjs && node r1-ownership-gate.test.mjs && node artifact-redirect-watch.test.mjs && node effort-preflight.test.mjs",
91
+ "test:all": "node bootstrap.test.mjs && node host-settings.test.mjs && node smoke.test.mjs && node regression.test.mjs && node e2e.test.mjs && node flow.test.mjs && node plan-decide.test.mjs && node plan-discard.test.mjs && node models-honesty.test.mjs && node evidence-gate.test.mjs && node sync-gate.test.mjs && node artifact-writer.test.mjs && node dag.test.mjs && node metrics.test.mjs && node broken-chain.test.mjs && node schema-warn.test.mjs && node template-copy.test.mjs && node task-binding.test.mjs && node role-merge.test.mjs && node wf-recovery.test.mjs && node panorama.test.mjs && node run-ownership.test.mjs && node role-identity.test.mjs && node command-parse.test.mjs && node member-registry.test.mjs && node schema-check.test.mjs && node run-health.test.mjs && node cordis-fs-port.test.mjs && node scaffold-fingerprint.test.mjs && node preset-lint.test.mjs && node stranded-tasks.test.mjs && node agent-scope-guard.test.mjs && node schema-warn-noise.test.mjs && node tool-card-status.test.mjs && node dag-status.test.mjs && node capacity-limits.test.mjs && node task-cas.test.mjs && node write-lock.test.mjs && node card-stall.test.mjs && node multi-session-and-confirm.test.mjs && node standing-rules.test.mjs && node scope-overlap.test.mjs && node task-cancel-protect.test.mjs && node wait-run.test.mjs && node rework-loop.test.mjs && node client-css-integrity.test.mjs && node interception.test.mjs && node mutation-catalog.test.mjs && node write-bypass-ratchet.test.mjs && node asi-hazards.test.mjs && node dispatch-contract.test.mjs && node closure-ledger.test.mjs && node rework-nature.test.mjs && node loop-guard.test.mjs && node vocab-consistency.test.mjs && node state-no-write.test.mjs && node routes-shared.test.mjs && node metrics-render.test.mjs && node metrics-collect.test.mjs && node first-runnable.test.mjs && node closing-budget.test.mjs && node scan-single-source.test.mjs && node tier.test.mjs && node dispatch-ledger.test.mjs && node tier-gate.test.mjs && node tier-badge.test.mjs && node validate-module.test.mjs && node settings.test.mjs && node settings-page.test.mjs && node policy.test.mjs && node phase-accounting.test.mjs && node log-parse-module.test.mjs && node command-parse-module.test.mjs && node authority.test.mjs && node write-tracer.test.mjs && node token-accounting.test.mjs && node lead-toolface.test.mjs && node settings-consumers.test.mjs && node settings-wiring.test.mjs && node state-perf-guard.test.mjs && node state-bounded.test.mjs && node state-sections.test.mjs && node artifact-ownership.test.mjs && node subagent-bar.test.mjs && node r1-ownership-gate.test.mjs && node artifact-redirect-watch.test.mjs && node effort-preflight.test.mjs",
92
92
  "test:toolcard": "node tool-card-status.test.mjs",
93
93
  "test:flow": "node flow.test.mjs",
94
94
  "test:decide": "node plan-decide.test.mjs",