@lmzhen/dsh-evolution-commands 0.3.23 → 0.3.25

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
@@ -23,7 +23,7 @@ function apply(ctx, rawConfig = {}) {
23
23
  name: "evolution",
24
24
  description: "Self-evolution status and approval controls",
25
25
  recordInput: false,
26
- 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" },
26
+ input: { hint: "pending [--detail] | 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
27
  async handler(invocation) {
28
28
  const input = invocation.rawInput?.trim() ?? "";
29
29
  const ok = (text) => ({
@@ -35,10 +35,17 @@ function apply(ctx, rawConfig = {}) {
35
35
  text
36
36
  });
37
37
  const approval = ctx.get("evolutionApproval");
38
- if (input === "pending") {
38
+ const pendingMatch = /^pending(?: --detail)?$/.exec(input);
39
+ if (pendingMatch) {
39
40
  const pending = approval ? [...await approval.list("pending"), ...await approval.list("executing")] : [];
40
41
  if (pending.length === 0) return ok("No pending evolution writes.");
41
- 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)" : ""));
42
+ const detailed = pendingMatch[0] === "pending --detail";
43
+ return ok(pending.map((p) => {
44
+ if (!detailed) return `${p.id} ${p.kind} ${p.status === "executing" ? "EXECUTING " : ""}${p.summary}`;
45
+ const status = p.status === "executing" ? "EXECUTING" : p.status;
46
+ const line = `${p.id} ${p.kind} ${status} ${p.summary}`;
47
+ return p.args === void 0 ? line : `${line}\n staged args: ${safeStagedArgs(p.args)}`;
48
+ }).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)" : ""));
42
49
  }
43
50
  if (input.startsWith("approve ")) {
44
51
  const id = input.slice(8).trim();
@@ -218,7 +225,10 @@ function apply(ctx, rawConfig = {}) {
218
225
  library,
219
226
  subagents,
220
227
  parent: invocation.agent,
221
- evolutionPolicy: { get: () => ctx.get("evolutionPolicy")?.get() }
228
+ evolutionPolicy: { get: () => ctx.get("evolutionPolicy")?.get() },
229
+ logger: { warn: (message) => {
230
+ ctx.logger.warn(message);
231
+ } }
222
232
  }, {
223
233
  timeoutMs: runTimeoutMs,
224
234
  descriptions: () => enrichment.descriptions,
@@ -319,33 +329,67 @@ function resolveAgentPresetDir(importMetaUrl) {
319
329
  return siblingCandidate;
320
330
  }
321
331
  }
332
+ const defaultFs = {
333
+ writeFileSync: (path, data) => {
334
+ writeFileSync(path, data);
335
+ },
336
+ existsSync: (path) => existsSync(path),
337
+ copyFileSync: (from, to) => {
338
+ copyFileSync(from, to);
339
+ },
340
+ renameSync: (from, to) => {
341
+ renameSync(from, to);
342
+ },
343
+ rmSync: (path, options) => {
344
+ rmSync(path, options);
345
+ }
346
+ };
322
347
  /**
323
348
  * S6.3 (E-40): atomically replace a set of files within one directory.
324
349
  * Each file is staged to a sibling `<name>.tmp`, then renamed into place
325
350
  * (atomic on the same filesystem); a pre-existing file keeps a single
326
351
  * `<name>.bak`. A failure at ANY staging step removes the staged temps and
327
- * leaves the previous files untouched — a half-written target is impossible.
352
+ * leaves the previous files untouched — a half-written target is impossible
353
+ * up to the staging and backup phases.
354
+ *
355
+ * F-211: the rename COMMIT phase does not carry the same guarantee. When a
356
+ * filesystem refuses to overwrite the destination, the code removes it and
357
+ * retries the rename; if the retry ALSO fails the destination is gone. The
358
+ * single `.bak` created above still holds the previous content, so the target
359
+ * is restored from it (best-effort) before the original rename error is
360
+ * rethrown, so a failed commit never leaves the file missing.
328
361
  */
329
- function atomicWriteFiles(targetDir, writes) {
362
+ function atomicWriteFiles(targetDir, writes, fs = defaultFs) {
330
363
  const stage = (name) => join(targetDir, `${name}.tmp`);
331
364
  try {
332
- for (const { name, content } of writes) writeFileSync(stage(name), content);
365
+ for (const { name, content } of writes) fs.writeFileSync(stage(name), content);
333
366
  for (const { name } of writes) {
334
367
  const finalPath = join(targetDir, name);
335
368
  const bakPath = join(targetDir, `${name}.bak`);
336
- if (existsSync(finalPath) && !existsSync(bakPath)) copyFileSync(finalPath, bakPath);
369
+ if (fs.existsSync(finalPath) && !fs.existsSync(bakPath)) fs.copyFileSync(finalPath, bakPath);
337
370
  }
338
371
  for (const { name } of writes) {
339
372
  const finalPath = join(targetDir, name);
373
+ const bakPath = join(targetDir, `${name}.bak`);
374
+ const tmp = stage(name);
340
375
  try {
341
- renameSync(stage(name), finalPath);
376
+ fs.renameSync(tmp, finalPath);
342
377
  } catch {
343
- rmSync(finalPath, { force: true });
344
- renameSync(stage(name), finalPath);
378
+ fs.rmSync(finalPath, { force: true });
379
+ try {
380
+ fs.renameSync(tmp, finalPath);
381
+ } catch (renameError) {
382
+ if (fs.existsSync(bakPath)) try {
383
+ fs.copyFileSync(bakPath, finalPath);
384
+ } catch {
385
+ throw new Error(`atomicWriteFiles: "${name}" was removed but recovery from ${bakPath} failed (${renameError instanceof Error ? renameError.message : String(renameError)}); verify the file manually`);
386
+ }
387
+ throw renameError;
388
+ }
345
389
  }
346
390
  }
347
391
  } catch (error) {
348
- for (const { name } of writes) rmSync(stage(name), {
392
+ for (const { name } of writes) fs.rmSync(stage(name), {
349
393
  force: true,
350
394
  recursive: true
351
395
  });
@@ -365,7 +409,17 @@ function atomicWriteFiles(targetDir, writes) {
365
409
  * evolution-maintenance outcome change (out of this package's scope).
366
410
  */
367
411
  function countMaintainRecommendations(text) {
368
- return text?.match(/^- \[/gm)?.length ?? 0;
412
+ if (!text) return 0;
413
+ return (text.split("\nNotes:")[0] ?? "").match(/^- \[/gm)?.length ?? 0;
414
+ }
415
+ /** F-328: render staged args for `pending --detail`, truncated to 500 chars.
416
+ * args may be non-JSON (a tool produced garbage), so the render is fail-safe. */
417
+ function safeStagedArgs(args) {
418
+ try {
419
+ return JSON.stringify(args).slice(0, 500);
420
+ } catch {
421
+ return "(unserializable)";
422
+ }
369
423
  }
370
424
  //#endregion
371
- export { apply, countMaintainRecommendations, name };
425
+ export { apply, atomicWriteFiles, countMaintainRecommendations, name };
@@ -24,6 +24,37 @@ export interface Config {
24
24
  maintainTimeoutMs?: number | undefined;
25
25
  }
26
26
  export declare function apply(ctx: Context, rawConfig?: Config): void;
27
+ /** F-211: the fs surface atomicWriteFiles uses — injectable so the commit-phase
28
+ * rename-failure path is deterministically testable. Defaults to node:fs. */
29
+ interface FsOps {
30
+ writeFileSync(path: string, data: string | Uint8Array): void;
31
+ existsSync(path: string): boolean;
32
+ copyFileSync(from: string, to: string): void;
33
+ renameSync(from: string, to: string): void;
34
+ rmSync(path: string, options?: {
35
+ force?: boolean;
36
+ recursive?: boolean;
37
+ }): void;
38
+ }
39
+ /**
40
+ * S6.3 (E-40): atomically replace a set of files within one directory.
41
+ * Each file is staged to a sibling `<name>.tmp`, then renamed into place
42
+ * (atomic on the same filesystem); a pre-existing file keeps a single
43
+ * `<name>.bak`. A failure at ANY staging step removes the staged temps and
44
+ * leaves the previous files untouched — a half-written target is impossible
45
+ * up to the staging and backup phases.
46
+ *
47
+ * F-211: the rename COMMIT phase does not carry the same guarantee. When a
48
+ * filesystem refuses to overwrite the destination, the code removes it and
49
+ * retries the rename; if the retry ALSO fails the destination is gone. The
50
+ * single `.bak` created above still holds the previous content, so the target
51
+ * is restored from it (best-effort) before the original rename error is
52
+ * rethrown, so a failed commit never leaves the file missing.
53
+ */
54
+ export declare function atomicWriteFiles(targetDir: string, writes: Array<{
55
+ name: string;
56
+ content: string | Uint8Array;
57
+ }>, fs?: FsOps): void;
27
58
  /**
28
59
  * Count the recommendation bullets in a rendered maintain plan.
29
60
  * `formatPlan` renders each recommendation as a line starting with `- [<kind>]`;
@@ -37,4 +68,5 @@ export declare function apply(ctx: Context, rawConfig?: Config): void;
37
68
  * evolution-maintenance outcome change (out of this package's scope).
38
69
  */
39
70
  export declare function countMaintainRecommendations(text: string | undefined): number;
71
+ export {};
40
72
  //# 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.23",
4
+ "version": "0.3.25",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -30,9 +30,9 @@
30
30
  ],
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
- "@lmzhen/dsh-evolution-approval": "^0.3.23",
34
- "@lmzhen/dsh-evolution-core": "^0.3.23",
35
- "@lmzhen/dsh-evolution-maintenance": "^0.3.23"
33
+ "@lmzhen/dsh-evolution-approval": "^0.3.25",
34
+ "@lmzhen/dsh-evolution-core": "^0.3.25",
35
+ "@lmzhen/dsh-evolution-maintenance": "^0.3.25"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@deepseek-ai/cordis": "^4.0.1",