dsh-subagent-profile 0.3.2 → 0.3.4

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 (40) hide show
  1. package/README.md +77 -40
  2. package/README.zh.md +111 -74
  3. package/docs/screenshots/dispatch-card.png +0 -0
  4. package/docs/screenshots/settings-page1.png +0 -0
  5. package/docs/screenshots/settings-page2.png +0 -0
  6. package/index.mjs +276 -81
  7. package/lib/client.js +3218 -166
  8. package/lib/core/adoption-reminder.mjs +48 -0
  9. package/lib/core/adoption-tracker.mjs +430 -0
  10. package/lib/core/background-ledger.mjs +71 -0
  11. package/lib/core/catalog-cache.mjs +45 -7
  12. package/lib/core/catalog.mjs +6 -6
  13. package/lib/core/cost-evidence.mjs +145 -0
  14. package/lib/core/cost-guard.mjs +71 -44
  15. package/lib/core/decision-trace.mjs +413 -0
  16. package/lib/core/delegation.mjs +111 -50
  17. package/lib/core/dispatch-gates.mjs +153 -0
  18. package/lib/core/dispatch-guard.mjs +156 -0
  19. package/lib/core/dispatch-schema.mjs +103 -14
  20. package/lib/core/dispatch-tool.mjs +220 -204
  21. package/lib/core/draft-gates.mjs +45 -0
  22. package/lib/core/drafts-store.mjs +45 -0
  23. package/lib/core/escape.mjs +130 -0
  24. package/lib/core/evolution-advice.mjs +224 -0
  25. package/lib/core/evolution-ledger.mjs +300 -0
  26. package/lib/core/evolution-summary.mjs +255 -0
  27. package/lib/core/http-routes.mjs +256 -72
  28. package/lib/core/intersection.mjs +6 -9
  29. package/lib/core/presets-sync.mjs +161 -43
  30. package/lib/core/prices.mjs +46 -0
  31. package/lib/core/profile-directory.mjs +139 -0
  32. package/lib/core/profile-provider.mjs +42 -39
  33. package/lib/core/profiles-store.mjs +103 -76
  34. package/lib/core/pure.mjs +110 -66
  35. package/lib/core/reminder-store.mjs +172 -0
  36. package/lib/core/shims.mjs +67 -76
  37. package/lib/core/whitelist.mjs +23 -17
  38. package/package.json +82 -83
  39. package/presets/orchestrator/agent.cordis.yml +59 -87
  40. package/presets/orchestrator/NOTICE +0 -3
package/index.mjs CHANGED
@@ -1,52 +1,124 @@
1
- // index.mjs — dsh-subagent-profile 宿主侧正式插件 bundle(只做装配)。
2
- // 源自原型动态插件 `code.host` 主体;宿主导入面(registerTool/defineTool/
3
- // handle)收敛在 lib/core/shims.mjs,装配块按模块拆分驻留 lib/core/(catalog /
4
- // presets-sync / profiles-store / cost-guard / whitelist / intersection /
5
- // delegation / pure / shims / http-routes / profile-provider / dispatch-tool)。
6
- // apply 的装配辅助函数(syncBundledPresetsToHome / provideProfileService /
7
- // registerSystemPromptSections / registerSettingsRoutes)保持 section 文本与
8
- // 门控逐字不变;`enabled` 始终经 getter 注入,门控读取当前值。
1
+ // index.mjs — dsh-subagent-profile 宿主侧插件 bundle(只做装配)。宿主导入面收敛在
2
+ // lib/core/shims.mjs,装配块按模块拆分驻留 lib/core/;apply 的装配辅助函数保持
3
+ // section 文本与门控逐字不变,`enabled` 始终经 getter 注入(门控读取当前值)。
9
4
 
5
+ import { readdirSync, renameSync, rmSync } from 'node:fs';
6
+ import { createRequire } from 'node:module';
10
7
  import { join } from 'node:path';
11
8
  import { syncBundledPresets } from './lib/core/presets-sync.mjs';
12
9
  import { dshHome, createProfileStore } from './lib/core/profiles-store.mjs';
13
10
  import { createHttpRoutes } from './lib/core/http-routes.mjs';
14
11
  import { createProfileProvider } from './lib/core/profile-provider.mjs';
15
12
  import { createDispatchTool } from './lib/core/dispatch-tool.mjs';
13
+ import { createDispatchGuard } from './lib/core/dispatch-guard.mjs';
16
14
  import { createCatalogCache } from './lib/core/catalog-cache.mjs';
