dsh-plugin-tool-management 0.8.0 → 0.9.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 (60) hide show
  1. package/CHANGELOG.md +67 -1
  2. package/README.md +53 -48
  3. package/README_EN.md +47 -44
  4. package/docs/images/1-EN.png +0 -0
  5. package/docs/images/1.png +0 -0
  6. package/docs/images/2-EN.png +0 -0
  7. package/docs/images/2.png +0 -0
  8. package/docs/images/3-EN.png +0 -0
  9. package/docs/images/3.png +0 -0
  10. package/docs/images/4-EN.png +0 -0
  11. package/docs/images/4.png +0 -0
  12. package/docs/images/5-EN.png +0 -0
  13. package/docs/images/5.png +0 -0
  14. package/docs/images/6-EN.png +0 -0
  15. package/docs/images/6.png +0 -0
  16. package/docs/images/7-EN.png +0 -0
  17. package/docs/images/7.png +0 -0
  18. package/docs/images/8-EN.png +0 -0
  19. package/docs/images/8.png +0 -0
  20. package/docs/update.md +39 -0
  21. package/lib/agents-md/preset-id.js +1 -1
  22. package/lib/agents-md/service.js +46 -9
  23. package/lib/approval-policy.js +1 -1
  24. package/lib/client.js +431 -135
  25. package/lib/compat/preset-reach.js +155 -93
  26. package/lib/compat/probe.js +6 -4
  27. package/lib/context-inject.js +304 -0
  28. package/lib/hub.js +155 -13
  29. package/lib/imports/upload.js +34 -14
  30. package/lib/index.js +870 -226
  31. package/lib/mcp/state-section.js +46 -15
  32. package/lib/rules/archive-engine.js +182 -57
  33. package/lib/rules/archive.js +57 -1
  34. package/lib/rules/service.js +386 -156
  35. package/lib/scene-prompt-sync.js +101 -12
  36. package/lib/skills/catalog.js +125 -0
  37. package/lib/skills/core.js +38 -4
  38. package/lib/skills/service.js +36 -10
  39. package/lib/subagents/catalog.js +15 -5
  40. package/lib/subagents/service.js +39 -28
  41. package/lib/subagents/tools.js +18 -13
  42. package/package.json +14 -9
  43. package/screenshots.json +8 -8
  44. package/docs/images/1/345/234/272/346/231/257.png +0 -0
  45. package/docs/images/1/345/234/272/346/231/257_en.png +0 -0
  46. package/docs/images/2MCP.png +0 -0
  47. package/docs/images/2MCP_en.png +0 -0
  48. package/docs/images/3/346/212/200/350/203/275.png +0 -0
  49. package/docs/images/3/346/212/200/350/203/275_en.png +0 -0
  50. package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223.png +0 -0
  51. package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223_en.png +0 -0
  52. package/docs/images/5/346/217/220/347/244/272/350/257/215.png +0 -0
  53. package/docs/images/5/346/217/220/347/244/272/350/257/215_en.png +0 -0
  54. package/docs/images/6/350/256/260/345/277/206.png +0 -0
  55. package/docs/images/6/350/256/260/345/277/206_en.png +0 -0
  56. package/docs/images/7/344/274/232/350/257/235.png +0 -0
  57. package/docs/images/7/344/274/232/350/257/235_en.png +0 -0
  58. package/docs/images/8/345/205/274/345/256/271.png +0 -0
  59. package/docs/images/8/345/205/274/345/256/271_en.png +0 -0
  60. package/lib/prompt-sections.js +0 -148
@@ -5,68 +5,90 @@
5
5
  *
6
6
  * WHY THIS EXISTS
7
7
  * ---------------
