@lmzhen/dsh-evolution-commands 0.3.67 → 0.3.69

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,7 +2,7 @@ import { createRequire } from "node:module";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { effectiveSessionPolicy } from "@lmzhen/dsh-evolution-approval";
4
4
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
5
- import { SkillLibrary, appendEvolutionEvent, buildLearnPrompt, clampedNumber, composePresetComposition, eventsFile, evolutionRoot, resolveRootConfig, resolveSkillsRoot } from "@lmzhen/dsh-evolution-core";
5
+ import { MAX_TIMER_DELAY_MS, SkillLibrary, appendEvolutionEvent, assertSkillsRootAliasRetired, buildLearnPrompt, clampedNumber, composePresetComposition, eventsFile, evolutionRoot, resolveRootConfig, resolveSkillsRoot } from "@lmzhen/dsh-evolution-core";
6
6
  import { buildEnrichment, buildMaintainFacts, runMaintain, snapshotFromLibrary } from "@lmzhen/dsh-evolution-maintenance";
7
7
  import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
8
8
  import { dirname, join } from "node:path";
@@ -26,17 +26,32 @@ const EVOLUTION_BUNDLE_TAILS = new Set([
26
26
  "dsh-evolution-host",
27
27
  "dsh-evolution-preset"
28
28
  ]);
