@lmzhen/dsh-evolution-commands 0.17.1 → 0.18.1

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 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$1 = "cordis.patch.yml";
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$1),
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; it is removed in 0.7.0)`);
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 patch file name (`packages/boot/app-boot/src/profile.ts`,
553
- * `PROFILE_PATCH_FILENAME`). */
554
- const PROFILE_PATCH_FILENAME = "cordis.patch.yml";
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
- /** The bundle rows one profile manifest mounts (scope-agnostic, as written).
581
- * @param profileDir - the profile directory.
582
- * @returns the bundle names; empty when the manifest is missing or torn.
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 profileBundles(profileDir) {
616
+ function readTextOrEmpty(path) {
585
617
  try {
586
- const bundles = JSON.parse(readFileSync(join(profileDir, "package.json"), "utf8")).dsh?.profile?.bundles;
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
- try {
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
- /** Whether a base's `requires.service` precondition is met by one profile's bundles — the same
604
- * judgement the installer applies (`baseUnavailableReason`), read from the same rows.
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 bundles - the profile's bundle names.
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, bundles) {
610
- if (typeof base.unsupported === "string") return false;
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 profile patch
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 profile's own patch layer, so this probe reads that
620
- * file (`PROFILE_PATCH_FILENAME`) in every profile of the home. A probe narrowed to one base id
621
- * leaves a `--base ptc|cordis` install invisible: installForm then degrades to `none`/`host` and
622
- * the action ladder recommends installing `evolution-all` on top — the exact double-mount the
623
- * doctor exists to prevent.
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 { dir } of profileDirectories(home)) {
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, bundles)) continue;
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 presetFreshness = probePresetFreshness(home);
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 evolutionCommand = {
1352
- name: "evolution",
1353
- description: "自进化:状态与待批准写入",
1354
- recordInput: false,
1355
- input: { hint: renderHint() },
1356
- run: async (invocation) => {
1357
- const rawInputTrimmed = invocation.rawInput.trim();
1358
- const input = rawInputTrimmed.replace(/\s+/g, " ");
1359
- const ok = (text) => ({
1360
- kind: "success",
1361
- text
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 err = (text) => ({
1364
- kind: "error",
1365
- text
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
- const invocationAgent = invocation.agent;
1368
- const agentMissing = (need) => invocationAgent === void 0 ? err(errorText("e-305-this-invocation-carries-no", { a1: need })) : void 0;
1369
- const unreplayableWriteRefusal = (what) => {
1370
- if (approval === void 0) return void 0;
1371
- const session = invocationAgent?.session;
1372
- const sessionPolicy = effectiveSessionPolicy(ctx, session);
1373
- if (!(approval.isEnabled !== false && sessionPolicy !== "never" && approval.stageForeground !== false)) return void 0;
1374
- return err(errorText("e-306-this-deployment-stages-foreground", { a1: what }));
1375
- };
1376
- const approval = ctx.get("evolutionApproval");
1377
- const pendingMatch = /^pending(?: --detail)?$/.exec(input);
1378
- if (pendingMatch) {
1379
- if (!approval) return err(errorText("e-301-approval-service-not-mounted"));
1380
- const listed = [...await approval.list("pending"), ...await approval.list("executing")];
1381
- const pending = [...new Map(listed.map((row) => [row.id, row])).values()];
1382
- if (pending.length === 0) return ok("No pending evolution writes.");
1383
- const detailed = pendingMatch[0] === "pending --detail";
1384
- const nowMs = Date.now();
1385
- const attribution = (p) => {
1386
- const age = elapsedSince(p.createdAt, nowMs);
1387
- const unit = age.n === 1 ? age.unit.replace(/s$/u, "") : age.unit;
1388
- 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)})`}`;
1389
- };
1390
- return ok(pending.map((p) => {
1391
- if (!detailed) return `${p.id} ${p.kind} ${p.status === "executing" ? "EXECUTING " : ""}${p.summary}${attribution(p)}`;
1392
- const status = p.status === "executing" ? "EXECUTING" : p.status;
1393
- const line = `${p.id} ${p.kind} ${status} ${p.summary}${attribution(p)}`;
1394
- return p.args === void 0 ? line : `${line}\n staged args: ${safeStagedArgs(p.args)}`;
1395
- }).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)" : ""));
1396
- }
1397
- if (input.startsWith("approve ")) {
1398
- const id = input.slice(8).trim();
1399
- const result = approval ? await approval.approve(id) : {
1400
- ok: false,
1401
- message: errorText("e-301-approval-service-not-mounted-2")
1402
- };
1403
- return result.ok ? ok(result.message) : err(result.message);
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 readFailures = [];
1612
- const snapshots = await snapshotFromLibrary(library, {
1613
- descriptions: enrichment.descriptions,
1614
- supportFiles: enrichment.supportFiles,
1615
- quality: enrichment.quality,
1616
- protected: enrichment.protected,
1617
- catalogInvalid: enrichment.catalogInvalid,
1618
- demand: enrichment.demand,
1619
- liveness: enrichment.liveness,
1620
- supportChars: enrichment.supportChars,
1621
- onReadError: (name, error) => {
1622
- readFailures.push(name);
1623
- ctx.logger.warn(`evolution-maintenance: skipping unreadable skill "${name}": ${error instanceof Error ? error.message : String(error)}`);
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
- 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`);
1627
- 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`);
1628
- if (snapshots.length === 0) return ok("Maintenance scan: empty skill library. Nothing to do.");
1629
- const { facts } = buildMaintainFacts(snapshots, enrichment.usageObservedValue, void 0);
1630
- return ok(`Maintenance facts (0-token preview):\n${facts}`);
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
- const maintainArgs = /^maintain(?:\s+--timeout[ =](\d+))?\s*$/.exec(input);
1633
- if (maintainArgs) {
1634
- const runTimeoutMs = maintainArgs[1] ? Number(maintainArgs[1]) : config.maintainTimeoutMs ?? 6e5;
1635
- 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.`);
1636
- 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.`);
1637
- const cooldownMs = clampedNumber(config.maintainCooldownMs, 3e4, { min: 0 });
1638
- const sinceLast = Date.now() - lastMaintainAt;
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
- maintainInFlightSince = Date.now();
1646
- const library = newSkillLibrary({
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
- return ok(`Maintenance scan ${outcome.runId}:\n${outcome.text ?? ""}`);
1687
- } catch (error) {
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
- 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);
1695
- const presetInstall = /^preset install(?: --base (\S+))?$/.exec(input);
1696
- if (presetInstall !== null || /^preset\b/.test(input)) {
1697
- 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);
1698
- {
1699
- let installedBundles;
1700
- try {
1701
- installedBundles = collectEvolutionBundles(evolutionRoot(), (error) => {
1702
- throw error;
1703
- });
1704
- } catch (enumerationError) {
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
- try {
1712
- const source = resolveAgentPresetDir(import.meta.url);
1713
- const table = readAgentPresetBases(source);
1714
- const requestedRaw = presetInstall[1] ?? table.defaultBase;
1715
- const requested = [...new Set(requestedRaw.split(",").map((part) => part.trim()).filter((part) => part !== ""))];
1716
- if (requested.length === 0) return err(`No agent-preset base named — ${join(source, "bases.json")} lists ${table.entries.map((entry) => entry.name).join(", ")}`);
1717
- const selected = [];
1718
- for (const name of requested) {
1719
- const entry = table.entries.find((candidate) => candidate.name === name);
1720
- if (entry === void 0) return err(`Unknown agent-preset base "${name}" — ${join(source, "bases.json")} lists ${table.entries.map((candidate) => candidate.name).join(", ")}`);
1721
- selected.push(entry);
1722
- }
1723
- const deltaPath = join(source, "agent.cordis.yml");
1724
- if (!existsSync(deltaPath)) return err(`Preset file missing from ${source} — is the dsh-evolution-agent-preset package installed?`);
1725
- const profile = presetProfileTarget(ctx);
1726
- 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>`.");
1727
- const delta = readFileSync(deltaPath, "utf8");
1728
- const current = existsSync(profile.patchPath) ? readFileSync(profile.patchPath, "utf8") : "";
1729
- let merged = current;
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
- return ok(`Evolution agent preset row installed (${written.join("; ")}). Restart the session switcher to select it.`);
1755
- } catch (error) {
1756
- return err(`Preset install failed: ${error instanceof Error ? error.message : String(error)}`);
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
- if (input.startsWith("restructure ")) {
1760
- const planTail = /\s--plan\s+(\S+)\s*$/.exec(rawInputTrimmed) ?? null;
1761
- const planRunId = planTail?.[1] ?? void 0;
1762
- const rest = planTail ? rawInputTrimmed.slice(0, planTail.index) : rawInputTrimmed;
1763
- const match = /^restructure\s+(\S+)\s+"([^"]+)"\s+(\S+)$/.exec(rest);
1764
- if (!match) return err("Usage: /evolution restructure <name> \"<## heading>\" <to_file>");
1765
- const name = match[1] ?? "";
1766
- const heading = match[2] ?? "";
1767
- const toFile = match[3] ?? "";
1768
- if (!toFile.startsWith("references/")) return err("to_file must live under references/ (log/detail destination).");
1769
- const ioRegistry = ctx.get("evolutionIo");
1770
- if (!ioRegistry) return err("Evolution IO registry not mounted — restructure unavailable.");
1771
- if (approval) {
1772
- const session = invocationAgent?.session;
1773
- const sessionPolicy = effectiveSessionPolicy(ctx, session);
1774
- const stagesForeground = approval.isEnabled !== false && sessionPolicy !== "never" && approval.stageForeground !== false;
1775
- if (stagesForeground && session === void 0) return err(errorText("e-306-this-deployment-stages-foreground-2"));
1776
- 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.");
1777
- const decision = await approval.request({
1778
- kind: "skill",
1779
- summary: `/evolution restructure ${name}${planRunId ? ` (plan ${planRunId})` : ""}`,
1780
- args: {
1781
- operation: {
1782
- action: "restructure",
1783
- name,
1784
- restructure: [{
1785
- heading,
1786
- to_file: toFile
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
- ...session !== void 0 ? {
1794
- sessionId: session.id,
1795
- session
1796
- } : {},
1797
- ...sessionPolicy !== void 0 ? { sessionPolicy } : {}
1798
- });
1799
- if (decision.action === "staged") return ok(`${decision.message}${planRunId ? `\n[audit] plan=${planRunId}` : ""}`);
1800
- }
1801
- const result = await newSkillLibrary({
1802
- config: { root: skillsRootValue },
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 policySetMatch = /^policy set ([A-Za-z][A-Za-z0-9.-]*) (.+?)(?: --expect (\d+))?$/.exec(input);
1849
- if (policySetMatch) {
1850
- const provider = ctx.get("settings");
1851
- if (provider?.update === void 0 || provider.describe === void 0) return err(errorText("e-311-no-settings-service-is"));
1852
- const rawId = policySetMatch[1] ?? "";
1853
- const rawValue = policySetMatch[2] ?? "";
1854
- let id;
1855
- try {
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
- const entry = PARAM_EXPOSURE.find((candidate) => candidate.id === id);
1861
- if (entry === void 0) return err(errorText("e-313-unknown-parameter-id-evolution", { a1: id }));
1862
- if (entry.tier !== "E3") return err(errorText("e-314-id-is-a-deployment", {
1863
- a1: id,
1864
- a2: entry.tier,
1865
- a3: entry.owner
1866
- }));
1867
- const namespace = paramSettingsId(entry.owner);
1868
- if (namespace === void 0) return err(errorText("e-315-id-has-no-user", {
1869
- a1: id,
1870
- a2: entry.owner
1871
- }));
1872
- let descriptor;
1873
- try {
1874
- descriptor = provider.describe({ redactSecrets: false }).find((candidate) => candidate.ns === namespace);
1875
- } catch (error) {
1876
- return err(errorText("e-307-the-settings-service-could-2", { a1: error instanceof Error ? error.message : String(error) }));
1877
- }
1878
- if (descriptor === void 0) return err(errorText("e-316-namespace-namespace-is-not", {
1879
- a1: namespace,
1880
- a2: entry.owner
1881
- }));
1882
- const current = descriptor.value;
1883
- const resolved = typeof current === "object" && current !== null ? current : void 0;
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
- return ok(renderPolicySet({
1900
- id,
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
- if (input === "migrate") {
1909
- const attempt = await migrateFromContext(ctx);
1910
- if (!attempt.ok) {
1911
- if (attempt.problem === "settings") return err(errorText("e-311-no-settings-service-is"));
1912
- if (attempt.problem === "home") return err(errorText("e-320-the-profile-home-is-not"));
1913
- return err(errorText("e-321-no-io-provider-is"));
1914
- }
1915
- return ok(renderNamespaceMigration(attempt.outcome));
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
- if (input === "doctor" || input === "doctor --json") {
1918
- const report = await diagnose(ctx, { home: evolutionRoot() });
1919
- if (input === "doctor --json") return ok(JSON.stringify(report, null, 2));
1920
- return ok(renderDoctorText(report));
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 ok(`自进化:记忆、技能、会话回顾、技能整理——状态、待批准写入与维护。\n${renderHelpText()}`);
1923
- },
1924
- handler: (invocation) => evolutionCommand.run(invocation).catch((error) => ({
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) return;
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
  };