@lmzhen/dsh-evolution-commands 0.3.76 → 0.3.78

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/README.md CHANGED
@@ -29,5 +29,11 @@ Independent of request-prefix construction. This package does not alter the asse
29
29
 
30
30
 
31
31
  - No known durable consumer gaps at this time. Runtime contracts are covered by package and boundary tests.
32
+ - **① variant form: the `/evolution …` command face is visible in every session**
33
+ (accepted, not fixed — 0.3.78). Commands are registered in scope layers, and the
34
+ family registers once on the host plane, so a session on a platform original
35
+ preset still lists the management-only subcommands even though no automatic
36
+ family behaviour reaches it. The difference and its reasoning are single-sourced
37
+ in `INSTALL.md` ("Known difference").
32
38
 
33
39
  **Runtime invariant:** No companion is published. The platform auto-assembles nothing and the family mounts no `<pkg>/invariant` cordis row, so a companion here would never execute (v37 S2.1 / I-3).
package/lib/index.js CHANGED
@@ -102,6 +102,165 @@ function memoryInterpolationIssues(home) {
102
102
  }
103
103
  return issues;
104
104
  }
105
+ /** G3-② (B2): the family delta container. An optionalDependency of this
106
+ * package, so resolution may legitimately fail; every failure degrades to
107
+ * "unknown" — the doctor never throws.
108
+ * Platform anchor: agentPresets.read(id) — preset/agent-presets/src/index.ts:501. */
109
+ const AGENT_PRESET_PACKAGE = "@lmzhen/dsh-evolution-agent-preset";
110
+ /** Resolve one asset the package declares in its exports map.
111
+ * @param asset - bases.json or agent.cordis.yml.
112
+ * @returns the file path, or null when the package is not installed. */
113
+ function resolveAgentPresetAsset(asset) {
114
+ try {
115
+ return fileURLToPath(import.meta.resolve(`${AGENT_PRESET_PACKAGE}/${asset}`));
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+ /** The base rows of bases.json: the comparison iterates the same table the
121
+ * installer reads, so doctor cannot check a different set of variants.
122
+ * @param path - resolved bases.json.
123
+ * @returns the rows, or null when the table is unreadable or malformed. */
124
+ function readAgentPresetBases$1(path) {
125
+ try {
126
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
127
+ if (!Array.isArray(parsed.bases)) return null;
128
+ const rows = [];
129
+ for (const raw of parsed.bases) {
130
+ const entry = raw;
131
+ if (typeof entry.name !== "string" || typeof entry.id !== "string") return null;
132
+ rows.push({
133
+ name: entry.name,
134
+ id: entry.id
135
+ });
136
+ }
137
+ return rows.length > 0 ? rows : null;
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+ /** Preset directories already on disk, used to keep an installed variant
143
+ * VISIBLE when the base table itself could not be read.
144
+ * @param root - the .agent-presets directory.
145
+ * @returns the directory names; empty when nothing is installed. */
146
+ function installedPresetIds(root) {
147
+ const ids = [];
148
+ let entries;
149
+ try {
150
+ entries = readdirSync(root, { withFileTypes: true });
151
+ } catch {
152
+ return ids;
153
+ }
154
+ for (const entry of entries) if (entry.isDirectory()) ids.push(entry.name);
155
+ return ids;
156
+ }
157
+ /**
158
+ * G3-② (B2): recompute what a fresh generation would write for every
159
+ * installed variant and compare it byte-for-byte with the file on disk.
160
+ *
161
+ * The recomputation goes through composePresetComposition — the SAME rule
162
+ * /evolution preset install and install-layered.mjs apply (collision guard +
163
+ * row-overrides.json), so doctor cannot disagree with the installer about what
164
+ * "fresh" means. Read-only by contract: the snapshot is never rewritten.
165
+ * @param home - resolved DSH_HOME.
166
+ * @param registry - the platform agent-preset registry, undefined when unmounted.
167
+ * @returns one row per base in the family bases.json.
168
+ */
169
+ async function probePresetFreshness(home, registry) {
170
+ const presetsRoot = join(home, ".agent-presets");
171
+ const basesPath = resolveAgentPresetAsset("bases.json");
172
+ const deltaPath = resolveAgentPresetAsset("agent.cordis.yml");
173
+ const bases = basesPath === null ? null : readAgentPresetBases$1(basesPath);
174
+ let delta = null;
175
+ let deltaFailure = `${AGENT_PRESET_PACKAGE}/agent.cordis.yml could not be resolved — is the family agent-preset package installed?`;
176
+ if (deltaPath !== null) try {
177
+ delta = readFileSync(deltaPath, "utf8");
178
+ } catch (error) {
179
+ deltaFailure = `${deltaPath} could not be read (${error instanceof Error ? error.message : String(error)})`;
180
+ }
181
+ if (bases === null) {
182
+ const tablePath = basesPath ?? `${AGENT_PRESET_PACKAGE}/bases.json`;
183
+ return installedPresetIds(presetsRoot).map((id) => ({
184
+ base: id,
185
+ destination: join(presetsRoot, id),
186
+ status: "unknown",
187
+ detail: `the family base table could not be read (${tablePath}) — a fresh generation cannot be recomputed`
188
+ }));
189
+ }
190
+ const rows = [];
191
+ for (const base of bases) {
192
+ const destination = join(presetsRoot, base.id);
193
+ const compositionPath = join(destination, "agent.cordis.yml");
194
+ if (!existsSync(compositionPath)) {
195
+ rows.push({
196
+ base: base.name,
197
+ destination,
198
+ status: "absent"
199
+ });
200
+ continue;
201
+ }
202
+ if (registry === void 0) {
203
+ rows.push({
204
+ base: base.name,
205
+ destination,
206
+ status: "unknown",
207
+ detail: "the platform agent-preset registry is not mounted, so the runtime composition cannot be read"
208
+ });
209
+ continue;
210
+ }
211
+ if (delta === null) {
212
+ rows.push({
213
+ base: base.name,
214
+ destination,
215
+ status: "unknown",
216
+ detail: deltaFailure
217
+ });
218
+ continue;
219
+ }
220
+ let platform;
221
+ try {
222
+ platform = await registry.read(base.name);
223
+ } catch (error) {
224
+ rows.push({
225
+ base: base.name,
226
+ destination,
227
+ status: "unknown",
228
+ detail: `the runtime composition for base "${base.name}" could not be read (${error instanceof Error ? error.message : String(error)})`
229
+ });
230
+ continue;
231
+ }
232
+ let fresh;
233
+ try {
234
+ fresh = composePresetComposition(platform, delta);
235
+ } catch (error) {
236
+ rows.push({
237
+ base: base.name,
238
+ destination,
239
+ status: "unknown",
240
+ detail: `a fresh generation refused to compose (${error instanceof Error ? error.message : String(error)})`
241
+ });
242
+ continue;
243
+ }
244
+ let onDisk;
245
+ try {
246
+ onDisk = readFileSync(compositionPath, "utf8");
247
+ } catch (error) {
248
+ rows.push({
249
+ base: base.name,
250
+ destination,
251
+ status: "unknown",
252
+ detail: `${compositionPath} could not be read (${error instanceof Error ? error.message : String(error)})`
253
+ });
254
+ continue;
255
+ }
256
+ rows.push({
257
+ base: base.name,
258
+ destination,
259
+ status: fresh === onDisk ? "fresh" : "differs"
260
+ });
261
+ }
262
+ return rows;
263
+ }
105
264
  /**
106
265
  * Diagnose the deployment. `ctx` supplies service presence (a bare stub with
107
266
  * only `get` is enough); `home` defaults to the evolution root so tests can
@@ -115,6 +274,7 @@ async function diagnose(ctx, options = {}) {
115
274
  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`);
116
275
  });
117
276
  const has = (name) => ctx.get(name) !== void 0;
277
+ const presetFreshness = await probePresetFreshness(home, ctx.get("agentPresets"));
118
278
  const full = bundles.some((name) => tailOf(name) === "dsh-evolution-all");
119
279
  const host = bundles.some((name) => tailOf(name) === "dsh-evolution-host");
120
280
  const preset = bundles.some((name) => tailOf(name) === "dsh-evolution-preset");
@@ -145,6 +305,7 @@ async function diagnose(ctx, options = {}) {
145
305
  executingCount = null;
146
306
  }
147
307
  const installForm = full ? "full" : preset ? "preset" : layered ? "layered" : host ? "host" : presetDirInstalled ? "preset-only" : "none";
308
+ const deploymentForm = installForm === "layered" ? "variant" : installForm === "full" || installForm === "preset" ? "attach" : installForm === "host" ? "host-only" : installForm === "preset-only" ? "preset-only" : "none";
148
309
  const mountConflicts = conflicts.filter((row) => !row.includes("DEGRADED"));
149
310
  const undecidable = mountConflicts.length !== conflicts.length;
150
311
  const actions = [];
@@ -152,7 +313,7 @@ async function diagnose(ctx, options = {}) {
152
313
  else if (undecidable) actions.push("The install form is UNDECIDABLE (see the DEGRADED row above): an unreadable profiles directory or profile manifest hides whatever is installed, so this report must not add or remove a bundle. Fix the reported read failure, then re-run /evolution doctor.");
153
314
  else if (installForm === "none") actions.push("Install the default full bundle: dsh plugin --profile web add @lmzhen/dsh-evolution-all");
154
315
  else if (installForm === "preset-only") actions.push("The Evolution preset is delivered but no profile mounts an evolution bundle — re-add @lmzhen/dsh-evolution-host for the layered layout (do NOT add all on top of the preset: that double-mounts the model rows), or remove .agent-presets/evolution if the layered layout is no longer wanted.");
155
- 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.");
316
+ else if (installForm === "layered") actions.push("Variant form (session opt-in): model tools follow the Evolution preset, and a session on a platform original preset carries no family rows; add @lmzhen/dsh-evolution-all instead if every session should have them.");
156
317
  const env = envIssues();
157
318
  if (env.length > 0) actions.push("Fix the DSH_EVOLUTION_* variable listed above.");
158
319
  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.");
@@ -160,8 +321,13 @@ async function diagnose(ctx, options = {}) {
160
321
  const memoryIssues = memoryInterpolationIssues(home);
161
322
  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.");
162
323
  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.`);
324
+ for (const row of presetFreshness) {
325
+ if (row.status !== "differs") continue;
326
+ actions.push(`Preset variant ${row.destination} DIFFERS from a fresh generation — it is an install-time snapshot; re-run the installer (/evolution preset install) to regenerate`);
327
+ }
163
328
  return {
164
329
  installForm,
330
+ deploymentForm,
165
331
  bundles,
166
332
  conflicts,
167
333
  envIssues: env,
@@ -169,17 +335,23 @@ async function diagnose(ctx, options = {}) {
169
335
  services,
170
336
  pendingCount,
171
337
  executingCount,
338
+ presetFreshness,
172
339
  actions
173
340
  };
174
341
  }
175
342
  function renderDoctorText(report) {
176
343
  const lines = [
177
- `Evolution doctor — install form: ${report.installForm}`,
344
+ `Evolution doctor — install form: ${report.installForm} (deployment: ${report.deploymentForm})`,
178
345
  `bundles (all profiles): ${report.bundles.length > 0 ? report.bundles.join(", ") : "(none)"}`,
179
346
  `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}`,
180
347
  `pending: ${report.pendingCount === null ? "unknown" : report.pendingCount}`,
181
348
  `executing: ${report.executingCount === null ? "unknown" : report.executingCount}`
182
349
  ];
350
+ for (const row of report.presetFreshness) {
351
+ if (row.status === "absent") continue;
352
+ const verdict = row.status === "fresh" ? "fresh — a fresh generation matches this file byte-for-byte" : row.status === "differs" ? "DIFFERS from a fresh generation — it is an install-time snapshot; re-run the installer (/evolution preset install) to regenerate" : `unknown — a fresh generation could not be recomputed (${row.detail ?? "reason not recorded"})`;
353
+ lines.push(`preset: ${row.destination} ${verdict}`);
354
+ }
183
355
  if (report.conflicts.length > 0) lines.push("conflicts:", ...report.conflicts.map((line) => ` ! ${line}`));
184
356
  if (report.envIssues.length > 0) lines.push("env:", ...report.envIssues.map((line) => ` ! ${line}`));
185
357
  if (report.memoryIssues.length > 0) lines.push("memory:", ...report.memoryIssues.map((line) => ` ! ${line}`));
@@ -242,8 +414,8 @@ const COMMAND_ENTRIES = [
242
414
  summary: "run a maintenance scan (--facts: 0-token preview)"
243
415
  },
244
416
  {
245
- usage: "preset install [--base <name>]",
246
- summary: "generate the Evolution agent preset into the user root (bases from the agent package's bases.json)"
417
+ usage: "preset install [--base <name>[,<name>...]]",
418
+ summary: "generate one Evolution agent preset per named base into the user root (bases from the agent package's bases.json)"
247
419
  },
248
420
  {
249
421
  usage: "restructure <name> \"<heading>\" <to_file> [--plan <runId>]",
@@ -555,7 +727,7 @@ function apply(ctx, rawConfig = {}) {
555
727
  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);
556
728
  const presetInstall = /^preset install(?: --base (\S+))?$/.exec(input);
557
729
  if (presetInstall !== null || /^preset\b/.test(input)) {
558
- if (presetInstall === null) return err("Unknown preset arguments: expected `preset install` or `preset install --base <name>` (the names live in the agent package's bases.json). Got: " + input);
730
+ if (presetInstall === null) return err("Unknown preset arguments: expected `preset install` or `preset install --base <name>[,<name>...]` (the names live in the agent package's bases.json). Got: " + input);
559
731
  {
560
732
  let installedBundles;
561
733
  try {
@@ -572,27 +744,43 @@ function apply(ctx, rawConfig = {}) {
572
744
  try {
573
745
  const source = resolveAgentPresetDir(import.meta.url);
574
746
  const table = readAgentPresetBases(source);
575
- const requested = presetInstall[1] ?? table.defaultBase;
576
- const base = table.entries.find((entry) => entry.name === requested);
577
- if (base === void 0) return err(`Unknown agent-preset base "${requested}" — ${join(source, "bases.json")} lists ${table.entries.map((entry) => entry.name).join(", ")}`);
578
- const target = join(evolutionRoot(), ".agent-presets", base.id);
747
+ const requestedRaw = presetInstall[1] ?? table.defaultBase;
748
+ const requested = [...new Set(requestedRaw.split(",").map((part) => part.trim()).filter((part) => part !== ""))];
749
+ if (requested.length === 0) return err(`No agent-preset base named — ${join(source, "bases.json")} lists ${table.entries.map((entry) => entry.name).join(", ")}`);
750
+ const selected = [];
751
+ for (const name of requested) {
752
+ const entry = table.entries.find((candidate) => candidate.name === name);
753
+ if (entry === void 0) return err(`Unknown agent-preset base "${name}" — ${join(source, "bases.json")} lists ${table.entries.map((candidate) => candidate.name).join(", ")}`);
754
+ selected.push(entry);
755
+ }
756
+ const rawBases = JSON.parse(readFileSync(join(source, "bases.json"), "utf8"));
579
757
  const deltaPath = join(source, "agent.cordis.yml");
580
- const presetPath = join(source, base.metadata);
581
- if (!existsSync(deltaPath) || !existsSync(presetPath)) return err(`Preset file missing from ${source} — is the dsh-evolution-agent-preset package installed?`);
758
+ if (!existsSync(deltaPath)) return err(`Preset file missing from ${source} — is the dsh-evolution-agent-preset package installed?`);
582
759
  const registry = ctx.get("agentPresets");
583
760
  if (!registry) return err("Agent preset registry not mounted — cannot compose the Evolution preset against the runtime standard.");
584
- const composition = composePresetComposition(await registry.read(base.name), readFileSync(deltaPath, "utf8"));
585
- mkdirSync(target, { recursive: true });
586
- atomicWriteFiles(target, [{
587
- name: "agent.cordis.yml",
588
- content: composition
589
- }, {
590
- name: base.metadata,
591
- content: readFileSync(presetPath)
592
- }], void 0, (message) => {
593
- ctx.logger.warn(message);
594
- });
595
- return ok(`Evolution agent preset installed to ${target} (runtime ${base.name} + delta). Restart the session switcher to select it.`);
761
+ const written = [];
762
+ for (const base of selected) {
763
+ const ability = rawBases.bases?.find((candidate) => candidate.name === base.name);
764
+ if (typeof ability?.unsupported === "string") return err(`Base "${base.name}" is registered as UNSUPPORTED: ${ability.unsupported}`);
765
+ const requiredService = ability?.requires?.service;
766
+ if (typeof requiredService === "string" && ctx.get(requiredService) === void 0) return err(`Base "${base.name}" requires the "${requiredService}" service, which this deployment does not provide — the generated preset would refuse to mount.`);
767
+ const target = join(evolutionRoot(), ".agent-presets", base.id);
768
+ const presetPath = join(source, base.metadata);
769
+ if (!existsSync(presetPath)) return err(`Preset file missing from ${source} — is the dsh-evolution-agent-preset package installed?`);
770
+ const composition = composePresetComposition(await registry.read(base.name), readFileSync(deltaPath, "utf8"));
771
+ mkdirSync(target, { recursive: true });
772
+ atomicWriteFiles(target, [{
773
+ name: "agent.cordis.yml",
774
+ content: composition
775
+ }, {
776
+ name: base.metadata,
777
+ content: readFileSync(presetPath)
778
+ }], void 0, (message) => {
779
+ ctx.logger.warn(message);
780
+ });
781
+ written.push(`${base.name} → ${target}`);
782
+ }
783
+ return ok(`Evolution agent preset installed (${written.join("; ")}). Restart the session switcher to select it.`);
596
784
  } catch (error) {
597
785
  return err(`Preset install failed: ${error instanceof Error ? error.message : String(error)}`);
598
786
  }
@@ -727,8 +915,9 @@ const defaultFs = {
727
915
  mtime: (path) => {
728
916
  try {
729
917
  return statSync(path).mtimeMs;
730
- } catch {
731
- return null;
918
+ } catch (error) {
919
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
920
+ throw error;
732
921
  }
733
922
  }
734
923
  };
@@ -1,3 +1,15 @@
1
+ /** G3-② (B2): one delivered preset variant compared against a fresh generation.
2
+ * "absent" is the user simply not using that base (never an action); "unknown"
3
+ * is a comparison that could not run, kept distinct from a verified "fresh". */
4
+ export interface PresetFreshnessRow {
5
+ /** Agent-preset base name (the id the platform registry read() takes). */
6
+ base: string;
7
+ /** Install destination <home>/.agent-presets/<base.id> that was compared. */
8
+ destination: string;
9
+ status: 'fresh' | 'differs' | 'absent' | 'unknown';
10
+ /** Why the comparison could not run — set on unknown rows only. */
11
+ detail?: string;
12
+ }
1
13
  export interface DoctorReport {
2
14
  /** OPT-23 (2026-09): `preset-only` — the delivered Evolution preset
3
15
  * artifact exists but NO profile carries an evolution bundle (e.g. the
@@ -6,6 +18,13 @@ export interface DoctorReport {
6
18
  * installing `all` — which double-mounts the preset's model rows (the
7
19
  * exact conflict this report flags). */
8
20
  installForm: 'full' | 'host' | 'preset' | 'layered' | 'preset-only' | 'none';
21
+ /** The PRODUCT form the install form implies (0.3.77, C axis). `variant` =
22
+ * session-level opt-in: the model tools live in the Evolution preset, so a
23
+ * session on any platform original preset carries none of the family and the
24
+ * cross-session consumers skip it (config `sessionScoped`). `attach` =
25
+ * profile-level: the model rows sit at profile root and every session carries
26
+ * them. The two are mutually exclusive install targets (E-33). */
27
+ deploymentForm: 'variant' | 'attach' | 'host-only' | 'preset-only' | 'none';
9
28
  /** Aggregated across ALL profiles under `home` (N13, v12): doctor answers
10
29
  * "is any profile carrying an evolution bundle / which install forms exist"
11
30
  * — not "what this runtime mounted". `services.review` is the runtime-side
@@ -31,6 +50,12 @@ export interface DoctorReport {
31
50
  /** v23 (AP-2): claimed-but-crashed records — the only state that needs an
32
51
  * operator action (reject) to clear; surfaced separately from pending. */
33
52
  executingCount: number | null;
53
+ /** G3-② (B2): how the delivered preset variants compare with what a fresh
54
+ * generation would write against THIS runtime platform. A variant embeds the
55
+ * platform composition of its install day, so a platform change or a family
56
+ * upgrade leaves a file describing a platform that no longer exists. Read-only:
57
+ * a stale snapshot is reported, never repaired. */
58
+ presetFreshness: PresetFreshnessRow[];
34
59
  actions: string[];
35
60
  }
36
61
  /** Profile bundle rows for evolution-family packages across all profiles.
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.76",
4
+ "version": "0.3.78",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,12 +27,12 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@deepseek-ai/schemastery": "^3.18.1",
30
- "@lmzhen/dsh-evolution-approval": "^0.3.76",
31
- "@lmzhen/dsh-evolution-core": "^0.3.76",
32
- "@lmzhen/dsh-evolution-maintenance": "^0.3.76"
30
+ "@lmzhen/dsh-evolution-approval": "^0.3.78",
31
+ "@lmzhen/dsh-evolution-core": "^0.3.78",
32
+ "@lmzhen/dsh-evolution-maintenance": "^0.3.78"
33
33
  },
34
34
  "optionalDependencies": {
35
- "@lmzhen/dsh-evolution-agent-preset": "^0.3.76"
35
+ "@lmzhen/dsh-evolution-agent-preset": "^0.3.78"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@deepseek-ai/cordis": "^4.0.1",