dsh-subagent-profile 0.3.3 → 0.4.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 (42) hide show
  1. package/README.md +49 -20
  2. package/README.zh.md +102 -73
  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 +153 -75
  7. package/lib/client.js +3081 -444
  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/cost-evidence.mjs +145 -0
  12. package/lib/core/cost-guard.mjs +1 -1
  13. package/lib/core/decision-trace.mjs +11 -31
  14. package/lib/core/delegation.mjs +11 -7
  15. package/lib/core/dispatch-gates.mjs +12 -5
  16. package/lib/core/dispatch-guard.mjs +6 -0
  17. package/lib/core/dispatch-schema.mjs +10 -5
  18. package/lib/core/dispatch-tool.mjs +52 -45
  19. package/lib/core/draft-gates.mjs +45 -0
  20. package/lib/core/drafts-store.mjs +45 -0
  21. package/lib/core/evolution-advice.mjs +323 -0
  22. package/lib/core/evolution-assets.mjs +167 -0
  23. package/lib/core/evolution-draft.mjs +312 -0
  24. package/lib/core/evolution-engine.mjs +269 -0
  25. package/lib/core/evolution-generate.mjs +196 -0
  26. package/lib/core/evolution-ledger.mjs +149 -33
  27. package/lib/core/evolution-persistence.mjs +213 -0
  28. package/lib/core/evolution-renewal.mjs +64 -0
  29. package/lib/core/evolution-routes.mjs +109 -0
  30. package/lib/core/evolution-summary.mjs +46 -223
  31. package/lib/core/http-helpers.mjs +35 -0
  32. package/lib/core/http-routes.mjs +169 -115
  33. package/lib/core/presets-sync.mjs +254 -256
  34. package/lib/core/prices.mjs +46 -0
  35. package/lib/core/profile-directory.mjs +139 -0
  36. package/lib/core/profile-provider.mjs +5 -3
  37. package/lib/core/profiles-store.mjs +24 -6
  38. package/lib/core/reminder-store.mjs +172 -0
  39. package/lib/core/session-read.mjs +17 -0
  40. package/lib/core/shims.mjs +13 -2
  41. package/package.json +82 -82
  42. package/presets/orchestrator/agent.cordis.yml +243 -243
package/index.mjs CHANGED
@@ -1,12 +1,6 @@
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
- // dispatch-guard)。
7
- // apply 的装配辅助函数(syncBundledPresetsToHome / provideProfileService /
8
- // registerSystemPromptSections / registerSettingsRoutes)保持 section 文本与
9
- // 门控逐字不变;`enabled` 始终经 getter 注入,门控读取当前值。
1
+ // index.mjs — dsh-subagent-profile 宿主侧插件 bundle(只做装配)。宿主导入面收敛在
2
+ // lib/core/shims.mjs,装配块按模块拆分驻留 lib/core/;apply 的装配辅助函数保持
3
+ // section 文本与门控逐字不变,`enabled` 始终经 getter 注入(门控读取当前值)。
10
4
 
11
5
  import { readdirSync, renameSync, rmSync } from 'node:fs';
12
6
  import { createRequire } from 'node:module';
@@ -20,18 +14,23 @@ import { createDispatchGuard } from './lib/core/dispatch-guard.mjs';
20
14
  import { createCatalogCache } from './lib/core/catalog-cache.mjs';
21
15
  import { createFailureLedger } from './lib/core/decision-trace.mjs';
22
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
23
  import { createEscapeStore, recordEscapeAllowProvider } from './lib/core/escape.mjs';
24
- import { tierSortKey } from './lib/core/pure.mjs';
25
24
  import { resolveWhitelist, FALLBACK_WHITELIST } from './lib/core/whitelist.mjs';
26
- import { buildAdviceText, refreshSummaries } from './lib/core/evolution-summary.mjs';
25
+ import { profileDirectoryRows, profileStatsFromSummaries, applyProfileStats } from './lib/core/profile-directory.mjs';
26
+ import { buildAdviceText, refreshSummaries, readSummaries } from './lib/core/evolution-advice.mjs';
27
+ import { createEvolutionAssembly } from './lib/core/evolution-engine.mjs';
27
28
 
