@lmzhen/dsh-evolution-commands 0.3.22 → 0.3.24
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 +66 -13
- package/lib/types/index.d.ts +32 -0
- package/package.json +4 -4
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
|
-
|
|
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
|
-
|
|
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(
|
|
376
|
+
fs.renameSync(tmp, finalPath);
|
|
342
377
|
} catch {
|
|
343
|
-
rmSync(finalPath, { force: true });
|
|
344
|
-
|
|
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
|
});
|
|
@@ -367,5 +411,14 @@ function atomicWriteFiles(targetDir, writes) {
|
|
|
367
411
|
function countMaintainRecommendations(text) {
|
|
368
412
|
return text?.match(/^- \[/gm)?.length ?? 0;
|
|
369
413
|
}
|
|
414
|
+
/** F-328: render staged args for `pending --detail`, truncated to 500 chars.
|
|
415
|
+
* args may be non-JSON (a tool produced garbage), so the render is fail-safe. */
|
|
416
|
+
function safeStagedArgs(args) {
|
|
417
|
+
try {
|
|
418
|
+
return JSON.stringify(args).slice(0, 500);
|
|
419
|
+
} catch {
|
|
420
|
+
return "(unserializable)";
|
|
421
|
+
}
|
|
422
|
+
}
|
|
370
423
|
//#endregion
|
|
371
|
-
export { apply, countMaintainRecommendations, name };
|
|
424
|
+
export { apply, atomicWriteFiles, countMaintainRecommendations, name };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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.
|
|
4
|
+
"version": "0.3.24",
|
|
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.
|
|
34
|
-
"@lmzhen/dsh-evolution-core": "^0.3.
|
|
35
|
-
"@lmzhen/dsh-evolution-maintenance": "^0.3.
|
|
33
|
+
"@lmzhen/dsh-evolution-approval": "^0.3.24",
|
|
34
|
+
"@lmzhen/dsh-evolution-core": "^0.3.24",
|
|
35
|
+
"@lmzhen/dsh-evolution-maintenance": "^0.3.24"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@deepseek-ai/cordis": "^4.0.1",
|