@lmzhen/dsh-evolution-commands 0.3.67 → 0.3.69
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 +68 -26
- package/lib/types/doctor.d.ts +8 -2
- package/lib/types/index.d.ts +3 -2
- package/package.json +7 -5
package/lib/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { effectiveSessionPolicy } from "@lmzhen/dsh-evolution-approval";
|
|
4
4
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
5
|
-
import { SkillLibrary, appendEvolutionEvent, buildLearnPrompt, clampedNumber, composePresetComposition, eventsFile, evolutionRoot, resolveRootConfig, resolveSkillsRoot } from "@lmzhen/dsh-evolution-core";
|
|
5
|
+
import { MAX_TIMER_DELAY_MS, SkillLibrary, appendEvolutionEvent, assertSkillsRootAliasRetired, buildLearnPrompt, clampedNumber, composePresetComposition, eventsFile, evolutionRoot, resolveRootConfig, resolveSkillsRoot } from "@lmzhen/dsh-evolution-core";
|
|
6
6
|
import { buildEnrichment, buildMaintainFacts, runMaintain, snapshotFromLibrary } from "@lmzhen/dsh-evolution-maintenance";
|
|
7
7
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
@@ -26,17 +26,32 @@ const EVOLUTION_BUNDLE_TAILS = new Set([
|
|
|
26
26
|
"dsh-evolution-host",
|
|
27
27
|
"dsh-evolution-preset"
|
|
28
28
|
]);
|
|
29
|
-
/** Profile bundle rows for evolution-family packages across all profiles.
|
|
30
|
-
|
|
29
|
+
/** Profile bundle rows for evolution-family packages across all profiles.
|
|
30
|
+
*
|
|
31
|
+
* `onReadError` reports a profile whose manifest could not be READ or PARSED.
|
|
32
|
+
* Like the directory-enumeration failure, it is fail-closed at every caller:
|
|
33
|
+
* a truncated manifest is indistinguishable from "no bundles" by the returned
|
|
34
|
+
* list alone, and treating it as "no bundles" is exactly the fail-open state
|
|
35
|
+
* the double-mount exclusion exists to prevent. */
|
|
36
|
+
function collectEvolutionBundles(home, onReadError) {
|
|
31
37
|
const profilesDir = join(home, "profiles");
|
|
32
38
|
if (!existsSync(profilesDir)) return [];
|
|
33
39
|
const bundles = [];
|
|
34
|
-
|
|
40
|
+
let profileEntries;
|
|
41
|
+
try {
|
|
42
|
+
profileEntries = readdirSync(profilesDir, { withFileTypes: true });
|
|
43
|
+
} catch (error) {
|
|
44
|
+
onReadError?.(error, "profiles-dir");
|
|
45
|
+
return bundles;
|
|
46
|
+
}
|
|
47
|
+
for (const profile of profileEntries) {
|
|
35
48
|
if (!profile.isDirectory()) continue;
|
|
36
49
|
try {
|
|
37
50
|
const manifest = JSON.parse(readFileSync(join(profilesDir, profile.name, "package.json"), "utf8"));
|
|
38
51
|
for (const name of manifest.dsh?.profile?.bundles ?? []) if (EVOLUTION_BUNDLE_TAILS.has(tailOf(name))) bundles.push(name);
|
|
39
|
-
} catch {
|
|
52
|
+
} catch (error) {
|
|
53
|
+
onReadError?.(error, "profile-manifest");
|
|
54
|
+
}
|
|
40
55
|
}
|
|
41
56
|
return bundles;
|
|
42
57
|
}
|
|
@@ -58,18 +73,21 @@ function envIssues() {
|
|
|
58
73
|
*/
|
|
59
74
|
async function diagnose(ctx, options = {}) {
|
|
60
75
|
const home = options.home ?? evolutionRoot();
|
|
61
|
-
const
|
|
76
|
+
const conflicts = [];
|
|
77
|
+
const bundles = collectEvolutionBundles(home, (error, scope) => {
|
|
78
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
79
|
+
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`);
|
|
80
|
+
});
|
|
62
81
|
const has = (name) => ctx.get(name) !== void 0;
|
|
63
82
|
const full = bundles.some((name) => tailOf(name) === "dsh-evolution-all");
|
|
64
83
|
const host = bundles.some((name) => tailOf(name) === "dsh-evolution-host");
|
|
65
84
|
const preset = bundles.some((name) => tailOf(name) === "dsh-evolution-preset");
|
|
66
85
|
const presetDirInstalled = existsSync(join(join(home, ".agent-presets", "evolution"), "agent.cordis.yml"));
|
|
67
86
|
const layered = host && presetDirInstalled;
|
|
68
|
-
const conflicts = [];
|
|
69
87
|
if (full && host) conflicts.push("evolution-all and evolution-host are installed together — the infra rows double-mount and startup fails loud. Keep ONE: remove the other bundle.");
|
|
70
88
|
if (full && preset) conflicts.push("evolution-all and evolution-preset are installed together — the infra rows double-mount. Keep ONE.");
|
|
71
89
|
if (host && preset) conflicts.push("evolution-host and evolution-preset are installed together — the infra rows double-mount. Keep ONE.");
|
|
72
|
-
if (full &&
|
|
90
|
+
if (full && presetDirInstalled) conflicts.push("evolution-all and the layered Evolution preset are both present — the model rows double-mount. Keep ONE (use layered without all, or drop the preset).");
|
|
73
91
|
if (preset && presetDirInstalled) conflicts.push("evolution-preset and the layered Evolution preset are both present — the model rows double-mount (the preset bundle carries the same model rows as all). Keep ONE (drop the preset bundle, or remove the layered preset).");
|
|
74
92
|
const services = {
|
|
75
93
|
review: full || host || preset,
|
|
@@ -99,7 +117,7 @@ async function diagnose(ctx, options = {}) {
|
|
|
99
117
|
if (env.length > 0) actions.push("Fix the DSH_EVOLUTION_* variable listed above.");
|
|
100
118
|
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.");
|
|
101
119
|
if (pendingCount === null && services.approval) actions.push("Approval service is mounted but pending listing failed — check the evolution state service.");
|
|
102
|
-
if ((executingCount ?? 0) > 0) actions.push(`${executingCount} staged write(s) are
|
|
120
|
+
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.`);
|
|
103
121
|
return {
|
|
104
122
|
installForm,
|
|
105
123
|
bundles,
|
|
@@ -115,7 +133,7 @@ function renderDoctorText(report) {
|
|
|
115
133
|
const lines = [
|
|
116
134
|
`Evolution doctor — install form: ${report.installForm}`,
|
|
117
135
|
`bundles (all profiles): ${report.bundles.length > 0 ? report.bundles.join(", ") : "(none)"}`,
|
|
118
|
-
`services: review=${report.services.review} curator=${report.services.curator} approval=${report.services.approval} skillUsage=${report.services.skillUsage} io=${report.services.io}`,
|
|
136
|
+
`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}`,
|
|
119
137
|
`pending: ${report.pendingCount === null ? "unknown" : report.pendingCount}`,
|
|
120
138
|
`executing: ${report.executingCount === null ? "unknown" : report.executingCount}`
|
|
121
139
|
];
|
|
@@ -217,21 +235,20 @@ const Config = z.object({
|
|
|
217
235
|
});
|
|
218
236
|
function apply(ctx, rawConfig = {}) {
|
|
219
237
|
const config = rawConfig;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const skillsRootValue = rootConfig.root;
|
|
238
|
+
assertSkillsRootAliasRetired(rawConfig);
|
|
239
|
+
const skillsRootValue = resolveRootConfig(rawConfig).root;
|
|
223
240
|
let lastMaintainAt = 0;
|
|
224
241
|
let lastMaintainRunId = "";
|
|
225
242
|
let maintainInFlightSince = 0;
|
|
226
243
|
ctx.inject(["commands"], (commandCtx) => {
|
|
227
244
|
const commands = commandCtx.commands;
|
|
228
|
-
|
|
245
|
+
const evolutionCommand = {
|
|
229
246
|
name: "evolution",
|
|
230
247
|
description: "Self-evolution status and approval controls",
|
|
231
248
|
recordInput: false,
|
|
232
249
|
input: { hint: renderHint() },
|
|
233
|
-
async
|
|
234
|
-
const rawInputTrimmed = invocation.rawInput
|
|
250
|
+
run: async (invocation) => {
|
|
251
|
+
const rawInputTrimmed = invocation.rawInput.trim();
|
|
235
252
|
const input = rawInputTrimmed.replace(/\s+/g, " ");
|
|
236
253
|
const ok = (text) => ({
|
|
237
254
|
kind: "success",
|
|
@@ -244,7 +261,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
244
261
|
const approval = ctx.get("evolutionApproval");
|
|
245
262
|
const pendingMatch = /^pending(?: --detail)?$/.exec(input);
|
|
246
263
|
if (pendingMatch) {
|
|
247
|
-
const
|
|
264
|
+
const listed = approval ? [...await approval.list("pending"), ...await approval.list("executing")] : [];
|
|
265
|
+
const pending = [...new Map(listed.map((row) => [row.id, row])).values()];
|
|
248
266
|
if (pending.length === 0) return ok("No pending evolution writes.");
|
|
249
267
|
const detailed = pendingMatch[0] === "pending --detail";
|
|
250
268
|
return ok(pending.map((p) => {
|
|
@@ -385,7 +403,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
385
403
|
}
|
|
386
404
|
if (input === "learn" || input.startsWith("learn ")) {
|
|
387
405
|
const request = rawInputTrimmed === "learn" ? "" : rawInputTrimmed.slice(6).trim();
|
|
388
|
-
|
|
406
|
+
const message = createUserMessage({
|
|
389
407
|
content: [{
|
|
390
408
|
type: "text",
|
|
391
409
|
text: buildLearnPrompt(request)
|
|
@@ -396,7 +414,11 @@ function apply(ctx, rawConfig = {}) {
|
|
|
396
414
|
form: "notice",
|
|
397
415
|
summary: "learn request"
|
|
398
416
|
}
|
|
399
|
-
})
|
|
417
|
+
});
|
|
418
|
+
const followup = invocation.agent.followup;
|
|
419
|
+
const woke = typeof followup === "function";
|
|
420
|
+
if (woke) followup(message);
|
|
421
|
+
else invocation.agent.inject(message);
|
|
400
422
|
const eventIo = ctx.get("evolutionIo")?.provider();
|
|
401
423
|
if (eventIo) appendEvolutionEvent(eventIo, eventsFile(evolutionRoot()), {
|
|
402
424
|
type: "learn",
|
|
@@ -405,7 +427,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
405
427
|
}).catch((error) => {
|
|
406
428
|
ctx.logger.warn(`evolution-commands: failed to record learn event: ${String(error)}`);
|
|
407
429
|
});
|
|
408
|
-
return ok("Learning request sent to this session. Follow it now.");
|
|
430
|
+
return woke ? ok("Learning request sent to this session. Follow it now.") : ok("Learning request queued for this session — this host exposes no wake-up channel, so it is read on your next message.");
|
|
409
431
|
}
|
|
410
432
|
if (input === "maintain --facts") {
|
|
411
433
|
const ioRegistry = ctx.get("evolutionIo");
|
|
@@ -424,7 +446,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
424
446
|
const maintainArgs = /^maintain(?:\s+--timeout[ =](\d+))?\s*$/.exec(input);
|
|
425
447
|
if (maintainArgs) {
|
|
426
448
|
const runTimeoutMs = maintainArgs[1] ? Number(maintainArgs[1]) : config.maintainTimeoutMs ?? 6e5;
|
|
427
|
-
if (!Number.isSafeInteger(runTimeoutMs) || runTimeoutMs <= 0 || runTimeoutMs >
|
|
449
|
+
if (!Number.isSafeInteger(runTimeoutMs) || runTimeoutMs <= 0 || runTimeoutMs > MAX_TIMER_DELAY_MS) return err(maintainArgs[1] ? `Invalid --timeout value: expected a positive integer number of milliseconds up to ${MAX_TIMER_DELAY_MS} (e.g. /evolution maintain --timeout 600000).` : `The maintainTimeoutMs config is invalid: expected a positive integer number of milliseconds up to ${MAX_TIMER_DELAY_MS}. Fix the evolution-commands row config (maintainTimeoutMs), then retry.`);
|
|
428
450
|
if (maintainInFlightSince > 0) return err(`Maintenance scan is already running (since ~${Math.max(1, Math.round((Date.now() - maintainInFlightSince) / 1e3))}s ago) — re-submitting now would cancel it. Wait for it to settle; the result appears when it finishes.`);
|
|
429
451
|
const cooldownMs = clampedNumber(config.maintainCooldownMs, 3e4, { min: 0 });
|
|
430
452
|
const sinceLast = Date.now() - lastMaintainAt;
|
|
@@ -445,7 +467,9 @@ function apply(ctx, rawConfig = {}) {
|
|
|
445
467
|
tools: { get: (name) => ctx.get("tools")?.get(name) },
|
|
446
468
|
logger: { warn: (message) => {
|
|
447
469
|
ctx.logger.warn(message);
|
|
448
|
-
} }
|
|
470
|
+
} },
|
|
471
|
+
rootExists: skillsRootValue === "" ? void 0 : () => ioRegistry.provider().exists(skillsRootValue),
|
|
472
|
+
skillRoot: skillsRootValue
|
|
449
473
|
}, {
|
|
450
474
|
timeoutMs: runTimeoutMs,
|
|
451
475
|
signal: invocation.signal,
|
|
@@ -477,6 +501,19 @@ function apply(ctx, rawConfig = {}) {
|
|
|
477
501
|
}
|
|
478
502
|
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);
|
|
479
503
|
if (input === "preset install") {
|
|
504
|
+
{
|
|
505
|
+
let installedBundles;
|
|
506
|
+
try {
|
|
507
|
+
installedBundles = collectEvolutionBundles(evolutionRoot(), (error) => {
|
|
508
|
+
throw error;
|
|
509
|
+
});
|
|
510
|
+
} catch (enumerationError) {
|
|
511
|
+
return err(`Refusing to install the layered Evolution preset: the home profile directory or one of its profile manifests could not be read (${enumerationError instanceof Error ? enumerationError.message : String(enumerationError)}) — the all/preset double-mount check could not run. Resolve access to the profiles directory and retry, or install via the layered installer, which enforces the same exclusion up front.`);
|
|
512
|
+
}
|
|
513
|
+
const allBundle = installedBundles.find((name) => name === "dsh-evolution-all" || name.endsWith("/dsh-evolution-all"));
|
|
514
|
+
const presetBundle = installedBundles.find((name) => name === "dsh-evolution-preset" || name.endsWith("/dsh-evolution-preset"));
|
|
515
|
+
if (allBundle || presetBundle) return err(`Refusing to install the layered Evolution preset: "${allBundle ?? presetBundle}" is installed in this home — its model rows (tool-memory / tool-skill-manage / tool-session-query / skill-catalog) would double-mount with the preset's and the profile fails loud at startup. Keep ONE: remove the ${allBundle ? "evolution-all bundle" : "evolution-preset bundle"} before installing the layered preset (see /evolution doctor).`);
|
|
516
|
+
}
|
|
480
517
|
const target = join(evolutionRoot(), ".agent-presets", "evolution");
|
|
481
518
|
try {
|
|
482
519
|
const source = resolveAgentPresetDir(import.meta.url);
|
|
@@ -533,8 +570,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
533
570
|
libraryOrigin: "foreground"
|
|
534
571
|
},
|
|
535
572
|
origin: "foreground",
|
|
536
|
-
...session
|
|
537
|
-
|
|
573
|
+
...session.id ? { sessionId: session.id } : {},
|
|
574
|
+
session,
|
|
538
575
|
...sessionPolicy !== void 0 ? { sessionPolicy } : {}
|
|
539
576
|
});
|
|
540
577
|
if (decision.action === "staged") return ok(`${decision.message}${planRunId ? `\n[audit] plan=${planRunId}` : ""}`);
|
|
@@ -560,8 +597,13 @@ function apply(ctx, rawConfig = {}) {
|
|
|
560
597
|
return ok(renderDoctorText(report));
|
|
561
598
|
}
|
|
562
599
|
return ok(`Evolution: memory, skills, review, curator — self-evolution status, approval and maintenance.\n${renderHelpText()}`);
|
|
563
|
-
}
|
|
564
|
-
|
|
600
|
+
},
|
|
601
|
+
handler: (invocation) => evolutionCommand.run(invocation).catch((error) => ({
|
|
602
|
+
kind: "error",
|
|
603
|
+
text: `evolution: command failed: ${error instanceof Error ? error.message : String(error)}\nRun /evolution doctor to inspect services and state files; a corrupted state file is quarantined as <file>.corrupt for inspection.`
|
|
604
|
+
}))
|
|
605
|
+
};
|
|
606
|
+
commandCtx.effect(() => commands.register(evolutionCommand));
|
|
565
607
|
});
|
|
566
608
|
}
|
|
567
609
|
/**
|
package/lib/types/doctor.d.ts
CHANGED
|
@@ -21,8 +21,14 @@ export interface DoctorReport {
|
|
|
21
21
|
executingCount: number | null;
|
|
22
22
|
actions: string[];
|
|
23
23
|
}
|
|
24
|
-
/** Profile bundle rows for evolution-family packages across all profiles.
|
|
25
|
-
|
|
24
|
+
/** Profile bundle rows for evolution-family packages across all profiles.
|
|
25
|
+
*
|
|
26
|
+
* `onReadError` reports a profile whose manifest could not be READ or PARSED.
|
|
27
|
+
* Like the directory-enumeration failure, it is fail-closed at every caller:
|
|
28
|
+
* a truncated manifest is indistinguishable from "no bundles" by the returned
|
|
29
|
+
* list alone, and treating it as "no bundles" is exactly the fail-open state
|
|
30
|
+
* the double-mount exclusion exists to prevent. */
|
|
31
|
+
export declare function collectEvolutionBundles(home: string, onReadError?: (error: unknown, scope: 'profiles-dir' | 'profile-manifest') => void): string[];
|
|
26
32
|
/**
|
|
27
33
|
* Diagnose the deployment. `ctx` supplies service presence (a bare stub with
|
|
28
34
|
* only `get` is enough); `home` defaults to the evolution root so tests can
|
package/lib/types/index.d.ts
CHANGED
|
@@ -11,8 +11,9 @@ export interface Config {
|
|
|
11
11
|
* resolved through resolveSkillsRoot() so an empty/whitespace value falls
|
|
12
12
|
* back to the default instead of a CWD-relative root. */
|
|
13
13
|
root?: string | undefined;
|
|
14
|
-
/**
|
|
15
|
-
*
|
|
14
|
+
/** V27 G2.4 (M-08): RETIRED alias of `root`. The field stays declared so the
|
|
15
|
+
* loader can hand it to the load-time gate (which rejects it loudly) instead
|
|
16
|
+
* of dropping it silently; the plugin never reads it as a root. */
|
|
16
17
|
skillsRoot?: string | undefined;
|
|
17
18
|
/** Cooldown window for scan commands (ms) — misclick/rapid-trigger guard;
|
|
18
19
|
* secondary calls inside the window return the previous runId instead of
|
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.
|
|
4
|
+
"version": "0.3.69",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -31,19 +31,21 @@
|
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
34
|
-
"@lmzhen/dsh-evolution-approval": "^0.3.
|
|
35
|
-
"@lmzhen/dsh-evolution-core": "^0.3.
|
|
36
|
-
"@lmzhen/dsh-evolution-maintenance": "^0.3.
|
|
34
|
+
"@lmzhen/dsh-evolution-approval": "^0.3.69",
|
|
35
|
+
"@lmzhen/dsh-evolution-core": "^0.3.69",
|
|
36
|
+
"@lmzhen/dsh-evolution-maintenance": "^0.3.69"
|
|
37
37
|
},
|
|
38
38
|
"optionalDependencies": {
|
|
39
|
-
"@lmzhen/dsh-evolution-agent-preset": "^0.3.
|
|
39
|
+
"@lmzhen/dsh-evolution-agent-preset": "^0.3.69"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
43
|
+
"@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
|
|
43
44
|
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
44
45
|
"@deepseek-ai/dsh-llm": "^0.1.1-rc.2"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
48
|
+
"@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
|
|
47
49
|
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2"
|
|
48
50
|
}
|
|
49
51
|
}
|