29
- /** Profile bundle rows for evolution-family packages across all profiles. */
30
- function collectEvolutionBundles(home) {
29
+ /** Profile bundle rows for evolution-family packages across all profiles.
30
+ *
31
+ * `onReadError` reports a profile whose manifest could not be READ or PARSED.
32
+ * Like the directory-enumeration failure, it is fail-closed at every caller:
33
+ * a truncated manifest is indistinguishable from "no bundles" by the returned
34
+ * list alone, and treating it as "no bundles" is exactly the fail-open state
35
+ * the double-mount exclusion exists to prevent. */
36
+ function collectEvolutionBundles(home, onReadError) {
31
37
  const profilesDir = join(home, "profiles");
32
38
  if (!existsSync(profilesDir)) return [];
33
39
  const bundles = [];
34
- for (const profile of readdirSync(profilesDir, { withFileTypes: true })) {
40
+ let profileEntries;
41
+ try {
42
+ profileEntries = readdirSync(profilesDir, { withFileTypes: true });
43
+ } catch (error) {
44
+ onReadError?.(error, "profiles-dir");
45
+ return bundles;
46
+ }
47
+ for (const profile of profileEntries) {
35
48
  if (!profile.isDirectory()) continue;
36
49
  try {
37
50
  const manifest = JSON.parse(readFileSync(join(profilesDir, profile.name, "package.json"), "utf8"));
38
51
  for (const name of manifest.dsh?.profile?.bundles ?? []) if (EVOLUTION_BUNDLE_TAILS.has(tailOf(name))) bundles.push(name);
39
- } catch {}
52
+ } catch (error) {
53
+ onReadError?.(error, "profile-manifest");
54
+ }
40
55
  }
41
56
  return bundles;
42
57
  }
@@ -58,18 +73,21 @@ function envIssues() {
58
73
  */
59
74
  async function diagnose(ctx, options = {}) {
60
75
  const home = options.home ?? evolutionRoot();
61
- const bundles = collectEvolutionBundles(home);
76
+ const conflicts = [];
77
+ const bundles = collectEvolutionBundles(home, (error, scope) => {
78
+ const detail = error instanceof Error ? error.message : String(error);
79
+ conflicts.push(scope === "profiles-dir" ? `the home profiles directory could not be enumerated (${detail}) — bundle-based install-form and conflict checks are DEGRADED and may miss installed bundles` : `a profile manifest could not be read or parsed (${detail}) — this profile's bundle rows are UNKNOWN, so the install-form and conflict checks are DEGRADED and may miss installed bundles`);
80
+ });
62
81
  const has = (name) => ctx.get(name) !== void 0;
63
82
  const full = bundles.some((name) => tailOf(name) === "dsh-evolution-all");
64
83
  const host = bundles.some((name) => tailOf(name) === "dsh-evolution-host");
65
84
  const preset = bundles.some((name) => tailOf(name) === "dsh-evolution-preset");
66
85
  const presetDirInstalled = existsSync(join(join(home, ".agent-presets", "evolution"), "agent.cordis.yml"));
67
86
  const layered = host && presetDirInstalled;
68
- const conflicts = [];
69
87
  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.");
70
88
  if (full && preset) conflicts.push("evolution-all and evolution-preset are installed together — the infra rows double-mount. Keep ONE.");
71
89
  if (host && preset) conflicts.push("evolution-host and evolution-preset are installed together — the infra rows double-mount. Keep ONE.");
72
- 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).");
90
+ if (full && presetDirInstalled) 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).");
73
91
  if (preset && presetDirInstalled) conflicts.push("evolution-preset and the layered Evolution preset are both present — the model rows double-mount (the preset bundle carries the same model rows as all). Keep ONE (drop the preset bundle, or remove the layered preset).");
74
92
  const services = {
75
93
  review: full || host || preset,
@@ -99,7 +117,7 @@ async function diagnose(ctx, options = {}) {
99
117
  if (env.length > 0) actions.push("Fix the DSH_EVOLUTION_* variable listed above.");
100
118
  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.");
101
119
  if (pendingCount === null && services.approval) actions.push("Approval service is mounted but pending listing failed — check the evolution state service.");
102
- if ((executingCount ?? 0) > 0) actions.push(`${executingCount} staged write(s) are stuck EXECUTING (the approving run crashed) — inspect with /evolution pending and reject them after verifying the write effect.`);
120
+ 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.`);
103
121
  return {
104
122
  installForm,
105
123
  bundles,
@@ -115,7 +133,7 @@ function renderDoctorText(report) {
115
133
  const lines = [
116
134
  `Evolution doctor — install form: ${report.installForm}`,
117
135
  `bundles (all profiles): ${report.bundles.length > 0 ? report.bundles.join(", ") : "(none)"}`,
118
- `services: review=${report.services.review} curator=${report.services.curator} approval=${report.services.approval} skillUsage=${report.services.skillUsage} io=${report.services.io}`,
136
+ `services: review=${report.services.review} (inferred from bundles, all profiles) curator=${report.services.curator} approval=${report.services.approval} skillUsage=${report.services.skillUsage} io=${report.services.io}`,
119
137
  `pending: ${report.pendingCount === null ? "unknown" : report.pendingCount}`,
120
138
  `executing: ${report.executingCount === null ? "unknown" : report.executingCount}`
121
139
  ];
@@ -217,21 +235,20 @@ const Config = z.object({
217
235
  });
218
236
  function apply(ctx, rawConfig = {}) {
219
237
  const config = rawConfig;
220
- const rootConfig = resolveRootConfig(rawConfig);
221
- if (rootConfig.usedDeprecatedAlias) ctx.logger.warn("evolution-commands: config \"skillsRoot\" is deprecated (E-7); use \"root\" — the alias is honoured until 0.3.65");
222
- const skillsRootValue = rootConfig.root;
238
+ assertSkillsRootAliasRetired(rawConfig);
239
+ const skillsRootValue = resolveRootConfig(rawConfig).root;
223
240
  let lastMaintainAt = 0;
224
241
  let lastMaintainRunId = "";
225
242
  let maintainInFlightSince = 0;
226
243
  ctx.inject(["commands"], (commandCtx) => {
227
244
  const commands = commandCtx.commands;
228
- commandCtx.effect(() => commands.register({
245
+ const evolutionCommand = {
229
246
  name: "evolution",
230
247
  description: "Self-evolution status and approval controls",
231
248
  recordInput: false,
232
249
  input: { hint: renderHint() },
233
- async handler(invocation) {
234
- const rawInputTrimmed = invocation.rawInput?.trim() ?? "";
250
+ run: async (invocation) => {
251
+ const rawInputTrimmed = invocation.rawInput.trim();
235
252
  const input = rawInputTrimmed.replace(/\s+/g, " ");
236
253
  const ok = (text) => ({
237
254
  kind: "success",
@@ -244,7 +261,8 @@ function apply(ctx, rawConfig = {}) {
244
261
  const approval = ctx.get("evolutionApproval");
245
262
  const pendingMatch = /^pending(?: --detail)?$/.exec(input);
246
263
  if (pendingMatch) {
247
- const pending = approval ? [...await approval.list("pending"), ...await approval.list("executing")] : [];
264
+ const listed = approval ? [...await approval.list("pending"), ...await approval.list("executing")] : [];
265
+ const pending = [...new Map(listed.map((row) => [row.id, row])).values()];
248
266
  if (pending.length === 0) return ok("No pending evolution writes.");
249
267
  const detailed = pendingMatch[0] === "pending --detail";
250
268
  return ok(pending.map((p) => {
@@ -385,7 +403,7 @@ function apply(ctx, rawConfig = {}) {
385
403
  }
386
404
  if (input === "learn" || input.startsWith("learn ")) {
387
405
  const request = rawInputTrimmed === "learn" ? "" : rawInputTrimmed.slice(6).trim();
388
- invocation.agent.inject(createUserMessage({
406
+ const message = createUserMessage({
389
407
  content: [{
390
408
  type: "text",
391
409
  text: buildLearnPrompt(request)
@@ -396,7 +414,11 @@ function apply(ctx, rawConfig = {}) {
396
414
  form: "notice",
397
415
  summary: "learn request"
398
416
  }
399
- }));
417
+ });
418
+ const followup = invocation.agent.followup;
419
+ const woke = typeof followup === "function";
420
+ if (woke) followup(message);
421
+ else invocation.agent.inject(message);
400
422
  const eventIo = ctx.get("evolutionIo")?.provider();
401
423
  if (eventIo) appendEvolutionEvent(eventIo, eventsFile(evolutionRoot()), {
402
424
  type: "learn",
@@ -405,7 +427,7 @@ function apply(ctx, rawConfig = {}) {
405
427
  }).catch((error) => {
406
428
  ctx.logger.warn(`evolution-commands: failed to record learn event: ${String(error)}`);
407
429
  });
408
- return ok("Learning request sent to this session. Follow it now.");
430
+ return woke ? ok("Learning request sent to this session. Follow it now.") : ok("Learning request queued for this session — this host exposes no wake-up channel, so it is read on your next message.");
409
431
  }
410
432
  if (input === "maintain --facts") {
411
433
  const ioRegistry = ctx.get("evolutionIo");
@@ -424,7 +446,7 @@ function apply(ctx, rawConfig = {}) {
424
446
  const maintainArgs = /^maintain(?:\s+--timeout[ =](\d+))?\s*$/.exec(input);
425
447
  if (maintainArgs) {
426
448
  const runTimeoutMs = maintainArgs[1] ? Number(maintainArgs[1]) : config.maintainTimeoutMs ?? 6e5;
427
- if (!Number.isSafeInteger(runTimeoutMs) || runTimeoutMs <= 0 || runTimeoutMs > 2147483647) return err(maintainArgs[1] ? "Invalid --timeout value: expected a positive integer number of milliseconds up to 2147483647 (e.g. /evolution maintain --timeout 600000)." : "The maintainTimeoutMs config is invalid: expected a positive integer number of milliseconds up to 2147483647. Fix the evolution-commands row config (maintainTimeoutMs), then retry.");
449
+ if (!Number.isSafeInteger(runTimeoutMs) || runTimeoutMs <= 0 || runTimeoutMs > MAX_TIMER_DELAY_MS) return err(maintainArgs[1] ? `Invalid --timeout value: expected a positive integer number of milliseconds up to ${MAX_TIMER_DELAY_MS} (e.g. /evolution maintain --timeout 600000).` : `The maintainTimeoutMs config is invalid: expected a positive integer number of milliseconds up to ${MAX_TIMER_DELAY_MS}. Fix the evolution-commands row config (maintainTimeoutMs), then retry.`);
428
450
  if (maintainInFlightSince > 0) return err(`Maintenance scan is already running (since ~${Math.max(1, Math.round((Date.now() - maintainInFlightSince) / 1e3))}s ago) — re-submitting now would cancel it. Wait for it to settle; the result appears when it finishes.`);
429
451
  const cooldownMs = clampedNumber(config.maintainCooldownMs, 3e4, { min: 0 });
430
452
  const sinceLast = Date.now() - lastMaintainAt;
@@ -445,7 +467,9 @@ function apply(ctx, rawConfig = {}) {
445
467
  tools: { get: (name) => ctx.get("tools")?.get(name) },
446
468
  logger: { warn: (message) => {
447
469
  ctx.logger.warn(message);
448
- } }
470
+ } },
471
+ rootExists: skillsRootValue === "" ? void 0 : () => ioRegistry.provider().exists(skillsRootValue),
472
+ skillRoot: skillsRootValue
449
473
  }, {
450
474
  timeoutMs: runTimeoutMs,
451
475
  signal: invocation.signal,
@@ -477,6 +501,19 @@ function apply(ctx, rawConfig = {}) {
477
501
  }
478
502
  if (/^maintain\b/.test(input)) return err("Unknown maintain arguments: expected bare `maintain`, `maintain --timeout <ms>` or `maintain --timeout=<ms>` (a positive integer). Got: " + input);
479
503
  if (input === "preset install") {
504
+ {
505
+ let installedBundles;
506
+ try {
507
+ installedBundles = collectEvolutionBundles(evolutionRoot(), (error) => {
508
+ throw error;
509
+ });
510
+ } catch (enumerationError) {
511
+ return err(`Refusing to install the layered Evolution preset: the home profile directory or one of its profile manifests could not be read (${enumerationError instanceof Error ? enumerationError.message : String(enumerationError)}) — the all/preset double-mount check could not run. Resolve access to the profiles directory and retry, or install via the layered installer, which enforces the same exclusion up front.`);
512
+ }
513
+ const allBundle = installedBundles.find((name) => name === "dsh-evolution-all" || name.endsWith("/dsh-evolution-all"));
514
+ const presetBundle = installedBundles.find((name) => name === "dsh-evolution-preset" || name.endsWith("/dsh-evolution-preset"));
515
+ if (allBundle || presetBundle) return err(`Refusing to install the layered Evolution preset: "${allBundle ?? presetBundle}" is installed in this home — its model rows (tool-memory / tool-skill-manage / tool-session-query / skill-catalog) would double-mount with the preset's and the profile fails loud at startup. Keep ONE: remove the ${allBundle ? "evolution-all bundle" : "evolution-preset bundle"} before installing the layered preset (see /evolution doctor).`);
516
+ }
480
517
  const target = join(evolutionRoot(), ".agent-presets", "evolution");
481
518
  try {
482
519
  const source = resolveAgentPresetDir(import.meta.url);
@@ -533,8 +570,8 @@ function apply(ctx, rawConfig = {}) {
533
570
  libraryOrigin: "foreground"
534
571
  },
535
572
  origin: "foreground",
536
- ...session?.id ? { sessionId: session.id } : {},
537
- ...session ? { session } : {},
573
+ ...session.id ? { sessionId: session.id } : {},
574
+ session,
538
575
  ...sessionPolicy !== void 0 ? { sessionPolicy } : {}
539
576
  });
540
577
  if (decision.action === "staged") return ok(`${decision.message}${planRunId ? `\n[audit] plan=${planRunId}` : ""}`);
@@ -560,8 +597,13 @@ function apply(ctx, rawConfig = {}) {
560
597
  return ok(renderDoctorText(report));
561
598
  }
562
599
  return ok(`Evolution: memory, skills, review, curator — self-evolution status, approval and maintenance.\n${renderHelpText()}`);
563
- }
564
- }));
600
+ },
601
+ handler: (invocation) => evolutionCommand.run(invocation).catch((error) => ({
602
+ kind: "error",
603
+ text: `evolution: command failed: ${error instanceof Error ? error.message : String(error)}\nRun /evolution doctor to inspect services and state files; a corrupted state file is quarantined as <file>.corrupt for inspection.`
604
+ }))
605
+ };
606
+ commandCtx.effect(() => commands.register(evolutionCommand));
565
607
  });