8
- * `prompt-sections.ts` registers a per-agent `systemPrompt` section, and the
9
- * registration SUCCEEDS under every preset. But `@deepseek-ai/dsh-persona`
10
- * with `complete: true` makes the prompt registry restore its prefix as the
11
- * ONLY section at assembly time, so the section never reaches the model — and
12
- * the plugin had no way to notice. The memories page kept reporting
13
- * "injected" while the model saw nothing.
8
+ * The plugin TELLS the model four things (scene memories, the subagent roster,
9
+ * the MCP server list with the user's notes, and the scene prompt) by injecting
10
+ * a synthetic message at `agent/pre-step` (see `src/context-inject.ts`). A preset
11
+ * can still decide it wants none of that: `@deepseek-ai/dsh-persona` with
12
+ * `complete: true` declares its prompt the only one, and `includeRuntimeContext:
13
+ * false` switches the official runtime-context snapshot off — the shipped
14
+ * `minimal` preset sets both. The plugin then injects nothing by default (the
15
+ * user can force it on the compat page), and no surface may claim otherwise.
16
+ *
17
+ * A second, different question is whether the OFFICIAL carriers are mounted at
18
+ * all: `@deepseek-ai/dsh-agent-instructions` (AGENTS.md) and
19
+ * `@deepseek-ai/dsh-tool-skill` (the skill catalog) are host rows that the Web
20
+ * bundle disables in favour of per-preset rows — a preset that mounts neither
21
+ * gives the model no AGENTS.md text and no skill catalog, whichever persona it
22
+ * declares.
14
23
  *
15
24
  * WHAT IS MEASURED, AND HOW
16
25
  * -------------------------
17
26
  * Answers come from each preset's own composition text, read through the
18
27
  * roster's public `read(id)` — evidence, not inference, and no mount:
19
28
  *
20
- * - `personaComplete`: does the preset mount `@deepseek-ai/dsh-persona`
21
- * with `complete: true`? (that flag suppresses EVERY other prompt section)
29
+ * - `personaComplete` / `includeRuntimeContext`: does the preset declare
30
+ * "nothing but my prompt"? (both suppress plugin-injected text)
22
31
  * - `agentInstructions`: is `@deepseek-ai/dsh-agent-instructions` mounted?
23
- * (the row that carries `~/.dsh/AGENTS.md` into the prompt)
32
+ * (the row that carries `~/.dsh/AGENTS.md` into the conversation)
24
33
  * - `toolSkill`: is `@deepseek-ai/dsh-tool-skill` mounted?
25
34
  * (the row that gives the model the skill catalog)
26
- * - `subagentTool`: is `@deepseek-ai/dsh-tool-subagent` mounted?
27
- * (the official delegation tool; the shipped `minimal` preset has none)
28
35
  * - `mcpClient`: does the composition mount its own MCP client?
29
- * (the shipped presets mount none, so MCP comes from the host plane and is
30
- * equally available under every preset — see `ReachContext.mcpTools`)
36
+ * (the shipped presets mount none, so MCP *tools* come from the host plane
37
+ * and are callable under every preset — see `ReachContext.mcpTools`)
38
+ *
39
+ * TOOLS ARE NOT TEXT — the distinction is the whole point:
31
40
  *
32
- * The plugin's own 14 model tools need NO probe: they register in the HOST
33
- * plane (this plugin's loader row in the profile patch), so every preset's
34
- * session resolves them. Verified against the shipped `minimal` preset on
35
- * 2026-09-14: tools callable, skill catalog absent, memory and AGENTS.md
36
- * injection suppressed.
41
+ * - TOOL plane (host, preset-independent): the plugin's own model tools and
42
+ * every `mcp__*` tool register in the HOST plane (this plugin's loader row in
43
+ * the profile patch), so every preset's session resolves them.
44
+ * - TEXT plane (preset-dependent): the runtime snapshot the plugin injects, the
45
+ * AGENTS.md carrier, and the skill catalog. Callable is not the same as
46
+ * known: under `minimal` the model can call every `mcp__*` tool while knowing
47
+ * nothing about the servers behind them — no names, no tool counts, no
48
+ * enablement, and none of the user's notes.
37
49
  *
38
50
  * Reading never mounts. `list()`/`read(id)` are roster reads, so building the
39
51
  * matrix cannot activate a preset early — the same guarantee
40
52
  * `compositionInventory()` gives the plugin-listing surfaces.
41
53
  *
42
54
  * This module is deliberately advisory: it never changes what is injected and
43
- * never refuses an operation. A preset that suppresses prompt text is doing
44
- * exactly what its author asked for; the plugin's job is to say so out loud
45
- * instead of reporting a success the model cannot observe.
55
+ * never refuses an operation. A preset that asks for no extra text is doing
56
+ * exactly what its author asked for; the plugin's job is to say so out loud and
57
+ * point at the switch that overrides it.
46
58
  */
59
+ /** Whether a preset declares "no extra text": persona `complete: true` or runtime context off. */
60
+ export function isSuppressingPreset(facts) {
61
+ return facts.personaComplete === true || facts.includeRuntimeContext === false;
62
+ }
63
+ /** Map composition facts onto the injection decision's facts (parse failure → not suppressing). */
64
+ export function injectionFactsOf(facts) {
65
+ if (facts === undefined)
66
+ return undefined;
67
+ return {
68
+ suppressing: isSuppressingPreset(facts),
69
+ carriesAgentsMd: facts.agentInstructions === 'mounted',
70
+ carriesSkillCatalog: facts.toolSkill === 'mounted',
71
+ };
72
+ }
47
73
  /** Module specifiers whose mounting decides reachability. */
48
74
  const PERSONA_MODULE = '@deepseek-ai/dsh-persona';
49
75
  const AGENT_INSTRUCTIONS_MODULE = '@deepseek-ai/dsh-agent-instructions';
50
76
  const TOOL_SKILL_MODULE = '@deepseek-ai/dsh-tool-skill';
51
- /**
52
- * The OFFICIAL delegation tool. Absent in the shipped `minimal` preset, present
53
- * in the other three — the one column whose answer really does differ per
54
- * preset. (This plugin's own `subagent_list`/`subagent_run` are host-plane and
55
- * therefore preset-independent; the page's footer says so.)
56
- */
57
- const SUBAGENT_TOOL_MODULE = '@deepseek-ai/dsh-tool-subagent';
58
77
  /**
59
78
  * An MCP client mounted INSIDE a composition. The shipped presets mount none,
60
- * so MCP normally comes from the host plane (the `$DSH_HOME/cordis.patch.yml`
61
- * layer) and works under every preset; a user-authored preset that mounts its
62
- * own client only carries MCP under itself, and this scan is how the page can
63
- * tell the two apart.
79
+ * so MCP tools normally come from the host plane (the `$DSH_HOME/cordis.patch.yml`
80
+ * layer) and are callable under every preset; a user-authored preset that mounts
81
+ * its own client only carries those tools under itself, and this scan is how the
82
+ * page can tell the two apart. Tool reachability is not the column's answer,
83
+ * though — the server list and the user's notes are a prompt section, which a
84
+ * complete persona suppresses whichever client serves the tools.
64
85
  */
65
86
  const MCP_CLIENT_MODULE = '@deepseek-ai/dsh-mcp-client';
66
87
  const NAME_LINE = /^(\s*)name:\s*(['"]?)([^'"\s#]+)\2\s*(?:#.*)?$/;
67
88
  const ENTRY_LINE = /^(\s*)-\s/;
68
89
  const DISABLED_LINE = /^\s*disabled:\s*(.+?)\s*(?:#.*)?$/;
69
90
  const COMPLETE_LINE = /^\s*complete:\s*(true|false)\s*(?:#.*)?$/;
91
+ const RUNTIME_CONTEXT_LINE = /^\s*includeRuntimeContext:\s*(true|false)\s*(?:#.*)?$/;
70
92
  /** Leading whitespace width of a line, used as its nesting level. */
71
93
  function indentOf(line) {
72
94
  const match = /^(\s*)/.exec(line);
@@ -153,15 +175,18 @@ export function readCompositionFacts(text) {
153
175
  if (lines.length <= 1 && lines[0] === '') {
154
176
  return {
155
177
  personaComplete: 'unknown',
178
+ includeRuntimeContext: 'unknown',
156
179
  personaMounted: false,
157
180
  agentInstructions: 'absent',
158
181
  toolSkill: 'absent',
159
- subagentTool: 'absent',
160
182
  mcpClient: 'absent',
161
183
  parseFailure: '组合文件为空',
162
184
  };
163
185
  }
164
186
  let personaComplete = false;
187
+ // Schema default is `true`: a mounted persona row that does not state the flag keeps
188
+ // the official runtime context (only `false` suppresses it).
189
+ let includeRuntimeContext = true;
165
190
  let personaMounted = false;
166
191
  for (let i = 0; i < lines.length; i += 1) {
167
192
  const named = NAME_LINE.exec(lines[i]);
@@ -173,29 +198,30 @@ export function readCompositionFacts(text) {
173
198
  personaMounted = true;
174
199
  for (const line of rowBlockAround(lines, i)) {
175
200
  const complete = COMPLETE_LINE.exec(line);
176
- if (complete !== null) {
201
+ if (complete !== null)
177
202
  personaComplete = complete[1] === 'true';
178
- break;
179
- }
203
+ const runtime = RUNTIME_CONTEXT_LINE.exec(line);
204
+ if (runtime !== null)
205
+ includeRuntimeContext = runtime[1] === 'true';
180
206
  }
181
207
  break;
182
208
  }
183
209
  return {
184
210
  personaComplete,
211
+ includeRuntimeContext,
185
212
  personaMounted,
186
213
  agentInstructions: presenceOf(lines, AGENT_INSTRUCTIONS_MODULE),
187
214
  toolSkill: presenceOf(lines, TOOL_SKILL_MODULE),
188
- subagentTool: presenceOf(lines, SUBAGENT_TOOL_MODULE),
189
215
  mcpClient: presenceOf(lines, MCP_CLIENT_MODULE),
190
216
  };
191
217
  }
192
218
  catch (error) {
193
219
  return {
194
220
  personaComplete: 'unknown',
221
+ includeRuntimeContext: 'unknown',
195
222
  personaMounted: false,
196
223
  agentInstructions: 'absent',
197
224
  toolSkill: 'absent',
198
- subagentTool: 'absent',
199
225
  mcpClient: 'absent',
200
226
  parseFailure: String(error?.message ?? error),
201
227
  };
@@ -203,48 +229,61 @@ export function readCompositionFacts(text) {
203
229
  }
204
230
  /** Map parsed facts onto the five user-visible capability columns. */
205
231
  export function deriveReach(facts, ctx) {
206
- const suppressed = facts.personaComplete === true;
232
+ const suppressed = isSuppressingPreset(facts);
207
233
  const personaUnknown = facts.personaComplete === 'unknown';
208
- let memory = suppressed ? 'suppressed' : personaUnknown ? 'unknown' : 'ok';
209
- let agentsMd;
210
- if (suppressed)
211
- agentsMd = 'suppressed';
212
- else if (personaUnknown)
213
- agentsMd = 'unknown';
214
- else if (facts.agentInstructions === 'absent')
215
- agentsMd = 'absent';
216
- else if (facts.agentInstructions === 'conditional')
217
- agentsMd = 'unknown';
218
- else
219
- agentsMd = 'ok';
220
- let skillCatalog;
221
- if (facts.toolSkill === 'mounted')
222
- skillCatalog = 'ok';
223
- else if (facts.toolSkill === 'conditional')
224
- skillCatalog = 'unknown';
225
- else
226
- skillCatalog = 'absent';
227
- // The official delegation tool is a plain mounting fact: it is a model-facing
228
- // tool row, so nothing the persona does can hide it.
229
- let subagent;
230
- if (facts.subagentTool === 'mounted')
231
- subagent = 'ok';
232
- else if (facts.subagentTool === 'conditional')
233
- subagent = 'unknown';
234
- else
235
- subagent = 'absent';
236
- // MCP: a client mounted in THIS composition only serves this preset; otherwise
237
- // the answer is the host plane's, which every preset shares. An unknown host
238
- // count is reported as `unknown` rather than assumed to be fine.
239
- let mcp;
240
- if (facts.mcpClient === 'mounted')
241
- mcp = 'ok';
242
- else if (facts.mcpClient === 'conditional')
243
- mcp = 'unknown';
244
- else if (ctx === undefined || ctx.mcpTools === undefined)
245
- mcp = 'unknown';
246
- else
247
- mcp = ctx.mcpTools > 0 ? 'ok' : 'absent';
234
+ const forceUnderSuppressing = ctx?.inject?.underSuppressingPresets === true;
235
+ const domainOff = (key) => ctx?.inject?.domains?.[key] === false;
236
+ /**
237
+ * 本插件自己的文本(场景和记忆 / MCP 服务器与备注 / 技能目录 / 子智能体目录 / 提示词)
238
+ * 走 `agent/pre-step` 注入消息(src/context-inject.ts)—— 不再依赖系统提示词段,所以
239
+ * `persona complete` 压不到它。可达性只看两件事:域开关有没有关、预设压制时有没有开
240
+ * 「仍然注入」。预设信息读不到(`personaUnknown` 且无压制信号)时按可达处理。
241
+ */
242
+ const pluginText = (key) => {
243
+ if (domainOff(key))
244
+ return 'absent';
245
+ if (suppressed && !forceUnderSuppressing)
246
+ return 'suppressed';
247
+ return 'ok';
248
+ };
249
+ const memory = pluginText('memory');
250
+ const mcp = pluginText('mcp');
251
+ /**
252
+ * 官方两条行承载的能力(提示词 / 技能目录):挂得上就官方送(它也走 pre-step 注入消息,
253
+ * persona `complete` 压不到),挂不上就由本插件的同名域兜底 —— 所以"这一行没挂"不等于
254
+ * "模型看不到",只有兜底域也关掉(或缺省 `跟随预设`)时才是。
255
+ */
256
+ const officialOrFallback = (domainKey, carrier) => {
257
+ // 官方那条行挂着 = **官方自己送到**,与本插件的域开关无关(开关只管本插件的兜底):
258
+ // 用户实测(2026-09-16)—— 把注入域全关掉后,标准 / ptc / 创造三行的技能与提示词
259
+ // 依然应该是绿的(模型确实拿得到,官方 `dsh-tool-skill` / `dsh-agent-instructions` 送),
260
+ // 只有极简那行是红的(两条行都没挂 + 兜底也关着)。
261
+ if (carrier === 'mounted')
262
+ return 'ok';
263
+ // 组合文本读不出来时既不能断言官方到得了,也不能断言本插件会兜底
264
+ // (注入侧对读不到的预设保守跳过兜底域)→ 如实报"判断不了"。
265
+ if (personaUnknown)
266
+ return 'unknown';
267
+ // 官方行"可能挂着"(有条件启用)同样判断不了:注入侧对本插件兜底域的门槛是"官方明确没挂"
268
+ // (`carrier !== false`),所以这种预设下兜底根本不会发,官方挂不挂只有挂载期才知道。
269
+ if (carrier === 'conditional')
270
+ return 'unknown';
271
+ if (domainOff(domainKey))
272
+ return 'absent';
273
+ if (suppressed && !forceUnderSuppressing)
274
+ return 'suppressed';
275
+ return 'ok';
276
+ };
277
+ const agentsMd = officialOrFallback('prompt', facts.agentInstructions);
278
+ const skillCatalog = officialOrFallback('skills', facts.toolSkill);
279
+ // 子智能体列(用户裁定 2026-09-16 第二版):与记忆 / MCP 两列**同口径** —— 答"人设目录
280
+ // 到不到得了模型"。内容由本插件的注入域送达,跑起来用本插件自己的 `subagent_manager_run`
281
+ // (宿主平面工具,任何预设下都在),所以可达性只看域开关与压制。此前这一列只答"官方
282
+ // `dsh-tool-subagent` 挂没挂"——于是极简下就算开了「极简模式也注入」也永远红着,而那条官方
283
+ // 工具只是另一种委派入口(它认的是通用子代理,不认本插件的人设目录),与人设可达无关。
284
+ const subagent = pluginText('subagents');
285
+ // MCP 列答的是"服务器清单与备注到不到得了":宿主工具数不再参与 —— 清单为空时注入出去
286
+ // 也是空段(没什么可看的),"有几台 server"由插件页面回答,不是这一列的事。
248
287
  return { memory, agentsMd, skillCatalog, subagent, mcp };
249
288
  }
250
289
  /** Narrow the roster off a cordis context without throwing. */
@@ -277,6 +316,7 @@ async function composeRow(roster, meta, ctx) {
277
316
  personaMounted: false,
278
317
  agentInstructions: 'absent',
279
318
  toolSkill: 'absent',
319
+ suppressing: false,
280
320
  memory: 'unknown',
281
321
  agentsMd: 'unknown',
282
322
  skillCatalog: 'unknown',
@@ -296,6 +336,7 @@ async function composeRow(roster, meta, ctx) {
296
336
  personaMounted: false,
297
337
  agentInstructions: 'absent',
298
338
  toolSkill: 'absent',
339
+ suppressing: false,
299
340
  memory: 'unknown',
300
341
  agentsMd: 'unknown',
301
342
  skillCatalog: 'unknown',
@@ -312,6 +353,7 @@ async function composeRow(roster, meta, ctx) {
312
353
  personaMounted: facts.personaMounted,
313
354
  agentInstructions: facts.agentInstructions,
314
355
  toolSkill: facts.toolSkill,
356
+ suppressing: isSuppressingPreset(facts),
315
357
  ...reach,
316
358
  ...(facts.parseFailure === undefined ? {} : { reason: `解析组合失败:${facts.parseFailure}` }),
317
359
  };
@@ -363,7 +405,7 @@ export async function assessPresetReach(roster, ctx) {
363
405
  const suppressed = rows.filter((row) => row.memory === 'suppressed').length;
364
406
  const summary = rows.length === 0
365
407
  ? '未发现任何 Agent 预设'
366
- : `预设 ${rows.length} 个 · 抑制记忆注入 ${suppressed} 个 · 技能目录缺失 ${rows.filter((row) => row.skillCatalog === 'absent').length} 个 · 无官方子智能体工具 ${rows.filter((row) => row.subagent === 'absent').length} 个`;
408
+ : `预设 ${rows.length} 个 · 压制型预设(本插件默认不注入)${suppressed} 个 · 提示词通道缺失 ${rows.filter((row) => row.agentsMd === 'absent').length} 个 · 技能目录缺失 ${rows.filter((row) => row.skillCatalog === 'absent').length} 个 · 子智能体目录缺失 ${rows.filter((row) => row.subagent === 'absent').length} 个`;
367
409
  return {
368
410
  rows,
369
411
  defaultId: roster.defaultId === undefined ? null : String(roster.defaultId),
@@ -378,21 +420,41 @@ export async function assessPresetReach(roster, ctx) {
378
420
  *
379
421
  * This is the load-bearing half of the fix: a model that lists memories while
380
422
  * running under a suppressing preset would otherwise assume those memories are
381
- * already in its context. Pure and synchronous so a contract test can pin the
382
- * wording to the facts.
423
+ * already in its context. The same reasoning covers MCP and the subagent roster:
424
+ * none of them reach the model while the preset keeps the plugin's runtime
425
+ * snapshot out of the conversation, so the notice says which tool to call
426
+ * instead — and where the user can flip that off.
427
+ *
428
+ * Pure and synchronous so a contract test can pin the wording to the facts.
383
429
  *
384
- * @returns the notice, or `''` when nothing is suppressed.
430
+ * 写法纪律(2026-09-16 第三版):
431
+ * - **不点名任何工具**。这条提示挂在 5 个发现型工具上,点名就必然出现循环
432
+ * (`mcp_manager_list` 的结果里写着「用 mcp_manager_list 查询」——
433
+ * 用户实测发现)。按域名过滤只是把循环换成残缺列表;干脆不枚举:**域 → 工具**的
434
+ * 映射属于工具描述(常驻层),提示只负责说清「什么不在你上下文里」。
435
+ * - 全域一句话说完,不逐域展开;不复述工具 schema 的细节。
436
+ * - 第二版把"被 persona complete 压制"当成不可改变的事实;现在本插件改走 pre-step
437
+ * 注入消息(src/context-inject.ts),压制型预设下**默认不注入但可以打开**,
438
+ * 提示里如实给出这个开关的位置。
439
+ *
440
+ * @returns the notice, or `''` when nothing is out of context.
385
441
  */
386
- export function reachNoticeFor(presetId, facts) {
442
+ export function reachNoticeFor(presetId, facts, inject) {
387
443
  const parts = [];
388
- if (facts.personaComplete === true) {
389
- parts.push(`场景记忆正文与 ~/.dsh/AGENTS.md 都不会自动进入你的上下文:预设「${presetId}」的 persona 是 complete,提示词只保留该 persona 本身。`, '不要假设你已经看到任何记忆正文;需要内容时用 rule_manager_read 逐条读取。');
390
- }
391
- else if (facts.agentInstructions === 'absent') {
392
- parts.push(`~/.dsh/AGENTS.md 不会自动进入你的上下文:预设「${presetId}」未挂载 @deepseek-ai/dsh-agent-instructions。`);
444
+ const domainOff = (key) => inject?.domains?.[key] === false;
445
+ if (isSuppressingPreset(facts) && inject?.underSuppressingPresets !== true) {
446
+ parts.push(`预设「${presetId}」声明只要它自己的文本(persona complete / 关闭运行时上下文):` +
447
+ '本插件注入的 —— 场景和记忆、MCP、技能、子智能体、提示词 —— 默认不注入,' +
448
+ '需要时用对应的 list / read 工具按需读取;不要假设你已经看到它们。' +
449
+ '(想让它在这类预设下也注入:插件的「兼容」页 → 注入。)');
393
450
  }
394
- if (facts.toolSkill === 'absent') {
395
- parts.push(`技能目录在该预设下不可见(未挂载 @deepseek-ai/dsh-tool-skill)。`);
451
+ else {
452
+ if (facts.agentInstructions !== 'mounted' && domainOff('prompt')) {
453
+ parts.push(`提示词(~/.dsh/AGENTS.md)不会自动进入你的上下文:预设「${presetId}」未挂载 @deepseek-ai/dsh-agent-instructions,本插件的注入兜底也关着。`);
454
+ }
455
+ if (facts.toolSkill !== 'mounted' && domainOff('skills')) {
456
+ parts.push('技能目录不在你的上下文里(官方 @deepseek-ai/dsh-tool-skill 行没挂,本插件的注入兜底也关着)。');
457
+ }
396
458
  }
397
459
  if (parts.length === 0)
398
460
  return '';
@@ -404,7 +466,7 @@ export function reachNoticeFor(presetId, facts) {
404
466
  * Every failure path answers `''` — a boundary notice must never turn a
405
467
  * successful read into an error.
406
468
  */
407
- export async function reachNoticeForAgent(roster, agentCtx) {
469
+ export async function reachNoticeForAgent(roster, agentCtx, inject) {
408
470
  if (roster === undefined || typeof roster.composedPreset !== 'function' || agentCtx === undefined || agentCtx === null)
409
471
  return '';
410
472
  let presetId = '';
@@ -417,7 +479,7 @@ export async function reachNoticeForAgent(roster, agentCtx) {
417
479
  if (presetId === '' || typeof roster.read !== 'function')
418
480
  return '';
419
481
  try {
420
- return reachNoticeFor(presetId, readCompositionFacts(String((await roster.read(presetId)) ?? '')));
482
+ return reachNoticeFor(presetId, readCompositionFacts(String((await roster.read(presetId)) ?? '')), inject);
421
483
  }
422
484
  catch {
423
485
  return '';
@@ -41,15 +41,17 @@ export const IDENTITY_PACKAGES = [
41
41
  '@deepseek-ai/dsh-storage-domain',
42
42
  '@deepseek-ai/dsh-spill-local',
43
43
  ];
44
- /** Peers this plugin was written and verified against. */
45
- export const EXPECTED_PEER_RANGE = '>=0.1.5-rc.2 <0.2.0-0';
44
+ /** Peers this plugin was written and verified against(只声明最低版本,无上界)。 */
45
+ export const EXPECTED_PEER_RANGE = '>=0.1.5-rc.2';
46
46
  /** The release this plugin's adapters were last verified against. */
47
47
  export const VERIFIED_HOST_VERSION = '0.1.5-rc.2';
48
48
  /**
49
49
  * peer range 的下界 —— 界面展示用。
50
50
  *
51
- * 上界 `<0.2.0-0` 是 semver「不跨下一个 minor」的**约定写法**,不是被支持的版本;
52
- * 把它当作「要求范围」展示会让人以为插件支持一批尚不存在的版本。
51
+ * 刻意不带 semver 上界:官方持续发版,硬上界会在宿主跨 minor 升级时直接挡住
52
+ * 插件安装(pnpm peer 校验失败)。上界改为由**运行时能力探测**兜底 ——
53
+ * 宿主版本高于已验证范围时,`assessHost` 会逐项探测并把缺失能力降级/禁用,
54
+ * 而不是在安装期拒绝整包。展示时只应把「最低要求版本」当事实。
53
55
  * 从 EXPECTED_PEER_RANGE 派生,避免两处手写漂移。
54
56
  */
55
57
  export const EXPECTED_MIN_HOST_VERSION = EXPECTED_PEER_RANGE.match(/^>=\s*([^\s]+)/)?.[1] ?? VERIFIED_HOST_VERSION;