@lmzhen/dsh-evolution-commands 0.17.1 → 0.18.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 +649 -592
- package/lib/types/doctor.d.ts +5 -1
- package/lib/types/preset-source.d.ts +29 -0
- package/package.json +6 -6
package/lib/index.js
CHANGED
|
@@ -2,8 +2,8 @@ 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, appendEvolutionEvent, applyNamespaceMigration, assertSkillsRootAliasRetired, buildLearnPrompt, canonicalWriteId, clampedNumber, classifyNamespaceMigration, composePresetEntry, elapsedSince, errorText, eventsFile, evolutionRoot, isDeprecatedParamId, isMissingPath, mergePresetRow, newSkillLibrary, paramSettingsId, partitionVersions, planNamespaceMigration, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, resolveBasePresetPatch, resolveParamId, resolveRootConfig, scopedProbeReport } from "@lmzhen/dsh-evolution-core";
|
|
6
|
-
import { buildEnrichment, buildMaintainFacts, runMaintain, snapshotFromLibrary } from "@lmzhen/dsh-evolution-maintenance";
|
|
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, evolutionRoot, isDeprecatedParamId, isMissingPath, mergePresetRow, newSkillLibrary, paramSettingsId, partitionVersions, planNamespaceMigration, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, resolveBasePresetPatch, resolveParamId, resolveRootConfig, scopedProbeReport } from "@lmzhen/dsh-evolution-core";
|
|
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";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
@@ -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
|
|
238
|
+
const PROFILE_PATCH_FILENAME = "cordis.patch.yml";
|
|
239
239
|
/**
|
|
240
240
|
* The profile patch this process writes into, or undefined outside a profile.
|
|
241
241
|
*
|
|
@@ -251,11 +251,33 @@ 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),
|
|
255
255
|
bundles: Array.isArray(profile.startedBundles) ? profile.startedBundles : []
|
|
256
256
|
};
|
|
257
257
|
}
|
|
258
258
|
/**
|
|
259
|
+
* The ONE host-side answer to "can this base be used here?".
|
|
260
|
+
*
|
|
261
|
+
* Both host entry points ask it: `/evolution preset install` refuses a base through this function,
|
|
262
|
+
* and `/evolution doctor` enumerates the installable bases through it, with the SAME probe — the
|
|
263
|
+
* runtime's own answer (`ctx.get(<service>)`), which is exactly the reason a later mount would
|
|
264
|
+
* refuse. A doctor that infers the answer from bundle names instead (it did until v46 S2.8) can
|
|
265
|
+
* disagree with the write entry it tells the operator to run: a deployment that mounts the web-app
|
|
266
|
+
* bundle while the service's own row is disabled installs nothing, yet reads as installable. The
|
|
267
|
+
* source-checkout installer runs outside the host and cannot read the service store; it asks the
|
|
268
|
+
* target profile's bundle rows instead and says so (`install-layered.mjs`'s
|
|
269
|
+
* `baseUnavailableReason`).
|
|
270
|
+
* @param entry - the table row.
|
|
271
|
+
* @param servicePresent - whether a service name resolves in this deployment.
|
|
272
|
+
* @returns the refusal text, or undefined when the base is usable here.
|
|
273
|
+
*/
|
|
274
|
+
function baseRefusalReason(entry, servicePresent) {
|
|
275
|
+
if (typeof entry.unsupported === "string" && entry.unsupported !== "") return `Base "${entry.name}" is registered as UNSUPPORTED: ${entry.unsupported}`;
|
|
276
|
+
const service = entry.requires?.service;
|
|
277
|
+
if (typeof service !== "string" || service === "") return void 0;
|
|
278
|
+
return servicePresent(service) ? void 0 : `Base "${entry.name}" requires the "${service}" service, which this deployment does not provide — the generated preset would refuse to mount.`;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
259
281
|
* Resolve the platform base preset patch one `--base` composes.
|
|
260
282
|
*
|
|
261
283
|
* The module graph is probed FIRST: a desktop/Electron host resolves the bundle package inside
|
|
@@ -381,7 +403,7 @@ function paramDivergences(platform) {
|
|
|
381
403
|
issues.push(`user override: ${entryId} sets ${keys.length} parameter(s) — ${shown.join(", ")}${more}`);
|
|
382
404
|
for (const key of keys) {
|
|
383
405
|
if (!isDeprecatedParamId(key)) continue;
|
|
384
|
-
issues.push(`deprecated name: ${entryId} still writes "${key}" — write "${resolveParamId(key)}" instead (writes refuse the alias;
|
|
406
|
+
issues.push(`deprecated name: ${entryId} still writes "${key}" — write "${resolveParamId(key)}" instead (writes refuse the alias; removal was planned for 0.7.0 and is deferred — the alias still ships)`);
|
|
385
407
|
}
|
|
386
408
|
}
|
|
387
409
|
if (platform.plugins === void 0) {
|
|
@@ -549,9 +571,19 @@ function readAgentPresetBases$1(path) {
|
|
|
549
571
|
return null;
|
|
550
572
|
}
|
|
551
573
|
}
|
|
552
|
-
/** The profile
|
|
553
|
-
*
|
|
554
|
-
|
|
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
|
+
}
|
|
555
587
|
/** Every profile directory under one DSH_HOME, by name.
|
|
556
588
|
* @param home - resolved DSH_HOME.
|
|
557
589
|
* @returns `{ profile, dir }` per directory under `profiles/` that carries a manifest.
|
|
@@ -577,16 +609,15 @@ function profileDirectories(home) {
|
|
|
577
609
|
}
|
|
578
610
|
return out;
|
|
579
611
|
}
|
|
580
|
-
/**
|
|
581
|
-
* @param
|
|
582
|
-
* @returns the
|
|
612
|
+
/** One optional file's text, or `''` when it is absent or unreadable.
|
|
613
|
+
* @param path - the file to read.
|
|
614
|
+
* @returns the text.
|
|
583
615
|
*/
|
|
584
|
-
function
|
|
616
|
+
function readTextOrEmpty(path) {
|
|
585
617
|
try {
|
|
586
|
-
|
|
587
|
-
return Array.isArray(bundles) ? bundles.filter((name) => typeof name === "string") : [];
|
|
618
|
+
return readFileSync(path, "utf8");
|
|
588
619
|
} catch {
|
|
589
|
-
return
|
|
620
|
+
return "";
|
|
590
621
|
}
|
|
591
622
|
}
|
|
592
623
|
/** The profile patch text, or `''` when the profile has none.
|
|
@@ -594,33 +625,42 @@ function profileBundles(profileDir) {
|
|
|
594
625
|
* @returns the patch text.
|
|
595
626
|
*/
|
|
596
627
|
function profilePatchText(profileDir) {
|
|
597
|
-
|
|
598
|
-
return readFileSync(join(profileDir, PROFILE_PATCH_FILENAME), "utf8");
|
|
599
|
-
} catch {
|
|
600
|
-
return "";
|
|
601
|
-
}
|
|
628
|
+
return readTextOrEmpty(join(profileDir, PROFILE_PATCH_FILENAME));
|
|
602
629
|
}
|
|
603
|
-
/**
|
|
604
|
-
*
|
|
630
|
+
/** Every USER-layer patch text that can carry a family preset row: each profile's own patch, then
|
|
631
|
+
* the home layer (`<home>/cordis.patch.yml`), which the platform composes AFTER the profile layer
|
|
632
|
+
* (`readProfilePatches` in `packages/boot/app-boot/src/profile-context.ts`). A row written there
|
|
633
|
+
* mounts exactly like one in the profile patch, so a probe reading only the per-profile patches
|
|
634
|
+
* reports a home-layer install as no install at all.
|
|
635
|
+
* @param home - resolved DSH_HOME.
|
|
636
|
+
* @returns the patch texts that exist, in profile-then-home order.
|
|
637
|
+
*/
|
|
638
|
+
function userPatchTexts(home) {
|
|
639
|
+
const texts = profileDirectories(home).map(({ dir }) => profilePatchText(dir));
|
|
640
|
+
texts.push(readTextOrEmpty(join(home, PROFILE_PATCH_FILENAME)));
|
|
641
|
+
return texts.filter((text) => text !== "");
|
|
642
|
+
}
|
|
643
|
+
/** Whether a base's `requires.service` precondition is met HERE — the SAME function and the SAME
|
|
644
|
+
* probe `/evolution preset install` refuses through (`baseRefusalReason` over the runtime's own
|
|
645
|
+
* service store), so the doctor and the write entry cannot reach different conclusions about one
|
|
646
|
+
* deployment.
|
|
605
647
|
* @param base - the table row.
|
|
606
|
-
* @param
|
|
648
|
+
* @param servicePresent - whether a service name resolves in this runtime.
|
|
607
649
|
* @returns true when the base can be installed here.
|
|
608
650
|
*/
|
|
609
|
-
function baseInstallable(base,
|
|
610
|
-
|
|
611
|
-
const service = base.requires?.service;
|
|
612
|
-
if (typeof service !== "string" || service === "") return true;
|
|
613
|
-
return bundles.some((name) => name.trim().endsWith("dsh-web-app"));
|
|
651
|
+
function baseInstallable(base, servicePresent) {
|
|
652
|
+
return baseRefusalReason(base, servicePresent) === void 0;
|
|
614
653
|
}
|
|
615
654
|
/**
|
|
616
|
-
* S1-F1: ids of the family's INSTALLED preset rows — every declared preset row in a
|
|
617
|
-
* that can actually carry the family model rows.
|
|
655
|
+
* S1-F1: ids of the family's INSTALLED preset rows — every declared preset row in a USER patch
|
|
656
|
+
* layer that can actually carry the family model rows.
|
|
618
657
|
*
|
|
619
|
-
* The artifact is a declared preset ROW in a
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
658
|
+
* The artifact is a declared preset ROW in a user patch layer, so this probe reads every profile's
|
|
659
|
+
* own patch AND the home layer (`userPatchTexts`). A probe narrowed to one base id leaves a
|
|
660
|
+
* `--base ptc|cordis` install invisible; one narrowed to the profile layer alone (it was until v46
|
|
661
|
+
* S2.8/T4-07) leaves a home-layer install invisible — either way installForm degrades to
|
|
662
|
+
* `none`/`host` and the action ladder recommends installing `evolution-all` on top, the exact
|
|
663
|
+
* double-mount the doctor exists to prevent.
|
|
624
664
|
*
|
|
625
665
|
* A row counts only when the base table names its id as a SUPPORTED family base (the `unsupported`
|
|
626
666
|
* minimal base carries no model rows and cannot double-mount anything). When the table itself
|
|
@@ -633,8 +673,7 @@ function installedLayeredPresetIds(home) {
|
|
|
633
673
|
const basesPath = resolveAgentPresetAsset("bases.json");
|
|
634
674
|
const bases = basesPath === null ? null : readAgentPresetBases$1(basesPath);
|
|
635
675
|
const ids = /* @__PURE__ */ new Set();
|
|
636
|
-
for (const
|
|
637
|
-
const text = profilePatchText(dir);
|
|
676
|
+
for (const text of userPatchTexts(home)) {
|
|
638
677
|
if (text === "") continue;
|
|
639
678
|
if (bases === null) {
|
|
640
679
|
for (const match of text.matchAll(/^\s*- id:\s*(preset-[a-z0-9-]+)\s*$/gm)) if (match[1]?.startsWith(`preset-evolution`)) ids.add(match[1].slice(7));
|
|
@@ -657,9 +696,10 @@ function installedLayeredPresetIds(home) {
|
|
|
657
696
|
* installer about what "fresh" means. Read-only by contract: the snapshot is
|
|
658
697
|
* never rewritten.
|
|
659
698
|
* @param home - resolved DSH_HOME.
|
|
699
|
+
* @param servicePresent - whether a service name resolves in this runtime (the write entry's probe).
|
|
660
700
|
* @returns one row per profile × installable base of the family bases.json.
|
|
661
701
|
*/
|
|
662
|
-
function probePresetFreshness(home) {
|
|
702
|
+
function probePresetFreshness(home, servicePresent) {
|
|
663
703
|
const basesPath = resolveAgentPresetAsset("bases.json");
|
|
664
704
|
const deltaPath = resolveAgentPresetAsset("agent.cordis.yml");
|
|
665
705
|
const bases = basesPath === null ? null : readAgentPresetBases$1(basesPath);
|
|
@@ -690,7 +730,7 @@ function probePresetFreshness(home) {
|
|
|
690
730
|
const text = profilePatchText(dir);
|
|
691
731
|
const bundles = profileBundles(dir);
|
|
692
732
|
for (const base of bases) {
|
|
693
|
-
if (!baseInstallable(base,
|
|
733
|
+
if (!baseInstallable(base, servicePresent)) continue;
|
|
694
734
|
const block = presetRowBlock(text, presetRowId(base.id));
|
|
695
735
|
if (block === null) {
|
|
696
736
|
rows.push({
|
|
@@ -807,7 +847,8 @@ async function diagnose(ctx, options = {}) {
|
|
|
807
847
|
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`);
|
|
808
848
|
});
|
|
809
849
|
const has = (name) => ctx.get(name) !== void 0;
|
|
810
|
-
const
|
|
850
|
+
const capabilityAbsence = absentHostServices(has);
|
|
851
|
+
const presetFreshness = probePresetFreshness(home, has);
|
|
811
852
|
const platform = await readPlatformView(ctx);
|
|
812
853
|
const familyBundleRows = platform.bundles?.filter((row) => tailOf(row.name).startsWith("dsh-evolution-"));
|
|
813
854
|
const runtimeBundles = familyBundleRows === void 0 ? void 0 : familyBundleRows.filter((row) => row.enabled).map((row) => row.name);
|
|
@@ -838,7 +879,9 @@ async function diagnose(ctx, options = {}) {
|
|
|
838
879
|
};
|
|
839
880
|
let pendingCount = null;
|
|
840
881
|
let executingCount = null;
|
|
882
|
+
let pendingTtlMs = null;
|
|
841
883
|
const approvalService = ctx.get("evolutionApproval");
|
|
884
|
+
if (typeof approvalService?.ttlMs === "number" && Number.isFinite(approvalService.ttlMs)) pendingTtlMs = approvalService.ttlMs;
|
|
842
885
|
const approvalList = approvalService?.list?.bind(approvalService);
|
|
843
886
|
if (approvalList !== void 0) try {
|
|
844
887
|
const pendingProbe = await probeBounded(() => approvalList("pending"), "approval pending listing");
|
|
@@ -867,8 +910,10 @@ async function diagnose(ctx, options = {}) {
|
|
|
867
910
|
else if (installForm === "layered") actions.push("Variant form (session opt-in): model tools follow the Evolution preset, and a session on a platform original preset carries no family rows; add @lmzhen/dsh-evolution-all instead if every session should have them.");
|
|
868
911
|
const env = envIssues();
|
|
869
912
|
if (env.length > 0) actions.push("Fix the DSH_EVOLUTION_* variable listed above.");
|
|
913
|
+
if (capabilityAbsence.length > 0) actions.push(`The family probes ${capabilityAbsence.length} platform capability/ies this deployment does not mount (${capabilityAbsence.map((entry) => entry.service).join(", ")}) — mount the rows that carry the ones you need; the features they gate are listed above.`);
|
|
870
914
|
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.");
|
|
871
915
|
if (pendingCount === null && services.approval) actions.push("Approval service is mounted but pending listing failed — check the evolution state service.");
|
|
916
|
+
if (pendingTtlMs !== null && pendingTtlMs > 0) actions.push(`Staged writes expire after ${pendingTtlMs} ms (evolution-approval.pendingTtlMs): an expired write is closed as rejected and is never executed.`);
|
|
872
917
|
const presetIssueRows = presetIssues(platform.presets);
|
|
873
918
|
if (presetIssueRows.length > 0) actions.push("Resolve the agent-preset finding above: a broken preset composes no session, and a row two presets declare is resolved by layer order (keep the declaration in one preset).");
|
|
874
919
|
const legacy = await legacyMigrationRow(ctx, home);
|
|
@@ -898,6 +943,7 @@ async function diagnose(ctx, options = {}) {
|
|
|
898
943
|
paramIssues,
|
|
899
944
|
queryIssues,
|
|
900
945
|
services,
|
|
946
|
+
capabilityAbsence,
|
|
901
947
|
pendingCount,
|
|
902
948
|
executingCount,
|
|
903
949
|
presetFreshness,
|
|
@@ -1036,6 +1082,7 @@ function renderDoctorText(report) {
|
|
|
1036
1082
|
report.dormantBundles.length === 0 ? null : `installed but not selected: ${report.dormantBundles.join(", ")} — the Plugins page lists them; this runtime mounts neither`,
|
|
1037
1083
|
`bundles (all profiles): ${report.bundles.length > 0 ? report.bundles.join(", ") : "(none)"}`,
|
|
1038
1084
|
`services: review=${report.services.review} (${report.serviceSource === "platform" ? "the live row, this runtime" : "inferred from bundles, all profiles"}) curator=${report.services.curator} approval=${report.services.approval} skillUsage=${report.services.skillUsage} io=${report.services.io}`,
|
|
1085
|
+
report.capabilityAbsence.length === 0 ? null : `capability absence (${report.capabilityAbsence.length}): ${report.capabilityAbsence.map((entry) => `${entry.service} — ${entry.feature}`).join("; ")}`,
|
|
1039
1086
|
scopedProbeLine(report.scopedProbe),
|
|
1040
1087
|
legacyLine(report.legacy),
|
|
1041
1088
|
`pending: ${report.pendingCount === null ? "unknown" : report.pendingCount}`,
|
|
@@ -1348,580 +1395,568 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1348
1395
|
let maintainInFlightSince = 0;
|
|
1349
1396
|
ctx.inject(["commands"], (commandCtx) => {
|
|
1350
1397
|
const commands = commandCtx.commands;
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1398
|
+
const commandBody = { run: async (invocation) => {
|
|
1399
|
+
const rawInputTrimmed = invocation.rawInput.trim();
|
|
1400
|
+
const input = rawInputTrimmed.replace(/\s+/g, " ");
|
|
1401
|
+
const ok = (text) => ({
|
|
1402
|
+
kind: "success",
|
|
1403
|
+
text
|
|
1404
|
+
});
|
|
1405
|
+
const err = (text) => ({
|
|
1406
|
+
kind: "error",
|
|
1407
|
+
text
|
|
1408
|
+
});
|
|
1409
|
+
const invocationAgent = invocation.agent;
|
|
1410
|
+
const agentMissing = (need) => invocationAgent === void 0 ? err(errorText("e-305-this-invocation-carries-no", { a1: need })) : void 0;
|
|
1411
|
+
const unreplayableWriteRefusal = (what) => {
|
|
1412
|
+
if (approval === void 0) return void 0;
|
|
1413
|
+
const session = invocationAgent?.session;
|
|
1414
|
+
const sessionPolicy = effectiveSessionPolicy(ctx, session);
|
|
1415
|
+
if (!(approval.isEnabled !== false && sessionPolicy !== "never" && approval.stageForeground !== false)) return void 0;
|
|
1416
|
+
return err(errorText("e-306-this-deployment-stages-foreground", { a1: what }));
|
|
1417
|
+
};
|
|
1418
|
+
const approval = ctx.get("evolutionApproval");
|
|
1419
|
+
const pendingMatch = /^pending(?: --detail)?$/.exec(input);
|
|
1420
|
+
if (pendingMatch) {
|
|
1421
|
+
if (!approval) return err(errorText("e-301-approval-service-not-mounted"));
|
|
1422
|
+
const listed = [...await approval.list("pending"), ...await approval.list("executing")];
|
|
1423
|
+
const pending = [...new Map(listed.map((row) => [row.id, row])).values()];
|
|
1424
|
+
const ttlMs = typeof approval.ttlMs === "number" && Number.isFinite(approval.ttlMs) ? approval.ttlMs : 0;
|
|
1425
|
+
const ttlNote = ttlMs > 0 ? `\n(staged writes expire after ${ttlMs} ms: an expired write is closed as REJECTED and is never executed — stage it again if it is still wanted)` : "\n(staged writes never expire — the window waits for a decision; set the evolution-approval row pendingTtlMs to bound it)";
|
|
1426
|
+
if (pending.length === 0) return ok("No pending evolution writes." + ttlNote);
|
|
1427
|
+
const detailed = pendingMatch[0] === "pending --detail";
|
|
1428
|
+
const nowMs = Date.now();
|
|
1429
|
+
const attribution = (p) => {
|
|
1430
|
+
const age = elapsedSince(p.createdAt, nowMs);
|
|
1431
|
+
const unit = age.n === 1 ? age.unit.replace(/s$/u, "") : age.unit;
|
|
1432
|
+
return ` · ${age.unit === "now" ? "just now" : `${age.n} ${unit} ago`}${p.origin === void 0 ? "" : ` from ${p.origin}`}${p.sessionId === void 0 ? "" : ` (session ${p.sessionId.slice(0, 8)})`}`;
|
|
1433
|
+
};
|
|
1434
|
+
return ok(pending.map((p) => {
|
|
1435
|
+
if (!detailed) return `${p.id} ${p.kind} ${p.status === "executing" ? "EXECUTING " : ""}${p.summary}${attribution(p)}`;
|
|
1436
|
+
const status = p.status === "executing" ? "EXECUTING" : p.status;
|
|
1437
|
+
const line = `${p.id} ${p.kind} ${status} ${p.summary}${attribution(p)}`;
|
|
1438
|
+
return p.args === void 0 ? line : `${line}\n staged args: ${safeStagedArgs(p.args)}`;
|
|
1439
|
+
}).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)" : "") + ttlNote);
|
|
1440
|
+
}
|
|
1441
|
+
if (input.startsWith("approve ")) {
|
|
1442
|
+
const id = input.slice(8).trim();
|
|
1443
|
+
const result = approval ? await approval.approve(id) : {
|
|
1444
|
+
ok: false,
|
|
1445
|
+
message: errorText("e-301-approval-service-not-mounted-2")
|
|
1446
|
+
};
|
|
1447
|
+
return result.ok ? ok(result.message) : err(result.message);
|
|
1448
|
+
}
|
|
1449
|
+
if (input.startsWith("reject ")) {
|
|
1450
|
+
const id = input.slice(7).trim();
|
|
1451
|
+
const result = approval ? await approval.reject(id) : {
|
|
1452
|
+
ok: false,
|
|
1453
|
+
message: errorText("e-301-approval-service-not-mounted-2")
|
|
1454
|
+
};
|
|
1455
|
+
return result.ok ? ok(result.message) : err(result.message);
|
|
1456
|
+
}
|
|
1457
|
+
if (input.startsWith("release ")) {
|
|
1458
|
+
const id = input.slice(8).trim();
|
|
1459
|
+
if (!approval) return err(errorText("e-301-approval-service-not-mounted-2"));
|
|
1460
|
+
if (!approval.release) return err(errorText("e-304-the-mounted-approval-service"));
|
|
1461
|
+
const result = await approval.release(id);
|
|
1462
|
+
return result.ok ? ok(result.message) : err(result.message);
|
|
1463
|
+
}
|
|
1464
|
+
if (input === "curator run") {
|
|
1465
|
+
const curator = ctx.get("evolutionCurator");
|
|
1466
|
+
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1467
|
+
const result = await curator.run({ ignoreGates: true });
|
|
1468
|
+
if (result.skipped !== void 0) return ok(`Curator run skipped (${result.skipped}): no curation pass was executed.\nrunId=${result.report.runId}`);
|
|
1469
|
+
return ok(`Curator run complete: ${result.stale.length} stale, ${result.archived.length} archived, ${result.errors.length} failed.\nrunId=${result.report.runId}${result.report.snapshotPath ? `\nsnapshot=${result.report.snapshotPath}` : ""}`);
|
|
1470
|
+
}
|
|
1471
|
+
if (input === "curator pause" || input === "curator resume") {
|
|
1472
|
+
const curator = ctx.get("evolutionCurator");
|
|
1473
|
+
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1474
|
+
const paused = input === "curator pause";
|
|
1475
|
+
await curator.setPaused(paused);
|
|
1476
|
+
const notPersisted = ctx.get("evolutionState") !== void 0 ? "" : "\nNOTE: no evolution state service is mounted — this setting is NOT persisted and will not survive this process.";
|
|
1477
|
+
return ok((paused ? "Curator automatic curation paused. Manual /evolution curator run is unaffected; resume with /evolution curator resume." : "Curator automatic curation resumed. The next scheduled pass waits one interval (first-run defer semantics).") + notPersisted);
|
|
1478
|
+
}
|
|
1479
|
+
if (input === "curator status") {
|
|
1480
|
+
const curator = ctx.get("evolutionCurator");
|
|
1481
|
+
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1482
|
+
const state = await curator.status();
|
|
1483
|
+
if (!state) return ok("No curator state yet: the first automatic pass is deferred until the interval elapses.");
|
|
1484
|
+
const lastRun = typeof state.lastRunAt === "number" && Number.isFinite(state.lastRunAt) && state.lastRunAt > 0 ? new Date(state.lastRunAt).toISOString() : "unknown";
|
|
1485
|
+
return ok([
|
|
1486
|
+
`paused=${state.paused}`,
|
|
1487
|
+
`runs=${state.runCount}`,
|
|
1488
|
+
`lastRun=${lastRun}`,
|
|
1489
|
+
`summary=${state.lastSummary}`
|
|
1490
|
+
].join("\n"));
|
|
1491
|
+
}
|
|
1492
|
+
if (input === "mutations") {
|
|
1493
|
+
const curator = ctx.get("evolutionCurator");
|
|
1494
|
+
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1495
|
+
const records = await curator.skills.listMutations();
|
|
1496
|
+
const usable = Array.isArray(records) ? records.filter((row) => typeof row === "object" && row !== null && typeof row.at === "string" && typeof row.skillName === "string" && typeof row.action === "string" && typeof row.summary === "string") : [];
|
|
1497
|
+
if (usable.length === 0) return ok("No mutation records yet.");
|
|
1498
|
+
const recent = usable.slice(-5).reverse().map((record) => `${record.at.slice(0, 19)} ${record.skillName} ${record.action} ${record.summary}`);
|
|
1499
|
+
return ok(`Mutations: ${usable.length} recorded (recent 5):\n${recent.join("\n")}`);
|
|
1500
|
+
}
|
|
1501
|
+
if (input === "curator scope") {
|
|
1502
|
+
const curator = ctx.get("evolutionCurator");
|
|
1503
|
+
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1504
|
+
const view = await curator.scopeView();
|
|
1505
|
+
const line = (label, names) => `${label}: ${names.length}${names.length === 0 ? "" : `\n ${names.join(", ")}`}`;
|
|
1506
|
+
return ok([
|
|
1507
|
+
`Lifecycle scope at ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`,
|
|
1508
|
+
line("Managed (may transition)", view.managed),
|
|
1509
|
+
line("Watched (stale / quality-warned)", view.watched),
|
|
1510
|
+
line("Quality-warned", view.qualityWarned),
|
|
1511
|
+
line("Exempted (exclude / referenced)", view.exempted),
|
|
1512
|
+
line("Protected (pinned / bundled / hub / builtin)", view.protected)
|
|
1513
|
+
].join("\n"));
|
|
1514
|
+
}
|
|
1515
|
+
if (input === "curator report") {
|
|
1516
|
+
const curator = ctx.get("evolutionCurator");
|
|
1517
|
+
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1518
|
+
const report = await curator.latestReport();
|
|
1519
|
+
if (!report) return ok("No curator report available.");
|
|
1520
|
+
if (typeof report !== "object" || typeof report.runId !== "string" || typeof report.startedAt !== "string" || !Array.isArray(report.archived) || !Array.isArray(report.failed)) return err("Report file unreadable.");
|
|
1521
|
+
const reportAny = report;
|
|
1522
|
+
const nameOf = (item) => typeof item === "object" && item !== null && typeof item.name === "string" ? item.name : "?";
|
|
1523
|
+
const reasonOf = (item) => typeof item === "object" && item !== null && typeof item.reason === "string" ? item.reason : "?";
|
|
1524
|
+
const aborted = typeof reportAny.aborted === "string" ? reportAny.aborted : void 0;
|
|
1525
|
+
const unattributed = Array.isArray(reportAny.unattributed) ? reportAny.unattributed.length : 0;
|
|
1526
|
+
return ok([
|
|
1527
|
+
`runId=${reportAny.runId}`,
|
|
1528
|
+
`startedAt=${reportAny.startedAt}`,
|
|
1529
|
+
`archived=${reportAny.archived.map(nameOf).join(", ") || "(none)"}`,
|
|
1530
|
+
`failed=${reportAny.failed.map((item) => `${nameOf(item)}: ${reasonOf(item)}`).join(", ") || "(none)"}`,
|
|
1531
|
+
...aborted === void 0 ? [] : [`aborted=${aborted}`],
|
|
1532
|
+
...unattributed === 0 ? [] : [`unattributed=${unattributed}`]
|
|
1533
|
+
].join("\n"));
|
|
1534
|
+
}
|
|
1535
|
+
if (input === "restore" || input.startsWith("restore ")) {
|
|
1536
|
+
const tail = input.slice(7).trim();
|
|
1537
|
+
if (tail !== "") return err(`\`restore\` takes no arguments (got "${tail}"). Use \`restore\` for a whole-tree rollback, or \`skill restore <name>\` for one skill.`);
|
|
1538
|
+
const restoreRefusal = unreplayableWriteRefusal("restore");
|
|
1539
|
+
if (restoreRefusal) return restoreRefusal;
|
|
1540
|
+
const curator = ctx.get("evolutionCurator");
|
|
1541
|
+
const result = curator ? await curator.restoreSnapshot() : {
|
|
1542
|
+
ok: false,
|
|
1543
|
+
message: errorText("e-302-curator-service-not-mounted")
|
|
1544
|
+
};
|
|
1545
|
+
return result.ok ? ok(result.message) : err(result.message);
|
|
1546
|
+
}
|
|
1547
|
+
if (input.startsWith("consolidate ")) {
|
|
1548
|
+
const consolidateRefusal = unreplayableWriteRefusal("consolidate");
|
|
1549
|
+
if (consolidateRefusal) return consolidateRefusal;
|
|
1550
|
+
const planTail = /\s--plan\s+(\S+)\s*$/.exec(input) ?? null;
|
|
1551
|
+
const planRunId = planTail?.[1] ?? void 0;
|
|
1552
|
+
const [target, ...sources] = (planTail ? input.slice(0, planTail.index) : input).slice(12).trim().split(/\s+/).filter(Boolean);
|
|
1553
|
+
if (!target || sources.length === 0) return err("Usage: /evolution consolidate <target> <source...>");
|
|
1554
|
+
const curator = ctx.get("evolutionCurator");
|
|
1555
|
+
const result = curator ? await curator.consolidate(target, sources) : {
|
|
1556
|
+
ok: false,
|
|
1557
|
+
message: errorText("e-302-curator-service-not-mounted")
|
|
1558
|
+
};
|
|
1559
|
+
if (!result.ok) return err(result.message);
|
|
1560
|
+
return ok(planRunId ? `${result.message}\n[audit] plan=${planRunId}` : result.message);
|
|
1561
|
+
}
|
|
1562
|
+
if (input.startsWith("skill restore ")) {
|
|
1563
|
+
const name = input.slice(14).trim();
|
|
1564
|
+
if (!name) return err("Usage: /evolution skill restore <name>");
|
|
1565
|
+
const skillRestoreRefusal = unreplayableWriteRefusal("skill restore");
|
|
1566
|
+
if (skillRestoreRefusal) return skillRestoreRefusal;
|
|
1567
|
+
const curator = ctx.get("evolutionCurator");
|
|
1568
|
+
const result = curator ? await curator.restore(name) : {
|
|
1569
|
+
ok: false,
|
|
1570
|
+
message: errorText("e-302-curator-service-not-mounted")
|
|
1571
|
+
};
|
|
1572
|
+
return result.ok ? ok(result.message) : err(result.message);
|
|
1573
|
+
}
|
|
1574
|
+
if (input.startsWith("skill history ")) {
|
|
1575
|
+
const name = input.slice(14).trim();
|
|
1576
|
+
if (!name) return err("Usage: /evolution skill history <name>");
|
|
1577
|
+
const curator = ctx.get("evolutionCurator");
|
|
1578
|
+
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1579
|
+
const versions = await curator.history(name);
|
|
1580
|
+
if (versions.length === 0) return ok(`No content versions are recorded for "${name}" yet — they appear after the next write through skill_manage.`);
|
|
1581
|
+
const groups = partitionVersions(versions);
|
|
1582
|
+
const row = (entry) => `v${entry.v}\t${entry.at}\t${entry.action}\t${entry.chars} chars\t${entry.hash.slice(0, 12)}`;
|
|
1583
|
+
const lines = [`Content versions for "${name}" (oldest first, ${groups.content.length} of at most the retained count):`, ...groups.content.map(row)];
|
|
1584
|
+
if (groups.support.length > 0) lines.push("Support-file versions (history only — undo restores SKILL.md, not these bytes):", ...groups.support.map(row));
|
|
1585
|
+
return ok(lines.join("\n"));
|
|
1586
|
+
}
|
|
1587
|
+
if (input.startsWith("skill undo ")) {
|
|
1588
|
+
const rest = input.slice(11).trim();
|
|
1589
|
+
const toMatch = /--to\s+v?(\d+)\s*$/.exec(rest);
|
|
1590
|
+
const name = (toMatch?.index === void 0 ? rest : rest.slice(0, toMatch.index)).trim();
|
|
1591
|
+
if (!name) return err("Usage: /evolution skill undo <name> [--to v<N>]");
|
|
1592
|
+
const skillUndoRefusal = unreplayableWriteRefusal("skill undo");
|
|
1593
|
+
if (skillUndoRefusal) return skillUndoRefusal;
|
|
1594
|
+
const curator = ctx.get("evolutionCurator");
|
|
1595
|
+
const target = toMatch?.[1] === void 0 ? void 0 : Number(toMatch[1]);
|
|
1596
|
+
const result = curator ? await (target === void 0 ? curator.undo(name) : curator.undo(name, target)) : {
|
|
1597
|
+
ok: false,
|
|
1598
|
+
message: errorText("e-302-curator-service-not-mounted")
|
|
1599
|
+
};
|
|
1600
|
+
return result.ok ? ok(result.message) : err(result.message);
|
|
1601
|
+
}
|
|
1602
|
+
if (input === "skills health") {
|
|
1603
|
+
const curator = ctx.get("evolutionCurator");
|
|
1604
|
+
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1605
|
+
const [rows, observed] = await Promise.all([curator.healthView(), curator.usageObserved()]);
|
|
1606
|
+
const banner = observed ? "" : "Usage observation not yet established — churn (write-ghost) rows are suppressed.";
|
|
1607
|
+
const line = (row) => `${row.verdict.padEnd(18)} ${row.name}${row.reasons.length > 0 ? ` — ${row.reasons.join("; ")}` : ""}`;
|
|
1608
|
+
if (rows.length === 0) return ok(banner ? `Structure health: all skills healthy. ${banner}` : "Structure health: all skills healthy.");
|
|
1609
|
+
return ok([banner ? `Structure health (${rows.length} degraded): ${banner}` : `Structure health (${rows.length} degraded):`, ...rows.map(line)].join("\n"));
|
|
1610
|
+
}
|
|
1611
|
+
if (input === "skills refresh") {
|
|
1612
|
+
ctx.emit("evolution/skills-refresh");
|
|
1613
|
+
return ok("Skill catalog refresh requested: caches dropped, catalog re-read on next lookup.");
|
|
1614
|
+
}
|
|
1615
|
+
if (input === "learn" || input.startsWith("learn ")) {
|
|
1616
|
+
const request = rawInputTrimmed === "learn" ? "" : rawInputTrimmed.slice(6).trim();
|
|
1617
|
+
const message = createUserMessage({
|
|
1618
|
+
content: [{
|
|
1619
|
+
type: "text",
|
|
1620
|
+
text: buildLearnPrompt(request)
|
|
1621
|
+
}],
|
|
1622
|
+
source: {
|
|
1623
|
+
kind: "evolution-commands",
|
|
1624
|
+
form: "notice",
|
|
1625
|
+
summary: "learn request"
|
|
1626
|
+
}
|
|
1362
1627
|
});
|
|
1363
|
-
const
|
|
1364
|
-
|
|
1365
|
-
|
|
1628
|
+
const missingAgent = agentMissing("learn");
|
|
1629
|
+
if (missingAgent) return missingAgent;
|
|
1630
|
+
const agent = invocationAgent;
|
|
1631
|
+
let woke = false;
|
|
1632
|
+
if (typeof agent.followup === "function") {
|
|
1633
|
+
agent.followup(message);
|
|
1634
|
+
woke = true;
|
|
1635
|
+
} else if (typeof agent.inject === "function") agent.inject(message);
|
|
1636
|
+
else return err(errorText("e-305-the-invocation-agent-exposes"));
|
|
1637
|
+
const eventIo = ctx.get("evolutionIo")?.provider();
|
|
1638
|
+
if (eventIo) appendEvolutionEvent(eventIo, eventsFile(evolutionRoot()), {
|
|
1639
|
+
type: "learn",
|
|
1640
|
+
source: "manual",
|
|
1641
|
+
...request ? { request } : {}
|
|
1642
|
+
}).catch((error) => {
|
|
1643
|
+
ctx.logger.warn(`evolution-commands: failed to record learn event: ${String(error)}`);
|
|
1366
1644
|
});
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
};
|
|
1376
|
-
const
|
|
1377
|
-
const
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
if (input.startsWith("reject ")) {
|
|
1406
|
-
const id = input.slice(7).trim();
|
|
1407
|
-
const result = approval ? await approval.reject(id) : {
|
|
1408
|
-
ok: false,
|
|
1409
|
-
message: errorText("e-301-approval-service-not-mounted-2")
|
|
1410
|
-
};
|
|
1411
|
-
return result.ok ? ok(result.message) : err(result.message);
|
|
1412
|
-
}
|
|
1413
|
-
if (input.startsWith("release ")) {
|
|
1414
|
-
const id = input.slice(8).trim();
|
|
1415
|
-
if (!approval) return err(errorText("e-301-approval-service-not-mounted-2"));
|
|
1416
|
-
if (!approval.release) return err(errorText("e-304-the-mounted-approval-service"));
|
|
1417
|
-
const result = await approval.release(id);
|
|
1418
|
-
return result.ok ? ok(result.message) : err(result.message);
|
|
1419
|
-
}
|
|
1420
|
-
if (input === "curator run") {
|
|
1421
|
-
const curator = ctx.get("evolutionCurator");
|
|
1422
|
-
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1423
|
-
const result = await curator.run({ ignoreGates: true });
|
|
1424
|
-
if (result.skipped !== void 0) return ok(`Curator run skipped (${result.skipped}): no curation pass was executed.\nrunId=${result.report.runId}`);
|
|
1425
|
-
return ok(`Curator run complete: ${result.stale.length} stale, ${result.archived.length} archived, ${result.errors.length} failed.\nrunId=${result.report.runId}${result.report.snapshotPath ? `\nsnapshot=${result.report.snapshotPath}` : ""}`);
|
|
1426
|
-
}
|
|
1427
|
-
if (input === "curator pause" || input === "curator resume") {
|
|
1428
|
-
const curator = ctx.get("evolutionCurator");
|
|
1429
|
-
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1430
|
-
const paused = input === "curator pause";
|
|
1431
|
-
await curator.setPaused(paused);
|
|
1432
|
-
const notPersisted = ctx.get("evolutionState") !== void 0 ? "" : "\nNOTE: no evolution state service is mounted — this setting is NOT persisted and will not survive this process.";
|
|
1433
|
-
return ok((paused ? "Curator automatic curation paused. Manual /evolution curator run is unaffected; resume with /evolution curator resume." : "Curator automatic curation resumed. The next scheduled pass waits one interval (first-run defer semantics).") + notPersisted);
|
|
1434
|
-
}
|
|
1435
|
-
if (input === "curator status") {
|
|
1436
|
-
const curator = ctx.get("evolutionCurator");
|
|
1437
|
-
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1438
|
-
const state = await curator.status();
|
|
1439
|
-
if (!state) return ok("No curator state yet: the first automatic pass is deferred until the interval elapses.");
|
|
1440
|
-
const lastRun = typeof state.lastRunAt === "number" && Number.isFinite(state.lastRunAt) && state.lastRunAt > 0 ? new Date(state.lastRunAt).toISOString() : "unknown";
|
|
1441
|
-
return ok([
|
|
1442
|
-
`paused=${state.paused}`,
|
|
1443
|
-
`runs=${state.runCount}`,
|
|
1444
|
-
`lastRun=${lastRun}`,
|
|
1445
|
-
`summary=${state.lastSummary}`
|
|
1446
|
-
].join("\n"));
|
|
1447
|
-
}
|
|
1448
|
-
if (input === "mutations") {
|
|
1449
|
-
const curator = ctx.get("evolutionCurator");
|
|
1450
|
-
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1451
|
-
const records = await curator.skills.listMutations();
|
|
1452
|
-
const usable = Array.isArray(records) ? records.filter((row) => typeof row === "object" && row !== null && typeof row.at === "string" && typeof row.skillName === "string" && typeof row.action === "string" && typeof row.summary === "string") : [];
|
|
1453
|
-
if (usable.length === 0) return ok("No mutation records yet.");
|
|
1454
|
-
const recent = usable.slice(-5).reverse().map((record) => `${record.at.slice(0, 19)} ${record.skillName} ${record.action} ${record.summary}`);
|
|
1455
|
-
return ok(`Mutations: ${usable.length} recorded (recent 5):\n${recent.join("\n")}`);
|
|
1456
|
-
}
|
|
1457
|
-
if (input === "curator scope") {
|
|
1458
|
-
const curator = ctx.get("evolutionCurator");
|
|
1459
|
-
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1460
|
-
const view = await curator.scopeView();
|
|
1461
|
-
const line = (label, names) => `${label}: ${names.length}${names.length === 0 ? "" : `\n ${names.join(", ")}`}`;
|
|
1462
|
-
return ok([
|
|
1463
|
-
`Lifecycle scope at ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`,
|
|
1464
|
-
line("Managed (may transition)", view.managed),
|
|
1465
|
-
line("Watched (stale / quality-warned)", view.watched),
|
|
1466
|
-
line("Quality-warned", view.qualityWarned),
|
|
1467
|
-
line("Exempted (exclude / referenced)", view.exempted),
|
|
1468
|
-
line("Protected (pinned / bundled / hub / builtin)", view.protected)
|
|
1469
|
-
].join("\n"));
|
|
1470
|
-
}
|
|
1471
|
-
if (input === "curator report") {
|
|
1472
|
-
const curator = ctx.get("evolutionCurator");
|
|
1473
|
-
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1474
|
-
const report = await curator.latestReport();
|
|
1475
|
-
if (!report) return ok("No curator report available.");
|
|
1476
|
-
if (typeof report !== "object" || typeof report.runId !== "string" || typeof report.startedAt !== "string" || !Array.isArray(report.archived) || !Array.isArray(report.failed)) return err("Report file unreadable.");
|
|
1477
|
-
const reportAny = report;
|
|
1478
|
-
const nameOf = (item) => typeof item === "object" && item !== null && typeof item.name === "string" ? item.name : "?";
|
|
1479
|
-
const reasonOf = (item) => typeof item === "object" && item !== null && typeof item.reason === "string" ? item.reason : "?";
|
|
1480
|
-
const aborted = typeof reportAny.aborted === "string" ? reportAny.aborted : void 0;
|
|
1481
|
-
const unattributed = Array.isArray(reportAny.unattributed) ? reportAny.unattributed.length : 0;
|
|
1482
|
-
return ok([
|
|
1483
|
-
`runId=${reportAny.runId}`,
|
|
1484
|
-
`startedAt=${reportAny.startedAt}`,
|
|
1485
|
-
`archived=${reportAny.archived.map(nameOf).join(", ") || "(none)"}`,
|
|
1486
|
-
`failed=${reportAny.failed.map((item) => `${nameOf(item)}: ${reasonOf(item)}`).join(", ") || "(none)"}`,
|
|
1487
|
-
...aborted === void 0 ? [] : [`aborted=${aborted}`],
|
|
1488
|
-
...unattributed === 0 ? [] : [`unattributed=${unattributed}`]
|
|
1489
|
-
].join("\n"));
|
|
1490
|
-
}
|
|
1491
|
-
if (input === "restore" || input.startsWith("restore ")) {
|
|
1492
|
-
const tail = input.slice(7).trim();
|
|
1493
|
-
if (tail !== "") return err(`\`restore\` takes no arguments (got "${tail}"). Use \`restore\` for a whole-tree rollback, or \`skill restore <name>\` for one skill.`);
|
|
1494
|
-
const restoreRefusal = unreplayableWriteRefusal("restore");
|
|
1495
|
-
if (restoreRefusal) return restoreRefusal;
|
|
1496
|
-
const curator = ctx.get("evolutionCurator");
|
|
1497
|
-
const result = curator ? await curator.restoreSnapshot() : {
|
|
1498
|
-
ok: false,
|
|
1499
|
-
message: errorText("e-302-curator-service-not-mounted")
|
|
1500
|
-
};
|
|
1501
|
-
return result.ok ? ok(result.message) : err(result.message);
|
|
1502
|
-
}
|
|
1503
|
-
if (input.startsWith("consolidate ")) {
|
|
1504
|
-
const consolidateRefusal = unreplayableWriteRefusal("consolidate");
|
|
1505
|
-
if (consolidateRefusal) return consolidateRefusal;
|
|
1506
|
-
const planTail = /\s--plan\s+(\S+)\s*$/.exec(input) ?? null;
|
|
1507
|
-
const planRunId = planTail?.[1] ?? void 0;
|
|
1508
|
-
const [target, ...sources] = (planTail ? input.slice(0, planTail.index) : input).slice(12).trim().split(/\s+/).filter(Boolean);
|
|
1509
|
-
if (!target || sources.length === 0) return err("Usage: /evolution consolidate <target> <source...>");
|
|
1510
|
-
const curator = ctx.get("evolutionCurator");
|
|
1511
|
-
const result = curator ? await curator.consolidate(target, sources) : {
|
|
1512
|
-
ok: false,
|
|
1513
|
-
message: errorText("e-302-curator-service-not-mounted")
|
|
1514
|
-
};
|
|
1515
|
-
if (!result.ok) return err(result.message);
|
|
1516
|
-
return ok(planRunId ? `${result.message}\n[audit] plan=${planRunId}` : result.message);
|
|
1517
|
-
}
|
|
1518
|
-
if (input.startsWith("skill restore ")) {
|
|
1519
|
-
const name = input.slice(14).trim();
|
|
1520
|
-
if (!name) return err("Usage: /evolution skill restore <name>");
|
|
1521
|
-
const skillRestoreRefusal = unreplayableWriteRefusal("skill restore");
|
|
1522
|
-
if (skillRestoreRefusal) return skillRestoreRefusal;
|
|
1523
|
-
const curator = ctx.get("evolutionCurator");
|
|
1524
|
-
const result = curator ? await curator.restore(name) : {
|
|
1525
|
-
ok: false,
|
|
1526
|
-
message: errorText("e-302-curator-service-not-mounted")
|
|
1527
|
-
};
|
|
1528
|
-
return result.ok ? ok(result.message) : err(result.message);
|
|
1529
|
-
}
|
|
1530
|
-
if (input.startsWith("skill history ")) {
|
|
1531
|
-
const name = input.slice(14).trim();
|
|
1532
|
-
if (!name) return err("Usage: /evolution skill history <name>");
|
|
1533
|
-
const curator = ctx.get("evolutionCurator");
|
|
1534
|
-
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1535
|
-
const versions = await curator.history(name);
|
|
1536
|
-
if (versions.length === 0) return ok(`No content versions are recorded for "${name}" yet — they appear after the next write through skill_manage.`);
|
|
1537
|
-
const groups = partitionVersions(versions);
|
|
1538
|
-
const row = (entry) => `v${entry.v}\t${entry.at}\t${entry.action}\t${entry.chars} chars\t${entry.hash.slice(0, 12)}`;
|
|
1539
|
-
const lines = [`Content versions for "${name}" (oldest first, ${groups.content.length} of at most the retained count):`, ...groups.content.map(row)];
|
|
1540
|
-
if (groups.support.length > 0) lines.push("Support-file versions (history only — undo restores SKILL.md, not these bytes):", ...groups.support.map(row));
|
|
1541
|
-
return ok(lines.join("\n"));
|
|
1542
|
-
}
|
|
1543
|
-
if (input.startsWith("skill undo ")) {
|
|
1544
|
-
const rest = input.slice(11).trim();
|
|
1545
|
-
const toMatch = /--to\s+v?(\d+)\s*$/.exec(rest);
|
|
1546
|
-
const name = (toMatch?.index === void 0 ? rest : rest.slice(0, toMatch.index)).trim();
|
|
1547
|
-
if (!name) return err("Usage: /evolution skill undo <name> [--to v<N>]");
|
|
1548
|
-
const skillUndoRefusal = unreplayableWriteRefusal("skill undo");
|
|
1549
|
-
if (skillUndoRefusal) return skillUndoRefusal;
|
|
1550
|
-
const curator = ctx.get("evolutionCurator");
|
|
1551
|
-
const target = toMatch?.[1] === void 0 ? void 0 : Number(toMatch[1]);
|
|
1552
|
-
const result = curator ? await (target === void 0 ? curator.undo(name) : curator.undo(name, target)) : {
|
|
1553
|
-
ok: false,
|
|
1554
|
-
message: errorText("e-302-curator-service-not-mounted")
|
|
1555
|
-
};
|
|
1556
|
-
return result.ok ? ok(result.message) : err(result.message);
|
|
1557
|
-
}
|
|
1558
|
-
if (input === "skills health") {
|
|
1559
|
-
const curator = ctx.get("evolutionCurator");
|
|
1560
|
-
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1561
|
-
const [rows, observed] = await Promise.all([curator.healthView(), curator.usageObserved()]);
|
|
1562
|
-
const banner = observed ? "" : "Usage observation not yet established — churn (write-ghost) rows are suppressed.";
|
|
1563
|
-
const line = (row) => `${row.verdict.padEnd(18)} ${row.name}${row.reasons.length > 0 ? ` — ${row.reasons.join("; ")}` : ""}`;
|
|
1564
|
-
if (rows.length === 0) return ok(banner ? `Structure health: all skills healthy. ${banner}` : "Structure health: all skills healthy.");
|
|
1565
|
-
return ok([banner ? `Structure health (${rows.length} degraded): ${banner}` : `Structure health (${rows.length} degraded):`, ...rows.map(line)].join("\n"));
|
|
1566
|
-
}
|
|
1567
|
-
if (input === "skills refresh") {
|
|
1568
|
-
ctx.emit("evolution/skills-refresh");
|
|
1569
|
-
return ok("Skill catalog refresh requested: caches dropped, catalog re-read on next lookup.");
|
|
1570
|
-
}
|
|
1571
|
-
if (input === "learn" || input.startsWith("learn ")) {
|
|
1572
|
-
const request = rawInputTrimmed === "learn" ? "" : rawInputTrimmed.slice(6).trim();
|
|
1573
|
-
const message = createUserMessage({
|
|
1574
|
-
content: [{
|
|
1575
|
-
type: "text",
|
|
1576
|
-
text: buildLearnPrompt(request)
|
|
1577
|
-
}],
|
|
1578
|
-
source: {
|
|
1579
|
-
kind: "evolution-commands",
|
|
1580
|
-
form: "notice",
|
|
1581
|
-
summary: "learn request"
|
|
1582
|
-
}
|
|
1583
|
-
});
|
|
1584
|
-
const missingAgent = agentMissing("learn");
|
|
1585
|
-
if (missingAgent) return missingAgent;
|
|
1586
|
-
const agent = invocationAgent;
|
|
1587
|
-
let woke = false;
|
|
1588
|
-
if (typeof agent.followup === "function") {
|
|
1589
|
-
agent.followup(message);
|
|
1590
|
-
woke = true;
|
|
1591
|
-
} else if (typeof agent.inject === "function") agent.inject(message);
|
|
1592
|
-
else return err(errorText("e-305-the-invocation-agent-exposes"));
|
|
1593
|
-
const eventIo = ctx.get("evolutionIo")?.provider();
|
|
1594
|
-
if (eventIo) appendEvolutionEvent(eventIo, eventsFile(evolutionRoot()), {
|
|
1595
|
-
type: "learn",
|
|
1596
|
-
source: "manual",
|
|
1597
|
-
...request ? { request } : {}
|
|
1598
|
-
}).catch((error) => {
|
|
1599
|
-
ctx.logger.warn(`evolution-commands: failed to record learn event: ${String(error)}`);
|
|
1600
|
-
});
|
|
1601
|
-
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.");
|
|
1602
|
-
}
|
|
1603
|
-
if (input === "maintain --facts") {
|
|
1604
|
-
const ioRegistry = ctx.get("evolutionIo");
|
|
1605
|
-
if (!ioRegistry) return err("Evolution IO registry not mounted — maintenance facts unavailable.");
|
|
1645
|
+
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.");
|
|
1646
|
+
}
|
|
1647
|
+
if (input === "maintain --facts") {
|
|
1648
|
+
const ioRegistry = ctx.get("evolutionIo");
|
|
1649
|
+
if (!ioRegistry) return err("Evolution IO registry not mounted — maintenance facts unavailable.");
|
|
1650
|
+
const library = newSkillLibrary({
|
|
1651
|
+
config: { root: skillsRootValue },
|
|
1652
|
+
io: ioRegistry.provider()
|
|
1653
|
+
});
|
|
1654
|
+
const enrichment = await buildEnrichment(ctx, library);
|
|
1655
|
+
const readFailures = [];
|
|
1656
|
+
const snapshots = await snapshotFromLibrary(library, {
|
|
1657
|
+
...enrichmentSnapshotOptions(enrichment),
|
|
1658
|
+
onReadError: (name, error) => {
|
|
1659
|
+
readFailures.push(name);
|
|
1660
|
+
ctx.logger.warn(`evolution-maintenance: skipping unreadable skill "${name}": ${error instanceof Error ? error.message : String(error)}`);
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
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`);
|
|
1664
|
+
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`);
|
|
1665
|
+
if (snapshots.length === 0) return ok("Maintenance scan: empty skill library. Nothing to do.");
|
|
1666
|
+
const { facts } = buildMaintainFacts(snapshots, enrichment.usageObservedValue, void 0);
|
|
1667
|
+
return ok(`Maintenance facts (0-token preview):\n${facts}`);
|
|
1668
|
+
}
|
|
1669
|
+
const maintainArgs = /^maintain(?:\s+--timeout[ =](\d+))?\s*$/.exec(input);
|
|
1670
|
+
if (maintainArgs) {
|
|
1671
|
+
const runTimeoutMs = maintainArgs[1] ? Number(maintainArgs[1]) : config.maintainTimeoutMs ?? 6e5;
|
|
1672
|
+
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.`);
|
|
1673
|
+
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.`);
|
|
1674
|
+
const cooldownMs = clampedNumber(config.maintainCooldownMs, 3e4, { min: 0 });
|
|
1675
|
+
const sinceLast = Date.now() - lastMaintainAt;
|
|
1676
|
+
if (cooldownMs > 0 && sinceLast < cooldownMs) return err(`Maintenance cooldown active (${Math.ceil((cooldownMs - sinceLast) / 1e3)}s)${lastMaintainRunId === "" ? "" : ` — latest scan ${lastMaintainRunId}`}; re-running now would spend another model call.`);
|
|
1677
|
+
const ioRegistry = ctx.get("evolutionIo");
|
|
1678
|
+
const subagents = ctx.get("subagents");
|
|
1679
|
+
if (!ioRegistry) return err("Evolution IO registry not mounted — maintenance scan unavailable.");
|
|
1680
|
+
if (!subagents) return err("Subagents service not mounted — maintenance scan unavailable.");
|
|
1681
|
+
try {
|
|
1682
|
+
maintainInFlightSince = Date.now();
|
|
1606
1683
|
const library = newSkillLibrary({
|
|
1607
1684
|
config: { root: skillsRootValue },
|
|
1608
1685
|
io: ioRegistry.provider()
|
|
1609
1686
|
});
|
|
1610
1687
|
const enrichment = await buildEnrichment(ctx, library);
|
|
1611
|
-
const
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1688
|
+
const outcome = await runMaintain({
|
|
1689
|
+
library,
|
|
1690
|
+
subagents,
|
|
1691
|
+
parent: invocation.agent,
|
|
1692
|
+
evolutionPolicy: { get: () => ctx.get("evolutionPolicy")?.get() },
|
|
1693
|
+
tools: { get: (name) => ctx.get("tools")?.get(name) },
|
|
1694
|
+
logger: { warn: (message) => {
|
|
1695
|
+
ctx.logger.warn(message);
|
|
1696
|
+
} },
|
|
1697
|
+
rootExists: skillsRootValue === "" ? void 0 : () => ioRegistry.provider().exists(skillsRootValue),
|
|
1698
|
+
skillRoot: skillsRootValue
|
|
1699
|
+
}, {
|
|
1700
|
+
timeoutMs: runTimeoutMs,
|
|
1701
|
+
signal: invocation.signal,
|
|
1702
|
+
enrichment: () => enrichment
|
|
1703
|
+
});
|
|
1704
|
+
if (outcome.ok) lastMaintainRunId = outcome.runId ?? "";
|
|
1705
|
+
if (!outcome.ok) return err(outcome.error ?? "Maintenance scan failed.");
|
|
1706
|
+
appendEvolutionEvent(ioRegistry.provider(), eventsFile(evolutionRoot()), {
|
|
1707
|
+
type: "maintain",
|
|
1708
|
+
source: "manual",
|
|
1709
|
+
runId: outcome.runId,
|
|
1710
|
+
verdict: outcome.verdict,
|
|
1711
|
+
recommendations: outcome.recommendationCount
|
|
1712
|
+
}).catch((error) => {
|
|
1713
|
+
ctx.logger.warn(`evolution-commands: failed to record maintain event: ${String(error)}`);
|
|
1625
1714
|
});
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1715
|
+
return ok(`Maintenance scan ${outcome.runId}:\n${outcome.text ?? ""}`);
|
|
1716
|
+
} catch (error) {
|
|
1717
|
+
return err(`Maintenance scan failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1718
|
+
} finally {
|
|
1719
|
+
maintainInFlightSince = 0;
|
|
1720
|
+
lastMaintainAt = Date.now();
|
|
1631
1721
|
}
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
if (cooldownMs > 0 && sinceLast < cooldownMs) return err(`Maintenance cooldown active (${Math.ceil((cooldownMs - sinceLast) / 1e3)}s)${lastMaintainRunId === "" ? "" : ` — latest scan ${lastMaintainRunId}`}; re-running now would spend another model call.`);
|
|
1640
|
-
const ioRegistry = ctx.get("evolutionIo");
|
|
1641
|
-
const subagents = ctx.get("subagents");
|
|
1642
|
-
if (!ioRegistry) return err("Evolution IO registry not mounted — maintenance scan unavailable.");
|
|
1643
|
-
if (!subagents) return err("Subagents service not mounted — maintenance scan unavailable.");
|
|
1722
|
+
}
|
|
1723
|
+
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);
|
|
1724
|
+
const presetInstall = /^preset install(?: --base (\S+))?$/.exec(input);
|
|
1725
|
+
if (presetInstall !== null || /^preset\b/.test(input)) {
|
|
1726
|
+
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);
|
|
1727
|
+
{
|
|
1728
|
+
let installedBundles;
|
|
1644
1729
|
try {
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
config: { root: skillsRootValue },
|
|
1648
|
-
io: ioRegistry.provider()
|
|
1649
|
-
});
|
|
1650
|
-
const enrichment = await buildEnrichment(ctx, library);
|
|
1651
|
-
const outcome = await runMaintain({
|
|
1652
|
-
library,
|
|
1653
|
-
subagents,
|
|
1654
|
-
parent: invocation.agent,
|
|
1655
|
-
evolutionPolicy: { get: () => ctx.get("evolutionPolicy")?.get() },
|
|
1656
|
-
tools: { get: (name) => ctx.get("tools")?.get(name) },
|
|
1657
|
-
logger: { warn: (message) => {
|
|
1658
|
-
ctx.logger.warn(message);
|
|
1659
|
-
} },
|
|
1660
|
-
rootExists: skillsRootValue === "" ? void 0 : () => ioRegistry.provider().exists(skillsRootValue),
|
|
1661
|
-
skillRoot: skillsRootValue
|
|
1662
|
-
}, {
|
|
1663
|
-
timeoutMs: runTimeoutMs,
|
|
1664
|
-
signal: invocation.signal,
|
|
1665
|
-
descriptions: () => enrichment.descriptions,
|
|
1666
|
-
supportFiles: () => enrichment.supportFiles,
|
|
1667
|
-
quality: () => enrichment.quality,
|
|
1668
|
-
protected: () => enrichment.protected,
|
|
1669
|
-
catalogInvalid: () => enrichment.catalogInvalid,
|
|
1670
|
-
usageObserved: () => enrichment.usageObservedValue,
|
|
1671
|
-
demand: () => enrichment.demand,
|
|
1672
|
-
liveness: () => enrichment.liveness,
|
|
1673
|
-
supportChars: () => enrichment.supportChars
|
|
1674
|
-
});
|
|
1675
|
-
if (outcome.ok) lastMaintainRunId = outcome.runId ?? "";
|
|
1676
|
-
if (!outcome.ok) return err(outcome.error ?? "Maintenance scan failed.");
|
|
1677
|
-
appendEvolutionEvent(ioRegistry.provider(), eventsFile(evolutionRoot()), {
|
|
1678
|
-
type: "maintain",
|
|
1679
|
-
source: "manual",
|
|
1680
|
-
runId: outcome.runId,
|
|
1681
|
-
verdict: outcome.verdict,
|
|
1682
|
-
recommendations: outcome.recommendationCount
|
|
1683
|
-
}).catch((error) => {
|
|
1684
|
-
ctx.logger.warn(`evolution-commands: failed to record maintain event: ${String(error)}`);
|
|
1730
|
+
installedBundles = collectEvolutionBundles(evolutionRoot(), (error) => {
|
|
1731
|
+
throw error;
|
|
1685
1732
|
});
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
return err(`Maintenance scan failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1689
|
-
} finally {
|
|
1690
|
-
maintainInFlightSince = 0;
|
|
1691
|
-
lastMaintainAt = Date.now();
|
|
1733
|
+
} catch (enumerationError) {
|
|
1734
|
+
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.`);
|
|
1692
1735
|
}
|
|
1736
|
+
const allBundle = installedBundles.find((name) => name === "dsh-evolution-all" || name.endsWith("/dsh-evolution-all"));
|
|
1737
|
+
const presetBundle = installedBundles.find((name) => name === "dsh-evolution-preset" || name.endsWith("/dsh-evolution-preset"));
|
|
1738
|
+
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).`);
|
|
1693
1739
|
}
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
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.`);
|
|
1706
|
-
}
|
|
1707
|
-
const allBundle = installedBundles.find((name) => name === "dsh-evolution-all" || name.endsWith("/dsh-evolution-all"));
|
|
1708
|
-
const presetBundle = installedBundles.find((name) => name === "dsh-evolution-preset" || name.endsWith("/dsh-evolution-preset"));
|
|
1709
|
-
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).`);
|
|
1740
|
+
try {
|
|
1741
|
+
const source = resolveAgentPresetDir(import.meta.url);
|
|
1742
|
+
const table = readAgentPresetBases(source);
|
|
1743
|
+
const requestedRaw = presetInstall[1] ?? table.defaultBase;
|
|
1744
|
+
const requested = [...new Set(requestedRaw.split(",").map((part) => part.trim()).filter((part) => part !== ""))];
|
|
1745
|
+
if (requested.length === 0) return err(`No agent-preset base named — ${join(source, "bases.json")} lists ${table.entries.map((entry) => entry.name).join(", ")}`);
|
|
1746
|
+
const selected = [];
|
|
1747
|
+
for (const name of requested) {
|
|
1748
|
+
const entry = table.entries.find((candidate) => candidate.name === name);
|
|
1749
|
+
if (entry === void 0) return err(`Unknown agent-preset base "${name}" — ${join(source, "bases.json")} lists ${table.entries.map((candidate) => candidate.name).join(", ")}`);
|
|
1750
|
+
selected.push(entry);
|
|
1710
1751
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
const
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
const written = [];
|
|
1731
|
-
for (const base of selected) {
|
|
1732
|
-
if (typeof base.unsupported === "string") return err(`Base "${base.name}" is registered as UNSUPPORTED: ${base.unsupported}`);
|
|
1733
|
-
const requiredService = base.requires?.service;
|
|
1734
|
-
if (typeof requiredService === "string" && ctx.get(requiredService) === void 0) return err(`Base "${base.name}" requires the "${requiredService}" service, which this deployment does not provide — the generated preset would refuse to mount.`);
|
|
1735
|
-
const basePatch = resolvePresetBasePatch(base.name, profile);
|
|
1736
|
-
const rowId = presetRowId(base.id);
|
|
1737
|
-
const entryText = composePresetEntry(readFileSync(basePatch, "utf8"), delta, {
|
|
1738
|
-
rowId,
|
|
1739
|
-
id: base.id,
|
|
1740
|
-
name: base.display.name,
|
|
1741
|
-
description: base.display.description,
|
|
1742
|
-
order: base.display.order
|
|
1743
|
-
});
|
|
1744
|
-
const next = mergePresetRow(merged, entryText, rowId);
|
|
1745
|
-
written.push(next === merged ? `${base.name} → ${profile.patchPath} (already current)` : `${base.name} → ${rowId} in ${profile.patchPath}`);
|
|
1746
|
-
merged = next;
|
|
1747
|
-
}
|
|
1748
|
-
if (merged !== current) atomicWriteFiles(dirname(profile.patchPath), [{
|
|
1749
|
-
name: PROFILE_PATCH_FILENAME$1,
|
|
1750
|
-
content: presetPatchText(merged)
|
|
1751
|
-
}], void 0, (message) => {
|
|
1752
|
-
ctx.logger.warn(message);
|
|
1752
|
+
const deltaPath = join(source, "agent.cordis.yml");
|
|
1753
|
+
if (!existsSync(deltaPath)) return err(`Preset file missing from ${source} — is the dsh-evolution-agent-preset package installed?`);
|
|
1754
|
+
const profile = presetProfileTarget(ctx);
|
|
1755
|
+
if (profile === void 0) return err("No profile is mounted in this process, so there is no profile patch to write the preset row into — run the command in a session started by `dsh --profile <name>`.");
|
|
1756
|
+
const delta = readFileSync(deltaPath, "utf8");
|
|
1757
|
+
const current = existsSync(profile.patchPath) ? readFileSync(profile.patchPath, "utf8") : "";
|
|
1758
|
+
let merged = current;
|
|
1759
|
+
const written = [];
|
|
1760
|
+
for (const base of selected) {
|
|
1761
|
+
const refusal = baseRefusalReason(base, (name) => ctx.get(name) !== void 0);
|
|
1762
|
+
if (refusal !== void 0) return err(refusal);
|
|
1763
|
+
const basePatch = resolvePresetBasePatch(base.name, profile);
|
|
1764
|
+
const rowId = presetRowId(base.id);
|
|
1765
|
+
const entryText = composePresetEntry(readFileSync(basePatch, "utf8"), delta, {
|
|
1766
|
+
rowId,
|
|
1767
|
+
id: base.id,
|
|
1768
|
+
name: base.display.name,
|
|
1769
|
+
description: base.display.description,
|
|
1770
|
+
order: base.display.order
|
|
1753
1771
|
});
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1772
|
+
const next = mergePresetRow(merged, entryText, rowId);
|
|
1773
|
+
written.push(next === merged ? `${base.name} → ${profile.patchPath} (already current)` : `${base.name} → ${rowId} in ${profile.patchPath}`);
|
|
1774
|
+
merged = next;
|
|
1757
1775
|
}
|
|
1776
|
+
if (merged !== current) atomicWriteFiles(dirname(profile.patchPath), [{
|
|
1777
|
+
name: PROFILE_PATCH_FILENAME,
|
|
1778
|
+
content: presetPatchText(merged)
|
|
1779
|
+
}], void 0, (message) => {
|
|
1780
|
+
ctx.logger.warn(message);
|
|
1781
|
+
});
|
|
1782
|
+
return ok(`Evolution agent preset row installed (${written.join("; ")}). Restart the session switcher to select it.`);
|
|
1783
|
+
} catch (error) {
|
|
1784
|
+
return err(`Preset install failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1758
1785
|
}
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
}
|
|
1789
|
-
origin: "foreground",
|
|
1790
|
-
libraryOrigin: "foreground"
|
|
1786
|
+
}
|
|
1787
|
+
if (input.startsWith("restructure ")) {
|
|
1788
|
+
const planTail = /\s--plan\s+(\S+)\s*$/.exec(rawInputTrimmed) ?? null;
|
|
1789
|
+
const planRunId = planTail?.[1] ?? void 0;
|
|
1790
|
+
const rest = planTail ? rawInputTrimmed.slice(0, planTail.index) : rawInputTrimmed;
|
|
1791
|
+
const match = /^restructure\s+(\S+)\s+"([^"]+)"\s+(\S+)$/.exec(rest);
|
|
1792
|
+
if (!match) return err("Usage: /evolution restructure <name> \"<## heading>\" <to_file>");
|
|
1793
|
+
const name = match[1] ?? "";
|
|
1794
|
+
const heading = match[2] ?? "";
|
|
1795
|
+
const toFile = match[3] ?? "";
|
|
1796
|
+
if (!toFile.startsWith("references/")) return err("to_file must live under references/ (log/detail destination).");
|
|
1797
|
+
const ioRegistry = ctx.get("evolutionIo");
|
|
1798
|
+
if (!ioRegistry) return err("Evolution IO registry not mounted — restructure unavailable.");
|
|
1799
|
+
if (approval) {
|
|
1800
|
+
const session = invocationAgent?.session;
|
|
1801
|
+
const sessionPolicy = effectiveSessionPolicy(ctx, session);
|
|
1802
|
+
const stagesForeground = approval.isEnabled !== false && sessionPolicy !== "never" && approval.stageForeground !== false;
|
|
1803
|
+
if (stagesForeground && session === void 0) return err(errorText("e-306-this-deployment-stages-foreground-2"));
|
|
1804
|
+
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.");
|
|
1805
|
+
const decision = await approval.request({
|
|
1806
|
+
kind: "skill",
|
|
1807
|
+
summary: `/evolution restructure ${name}${planRunId ? ` (plan ${planRunId})` : ""}`,
|
|
1808
|
+
args: {
|
|
1809
|
+
operation: {
|
|
1810
|
+
action: "restructure",
|
|
1811
|
+
name,
|
|
1812
|
+
restructure: [{
|
|
1813
|
+
heading,
|
|
1814
|
+
to_file: toFile
|
|
1815
|
+
}]
|
|
1791
1816
|
},
|
|
1792
1817
|
origin: "foreground",
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
io: ioRegistry.provider(),
|
|
1804
|
-
ctx,
|
|
1805
|
-
threatExemptLabels: config.threatExemptLabels,
|
|
1806
|
-
limits: {
|
|
1807
|
-
...DEFAULT_SKILL_LIMITS,
|
|
1808
|
-
...policyStageLimits(ctx.get("evolutionPolicy")?.get?.())
|
|
1809
|
-
}
|
|
1810
|
-
}).restructure(name, [{
|
|
1811
|
-
heading,
|
|
1812
|
-
toFile
|
|
1813
|
-
}], "foreground");
|
|
1814
|
-
if (!result.ok) return err(result.message);
|
|
1815
|
-
await ctx.get("skillUsage")?.record?.(name, "patch");
|
|
1816
|
-
return ok(planRunId ? `${result.message}\n[audit] plan=${planRunId}` : result.message);
|
|
1817
|
-
}
|
|
1818
|
-
if (input === "replay") {
|
|
1819
|
-
const replay = ctx.get("evolutionReplay");
|
|
1820
|
-
if (!replay) return err(errorText("e-303-replay-service-not-mounted"));
|
|
1821
|
-
return ok(replay.compare().report);
|
|
1822
|
-
}
|
|
1823
|
-
const paramsMatch = /^params(?: --group ([a-z-]+))?(?: --json)?$/.exec(input);
|
|
1824
|
-
if (paramsMatch) {
|
|
1825
|
-
const provider = ctx.get("settings");
|
|
1826
|
-
const sections = /* @__PURE__ */ new Map();
|
|
1827
|
-
if (provider?.describe !== void 0) try {
|
|
1828
|
-
for (const descriptor of provider.describe({ redactSecrets: false })) {
|
|
1829
|
-
const section = {};
|
|
1830
|
-
if (descriptor.user !== void 0) section.user = descriptor.user;
|
|
1831
|
-
const resolved = descriptor.value;
|
|
1832
|
-
if (typeof resolved === "object" && resolved !== null) section.value = resolved;
|
|
1833
|
-
sections.set(descriptor.ns, section);
|
|
1834
|
-
}
|
|
1835
|
-
} catch (error) {
|
|
1836
|
-
return err(errorText("e-307-the-settings-service-could", { a1: error instanceof Error ? error.message : String(error) }));
|
|
1837
|
-
}
|
|
1838
|
-
const rows = paramSurfaceRows(sections, PARAM_EXPOSURE);
|
|
1839
|
-
const group = paramsMatch[1];
|
|
1840
|
-
const selected = group === void 0 ? rows : rows.filter((row) => row.group === group);
|
|
1841
|
-
if (group !== void 0 && selected.length === 0) return err(errorText("e-308-unknown-parameter-group-group", {
|
|
1842
|
-
a1: group,
|
|
1843
|
-
a2: paramGroups(PARAM_EXPOSURE).join(", ")
|
|
1844
|
-
}));
|
|
1845
|
-
if (input.endsWith("--json")) return ok(renderParamJson(selected));
|
|
1846
|
-
return ok(renderParamRows(selected, { providerMounted: provider !== void 0 }));
|
|
1818
|
+
libraryOrigin: "foreground"
|
|
1819
|
+
},
|
|
1820
|
+
origin: "foreground",
|
|
1821
|
+
...session !== void 0 ? {
|
|
1822
|
+
sessionId: session.id,
|
|
1823
|
+
session
|
|
1824
|
+
} : {},
|
|
1825
|
+
...sessionPolicy !== void 0 ? { sessionPolicy } : {}
|
|
1826
|
+
});
|
|
1827
|
+
if (decision.action === "staged") return ok(`${decision.message}${planRunId ? `\n[audit] plan=${planRunId}` : ""}`);
|
|
1847
1828
|
}
|
|
1848
|
-
const
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
id = canonicalWriteId(rawId);
|
|
1857
|
-
} catch (error) {
|
|
1858
|
-
return err(errorText("e-312-error-instanceof-Error-error", { a1: error instanceof Error ? error.message : String(error) }));
|
|
1829
|
+
const result = await newSkillLibrary({
|
|
1830
|
+
config: { root: skillsRootValue },
|
|
1831
|
+
io: ioRegistry.provider(),
|
|
1832
|
+
ctx,
|
|
1833
|
+
threatExemptLabels: config.threatExemptLabels,
|
|
1834
|
+
limits: {
|
|
1835
|
+
...DEFAULT_SKILL_LIMITS,
|
|
1836
|
+
...policyStageLimits(ctx.get("evolutionPolicy")?.get?.())
|
|
1859
1837
|
}
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
const expectedRaw = policySetMatch[3];
|
|
1885
|
-
const expected = expectedRaw === void 0 ? descriptor.revision : Number(expectedRaw);
|
|
1886
|
-
if (expectedRaw !== void 0 && descriptor.revision !== void 0 && Number(expectedRaw) !== descriptor.revision) return err(errorText("e-309-revision-conflict-you-sent", {
|
|
1887
|
-
a1: expectedRaw,
|
|
1888
|
-
a2: descriptor.revision
|
|
1889
|
-
}));
|
|
1890
|
-
const value = parseParamValue(rawValue);
|
|
1891
|
-
try {
|
|
1892
|
-
await provider.update(namespace, { [id]: value }, expected);
|
|
1893
|
-
} catch (error) {
|
|
1894
|
-
const code = error?.code;
|
|
1895
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1896
|
-
if (code === "SETTINGS_CONFLICT") return err(errorText("e-309-revision-conflict-message-Re", { a1: message }));
|
|
1897
|
-
return err(errorText("e-310-the-settings-service-refused", { a1: message }));
|
|
1838
|
+
}).restructure(name, [{
|
|
1839
|
+
heading,
|
|
1840
|
+
toFile
|
|
1841
|
+
}], "foreground");
|
|
1842
|
+
if (!result.ok) return err(result.message);
|
|
1843
|
+
await ctx.get("skillUsage")?.record?.(name, "patch");
|
|
1844
|
+
return ok(planRunId ? `${result.message}\n[audit] plan=${planRunId}` : result.message);
|
|
1845
|
+
}
|
|
1846
|
+
if (input === "replay") {
|
|
1847
|
+
const replay = ctx.get("evolutionReplay");
|
|
1848
|
+
if (!replay) return err(errorText("e-303-replay-service-not-mounted"));
|
|
1849
|
+
return ok(replay.compare().report);
|
|
1850
|
+
}
|
|
1851
|
+
const paramsMatch = /^params(?: --group ([a-z-]+))?(?: --json)?$/.exec(input);
|
|
1852
|
+
if (paramsMatch) {
|
|
1853
|
+
const provider = ctx.get("settings");
|
|
1854
|
+
const sections = /* @__PURE__ */ new Map();
|
|
1855
|
+
if (provider?.describe !== void 0) try {
|
|
1856
|
+
for (const descriptor of provider.describe({ redactSecrets: false })) {
|
|
1857
|
+
const section = {};
|
|
1858
|
+
if (descriptor.user !== void 0) section.user = descriptor.user;
|
|
1859
|
+
const resolved = descriptor.value;
|
|
1860
|
+
if (typeof resolved === "object" && resolved !== null) section.value = resolved;
|
|
1861
|
+
sections.set(descriptor.ns, section);
|
|
1898
1862
|
}
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
namespace,
|
|
1902
|
-
applies: entry.applies,
|
|
1903
|
-
before: descriptor.user?.[id] ?? resolved?.[id],
|
|
1904
|
-
after: value,
|
|
1905
|
-
wasOverridden: descriptor.user !== void 0 && Object.hasOwn(descriptor.user, id)
|
|
1906
|
-
}));
|
|
1863
|
+
} catch (error) {
|
|
1864
|
+
return err(errorText("e-307-the-settings-service-could", { a1: error instanceof Error ? error.message : String(error) }));
|
|
1907
1865
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1866
|
+
const rows = paramSurfaceRows(sections, PARAM_EXPOSURE);
|
|
1867
|
+
const group = paramsMatch[1];
|
|
1868
|
+
const selected = group === void 0 ? rows : rows.filter((row) => row.group === group);
|
|
1869
|
+
if (group !== void 0 && selected.length === 0) return err(errorText("e-308-unknown-parameter-group-group", {
|
|
1870
|
+
a1: group,
|
|
1871
|
+
a2: paramGroups(PARAM_EXPOSURE).join(", ")
|
|
1872
|
+
}));
|
|
1873
|
+
if (input.endsWith("--json")) return ok(renderParamJson(selected));
|
|
1874
|
+
return ok(renderParamRows(selected, { providerMounted: provider !== void 0 }));
|
|
1875
|
+
}
|
|
1876
|
+
const policySetMatch = /^policy set ([A-Za-z][A-Za-z0-9.-]*) (.+?)(?: --expect (\d+))?$/.exec(input);
|
|
1877
|
+
if (policySetMatch) {
|
|
1878
|
+
const provider = ctx.get("settings");
|
|
1879
|
+
if (provider?.update === void 0 || provider.describe === void 0) return err(errorText("e-311-no-settings-service-is"));
|
|
1880
|
+
const rawId = policySetMatch[1] ?? "";
|
|
1881
|
+
const rawValue = policySetMatch[2] ?? "";
|
|
1882
|
+
let id;
|
|
1883
|
+
try {
|
|
1884
|
+
id = canonicalWriteId(rawId);
|
|
1885
|
+
} catch (error) {
|
|
1886
|
+
return err(errorText("e-312-error-instanceof-Error-error", { a1: error instanceof Error ? error.message : String(error) }));
|
|
1916
1887
|
}
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1888
|
+
const entry = PARAM_EXPOSURE.find((candidate) => candidate.id === id);
|
|
1889
|
+
if (entry === void 0) return err(errorText("e-313-unknown-parameter-id-evolution", { a1: id }));
|
|
1890
|
+
if (entry.tier !== "E3") return err(errorText("e-314-id-is-a-deployment", {
|
|
1891
|
+
a1: id,
|
|
1892
|
+
a2: entry.tier,
|
|
1893
|
+
a3: entry.owner
|
|
1894
|
+
}));
|
|
1895
|
+
const namespace = paramSettingsId(entry.owner);
|
|
1896
|
+
if (namespace === void 0) return err(errorText("e-315-id-has-no-user", {
|
|
1897
|
+
a1: id,
|
|
1898
|
+
a2: entry.owner
|
|
1899
|
+
}));
|
|
1900
|
+
let descriptor;
|
|
1901
|
+
try {
|
|
1902
|
+
descriptor = provider.describe({ redactSecrets: false }).find((candidate) => candidate.ns === namespace);
|
|
1903
|
+
} catch (error) {
|
|
1904
|
+
return err(errorText("e-307-the-settings-service-could-2", { a1: error instanceof Error ? error.message : String(error) }));
|
|
1921
1905
|
}
|
|
1922
|
-
return
|
|
1923
|
-
|
|
1924
|
-
|
|
1906
|
+
if (descriptor === void 0) return err(errorText("e-316-namespace-namespace-is-not", {
|
|
1907
|
+
a1: namespace,
|
|
1908
|
+
a2: entry.owner
|
|
1909
|
+
}));
|
|
1910
|
+
const current = descriptor.value;
|
|
1911
|
+
const resolved = typeof current === "object" && current !== null ? current : void 0;
|
|
1912
|
+
const expectedRaw = policySetMatch[3];
|
|
1913
|
+
const expected = expectedRaw === void 0 ? descriptor.revision : Number(expectedRaw);
|
|
1914
|
+
if (expectedRaw !== void 0 && descriptor.revision !== void 0 && Number(expectedRaw) !== descriptor.revision) return err(errorText("e-309-revision-conflict-you-sent", {
|
|
1915
|
+
a1: expectedRaw,
|
|
1916
|
+
a2: descriptor.revision
|
|
1917
|
+
}));
|
|
1918
|
+
const value = parseParamValue(rawValue);
|
|
1919
|
+
try {
|
|
1920
|
+
await provider.update(namespace, { [id]: value }, expected);
|
|
1921
|
+
} catch (error) {
|
|
1922
|
+
const code = error?.code;
|
|
1923
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1924
|
+
if (code === "SETTINGS_CONFLICT") return err(errorText("e-309-revision-conflict-message-Re", { a1: message }));
|
|
1925
|
+
return err(errorText("e-310-the-settings-service-refused", { a1: message }));
|
|
1926
|
+
}
|
|
1927
|
+
return ok(renderPolicySet({
|
|
1928
|
+
id,
|
|
1929
|
+
namespace,
|
|
1930
|
+
applies: entry.applies,
|
|
1931
|
+
before: descriptor.user?.[id] ?? resolved?.[id],
|
|
1932
|
+
after: value,
|
|
1933
|
+
wasOverridden: descriptor.user !== void 0 && Object.hasOwn(descriptor.user, id)
|
|
1934
|
+
}));
|
|
1935
|
+
}
|
|
1936
|
+
if (input === "migrate") {
|
|
1937
|
+
const attempt = await migrateFromContext(ctx);
|
|
1938
|
+
if (!attempt.ok) {
|
|
1939
|
+
if (attempt.problem === "settings") return err(errorText("e-311-no-settings-service-is"));
|
|
1940
|
+
if (attempt.problem === "home") return err(errorText("e-320-the-profile-home-is-not"));
|
|
1941
|
+
return err(errorText("e-321-no-io-provider-is"));
|
|
1942
|
+
}
|
|
1943
|
+
return ok(renderNamespaceMigration(attempt.outcome));
|
|
1944
|
+
}
|
|
1945
|
+
if (input === "doctor" || input === "doctor --json") {
|
|
1946
|
+
const report = await diagnose(ctx, { home: evolutionRoot() });
|
|
1947
|
+
if (input === "doctor --json") return ok(JSON.stringify(report, null, 2));
|
|
1948
|
+
return ok(renderDoctorText(report));
|
|
1949
|
+
}
|
|
1950
|
+
if (input === "" || input === "help") return ok(`自进化:记忆、技能、会话回顾、技能整理——状态、待批准写入与维护。\n${renderHelpText()}`);
|
|
1951
|
+
return err(`/evolution ${input} is not a subcommand this build answers (a known subcommand whose argument is missing lands here too).\n${renderHint()}`);
|
|
1952
|
+
} };
|
|
1953
|
+
/** The registered definition: declared members only (A46). */
|
|
1954
|
+
const evolutionCommand = {
|
|
1955
|
+
name: "evolution",
|
|
1956
|
+
description: "自进化:状态与待批准写入",
|
|
1957
|
+
recordInput: false,
|
|
1958
|
+
input: { hint: renderHint() },
|
|
1959
|
+
handler: (invocation) => commandBody.run(invocation).catch((error) => ({
|
|
1925
1960
|
kind: "error",
|
|
1926
1961
|
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.`
|
|
1927
1962
|
}))
|
|
@@ -1929,22 +1964,44 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1929
1964
|
commandCtx.effect(() => commands.register(evolutionCommand));
|
|
1930
1965
|
});
|
|
1931
1966
|
let migrationRunning = false;
|
|
1967
|
+
/**
|
|
1968
|
+
* A trigger that arrived while an attempt was in flight (T4-12/A54).
|
|
1969
|
+
*
|
|
1970
|
+
* The guard used to SWALLOW it, and the comment above claimed a failed attempt "re-arms the
|
|
1971
|
+
* guard for the next trigger" — but both triggers are lifecycle moments that have already
|
|
1972
|
+
* passed by then, so there was no next trigger: one failed attempt (a slow or unreadable legacy
|
|
1973
|
+
* document is enough) ended the automatic migration for that boot, leaving only a warning that
|
|
1974
|
+
* suggests running the command by hand. The trigger is queued here and consumed at the attempt's
|
|
1975
|
+
* settle point instead.
|
|
1976
|
+
*/
|
|
1977
|
+
let migrationQueued = false;
|
|
1978
|
+
/** The attempt failed: re-arm the guard, and run the queued trigger NOW (its only chance left). */
|
|
1979
|
+
const settleFailure = () => {
|
|
1980
|
+
migrationRunning = false;
|
|
1981
|
+
if (!migrationQueued) return;
|
|
1982
|
+
migrationQueued = false;
|
|
1983
|
+
runMigrationOnce();
|
|
1984
|
+
};
|
|
1932
1985
|
const runMigrationOnce = () => {
|
|
1933
|
-
if (migrationRunning)
|
|
1986
|
+
if (migrationRunning) {
|
|
1987
|
+
migrationQueued = true;
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1934
1990
|
migrationRunning = true;
|
|
1935
1991
|
(async () => {
|
|
1936
1992
|
try {
|
|
1937
1993
|
const attempt = await migrateFromContext(ctx);
|
|
1938
1994
|
if (!attempt.ok) {
|
|
1939
|
-
migrationRunning = false;
|
|
1940
1995
|
ctx.logger.warn("evolution-commands: the legacy settings migration cannot run here (no %s) — run /evolution migrate after that service is mounted", attempt.problem);
|
|
1996
|
+
settleFailure();
|
|
1941
1997
|
return;
|
|
1942
1998
|
}
|
|
1943
1999
|
const { outcome } = attempt;
|
|
1944
2000
|
if (outcome.source !== void 0 && outcome.report !== void 0 && outcome.report.written.length > 0) ctx.logger.info("evolution-commands: migrated the legacy settings document (%s)", renderNamespaceMigration(outcome));
|
|
2001
|
+
migrationQueued = false;
|
|
1945
2002
|
} catch (error) {
|
|
1946
|
-
migrationRunning = false;
|
|
1947
2003
|
ctx.logger.warn("evolution-commands: the legacy settings migration did not run — %s", error instanceof Error ? error.message : String(error));
|
|
2004
|
+
settleFailure();
|
|
1948
2005
|
}
|
|
1949
2006
|
})();
|
|
1950
2007
|
};
|