@lmzhen/dsh-evolution-commands 0.20.0 → 0.22.0
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 +214 -96
- package/lib/types/doctor.d.ts +7 -0
- package/lib/types/index.d.ts +10 -5
- package/lib/types/profiles.d.ts +74 -0
- package/package.json +5 -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 { DEFAULT_SKILL_LIMITS, MAX_TIMER_DELAY_MS, PARAM_EXPOSURE, PARAM_GROUP_LABELS, absentHostServices, appendEvolutionEvent, applyNamespaceMigration, assertSkillsRootAliasRetired, buildLearnPrompt, canonicalWriteId, clampedNumber, classifyNamespaceMigration, composePresetEntry, elapsedSince, errorText, eventsFile, evolutionHome, evolutionRoot, isDeprecatedParamId, isMissingPath, isPresent, isUnknown, mergePresetRow, newRunRegistry, newSkillLibrary, paramSettingsId, partitionVersions, planNamespaceMigration, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, probeText, reportsSweepLockTarget, resolveBasePresetPatch, resolveParamId, resolveRootConfig, scopedProbeReport, sweepReports, transactIo } from "@lmzhen/dsh-evolution-core";
|
|
5
|
+
import { DEFAULT_SKILL_LIMITS, MAX_TIMER_DELAY_MS, PARAM_EXPOSURE, PARAM_GROUP_LABELS, absentHostServices, appendEvolutionEvent, applyNamespaceMigration, assertSkillsRootAliasRetired, buildLearnPrompt, canonicalWriteId, clampedNumber, classifyNamespaceMigration, composePresetEntry, elapsedSince, errorText, eventsFile, evolutionHome, evolutionRoot, isDeprecatedParamId, isMissingPath, isPresent, isUnknown, liveBool, mergePresetRow, newRunRegistry, newSkillLibrary, paramSettingsId, partitionVersions, planNamespaceMigration, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, probeText, reportsSweepLockTarget, resolveBasePresetPatch, resolveParamId, resolveRootConfig, scopedProbeReport, sweepReports, transactIo } from "@lmzhen/dsh-evolution-core";
|
|
6
6
|
import { buildEnrichment, buildMaintainFacts, enrichmentSnapshotOptions, runMaintain, snapshotFromLibrary } from "@lmzhen/dsh-evolution-maintenance";
|
|
7
7
|
import { copyFileSync, existsSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
@@ -235,7 +235,7 @@ async function readPlatformView(ctx) {
|
|
|
235
235
|
*/
|
|
236
236
|
/** The patch file name inside a profile directory (`packages/boot/app-boot/src/profile.ts`,
|
|
237
237
|
* `PROFILE_PATCH_FILENAME`). */
|
|
238
|
-
const PROFILE_PATCH_FILENAME = "cordis.patch.yml";
|
|
238
|
+
const PROFILE_PATCH_FILENAME$1 = "cordis.patch.yml";
|
|
239
239
|
/**
|
|
240
240
|
* The profile patch this process writes into, or undefined outside a profile.
|
|
241
241
|
*
|
|
@@ -251,7 +251,7 @@ function presetProfileTarget(ctx) {
|
|
|
251
251
|
return {
|
|
252
252
|
profile: profile.name,
|
|
253
253
|
profileDir: profile.dir,
|
|
254
|
-
patchPath: typeof profile.patchPath === "string" && profile.patchPath !== "" ? profile.patchPath : join(profile.dir, PROFILE_PATCH_FILENAME),
|
|
254
|
+
patchPath: typeof profile.patchPath === "string" && profile.patchPath !== "" ? profile.patchPath : join(profile.dir, PROFILE_PATCH_FILENAME$1),
|
|
255
255
|
bundles: Array.isArray(profile.startedBundles) ? profile.startedBundles : []
|
|
256
256
|
};
|
|
257
257
|
}
|
|
@@ -314,6 +314,135 @@ function moduleGraphPath(specifier) {
|
|
|
314
314
|
return fileURLToPath(resolve(specifier));
|
|
315
315
|
}
|
|
316
316
|
//#endregion
|
|
317
|
+
//#region lib/types/profiles.js
|
|
318
|
+
/**
|
|
319
|
+
* The ONE reader of `<home>/profiles` (G3, 0.22).
|
|
320
|
+
*
|
|
321
|
+
* Two very different questions used to share one bare helper: diagnostics want EVERY profile under
|
|
322
|
+
* the home (doctor's cross-profile summary), while the preset-install exclusion must only ever look
|
|
323
|
+
* at THIS plane — a leftover profile from the other plane's home used to make the exclusion refuse a
|
|
324
|
+
* legitimate install (the two-plane split made that reachable: `~/.dsh/profiles/web` sat next
|
|
325
|
+
* to `profiles/desktop`). The directory is therefore read here and nowhere else:
|
|
326
|
+
* `scanProfiles` is the single `readdirSync` of it, `homeProfiles` answers the diagnostic
|
|
327
|
+
* question, and `installExclusionTarget` answers the write-path one — fail-closed, because an
|
|
328
|
+
* unreadable home must never read as "nothing is installed".
|
|
329
|
+
*
|
|
330
|
+
* @module evolution-commands/src/profiles
|
|
331
|
+
*/
|
|
332
|
+
/** The patch file name inside a profile directory (the platform's own spelling). */
|
|
333
|
+
const PROFILE_PATCH_FILENAME = "cordis.patch.yml";
|
|
334
|
+
/** The ONLY directory read of `<home>/profiles` in the family.
|
|
335
|
+
* `profiles/node_modules` is a platform-created directory that never carries a manifest (S2.1), dot-entries
|
|
336
|
+
* are not profiles, and a directory without `package.json` is not one either.
|
|
337
|
+
* @param home - the resolved DSH home.
|
|
338
|
+
* @returns the scanned entries, or the enumeration error (never a silent empty list).
|
|
339
|
+
*/
|
|
340
|
+
function scanProfiles(home) {
|
|
341
|
+
const root = join(home, "profiles");
|
|
342
|
+
if (!existsSync(root)) return {
|
|
343
|
+
kind: "scanned",
|
|
344
|
+
profiles: []
|
|
345
|
+
};
|
|
346
|
+
let entries;
|
|
347
|
+
try {
|
|
348
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
349
|
+
} catch (error) {
|
|
350
|
+
return {
|
|
351
|
+
kind: "unreadable",
|
|
352
|
+
error
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
const profiles = [];
|
|
356
|
+
for (const entry of entries) {
|
|
357
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
358
|
+
if (entry.name === "node_modules") continue;
|
|
359
|
+
const dir = join(root, entry.name);
|
|
360
|
+
if (!existsSync(join(dir, "package.json"))) continue;
|
|
361
|
+
profiles.push({
|
|
362
|
+
profile: entry.name,
|
|
363
|
+
dir
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
kind: "scanned",
|
|
368
|
+
profiles
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/** Every profile directory under one home, by name — the DIAGNOSTIC question (doctor's cross-profile
|
|
372
|
+
* summary). An unreadable home answers the empty list here because doctor reports the failure on its
|
|
373
|
+
* own DEGRADED row; a write path must ask {@link installExclusionTarget} instead.
|
|
374
|
+
* @param home - the resolved DSH home.
|
|
375
|
+
* @returns one entry per profile directory carrying a manifest.
|
|
376
|
+
*/
|
|
377
|
+
function homeProfiles(home) {
|
|
378
|
+
const scan = scanProfiles(home);
|
|
379
|
+
return scan.kind === "scanned" ? scan.profiles : [];
|
|
380
|
+
}
|
|
381
|
+
/** The bundle names one profile's manifest declares.
|
|
382
|
+
* @param profileDir - the profile directory.
|
|
383
|
+
* @param onError - called with the read/parse failure; a torn manifest is NOT "no bundles".
|
|
384
|
+
* @returns the declared bundle names (empty when the manifest is absent).
|
|
385
|
+
*/
|
|
386
|
+
function profileBundles(profileDir, onError) {
|
|
387
|
+
try {
|
|
388
|
+
const bundles = JSON.parse(readFileSync(join(profileDir, "package.json"), "utf8")).dsh?.profile?.bundles;
|
|
389
|
+
return Array.isArray(bundles) ? bundles.filter((name) => typeof name === "string") : [];
|
|
390
|
+
} catch (error) {
|
|
391
|
+
onError?.(error);
|
|
392
|
+
return [];
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/** One optional file's text, or `''` when it is absent or unreadable.
|
|
396
|
+
* @param path - the file to read.
|
|
397
|
+
* @returns the text.
|
|
398
|
+
*/
|
|
399
|
+
function readTextOrEmpty(path) {
|
|
400
|
+
try {
|
|
401
|
+
return readFileSync(path, "utf8");
|
|
402
|
+
} catch {
|
|
403
|
+
return "";
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/** The profile's own patch text, or `''` when it has none.
|
|
407
|
+
* @param profileDir - the profile directory.
|
|
408
|
+
* @returns the patch text.
|
|
409
|
+
*/
|
|
410
|
+
function profilePatchText(profileDir) {
|
|
411
|
+
return readTextOrEmpty(join(profileDir, PROFILE_PATCH_FILENAME));
|
|
412
|
+
}
|
|
413
|
+
/** Where the preset-install exclusion must look: THIS plane only, or an explicit refusal.
|
|
414
|
+
*
|
|
415
|
+
* The write path does NOT scan the home: the mounted profile is the whole question (its started
|
|
416
|
+
* bundles plus the ones its own manifest declares — either can double-mount), and a question that
|
|
417
|
+
* cannot be answered still REFUSES. That is what makes a leftover profile from the other plane
|
|
418
|
+
* harmless here: it is not this plane's profile, so it is never consulted.
|
|
419
|
+
* @param mounted - the mounted profile, or undefined outside a profile (a bare library mount).
|
|
420
|
+
* @returns the profile to judge with its bundle rows, or the reason a refusal is mandatory.
|
|
421
|
+
*/
|
|
422
|
+
function installExclusionTarget(mounted) {
|
|
423
|
+
if (mounted === void 0 || mounted.dir === "") return {
|
|
424
|
+
kind: "undecidable",
|
|
425
|
+
reason: "no profile is mounted in this process, so the double-mount question cannot be answered here"
|
|
426
|
+
};
|
|
427
|
+
let manifestFailure;
|
|
428
|
+
const declared = profileBundles(mounted.dir, (error) => {
|
|
429
|
+
manifestFailure = error;
|
|
430
|
+
});
|
|
431
|
+
const manifestCode = manifestFailure?.code;
|
|
432
|
+
if (manifestFailure !== void 0 && manifestCode !== "ENOENT") return {
|
|
433
|
+
kind: "undecidable",
|
|
434
|
+
reason: "the mounted profile manifest could not be read"
|
|
435
|
+
};
|
|
436
|
+
return {
|
|
437
|
+
kind: "mounted",
|
|
438
|
+
profile: {
|
|
439
|
+
profile: mounted.profile,
|
|
440
|
+
dir: mounted.dir
|
|
441
|
+
},
|
|
442
|
+
bundles: [...mounted.bundles, ...declared]
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
//#endregion
|
|
317
446
|
//#region lib/types/doctor.js
|
|
318
447
|
/**
|
|
319
448
|
* `/evolution doctor` — read-only self-check (0.3.55, WB2).
|
|
@@ -380,8 +509,10 @@ function presetIssues(presets) {
|
|
|
380
509
|
* build-time guards cannot. Three classes, each with its own move:
|
|
381
510
|
* - a user override (the effective value is no longer the deployment's),
|
|
382
511
|
* - a deprecated alias still written in a user section (writes refuse it),
|
|
383
|
-
* - a declared user-writable parameter whose owner publishes no
|
|
384
|
-
*
|
|
512
|
+
* - a declared user-writable parameter whose owner publishes no settings namespace AT ALL
|
|
513
|
+
* (a registry fact: writes answer E-315), and
|
|
514
|
+
* - a declared user-writable parameter whose owning row this runtime does not mount
|
|
515
|
+
* (the registry promises a face this composition never carries).
|
|
385
516
|
* G5: the two layers and the live rows come from the platform (`configEditor.configuration()`,
|
|
386
517
|
* `pluginManager.listPlugins()`); the registry side (which spellings are deprecated, which rows
|
|
387
518
|
* declare an E3 parameter) stays the family's own table. A surface that is not mounted is
|
|
@@ -411,14 +542,20 @@ function paramDivergences(platform) {
|
|
|
411
542
|
return issues;
|
|
412
543
|
}
|
|
413
544
|
const live = liveRowTails(platform.plugins);
|
|
414
|
-
const
|
|
545
|
+
const noUserLayer = [];
|
|
546
|
+
const unmounted = [];
|
|
547
|
+
for (const entry of PARAM_EXPOSURE) {
|
|
548
|
+
if (entry.tier !== "E3") continue;
|
|
415
549
|
const namespace = paramSettingsId(entry.owner);
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
550
|
+
if (namespace === void 0) noUserLayer.push(entry.id);
|
|
551
|
+
else if (!live.has(namespace)) unmounted.push(entry.id);
|
|
552
|
+
}
|
|
553
|
+
for (const [ids, sentence] of [[noUserLayer, "declared user-writable but the owner publishes no settings namespace"], [unmounted, "declared user-writable but unreachable here"]]) {
|
|
554
|
+
if (ids.length === 0) continue;
|
|
555
|
+
const shown = ids.slice(0, 6).join(", ");
|
|
556
|
+
const more = ids.length > 6 ? ` (+${ids.length - 6} more)` : "";
|
|
557
|
+
const why = sentence.includes("no settings namespace") ? "those parameters have no user layer at all (writes answer E-315) — register the owner in PARAM_NAMESPACES or move its rows to E2" : "the owning row is not mounted in this runtime";
|
|
558
|
+
issues.push(`${sentence}: ${shown}${more} — ${why}`);
|
|
422
559
|
}
|
|
423
560
|
return issues;
|
|
424
561
|
}
|
|
@@ -452,22 +589,16 @@ function asMapping(value) {
|
|
|
452
589
|
* list alone, and treating it as "no bundles" is exactly the fail-open state
|
|
453
590
|
* the double-mount exclusion exists to prevent. */
|
|
454
591
|
function collectEvolutionBundles(home, onReadError) {
|
|
455
|
-
const
|
|
456
|
-
if (
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
try {
|
|
460
|
-
profileEntries = readdirSync(profilesDir, { withFileTypes: true });
|
|
461
|
-
} catch (error) {
|
|
462
|
-
onReadError?.(error, "profiles-dir");
|
|
463
|
-
return bundles;
|
|
592
|
+
const scan = scanProfiles(home);
|
|
593
|
+
if (scan.kind === "unreadable") {
|
|
594
|
+
onReadError?.(scan.error, "profiles-dir");
|
|
595
|
+
return [];
|
|
464
596
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
if (profile.name === "node_modules" || profile.name.startsWith(".")) continue;
|
|
597
|
+
const bundles = [];
|
|
598
|
+
for (const { dir } of scan.profiles) {
|
|
468
599
|
let raw;
|
|
469
600
|
try {
|
|
470
|
-
raw = readFileSync(join(
|
|
601
|
+
raw = readFileSync(join(dir, "package.json"), "utf8");
|
|
471
602
|
} catch (error) {
|
|
472
603
|
if (error?.code === "ENOENT") continue;
|
|
473
604
|
onReadError?.(error, "profile-manifest");
|
|
@@ -571,62 +702,6 @@ function readAgentPresetBases$1(path) {
|
|
|
571
702
|
return null;
|
|
572
703
|
}
|
|
573
704
|
}
|
|
574
|
-
/** The bundle rows one profile manifest mounts (scope-agnostic, as written) — the target a preset
|
|
575
|
-
* recompute resolves its platform base patch through.
|
|
576
|
-
* @param profileDir - the profile directory.
|
|
577
|
-
* @returns the bundle names; empty when the manifest is missing or torn.
|
|
578
|
-
*/
|
|
579
|
-
function profileBundles(profileDir) {
|
|
580
|
-
try {
|
|
581
|
-
const bundles = JSON.parse(readFileSync(join(profileDir, "package.json"), "utf8")).dsh?.profile?.bundles;
|
|
582
|
-
return Array.isArray(bundles) ? bundles.filter((name) => typeof name === "string") : [];
|
|
583
|
-
} catch {
|
|
584
|
-
return [];
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
/** Every profile directory under one DSH_HOME, by name.
|
|
588
|
-
* @param home - resolved DSH_HOME.
|
|
589
|
-
* @returns `{ profile, dir }` per directory under `profiles/` that carries a manifest.
|
|
590
|
-
*/
|
|
591
|
-
function profileDirectories(home) {
|
|
592
|
-
const out = [];
|
|
593
|
-
const root = join(home, "profiles");
|
|
594
|
-
let entries;
|
|
595
|
-
try {
|
|
596
|
-
entries = readdirSync(root, { withFileTypes: true });
|
|
597
|
-
} catch {
|
|
598
|
-
return out;
|
|
599
|
-
}
|
|
600
|
-
for (const entry of entries) {
|
|
601
|
-
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
602
|
-
if (entry.name === "node_modules") continue;
|
|
603
|
-
const dir = join(root, entry.name);
|
|
604
|
-
if (!existsSync(join(dir, "package.json"))) continue;
|
|
605
|
-
out.push({
|
|
606
|
-
profile: entry.name,
|
|
607
|
-
dir
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
return out;
|
|
611
|
-
}
|
|
612
|
-
/** One optional file's text, or `''` when it is absent or unreadable.
|
|
613
|
-
* @param path - the file to read.
|
|
614
|
-
* @returns the text.
|
|
615
|
-
*/
|
|
616
|
-
function readTextOrEmpty(path) {
|
|
617
|
-
try {
|
|
618
|
-
return readFileSync(path, "utf8");
|
|
619
|
-
} catch {
|
|
620
|
-
return "";
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
/** The profile patch text, or `''` when the profile has none.
|
|
624
|
-
* @param profileDir - the profile directory.
|
|
625
|
-
* @returns the patch text.
|
|
626
|
-
*/
|
|
627
|
-
function profilePatchText(profileDir) {
|
|
628
|
-
return readTextOrEmpty(join(profileDir, PROFILE_PATCH_FILENAME));
|
|
629
|
-
}
|
|
630
705
|
/** Every USER-layer patch text that can carry a family preset row: each profile's own patch, then
|
|
631
706
|
* the home layer (`<home>/cordis.patch.yml`), which the platform composes AFTER the profile layer
|
|
632
707
|
* (`readProfilePatches` in `packages/boot/app-boot/src/profile-context.ts`). A row written there
|
|
@@ -636,7 +711,7 @@ function profilePatchText(profileDir) {
|
|
|
636
711
|
* @returns the patch texts that exist, in profile-then-home order.
|
|
637
712
|
*/
|
|
638
713
|
function userPatchTexts(home) {
|
|
639
|
-
const texts =
|
|
714
|
+
const texts = homeProfiles(home).map(({ dir }) => profilePatchText(dir));
|
|
640
715
|
texts.push(readTextOrEmpty(join(home, PROFILE_PATCH_FILENAME)));
|
|
641
716
|
return texts.filter((text) => text !== "");
|
|
642
717
|
}
|
|
@@ -710,7 +785,7 @@ function probePresetFreshness(home, servicePresent) {
|
|
|
710
785
|
} catch (error) {
|
|
711
786
|
deltaFailure = `${deltaPath} could not be read (${error instanceof Error ? error.message : String(error)})`;
|
|
712
787
|
}
|
|
713
|
-
const profiles =
|
|
788
|
+
const profiles = homeProfiles(home);
|
|
714
789
|
if (bases === null) {
|
|
715
790
|
const tablePath = basesPath ?? `${AGENT_PRESET_PACKAGE}/bases.json`;
|
|
716
791
|
return profiles.flatMap(({ profile, dir }) => {
|
|
@@ -841,6 +916,7 @@ async function legacyMigrationRow(ctx, home) {
|
|
|
841
916
|
}
|
|
842
917
|
async function diagnose(ctx, options = {}) {
|
|
843
918
|
const home = options.home ?? evolutionRoot();
|
|
919
|
+
const mounted = options.mounted;
|
|
844
920
|
const conflicts = [];
|
|
845
921
|
const bundles = collectEvolutionBundles(home, (error, scope) => {
|
|
846
922
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -936,6 +1012,7 @@ async function diagnose(ctx, options = {}) {
|
|
|
936
1012
|
installForm,
|
|
937
1013
|
deploymentForm,
|
|
938
1014
|
bundles,
|
|
1015
|
+
foreignProfiles: homeProfiles(home).filter(({ profile, dir }) => mounted === void 0 || profile !== mounted.profile && dir !== mounted.dir).map(({ profile }) => profile),
|
|
939
1016
|
conflicts,
|
|
940
1017
|
envIssues: env,
|
|
941
1018
|
memoryIssues,
|
|
@@ -1094,6 +1171,7 @@ function renderDoctorText(report) {
|
|
|
1094
1171
|
lines.push(`preset: ${row.base} → ${row.destination} ${verdict}`);
|
|
1095
1172
|
}
|
|
1096
1173
|
if (report.presetIssues.length > 0) lines.push("agent presets:", ...report.presetIssues.map((line) => ` ! ${line}`));
|
|
1174
|
+
if (report.foreignProfiles.length > 0) lines.push("profiles-foreign: " + report.foreignProfiles.join(", ") + " — not part of this plane: /evolution preset install ignores them, while the bundles (all profiles) line above counts them");
|
|
1097
1175
|
if (report.conflicts.length > 0) lines.push("conflicts:", ...report.conflicts.map((line) => ` ! ${line}`));
|
|
1098
1176
|
if (report.envIssues.length > 0) lines.push("env:", ...report.envIssues.map((line) => ` ! ${line}`));
|
|
1099
1177
|
if (report.memoryIssues.length > 0) lines.push("memory:", ...report.memoryIssues.map((line) => ` ! ${line}`));
|
|
@@ -1318,6 +1396,14 @@ const COMMAND_ENTRIES = [
|
|
|
1318
1396
|
usage: "skill undo <name> [--to v<N>]",
|
|
1319
1397
|
summary: "把某个技能的内容回退到某一版(默认上一版;只动内容,撤销本身也记为一版)"
|
|
1320
1398
|
},
|
|
1399
|
+
{
|
|
1400
|
+
usage: "skill pin <name>",
|
|
1401
|
+
summary: "钉住一个技能:免于删除、后台审查与生命周期整理(立即生效、随时可解钉;与 skill_manage 的 pin 动作同一语义,不走审批)"
|
|
1402
|
+
},
|
|
1403
|
+
{
|
|
1404
|
+
usage: "skill unpin <name>",
|
|
1405
|
+
summary: "解钉一个技能:撤销上面的冻结(立即生效、不走审批)"
|
|
1406
|
+
},
|
|
1321
1407
|
{
|
|
1322
1408
|
usage: "skills health",
|
|
1323
1409
|
summary: "技能库结构体检"
|
|
@@ -1400,7 +1486,7 @@ const Config = z.object({
|
|
|
1400
1486
|
skillsRoot: z.string().default(""),
|
|
1401
1487
|
maintainCooldownMs: z.number().min(0).default(3e4),
|
|
1402
1488
|
maintainTimeoutMs: z.number().min(1).default(6e5),
|
|
1403
|
-
maintainHandoffOnSettle: z.boolean().default(false),
|
|
1489
|
+
maintainHandoffOnSettle: z.boolean().default(false).volatile(),
|
|
1404
1490
|
threatExemptLabels: z.array(z.string()).default([])
|
|
1405
1491
|
});
|
|
1406
1492
|
/** How one run reads in `maintain status` output. */
|
|
@@ -1553,6 +1639,12 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1553
1639
|
const config = rawConfig;
|
|
1554
1640
|
assertSkillsRootAliasRetired(rawConfig);
|
|
1555
1641
|
const skillsRootValue = resolveRootConfig(rawConfig).root;
|
|
1642
|
+
ctx.inject(["settings"], (scope) => {
|
|
1643
|
+
const settings = scope.settings;
|
|
1644
|
+
if (typeof settings?.configure !== "function") return;
|
|
1645
|
+
const disposer = settings.configure({ auto: false }, ctx.fiber);
|
|
1646
|
+
if (typeof disposer === "function") scope.effect(() => disposer, "evolution-commands: settings presentation");
|
|
1647
|
+
});
|
|
1556
1648
|
let runs;
|
|
1557
1649
|
ctx.inject(["evolutionIo"], (ioCtx) => {
|
|
1558
1650
|
const ioRegistry = ioCtx.get("evolutionIo");
|
|
@@ -1782,6 +1874,23 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1782
1874
|
};
|
|
1783
1875
|
return result.ok ? ok(result.message) : err(result.message);
|
|
1784
1876
|
}
|
|
1877
|
+
if (input === "skill pin" || input.startsWith("skill pin ") || input === "skill unpin" || input.startsWith("skill unpin ")) {
|
|
1878
|
+
const pinned = !input.startsWith("skill unpin");
|
|
1879
|
+
const name = input.slice(pinned ? 10 : 12).trim();
|
|
1880
|
+
if (!name) return err(`Usage: /evolution skill ${pinned ? "pin" : "unpin"} <name>`);
|
|
1881
|
+
const ioRegistry = ctx.get("evolutionIo");
|
|
1882
|
+
if (!ioRegistry) return err("Evolution IO registry not mounted — skill pinning unavailable.");
|
|
1883
|
+
const result = await newSkillLibrary({
|
|
1884
|
+
config: { root: skillsRootValue },
|
|
1885
|
+
io: ioRegistry.provider(),
|
|
1886
|
+
ctx,
|
|
1887
|
+
limits: {
|
|
1888
|
+
...DEFAULT_SKILL_LIMITS,
|
|
1889
|
+
...policyStageLimits(ctx.get("evolutionPolicy")?.get?.())
|
|
1890
|
+
}
|
|
1891
|
+
}).setPinned(name, pinned);
|
|
1892
|
+
return result.ok ? ok(result.message) : err(result.message);
|
|
1893
|
+
}
|
|
1785
1894
|
if (input === "skills health") {
|
|
1786
1895
|
const curator = ctx.get("evolutionCurator");
|
|
1787
1896
|
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
@@ -1937,7 +2046,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1937
2046
|
}).catch((error) => {
|
|
1938
2047
|
ctx.logger.warn(`evolution-commands: failed to record maintain event: ${String(error)}`);
|
|
1939
2048
|
});
|
|
1940
|
-
if (config.maintainHandoffOnSettle
|
|
2049
|
+
if (liveBool(config.maintainHandoffOnSettle) && resultRef !== void 0 && runAgent !== void 0) try {
|
|
1941
2050
|
if (deliverHandoff(runAgent, createUserMessage({
|
|
1942
2051
|
content: [{
|
|
1943
2052
|
type: "text",
|
|
@@ -2096,17 +2205,17 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2096
2205
|
if (presetInstall !== null || /^preset\b/.test(input)) {
|
|
2097
2206
|
if (presetInstall === null) return err("Unknown preset arguments: expected `preset install` or `preset install --base <name>[,<name>...]` (the names live in the agent package's bases.json). Got: " + input);
|
|
2098
2207
|
{
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
|
|
2208
|
+
const mounted = presetProfileTarget(ctx);
|
|
2209
|
+
const exclusion = installExclusionTarget(mounted === void 0 ? void 0 : {
|
|
2210
|
+
profile: mounted.profile,
|
|
2211
|
+
dir: mounted.profileDir,
|
|
2212
|
+
bundles: mounted.bundles
|
|
2213
|
+
});
|
|
2214
|
+
if (exclusion.kind === "undecidable") return err(`Refusing to install the layered Evolution preset: ${exclusion.reason} — the all/preset double-mount check could not run. Resolve access to this profile and retry, or install via the layered installer, which enforces the same exclusion up front.`);
|
|
2215
|
+
const installedBundles = exclusion.bundles;
|
|
2107
2216
|
const allBundle = installedBundles.find((name) => name === "dsh-evolution-all" || name.endsWith("/dsh-evolution-all"));
|
|
2108
2217
|
const presetBundle = installedBundles.find((name) => name === "dsh-evolution-preset" || name.endsWith("/dsh-evolution-preset"));
|
|
2109
|
-
if (allBundle || presetBundle) return err(`Refusing to install the layered Evolution preset: "${allBundle ?? presetBundle}" is installed in this
|
|
2218
|
+
if (allBundle || presetBundle) return err(`Refusing to install the layered Evolution preset: "${allBundle ?? presetBundle}" is installed in this profile — 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).`);
|
|
2110
2219
|
}
|
|
2111
2220
|
try {
|
|
2112
2221
|
const source = resolveAgentPresetDir(import.meta.url);
|
|
@@ -2145,7 +2254,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2145
2254
|
merged = next;
|
|
2146
2255
|
}
|
|
2147
2256
|
if (merged !== current) atomicWriteFiles(dirname(profile.patchPath), [{
|
|
2148
|
-
name: PROFILE_PATCH_FILENAME,
|
|
2257
|
+
name: PROFILE_PATCH_FILENAME$1,
|
|
2149
2258
|
content: presetPatchText(merged)
|
|
2150
2259
|
}], void 0, (message) => {
|
|
2151
2260
|
ctx.logger.warn(message);
|
|
@@ -2314,7 +2423,16 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2314
2423
|
return ok(renderNamespaceMigration(attempt.outcome));
|
|
2315
2424
|
}
|
|
2316
2425
|
if (input === "doctor" || input === "doctor --json") {
|
|
2317
|
-
const
|
|
2426
|
+
const mountedForDoctor = presetProfileTarget(ctx);
|
|
2427
|
+
const mounted = mountedForDoctor === void 0 ? {} : { mounted: {
|
|
2428
|
+
profile: mountedForDoctor.profile,
|
|
2429
|
+
dir: mountedForDoctor.profileDir,
|
|
2430
|
+
bundles: mountedForDoctor.bundles
|
|
2431
|
+
} };
|
|
2432
|
+
const report = await diagnose(ctx, {
|
|
2433
|
+
home: evolutionRoot(),
|
|
2434
|
+
...mounted
|
|
2435
|
+
});
|
|
2318
2436
|
if (input === "doctor --json") return ok(JSON.stringify(report, null, 2));
|
|
2319
2437
|
return ok(renderDoctorText(report));
|
|
2320
2438
|
}
|
package/lib/types/doctor.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type PlatformServiceProbe, type ScopedProbeReport } from '@lmzhen/dsh-evolution-core';
|
|
2
|
+
import type { MountedProfile } from './profiles.ts';
|
|
2
3
|
/** P2-20 (audit): the TS-side copy of the install-target tails. The script
|
|
3
4
|
* side (install-layered.mjs, verify-profile-bundles.mjs) imports the list
|
|
4
5
|
* from packages/scripts/lib-family-packages.mjs; guard-scripts.spec.ts pins
|
|
@@ -79,6 +80,11 @@ export interface DoctorReport {
|
|
|
79
80
|
* counterpart (inferred from the install form, P0-1) and may disagree on a
|
|
80
81
|
* multi-profile machine; the render marks the aggregation explicitly. */
|
|
81
82
|
bundles: string[];
|
|
83
|
+
/** G3 (0.22): profile directories under this home that are NOT the profile this process runs in.
|
|
84
|
+
* On a one-home-two-planes machine (or after a home clone) they are leftovers from the other
|
|
85
|
+
* plane: the write-path exclusion ignores them, while {@link bundles} still counts them — this
|
|
86
|
+
* list is what makes that difference visible instead of mysterious. */
|
|
87
|
+
foreignProfiles: string[];
|
|
82
88
|
conflicts: string[];
|
|
83
89
|
envIssues: string[];
|
|
84
90
|
/** S0.2 (v37 P0-1): memory files carrying the platform's `{{` prompt-variable
|
|
@@ -153,6 +159,7 @@ export declare function diagnose(ctx: {
|
|
|
153
159
|
get(name: string): unknown;
|
|
154
160
|
}, options?: {
|
|
155
161
|
home?: string;
|
|
162
|
+
mounted?: MountedProfile;
|
|
156
163
|
}): Promise<DoctorReport>;
|
|
157
164
|
export declare function renderDoctorText(report: DoctorReport): string;
|
|
158
165
|
//# sourceMappingURL=doctor.d.ts.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Human commands for the evolution family: /evolution learn|pending|curator|restore|consolidate|skills refresh.
|
|
3
3
|
* @module @lmzhen/dsh-evolution-commands
|
|
4
4
|
*/
|
|
5
|
-
import type { Context } from '@deepseek-ai/cordis';
|
|
5
|
+
import type { Context, Volatile } from '@deepseek-ai/cordis';
|
|
6
6
|
import z from '@deepseek-ai/schemastery';
|
|
7
7
|
import type { ContextFormed } from '@deepseek-ai/dsh-llm';
|
|
8
8
|
declare module '@deepseek-ai/dsh-llm' {
|
|
@@ -38,8 +38,13 @@ export interface Config {
|
|
|
38
38
|
maintainTimeoutMs?: number | undefined;
|
|
39
39
|
/** G2 (0.20.0): when a scan settles successfully, hand its report to the model
|
|
40
40
|
* automatically (the same pointer+protocol injection `maintain handoff` performs).
|
|
41
|
-
* Default false: the injection wakes a turn, which costs a model call.
|
|
42
|
-
|
|
41
|
+
* Default false: the injection wakes a turn, which costs a model call.
|
|
42
|
+
*
|
|
43
|
+
* `.volatile()` (0.21.0, G3): the field carries a USER LAYER, so the loader hands the live
|
|
44
|
+
* reference and the switch is read at every settle — a settings write turns it on or off without
|
|
45
|
+
* a reload. A directly constructed plugin still passes a plain boolean (the schema's default is
|
|
46
|
+
* the loader's face), so the reader accepts both. */
|
|
47
|
+
maintainHandoffOnSettle?: boolean | Volatile<boolean> | undefined;
|
|
43
48
|
/** Threat-scan exemption labels for WRITE-path skill mutations (P2-18).
|
|
44
49
|
* Threaded into the SkillLibrary this package constructs for `restructure`
|
|
45
50
|
* (its only write path); the core-side constructor option carries the same
|
|
@@ -55,14 +60,14 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
55
60
|
skillsRoot: z<string, string, "defined">;
|
|
56
61
|
maintainCooldownMs: z<number, number, "defined">;
|
|
57
62
|
maintainTimeoutMs: z<number, number, "defined">;
|
|
58
|
-
maintainHandoffOnSettle: z<boolean, boolean, "defined">;
|
|
63
|
+
maintainHandoffOnSettle: z<boolean, boolean, "volatile-defined">;
|
|
59
64
|
threatExemptLabels: z<string[], string[], "defined">;
|
|
60
65
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
61
66
|
root: z<string, string, "defined">;
|
|
62
67
|
skillsRoot: z<string, string, "defined">;
|
|
63
68
|
maintainCooldownMs: z<number, number, "defined">;
|
|
64
69
|
maintainTimeoutMs: z<number, number, "defined">;
|
|
65
|
-
maintainHandoffOnSettle: z<boolean, boolean, "defined">;
|
|
70
|
+
maintainHandoffOnSettle: z<boolean, boolean, "volatile-defined">;
|
|
66
71
|
threatExemptLabels: z<string[], string[], "defined">;
|
|
67
72
|
}>>, "plain">;
|
|
68
73
|
export declare function apply(ctx: Context, rawConfig?: Config): void;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** The patch file name inside a profile directory (the platform's own spelling). */
|
|
2
|
+
export declare const PROFILE_PATCH_FILENAME = "cordis.patch.yml";
|
|
3
|
+
/** One profile directory under a home. */
|
|
4
|
+
export interface HomeProfile {
|
|
5
|
+
/** The profile's directory name (what `--profile` spells). */
|
|
6
|
+
readonly profile: string;
|
|
7
|
+
/** Its absolute (or home-relative-joined) directory. */
|
|
8
|
+
readonly dir: string;
|
|
9
|
+
}
|
|
10
|
+
/** The outcome of reading `<home>/profiles`: the entries, or why they could not be read. */
|
|
11
|
+
export type ProfilesScan = {
|
|
12
|
+
readonly kind: 'scanned';
|
|
13
|
+
readonly profiles: readonly HomeProfile[];
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: 'unreadable';
|
|
16
|
+
readonly error: unknown;
|
|
17
|
+
};
|
|
18
|
+
/** The ONLY directory read of `<home>/profiles` in the family.
|
|
19
|
+
* `profiles/node_modules` is a platform-created directory that never carries a manifest (S2.1), dot-entries
|
|
20
|
+
* are not profiles, and a directory without `package.json` is not one either.
|
|
21
|
+
* @param home - the resolved DSH home.
|
|
22
|
+
* @returns the scanned entries, or the enumeration error (never a silent empty list).
|
|
23
|
+
*/
|
|
24
|
+
export declare function scanProfiles(home: string): ProfilesScan;
|
|
25
|
+
/** Every profile directory under one home, by name — the DIAGNOSTIC question (doctor's cross-profile
|
|
26
|
+
* summary). An unreadable home answers the empty list here because doctor reports the failure on its
|
|
27
|
+
* own DEGRADED row; a write path must ask {@link installExclusionTarget} instead.
|
|
28
|
+
* @param home - the resolved DSH home.
|
|
29
|
+
* @returns one entry per profile directory carrying a manifest.
|
|
30
|
+
*/
|
|
31
|
+
export declare function homeProfiles(home: string): readonly HomeProfile[];
|
|
32
|
+
/** The bundle names one profile's manifest declares.
|
|
33
|
+
* @param profileDir - the profile directory.
|
|
34
|
+
* @param onError - called with the read/parse failure; a torn manifest is NOT "no bundles".
|
|
35
|
+
* @returns the declared bundle names (empty when the manifest is absent).
|
|
36
|
+
*/
|
|
37
|
+
export declare function profileBundles(profileDir: string, onError?: (error: unknown) => void): string[];
|
|
38
|
+
/** One optional file's text, or `''` when it is absent or unreadable.
|
|
39
|
+
* @param path - the file to read.
|
|
40
|
+
* @returns the text.
|
|
41
|
+
*/
|
|
42
|
+
export declare function readTextOrEmpty(path: string): string;
|
|
43
|
+
/** The profile's own patch text, or `''` when it has none.
|
|
44
|
+
* @param profileDir - the profile directory.
|
|
45
|
+
* @returns the patch text.
|
|
46
|
+
*/
|
|
47
|
+
export declare function profilePatchText(profileDir: string): string;
|
|
48
|
+
/** The profile this PROCESS is mounted in, as the write-path exclusion must see it. */
|
|
49
|
+
export interface MountedProfile {
|
|
50
|
+
/** The mounted profile's name. */
|
|
51
|
+
readonly profile: string;
|
|
52
|
+
/** Its directory. */
|
|
53
|
+
readonly dir: string;
|
|
54
|
+
/** The bundles this plane actually STARTED (the platform's `startedBundles`). */
|
|
55
|
+
readonly bundles: readonly string[];
|
|
56
|
+
}
|
|
57
|
+
/** Where the preset-install exclusion must look: THIS plane only, or an explicit refusal.
|
|
58
|
+
*
|
|
59
|
+
* The write path does NOT scan the home: the mounted profile is the whole question (its started
|
|
60
|
+
* bundles plus the ones its own manifest declares — either can double-mount), and a question that
|
|
61
|
+
* cannot be answered still REFUSES. That is what makes a leftover profile from the other plane
|
|
62
|
+
* harmless here: it is not this plane's profile, so it is never consulted.
|
|
63
|
+
* @param mounted - the mounted profile, or undefined outside a profile (a bare library mount).
|
|
64
|
+
* @returns the profile to judge with its bundle rows, or the reason a refusal is mandatory.
|
|
65
|
+
*/
|
|
66
|
+
export declare function installExclusionTarget(mounted: MountedProfile | undefined): {
|
|
67
|
+
readonly kind: 'mounted';
|
|
68
|
+
readonly profile: HomeProfile;
|
|
69
|
+
readonly bundles: readonly string[];
|
|
70
|
+
} | {
|
|
71
|
+
readonly kind: 'undecidable';
|
|
72
|
+
readonly reason: string;
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=profiles.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 (/evolution pending|curator|maintain|doctor) (community build)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.22.0",
|
|
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.4",
|
|
30
|
-
"@lmzhen/dsh-evolution-approval": "^0.
|
|
31
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
32
|
-
"@lmzhen/dsh-evolution-maintenance": "^0.
|
|
30
|
+
"@lmzhen/dsh-evolution-approval": "^0.22.0",
|
|
31
|
+
"@lmzhen/dsh-evolution-core": "^0.22.0",
|
|
32
|
+
"@lmzhen/dsh-evolution-maintenance": "^0.22.0"
|
|
33
33
|
},
|
|
34
34
|
"optionalDependencies": {
|
|
35
|
-
"@lmzhen/dsh-evolution-agent-preset": "^0.
|
|
35
|
+
"@lmzhen/dsh-evolution-agent-preset": "^0.22.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@deepseek-ai/cordis": "^4.0.1",
|