@lmzhen/dsh-evolution-commands 0.26.15 → 0.27.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.
package/lib/index.js CHANGED
@@ -1,13 +1,470 @@
1
1
  import { createRequire } from "node:module";
2
+ import { DEFAULT_SKILL_LIMITS, DIAGNOSTICS_FACT_KEYS, DIAGNOSTICS_RUNTIME_KEYS, DIAGNOSTICS_VIEW_SCHEMA_VERSION, EVOLUTION_PRESET_IDS, MAINTAINER_PRESET_ID, MAX_TIMER_DELAY_MS, PARAM_EXPOSURE, PARAM_GROUP_LABELS, PLATFORM_LINE, REVIEWER_PRESET_ID, absentHostServices, appendEvolutionEvent, applyNamespaceMigration, asRecord, assertSkillsRootAliasRetired, atomicWriteFiles, authorityFrom, buildLearnPrompt, canonicalWriteId, clampedNumber, classifyNamespaceMigration, classifySessionEvent, createSubagentChannel, elapsedSince, errorText, eventsFile, evolutionHome, evolutionRoot, hostSurface, isDeprecatedParamId, isPresent, isRecord, isUnknown, liveBool, newRunRegistry, newSkillLibrary, paramSettingsId, partitionVersions, planNamespaceMigration, policyStageLimits, probeMtime, probeText, reportsSweepLockTarget, resolveParamId, resolveRootConfig, scopedPackageName, scopedProbeReport, sweepReports, transactIo } from "@lmzhen/dsh-evolution-core";
3
+ import { HOST_ROUTES, json, methodNotAllowed, registerHostRoutes } from "@lmzhen/dsh-evolution-core/http-routes";
4
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
2
7
  import z from "@deepseek-ai/schemastery";
3
8
  import { effectiveSessionPolicy } from "@lmzhen/dsh-evolution-approval";
4
9
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
5
- import { DEFAULT_SKILL_LIMITS, MAX_TIMER_DELAY_MS, PARAM_EXPOSURE, PARAM_GROUP_LABELS, absentHostServices, appendEvolutionEvent, applyNamespaceMigration, asRecord, assertSkillsRootAliasRetired, atomicWriteFiles, buildLearnPrompt, canonicalWriteId, clampedNumber, classifyNamespaceMigration, composePresetEntry, elapsedSince, errorText, eventsFile, evolutionHome, evolutionRoot, isDeprecatedParamId, isPresent, isRecord, isUnknown, liveBool, mergePresetRow, newRunRegistry, newSkillLibrary, paramSettingsId, partitionVersions, planNamespaceMigration, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, probeText, reportsSweepLockTarget, resolveBasePresetPatch, resolveParamId, resolveRootConfig, scopedProbeReport, sweepReports, transactIo } from "@lmzhen/dsh-evolution-core";
10
+ import { composePresetEntry, mergePresetRow, presetPatchText, presetRowBlock, presetRowBody, presetRowId, resolveBasePresetPatch } from "@lmzhen/dsh-evolution-core/composition";
6
11
  import { buildEnrichment, buildMaintainFacts, enrichmentSnapshotOptions, runMaintain, snapshotFromLibrary } from "@lmzhen/dsh-evolution-maintenance";
