@yangdcm/dsh-expert-team 1.3.4 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,86 @@
3
3
  本包遵循[语义化版本](https://semver.org/lang/zh-CN/)。dsh 宿主版本线的对应关系写在
4
4
  `package.json` 的 `engines.dsh` 与 `dsh.compatibility` 里,插件市场按它判断"这个插件跟你的宿主兼不兼容"。
5
5
 
6
+ ## 1.3.6
7
+
8
+ **文档与素材:README 重写成"能一眼看懂它在干什么"的结构,并补上手绘矢量图。**
9
+
10
+ 纯文档/资产版本 —— `lib/`、`client.js` **零改动**,无依赖变化,`files` 字段未动。
11
+
12
+ ### README(中英一致)
13
+
14
+ - 新骨架:一句话组队交付 → **一行关键指标** → 流水线图 → **30 秒看懂**(阶段 → 谁在做 → 落哪个工件,
15
+ 工件名全部取自包内模板与代码)→ 为什么不是"一个 agent 硬做" → **看一眼它在干什么**(图 2–5,逐图编号图注)
16
+ → 它为什么可靠 → 安装/上手/插件结构/开发/排障/自定义预设/**诚实边界**/兼容性/许可。
17
+ - 新增"它为什么可靠"一节,把工程纪律写成可核对的卖点:状态机由**插件代码**强制(`lib/interception.js` 挂
18
+ `tools/post-execute`)、零依赖零构建零安装钩子、**80 个测试文件**、**136 条变异目录**、多组棘轮
19
+ (`vocab-consistency` / `scan-single-source` / `write-bypass-ratchet` / `settings-consumers` / `state-perf-guard`)、
20
+ "两种零要分得清"、失败必须出声。
21
+ - 关键数字都标注了出处(`lib/vocab.js`、`presets/expert-team/agent.cordis.yml`、`package.json`),不写没有出处的指标;
22
+ 性能修复的端到端数字明确标注**待实机复测**。
23
+
24
+ ### 新增素材
25
+
26
+ - `docs/images/pipeline.svg` —— 手绘矢量:9 阶段流水线、**硬门**(规格评审)与**确认门**(方案确认,默认开、可关)
27
+ 的区分、实现阶段的依赖 DAG 并行扇出、每阶段下挂角色 chip。
28
+ - `docs/images/hero.svg` —— 横幅(深色底、项目名、一句话、三个数字)。
29
+ - `docs/images/settings.png` —— 官方 `设置 →「专家团」` 分节截图(18 项、中文标签、改动即保存并即时生效)。
30
+ - 两张 SVG 均为**纯矢量**:无脚本、无 `<style>`、无外部引用(属性直挂,避免被托管方消毒器剥掉样式)。
31
+
32
+ ### 已知取舍
33
+
34
+ - `files` 未包含 `docs/`,因此 **npm 页面上的 README 图片可能不显示**(GitHub 上正常)。若要两处都显示,
35
+ 需要把 `docs/images` 加进 `files`(改变产物契约)或改用绝对图片地址 —— 本轮**有意未改**,留待决定。
36
+
37
+ ## 1.3.5
38
+
39
+ **性能:修掉 `/state` 的 30 秒级阻塞(它会把整个 `dsh web` 一起拖慢)**,另带两件早已排定的小事。
40
+
41
+ ### 症状与根因(都是实测值,不是估算)
42
+
43
+ - `GET /plugins/dsh-expert-team/state` 实测 **热态 7.1–9.8 s、冷态 283.6 s**,而面板**绘制只要 52 ms**。
44
+ - **根因 D(最大头)**:端点对**每个**子会话 `await childSessionTiming`,而它对"活存储里查不到"的子代理
45
+ 退回 `sessionQuery.readSession(id)` —— 那是**全量读该子会话日志**。本机实测:单条最大日志
46
+ **23.4 MiB(70,008 个 zstd frame)→ 解压 0.44 s / 45.4 MiB / 94,820 个事件**;21 条 >1MB 的会话日志
47
+ 仅"解压 + 过管道"就要 **1.11 s**(纯磁盘下限;host 还要逐行 `JSON.parse` + `structuredClone` + replay)。
48
+ - **根因 B**:`WF_EVENT_TTL_MS` 原来是 **3000 ms —— 恰好等于默认轮询间隔 3000 ms** ⇒ **每个 tick 必失效**,
49
+ 父会话日志(多 MB)被一遍遍全量重读。
50
+ - **根因 H3**:客户端**没有 single-flight** —— 实测 8 秒内发出 5 个 `/state`、其中 **4 个重叠**(上一个没回就发下一个)。
51
+ - **后果(这才最要紧)**:这些同步 CPU(≈2.3 s/请求)压在事件循环上,把**整个 web** 拖慢 ——
52
+ 轻量端点 `/settings` 实测被拖到 **20.7 s / 42.5 s**(关掉面板后仍在排队,因为积压已经形成)。
53
+
54
+ ### 修复
55
+
56
+ - **热点 D**:新增 `subHeaderIndex()` —— 子会话时间**只查表、永不读日志**。三个来源都便宜:
57
+ ① `listSubagentStatusBySession` 的 `listSessions()` 分支本来就顺带带出了 `createdAt/parentId/depth`;
58
+ ② 活存储 `ctx.sessions.get(id).header`;③ workflow 事件流的 `startedAt`(父会话日志已按 TTL 缓存)。
59
+ `/state` 的两个计时调用点全部改用它。三源都拿不到仍**如实降级** `0/''/0`(UI 显示「无时间记录」),**不臆造**。
60
+ - **热点 B**:`WF_EVENT_TTL_MS` 3000 → **30000**(TTL 只是兜底;代价是"run 在会话中途结束"最多晚 30 秒反映到面板)。
61
+ - **客户端**:`load()` 加 **single-flight**(未回绝不发下一个)+ **自适应退避**
62
+ `clamp(max(基础间隔, 上次耗时×2), 基础间隔, 30000)`;`display.pollMs` 仍是基础节奏(设置说了算)。
63
+ - **写侧守卫**:`/state` 只在**该 session 真实存在**时才 `rememberSessionRun` 落盘 —— 诊断时一个**假 id**
64
+ 曾被写进 `$DSH_HOME/expert-team/session-runs.json`(污染用户数据)。
65
+
66
+ 预计效果:热态从 7.1–9.8 s 降到 **0.15–0.4 s** 量级(以实机复测为准)。
67
+
68
+ ### 另两件小事
69
+
70
+ - 设置页页头那句「标着「暂未生效」的项尚未接线…」改为**条件渲染**:`INERT_SETTINGS` 现在是空的,
71
+ 无条件渲染会让用户去找一个不存在的标注。判据直接取 hint 里的标记(与后端**同一真源**)。
72
+ - 新增反向参数 **`--one-shot`** / **`--code`**:1.3.4 把设置接成"无 flag 时的默认"之后出现不对称 ——
73
+ 设置成 `persist=true` / `artifacts-only` 后,单条命令**没法反悔**。解析改为**三态**
74
+ (`true` 显式要 / `false` 显式不要 / `null` 未表态听设置),优先级仍是 **flag > 设置 > 常量**。
75
+ (曾加过一个 `--no-persist` 别名,被本仓「实现了就必须有文档」的双向一致性断言挡下 ⇒ 去掉别名,一概念一名。)
76
+
77
+ ### 护栏(进 `test:all`)
78
+
79
+ 新增 `state-perf-guard.test.mjs`:热路径**零次 `readSession`**(spy 计数)、源码里不再有
80
+ `await childSessionTiming(ctx, s.id)`、客户端 single-flight 与退避公式、假 id 不落盘、三态反向参数与优先级。
81
+
82
+ ### 1.3.6 待办(诊断列出、本版**有意不做**,避免扩大范围)
83
+
84
+ 懒加载分节(`?section=people`)、`roleFromChildLog` 流式首帧、`listPersisted` 475 个文件的 header 缓存、
85
+ `maxSubs` / deadline 截断。
6
86
  ## 1.3.4
7
87
 
8
88
  **把 1.3.2 留下的 7 个"有持久化、无消费者"的设置真的接上**(display 4 + roster 3),并清掉当时的假承诺。
package/README.en.md CHANGED
@@ -6,67 +6,140 @@ English | [中文](README.md)
6
6
  [![license](https://img.shields.io/npm/l/@yangdcm/dsh-expert-team)](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](docs/images/hero.svg)
10
+
9
11
  > **One sentence in, a gated team delivery out.** `/team build a payments module with login`
10
12
  > assembles a 12-role expert team and runs
11
13
  > clarify → research → design → spec-review → plan-approval → implement → review → test → deliver,
12
14
  > with implementers editing your workspace directly and every hand-off persisted as a reviewable artifact.
13
15
 
14
- A plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh).
15
- **Zero runtime dependencies, no build step, no install hooks.**
16
+ A plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh):
17
+ **zero runtime dependencies, no build step, no install hooks.**
16
18
 
17
- ![Quality-gate violations surfaced live](docs/images/panel-gate.png)
18
- ![Members, models and task detail](docs/images/panel-live.png)
19
- ![Phase progress and artifact preview](docs/images/panel-flow.png)
19
+ | 12 roles | 9 phases | 80 test files | 0 runtime deps | 0 build steps |
20
+ |---|---|---|---|---|
21
+ | own persona / `toolFilter` / `maxDepth: 1` | 1 hard gate + 1 approval gate | incl. a 136-entry mutation catalog and several ratchets | `dependencies: {}` | no bundler, no `prepare` hook |
22
+
23
+ ![The 9-phase gated pipeline](docs/images/pipeline.svg)
20
24
 
21
- <sub>Real screenshots of the overlay: the gate-violation banner, the roster (who is running, on which model), and task/artifact detail.</sub>
25
+ <sub>Figure 1: the 9-phase gated pipeline. `spec-review` is a **hard gate** if the SPEC.md
26
+ "boundaries and prohibitions" section is empty, the run does not advance (`lib/interception.js`).
27
+ `plan-approval` is an approval gate that is **on by default** (`identity.keepPlanGate`, can be turned off in settings).
28
+ The `implement` phase **fans out** along the dependency DAG: several implementers start at once, each touching only its own files.</sub>
22
29
 
23
30
  ---
24
31
 
25
- ## What it solves
32
+ ## In 30 seconds
33
+
34
+ ```text
35
+ $ /team build a payments module with login
36
+
37
+ ├─ clarify pm pins down boundaries & acceptance → SPEC.md ("boundaries and prohibitions")
38
+ ├─ research researcher evidence and options → RESEARCH.md
39
+ ├─ design architect module split, dependency DAG → PLAN.md
40
+ ├─ spec-review reviewer ▣ HARD GATE: no boundaries, no pass ← cannot advance
41
+ ├─ plan-approval you ▣ approval gate (on by default)
42
+ ├─ implement backend … fan out along the DAG → edits your workspace directly
43
+ ├─ review sec·reviewer independent review (cross-model) → REVIEW.md
44
+ ├─ test qa repro, coverage, regressions → TEST.md
45
+ └─ deliver docs wrap-up and cost → SUMMARY.md · METRICS.md
46
+
47
+ Persisted throughout: TASKS.json · ROSTER.json · STATE.json · AUTHORITY.md · RUN.log.md
48
+ On disk at: <your workspace>/team/<run-id>/
49
+ ```
50
+
51
+ <sub>This is the "phase → who works → which artifact" mapping (phase names come from the single source
52
+ `lib/vocab.js`; artifact names are taken from the shipped templates and code). To watch what it is doing
53
+ *right now*, use the overlay in `dsh web`, or `/team canvas` for the full-screen canvas.</sub>
54
+
55
+ ## Why not "one agent doing it all"
26
56
 
27
57
  A single agent doing large work fails in three predictable ways: **context drift** (long tasks wander),
28
- **self-review** (nobody verifies independently), and **rework that never converges**. The team attacks
29
- all three:
58
+ **self-review** (nobody verifies independently), and **rework that never converges**. Four mechanisms
59
+ answer them:
30
60
 
31
61
  | Mechanism | How |
32
62
  |---|---|
33
- | **Role separation** | 12 roles, each with its own persona, tool boundary (`toolFilter`) and delegation depth (`maxDepth: 1`). PM/architect only read and write planning artifacts, reviewers are read-only, only implementers touch code |
34
- | **Phase gating** | 9 phases; every hand-off travels as *structured return value + artifact file*, not as chat history |
35
- | **Quality gates enforced in code** | State-machine consistency is enforced by the plugin (not requested in a prompt): unfinished tasks cannot be marked completed, quality findings must be adjudicated by qa/reviewer, coverage gaps and rework over budget are flagged. Violations appear **live** in the overlay and in `/team status` |
36
- | **Convergence and accounting** | Per-run token/time/first-runnable/closing-budget metrics; `/team learn` distils cross-run lessons and re-injects them before the next run starts |
63
+ | **Role separation** | 12 roles, each with its own persona, tool boundary (`toolFilter`) and delegation depth (`maxDepth: 1`); product/architecture roles only read and write planning artifacts, review/security are read-only, only implementers touch code |
64
+ | **Phase gates** | 9 phases; every hand-off travels two channels — a structured return value **and** an artifact file. State never rides on chat history |
65
+ | **Quality gates** | State-machine consistency is **enforced by plugin code**, not requested in a prompt: a task cannot be marked completed while unfinished, quality issues must be adjudicated by qa/reviewer, coverage gaps and over-budget rework are caught violations show up **live** in the overlay and in `/team status` |
66
+ | **Convergence & accounting** | Every run records tokens, elapsed time, time-to-first-artifact and a closing budget; `/team learn` distills cross-run experience and feeds it back before the next run starts |
37
67
 
38
- ## Roles and phases
68
+ ## What it looks like in action
39
69
 
40
- **12 roles**: pm · architect · researcher · ui · backend · frontend · dba · sec · reviewer · qa · devops · docs.
41
- The roster is trimmed per task; small jobs start only the roles they need.
70
+ ![Quality-gate violations surfaced live](docs/images/panel-gate.png)
42
71
 
43
- **9 phases**: `clarify research design spec-review plan-approval implement review test → deliver`
44
- (`/team --tier` picks a quick / standard / strict pipeline).
72
+ <sub>Figure 2: **gate violations**. Look at the banner at the top the violation and its refusal reason
73
+ (e.g. "SPEC.md's boundary section has entered `implement` but still has no 'expected rejection' row")
74
+ is decided by `lib/interception.js`, hooked onto the host's `tools/post-execute` waterfall, and surfaced
75
+ immediately. This is code, not a prompt reminder.</sub>
76
+
77
+ ![Members, models and task detail](docs/images/panel-live.png)
78
+
79
+ <sub>Figure 3: **the roster**. Look at the member list — who is running, on which model, and what it is doing;
80
+ expand a member for its tasks and artifacts. Models are configurable per role; heterogeneous models are used for cross-checking.</sub>
81
+
82
+ ![Phase progress and artifact preview](docs/images/panel-flow.png)
83
+
84
+ <sub>Figure 4: **phases and artifacts**. Look at the phase bar and the preview pane — the current phase, the phases
85
+ 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>
86
+
87
+ ![The expert-team section inside the official settings page](docs/images/settings.png)
88
+
89
+ <sub>Figure 5: **settings**. Look at the official `Settings → Expert team` page — 18 settings, Chinese labels,
90
+ **saved on change and applied immediately** (caps, rounds, the tier gate and the oscillation detector are recomputed
91
+ in-process). Values live in the host namespace `expert-team`, so they travel with the plugin market's backup/restore.</sub>
92
+
93
+ ## Why it is dependable
94
+
95
+ - **The state machine is enforced by plugin code, not requested by prompt.** `lib/interception.js` moves the
96
+ "task-ledger contract" and the "spec boundary" rules onto the host's `tools/post-execute` waterfall:
97
+ duplicate ids / cycles / self-dependencies are rejected on the spot (`HARD_GRAPH_CODES`), and an empty SPEC
98
+ boundary section cannot enter `implement` (`SPEC_COMPLETE_PHASES`). **Silence in the spec means permission —
99
+ that is the number-one source of rework.**
100
+ - **Zero runtime dependencies, zero devDependencies, no build step, no `prepare`/`postinstall` hooks.**
101
+ What you install is exactly what runs; there is no "unknown script at install time" layer.
102
+ - **80 test files plus a 136-entry mutation catalog.** `npm run test:all` needs no `install` (it is what CI runs);
103
+ the mutation catalog requires every mutant to be killed by at least one test — the suite is not "green",
104
+ it is *able to catch errors*.
105
+ - **Several ratchet tests** pin down rules that were already thought through, so they cannot quietly regress:
106
+ `vocab-consistency` (one source for vocabulary and role labels), `scan-single-source` (no fact with two homes),
107
+ `write-bypass-ratchet` (no write path may bypass interception), `settings-consumers`
108
+ (**every setting must have a consumer**; the allow-list is compared as a set, so it can only shrink),
109
+ `state-perf-guard` (sub-session timing must perform **zero** log reads — a performance regression fails the suite).
110
+ - **Two kinds of zero, kept apart.** "I don't know what exists" must not look like "there is nothing":
111
+ a failed tool-face narrowing distinguishes `no-known-names` from `nothing-to-deny`; when `/state` cannot obtain
112
+ a timestamp it returns `hasTimestamp: false` instead of passing `0` off as a measurement.
113
+ - **Failures must be loud.** Out-of-bounds writes, artifact divergence and over-budget rework always raise an
114
+ explicit error; nothing is truncated silently — silent failure is the most expensive bug class in this repo.
115
+ - **Performance is measured and guarded.** `/state` used to read every sub-session log in full: measured at
116
+ 7.1–9.8 s warm and 283.6 s cold (75 sub-sessions), and it blocked the whole `dsh web` event loop. 1.3.5
117
+ replaced that with a table lookup (measured at 0.0026 ms per call, zero `readSession` calls);
118
+ **the end-to-end post-fix number is still pending a re-measurement on a real host**.
119
+ `state-perf-guard.test.mjs` keeps it from coming back.
45
120
 
46
121
  ## Install
47
122
 
48
123
  **Requirements**
49
124
 
50
- - `dsh web` (developed and verified against **0.1.5-rc.1**; earlier versions are untested)
125
+ - `dsh web` (developed and verified on **0.1.5-rc.1**; earlier versions are untested)
51
126
  - Node.js ≥ 20
52
- - The 12 role tools (`subagent_pm`, `subagent_architect`, …) require a session running the
53
- **「专家团模式」 ("Expert Team mode")** preset. Without it the team falls back to the generic
54
- `subagent` tool with personas written into the prompt — nothing breaks, you just lose the
55
- config-level boundary guarantees.
127
+ - The 12 role tools (`subagent_pm` / `subagent_architect` / …) are available when the session uses the
128
+ **"Expert team mode"** preset; otherwise the plugin falls back to the generic `subagent`
129
+ (role personas go into the prompt) — nothing is lost except the configuration-level boundary guarantees
56
130
 
57
- **Option 1 CLI (recommended)**
131
+ **Option 1: command line (recommended)**
58
132
 
59
133
  ```sh
60
134
  dsh plugin --profile web add @yangdcm/dsh-expert-team
61
135
  # then restart dsh web so the new bundle joins the composition
62
136
  ```
63
137
 
64
- **Option 2 plugin market** (listing not submitted yet, so it may not be searchable)
138
+ **Option 2: plugin market** (listing not submitted yet it may not be searchable today)
65
139
 
66
- If it has been listed: `dsh web` → **Settings → Plugin market** → search for "专家团" / "expert team" → install → refresh.
67
- ```
140
+ Once listed: `dsh web` → **Settings → Plugin market** → search "expert team" → install → refresh the page.
68
141
 
69
- **Option 3 from source (development)**
142
+ **Option 3: from source (development / unpublished)**
70
143
 
71
144
  ```sh
72
145
  cd ~/.dsh/profiles/web
@@ -75,141 +148,157 @@ cd ~/.dsh/profiles/web
75
148
  pnpm install && dsh web
76
149
  ```
77
150
 
78
- > **The skill is never copied to disk**: when the plugin loads it registers the `expert-team` skill as a
79
- > **runtime entry** in the host skill registry (relative resources resolve through `resourceBase` back into
80
- > the package), so nothing appears under `$DSH_HOME/skills/` — uninstalling stays clean. Only when the host
81
- > has no skill registry does it fall back to copying.
82
- >
83
- > **The 「专家团模式」 preset is still copied** into `$DSH_HOME/.agent-presets/` (the host offers no
84
- > runtime API to add a preset scan root), but it carries a version stamp and is re-materialised in full on
85
- > upgrade instead of silently going stale. `/team uninstall` reclaims the copies this plugin laid down —
86
- > it only removes directories carrying our stamp, and never touches content you authored yourself.
151
+ > **The skill is not copied to disk**: on load the plugin registers the `expert-team` skill as a **runtime entry**
152
+ > in the host's skill registry (relative resources point back into the package via `resourceBase`), so nothing
153
+ > appears under `$DSH_HOME/skills/` — uninstalling leaves no residue. It only falls back to copying into
154
+ > `$DSH_HOME/skills/` when the host has no skill registry.
87
155
  >
88
- > **The preset is laid down when the plugin loads**: after `/team uninstall`, a restart of `dsh web`
89
- > re-materialises it automatically (no manual rescue needed).
156
+ > **The "Expert team mode" preset is copied** into `$DSH_HOME/.agent-presets/` (the host exposes no runtime API
157
+ > to add a scan root), but it carries a version stamp: on upgrade the whole directory is re-laid, so it never
158
+ > silently stays behind. **The plugin lays it down at load time** — after `/team uninstall`, a restart of
159
+ > `dsh web` re-lays it automatically; no manual rescue needed.
90
160
  >
91
- > **Where settings live**: **Settings →「专家团」** — a full page inside the host's official settings menu
92
- > (the `settings.section` slot, `id: expert-team`, `order: 50`), sharing the entry point and panel chrome with
93
- > every other plugin's settings. The data layer is the host namespace `expert-team` (the host owns it, it travels
94
- > with the plugin market's **backup and restore**, and a changed value recomputes limits, round caps and the tier
95
- > gate in-process — no restart). The overlay's old "settings" tab has been removed: one form, one place.
96
- > **Honest boundary**: the default roster (an array of role ids) is deliberately *not* in the host schema (its value
97
- > type cannot be expressed reliably); it stays with the corresponding control on that page and
98
- > `$DSH_HOME/expert-team/settings.json`. On a host with no settings service every setting falls back to that file.
161
+ > **Where settings live**: **Settings Expert team** — a full page inside the official settings menu
162
+ > (the `settings.section` slot, `id: expert-team`, `order: 50`), sharing the same entry point and panel chrome
163
+ > as other plugins' settings. The data layer is the host namespace `expert-team` (owned by the host, travels with
164
+ > the plugin market's **backup and restore**; after a change, caps/rounds/the tier gate are recomputed
165
+ > **in-process** — no restart). The overlay's old "settings" tab was removed so the same form renders in one place.
166
+ > **Honest boundary**: `default roster` (an array of role ids) is deliberately not part of the host schema
167
+ > (unreliable to express there); the control on that page and `$DSH_HOME/expert-team/settings.json` cover it.
168
+ > When the host has no settings service, every setting falls back to that file.
99
169
  >
100
- > **The A-line switch**: the "narrow the lead's tool face" gate (`gates.leadToolFace`, default `on`)
101
- > decides whether execution tools (`bash/write/edit/grep/glob`) are taken away from the lead and given
102
- > to the role subagents. Turn it off with `config.leadToolFace` or `DSH_EXPERT_TEAM_LEAD_TOOLFACE=off`.
170
+ > **The A-line switch**: "Gates → Narrow the lead's tool face" (`gates.leadToolFace`, default `on`) decides whether
171
+ > execution tools (`bash/write/edit/grep/glob`) are taken away from the lead and given to the role subagents.
172
+ > Turn it off with `config.leadToolFace` or `DSH_EXPERT_TEAM_LEAD_TOOLFACE=off`.
103
173
 
104
174
  ### After installing
105
175
 
106
- 1. **Restart `dsh web` once**: on load the plugin lays the 「专家团模式」 preset down into
176
+ 1. **Restart `dsh web` once**: on load the plugin lays the "Expert team mode" preset into
107
177
  `$DSH_HOME/.agent-presets/expert-team` (version-stamped; upgrades re-lay the whole directory) —
108
- **no manual preset creation needed**. After that restart the preset picker lists it, and all 12 role
109
- subagent tools are in place (visible under `Settings → Plugins → Plugin list → Session plugins`).
110
- 2. Switch the session to that preset, then run `/team <one-line goal>`.
111
- 3. Change settings under **Settings →「专家团」** (values live in the host namespace `expert-team`, so they
178
+ **you never create a preset by hand**. After the restart the preset appears in the picker and all 12 role
179
+ subagent tools are in place (see them under `Settings → Plugins → Plugin list → Session plugins`).
180
+ 2. Switch the session to "Expert team mode", then run `/team <one-sentence goal>`.
181
+ 3. Change settings under **Settings Expert team** (values live in the host namespace `expert-team`, so they
112
182
  travel with the plugin market's backup/restore).
113
183
 
114
- **Troubleshooting**: a missing preset, or a same-id preset squatting on ours, is healed by **one `dsh web`
115
- restart** (the plugin re-lays on load); running `/team <task>` once does the same. See the
116
- [Troubleshooting](#troubleshooting) section below for the details — including the easiest trap to fall into:
117
- **never** create a preset with the id `expert-team`.
118
-
119
- ### Custom presets (when you want to change expert-team's defaults)
120
-
121
- - **Create**: `Settings → Agent presets → create a custom preset with "creation mode"` (its mechanism is
122
- "copy an existing preset"; the result lands in `$DSH_HOME/.agent-presets/<id>/`).
123
- - **To customise expert-team, copy 「专家团模式」 as the source under an id of your own** (e.g. `my-team`): the
124
- copy already carries the 12 role tools and their skill directory, and your edits stay inside `my-team/`.
125
- **Never edit files under `expert-team/`** — that copy belongs to the plugin and is re-laid wholesale on
126
- load/upgrade.
127
- - A newly created preset **may only appear in the picker after a `dsh web` restart** (the host reads its roster
128
- at startup).
184
+ **Troubleshooting**: if the preset is gone or occupied by an unrelated preset of the same id, **one restart of
185
+ `dsh web` heals it** (the plugin re-lays it at load); alternatively run `/team <task>` once. See the
186
+ [Troubleshooting](#troubleshooting) section for the easiest trap to fall into: **do not** use the id
187
+ `expert-team` when creating a preset.
129
188
 
130
189
  ## Quick start
131
190
 
132
191
  ```
133
- /team build a payments module with login # one-shot: assemble, deliver, report
134
- /team --persist refactor the orders module # persistent live team; members stay commandable, resumable across sessions
135
- /team --no-code review the existing API # artifacts only (plan/review/test), no code changes
136
- /team --confirm <task> # scaffold the run but do not dispatch until you click "execute"
137
- /team status # phases, roster, model plan and live gate violations
138
- /team resume <run-id> # resume a run in a later session
192
+ /team build a payments module with login # one sentence in, a one-shot team delivery out
193
+ /team --persist refactor the orders module # persistent live team: members can be re-tasked, survives sessions
194
+ /team --one-shot run a small chore # inverse override: run once even if persistence is the default
195
+ /team --no-code review the existing API # produce planning/review/test artifacts only, change no code
196
+ /team --code implement it # inverse override: touch code even if "artifacts only" is the default
197
+ /team --confirm a big redesign # create the run but do not dispatch; click "run" in the overlay
198
+ /team uninstall # reclaim what this plugin laid down under $DSH_HOME
199
+ /team status # phase, members, model plan and live violations for every run
200
+ /team resume <run-id> # resume across sessions
139
201
  ```
140
202
 
141
- `/team help` lists the full surface (`/team canvas` visual canvas, `/team codeindex` code index,
142
- `/team learn` self-learning, `/team limit` quota, `/team settle` cold-start settlement, …).
203
+ Full command list (`/team canvas` visual canvas, `/team codeindex` code index, `/team learn` self-learning,
204
+ `/team limit` quotas, `/team settle` cold-start settlement, …) — see `/team help`.
143
205
 
144
- **Where output lands**
206
+ **Where artifacts land**
145
207
 
146
- - `<your workspace>/team/<run-id>/` — artifacts: `SPEC / PLAN / TASKS / ROSTER / STATE / REVIEW / TEST / SUMMARY / RUN.log.md`
147
- - `$DSH_HOME/expert-team/` — machine-local preferences and cross-project lessons: `settings.json`, `session-runs.json`, `LEARNINGS.md`
208
+ - `<your workspace>/team/<run-id>/` — `SPEC / PLAN / TASKS / ROSTER / STATE / REVIEW / TEST / SUMMARY / RUN.log.md` etc.
209
+ - `$DSH_HOME/expert-team/` — machine-local preferences and cross-project experience: `settings.json`,
210
+ `session-runs.json`, `LEARNINGS.md`
148
211
 
149
212
  ## Layout
150
213
 
151
214
  ```
152
- cordis.patch.yml the only composition contribution: one host-plane /team command row
153
- lib/command.js /team command: parse + scaffold workspace + install skill + launch + 11 overlay routes
154
- lib/validate.js pure validators for state machine, quality gates and capacity limits
155
- lib/tier.js single source of truth for pipeline tiers
156
- lib/metrics/ token accounting, first-runnable timing, closing budget, METRICS rendering
157
- lib/routes/ shared route plumbing (uniform 405/500/JSON handling)
158
- client.js the overlay panel (module-loader bundle; requires only 'react')
159
- skills/expert-team/ the orchestration "brain": SKILL.md + references/ + artifact templates
160
- presets/expert-team/ the 「专家团模式」 preset: 12 role subagent tool instances
215
+ cordis.patch.yml the only composition contribution: a host-plane /team command line
216
+ lib/command.js /team command: parse + create workspace + install skill + launch team + 11 overlay routes
217
+ lib/validate.js pure-function validators for the state machine / quality gates / capacity caps
218
+ lib/interception.js moves the ledger contract and spec boundary onto tools/post-execute (gates in code)
219
+ lib/tier.js single source for the process-tier vocabulary
220
+ lib/vocab.js single source for phases/roles/tiers (both host and client derive from it)
221
+ lib/metrics/ token accounting, time-to-first-artifact, closing budget, METRICS rendering
222
+ lib/routes/ shared route helpers (uniform 405/500/JSON handling, local-origin guard)
223
+ client.js the client overlay (module-loader bundle, only require('react'))
224
+ skills/expert-team/ the orchestration "brain": SKILL.md + references/ + assets/templates/
225
+ presets/expert-team/ the "Expert team mode" preset: 12 role subagent tool instances
161
226
  ```
162
227
 
163
- The orchestration protocol lives in the skill rather than in code, so the team protocol can evolve
164
- without a package release.
228
+ Almost all of the orchestration protocol lives in the skill rather than in code so the team protocol can
229
+ evolve with the skill, without shipping a new package.
165
230
 
166
231
  ## Development
167
232
 
168
233
  ```sh
169
- npm run test:all # 75 test files, zero dependencies, no install needed (this is what CI runs)
170
- npm run rename <name> # re-brand a fork: syncs 4 spellings of the package name across 13 files
171
- npm run check:name # verify no placeholder package name is left behind
234
+ npm run test:all # 80 test files, zero dependencies, no install needed (this is what CI runs)
235
+ npm run rename <name> # after forking: syncs 4 package-name spellings across 13 files
236
+ npm run check:name # check for leftover placeholder package names
172
237
  ```
173
238
 
174
- `npm run gate` (`gate:preset` / `gate:sync` / `gate:evidence` / `gate:bypass` / `gate:mutation`) is a
175
- **developer-machine-only** set: `gate:sync` diffs the self-installed copies under your local
176
- `$DSH_HOME`, and `gate:preset` borrows the Config schemas shipped inside your local dsh install
177
- (the dsh path is auto-detected; override with `DSH_INSTALL`). Neither runs in CI.
239
+ `npm run gate` (`gate:preset` / `gate:sync` / `gate:evidence` / `gate:bypass` / `gate:mutation`) are
240
+ **development-machine-only** gates: `gate:sync` compares against the bootstrap copy under your local
241
+ `$DSH_HOME`, and `gate:preset` borrows the Config schema shipped with the dsh installation
242
+ (auto-detected; override with `DSH_INSTALL`). They are therefore **not run in CI**.
178
243
 
179
244
  ## Troubleshooting
180
245
 
181
- **No「专家团」page in the settings menu?** Make sure you are on 1.3.0 or newer
182
- (`dsh plugin --profile web add @yangdcm/dsh-expert-team` to upgrade) and then **restart `dsh web`** — the page
183
- did not exist before 1.3.0.
184
-
185
- **The「专家团模式」preset shows as the bare id (`expert-team`) and sessions have none of the 12 role tools?**
186
- That means `$DSH_HOME/.agent-presets/expert-team/` does not hold our preset (it was deleted, or a same-named
187
- preset took the id). Fix it in this order:
188
-
189
- 1. Run `/team <any small task>` once — the plugin re-lays a version-stamped copy from the package (simplest; from
190
- 1.3.0 it also does this automatically on load);
191
- 2. Or copy over it: `cp -f <package>/presets/expert-team/{agent.cordis.yml,preset.yml} ~/.dsh/.agent-presets/expert-team/`;
192
- 3. Do **not** "create preset" with the same id in Settings → Agent presets — that only gives you a composition of
193
- the *source* you picked (e.g. standard mode), not ours.
194
-
195
- Afterwards **restart `dsh web`** (the preset roster is fixed at startup; a page refresh is not enough) and open a
196
- **new session** (a session's preset is fixed when it is created).
197
-
198
- ## Known limitations
199
-
200
- - **The local routes are guarded, but that is not authentication**: all 11 overlay routes now check
201
- `Host` (blocks DNS rebinding), the `Origin` of write requests (blocks cross-site writes), and the
202
- client address (blocks non-loopback clients); write requests must also send `application/json`
203
- (blocks form / text-plain "simple requests" that never trigger a preflight). `/file` now goes through
204
- the host `ctx.fs` policy and reports 403 instead of falling back to a raw read when the policy denies it.
205
- **Residual risk, stated plainly**: a local non-browser process can forge any header, and dsh plugins
206
- have no authentication model so do not expose `dsh web` to an untrusted network (with
207
- `host: 0.0.0.0` the guard only stops clients without a loopback address).
208
- - **Web profile only**: the overlay and routes need `webServer`. The command and artifacts still work without it.
209
- - **Preset drift**: the bundled 「专家团模式」 is a copy of the official `standard` preset plus the role
210
- tools; upstream preset restructuring needs a matching update here. Upgrades re-materialise it by version
211
- stamp, and `/team uninstall` reclaims it (a same-named preset you authored yourself is left alone).
212
- - Calls `git status --porcelain` (read-only) to judge artifact freshness.
246
+ **The "Expert team" page is missing from the settings menu?** Check the version (≥ 1.3.0):
247
+ `dsh plugin --profile web add @yangdcm/dsh-expert-team`, or `npm view @yangdcm/dsh-expert-team version`,
248
+ then **restart `dsh web`** — the page did not exist before 1.3.0.
249
+
250
+ **The preset shows as a bare id (`expert-team`) and the session has no role tools?** Then
251
+ `$DSH_HOME/.agent-presets/expert-team/` does not hold our copy (it was deleted, or an unrelated preset
252
+ took the id). Fix in this order:
253
+
254
+ 1. run `/team <any small task>` — the plugin re-lays a version-stamped copy from the package (easiest; since 1.3.0
255
+ it is also laid down at plugin load);
256
+ 2. or copy from the package: `cp -f <package>/presets/expert-team/{agent.cordis.yml,preset.yml} ~/.dsh/.agent-presets/expert-team/`;
257
+ 3. **do not** create a preset with the same id in `Settings → Agent presets`you would only get a copy of the
258
+ **source** you picked (e.g. standard mode), not our preset.
259
+
260
+ Then **restart `dsh web`** (the preset roster is read at startup; refreshing the page is not enough) and open a
261
+ **new session** (the preset is fixed when a session is created).
262
+
263
+ **The overlay opens slowly / `dsh web` feels sluggish?** Before 1.3.5, `/state` read every sub-session log in full
264
+ (measured 7–10 s warm, 283 s cold) and blocked the event loop. Upgrade to ≥ 1.3.5 and restart `dsh web`.
265
+
266
+ ## Custom presets (when you want to change expert-team's defaults)
267
+
268
+ - **Create**: `Settings Agent presets create a custom preset with "Creator mode"` (the mechanism is
269
+ "copy an existing preset"; output lands in `$DSH_HOME/.agent-presets/<id>/`).
270
+ - **To customize the expert team, copy from "Expert team mode" and pick your own id** (e.g. `my-team`):
271
+ the copy comes with all 12 role tools and the skill directory, and your edits stay in `my-team/`.
272
+ **Never edit files in `expert-team/`** that copy belongs to the plugin and is re-laid on load/upgrade.
273
+ - A newly created preset may only appear in the picker **after a restart of `dsh web`** (the host reads its
274
+ roster at startup).
275
+
276
+ ## Honest boundaries
277
+
278
+ - **The local-origin guard is in place, but it is not authentication.** All 11 overlay routes validate the Host
279
+ header (against DNS rebinding), the Origin of mutating methods (against cross-site writes) and the client
280
+ address (against the LAN); mutating methods additionally require `application/json` (blocking form/text-plain
281
+ "simple requests" that skip preflight). `/file` goes through the host `ctx.fs` policy and reports 403 honestly
282
+ when a read is denied rather than **falling back to a raw read**.
283
+ **Residual risk, stated plainly**: any local non-browser process can forge arbitrary request headers, and dsh
284
+ plugins have no authentication model — so do not expose `dsh web` to an untrusted network (with the host
285
+ configured as `host: 0.0.0.0`, the guard only blocks clients that are not on loopback).
286
+ - **Web profile only**: the overlay and routes depend on `webServer`; without an overlay the command and the
287
+ artifacts still work.
288
+ - **Preset drift**: the shipped "Expert team mode" is a copy of the official `standard` preset plus the role tools;
289
+ if the host changes its built-in preset structure, this needs a sync. Upgrades re-lay the whole directory by
290
+ version stamp; `/team uninstall` reclaims it (a user-authored preset with the same name is never touched).
291
+ - **It intentionally runs `git status --porcelain` (read-only)** to judge artifact freshness.
292
+ - **Unwired settings are never dressed up as working.** Every setting either really takes effect or is marked
293
+ "not yet in effect" (driven by the single source `INERT_SETTINGS`) and watched by
294
+ `settings-consumers.test.mjs` — that table is currently empty.
295
+
296
+ ## Compatibility
297
+
298
+ - `engines.dsh: >=0.1.5-rc.1`, declared in `package.json` under `dsh.compatibility` (`dshReleases` marks it
299
+ release by release); the plugin market uses it to decide whether this plugin fits your host.
300
+ - Node.js ≥ 20.
301
+ - Profile: `web` (see "Honest boundaries" above).
213
302
 
214
303
  ## License
215
304
 
package/README.md CHANGED
@@ -6,41 +6,94 @@
6
6
  [![license](https://img.shields.io/npm/l/@yangdcm/dsh-expert-team)](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
+ ![专家团:一句话组队交付](docs/images/hero.svg)
10
+
9
11
  > **一句话组队交付**:`/team 做一个带登录的支付模块` —— 自动组建 12 角色专家团,走
10
12
  > 澄清 → 调研 → 设计 → 规格评审 → 方案确认 → 实现 → 审查 → 测试 → 交付 的门控流水线,
11
13
  > 实现者直接改你工作区的代码,全程留痕成可复核的工件。
12
14
 
13
- 装在 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 上的 dsh 插件,
14
- **零运行时依赖、无构建步骤、无安装钩子**。
15
+ 装在 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 上的 dsh 插件:**零运行时依赖、无构建步骤、无安装钩子**。
15
16
 
16
- ![质量门禁违规实时可见](docs/images/panel-gate.png)
17
- ![成员模型与任务详情](docs/images/panel-live.png)
18
- ![阶段推进与工件预览](docs/images/panel-flow.png)
17
+ | 12 角色 | 9 阶段 | 80 个测试文件 | 0 运行时依赖 | 0 构建步骤 |
18
+ |---|---|---|---|---|
19
+ | 各带人设 / `toolFilter` / `maxDepth: 1` | 含 1 道硬门 + 1 道确认门 | 含 136 条变异目录与多组棘轮 | `dependencies: {}` | 无 bundler、无 `prepare` 钩子 |
20
+
21
+ ![9 阶段门控流水线](docs/images/pipeline.svg)
19
22
 
20
- <sub>截图即真实浮层:门禁违规横幅、角色编制(谁在跑、用哪个模型)、任务详情与工件预览。</sub>
23
+ <sub>图 1:9 阶段门控流水线。「规格评审」是**硬门** —— SPEC.md 的「边界与禁止项」没填就不放行(`lib/interception.js`);「方案确认」是默认开启的**确认门**(`identity.keepPlanGate`,可在设置里关掉);「实现」阶段按依赖 DAG **并行扇出**,多个实现者同时开工、各自只改自己那份文件。</sub>
21
24
 
22
25
  ---
23
26
 
24
- ## 它解决什么
27
+ ## 30 秒看懂
28
+
29
+ ```text
30
+ $ /team 做一个带登录的支付模块
31
+
32
+ ├─ 澄清 pm 问清边界与验收口径 → SPEC.md(含「边界与禁止项」)
33
+ ├─ 调研 researcher 证据与选型 → RESEARCH.md
34
+ ├─ 设计 architect 模块拆分与依赖 DAG → PLAN.md
35
+ ├─ 规格评审 reviewer ▣ 硬门:边界没填不放行 ← 不过不进下一步
36
+ ├─ 方案确认 你 ▣ 确认门(默认开,可关)
37
+ ├─ 实现 backend … 按依赖 DAG 并行扇出 → 直接改你工作区的代码
38
+ ├─ 审查 sec·reviewer 独立评审(换模型交叉验证) → REVIEW.md
39
+ ├─ 测试 qa 复现、覆盖、回归 → TEST.md
40
+ └─ 交付 docs 收尾结论与成本 → SUMMARY.md · METRICS.md
41
+
42
+ 全程留痕:TASKS.json · ROSTER.json · STATE.json · AUTHORITY.md · RUN.log.md
43
+ 落盘位置:<你的工作区>/team/<run-id>/
44
+ ```
45
+
46
+ <sub>上表是「阶段 → 谁在做 → 落哪个工件」的对应关系(阶段名出自 `lib/vocab.js` 的唯一真源,工件名取自包内模板与代码)。想连它**正在做什么**一起看,用 `dsh web` 里的浮层,或 `/team canvas` 打开全屏画布。</sub>
25
47
 
26
- 单个 agent 干大活有三个固定失败模式:**上下文漂移**(长任务越做越偏)、**自己批自己**
27
- (没人独立验证)、**返工不收敛**(同一个问题来回改)。专家团用四件事对付它们:
48
+ ## 为什么不是「一个 agent 硬做」
49
+
50
+ 单个 agent 干大活有三个固定失败模式:**上下文漂移**(长任务越做越偏)、**自己批自己**(没人独立验证)、
51
+ **返工不收敛**(同一个问题来回改)。专家团用四件事对付它们:
28
52
 
29
53
  | 机制 | 做法 |
30
54
  |---|---|
31
55
  | **角色分工** | 12 个角色各带独立人设、工具边界(`toolFilter`)、委派深度(`maxDepth: 1`);产品/架构只读写计划工件,审查/安全只读,实现者才动代码 |
32
- | **阶段门控** | 9 个阶段,每次交接走「结构化返回值 + 工件文件」双通道,不靠聊天记录传状态 |
33
- | **质量门禁** | 状态机一致性由**插件代码强制**(不是提示词请求):任务未完成不能标 completed、质量问题必须由 qa/reviewer 裁决、覆盖率缺口、超轮次返工——违规**实时**显示在浮层并计入 `/team status` |
34
- | **收敛与记账** | 每 run 记 token/耗时/首产物时间/收尾预算;`/team learn` 跨 run 蒸馏经验并在下次开工前回注 |
56
+ | **阶段门控** | 9 个阶段,每次交接走「结构化返回值 + 工件文件」双通道 —— 状态不靠聊天记录传递 |
57
+ | **质量门禁** | 状态机一致性由**插件代码强制**(不是提示词请求):任务未完成不能标 completed、质量问题必须由 qa/reviewer 裁决、覆盖率缺口、超轮次返工 —— 违规**实时**显示在浮层并计入 `/team status` |
58
+ | **收敛与记账** | 每 run 记 token/耗时/首产物时间/收尾预算;`/team learn` 跨 run 蒸馏经验,并在下次开工前回注 |
59
+
60
+ ## 看一眼它在干什么
61
+
62
+ ![质量门禁违规实时可见](docs/images/panel-gate.png)
63
+
64
+ <sub>图 2:**门禁违规**。看顶部那条横幅 —— 违规项与拒绝理由(例如"SPEC.md 的边界章节已进入 `implement` 但仍无任何一行填写")由 `lib/interception.js` 挂在宿主 `tools/post-execute` 上当场判出后推出,不是提示词提醒。</sub>
65
+
66
+ ![成员模型与任务详情](docs/images/panel-live.png)
67
+
68
+ <sub>图 3:**角色编制**。看成员列表 —— 谁在跑、用哪个模型、当前在做什么;展开任一成员可看它的任务与产物。模型可按角色分别配置,异构模型用于交叉验证。</sub>
69
+
70
+ ![阶段推进与工件预览](docs/images/panel-flow.png)
71
+
72
+ <sub>图 4:**阶段与工件**。看阶段条与预览区 —— 当前阶段、已过阶段、以及该阶段真正写下的工件正文(工件是唯一真源,浮层只是它的视图)。</sub>
35
73
 
36
- ## 角色与阶段
74
+ ![官方设置页里的专家团分节](docs/images/settings.png)
37
75
 
38
- **12 角色**:产品(pm) · 架构(architect) · 调研(researcher) · 界面设计(ui) · 后端(backend) ·
39
- 前端(frontend) · 数据(dba) · 安全审计(sec) · 评审(reviewer) · 测试(qa) · 运维(devops) · 文档(docs)。
40
- 按任务复杂度裁剪:小活只起需要的角色。
76
+ <sub>图 5:**设置**。看官方 `设置 →「专家团」` 这一页 —— 18 个设置项、中文标签、**改动即保存并即时生效**(上限/轮次/档位门/振荡检测在进程内重算);值存在宿主命名空间 `expert-team`,随插件市场的备份/恢复一起走。</sub>
41
77
 
42
- **9 阶段**:`澄清 → 调研 → 设计 → 规格评审 → 方案确认 → 实现 → 审查 → 测试 → 交付`
43
- (`/team --tier 快速档|标准档|严格档` 控制流程档位)。
78
+ ## 它为什么可靠
79
+
80
+ - **状态机由插件代码强制,不由提示词请求。** `lib/interception.js` 把「台账契约」与「规格边界」两条规则搬到宿主的
81
+ `tools/post-execute` 瀑布上:重复 id / 环 / 自依赖当场顶回(`HARD_GRAPH_CODES`),SPEC 边界未填不许进实现
82
+ (`SPEC_COMPLETE_PHASES`)。**规格沉默等于允许,那正是头号返工源。**
83
+ - **零运行时依赖、零 devDependencies、无构建步骤、无 `prepare`/`postinstall` 钩子。** 装完就是能跑的那份代码,
84
+ 没有"安装时执行未知脚本"这一层。
85
+ - **80 个测试文件 + 136 条变异目录。** `npm run test:all` 无需 `install` 即可跑(CI 跑的就是它);
86
+ `mutation-catalog` 要求每个变异体都至少被一个测试杀掉 —— 测试不是"跑绿了",而是"能抓到错"。
87
+ - **多组棘轮(ratchet)测试**,把"已经想清楚的规矩"钉住,防止悄悄退化:
88
+ `vocab-consistency`(术语与角色标签单一真源)、`scan-single-source`(同一事实不许有两个家)、
89
+ `write-bypass-ratchet`(写侧不许绕过拦截)、`settings-consumers`(**每个设置项都必须有消费者**,白名单集合相等 ⇒ 只减不增)、
90
+ `state-perf-guard`(子会话计时**零次**读日志,性能回归即红)。
91
+ - **两种零要分得清。** "我不知道有什么"与"确实没有"不长成同一个样子:工具面收窄失败时区分
92
+ `no-known-names` / `nothing-to-deny`;`/state` 取不到时间戳时给 `hasTimestamp: false`,而不是用 `0` 冒充。
93
+ - **失败必须出声。** 写侧越界、工件分叉、超轮次返工一律**显式报错**,不做静默截断 —— 静默失败是本仓最贵的 bug 类型。
94
+ - **性能有实测、也有护栏。** 曾经 `/state` 会逐条全量读子会话日志:实测热态 7.1–9.8 s、冷态 283.6 s(75 个子会话),
95
+ 还会堵住整个 `dsh web` 的事件循环。1.3.5 改成只查表(实测 0.0026 ms/次、零次 `readSession`),
96
+ **修复后的端到端数字待实机复测**;`state-perf-guard.test.mjs` 守着它不许回退。
44
97
 
45
98
  ## 安装
46
99
 
@@ -61,7 +114,6 @@ dsh plugin --profile web add @yangdcm/dsh-expert-team
61
114
  **方式二:插件市场**(收录尚未提交 ⇒ 目前可能搜不到)
62
115
 
63
116
  若已被收录:`dsh web` → **设置 → 插件市场** → 搜索「专家团」→ 一键安装 → 刷新页面。
64
- ```
65
117
 
66
118
  **方式三:从源码(开发/未发布时)**
67
119
 
@@ -76,11 +128,9 @@ pnpm install && dsh web
76
128
  > (相对资源用 `resourceBase` 指回包内目录),所以 `$DSH_HOME/skills/` 下不会出现副本 ——
77
129
  > 卸载即干净。宿主没有 skill 注册表时才会回退为复制到 `$DSH_HOME/skills/`。
78
130
  >
79
- > **「专家团模式」preset 仍会复制**到 `$DSH_HOME/.agent-presets/`(宿主没有"运行时加扫描根"的
80
- > API),但带版本戳:升级后整目录重铺,不会静默停在旧版本。用 `/team uninstall` 可回收本插件
81
- > 铺下的副本(只删带我们版本戳的目录,用户自己写的同名内容一律保留)。
82
- >
83
- > **插件加载时就把 preset 铺到位**:`/team uninstall` 回收之后,重启 `dsh web` 即会**自愈重铺**(不需要手工救)。
131
+ > **「专家团模式」preset 会复制**到 `$DSH_HOME/.agent-presets/`(宿主没有"运行时加扫描根"的 API),
132
+ > 但带版本戳:升级后整目录重铺,不会静默停在旧版本。**插件加载时就会把它铺到位** ——
133
+ > 所以 `/team uninstall` 回收之后,重启 `dsh web` 即会自愈重铺,不需要手工救。
84
134
  >
85
135
  > **设置在哪改**:**设置 →「专家团」** —— 官方设置菜单里的一整页(`settings.section` 槽,
86
136
  > `id: expert-team`、`order: 50`),与其它插件的设置同一入口、同一套面板 chrome。数据层是宿主命名空间
@@ -104,19 +154,14 @@ pnpm install && dsh web
104
154
  **排障**:预设丢了、或被同名预设占住 —— **重启一次 `dsh web` 即自愈**(插件加载会重铺),也可跑一次 `/team <任务>`。
105
155
  细节见下面「排障」一节,其中包含那条最容易踩的坑:**不要**用 `expert-team` 这个 id 去「创建 preset」。
106
156
 
107
- ### 自定义预设(想改专家团默认行为时)
108
-
109
- - **创建**:`设置 → Agent 预设 → 用「创造模式」创作自定义预设`(其机制是"复制一份既有预设",产出落在 `$DSH_HOME/.agent-presets/<id>/`)。
110
- - **要定制专家团,请以「专家团模式」为源、换一个你自己的 id**(例如 `my-team`):复制出来的目录天然带上 12 个角色工具与它的 skill 目录,
111
- 你的改动只写在 `my-team/` 里。**永远不要改 `expert-team/` 里的文件** —— 那份是插件所有的,加载/升级会整目录重铺覆盖。
112
- - 新建的预设**可能要重启 `dsh web` 后**才出现在选择器里(宿主名册在启动时读取)。
113
-
114
157
  ## 快速上手
115
158
 
116
159
  ```
117
160
  /team 做一个带登录的支付模块 # 一句话组队(一次性,自动组队并交付)
118
161
  /team --persist 重构订单模块 # 持久化活团队:成员可反复指挥、跨会话恢复
162
+ /team --one-shot 跑一个小活 # 反向覆盖:即使默认设了持久化,这次也只跑一次
119
163
  /team --no-code 评审现有 API 设计 # 只产出计划/评审/测试工件,不改代码
164
+ /team --code 直接改 # 反向覆盖:即使默认设了"只出工件",这次也动代码
120
165
  /team --confirm 大改版需求 # 先建 run、不自动派工,浮层点「执行」才开工
121
166
  /team uninstall # 回收本插件铺到 $DSH_HOME 的副本(skill 默认走运行时注册,本就不落地)
122
167
  /team status # 所有 run 的阶段、成员、模型计划、实时违规
@@ -137,11 +182,13 @@ pnpm install && dsh web
137
182
  cordis.patch.yml 唯一的组合贡献:一个 host 面的 /team 命令行
138
183
  lib/command.js /team 命令:解析 + 建工作区 + 装 skill + 触发团队 + 11 条浮层路由
139
184
  lib/validate.js 状态机/质量门禁/容量上限的纯函数校验器
185
+ lib/interception.js 把「台账契约」「规格边界」搬到宿主 tools/post-execute 瀑布上(硬门在代码里)
140
186
  lib/tier.js 流程档位词表的唯一真源
187
+ lib/vocab.js 阶段/角色/档位词表的唯一真源(host 与 client 两侧都从这里派生)
141
188
  lib/metrics/ token 记账、首产物耗时、收尾预算、METRICS 渲染
142
- lib/routes/ 路由层共享件(统一 405/500/JSON 处理)
189
+ lib/routes/ 路由层共享件(统一 405/500/JSON 处理、本机来源守卫)
143
190
  client.js 客户端浮层(模块加载器 bundle,仅 require('react'))
144
- skills/expert-team/ 编排「大脑」:SKILL.md + references/ + 工件模板
191
+ skills/expert-team/ 编排「大脑」:SKILL.md + references/ + assets/templates/
145
192
  presets/expert-team/ 「专家团模式」preset:12 个角色 subagent 工具实例
146
193
  ```
147
194
 
@@ -150,7 +197,7 @@ presets/expert-team/ 「专家团模式」preset:12 个角色 subagent 工
150
197
  ## 开发
151
198
 
152
199
  ```sh
153
- npm run test:all # 75 个测试文件,零依赖、无需 install(CI 跑的就是它)
200
+ npm run test:all # 80 个测试文件,零依赖、无需 install(CI 跑的就是它)
154
201
  npm run rename <新包名> # fork 后改名:自动同步 13 个文件里 4 种包名写法
155
202
  npm run check:name # 检查占位包名残留
156
203
  ```
@@ -174,9 +221,19 @@ dsh 安装里插件自带的 Config schema(dsh 路径自动探测,可用 `DS
174
221
 
175
222
  改完**重启 `dsh web`**(预设名册在启动时固化,刷新页面不够),再开**新会话**(预设只在会话创建时固定)。
176
223
 
177
- ## 已知限制
224
+ **浮层打开很慢 / 整个 `dsh web` 发卡?** 1.3.5 之前 `/state` 会逐条全量读子会话日志(实测热态 7–10 s、
225
+ 冷态 283 s),并堵住事件循环。升级到 ≥ 1.3.5 后重启 `dsh web` 即可。
178
226
 
179
- - **本机来源守卫已就位,但不是鉴权**:11 条浮层路由现在统一校验 Host(挡 DNS rebinding)、
227
+ ## 自定义预设(想改专家团默认行为时)
228
+
229
+ - **创建**:`设置 → Agent 预设 → 用「创造模式」创作自定义预设`(其机制是"复制一份既有预设",产出落在 `$DSH_HOME/.agent-presets/<id>/`)。
230
+ - **要定制专家团,请以「专家团模式」为源、换一个你自己的 id**(例如 `my-team`):复制出来的目录天然带上 12 个角色工具与它的 skill 目录,
231
+ 你的改动只写在 `my-team/` 里。**永远不要改 `expert-team/` 里的文件** —— 那份是插件所有的,加载/升级会整目录重铺覆盖。
232
+ - 新建的预设**可能要重启 `dsh web` 后**才出现在选择器里(宿主名册在启动时读取)。
233
+
234
+ ## 诚实边界
235
+
236
+ - **本机来源守卫已就位,但不是鉴权**:11 条浮层路由统一校验 Host(挡 DNS rebinding)、
180
237
  写方法的 Origin(挡跨站写入)、客户端地址(挡局域网),写方法还要求 `application/json`
181
238
  (挡 form / text-plain 这类不触发预检的"简单请求");`/file` 改走宿主 `ctx.fs` 策略,
182
239
  读被策略拒绝时如实报 403 而**不退回裸读**。
@@ -186,7 +243,16 @@ dsh 安装里插件自带的 Config schema(dsh 路径自动探测,可用 `DS
186
243
  - **preset 漂移**:随包的「专家团模式」是官方 `standard` preset 的拷贝 + 角色工具,
187
244
  宿主若调整内置 preset 结构,需要同步更新。升级插件时会按版本戳整目录重铺;
188
245
  `/team uninstall` 可回收它(用户自己写的同名 preset 不会被碰)。
189
- - 会调用 `git status --porcelain`(只读)用于工件新鲜度判断。
246
+ - **会有意调用 `git status --porcelain`(只读)** 用于工件新鲜度判断。
247
+ - **未接线项不装作可用**:设置页的项要么真的生效,要么被标为「暂未生效」(由 `INERT_SETTINGS` 单一真源驱动),
248
+ 并由 `settings-consumers.test.mjs` 盯着 —— 目前该表为空。
249
+
250
+ ## 兼容性
251
+
252
+ - `engines.dsh: >=0.1.5-rc.1`,声明在 `package.json` 的 `dsh.compatibility`(`dshReleases` 逐版本标注);
253
+ 插件市场按它判断"这个插件跟你的宿主兼不兼容"。
254
+ - Node.js ≥ 20。
255
+ - profile:`web`(见上「诚实边界」)。
190
256
 
191
257
  ## License
192
258
 
package/client.js CHANGED
@@ -1190,6 +1190,7 @@ window.__ModuleLoader__.load({
1190
1190
  h('button', { className: 'exp-settings-retry', onClick: function () { setRetry(retry + 1) } }, esc(t('重试', 'Retry'))))
1191
1191
  if (!data || !data.schema) return h('div', { className: 'exp-settings' }, h('div', { className: 'exp-empty' }, t('(正在读取设置…)', '(loading settings…)')))
1192
1192
  var model = settingsFormModel(data.schema, data.settings)
1193
+ var hasInertMark = model.some(function (g) { return (g.rows || []).some(function (r) { return /暂未生效/.test(String(r.hint || '')) }) })
1193
1194
  var rows = []
1194
1195
  model.forEach(function (g) {
1195
1196
  rows.push(h('div', { key: 'h-' + g.group, className: 'exp-settings-group' }, esc(g.label) + (g.hint ? ' · ' + esc(g.hint) : '')))
@@ -1222,7 +1223,11 @@ window.__ModuleLoader__.load({
1222
1223
  // ③ **没有任何一项需要重启** ⇒ 回执只说"已保存"(`needsRestart` 恒 false)。
1223
1224
  // ④ 标着「暂未生效」的项 = **还没接线**(`INERT_SETTINGS`,见 lib/settings.js):写在这里
1224
1225
  // 不是承诺,而是如实告知;逐项标记由 per-item hint 携带,不在这里重复。
1225
- h('div', { className: 'exp-settings-head' }, esc(t('改动即保存并即时生效(上限 / 轮次 / 档位门 / 振荡检测开关在进程内重算)。标着「暂未生效」的项尚未接线,改了不会有作用。', 'Saved on change and applied immediately (caps, rounds, the tier gate and the oscillation switch are recomputed in-process). Items marked as not yet in effect are not wired up — changing them does nothing.'))),
1226
+ // 「暂未生效」那句**只在真有这种项时**才渲染(1.3.5):INERT_SETTINGS 现在是空的,
1227
+ // 无条件渲染会让用户去找一个不存在的标注。判据直接取 hint 里的标记 ⇒ 与后端单一真源一致。
1228
+ h('div', { className: 'exp-settings-head' }, esc(hasInertMark
1229
+ ? t('改动即保存并即时生效(上限 / 轮次 / 档位门 / 振荡检测开关在进程内重算)。标着「暂未生效」的项尚未接线,改了不会有作用。', 'Saved on change and applied immediately (caps, rounds, the tier gate and the oscillation switch are recomputed in-process). Items marked as not yet in effect are not wired up — changing them does nothing.')
1230
+ : t('改动即保存并即时生效(上限 / 轮次 / 档位门 / 振荡检测开关在进程内重算)。', 'Saved on change and applied immediately (caps, rounds, the tier gate and the oscillation switch are recomputed in-process).'))),
1226
1231
  rows,
1227
1232
  h('div', { className: 'exp-settings-msg' + (err ? ' bad' : '') }, esc(err ? '✗ ' + err : (msg || ''))))
1228
1233
  }
@@ -1345,7 +1350,22 @@ window.__ModuleLoader__.load({
1345
1350
  if (selRunV && selRunV.workspace && selRunV.runId) { q.push('workspace=' + encodeURIComponent(selRunV.workspace)); q.push('run=' + encodeURIComponent(selRunV.runId)) }
1346
1351
  return url + (q.length ? ('?' + q.join('&')) : '')
1347
1352
  }
1353
+ // ── single-flight + 自适应退避(2026-09-15 性能修复)──────────────────────────
1354
+ // 实测:8 秒内曾发 5 个 /state、其中 4 个重叠,而单发可达 7–283 秒 ⇒ 队列只增不减,
1355
+ // 服务端被压垮(连轻量端点 /settings 都涨到 20.7 s / 42.5 s,整个 dsh web 一起卡)。
1356
+ // 两条纪律:① **未回绝不发下一个**(晚一点看到状态,好过把 web 拖死);
1357
+ // ② 间隔随上次耗时**退避**(见 reschedule),重活端点不该 1.2 秒一发。
1358
+ var inFlightRef = useRef(false)
1359
+ var lastMsRef = useRef(0)
1348
1360
  function load() {
1361
+ if (inFlightRef.current) return
1362
+ inFlightRef.current = true
1363
+ var t0 = (window.performance && performance.now) ? performance.now() : Date.now()
1364
+ var done = function () {
1365
+ inFlightRef.current = false
1366
+ var now = (window.performance && performance.now) ? performance.now() : Date.now()
1367
+ lastMsRef.current = Math.max(0, now - t0)
1368
+ }
1349
1369
  fetch(stateUrl()).then(function (r) { return r.ok ? r.json() : null }).then(function (d) {
1350
1370
  if (d && d.ok) {
1351
1371
  setData(d); setErr(''); reschedule(d)
@@ -1380,16 +1400,20 @@ window.__ModuleLoader__.load({
1380
1400
  } catch (e) {}
1381
1401
  } else if (d && !d.ok && d.runsAvailable === 0) { setData(null); if (d.runs) setRunsMeta(d.runs); setErr(t('还没有专家团 run。先运行 /team <task> 开一个。', 'No team run yet — run /team <task> first.')) }
1382
1402
  else if (d && !d.ok) { if (d.runs) setRunsMeta(d.runs); setErr(d.error || ''); }
1383
- }).catch(function () {})
1403
+ }).catch(function () {}).then(done, done)
1384
1404
  }
1385
1405
  // Adaptive refresh: while any member is running, poll fast (workers are
1386
- // touching files right now); otherwise 3s. Re-schedules after each load.
1406
+ // touching files right now); otherwise the configured cadence.
1407
+ // 2026-09-15:再叠一层**退避** —— 间隔 = clamp(max(基础间隔, 上次耗时×2), 基础间隔, 30000)。
1408
+ // 单发越慢 ⇒ 下次越晚;display.pollMs 仍是基础节奏(设置里那一项依然说了算)。
1387
1409
  function reschedule(d) {
1388
1410
  var m = (d && d.members) || {}
1389
1411
  var busy = Object.keys(m).some(function (k) { var v = m[k]; return v && typeof v === 'object' && (v.activity === 'running' || v.shortStatus === 'running') })
1390
1412
  clearInterval(pollRef.current)
1391
1413
  // 1.3.4:间隔来自设置 display.pollMs(忙碌时按同一意图加速到 40%,下限 300ms)
1392
- pollRef.current = setInterval(load, busy ? Math.max(300, Math.round(dispCfg.pollMs * 0.4)) : dispCfg.pollMs)
1414
+ var base = busy ? Math.max(300, Math.round(dispCfg.pollMs * 0.4)) : dispCfg.pollMs
1415
+ var backoff = Math.max(base, Math.min(30000, Math.round((lastMsRef.current || 0) * 2)))
1416
+ pollRef.current = setInterval(load, backoff)
1393
1417
  }
1394
1418
  useEffect(function () {
1395
1419
  if (!isOpen && !viewMode) return undefined // panel closed & not canvas → no polling
@@ -29,6 +29,8 @@ export const USAGE = [
29
29
  ' /team help 显示本用法(等价:? / -h / --help)',
30
30
  ' /team --persist <task> 启动持久化活团队(成员可反复指挥,跨会话恢复)',
31
31
  ' /team --no-code <task> 只产出计划/评审/测试工件,不改代码',
32
+ ' /team --code <task> 显式要「代码 + 工件」(压过设置里的「交付口径 = 只出工件」)',
33
+ ' /team --one-shot <task> 显式要「一次性组队」(压过设置里的「默认持久化活团队」)',
32
34
  ' /team --confirm <task> 大需求先确认:建 run 但**不自动派工**,面板点「执行」才开工',
33
35
  ' /team --tier <档位> <task> 指定流程档位:快速档 / 标准档 / 严格档(不写则在浮层三选一,每次都问)',
34
36
  ' /team tier <档位> [--run <run>] 运行中**升档**(快速档→标准档→严格档;不允许降档)',
@@ -145,8 +147,11 @@ export function parseTeamCommand(rawInput) {
145
147
  // Consume leading flags; everything after the last flag is the task text.
146
148
  const tokens = input.split(/\s+/);
147
149
  let i = 0;
148
- let persist = false;
149
- let noCode = false;
150
+ // 三态(1.3.5):null = 未表态(听设置);true = 显式要;false = **显式不要**(--one-shot / --code)。
151
+ // 为什么必须三态:1.3.4 把设置接进"无 flag 时的默认"后就出现不对称 —— 设置成 persist=true 之后,
152
+ // 单条命令没有任何办法要一次性运行(旧代码的布尔默认把"没给 flag"和"显式否"混成同一个 false)。
153
+ let persist = null;
154
+ let noCode = null;
150
155
  let roles = null;
151
156
  let runName = null;
152
157
  let profile = null;
@@ -156,7 +161,9 @@ export function parseTeamCommand(rawInput) {
156
161
  while (i < tokens.length && tokens[i].startsWith('--')) {
157
162
  const flag = tokens[i];
158
163
  if (flag === '--persist') persist = true;
164
+ else if (flag === '--one-shot') persist = false;
159
165
  else if (flag === '--no-code') noCode = true;
166
+ else if (flag === '--code') noCode = false;
160
167
  else if (flag === '--allow-rebuild') allowRebuild = true;
161
168
  else if (flag.startsWith('--roles')) {
162
169
  const eq = flag.indexOf('=');
package/lib/command.js CHANGED
@@ -1414,6 +1414,21 @@ function sessionRunsPath() { return join(dshHome(), 'expert-team', 'session-runs
1414
1414
  * 旧数据没有 `via`;`runOwnerSession()` 会把"最早那条"当作归属兜底,并对
1415
1415
  * 是否真的可信如实回报(见 `ownerResolved`)。
1416
1416
  */
1417
+ /**
1418
+ * 该 session id 是否**真实存在**(写侧守卫,2026-09-15)。
1419
+ *
1420
+ * 为什么需要:/state 允许带任意 `sessionId` 查询,而旧实现在"显式指定 run+workspace"时
1421
+ * **无条件** rememberSessionRun(sid, …) 落盘 —— 性能诊断时用一个**假 id** 就被写进了
1422
+ * `$DSH_HOME/expert-team/session-runs.json`(污染用户数据)。
1423
+ * 只查活存储(便宜、不读日志);查不到 ⇒ 当只读、**不写盘**。
1424
+ * 代价:已结束的会话"查看某 run"不再被记住(面板退回"按当前工作区自动选 run")——
1425
+ * 这是**有意**的取舍:宁可不记,也不写假 id。
1426
+ */
1427
+ function sessionExists(ctx, sid) {
1428
+ const id = String(sid || '');
1429
+ if (!id) return false;
1430
+ try { return Boolean(ctx?.get?.('sessions')?.get?.(id)); } catch { return false; }
1431
+ }
1417
1432
  function rememberSessionRun(sid, ws, runId, via) {
1418
1433
  if (!sid || !ws || !runId) return;
1419
1434
  const prev = SESSION_RUNS.get(sid);
@@ -1741,8 +1756,10 @@ export function resolveRosterDefaults(mode, rosterSet, fallbackRoles) {
1741
1756
  ? rosterSet['roster.defaultRoles'].slice() : null;
1742
1757
  const explicitRoles = Array.isArray(m.roles) && m.roles.length ? m.roles.slice() : null;
1743
1758
  return {
1744
- persist: m.persist === true ? true : Boolean(rosterSet && rosterSet['roster.persist'] === true),
1745
- noCode: m.noCode === true ? true : Boolean(rosterSet && rosterSet['roster.deliverable'] === 'artifacts-only'),
1759
+ // 三态(1.3.5):true = 显式要;false = **显式不要**(--one-shot / --code 压过设置);
1760
+ // null/undefined = 未表态 听设置(见 command-parse.js 的三态注释)。
1761
+ persist: m.persist === true ? true : (m.persist === false ? false : Boolean(rosterSet && rosterSet['roster.persist'] === true)),
1762
+ noCode: m.noCode === true ? true : (m.noCode === false ? false : Boolean(rosterSet && rosterSet['roster.deliverable'] === 'artifacts-only')),
1746
1763
  roles: explicitRoles || settingsRoles,
1747
1764
  rolesBase: settingsRoles || base,
1748
1765
  rolesFromSettings: !explicitRoles && Boolean(settingsRoles),
@@ -2723,6 +2740,53 @@ async function childSessionTiming(ctx, id) {
2723
2740
  return out;
2724
2741
  }
2725
2742
 
2743
+ /**
2744
+ * 子会话 header **索引**:只查表,**永不读日志**。
2745
+ *
2746
+ * 为什么必须有它(2026-09-15 性能诊断):/state 曾对**每个**子会话调 childSessionTiming,
2747
+ * 而它对"活存储里已查不到"的子代理会退回 sessionQuery.readSession(id) —— 那是**全量读该子会话日志**
2748
+ * (本机实测 21 条 = 23.6 MiB zstd / ~31,000 事件;别的 run 测到 75/92 条)。结果:端点热态 7.1–9.8 s、
2749
+ * 冷态 **283.6 s**,而面板绘制只要 52 ms;请求还压在事件循环上,把整个 dsh web 一起拖慢
2750
+ * (轻量端点 /settings 因此涨到 20.7 s / 42.5 s)。
2751
+ *
2752
+ * 三个来源都**便宜**,按"准且省"排序:
2753
+ * ① listSubagentStatusBySession 已经带出的 durable header 字段(createdAt/parentId/depth)
2754
+ * —— 那一支本来就要 listSessions(),字段是**顺带**拿到的,不额外读任何日志;
2755
+ * ② 活存储 ctx.sessions.get(id).header(子代理还在跑时最省);
2756
+ * ③ workflow 事件流的 startedAt(父会话日志已按 WF_EVENT_TTL_MS 缓存,见 workflowEventIndex)。
2757
+ * 三源都拿不到 ⇒ 保持 0/''/0,UI 按「无时间记录」**如实降级**(hasTimestamp:false)——
2758
+ * **不臆造**,也**绝不**为了"看起来有数据"退回全量读日志。
2759
+ *
2760
+ * @returns {Map<string, {createdAt:number, parentId:string, depth:number}>}
2761
+ */
2762
+ function subHeaderIndex(ctx, subs, wfLabels) {
2763
+ const out = new Map();
2764
+ for (const s of Array.isArray(subs) ? subs : []) {
2765
+ const id = String((s && s.id) || '');
2766
+ if (!id) continue;
2767
+ let createdAt = Number(s.createdAt) || 0;
2768
+ let parentId = String(s.parentId || '');
2769
+ let depth = Number(s.depth) || 0;
2770
+ if (!createdAt) {
2771
+ try {
2772
+ const hdr = ctx?.get?.('sessions')?.get?.(id)?.header;
2773
+ if (hdr) {
2774
+ createdAt = Number(hdr.createdAt) || 0;
2775
+ if (!parentId) parentId = String(hdr.parentSession || '');
2776
+ if (!depth) depth = Number(hdr.delegationDepth) || 0;
2777
+ }
2778
+ } catch { /* best-effort */ }
2779
+ }
2780
+ if (!createdAt && wfLabels) {
2781
+ const w = typeof wfLabels.get === 'function' ? wfLabels.get(id) : wfLabels[id];
2782
+ const at = w && Number(w.startedAt);
2783
+ if (at) createdAt = at;
2784
+ }
2785
+ out.set(id, { createdAt, parentId, depth });
2786
+ }
2787
+ return out;
2788
+ }
2789
+
2726
2790
  async function listSubagentStatusBySession(ctx, sid, knownIds) {
2727
2791
  const root = sid ? await rootSessionId(ctx, sid) : '';
2728
2792
  let rows = [];
@@ -2900,7 +2964,11 @@ async function roleFromChildLog(ctx, id) {
2900
2964
  * tick, while a run that finishes mid-session still converges within a tick.
2901
2965
  * Best-effort: any read failure yields empty results (never throws).
2902
2966
  */
2903
- const WF_EVENT_TTL_MS = 3000;
2967
+ // ⚠️ 2026-09-15 性能诊断:TTL 原为 **3000ms,恰好等于默认轮询间隔 3000ms** ⇒ **每个 tick 必失效**,
2968
+ // 父会话日志(多 MB)被一遍遍全量重读,这是热态 7.1–9.8 s 的主要来源之一。现改为 30000ms:
2969
+ // TTL 只是**兜底**(理想是按日志 mtime/size 失效,留待后续),代价是"run 在会话中途结束"最多晚
2970
+ // 30 秒反映到面板 —— 与"每 3 秒重读整个日志"相比,这个交换值得。
2971
+ const WF_EVENT_TTL_MS = 30000;
2904
2972
  const WF_EVENT_CACHE = new Map(); // sessionId -> { at, labels: Map, runs: [] }
2905
2973
  async function workflowEventIndex(ctx, sid) {
2906
2974
  const key = String(sid || '');
@@ -4581,7 +4649,7 @@ function warnLeadToolFaceOnce(status, detail) {
4581
4649
  LEAD_TOOLFACE_WARNED.add(status);
4582
4650
  console.warn(`[expert-team] lead 工具面**未**收窄(${status}):${detail}`);
4583
4651
  }
4584
- export const _live = { pushActivity, phaseAccountingViolations, loggedPhases, authorityViolations, WRITE_TRACER, createWriteTracer, formatConflict, summarizeTool, parseLogLine, roleOfSub, mapRoleToSub, membersFromState, buildRoleSubMap, resolveSubRoles, childSessionTiming, 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 };
4652
+ 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 };
4585
4653
 
4586
4654
  export function apply(ctx, config) {
4587
4655
  // 留一份 config:设置在运行时改变(官方面板 / 浮层)时要**用同一份 config** 重算上限与档位门,
@@ -4870,7 +4938,8 @@ export function apply(ctx, config) {
4870
4938
  let sel = null;
4871
4939
  if (runParam && wsParam) {
4872
4940
  sel = await snapshotRun(wsParam, runParam);
4873
- if (sel) rememberSessionRun(sid, wsParam, runParam, 'view'); // user deliberately picked a run
4941
+ // 只在**该会话真实存在**时才记忆(假 id 不得落盘,见 sessionExists)
4942
+ if (sel && sessionExists(ctx, sid)) rememberSessionRun(sid, wsParam, runParam, 'view'); // user deliberately picked a run
4874
4943
  } else if (runParam && workspaces.length === 1) {
4875
4944
  sel = await snapshotRun(workspaces[0], runParam);
4876
4945
  } else {
@@ -4947,10 +5016,8 @@ export function apply(ctx, config) {
4947
5016
  if (membersFromState(sel.stateMembers).byRole.size) {
4948
5017
  subById = buildRoleSubMap(subs, sel.stateMembers, wfLabels); // 已登记:不过滤、不丢
4949
5018
  } else {
4950
- const timingOfSubs = new Map();
4951
- for (const s of subs) {
4952
- try { timingOfSubs.set(String(s.id || ''), await childSessionTiming(ctx, s.id)); } catch { /* best-effort */ }
4953
- }
5019
+ // 只查表、不读日志(热点 D 修复):见 subHeaderIndex 的注释。
5020
+ const timingOfSubs = subHeaderIndex(ctx, subs, wfLabels);
4954
5021
  const scoped = filterRunScopedSubs(subs, {
4955
5022
  wfLabels,
4956
5023
  runId: sel.runId,
@@ -5010,12 +5077,14 @@ export function apply(ctx, config) {
5010
5077
  }
5011
5078
  // 时间维度(任何"分批/波次"视图都需要):createdAt 取自子会话 header,
5012
5079
  // lastEventAt 复用下面已收集的 per-agent feed 最后一条事件时间(零额外开销)。
5013
- // **必须走 childSessionTiming 这条 durable 路径**:已结束的子代理会被从活存储里
5014
- // 清掉,只查 ctx.sessions.get() 会让每个成员都是 0(面板显示「无创建时间记录,
5015
- // 无法分批」,尽管会话日志第一行就写着 createdAt)。
5016
- // 不臆造:三源都拿不到就保持 0,UI 按"无时间数据"如实降级。
5080
+ // subHeaderIndex(**只查表**):已结束的子代理会被从活存储里清掉,但它们的
5081
+ // durable header 字段由 listSubagentStatusBySession 的 listSessions() 分支
5082
+ // **顺带**带出来了(createdAt/parentId/depth),所以不需要再读一次日志。
5083
+ // 2026-09-15:此前这里对每个子会话 await childSessionTiming(会全量 readSession),
5084
+ // 是 /state 冷态 283.6 s 的根因。不臆造:三源都拿不到就保持 0,UI 如实降级。
5085
+ const timingIdx = subHeaderIndex(ctx, subs, wfLabels);
5017
5086
  const timingOf = {};
5018
- for (const s of subs) timingOf[String(s.id || '')] = await childSessionTiming(ctx, s.id);
5087
+ for (const s of subs) timingOf[String(s.id || '')] = timingIdx.get(String(s.id || '')) || { createdAt: 0, parentId: '', depth: 0 };
5019
5088
  sel.agents = subs.map((s) => {
5020
5089
  const sid2 = String(s.id || '');
5021
5090
  const t = timingOf[sid2] || { createdAt: 0, parentId: '', depth: 0 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yangdcm/dsh-expert-team",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
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",
@@ -84,7 +84,7 @@
84
84
  "check:name": "node scripts/rename-package.mjs --check",
85
85
  "test:regression": "node regression.test.mjs",
86
86
  "test:e2e": "node e2e.test.mjs",
87
- "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",
87
+ "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",
88
88
  "test:toolcard": "node tool-card-status.test.mjs",
89
89
  "test:flow": "node flow.test.mjs",
90
90
  "test:decide": "node plan-decide.test.mjs",