28
29
  export const name = 'dsh-subagent-profile';
29
30
  export const inject = ['subagents', 'tools', 'agents'];
30
31
 
31
- // 自装 bundled 的 "orchestrator" agent 预设到 DSH agent-presets 根,让该模式
32
- // 无需手动复制就出现在新建会话选择器里(与官方 dsh-liangshen 自安装相同)。
33
- // 幂等:字节一致的树跳过;bundle 变更重写预设——即预期的升级路径。fail-soft:
34
- // 写入被拒时 dispatch 工具与设置页照常工作。
32
+ // 自装 bundled 的 "orchestrator" 预设到 agent-presets 根(与官方自安装相同)。
33
+ // 幂等:字节一致跳过、bundle 变更重写;fail-soft:写入被拒时工具与设置页照常工作。
35
34
  function syncBundledPresetsToHome(ctx) {
36
35
  try {
37
36
  const presetRoot = join(dshHome(), '.agent-presets');
@@ -44,12 +43,9 @@ function syncBundledPresetsToHome(ctx) {
44
43
  }
45
44
  }
46
45
 
47
- // 卸载清理:把本插件持久化的 3 个数据文件 + 自装的 orchestrator 预设目录改名备份
48
- // (只动本插件拥有的 orchestrator 目录,不碰 .agent-presets 下其它插件/用户的目录)。
49
- // 改为「改名到 <name>.removed-<ts>」而非 rmSync 删除:ctx.effect 的清理器在禁用/热
50
- // 重载/进程退出时都可能触发,硬删除会静默丢失用户 profiles/state/失败台账(S1)。
51
- // 备份保留可恢复副本;规范路径被清空,重装/重启后经 syncBundledPresetsToHome 重新
52
- // 同步预设、数据文件按需重新生成。fail-soft:改名失败不抛(卸载不应因清理失败而阻断)。
46
+ // 卸载清理:本插件 3 个数据文件 + orchestrator 预设目录改名备份(effect 清理器
47
+ // 在禁用/热重载/退出时都可能触发,硬删除会静默丢用户数据)。备份保留可恢复副本;
48
+ // 规范路径清空后重启按需重建。fail-soft:改名失败不抛。
53
49
  function removeOwnedData(home) {
54
50
  const stamp = Date.now();
55
51
  for (const file of ['subagent-profiles.json', 'subagent-profiles.state.json', 'subagent-profiles.failed-traces.json']) {
@@ -58,10 +54,8 @@ function removeOwnedData(home) {
58
54
  try { renameSync(join(home, '.agent-presets', 'orchestrator'), join(home, '.agent-presets', `orchestrator.removed-${stamp}`)); } catch { /* best effort */ }
59
55
  }
60
56
 
61
- // 启动清理 S1 改名备份残留:.removed-<ts> 备份在下次正常启动后已无保留价值(数据
62
- // 文件按需重新生成),启动时清掉防积累(热重载/禁用反复触发 effect 会持续产生新
63
- // 备份)。只清本插件前缀(subagent-profiles.* 与 orchestrator.removed-*),不碰其它
64
- // 文件;卸载后用户手动改回原名的场景不受影响(改回后无 .removed 残留)。fail-soft。
57
+ // 启动清理 .removed-* 备份残留(只清本插件前缀,防热重载反复产生新备份积累);
58
+ // 卸载后用户手动改回原名的场景不受影响。fail-soft。
65
59
  function cleanRemovedBackups(home) {
66
60
  for (const dir of [home, join(home, '.agent-presets')]) {
67
61
  let names;
@@ -78,13 +72,28 @@ function cleanRemovedBackups(home) {
78
72
  }
79
73
  }
80
74
 
81
- // 卸载接线:注销 dispatch 工具(若仍注册)+ 清空失败台账 + 级联取消在途派发 +
82
- // 清空守卫记账 + 删本插件数据文件与自装预设目录。另挂钩宿主 agent/disposed
83
- // (Agent 注册 fiber 卸载时发射):父会话逻辑结束时按父释放并发/token 记账,
84
- // 防配额随会话累积驻留。
85
- function registerTeardown(ctx, dispatch, ledger, guard, home) {
75
+ // parent_adopted 装配:tracker 工厂 + ctx 订阅 session/event(按 session.id 过滤),
76
+ // 卸载退订并清理定时器。onDecision getRefreshAdvice 延迟读取(先判定后聚合时为空
77
+ // 操作);onDecided 把「明确未采纳」转成提醒中心条目(同源审计 + 注意级提醒)。
78
+ function setupAdoptionTracker(ctx, home, getRefreshAdvice, onUnadopted) {
79
+ const adoptionTracker = createAdoptionTracker({
80
+ stateFile: join(home, 'subagent-evolution', 'adopted-state.json'),
81
+ warn: (message) => ctx.logger.warn(`[dsh-subagent-profile] ${message}`),
82
+ onDecision: () => { try { getRefreshAdvice()(); } catch { /* 聚合重算失败不影响判定(fail-soft) */ } },
83
+ onDecided: onUnadopted,
84
+ });
85
+ const onSessionEvent = (session, event) => adoptionTracker.handleEvent(session, event);
86
+ ctx.on('session/event', onSessionEvent);
87
+ ctx.effect(() => () => { ctx.off('session/event', onSessionEvent); adoptionTracker.dispose(); });
88
+ return adoptionTracker;
89
+ }
90
+
91
+ // 卸载接线:注销工具 + 清空台账 + 级联取消 + 删数据文件与预设目录。agent/disposed
92
+ // 挂钩按父释放并发/token 记账,并把该父会话未决采纳判定判「明确未采纳」。
93
+ function registerTeardown(ctx, dispatch, ledger, guard, home, backgroundLedger, adoptionTracker) {
86
94
  ctx.effect(() => dispatch.dispose);
87
95
  ctx.effect(() => ledger.clear);
96
+ ctx.effect(() => backgroundLedger.clear);
88
97
  ctx.effect(() => () => {
89
98
  guard.cancelAll();
90
99
  guard.reset();
@@ -94,6 +103,7 @@ function registerTeardown(ctx, dispatch, ledger, guard, home) {
94
103
  const parentSessionId = agent?.session?.header?.id;
95
104
  if (parentSessionId !== undefined && parentSessionId !== null && parentSessionId !== '') {
96
105
  guard.resetParent(parentSessionId);
106
+ adoptionTracker.parentDisposed(parentSessionId);
97
107
  }
98
108
  };
99
109
  ctx.on('agent/disposed', onAgentDisposed);
@@ -159,26 +169,28 @@ function sectionGatePasses(ctx, getEnabled, context) {
159
169
  // dispatch:profiles section 文本。引号引用:description 套引号;为空时显示
160
170
  // 占位符(不套引号)。压平在存储层完成(sanitizeProfile),此处仅负责显示层包裹。
161
171
  // 门控 profiles section 内附一行行为规则提示(非开放的 persona 注入)。
162
- function profileSectionText(store, gate, context) {
172
+ function profileSectionText(store, gate, context, summaries) {
163
173
  if (!gate(context)) return '';
164
- const rows = [...store.profiles.values()]
165
- .filter((p) => p.enabled !== false)
166
- .sort((a, b) => tierSortKey(a.tokenTier) - tierSortKey(b.tokenTier))
167
- .map((p) => {
168
- const desc = typeof p.description === 'string' && p.description.length > 0 ? `"${p.description}"` : '(无描述)';
169
- return `- ${p.id}: ${desc}${p.preset !== undefined ? ` (preset: ${p.preset})` : ''}`;
170
- });
174
+ const rows = applyProfileStats(profileDirectoryRows(store), profileStatsFromSummaries(store, summaries));
171
175
  if (rows.length === 0) return '';
172
- const note = '- 选择方案时先匹配任务复杂度与方案描述的能力边界,成本只在能力都胜任的方案之间比较——复杂任务不得为省 token 改用能力不足的便宜方案。\n- 别把 1-2 步即可自查/可搜完的小事委派出去 —— 几分钟内能自查完的直接做。';
173
- return `Available dispatch profiles (dispatch.profile):\n${rows.join('\n')}\n${note}`;
176
+ const lines = rows.map((p) => {
177
+ const desc = p.description.length > 0 ? `"${p.description}"` : '(无描述)';
178
+ const meta = [];
179
+ if (p.preset !== undefined) meta.push(`preset: ${p.preset}`);
180
+ else if (p.model !== undefined) meta.push(`model: ${p.model}`);
181
+ if (p.tokenTier !== undefined) meta.push(`tier: ${p.tokenTier}`);
182
+ if (p.avgCost !== undefined) meta.push(`平均 ${p.avgCost} 元/次`);
183
+ if (p.successRate !== undefined) meta.push(`成功率 ${p.successRate}${p.n !== undefined ? ` (N=${p.n})` : ''}`);
184
+ return `- ${p.id}: ${desc}${meta.length > 0 ? ` (${meta.join(' · ')})` : ''}`;
185
+ });
186
+ const note = '- 选择方案时先匹配任务复杂度与方案描述的能力边界,成本只在能力都胜任的方案之间比较——复杂任务不得为省 token 改用能力不足的便宜方案。\n- 别把 1-2 步即可自查/可搜完的小事委派出去 —— 几分钟内能自查完的直接做。\n- 优先使用已保存的 profile;只有少数字段需要临时覆盖时才传 per-call 参数。查资料/汇总/读文件等轻任务优先 cheap 或 flash。';
187
+ return `Available dispatch profiles (dispatch.profile):\n${lines.join('\n')}\n${note}`;
174
188
  }
175
189
 
176
190
  // orchestrator:mode section 文本(逐字保留;与 profiles 同门控)。
177
191
  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 / 主协调模式」时即指本预设,请据此协作。';
178
192
 
179
- // 建议注入候选池:system-trust 白名单(agentPresets 可选;解析失败回退内置
180
- // 回退名单——fail-loud 检查在建议生成路径进行,不在此处)。刻意不叠加逃生舱放行
181
- // 集:只读建议不享受逃生舱(逃生舱仅放行「其余三道闸全过」的显式派发,不扩建议池)。
193
+ // 建议注入候选池:受信任预设白名单(解析失败回退内置名单)。不叠加逃生舱放行集。
182
194
  async function resolveAdviceWhitelist(ctx) {
183
195
  try {
184
196
  return new Set(await resolveWhitelist(ctx.get('agentPresets')));
@@ -187,17 +199,26 @@ async function resolveAdviceWhitelist(ctx) {
187
199
  }
188
200
  }
189
201
 
190
- // 目录 section:渲染可用方案(systemPrompt section text 接受函数,官方
191
- // tool-subagent 即此用法)。门控注意:`enabled` 必须经 getter 实时读取,
192
- // 使 /set-enabled 切换立即生效、无需重启。
193
- function registerSystemPromptSections(ctx, store, getEnabled, getEvolutionAdvice, adviceEnv) {
202
+ async function setupEvolution(ctx, home, store, catalog, evoLedger, getEvolutionAdvice) {
203
+ const adviceWhitelist = await resolveAdviceWhitelist(ctx);
204
+ const adviceEnv = { summariesFile: join(home, 'subagent-evolution', 'summaries.json'), dispatchFile: join(home, 'subagent-evolution', 'dispatch.jsonl'), whitelist: adviceWhitelist, logger: ctx.logger, onLoss: () => evoLedger.markMigrationLoss() };
205
+ const { adviceSource, evolution } = createEvolutionAssembly({ ctx, home, store, catalog, evoLedger, adviceWhitelist, getEvolutionAdvice, pluginVersion: readPluginVersion() });
206
+ return { adviceEnv, adviceSource, evolution };
207
+ }
208
+
209
+ // 系统提示各 section:门控经 sectionGatePasses;`enabled` 经 getter 实时读取。
210
+ function registerSystemPromptSections(ctx, store, getEnabled, getEvolutionAdvice, adviceEnv, guard) {
194
211
  const pluginSystemPrompt = ctx.get('systemPrompt');
195
212
  if (pluginSystemPrompt === undefined) return;
196
213
  const gate = (context) => sectionGatePasses(ctx, getEnabled, context);
197
214
  pluginSystemPrompt.section({
198
215
  name: 'dispatch:profiles',
199
216
  order: 116.5,
200
- text: (context) => profileSectionText(store, gate, context),
217
+ text: (context) => {
218
+ let summaries = null;
219
+ try { summaries = readSummaries(adviceEnv.summariesFile, adviceEnv.logger, { onLoss: adviceEnv.onLoss }); } catch { /* 统计只增强,不影响目录注入 */ }
220
+ return profileSectionText(store, gate, context, summaries);
221
+ },
201
222
  });
202
223
  // 宣告自装的 orchestrator 预设,让当前 agent 知道该模式存在、可引导用户使用。
203
224
  // 门控与 profiles 段相同——只有可派发的 agent 才看得到。
@@ -206,10 +227,7 @@ function registerSystemPromptSections(ctx, store, getEnabled, getEvolutionAdvice
206
227
  order: 117,
207
228
  text: (context) => (gate(context) ? ORCHESTRATOR_MODE_TEXT : ''),
208
229
  });
209
- // 只读建议段:门控 = orchestrator(sectionGatePasses)+ evolutionAdvice 开关。
210
- // 只进父 Agent(复用同一门控)、默认关、只含确定性聚合数字与建议文案,不含派生原文。
211
- // 生成异常 try/catch 兜底:fail-loud 检查(非 system 候选)落 warn 日志但绝不
212
- // 阻断每次提示装配——损坏/手改的 summaries 不得让 orchestrator 会话无法启动。
230
+ // 派发优化建议段:门控 = orchestrator + evolutionAdvice 开关;异常兜底不阻断装配。
213
231
  pluginSystemPrompt.section({
214
232
  name: 'evolution:advice',
215
233
  order: 116.8,
@@ -218,11 +236,26 @@ function registerSystemPromptSections(ctx, store, getEnabled, getEvolutionAdvice
218
236
  try {
219
237
  return buildAdviceText(adviceEnv);
220
238
  } catch (error) {
221
- adviceEnv.logger.warn(`[dsh-subagent-profile] evolution:advice 生成失败,本次不注入:${error instanceof Error ? error.message : String(error)}`);
222
- return '';
239
+ // 候选校验失败(如逃生舱放行的非 system 预设)不再静默空转——logger.warn
240
+ // 留痕 + 注入段标注「建议暂不可用及原因」,父 Agent 可见、派发行为不受影响。
241
+ const reason = error instanceof Error ? error.message : String(error);
242
+ adviceEnv.logger.warn(`[dsh-subagent-profile] evolution:advice 生成失败:${reason}`);
243
+ return `(派发优化建议暂不可用:${reason}。派发行为不受影响。)`;
223
244
  }
224
245
  },
225
246
  });
247
+ // 派发预算余量段:与 profiles 同门控;无父 sessionId 不注入(无法关联具体会话)。
248
+ pluginSystemPrompt.section({
249
+ name: 'dispatch:budget',
250
+ order: 116.7,
251
+ text: (context) => {
252
+ if (!gate(context)) return '';
253
+ const parentSessionId = context?.agent?.session?.header?.id;
254
+ if (typeof parentSessionId !== 'string' || parentSessionId === '') return '';
255
+ const b = guard.snapshot(parentSessionId);
256
+ return `派发预算余量:本会话在途 ${b.concurrency}/${b.maxConcurrent},累计 token ${b.tokens}/${b.maxParentTokens},剩余 ${Math.max(0, b.maxParentTokens - b.tokens)} token。`;
257
+ },
258
+ });
226
259
  }
227
260
 
228
261
  // Client 设置 UI 的 HTTP loopback 路由(webServer.register ↔ client fetch,纯 JSON)。
@@ -230,7 +263,7 @@ function registerSystemPromptSections(ctx, store, getEnabled, getEvolutionAdvice
230
263
  // webServer 可选——无头部署保留 dispatch 工具、只丢设置页。webServer 的激活
231
264
  // (listen)是异步的,可能晚于本插件 inject 依赖解析完成,故在等它的 inject
232
265
  // 子 scope 内注册(apply 时 ctx.get 会读到 undefined)。
233
- function registerSettingsRoutes(ctx, store, getEnabled, setEnabled, syncTool, catalog, ledger, getAudit, getEvolutionAdvice, setEvolutionAdvice, getEscapeEnabled, setEscapeEnabled, escape, refreshAdvice) {
266
+ function registerSettingsRoutes(ctx, store, getEnabled, setEnabled, syncTool, catalog, ledger, backgroundLedger, draftsStore, applyDraft, getAudit, getEvolutionAdvice, setEvolutionAdvice, getEscapeEnabled, setEscapeEnabled, escape, refreshAdvice, summariesFile, dispatchFile, adviceWhitelist, previewDraft, reminderStore, adviceSource, evolution) {
234
267
  ctx.inject(['webServer'], (scope) => {
235
268
  scope.effect(createHttpRoutes({
236
269
  webServer: scope.webServer,
@@ -240,6 +273,9 @@ function registerSettingsRoutes(ctx, store, getEnabled, setEnabled, syncTool, ca
240
273
  syncTool,
241
274
  catalog,
242
275
  ledger,
276
+ backgroundLedger,
277
+ draftsStore,
278
+ applyDraft,
243
279
  getAudit,
244
280
  getEvolutionAdvice,
245
281
  setEvolutionAdvice,
@@ -247,6 +283,13 @@ function registerSettingsRoutes(ctx, store, getEnabled, setEnabled, syncTool, ca
247
283
  setEscapeEnabled,
248
284
  escape,
249
285
  refreshAdvice,
286
+ summariesFile,
287
+ dispatchFile,
288
+ adviceWhitelist,
289
+ previewDraft,
290
+ reminderStore,
291
+ adviceSource,
292
+ evolution,
250
293
  logger: ctx.logger,
251
294
  }), 'dsh-subagent-profile: settings routes');
252
295
  });
@@ -277,7 +320,8 @@ function readPluginVersion() {
277
320
 
278
321
  // 逃生舱装配:放行集 store + 开关态(state.json 持久化)+ 访问器。开关态经对象字段
279
322
  // 可变(/set-escape 改写),getEscapeSet 开关关时恒空数组(零叠加、零放行审计)。
280
- function setupEscape(ctx, store, evoLedger, home) {
323
+ // 放行时 mint P0(紧急)提醒(高可见治理事件,与放行审计同源留痕)。
324
+ function setupEscape(ctx, store, evoLedger, home, getReminderStore) {
281
325
  const escape = createEscapeStore({ dshHome: home, logger: ctx.logger, onGovernanceFailure: () => evoLedger.markGovernanceFailure() });
282
326
  const state = { escapeEnabled: store.loadEscapeEnabled() };
283
327
  return {
@@ -285,13 +329,16 @@ function setupEscape(ctx, store, evoLedger, home) {
285
329
  getEscapeSet: () => (state.escapeEnabled ? escape.list() : []),
286
330
  getEscapeEnabled: () => state.escapeEnabled,
287
331
  setEscapeEnabled: (next) => { state.escapeEnabled = next; },
288
- recordEscapeAllowProvider: (parent, preset) => recordEscapeAllowProvider(evoLedger, parent, preset),
332
+ recordEscapeAllowProvider: (parent, preset) => {
333
+ recordEscapeAllowProvider(evoLedger, parent, preset);
334
+ mintReminder(getReminderStore(), { severity: 'P0', kind: 'escape-allow', title: '逃生舱放行非官方预设', detail: '预设 "' + preset + '" 经逃生舱放行,其余安全检查仍全量生效', sessionId: parent?.session?.header?.id });
335
+ },
289
336
  };
290
337
  }
291
338
 
292
339
  // `dispatch` 工具装配(defineTool + execute + syncTool)。须在 HTTP inject 之前
293
340
  // 构造,使 createHttpRoutes 能拿 dispatch.syncTool 供 /set-enabled 注册/注销。
294
- function createDispatch(ctx, store, catalog, ledger, guard, evoLedger, getEscapeSet, getEnabled) {
341
+ function createDispatch(ctx, store, catalog, ledger, guard, evoLedger, backgroundLedger, adoptionTracker, getEscapeSet, getEnabled, getEvolutionAdvice) {
295
342
  return createDispatchTool({
296
343
  register: (tool) => ctx.tools.register(tool),
297
344
  store,
@@ -303,18 +350,35 @@ function createDispatch(ctx, store, catalog, ledger, guard, evoLedger, getEscape
303
350
  ledger,
304
351
  guard,
305
352
  evoLedger,
353
+ backgroundLedger,
354
+ adoptionTracker,
306
355
  getEscapeSet,
356
+ getEvolutionAdvice,
307
357
  });
308
358
  }
309
359
 
360
+ // auto-profile 落库:三道安全检查评估(draft-gates)→ 任一 fail 即 throw → 落库 + 审计。
361
+ async function applyDraftProfile({ ctx, store, catalog, getEscapeSet, evoLedger, draft }) {
362
+ const { ok, checks, clean } = await assessDraftProfile({ ctx, store, catalog, getEscapeSet, draft });
363
+ if (!ok) {
364
+ const failed = checks.find((c) => c.verdict === 'fail');
365
+ throw new Error(failed !== undefined && typeof failed.reason === 'string' ? failed.reason : 'draft 未通过安全检查');
366
+ }
367
+ store.profiles.set(clean.id, { ...clean, persisted: true });
368
+ const persisted = store.persistProfiles();
369
+ evoLedger.recordGovernanceAudit({ kind: 'draft-apply', profileId: clean.id, source: typeof draft.source === 'string' ? draft.source : 'human' });
370
+ return { id: clean.id, persisted: persisted.persisted };
371
+ }
372
+
310
373
  export async function apply(ctx) {
311
374
  const home = dshHome();
312
- // 启动清理:上个生命周期留下的 .removed-* 备份残留(S1 改名的副产物)。
313
- cleanRemovedBackups(home);
375
+ cleanRemovedBackups(home); // 上个生命周期留下的 .removed-* 备份残留。
376
+ // reminderStore 变量提前声明:evoLedger.onAlert 与 escape mint 钩子延迟读取。
377
+ let reminderStore;
314
378
  // 派发台账 + 审计分级须在 store 之前构造:store 的治理审计钩子指向其 markGovernanceFailure。
315
- const evoLedger = createEvolutionLedger({ dshHome: home, pluginVersion: readPluginVersion(), warn: (message) => ctx.logger.warn(`[dsh-subagent-profile] ${message}`) });
379
+ 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 }) });
316
380
  const store = createProfileStore({ dshHome: home, logger: ctx.logger, onGovernanceFailure: () => evoLedger.markGovernanceFailure() });
317
- const escapeCtl = setupEscape(ctx, store, evoLedger, home);
381
+ const escapeCtl = setupEscape(ctx, store, evoLedger, home, () => reminderStore);
318
382
  let enabled = store.loadEnabled();
319
383
  let evolutionAdvice = store.loadEvolutionAdvice();
320
384
  store.loadProfiles();
@@ -323,21 +387,35 @@ export async function apply(ctx) {
323
387
  const catalog = createSharedCatalog(ctx);
324
388
  const ledger = createFailureLedger({ warn: (message) => ctx.logger.warn(`[dsh-subagent-profile] ${message}`), stateFile: join(home, 'subagent-profiles.failed-traces.json') });
325
389
  const guard = createDispatchGuard({ warn: (message) => ctx.logger.warn(`[dsh-subagent-profile] ${message}`) });
326
- const dispatch = createDispatch(ctx, store, catalog, ledger, guard, evoLedger, escapeCtl.getEscapeSet, () => enabled);
390
+ const backgroundLedger = createBackgroundLedger({ warn: (message) => ctx.logger.warn(`[dsh-subagent-profile] ${message}`) });
391
+ const draftsStore = createDraftsStore({ dshHome: home, onGovernanceFailure: () => evoLedger.markGovernanceFailure() });
392
+ reminderStore = createReminderStore({ dshHome: home, audit: (entry) => evoLedger.recordGovernanceAudit(entry), warn: (message) => ctx.logger.warn(`[dsh-subagent-profile] ${message}`) });
393
+ let refreshAdvice = () => {};
394
+ const adoptionTracker = setupAdoptionTracker(ctx, home, () => refreshAdvice, (rec) => mintUnadoptedReminder(reminderStore, evoLedger, rec));
395
+ const dispatch = createDispatch(ctx, store, catalog, ledger, guard, evoLedger, backgroundLedger, adoptionTracker, escapeCtl.getEscapeSet, () => enabled, () => evolutionAdvice);
327
396
  provideProfileService(ctx, store);
328
- const adviceWhitelist = await resolveAdviceWhitelist(ctx);
329
- const adviceEnv = { summariesFile: join(home, 'subagent-evolution', 'summaries.json'), dispatchFile: join(home, 'subagent-evolution', 'dispatch.jsonl'), whitelist: adviceWhitelist, logger: ctx.logger };
330
- // 生产聚合触发点(T1 修复):/options/refresh 手动刷新时重算 summaries.json,让
331
- // evolution:advice 有初始生成路径(原先 computeSummaries/writeSummaries 只被测试调用)。
332
- const refreshAdvice = () => refreshSummaries({
333
- dispatchFile: join(home, 'subagent-evolution', 'dispatch.jsonl'),
334
- summariesFile: join(home, 'subagent-evolution', 'summaries.json'),
335
- logger: ctx.logger,
336
- });
337
- registerSystemPromptSections(ctx, store, () => enabled, () => evolutionAdvice, adviceEnv);
397
+ const { adviceEnv, adviceSource, evolution } = await setupEvolution(ctx, home, store, catalog, evoLedger, () => evolutionAdvice);
398
+ // 生产聚合触发点(T1 修复):/options/refresh /list 触发的惰性重算;parent_adopted
399
+ // 的「已确认未采纳」计数经 opts 惰性 join 进 weighted_success(-0.3 惩罚)。
400
+ refreshAdvice = () => refreshSummaries({ dispatchFile: adviceEnv.dispatchFile, summariesFile: adviceEnv.summariesFile, logger: ctx.logger, opts: { parentAdoptedConfirmedFalse: adoptionTracker.confirmedFalseCounts() } });
401
+ registerSystemPromptSections(ctx, store, () => enabled, () => evolutionAdvice, adviceEnv, guard);
338
402
  const disposeProvider = createProfileProvider({ subagents: ctx.subagents, store, getEnabled: () => enabled, logger: ctx.logger, catalog, getEscapeSet: escapeCtl.getEscapeSet, recordEscapeAllowProvider: escapeCtl.recordEscapeAllowProvider });
339
403
  if (typeof disposeProvider === 'function') ctx.effect(() => disposeProvider);
404
+ const applyDraft = (draft) => applyDraftProfile({ ctx, store, catalog, getEscapeSet: escapeCtl.getEscapeSet, evoLedger, draft });
405
+ // /draft/preview:只读安全检查预览(不落库)。body 为 {config, name, description, source}。
406
+ const previewDraft = (body) => assessDraftProfile({
407
+ ctx,
408
+ store,
409
+ catalog,
410
+ getEscapeSet: escapeCtl.getEscapeSet,
411
+ draft: {
412
+ config: body !== null && typeof body === 'object' && typeof body.config === 'object' && body.config !== null ? body.config : {},
413
+ name: body !== null && typeof body === 'object' && typeof body.name === 'string' ? body.name : '',
414
+ description: body !== null && typeof body === 'object' && typeof body.description === 'string' ? body.description : '',
415
+ source: body !== null && typeof body === 'object' && typeof body.source === 'string' ? body.source : 'human',
416
+ },
417
+ });
340
418
  // Client 设置 UI 的 HTTP loopback 路由 —— lib/core/http-routes.mjs。
341
- registerSettingsRoutes(ctx, store, () => enabled, (next) => { enabled = next; }, dispatch.syncTool, catalog, ledger, () => evoLedger.auditState(), () => evolutionAdvice, (next) => { evolutionAdvice = next; }, escapeCtl.getEscapeEnabled, escapeCtl.setEscapeEnabled, escapeCtl.escape, refreshAdvice);
342
- registerTeardown(ctx, dispatch, ledger, guard, home);
419
+ 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, adviceSource, evolution);
420
+ registerTeardown(ctx, dispatch, ledger, guard, home, backgroundLedger, adoptionTracker);
343
421
  }