7
- import { preferProvider } from "@lmzhen/dsh-evolution-io";
8
- import { existsSync, readFileSync, readdirSync } from "node:fs";
9
- import { dirname, join } from "node:path";
10
- import { fileURLToPath } from "node:url";
12
+ //#region lib/types/command-fallback.js
13
+ /**
14
+ * G6/W8: the command-face fallback for every Host data-plane surface.
15
+ *
16
+ * The browser panels depend on the platform's `connection` carrier (exact Fetch routes). When that
17
+ * carrier is absent (headless host, E-325) or a panel answer is missing, the operator is owed the
18
+ * equivalent command — the SAME use case, not a different implementation. This table is the ONE
19
+ * home of those pairs; the INSTALL runbook and the client `fallbacks.generated.ts` are generated
20
+ * from it (W7), and a spec pins its key set to HOST_ROUTES' four surfaces.
21
+ *
22
+ * Not exported from the package: this is a command-side contract, not a plugin API (W8 §2).
23
+ * @module @lmzhen/dsh-evolution-commands/command-fallback
24
+ */
25
+ /**
26
+ * One entry per Host surface. The command column names the same service methods the routes call,
27
+ * so the answers (and refusal sentences) match the panel.
28
+ */
29
+ const COMMAND_FALLBACKS = {
30
+ approval: { command: "/evolution pending … /evolution approve <id> … /evolution reject <id> … /evolution release <id>" },
31
+ "skill-history": { command: "/evolution restore <name> [v](查看:/evolution skills history <name>)" },
32
+ "skill-health": { command: "/evolution skills refresh(健康行:/evolution doctor)" },
33
+ diagnostics: {
34
+ command: "/evolution doctor",
35
+ platformCli: "dsh --json /evolution doctor"
36
+ }
37
+ };
38
+ //#endregion
39
+ //#region lib/types/doctor-view.js
40
+ const FACT_HOMES = {
41
+ "platform-line": "README.md(兼容线声明)",
42
+ "family-version": "README.md(家族版本)",
43
+ "install-form": "packages/INSTALL.md(安装形态与验证状态)",
44
+ "preset-ids": "evolution-core/README.md(preset id 冻结表)",
45
+ "readonly-contract": "evolution-review/README.md(只读契约)",
46
+ "upgrade-order": "packages/INSTALL.md(升级 runbook)",
47
+ "fallback-map": "packages/INSTALL.md(命令兜底表)"
48
+ };
49
+ const FACT_LABELS = {
50
+ "platform-line": "平台兼容线",
51
+ "family-version": "家族版本",
52
+ "install-form": "安装形态",
53
+ "preset-ids": "受限 preset id",
54
+ "readonly-contract": "只读契约",
55
+ "upgrade-order": "升级顺序",
56
+ "fallback-map": "命令兜底"
57
+ };
58
+ const RUNTIME_LABELS = {
59
+ "subagent-channel": "子代理通道",
60
+ "restricted-preset": "受限 preset",
61
+ "host-surface": "Host 数据面",
62
+ "session-fold": "会话事件 fold 单入口",
63
+ "platform-services": "平台服务探测",
64
+ runs: "运行登记",
65
+ "platform-pairing": "平台/家族配对",
66
+ "install-form-detected": "实际安装形态"
67
+ };
68
+ function factsOf(inputs) {
69
+ const values = {
70
+ "platform-line": inputs.platformLine.version + "(floor " + inputs.platformLine.floor + ")",
71
+ "family-version": inputs.familyVersion,
72
+ "install-form": inputs.form.id + "(" + inputs.form.confidence + ";证据:" + inputs.form.evidence.join("、") + ")",
73
+ "preset-ids": REVIEWER_PRESET_ID + " / " + MAINTAINER_PRESET_ID + "(家族 preset 集合:" + EVOLUTION_PRESET_IDS.join(", ") + ")",
74
+ "readonly-contract": REVIEWER_PRESET_ID + " 的工具集由 restrictor allow 集合决定;维护行同构(" + MAINTAINER_PRESET_ID + ")",
75
+ "upgrade-order": "先升平台 → 再升家族 → 最后发布(见 INSTALL runbook)",
76
+ "fallback-map": Object.keys(COMMAND_FALLBACKS).join("、") + "(每面的命令见 INSTALL 命令兜底表)"
77
+ };
78
+ return DIAGNOSTICS_FACT_KEYS.map((key) => ({
79
+ key,
80
+ label: FACT_LABELS[key] ?? key,
81
+ value: values[key] ?? "unknown",
82
+ home: FACT_HOMES[key] ?? "README.md"
83
+ })).sort((a, b) => a.key.localeCompare(b.key));
84
+ }
85
+ function channelRow(channels) {
86
+ const unavailable = [channels.review, channels.maintenance].filter((state) => state.kind === "unavailable");
87
+ if (unavailable.length === 0) return {
88
+ key: "subagent-channel",
89
+ label: RUNTIME_LABELS["subagent-channel"] ?? "子代理通道",
90
+ status: "ok",
91
+ detail: "reviewer/maintainer 通道均可用(spawn provider + agentPreset 能力)"
92
+ };
93
+ const reasons = unavailable.map((state) => state.code + ": " + state.reason);
94
+ return {
95
+ key: "subagent-channel",
96
+ label: RUNTIME_LABELS["subagent-channel"] ?? "子代理通道",
97
+ status: "warn",
98
+ detail: reasons.join(";"),
99
+ remedy: "显式子代理运行会标记为不可用(不会静默退回 inject);运行 /evolution doctor 查看修复指引。"
100
+ };
101
+ }
102
+ function presetRow(presets) {
103
+ const entries = [presets.reviewer, presets.maintainer];
104
+ if (entries.every((entry) => entry === void 0)) return {
105
+ key: "restricted-preset",
106
+ label: RUNTIME_LABELS["restricted-preset"] ?? "受限 preset",
107
+ status: "unknown",
108
+ detail: "agentPresets 未挂载或未暴露 list(),无法探测受限 preset。",
109
+ remedy: "挂载 agentPresets(随平台 Web 组合)后运行 /evolution doctor。"
110
+ };
111
+ const broken = entries.filter((entry) => entry !== void 0 && entry.broken !== void 0);
112
+ const missing = entries.filter((entry) => entry === void 0 || !entry.registered);
113
+ if (broken.length > 0) return {
114
+ key: "restricted-preset",
115
+ label: RUNTIME_LABELS["restricted-preset"] ?? "受限 preset",
116
+ status: "fail",
117
+ detail: errorText("e-324-restricted-preset-unavailable", { a1: broken.map((entry) => entry.broken ?? "").join(";") }),
118
+ remedy: "重载插件行使 ensureRestrictedPreset 重新注册;运行 /evolution doctor 查看 preset id 与 allow 集合。"
119
+ };
120
+ if (missing.length > 0) return {
121
+ key: "restricted-preset",
122
+ label: RUNTIME_LABELS["restricted-preset"] ?? "受限 preset",
123
+ status: "warn",
124
+ detail: "受限 preset 尚未注册(首次显式子代理运行时创建)。",
125
+ remedy: "运行一次审阅/维护,或在挂载后运行 /evolution doctor。"
126
+ };
127
+ return {
128
+ key: "restricted-preset",
129
+ label: RUNTIME_LABELS["restricted-preset"] ?? "受限 preset",
130
+ status: "ok",
131
+ detail: REVIEWER_PRESET_ID + " / " + MAINTAINER_PRESET_ID + " 均已注册"
132
+ };
133
+ }
134
+ function hostRow(host, count) {
135
+ if (!host.mounted) {
136
+ const fallback = COMMAND_FALLBACKS.approval.command.split(" … ")[0] ?? "";
137
+ return {
138
+ key: "host-surface",
139
+ label: RUNTIME_LABELS["host-surface"] ?? "Host 数据面",
140
+ status: "warn",
141
+ detail: errorText("e-325-host-data-plane-unavailable") + "(" + (host.reason ?? "unknown") + ")",
142
+ remedy: "改用命令面完成同等操作:" + fallback + " 等;运行 /evolution doctor 查看已挂载服务。"
143
+ };
144
+ }
145
+ const remote = host.deploymentAllowsRemote === true ? "允许远程权限(平台准入策略原样报告)" : "仅本机权限(平台准入策略原样报告)";
146
+ const mounted = count === void 0 ? "已挂载" : "已挂载 " + String(count.mounted) + "/" + String(count.total);
147
+ return {
148
+ key: "host-surface",
149
+ label: RUNTIME_LABELS["host-surface"] ?? "Host 数据面",
150
+ status: "ok",
151
+ detail: mounted + ";" + remote
152
+ };
153
+ }
154
+ function pairingRow(inputs) {
155
+ switch (inputs.platformPairing ?? "unknown") {
156
+ case "in-window": return {
157
+ key: "platform-pairing",
158
+ label: RUNTIME_LABELS["platform-pairing"] ?? "平台/家族配对",
159
+ status: "ok",
160
+ detail: "宿主版本 " + String(inputs.hostVersion) + " 在兼容窗口 " + inputs.platformLine.version + " 内"
161
+ };
162
+ case "host-newer": return {
163
+ key: "platform-pairing",
164
+ label: RUNTIME_LABELS["platform-pairing"] ?? "平台/家族配对",
165
+ status: "warn",
166
+ detail: "宿主 patch 线已更新(" + String(inputs.hostVersion) + ",家族声明 " + inputs.platformLine.version + ")。",
167
+ remedy: "按升级 runbook 先升平台线再升家族。"
168
+ };
169
+ case "out-of-window": return {
170
+ key: "platform-pairing",
171
+ label: RUNTIME_LABELS["platform-pairing"] ?? "平台/家族配对",
172
+ status: "fail",
173
+ detail: "宿主版本 " + String(inputs.hostVersion) + " 落在兼容窗口(floor " + inputs.platformLine.floor + ")外。",
174
+ remedy: "先升/降平台到兼容窗口,再重载家族。"
175
+ };
176
+ case "unknown": return {
177
+ key: "platform-pairing",
178
+ label: RUNTIME_LABELS["platform-pairing"] ?? "平台/家族配对",
179
+ status: "unknown",
180
+ detail: inputs.hostVersion === void 0 ? "读不到宿主版本。" : "无法判定版本窗口。",
181
+ remedy: "在宿主会话里运行 /evolution doctor。"
182
+ };
183
+ }
184
+ }
185
+ function runtimeOf(inputs) {
186
+ const missing = inputs.missingServices ?? [];
187
+ const fold = inputs.sessionFoldClassifierPresent !== false;
188
+ return [
189
+ channelRow(inputs.channels),
190
+ presetRow(inputs.presets),
191
+ hostRow(inputs.host, inputs.hostRouteCount),
192
+ fold ? {
193
+ key: "session-fold",
194
+ label: RUNTIME_LABELS["session-fold"] ?? "会话事件 fold 单入口",
195
+ status: "ok",
196
+ detail: "classifySessionEvent/isIgnorableEvent 存在:model 消费,plugin-record/unknown 忽略。"
197
+ } : {
198
+ key: "session-fold",
199
+ label: RUNTIME_LABELS["session-fold"] ?? "会话事件 fold 单入口",
200
+ status: "fail",
201
+ detail: "分类器缺失,无法证明单一 fold 入口。",
202
+ remedy: "升级家族(classifySessionEvent 是 core 根导出)。"
203
+ },
204
+ missing.length === 0 ? {
205
+ key: "platform-services",
206
+ label: RUNTIME_LABELS["platform-services"] ?? "平台服务探测",
207
+ status: "ok",
208
+ detail: "探测表中的服务均已挂载"
209
+ } : {
210
+ key: "platform-services",
211
+ label: RUNTIME_LABELS["platform-services"] ?? "平台服务探测",
212
+ status: "warn",
213
+ detail: "缺少:" + missing.join("、"),
214
+ remedy: "安装行(evolution-host/evolution-all)或检查 profile 行。"
215
+ },
216
+ inputs.runs.orphaned > 0 ? {
217
+ key: "runs",
218
+ label: RUNTIME_LABELS.runs ?? "运行登记",
219
+ status: "warn",
220
+ detail: "running=" + String(inputs.runs.running) + ",orphaned=" + String(inputs.runs.orphaned),
221
+ remedy: "运行 /evolution runs 收敛遗留运行。"
222
+ } : {
223
+ key: "runs",
224
+ label: RUNTIME_LABELS.runs ?? "运行登记",
225
+ status: "ok",
226
+ detail: "running=" + String(inputs.runs.running) + ",orphaned=0"
227
+ },
228
+ pairingRow(inputs),
229
+ inputs.form.confidence === "declared" ? {
230
+ key: "install-form-detected",
231
+ label: RUNTIME_LABELS["install-form-detected"] ?? "实际安装形态",
232
+ status: "ok",
233
+ detail: inputs.form.id + "(声明)"
234
+ } : {
235
+ key: "install-form-detected",
236
+ label: RUNTIME_LABELS["install-form-detected"] ?? "实际安装形态",
237
+ status: "warn",
238
+ detail: inputs.form.id + "(推断:" + inputs.form.evidence.join("、") + ")",
239
+ remedy: "在 profile 里显式声明安装行,核对 INSTALL 的形态表。"
240
+ }
241
+ ].sort((a, b) => a.key.localeCompare(b.key));
242
+ }
243
+ /**
244
+ * Build the frozen DiagnosticsView. Pure; never throws.
245
+ * @param inputs - everything the 7+8 rows read (assembleDoctorInputs probes the live host).
246
+ */
247
+ function buildDoctorView(inputs) {
248
+ return {
249
+ schemaVersion: DIAGNOSTICS_VIEW_SCHEMA_VERSION,
250
+ generatedAt: inputs.now.toISOString(),
251
+ facts: factsOf(inputs),
252
+ runtime: runtimeOf(inputs)
253
+ };
254
+ }
255
+ /** A safe view for a host whose inputs could not be assembled: every row names the failure. */
256
+ function unavailableDoctorView(now, reason) {
257
+ const facts = DIAGNOSTICS_FACT_KEYS.map((key) => ({
258
+ key,
259
+ label: FACT_LABELS[key] ?? key,
260
+ value: "unknown",
261
+ home: FACT_HOMES[key] ?? "README.md"
262
+ }));
263
+ const runtime = DIAGNOSTICS_RUNTIME_KEYS.map((key) => ({
264
+ key,
265
+ label: RUNTIME_LABELS[key] ?? key,
266
+ status: "unknown",
267
+ detail: "无法装配 doctor 输入:" + reason,
268
+ remedy: "运行 /evolution doctor 重试;若持续失败,检查宿主日志。"
269
+ }));
270
+ return {
271
+ schemaVersion: DIAGNOSTICS_VIEW_SCHEMA_VERSION,
272
+ generatedAt: now.toISOString(),
273
+ facts: facts.sort((a, b) => a.key.localeCompare(b.key)),
274
+ runtime: runtime.sort((a, b) => a.key.localeCompare(b.key))
275
+ };
276
+ }
277
+ /** The family version this build carries (nearest package.json above the running module). */
278
+ function familyVersion() {
279
+ try {
280
+ let dir = dirname(fileURLToPath(import.meta.url));
281
+ for (let hops = 0; hops < 32; hops += 1) {
282
+ const manifest = join(dir, "package.json");
283
+ if (existsSync(manifest)) {
284
+ const parsed = JSON.parse(readFileSync(manifest, "utf8"));
285
+ if (typeof parsed.version === "string") return parsed.version;
286
+ }
287
+ const parent = dirname(dir);
288
+ if (parent === dir) break;
289
+ dir = parent;
290
+ }
291
+ } catch {}
292
+ return "unknown";
293
+ }
294
+ /** The soft service probe list for the platform-services row. */
295
+ const SERVICES_PROBED = [
296
+ "connection",
297
+ "agentPresets",
298
+ "subagents",
299
+ "commands",
300
+ "settings"
301
+ ];
302
+ /** The host's own version, read from the installed platform manifest (soft: absent = unknown). */
303
+ function hostRuntimeVersion() {
304
+ for (const packageName of ["@deepseek-ai/dsh-app-boot", "@deepseek-ai/dsh"]) try {
305
+ const manifest = createRequire(import.meta.url).resolve(packageName + "/package.json");
306
+ const parsed = JSON.parse(readFileSync(manifest, "utf8"));
307
+ if (typeof parsed.version === "string" && parsed.version !== "") return parsed.version;
308
+ } catch {}
309
+ }
310
+ /** The family bundles the platform reports (soft probe: a host without the manager answers []). */
311
+ async function installedFamilyBundles(ctx) {
312
+ const listed = await ctx.get("pluginManager")?.listBundles?.();
313
+ if (!Array.isArray(listed)) return [];
314
+ const names = [];
315
+ for (const entry of listed) {
316
+ if (typeof entry !== "object" || entry === null) continue;
317
+ const name = entry.name;
318
+ if (typeof name === "string" && name.includes("dsh-evolution-")) names.push(name);
319
+ }
320
+ return names;
321
+ }
322
+ /**
323
+ * Assemble the view inputs through the live Context's soft probes.
324
+ *
325
+ * Every probe is optional: an absent service becomes a row value (unknown/warn), never a throw. The
326
+ * channel probe uses W5's own `createSubagentChannel` state machinery, so the doctor reports the
327
+ * SAME availability the review/maintenance runs would see.
328
+ * @param ctx - the host context.
329
+ * @param extras - call-site facts the context cannot answer (runs, host version, install form).
330
+ */
331
+ async function assembleDoctorInputs(ctx, extras = {}) {
332
+ const get = (name) => ctx.get(name);
333
+ const channel = createSubagentChannel(ctx);
334
+ const probeChannel = async (kind) => {
335
+ try {
336
+ return await channel.prepare({
337
+ kind,
338
+ allow: ["skill"]
339
+ });
340
+ } catch (error) {
341
+ return {
342
+ kind: "unavailable",
343
+ code: "E-323",
344
+ reason: error instanceof Error ? error.message : String(error)
345
+ };
346
+ }
347
+ };
348
+ const [review, maintenance] = await Promise.all([probeChannel("reviewer"), probeChannel("maintainer")]);
349
+ const presetsService = get("agentPresets");
350
+ let listed = [];
351
+ try {
352
+ const value = await presetsService?.list?.();
353
+ if (Array.isArray(value)) listed = value;
354
+ } catch {
355
+ listed = [];
356
+ }
357
+ const presetOf = (id) => {
358
+ if (listed.length === 0) return void 0;
359
+ const entry = listed.find((candidate) => candidate.id === id);
360
+ if (entry === void 0) return {
361
+ id,
362
+ registered: false
363
+ };
364
+ return {
365
+ id,
366
+ registered: true,
367
+ ...typeof entry.broken === "string" ? { broken: entry.broken } : {}
368
+ };
369
+ };
370
+ const bundles = await installedFamilyBundles(ctx);
371
+ const hasAll = bundles.some((name) => name.endsWith("dsh-evolution-all"));
372
+ const hasPreset = bundles.some((name) => name.endsWith("dsh-evolution-preset"));
373
+ const hasHost = bundles.some((name) => name.endsWith("dsh-evolution-host"));
374
+ const hasFamilyPreset = listed.some((entry) => typeof entry.id === "string" && EVOLUTION_PRESET_IDS.includes(entry.id));
375
+ const detectedForm = hasAll || hasPreset ? {
376
+ id: "M1",
377
+ evidence: [hasAll ? "bundle dsh-evolution-all installed" : "bundle dsh-evolution-preset installed"],
378
+ confidence: "declared"
379
+ } : hasHost ? hasFamilyPreset ? {
380
+ id: "M4",
381
+ evidence: ["bundle dsh-evolution-host installed", "family agent preset registered"],
382
+ confidence: "declared"
383
+ } : {
384
+ id: "M3",
385
+ evidence: ["bundle dsh-evolution-host installed"],
386
+ confidence: "declared"
387
+ } : {
388
+ id: "M3",
389
+ evidence: ["no family bundle was reported by the platform manager"],
390
+ confidence: "inferred"
391
+ };
392
+ const platformVersion = hostRuntimeVersion();
393
+ const missingServices = SERVICES_PROBED.filter((name) => get(name) === void 0);
394
+ const host = hostSurface(ctx);
395
+ const hostMounted = host.available;
396
+ return {
397
+ now: extras.now ?? /* @__PURE__ */ new Date(),
398
+ platformLine: {
399
+ version: PLATFORM_LINE.version,
400
+ floor: PLATFORM_LINE.floor
401
+ },
402
+ familyVersion: familyVersion(),
403
+ hostVersion: extras.hostVersion ?? platformVersion,
404
+ form: extras.form ?? detectedForm,
405
+ probes: {
406
+ agentPresets: get("agentPresets") !== void 0,
407
+ connection: get("connection") !== void 0
408
+ },
409
+ channels: {
410
+ review,
411
+ maintenance
412
+ },
413
+ presets: {
414
+ reviewer: presetOf(REVIEWER_PRESET_ID),
415
+ maintainer: presetOf(MAINTAINER_PRESET_ID)
416
+ },
417
+ host: {
418
+ mounted: hostMounted,
419
+ ...hostMounted ? {} : { reason: host.reason },
420
+ ...host.available ? { deploymentAllowsRemote: host.allowsRemoteAuthorities } : {}
421
+ },
422
+ runs: extras.runs ?? {
423
+ running: 0,
424
+ orphaned: 0
425
+ },
426
+ missingServices,
427
+ sessionFoldClassifierPresent: typeof classifySessionEvent === "function",
428
+ platformPairing: extras.platformPairing ?? (platformVersion === void 0 ? "unknown" : platformVersion === PLATFORM_LINE.version ? "in-window" : "host-newer")
429
+ };
430
+ }
431
+ //#endregion
432
+ //#region lib/types/diagnostics-route.js
433
+ /**
434
+ * G6/W8: the diagnostics Host route — the browser face of the SAME DiagnosticsView the CLI prints.
435
+ *
436
+ * Registration belongs to W6 (`registerHostRoutes` is called by the commands plugin's apply), so
437
+ * this module only supplies the handler. A failed input assembly is NOT a transport error: the
438
+ * handler answers an all-unknown view naming the failure, because doctor must never throw (W8 §6).
439
+ * @module @lmzhen/dsh-evolution-commands/diagnostics-route
440
+ */
441
+ /**
442
+ * Build the diagnostics route over one input producer.
443
+ * @param getInputs - assembles the live view inputs (soft probes; may reject).
444
+ * @returns the exact Fetch route for `HOST_ROUTES.diagnostics.view`.
445
+ */
446
+ function makeDiagnosticsRoutes(getInputs) {
447
+ return [{
448
+ path: HOST_ROUTES.diagnostics.view,
449
+ methods: ["GET"],
450
+ requestBody: "buffered",
451
+ fetch: async (request) => {
452
+ if (request.method !== "GET") return methodNotAllowed(["GET"]);
453
+ try {
454
+ return json(200, {
455
+ ok: true,
456
+ data: buildDoctorView(await getInputs())
457
+ });
458
+ } catch (error) {
459
+ return json(200, {
460
+ ok: true,
461
+ data: unavailableDoctorView(/* @__PURE__ */ new Date(), error instanceof Error ? error.message : String(error))
462
+ });
463
+ }
464
+ }
465
+ }];
466
+ }
467
+ //#endregion
11
468
  //#region lib/types/migration.js
