@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 +80 -0
- package/README.en.md +220 -131
- package/README.md +103 -37
- package/client.js +28 -4
- package/lib/command-parse.js +9 -2
- package/lib/command.js +83 -14
- package/package.json +2 -2
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)
|
|
7
7
|
[](https://github.com/yangdcm/dsh-expert-team/actions/workflows/ci.yml)
|
|
8
8
|
|
|
9
|
+

|
|
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
|
-
**
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
+

|
|
20
24
|
|
|
21
|
-
<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
|
-
##
|
|
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**.
|
|
29
|
-
|
|
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`)
|
|
34
|
-
| **Phase
|
|
35
|
-
| **Quality gates
|
|
36
|
-
| **Convergence
|
|
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
|
-
##
|
|
68
|
+
## What it looks like in action
|
|
39
69
|
|
|
40
|
-
|
|
41
|
-
The roster is trimmed per task; small jobs start only the roles they need.
|
|
70
|
+

|
|
42
71
|
|
|
43
|
-
|
|
44
|
-
(
|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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
|
|
138
|
+
**Option 2: plugin market** (listing not submitted yet ⇒ it may not be searchable today)
|
|
65
139
|
|
|
66
|
-
|
|
67
|
-
```
|
|
140
|
+
Once listed: `dsh web` → **Settings → Plugin market** → search "expert team" → install → refresh the page.
|
|
68
141
|
|
|
69
|
-
**Option 3
|
|
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
|
|
79
|
-
>
|
|
80
|
-
>
|
|
81
|
-
> has no skill registry
|
|
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
|
|
89
|
-
>
|
|
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
|
|
92
|
-
> (the `settings.section` slot, `id: expert-team`, `order: 50`), sharing the entry point and panel chrome
|
|
93
|
-
>
|
|
94
|
-
>
|
|
95
|
-
>
|
|
96
|
-
> **Honest boundary**:
|
|
97
|
-
>
|
|
98
|
-
>
|
|
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**:
|
|
101
|
-
>
|
|
102
|
-
>
|
|
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
|
|
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
|
-
**
|
|
109
|
-
subagent tools are in place (
|
|
110
|
-
2. Switch the session to
|
|
111
|
-
3. Change settings under **Settings
|
|
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**:
|
|
115
|
-
|
|
116
|
-
[Troubleshooting](#troubleshooting) section
|
|
117
|
-
|
|
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
|
|
134
|
-
/team --persist refactor the orders module # persistent live team
|
|
135
|
-
/team --
|
|
136
|
-
/team --
|
|
137
|
-
/team
|
|
138
|
-
/team
|
|
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
|
-
|
|
142
|
-
`/team
|
|
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
|
|
206
|
+
**Where artifacts land**
|
|
145
207
|
|
|
146
|
-
- `<your workspace>/team/<run-id>/` —
|
|
147
|
-
- `$DSH_HOME/expert-team/` — machine-local preferences and cross-project
|
|
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:
|
|
153
|
-
lib/command.js /team command: parse +
|
|
154
|
-
lib/validate.js pure validators for state machine
|
|
155
|
-
lib/
|
|
156
|
-
lib/
|
|
157
|
-
lib/
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
164
|
-
without a package
|
|
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 #
|
|
170
|
-
npm run rename <name> #
|
|
171
|
-
npm run check:name #
|
|
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`)
|
|
175
|
-
**
|
|
176
|
-
`$DSH_HOME`, and `gate:preset` borrows the Config
|
|
177
|
-
(
|
|
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
|
-
**
|
|
182
|
-
|
|
183
|
-
did not exist before 1.3.0.
|
|
184
|
-
|
|
185
|
-
**The
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
1.
|
|
190
|
-
|
|
191
|
-
2.
|
|
192
|
-
3.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
**new session** (
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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)
|
|
7
7
|
[](https://github.com/yangdcm/dsh-expert-team/actions/workflows/ci.yml)
|
|
8
8
|
|
|
9
|
+

|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
| 12 角色 | 9 阶段 | 80 个测试文件 | 0 运行时依赖 | 0 构建步骤 |
|
|
18
|
+
|---|---|---|---|---|
|
|
19
|
+
| 各带人设 / `toolFilter` / `maxDepth: 1` | 含 1 道硬门 + 1 道确认门 | 含 136 条变异目录与多组棘轮 | `dependencies: {}` | 无 bundler、无 `prepare` 钩子 |
|
|
20
|
+
|
|
21
|
+

|
|
19
22
|
|
|
20
|
-
<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
|
-
|
|
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
|
|
34
|
-
| **收敛与记账** | 每 run 记 token/耗时/首产物时间/收尾预算;`/team learn` 跨 run
|
|
56
|
+
| **阶段门控** | 9 个阶段,每次交接走「结构化返回值 + 工件文件」双通道 —— 状态不靠聊天记录传递 |
|
|
57
|
+
| **质量门禁** | 状态机一致性由**插件代码强制**(不是提示词请求):任务未完成不能标 completed、质量问题必须由 qa/reviewer 裁决、覆盖率缺口、超轮次返工 —— 违规**实时**显示在浮层并计入 `/team status` |
|
|
58
|
+
| **收敛与记账** | 每 run 记 token/耗时/首产物时间/收尾预算;`/team learn` 跨 run 蒸馏经验,并在下次开工前回注 |
|
|
59
|
+
|
|
60
|
+
## 看一眼它在干什么
|
|
61
|
+
|
|
62
|
+

|
|
63
|
+
|
|
64
|
+
<sub>图 2:**门禁违规**。看顶部那条横幅 —— 违规项与拒绝理由(例如"SPEC.md 的边界章节已进入 `implement` 但仍无任何一行填写")由 `lib/interception.js` 挂在宿主 `tools/post-execute` 上当场判出后推出,不是提示词提醒。</sub>
|
|
65
|
+
|
|
66
|
+

|
|
67
|
+
|
|
68
|
+
<sub>图 3:**角色编制**。看成员列表 —— 谁在跑、用哪个模型、当前在做什么;展开任一成员可看它的任务与产物。模型可按角色分别配置,异构模型用于交叉验证。</sub>
|
|
69
|
+
|
|
70
|
+

|
|
71
|
+
|
|
72
|
+
<sub>图 4:**阶段与工件**。看阶段条与预览区 —— 当前阶段、已过阶段、以及该阶段真正写下的工件正文(工件是唯一真源,浮层只是它的视图)。</sub>
|
|
35
73
|
|
|
36
|
-
|
|
74
|
+

|
|
37
75
|
|
|
38
|
-
|
|
39
|
-
前端(frontend) · 数据(dba) · 安全审计(sec) · 评审(reviewer) · 测试(qa) · 运维(devops) · 文档(docs)。
|
|
40
|
-
按任务复杂度裁剪:小活只起需要的角色。
|
|
76
|
+
<sub>图 5:**设置**。看官方 `设置 →「专家团」` 这一页 —— 18 个设置项、中文标签、**改动即保存并即时生效**(上限/轮次/档位门/振荡检测在进程内重算);值存在宿主命名空间 `expert-team`,随插件市场的备份/恢复一起走。</sub>
|
|
41
77
|
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
80
|
-
>
|
|
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 #
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
package/lib/command-parse.js
CHANGED
|
@@ -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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
1745
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4951
|
-
|
|
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
|
-
//
|
|
5014
|
-
//
|
|
5015
|
-
//
|
|
5016
|
-
//
|
|
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 || '')] =
|
|
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.
|
|
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",
|