@lmzhen/dsh-evolution-commands 0.3.81 → 0.3.83
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/README.md +14 -2
- package/lib/index.js +159 -12
- package/lib/types/doctor.d.ts +36 -0
- package/lib/types/registry.d.ts +3 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @lmzhen/dsh-evolution-commands
|
|
2
2
|
|
|
3
3
|
Human commands for the evolution family
|
|
4
4
|
|
|
5
|
+
## Command reference
|
|
6
|
+
|
|
7
|
+
The full `/evolution` subcommand table is single-sourced in the family
|
|
8
|
+
README's **Command reference** section (`packages/README.md` in the source
|
|
9
|
+
repository; it is named here instead of linked because a repo-relative
|
|
10
|
+
markdown link out of this package would be dead inside the npm published
|
|
11
|
+
tree). It is rendered from the subcommand registry (`src/registry.ts`, the same
|
|
12
|
+
single source as the `/evolution` input-declaration hint and the
|
|
13
|
+
bare-command help output), and `tests/registry.spec.ts` (T-WD2) pins that
|
|
14
|
+
rendered table byte-for-byte. This package README cites that home instead
|
|
15
|
+
of restating the table (doc-facts rule N19: never copy a fact across docs).
|
|
16
|
+
|
|
5
17
|
## Model Experience
|
|
6
18
|
|
|
7
19
|
### Indirect model surface
|
|
8
20
|
|
|
9
21
|
#### What the model sees
|
|
10
22
|
|
|
11
|
-
`@
|
|
23
|
+
`@lmzhen/dsh-evolution-commands` registers no direct prompt or tool schema itself. Model-visible effects are owned by the packages that consume this service.
|
|
12
24
|
|
|
13
25
|
#### Token effect
|
|
14
26
|
|
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 { MAX_TIMER_DELAY_MS, appendEvolutionEvent, assertSkillsRootAliasRetired, buildLearnPrompt, clampedNumber, composePresetComposition, eventsFile, evolutionRoot, newSkillLibrary, resolveRootConfig } from "@lmzhen/dsh-evolution-core";
|
|
5
|
+
import { MAX_TIMER_DELAY_MS, appendEvolutionEvent, assertSkillsRootAliasRetired, buildLearnPrompt, clampedNumber, composePresetComposition, eventsFile, evolutionRoot, isMissingPath, newSkillLibrary, resolveRootConfig, scopedProbeReport } 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";
|
|
@@ -321,10 +321,11 @@ async function diagnose(ctx, options = {}) {
|
|
|
321
321
|
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.");
|
|
322
322
|
if (full && preset) conflicts.push("evolution-all and evolution-preset are installed together — the infra rows double-mount. Keep ONE.");
|
|
323
323
|
if (host && preset) conflicts.push("evolution-host and evolution-preset are installed together — the infra rows double-mount. Keep ONE.");
|
|
324
|
-
if (full && presetDirInstalled) conflicts.push(`evolution-all and the layered Evolution preset (${layeredArtifactLabel}) are both present — the model rows double-
|
|
325
|
-
if (preset && presetDirInstalled) conflicts.push(`evolution-preset and the layered Evolution preset (${layeredArtifactLabel}) are both present — the model rows double-
|
|
324
|
+
if (full && presetDirInstalled) conflicts.push(`evolution-all and the layered Evolution preset (${layeredArtifactLabel}) are both present — the four model rows AND the systemPrompt sections double-instance across the profile and preset layers; the preset loader does not fail on it, shadowing semantics take the nearest layer, so an Evolution-preset session silently runs the preset's copies of everything. Keep ONE (use layered without all, or drop the preset).`);
|
|
325
|
+
if (preset && presetDirInstalled) conflicts.push(`evolution-preset and the layered Evolution preset (${layeredArtifactLabel}) are both present — the preset bundle carries the same model rows as all, so the four model rows AND the systemPrompt sections double-instance and shadowing semantics take the nearest layer (the preset loader does not fail on it). Keep ONE (drop the preset bundle, or remove the layered preset).`);
|
|
326
|
+
const reviewMounted = full || host || preset;
|
|
326
327
|
const services = {
|
|
327
|
-
review:
|
|
328
|
+
review: reviewMounted,
|
|
328
329
|
curator: has("evolutionCurator"),
|
|
329
330
|
approval: has("evolutionApproval"),
|
|
330
331
|
skillUsage: has("skillUsage"),
|
|
@@ -333,10 +334,13 @@ async function diagnose(ctx, options = {}) {
|
|
|
333
334
|
let pendingCount = null;
|
|
334
335
|
let executingCount = null;
|
|
335
336
|
const approvalService = ctx.get("evolutionApproval");
|
|
336
|
-
|
|
337
|
-
|
|
337
|
+
const approvalList = approvalService?.list?.bind(approvalService);
|
|
338
|
+
if (approvalList !== void 0) try {
|
|
339
|
+
const pendingProbe = await probeBounded(() => approvalList("pending"), "approval pending listing");
|
|
340
|
+
const rows = pendingProbe.ok ? pendingProbe.value : void 0;
|
|
338
341
|
pendingCount = Array.isArray(rows) ? rows.length : null;
|
|
339
|
-
const
|
|
342
|
+
const executingProbe = await probeBounded(() => approvalList("executing"), "approval executing listing");
|
|
343
|
+
const executing = executingProbe.ok ? executingProbe.value : void 0;
|
|
340
344
|
executingCount = Array.isArray(executing) ? executing.length : null;
|
|
341
345
|
} catch {
|
|
342
346
|
pendingCount = null;
|
|
@@ -346,6 +350,10 @@ async function diagnose(ctx, options = {}) {
|
|
|
346
350
|
const deploymentForm = installForm === "layered" ? "variant" : installForm === "full" || installForm === "preset" ? "attach" : installForm === "host" ? "host-only" : installForm === "preset-only" ? "preset-only" : "none";
|
|
347
351
|
const mountConflicts = conflicts.filter((row) => !row.includes("DEGRADED"));
|
|
348
352
|
const undecidable = mountConflicts.length !== conflicts.length;
|
|
353
|
+
const scopedProbe = {
|
|
354
|
+
rows: undecidable ? null : reviewMounted ? ["evolution-review", "skill-usage"] : [],
|
|
355
|
+
...scopedProbeReport()
|
|
356
|
+
};
|
|
349
357
|
const actions = [];
|
|
350
358
|
if (mountConflicts.length > 0) actions.push("Resolve the conflict first: keep exactly one of evolution-all / evolution-host / evolution-preset / layered.");
|
|
351
359
|
else if (undecidable) actions.push("The install form is UNDECIDABLE (see the DEGRADED row above): an unreadable profiles directory or profile manifest hides whatever is installed, so this report must not add or remove a bundle. Fix the reported read failure, then re-run /evolution doctor.");
|
|
@@ -357,7 +365,12 @@ async function diagnose(ctx, options = {}) {
|
|
|
357
365
|
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.");
|
|
358
366
|
if (pendingCount === null && services.approval) actions.push("Approval service is mounted but pending listing failed — check the evolution state service.");
|
|
359
367
|
const memoryIssues = memoryInterpolationIssues(home);
|
|
368
|
+
const budgetIssues = memoryBudgetIssues(ctx);
|
|
369
|
+
const queryIssues = await sessionQueryIssues(ctx);
|
|
370
|
+
if (queryIssues.length > 0) actions.push("Session search is degraded: isolate the session named above (or wait for the platform fix described in the family maintenance notes) before retrying the same query");
|
|
371
|
+
if (budgetIssues.length > 0) actions.push("Align the memory budget: leave memory-files memoryCharLimit/userCharLimit UNSET so the store follows evolution-policy, or set both surfaces to the same value (the review plans against the policy value while the store enforces its own)");
|
|
360
372
|
if (memoryIssues.length > 0) actions.push("Rewrite the memory entries listed above (or run a build with the render-time neutralization) — they broke prompt assembly on older builds.");
|
|
373
|
+
if (scopedProbe.rows !== null && scopedProbe.rows.length > 0 && scopedProbe.verdict === "never-hit") actions.push("The session-scoped rows are mounted but the family-tool probe has never matched in this process — review injection and skill-usage telemetry skipped every session observed. HOST-ONLY install: the model rows (tool-memory / tool-skill-manage) sit in evolution-host devDependencies, so no session carries them; install a bundle that mounts them (see INSTALL.md). VARIANT install: only a session on the Evolution preset matches, so open one — a session on a platform original preset is the intended skip, not a fault. Either way, check that no profile overlay disables those two rows.");
|
|
361
374
|
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. For a verified orphan (this build: S2-P2-22), /evolution release <id> returns it to the pending window instead.`);
|
|
362
375
|
for (const row of presetFreshness) {
|
|
363
376
|
if (row.status !== "differs") continue;
|
|
@@ -370,18 +383,126 @@ async function diagnose(ctx, options = {}) {
|
|
|
370
383
|
conflicts,
|
|
371
384
|
envIssues: env,
|
|
372
385
|
memoryIssues,
|
|
386
|
+
budgetIssues,
|
|
387
|
+
queryIssues,
|
|
373
388
|
services,
|
|
374
389
|
pendingCount,
|
|
375
390
|
executingCount,
|
|
376
391
|
presetFreshness,
|
|
392
|
+
scopedProbe,
|
|
377
393
|
actions
|
|
378
394
|
};
|
|
379
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* S2-12③ (FLOW5-4): the memory budget is configured in TWO places —
|
|
398
|
+
* `memory-files`' `memoryCharLimit`/`userCharLimit` (what the STORE enforces)
|
|
399
|
+
* and `evolution-policy`'s `memoryChars`/`userChars` (what the review PLANS
|
|
400
|
+
* against; `memory-files` follows the policy only for an UNSET limit). A
|
|
401
|
+
* disagreement therefore means an explicit contradiction, or a `memory-files`
|
|
402
|
+
* row mounted before the policy existed — either way the reviewer budgets ops
|
|
403
|
+
* the store will refuse. Read-only, like every other doctor row.
|
|
404
|
+
*
|
|
405
|
+
* @param ctx - the runtime service view (`get(name)`).
|
|
406
|
+
* @returns one message per disagreeing surface; empty when not comparable.
|
|
407
|
+
*/
|
|
408
|
+
/** S4 review (source-first pass) R-1: how long a diagnostic probe may wait.
|
|
409
|
+
* Doctor must ANSWER even when the thing it probes is blocked — the failure it
|
|
410
|
+
* reports (a concurrent writer stalling the corpus) is exactly the one that
|
|
411
|
+
* would otherwise hang the command forever. */
|
|
412
|
+
const PROBE_TIMEOUT_MS = 5e3;
|
|
413
|
+
/**
|
|
414
|
+
* Bound a diagnostic probe. A never-settling service is a FINDING, not a hang:
|
|
415
|
+
* the probe reports it like any other failure.
|
|
416
|
+
*
|
|
417
|
+
* Takes a THUNK, not a promise (review R-2): a mounted-but-broken service can
|
|
418
|
+
* throw synchronously, and `probeBounded(Promise.resolve(service.list()))` would
|
|
419
|
+
* let that throw escape the probe — crashing the command instead of reporting.
|
|
420
|
+
*
|
|
421
|
+
* @param work - the probe's work, invoked inside the guard.
|
|
422
|
+
* @param label - the surface being probed (named in the finding).
|
|
423
|
+
* @returns the value, or a message describing the failure/timeout.
|
|
424
|
+
*/
|
|
425
|
+
async function probeBounded(work, label) {
|
|
426
|
+
let started;
|
|
427
|
+
try {
|
|
428
|
+
started = work();
|
|
429
|
+
} catch (error) {
|
|
430
|
+
return {
|
|
431
|
+
ok: false,
|
|
432
|
+
message: `${label} failed: ${error instanceof Error ? error.message : String(error)}`
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
let timer;
|
|
436
|
+
const timeout = new Promise((resolve) => {
|
|
437
|
+
timer = setTimeout(() => {
|
|
438
|
+
resolve({
|
|
439
|
+
ok: false,
|
|
440
|
+
message: `${label} did not answer within ${PROBE_TIMEOUT_MS}ms — the corpus is blocked (a concurrent writer can stall the observation) or the service is wedged; isolate the most recently written session and re-run`
|
|
441
|
+
});
|
|
442
|
+
}, PROBE_TIMEOUT_MS);
|
|
443
|
+
});
|
|
444
|
+
const settled = started.then((value) => ({
|
|
445
|
+
ok: true,
|
|
446
|
+
value
|
|
447
|
+
}), (error) => ({
|
|
448
|
+
ok: false,
|
|
449
|
+
message: `${label} failed: ${error instanceof Error ? error.message : String(error)}`
|
|
450
|
+
}));
|
|
451
|
+
const outcome = await Promise.race([settled, timeout]);
|
|
452
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
453
|
+
return outcome;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* S4/P-1+P-2 (platform report P-1/P-2, family mitigation) — the session-query
|
|
457
|
+
* index can fail AS A WHOLE: a concurrent writer leaves the persistence
|
|
458
|
+
* observation unstable ('did not stabilize after one retry') and ONE
|
|
459
|
+
* header-conflicting session aborts the same try block, which the tool surface
|
|
460
|
+
* folds into 'storage is unavailable'. Doctor cannot read the index's row count
|
|
461
|
+
* or the last stabilization result (the platform exposes no such API — recorded
|
|
462
|
+
* in the platform report), but it CAN exercise the corpus listing and report the
|
|
463
|
+
* verbatim failure with the actionable half: isolate the offending session.
|
|
464
|
+
*
|
|
465
|
+
* @param ctx - the runtime service view (`get(name)`).
|
|
466
|
+
* @returns one or two messages; empty when the service is absent or healthy.
|
|
467
|
+
*/
|
|
468
|
+
async function sessionQueryIssues(ctx) {
|
|
469
|
+
const service = ctx.get("sessionQuery");
|
|
470
|
+
const list = service?.listSessions?.bind(service);
|
|
471
|
+
if (list === void 0) return [];
|
|
472
|
+
const outcome = await probeBounded(() => list(), "session-query listing");
|
|
473
|
+
if (!outcome.ok) return [outcome.message, "isolate the offending session — move it OUT of the corpus with a manifest (never delete it) and re-run; the platform already retries the observation once, so retrying first is not the answer"];
|
|
474
|
+
return Array.isArray(outcome.value) ? [] : ["session-query answered a listing with a non-list value — the service is mounted but not honouring its read contract"];
|
|
475
|
+
}
|
|
476
|
+
function memoryBudgetIssues(ctx) {
|
|
477
|
+
const budget = ctx.get("evolutionMemoryBudget");
|
|
478
|
+
const policy = ctx.get("evolutionPolicy")?.get?.();
|
|
479
|
+
if (budget === void 0 || policy === void 0) return [];
|
|
480
|
+
const issues = [];
|
|
481
|
+
if (budget.memoryCharLimit !== void 0 && policy.memoryChars !== void 0 && budget.memoryCharLimit !== policy.memoryChars) issues.push(`store enforces memoryCharLimit=${budget.memoryCharLimit} (source: ${budget.memorySource ?? "unknown"}) while evolution-policy declares memoryChars=${policy.memoryChars} — the review plans against the policy value and the store refuses what exceeds its own`);
|
|
482
|
+
if (budget.userCharLimit !== void 0 && policy.userChars !== void 0 && budget.userCharLimit !== policy.userChars) issues.push(`store enforces userCharLimit=${budget.userCharLimit} (source: ${budget.userSource ?? "unknown"}) while evolution-policy declares userChars=${policy.userChars} — same divergence as the memory limit`);
|
|
483
|
+
return issues;
|
|
484
|
+
}
|
|
485
|
+
/** S0-4 (v43 G-1 / J-1): the scoped-row reconciliation line. The verdict is the
|
|
486
|
+
* runtime witness, the row set is what this home installs, and the two forms are
|
|
487
|
+
* spelled out because they read identically in the numbers (`never-hit`) while
|
|
488
|
+
* needing opposite responses: under the host-only form nothing can ever match,
|
|
489
|
+
* under the variant form the match arrives with the first Evolution-preset
|
|
490
|
+
* session.
|
|
491
|
+
* @param check - the report's scoped-row check.
|
|
492
|
+
* @returns one line, or the earlier diagnostic when either half is unknown. */
|
|
493
|
+
function scopedProbeLine(check) {
|
|
494
|
+
if (check.rows === null) return `scoped rows: (undecidable), probe=${check.verdict} — a DEGRADED bundle read hides which bundles are installed, so the scoped rows cannot be reconciled with the probe`;
|
|
495
|
+
if (check.rows.length === 0) return `scoped rows: (none mounted), probe=${check.verdict} — no installed bundle carries the session-scoped rows (evolution-review / skill-usage), so the gate is inactive here`;
|
|
496
|
+
const rows = check.rows.map((row) => `${row}=on`).join("/");
|
|
497
|
+
const detail = check.verdict === "hit" ? `${check.hits} scoped evaluation(s) carried the family tools` : check.verdict === "never-hit" ? `${check.misses} scoped evaluation(s) resolved false — review injection and skill-usage telemetry are inert for every session observed; HOST-ONLY installs reach this because tool-memory / tool-skill-manage are not dependencies of evolution-host, VARIANT installs only match once a session runs the Evolution preset` : "the gate has not evaluated a session in this process yet — no session event has reached it since startup";
|
|
498
|
+
return `scoped rows: ${rows}, probe=${check.verdict} (${detail})`;
|
|
499
|
+
}
|
|
380
500
|
function renderDoctorText(report) {
|
|
381
501
|
const lines = [
|
|
382
502
|
`Evolution doctor — install form: ${report.installForm} (deployment: ${report.deploymentForm})`,
|
|
383
503
|
`bundles (all profiles): ${report.bundles.length > 0 ? report.bundles.join(", ") : "(none)"}`,
|
|
384
504
|
`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}`,
|
|
505
|
+
scopedProbeLine(report.scopedProbe),
|
|
385
506
|
`pending: ${report.pendingCount === null ? "unknown" : report.pendingCount}`,
|
|
386
507
|
`executing: ${report.executingCount === null ? "unknown" : report.executingCount}`
|
|
387
508
|
];
|
|
@@ -393,6 +514,8 @@ function renderDoctorText(report) {
|
|
|
393
514
|
if (report.conflicts.length > 0) lines.push("conflicts:", ...report.conflicts.map((line) => ` ! ${line}`));
|
|
394
515
|
if (report.envIssues.length > 0) lines.push("env:", ...report.envIssues.map((line) => ` ! ${line}`));
|
|
395
516
|
if (report.memoryIssues.length > 0) lines.push("memory:", ...report.memoryIssues.map((line) => ` ! ${line}`));
|
|
517
|
+
if (report.budgetIssues.length > 0) lines.push("memory budget:", ...report.budgetIssues.map((line) => ` ! ${line}`));
|
|
518
|
+
if (report.queryIssues.length > 0) lines.push("session search:", ...report.queryIssues.map((line) => ` ! ${line}`));
|
|
396
519
|
if (report.actions.length > 0) lines.push("next steps:", ...report.actions.map((line) => ` → ${line}`));
|
|
397
520
|
return lines.join("\n");
|
|
398
521
|
}
|
|
@@ -518,6 +641,13 @@ function apply(ctx, rawConfig = {}) {
|
|
|
518
641
|
});
|
|
519
642
|
const invocationAgent = invocation.agent;
|
|
520
643
|
const agentMissing = (need) => invocationAgent === void 0 ? err(`E-305: this invocation carries no agent — \`${need}\` needs a session-backed call (run it from a session in the GUI or the CLI).`) : void 0;
|
|
644
|
+
const unreplayableWriteRefusal = (what) => {
|
|
645
|
+
if (approval === void 0) return void 0;
|
|
646
|
+
const session = invocationAgent?.session;
|
|
647
|
+
const sessionPolicy = effectiveSessionPolicy(ctx, session);
|
|
648
|
+
if (!(approval.isEnabled !== false && sessionPolicy !== "never" && approval.stageForeground !== false)) return void 0;
|
|
649
|
+
return err(`E-306: this deployment stages foreground writes, but \`/evolution ${what}\` is not replayable through the skill runner — there is nothing to stage. Run it from a session whose approval policy is 'never', or set \`stageForeground: false\` on the evolution-approval row, then repeat the command.`);
|
|
650
|
+
};
|
|
521
651
|
const approval = ctx.get("evolutionApproval");
|
|
522
652
|
const pendingMatch = /^pending(?: --detail)?$/.exec(input);
|
|
523
653
|
if (pendingMatch) {
|
|
@@ -630,6 +760,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
630
760
|
if (input === "restore" || input.startsWith("restore ")) {
|
|
631
761
|
const tail = input.slice(7).trim();
|
|
632
762
|
if (tail !== "") return err(`\`restore\` takes no arguments (got "${tail}"). Use \`restore\` for a whole-tree rollback, or \`skill restore <name>\` for one skill.`);
|
|
763
|
+
const restoreRefusal = unreplayableWriteRefusal("restore");
|
|
764
|
+
if (restoreRefusal) return restoreRefusal;
|
|
633
765
|
const curator = ctx.get("evolutionCurator");
|
|
634
766
|
const result = curator ? await curator.restoreSnapshot() : {
|
|
635
767
|
ok: false,
|
|
@@ -638,6 +770,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
638
770
|
return result.ok ? ok(result.message) : err(result.message);
|
|
639
771
|
}
|
|
640
772
|
if (input.startsWith("consolidate ")) {
|
|
773
|
+
const consolidateRefusal = unreplayableWriteRefusal("consolidate");
|
|
774
|
+
if (consolidateRefusal) return consolidateRefusal;
|
|
641
775
|
const planTail = /\s--plan\s+(\S+)\s*$/.exec(input) ?? null;
|
|
642
776
|
const planRunId = planTail?.[1] ?? void 0;
|
|
643
777
|
const [target, ...sources] = (planTail ? input.slice(0, planTail.index) : input).slice(12).trim().split(/\s+/).filter(Boolean);
|
|
@@ -653,6 +787,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
653
787
|
if (input.startsWith("skill restore ")) {
|
|
654
788
|
const name = input.slice(14).trim();
|
|
655
789
|
if (!name) return err("Usage: /evolution skill restore <name>");
|
|
790
|
+
const skillRestoreRefusal = unreplayableWriteRefusal("skill restore");
|
|
791
|
+
if (skillRestoreRefusal) return skillRestoreRefusal;
|
|
656
792
|
const curator = ctx.get("evolutionCurator");
|
|
657
793
|
const result = curator ? await curator.restore(name) : {
|
|
658
794
|
ok: false,
|
|
@@ -714,13 +850,22 @@ function apply(ctx, rawConfig = {}) {
|
|
|
714
850
|
io: ioRegistry.provider()
|
|
715
851
|
});
|
|
716
852
|
const enrichment = await buildEnrichment(ctx, library);
|
|
717
|
-
const
|
|
853
|
+
const readFailures = [];
|
|
854
|
+
const snapshots = await snapshotFromLibrary(library, {
|
|
718
855
|
descriptions: enrichment.descriptions,
|
|
719
856
|
supportFiles: enrichment.supportFiles,
|
|
720
857
|
quality: enrichment.quality,
|
|
721
858
|
protected: enrichment.protected,
|
|
722
|
-
catalogInvalid: enrichment.catalogInvalid
|
|
723
|
-
|
|
859
|
+
catalogInvalid: enrichment.catalogInvalid,
|
|
860
|
+
onReadError: (name, error) => {
|
|
861
|
+
readFailures.push(name);
|
|
862
|
+
ctx.logger.warn(`evolution-maintenance: skipping unreadable skill "${name}": ${error instanceof Error ? error.message : String(error)}`);
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
if (snapshots.length === 0 && readFailures.length > 0) return err(`maintenance scan could not read ${readFailures.length} listed skill(s) (${readFailures.slice(0, 10).join(", ")}${readFailures.length > 10 ? ", …" : ""}) and read no skill at all — the library was NOT audited; fix the unreadable skills (or their directory names) and run the scan again`);
|
|
866
|
+
if (snapshots.length === 0 && skillsRootValue !== "" && !await ioRegistry.provider().exists(skillsRootValue)) return err(`maintenance scan found an empty skill library, but the configured skill root does not exist (${skillsRootValue}) — point the skillsRoot config at a real directory (a literal \`~\` is not expanded) and run the scan again`);
|
|
867
|
+
if (snapshots.length === 0) return ok("Maintenance scan: empty skill library. Nothing to do.");
|
|
868
|
+
const { facts } = buildMaintainFacts(snapshots, enrichment.usageObservedValue, void 0);
|
|
724
869
|
return ok(`Maintenance facts (0-token preview):\n${facts}`);
|
|
725
870
|
}
|
|
726
871
|
const maintainArgs = /^maintain(?:\s+--timeout[ =](\d+))?\s*$/.exec(input);
|
|
@@ -858,7 +1003,9 @@ function apply(ctx, rawConfig = {}) {
|
|
|
858
1003
|
if (approval) {
|
|
859
1004
|
const session = invocationAgent?.session;
|
|
860
1005
|
const sessionPolicy = effectiveSessionPolicy(ctx, session);
|
|
861
|
-
|
|
1006
|
+
const stagesForeground = approval.isEnabled !== false && sessionPolicy !== "never" && approval.stageForeground !== false;
|
|
1007
|
+
if (stagesForeground && session === void 0) return err("E-306: this deployment stages foreground writes, but this invocation carries no agent session — `/evolution restructure` would stage a record with no session attribution. Run it from a session in the GUI or the CLI, or set `stageForeground: false` on the evolution-approval row, then repeat the command.");
|
|
1008
|
+
if (stagesForeground && !approval.hasRunner("skill")) return err("Restructure cannot be staged: no skill replay runner is registered — mount the tool-skill-manage row (evolution-agent preset, or evolution-all) or disable evolution-approval.");
|
|
862
1009
|
const decision = await approval.request({
|
|
863
1010
|
kind: "skill",
|
|
864
1011
|
summary: `/evolution restructure ${name}${planRunId ? ` (plan ${planRunId})` : ""}`,
|
|
@@ -976,7 +1123,7 @@ const defaultFs = {
|
|
|
976
1123
|
try {
|
|
977
1124
|
return statSync(path).mtimeMs;
|
|
978
1125
|
} catch (error) {
|
|
979
|
-
if (error
|
|
1126
|
+
if (isMissingPath(error)) return null;
|
|
980
1127
|
throw error;
|
|
981
1128
|
}
|
|
982
1129
|
}
|
package/lib/types/doctor.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ScopedProbeReport } from '@lmzhen/dsh-evolution-core';
|
|
1
2
|
/** G3-② (B2): one delivered preset variant compared against a fresh generation.
|
|
2
3
|
* "absent" is the user simply not using that base (never an action); "unknown"
|
|
3
4
|
* is a comparison that could not run, kept distinct from a verified "fresh". */
|
|
@@ -10,6 +11,29 @@ export interface PresetFreshnessRow {
|
|
|
10
11
|
/** Why the comparison could not run — set on unknown rows only. */
|
|
11
12
|
detail?: string;
|
|
12
13
|
}
|
|
14
|
+
/** S0-4 (v43 G-1 / J-1): the session-scoped rows against the runtime witness.
|
|
15
|
+
*
|
|
16
|
+
* The gate has two halves nobody reconciled: the bundle turns `sessionScoped` on
|
|
17
|
+
* for `evolution-review` and `skill-usage`, and the runtime witness knows
|
|
18
|
+
* whether ANY session ever saw the family's model tools. `never-hit` with the
|
|
19
|
+
* rows mounted is a finding — the host-only form mounts no model row at all
|
|
20
|
+
* (tool-memory / tool-skill-manage are devDependencies of evolution-host) and an
|
|
21
|
+
* overlay may disable either row, so review injection and skill-usage telemetry
|
|
22
|
+
* run for nobody. `idle` claims nothing: the gate simply has not been asked yet
|
|
23
|
+
* in this process. Read-only, like every other row of this report. */
|
|
24
|
+
export interface ScopedProbeCheck {
|
|
25
|
+
/** The scoped rows the installed bundles mount (`evolution-review`,
|
|
26
|
+
* `skill-usage`); empty when no bundle is installed, and null when a DEGRADED
|
|
27
|
+
* bundle read makes the set undecidable — an unreadable manifest is not
|
|
28
|
+
* evidence of absence (the INST-01 / S2.1 discipline). */
|
|
29
|
+
rows: string[] | null;
|
|
30
|
+
/** The runtime witness, verbatim (`scopedProbeReport()`). */
|
|
31
|
+
verdict: ScopedProbeReport['verdict'];
|
|
32
|
+
/** Scoped gate evaluations that resolved true. */
|
|
33
|
+
hits: number;
|
|
34
|
+
/** Scoped gate evaluations that resolved false. */
|
|
35
|
+
misses: number;
|
|
36
|
+
}
|
|
13
37
|
export interface DoctorReport {
|
|
14
38
|
/** OPT-23 (2026-09): `preset-only` — the delivered Evolution preset
|
|
15
39
|
* artifact exists but NO profile carries an evolution bundle (e.g. the
|
|
@@ -39,6 +63,14 @@ export interface DoctorReport {
|
|
|
39
63
|
* pre-step of every session under this home, and an operator may want to clean
|
|
40
64
|
* the file regardless. */
|
|
41
65
|
memoryIssues: string[];
|
|
66
|
+
/** S4/P-1+P-2 (platform gap, family mitigation): the session-query corpus
|
|
67
|
+
* listing failed — reported verbatim, with the isolation recipe. Empty when the
|
|
68
|
+
* service is absent or answering. */
|
|
69
|
+
queryIssues: string[];
|
|
70
|
+
/** S2-12③ (FLOW5-4): the memory budget's two configuration surfaces disagree —
|
|
71
|
+
* `memory-files`' store limit vs `evolution-policy`'s planning value. Empty
|
|
72
|
+
* when either surface is absent (nothing to compare) or they agree. */
|
|
73
|
+
budgetIssues: string[];
|
|
42
74
|
services: {
|
|
43
75
|
review: boolean;
|
|
44
76
|
curator: boolean;
|
|
@@ -56,6 +88,10 @@ export interface DoctorReport {
|
|
|
56
88
|
* upgrade leaves a file describing a platform that no longer exists. Read-only:
|
|
57
89
|
* a stale snapshot is reported, never repaired. */
|
|
58
90
|
presetFreshness: PresetFreshnessRow[];
|
|
91
|
+
/** S0-4 (v43 G-1 / J-1): the scoped rows × the probe witness, so a deployment
|
|
92
|
+
* whose cross-session consumers are inert for every session stops looking
|
|
93
|
+
* healthy. See {@link ScopedProbeCheck}. */
|
|
94
|
+
scopedProbe: ScopedProbeCheck;
|
|
59
95
|
actions: string[];
|
|
60
96
|
}
|
|
61
97
|
/** Profile bundle rows for evolution-family packages across all profiles.
|
package/lib/types/registry.d.ts
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* 0.3.55 (WD1): the input-declaration hint, the bare `/evolution` help output
|
|
5
5
|
* and the README command table all render FROM this table; a hand-written
|
|
6
6
|
* copy anywhere is drift waiting to happen (v10 R-13 proved it once).
|
|
7
|
+
* PLAN S5.3 (2026-09-16): "the README command table" means the FAMILY README
|
|
8
|
+
* (`packages/README.md` — the T-WD2 anchor). Package-level READMEs cite that
|
|
9
|
+
* home instead of restating the table (doc-facts rule N19).
|
|
7
10
|
* Keep `usage` in the exact shape a user can type after `/evolution`.
|
|
8
11
|
*/
|
|
9
12
|
export interface CommandEntry {
|
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.83",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
30
|
-
"@lmzhen/dsh-evolution-approval": "^0.3.
|
|
31
|
-
"@lmzhen/dsh-evolution-core": "^0.3.
|
|
32
|
-
"@lmzhen/dsh-evolution-maintenance": "^0.3.
|
|
30
|
+
"@lmzhen/dsh-evolution-approval": "^0.3.83",
|
|
31
|
+
"@lmzhen/dsh-evolution-core": "^0.3.83",
|
|
32
|
+
"@lmzhen/dsh-evolution-maintenance": "^0.3.83"
|
|
33
33
|
},
|
|
34
34
|
"optionalDependencies": {
|
|
35
|
-
"@lmzhen/dsh-evolution-agent-preset": "^0.3.
|
|
35
|
+
"@lmzhen/dsh-evolution-agent-preset": "^0.3.83"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@deepseek-ai/cordis": "^4.0.1",
|