17
- import { tierSortKey } from './lib/core/pure.mjs';
15
+ import { createFailureLedger } from './lib/core/decision-trace.mjs';
16
+ import { createEvolutionLedger } from './lib/core/evolution-ledger.mjs';
17
+ import { createAdoptionTracker } from './lib/core/adoption-tracker.mjs';
18
+ import { createReminderStore } from './lib/core/reminder-store.mjs';
19
+ import { mintReminder, mintUnadoptedReminder } from './lib/core/adoption-reminder.mjs';
20
+ import { createBackgroundLedger } from './lib/core/background-ledger.mjs';
21
+ import { createDraftsStore } from './lib/core/drafts-store.mjs';
22
+ import { assessDraftProfile } from './lib/core/draft-gates.mjs';
23
+ import { createEscapeStore, recordEscapeAllowProvider } from './lib/core/escape.mjs';
24
+ import { resolveWhitelist, FALLBACK_WHITELIST } from './lib/core/whitelist.mjs';
25
+ import { profileDirectoryRows, profileStatsFromSummaries, applyProfileStats } from './lib/core/profile-directory.mjs';
26
+ import { buildAdviceText, refreshSummaries, readSummaries } from './lib/core/evolution-advice.mjs';
18
27
 
19
28
  export const name = 'dsh-subagent-profile';
20
29
  export const inject = ['subagents', 'tools', 'agents'];
21
30
 
22
- // Self-install the bundled "orchestrator" agent preset into the DSH
23
- // agent-presets root so the mode appears in the new-session picker without
24
- // manual copying (mirrors the shipped dsh-liangshen self-install). Idempotent:
25
- // byte-identical trees are skipped; a bundle change rewrites the preset — the
26
- // intended upgrade path. Fail-soft: the dispatch tool and settings page keep
27
- // working even if the write is denied.
31
+ // 自装 bundled "orchestrator" 预设到 agent-presets 根(与官方自安装相同)。
32
+ // 幂等:字节一致跳过、bundle 变更重写;fail-soft:写入被拒时工具与设置页照常工作。
28
33
  function syncBundledPresetsToHome(ctx) {
29
34
  try {
30
35
  const presetRoot = join(dshHome(), '.agent-presets');
31
36
  const sync = syncBundledPresets(presetRoot);
32
37
  for (const { id, error } of sync.failed) ctx.logger.warn(`[dsh-subagent-profile] preset ${id} sync failed: ${error}`);
33
38
  if (sync.synced.length > 0) ctx.logger.info(`[dsh-subagent-profile] presets synced into ${presetRoot}: ${sync.synced.join(', ')}`);
39
+ if (sync.userModified.length > 0) ctx.logger.warn(`[dsh-subagent-profile] presets left untouched (user-modified): ${sync.userModified.join(', ')}`);
34
40
  } catch (error) {
35
41
  ctx.logger.warn('[dsh-subagent-profile] preset sync failed:', error instanceof Error ? error.message : String(error));
36
42
  }
37
43
  }
38
44
 
