@lmzhen/dsh-evolution-commands 0.3.54 → 0.3.56

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
@@ -2,9 +2,186 @@ import { createRequire } from "node:module";
2
2
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
3
3
  import { SkillLibrary, appendEvolutionEvent, buildLearnPrompt, clampedNumber, composePresetComposition, eventsFile, evolutionRoot, resolveSkillsRoot } from "@lmzhen/dsh-evolution-core";
4
4
  import { buildEnrichment, buildMaintainFacts, runMaintain, snapshotFromLibrary } from "@lmzhen/dsh-evolution-maintenance";
5
- import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
5
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
6
6
  import { dirname, join } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
+ //#region lib/types/doctor.js
9
+ /**
10
+ * `/evolution doctor` — read-only self-check (0.3.55, WB2).
11
+ *
12
+ * Turns "what the README would explain" into "the tool tells you directly":
13
+ * install form, three-way conflict detection (all/host/preset + layered), the
14
+ * environment surfaces that bit us before (v10 P2-12/13), and which evolution
15
+ * services are actually mounted in this runtime. Always ends with suggested
16
+ * actions so any finding carries its next step.
17
+ */
18
+ /** Profile bundle rows for evolution-family packages across all profiles. */
19
+ function collectEvolutionBundles(home) {
20
+ const profilesDir = join(home, "profiles");
21
+ if (!existsSync(profilesDir)) return [];
22
+ const bundles = [];
23
+ for (const profile of readdirSync(profilesDir, { withFileTypes: true })) {
24
+ if (!profile.isDirectory()) continue;
25
+ try {
26
+ const manifest = JSON.parse(readFileSync(join(profilesDir, profile.name, "package.json"), "utf8"));
27
+ for (const name of manifest.dsh?.profile?.bundles ?? []) if (/evolution-(all|host|preset)/.test(name)) bundles.push(name);
28
+ } catch {}
29
+ }
30
+ return bundles;
31
+ }
32
+ const SESSION_QUERY_MODES = new Set([
33
+ "startup",
34
+ "first-search",
35
+ "never"
36
+ ]);
37
+ function envIssues() {
38
+ const issues = [];
39
+ const query = process.env.DSH_EVOLUTION_SESSION_QUERY;
40
+ 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.`);
41
+ return issues;
42
+ }
43
+ /**
44
+ * Diagnose the deployment. `ctx` supplies service presence (a bare stub with
45
+ * only `get` is enough); `home` defaults to the evolution root so tests can
46
+ * point doctor at a temp DSH_HOME.
47
+ */
48
+ async function diagnose(ctx, options = {}) {
49
+ const home = options.home ?? evolutionRoot();
50
+ const bundles = collectEvolutionBundles(home);
51
+ const has = (name) => ctx.get(name) !== void 0;
52
+ const full = bundles.some((name) => /evolution-all/.test(name));
53
+ const host = bundles.some((name) => /evolution-host/.test(name));
54
+ const preset = bundles.some((name) => /evolution-preset/.test(name));
55
+ const presetDir = join(home, ".agent-presets", "evolution");
56
+ const layered = host && existsSync(presetDir);
57
+ const conflicts = [];
58
+ if (full && host) conflicts.push("evolution-all and evolution-host are installed together — the infra rows double-mount and startup fails loud. Keep ONE: remove the other bundle.");
59
+ if (full && preset) conflicts.push("evolution-all and evolution-preset are installed together — the infra rows double-mount. Keep ONE.");
60
+ if (host && preset) conflicts.push("evolution-host and evolution-preset are installed together — the infra rows double-mount. Keep ONE.");
61
+ if (full && layered) conflicts.push("evolution-all and the layered Evolution preset are both present — the model rows double-mount. Keep ONE (use layered without all, or drop the preset).");
62
+ const services = {
63
+ review: has("evolutionReview"),
64
+ curator: has("evolutionCurator"),
65
+ approval: has("evolutionApproval"),
66
+ skillUsage: has("skillUsage"),
67
+ io: has("evolutionIo")
68
+ };
69
+ let pendingCount = null;
70
+ const approvalService = ctx.get("evolutionApproval");
71
+ if (approvalService?.list) try {
72
+ const rows = await approvalService.list("pending");
73
+ pendingCount = Array.isArray(rows) ? rows.length : null;
74
+ } catch {
75
+ pendingCount = null;
76
+ }
77
+ const installForm = full ? "full" : preset ? "preset" : layered ? "layered" : host ? "host" : "none";
78
+ const actions = [];
79
+ if (conflicts.length > 0) actions.push("Resolve the conflict first: keep exactly one of evolution-all / evolution-host / evolution-preset / layered.");
80
+ else if (installForm === "none") actions.push("Install the default full bundle: dsh plugin --profile web add @lmzhen/dsh-evolution-all");
81
+ else if (installForm === "layered") actions.push("Model tools follow the Evolution preset per session; add @lmzhen/dsh-evolution-all instead if every session should have them.");
82
+ const env = envIssues();
83
+ if (env.length > 0) actions.push("Fix the DSH_EVOLUTION_* variable listed above.");
84
+ if (services.review && !services.curator) actions.push("Curator service is not mounted — automatic curation is off; verify the host/all bundle row set is complete.");
85
+ if (pendingCount === null && services.approval) actions.push("Approval service is mounted but pending listing failed — check the evolution state service.");
86
+ return {
87
+ installForm,
88
+ bundles,
89
+ conflicts,
90
+ envIssues: env,
91
+ services,
92
+ pendingCount,
93
+ actions
94
+ };
95
+ }
96
+ function renderDoctorText(report) {
97
+ const lines = [
98
+ `Evolution doctor — install form: ${report.installForm}`,
99
+ `bundles: ${report.bundles.length > 0 ? report.bundles.join(", ") : "(none)"}`,
100
+ `services: review=${report.services.review} curator=${report.services.curator} approval=${report.services.approval} skillUsage=${report.services.skillUsage} io=${report.services.io}`,
101
+ `pending: ${report.pendingCount === null ? "unknown" : report.pendingCount}`
102
+ ];
103
+ if (report.conflicts.length > 0) lines.push("conflicts:", ...report.conflicts.map((line) => ` ! ${line}`));
104
+ if (report.envIssues.length > 0) lines.push("env:", ...report.envIssues.map((line) => ` ! ${line}`));
105
+ if (report.actions.length > 0) lines.push("next steps:", ...report.actions.map((line) => ` → ${line}`));
106
+ return lines.join("\n");
107
+ }
108
+ //#endregion
109
+ //#region lib/types/registry.js
110
+ const COMMAND_ENTRIES = [
111
+ {
112
+ usage: "pending [--detail]",
113
+ summary: "list staged evolution writes (--detail shows staged args)"
114
+ },
115
+ {
116
+ usage: "approve <id>",
117
+ summary: "replay an approved staged write through its runner"
118
+ },
119
+ {
120
+ usage: "reject <id>",
121
+ summary: "drop a staged write without running it"
122
+ },
123
+ {
124
+ usage: "doctor",
125
+ summary: "read-only self-check: install form, conflicts, env, services"
126
+ },
127
+ {
128
+ usage: "curator run|pause|resume|status|report|scope",
129
+ summary: "run one curation pass, control or inspect automatic curation"
130
+ },
131
+ {
132
+ usage: "mutations",
133
+ summary: "list skill-mutation audit records"
134
+ },
135
+ {
136
+ usage: "restore",
137
+ summary: "restore skills from the latest snapshot"
138
+ },
139
+ {
140
+ usage: "consolidate <target> <sources...>",
141
+ summary: "merge source skills into a target umbrella skill"
142
+ },
143
+ {
144
+ usage: "skill restore <name>",
145
+ summary: "restore one archived skill by name"
146
+ },
147
+ {
148
+ usage: "skills health",
149
+ summary: "structure-health verdicts for the skill library"
150
+ },
151
+ {
152
+ usage: "skills refresh",
153
+ summary: "drop the catalog caches and re-read the tree"
154
+ },
155
+ {
156
+ usage: "learn [request]",
157
+ summary: "send a learning request to this session"
158
+ },
159
+ {
160
+ usage: "maintain [--timeout=<ms> | --facts]",
161
+ summary: "run a maintenance scan (--facts: 0-token preview)"
162
+ },
163
+ {
164
+ usage: "preset install",
165
+ summary: "generate the Evolution agent preset into the user root"
166
+ },
167
+ {
168
+ usage: "restructure <name> \"<heading>\" <to_file>",
169
+ summary: "move a body section into a references/ file"
170
+ },
171
+ {
172
+ usage: "replay",
173
+ summary: "compare prompt-bundle replay for this session"
174
+ }
175
+ ];
176
+ /** The input-declaration hint (single line, ` | `-separated). */
177
+ function renderHint() {
178
+ return COMMAND_ENTRIES.map((entry) => entry.usage).join(" | ");
179
+ }
180
+ /** The bare `/evolution` help body (one line per subcommand). */
181
+ function renderHelpText() {
182
+ return COMMAND_ENTRIES.map((entry) => ` ${entry.usage.padEnd(44)} ${entry.summary}`).join("\n");
183
+ }
184
+ //#endregion
8
185
  //#region lib/types/index.js
9
186
  /**
10
187
  * Human commands for the evolution family: /evolution learn|pending|curator|restore|consolidate|skills refresh.
@@ -23,7 +200,7 @@ function apply(ctx, rawConfig = {}) {
23
200
  name: "evolution",
24
201
  description: "Self-evolution status and approval controls",
25
202
  recordInput: false,
26
- input: { hint: "pending [--detail] | approve <id> | reject <id> | curator run|pause|resume|status|report|scope | mutations | restore | consolidate <target> <sources...> | skill restore <name> | skills health | skills refresh | learn [request] | maintain [--timeout=<ms> | --facts] | preset install | restructure <name> \"<heading>\" <to_file> | replay" },
203
+ input: { hint: renderHint() },
27
204
  async handler(invocation) {
28
205
  const input = invocation.rawInput?.trim() ?? "";
29
206
  const ok = (text) => ({
@@ -51,7 +228,7 @@ function apply(ctx, rawConfig = {}) {
51
228
  const id = input.slice(8).trim();
52
229
  const result = approval ? await approval.approve(id) : {
53
230
  ok: false,
54
- message: "approval service not mounted"
231
+ message: "E-301: approval service not mounted. Next: the evolution-approval row ships with evolution-host/evolution-all — run /evolution doctor to see which services are mounted."
55
232
  };
56
233
  return result.ok ? ok(result.message) : err(result.message);
57
234
  }
@@ -59,19 +236,19 @@ function apply(ctx, rawConfig = {}) {
59
236
  const id = input.slice(7).trim();
60
237
  const result = approval ? await approval.reject(id) : {
61
238
  ok: false,
62
- message: "approval service not mounted"
239
+ message: "E-301: approval service not mounted. Next: the evolution-approval row ships with evolution-host/evolution-all — run /evolution doctor to see which services are mounted."
63
240
  };
64
241
  return result.ok ? ok(result.message) : err(result.message);
65
242
  }
66
243
  if (input === "curator run") {
67
244
  const curator = ctx.get("evolutionCurator");
68
- if (!curator) return err("Curator service not mounted.");
245
+ if (!curator) return err("E-302: curator service not mounted. Next: mount the evolution-curator row (evolution-host/evolution-all) and run /evolution doctor.");
69
246
  const result = await curator.run({ ignoreGates: true });
70
247
  return ok(`Curator run complete: ${result.stale.length} stale, ${result.archived.length} archived, ${result.errors.length} failed.\nrunId=${result.report.runId}${result.report.snapshotPath ? `\nsnapshot=${result.report.snapshotPath}` : ""}`);
71
248
  }
72
249
  if (input === "curator pause" || input === "curator resume") {
73
250
  const curator = ctx.get("evolutionCurator");
74
- if (!curator) return err("Curator service not mounted.");
251
+ if (!curator) return err("E-302: curator service not mounted. Next: mount the evolution-curator row (evolution-host/evolution-all) and run /evolution doctor.");
75
252
  const paused = input === "curator pause";
76
253
  await curator.setPaused(paused);
77
254
  const notPersisted = ctx.get("evolutionState") !== void 0 ? "" : "\nNOTE: no evolution state service is mounted — this setting is NOT persisted and will not survive this process.";
@@ -79,7 +256,7 @@ function apply(ctx, rawConfig = {}) {
79
256
  }
80
257
  if (input === "curator status") {
81
258
  const curator = ctx.get("evolutionCurator");
82
- if (!curator) return err("Curator service not mounted.");
259
+ if (!curator) return err("E-302: curator service not mounted. Next: mount the evolution-curator row (evolution-host/evolution-all) and run /evolution doctor.");
83
260
  const state = await curator.status();
84
261
  if (!state) return ok("No curator state yet: the first automatic pass is deferred until the interval elapses.");
85
262
  const lastRun = typeof state.lastRunAt === "number" && Number.isFinite(state.lastRunAt) && state.lastRunAt > 0 ? new Date(state.lastRunAt).toISOString() : "unknown";
@@ -92,7 +269,7 @@ function apply(ctx, rawConfig = {}) {
92
269
  }
93
270
  if (input === "mutations") {
94
271
  const curator = ctx.get("evolutionCurator");
95
- if (!curator) return err("Curator service not mounted.");
272
+ if (!curator) return err("E-302: curator service not mounted. Next: mount the evolution-curator row (evolution-host/evolution-all) and run /evolution doctor.");
96
273
  const records = await curator.skills.listMutations();
97
274
  const usable = Array.isArray(records) ? records.filter((row) => typeof row === "object" && row !== null && typeof row.at === "string" && typeof row.skillName === "string" && typeof row.action === "string" && typeof row.summary === "string") : [];
98
275
  if (usable.length === 0) return ok("No mutation records yet.");
@@ -101,7 +278,7 @@ function apply(ctx, rawConfig = {}) {
101
278
  }
102
279
  if (input === "curator scope") {
103
280
  const curator = ctx.get("evolutionCurator");
104
- if (!curator) return err("Curator service not mounted.");
281
+ if (!curator) return err("E-302: curator service not mounted. Next: mount the evolution-curator row (evolution-host/evolution-all) and run /evolution doctor.");
105
282
  const view = await curator.scopeView();
106
283
  const line = (label, names) => `${label}: ${names.length}${names.length === 0 ? "" : `\n ${names.join(", ")}`}`;
107
284
  return ok([
@@ -115,7 +292,7 @@ function apply(ctx, rawConfig = {}) {
115
292
  }
116
293
  if (input === "curator report") {
117
294
  const curator = ctx.get("evolutionCurator");
118
- if (!curator) return err("Curator service not mounted.");
295
+ if (!curator) return err("E-302: curator service not mounted. Next: mount the evolution-curator row (evolution-host/evolution-all) and run /evolution doctor.");
119
296
  const report = await curator.latestReport();
120
297
  if (!report) return ok("No curator report available.");
121
298
  if (typeof report !== "object" || typeof report.runId !== "string" || typeof report.startedAt !== "string" || !Array.isArray(report.archived) || !Array.isArray(report.failed)) return err("Report file unreadable.");
@@ -133,7 +310,7 @@ function apply(ctx, rawConfig = {}) {
133
310
  const curator = ctx.get("evolutionCurator");
134
311
  const result = curator ? await curator.restoreSnapshot() : {
135
312
  ok: false,
136
- message: "Curator service not mounted."
313
+ message: "E-302: curator service not mounted. Next: mount the evolution-curator row (evolution-host/evolution-all) and run /evolution doctor."
137
314
  };
138
315
  return result.ok ? ok(result.message) : err(result.message);
139
316
  }
@@ -145,7 +322,7 @@ function apply(ctx, rawConfig = {}) {
145
322
  const curator = ctx.get("evolutionCurator");
146
323
  const result = curator ? await curator.consolidate(target, sources) : {
147
324
  ok: false,
148
- message: "Curator service not mounted."
325
+ message: "E-302: curator service not mounted. Next: mount the evolution-curator row (evolution-host/evolution-all) and run /evolution doctor."
149
326
  };
150
327
  if (!result.ok) return err(result.message);
151
328
  return ok(planRunId ? `${result.message}\n[audit] plan=${planRunId}` : result.message);
@@ -156,13 +333,13 @@ function apply(ctx, rawConfig = {}) {
156
333
  const curator = ctx.get("evolutionCurator");
157
334
  const result = curator ? await curator.restore(name) : {
158
335
  ok: false,
159
- message: "Curator service not mounted."
336
+ message: "E-302: curator service not mounted. Next: mount the evolution-curator row (evolution-host/evolution-all) and run /evolution doctor."
160
337
  };
161
338
  return result.ok ? ok(result.message) : err(result.message);
162
339
  }
163
340
  if (input === "skills health") {
164
341
  const curator = ctx.get("evolutionCurator");
165
- if (!curator) return err("Curator service not mounted.");
342
+ if (!curator) return err("E-302: curator service not mounted. Next: mount the evolution-curator row (evolution-host/evolution-all) and run /evolution doctor.");
166
343
  const [rows, observed] = await Promise.all([curator.healthView(), curator.usageObserved()]);
167
344
  const banner = observed ? "" : "Usage observation not yet established — churn (write-ghost) rows are suppressed.";
168
345
  const line = (row) => `${row.verdict.padEnd(18)} ${row.name}${row.reasons.length > 0 ? ` — ${row.reasons.join("; ")}` : ""}`;
@@ -312,10 +489,15 @@ function apply(ctx, rawConfig = {}) {
312
489
  }
313
490
  if (input === "replay") {
314
491
  const replay = ctx.get("evolutionReplay");
315
- if (!replay) return err("Replay service not mounted.");
492
+ if (!replay) return err("E-303: replay service not mounted. Next: mount the evolution-replay row (evolution-host/evolution-all) and run /evolution doctor.");
316
493
  return ok(replay.compare().report);
317
494
  }
318
- return ok("Evolution: memory, skills, review, curator. Use /evolution pending | approve <id> | reject <id> | curator run | curator status | curator pause | curator resume | curator report | curator scope | mutations | restore | consolidate <target> <source...> | skill restore <name> | skills health | skills refresh | learn [request] | maintain [--timeout=<ms> | --facts] | preset install | restructure <name> \"<heading>\" <to_file> | replay.");
495
+ if (input === "doctor" || input === "doctor --json") {
496
+ const report = await diagnose(ctx, { home: evolutionRoot() });
497
+ if (input === "doctor --json") return ok(JSON.stringify(report, null, 2));
498
+ return ok(renderDoctorText(report));
499
+ }
500
+ return ok(`Evolution: memory, skills, review, curator — self-evolution status, approval and maintenance.\n${renderHelpText()}`);
319
501
  }
320
502
  }));
321
503
  });
@@ -0,0 +1,29 @@
1
+ export interface DoctorReport {
2
+ installForm: 'full' | 'host' | 'preset' | 'layered' | 'none';
3
+ bundles: string[];
4
+ conflicts: string[];
5
+ envIssues: string[];
6
+ services: {
7
+ review: boolean;
8
+ curator: boolean;
9
+ approval: boolean;
10
+ skillUsage: boolean;
11
+ io: boolean;
12
+ };
13
+ pendingCount: number | null;
14
+ actions: string[];
15
+ }
16
+ /** Profile bundle rows for evolution-family packages across all profiles. */
17
+ export declare function collectEvolutionBundles(home: string): string[];
18
+ /**
19
+ * Diagnose the deployment. `ctx` supplies service presence (a bare stub with
20
+ * only `get` is enough); `home` defaults to the evolution root so tests can
21
+ * point doctor at a temp DSH_HOME.
22
+ */
23
+ export declare function diagnose(ctx: {
24
+ get(name: string): unknown;
25
+ }, options?: {
26
+ home?: string;
27
+ }): Promise<DoctorReport>;
28
+ export declare function renderDoctorText(report: DoctorReport): string;
29
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Subcommand registry — the SINGLE SOURCE for the /evolution command surface.
3
+ *
4
+ * 0.3.55 (WD1): the input-declaration hint, the bare `/evolution` help output
5
+ * and the README command table all render FROM this table; a hand-written
6
+ * copy anywhere is drift waiting to happen (v10 R-13 proved it once).
7
+ * Keep `usage` in the exact shape a user can type after `/evolution`.
8
+ */
9
+ export interface CommandEntry {
10
+ /** The subcommand as typed after `/evolution`. Variants use `|` (curator run|pause). */
11
+ usage: string;
12
+ /** One-line purpose rendered in the help output. */
13
+ summary: string;
14
+ }
15
+ export declare const COMMAND_ENTRIES: CommandEntry[];
16
+ /** The input-declaration hint (single line, ` | `-separated). */
17
+ export declare function renderHint(): string;
18
+ /** The bare `/evolution` help body (one line per subcommand). */
19
+ export declare function renderHelpText(): string;
20
+ /** Markdown command table fragment for README surfaces (T-WD2 pins it).
21
+ * `|` inside a usage (curator run|pause|…) is escaped for the table cell. */
22
+ export declare function renderCommandTable(): string;
23
+ //# sourceMappingURL=registry.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-commands",
3
- "description": "Human commands for the evolution family (community build)",
4
- "version": "0.3.54",
3
+ "description": "Human commands for the evolution family (/evolution pending|curator|maintain|doctor) (community build)",
4
+ "version": "0.3.56",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -30,12 +30,12 @@
30
30
  ],
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
- "@lmzhen/dsh-evolution-approval": "^0.3.54",
34
- "@lmzhen/dsh-evolution-core": "^0.3.54",
35
- "@lmzhen/dsh-evolution-maintenance": "^0.3.54"
33
+ "@lmzhen/dsh-evolution-approval": "^0.3.56",
34
+ "@lmzhen/dsh-evolution-core": "^0.3.56",
35
+ "@lmzhen/dsh-evolution-maintenance": "^0.3.56"
36
36
  },
37
37
  "optionalDependencies": {
38
- "@lmzhen/dsh-evolution-agent-preset": "^0.3.54"
38
+ "@lmzhen/dsh-evolution-agent-preset": "^0.3.56"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@deepseek-ai/cordis": "^4.0.1",