566
608
  }
567
609
  /**
@@ -21,8 +21,14 @@ export interface DoctorReport {
21
21
  executingCount: number | null;
22
22
  actions: string[];
23
23
  }
24
- /** Profile bundle rows for evolution-family packages across all profiles. */
25
- export declare function collectEvolutionBundles(home: string): string[];
24
+ /** Profile bundle rows for evolution-family packages across all profiles.
25
+ *
26
+ * `onReadError` reports a profile whose manifest could not be READ or PARSED.
27
+ * Like the directory-enumeration failure, it is fail-closed at every caller:
28
+ * a truncated manifest is indistinguishable from "no bundles" by the returned
29
+ * list alone, and treating it as "no bundles" is exactly the fail-open state
30
+ * the double-mount exclusion exists to prevent. */
31
+ export declare function collectEvolutionBundles(home: string, onReadError?: (error: unknown, scope: 'profiles-dir' | 'profile-manifest') => void): string[];
26
32
  /**
27
33
  * Diagnose the deployment. `ctx` supplies service presence (a bare stub with
28
34
  * only `get` is enough); `home` defaults to the evolution root so tests can
@@ -11,8 +11,9 @@ export interface Config {
11
11
  * resolved through resolveSkillsRoot() so an empty/whitespace value falls
12
12
  * back to the default instead of a CWD-relative root. */
