@lmzhen/dsh-evolution-commands 0.3.16 → 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) => ({
@@ -36,8 +37,9 @@ function apply(ctx, rawConfig = {}) {
36
37
  });
37
38
  const approval = ctx.get("evolutionApproval");
38
39
  if (input === "pending") {
39
- const pending = approval ? await approval.list("pending") : [];
40
- return ok(pending.length === 0 ? "No pending evolution writes." : pending.map((p) => `${p.id} ${p.kind} ${p.summary}`).join("\n"));
40
+ const pending = approval ? [...await approval.list("pending"), ...await approval.list("executing")] : [];
41
+ if (pending.length === 0) return ok("No pending evolution writes.");
42
+ return ok(pending.map((p) => `${p.id} ${p.kind} ${p.status === "executing" ? "EXECUTING " : ""}${p.summary}`).join("\n") + (pending.some((p) => p.status === "executing") ? "\n(EXECUTING: a previous approve may have crashed after running; verify the write manually, then reject — approve will not re-run it)" : ""));
41
43
  }
42
44
  if (input.startsWith("approve ")) {
43
45
  const id = input.slice(8).trim();
@@ -155,6 +157,10 @@ function apply(ctx, rawConfig = {}) {
155
157
  if (rows.length === 0) return ok(banner ? `Structure health: all skills healthy. ${banner}` : "Structure health: all skills healthy.");
156
158
  return ok([banner ? `Structure health (${rows.length} degraded): ${banner}` : `Structure health (${rows.length} degraded):`, ...rows.map(line)].join("\n"));
157
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
+ }
158
164
  if (input === "learn" || input.startsWith("learn ")) {
159
165
  const request = input === "learn" ? "" : input.slice(6).trim();
160
166
  invocation.agent.inject(createUserMessage({
@@ -212,7 +218,8 @@ function apply(ctx, rawConfig = {}) {
212
218
  const outcome = await runMaintain({
213
219
  library,
214
220
  subagents,
215
- parent: invocation.agent
221
+ parent: invocation.agent,
222
+ evolutionPolicy: { get: () => ctx.get("evolutionPolicy")?.get() }
216
223
  }, {
217
224
  timeoutMs: runTimeoutMs,
218
225
  descriptions: () => enrichment.descriptions,
@@ -229,7 +236,7 @@ function apply(ctx, rawConfig = {}) {
229
236
  source: "manual",
230
237
  runId: outcome.runId,
231
238
  verdict: outcome.verdict,
232
- recommendations: outcome.text?.split("\n").filter((line) => line.startsWith("- [")).length ?? 0
239
+ recommendations: countMaintainRecommendations(outcome.text)
233
240
  }).catch((error) => {
234
241
  ctx.logger.warn(`evolution-commands: failed to record maintain event: ${String(error)}`);
235
242
  });
@@ -253,8 +260,13 @@ function apply(ctx, rawConfig = {}) {
253
260
  if (!registry) return err("Agent preset registry not mounted — cannot compose the Evolution preset against the runtime standard.");
254
261
  const composition = composePresetComposition(await registry.read("standard"), readFileSync(deltaPath, "utf8"));
255
262
  mkdirSync(target, { recursive: true });
256
- writeFileSync(join(target, "agent.cordis.yml"), composition);
257
- 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
+ }]);
258
270
  return ok(`Evolution agent preset installed to ${target} (runtime standard + delta). Restart the session switcher to select it.`);
259
271
  } catch (error) {
260
272
  return err(`Preset install failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -284,9 +296,9 @@ function apply(ctx, rawConfig = {}) {
284
296
  if (!replay) return err("Replay service not mounted.");
285
297
  return ok(replay.compare().report);
286
298
  }
287
- 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.");
288
300
  }
289
- });
301
+ }));
290
302
  });
291
303
  }
292
304
  /**
@@ -299,13 +311,62 @@ function apply(ctx, rawConfig = {}) {
299
311
  */
300
312
  function resolveAgentPresetDir(importMetaUrl) {
301
313
  const dir = dirname(fileURLToPath(importMetaUrl));
302
- 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")];
303
316
  for (const candidate of candidates) if (existsSync(join(candidate, "agent.cordis.yml")) && existsSync(join(candidate, "preset.yml"))) return candidate;
304
317
  try {
305
318
  return dirname(createRequire(importMetaUrl).resolve("@lmzhen/dsh-evolution-agent-preset/package.json"));
306
319
  } catch {
307
- return candidates[0] ?? join(dir, "..", "dsh-evolution-agent-preset");
320
+ return siblingCandidate;
308
321
  }
309
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
+ }
310
371
  //#endregion
311
- 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.16",
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.16",
37
- "@lmzhen/dsh-evolution-maintenance": "^0.3.16"
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",