@lmzhen/dsh-evolution-commands 0.21.0 → 0.23.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 +459 -295
- package/lib/types/doctor.d.ts +7 -0
- package/lib/types/index.d.ts +2 -42
- package/lib/types/parse.d.ts +65 -0
- package/lib/types/profiles.d.ts +74 -0
- package/lib/types/registry.d.ts +8 -0
- package/package.json +7 -6
package/lib/index.js
CHANGED
|
@@ -2,9 +2,10 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { effectiveSessionPolicy } from "@lmzhen/dsh-evolution-approval";
|
|
4
4
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
5
|
-
import { DEFAULT_SKILL_LIMITS, MAX_TIMER_DELAY_MS, PARAM_EXPOSURE, PARAM_GROUP_LABELS, absentHostServices, appendEvolutionEvent, applyNamespaceMigration, assertSkillsRootAliasRetired, buildLearnPrompt, canonicalWriteId, clampedNumber, classifyNamespaceMigration, composePresetEntry, elapsedSince, errorText, eventsFile, evolutionHome, evolutionRoot, isDeprecatedParamId,
|
|
5
|
+
import { DEFAULT_SKILL_LIMITS, MAX_TIMER_DELAY_MS, PARAM_EXPOSURE, PARAM_GROUP_LABELS, absentHostServices, appendEvolutionEvent, applyNamespaceMigration, asRecord, assertSkillsRootAliasRetired, atomicWriteFiles, buildLearnPrompt, canonicalWriteId, clampedNumber, classifyNamespaceMigration, composePresetEntry, elapsedSince, errorText, eventsFile, evolutionHome, evolutionRoot, isDeprecatedParamId, isPresent, isRecord, isUnknown, liveBool, mergePresetRow, newRunRegistry, newSkillLibrary, paramSettingsId, partitionVersions, planNamespaceMigration, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, probeText, reportsSweepLockTarget, resolveBasePresetPatch, resolveParamId, resolveRootConfig, scopedProbeReport, sweepReports, transactIo } from "@lmzhen/dsh-evolution-core";
|
|
6
6
|
import { buildEnrichment, buildMaintainFacts, enrichmentSnapshotOptions, runMaintain, snapshotFromLibrary } from "@lmzhen/dsh-evolution-maintenance";
|
|
7
|
-
import {
|
|
7
|
+
import { preferProvider } from "@lmzhen/dsh-evolution-io";
|
|
8
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
8
9
|
import { dirname, join } from "node:path";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
10
11
|
//#region lib/types/migration.js
|
|
@@ -194,7 +195,7 @@ async function objectOf(read) {
|
|
|
194
195
|
if (read === void 0) return void 0;
|
|
195
196
|
try {
|
|
196
197
|
const value = await read();
|
|
197
|
-
return
|
|
198
|
+
return isRecord(value) ? value : void 0;
|
|
198
199
|
} catch {
|
|
199
200
|
return;
|
|
200
201
|
}
|
|
@@ -235,7 +236,7 @@ async function readPlatformView(ctx) {
|
|
|
235
236
|
*/
|
|
236
237
|
/** The patch file name inside a profile directory (`packages/boot/app-boot/src/profile.ts`,
|
|
237
238
|
* `PROFILE_PATCH_FILENAME`). */
|
|
238
|
-
const PROFILE_PATCH_FILENAME = "cordis.patch.yml";
|
|
239
|
+
const PROFILE_PATCH_FILENAME$1 = "cordis.patch.yml";
|
|
239
240
|
/**
|
|
240
241
|
* The profile patch this process writes into, or undefined outside a profile.
|
|
241
242
|
*
|
|
@@ -251,7 +252,7 @@ function presetProfileTarget(ctx) {
|
|
|
251
252
|
return {
|
|
252
253
|
profile: profile.name,
|
|
253
254
|
profileDir: profile.dir,
|
|
254
|
-
patchPath: typeof profile.patchPath === "string" && profile.patchPath !== "" ? profile.patchPath : join(profile.dir, PROFILE_PATCH_FILENAME),
|
|
255
|
+
patchPath: typeof profile.patchPath === "string" && profile.patchPath !== "" ? profile.patchPath : join(profile.dir, PROFILE_PATCH_FILENAME$1),
|
|
255
256
|
bundles: Array.isArray(profile.startedBundles) ? profile.startedBundles : []
|
|
256
257
|
};
|
|
257
258
|
}
|
|
@@ -314,6 +315,135 @@ function moduleGraphPath(specifier) {
|
|
|
314
315
|
return fileURLToPath(resolve(specifier));
|
|
315
316
|
}
|
|
316
317
|
//#endregion
|
|
318
|
+
//#region lib/types/profiles.js
|
|
319
|
+
/**
|
|
320
|
+
* The ONE reader of `<home>/profiles` (G3, 0.22).
|
|
321
|
+
*
|
|
322
|
+
* Two very different questions used to share one bare helper: diagnostics want EVERY profile under
|
|
323
|
+
* the home (doctor's cross-profile summary), while the preset-install exclusion must only ever look
|
|
324
|
+
* at THIS plane — a leftover profile from the other plane's home used to make the exclusion refuse a
|
|
325
|
+
* legitimate install (the two-plane split made that reachable: `~/.dsh/profiles/web` sat next
|
|
326
|
+
* to `profiles/desktop`). The directory is therefore read here and nowhere else:
|
|
327
|
+
* `scanProfiles` is the single `readdirSync` of it, `homeProfiles` answers the diagnostic
|
|
328
|
+
* question, and `installExclusionTarget` answers the write-path one — fail-closed, because an
|
|
329
|
+
* unreadable home must never read as "nothing is installed".
|
|
330
|
+
*
|
|
331
|
+
* @module evolution-commands/src/profiles
|
|
332
|
+
*/
|
|
333
|
+
/** The patch file name inside a profile directory (the platform's own spelling). */
|
|
334
|
+
const PROFILE_PATCH_FILENAME = "cordis.patch.yml";
|
|
335
|
+
/** The ONLY directory read of `<home>/profiles` in the family.
|
|
336
|
+
* `profiles/node_modules` is a platform-created directory that never carries a manifest (S2.1), dot-entries
|
|
337
|
+
* are not profiles, and a directory without `package.json` is not one either.
|
|
338
|
+
* @param home - the resolved DSH home.
|
|
339
|
+
* @returns the scanned entries, or the enumeration error (never a silent empty list).
|
|
340
|
+
*/
|
|
341
|
+
function scanProfiles(home) {
|
|
342
|
+
const root = join(home, "profiles");
|
|
343
|
+
if (!existsSync(root)) return {
|
|
344
|
+
kind: "scanned",
|
|
345
|
+
profiles: []
|
|
346
|
+
};
|
|
347
|
+
let entries;
|
|
348
|
+
try {
|
|
349
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
350
|
+
} catch (error) {
|
|
351
|
+
return {
|
|
352
|
+
kind: "unreadable",
|
|
353
|
+
error
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
const profiles = [];
|
|
357
|
+
for (const entry of entries) {
|
|
358
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
359
|
+
if (entry.name === "node_modules") continue;
|
|
360
|
+
const dir = join(root, entry.name);
|
|
361
|
+
if (!existsSync(join(dir, "package.json"))) continue;
|
|
362
|
+
profiles.push({
|
|
363
|
+
profile: entry.name,
|
|
364
|
+
dir
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
kind: "scanned",
|
|
369
|
+
profiles
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
/** Every profile directory under one home, by name — the DIAGNOSTIC question (doctor's cross-profile
|
|
373
|
+
* summary). An unreadable home answers the empty list here because doctor reports the failure on its
|
|
374
|
+
* own DEGRADED row; a write path must ask {@link installExclusionTarget} instead.
|
|
375
|
+
* @param home - the resolved DSH home.
|
|
376
|
+
* @returns one entry per profile directory carrying a manifest.
|
|
377
|
+
*/
|
|
378
|
+
function homeProfiles(home) {
|
|
379
|
+
const scan = scanProfiles(home);
|
|
380
|
+
return scan.kind === "scanned" ? scan.profiles : [];
|
|
381
|
+
}
|
|
382
|
+
/** The bundle names one profile's manifest declares.
|
|
383
|
+
* @param profileDir - the profile directory.
|
|
384
|
+
* @param onError - called with the read/parse failure; a torn manifest is NOT "no bundles".
|
|
385
|
+
* @returns the declared bundle names (empty when the manifest is absent).
|
|
386
|
+
*/
|
|
387
|
+
function profileBundles(profileDir, onError) {
|
|
388
|
+
try {
|
|
389
|
+
const bundles = JSON.parse(readFileSync(join(profileDir, "package.json"), "utf8")).dsh?.profile?.bundles;
|
|
390
|
+
return Array.isArray(bundles) ? bundles.filter((name) => typeof name === "string") : [];
|
|
391
|
+
} catch (error) {
|
|
392
|
+
onError?.(error);
|
|
393
|
+
return [];
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/** One optional file's text, or `''` when it is absent or unreadable.
|
|
397
|
+
* @param path - the file to read.
|
|
398
|
+
* @returns the text.
|
|
399
|
+
*/
|
|
400
|
+
function readTextOrEmpty(path) {
|
|
401
|
+
try {
|
|
402
|
+
return readFileSync(path, "utf8");
|
|
403
|
+
} catch {
|
|
404
|
+
return "";
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/** The profile's own patch text, or `''` when it has none.
|
|
408
|
+
* @param profileDir - the profile directory.
|
|
409
|
+
* @returns the patch text.
|
|
410
|
+
*/
|
|
411
|
+
function profilePatchText(profileDir) {
|
|
412
|
+
return readTextOrEmpty(join(profileDir, PROFILE_PATCH_FILENAME));
|
|
413
|
+
}
|
|
414
|
+
/** Where the preset-install exclusion must look: THIS plane only, or an explicit refusal.
|
|
415
|
+
*
|
|
416
|
+
* The write path does NOT scan the home: the mounted profile is the whole question (its started
|
|
417
|
+
* bundles plus the ones its own manifest declares — either can double-mount), and a question that
|
|
418
|
+
* cannot be answered still REFUSES. That is what makes a leftover profile from the other plane
|
|
419
|
+
* harmless here: it is not this plane's profile, so it is never consulted.
|
|
420
|
+
* @param mounted - the mounted profile, or undefined outside a profile (a bare library mount).
|
|
421
|
+
* @returns the profile to judge with its bundle rows, or the reason a refusal is mandatory.
|
|
422
|
+
*/
|
|
423
|
+
function installExclusionTarget(mounted) {
|
|
424
|
+
if (mounted === void 0 || mounted.dir === "") return {
|
|
425
|
+
kind: "undecidable",
|
|
426
|
+
reason: "no profile is mounted in this process, so the double-mount question cannot be answered here"
|
|
427
|
+
};
|
|
428
|
+
let manifestFailure;
|
|
429
|
+
const declared = profileBundles(mounted.dir, (error) => {
|
|
430
|
+
manifestFailure = error;
|
|
431
|
+
});
|
|
432
|
+
const manifestCode = manifestFailure?.code;
|
|
433
|
+
if (manifestFailure !== void 0 && manifestCode !== "ENOENT") return {
|
|
434
|
+
kind: "undecidable",
|
|
435
|
+
reason: "the mounted profile manifest could not be read"
|
|
436
|
+
};
|
|
437
|
+
return {
|
|
438
|
+
kind: "mounted",
|
|
439
|
+
profile: {
|
|
440
|
+
profile: mounted.profile,
|
|
441
|
+
dir: mounted.dir
|
|
442
|
+
},
|
|
443
|
+
bundles: [...mounted.bundles, ...declared]
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
//#endregion
|
|
317
447
|
//#region lib/types/doctor.js
|
|
318
448
|
/**
|
|
319
449
|
* `/evolution doctor` — read-only self-check (0.3.55, WB2).
|
|
@@ -450,7 +580,7 @@ function liveRowTails(plugins) {
|
|
|
450
580
|
* @returns the mapping, or undefined for anything else.
|
|
451
581
|
*/
|
|
452
582
|
function asMapping(value) {
|
|
453
|
-
return
|
|
583
|
+
return asRecord(value);
|
|
454
584
|
}
|
|
455
585
|
/** Profile bundle rows for evolution-family packages across all profiles.
|
|
456
586
|
*
|
|
@@ -460,22 +590,16 @@ function asMapping(value) {
|
|
|
460
590
|
* list alone, and treating it as "no bundles" is exactly the fail-open state
|
|
461
591
|
* the double-mount exclusion exists to prevent. */
|
|
462
592
|
function collectEvolutionBundles(home, onReadError) {
|
|
463
|
-
const
|
|
464
|
-
if (
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
try {
|
|
468
|
-
profileEntries = readdirSync(profilesDir, { withFileTypes: true });
|
|
469
|
-
} catch (error) {
|
|
470
|
-
onReadError?.(error, "profiles-dir");
|
|
471
|
-
return bundles;
|
|
593
|
+
const scan = scanProfiles(home);
|
|
594
|
+
if (scan.kind === "unreadable") {
|
|
595
|
+
onReadError?.(scan.error, "profiles-dir");
|
|
596
|
+
return [];
|
|
472
597
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
if (profile.name === "node_modules" || profile.name.startsWith(".")) continue;
|
|
598
|
+
const bundles = [];
|
|
599
|
+
for (const { dir } of scan.profiles) {
|
|
476
600
|
let raw;
|
|
477
601
|
try {
|
|
478
|
-
raw = readFileSync(join(
|
|
602
|
+
raw = readFileSync(join(dir, "package.json"), "utf8");
|
|
479
603
|
} catch (error) {
|
|
480
604
|
if (error?.code === "ENOENT") continue;
|
|
481
605
|
onReadError?.(error, "profile-manifest");
|
|
@@ -579,62 +703,6 @@ function readAgentPresetBases$1(path) {
|
|
|
579
703
|
return null;
|
|
580
704
|
}
|
|
581
705
|
}
|
|
582
|
-
/** The bundle rows one profile manifest mounts (scope-agnostic, as written) — the target a preset
|
|
583
|
-
* recompute resolves its platform base patch through.
|
|
584
|
-
* @param profileDir - the profile directory.
|
|
585
|
-
* @returns the bundle names; empty when the manifest is missing or torn.
|
|
586
|
-
*/
|
|
587
|
-
function profileBundles(profileDir) {
|
|
588
|
-
try {
|
|
589
|
-
const bundles = JSON.parse(readFileSync(join(profileDir, "package.json"), "utf8")).dsh?.profile?.bundles;
|
|
590
|
-
return Array.isArray(bundles) ? bundles.filter((name) => typeof name === "string") : [];
|
|
591
|
-
} catch {
|
|
592
|
-
return [];
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
/** Every profile directory under one DSH_HOME, by name.
|
|
596
|
-
* @param home - resolved DSH_HOME.
|
|
597
|
-
* @returns `{ profile, dir }` per directory under `profiles/` that carries a manifest.
|
|
598
|
-
*/
|
|
599
|
-
function profileDirectories(home) {
|
|
600
|
-
const out = [];
|
|
601
|
-
const root = join(home, "profiles");
|
|
602
|
-
let entries;
|
|
603
|
-
try {
|
|
604
|
-
entries = readdirSync(root, { withFileTypes: true });
|
|
605
|
-
} catch {
|
|
606
|
-
return out;
|
|
607
|
-
}
|
|
608
|
-
for (const entry of entries) {
|
|
609
|
-
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
610
|
-
if (entry.name === "node_modules") continue;
|
|
611
|
-
const dir = join(root, entry.name);
|
|
612
|
-
if (!existsSync(join(dir, "package.json"))) continue;
|
|
613
|
-
out.push({
|
|
614
|
-
profile: entry.name,
|
|
615
|
-
dir
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
return out;
|
|
619
|
-
}
|
|
620
|
-
/** One optional file's text, or `''` when it is absent or unreadable.
|
|
621
|
-
* @param path - the file to read.
|
|
622
|
-
* @returns the text.
|
|
623
|
-
*/
|
|
624
|
-
function readTextOrEmpty(path) {
|
|
625
|
-
try {
|
|
626
|
-
return readFileSync(path, "utf8");
|
|
627
|
-
} catch {
|
|
628
|
-
return "";
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
/** The profile patch text, or `''` when the profile has none.
|
|
632
|
-
* @param profileDir - the profile directory.
|
|
633
|
-
* @returns the patch text.
|
|
634
|
-
*/
|
|
635
|
-
function profilePatchText(profileDir) {
|
|
636
|
-
return readTextOrEmpty(join(profileDir, PROFILE_PATCH_FILENAME));
|
|
637
|
-
}
|
|
638
706
|
/** Every USER-layer patch text that can carry a family preset row: each profile's own patch, then
|
|
639
707
|
* the home layer (`<home>/cordis.patch.yml`), which the platform composes AFTER the profile layer
|
|
640
708
|
* (`readProfilePatches` in `packages/boot/app-boot/src/profile-context.ts`). A row written there
|
|
@@ -644,7 +712,7 @@ function profilePatchText(profileDir) {
|
|
|
644
712
|
* @returns the patch texts that exist, in profile-then-home order.
|
|
645
713
|
*/
|
|
646
714
|
function userPatchTexts(home) {
|
|
647
|
-
const texts =
|
|
715
|
+
const texts = homeProfiles(home).map(({ dir }) => profilePatchText(dir));
|
|
648
716
|
texts.push(readTextOrEmpty(join(home, PROFILE_PATCH_FILENAME)));
|
|
649
717
|
return texts.filter((text) => text !== "");
|
|
650
718
|
}
|
|
@@ -718,7 +786,7 @@ function probePresetFreshness(home, servicePresent) {
|
|
|
718
786
|
} catch (error) {
|
|
719
787
|
deltaFailure = `${deltaPath} could not be read (${error instanceof Error ? error.message : String(error)})`;
|
|
720
788
|
}
|
|
721
|
-
const profiles =
|
|
789
|
+
const profiles = homeProfiles(home);
|
|
722
790
|
if (bases === null) {
|
|
723
791
|
const tablePath = basesPath ?? `${AGENT_PRESET_PACKAGE}/bases.json`;
|
|
724
792
|
return profiles.flatMap(({ profile, dir }) => {
|
|
@@ -849,6 +917,7 @@ async function legacyMigrationRow(ctx, home) {
|
|
|
849
917
|
}
|
|
850
918
|
async function diagnose(ctx, options = {}) {
|
|
851
919
|
const home = options.home ?? evolutionRoot();
|
|
920
|
+
const mounted = options.mounted;
|
|
852
921
|
const conflicts = [];
|
|
853
922
|
const bundles = collectEvolutionBundles(home, (error, scope) => {
|
|
854
923
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -944,6 +1013,7 @@ async function diagnose(ctx, options = {}) {
|
|
|
944
1013
|
installForm,
|
|
945
1014
|
deploymentForm,
|
|
946
1015
|
bundles,
|
|
1016
|
+
foreignProfiles: homeProfiles(home).filter(({ profile, dir }) => mounted === void 0 || profile !== mounted.profile && dir !== mounted.dir).map(({ profile }) => profile),
|
|
947
1017
|
conflicts,
|
|
948
1018
|
envIssues: env,
|
|
949
1019
|
memoryIssues,
|
|
@@ -1102,6 +1172,7 @@ function renderDoctorText(report) {
|
|
|
1102
1172
|
lines.push(`preset: ${row.base} → ${row.destination} ${verdict}`);
|
|
1103
1173
|
}
|
|
1104
1174
|
if (report.presetIssues.length > 0) lines.push("agent presets:", ...report.presetIssues.map((line) => ` ! ${line}`));
|
|
1175
|
+
if (report.foreignProfiles.length > 0) lines.push("profiles-foreign: " + report.foreignProfiles.join(", ") + " — not part of this plane: /evolution preset install ignores them, while the bundles (all profiles) line above counts them");
|
|
1105
1176
|
if (report.conflicts.length > 0) lines.push("conflicts:", ...report.conflicts.map((line) => ` ! ${line}`));
|
|
1106
1177
|
if (report.envIssues.length > 0) lines.push("env:", ...report.envIssues.map((line) => ` ! ${line}`));
|
|
1107
1178
|
if (report.memoryIssues.length > 0) lines.push("memory:", ...report.memoryIssues.map((line) => ` ! ${line}`));
|
|
@@ -1218,9 +1289,24 @@ const SOURCE_LABELS = Object.freeze({
|
|
|
1218
1289
|
});
|
|
1219
1290
|
/** What the E-tiers mean, so the count line reads without the docs. */
|
|
1220
1291
|
const TIER_LEGEND = "档位:E0 固定/E1 只读/E2 安装时定/E3 你可改/E4 装完固化。";
|
|
1292
|
+
/**
|
|
1293
|
+
* The source column's word for one row (F-1).
|
|
1294
|
+
*
|
|
1295
|
+
* `unregistered` covers TWO different facts: the owner publishes no settings namespace (the value
|
|
1296
|
+
* really cannot be written — E-315/E-316) and the settings service itself is missing (then whether
|
|
1297
|
+
* the value could be written is UNKNOWN). Without a provider the column must not claim "不可改", or it
|
|
1298
|
+
* contradicts the legend and the footer of the very same output.
|
|
1299
|
+
* @param row - the row's surface facts.
|
|
1300
|
+
* @param providerMounted - whether a settings provider answered.
|
|
1301
|
+
* @returns the label for the source column.
|
|
1302
|
+
*/
|
|
1303
|
+
function sourceLabel(row, providerMounted) {
|
|
1304
|
+
const label = SOURCE_LABELS[row.source] ?? row.source;
|
|
1305
|
+
return providerMounted || row.source !== "unregistered" ? label : "未知";
|
|
1306
|
+
}
|
|
1221
1307
|
function renderParamRows(rows, options) {
|
|
1222
1308
|
const header = pad("参数", 36) + pad("分组", 12) + pad("档位", 6) + pad("生效", 8) + pad("来源", 10) + "当前值";
|
|
1223
|
-
const body = rows.map((row) => pad(row.id, 36) + pad(PARAM_GROUP_LABELS[row.group], 12) + pad(row.tier, 6) + pad(APPLIES_LABELS[row.applies] ?? row.applies, 8) + pad(
|
|
1309
|
+
const body = rows.map((row) => pad(row.id, 36) + pad(PARAM_GROUP_LABELS[row.group], 12) + pad(row.tier, 6) + pad(APPLIES_LABELS[row.applies] ?? row.applies, 8) + pad(sourceLabel(row, options.providerMounted), 10) + renderValue(row.value));
|
|
1224
1310
|
const tiers = [
|
|
1225
1311
|
"E0",
|
|
1226
1312
|
"E1",
|
|
@@ -1277,116 +1363,160 @@ function renderParamJson(rows) {
|
|
|
1277
1363
|
}
|
|
1278
1364
|
//#endregion
|
|
1279
1365
|
//#region lib/types/registry.js
|
|
1366
|
+
/** The flags the TABLE declares for one subcommand (name → `--flag`), DERIVED from its usage. */
|
|
1367
|
+
function flagsOf(name) {
|
|
1368
|
+
const entry = COMMAND_ENTRIES.find((candidate) => entryName(candidate) === name);
|
|
1369
|
+
return entry === void 0 ? [] : entryFlags(entry);
|
|
1370
|
+
}
|
|
1371
|
+
/** C7 shape, DERIVED: the subcommand name is the first token of `usage` (variants use `|`). */
|
|
1372
|
+
function entryName(entry) {
|
|
1373
|
+
const [head = ""] = entry.usage.split(" ");
|
|
1374
|
+
return head.split("|")[0] ?? "";
|
|
1375
|
+
}
|
|
1376
|
+
/** C7 shape, DERIVED: the flags a `usage` declares, in the form `--flag` (value flags keep their name). */
|
|
1377
|
+
function entryFlags(entry) {
|
|
1378
|
+
const found = [];
|
|
1379
|
+
for (const match of entry.usage.matchAll(/--[a-z][a-z-]*/g)) if (!found.includes(match[0])) found.push(match[0]);
|
|
1380
|
+
return found;
|
|
1381
|
+
}
|
|
1280
1382
|
const COMMAND_ENTRIES = [
|
|
1281
1383
|
{
|
|
1384
|
+
readonly: true,
|
|
1282
1385
|
usage: "pending [--detail]",
|
|
1283
1386
|
summary: "列出暂存的写入(--detail 显示参数)"
|
|
1284
1387
|
},
|
|
1285
1388
|
{
|
|
1389
|
+
readonly: false,
|
|
1286
1390
|
usage: "approve <id>",
|
|
1287
1391
|
summary: "执行一条已批准的写入"
|
|
1288
1392
|
},
|
|
1289
1393
|
{
|
|
1394
|
+
readonly: false,
|
|
1290
1395
|
usage: "reject <id>",
|
|
1291
1396
|
summary: "丢弃一条暂存写入,不执行"
|
|
1292
1397
|
},
|
|
1293
1398
|
{
|
|
1399
|
+
readonly: false,
|
|
1294
1400
|
usage: "release <id>",
|
|
1295
1401
|
summary: "把卡在执行中的写入放回待批窗口(先确认效果再批准 or reject)"
|
|
1296
1402
|
},
|
|
1297
1403
|
{
|
|
1404
|
+
readonly: true,
|
|
1298
1405
|
usage: "doctor [--json]",
|
|
1299
1406
|
summary: "只读自检:安装形态、冲突、环境、服务(--json 供脚本用)"
|
|
1300
1407
|
},
|
|
1301
1408
|
{
|
|
1409
|
+
readonly: false,
|
|
1302
1410
|
usage: "curator run|pause|resume|status|report|scope",
|
|
1303
1411
|
summary: "立即运行一次自动整理,或暂停/恢复/查看状态"
|
|
1304
1412
|
},
|
|
1305
1413
|
{
|
|
1414
|
+
readonly: true,
|
|
1306
1415
|
usage: "mutations",
|
|
1307
1416
|
summary: "列出技能改动记录"
|
|
1308
1417
|
},
|
|
1309
1418
|
{
|
|
1419
|
+
readonly: false,
|
|
1310
1420
|
usage: "restore",
|
|
1311
1421
|
summary: "从最近的快照恢复技能"
|
|
1312
1422
|
},
|
|
1313
1423
|
{
|
|
1424
|
+
readonly: false,
|
|
1314
1425
|
usage: "consolidate <target> <sources...> [--plan <runId>]",
|
|
1315
1426
|
summary: "把若干技能合并进一个总括技能"
|
|
1316
1427
|
},
|
|
1317
1428
|
{
|
|
1429
|
+
readonly: false,
|
|
1318
1430
|
usage: "skill restore <name>",
|
|
1319
1431
|
summary: "按名字恢复一个已归档的技能"
|
|
1320
1432
|
},
|
|
1321
1433
|
{
|
|
1434
|
+
readonly: true,
|
|
1322
1435
|
usage: "skill history <name>",
|
|
1323
1436
|
summary: "列出某个技能的内容版本(含时间、动作、字符数、哈希前缀)"
|
|
1324
1437
|
},
|
|
1325
1438
|
{
|
|
1439
|
+
readonly: false,
|
|
1326
1440
|
usage: "skill undo <name> [--to v<N>]",
|
|
1327
1441
|
summary: "把某个技能的内容回退到某一版(默认上一版;只动内容,撤销本身也记为一版)"
|
|
1328
1442
|
},
|
|
1329
1443
|
{
|
|
1444
|
+
readonly: false,
|
|
1330
1445
|
usage: "skill pin <name>",
|
|
1331
1446
|
summary: "钉住一个技能:免于删除、后台审查与生命周期整理(立即生效、随时可解钉;与 skill_manage 的 pin 动作同一语义,不走审批)"
|
|
1332
1447
|
},
|
|
1333
1448
|
{
|
|
1449
|
+
readonly: false,
|
|
1334
1450
|
usage: "skill unpin <name>",
|
|
1335
1451
|
summary: "解钉一个技能:撤销上面的冻结(立即生效、不走审批)"
|
|
1336
1452
|
},
|
|
1337
1453
|
{
|
|
1454
|
+
readonly: true,
|
|
1338
1455
|
usage: "skills health",
|
|
1339
1456
|
summary: "技能库结构体检"
|
|
1340
1457
|
},
|
|
1341
1458
|
{
|
|
1459
|
+
readonly: false,
|
|
1342
1460
|
usage: "skills refresh",
|
|
1343
1461
|
summary: "丢弃目录缓存并重新读取技能树"
|
|
1344
1462
|
},
|
|
1345
1463
|
{
|
|
1464
|
+
readonly: false,
|
|
1346
1465
|
usage: "learn [request]",
|
|
1347
1466
|
summary: "向本会话发一条学习请求"
|
|
1348
1467
|
},
|
|
1349
1468
|
{
|
|
1469
|
+
readonly: false,
|
|
1350
1470
|
usage: "maintain [--timeout=<ms> | --facts]",
|
|
1351
1471
|
summary: "起一次维护扫描并立即回 run id(后台跑;--facts 为 0 token 预览)"
|
|
1352
1472
|
},
|
|
1353
1473
|
{
|
|
1474
|
+
readonly: true,
|
|
1354
1475
|
usage: "maintain status [<id>]",
|
|
1355
1476
|
summary: "看维护扫描:在飞的那次(含已跑多久)或某个 run 的终态"
|
|
1356
1477
|
},
|
|
1357
1478
|
{
|
|
1479
|
+
readonly: true,
|
|
1358
1480
|
usage: "maintain report [<id>]",
|
|
1359
1481
|
summary: "读某个 run 的结果正文(省略 id 取本 home 最新一条;没有结果就明说,不装成空结果)"
|
|
1360
1482
|
},
|
|
1361
1483
|
{
|
|
1484
|
+
readonly: false,
|
|
1362
1485
|
usage: "maintain handoff [<id>]",
|
|
1363
1486
|
summary: "把某次维护报告交给模型:注入指针与汇报协议,模型只汇报、等你确认后再动手"
|
|
1364
1487
|
},
|
|
1365
1488
|
{
|
|
1489
|
+
readonly: false,
|
|
1366
1490
|
usage: "maintain cancel [<id>]",
|
|
1367
1491
|
summary: "停掉在飞的那次维护扫描(省略 id 仅当最新一条是本进程在跑的那次)"
|
|
1368
1492
|
},
|
|
1369
1493
|
{
|
|
1494
|
+
readonly: false,
|
|
1370
1495
|
usage: "migrate",
|
|
1371
1496
|
summary: "把旧设置文档里家族的旧分区迁进现在的行(幂等;已迁移过就报「已一致」)"
|
|
1372
1497
|
},
|
|
1373
1498
|
{
|
|
1499
|
+
readonly: false,
|
|
1374
1500
|
usage: "policy set <id> <value> [--expect <revision>]",
|
|
1375
1501
|
summary: "通过设置服务写入一个你可改的参数 (E3 only; E1/E2 stay in cordis.yml)"
|
|
1376
1502
|
},
|
|
1377
1503
|
{
|
|
1504
|
+
readonly: true,
|
|
1378
1505
|
usage: "params [--group <name>] [--json]",
|
|
1379
1506
|
summary: "列出全部参数:分组、档位、生效时机、来源与当前值(--json feeds scripts)"
|
|
1380
1507
|
},
|
|
1381
1508
|
{
|
|
1509
|
+
readonly: false,
|
|
1382
1510
|
usage: "preset install [--base <name>[,<name>...]]",
|
|
1383
1511
|
summary: "把自进化 Agent 预设**行**写进当前 profile 的 patch 层(`profiles/<p>/cordis.patch.yml`;bases 来自 agent 包的 bases.json)"
|
|
1384
1512
|
},
|
|
1385
1513
|
{
|
|
1514
|
+
readonly: false,
|
|
1386
1515
|
usage: "restructure <name> \"<heading>\" <to_file> [--plan <runId>]",
|
|
1387
1516
|
summary: "把技能正文的一节移到 references 文件"
|
|
1388
1517
|
},
|
|
1389
1518
|
{
|
|
1519
|
+
readonly: true,
|
|
1390
1520
|
usage: "replay",
|
|
1391
1521
|
summary: "比较不同会话与重启之间的计划结果(从活动记录回填)"
|
|
1392
1522
|
}
|
|
@@ -1404,6 +1534,137 @@ function renderHelpText() {
|
|
|
1404
1534
|
].join("\n");
|
|
1405
1535
|
}
|
|
1406
1536
|
//#endregion
|
|
1537
|
+
//#region lib/types/parse.js
|
|
1538
|
+
/**
|
|
1539
|
+
* F-4/G7.4 (C7): the ONE parser for the /evolution command surface.
|
|
1540
|
+
*
|
|
1541
|
+
* The surface used to answer each subcommand with its own regex (`^params(?: --group ([a-z-]+))?(?: --json)?$`
|
|
1542
|
+
* and nine siblings), so `params --json --group review` and `params --group=review` were refused as
|
|
1543
|
+
* "not a subcommand", while a sibling regex accepted `=` — and a mistyped group name degraded into the
|
|
1544
|
+
* generic error instead of the one that lists the known groups.
|
|
1545
|
+
*
|
|
1546
|
+
* This parser is order- and `=`-tolerant, matches the subcommand name case-insensitively, keeps a flag
|
|
1547
|
+
* VALUE's case intact (the domain layer is the one that validates it), and answers a SYNTAX failure when a
|
|
1548
|
+
* declared value flag is left without its value (F-3: `policy set a.b --expect 3` must not read
|
|
1549
|
+
* `--expect 3` as the value).
|
|
1550
|
+
*
|
|
1551
|
+
* @module @lmzhen/dsh-evolution-commands/parse
|
|
1552
|
+
*/
|
|
1553
|
+
/** The flags whose declared form takes a value (`--group <name>`, `--expect <revision>`, …). */
|
|
1554
|
+
/**
|
|
1555
|
+
* Exported so the command-table gate can tie it to the table's usage lines (P2: two declarations of
|
|
1556
|
+
* "which flags take a value" would otherwise be free to drift apart).
|
|
1557
|
+
*/
|
|
1558
|
+
const VALUE_FLAGS = [
|
|
1559
|
+
"group",
|
|
1560
|
+
"expect",
|
|
1561
|
+
"base",
|
|
1562
|
+
"to",
|
|
1563
|
+
"timeout",
|
|
1564
|
+
"plan"
|
|
1565
|
+
];
|
|
1566
|
+
/**
|
|
1567
|
+
* Narrow a parse result: true when it is the syntax failure rather than a command.
|
|
1568
|
+
* @param result - what {@link parseSubcommand} answered.
|
|
1569
|
+
* @returns whether the line was refused.
|
|
1570
|
+
*/
|
|
1571
|
+
function isParseFailure(result) {
|
|
1572
|
+
return result.kind !== void 0;
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Split a command line into tokens: whitespace separates, and a double-quoted run is ONE token with its
|
|
1576
|
+
* quotes removed (the `restructure <name> "<heading>" <to_file>` form needs that; the old regexes each
|
|
1577
|
+
* carried their own quoting rule).
|
|
1578
|
+
* @param input - the text after `/evolution`.
|
|
1579
|
+
* @returns the tokens, in order.
|
|
1580
|
+
*/
|
|
1581
|
+
function tokenize(input) {
|
|
1582
|
+
const tokens = [];
|
|
1583
|
+
let current = "";
|
|
1584
|
+
let quoted = false;
|
|
1585
|
+
for (const char of input.trim()) {
|
|
1586
|
+
if (char === String.fromCharCode(34)) {
|
|
1587
|
+
quoted = !quoted;
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
if (!quoted && /\s/.test(char)) {
|
|
1591
|
+
if (current !== "") {
|
|
1592
|
+
tokens.push(current);
|
|
1593
|
+
current = "";
|
|
1594
|
+
}
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
current += char;
|
|
1598
|
+
}
|
|
1599
|
+
if (current !== "") tokens.push(current);
|
|
1600
|
+
return tokens;
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Read one SUB-FORM line: `maintain status`, `maintain report`, … — the shape the regexes used to own.
|
|
1604
|
+
*
|
|
1605
|
+
* A sub-form matches only when: the line parses, the name matches, the first positional is the sub-form,
|
|
1606
|
+
* and every flag the line carries is DECLARED by the table (an undeclared flag keeps the old
|
|
1607
|
+
* "unknown arguments" answer instead of silently becoming a bare flag).
|
|
1608
|
+
* @param input - the text after `/evolution`.
|
|
1609
|
+
* @param name - the subcommand name the table declares (e.g. `maintain`).
|
|
1610
|
+
* @param sub - the sub-form word (e.g. `status`).
|
|
1611
|
+
* @param declaredFlags - the flags the table declares for `name` (`flagsOf(name)`).
|
|
1612
|
+
* @returns the parsed command, or null when this line is not that sub-form.
|
|
1613
|
+
*/
|
|
1614
|
+
function subcommandOf(input, name, sub, declaredFlags) {
|
|
1615
|
+
const parsed = parseSubcommand(input);
|
|
1616
|
+
if (isParseFailure(parsed) || parsed.name !== name) return null;
|
|
1617
|
+
if (parsed.positionals[0] !== sub) return null;
|
|
1618
|
+
for (const flag of parsed.flags.keys()) if (!declaredFlags.includes("--" + flag)) return null;
|
|
1619
|
+
return parsed;
|
|
1620
|
+
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Parse one command line into its name, flags and positionals.
|
|
1623
|
+
* @param input - the text after `/evolution`.
|
|
1624
|
+
* @returns the parsed command, or the syntax failure that refused it.
|
|
1625
|
+
*/
|
|
1626
|
+
function parseSubcommand(input) {
|
|
1627
|
+
const [first, ...rest] = tokenize(input);
|
|
1628
|
+
if (first === void 0) return {
|
|
1629
|
+
kind: "unknown-subcommand",
|
|
1630
|
+
name: ""
|
|
1631
|
+
};
|
|
1632
|
+
const name = first.toLowerCase();
|
|
1633
|
+
const flags = /* @__PURE__ */ new Map();
|
|
1634
|
+
const positionals = [];
|
|
1635
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
1636
|
+
const token = rest[index];
|
|
1637
|
+
if (token === void 0) continue;
|
|
1638
|
+
if (!token.startsWith("--")) {
|
|
1639
|
+
positionals.push(token);
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
const equals = token.indexOf("=");
|
|
1643
|
+
const flag = (equals < 0 ? token : token.slice(0, equals)).slice(2);
|
|
1644
|
+
if (equals >= 0) {
|
|
1645
|
+
flags.set(flag, token.slice(equals + 1));
|
|
1646
|
+
continue;
|
|
1647
|
+
}
|
|
1648
|
+
if (!VALUE_FLAGS.includes(flag)) {
|
|
1649
|
+
flags.set(flag, true);
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
const next = rest[index + 1];
|
|
1653
|
+
if (next === void 0 || next.startsWith("--")) return {
|
|
1654
|
+
kind: "missing-value",
|
|
1655
|
+
name,
|
|
1656
|
+
flag
|
|
1657
|
+
};
|
|
1658
|
+
flags.set(flag, next);
|
|
1659
|
+
index += 1;
|
|
1660
|
+
}
|
|
1661
|
+
return {
|
|
1662
|
+
name,
|
|
1663
|
+
flags,
|
|
1664
|
+
positionals
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
//#endregion
|
|
1407
1668
|
//#region lib/types/index.js
|
|
1408
1669
|
/**
|
|
1409
1670
|
* Human commands for the evolution family: /evolution learn|pending|curator|restore|consolidate|skills refresh.
|
|
@@ -1565,19 +1826,6 @@ async function readRunReport(io, record) {
|
|
|
1565
1826
|
};
|
|
1566
1827
|
}
|
|
1567
1828
|
}
|
|
1568
|
-
/**
|
|
1569
|
-
* Read a switch that may be a live settings reference.
|
|
1570
|
-
*
|
|
1571
|
-
* The loader hands a `.volatile()` field as `{ get() }` and the value is read at USE time; a
|
|
1572
|
-
* directly constructed plugin passes a plain boolean (the schema is the loader's face — the same
|
|
1573
|
-
* split the numeric knobs document as their second clamp layer). A single-form read is wrong
|
|
1574
|
-
* either way: `=== true` is never true for a reference, and `?.get()` ignores the plain value.
|
|
1575
|
-
* @param value - the configured switch, in either form, or undefined.
|
|
1576
|
-
* @returns the switch; undefined reads as off.
|
|
1577
|
-
*/
|
|
1578
|
-
function liveFlag(value) {
|
|
1579
|
-
return typeof value === "boolean" ? value : value?.get() ?? false;
|
|
1580
|
-
}
|
|
1581
1829
|
function apply(ctx, rawConfig = {}) {
|
|
1582
1830
|
const config = rawConfig;
|
|
1583
1831
|
assertSkillsRootAliasRetired(rawConfig);
|
|
@@ -1593,7 +1841,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1593
1841
|
const ioRegistry = ioCtx.get("evolutionIo");
|
|
1594
1842
|
if (!ioRegistry) return;
|
|
1595
1843
|
const registry = newRunRegistry({
|
|
1596
|
-
io: ioRegistry
|
|
1844
|
+
io: preferProvider(ioRegistry, ["fs"]),
|
|
1597
1845
|
home: evolutionHome(),
|
|
1598
1846
|
warn: (message) => {
|
|
1599
1847
|
ctx.logger.warn(message);
|
|
@@ -1634,15 +1882,15 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1634
1882
|
return err(errorText("e-306-this-deployment-stages-foreground", { a1: what }));
|
|
1635
1883
|
};
|
|
1636
1884
|
const approval = ctx.get("evolutionApproval");
|
|
1637
|
-
const
|
|
1638
|
-
if (
|
|
1885
|
+
const pendingParsed = parseSubcommand(input);
|
|
1886
|
+
if (!isParseFailure(pendingParsed) && pendingParsed.name === "pending") {
|
|
1639
1887
|
if (!approval) return err(errorText("e-301-approval-service-not-mounted"));
|
|
1640
1888
|
const listed = [...await approval.list("pending"), ...await approval.list("executing")];
|
|
1641
1889
|
const pending = [...new Map(listed.map((row) => [row.id, row])).values()];
|
|
1642
1890
|
const ttlMs = typeof approval.ttlMs === "number" && Number.isFinite(approval.ttlMs) ? approval.ttlMs : 0;
|
|
1643
1891
|
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)";
|
|
1644
1892
|
if (pending.length === 0) return ok("No pending evolution writes." + ttlNote);
|
|
1645
|
-
const detailed =
|
|
1893
|
+
const detailed = pendingParsed.flags.get("detail") === true;
|
|
1646
1894
|
const nowMs = Date.now();
|
|
1647
1895
|
const attribution = (p) => {
|
|
1648
1896
|
const age = elapsedSince(p.createdAt, nowMs);
|
|
@@ -1825,7 +2073,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1825
2073
|
if (!ioRegistry) return err("Evolution IO registry not mounted — skill pinning unavailable.");
|
|
1826
2074
|
const result = await newSkillLibrary({
|
|
1827
2075
|
config: { root: skillsRootValue },
|
|
1828
|
-
io: ioRegistry
|
|
2076
|
+
io: preferProvider(ioRegistry, ["fs"]),
|
|
1829
2077
|
ctx,
|
|
1830
2078
|
limits: {
|
|
1831
2079
|
...DEFAULT_SKILL_LIMITS,
|
|
@@ -1879,12 +2127,13 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1879
2127
|
});
|
|
1880
2128
|
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.");
|
|
1881
2129
|
}
|
|
1882
|
-
|
|
2130
|
+
const factsParsed = parseSubcommand(input);
|
|
2131
|
+
if (!isParseFailure(factsParsed) && factsParsed.name === "maintain" && factsParsed.flags.get("facts") === true && factsParsed.positionals.length === 0) {
|
|
1883
2132
|
const ioRegistry = ctx.get("evolutionIo");
|
|
1884
2133
|
if (!ioRegistry) return err("Evolution IO registry not mounted — maintenance facts unavailable.");
|
|
1885
2134
|
const library = newSkillLibrary({
|
|
1886
2135
|
config: { root: skillsRootValue },
|
|
1887
|
-
io: ioRegistry
|
|
2136
|
+
io: preferProvider(ioRegistry, ["fs"])
|
|
1888
2137
|
});
|
|
1889
2138
|
const enrichment = await buildEnrichment(ctx, library);
|
|
1890
2139
|
const readFailures = [];
|
|
@@ -1896,15 +2145,17 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1896
2145
|
}
|
|
1897
2146
|
});
|
|
1898
2147
|
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`);
|
|
1899
|
-
if (snapshots.length === 0 && skillsRootValue !== "" && !await ioRegistry
|
|
2148
|
+
if (snapshots.length === 0 && skillsRootValue !== "" && !await preferProvider(ioRegistry, ["fs"]).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`);
|
|
1900
2149
|
if (snapshots.length === 0) return ok("Maintenance scan: empty skill library. Nothing to do.");
|
|
1901
2150
|
const { facts } = buildMaintainFacts(snapshots, enrichment.usageObservedValue, void 0);
|
|
1902
2151
|
return ok(`Maintenance facts (0-token preview):\n${facts}`);
|
|
1903
2152
|
}
|
|
1904
|
-
const
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
2153
|
+
const maintainParsed = parseSubcommand(input);
|
|
2154
|
+
const maintainFlagsDeclared = [...isParseFailure(maintainParsed) ? [] : maintainParsed.flags.keys()].every((flag) => flagsOf("maintain").includes("--" + flag));
|
|
2155
|
+
if (!isParseFailure(maintainParsed) && maintainParsed.name === "maintain" && maintainParsed.positionals.length === 0 && maintainFlagsDeclared) {
|
|
2156
|
+
const timeoutFlag = maintainParsed.flags.get("timeout");
|
|
2157
|
+
const runTimeoutMs = typeof timeoutFlag === "string" ? Number(timeoutFlag) : config.maintainTimeoutMs ?? 6e5;
|
|
2158
|
+
if (!Number.isSafeInteger(runTimeoutMs) || runTimeoutMs <= 0 || runTimeoutMs > MAX_TIMER_DELAY_MS) return err(timeoutFlag !== void 0 ? `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.`);
|
|
1908
2159
|
const registry = runs;
|
|
1909
2160
|
if (!registry) return err("Evolution IO registry not mounted — maintenance scan unavailable.");
|
|
1910
2161
|
const synced = await syncRuns(registry);
|
|
@@ -1935,7 +2186,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1935
2186
|
try {
|
|
1936
2187
|
const library = newSkillLibrary({
|
|
1937
2188
|
config: { root: skillsRootValue },
|
|
1938
|
-
io: ioRegistry
|
|
2189
|
+
io: preferProvider(ioRegistry, ["fs"])
|
|
1939
2190
|
});
|
|
1940
2191
|
const enrichment = await buildEnrichment(ctx, library);
|
|
1941
2192
|
const outcome = await runMaintain({
|
|
@@ -1947,7 +2198,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1947
2198
|
logger: { warn: (message) => {
|
|
1948
2199
|
ctx.logger.warn(message);
|
|
1949
2200
|
} },
|
|
1950
|
-
rootExists: skillsRootValue === "" ? void 0 : () => ioRegistry
|
|
2201
|
+
rootExists: skillsRootValue === "" ? void 0 : () => preferProvider(ioRegistry, ["fs"]).exists(skillsRootValue),
|
|
1951
2202
|
skillRoot: skillsRootValue
|
|
1952
2203
|
}, {
|
|
1953
2204
|
timeoutMs: runTimeoutMs,
|
|
@@ -1956,7 +2207,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1956
2207
|
enrichment: () => enrichment
|
|
1957
2208
|
});
|
|
1958
2209
|
const endedAt = Date.now();
|
|
1959
|
-
const resultRef = await writeMaintainReport(ioRegistry
|
|
2210
|
+
const resultRef = await writeMaintainReport(preferProvider(ioRegistry, ["fs"]), {
|
|
1960
2211
|
runId: handle.id,
|
|
1961
2212
|
...outcome.ok ? {
|
|
1962
2213
|
verdict: outcome.verdict,
|
|
@@ -1980,7 +2231,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1980
2231
|
...outcome.ok ? {} : { failure: handle.signal.aborted ? "cancelled: the scan was stopped before it settled" : outcome.error ?? "Maintenance scan failed." }
|
|
1981
2232
|
});
|
|
1982
2233
|
if (outcome.ok) {
|
|
1983
|
-
appendEvolutionEvent(ioRegistry
|
|
2234
|
+
appendEvolutionEvent(preferProvider(ioRegistry, ["fs"]), eventsFile(runRoot), {
|
|
1984
2235
|
type: "maintain",
|
|
1985
2236
|
source: "manual",
|
|
1986
2237
|
runId: handle.id,
|
|
@@ -1989,7 +2240,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1989
2240
|
}).catch((error) => {
|
|
1990
2241
|
ctx.logger.warn(`evolution-commands: failed to record maintain event: ${String(error)}`);
|
|
1991
2242
|
});
|
|
1992
|
-
if (
|
|
2243
|
+
if (liveBool(config.maintainHandoffOnSettle) && resultRef !== void 0 && runAgent !== void 0) try {
|
|
1993
2244
|
if (deliverHandoff(runAgent, createUserMessage({
|
|
1994
2245
|
content: [{
|
|
1995
2246
|
type: "text",
|
|
@@ -2034,12 +2285,12 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2034
2285
|
`- stop: /evolution maintain cancel ${handle.id}`
|
|
2035
2286
|
].join("\n"));
|
|
2036
2287
|
}
|
|
2037
|
-
const maintainStatus =
|
|
2038
|
-
if (maintainStatus) {
|
|
2288
|
+
const maintainStatus = subcommandOf(input, "maintain", "status", flagsOf("maintain"));
|
|
2289
|
+
if (maintainStatus !== null) {
|
|
2039
2290
|
const registry = runs;
|
|
2040
2291
|
if (!registry) return err("Evolution IO registry not mounted — run status unavailable.");
|
|
2041
2292
|
const synced = await syncRuns(registry);
|
|
2042
|
-
const wanted = maintainStatus[1];
|
|
2293
|
+
const wanted = maintainStatus.positionals[1];
|
|
2043
2294
|
if (wanted !== void 0) {
|
|
2044
2295
|
const record = registry.find(wanted);
|
|
2045
2296
|
if (!record) {
|
|
@@ -2055,12 +2306,12 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2055
2306
|
const recent = all.filter((record) => record.state !== "running").slice(0, 5);
|
|
2056
2307
|
return ok([live.length === 0 ? "No maintenance run in flight." : `In flight:\n${live.map(renderRunRecord).join("\n")}`, recent.length === 0 ? "" : `Recent:\n${recent.map(renderRunRecord).join("\n")}`].filter((part) => part !== "").join("\n"));
|
|
2057
2308
|
}
|
|
2058
|
-
const maintainReport =
|
|
2059
|
-
if (maintainReport) {
|
|
2309
|
+
const maintainReport = subcommandOf(input, "maintain", "report", flagsOf("maintain"));
|
|
2310
|
+
if (maintainReport !== null) {
|
|
2060
2311
|
const registry = runs;
|
|
2061
2312
|
if (!registry) return err("Evolution IO registry not mounted — run results unavailable.");
|
|
2062
2313
|
const synced = await syncRuns(registry);
|
|
2063
|
-
const wanted = maintainReport[1];
|
|
2314
|
+
const wanted = maintainReport.positionals[1];
|
|
2064
2315
|
if (wanted === void 0 && !synced.ok) return err(unreadableIndex(synced.note));
|
|
2065
2316
|
const record = wanted === void 0 ? newestRun(registry) : registry.find(wanted);
|
|
2066
2317
|
if (!record) {
|
|
@@ -2073,17 +2324,17 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2073
2324
|
if (wanted === void 0 && record.state === "running") return err(`The newest run ${record.id} is still running — its report appears when it settles; /evolution maintain status ${record.id} shows where it is.`);
|
|
2074
2325
|
const ioRegistry = ctx.get("evolutionIo");
|
|
2075
2326
|
if (!ioRegistry) return err("Evolution IO registry not mounted — run results unavailable.");
|
|
2076
|
-
const probed = await readRunReport(ioRegistry
|
|
2327
|
+
const probed = await readRunReport(preferProvider(ioRegistry, ["fs"]), record);
|
|
2077
2328
|
if (probed.kind === "missing") return err(`Run ${record.id} (${record.state}) has NO result: no report was written${record.failure === void 0 ? "" : ` (${record.failure})`}.`);
|
|
2078
2329
|
if (probed.kind === "unknown") return err(`Run ${record.id} has a report that could not be read (${probed.reason}) — this is not "no result".`);
|
|
2079
2330
|
return ok(probed.text);
|
|
2080
2331
|
}
|
|
2081
|
-
const maintainCancel =
|
|
2082
|
-
if (maintainCancel) {
|
|
2332
|
+
const maintainCancel = subcommandOf(input, "maintain", "cancel", flagsOf("maintain"));
|
|
2333
|
+
if (maintainCancel !== null) {
|
|
2083
2334
|
const registry = runs;
|
|
2084
2335
|
if (!registry) return err("Evolution IO registry not mounted — run cancellation unavailable.");
|
|
2085
2336
|
const synced = await syncRuns(registry);
|
|
2086
|
-
const wanted = maintainCancel[1];
|
|
2337
|
+
const wanted = maintainCancel.positionals[1];
|
|
2087
2338
|
if (wanted === void 0 && !synced.ok) return err(unreadableIndex(synced.note));
|
|
2088
2339
|
const record = wanted === void 0 ? newestRun(registry) : registry.find(wanted);
|
|
2089
2340
|
if (!record) {
|
|
@@ -2100,12 +2351,12 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2100
2351
|
}
|
|
2101
2352
|
return ok(`Maintenance run ${record.id} cancelled — the scan stops at its next checkpoint. /evolution maintain status ${record.id} shows the terminal state.`);
|
|
2102
2353
|
}
|
|
2103
|
-
const maintainHandoff =
|
|
2104
|
-
if (maintainHandoff) {
|
|
2354
|
+
const maintainHandoff = subcommandOf(input, "maintain", "handoff", flagsOf("maintain"));
|
|
2355
|
+
if (maintainHandoff !== null) {
|
|
2105
2356
|
const registry = runs;
|
|
2106
2357
|
if (!registry) return err("Evolution IO registry not mounted — the report cannot be handed over.");
|
|
2107
2358
|
const synced = await syncRuns(registry);
|
|
2108
|
-
const wanted = maintainHandoff[1];
|
|
2359
|
+
const wanted = maintainHandoff.positionals[1];
|
|
2109
2360
|
if (wanted === void 0 && !synced.ok) return err(unreadableIndex(synced.note));
|
|
2110
2361
|
const record = wanted === void 0 ? newestRun(registry) : registry.find(wanted);
|
|
2111
2362
|
if (!record) {
|
|
@@ -2118,7 +2369,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2118
2369
|
if (wanted === void 0 && record.state === "running") return err(`The newest run ${record.id} is still running — its report appears when it settles; /evolution maintain status ${record.id} shows where it is.`);
|
|
2119
2370
|
const ioRegistry = ctx.get("evolutionIo");
|
|
2120
2371
|
if (!ioRegistry) return err("Evolution IO registry not mounted — the report cannot be handed over.");
|
|
2121
|
-
const probed = await readRunReport(ioRegistry
|
|
2372
|
+
const probed = await readRunReport(preferProvider(ioRegistry, ["fs"]), record);
|
|
2122
2373
|
if (probed.kind === "missing") return err(`Run ${record.id} (${record.state}) has NO result: no report was written${record.failure === void 0 ? "" : ` (${record.failure})`}.`);
|
|
2123
2374
|
if (probed.kind === "unknown") return err(`Run ${record.id} has a report that could not be read (${probed.reason}) — this is not "no result".`);
|
|
2124
2375
|
const missingAgent = agentMissing("handoff");
|
|
@@ -2144,26 +2395,27 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2144
2395
|
return ok(woke ? `Handed run ${record.id} to the model — report: ${reportDigestPath(record)}. It will read the file and follow the action lines.` : `Handed run ${record.id} to the model, queued — this host exposes no wake-up channel, so it is read on your next message.`);
|
|
2145
2396
|
}
|
|
2146
2397
|
if (/^maintain\b/.test(input)) return err("Unknown maintain arguments: expected `maintain`, `maintain --timeout <ms>` / `maintain --timeout=<ms>`, `maintain status [<id>]`, `maintain report [<id>]`, `maintain handoff [<id>]` or `maintain cancel [<id>]`. Got: " + input);
|
|
2147
|
-
const
|
|
2148
|
-
|
|
2149
|
-
|
|
2398
|
+
const presetParsed = parseSubcommand(input);
|
|
2399
|
+
const presetInstall = isParseFailure(presetParsed) || presetParsed.name !== "preset" ? null : presetParsed.positionals[0] === "install" && presetParsed.positionals.length === 1 && [...presetParsed.flags.keys()].every((flag) => flagsOf("preset").includes("--" + flag)) ? presetParsed : false;
|
|
2400
|
+
if (presetInstall !== null && presetInstall !== false) {
|
|
2150
2401
|
{
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2402
|
+
const mounted = presetProfileTarget(ctx);
|
|
2403
|
+
const exclusion = installExclusionTarget(mounted === void 0 ? void 0 : {
|
|
2404
|
+
profile: mounted.profile,
|
|
2405
|
+
dir: mounted.profileDir,
|
|
2406
|
+
bundles: mounted.bundles
|
|
2407
|
+
});
|
|
2408
|
+
if (exclusion.kind === "undecidable") return err(`Refusing to install the layered Evolution preset: ${exclusion.reason} — the all/preset double-mount check could not run. Resolve access to this profile and retry, or install via the layered installer, which enforces the same exclusion up front.`);
|
|
2409
|
+
const installedBundles = exclusion.bundles;
|
|
2159
2410
|
const allBundle = installedBundles.find((name) => name === "dsh-evolution-all" || name.endsWith("/dsh-evolution-all"));
|
|
2160
2411
|
const presetBundle = installedBundles.find((name) => name === "dsh-evolution-preset" || name.endsWith("/dsh-evolution-preset"));
|
|
2161
|
-
if (allBundle || presetBundle) return err(`Refusing to install the layered Evolution preset: "${allBundle ?? presetBundle}" is installed in this
|
|
2412
|
+
if (allBundle || presetBundle) return err(`Refusing to install the layered Evolution preset: "${allBundle ?? presetBundle}" is installed in this profile — its model rows (tool-memory / tool-skill-manage / tool-session-query / skill-catalog) would double-mount with the preset's and the profile fails loud at startup. Keep ONE: remove the ${allBundle ? "evolution-all bundle" : "evolution-preset bundle"} before installing the layered preset (see /evolution doctor).`);
|
|
2162
2413
|
}
|
|
2163
2414
|
try {
|
|
2164
2415
|
const source = resolveAgentPresetDir(import.meta.url);
|
|
2165
2416
|
const table = readAgentPresetBases(source);
|
|
2166
|
-
const
|
|
2417
|
+
const baseFlag = presetInstall.flags.get("base");
|
|
2418
|
+
const requestedRaw = typeof baseFlag === "string" ? baseFlag : table.defaultBase;
|
|
2167
2419
|
const requested = [...new Set(requestedRaw.split(",").map((part) => part.trim()).filter((part) => part !== ""))];
|
|
2168
2420
|
if (requested.length === 0) return err(`No agent-preset base named — ${join(source, "bases.json")} lists ${table.entries.map((entry) => entry.name).join(", ")}`);
|
|
2169
2421
|
const selected = [];
|
|
@@ -2177,45 +2429,51 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2177
2429
|
const profile = presetProfileTarget(ctx);
|
|
2178
2430
|
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>`.");
|
|
2179
2431
|
const delta = readFileSync(deltaPath, "utf8");
|
|
2180
|
-
const
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
const
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
rowId
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2432
|
+
const hmr = ctx.get("hmr");
|
|
2433
|
+
const commit = () => {
|
|
2434
|
+
const current = existsSync(profile.patchPath) ? readFileSync(profile.patchPath, "utf8") : "";
|
|
2435
|
+
let merged = current;
|
|
2436
|
+
const written = [];
|
|
2437
|
+
for (const base of selected) {
|
|
2438
|
+
const refusal = baseRefusalReason(base, (name) => ctx.get(name) !== void 0);
|
|
2439
|
+
if (refusal !== void 0) return err(refusal);
|
|
2440
|
+
const basePatch = resolvePresetBasePatch(base.name, profile);
|
|
2441
|
+
const rowId = presetRowId(base.id);
|
|
2442
|
+
const entryText = composePresetEntry(readFileSync(basePatch, "utf8"), delta, {
|
|
2443
|
+
rowId,
|
|
2444
|
+
id: base.id,
|
|
2445
|
+
name: base.display.name,
|
|
2446
|
+
description: base.display.description,
|
|
2447
|
+
order: base.display.order
|
|
2448
|
+
});
|
|
2449
|
+
const next = mergePresetRow(merged, entryText, rowId);
|
|
2450
|
+
written.push(next === merged ? `${base.name} → ${profile.patchPath} (already current)` : `${base.name} → ${rowId} in ${profile.patchPath}`);
|
|
2451
|
+
merged = next;
|
|
2452
|
+
}
|
|
2453
|
+
if (merged !== current) atomicWriteFiles(dirname(profile.patchPath), [{
|
|
2454
|
+
name: PROFILE_PATCH_FILENAME$1,
|
|
2455
|
+
content: presetPatchText(merged)
|
|
2456
|
+
}], void 0, (message) => {
|
|
2457
|
+
ctx.logger.warn(message);
|
|
2194
2458
|
});
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
name: PROFILE_PATCH_FILENAME,
|
|
2201
|
-
content: presetPatchText(merged)
|
|
2202
|
-
}], void 0, (message) => {
|
|
2203
|
-
ctx.logger.warn(message);
|
|
2204
|
-
});
|
|
2205
|
-
return ok(`Evolution agent preset row installed (${written.join("; ")}). Restart the session switcher to select it.`);
|
|
2459
|
+
return ok(`Evolution agent preset row installed (${written.join("; ")}). Restart the session switcher to select it.`);
|
|
2460
|
+
};
|
|
2461
|
+
const runExclusive = hmr?.runExclusive;
|
|
2462
|
+
if (runExclusive === void 0) return commit();
|
|
2463
|
+
return await runExclusive.call(hmr, commit);
|
|
2206
2464
|
} catch (error) {
|
|
2207
2465
|
return err(`Preset install failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2208
2466
|
}
|
|
2209
2467
|
}
|
|
2210
2468
|
if (input.startsWith("restructure ")) {
|
|
2211
|
-
const
|
|
2212
|
-
|
|
2213
|
-
const
|
|
2214
|
-
const
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
const
|
|
2218
|
-
const
|
|
2469
|
+
const restParsed = parseSubcommand(rawInputTrimmed);
|
|
2470
|
+
if (isParseFailure(restParsed) || restParsed.name !== "restructure") return err("Usage: /evolution restructure <name> \"<## heading>\" <to_file>");
|
|
2471
|
+
const name = restParsed.positionals[0] ?? "";
|
|
2472
|
+
const heading = restParsed.positionals[1] ?? "";
|
|
2473
|
+
const toFile = restParsed.positionals[2] ?? "";
|
|
2474
|
+
if (name === "" || heading === "" || toFile === "") return err("Usage: /evolution restructure <name> \"<## heading>\" <to_file>");
|
|
2475
|
+
const planFlag = restParsed.flags.get("plan");
|
|
2476
|
+
const planRunId = typeof planFlag === "string" ? planFlag : void 0;
|
|
2219
2477
|
if (!toFile.startsWith("references/")) return err("to_file must live under references/ (log/detail destination).");
|
|
2220
2478
|
const ioRegistry = ctx.get("evolutionIo");
|
|
2221
2479
|
if (!ioRegistry) return err("Evolution IO registry not mounted — restructure unavailable.");
|
|
@@ -2251,7 +2509,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2251
2509
|
}
|
|
2252
2510
|
const result = await newSkillLibrary({
|
|
2253
2511
|
config: { root: skillsRootValue },
|
|
2254
|
-
io: ioRegistry
|
|
2512
|
+
io: preferProvider(ioRegistry, ["fs"]),
|
|
2255
2513
|
ctx,
|
|
2256
2514
|
threatExemptLabels: config.threatExemptLabels,
|
|
2257
2515
|
limits: {
|
|
@@ -2271,8 +2529,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2271
2529
|
if (!replay) return err(errorText("e-303-replay-service-not-mounted"));
|
|
2272
2530
|
return ok(replay.compare().report);
|
|
2273
2531
|
}
|
|
2274
|
-
const
|
|
2275
|
-
if (
|
|
2532
|
+
const paramsParsed = parseSubcommand(input);
|
|
2533
|
+
if (!isParseFailure(paramsParsed) && paramsParsed.name === "params") {
|
|
2276
2534
|
const provider = ctx.get("settings");
|
|
2277
2535
|
const sections = /* @__PURE__ */ new Map();
|
|
2278
2536
|
if (provider?.describe !== void 0) try {
|
|
@@ -2287,21 +2545,23 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2287
2545
|
return err(errorText("e-307-the-settings-service-could", { a1: error instanceof Error ? error.message : String(error) }));
|
|
2288
2546
|
}
|
|
2289
2547
|
const rows = paramSurfaceRows(sections, PARAM_EXPOSURE);
|
|
2290
|
-
const
|
|
2548
|
+
const groupFlag = paramsParsed.flags.get("group");
|
|
2549
|
+
const group = typeof groupFlag === "string" ? groupFlag : void 0;
|
|
2291
2550
|
const selected = group === void 0 ? rows : rows.filter((row) => row.group === group);
|
|
2292
2551
|
if (group !== void 0 && selected.length === 0) return err(errorText("e-308-unknown-parameter-group-group", {
|
|
2293
2552
|
a1: group,
|
|
2294
2553
|
a2: paramGroups(PARAM_EXPOSURE).join(", ")
|
|
2295
2554
|
}));
|
|
2296
|
-
if (
|
|
2555
|
+
if (paramsParsed.flags.get("json") === true) return ok(renderParamJson(selected));
|
|
2297
2556
|
return ok(renderParamRows(selected, { providerMounted: provider !== void 0 }));
|
|
2298
2557
|
}
|
|
2299
|
-
const
|
|
2300
|
-
if (
|
|
2558
|
+
const policyParsed = parseSubcommand(input);
|
|
2559
|
+
if (isParseFailure(policyParsed) && policyParsed.name === "policy" && policyParsed.kind === "missing-value") return err("/evolution policy set needs a value after --" + (policyParsed.flag ?? "") + "; usage: policy set <id> <value> [--expect <revision>]");
|
|
2560
|
+
if (!isParseFailure(policyParsed) && policyParsed.name === "policy" && policyParsed.positionals[0] === "set") {
|
|
2301
2561
|
const provider = ctx.get("settings");
|
|
2302
2562
|
if (provider?.update === void 0 || provider.describe === void 0) return err(errorText("e-311-no-settings-service-is"));
|
|
2303
|
-
const rawId =
|
|
2304
|
-
const rawValue =
|
|
2563
|
+
const rawId = policyParsed.positionals[1] ?? "";
|
|
2564
|
+
const rawValue = policyParsed.positionals[2] ?? "";
|
|
2305
2565
|
let id;
|
|
2306
2566
|
try {
|
|
2307
2567
|
id = canonicalWriteId(rawId);
|
|
@@ -2332,13 +2592,24 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2332
2592
|
}));
|
|
2333
2593
|
const current = descriptor.value;
|
|
2334
2594
|
const resolved = typeof current === "object" && current !== null ? current : void 0;
|
|
2335
|
-
const
|
|
2595
|
+
const expectFlag = policyParsed.flags.get("expect");
|
|
2596
|
+
const expectedRaw = typeof expectFlag === "string" ? expectFlag : void 0;
|
|
2336
2597
|
const expected = expectedRaw === void 0 ? descriptor.revision : Number(expectedRaw);
|
|
2337
2598
|
if (expectedRaw !== void 0 && descriptor.revision !== void 0 && Number(expectedRaw) !== descriptor.revision) return err(errorText("e-309-revision-conflict-you-sent", {
|
|
2338
2599
|
a1: expectedRaw,
|
|
2339
2600
|
a2: descriptor.revision
|
|
2340
2601
|
}));
|
|
2341
2602
|
const value = parseParamValue(rawValue);
|
|
2603
|
+
const floorId = entry.relation?.atLeast;
|
|
2604
|
+
if (floorId !== void 0 && typeof value === "number") {
|
|
2605
|
+
const sibling = resolved?.[floorId];
|
|
2606
|
+
const floor = typeof sibling === "number" ? sibling : NaN;
|
|
2607
|
+
if (Number.isFinite(floor) && value < floor) return err(errorText("e-322-a-parameter-would-break-a", {
|
|
2608
|
+
a1: id,
|
|
2609
|
+
a2: String(floor),
|
|
2610
|
+
a3: String(value)
|
|
2611
|
+
}));
|
|
2612
|
+
}
|
|
2342
2613
|
try {
|
|
2343
2614
|
await provider.update(namespace, { [id]: value }, expected);
|
|
2344
2615
|
} catch (error) {
|
|
@@ -2366,7 +2637,16 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2366
2637
|
return ok(renderNamespaceMigration(attempt.outcome));
|
|
2367
2638
|
}
|
|
2368
2639
|
if (input === "doctor" || input === "doctor --json") {
|
|
2369
|
-
const
|
|
2640
|
+
const mountedForDoctor = presetProfileTarget(ctx);
|
|
2641
|
+
const mounted = mountedForDoctor === void 0 ? {} : { mounted: {
|
|
2642
|
+
profile: mountedForDoctor.profile,
|
|
2643
|
+
dir: mountedForDoctor.profileDir,
|
|
2644
|
+
bundles: mountedForDoctor.bundles
|
|
2645
|
+
} };
|
|
2646
|
+
const report = await diagnose(ctx, {
|
|
2647
|
+
home: evolutionRoot(),
|
|
2648
|
+
...mounted
|
|
2649
|
+
});
|
|
2370
2650
|
if (input === "doctor --json") return ok(JSON.stringify(report, null, 2));
|
|
2371
2651
|
return ok(renderDoctorText(report));
|
|
2372
2652
|
}
|
|
@@ -2377,7 +2657,6 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2377
2657
|
const evolutionCommand = {
|
|
2378
2658
|
name: "evolution",
|
|
2379
2659
|
description: "自进化:状态与待批准写入",
|
|
2380
|
-
recordInput: false,
|
|
2381
2660
|
input: { hint: renderHint() },
|
|
2382
2661
|
handler: (invocation) => commandBody.run(invocation).catch((error) => ({
|
|
2383
2662
|
kind: "error",
|
|
@@ -2492,121 +2771,6 @@ function resolveAgentPresetDir(importMetaUrl) {
|
|
|
2492
2771
|
return overlayCandidate;
|
|
2493
2772
|
}
|
|
2494
2773
|
}
|
|
2495
|
-
const defaultFs = {
|
|
2496
|
-
writeFileSync: (path, data) => {
|
|
2497
|
-
writeFileSync(path, data);
|
|
2498
|
-
},
|
|
2499
|
-
existsSync: (path) => existsSync(path),
|
|
2500
|
-
copyFileSync: (from, to) => {
|
|
2501
|
-
copyFileSync(from, to);
|
|
2502
|
-
},
|
|
2503
|
-
renameSync: (from, to) => {
|
|
2504
|
-
renameSync(from, to);
|
|
2505
|
-
},
|
|
2506
|
-
rmSync: (path, options) => {
|
|
2507
|
-
rmSync(path, options);
|
|
2508
|
-
},
|
|
2509
|
-
mtime: (path) => {
|
|
2510
|
-
try {
|
|
2511
|
-
return statSync(path).mtimeMs;
|
|
2512
|
-
} catch (error) {
|
|
2513
|
-
if (isMissingPath(error)) return null;
|
|
2514
|
-
throw error;
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
};
|
|
2518
|
-
/**
|
|
2519
|
-
* S6.3 (E-40): atomically replace a set of files within one directory.
|
|
2520
|
-
* Each file is staged to a sibling `<name>.tmp`, then renamed into place
|
|
2521
|
-
* (atomic on the same filesystem); a pre-existing file keeps a single
|
|
2522
|
-
* `<name>.bak`. A failure at ANY staging step removes the staged temps and
|
|
2523
|
-
* leaves the previous files untouched — a half-written target is impossible
|
|
2524
|
-
* up to the staging and backup phases.
|
|
2525
|
-
*
|
|
2526
|
-
* F-211: the rename COMMIT phase does not carry the same guarantee. When a
|
|
2527
|
-
* filesystem refuses to overwrite the destination, the code removes it and
|
|
2528
|
-
* retries the rename; if the retry ALSO fails the destination is gone. The
|
|
2529
|
-
* single `.bak` created above still holds the previous content, so the target
|
|
2530
|
-
* is restored from it (best-effort) before the original rename error is
|
|
2531
|
-
* rethrown, so a failed commit never leaves the file missing.
|
|
2532
|
-
*
|
|
2533
|
-
* V4-17: the `.bak` is refreshed to the committed content after every
|
|
2534
|
-
* successful commit, so a later failed commit always recovers to the most
|
|
2535
|
-
* recent good generation (a once-at-first-install `.bak` could otherwise
|
|
2536
|
-
* restore content several installs old). The recovery error names the `.bak`
|
|
2537
|
-
* source (and mtime when available) so the caller knows the generation it
|
|
2538
|
-
* restored from.
|
|
2539
|
-
*/
|
|
2540
|
-
function atomicWriteFiles(targetDir, writes, fs = defaultFs, logger = console.warn) {
|
|
2541
|
-
const stage = (name) => join(targetDir, `${name}.tmp`);
|
|
2542
|
-
const dupes = [...new Set(writes.map((write) => write.name).filter((name, index, all) => all.indexOf(name) !== index))];
|
|
2543
|
-
if (dupes.length > 0) throw new Error(`atomicWriteFiles: duplicate input names: ${dupes.join(", ")}`);
|
|
2544
|
-
try {
|
|
2545
|
-
for (const { name, content } of writes) fs.writeFileSync(stage(name), content);
|
|
2546
|
-
for (const { name } of writes) {
|
|
2547
|
-
const finalPath = join(targetDir, name);
|
|
2548
|
-
const bakPath = join(targetDir, `${name}.bak`);
|
|
2549
|
-
if (fs.existsSync(finalPath) && !fs.existsSync(bakPath)) fs.copyFileSync(finalPath, bakPath);
|
|
2550
|
-
}
|
|
2551
|
-
const committed = [];
|
|
2552
|
-
for (const { name } of writes) {
|
|
2553
|
-
const finalPath = join(targetDir, name);
|
|
2554
|
-
const bakPath = join(targetDir, `${name}.bak`);
|
|
2555
|
-
const tmp = stage(name);
|
|
2556
|
-
try {
|
|
2557
|
-
fs.renameSync(tmp, finalPath);
|
|
2558
|
-
committed.push(name);
|
|
2559
|
-
} catch {
|
|
2560
|
-
try {
|
|
2561
|
-
fs.rmSync(finalPath, { force: true });
|
|
2562
|
-
} catch (removeError) {
|
|
2563
|
-
const already = committed.length > 0 ? `; already committed: ${committed.join(", ")}` : "";
|
|
2564
|
-
throw new Error(`atomicWriteFiles: could not remove "${name}" to replace it (${removeError instanceof Error ? removeError.message : String(removeError)})${already}`);
|
|
2565
|
-
}
|
|
2566
|
-
try {
|
|
2567
|
-
fs.renameSync(tmp, finalPath);
|
|
2568
|
-
committed.push(name);
|
|
2569
|
-
} catch (renameError) {
|
|
2570
|
-
const already = committed.length > 0 ? `; already committed: ${committed.join(", ")}` : "";
|
|
2571
|
-
if (fs.existsSync(bakPath)) {
|
|
2572
|
-
try {
|
|
2573
|
-
fs.copyFileSync(bakPath, finalPath);
|
|
2574
|
-
} catch {
|
|
2575
|
-
throw new Error(`atomicWriteFiles: "${name}" was removed but recovery from ${bakPath} failed (${renameError instanceof Error ? renameError.message : String(renameError)}); verify the file manually${already}`);
|
|
2576
|
-
}
|
|
2577
|
-
const bakMtime = fs.mtime?.(bakPath);
|
|
2578
|
-
const when = bakMtime !== null && bakMtime !== void 0 && Number.isFinite(bakMtime) ? ` (bak mtime ${new Date(bakMtime).toISOString()})` : "";
|
|
2579
|
-
throw new Error(`atomicWriteFiles: "${name}" was removed during commit and restored from the last good generation ${bakPath}${when}; original error: ${renameError instanceof Error ? renameError.message : String(renameError)}${already}`);
|
|
2580
|
-
}
|
|
2581
|
-
throw new Error(`${renameError instanceof Error ? renameError.message : String(renameError)}${already}`);
|
|
2582
|
-
}
|
|
2583
|
-
}
|
|
2584
|
-
}
|
|
2585
|
-
for (const { name } of writes) {
|
|
2586
|
-
const finalPath = join(targetDir, name);
|
|
2587
|
-
try {
|
|
2588
|
-
if (fs.existsSync(finalPath)) fs.copyFileSync(finalPath, join(targetDir, `${name}.bak`));
|
|
2589
|
-
} catch (refreshError) {
|
|
2590
|
-
logger(`atomicWriteFiles: committed "${name}" but failed to refresh its .bak (${refreshError instanceof Error ? refreshError.message : String(refreshError)}); a later failed commit will recover to an OLDER generation`);
|
|
2591
|
-
}
|
|
2592
|
-
}
|
|
2593
|
-
} catch (error) {
|
|
2594
|
-
const cleanupFailures = [];
|
|
2595
|
-
for (const { name } of writes) try {
|
|
2596
|
-
fs.rmSync(stage(name), {
|
|
2597
|
-
force: true,
|
|
2598
|
-
recursive: true
|
|
2599
|
-
});
|
|
2600
|
-
} catch (cleanupError) {
|
|
2601
|
-
cleanupFailures.push(`${name}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
2602
|
-
}
|
|
2603
|
-
if (cleanupFailures.length > 0) {
|
|
2604
|
-
const original = error instanceof Error ? error.message : String(error);
|
|
2605
|
-
throw new Error(`${original} (staged-temp cleanup also failed: ${cleanupFailures.join("; ")})`);
|
|
2606
|
-
}
|
|
2607
|
-
throw error;
|
|
2608
|
-
}
|
|
2609
|
-
}
|
|
2610
2774
|
/** F-328: render staged args for `pending --detail`, truncated to 500 chars.
|
|
2611
2775
|
* args may be non-JSON (a tool produced garbage), so the render is fail-safe.
|
|
2612
2776
|
* V4-19: a truncation is marked with `…(truncated N chars)` so the operator is
|