@lmzhen/dsh-evolution-commands 0.3.68 → 0.3.70

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
@@ -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,14 +73,17 @@ 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.");
@@ -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
  ];
@@ -224,12 +242,12 @@ function apply(ctx, rawConfig = {}) {
224
242
  let maintainInFlightSince = 0;
225
243
  ctx.inject(["commands"], (commandCtx) => {
226
244
  const commands = commandCtx.commands;
227
- commandCtx.effect(() => commands.register({
245
+ const evolutionCommand = {
228
246
  name: "evolution",
229
247
  description: "Self-evolution status and approval controls",
230
248
  recordInput: false,
231
249
  input: { hint: renderHint() },
232
- async handler(invocation) {
250
+ run: async (invocation) => {
233
251
  const rawInputTrimmed = invocation.rawInput.trim();
234
252
  const input = rawInputTrimmed.replace(/\s+/g, " ");
235
253
  const ok = (text) => ({
@@ -243,7 +261,8 @@ function apply(ctx, rawConfig = {}) {
243
261
  const approval = ctx.get("evolutionApproval");
244
262
  const pendingMatch = /^pending(?: --detail)?$/.exec(input);
245
263
  if (pendingMatch) {
246
- 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()];
247
266
  if (pending.length === 0) return ok("No pending evolution writes.");
248
267
  const detailed = pendingMatch[0] === "pending --detail";
249
268
  return ok(pending.map((p) => {
@@ -449,7 +468,7 @@ function apply(ctx, rawConfig = {}) {
449
468
  logger: { warn: (message) => {
450
469
  ctx.logger.warn(message);
451
470
  } },
452
- rootExists: () => ioRegistry.provider().exists(skillsRootValue),
471
+ rootExists: skillsRootValue === "" ? void 0 : () => ioRegistry.provider().exists(skillsRootValue),
453
472
  skillRoot: skillsRootValue
454
473
  }, {
455
474
  timeoutMs: runTimeoutMs,
@@ -482,6 +501,19 @@ function apply(ctx, rawConfig = {}) {
482
501
  }
483
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);
484
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
+ }
485
517
  const target = join(evolutionRoot(), ".agent-presets", "evolution");
486
518
  try {
487
519
  const source = resolveAgentPresetDir(import.meta.url);
@@ -565,8 +597,13 @@ function apply(ctx, rawConfig = {}) {
565
597
  return ok(renderDoctorText(report));
566
598
  }
567
599
  return ok(`Evolution: memory, skills, review, curator — self-evolution status, approval and maintenance.\n${renderHelpText()}`);
568
- }
569
- }));
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));
570
607
  });
571
608
  }
572
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
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.68",
4
+ "version": "0.3.70",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -31,21 +31,21 @@
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
33
  "@deepseek-ai/schemastery": "^3.18.1",
34
- "@lmzhen/dsh-evolution-approval": "^0.3.68",
35
- "@lmzhen/dsh-evolution-core": "^0.3.68",
36
- "@lmzhen/dsh-evolution-maintenance": "^0.3.68"
34
+ "@lmzhen/dsh-evolution-approval": "^0.3.70",
35
+ "@lmzhen/dsh-evolution-core": "^0.3.70",
36
+ "@lmzhen/dsh-evolution-maintenance": "^0.3.70"
37
37
  },
38
38
  "optionalDependencies": {
39
- "@lmzhen/dsh-evolution-agent-preset": "^0.3.68"
39
+ "@lmzhen/dsh-evolution-agent-preset": "^0.3.70"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@deepseek-ai/cordis": "^4.0.1",
43
- "@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
44
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
45
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.2"
43
+ "@deepseek-ai/dsh-commands": "^0.1.5-rc.2",
44
+ "@deepseek-ai/dsh-invariants": "^0.1.5-rc.2",
45
+ "@deepseek-ai/dsh-llm": "^0.1.5-rc.2"
46
46
  },
47
47
  "devDependencies": {
48
- "@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
49
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2"
48
+ "@deepseek-ai/dsh-commands": "^0.1.5-rc.2",
49
+ "@deepseek-ai/dsh-invariants": "^0.1.5-rc.2"
50
50
  }
51
51
  }