13
13
  root?: string | undefined;
14
- /** Deprecated alias of `root` (E-7, v18): honoured only while `root` is
15
- * empty, with a warning; removed after 0.3.65. */
14
+ /** V27 G2.4 (M-08): RETIRED alias of `root`. The field stays declared so the
15
+ * loader can hand it to the load-time gate (which rejects it loudly) instead
16
+ * of dropping it silently; the plugin never reads it as a root. */
16
17
  skillsRoot?: string | undefined;
17
18
  /** Cooldown window for scan commands (ms) — misclick/rapid-trigger guard;
18
19
  * secondary calls inside the window return the previous runId instead of
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.3.67",
4
+ "version": "0.3.69",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -31,19 +31,21 @@
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
33
  "@deepseek-ai/schemastery": "^3.18.1",
34
- "@lmzhen/dsh-evolution-approval": "^0.3.67",
35
- "@lmzhen/dsh-evolution-core": "^0.3.67",
36
- "@lmzhen/dsh-evolution-maintenance": "^0.3.67"
34
+ "@lmzhen/dsh-evolution-approval": "^0.3.69",
35
+ "@lmzhen/dsh-evolution-core": "^0.3.69",
36
+ "@lmzhen/dsh-evolution-maintenance": "^0.3.69"
37
37
  },
38
38
  "optionalDependencies": {
39
- "@lmzhen/dsh-evolution-agent-preset": "^0.3.67"
39
+ "@lmzhen/dsh-evolution-agent-preset": "^0.3.69"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@deepseek-ai/cordis": "^4.0.1",
43
+ "@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
43
44
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
44
45
  "@deepseek-ai/dsh-llm": "^0.1.1-rc.2"
45
46
  },
46
47
  "devDependencies": {
48
+ "@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
47
49
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2"
48
50
  }
49
51
  }