@lmzhen/dsh-evolution-commands 0.3.17 → 0.3.18

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,13 +2,13 @@ import { createRequire } from "node:module";
2
2
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
3
3
  import { SkillLibrary, appendEvolutionEvent, buildLearnPrompt, composePresetComposition, eventsFile } from "@lmzhen/dsh-evolution-core";
4
4
  import { buildEnrichment, buildMaintainFacts, runMaintain, snapshotFromLibrary } from "@lmzhen/dsh-evolution-maintenance";
5
- import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { homedir } from "node:os";
7
7
  import { dirname, join } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  //#region lib/types/index.js
10
10
  /**
11
- * Human commands for the evolution family: /evolution learn|pending|curator|restore|consolidate.
11
+ * Human commands for the evolution family: /evolution learn|pending|curator|restore|consolidate|skills refresh.
12
12
  * @module @lmzhen/dsh-evolution-commands
13
13
  */
14
14
  /** Enrichment maps shared by the full scan and the `--facts` preview (v12). */
@@ -19,11 +19,12 @@ function apply(ctx, rawConfig = {}) {
19
19
  let lastMaintainRunId = "";
20
20
  let maintainInFlightSince = 0;
21
21
  ctx.inject(["commands"], (commandCtx) => {
22
- commandCtx.commands.register({
22
+ const commands = commandCtx.commands;
23
+ commandCtx.effect(() => commands.register({
23
24
  name: "evolution",
24
25
  description: "Self-evolution status and approval controls",
25
26
  recordInput: false,
26
- input: { hint: "pending | approve <id> | reject <id> | curator run|pause|resume|status|report|scope | restore | consolidate <target> <sources...> | skill restore <name> | skills health | learn [request] | maintain [--timeout ms] | preset install | restructure <name> \"<heading>\" <to_file> | replay" },
27
+ input: { hint: "pending | 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" },
27
28
  async handler(invocation) {
28
29
  const input = invocation.rawInput?.trim() ?? "";
29
30
  const ok = (text) => ({
@@ -156,6 +157,10 @@ function apply(ctx, rawConfig = {}) {
156
157
  if (rows.length === 0) return ok(banner ? `Structure health: all skills healthy. ${banner}` : "Structure health: all skills healthy.");
157
158
  return ok([banner ? `Structure health (${rows.length} degraded): ${banner}` : `Structure health (${rows.length} degraded):`, ...rows.map(line)].join("\n"));
158
159
  }
160
+ if (input === "skills refresh") {
161
+ ctx.emit("evolution/skills-refresh");
162
+ return ok("Skill catalog refresh requested: caches dropped, catalog re-read on next lookup.");
163
+ }
159
164
  if (input === "learn" || input.startsWith("learn ")) {
160
165
  const request = input === "learn" ? "" : input.slice(6).trim();
161
166
  invocation.agent.inject(createUserMessage({
@@ -213,7 +218,8 @@ function apply(ctx, rawConfig = {}) {
213
218
  const outcome = await runMaintain({
214
219
  library,
215
220
  subagents,
216
- parent: invocation.agent
221
+ parent: invocation.agent,
222
+ evolutionPolicy: { get: () => ctx.get("evolutionPolicy")?.get() }
217
223
  }, {
218
224
  timeoutMs: runTimeoutMs,
219
225
  descriptions: () => enrichment.descriptions,
@@ -230,7 +236,7 @@ function apply(ctx, rawConfig = {}) {
230
236
  source: "manual",
231
237
  runId: outcome.runId,
232
238
  verdict: outcome.verdict,
233
- recommendations: outcome.text?.split("\n").filter((line) => line.startsWith("- [")).length ?? 0
239
+ recommendations: countMaintainRecommendations(outcome.text)
234
240
  }).catch((error) => {
235
241
  ctx.logger.warn(`evolution-commands: failed to record maintain event: ${String(error)}`);
236
242
  });
@@ -254,8 +260,13 @@ function apply(ctx, rawConfig = {}) {
254
260
  if (!registry) return err("Agent preset registry not mounted — cannot compose the Evolution preset against the runtime standard.");
255
261
  const composition = composePresetComposition(await registry.read("standard"), readFileSync(deltaPath, "utf8"));
256
262
  mkdirSync(target, { recursive: true });
257
- writeFileSync(join(target, "agent.cordis.yml"), composition);
258
- copyFileSync(presetPath, join(target, "preset.yml"));
263
+ atomicWriteFiles(target, [{
264
+ name: "agent.cordis.yml",
265
+ content: composition
266
+ }, {
267
+ name: "preset.yml",
268
+ content: readFileSync(presetPath)
269
+ }]);
259
270
  return ok(`Evolution agent preset installed to ${target} (runtime standard + delta). Restart the session switcher to select it.`);
260
271
  } catch (error) {
261
272
  return err(`Preset install failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -285,9 +296,9 @@ function apply(ctx, rawConfig = {}) {
285
296
  if (!replay) return err("Replay service not mounted.");
286
297
  return ok(replay.compare().report);
287
298
  }
288
- 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 | restore | consolidate <target> <source...> | skill restore <name> | skills health | learn [request] | maintain | preset install | restructure <name> \"<heading>\" <to_file> | replay.");
299
+ 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.");
289
300
  }
290
- });
301
+ }));
291
302
  });
292
303
  }
293
304
  /**
@@ -300,13 +311,62 @@ function apply(ctx, rawConfig = {}) {
300
311
  */
301
312
  function resolveAgentPresetDir(importMetaUrl) {
302
313
  const dir = dirname(fileURLToPath(importMetaUrl));
303
- const candidates = [join(dir, "..", "dsh-evolution-agent-preset"), join(dir, "..", "..", "evolution-agent")];
314
+ const siblingCandidate = join(dir, "..", "dsh-evolution-agent-preset");
315
+ const candidates = [siblingCandidate, join(dir, "..", "..", "evolution-agent")];
304
316
  for (const candidate of candidates) if (existsSync(join(candidate, "agent.cordis.yml")) && existsSync(join(candidate, "preset.yml"))) return candidate;
305
317
  try {
306
318
  return dirname(createRequire(importMetaUrl).resolve("@lmzhen/dsh-evolution-agent-preset/package.json"));
307
319
  } catch {
308
- return candidates[0] ?? join(dir, "..", "dsh-evolution-agent-preset");
320
+ return siblingCandidate;
309
321
  }
310
322
  }
323
+ /**
324
+ * S6.3 (E-40): atomically replace a set of files within one directory.
325
+ * Each file is staged to a sibling `<name>.tmp`, then renamed into place
326
+ * (atomic on the same filesystem); a pre-existing file keeps a single
327
+ * `<name>.bak`. A failure at ANY staging step removes the staged temps and
328
+ * leaves the previous files untouched — a half-written target is impossible.
329
+ */
330
+ function atomicWriteFiles(targetDir, writes) {
331
+ const stage = (name) => join(targetDir, `${name}.tmp`);
332
+ try {
333
+ for (const { name, content } of writes) writeFileSync(stage(name), content);
334
+ for (const { name } of writes) {
335
+ const finalPath = join(targetDir, name);
336
+ const bakPath = join(targetDir, `${name}.bak`);
337
+ if (existsSync(finalPath) && !existsSync(bakPath)) copyFileSync(finalPath, bakPath);
338
+ }
339
+ for (const { name } of writes) {
340
+ const finalPath = join(targetDir, name);
341
+ try {
342
+ renameSync(stage(name), finalPath);
343
+ } catch {
344
+ rmSync(finalPath, { force: true });
345
+ renameSync(stage(name), finalPath);
346
+ }
347
+ }
348
+ } catch (error) {
349
+ for (const { name } of writes) rmSync(stage(name), {
350
+ force: true,
351
+ recursive: true
352
+ });
353
+ throw error;
354
+ }
355
+ }
356
+ /**
357
+ * Count the recommendation bullets in a rendered maintain plan.
358
+ * `formatPlan` renders each recommendation as a line starting with `- [<kind>]`;
359
+ * this matches that bullet (anchored at line start) instead of any string
360
+ * prefix, so a `- note` line can never inflate the count.
361
+ *
362
+ * @param text - the rendered maintain result text, or `undefined`.
363
+ * @returns the number of recommendation bullets in `text`.
364
+ * @remarks the count is still text-derived — MaintainOutcome does not expose the
365
+ * structured plan array, so a structured count would require a
366
+ * evolution-maintenance outcome change (out of this package's scope).
367
+ */
368
+ function countMaintainRecommendations(text) {
369
+ return text?.match(/^- \[/gm)?.length ?? 0;
370
+ }
311
371
  //#endregion
312
- export { apply, name };
372
+ export { apply, countMaintainRecommendations, name };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Human commands for the evolution family: /evolution learn|pending|curator|restore|consolidate.
2
+ * Human commands for the evolution family: /evolution learn|pending|curator|restore|consolidate|skills refresh.
3
3
  * @module @deepseek-ai/dsh-evolution-commands
4
4
  */
5
5
  import type { Context } from '@deepseek-ai/cordis';
@@ -22,4 +22,17 @@ export interface Config {
22
22
  maintainTimeoutMs?: number | undefined;
23
23
  }
24
24
  export declare function apply(ctx: Context, rawConfig?: Config): void;
25
+ /**
26
+ * Count the recommendation bullets in a rendered maintain plan.
27
+ * `formatPlan` renders each recommendation as a line starting with `- [<kind>]`;
28
+ * this matches that bullet (anchored at line start) instead of any string
29
+ * prefix, so a `- note` line can never inflate the count.
30
+ *
31
+ * @param text - the rendered maintain result text, or `undefined`.
32
+ * @returns the number of recommendation bullets in `text`.
33
+ * @remarks the count is still text-derived — MaintainOutcome does not expose the
34
+ * structured plan array, so a structured count would require a
35
+ * evolution-maintenance outcome change (out of this package's scope).
36
+ */
37
+ export declare function countMaintainRecommendations(text: string | undefined): number;
25
38
  //# sourceMappingURL=index.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 (community build)",
4
- "version": "0.3.17",
4
+ "version": "0.3.18",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -33,8 +33,8 @@
33
33
  "license": "MIT",
34
34
  "dependencies": {
35
35
  "@deepseek-ai/schemastery": "^3.18.1",
36
- "@lmzhen/dsh-evolution-core": "^0.3.17",
37
- "@lmzhen/dsh-evolution-maintenance": "^0.3.17"
36
+ "@lmzhen/dsh-evolution-core": "^0.3.18",
37
+ "@lmzhen/dsh-evolution-maintenance": "^0.3.18"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@deepseek-ai/cordis": "^4.0.1",