dsh-advisor 0.1.0

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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +7 -0
  3. package/README.md +303 -0
  4. package/README.zh.md +166 -0
  5. package/cordis.patch.yml +6 -0
  6. package/lib/advisor-runtime.d.ts +242 -0
  7. package/lib/advisor-runtime.js +662 -0
  8. package/lib/advisor-runtime.js.map +1 -0
  9. package/lib/client/advisor-card.d.ts +90 -0
  10. package/lib/client/advisor-store.d.ts +310 -0
  11. package/lib/client/index.d.ts +39 -0
  12. package/lib/client/locales.d.ts +40 -0
  13. package/lib/client.d.ts +1 -0
  14. package/lib/client.js +840 -0
  15. package/lib/commands.d.ts +136 -0
  16. package/lib/commands.js +185 -0
  17. package/lib/commands.js.map +1 -0
  18. package/lib/config.d.ts +74 -0
  19. package/lib/config.js +93 -0
  20. package/lib/config.js.map +1 -0
  21. package/lib/delivery.d.ts +129 -0
  22. package/lib/delivery.js +169 -0
  23. package/lib/delivery.js.map +1 -0
  24. package/lib/emission-guard.d.ts +99 -0
  25. package/lib/emission-guard.js +155 -0
  26. package/lib/emission-guard.js.map +1 -0
  27. package/lib/gateway.d.ts +116 -0
  28. package/lib/gateway.js +214 -0
  29. package/lib/gateway.js.map +1 -0
  30. package/lib/index.d.ts +48 -0
  31. package/lib/index.js +485 -0
  32. package/lib/index.js.map +1 -0
  33. package/lib/kinds.d.ts +38 -0
  34. package/lib/kinds.js +24 -0
  35. package/lib/kinds.js.map +1 -0
  36. package/lib/prompts.d.ts +22 -0
  37. package/lib/prompts.js +38 -0
  38. package/lib/prompts.js.map +1 -0
  39. package/lib/settings.d.ts +96 -0
  40. package/lib/settings.js +141 -0
  41. package/lib/settings.js.map +1 -0
  42. package/lib/transcript.d.ts +257 -0
  43. package/lib/transcript.js +530 -0
  44. package/lib/transcript.js.map +1 -0
  45. package/package.json +90 -0
  46. package/scripts/build-client.mjs +268 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-external
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,7 @@
1
+ # Bilingual-pair consistency record: the git blob hash of each side as of the
2
+ # last confirmed-consistent state. Both languages carry equal authority; after
3
+ # editing either side, bring the other along and re-record with:
4
+ # git hash-object README.md
5
+ # git hash-object README.zh.md
6
+ README.md: 9c49bb1959b760bce4db030d92ca5efd57f0cbe7
7
+ README.zh.md: e4db627807e12277ab0e516b445ccb35138ba6a9
package/README.md ADDED
@@ -0,0 +1,303 @@
1
+ # dsh-advisor
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+ ![node](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933.svg)
7
+ ![dsh](https://img.shields.io/badge/dsh-DeepSeek%20Harness%20compatible-4B32C3.svg)
8
+
9
+ A standalone dsh plugin bundle porting the omp "advisor"
10
+ subsystem: a per-session reviewer model that observes the primary transcript,
11
+ reviews each stepped turn with an explicitly configured model (provider +
12
+ model are required), and injects severity-ranked advice (nit / concern /
13
+ blocker) back into the session — without polluting or recursively reviewing
14
+ itself.
15
+
16
+ Install with a single command (pnpm ≥ 10 needs one build-allow step — see [Install](#install)):
17
+
18
+ ```sh
19
+ dsh plugin --profile web add github:btspoony/dsh-advisor # <name> = your profile name; pin a commit with #<sha>
20
+ ```
21
+
22
+ **Advisory only.** The advisor never approves or rejects the primary agent's
23
+ actions; it never issues commands as if it were the primary agent. Every
24
+ delivered message is self-described advisory content, and a misbehaving
25
+ reviewer is bounded end to end (emission guard, immuneTurns cooldown, failure
26
+ policy) so it can never stall or pollute the primary loop.
27
+
28
+ ## Install
29
+
30
+ ### One-line git install
31
+
32
+ ```sh
33
+ dsh plugin --profile web add github:btspoony/dsh-advisor # <name> = your profile name; pin a commit with #<sha>
34
+ ```
35
+
36
+ A git install fetches **sources, not built artifacts**, so the bundle builds
37
+ itself on install (`prepare` self-build). pnpm ≥ 10 blocks a git dependency's
38
+ `prepare` by default: the first
39
+ `add` fails with `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`, and pnpm prints the
40
+ exact package key — allow the build in the profile's `pnpm-workspace.yaml`
41
+ (`onlyBuiltDependencies: [dsh-advisor]`, or run `dsh plugin --profile web
42
+ approve-builds`), then re-run the `add`. Treat that allowance as permission to
43
+ execute the package's code on your machine at install time, and pin a commit
44
+ (`#<sha>`) so a later push cannot silently change what runs.
45
+
46
+ ### Local directory install (recommended for development / verification)
47
+
48
+ ```sh
49
+ pnpm install # build the bundle (the prepare self-build)
50
+ dsh plugin --profile web add . # <name> = your profile name
51
+ ```
52
+
53
+ ### Verify
54
+
55
+ ```sh
56
+ dsh --profile web --dump-config # shows a "# == dsh-advisor" layer with the advisor row
57
+ dsh --profile web
58
+ ```
59
+
60
+ Tarball install and uninstall are covered in [docs/install.md](docs/install.md).
61
+
62
+ ## Config
63
+
64
+ ![Advisor card on the dsh web Settings (插件配置) page](docs/screenshots/advisor-settings-card.webp)
65
+
66
+ The advisor is off by default. When enabled, `provider` and `model` are
67
+ **mandatory**: `enabled: true` without both is a hard gate — the advisor never
68
+ starts a model call and reports a disabled-with-reason status. Unknown config
69
+ keys are rejected.
70
+
71
+ Configuration composes across **three surfaces** (later layers override earlier
72
+ ones; every surface uses the same key set):
73
+
74
+ 1. **Plugin-row config** — `$DSH_HOME/profiles/web/cordis.patch.yml`
75
+ (below). This is the composition base.
76
+ 2. **dsh web Settings page — the "插件配置" (Plugin Configuration) page** —
77
+ the Advisor **card** (id `advisor`, rendered after the upstream bash /
78
+ agent-loop / web-search cards) with the enabled toggle, provider / model
79
+ selects restricted to system-configured providers and their models, and the
80
+ optional fields. Saving writes into the `advisor` settings namespace and
81
+ overrides the plugin-row config without editing it. Saving applies to new
82
+ sessions immediately — no restart (the runtime reads the composed value
83
+ live). Requires a current dsh web build whose shell declares the
84
+ `settings.plugin.item` card slot and loads packages that declare
85
+ `dsh.client`. The card reads and writes the namespace through the
86
+ **official `GatewayService` RPC channel** (`/api/advisor/get` +
87
+ `/api/advisor/set`, claimed by the host's typertGateway — the same
88
+ mechanism the dsh `goals` service uses), which is **not gated by the
89
+ settings exposure allowlist**: the in-process write
90
+ (`ctx.settings.update`) carries no exposed-namespace check. No host
91
+ patching is applied or required.
92
+ 3. **`/advisor` command** — per-session and ephemeral: it flips a session
93
+ override, never the persisted config (see [Usage](#usage)).
94
+
95
+ Both persisted surfaces share the same hard gate: `enabled: true` with empty
96
+ `provider`/`model` never starts a model call (disabled-with-reason). The
97
+ Settings page additionally blocks saving while enabled with a required field
98
+ empty; the host-side gate stays the final line of defense on every path.
99
+
100
+ Plugin-row config:
101
+
102
+ ```yaml
103
+ # profiles/web/cordis.patch.yml — the profile's user patch layer
104
+ - id: advisor
105
+ config:
106
+ enabled: true # master switch (default false)
107
+ provider: deepseek-official # REQUIRED when enabled
108
+ model: deepseek-v4-flash # REQUIRED when enabled
109
+ systemPrompt: "" # optional; "" = built-in reviewer prompt
110
+ immuneTurns: 3 # int ≥ 0, default 3 — cooldown after a delivered interrupt
111
+ maxDeltaMessages: 60 # int ≥ 0, default 60 — delta window; 0 = unbounded
112
+ ```
113
+
114
+ | Key | Type / default | Meaning |
115
+ |---|---|---|
116
+ | `enabled` | bool, `false` | Master switch. |
117
+ | `provider` | string, optional | Provider route. Required (non-empty) when `enabled: true`. |
118
+ | `model` | string, optional | Model id. Required (non-empty) when `enabled: true`. |
119
+ | `systemPrompt` | string, `""` | Overrides the built-in reviewer prompt (severity definitions + JSON-frame output contract). |
120
+ | `immuneTurns` | int ≥ 0, `3` | After a concern/blocker is actually steered, the next N stepped primary turns must complete before another interrupting note may steer; notes inside the window downgrade to inject. |
121
+ | `maxDeltaMessages` | int ≥ 0, `60` | Bounded advisor input window. Deltas beyond N are truncated with a `… <earlier messages omitted>` marker; `0` = unbounded. |
122
+
123
+ **Model capability & budget**: the advisor call runs with `reasoningEffort:
124
+ 'off'` — sent only when the configured model's adapter declares that effort
125
+ (deepseek models do; any other model gets the option omitted automatically, so
126
+ non-reasoning providers keep working) — and a **5120-token** output cap (a
127
+ user-directed 20× supersession of the original 256). Extracted notes are
128
+ bounded (1000 chars) and the notice summary to 120 chars, so the raised budget
129
+ cannot translate into an unbounded injection into the primary session.
130
+
131
+ ## Usage
132
+
133
+ Once installed and enabled, the advisor observes every session. Control it per
134
+ session with the `/advisor` command (available when a command registry is
135
+ composed):
136
+
137
+ ```
138
+ /advisor toggle the advisor for this session
139
+ /advisor on enable the advisor for this session
140
+ /advisor off disable the advisor for this session
141
+ /advisor status show state, model, runtime status, pending count, last activity
142
+ ```
143
+
144
+ `/advisor on|off|toggle` are session-scoped and ephemeral: they flip a
145
+ per-session override, never the persisted config. Enabling a session whose
146
+ config lacks `provider`/`model` starts no model call — `/advisor status` (and
147
+ the `/advisor on` reply) shows the gate reason.
148
+
149
+ `/advisor on` is also the manual recovery path: a session advisor paused by a
150
+ quota/rate-limit (`quota_exhausted` — KD-5 has no auto-resume timer) resumes in
151
+ place, and a halted advisor (permanent model error, e.g. invalid credentials)
152
+ is rebuilt fresh for the session.
153
+
154
+ The advisor reviews on a dual-mode trigger, depending on the session shape:
155
+
156
+ - **Standard stepped sessions** — after each stepped primary turn that ends
157
+ normally (`completed`, `max-tokens`, or `error`), the advisor reviews the
158
+ incremental transcript delta.
159
+ - **Agentic / harness sessions** (never emit `turn/end`) — after each completed
160
+ agent reply round: when a new human input arrives (inbox-spliced input
161
+ included) after an unreviewed assistant increment, the advisor reviews that
162
+ increment.
163
+
164
+ Either way the advisor emits at most one note per review, ranked by severity:
165
+
166
+ - **nit** — a minor style, clarity, or quality suggestion; delivered via
167
+ `agent.inject` (non-waking, consumed at the next pre-step boundary).
168
+ - **concern** — a material risk or clearly better direction to weigh before
169
+ continuing; delivered via `agent.steer` (waking), subject to the
170
+ `immuneTurns` cooldown.
171
+ - **blocker** — continuing clearly wastes work (contradicts an explicit user
172
+ instruction, going in circles, fundamentally unsound); delivered via
173
+ `agent.steer`.
174
+
175
+ Injected advice appears in the session stream as a user-role message carrying
176
+ the advisor source kind and self-describing content, e.g.:
177
+
178
+ ```
179
+ [advisor:concern] extract the helper into a module and unit-test it
180
+ ```
181
+
182
+ The `[advisor:{severity}]` prefix is the only cue the primary model gets about
183
+ how to treat it — the primary system prompt never mentions advisories. Advisor
184
+ messages are excluded from later advisor deltas, so the advisor never reads
185
+ its own advice back.
186
+
187
+ ![Advisor note injected into the session stream](docs/screenshots/advisor-injected-note.webp)
188
+
189
+ ## How it works
190
+
191
+ The plugin subscribes to `session/event`. Two triggers render an incremental
192
+ markdown delta of the primary transcript (own advisor messages excluded) and
193
+ queue it on a per-session runtime: after each stepped `turn/end` in standard
194
+ stepped sessions, and — in agentic/harness sessions that never emit `turn/end`
195
+ — when a new human input arrives (inbox-spliced input included) after an
196
+ unreviewed assistant increment, i.e. at each completed agent reply round. The
197
+ runtime calls a separately configured model via `ctx.llm.stream`, extracts one
198
+ `{note, severity}` from the JSON-framed reply, gates it through an emission
199
+ guard (normalize / dedupe / content-free suppression / one-note-per-update),
200
+ and routes it: nit → inject, concern/blocker → steer. The advisor call runs
201
+ with reasoning off and a 20x token budget so the JSON note is never starved by
202
+ reasoning output. Compaction and surface rewrites reset the observer, the
203
+ emission guard, and the immuneTurns latch
204
+ (KD-5); the drain is fully async with a bounded backlog, so a failing or
205
+ quota'd advisor can only drop its own backlog — never park the primary loop.
206
+
207
+ ## Limitations & roadmap
208
+
209
+ The MVP deliberately drops full omp parity. Accepted gaps (tracked in the
210
+ harness iteration roadmap):
211
+
212
+ - **Single advisor per session** — no parallel advisor roster or WATCHDOG-style
213
+ file discovery (next iteration).
214
+ - **No advisor tools** — the reviewer is an independent model call only; it
215
+ cannot verify claims itself (next-next iteration).
216
+ - **No in-session advisor panel** — advice surfaces only as tagged injected
217
+ messages (the Advisor card on the "插件配置" settings page is a config
218
+ surface, not a session view; an in-session card is next-next iteration).
219
+ - **No transcript persistence or cost stats** — no resumable advisor history or
220
+ cost observability (next-next iteration).
221
+ - **No secret obfuscation of delta content** — secrets present in the transcript
222
+ can reach the advisor model; mitigate by configuring a trusted reviewer model.
223
+ - **No quarantine of unsafe advisor output** — a misbehaving note can carry
224
+ directive text; the JSON frame + validation + advisory-only framing
225
+ (`[advisor:…]`, "weigh, don't blindly obey") are the only mitigation, and the
226
+ note is delivered as-is into the primary transcript (roadmap).
227
+ - **No `syncBacklog` catch-up wait** — a far-behind advisor does not wait for
228
+ the primary loop; its backlog is bounded and dropped (never parks the
229
+ primary), so advisor notes may arrive after the next primary turn started
230
+ (roadmap: context-maintenance batch).
231
+ - **Bounded advisor context** — long-session full replays are truncated
232
+ (`maxDeltaMessages`), so the advisor may lose early context after compaction;
233
+ advisor context maintenance is roadmap (next-next iteration).
234
+
235
+ ## Development
236
+
237
+ The bundle builds itself on install: `package.json` declares `"prepare": "node
238
+ scripts/setup-dsh-links.mjs && pnpm build"` (the dev-time link farm plus the
239
+ same build `prepack` runs), so any clone is
240
+ immediately buildable **once `DSH_HOME` points at a dsh home whose
241
+ `source/current` is a dsh source tree** (or `DSH_SOURCE_DIR` points at such a
242
+ tree directly). The private
243
+ `@deepseek-ai/dsh-*` runtime dependencies are **peerDependencies only**; at dev
244
+ time `scripts/setup-dsh-links.mjs` (wired into `prepare`, standalone as
245
+ `pnpm dsh:link`, verified with `pnpm dsh:link:check`) links the REAL packages
246
+ from that tree into `node_modules/@deepseek-ai/` — every `@deepseek-ai/*`
247
+ package the tree declares (tool CLIs with a `bin` are skipped: linking them
248
+ would make pnpm write their bins into the shared tree), a bin-less shim for
249
+ the in-box `cordis` framework, and the tree's own `react`/`react-dom` copies
250
+ (node resolution — including externalized CJS deps — must see ONE react
251
+ identity, the identity the real client packages use; the dsh profile
252
+ convention `nodeLinker=hoisted` lives in `pnpm-workspace.yaml` (pnpm 11+
253
+ ignores non-auth settings in `.npmrc`), so no `.pnpm` per-package dirs shadow
254
+ those links). The farm is idempotent, prunes stale entries, and fails with
255
+ guidance when the tree is missing or a peer cannot be linked.
256
+ `pnpm-workspace.yaml` also sets `autoInstallPeers: false` (dsh profile
257
+ convention): the private peers must never be fetched from the npm registry.
258
+
259
+ ```sh
260
+ export DSH_HOME=~/.dsh # a dsh home with source/current (or set DSH_SOURCE_DIR)
261
+ pnpm install # registry deps + link farm (via prepare), no private-registry access
262
+ pnpm test # vitest (unit + the composed integration loop)
263
+ pnpm typecheck # tsc --noEmit (node) + tsc -p tsconfig.client.json --noEmit + tsc -p tsconfig.spec.json --noEmit
264
+ pnpm build # tsc -p tsconfig.build.json emit to lib/ + node scripts/build-client.mjs (client bundle)
265
+ pnpm pack # build + produce dsh-advisor-0.0.1.tgz
266
+ ```
267
+
268
+ On Windows the link farm creates directory entries as junctions (no special
269
+ privileges), but the cordis shim's file entries use file symlinks, which need
270
+ [Developer Mode](https://learn.microsoft.com/windows/apps/get-started/enable-your-device-for-development)
271
+ (or an admin shell) — enable it before `pnpm install`. Windows has no
272
+ `HOME`, so the script falls back to `USERPROFILE` to resolve the dsh source
273
+ tree.
274
+
275
+ The in-box `cordis` framework is declared as the scoped peer
276
+ `@deepseek-ai/cordis: ^4.0.1-rc.1` (the range carries the exact publish tag —
277
+ a comparator prerelease such as `^4.0.0-rc.7` never matches the vendored
278
+ `4.0.1-rc.1` per the node-semver tuple rule); after install the link farm's
279
+ bin-less cordis shim at `node_modules/@deepseek-ai/cordis` answers the scoped
280
+ name and resolves to the vendored files, because the real packages type and
281
+ run against the vendored build and module identity requires dev-time
282
+ `import '@deepseek-ai/cordis'` to resolve to the same files. The public
283
+ devDependencies (`schemastery`, `react`, …) resolve from the npm registry as
284
+ usual.
285
+
286
+ `prepack` runs `pnpm build`; `prepare` runs the link farm and the build, so
287
+ `pnpm pack` runs the build twice (once per lifecycle) — the documented
288
+ tradeoff that keeps git-install builds working. There is no `postinstall`
289
+ step: already-built tarball installs skip the build entirely.
290
+
291
+ The integration test (`tests/integration.test.ts`) composes the plugin into a
292
+ real cordis context with a stub LLM adapter and drives the full
293
+ turn → delta → advisor call → inject/steer cycle.
294
+
295
+ ## Documentation
296
+
297
+ | Doc | Content |
298
+ |---|---|
299
+ | [docs/install.md](docs/install.md) | full install guide: git / tarball / local-directory install, web Settings exposure, uninstall, `--dump-config` verification |
300
+
301
+ ## License
302
+
303
+ MIT
package/README.zh.md ADDED
@@ -0,0 +1,166 @@
1
+ # dsh-advisor
2
+
3
+ [English](README.md) | 中文
4
+
5
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+ ![node](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933.svg)
7
+ ![dsh](https://img.shields.io/badge/dsh-DeepSeek%20Harness%20compatible-4B32C3.svg)
8
+
9
+ 一个移植 omp「advisor」子系统的独立 dsh 插件组合包:一个按会话运行的评审模型,观察主会话 transcript,用显式配置的模型(provider 与 model 均为必填)评审每个已完成的 stepped turn,并把按严重度排序的建议(nit / concern / blocker)注入回会话 —— 不污染主循环,也不递归地评审自己。
10
+
11
+ 一条命令即可安装(pnpm ≥ 10 需要一次构建放行步骤 —— 见[安装](#安装)):
12
+
13
+ ```sh
14
+ dsh plugin --profile web add github:btspoony/dsh-advisor # <name> = 你的 profile 名;用 #<sha> 钉住 commit
15
+ ```
16
+
17
+ **仅作建议。** advisor 从不批准或否决主 agent 的动作,也绝不会像主 agent 那样发出命令。每条送达的消息都是自我描述的 advisory 内容;一个行为异常的评审者会被端到端约束(emission guard、immuneTurns 冷却、failure policy),因此它永远不会卡住或污染主循环。
18
+
19
+ ## 安装
20
+
21
+ ### 一条命令的 git 安装
22
+
23
+ ```sh
24
+ dsh plugin --profile web add github:btspoony/dsh-advisor # <name> = 你的 profile 名;用 #<sha> 钉住 commit
25
+ ```
26
+
27
+ git 安装拉取的是**源码而非构建产物**,因此组合包会在安装时自行构建(`prepare` 自建)。pnpm ≥ 10 默认拦截 git 依赖的 `prepare`:第一次 `add` 会报 `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`,pnpm 会打印出确切的包 key —— 在 profile 的 `pnpm-workspace.yaml` 中放行构建(`onlyBuiltDependencies: [dsh-advisor]`,或运行 `dsh plugin --profile web approve-builds`),然后重新执行 `add`。请把这次放行当作它本来的样子:允许该包的代码在安装时于你的机器上执行;并钉住 commit(`#<sha>`),这样之后的 push 无法悄悄改变实际运行的代码。
28
+
29
+ ### 本地目录安装(推荐用于开发 / 验证)
30
+
31
+ ```sh
32
+ pnpm install # 构建组合包(prepare 自建)
33
+ dsh plugin --profile web add . # <name> = 你的 profile 名
34
+ ```
35
+
36
+ ### 验证
37
+
38
+ ```sh
39
+ dsh --profile web --dump-config # 显示带 advisor 配置行的 "# == dsh-advisor" 层
40
+ dsh --profile web
41
+ ```
42
+
43
+ tarball 安装与卸载见 [docs/install.zh.md](docs/install.zh.md)。
44
+
45
+ ## 配置
46
+
47
+ ![dsh web Settings("插件配置")页上的 Advisor 卡片](docs/screenshots/advisor-settings-card.webp)
48
+
49
+ advisor 默认关闭。启用后,`provider` 与 `model` 为**必填**:`enabled: true` 而缺少两者之一是一个硬门禁 —— advisor 不会发起任何模型调用,并报告带原因的禁用状态(disabled-with-reason)。未知配置键会被拒绝。
50
+
51
+ 配置在**三个配置面**之间合成(后一层覆盖前一层;各处使用同一组键):
52
+
53
+ 1. **插件行 config** —— `$DSH_HOME/profiles/web/cordis.patch.yml`(见下)。这是合成 base。
54
+ 2. **dsh web Settings 页 —— "插件配置"页** —— Advisor **卡片**(id `advisor`,渲染在三张上游卡片 bash / agent-loop / web-search 之后),含 enabled 开关、只列出系统内已配置 provider 及其模型的 provider/model 选择框与可选字段。保存写入 `advisor` settings namespace,覆盖插件行 config 而无需改动它。保存后新会话立即生效,无需重启(运行时 live 读取合成值)。需要当前版本的 dsh web 构建(其 web shell 声明了 `settings.plugin.item` 卡片 slot 并能加载 `dsh.client` 声明包)。卡片通过**官方 `GatewayService` RPC 通道**读写该命名空间(`/api/advisor/get` + `/api/advisor/set`,由宿主的 typertGateway claims——与 dsh 内建 `goals` 服务同一机制),该通道**不受 settings 暴露白名单门控**:进程内写入(`ctx.settings.update`)没有 exposed-namespace 检查。无需也不施加任何宿主补丁。
55
+ 3. **`/advisor` 指令** —— 按会话且临时:翻转的是会话级 override,从不修改持久化配置(见[用法](#用法))。
56
+
57
+ 两个持久化配置面共享同一个硬门禁:`enabled: true` 而 `provider`/`model` 为空时绝不发起模型调用(disabled-with-reason)。Settings 页还会在 enabled 且必填字段为空时阻止保存;宿主侧硬门禁始终是所有路径上的最后防线。
58
+
59
+ 插件行配置:
60
+
61
+ ```yaml
62
+ # profiles/web/cordis.patch.yml — the profile's user patch layer
63
+ - id: advisor
64
+ config:
65
+ enabled: true # master switch (default false)
66
+ provider: deepseek-official # REQUIRED when enabled
67
+ model: deepseek-v4-flash # REQUIRED when enabled
68
+ systemPrompt: "" # optional; "" = built-in reviewer prompt
69
+ immuneTurns: 3 # int ≥ 0, default 3 — cooldown after a delivered interrupt
70
+ maxDeltaMessages: 60 # int ≥ 0, default 60 — delta window; 0 = unbounded
71
+ ```
72
+
73
+ | 键 | 类型 / 默认值 | 含义 |
74
+ |---|---|---|
75
+ | `enabled` | bool, `false` | 总开关。 |
76
+ | `provider` | string, optional | 供应商路由。`enabled: true` 时必须(非空)。 |
77
+ | `model` | string, optional | 模型 id。`enabled: true` 时必须(非空)。 |
78
+ | `systemPrompt` | string, `""` | 覆盖内置评审 prompt(严重度定义 + JSON-frame 输出契约)。 |
79
+ | `immuneTurns` | int ≥ 0, `3` | 实际 steer 过一次 concern/blocker 后,接下来 N 个完成的 stepped 主 turn 必须走完,另一条打断性 note 才可再次 steer;窗口内的 note 降级为 inject。 |
80
+ | `maxDeltaMessages` | int ≥ 0, `60` | 有界的 advisor 输入窗口。超过 N 的 delta 以 `… <earlier messages omitted>` 标记截断;`0` = 无上限。 |
81
+
82
+ **模型能力与预算**:advisor 调用以 `reasoningEffort: 'off'` 运行 —— 仅当所配置模型的 adapter 声明该档位时才发送(deepseek 模型声明;其他模型会自动省略该选项,因此非推理供应商照常工作)—— 并以 **5120 tokens** 作为输出上限(用户指示的 256 → 5120 的 20 倍超驰)。抽取出的 note 有界(1000 字符),notice summary 有界(120 字符),因此提高的预算不会变成注入主会话的无界内容。
83
+
84
+ ## 用法
85
+
86
+ 安装并启用后,advisor 观察每个会话。用 `/advisor` 指令按会话控制它(组合了 command registry 时可用):
87
+
88
+ ```
89
+ /advisor toggle the advisor for this session
90
+ /advisor on enable the advisor for this session
91
+ /advisor off disable the advisor for this session
92
+ /advisor status show state, model, runtime status, pending count, last activity
93
+ ```
94
+
95
+ `/advisor on|off|toggle` 是会话级且临时的:它们翻转的是按会话的 override,从不修改持久化配置。启用一个 config 缺少 `provider`/`model` 的会话不会发起模型调用 —— `/advisor status`(以及 `/advisor on` 的回复)会显示门禁原因。
96
+
97
+ `/advisor on` 也是手动恢复路径:被 quota/rate-limit 暂停的会话 advisor(`quota_exhausted` —— KD-5 没有自动恢复定时器)会在原地恢复;被终止的 advisor(永久性模型错误,如凭据无效)会为该会话全新重建。
98
+
99
+ advisor 采用双模式触发,取决于会话形态:
100
+
101
+ - **标准 stepped 会话** —— 在每个正常结束(`completed`、`max-tokens` 或 `error`)的 stepped 主 turn 之后,评审增量 transcript delta。
102
+ - **agentic / harness 会话**(从不发出 `turn/end`)—— 在每个完成的 agent 回复轮次之后:当新的用户输入(含 inbox 拼接输入)在未评审的 assistant 增量之后到达时,评审该增量。
103
+
104
+ 无论哪种模式,每次评审至多发出一条 note,按严重度排序:
105
+
106
+ - **nit** —— 轻微的样式、清晰度或质量建议;通过 `agent.inject` 送达(非唤醒,在下一个 pre-step 边界消费)。
107
+ - **concern** —— 在继续之前值得权衡的重大风险或明显更优的方向;通过 `agent.steer` 送达(唤醒),受 `immuneTurns` 冷却约束。
108
+ - **blocker** —— 继续下去明显是在浪费工作(与显式用户指令矛盾、原地打转、根本性不可行);通过 `agent.steer` 送达。
109
+
110
+ 注入的建议以 user-role 消息出现在会话流中,携带 advisor source kind 与自我描述的内容,例如:
111
+
112
+ ```
113
+ [advisor:concern] extract the helper into a module and unit-test it
114
+ ```
115
+
116
+ `[advisor:{severity}]` 前缀是主模型获得的关于如何对待它的唯一线索 —— 主 system prompt 从不提及 advisory。advisor 消息会被排除在此后的 advisor delta 之外,因此 advisor 永远不会读回自己的建议。
117
+
118
+ ![注入到会话流中的 advisor 建议](docs/screenshots/advisor-injected-note.webp)
119
+
120
+ ## 工作原理
121
+
122
+ 插件订阅 `session/event`。两种触发方式会把主 transcript 的增量 markdown delta(排除 advisor 自己的消息)渲染出来并放入按会话的 runtime 队列:标准 stepped 会话在每个 stepped `turn/end` 之后;agentic/harness 会话(从不发出 `turn/end`)则在新的用户输入(含 inbox 拼接输入)于未评审的 assistant 增量之后到达时 —— 即每个完成的 agent 回复轮次。runtime 通过 `ctx.llm.stream` 调用一个单独配置的模型,从 JSON-framed 回复中提取一条 `{note, severity}`,经过 emission guard 门禁(normalize / dedupe / content-free 抑制 / 每次更新至多一条 note),然后路由:nit → inject,concern/blocker → steer。advisor 调用以关闭推理(reasoning off)和 20 倍 token 预算运行,因此 JSON note 绝不会被推理输出挤占。compaction 与 surface 重写会重置 observer、emission guard 与 immuneTurns latch(KD-5);drain 完全异步且 backlog 有界,因此失败或 quota 耗尽的 advisor 只能丢弃自己的 backlog —— 永远不会卡住主循环。
123
+
124
+ ## 限制与路线图
125
+
126
+ MVP 有意放弃与 omp 的完整对等。已接受的差距(在 harness 迭代路线图中跟踪):
127
+
128
+ - **每个会话一个 advisor** —— 无并行 advisor roster 或 WATCHDOG 式文件发现(下一迭代)。
129
+ - **无 advisor tools** —— 评审者只是一个独立的模型调用;它无法自行核验主张(下下迭代)。
130
+ - **无会话内 advisor 面板** —— 建议仅以带标签的注入消息呈现("插件配置"设置页上的 Advisor 卡片是配置面,不是会话内视图;会话内卡片为下下迭代)。
131
+ - **无 transcript 持久化或成本统计** —— 无可恢复的 advisor 历史或成本可观测性(下下迭代)。
132
+ - **无 delta 内容密钥混淆** —— transcript 中出现的 secrets 可能到达 advisor 模型;请通过配置可信的评审模型来缓解。
133
+ - **不隔离不安全的 advisor 输出** —— 行为异常的 note 可能携带指令性文本;JSON frame + 校验 + advisory-only 框架(`[advisor:…]`、"weigh, don't blindly obey")是仅有的缓解手段,且 note 会原样送达主 transcript(路线图)。
134
+ - **无 `syncBacklog` 追赶等待** —— 落后很多的 advisor 不会等待主循环;其 backlog 有界且会被丢弃(永远不会卡住主循环),因此 advisor note 可能在下一次主 turn 开始之后才到达(路线图:context-maintenance batch)。
135
+ - **advisor 上下文有界** —— 长会话的完整重放会被截断(`maxDeltaMessages`),因此 compaction 后 advisor 可能丢失早期上下文;advisor 上下文维护在路线图中(下下迭代)。
136
+
137
+ ## 开发
138
+
139
+ 组合包在安装时自行构建:`package.json` 声明了 `"prepare": "node scripts/setup-dsh-links.mjs && pnpm build"`(开发期链接农场、与 `prepack` 相同的构建),因此任何克隆在 **`DSH_HOME` 指向一个含 `source/current` 的 dsh home(或 `DSH_SOURCE_DIR` 直接指向一个 dsh 源码树)** 后立即可构建。私有的 `@deepseek-ai/dsh-*` 运行时依赖**只声明为 peerDependencies**;开发期由 `scripts/setup-dsh-links.mjs`(挂在 `prepare` 上、独立命令为 `pnpm dsh:link`、用 `pnpm dsh:link:check` 校验)把该树里的**真实包**链接进 `node_modules/@deepseek-ai/` —— 树声明的每个 `@deepseek-ai/*` 包(声明 `bin` 的工具 CLI 会被跳过:链接它们会让 pnpm 向共享树写入 bin)、无 bin 的内置 `cordis` 框架 shim、以及树自带的 `react`/`react-dom` 副本(node 解析 —— 包括外部化的 CJS 依赖 —— 必须看到同一个 react 身份,即真实 client 包所用的身份;dsh profile 约定 `nodeLinker=hoisted` 放在 `pnpm-workspace.yaml`(pnpm 11+ 忽略 `.npmrc` 中的非认证设置),避免 `.pnpm` 逐包目录遮蔽这些链接)。农场幂等、会清理陈旧条目,并在树缺失或 peer 无法链接时给出明确指引。`pnpm-workspace.yaml` 还设了 `autoInstallPeers: false`(dsh profile 约定):私有 peer 绝不能从 npm registry 获取。
140
+
141
+ ```sh
142
+ export DSH_HOME=~/.dsh # 含 source/current 的 dsh home(或直接设置 DSH_SOURCE_DIR)
143
+ pnpm install # registry deps + 链接农场(经 prepare),无需访问私有 registry
144
+ pnpm test # vitest (unit + the composed integration loop)
145
+ pnpm typecheck # tsc --noEmit (node) + tsc -p tsconfig.client.json --noEmit + tsc -p tsconfig.spec.json --noEmit
146
+ pnpm build # tsc -p tsconfig.build.json emit to lib/ + node scripts/build-client.mjs (client bundle)
147
+ pnpm pack # build + produce dsh-advisor-0.0.1.tgz
148
+ ```
149
+
150
+ Windows 上链接农场的目录条目以 junction 创建(无需特权),但 cordis shim 的文件条目使用文件符号链接,需要开启[开发者模式](https://learn.microsoft.com/windows/apps/get-started/enable-your-device-for-development)(或以管理员 shell 运行)——请先开启再执行 `pnpm install`。Windows 没有 `HOME`,脚本回退到 `USERPROFILE` 解析 dsh 源码树。
151
+
152
+ 内置 `cordis` 框架声明为 scoped peer `@deepseek-ai/cordis: ^4.0.1-rc.1`(范围必须带精确的发布 tag —— 带 prerelease 的 comparator 只匹配同 `[major, minor, patch]` tuple,`^4.0.0-rc.7` 永远不匹配 vendored 的 `4.0.1-rc.1`);安装后链接农场的无 bin cordis shim 位于 `node_modules/@deepseek-ai/cordis`,以 scoped 名应答并解析到 vendored 文件,因为真实包是对着 vendored 构建类型化/运行的,模块身份要求开发期的 `import '@deepseek-ai/cordis'` 解析到同一份文件。其余公开 devDependencies(`schemastery`、`react` 等)照常从 npm registry 解析。
153
+
154
+ `prepack` 运行 `pnpm build`;`prepare` 运行链接农场与构建,因此 `pnpm pack` 会构建两次(每个生命周期一次)——这是为保持 git 安装可构建而接受的取舍。没有 `postinstall` 步骤:tarball 安装已带构建产物,完全跳过构建。
155
+
156
+ 集成测试(`tests/integration.test.ts`)把插件组合进一个带 stub LLM adapter 的真实 cordis 上下文,驱动完整的 turn → delta → advisor call → inject/steer 循环。
157
+
158
+ ## 文档
159
+
160
+ | 文档 | 内容 |
161
+ |---|---|
162
+ | [docs/install.zh.md](docs/install.zh.md) | 完整安装指南:git / tarball / 本地目录安装、web Settings 暴露、卸载、`--dump-config` 验证 |
163
+
164
+ ## 许可证
165
+
166
+ MIT
@@ -0,0 +1,6 @@
1
+ # The dsh-advisor bundle patch: inserts the advisor plugin row over the
2
+ # profile root. Per-row config (enabled/provider/model/...) lands in the
3
+ # user's profile cordis.patch.yml under id: advisor (T2 schema).
4
+ - insert:
5
+ - id: advisor
6
+ name: dsh-advisor