39
- // subagent-profiles service over the store's per-apply profiles Map —
40
- // lets the outside world enumerate and extend the registry without touching
41
- // internals.
45
+ // 卸载清理:本插件 3 个数据文件 + orchestrator 预设目录改名备份(effect 清理器
46
+ // 在禁用/热重载/退出时都可能触发,硬删除会静默丢用户数据)。备份保留可恢复副本;
47
+ // 规范路径清空后重启按需重建。fail-soft:改名失败不抛。
48
+ function removeOwnedData(home) {
49
+ const stamp = Date.now();
50
+ for (const file of ['subagent-profiles.json', 'subagent-profiles.state.json', 'subagent-profiles.failed-traces.json']) {
51
+ try { renameSync(join(home, file), join(home, `${file}.removed-${stamp}`)); } catch { /* 源不存在(正常首启)等:best effort */ }
52
+ }
53
+ try { renameSync(join(home, '.agent-presets', 'orchestrator'), join(home, '.agent-presets', `orchestrator.removed-${stamp}`)); } catch { /* best effort */ }
54
+ }
55
+
56
+ // 启动清理 .removed-* 备份残留(只清本插件前缀,防热重载反复产生新备份积累);
57
+ // 卸载后用户手动改回原名的场景不受影响。fail-soft。
58
+ function cleanRemovedBackups(home) {
59
+ for (const dir of [home, join(home, '.agent-presets')]) {
60
+ let names;
61
+ try { names = readdirSync(dir); } catch { continue; }
62
+ for (const name of names) {
63
+ try {
64
+ if (name.startsWith('subagent-profiles.') && name.includes('.removed-')) {
65
+ rmSync(join(dir, name), { force: true });
66
+ } else if (name.startsWith('orchestrator.removed-')) {
67
+ rmSync(join(dir, name), { recursive: true, force: true });
68
+ }
69
+ } catch { /* best effort */ }
70
+ }
71
+ }
72
+ }
73
+
74
+ // parent_adopted 装配:tracker 工厂 + 根 ctx 订阅 session/event(按 session.id 过滤),
75
+ // 卸载退订并清理定时器。onDecision 经 getRefreshAdvice 延迟读取(先判定后聚合时为空
76
+ // 操作);onDecided 把「明确未采纳」转成提醒中心条目(同源审计 + 注意级提醒)。
77
+ function setupAdoptionTracker(ctx, home, getRefreshAdvice, onUnadopted) {
78
+ const adoptionTracker = createAdoptionTracker({
79
+ stateFile: join(home, 'subagent-evolution', 'adopted-state.json'),
80
+ warn: (message) => ctx.logger.warn(`[dsh-subagent-profile] ${message}`),
81
+ onDecision: () => { try { getRefreshAdvice()(); } catch { /* 聚合重算失败不影响判定(fail-soft) */ } },
82
+ onDecided: onUnadopted,
83
+ });
84
+ const onSessionEvent = (session, event) => adoptionTracker.handleEvent(session, event);
85
+ ctx.on('session/event', onSessionEvent);
86
+ ctx.effect(() => () => { ctx.off('session/event', onSessionEvent); adoptionTracker.dispose(); });
87
+ return adoptionTracker;
88
+ }
89
+
90
+ // 卸载接线:注销工具 + 清空台账 + 级联取消 + 删数据文件与预设目录。agent/disposed
91
+ // 挂钩按父释放并发/token 记账,并把该父会话未决采纳判定判「明确未采纳」。
92
+ function registerTeardown(ctx, dispatch, ledger, guard, home, backgroundLedger, adoptionTracker) {
93
+ ctx.effect(() => dispatch.dispose);
94
+ ctx.effect(() => ledger.clear);
95
+ ctx.effect(() => backgroundLedger.clear);
96
+ ctx.effect(() => () => {
97
+ guard.cancelAll();
98
+ guard.reset();
99
+ removeOwnedData(home);
100
+ });
101
+ const onAgentDisposed = ({ agent }) => {
102
+ const parentSessionId = agent?.session?.header?.id;
103
+ if (parentSessionId !== undefined && parentSessionId !== null && parentSessionId !== '') {
104
+ guard.resetParent(parentSessionId);
105
+ adoptionTracker.parentDisposed(parentSessionId);
106
+ }
107
+ };
108
+ ctx.on('agent/disposed', onAgentDisposed);
109
+ ctx.effect(() => () => ctx.off('agent/disposed', onAgentDisposed));
110
+ }
111
+
112
+ // subagent-profiles service:包在 store 的 per-apply profiles Map 之上,对外只暴露
113
+ // 枚举/扩展注册表的窄接口,不触碰内部结构。
42
114
  function provideProfileService(ctx, store) {
43
115
  ctx.provide('subagent-profiles', {
44
116
  register(profile) {
45
117
  if (!profile || typeof profile.id !== 'string' || profile.id.length === 0) {
46
- throw new Error('subagent-profiles: profile id must be a non-empty string');
118
+ throw new Error('subagent-profiles: profile id 必须为非空字符串');
47
119
  }
48
120
  if (store.profiles.has(profile.id)) {
49
- throw new Error(`subagent-profiles: profile "${profile.id}" is already registered`);
121
+ throw new Error(`subagent-profiles: profile "${profile.id}" 已注册(id 需唯一)`);
50
122
  }
51
123
  const registered = { ...profile };
52
124
  store.profiles.set(profile.id, registered);
@@ -96,55 +168,94 @@ function sectionGatePasses(ctx, getEnabled, context) {
96
168
  // dispatch:profiles section 文本。引号引用:description 套引号;为空时显示
97
169
  // 占位符(不套引号)。压平在存储层完成(sanitizeProfile),此处仅负责显示层包裹。
98
170
  // 门控 profiles section 内附一行行为规则提示(非开放的 persona 注入)。
99
- function profileSectionText(store, gate, context) {
171
+ function profileSectionText(store, gate, context, summaries) {
100
172
  if (!gate(context)) return '';
101
- const rows = [...store.profiles.values()]
102
- .filter((p) => p.enabled !== false)
103
- .sort((a, b) => tierSortKey(a.tokenTier) - tierSortKey(b.tokenTier))
104
- .map((p) => {
105
- const desc = typeof p.description === 'string' && p.description.length > 0 ? `"${p.description}"` : '(无描述)';
106
- return `- ${p.id}: ${desc}${p.preset !== undefined ? ` (preset: ${p.preset})` : ''}`;
107
- });
173
+ const rows = applyProfileStats(profileDirectoryRows(store), profileStatsFromSummaries(store, summaries));
108
174
  if (rows.length === 0) return '';
109
- const note = '- 选择方案时先匹配任务复杂度与方案描述的能力边界,成本只在能力都胜任的方案之间比较——复杂任务不得为省 token 改用能力不足的便宜方案。\n- 别把 1-2 步即可自查/可搜完的小事委派出去 —— 几分钟内能自查完的直接做。';
110
- return `Available dispatch profiles (dispatch.profile):\n${rows.join('\n')}\n${note}`;
175
+ const lines = rows.map((p) => {
176
+ const desc = p.description.length > 0 ? `"${p.description}"` : '(无描述)';
177
+ const meta = [];
178
+ if (p.preset !== undefined) meta.push(`preset: ${p.preset}`);
179
+ else if (p.model !== undefined) meta.push(`model: ${p.model}`);
180
+ if (p.tokenTier !== undefined) meta.push(`tier: ${p.tokenTier}`);
181
+ if (p.avgCost !== undefined) meta.push(`平均 ${p.avgCost} 元/次`);
182
+ if (p.successRate !== undefined) meta.push(`成功率 ${p.successRate}${p.n !== undefined ? ` (N=${p.n})` : ''}`);
183
+ return `- ${p.id}: ${desc}${meta.length > 0 ? ` (${meta.join(' · ')})` : ''}`;
184
+ });
185
+ const note = '- 选择方案时先匹配任务复杂度与方案描述的能力边界,成本只在能力都胜任的方案之间比较——复杂任务不得为省 token 改用能力不足的便宜方案。\n- 别把 1-2 步即可自查/可搜完的小事委派出去 —— 几分钟内能自查完的直接做。\n- 优先使用已保存的 profile;只有少数字段需要临时覆盖时才传 per-call 参数。查资料/汇总/读文件等轻任务优先 cheap 或 flash。';
186
+ return `Available dispatch profiles (dispatch.profile):\n${lines.join('\n')}\n${note}`;
111
187
  }
112
188
 
113
189
  // orchestrator:mode section 文本(逐字保留;与 profiles 同门控)。
114
190
  const ORCHESTRATOR_MODE_TEXT = '本机已安装 dsh-subagent-profile 插件的「编排者模式」agent preset:新建会话的预设选择器中可选「编排者模式」。该模式把 Agent 定位为主协调者——拆解任务后按场景用 dispatch(内置 swap-standard=标准编码、researcher=调研检索,可在「子 Agent 方案」设置页自定义)与 subagent/subagent_fork/workflow 委派给子 Agent,再整合结果。preset 文件由插件维护于 ~/.dsh/.agent-presets,安装/升级时自动同步;用户提到「编排者模式 / orchestrator / 主协调模式」时即指本预设,请据此协作。';
115
191
 
116
- // Directory section rendering the available profiles (systemPrompt's
117
- // section `text` accepts a function, as the shipped tool-subagent proves).
118
- // gate: `enabled` must be read live (getter), so /set-enabled toggles
119
- // apply immediately without a restart.
120
- function registerSystemPromptSections(ctx, store, getEnabled) {
192
+ // 建议注入候选池:受信任预设白名单(解析失败回退内置名单)。不叠加逃生舱放行集。
193
+ async function resolveAdviceWhitelist(ctx) {
194
+ try {
195
+ return new Set(await resolveWhitelist(ctx.get('agentPresets')));
196
+ } catch {
197
+ return new Set(FALLBACK_WHITELIST);
198
+ }
199
+ }
200
+
201
+ // 系统提示各 section:门控经 sectionGatePasses;`enabled` 经 getter 实时读取。
202
+ function registerSystemPromptSections(ctx, store, getEnabled, getEvolutionAdvice, adviceEnv, guard) {
121
203
  const pluginSystemPrompt = ctx.get('systemPrompt');
122
204
  if (pluginSystemPrompt === undefined) return;
123
205
  const gate = (context) => sectionGatePasses(ctx, getEnabled, context);
124
206
  pluginSystemPrompt.section({
125
207
  name: 'dispatch:profiles',
126
208
  order: 116.5,
127
- text: (context) => profileSectionText(store, gate, context),
209
+ text: (context) => {
210
+ let summaries = null;
211
+ try { summaries = readSummaries(adviceEnv.summariesFile, adviceEnv.logger); } catch { /* 统计只增强,不影响目录注入 */ }
212
+ return profileSectionText(store, gate, context, summaries);
213
+ },
128
214
  });
129
- // Announce the self-installed orchestrator preset so the current agent
130
- // knows the mode exists and can point the user to it. Gated the same
131
- // way — only a dispatch-capable agent sees it.
215
+ // 宣告自装的 orchestrator 预设,让当前 agent 知道该模式存在、可引导用户使用。
216
+ // 门控与 profiles 段相同——只有可派发的 agent 才看得到。
132
217
  pluginSystemPrompt.section({
133
218
  name: 'orchestrator:mode',
134
219
  order: 117,
135
220
  text: (context) => (gate(context) ? ORCHESTRATOR_MODE_TEXT : ''),
136
221
  });
222
+ // 派发优化建议段:门控 = orchestrator + evolutionAdvice 开关;异常兜底不阻断装配。
223
+ pluginSystemPrompt.section({
224
+ name: 'evolution:advice',
225
+ order: 116.8,
226
+ text: (context) => {
227
+ if (!getEvolutionAdvice() || !gate(context)) return '';
228
+ try {
229
+ return buildAdviceText(adviceEnv);
230
+ } catch (error) {
231
+ // 候选校验失败(如逃生舱放行的非 system 预设)不再静默空转——logger.warn
232
+ // 留痕 + 注入段标注「建议暂不可用及原因」,父 Agent 可见、派发行为不受影响。
233
+ const reason = error instanceof Error ? error.message : String(error);
234
+ adviceEnv.logger.warn(`[dsh-subagent-profile] evolution:advice 生成失败:${reason}`);
235
+ return `(派发优化建议暂不可用:${reason}。派发行为不受影响。)`;
236
+ }
237
+ },
238
+ });
239
+ // 派发预算余量段:与 profiles 同门控;无父 sessionId 不注入(无法关联具体会话)。
240
+ pluginSystemPrompt.section({
241
+ name: 'dispatch:budget',
242
+ order: 116.7,
243
+ text: (context) => {
244
+ if (!gate(context)) return '';
245
+ const parentSessionId = context?.agent?.session?.header?.id;
246
+ if (typeof parentSessionId !== 'string' || parentSessionId === '') return '';
247
+ const b = guard.snapshot(parentSessionId);
248
+ return `派发预算余量:本会话在途 ${b.concurrency}/${b.maxConcurrent},累计 token ${b.tokens}/${b.maxParentTokens},剩余 ${Math.max(0, b.maxParentTokens - b.tokens)} token。`;
249
+ },
250
+ });
137
251
  }
138
252
 
139
- // HTTP loopback routes for the Client settings UI (webServer.register ↔
140
- // client fetch; JSON only) — the routes themselves live in
141
- // lib/core/http-routes.mjs (imported above); this wiring only injects the
142
- // per-apply deps. webServer is optional — a headless deployment keeps the
143
- // dispatch tool and drops only the settings page. webServer's activation
144
- // (listen) is async and may not be ready when this plugin's inject deps
145
- // resolve, so register inside an inject sub-scope that waits for it
146
- // (ctx.get would read undefined at apply time).
147
- function registerSettingsRoutes(ctx, store, getEnabled, setEnabled, syncTool, catalog) {
253
+ // Client 设置 UI HTTP loopback 路由(webServer.register ↔ client fetch,纯 JSON)。
254
+ // 路由本体在 lib/core/http-routes.mjs(上文已 import);这里只注入 per-apply 依赖。
255
+ // webServer 可选——无头部署保留 dispatch 工具、只丢设置页。webServer 的激活
256
+ // (listen)是异步的,可能晚于本插件 inject 依赖解析完成,故在等它的 inject
257
+ // scope 内注册(apply ctx.get 会读到 undefined)。
258
+ function registerSettingsRoutes(ctx, store, getEnabled, setEnabled, syncTool, catalog, ledger, backgroundLedger, draftsStore, applyDraft, getAudit, getEvolutionAdvice, setEvolutionAdvice, getEscapeEnabled, setEscapeEnabled, escape, refreshAdvice, summariesFile, dispatchFile, adviceWhitelist, previewDraft, reminderStore) {
148
259
  ctx.inject(['webServer'], (scope) => {
149
260
  scope.effect(createHttpRoutes({
150
261
  webServer: scope.webServer,
@@ -153,6 +264,22 @@ function registerSettingsRoutes(ctx, store, getEnabled, setEnabled, syncTool, ca
153
264
  setEnabled,
154
265
  syncTool,
155
266
  catalog,
267
+ ledger,
268
+ backgroundLedger,
269
+ draftsStore,
270
+ applyDraft,
271
+ getAudit,
272
+ getEvolutionAdvice,
273
+ setEvolutionAdvice,
274
+ getEscapeEnabled,
275
+ setEscapeEnabled,
276
+ escape,
277
+ refreshAdvice,
278
+ summariesFile,
279
+ dispatchFile,
280
+ adviceWhitelist,
281
+ previewDraft,
282
+ reminderStore,
156
283
  logger: ctx.logger,
157
284
  }), 'dsh-subagent-profile: settings routes');
158
285
  });
@@ -170,48 +297,116 @@ function createSharedCatalog(ctx) {
170
297
  });
171
298
  }
172
299
 
173
- export async function apply(ctx) {
174
- // Enable/disable switch (default on, runtime-toggled by the settings
175
- // page, persisted across restarts) + profile registry — lib/profiles-store
176
- // .mjs: createProfileStore. loadProfiles runs once at startup via the
177
- // explicit call below (the factory itself does not auto-load).
178
- const store = createProfileStore({ dshHome: dshHome(), logger: ctx.logger });
179
- let enabled = store.loadEnabled();
180
- store.loadProfiles();
181
- // Self-install the bundled "orchestrator" preset (idempotent, fail-soft).
182
- syncBundledPresetsToHome(ctx);
183
- // 进程级共享 catalog 快照(/options 三路由 + dispatch cost guard 共用)。
184
- const catalog = createSharedCatalog(ctx);
185
- // `dispatch` tool — lib/core/dispatch-tool.mjs: defineTool block (schema +
186
- // execute), the result-schema consistency lock and the syncTool
187
- // register/unregister logic.
188
- // Created BEFORE the HTTP inject so createHttpRoutes can capture
189
- // dispatch.syncTool (/set-enabled).
190
- const dispatch = createDispatchTool({
300
+ // 读本插件 package.json version 供派发台账 provenance 使用。fail-soft:读取/解析
301
+ // 失败回退 'unknown',绝不阻断插件启动。
302
+ function readPluginVersion() {
303
+ try {
304
+ const pkg = createRequire(import.meta.url)('./package.json');
305
+ return typeof pkg?.version === 'string' && pkg.version !== '' ? pkg.version : 'unknown';
306
+ } catch {
307
+ return 'unknown';
308
+ }
309
+ }
310
+
311
+ // 逃生舱装配:放行集 store + 开关态(state.json 持久化)+ 访问器。开关态经对象字段
312
+ // 可变(/set-escape 改写),getEscapeSet 开关关时恒空数组(零叠加、零放行审计)。
313
+ // 放行时 mint P0(紧急)提醒(高可见治理事件,与放行审计同源留痕)。
314
+ function setupEscape(ctx, store, evoLedger, home, getReminderStore) {
315
+ const escape = createEscapeStore({ dshHome: home, logger: ctx.logger, onGovernanceFailure: () => evoLedger.markGovernanceFailure() });
316
+ const state = { escapeEnabled: store.loadEscapeEnabled() };
317
+ return {
318
+ escape,
319
+ getEscapeSet: () => (state.escapeEnabled ? escape.list() : []),
320
+ getEscapeEnabled: () => state.escapeEnabled,
321
+ setEscapeEnabled: (next) => { state.escapeEnabled = next; },
322
+ recordEscapeAllowProvider: (parent, preset) => {
323
+ recordEscapeAllowProvider(evoLedger, parent, preset);
324
+ mintReminder(getReminderStore(), { severity: 'P0', kind: 'escape-allow', title: '逃生舱放行非官方预设', detail: '预设 "' + preset + '" 经逃生舱放行,其余安全检查仍全量生效', sessionId: parent?.session?.header?.id });
325
+ },
326
+ };
327
+ }
328
+
329
+ // `dispatch` 工具装配(defineTool + execute + syncTool)。须在 HTTP inject 之前
330
+ // 构造,使 createHttpRoutes 能拿 dispatch.syncTool 供 /set-enabled 注册/注销。
331
+ function createDispatch(ctx, store, catalog, ledger, guard, evoLedger, backgroundLedger, adoptionTracker, getEscapeSet, getEnabled, getEvolutionAdvice) {
332
+ return createDispatchTool({
191
333
  register: (tool) => ctx.tools.register(tool),
192
334
  store,
193
- getEnabled: () => enabled,
335
+ getEnabled,
194
336
  getService: (name) => ctx.get(name),
195
337
  logger: ctx.logger,
196
338
  subagents: ctx.subagents,
197
339
  catalog,
340
+ ledger,
341
+ guard,
342
+ evoLedger,
343
+ backgroundLedger,
344
+ adoptionTracker,
345
+ getEscapeSet,
346
+ getEvolutionAdvice,
198
347
  });
199
- // subagent-profiles service over the store's per-apply profiles Map.
348
+ }
349
+
350
+ // auto-profile 落库:三道安全检查评估(draft-gates)→ 任一 fail 即 throw → 落库 + 审计。
351
+ async function applyDraftProfile({ ctx, store, catalog, getEscapeSet, evoLedger, draft }) {
352
+ const { ok, checks, clean } = await assessDraftProfile({ ctx, store, catalog, getEscapeSet, draft });
353
+ if (!ok) {
354
+ const failed = checks.find((c) => c.verdict === 'fail');
355
+ throw new Error(failed !== undefined && typeof failed.reason === 'string' ? failed.reason : 'draft 未通过安全检查');
356
+ }
357
+ store.profiles.set(clean.id, { ...clean, persisted: true });
358
+ const persisted = store.persistProfiles();
359
+ evoLedger.recordGovernanceAudit({ kind: 'draft-apply', profileId: clean.id, source: typeof draft.source === 'string' ? draft.source : 'human' });
360
+ return { id: clean.id, persisted: persisted.persisted };
361
+ }
362
+
363
+ export async function apply(ctx) {
364
+ const home = dshHome();
365
+ cleanRemovedBackups(home); // 上个生命周期留下的 .removed-* 备份残留。
366
+ // reminderStore 变量提前声明:evoLedger.onAlert 与 escape mint 钩子延迟读取。
367
+ let reminderStore;
368
+ // 派发台账 + 审计分级须在 store 之前构造:store 的治理审计钩子指向其 markGovernanceFailure。
369
+ const evoLedger = createEvolutionLedger({ dshHome: home, pluginVersion: readPluginVersion(), warn: (message) => ctx.logger.warn(`[dsh-subagent-profile] ${message}`), onAlert: (lost) => mintReminder(reminderStore, { severity: 'P0', kind: 'audit-degraded', title: '审计降级', detail: '治理审计丢失 ' + lost + ' 条,健康状态已降级——请检查磁盘', sessionId: null }) });
370
+ const store = createProfileStore({ dshHome: home, logger: ctx.logger, onGovernanceFailure: () => evoLedger.markGovernanceFailure() });
371
+ const escapeCtl = setupEscape(ctx, store, evoLedger, home, () => reminderStore);
372
+ let enabled = store.loadEnabled();
373
+ let evolutionAdvice = store.loadEvolutionAdvice();
374
+ store.loadProfiles();
375
+ // 自装 bundled 的 orchestrator 预设(幂等、fail-soft)。
376
+ syncBundledPresetsToHome(ctx);
377
+ const catalog = createSharedCatalog(ctx);
378
+ const ledger = createFailureLedger({ warn: (message) => ctx.logger.warn(`[dsh-subagent-profile] ${message}`), stateFile: join(home, 'subagent-profiles.failed-traces.json') });
379
+ const guard = createDispatchGuard({ warn: (message) => ctx.logger.warn(`[dsh-subagent-profile] ${message}`) });
380
+ const backgroundLedger = createBackgroundLedger({ warn: (message) => ctx.logger.warn(`[dsh-subagent-profile] ${message}`) });
381
+ const draftsStore = createDraftsStore({ dshHome: home, onGovernanceFailure: () => evoLedger.markGovernanceFailure() });
382
+ reminderStore = createReminderStore({ dshHome: home, audit: (entry) => evoLedger.recordGovernanceAudit(entry), warn: (message) => ctx.logger.warn(`[dsh-subagent-profile] ${message}`) });
383
+ let refreshAdvice = () => {};
384
+ const adoptionTracker = setupAdoptionTracker(ctx, home, () => refreshAdvice, (rec) => mintUnadoptedReminder(reminderStore, evoLedger, rec));
385
+ const dispatch = createDispatch(ctx, store, catalog, ledger, guard, evoLedger, backgroundLedger, adoptionTracker, escapeCtl.getEscapeSet, () => enabled, () => evolutionAdvice);
200
386
  provideProfileService(ctx, store);
201
- // Gated system-prompt sections (profile directory + orchestrator mode).
202
- registerSystemPromptSections(ctx, store, () => enabled);
203
- // `profile` subagent provider lib/core/profile-provider.mjs:
204
- // createProfileProvider registers the provider and returns the disposer.
205
- const disposeProvider = createProfileProvider({
206
- subagents: ctx.subagents,
387
+ const adviceWhitelist = await resolveAdviceWhitelist(ctx);
388
+ const adviceEnv = { summariesFile: join(home, 'subagent-evolution', 'summaries.json'), dispatchFile: join(home, 'subagent-evolution', 'dispatch.jsonl'), whitelist: adviceWhitelist, logger: ctx.logger };
389
+ // 生产聚合触发点(T1 修复):/options/refresh /list 触发的惰性重算;parent_adopted
390
+ // 的「已确认未采纳」计数经 opts 惰性 join weighted_success(-0.3 惩罚)。
391
+ refreshAdvice = () => refreshSummaries({ dispatchFile: adviceEnv.dispatchFile, summariesFile: adviceEnv.summariesFile, logger: ctx.logger, opts: { parentAdoptedConfirmedFalse: adoptionTracker.confirmedFalseCounts() } });
392
+ registerSystemPromptSections(ctx, store, () => enabled, () => evolutionAdvice, adviceEnv, guard);
393
+ const disposeProvider = createProfileProvider({ subagents: ctx.subagents, store, getEnabled: () => enabled, logger: ctx.logger, catalog, getEscapeSet: escapeCtl.getEscapeSet, recordEscapeAllowProvider: escapeCtl.recordEscapeAllowProvider });
394
+ if (typeof disposeProvider === 'function') ctx.effect(() => disposeProvider);
395
+ const applyDraft = (draft) => applyDraftProfile({ ctx, store, catalog, getEscapeSet: escapeCtl.getEscapeSet, evoLedger, draft });
396
+ // /draft/preview:只读安全检查预览(不落库)。body 为 {config, name, description, source}。
397
+ const previewDraft = (body) => assessDraftProfile({
398
+ ctx,
207
399
  store,
208
- getEnabled: () => enabled,
209
- logger: ctx.logger,
210
400
  catalog,
401
+ getEscapeSet: escapeCtl.getEscapeSet,
402
+ draft: {
403
+ config: body !== null && typeof body === 'object' && typeof body.config === 'object' && body.config !== null ? body.config : {},
404
+ name: body !== null && typeof body === 'object' && typeof body.name === 'string' ? body.name : '',
405
+ description: body !== null && typeof body === 'object' && typeof body.description === 'string' ? body.description : '',
406
+ source: body !== null && typeof body === 'object' && typeof body.source === 'string' ? body.source : 'human',
407
+ },
211
408
  });
212
- if (typeof disposeProvider === 'function') ctx.effect(() => disposeProvider);
213
- // HTTP loopback routes for the Client settings UI lib/core/http-routes.mjs.
214
- registerSettingsRoutes(ctx, store, () => enabled, (next) => { enabled = next; }, dispatch.syncTool, catalog);
215
- // Teardown: unregister the dispatch tool (if still registered).
216
- ctx.effect(() => dispatch.dispose);
409
+ // Client 设置 UI HTTP loopback 路由 —— lib/core/http-routes.mjs。
410
+ registerSettingsRoutes(ctx, store, () => enabled, (next) => { enabled = next; }, dispatch.syncTool, catalog, ledger, backgroundLedger, draftsStore, applyDraft, () => evoLedger.auditState(), () => evolutionAdvice, (next) => { evolutionAdvice = next; }, escapeCtl.getEscapeEnabled, escapeCtl.setEscapeEnabled, escapeCtl.escape, refreshAdvice, join(home, 'subagent-evolution', 'summaries.json'), adviceEnv.dispatchFile, adviceEnv.whitelist, previewDraft, reminderStore);
411
+ registerTeardown(ctx, dispatch, ledger, guard, home, backgroundLedger, adoptionTracker);
217
412
  }