12
469
  /**
13
470
  * G3 (caller half): find the legacy settings document and run the namespace migration.
@@ -53,15 +510,18 @@ async function runNamespaceMigration(run) {
53
510
  await settings.update(rowId, patch);
54
511
  };
55
512
  if (settings.describe === void 0 || settings.update === void 0) return NOTHING;
513
+ let seen;
56
514
  for (const source of legacySettingsPaths(run.home)) {
57
515
  const text = await run.io.readText(source);
58
516
  if (text === null) continue;
59
517
  const plan = planNamespaceMigration(text);
60
- if (plan.entries.length === 0) return {
61
- source,
62
- plan,
63
- report: void 0
64
- };
518
+ if (plan.entries.length === 0) {
519
+ seen ??= {
520
+ source,
521
+ plan
522
+ };
523
+ continue;
524
+ }
65
525
  return {
66
526
  source,
67
527
  plan,
@@ -72,7 +532,11 @@ async function runNamespaceMigration(run) {
72
532
  })
73
533
  };
74
534
  }
75
- return NOTHING;
535
+ return seen === void 0 ? NOTHING : {
536
+ source: seen.source,
537
+ plan: seen.plan,
538
+ report: void 0
539
+ };
76
540
  }
77
541
  /**
78
542
  * Read the legacy document and report what is still unmigrated.
@@ -86,11 +550,24 @@ async function runNamespaceMigration(run) {
86
550
  async function readLegacyDocumentState(run) {
87
551
  const settings = run.settings;
88
552
  if (settings?.describe === void 0) return void 0;
553
+ let seen;
89
554
  for (const source of legacySettingsPaths(run.home)) {
90
555
  const text = await run.io.readText(source);
91
556
  if (text === null) continue;
92
557
  const plan = planNamespaceMigration(text);
93
- const read = (rowId) => settings.describe?.({ redactSecrets: false }).find((entry) => entry.ns === rowId);
558
+ if (plan.entries.length === 0) {
559
+ seen ??= {
560
+ source,
561
+ plan
562
+ };
563
+ continue;
564
+ }
565
+ return stateOf(source, plan);
566
+ }
567
+ return seen === void 0 ? void 0 : stateOf(seen.source, seen.plan);
568
+ /** The doctor-facing state of ONE document, judged against the seat. */
569
+ function stateOf(source, plan) {
570
+ const read = (rowId) => settings?.describe?.({ redactSecrets: false }).find((entry) => entry.ns === rowId);
94
571
  const decided = classifyNamespaceMigration(plan, read);
95
572
  return {
96
573
  source,
@@ -625,39 +1102,12 @@ function envIssues() {
625
1102
  if (query && !SESSION_QUERY_MODES.has(query)) issues.push(`DSH_EVOLUTION_SESSION_QUERY="${query}" is not one of startup|first-search|never — the patch normalizes it to 'startup' silently; set it to a listed mode.`);
626
1103
  return issues;
627
1104
  }
628
- /** S0.2 (v37 P0-1): memory files that still carry the platform's `{{` prompt
629
- * variable syntax. The platform interpolates EVERY `systemPrompt.context` text
630
- * once per model step and throws on an unknown/malformed reference, so one such
631
- * entry used to fail every turn of every session under this home. Current
632
- * builds neutralize the injected text (`MemoryStore.renderContext`), which is
633
- * why this is a residual-risk report instead of a hard failure: an older
634
- * still-running process is still bricked, and an operator may prefer to clean
635
- * the file. Read-only; an unreadable file is skipped (the doctor never throws).
636
- * @param home - resolved DSH_HOME.
637
- * @returns one message per affected memory file.
638
- */
639
- function memoryInterpolationIssues(home) {
640
- const issues = [];
641
- for (const file of ["MEMORY.md", "USER.md"]) {
642
- const path = join(home, "memories", file);
643
- if (!existsSync(path)) continue;
644
- let raw;
645
- try {
646
- raw = readFileSync(path, "utf8");
647
- } catch {
648
- continue;
649
- }
650
- const hits = raw.split("{{").length - 1;
651
- if (hits > 0) issues.push(`${file}: ${hits} "{{" occurrence(s) — the platform interpolates this syntax in prompt context and throws on an unknown reference, so a build without the render-time neutralization fails every later turn; rewrite the affected entries`);
652
- }
653
- return issues;
654
- }
655
1105
  /** G3-② (B2): the family delta container. An optionalDependency of this
656
1106
  * package, so resolution may legitimately fail; every failure degrades to
657
1107
  * "unknown" — the doctor never throws.
658
1108
  * Platform anchor: `packages/boot/app-boot/src/profile.ts` — a family preset is a ROW in the
659
1109
  * profile's own patch layer there (`PROFILE_PATCH_FILENAME`), never a preset directory. */
660
- const AGENT_PRESET_PACKAGE = "@lmzhen/dsh-evolution-agent-preset";
1110
+ const AGENT_PRESET_PACKAGE = scopedPackageName("dsh-evolution-agent-preset", import.meta.url);
661
1111
  /** Resolve one asset the package declares in its exports map.
662
1112
  * @param asset - bases.json or agent.cordis.yml.
663
1113
  * @returns the file path, or null when the package is not installed. */
@@ -995,20 +1445,19 @@ async function diagnose(ctx, options = {}) {
995
1445
  if (presetIssueRows.length > 0) actions.push("Resolve the agent-preset finding above: a broken preset composes no session, and a row two presets declare is resolved by layer order (keep the declaration in one preset).");
996
1446
  const legacy = await legacyMigrationRow(ctx, home);
997
1447
  if (legacy.state === "pending") actions.push(`The legacy settings document still holds ${legacy.pending.length} family value(s) — /evolution migrate moves them onto their rows (the document itself is left alone, so nothing is lost)`);
998
- const memoryIssues = memoryInterpolationIssues(home);
999
1448
  const budgetIssues = memoryBudgetIssues(ctx);
1000
1449
  const paramIssues = paramDivergences(platform);
1001
1450
  const queryIssues = await sessionQueryIssues(ctx);
1002
1451
  if (queryIssues.length > 0) actions.push("Session search is degraded: isolate the session named above (or wait for the platform fix described in the family maintenance notes) before retrying the same query");
1003
1452
  if (paramIssues.length > 0) actions.push("Review the parameter divergences above: /evolution params shows every row with its source, /evolution policy set writes a user value, and a declared-but-unreachable row needs its owning plugin row mounted in this profile.");
1004
1453
  if (budgetIssues.length > 0) actions.push("Align the memory budget: leave memory-files memoryCharLimit/userCharLimit UNSET so the store follows evolution-policy, or set both surfaces to the same value (the review plans against the policy value while the store enforces its own)");
1005
- if (memoryIssues.length > 0) actions.push("Rewrite the memory entries listed above (or run a build with the render-time neutralization) — they broke prompt assembly on older builds.");
1006
1454
  if (scopedProbe.rows !== null && scopedProbe.rows.length > 0 && scopedProbe.verdict === "never-hit") actions.push("The session-scoped rows are mounted but the family-tool probe has never matched in this process — review injection and skill-usage telemetry skipped every session observed. HOST-ONLY install: the model rows (tool-memory / tool-skill-manage) sit in evolution-host devDependencies, so no session carries them; install a bundle that mounts them (see INSTALL.md). VARIANT install: only a session on the Evolution preset matches, so open one — a session on a platform original preset is the intended skip, not a fault. Either way, check that no profile overlay disables those two rows.");
1007
1455
  if ((executingCount ?? 0) > 0) actions.push(`${executingCount} staged write(s) are EXECUTING (an approve crashed mid-run — or one is still in flight). Inspect with /evolution pending: if you started the approve, verify the landed write and do not reject it; only reject after verifying no write is intended. For a verified orphan (this build: S2-P2-22), /evolution release <id> returns it to the pending window instead.`);
1008
1456
  for (const row of presetFreshness) {
1009
1457
  if (row.status !== "differs") continue;
1010
1458
  actions.push(`Preset row for base "${row.base}" in ${row.destination} DIFFERS from a fresh generation — it is an install-time snapshot; re-run the installer (/evolution preset install) to regenerate`);
1011
1459
  }
1460
+ const view = buildDoctorView(await assembleDoctorInputs(ctx));
1012
1461
  return {
1013
1462
  installForm,
1014
1463
  deploymentForm,
@@ -1016,7 +1465,6 @@ async function diagnose(ctx, options = {}) {
1016
1465
  foreignProfiles: homeProfiles(home).filter(({ profile, dir }) => mounted === void 0 || profile !== mounted.profile && dir !== mounted.dir).map(({ profile }) => profile),
1017
1466
  conflicts,
1018
1467
  envIssues: env,
1019
- memoryIssues,
1020
1468
  budgetIssues,
1021
1469
  paramIssues,
1022
1470
  queryIssues,
@@ -1032,7 +1480,8 @@ async function diagnose(ctx, options = {}) {
1032
1480
  presetIssues: presetIssueRows,
1033
1481
  dormantBundles,
1034
1482
  serviceSource: reviewRow === void 0 ? "bundles" : "platform",
1035
- actions
1483
+ actions,
1484
+ view
1036
1485
  };
1037
1486
  }
1038
1487
  /**
@@ -1175,13 +1624,35 @@ function renderDoctorText(report) {
1175
1624
  if (report.foreignProfiles.length > 0) lines.push("profiles-foreign: " + report.foreignProfiles.join(", ") + " — not part of this plane: /evolution preset install ignores them, while the bundles (all profiles) line above counts them");
1176
1625
  if (report.conflicts.length > 0) lines.push("conflicts:", ...report.conflicts.map((line) => ` ! ${line}`));
1177
1626
  if (report.envIssues.length > 0) lines.push("env:", ...report.envIssues.map((line) => ` ! ${line}`));
1178
- if (report.memoryIssues.length > 0) lines.push("memory:", ...report.memoryIssues.map((line) => ` ! ${line}`));
1179
1627
  if (report.budgetIssues.length > 0) lines.push("memory budget:", ...report.budgetIssues.map((line) => ` ! ${line}`));
1180
1628
  if (report.paramIssues.length > 0) lines.push("parameters:", ...report.paramIssues.map((line) => ` ! ${line}`));
1181
1629
  if (report.queryIssues.length > 0) lines.push("session search:", ...report.queryIssues.map((line) => ` ! ${line}`));
1182
1630
  if (report.actions.length > 0) lines.push("next steps:", ...report.actions.map((line) => ` → ${line}`));
1183
1631
  return lines.filter((line) => line !== null).join("\n");
1184
1632
  }
1633
+ /**
1634
+ * G6/W8 tail: render the frozen DiagnosticsView as the doctor's diagnostics section.
1635
+ *
1636
+ * This is the ONE text face of the 7+8 rows: the same object `--json` and the diagnostics route
1637
+ * answer, so the three readers cannot disagree. Legacy install/conflict sections keep their own
1638
+ * renderer until the remaining W8-S7 rewrite lands.
1639
+ * @param view - the assembled view.
1640
+ * @returns the section text (ends with a newline).
1641
+ */
1642
+ function renderDiagnosticsText(view) {
1643
+ const lines = [];
1644
+ lines.push("诊断(schema v" + String(view.schemaVersion) + ",生成于 " + view.generatedAt + "):");
1645
+ lines.push(" 事实:");
1646
+ for (const fact of view.facts) lines.push(" - " + fact.label + ":" + fact.value + "(出处:" + fact.home + ")");
1647
+ lines.push(" 运行时:");
1648
+ for (const row of view.runtime) {
1649
+ const status = row.status === "ok" ? "正常" : row.status === "warn" ? "注意" : row.status === "fail" ? "异常" : "未知";
1650
+ const detail = row.detail === void 0 ? "" : ":" + row.detail;
1651
+ const remedy = row.remedy === void 0 ? "" : ";下一步:" + row.remedy;
1652
+ lines.push(" - [" + status + "] " + row.label + detail + remedy);
1653
+ }
1654
+ return lines.join("\n") + "\n";
1655
+ }
1185
1656
  //#endregion
1186
1657
  //#region lib/types/params.js
1187
1658
  /**
@@ -1368,10 +1839,20 @@ function flagsOf(name) {
1368
1839
  const entry = COMMAND_ENTRIES.find((candidate) => entryName(candidate) === name);
1369
1840
  return entry === void 0 ? [] : entryFlags(entry);
1370
1841
  }
1371
- /** C7 shape, DERIVED: the subcommand name is the first token of `usage` (variants use `|`). */
1842
+ /**
1843
+ * C7 shape, DERIVED: the entry IDENTITY key, and it is HIERARCHICAL (E-08).
1844
+ *
1845
+ * The key used to be the first token alone, and `flagsOf` finds an entry with `.find(...)` - the FIRST
1846
+ * match - so `maintain handoff` / `maintain cancel` and `skill restore` / `skill history` / `skill undo` /
1847
+ * `skill pin` all derived the SAME key as their parent and every flag lookup silently saw the parent entry
1848
+ * only (nine collisions). A bare parent (`maintain [--timeout]`) keeps its one-token key, so parent lookups
1849
+ * still resolve: the one call site that named an entry by a name the key no longer produced was the preset
1850
+ * dispatch branch, and it now asks for the entry own key (`preset install`).
1851
+ */
1372
1852
  function entryName(entry) {
1373
- const [head = ""] = entry.usage.split(" ");
1374
- return head.split("|")[0] ?? "";
1853
+ const [head = "", second = ""] = entry.usage.split(" ");
1854
+ const name = head.split("|")[0] ?? "";
1855
+ return /^[a-z][a-z-]*$/.test(second) && !second.startsWith("--") ? name + " " + second : name;
1375
1856
  }
1376
1857
  /** C7 shape, DERIVED: the flags a `usage` declares, in the form `--flag` (value flags keep their name). */
1377
1858
  function entryFlags(entry) {
@@ -1841,7 +2322,7 @@ function apply(ctx, rawConfig = {}) {
1841
2322
  const ioRegistry = ioCtx.get("evolutionIo");
1842
2323
  if (!ioRegistry) return;
1843
2324
  const registry = newRunRegistry({
1844
- io: preferProvider(ioRegistry, ["fs"]),
2325
+ io: ioRegistry.provider(),
1845
2326
  home: evolutionHome(),
1846
2327
  warn: (message) => {
1847
2328
  ctx.logger.warn(message);
@@ -1859,6 +2340,22 @@ function apply(ctx, rawConfig = {}) {
1859
2340
  runs = void 0;
1860
2341
  }, "evolution-commands.run-registry");
1861
2342
  });
2343
+ /** G6/W8: the run-registry snapshot the diagnostics `runs` row reads (one owner: the registry). */
2344
+ const doctorRuns = () => {
2345
+ try {
2346
+ const running = (runs?.runs() ?? []).filter((record) => record.state === "running").length;
2347
+ const active = runs?.inFlight();
2348
+ return {
2349
+ running,
2350
+ orphaned: Math.max(0, running - (active === void 0 ? 0 : 1))
2351
+ };
2352
+ } catch {
2353
+ return {
2354
+ running: 0,
2355
+ orphaned: 0
2356
+ };
2357
+ }
2358
+ };
1862
2359
  ctx.inject(["commands"], (commandCtx) => {
1863
2360
  const commands = commandCtx.commands;
1864
2361
  const commandBody = { run: async (invocation) => {
@@ -2072,8 +2569,9 @@ function apply(ctx, rawConfig = {}) {
2072
2569
  const ioRegistry = ctx.get("evolutionIo");
2073
2570
  if (!ioRegistry) return err("未挂载 evolution-io 注册表:无法固定或取消固定技能。");
2074
2571
  const result = await newSkillLibrary({
2572
+ authority: authorityFrom({ origin: "foreground" }),
2075
2573
  config: { root: skillsRootValue },
2076
- io: preferProvider(ioRegistry, ["fs"]),
2574
+ io: ioRegistry.provider(),
2077
2575
  ctx,
2078
2576
  limits: {
2079
2577
  ...DEFAULT_SKILL_LIMITS,
@@ -2149,8 +2647,9 @@ function apply(ctx, rawConfig = {}) {
2149
2647
  const ioRegistry = ctx.get("evolutionIo");
2150
2648
  if (!ioRegistry) return err("未挂载 evolution-io 注册表:维护事实不可用。");
2151
2649
  const library = newSkillLibrary({
2650
+ authority: authorityFrom({ origin: "foreground" }),
2152
2651
  config: { root: skillsRootValue },
2153
- io: preferProvider(ioRegistry, ["fs"])
2652
+ io: ioRegistry.provider()
2154
2653
  });
2155
2654
  const enrichment = await buildEnrichment(ctx, library);
2156
2655
  const readFailures = [];
@@ -2162,7 +2661,7 @@ function apply(ctx, rawConfig = {}) {
2162
2661
  }
2163
2662
  });
2164
2663
  if (snapshots.length === 0 && readFailures.length > 0) return err(`维护扫描读不出 ${readFailures.length} 个已登记技能(${readFailures.slice(0, 10).join(", ")}${readFailures.length > 10 ? ", …" : ""}),且一个技能都没读到:技能库未被审计。请修好读不出的技能(或它们的目录名)后重新扫描`);
2165
- if (snapshots.length === 0 && skillsRootValue !== "" && !await preferProvider(ioRegistry, ["fs"]).exists(skillsRootValue)) return err(`维护扫描发现技能库为空,但配置的技能根目录不存在(${skillsRootValue}):请把 skillsRoot 指向真实目录(字面量 \`~\` 不会被展开),然后重新扫描`);
2664
+ if (snapshots.length === 0 && skillsRootValue !== "" && !await ioRegistry.provider().exists(skillsRootValue)) return err(`维护扫描发现技能库为空,但配置的技能根目录不存在(${skillsRootValue}):请把 skillsRoot 指向真实目录(字面量 \`~\` 不会被展开),然后重新扫描`);
2166
2665
  if (snapshots.length === 0) return ok("维护扫描:技能库为空,无需处理。");
2167
2666
  const { facts } = buildMaintainFacts(snapshots, enrichment.usageObservedValue, void 0);
2168
2667
  return ok(`维护事实(零 token 预览):\n${facts}`);
@@ -2179,9 +2678,8 @@ function apply(ctx, rawConfig = {}) {
2179
2678
  if (!synced.ok) ctx.logger.warn(`evolution-commands: ${synced.note ?? "run index unreadable"} — the in-flight guard cannot see other processes' runs`);
2180
2679
  else if (synced.note !== void 0) ctx.logger.warn(`evolution-commands: ${synced.note}`);
2181
2680
  const ioRegistry = ctx.get("evolutionIo");
2182
- const subagents = ctx.get("subagents");
2183
2681
  if (!ioRegistry) return err("未挂载 evolution-io 注册表:维护扫描不可用。");
2184
- if (!subagents) return err("未挂载子智能体服务:维护扫描不可用。");
2682
+ const channel = createSubagentChannel(ctx);
2185
2683
  const running = registry.inFlight();
2186
2684
  if (running) {
2187
2685
  const elapsed = Math.max(1, Math.round((Date.now() - running.startedAt) / 1e3));
@@ -2202,20 +2700,21 @@ function apply(ctx, rawConfig = {}) {
2202
2700
  (async () => {
2203
2701
  try {
2204
2702
  const library = newSkillLibrary({
2703
+ authority: authorityFrom({ origin: "foreground" }),
2205
2704
  config: { root: skillsRootValue },
2206
- io: preferProvider(ioRegistry, ["fs"])
2705
+ io: ioRegistry.provider()
2207
2706
  });
2208
2707
  const enrichment = await buildEnrichment(ctx, library);
2209
2708
  const outcome = await runMaintain({
2210
2709
  library,
2211
- subagents,
2710
+ channel,
2212
2711
  parent: invocation.agent,
2213
2712
  evolutionPolicy: { get: () => ctx.get("evolutionPolicy")?.get() },
2214
2713
  tools: { get: (name) => ctx.get("tools")?.get(name) },
2215
2714
  logger: { warn: (message) => {
2216
2715
  ctx.logger.warn(message);
2217
2716
  } },
2218
- rootExists: skillsRootValue === "" ? void 0 : () => preferProvider(ioRegistry, ["fs"]).exists(skillsRootValue),
2717
+ rootExists: skillsRootValue === "" ? void 0 : () => ioRegistry.provider().exists(skillsRootValue),
2219
2718
  skillRoot: skillsRootValue
2220
2719
  }, {
2221
2720
  timeoutMs: runTimeoutMs,
@@ -2224,7 +2723,7 @@ function apply(ctx, rawConfig = {}) {
2224
2723
  enrichment: () => enrichment
2225
2724
  });
2226
2725
  const endedAt = Date.now();
2227
- const resultRef = await writeMaintainReport(preferProvider(ioRegistry, ["fs"]), {
2726
+ const resultRef = await writeMaintainReport(ioRegistry.provider(), {
2228
2727
  runId: handle.id,
2229
2728
  ...outcome.ok ? {
2230
2729
  verdict: outcome.verdict,
@@ -2241,6 +2740,15 @@ function apply(ctx, rawConfig = {}) {
2241
2740
  }, (message) => {
2242
2741
  ctx.logger.warn(message);
2243
2742
  }, runHome);
2743
+ if (resultRef !== void 0) {
2744
+ if (!isPresent(await probeMtime(ioRegistry.provider(), resultRef))) {
2745
+ await registry.settle(handle.id, {
2746
+ state: "failed",
2747
+ failure: "the maintain report file is missing at settle time (write/io race) — the scan result was not persisted."
2748
+ });
2749
+ return;
2750
+ }
2751
+ }
2244
2752
  const state = outcome.ok ? "succeeded" : handle.signal.aborted ? "cancelled" : "failed";
2245
2753
  await registry.settle(handle.id, {
2246
2754
  state,
@@ -2248,7 +2756,7 @@ function apply(ctx, rawConfig = {}) {
2248
2756
  ...outcome.ok ? {} : { failure: handle.signal.aborted ? "cancelled: the scan was stopped before it settled" : outcome.error ?? "Maintenance scan failed." }
2249
2757
  });
2250
2758
  if (outcome.ok) {
2251
- appendEvolutionEvent(preferProvider(ioRegistry, ["fs"]), eventsFile(runRoot), {
2759
+ appendEvolutionEvent(ioRegistry.provider(), eventsFile(runRoot), {
2252
2760
  type: "maintain",
2253
2761
  source: "manual",
2254
2762
  runId: handle.id,
@@ -2341,7 +2849,7 @@ function apply(ctx, rawConfig = {}) {
2341
2849
  if (wanted === void 0 && record.state === "running") return err(`最新一次运行 ${record.id} 仍在进行:它结束后才会出报告;用 /evolution maintain status ${record.id} 查看进度。`);
2342
2850
  const ioRegistry = ctx.get("evolutionIo");
2343
2851
  if (!ioRegistry) return err("未挂载 evolution-io 注册表:无法查看运行结果。");
2344
- const probed = await readRunReport(preferProvider(ioRegistry, ["fs"]), record);
2852
+ const probed = await readRunReport(ioRegistry.provider(), record);
2345
2853
  if (probed.kind === "missing") return err(`运行 ${record.id}(${record.state})没有结果:没有写出报告${record.failure === void 0 ? "" : `(${record.failure})`}。`);
2346
2854
  if (probed.kind === "unknown") return err(`运行 ${record.id} 有报告但读不出来(${probed.reason}):这「不是」没有结果。`);
2347
2855
  return ok(probed.text);
@@ -2386,7 +2894,7 @@ function apply(ctx, rawConfig = {}) {
2386
2894
  if (wanted === void 0 && record.state === "running") return err(`最新一次运行 ${record.id} 仍在进行:它结束后才会出报告;用 /evolution maintain status ${record.id} 查看进度。`);
2387
2895
  const ioRegistry = ctx.get("evolutionIo");
2388
2896
  if (!ioRegistry) return err("未挂载 evolution-io 注册表:无法交付报告。");
2389
- const probed = await readRunReport(preferProvider(ioRegistry, ["fs"]), record);
2897
+ const probed = await readRunReport(ioRegistry.provider(), record);
2390
2898
  if (probed.kind === "missing") return err(`运行 ${record.id}(${record.state})没有结果:没有写出报告${record.failure === void 0 ? "" : `(${record.failure})`}。`);
2391
2899
  if (probed.kind === "unknown") return err(`运行 ${record.id} 有报告但读不出来(${probed.reason}):这「不是」没有结果。`);
2392
2900
  const missingAgent = agentMissing("handoff");
@@ -2413,7 +2921,7 @@ function apply(ctx, rawConfig = {}) {
2413
2921
  }
2414
2922
  if (/^maintain\b/.test(input)) return err("未知的 maintain 参数:应为 `maintain`、`maintain --timeout <ms>` / `maintain --timeout=<ms>`、`maintain status [<id>]`、`maintain report [<id>]`、`maintain handoff [<id>]` 或 `maintain cancel [<id>]`。实际收到:" + input);
2415
2923
  const presetParsed = parseSubcommand(input);
2416
- const presetInstall = isParseFailure(presetParsed) || presetParsed.name !== "preset" ? null : presetParsed.positionals[0] === "install" && presetParsed.positionals.length === 1 && [...presetParsed.flags.keys()].every((flag) => flagsOf("preset").includes("--" + flag)) ? presetParsed : false;
2924
+ const presetInstall = isParseFailure(presetParsed) || presetParsed.name !== "preset" ? null : presetParsed.positionals[0] === "install" && presetParsed.positionals.length === 1 && [...presetParsed.flags.keys()].every((flag) => flagsOf("preset install").includes("--" + flag)) ? presetParsed : false;
2417
2925
  if (presetInstall !== null && presetInstall !== false) {
2418
2926
  {
2419
2927
  const mounted = presetProfileTarget(ctx);
@@ -2525,8 +3033,9 @@ function apply(ctx, rawConfig = {}) {
2525
3033
  if (decision.action === "staged") return ok(`${decision.message}${planRunId ? `\n[audit] plan=${planRunId}` : ""}`);
2526
3034
  }
2527
3035
  const result = await newSkillLibrary({
3036
+ authority: authorityFrom({ origin: "foreground" }),
2528
3037
  config: { root: skillsRootValue },
2529
- io: preferProvider(ioRegistry, ["fs"]),
3038
+ io: ioRegistry.provider(),
2530
3039
  ctx,
2531
3040
  threatExemptLabels: config.threatExemptLabels,
2532
3041
  limits: {
@@ -2664,8 +3173,8 @@ function apply(ctx, rawConfig = {}) {
2664
3173
  home: evolutionRoot(),
2665
3174
  ...mounted
2666
3175
  });
2667
- if (input === "doctor --json") return ok(JSON.stringify(report, null, 2));
2668
- return ok(renderDoctorText(report));
3176
+ if (input === "doctor --json") return ok(JSON.stringify(report.view, null, 2));
3177
+ return ok(renderDoctorText(report) + "\n" + renderDiagnosticsText(report.view));
2669
3178
  }
2670
3179
  if (input === "" || input === "help") return ok(`自进化:记忆、技能、会话回顾、技能整理——状态、待批准写入与维护。\n${renderHelpText()}`);
2671
3180
  return err(`/evolution ${input} 不是本构建能应答的子命令(已知子命令但缺参数也会落到这里)。\n${renderHint()}`);
@@ -2727,6 +3236,7 @@ function apply(ctx, rawConfig = {}) {
2727
3236
  ctx.inject(["settings"], () => {
2728
3237
  runMigrationOnce();
2729
3238
  });
3239
+ registerHostRoutes(ctx, () => makeDiagnosticsRoutes(() => assembleDoctorInputs(ctx, { runs: doctorRuns() })));
2730
3240
  (async () => {
2731
3241
  const loader = ctx.root.loader;
2732
3242
  try {
@@ -2783,7 +3293,8 @@ function resolveAgentPresetDir(importMetaUrl) {
2783
3293
  const overlayCandidate = join(dirname(fileURLToPath(importMetaUrl)), "..", "..", "evolution-agent");
2784
3294
  if (existsSync(join(overlayCandidate, "agent.cordis.yml")) && existsSync(join(overlayCandidate, "bases.json"))) return overlayCandidate;
2785
3295
  try {
2786
- return dirname(createRequire(importMetaUrl).resolve("@lmzhen/dsh-evolution-agent-preset/package.json"));
3296
+ const agentPresetPackage = scopedPackageName("dsh-evolution-agent-preset", importMetaUrl);
3297
+ return dirname(createRequire(importMetaUrl).resolve(agentPresetPackage + "/package.json"));
2787
3298
  } catch {
2788
3299
  return overlayCandidate;
2789
3300
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * G6/W8: the command-face fallback for every Host data-plane surface.
3
+ *
4
+ * The browser panels depend on the platform's `connection` carrier (exact Fetch routes). When that
5
+ * carrier is absent (headless host, E-325) or a panel answer is missing, the operator is owed the
6
+ * equivalent command — the SAME use case, not a different implementation. This table is the ONE
7
+ * home of those pairs; the INSTALL runbook and the client `fallbacks.generated.ts` are generated
8
+ * from it (W7), and a spec pins its key set to HOST_ROUTES' four surfaces.
9
+ *
10
+ * Not exported from the package: this is a command-side contract, not a plugin API (W8 §2).
11
+ * @module @lmzhen/dsh-evolution-commands/command-fallback
12
+ */
13
+ /** One surface's command-face path(s): the family command, plus the platform CLI when one exists. */
14
+ export interface CommandFallbackHint {
15
+ readonly command: string;
16
+ readonly platformCli?: string;
17
+ }
18
+ /** The four Host surfaces (keyed exactly like the frozen `COMMAND_FALLBACK_SURFACES`). */
19
+ export type CommandFallbackSurface = 'approval' | 'skill-history' | 'skill-health' | 'diagnostics';
20
+ /**
21
+ * One entry per Host surface. The command column names the same service methods the routes call,
22
+ * so the answers (and refusal sentences) match the panel.
23
+ */
24
+ export declare const COMMAND_FALLBACKS: Readonly<Record<CommandFallbackSurface, CommandFallbackHint>>;
25
+ /** The ordered surface ids (the frozen W1 set). */
26
+ export declare const COMMAND_FALLBACK_SURFACES: readonly CommandFallbackSurface[];
27
+ //# sourceMappingURL=command-fallback.d.ts.map
@@ -0,0 +1,9 @@
1
+ import type { ConnectionFetchRouteLike } from '@lmzhen/dsh-evolution-core/http-routes';
2
+ import { type DoctorInputs } from './doctor-view.ts';
3
+ /**
4
+ * Build the diagnostics route over one input producer.
5
+ * @param getInputs - assembles the live view inputs (soft probes; may reject).
6
+ * @returns the exact Fetch route for `HOST_ROUTES.diagnostics.view`.
7
+ */
8
+ export declare function makeDiagnosticsRoutes(getInputs: () => Promise<DoctorInputs>): ConnectionFetchRouteLike[];
9
+ //# sourceMappingURL=diagnostics-route.d.ts.map
@@ -0,0 +1,98 @@
1
+ /**
2
+ * G6/W8: `buildDoctorView` — the ONE producer of DiagnosticsView.
3
+ *
4
+ * CLI `--json`, the diagnostics route (W6) and the client diagnostics card (W7) all call this
5
+ * function, so they cannot drift: 7 facts (declared/derived values, homes named) + 8 runtime rows
6
+ * (measured state, never rendered as ok when unreadable). The function is pure and NEVER throws: a
7
+ * missing/undecidable provider becomes an `unknown`/`warn`/`fail` row with a remedy, because a
8
+ * doctor that fails to answer is worse than one that names its blind spots.
9
+ * @module @lmzhen/dsh-evolution-commands/doctor-view
10
+ */
11
+ import type { Context } from '@deepseek-ai/cordis';
12
+ import { type ChannelState, type DiagnosticsView } from '@lmzhen/dsh-evolution-core';
13
+ /** The install form the running profile was classified as (W8 §2 runtime install-form-detected). */
14
+ export type InstallFormId = 'M1' | 'M2' | 'M3' | 'M4';
15
+ /** One preset's probe result (reviewer/maintainer). */
16
+ export interface PresetFacts {
17
+ readonly registered: boolean;
18
+ readonly broken?: string | undefined;
19
+ readonly id?: string | undefined;
20
+ }
21
+ /** The platform/connection pairing verdict. */
22
+ export type PlatformPairing = 'in-window' | 'host-newer' | 'out-of-window' | 'unknown';
23
+ /**
24
+ * Every input the view reads. Callers assemble it (assembleDoctorInputs does the soft probing);
25
+ * tests drive each branch directly.
26
+ */
27
+ export interface DoctorInputs {
28
+ readonly now: Date;
29
+ readonly platformLine: {
30
+ readonly version: string;
31
+ readonly floor: string;
32
+ };
33
+ readonly familyVersion: string;
34
+ readonly hostVersion?: string | undefined;
35
+ readonly form: {
36
+ readonly id: InstallFormId;
37
+ readonly evidence: readonly string[];
38
+ readonly confidence: 'declared' | 'inferred';
39
+ };
40
+ readonly probes: {
41
+ readonly agentPresets: boolean;
42
+ readonly connection: boolean;
43
+ };
44
+ readonly channels: {
45
+ readonly review: ChannelState;
46
+ readonly maintenance: ChannelState;
47
+ };
48
+ readonly presets: {
49
+ readonly reviewer?: PresetFacts | undefined;
50
+ readonly maintainer?: PresetFacts | undefined;
51
+ };
52
+ readonly host: {
53
+ readonly mounted: boolean;
54
+ readonly reason?: string | undefined;
55
+ readonly deploymentAllowsRemote?: boolean | undefined;
56
+ };
57
+ readonly runs: {
58
+ readonly running: number;
59
+ readonly orphaned: number;
60
+ };
61
+ readonly missingServices?: readonly string[] | undefined;
62
+ readonly sessionFoldClassifierPresent?: boolean | undefined;
63
+ readonly platformPairing?: PlatformPairing | undefined;
64
+ /** Human-facing total of registered host routes, when the carrier reports one (12 today). */
65
+ readonly hostRouteCount?: {
66
+ readonly mounted: number;
67
+ readonly total: number;
68
+ } | undefined;
69
+ }
70
+ /**
71
+ * Build the frozen DiagnosticsView. Pure; never throws.
72
+ * @param inputs - everything the 7+8 rows read (assembleDoctorInputs probes the live host).
73
+ */
74
+ export declare function buildDoctorView(inputs: DoctorInputs): DiagnosticsView;
75
+ /** A safe view for a host whose inputs could not be assembled: every row names the failure. */
76
+ export declare function unavailableDoctorView(now: Date, reason: string): DiagnosticsView;
77
+ /** The family version this build carries (nearest package.json above the running module). */
78
+ export declare function familyVersion(): string;
79
+ /**
80
+ * Assemble the view inputs through the live Context's soft probes.
81
+ *
82
+ * Every probe is optional: an absent service becomes a row value (unknown/warn), never a throw. The
83
+ * channel probe uses W5's own `createSubagentChannel` state machinery, so the doctor reports the
84
+ * SAME availability the review/maintenance runs would see.
85
+ * @param ctx - the host context.
86
+ * @param extras - call-site facts the context cannot answer (runs, host version, install form).
87
+ */
88
+ export declare function assembleDoctorInputs(ctx: Context, extras?: {
89
+ readonly runs?: {
90
+ readonly running: number;
91
+ readonly orphaned: number;
92
+ } | undefined;
93
+ readonly hostVersion?: string | undefined;
94
+ readonly form?: DoctorInputs['form'] | undefined;
95
+ readonly platformPairing?: PlatformPairing | undefined;
96
+ readonly now?: Date | undefined;
97
+ }): Promise<DoctorInputs>;
98
+ //# sourceMappingURL=doctor-view.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { type PlatformServiceProbe, type ScopedProbeReport } from '@lmzhen/dsh-evolution-core';
2
+ import type { DiagnosticsView } from '@lmzhen/dsh-evolution-core';
2
3
  import type { MountedProfile } from './profiles.ts';
3
4
  /** P2-20 (audit): the TS-side copy of the install-target tails. The script
4
5
  * side (install-layered.mjs, verify-profile-bundles.mjs) imports the list
@@ -87,12 +88,6 @@ export interface DoctorReport {
87
88
  foreignProfiles: string[];
88
89
  conflicts: string[];
89
90
  envIssues: string[];
90
- /** S0.2 (v37 P0-1): memory files carrying the platform's `{{` prompt-variable
91
- * syntax. Residual-risk detector — current builds neutralize the INJECTED text
92
- * (`MemoryStore.renderContext`), but a still-running older build fails every
93
- * pre-step of every session under this home, and an operator may want to clean
94
- * the file regardless. */
95
- memoryIssues: string[];
96
91
  /** S4/P-1+P-2 (platform gap, family mitigation): the session-query corpus
97
92
  * listing failed — reported verbatim, with the isolation recipe. Empty when the
98
93
  * service is absent or answering. */
@@ -146,6 +141,9 @@ export interface DoctorReport {
146
141
  /** G5: where the review row's presence came from: the live entry, or the aggregate inference. */
147
142
  serviceSource: 'platform' | 'bundles';
148
143
  actions: string[];
144
+ /** G6/W8 tail: the frozen DiagnosticsView (7 facts + 8 runtime rows) — the SAME object the
145
+ * diagnostics route and the client card render; `--json` emits it and the text renderer prints it. */
146
+ readonly view: DiagnosticsView;
149
147
  }
150
148
  /** Profile bundle rows for evolution-family packages across all profiles.
151
149
  *
@@ -162,4 +160,14 @@ export declare function diagnose(ctx: {
162
160
  mounted?: MountedProfile;
163
161
  }): Promise<DoctorReport>;
164
162
  export declare function renderDoctorText(report: DoctorReport): string;
163
+ /**
164
+ * G6/W8 tail: render the frozen DiagnosticsView as the doctor's diagnostics section.
165
+ *
166
+ * This is the ONE text face of the 7+8 rows: the same object `--json` and the diagnostics route
167
+ * answer, so the three readers cannot disagree. Legacy install/conflict sections keep their own
168
+ * renderer until the remaining W8-S7 rewrite lands.
169
+ * @param view - the assembled view.
170
+ * @returns the section text (ends with a newline).
171
+ */
172
+ export declare function renderDiagnosticsText(view: DiagnosticsView): string;
165
173
  //# sourceMappingURL=doctor.d.ts.map
@@ -19,7 +19,16 @@ export interface CommandEntry {
19
19
  }
20
20
  /** The flags the TABLE declares for one subcommand (name → `--flag`), DERIVED from its usage. */
21
21
  export declare function flagsOf(name: string): string[];
22
- /** C7 shape, DERIVED: the subcommand name is the first token of `usage` (variants use `|`). */
22
+ /**
23
+ * C7 shape, DERIVED: the entry IDENTITY key, and it is HIERARCHICAL (E-08).
24
+ *
25
+ * The key used to be the first token alone, and `flagsOf` finds an entry with `.find(...)` - the FIRST
26
+ * match - so `maintain handoff` / `maintain cancel` and `skill restore` / `skill history` / `skill undo` /
27
+ * `skill pin` all derived the SAME key as their parent and every flag lookup silently saw the parent entry
28
+ * only (nine collisions). A bare parent (`maintain [--timeout]`) keeps its one-token key, so parent lookups
29
+ * still resolve: the one call site that named an entry by a name the key no longer produced was the preset
30
+ * dispatch branch, and it now asks for the entry own key (`preset install`).
31
+ */
23
32
  export declare function entryName(entry: CommandEntry): string;
24
33
  /** C7 shape, DERIVED: the flags a `usage` declares, in the form `--flag` (value flags keep their name). */
25
34
  export declare function entryFlags(entry: CommandEntry): string[];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-commands",
3
3
  "description": "Human commands for the evolution family (/evolution pending|curator|maintain|doctor) (community build)",
4
- "version": "0.26.15",
4
+ "version": "0.27.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -26,21 +26,23 @@
26
26
  ],
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
- "@deepseek-ai/schemastery": "^3.18.4",
30
- "@lmzhen/dsh-evolution-approval": "^0.26.15",
31
- "@lmzhen/dsh-evolution-core": "^0.26.15",
32
- "@lmzhen/dsh-evolution-maintenance": "^0.26.15"
29
+ "@deepseek-ai/schemastery": "^3.18.5",
30
+ "@lmzhen/dsh-evolution-approval": "^0.27.0",
31
+ "@lmzhen/dsh-evolution-core": "^0.27.0",
32
+ "@lmzhen/dsh-evolution-maintenance": "^0.27.0"
33
33
  },
34
34
  "optionalDependencies": {
35
- "@lmzhen/dsh-evolution-agent-preset": "^0.26.15"
35
+ "@lmzhen/dsh-evolution-agent-preset": "^0.27.0"
36
36
  },
37
37
  "peerDependencies": {
38
- "@deepseek-ai/cordis": "^4.0.1",
39
- "@deepseek-ai/dsh-commands": "^0.2.0-rc.1",
40
- "@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
41
- "@lmzhen/dsh-evolution-io": "^0.26.15"
38
+ "@deepseek-ai/cordis": "^4.0.5",
39
+ "@deepseek-ai/dsh-commands": "^0.2.1-rc.1",
40
+ "@deepseek-ai/dsh-llm": "^0.2.1-rc.1"
42
41
  },
43
42
  "devDependencies": {
44
- "@deepseek-ai/dsh-commands": "^0.2.0-rc.1"
43
+ "@deepseek-ai/dsh-commands": "^0.2.1-rc.1"
44
+ },
45
+ "engines": {
46
+ "dsh": ">=0.2.1-rc.1 <0.2.2-0"
45
47
  }
46
48
  }