@yangdcm/dsh-expert-team 1.3.10 → 1.3.12

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
@@ -3,6 +3,91 @@
3
3
  本包遵循[语义化版本](https://semver.org/lang/zh-CN/)。dsh 宿主版本线的对应关系写在
4
4
  `package.json` 的 `engines.dsh` 与 `dsh.compatibility` 里,插件市场按它判断"这个插件跟你的宿主兼不兼容"。
5
5
 
6
+ ## 1.3.12
7
+
8
+ **面向"可被 AI 检索与引用"的文档与元数据补齐**(纯文档/元数据,代码与 1.3.11 完全相同)。
9
+
10
+ - README(中英各自对等)新增:
11
+ - **速览 / At a glance** 事实表:包名 · npm · 仓库 · 宿主版本 · 运行时依赖 · Node · License · 一行安装 · 一行上手 · 过程产物路径 · 本机数据路径;
12
+ - 一句**同义词行**(dsh 插件 · DeepSeek Harness 多智能体 · agent team · orchestration · 角色化 subagent · DAG 并行 · 阶段门控 · 质量门禁 · 工件留痕)—— 自然成句,不堆砌关键词;
13
+ - **常见问题 / FAQ**:用自然语言问句覆盖「是什么 / 与"一个 agent 硬做"的区别 / 是否必须装别的插件 / 支持哪些 dsh 版本 / 数据放在哪 / 怎么卸载 / 会不会联网 / 支持哪些模型 / 不切 preset 能否用 / 打开很慢怎么办」;
14
+ - **术语 / Glossary**:12 角色 ↔ 代码 id、9 阶段 ↔ id(以 `lib/vocab.js` 为唯一真源)、主要工件清单;
15
+ - 主要小节标题改为**双语**(安装 / 依赖与推荐插件 / 快速上手 / 自定义预设 / 常见问题 / 术语)。
16
+ - **图片 alt 文本**全部换成带关键词的描述性文案(此前偏短 —— alt 是可机读文本,不该白丢)。
17
+ - 「快速上手」的命令块改为 **14 行速查表**(命令 + 一句话用途),措辞以 `lib/command-parse.js` 的 `USAGE` 为真源。
18
+ - 新增仓库根 [`llms.txt`](https://github.com/yangdcm/dsh-expert-team/blob/main/llms.txt):按 [llmstxt.org](https://llmstxt.org) 约定给 LLM 用的索引 —— 事实块(可逐条核对)+ 文档链接(只列仓库里真实存在的文件)+ 诚实边界;README 顶部加一行指路。
19
+ - `package.json` `keywords` 补 4 个:`role-based` / `spec-driven` / `dag` / `agentic-workflow`(`description` 未动)。
20
+ - GitHub 仓库 topics 补齐:`ai-agents` / `agent-orchestration` / `multi-agent-systems` / `llm` / `cordis` / `code-generation` / `workflow-automation`。
21
+
22
+ > 两条**没有做**的(有意):不写隐藏关键词/meta 标签(GitHub 会剥掉,且属取巧);不写夸大话术 —— 本节所有事实都能在仓库或实测里核到。
23
+
24
+ ## 1.3.11
25
+
26
+ **`/state` 的 `subs` / `roles` 两段收口 —— 这次不靠预测,靠 1.3.10 留下的分步计时开关实测定位。**
27
+ 父会话在真机上带 `DSH_EXPERT_TEAM_STATE_PROFILE=1` 复测,拿到确切归属(同一台机器、同一 session、
28
+ 连续两次请求):
29
+
30
+ ```
31
+ totalMs 2222 / 2047
32
+ profile.runs+select 21 / 15 ms ✓ 便宜
33
+ profile.subs 1327 / 1388 ms ✗ 主犯
34
+ profile.wfLabels 0 / 0 ms ✓
35
+ profile.roles 869 / 638 ms ✗ 次犯
36
+ profile.tail 3 / 3 ms ✓
37
+ rolesBudget: {per:4, left:0, deferred: 16 → 40} ← deferred 在**涨**
38
+ ```
39
+
40
+ ### ① `subs` 段:按 session id 备忘(并纠正两处"看起来像缓存、其实没用"的设置)
41
+
42
+ - **根因**:1.3.10 的"只在真缺人时才查"**没真正省下钱** —— 已结束/历史子会话本来就不在活注册表里,
43
+ `missingIds` 每请求都非空 ⇒ 每请求都退化成对 **475 个 artifact 逐个读 header** 的枚举;
44
+ 而当时设的 **2 s TTL 恰好等于真实请求间隔**(轮询 + 退避),**必然过期** ⇒ 等于没有缓存。
45
+ - **改法**:新增 `SUB_HEADER_MEMO`(按 session id 永久记住 `createdAt/parentId/depth`)——
46
+ 这条分支上的 id 恰恰是"活注册表里查不到"的那些,其 header 不可变,记住它们不会让用户看到旧状态;
47
+ 枚举本身的主失效键改为 **`(sessions 根目录, mtimeMs, size)`**(`sessionsRootStamp()`,实测 0.56 ms),
48
+ TTL 只作兜底并放宽到 **60 s**。
49
+ - **口径不变**:**活代理一律实时读,绝不进任何缓存**(有断言守着:活代理不写入备忘)。
50
+ - **折算前后**(单位成本取自上面的真机 profile:一次枚举 ≈ 1500–2400 ms,本机取 1800 ms):
51
+ **旧:每请求 ≈ 1800 ms;新:第 1 次 1800 ms,第 2 次起 0.02–0.09 ms** ⇒ 热态 `subs` ≈ **0 ms**。
52
+
53
+ ### ② `roles` 段:限次摊平改成**跨请求推进的队列**
54
+
55
+ - **根因**:1.3.10 每请求都把候选集**从零重算**,预算永远喂给队首同几条,而新派的子代理不断出现在队尾
56
+ ⇒ **进度不前进、`deferred` 只增不减**(实测 16 → 40)。"限次摊平"退化成"永远摊不完"。
57
+ - **改法**:模块级 `ROLE_PENDING` 队列 —— 每请求按"当前 subs 里仍未缓存"的顺序**重建队列**
58
+ (已缓存的自动掉队、既有顺序保留、新人追加队尾),再从队首消费 ≤ 4 条;角色结果**永久缓存**
59
+ (对已结束会话不可变)。`resetRoleReadBudget()` 只复位**本请求额度**,**不丢队列进度**。
60
+ - **两种零仍然分得开**:`deferred` = **还没解析**(队列长度,单调不增);`unresolved` = **解析不出来**
61
+ (读过但日志里没有角色)—— 新增这个数,正是为了不让两者混成一个。
62
+ - **实测(21 个子会话,每次 4 条预算)**:`deferred` **17 → 13 → 9 → 5 → 1 → 0**;
63
+ 6 个 tick 后**零次日志读**。折算:**旧 860 ms/请求且永不收敛 → 收敛后 ≈ 0 ms**。
64
+
65
+ ### ③ 推荐插件自检的**假警报**(用户贴的启动日志直接坐实)
66
+
67
+ - **现象**:日志说 `@vectorize-io/hindsight-coding-agents` 与 `dsh-cost-meter`"已安装但当前不可用 ⇒
68
+ `dsh plugin … add`",**紧跟着 `dsh-cost-meter` 自己就加载成功了** ⇒ 判定是错的、建议是误导的。
69
+ - **根因(两条)**:① 我们的 `apply()` 跑在其它插件之前,那一刻 `ctx.get('costMeter')`/工具表里当然还没有它们;
70
+ ② 对 `installed-not-ready` 也拼了安装命令,等于叫用户装一个**已经装好**的东西。
71
+ - **宿主没有 app 级 ready 事件**(已核源码):`dsh-app-boot` 的 `boot()` 顺序是
72
+ `mountRootInclude → await ctx.get('loader').await() → assertEntriesActivated → return`,
73
+ 期间不 emit 任何"就绪"事件;而 `loader.await()` 我们**不能用** —— 那棵树包含我们自己的挂载任务,
74
+ 在 init 里 await 它会**自等死锁**。
75
+ - **改法**:`scheduleOptionalPluginCheck()` —— **有界延迟重探**(250/1000/3000 ms,最后一次才下结论)
76
+ + **`ctx.inject([服务])` 事件驱动**(服务一出现立刻重探;服务始终不出现则回调不触发,零副作用);
77
+ 外加 **`/team help` 懒重探**(刚装上/刚修好配置不必为了这行提示再重启)。
78
+ - **文案分档**:`missing` ⇒ 给安装命令;`installed-not-ready` ⇒ 只说"已安装但当前未就绪(可能未配置或被关闭)",
79
+ **不再给安装命令**;`unknown` ⇒ 不出现(四态与"两种零可区分"的语义不变)。
80
+
81
+ ### 验证与口径
82
+
83
+ - `npm run test:all` **EXIT=0**(**80** 个测试文件;`state-perf-guard` 扩到 37 条断言:备忘只枚举一次、
84
+ 活代理不入备忘、目录戳失效、队列跨请求推进、deferred 单调不增、额度复位不丢进度、
85
+ `installed-not-ready` 不含安装命令、服务晚挂不再误报、就绪后重探与懒重探的接线)。
86
+ - **诚实边界**:上面"折算前后"的数字是**用父会话真机实测的单位成本 × 新代码的调用次数**折算的
87
+ (本机宿主仍跑 1.3.10,我无法把新代码装进运行中的进程)。**端到端 `totalMs` 请装完 1.3.11 后带
88
+ `DSH_EXPERT_TEAM_STATE_PROFILE=1` 复测**:预期 `subs`/`roles` 两段都接近 0,
89
+ `totalMs` 由 `runs+select`(~20 ms)+ `tail`(~3 ms)主导。
90
+
6
91
  ## 1.3.10
7
92
 
8
93
  **四项一次收口:推荐插件自检 · 画布轮询也走 single-flight · 底盘缓存 · 角色解析限次。**
package/README.en.md CHANGED
@@ -6,7 +6,7 @@ English | [中文](README.md)
6
6
  [![license](https://img.shields.io/npm/l/@yangdcm/dsh-expert-team)](https://github.com/yangdcm/dsh-expert-team/blob/main/LICENSE)
7
7
  [![CI](https://github.com/yangdcm/dsh-expert-team/actions/workflows/ci.yml/badge.svg)](https://github.com/yangdcm/dsh-expert-team/actions/workflows/ci.yml)
8
8
 
9
- ![expert-team: one sentence in, a gated team delivery out](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/hero.svg)
9
+ ![dsh expert-team plugin banner: a 12-role multi-agent team, a 9-phase gated pipeline, zero runtime dependencies](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/hero.svg)
10
10
 
11
11
  > **One sentence in, a gated team delivery out.** `/team build a payments module with login`
12
12
  > assembles a 12-role expert team and runs
@@ -22,6 +22,25 @@ 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
+
25
44
  ## Who it is for
26
45
 
27
46
  **A complete engineering department for small teams and solo builders** — no hiring, no assembling a team:
@@ -49,7 +68,7 @@ solo developers shipping a complete project · any long task where "someone inde
49
68
 
50
69
  <sub>One honest boundary: this is **not** a human team — product-level and scope-level decisions remain yours.</sub>
51
70
 
52
- ![The 9-phase gated pipeline](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/pipeline.svg)
71
+ ![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)
53
72
 
54
73
  <sub>Figure 1: the 9-phase gated pipeline. `spec-review` is a **hard gate** — if the SPEC.md
55
74
  "boundaries and prohibitions" section is empty, the run does not advance (`lib/interception.js`).
@@ -96,32 +115,32 @@ answer them:
96
115
 
97
116
  ## What it looks like in action
98
117
 
99
- ![The full-screen canvas: phase bar, progress and roster](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/canvas.png)
118
+ ![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)
100
119
 
101
120
  <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>
102
121
 
103
- ![The full-screen canvas, tasks view: the dependency graph](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/canvas-tasks.png)
122
+ ![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)
104
123
 
105
124
  <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>
106
125
 
107
- ![Quality-gate violations surfaced live](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/panel-gate.png)
126
+ ![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)
108
127
 
109
128
  <sub>Figure 4: **gate violations**. Look at the banner at the top — the violation and its refusal reason
110
129
  (e.g. "SPEC.md's boundary section has entered `implement` but still has no 'expected rejection' row")
111
130
  is decided by `lib/interception.js`, hooked onto the host's `tools/post-execute` waterfall, and surfaced
112
131
  immediately. This is code, not a prompt reminder.</sub>
113
132
 
114
- ![Members, models and task detail](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/panel-live.png)
133
+ ![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)
115
134
 
116
135
  <sub>Figure 5: **the roster**. Look at the member list — who is running, on which model, and what it is doing;
117
136
  expand a member for its tasks and artifacts. Models are configurable per role; heterogeneous models are used for cross-checking.</sub>
118
137
 
119
- ![Phase progress and artifact preview](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/panel-flow.png)
138
+ ![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)
120
139
 
121
140
  <sub>Figure 6: **phases and artifacts**. Look at the phase bar and the preview pane — the current phase, the phases
122
141
  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>
123
142
 
124
- ![The expert-team section inside the official settings page](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/settings.png)
143
+ ![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)
125
144
 
126
145
  <sub>Figure 7: **settings**. Look at the official `Settings → Expert team` page — 18 settings, Chinese labels,
127
146
  **saved on change and applied immediately** (caps, rounds, the tier gate and the oscillation detector are recomputed
@@ -271,20 +290,24 @@ dsh plugin --profile web add dshmarket
271
290
 
272
291
  ## Quick start
273
292
 
274
- ```
275
- /team build a payments module with login # one sentence in, a one-shot team delivery out
276
- /team --persist refactor the orders module # persistent live team: members can be re-tasked, survives sessions
277
- /team --one-shot run a small chore # inverse override: run once even if persistence is the default
278
- /team --no-code review the existing API # produce planning/review/test artifacts only, change no code
279
- /team --code implement it # inverse override: touch code even if "artifacts only" is the default
280
- /team --confirm a big redesign # create the run but do not dispatch; click "run" in the overlay
281
- /team uninstall # reclaim what this plugin laid down under $DSH_HOME
282
- /team status # phase, members, model plan and live violations for every run
283
- /team resume <run-id> # resume across sessions
284
- ```
285
-
286
- Full command list (`/team canvas` visual canvas, `/team codeindex` code index, `/team learn` self-learning,
287
- `/team limit` quotas, `/team settle` cold-start settlement, …) — see `/team help`.
293
+ | Command | What it does |
294
+ |---|---|
295
+ | `/team <one-sentence goal>` | one sentence in, a one-shot team delivery out |
296
+ | `/team --persist <task>` | persistent live team: members can be re-tasked, survives sessions |
297
+ | `/team --one-shot <task>` | inverse override: run once even if persistence is the default |
298
+ | `/team --no-code <task>` | produce planning/review/test artifacts only, change no code |
299
+ | `/team --code <task>` | inverse override: touch code even if "artifacts only" is the default |
300
+ | `/team --confirm <task>` | create the run but do not dispatch; click "run" in the overlay |
301
+ | `/team --tier <tier> <task>` | pick the process tier (fast / standard / strict) |
302
+ | `/team status` | phase, members, model plan and live violations for every run |
303
+ | `/team models [<run>]` | cost / model plan per role |
304
+ | `/team canvas [<run>]` | render the team canvas (HTML); `--watch` refreshes live |
305
+ | `/team learn` | aggregate logs `METRICS.md` + distilled lessons `LEARNINGS.md` |
306
+ | `/team wait [<run>]` | in-flight task progress (non-blocking) |
307
+ | `/team resume <run-id>` | resume across sessions |
308
+ | `/team uninstall` | reclaim what this plugin laid down under `$DSH_HOME` |
309
+
310
+ Full command list (`/team codeindex` code index, `/team limit` quotas, `/team settle` cold-start settlement, …) — see `/team help`.
288
311
 
289
312
  **Where artifacts land**
290
313
 
@@ -292,6 +315,51 @@ Full command list (`/team canvas` visual canvas, `/team codeindex` code index, `
292
315
  - `$DSH_HOME/expert-team/` — machine-local preferences and cross-project experience: `settings.json`,
293
316
  `session-runs.json`, `LEARNINGS.md`
294
317
 
318
+ ## FAQ
319
+
320
+ **What exactly is it?** A plugin you install into your local `dsh`: `/team <one-sentence goal>` spins up a 12-role subagent team (product / architect / researcher / UI / backend / frontend / data / security / reviewer / QA / DevOps / docs), delivers through a 9-phase gated pipeline inside your workspace, and writes the whole process down as reviewable artifacts.
321
+
322
+ **How is it different from "one agent doing it all"?** It targets the three classic failure modes: **context drift** (hand-offs carry structured returns *and* artifact files, not chat history), **grading your own homework** (review and test are separate roles; a `qa`/`reviewer` verdict is required), and **rework that never converges** (over-budget rounds and unclosed items are stopped by hard gates and reported, not hidden).
323
+
324
+ **Do I have to install other plugins?** **No.** This plugin has zero runtime dependencies; Hindsight (cross-project memory) is **recommended** and `dsh-cost-meter` (cost view) is **optional** — the full flow runs without either. See [Dependencies and recommended plugins](#dependencies-and-recommended-plugins).
325
+
326
+ **Which dsh versions are supported?** `engines.dsh: >=0.1.5-rc.1` (developed and verified on 0.1.5-rc.1); earlier versions are untested. Node.js ≥ 20.
327
+
328
+ **Where does the data live?** Artifacts in your workspace: `<workspace>/team/<run-id>/`. Machine-local preferences and cross-project lessons: `$DSH_HOME/expert-team/` (`settings.json` / `LEARNINGS.md` / `session-runs.json`).
329
+
330
+ **How do I uninstall?** `/team uninstall` reclaims the copies this plugin laid down under `$DSH_HOME` (the skill is registered at runtime and never copied), then remove the plugin via the CLI or the marketplace. **Note:** `LEARNINGS.md` and friends under `$DSH_HOME/expert-team/` are **your data** and are kept.
331
+
332
+ **Does it go online by itself?** No. It only calls the tools the host gives the session (files, shell, subagents); network access depends on the tool face you grant.
333
+
334
+ **Which models does it use?** Whatever the host provides; this plugin supports **per-role model configuration** (the overlay shows which model each member runs), and heterogeneous models can be used for cross-checking.
335
+
336
+ **Does it work without the "Expert team mode" preset?** Yes. `/team` is a host-plane command and runs under any preset; it then falls back to the generic `subagent` (role personas go into the prompt), losing only the configuration-level boundaries (`toolFilter` / `maxDepth: 1`).
337
+
338
+ **Why is the overlay/canvas slow to open?** See [Troubleshooting](#troubleshooting) — since 1.3.5 `/state` no longer reads every sub-session log in full; upgrade to ≥ 1.3.5 and restart `dsh web`.
339
+
340
+ ## Glossary
341
+
342
+ **Roles (12)**
343
+
344
+ | Term | Code id |
345
+ |---|---|
346
+ | Product | `pm` |
347
+ | Architect | `architect` |
348
+ | Researcher | `researcher` |
349
+ | UI/UX | `ui` |
350
+ | Backend | `backend` |
351
+ | Frontend | `frontend` |
352
+ | DBA | `dba` |
353
+ | Security | `sec` |
354
+ | Reviewer | `reviewer` |
355
+ | QA | `qa` |
356
+ | DevOps | `devops` |
357
+ | Docs | `docs` |
358
+
359
+ **Phases (9)**: `clarify` → `research` → `design` → `spec-review` (**hard gate**) → `方案确认` plan approval (**approval gate**, on by default, can be turned off) → `implement` → `review` → `test` → `deliver`. (Ids and Chinese labels come from the single source `lib/vocab.js`.)
360
+
361
+ **Key artifacts**: `SPEC.md` (spec and boundaries) · `RESEARCH.md` · `PLAN.md` · `TASKS.json` (task ledger) · `ROSTER.json` (staffing) · `STATE.json` · `AUTHORITY.md` (single source for write authority) · `REVIEW.md` · `TEST.md` · `SUMMARY.md` · `METRICS.md` (cost and timing) · `RUN.log.md`
362
+
295
363
  ## Layout
296
364
 
297
365
  ```
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![license](https://img.shields.io/npm/l/@yangdcm/dsh-expert-team)](https://github.com/yangdcm/dsh-expert-team/blob/main/LICENSE)
7
7
  [![CI](https://github.com/yangdcm/dsh-expert-team/actions/workflows/ci.yml/badge.svg)](https://github.com/yangdcm/dsh-expert-team/actions/workflows/ci.yml)
8
8
 
9
- ![专家团:一句话组队交付](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/hero.svg)
9
+ ![dsh 专家团插件横幅:12 角色多智能体团队 · 9 阶段门控流水线 · 零运行时依赖](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/hero.svg)
10
10
 
11
11
  > **一句话组队交付**:`/team 做一个带登录的支付模块` —— 自动组建 12 角色专家团,走
12
12
  > 澄清 → 调研 → 设计 → 规格评审 → 方案确认 → 实现 → 审查 → 测试 → 交付 的门控流水线,
@@ -18,7 +18,26 @@
18
18
  |---|---|---|---|---|
19
19
  | 各带人设 / `toolFilter` / `maxDepth: 1` | 含 1 道硬门 + 1 道确认门 | 含 136 条变异目录与多组棘轮 | `dependencies: {}` | 无 bundler、无 `prepare` 钩子 |
20
20
 
21
- > 零运行时依赖。推荐同时装 **Hindsight**(跨项目记忆)—— 见[依赖与推荐插件](#依赖与推荐插件)。
21
+ > 零运行时依赖。推荐同时装 **Hindsight**(跨项目记忆)—— 见[依赖与推荐插件](#依赖与推荐插件--dependencies-and-recommended-plugins)。
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)
22
41
 
23
42
  ## 为谁而做
24
43
 
@@ -46,7 +65,7 @@ UI/UX、前后端、数据、安全、评审、测试、运维、文档这 12
46
65
 
47
66
  <sub>一句诚实边界:它**不是**人类团队 —— 产品级与范围级决策仍由你拍板。</sub>
48
67
 
49
- ![9 阶段门控流水线](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/pipeline.svg)
68
+ ![专家团 9 阶段门控流水线:澄清→调研→设计→规格评审(硬门)→方案确认→实现(依赖 DAG 并行)→审查→测试→交付](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/pipeline.svg)
50
69
 
51
70
  <sub>图 1:9 阶段门控流水线。「规格评审」是**硬门** —— SPEC.md 的「边界与禁止项」没填就不放行(`lib/interception.js`);「方案确认」是默认开启的**确认门**(`identity.keepPlanGate`,可在设置里关掉);「实现」阶段按依赖 DAG **并行扇出**,多个实现者同时开工、各自只改自己那份文件。</sub>
52
71
 
@@ -87,27 +106,27 @@ $ /team 做一个带登录的支付模块
87
106
 
88
107
  ## 看一眼它在干什么
89
108
 
90
- ![全屏画布:阶段条与团队编制](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/canvas.png)
109
+ ![专家团全屏画布:阶段条与进度、角色化 subagent 团队编制(谁在跑、用哪个模型)](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/canvas.png)
91
110
 
92
111
  <sub>图 2:**全屏画布**。看阶段条与进度、团队编制(谁在跑、用哪个模型)、以及 `人 / 事 / 料 / 盘` 四个视角 —— 比浮层更完整的一层视图。</sub>
93
112
 
94
- ![全屏画布 · 事:任务依赖图](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/canvas-tasks.png)
113
+ ![专家团任务依赖图:任务按依赖 DAG 并行,含 repair 与 review 的返工闭环](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/canvas-tasks.png)
95
114
 
96
115
  <sub>图 3:**任务依赖图** —— 11 个任务按依赖 DAG 并行推进;7 个完成、4 个失败。失败会触发 `repair` 与**独立复验**(`repair-1 → review-2 → repair-2 → review-3`),直到通过或被如实判为需修订 —— 这就是「返工不收敛」的硬门禁在真实运行里的样子。</sub>
97
116
 
98
- ![质量门禁违规实时可见](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/panel-gate.png)
117
+ ![专家团质量门禁违规实时横幅:规格边界未填即被插件代码拦下](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/panel-gate.png)
99
118
 
100
119
  <sub>图 4:**门禁违规**。看顶部那条横幅 —— 违规项与拒绝理由(例如"SPEC.md 的边界章节已进入 `implement` 但仍无任何一行填写")由 `lib/interception.js` 挂在宿主 `tools/post-execute` 上当场判出后推出,不是提示词提醒。</sub>
101
120
 
102
- ![成员模型与任务详情](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/panel-live.png)
121
+ ![专家团浮层:角色成员列表、各自使用的模型、任务详情与工件预览](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/panel-live.png)
103
122
 
104
123
  <sub>图 5:**角色编制**。看成员列表 —— 谁在跑、用哪个模型、当前在做什么;展开任一成员可看它的任务与产物。模型可按角色分别配置,异构模型用于交叉验证。</sub>
105
124
 
106
- ![阶段推进与工件预览](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/panel-flow.png)
125
+ ![专家团阶段推进视图:当前阶段、已过阶段与该阶段的工件正文](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/panel-flow.png)
107
126
 
108
127
  <sub>图 6:**阶段与工件**。看阶段条与预览区 —— 当前阶段、已过阶段、以及该阶段真正写下的工件正文(工件是唯一真源,浮层只是它的视图)。</sub>
109
128
 
110
- ![官方设置页里的专家团分节](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/settings.png)
129
+ ![DeepSeek Harness 官方设置页里的「专家团」分节:18 个设置项、中文标签、改动即时生效](https://raw.githubusercontent.com/yangdcm/dsh-expert-team/main/docs/images/settings.png)
111
130
 
112
131
  <sub>图 7:**设置**。看官方 `设置 →「专家团」` 这一页 —— 18 个设置项、中文标签、**改动即保存并即时生效**(上限/轮次/档位门/振荡检测在进程内重算);值存在宿主命名空间 `expert-team`,随插件市场的备份/恢复一起走。</sub>
113
132
 
@@ -131,7 +150,7 @@ $ /team 做一个带登录的支付模块
131
150
  还会堵住整个 `dsh web` 的事件循环。1.3.5 改成只查表(实测 0.0026 ms/次、零次 `readSession`),
132
151
  **修复后的端到端数字待实机复测**;`state-perf-guard.test.mjs` 守着它不许回退。
133
152
 
134
- ## 安装
153
+ ## 安装 / Installation
135
154
 
136
155
  **要求**
137
156
 
@@ -190,7 +209,7 @@ pnpm install && dsh web
190
209
  **排障**:预设丢了、或被同名预设占住 —— **重启一次 `dsh web` 即自愈**(插件加载会重铺),也可跑一次 `/team <任务>`。
191
210
  细节见下面「排障」一节,其中包含那条最容易踩的坑:**不要**用 `expert-team` 这个 id 去「创建 preset」。
192
211
 
193
- ## 依赖与推荐插件
212
+ ## 依赖与推荐插件 / Dependencies and recommended plugins
194
213
 
195
214
  **必需**:无。本插件**零运行时依赖**(`package.json` 无 `dependencies` 字段;`lib/` 只 import 同目录文件与 Node 内建,
196
215
  `client.js` 只 `require('react')`,由宿主提供),只要求宿主 `DeepSeek Harness ≥ 0.1.5-rc.1`(web profile)。
@@ -229,28 +248,77 @@ dsh plugin --profile web add dshmarket
229
248
  > 装了 `dsh-browser` / `dsh-mcp-connector` / `@xmanrui/dsh-im` 之后,活动流里出现的就是它们真实的工具名与参数:
230
249
  > **纯显示,装了更清楚,不装不影响团队功能。**
231
250
 
232
- ## 快速上手
233
-
234
- ```
235
- /team 做一个带登录的支付模块 # 一句话组队(一次性,自动组队并交付)
236
- /team --persist 重构订单模块 # 持久化活团队:成员可反复指挥、跨会话恢复
237
- /team --one-shot 跑一个小活 # 反向覆盖:即使默认设了持久化,这次也只跑一次
238
- /team --no-code 评审现有 API 设计 # 只产出计划/评审/测试工件,不改代码
239
- /team --code 直接改 # 反向覆盖:即使默认设了"只出工件",这次也动代码
240
- /team --confirm 大改版需求 # 先建 run、不自动派工,浮层点「执行」才开工
241
- /team uninstall # 回收本插件铺到 $DSH_HOME 的副本(skill 默认走运行时注册,本就不落地)
242
- /team status # 所有 run 的阶段、成员、模型计划、实时违规
243
- /team resume <run-id> # 跨会话恢复
244
- ```
251
+ ## 快速上手 / Quick start
245
252
 
246
- 完整命令(`/team canvas` 可视化画布、`/team codeindex` 代码索引、`/team learn` 自学习、
247
- `/team limit` 配额、`/team settle` 冷启动清算……)见 `/team help`。
253
+ | 命令 | 作用 |
254
+ |---|---|
255
+ | `/team <一句话目标>` | 一句话组队(一次性,自动组队并交付) |
256
+ | `/team --persist <任务>` | 持久化活团队:成员可反复指挥、跨会话恢复 |
257
+ | `/team --one-shot <任务>` | 反向覆盖:即使默认设了持久化,这次也只跑一次 |
258
+ | `/team --no-code <任务>` | 只产出计划/评审/测试工件,不改代码 |
259
+ | `/team --code <任务>` | 反向覆盖:即使默认"只出工件",这次也动代码 |
260
+ | `/team --confirm <任务>` | 先建 run、不自动派工,浮层点「执行」才开工 |
261
+ | `/team --tier <档位> <任务>` | 指定流程档位(快速档 / 标准档 / 严格档) |
262
+ | `/team status` | 所有 run 的阶段、成员、模型计划、实时违规 |
263
+ | `/team models [<run>]` | 每个角色的成本 / 模型计划 |
264
+ | `/team canvas [<run>]` | 生成可视化团队画布(HTML);`--watch` 实时刷新 |
265
+ | `/team learn` | 聚合日志 → `METRICS.md` + 蒸馏经验到 `LEARNINGS.md` |
266
+ | `/team wait [<run>]` | 查看在飞任务进展(不阻塞) |
267
+ | `/team resume <run-id>` | 跨会话恢复 |
268
+ | `/team uninstall` | 回收本插件铺到 `$DSH_HOME` 的副本(skill 走运行时注册,本就不落地) |
269
+
270
+ 完整命令(`/team codeindex` 代码索引、`/team limit` 配额、`/team settle` 冷启动清算……)见 `/team help`。
248
271
 
249
272
  **产物落在哪**
250
273
 
251
274
  - `<你的工作区>/team/<run-id>/` —— `SPEC / PLAN / TASKS / ROSTER / STATE / REVIEW / TEST / SUMMARY / RUN.log.md` 等工件
252
275
  - `$DSH_HOME/expert-team/` —— 本机偏好与跨项目经验:`settings.json`、`session-runs.json`、`LEARNINGS.md`
253
276
 
277
+ ## 常见问题 / FAQ
278
+
279
+ **它到底是什么?** 一个装在本机 `dsh` 上的插件:`/team <一句话目标>` 会拉起一支 12 角色的 subagent 团队(产品 / 架构 / 调研 / UI / 前后端 / 数据 / 安全 / 评审 / 测试 / 运维 / 文档),按 9 阶段门控流程在你的工作区里交付,并把过程写成可复核的工件。
280
+
281
+ **和"直接让一个 agent 硬做"有什么区别?** 针对三个固定失败模式:**上下文漂移**(阶段与工件双通道交接)、**自己批自己**(评审/测试是独立角色,`qa`/`reviewer` 裁决才算过)、**返工不收敛**(超轮次与未闭环被硬门禁拦下并如实报错)。详见[为什么不是「一个 agent 硬做」](#为什么不是一个-agent-硬做)。
282
+
283
+ **必须再装别的插件吗?** **不必**。本插件零运行时依赖;Hindsight(跨项目记忆)是**推荐**、`dsh-cost-meter`(费用视图)是**可选**,不装也能跑完整个流程 —— 见[依赖与推荐插件](#依赖与推荐插件--dependencies-and-recommended-plugins)。
284
+
285
+ **支持哪些 dsh 版本?** `engines.dsh: >=0.1.5-rc.1`(开发与验证基线 0.1.5-rc.1);更早版本未经测试。Node.js ≥ 20。
286
+
287
+ **数据放在哪?** 工件在你的工作区 `<workspace>/team/<run-id>/`;本机偏好与跨项目经验在 `$DSH_HOME/expert-team/`(`settings.json` / `LEARNINGS.md` / `session-runs.json`)。
288
+
289
+ **怎么卸载?** `/team uninstall` 回收它铺到 `$DSH_HOME` 的副本(skill 走运行时注册、本就不落地),再从命令行或插件市场移除插件。**注意**:`$DSH_HOME/expert-team/` 下的 `LEARNINGS.md` 等是**你的数据**,卸载不会删。
290
+
291
+ **会自己联网吗?** 不会主动联网:它只调用宿主提供的工具(文件、shell、子代理);能不能联网取决于你给会话的工具面。
292
+
293
+ **支持哪些模型?** 由宿主决定;本插件支持**按角色分别配置模型**(浮层里能看到每个成员用哪个模型),异构模型可用于交叉验证。
294
+
295
+ **不切「专家团模式」preset 也能用吗?** 能。`/team` 是 host 平面命令,任何预设下都能跑;此时退回通用 `subagent`(角色人设写进 prompt),少的是配置层的边界保证(`toolFilter` / `maxDepth: 1`)。
296
+
297
+ **浮层/画布打开很慢?** 见[排障](#排障) —— 1.3.5 起 `/state` 不再逐条全量读子会话日志;升级到 ≥ 1.3.5 后重启 `dsh web` 即可。
298
+
299
+ ## 术语 / Glossary
300
+
301
+ **角色(12)**
302
+
303
+ | 中文 | English | 代码标识 |
304
+ |---|---|---|
305
+ | 产品 | Product | `pm` |
306
+ | 架构 | Architect | `architect` |
307
+ | 调研 | Researcher | `researcher` |
308
+ | 界面设计 | UI/UX | `ui` |
309
+ | 后端 | Backend | `backend` |
310
+ | 前端 | Frontend | `frontend` |
311
+ | 数据 | DBA | `dba` |
312
+ | 安全审计 | Security | `sec` |
313
+ | 评审 | Reviewer | `reviewer` |
314
+ | 测试 | QA | `qa` |
315
+ | 运维 | DevOps | `devops` |
316
+ | 文档 | Docs | `docs` |
317
+
318
+ **阶段(9)**:`clarify` 澄清 → `research` 调研 → `design` 设计 → `spec-review` 规格评审(**硬门**)→ `方案确认` 方案确认(**确认门**,默认开、可关)→ `implement` 实现 → `review` 审查 → `test` 测试 → `deliver` 交付。(id 与中文名的唯一真源是 `lib/vocab.js`。)
319
+
320
+ **主要工件**:`SPEC.md`(规格与边界)· `RESEARCH.md`(调研)· `PLAN.md`(方案)· `TASKS.json`(任务台账)· `ROSTER.json`(编制)· `STATE.json`(状态)· `AUTHORITY.md`(写入权限单源)· `REVIEW.md`(评审)· `TEST.md`(测试)· `SUMMARY.md`(交付总结)· `METRICS.md`(成本与耗时)· `RUN.log.md`(运行日志)
321
+
254
322
  ## 插件结构
255
323
 
256
324
  ```
@@ -299,7 +367,7 @@ dsh 安装里插件自带的 Config schema(dsh 路径自动探测,可用 `DS
299
367
  **浮层打开很慢 / 整个 `dsh web` 发卡?** 1.3.5 之前 `/state` 会逐条全量读子会话日志(实测热态 7–10 s、
300
368
  冷态 283 s),并堵住事件循环。升级到 ≥ 1.3.5 后重启 `dsh web` 即可。
301
369
 
302
- ## 自定义预设(想改专家团默认行为时)
370
+ ## 自定义预设 / Custom presets(想改专家团默认行为时)
303
371
 
304
372
  - **创建**:`设置 → Agent 预设 → 用「创造模式」创作自定义预设`(其机制是"复制一份既有预设",产出落在 `$DSH_HOME/.agent-presets/<id>/`)。
305
373
  - **要定制专家团,请以「专家团模式」为源、换一个你自己的 id**(例如 `my-team`):复制出来的目录天然带上 12 个角色工具与它的 skill 目录,
package/lib/command.js CHANGED
@@ -985,16 +985,27 @@ function detectOptionalPlugins(ctx) {
985
985
  const needsAttention = items.filter((i) => i.status === 'missing' || i.status === 'installed-not-ready');
986
986
  let hint = '';
987
987
  if (needsAttention.length) {
988
+ // 分档文案(2026-09-15 真实踩到):对 `installed-not-ready` **不能**再给安装命令 ——
989
+ // 那等于叫用户装一个已经装好的东西(用户机器上 dsh-cost-meter 明明紧接着自己加载成功了,
990
+ // 我们却提示他去 `dsh plugin add dsh-cost-meter` ⇒ 假警报 + 误导)。
991
+ // · missing(确认没装)⇒ 给安装命令;
992
+ // · installed-not-ready(装了但此刻拿不到)⇒ 只说事实与可能原因,**不给命令**;
993
+ // · unknown ⇒ 不进 needsAttention,不出现。
988
994
  const parts = needsAttention.map((i) => {
989
- const why = i.status === 'missing' ? '未安装' : '已安装但当前不可用(未配置或被关闭)';
990
- return `${i.pkg}(${i.what}):${why} ${i.install}`;
995
+ const head = `${i.pkg}(${i.what})`;
996
+ return i.status === 'missing'
997
+ ? `${head}:未安装 ⇒ ${i.install}`
998
+ : `${head}:已安装但当前未就绪(可能未配置或被关闭;若刚装上,稍后或重启后再看)`;
991
999
  });
992
1000
  hint = `[expert-team] 推荐插件未就绪(不影响使用,只是少了对应体验):${parts.join(';')}`;
993
1001
  }
994
1002
  return { items, needsAttention, hint };
995
1003
  }
996
1004
  let OPTIONAL_HINT_DONE = false;
997
- /** 加载时提示一次(**装了/都齐则完全不吭声**;进程内最多一行)。 */
1005
+ /**
1006
+ * 提示一次(**装了/都齐则完全不吭声**;进程内最多一行)。
1007
+ * 语义保持:只在"真的有需要关注的东西"时打印,且只打印一次。
1008
+ */
998
1009
  function hintOptionalPluginsOnce(ctx) {
999
1010
  if (OPTIONAL_HINT_DONE) return '';
1000
1011
  try {
@@ -1005,7 +1016,53 @@ function hintOptionalPluginsOnce(ctx) {
1005
1016
  return r.hint;
1006
1017
  } catch { return ''; }
1007
1018
  }
1008
- function _resetOptionalHintOnce() { OPTIONAL_HINT_DONE = false; }
1019
+ /** 懒重探(`/team help` 等"真正用到"的时刻调用):结果更新,但同一次进程里仍最多打印一行。 */
1020
+ function recheckOptionalPlugins(ctx) {
1021
+ try { return detectOptionalPlugins(ctx); } catch { return { items: [], needsAttention: [], hint: '' }; }
1022
+ }
1023
+ // ── 探测时机(2026-09-15 第二轮修复)─────────────────────────────────────────────
1024
+ // **问题(用户贴的启动日志直接坐实)**:我们的 `apply()` 跑在其它插件之前,
1025
+ // 那一刻 `ctx.get('costMeter')` / 工具表里当然还没有它们 ⇒ 已装且可用的插件被误判成
1026
+ // `installed-not-ready`,日志里紧跟着 `dsh-cost-meter` 自己就加载成功了 ⇒ **假警报**。
1027
+ //
1028
+ // **宿主没有 app 级 ready 事件**(已核源码):`dsh-app-boot` 的 `boot()` 顺序是
1029
+ // `await mountRootInclude(...)` → `await ctx.get('loader')?.await()` → `assertEntriesActivated` → return,
1030
+ // 期间**不 emit** 任何"就绪"事件(只 emit `loader/config-update`)。
1031
+ // 而 `loader.await()` 我们也**不能**用:那棵树包含我们自己的挂载任务 ⇒ 在 init 里 await 它会自等死锁。
1032
+ // ⇒ 采用两条**不会有死锁**的机制:
1033
+ // ① 有界延迟重探(下面的 delays):每次重探都拿最新事实,直到"无待关注项"或重试用尽;
1034
+ // ② `ctx.inject([服务])` 事件驱动(服务/工具一旦出现立刻重探)—— 这是 cordis 的正确姿势,
1035
+ // 服务始终不出现时回调**只是不触发**,不会报错、也不阻塞。
1036
+ // 结果:**已装且可用的插件不再被误报**;刚装上的插件也不必为了这行提示再重启一次(懒重探兜底)。
1037
+ const OPTIONAL_PROBE_DELAYS_MS = [250, 1000, 3000];
1038
+ let OPTIONAL_PROBE_TIMERS = [];
1039
+ function scheduleOptionalPluginCheck(ctx, delays) {
1040
+ const plan = Array.isArray(delays) ? delays : OPTIONAL_PROBE_DELAYS_MS;
1041
+ const timers = [];
1042
+ const run = (isLast) => {
1043
+ if (OPTIONAL_HINT_DONE) return;
1044
+ let r;
1045
+ try { r = detectOptionalPlugins(ctx); } catch { return; }
1046
+ if (!r.needsAttention.length) { OPTIONAL_HINT_DONE = true; return; } // 都就绪 ⇒ 一声不吭(且不再打扰)
1047
+ if (isLast) { OPTIONAL_HINT_DONE = true; console.log(r.hint); }
1048
+ };
1049
+ plan.forEach((ms, i) => {
1050
+ const t = setTimeout(() => run(i === plan.length - 1), ms);
1051
+ if (t && typeof t.unref === 'function') t.unref();
1052
+ timers.push(t);
1053
+ });
1054
+ OPTIONAL_PROBE_TIMERS = timers;
1055
+ // 事件驱动兜底:服务一出现就重探(不出现则永不触发,零副作用)。
1056
+ for (const svc of ['costMeter', 'tools']) {
1057
+ try { if (ctx && typeof ctx.inject === 'function') ctx.inject([svc], () => run(true)); } catch { /* best-effort */ }
1058
+ }
1059
+ return timers;
1060
+ }
1061
+ function _resetOptionalHintOnce() {
1062
+ OPTIONAL_HINT_DONE = false;
1063
+ for (const t of OPTIONAL_PROBE_TIMERS) { try { clearTimeout(t); } catch { /* ignore */ } }
1064
+ OPTIONAL_PROBE_TIMERS = [];
1065
+ }
1009
1066
 
1010
1067
  function warnInstallFailureOnce(kind) {
1011
1068
  if (INSTALL_FAILURE_WARNED.has(kind)) return;
@@ -2889,39 +2946,66 @@ function subHeaderIndex(ctx, subs, wfLabels) {
2889
2946
  }
2890
2947
 
2891
2948
  /**
2892
- * 并发合并 + TTL 的 `sessionQuery.listSessions()`。
2949
+ * 并发合并 + **目录戳失效** + 长 TTL 的 `sessionQuery.listSessions()`。
2893
2950
  *
2894
2951
  * 为什么需要它(2026-09-15 性能诊断):宿主的 listSessions 在持久化层是
2895
- * **枚举全部 artifact、逐个读 header**(本机 475 个、实测 ~1.5–2.4 s),而 /state 每 3 秒轮询;
2896
- * 客户端在飞重叠时更会 6 个请求同时跑 ⇒ 同一份枚举被重算 6 遍。
2952
+ * **枚举全部 artifact、逐个读 header**(本机 475 个、实测 ~1.5–2.4 s),而 /state 每 3 秒轮询。
2953
+ *
2954
+ * **2026-09-15 第二轮(profile 实测 subs 段 1.3–1.4 s 之后的纠正)**:
2955
+ * ① **2 s TTL 等于没有**:真实请求间隔就在 2 s 上下(轮询 + 退避),必然过期 ⇒ 每请求重算。
2956
+ * 现在 TTL 只作**兜底**(60 s),主失效键改成 `(sessions 根目录, mtimeMs, size)`
2957
+ * —— 新增/删除会话必然改动目录 mtime/size ⇒ **立刻失效**,不必靠 TTL 猜新鲜度。
2958
+ * ② **真正省时的是下面的 `SUB_HEADER_MEMO`**:按 session id 记住**不可变**的 header 字段,
2959
+ * 第二个请求起**根本不再枚举**。这才是把 subs 段打到近 0 的手段,而不是调 TTL 骗自己。
2897
2960
  *
2898
2961
  * **缓存纪律(本仓对"看到的就是真的"很敏感,所以写死在这里)**:
2899
- * · 只服务"**已结束/已释放**的子代理"这一支(`listSubagentStatusBySession` 的兜底分支)——
2900
- * 它们的 header 不再变化,短 TTL 不会让用户看到旧状态;
2901
- * · **活代理**的状态一律走 listChildren/agents **实时读**,绝不经过这个缓存;
2902
- * · 失效条件:2 s TTL(兜底)+ 并发合并(同刻只跑一次)。TTL 之后必然重算,
2903
- * 所以"新生效的会话"最多晚 2 秒可见 —— 这是**有界的**,且不会让在飞代理看起来停滞。
2962
+ * · 只服务"活注册表里**查不到**的子代理"这一支 ⇒ 都是**已结束/历史**会话,其 header 不再变化;
2963
+ * · **活代理**的状态一律走 listChildren/agents **实时读**,绝不经过这里的任何缓存;
2964
+ * · 目录戳取不到时退化为 60 s TTL 兜底(有界:新生效会话最多晚 60 s 可见,且只在 stat 失败时)。
2904
2965
  */
2905
- const LIST_SESSIONS_TTL_MS = 2000;
2906
- let LIST_SESSIONS_CACHE = { at: 0, sessions: null, inflight: null };
2907
- async function cachedListSessions(q) {
2966
+ const LIST_SESSIONS_TTL_MS = 60000;
2967
+ let LIST_SESSIONS_CACHE = { at: 0, stamp: null, sessions: null, inflight: null };
2968
+ /** 会话目录的廉价失效戳(mtime + size)。取不到 ⇒ null ⇒ 只用 TTL 兜底。 */
2969
+ async function sessionsRootStamp() {
2970
+ try {
2971
+ const st = await stat(join(dshHome(), 'sessions'));
2972
+ return `${Math.round(Number(st.mtimeMs) || 0)}:${Number(st.size) || 0}`;
2973
+ } catch { return null; }
2974
+ }
2975
+ async function cachedListSessions(q, stamp) {
2908
2976
  const now = Date.now();
2909
- if (LIST_SESSIONS_CACHE.sessions && (now - LIST_SESSIONS_CACHE.at) < LIST_SESSIONS_TTL_MS) {
2977
+ const stampOk = (stamp === undefined || stamp === null) ? true : (LIST_SESSIONS_CACHE.stamp === stamp);
2978
+ if (LIST_SESSIONS_CACHE.sessions && (now - LIST_SESSIONS_CACHE.at) < LIST_SESSIONS_TTL_MS && stampOk) {
2910
2979
  return LIST_SESSIONS_CACHE.sessions;
2911
2980
  }
2912
2981
  if (LIST_SESSIONS_CACHE.inflight) return LIST_SESSIONS_CACHE.inflight; // 同刻只有一个枚举在跑
2982
+ SUB_HEADER_STATS.enumCalls += 1;
2913
2983
  const p = Promise.resolve()
2914
2984
  .then(() => q.listSessions())
2915
2985
  .then((sessions) => {
2916
- LIST_SESSIONS_CACHE = { at: Date.now(), sessions: Array.isArray(sessions) ? sessions : [], inflight: null };
2986
+ LIST_SESSIONS_CACHE = { at: Date.now(), stamp: (stamp === undefined ? null : stamp), sessions: Array.isArray(sessions) ? sessions : [], inflight: null };
2917
2987
  return LIST_SESSIONS_CACHE.sessions;
2918
2988
  })
2919
- .catch((e) => { LIST_SESSIONS_CACHE = { at: 0, sessions: null, inflight: null }; throw e; });
2989
+ .catch((e) => { LIST_SESSIONS_CACHE = { at: 0, stamp: null, sessions: null, inflight: null }; throw e; });
2920
2990
  LIST_SESSIONS_CACHE.inflight = p;
2921
2991
  return p;
2922
2992
  }
2923
2993
  /** 仅供测试:清空枚举缓存并复位(生产代码不需要调用它)。 */
2924
- function _resetListSessionsCache() { LIST_SESSIONS_CACHE = { at: 0, sessions: null, inflight: null }; }
2994
+ function _resetListSessionsCache() { LIST_SESSIONS_CACHE = { at: 0, stamp: null, sessions: null, inflight: null }; }
2995
+
2996
+ // ── 已结束子会话的 header 备忘(按 session id,**永久**)─────────────────────────
2997
+ // createdAt / parentId / depth 对**已结束**会话是不可变的;而用到备忘的这条分支,其 id 恰恰是
2998
+ // "活注册表里查不到"的那些 ⇒ 记住它们**不会**让用户看到旧状态(活代理走实时路径,永不写入这里)。
2999
+ // 旧实现每请求都要枚举 475 个 artifact 才能回答"这些人是何时建的、谁派工的"——
3000
+ // 有了备忘,第二个请求起直接查表。
3001
+ const SUB_HEADER_MEMO = new Map();
3002
+ const SUB_HEADER_STATS = { memoHits: 0, memoWrites: 0, enumCalls: 0 };
3003
+ function _resetSubHeaderMemo() {
3004
+ SUB_HEADER_MEMO.clear();
3005
+ SUB_HEADER_STATS.memoHits = 0;
3006
+ SUB_HEADER_STATS.memoWrites = 0;
3007
+ SUB_HEADER_STATS.enumCalls = 0;
3008
+ }
2925
3009
 
2926
3010
  async function listSubagentStatusBySession(ctx, sid, knownIds) {
2927
3011
  const root = sid ? await rootSessionId(ctx, sid) : '';
@@ -2955,36 +3039,52 @@ async function listSubagentStatusBySession(ctx, sid, knownIds) {
2955
3039
  // Fallback: completed subagents may be disposed from the live registry —
2956
3040
  // the durable session projection (sessionQuery) still lists them.
2957
3041
  //
2958
- // ⚠️ 2026-09-15 性能修复:**只有真的缺人才花这笔钱**。
2959
- // 旧实现只要 `knownIds` 非空就无条件 `listSessions()`,而它在宿主侧是"枚举全部 artifact、
2960
- // 逐个读 header"(本机 475 个、~1.5–2.4 s);代理都还在活注册表里时这笔钱白花。
2961
- // 现在先算"活注册表里查不到的人",一个都不缺就整段跳过 常见情形直接省下这笔开销。
2962
- // 并发合并与 TTL 见 cachedListSessions() 的注释(缓存纪律写在那里)。
3042
+ // ⚠️ 2026-09-15 两轮性能修复:
3043
+ // 第一轮:只有**真的缺人**才花这笔钱(旧实现无条件 listSessions());
3044
+ // 第二轮:缺的那些**先查 `SUB_HEADER_MEMO`**(已结束会话的 header 不可变 永久备忘),
3045
+ // 只有仍然未知的才去枚举第二个请求起通常**零枚举**。
2963
3046
  const missingIds = (knownIds || []).filter((id) => !rows.some((r) => r.id === id));
2964
3047
  if (missingIds.length) {
2965
- try {
2966
- const q = (ctx && typeof ctx.get === 'function') ? ctx.get('sessionQuery') : null;
2967
- if (q && typeof q.listSessions === 'function') {
2968
- const sessions = await cachedListSessions(q);
2969
- const byId = new Map(rows.map((r) => [r.id, r]));
2970
- for (const rec of sessions || []) {
2971
- const h = rec?.header || rec;
2972
- const aid = h?.id;
2973
- if (!aid || byId.has(aid) || !knownIds.includes(aid)) continue;
2974
- // ⚠️ header 里的 createdAt / parentSession / delegationDepth **必须带出来**。
2975
- // 旧实现只取 id,把它们丢掉,导致面板上每个成员都是 createdAt=0 ⇒ 流转图
2976
- // 判为「无创建时间记录,无法分批」,尽管会话日志第一行明明写着
2977
- // {"type":"session","createdAt":…,"parentSession":…,"delegationDepth":1}。
2978
- byId.set(aid, {
2979
- id: aid, mode: 'continuable', label: '', activity: 'inactive', model: '',
2980
- createdAt: Number(h?.createdAt) || 0,
2981
- parentId: String(h?.parentSession || ''),
2982
- depth: Number(h?.delegationDepth) || 0,
2983
- });
3048
+ const memoById = new Map(rows.map((r) => [r.id, r]));
3049
+ const stillUnknown = [];
3050
+ for (const id of missingIds) {
3051
+ const memo = SUB_HEADER_MEMO.get(id);
3052
+ if (!memo) { stillUnknown.push(id); continue; }
3053
+ SUB_HEADER_STATS.memoHits += 1;
3054
+ memoById.set(id, {
3055
+ id, mode: 'continuable', label: '', activity: 'inactive', model: '',
3056
+ createdAt: memo.createdAt, parentId: memo.parentId, depth: memo.depth,
3057
+ });
3058
+ }
3059
+ rows = [...memoById.values()];
3060
+ if (stillUnknown.length) {
3061
+ try {
3062
+ const q = (ctx && typeof ctx.get === 'function') ? ctx.get('sessionQuery') : null;
3063
+ if (q && typeof q.listSessions === 'function') {
3064
+ const sessions = await cachedListSessions(q, await sessionsRootStamp());
3065
+ const byId = new Map(rows.map((r) => [r.id, r]));
3066
+ for (const rec of sessions || []) {
3067
+ const h = rec?.header || rec;
3068
+ const aid = h?.id;
3069
+ if (!aid || byId.has(aid) || !knownIds.includes(aid)) continue;
3070
+ // ⚠️ header 里的 createdAt / parentSession / delegationDepth **必须带出来**。
3071
+ // 旧实现只取 id,把它们丢掉,导致面板上每个成员都是 createdAt=0 ⇒ 流转图
3072
+ // 判为「无创建时间记录,无法分批」,尽管会话日志第一行明明写着
3073
+ // {"type":"session","createdAt":…,"parentSession":…,"delegationDepth":1}。
3074
+ const entry = {
3075
+ createdAt: Number(h?.createdAt) || 0,
3076
+ parentId: String(h?.parentSession || ''),
3077
+ depth: Number(h?.delegationDepth) || 0,
3078
+ };
3079
+ // 只有"活注册表里查不到"的 id 会走到这里(活代理走实时路径)⇒ header 不可变 ⇒ 备忘安全。
3080
+ SUB_HEADER_MEMO.set(aid, entry);
3081
+ SUB_HEADER_STATS.memoWrites += 1;
3082
+ byId.set(aid, { id: aid, mode: 'continuable', label: '', activity: 'inactive', model: '', ...entry });
3083
+ }
3084
+ rows = [...byId.values()];
2984
3085
  }
2985
- rows = [...byId.values()];
2986
- }
2987
- } catch { /* best-effort */ }
3086
+ } catch { /* best-effort */ }
3087
+ }
2988
3088
  }
2989
3089
  return Array.isArray(rows) ? rows : [];
2990
3090
  }
@@ -3056,16 +3156,52 @@ const SUB_ROLE_LOG_CACHE = new Map();
3056
3156
  // 投影/限量/流式参数 —— `projectionMode` 只存在于内部 `corpus.read`,公开方法不暴露;
3057
3157
  // `filterEvents`/`readEvent` 也都是"先把整条日志 load 进内存再筛"。要"只读首帧"只能绕过
3058
3158
  // 公开接口去碰持久化层的内部方法(`readFirstZstdLine` 之类),那是**非契约、脆的**。
3059
- // ⇒ 采用**有界**做法:每次 /state 最多读 N 条子会话日志(角色结果**永久缓存**,几个 tick
3060
- // 内自然收敛),其余留到下一个 tick。**这不是"优化成流式",而是"把一次 20 秒的尖峰摊平"**,
3061
- // 并把"本轮被推迟"的条数如实交给调用方(rolesDeferred)⇒「还没解析」与「解析不出来」
3062
- // 两件事分得清(本仓纪律:两种零必须可区分),**不臆造角色**。
3159
+ // ⇒ 采用**有界**做法:每次 /state 最多读 N 条子会话日志(角色结果**永久缓存**),其余留到下一个 tick
3160
+ // **这不是"优化成流式",而是"把一次 20 秒的尖峰摊平"**,并把"本轮被推迟"的条数如实交给
3161
+ // 调用方(rolesDeferred)⇒「还没解析」与「解析不出来」两件事分得清
3162
+ // (本仓纪律:两种零必须可区分),**不臆造角色**。
3163
+ //
3164
+ // ⚠️ 2026-09-15 第二轮(profile 实测 roles 段 0.6–0.9 s、且 `deferred` 从 16 涨到 40 之后):
3165
+ // 旧实现每请求都把候选集**从零重算**,预算永远花在队首那几条上,而新派的人不断出现在队尾
3166
+ // ⇒ **进度不前进、deferred 只增不减**("限次摊平"退化成"永远摊不完")。
3167
+ // 现在改成**跨请求保留进度**:每次按"当前 subs 里仍未缓存"的顺序**重建队列**(已缓存的自动掉队、
3168
+ // 既有顺序保留),再从队首消费 ≤N 条 ⇒ 进度单调推进,`deferred` 单调不增(除非真的又新派了人)。
3063
3169
  const ROLE_READ_BUDGET_PER_REQUEST = 4;
3064
3170
  let ROLE_READS_LEFT = ROLE_READ_BUDGET_PER_REQUEST;
3065
- let ROLE_READS_DEFERRED = 0;
3066
- /** /state 每次进来复位预算(同一请求内多次调用共享这一份)。 */
3067
- function resetRoleReadBudget() { ROLE_READS_LEFT = ROLE_READ_BUDGET_PER_REQUEST; ROLE_READS_DEFERRED = 0; }
3068
- function roleReadBudgetSnapshot() { return { per: ROLE_READ_BUDGET_PER_REQUEST, left: ROLE_READS_LEFT, deferred: ROLE_READS_DEFERRED }; }
3171
+ /** 待解析队列(跨请求保留进度):元素是子会话 id。 */
3172
+ let ROLE_PENDING = [];
3173
+ /** /state 每次进来复位**本请求的读取额度**(队列进度**不**复位)。 */
3174
+ function resetRoleReadBudget() { ROLE_READS_LEFT = ROLE_READ_BUDGET_PER_REQUEST; }
3175
+ /**
3176
+ * 按当前 subs 重建待解析队列:保留既有顺序里"仍在场且仍未缓存"的,再把新出现的未缓存 id 追加到队尾。
3177
+ * 幂等、无副作用;已缓存(含"解析不出来"缓存为 '')的条目自动掉队。
3178
+ */
3179
+ function syncRolePending(subs) {
3180
+ const present = new Set();
3181
+ for (const s of subs || []) { const id = String((s && s.id) || ''); if (id) present.add(id); }
3182
+ const seen = new Set();
3183
+ const next = [];
3184
+ for (const id of ROLE_PENDING) {
3185
+ if (SUB_ROLE_LOG_CACHE.has(id) || !present.has(id) || seen.has(id)) continue;
3186
+ seen.add(id); next.push(id);
3187
+ }
3188
+ for (const s of subs || []) {
3189
+ const id = String((s && s.id) || '');
3190
+ if (!id || seen.has(id) || SUB_ROLE_LOG_CACHE.has(id)) continue;
3191
+ seen.add(id); next.push(id);
3192
+ }
3193
+ ROLE_PENDING = next;
3194
+ return ROLE_PENDING;
3195
+ }
3196
+ /** 仅供测试:清空待解析队列(生产代码不需要调用它)。 */
3197
+ function _resetRolePending() { ROLE_PENDING = []; }
3198
+ function roleReadBudgetSnapshot() {
3199
+ // deferred = **还没解析**(队列里等着读的);unresolved = **解析不出来**(读过但日志里没有角色)。
3200
+ // 两者必须是两个数:本仓纪律"两种零要分得清"。
3201
+ let unresolved = 0;
3202
+ for (const v of SUB_ROLE_LOG_CACHE.values()) if (!v) unresolved += 1;
3203
+ return { per: ROLE_READ_BUDGET_PER_REQUEST, left: ROLE_READS_LEFT, deferred: ROLE_PENDING.length, unresolved };
3204
+ }
3069
3205
  /**
3070
3206
  * Infer a child's role from its own session log. **Only the first `user/message`
3071
3207
  * counts** — it is the delegation prompt the lead passed, e.g.
@@ -3078,9 +3214,9 @@ function roleReadBudgetSnapshot() { return { per: ROLE_READ_BUDGET_PER_REQUEST,
3078
3214
  async function roleFromChildLog(ctx, id) {
3079
3215
  if (!id) return '';
3080
3216
  if (SUB_ROLE_LOG_CACHE.has(id)) return SUB_ROLE_LOG_CACHE.get(id);
3081
- // 预算用尽 ⇒ 本轮不读,**且不写缓存**(下一 tick 再试)。这是"本轮被推迟",
3082
- // 不是"解析不出来" —— 调用方通过 rolesDeferred 如实区分两者(两种零可区分)。
3083
- if (ROLE_READS_LEFT <= 0) { ROLE_READS_DEFERRED += 1; return ''; }
3217
+ // 预算用尽 ⇒ 本轮不读,**且不写缓存**(下一个 tick 再试)。这是"本轮被推迟",
3218
+ // 不是"解析不出来" —— 调用方通过 rolesDeferred(队列长度)如实区分两者(两种零可区分)。
3219
+ if (ROLE_READS_LEFT <= 0) { return ''; }
3084
3220
  ROLE_READS_LEFT -= 1;
3085
3221
  let role = '';
3086
3222
  try {
@@ -3232,16 +3368,33 @@ async function workflowRuns(ctx, sid, subs) {
3232
3368
  * (2026-09-11 真实踩到:30 个子代理里只有 5 个有角色,正是 wfLabels 解析不出的那 5 个。)
3233
3369
  */
3234
3370
  async function resolveSubRoles(ctx, subs, wfLabels) {
3371
+ const byId = new Map();
3372
+ for (const s of subs || []) { const id = String((s && s.id) || ''); if (id) byId.set(id, s); }
3373
+ // ① 便宜来源(自带 label / 父会话事件流 label)与已缓存结果 —— **不花预算、不读日志**
3235
3374
  for (const s of subs || []) {
3236
3375
  try {
3237
- // ① 自带 label(显式角色/括号 id/关键词)与 ③ 父会话事件流的 workflow 派工 label
3238
3376
  const known = roleOfSub(s, wfLabels);
3239
3377
  if (known) { if (!s.role) s.role = known; continue; }
3240
- // 最后兜底:子会话首条 prompt
3241
- const r = await roleFromChildLog(ctx, String(s.id || ''));
3242
- if (r) s.role = r;
3378
+ const id = String((s && s.id) || '');
3379
+ if (!id) continue;
3380
+ if (SUB_ROLE_LOG_CACHE.has(id)) {
3381
+ const cached = SUB_ROLE_LOG_CACHE.get(id);
3382
+ if (cached && !s.role) s.role = cached;
3383
+ }
3243
3384
  } catch (e) { /* best-effort */ }
3244
3385
  }
3386
+ // ② 跨请求推进:重建待解析队列(保留进度、已缓存的掉队、新人追加到队尾),再消费本请求额度。
3387
+ // 这样 `deferred`(队列长度)只减不增,除非真的又新派了人 —— 旧实现每请求从零重算,
3388
+ // 预算永远喂给队首同几条,新派的人永远轮不到(实测 deferred 16 → 40)。
3389
+ syncRolePending(subs);
3390
+ while (ROLE_READS_LEFT > 0 && ROLE_PENDING.length) {
3391
+ const id = ROLE_PENDING.shift();
3392
+ if (SUB_ROLE_LOG_CACHE.has(id)) continue;
3393
+ const r = await roleFromChildLog(ctx, id);
3394
+ if (!SUB_ROLE_LOG_CACHE.has(id)) { ROLE_PENDING.unshift(id); break; } // 预算耗尽(未写缓存)⇒ 放回队首,下一轮继续
3395
+ const s = byId.get(id);
3396
+ if (s && r && !s.role) s.role = r;
3397
+ }
3245
3398
  return subs;
3246
3399
  }
3247
3400
  // sub rows → Map<role, sub> (first match wins; unknown labels map to '' and
@@ -4767,7 +4920,10 @@ async function executeTeamCommand(ctx, invocation) {
4767
4920
  const memRun = (mem && mem.workspace === cwd) ? mem.runId : '';
4768
4921
  const runArg = (s) => s || memRun;
4769
4922
  switch (c.kind) {
4770
- case 'help': return { kind: 'success', text: USAGE };
4923
+ // 懒重探兜底(2026-09-15):加载当刻的探测可能"太早"(那时别人的服务还没挂上)。
4924
+ // `/team help` 是"用户真的来看能做什么"的时刻 ⇒ 顺手重探一次并更新结论,
4925
+ // 这样刚装上/刚修好配置的插件**不必为了这一行提示再重启**。
4926
+ case 'help': recheckOptionalPlugins(ctx); return { kind: 'success', text: USAGE };
4771
4927
  case 'status': return renderStatus(cwd);
4772
4928
  case 'members': return renderMembers(cwd);
4773
4929
  case 'models': return renderModels(cwd, runArg(c.run));
@@ -4812,7 +4968,7 @@ function warnLeadToolFaceOnce(status, detail) {
4812
4968
  LEAD_TOOLFACE_WARNED.add(status);
4813
4969
  console.warn(`[expert-team] lead 工具面**未**收窄(${status}):${detail}`);
4814
4970
  }
4815
- export const _live = { pushActivity, phaseAccountingViolations, loggedPhases, authorityViolations, WRITE_TRACER, createWriteTracer, formatConflict, summarizeTool, parseLogLine, roleOfSub, mapRoleToSub, membersFromState, 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, 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, hintOptionalPluginsOnce, _resetOptionalHintOnce, loaderEntryNames, hindsightToolReady, costMeterReady, listSubagentStatusBySession, cachedListSessions, _resetListSessionsCache, LIST_SESSIONS_TTL_MS, resetRoleReadBudget, roleReadBudgetSnapshot, ROLE_READ_BUDGET_PER_REQUEST, ROLE_READ_LOG_CACHE: SUB_ROLE_LOG_CACHE };
4971
+ export const _live = { pushActivity, phaseAccountingViolations, loggedPhases, authorityViolations, WRITE_TRACER, createWriteTracer, formatConflict, summarizeTool, parseLogLine, roleOfSub, mapRoleToSub, membersFromState, 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, 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, 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 };
4816
4972
 
4817
4973
  export function apply(ctx, config) {
4818
4974
  // 留一份 config:设置在运行时改变(官方面板 / 浮层)时要**用同一份 config** 重算上限与档位门,
@@ -4858,8 +5014,10 @@ export function apply(ctx, config) {
4858
5014
  // skill 已有运行时注册时这里立即返回(零成本);只有宿主没有 skill 注册表时才真复制。
4859
5015
  installInBackground('skill', () => ensureSkillInstalled());
4860
5016
  // 推荐插件自检:纯读、缺了只打一行、装了完全不打扰(见 detectOptionalPlugins 的注释)。
4861
- // 放在加载路径上但**不 await**(探测本身是同步的,这里只是不阻塞后续注册)。
4862
- hintOptionalPluginsOnce(ctx);
5017
+ // ⚠️ **不能在加载当刻就下结论**:我们的 apply() 跑在其它插件之前,那一刻 costMeter/工具表
5018
+ // 里还没有它们 ⇒ 已装且可用的插件会被误判成"已安装但当前未就绪"(2026-09-15 真实假警报)。
5019
+ // 现在改为**就绪后重探**(有界延迟 + `ctx.inject` 事件驱动,见 scheduleOptionalPluginCheck)。
5020
+ scheduleOptionalPluginCheck(ctx);
4863
5021
  void loadSessionRuns(); // session→run memory for overlay auto-select
4864
5022
  ctx.commands.register({
4865
5023
  name: 'team',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yangdcm/dsh-expert-team",
3
- "version": "1.3.10",
3
+ "version": "1.3.12",
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",
@@ -16,7 +16,11 @@
16
16
  "workflow",
17
17
  "subagent",
18
18
  "code-review",
19
- "quality-gate"
19
+ "quality-gate",
20
+ "role-based",
21
+ "spec-driven",
22
+ "dag",
23
+ "agentic-workflow"
20
24
  ],
21
25
  "repository": {
22
26
  "type": "git",