@lmzhen/dsh-evolution-commands 0.26.26 → 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 +518 -41
- package/lib/types/command-fallback.d.ts +27 -0
- package/lib/types/diagnostics-route.d.ts +9 -0
- package/lib/types/doctor-view.d.ts +98 -0
- package/lib/types/doctor.d.ts +14 -6
- package/package.json +13 -10
package/lib/index.js
CHANGED
|
@@ -1,12 +1,470 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { DEFAULT_SKILL_LIMITS, MAX_TIMER_DELAY_MS, PARAM_EXPOSURE, PARAM_GROUP_LABELS, absentHostServices, appendEvolutionEvent, applyNamespaceMigration, asRecord, assertSkillsRootAliasRetired, atomicWriteFiles, authorityFrom, buildLearnPrompt, canonicalWriteId, clampedNumber, classifyNamespaceMigration,
|
|
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";
|
|
3
7
|
import z from "@deepseek-ai/schemastery";
|
|
4
8
|
import { effectiveSessionPolicy } from "@lmzhen/dsh-evolution-approval";
|
|
5
9
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
10
468
|
//#region lib/types/migration.js
|
|
11
469
|
/**
|
|
12
470
|
* G3 (caller half): find the legacy settings document and run the namespace migration.
|
|
@@ -644,33 +1102,6 @@ function envIssues() {
|
|
|
644
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.`);
|
|
645
1103
|
return issues;
|
|
646
1104
|
}
|
|
647
|
-
/** S0.2 (v37 P0-1): memory files that still carry the platform's `{{` prompt
|
|
648
|
-
* variable syntax. The platform interpolates EVERY `systemPrompt.context` text
|
|
649
|
-
* once per model step and throws on an unknown/malformed reference, so one such
|
|
650
|
-
* entry used to fail every turn of every session under this home. Current
|
|
651
|
-
* builds neutralize the injected text (`MemoryStore.renderContext`), which is
|
|
652
|
-
* why this is a residual-risk report instead of a hard failure: an older
|
|
653
|
-
* still-running process is still bricked, and an operator may prefer to clean
|
|
654
|
-
* the file. Read-only; an unreadable file is skipped (the doctor never throws).
|
|
655
|
-
* @param home - resolved DSH_HOME.
|
|
656
|
-
* @returns one message per affected memory file.
|
|
657
|
-
*/
|
|
658
|
-
function memoryInterpolationIssues(home) {
|
|
659
|
-
const issues = [];
|
|
660
|
-
for (const file of ["MEMORY.md", "USER.md"]) {
|
|
661
|
-
const path = join(home, "memories", file);
|
|
662
|
-
if (!existsSync(path)) continue;
|
|
663
|
-
let raw;
|
|
664
|
-
try {
|
|
665
|
-
raw = readFileSync(path, "utf8");
|
|
666
|
-
} catch {
|
|
667
|
-
continue;
|
|
668
|
-
}
|
|
669
|
-
const hits = raw.split("{{").length - 1;
|
|
670
|
-
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`);
|
|
671
|
-
}
|
|
672
|
-
return issues;
|
|
673
|
-
}
|
|
674
1105
|
/** G3-② (B2): the family delta container. An optionalDependency of this
|
|
675
1106
|
* package, so resolution may legitimately fail; every failure degrades to
|
|
676
1107
|
* "unknown" — the doctor never throws.
|
|
@@ -1014,20 +1445,19 @@ async function diagnose(ctx, options = {}) {
|
|
|
1014
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).");
|
|
1015
1446
|
const legacy = await legacyMigrationRow(ctx, home);
|
|
1016
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)`);
|
|
1017
|
-
const memoryIssues = memoryInterpolationIssues(home);
|
|
1018
1448
|
const budgetIssues = memoryBudgetIssues(ctx);
|
|
1019
1449
|
const paramIssues = paramDivergences(platform);
|
|
1020
1450
|
const queryIssues = await sessionQueryIssues(ctx);
|
|
1021
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");
|
|
1022
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.");
|
|
1023
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)");
|
|
1024
|
-
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.");
|
|
1025
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.");
|
|
1026
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.`);
|
|
1027
1456
|
for (const row of presetFreshness) {
|
|
1028
1457
|
if (row.status !== "differs") continue;
|
|
1029
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`);
|
|
1030
1459
|
}
|
|
1460
|
+
const view = buildDoctorView(await assembleDoctorInputs(ctx));
|
|
1031
1461
|
return {
|
|
1032
1462
|
installForm,
|
|
1033
1463
|
deploymentForm,
|
|
@@ -1035,7 +1465,6 @@ async function diagnose(ctx, options = {}) {
|
|
|
1035
1465
|
foreignProfiles: homeProfiles(home).filter(({ profile, dir }) => mounted === void 0 || profile !== mounted.profile && dir !== mounted.dir).map(({ profile }) => profile),
|
|
1036
1466
|
conflicts,
|
|
1037
1467
|
envIssues: env,
|
|
1038
|
-
memoryIssues,
|
|
1039
1468
|
budgetIssues,
|
|
1040
1469
|
paramIssues,
|
|
1041
1470
|
queryIssues,
|
|
@@ -1051,7 +1480,8 @@ async function diagnose(ctx, options = {}) {
|
|
|
1051
1480
|
presetIssues: presetIssueRows,
|
|
1052
1481
|
dormantBundles,
|
|
1053
1482
|
serviceSource: reviewRow === void 0 ? "bundles" : "platform",
|
|
1054
|
-
actions
|
|
1483
|
+
actions,
|
|
1484
|
+
view
|
|
1055
1485
|
};
|
|
1056
1486
|
}
|
|
1057
1487
|
/**
|
|
@@ -1194,13 +1624,35 @@ function renderDoctorText(report) {
|
|
|
1194
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");
|
|
1195
1625
|
if (report.conflicts.length > 0) lines.push("conflicts:", ...report.conflicts.map((line) => ` ! ${line}`));
|
|
1196
1626
|
if (report.envIssues.length > 0) lines.push("env:", ...report.envIssues.map((line) => ` ! ${line}`));
|
|
1197
|
-
if (report.memoryIssues.length > 0) lines.push("memory:", ...report.memoryIssues.map((line) => ` ! ${line}`));
|
|
1198
1627
|
if (report.budgetIssues.length > 0) lines.push("memory budget:", ...report.budgetIssues.map((line) => ` ! ${line}`));
|
|
1199
1628
|
if (report.paramIssues.length > 0) lines.push("parameters:", ...report.paramIssues.map((line) => ` ! ${line}`));
|
|
1200
1629
|
if (report.queryIssues.length > 0) lines.push("session search:", ...report.queryIssues.map((line) => ` ! ${line}`));
|
|
1201
1630
|
if (report.actions.length > 0) lines.push("next steps:", ...report.actions.map((line) => ` → ${line}`));
|
|
1202
1631
|
return lines.filter((line) => line !== null).join("\n");
|
|
1203
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
|
+
}
|
|
1204
1656
|
//#endregion
|
|
1205
1657
|
//#region lib/types/params.js
|
|
1206
1658
|
/**
|
|
@@ -1888,6 +2340,22 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1888
2340
|
runs = void 0;
|
|
1889
2341
|
}, "evolution-commands.run-registry");
|
|
1890
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
|
+
};
|
|
1891
2359
|
ctx.inject(["commands"], (commandCtx) => {
|
|
1892
2360
|
const commands = commandCtx.commands;
|
|
1893
2361
|
const commandBody = { run: async (invocation) => {
|
|
@@ -2210,9 +2678,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2210
2678
|
if (!synced.ok) ctx.logger.warn(`evolution-commands: ${synced.note ?? "run index unreadable"} — the in-flight guard cannot see other processes' runs`);
|
|
2211
2679
|
else if (synced.note !== void 0) ctx.logger.warn(`evolution-commands: ${synced.note}`);
|
|
2212
2680
|
const ioRegistry = ctx.get("evolutionIo");
|
|
2213
|
-
const subagents = ctx.get("subagents");
|
|
2214
2681
|
if (!ioRegistry) return err("未挂载 evolution-io 注册表:维护扫描不可用。");
|
|
2215
|
-
|
|
2682
|
+
const channel = createSubagentChannel(ctx);
|
|
2216
2683
|
const running = registry.inFlight();
|
|
2217
2684
|
if (running) {
|
|
2218
2685
|
const elapsed = Math.max(1, Math.round((Date.now() - running.startedAt) / 1e3));
|
|
@@ -2240,7 +2707,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2240
2707
|
const enrichment = await buildEnrichment(ctx, library);
|
|
2241
2708
|
const outcome = await runMaintain({
|
|
2242
2709
|
library,
|
|
2243
|
-
|
|
2710
|
+
channel,
|
|
2244
2711
|
parent: invocation.agent,
|
|
2245
2712
|
evolutionPolicy: { get: () => ctx.get("evolutionPolicy")?.get() },
|
|
2246
2713
|
tools: { get: (name) => ctx.get("tools")?.get(name) },
|
|
@@ -2273,6 +2740,15 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2273
2740
|
}, (message) => {
|
|
2274
2741
|
ctx.logger.warn(message);
|
|
2275
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
|
+
}
|
|
2276
2752
|
const state = outcome.ok ? "succeeded" : handle.signal.aborted ? "cancelled" : "failed";
|
|
2277
2753
|
await registry.settle(handle.id, {
|
|
2278
2754
|
state,
|
|
@@ -2697,8 +3173,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2697
3173
|
home: evolutionRoot(),
|
|
2698
3174
|
...mounted
|
|
2699
3175
|
});
|
|
2700
|
-
if (input === "doctor --json") return ok(JSON.stringify(report, null, 2));
|
|
2701
|
-
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));
|
|
2702
3178
|
}
|
|
2703
3179
|
if (input === "" || input === "help") return ok(`自进化:记忆、技能、会话回顾、技能整理——状态、待批准写入与维护。\n${renderHelpText()}`);
|
|
2704
3180
|
return err(`/evolution ${input} 不是本构建能应答的子命令(已知子命令但缺参数也会落到这里)。\n${renderHint()}`);
|
|
@@ -2760,6 +3236,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2760
3236
|
ctx.inject(["settings"], () => {
|
|
2761
3237
|
runMigrationOnce();
|
|
2762
3238
|
});
|
|
3239
|
+
registerHostRoutes(ctx, () => makeDiagnosticsRoutes(() => assembleDoctorInputs(ctx, { runs: doctorRuns() })));
|
|
2763
3240
|
(async () => {
|
|
2764
3241
|
const loader = ctx.root.loader;
|
|
2765
3242
|
try {
|
|
@@ -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
|
package/lib/types/doctor.d.ts
CHANGED
|
@@ -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
|
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.
|
|
4
|
+
"version": "0.27.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -26,20 +26,23 @@
|
|
|
26
26
|
],
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
30
|
-
"@lmzhen/dsh-evolution-approval": "^0.
|
|
31
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
32
|
-
"@lmzhen/dsh-evolution-maintenance": "^0.
|
|
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.
|
|
35
|
+
"@lmzhen/dsh-evolution-agent-preset": "^0.27.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
39
|
-
"@deepseek-ai/dsh-commands": "^0.2.
|
|
40
|
-
"@deepseek-ai/dsh-llm": "^0.2.
|
|
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"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@deepseek-ai/dsh-commands": "^0.2.
|
|
43
|
+
"@deepseek-ai/dsh-commands": "^0.2.1-rc.1"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"dsh": ">=0.2.1-rc.1 <0.2.2-0"
|
|
44
47
|
}
|
|
45
48
|
}
|