@lmzhen/dsh-evolution-commands 0.22.0 → 0.24.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 +385 -287
- package/lib/types/index.d.ts +2 -42
- package/lib/types/parse.d.ts +65 -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
|
}
|
|
@@ -272,10 +273,10 @@ function presetProfileTarget(ctx) {
|
|
|
272
273
|
* @returns the refusal text, or undefined when the base is usable here.
|
|
273
274
|
*/
|
|
274
275
|
function baseRefusalReason(entry, servicePresent) {
|
|
275
|
-
if (typeof entry.unsupported === "string" && entry.unsupported !== "") return
|
|
276
|
+
if (typeof entry.unsupported === "string" && entry.unsupported !== "") return `基底「${entry.name}」被登记为 UNSUPPORTED(不受支持):${entry.unsupported}`;
|
|
276
277
|
const service = entry.requires?.service;
|
|
277
278
|
if (typeof service !== "string" || service === "") return void 0;
|
|
278
|
-
return servicePresent(service) ? void 0 :
|
|
279
|
+
return servicePresent(service) ? void 0 : `基底 "${entry.name}" 需要 "${service}" 服务,而本部署没有提供它 —— 生成的预设会拒绝挂载。`;
|
|
279
280
|
}
|
|
280
281
|
/**
|
|
281
282
|
* Resolve the platform base preset patch one `--base` composes.
|
|
@@ -422,7 +423,7 @@ function profilePatchText(profileDir) {
|
|
|
422
423
|
function installExclusionTarget(mounted) {
|
|
423
424
|
if (mounted === void 0 || mounted.dir === "") return {
|
|
424
425
|
kind: "undecidable",
|
|
425
|
-
reason: "
|
|
426
|
+
reason: "本进程没有挂载 profile,因此无法在这里回答双挂载问题"
|
|
426
427
|
};
|
|
427
428
|
let manifestFailure;
|
|
428
429
|
const declared = profileBundles(mounted.dir, (error) => {
|
|
@@ -431,7 +432,7 @@ function installExclusionTarget(mounted) {
|
|
|
431
432
|
const manifestCode = manifestFailure?.code;
|
|
432
433
|
if (manifestFailure !== void 0 && manifestCode !== "ENOENT") return {
|
|
433
434
|
kind: "undecidable",
|
|
434
|
-
reason: "
|
|
435
|
+
reason: "读不出已挂载 profile 的清单"
|
|
435
436
|
};
|
|
436
437
|
return {
|
|
437
438
|
kind: "mounted",
|
|
@@ -521,7 +522,7 @@ function presetIssues(presets) {
|
|
|
521
522
|
* @returns one readable line per divergence, empty when there is nothing to compare.
|
|
522
523
|
*/
|
|
523
524
|
function paramDivergences(platform) {
|
|
524
|
-
if (platform.configuration === void 0) return ["
|
|
525
|
+
if (platform.configuration === void 0) return ["平台上没有挂载配置面(configEditor.configuration):参数差异没有被检查"];
|
|
525
526
|
const issues = [];
|
|
526
527
|
for (const row of platform.configuration) {
|
|
527
528
|
const override = asMapping(row.override);
|
|
@@ -538,7 +539,7 @@ function paramDivergences(platform) {
|
|
|
538
539
|
}
|
|
539
540
|
}
|
|
540
541
|
if (platform.plugins === void 0) {
|
|
541
|
-
issues.push("
|
|
542
|
+
issues.push("平台上没有挂载插件行面(pluginManager.listPlugins):已声明但不可达的参数没有被检查");
|
|
542
543
|
return issues;
|
|
543
544
|
}
|
|
544
545
|
const live = liveRowTails(platform.plugins);
|
|
@@ -579,7 +580,7 @@ function liveRowTails(plugins) {
|
|
|
579
580
|
* @returns the mapping, or undefined for anything else.
|
|
580
581
|
*/
|
|
581
582
|
function asMapping(value) {
|
|
582
|
-
return
|
|
583
|
+
return asRecord(value);
|
|
583
584
|
}
|
|
584
585
|
/** Profile bundle rows for evolution-family packages across all profiles.
|
|
585
586
|
*
|
|
@@ -920,7 +921,7 @@ async function diagnose(ctx, options = {}) {
|
|
|
920
921
|
const conflicts = [];
|
|
921
922
|
const bundles = collectEvolutionBundles(home, (error, scope) => {
|
|
922
923
|
const detail = error instanceof Error ? error.message : String(error);
|
|
923
|
-
conflicts.push(scope === "profiles-dir" ?
|
|
924
|
+
conflicts.push(scope === "profiles-dir" ? `本 home 的 profiles 目录无法枚举(${detail}):基于 bundle 的安装形态与冲突检查已降级(DEGRADED),可能漏掉已安装的 bundle` : `本 profile 清单读不出来或无法解析(${detail}):本 profile 的 bundle 行为未知,因此安装形态与冲突检查已降级(DEGRADED),可能漏掉已安装的 bundle`);
|
|
924
925
|
});
|
|
925
926
|
const has = (name) => ctx.get(name) !== void 0;
|
|
926
927
|
const capabilityAbsence = absentHostServices(has);
|
|
@@ -1078,7 +1079,7 @@ async function probeBounded(work, label) {
|
|
|
1078
1079
|
timer = setTimeout(() => {
|
|
1079
1080
|
resolve({
|
|
1080
1081
|
ok: false,
|
|
1081
|
-
message: `${label}
|
|
1082
|
+
message: `${label} 在 ${PROBE_TIMEOUT_MS}ms 内没有应答:语料被阻塞(并发写入方可能拖住观测),或服务卡死。请隔离最近写入的会话后重试。`
|
|
1082
1083
|
});
|
|
1083
1084
|
}, PROBE_TIMEOUT_MS);
|
|
1084
1085
|
});
|
|
@@ -1288,9 +1289,24 @@ const SOURCE_LABELS = Object.freeze({
|
|
|
1288
1289
|
});
|
|
1289
1290
|
/** What the E-tiers mean, so the count line reads without the docs. */
|
|
1290
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
|
+
}
|
|
1291
1307
|
function renderParamRows(rows, options) {
|
|
1292
1308
|
const header = pad("参数", 36) + pad("分组", 12) + pad("档位", 6) + pad("生效", 8) + pad("来源", 10) + "当前值";
|
|
1293
|
-
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));
|
|
1294
1310
|
const tiers = [
|
|
1295
1311
|
"E0",
|
|
1296
1312
|
"E1",
|
|
@@ -1347,116 +1363,160 @@ function renderParamJson(rows) {
|
|
|
1347
1363
|
}
|
|
1348
1364
|
//#endregion
|
|
1349
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
|
+
}
|
|
1350
1382
|
const COMMAND_ENTRIES = [
|
|
1351
1383
|
{
|
|
1384
|
+
readonly: true,
|
|
1352
1385
|
usage: "pending [--detail]",
|
|
1353
1386
|
summary: "列出暂存的写入(--detail 显示参数)"
|
|
1354
1387
|
},
|
|
1355
1388
|
{
|
|
1389
|
+
readonly: false,
|
|
1356
1390
|
usage: "approve <id>",
|
|
1357
1391
|
summary: "执行一条已批准的写入"
|
|
1358
1392
|
},
|
|
1359
1393
|
{
|
|
1394
|
+
readonly: false,
|
|
1360
1395
|
usage: "reject <id>",
|
|
1361
1396
|
summary: "丢弃一条暂存写入,不执行"
|
|
1362
1397
|
},
|
|
1363
1398
|
{
|
|
1399
|
+
readonly: false,
|
|
1364
1400
|
usage: "release <id>",
|
|
1365
|
-
summary: "
|
|
1401
|
+
summary: "把卡在执行中的写入放回待批窗口(先确认效果再批准或驳回)"
|
|
1366
1402
|
},
|
|
1367
1403
|
{
|
|
1404
|
+
readonly: true,
|
|
1368
1405
|
usage: "doctor [--json]",
|
|
1369
1406
|
summary: "只读自检:安装形态、冲突、环境、服务(--json 供脚本用)"
|
|
1370
1407
|
},
|
|
1371
1408
|
{
|
|
1409
|
+
readonly: false,
|
|
1372
1410
|
usage: "curator run|pause|resume|status|report|scope",
|
|
1373
1411
|
summary: "立即运行一次自动整理,或暂停/恢复/查看状态"
|
|
1374
1412
|
},
|
|
1375
1413
|
{
|
|
1414
|
+
readonly: true,
|
|
1376
1415
|
usage: "mutations",
|
|
1377
1416
|
summary: "列出技能改动记录"
|
|
1378
1417
|
},
|
|
1379
1418
|
{
|
|
1419
|
+
readonly: false,
|
|
1380
1420
|
usage: "restore",
|
|
1381
1421
|
summary: "从最近的快照恢复技能"
|
|
1382
1422
|
},
|
|
1383
1423
|
{
|
|
1424
|
+
readonly: false,
|
|
1384
1425
|
usage: "consolidate <target> <sources...> [--plan <runId>]",
|
|
1385
1426
|
summary: "把若干技能合并进一个总括技能"
|
|
1386
1427
|
},
|
|
1387
1428
|
{
|
|
1429
|
+
readonly: false,
|
|
1388
1430
|
usage: "skill restore <name>",
|
|
1389
1431
|
summary: "按名字恢复一个已归档的技能"
|
|
1390
1432
|
},
|
|
1391
1433
|
{
|
|
1434
|
+
readonly: true,
|
|
1392
1435
|
usage: "skill history <name>",
|
|
1393
1436
|
summary: "列出某个技能的内容版本(含时间、动作、字符数、哈希前缀)"
|
|
1394
1437
|
},
|
|
1395
1438
|
{
|
|
1439
|
+
readonly: false,
|
|
1396
1440
|
usage: "skill undo <name> [--to v<N>]",
|
|
1397
1441
|
summary: "把某个技能的内容回退到某一版(默认上一版;只动内容,撤销本身也记为一版)"
|
|
1398
1442
|
},
|
|
1399
1443
|
{
|
|
1444
|
+
readonly: false,
|
|
1400
1445
|
usage: "skill pin <name>",
|
|
1401
1446
|
summary: "钉住一个技能:免于删除、后台审查与生命周期整理(立即生效、随时可解钉;与 skill_manage 的 pin 动作同一语义,不走审批)"
|
|
1402
1447
|
},
|
|
1403
1448
|
{
|
|
1449
|
+
readonly: false,
|
|
1404
1450
|
usage: "skill unpin <name>",
|
|
1405
1451
|
summary: "解钉一个技能:撤销上面的冻结(立即生效、不走审批)"
|
|
1406
1452
|
},
|
|
1407
1453
|
{
|
|
1454
|
+
readonly: true,
|
|
1408
1455
|
usage: "skills health",
|
|
1409
1456
|
summary: "技能库结构体检"
|
|
1410
1457
|
},
|
|
1411
1458
|
{
|
|
1459
|
+
readonly: false,
|
|
1412
1460
|
usage: "skills refresh",
|
|
1413
1461
|
summary: "丢弃目录缓存并重新读取技能树"
|
|
1414
1462
|
},
|
|
1415
1463
|
{
|
|
1464
|
+
readonly: false,
|
|
1416
1465
|
usage: "learn [request]",
|
|
1417
1466
|
summary: "向本会话发一条学习请求"
|
|
1418
1467
|
},
|
|
1419
1468
|
{
|
|
1469
|
+
readonly: false,
|
|
1420
1470
|
usage: "maintain [--timeout=<ms> | --facts]",
|
|
1421
1471
|
summary: "起一次维护扫描并立即回 run id(后台跑;--facts 为 0 token 预览)"
|
|
1422
1472
|
},
|
|
1423
1473
|
{
|
|
1474
|
+
readonly: true,
|
|
1424
1475
|
usage: "maintain status [<id>]",
|
|
1425
1476
|
summary: "看维护扫描:在飞的那次(含已跑多久)或某个 run 的终态"
|
|
1426
1477
|
},
|
|
1427
1478
|
{
|
|
1479
|
+
readonly: true,
|
|
1428
1480
|
usage: "maintain report [<id>]",
|
|
1429
1481
|
summary: "读某个 run 的结果正文(省略 id 取本 home 最新一条;没有结果就明说,不装成空结果)"
|
|
1430
1482
|
},
|
|
1431
1483
|
{
|
|
1484
|
+
readonly: false,
|
|
1432
1485
|
usage: "maintain handoff [<id>]",
|
|
1433
1486
|
summary: "把某次维护报告交给模型:注入指针与汇报协议,模型只汇报、等你确认后再动手"
|
|
1434
1487
|
},
|
|
1435
1488
|
{
|
|
1489
|
+
readonly: false,
|
|
1436
1490
|
usage: "maintain cancel [<id>]",
|
|
1437
1491
|
summary: "停掉在飞的那次维护扫描(省略 id 仅当最新一条是本进程在跑的那次)"
|
|
1438
1492
|
},
|
|
1439
1493
|
{
|
|
1494
|
+
readonly: false,
|
|
1440
1495
|
usage: "migrate",
|
|
1441
1496
|
summary: "把旧设置文档里家族的旧分区迁进现在的行(幂等;已迁移过就报「已一致」)"
|
|
1442
1497
|
},
|
|
1443
1498
|
{
|
|
1499
|
+
readonly: false,
|
|
1444
1500
|
usage: "policy set <id> <value> [--expect <revision>]",
|
|
1445
|
-
summary: "通过设置服务写入一个你可改的参数
|
|
1501
|
+
summary: "通过设置服务写入一个你可改的参数 (仅 E3;E1/E2 仍留在 cordis.yml)"
|
|
1446
1502
|
},
|
|
1447
1503
|
{
|
|
1504
|
+
readonly: true,
|
|
1448
1505
|
usage: "params [--group <name>] [--json]",
|
|
1449
|
-
summary: "列出全部参数:分组、档位、生效时机、来源与当前值(--json
|
|
1506
|
+
summary: "列出全部参数:分组、档位、生效时机、来源与当前值(--json 便于脚本处理)"
|
|
1450
1507
|
},
|
|
1451
1508
|
{
|
|
1509
|
+
readonly: false,
|
|
1452
1510
|
usage: "preset install [--base <name>[,<name>...]]",
|
|
1453
1511
|
summary: "把自进化 Agent 预设**行**写进当前 profile 的 patch 层(`profiles/<p>/cordis.patch.yml`;bases 来自 agent 包的 bases.json)"
|
|
1454
1512
|
},
|
|
1455
1513
|
{
|
|
1514
|
+
readonly: false,
|
|
1456
1515
|
usage: "restructure <name> \"<heading>\" <to_file> [--plan <runId>]",
|
|
1457
1516
|
summary: "把技能正文的一节移到 references 文件"
|
|
1458
1517
|
},
|
|
1459
1518
|
{
|
|
1519
|
+
readonly: true,
|
|
1460
1520
|
usage: "replay",
|
|
1461
1521
|
summary: "比较不同会话与重启之间的计划结果(从活动记录回填)"
|
|
1462
1522
|
}
|
|
@@ -1474,6 +1534,137 @@ function renderHelpText() {
|
|
|
1474
1534
|
].join("\n");
|
|
1475
1535
|
}
|
|
1476
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
|
|
1477
1668
|
//#region lib/types/index.js
|
|
1478
1669
|
/**
|
|
1479
1670
|
* Human commands for the evolution family: /evolution learn|pending|curator|restore|consolidate|skills refresh.
|
|
@@ -1508,12 +1699,12 @@ function newestRun(registry) {
|
|
|
1508
1699
|
/** Sentence F, ONE wording for the three id-less forms (design `10` cell 13): an index that could
|
|
1509
1700
|
* not be read must never answer as "there is no recent run" (G2/0.20.0). */
|
|
1510
1701
|
function unreadableIndex(note) {
|
|
1511
|
-
return
|
|
1702
|
+
return `本 home 的运行索引读不出来(${note ?? "原因未知"}):这不等于「最近没有运行」。`;
|
|
1512
1703
|
}
|
|
1513
1704
|
/** The explicit-id twin of {@link unreadableIndex}, worded like `maintain status` so the four
|
|
1514
1705
|
* readers cannot drift (review P1-1). */
|
|
1515
1706
|
function unreadableForId(id, note) {
|
|
1516
|
-
return
|
|
1707
|
+
return `运行 ${id} 不在本进程的历史里,且本 home 的运行索引读不出来(${note ?? "原因未知"}):但这不能证明那次运行没发生过。`;
|
|
1517
1708
|
}
|
|
1518
1709
|
/** The reader-facing report path: the `.md` digest beside the run's `.json` result. */
|
|
1519
1710
|
function reportDigestPath(record) {
|
|
@@ -1650,7 +1841,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1650
1841
|
const ioRegistry = ioCtx.get("evolutionIo");
|
|
1651
1842
|
if (!ioRegistry) return;
|
|
1652
1843
|
const registry = newRunRegistry({
|
|
1653
|
-
io: ioRegistry
|
|
1844
|
+
io: preferProvider(ioRegistry, ["fs"]),
|
|
1654
1845
|
home: evolutionHome(),
|
|
1655
1846
|
warn: (message) => {
|
|
1656
1847
|
ctx.logger.warn(message);
|
|
@@ -1658,7 +1849,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1658
1849
|
});
|
|
1659
1850
|
runs = registry;
|
|
1660
1851
|
registry.load().then((loaded) => {
|
|
1661
|
-
if (!loaded.ok) ctx.logger.warn(`evolution-commands: the run index is not usable — ${loaded.note ?? "
|
|
1852
|
+
if (!loaded.ok) ctx.logger.warn(`evolution-commands: the run index is not usable — ${loaded.note ?? "原因未知"}`);
|
|
1662
1853
|
else if (loaded.note !== void 0) ctx.logger.warn(`evolution-commands: ${loaded.note}`);
|
|
1663
1854
|
}).catch((error) => {
|
|
1664
1855
|
ctx.logger.warn(`evolution-commands: could not read the run index (${error instanceof Error ? error.message : String(error)})`);
|
|
@@ -1691,15 +1882,15 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1691
1882
|
return err(errorText("e-306-this-deployment-stages-foreground", { a1: what }));
|
|
1692
1883
|
};
|
|
1693
1884
|
const approval = ctx.get("evolutionApproval");
|
|
1694
|
-
const
|
|
1695
|
-
if (
|
|
1885
|
+
const pendingParsed = parseSubcommand(input);
|
|
1886
|
+
if (!isParseFailure(pendingParsed) && pendingParsed.name === "pending") {
|
|
1696
1887
|
if (!approval) return err(errorText("e-301-approval-service-not-mounted"));
|
|
1697
1888
|
const listed = [...await approval.list("pending"), ...await approval.list("executing")];
|
|
1698
1889
|
const pending = [...new Map(listed.map((row) => [row.id, row])).values()];
|
|
1699
1890
|
const ttlMs = typeof approval.ttlMs === "number" && Number.isFinite(approval.ttlMs) ? approval.ttlMs : 0;
|
|
1700
|
-
const ttlNote = ttlMs > 0 ? `\n
|
|
1701
|
-
if (pending.length === 0) return ok("
|
|
1702
|
-
const detailed =
|
|
1891
|
+
const ttlNote = ttlMs > 0 ? `\n(暂存写入 ${ttlMs} 毫秒后过期:过期即关闭为 REJECTED,且永不执行。若仍需要,请重新暂存。)` : "\n(staged writes never expire — the window waits for a decision; set the evolution-approval row pendingTtlMs to bound it)";
|
|
1892
|
+
if (pending.length === 0) return ok("没有待批准的自进化写入。" + ttlNote);
|
|
1893
|
+
const detailed = pendingParsed.flags.get("detail") === true;
|
|
1703
1894
|
const nowMs = Date.now();
|
|
1704
1895
|
const attribution = (p) => {
|
|
1705
1896
|
const age = elapsedSince(p.createdAt, nowMs);
|
|
@@ -1740,22 +1931,22 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1740
1931
|
const curator = ctx.get("evolutionCurator");
|
|
1741
1932
|
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1742
1933
|
const result = await curator.run({ ignoreGates: true });
|
|
1743
|
-
if (result.skipped !== void 0) return ok(
|
|
1744
|
-
return ok(
|
|
1934
|
+
if (result.skipped !== void 0) return ok(`策展轮已跳过(${result.skipped}):未执行策展过程。\nrunId=${result.report.runId}`);
|
|
1935
|
+
return ok(`策展轮完成:${result.stale.length} 项过期,${result.archived.length} 项已归档,${result.errors.length} 项失败。\nrunId=${result.report.runId}${result.report.snapshotPath ? `\nsnapshot=${result.report.snapshotPath}` : ""}`);
|
|
1745
1936
|
}
|
|
1746
1937
|
if (input === "curator pause" || input === "curator resume") {
|
|
1747
1938
|
const curator = ctx.get("evolutionCurator");
|
|
1748
1939
|
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1749
1940
|
const paused = input === "curator pause";
|
|
1750
1941
|
await curator.setPaused(paused);
|
|
1751
|
-
const notPersisted = ctx.get("evolutionState") !== void 0 ? "" : "\
|
|
1752
|
-
return ok((paused ? "
|
|
1942
|
+
const notPersisted = ctx.get("evolutionState") !== void 0 ? "" : "\n注意:没有挂载 evolution 状态服务 —— 该设置不会被持久化,也不会在此进程之后保留。";
|
|
1943
|
+
return ok((paused ? "策展自动轮已暂停。手动 /evolution curator run 不受影响;用 /evolution curator resume 恢复。" : "策展自动轮已恢复。下一次定时轮要等一个间隔(首轮延后语义)。") + notPersisted);
|
|
1753
1944
|
}
|
|
1754
1945
|
if (input === "curator status") {
|
|
1755
1946
|
const curator = ctx.get("evolutionCurator");
|
|
1756
1947
|
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1757
1948
|
const state = await curator.status();
|
|
1758
|
-
if (!state) return ok("
|
|
1949
|
+
if (!state) return ok("还没有策展状态:首个自动轮次要等间隔到达后才会执行。");
|
|
1759
1950
|
const lastRun = typeof state.lastRunAt === "number" && Number.isFinite(state.lastRunAt) && state.lastRunAt > 0 ? new Date(state.lastRunAt).toISOString() : "unknown";
|
|
1760
1951
|
return ok([
|
|
1761
1952
|
`paused=${state.paused}`,
|
|
@@ -1769,9 +1960,9 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1769
1960
|
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1770
1961
|
const records = await curator.skills.listMutations();
|
|
1771
1962
|
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") : [];
|
|
1772
|
-
if (usable.length === 0) return ok("
|
|
1963
|
+
if (usable.length === 0) return ok("还没有写入记录。");
|
|
1773
1964
|
const recent = usable.slice(-5).reverse().map((record) => `${record.at.slice(0, 19)} ${record.skillName} ${record.action} ${record.summary}`);
|
|
1774
|
-
return ok(
|
|
1965
|
+
return ok(`写入记录:已记录 ${usable.length} 条(最近 5 条):\n${recent.join("\n")}`);
|
|
1775
1966
|
}
|
|
1776
1967
|
if (input === "curator scope") {
|
|
1777
1968
|
const curator = ctx.get("evolutionCurator");
|
|
@@ -1791,8 +1982,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1791
1982
|
const curator = ctx.get("evolutionCurator");
|
|
1792
1983
|
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1793
1984
|
const report = await curator.latestReport();
|
|
1794
|
-
if (!report) return ok("
|
|
1795
|
-
if (typeof report !== "object" || typeof report.runId !== "string" || typeof report.startedAt !== "string" || !Array.isArray(report.archived) || !Array.isArray(report.failed)) return err("
|
|
1985
|
+
if (!report) return ok("暂无策展报告。");
|
|
1986
|
+
if (typeof report !== "object" || typeof report.runId !== "string" || typeof report.startedAt !== "string" || !Array.isArray(report.archived) || !Array.isArray(report.failed)) return err("报告文件读不出来。");
|
|
1796
1987
|
const reportAny = report;
|
|
1797
1988
|
const nameOf = (item) => typeof item === "object" && item !== null && typeof item.name === "string" ? item.name : "?";
|
|
1798
1989
|
const reasonOf = (item) => typeof item === "object" && item !== null && typeof item.reason === "string" ? item.reason : "?";
|
|
@@ -1809,7 +2000,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1809
2000
|
}
|
|
1810
2001
|
if (input === "restore" || input.startsWith("restore ")) {
|
|
1811
2002
|
const tail = input.slice(7).trim();
|
|
1812
|
-
if (tail !== "") return err(`\`restore\`
|
|
2003
|
+
if (tail !== "") return err(`\`restore\` 不接受参数(收到 "${tail}")。\`restore\` 做整树回滚,\`skill restore <name>\` 恢复单个技能。`);
|
|
1813
2004
|
const restoreRefusal = unreplayableWriteRefusal("restore");
|
|
1814
2005
|
if (restoreRefusal) return restoreRefusal;
|
|
1815
2006
|
const curator = ctx.get("evolutionCurator");
|
|
@@ -1825,7 +2016,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1825
2016
|
const planTail = /\s--plan\s+(\S+)\s*$/.exec(input) ?? null;
|
|
1826
2017
|
const planRunId = planTail?.[1] ?? void 0;
|
|
1827
2018
|
const [target, ...sources] = (planTail ? input.slice(0, planTail.index) : input).slice(12).trim().split(/\s+/).filter(Boolean);
|
|
1828
|
-
if (!target || sources.length === 0) return err("
|
|
2019
|
+
if (!target || sources.length === 0) return err("用法:/evolution consolidate <target> <source...>");
|
|
1829
2020
|
const curator = ctx.get("evolutionCurator");
|
|
1830
2021
|
const result = curator ? await curator.consolidate(target, sources) : {
|
|
1831
2022
|
ok: false,
|
|
@@ -1836,7 +2027,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1836
2027
|
}
|
|
1837
2028
|
if (input.startsWith("skill restore ")) {
|
|
1838
2029
|
const name = input.slice(14).trim();
|
|
1839
|
-
if (!name) return err("
|
|
2030
|
+
if (!name) return err("用法:/evolution skill restore <name>");
|
|
1840
2031
|
const skillRestoreRefusal = unreplayableWriteRefusal("skill restore");
|
|
1841
2032
|
if (skillRestoreRefusal) return skillRestoreRefusal;
|
|
1842
2033
|
const curator = ctx.get("evolutionCurator");
|
|
@@ -1848,22 +2039,22 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1848
2039
|
}
|
|
1849
2040
|
if (input.startsWith("skill history ")) {
|
|
1850
2041
|
const name = input.slice(14).trim();
|
|
1851
|
-
if (!name) return err("
|
|
2042
|
+
if (!name) return err("用法:/evolution skill history <name>");
|
|
1852
2043
|
const curator = ctx.get("evolutionCurator");
|
|
1853
2044
|
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1854
2045
|
const versions = await curator.history(name);
|
|
1855
|
-
if (versions.length === 0) return ok(
|
|
2046
|
+
if (versions.length === 0) return ok(`技能 "${name}" 还没有内容版本:下一次经 skill_manage 写入后才会出现。`);
|
|
1856
2047
|
const groups = partitionVersions(versions);
|
|
1857
2048
|
const row = (entry) => `v${entry.v}\t${entry.at}\t${entry.action}\t${entry.chars} chars\t${entry.hash.slice(0, 12)}`;
|
|
1858
|
-
const lines = [
|
|
1859
|
-
if (groups.support.length > 0) lines.push("
|
|
2049
|
+
const lines = [`技能 "${name}" 的内容版本(由旧到新,${groups.content.length} 条,最多保留这么多):`, ...groups.content.map(row)];
|
|
2050
|
+
if (groups.support.length > 0) lines.push("支撑文件版本(仅在历史里 —— 撤销恢复的是 SKILL.md,不是这些字节):", ...groups.support.map(row));
|
|
1860
2051
|
return ok(lines.join("\n"));
|
|
1861
2052
|
}
|
|
1862
2053
|
if (input.startsWith("skill undo ")) {
|
|
1863
2054
|
const rest = input.slice(11).trim();
|
|
1864
2055
|
const toMatch = /--to\s+v?(\d+)\s*$/.exec(rest);
|
|
1865
2056
|
const name = (toMatch?.index === void 0 ? rest : rest.slice(0, toMatch.index)).trim();
|
|
1866
|
-
if (!name) return err("
|
|
2057
|
+
if (!name) return err("用法:/evolution skill undo <name> [--to v<N>]");
|
|
1867
2058
|
const skillUndoRefusal = unreplayableWriteRefusal("skill undo");
|
|
1868
2059
|
if (skillUndoRefusal) return skillUndoRefusal;
|
|
1869
2060
|
const curator = ctx.get("evolutionCurator");
|
|
@@ -1877,12 +2068,12 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1877
2068
|
if (input === "skill pin" || input.startsWith("skill pin ") || input === "skill unpin" || input.startsWith("skill unpin ")) {
|
|
1878
2069
|
const pinned = !input.startsWith("skill unpin");
|
|
1879
2070
|
const name = input.slice(pinned ? 10 : 12).trim();
|
|
1880
|
-
if (!name) return err(
|
|
2071
|
+
if (!name) return err(`用法:/evolution skill ${pinned ? "pin" : "unpin"} <name>`);
|
|
1881
2072
|
const ioRegistry = ctx.get("evolutionIo");
|
|
1882
|
-
if (!ioRegistry) return err("
|
|
2073
|
+
if (!ioRegistry) return err("未挂载 evolution-io 注册表:无法固定或取消固定技能。");
|
|
1883
2074
|
const result = await newSkillLibrary({
|
|
1884
2075
|
config: { root: skillsRootValue },
|
|
1885
|
-
io: ioRegistry
|
|
2076
|
+
io: preferProvider(ioRegistry, ["fs"]),
|
|
1886
2077
|
ctx,
|
|
1887
2078
|
limits: {
|
|
1888
2079
|
...DEFAULT_SKILL_LIMITS,
|
|
@@ -1895,14 +2086,14 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1895
2086
|
const curator = ctx.get("evolutionCurator");
|
|
1896
2087
|
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
1897
2088
|
const [rows, observed] = await Promise.all([curator.healthView(), curator.usageObserved()]);
|
|
1898
|
-
const banner = observed ? "" : "
|
|
2089
|
+
const banner = observed ? "" : "使用观测尚未建立 —— 频繁变动(写入幽灵)行已被抑制。";
|
|
1899
2090
|
const line = (row) => `${row.verdict.padEnd(18)} ${row.name}${row.reasons.length > 0 ? ` — ${row.reasons.join("; ")}` : ""}`;
|
|
1900
2091
|
if (rows.length === 0) return ok(banner ? `Structure health: all skills healthy. ${banner}` : "Structure health: all skills healthy.");
|
|
1901
2092
|
return ok([banner ? `Structure health (${rows.length} degraded): ${banner}` : `Structure health (${rows.length} degraded):`, ...rows.map(line)].join("\n"));
|
|
1902
2093
|
}
|
|
1903
2094
|
if (input === "skills refresh") {
|
|
1904
2095
|
ctx.emit("evolution/skills-refresh");
|
|
1905
|
-
return ok("
|
|
2096
|
+
return ok("已请求重读技能目录:缓存已丢弃,下次查询时重新读取。");
|
|
1906
2097
|
}
|
|
1907
2098
|
if (input === "learn" || input.startsWith("learn ")) {
|
|
1908
2099
|
const request = rawInputTrimmed === "learn" ? "" : rawInputTrimmed.slice(6).trim();
|
|
@@ -1934,14 +2125,15 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1934
2125
|
}).catch((error) => {
|
|
1935
2126
|
ctx.logger.warn(`evolution-commands: failed to record learn event: ${String(error)}`);
|
|
1936
2127
|
});
|
|
1937
|
-
return woke ? ok("
|
|
2128
|
+
return woke ? ok("已向本会话发送学习请求,请现在执行。") : ok("学习请求已排入本会话队列:本宿主没有唤醒通道,会在你下一条消息时读取。");
|
|
1938
2129
|
}
|
|
1939
|
-
|
|
2130
|
+
const factsParsed = parseSubcommand(input);
|
|
2131
|
+
if (!isParseFailure(factsParsed) && factsParsed.name === "maintain" && factsParsed.flags.get("facts") === true && factsParsed.positionals.length === 0) {
|
|
1940
2132
|
const ioRegistry = ctx.get("evolutionIo");
|
|
1941
|
-
if (!ioRegistry) return err("
|
|
2133
|
+
if (!ioRegistry) return err("未挂载 evolution-io 注册表:维护事实不可用。");
|
|
1942
2134
|
const library = newSkillLibrary({
|
|
1943
2135
|
config: { root: skillsRootValue },
|
|
1944
|
-
io: ioRegistry
|
|
2136
|
+
io: preferProvider(ioRegistry, ["fs"])
|
|
1945
2137
|
});
|
|
1946
2138
|
const enrichment = await buildEnrichment(ctx, library);
|
|
1947
2139
|
const readFailures = [];
|
|
@@ -1952,29 +2144,31 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1952
2144
|
ctx.logger.warn(`evolution-maintenance: skipping unreadable skill "${name}": ${error instanceof Error ? error.message : String(error)}`);
|
|
1953
2145
|
}
|
|
1954
2146
|
});
|
|
1955
|
-
if (snapshots.length === 0 && readFailures.length > 0) return err(
|
|
1956
|
-
if (snapshots.length === 0 && skillsRootValue !== "" && !await ioRegistry
|
|
1957
|
-
if (snapshots.length === 0) return ok("
|
|
2147
|
+
if (snapshots.length === 0 && readFailures.length > 0) return err(`维护扫描读不出 ${readFailures.length} 个已登记技能(${readFailures.slice(0, 10).join(", ")}${readFailures.length > 10 ? ", …" : ""}),且一个技能都没读到:技能库未被审计。请修好读不出的技能(或它们的目录名)后重新扫描`);
|
|
2148
|
+
if (snapshots.length === 0 && skillsRootValue !== "" && !await preferProvider(ioRegistry, ["fs"]).exists(skillsRootValue)) return err(`维护扫描发现技能库为空,但配置的技能根目录不存在(${skillsRootValue}):请把 skillsRoot 指向真实目录(字面量 \`~\` 不会被展开),然后重新扫描`);
|
|
2149
|
+
if (snapshots.length === 0) return ok("维护扫描:技能库为空,无需处理。");
|
|
1958
2150
|
const { facts } = buildMaintainFacts(snapshots, enrichment.usageObservedValue, void 0);
|
|
1959
|
-
return ok(
|
|
2151
|
+
return ok(`维护事实(零 token 预览):\n${facts}`);
|
|
1960
2152
|
}
|
|
1961
|
-
const
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
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 ? `--timeout 值不合法:应为不超过 ${MAX_TIMER_DELAY_MS} 的正整数毫秒(例如 /evolution maintain --timeout 600000)。` : `maintainTimeoutMs 配置不合法:应为不超过 ${MAX_TIMER_DELAY_MS} 的正整数毫秒。请修正 evolution-commands 行的配置(maintainTimeoutMs)后重试。`);
|
|
1965
2159
|
const registry = runs;
|
|
1966
|
-
if (!registry) return err("
|
|
2160
|
+
if (!registry) return err("未挂载 evolution-io 注册表:维护扫描不可用。");
|
|
1967
2161
|
const synced = await syncRuns(registry);
|
|
1968
2162
|
if (!synced.ok) ctx.logger.warn(`evolution-commands: ${synced.note ?? "run index unreadable"} — the in-flight guard cannot see other processes' runs`);
|
|
1969
2163
|
else if (synced.note !== void 0) ctx.logger.warn(`evolution-commands: ${synced.note}`);
|
|
1970
2164
|
const ioRegistry = ctx.get("evolutionIo");
|
|
1971
2165
|
const subagents = ctx.get("subagents");
|
|
1972
|
-
if (!ioRegistry) return err("
|
|
1973
|
-
if (!subagents) return err("
|
|
2166
|
+
if (!ioRegistry) return err("未挂载 evolution-io 注册表:维护扫描不可用。");
|
|
2167
|
+
if (!subagents) return err("未挂载子智能体服务:维护扫描不可用。");
|
|
1974
2168
|
const running = registry.inFlight();
|
|
1975
2169
|
if (running) {
|
|
1976
2170
|
const elapsed = Math.max(1, Math.round((Date.now() - running.startedAt) / 1e3));
|
|
1977
|
-
return err(
|
|
2171
|
+
return err(`维护运行 ${running.id} 已经在进行中(约 ${elapsed}s 前启动):现在重交会再开一次扫描。查看状态:/evolution maintain status ${running.id};停止它:/evolution maintain cancel ${running.id}。`);
|
|
1978
2172
|
}
|
|
1979
2173
|
const cooldownMs = clampedNumber(config.maintainCooldownMs, 3e4, { min: 0 });
|
|
1980
2174
|
const lastSettled = registry.lastSettledAt();
|
|
@@ -1982,7 +2176,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1982
2176
|
if (cooldownMs > 0 && sinceLast < cooldownMs) {
|
|
1983
2177
|
const remaining = Math.ceil((cooldownMs - sinceLast) / 1e3);
|
|
1984
2178
|
const latest = registry.runs().find((record) => record.state !== "running");
|
|
1985
|
-
return err(
|
|
2179
|
+
return err(`维护冷却中(${remaining}s)${latest === void 0 ? "" : `,最近一次运行 ${latest.id}`}:现在重跑会再消耗一次模型调用。`);
|
|
1986
2180
|
}
|
|
1987
2181
|
const runHome = evolutionHome();
|
|
1988
2182
|
const runRoot = evolutionRoot();
|
|
@@ -1992,7 +2186,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1992
2186
|
try {
|
|
1993
2187
|
const library = newSkillLibrary({
|
|
1994
2188
|
config: { root: skillsRootValue },
|
|
1995
|
-
io: ioRegistry
|
|
2189
|
+
io: preferProvider(ioRegistry, ["fs"])
|
|
1996
2190
|
});
|
|
1997
2191
|
const enrichment = await buildEnrichment(ctx, library);
|
|
1998
2192
|
const outcome = await runMaintain({
|
|
@@ -2004,7 +2198,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2004
2198
|
logger: { warn: (message) => {
|
|
2005
2199
|
ctx.logger.warn(message);
|
|
2006
2200
|
} },
|
|
2007
|
-
rootExists: skillsRootValue === "" ? void 0 : () => ioRegistry
|
|
2201
|
+
rootExists: skillsRootValue === "" ? void 0 : () => preferProvider(ioRegistry, ["fs"]).exists(skillsRootValue),
|
|
2008
2202
|
skillRoot: skillsRootValue
|
|
2009
2203
|
}, {
|
|
2010
2204
|
timeoutMs: runTimeoutMs,
|
|
@@ -2013,7 +2207,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2013
2207
|
enrichment: () => enrichment
|
|
2014
2208
|
});
|
|
2015
2209
|
const endedAt = Date.now();
|
|
2016
|
-
const resultRef = await writeMaintainReport(ioRegistry
|
|
2210
|
+
const resultRef = await writeMaintainReport(preferProvider(ioRegistry, ["fs"]), {
|
|
2017
2211
|
runId: handle.id,
|
|
2018
2212
|
...outcome.ok ? {
|
|
2019
2213
|
verdict: outcome.verdict,
|
|
@@ -2037,7 +2231,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2037
2231
|
...outcome.ok ? {} : { failure: handle.signal.aborted ? "cancelled: the scan was stopped before it settled" : outcome.error ?? "Maintenance scan failed." }
|
|
2038
2232
|
});
|
|
2039
2233
|
if (outcome.ok) {
|
|
2040
|
-
appendEvolutionEvent(ioRegistry
|
|
2234
|
+
appendEvolutionEvent(preferProvider(ioRegistry, ["fs"]), eventsFile(runRoot), {
|
|
2041
2235
|
type: "maintain",
|
|
2042
2236
|
source: "manual",
|
|
2043
2237
|
runId: handle.id,
|
|
@@ -2073,7 +2267,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2073
2267
|
} catch (error) {
|
|
2074
2268
|
ctx.logger.warn(`evolution-commands: automatic report handoff failed (${String(error)})`);
|
|
2075
2269
|
}
|
|
2076
|
-
} else ctx.logger.warn(`evolution-commands: maintenance run ${handle.id} produced no plan (${outcome.error ?? "
|
|
2270
|
+
} else ctx.logger.warn(`evolution-commands: maintenance run ${handle.id} produced no plan (${outcome.error ?? "原因未知"})`);
|
|
2077
2271
|
} catch (error) {
|
|
2078
2272
|
const aborted = handle.signal.aborted;
|
|
2079
2273
|
const failure = aborted ? "cancelled: the scan was stopped before it settled" : `Maintenance scan failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
@@ -2091,93 +2285,93 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2091
2285
|
`- stop: /evolution maintain cancel ${handle.id}`
|
|
2092
2286
|
].join("\n"));
|
|
2093
2287
|
}
|
|
2094
|
-
const maintainStatus =
|
|
2095
|
-
if (maintainStatus) {
|
|
2288
|
+
const maintainStatus = subcommandOf(input, "maintain", "status", flagsOf("maintain"));
|
|
2289
|
+
if (maintainStatus !== null) {
|
|
2096
2290
|
const registry = runs;
|
|
2097
|
-
if (!registry) return err("
|
|
2291
|
+
if (!registry) return err("未挂载 evolution-io 注册表:无法查看运行状态。");
|
|
2098
2292
|
const synced = await syncRuns(registry);
|
|
2099
|
-
const wanted = maintainStatus[1];
|
|
2293
|
+
const wanted = maintainStatus.positionals[1];
|
|
2100
2294
|
if (wanted !== void 0) {
|
|
2101
2295
|
const record = registry.find(wanted);
|
|
2102
2296
|
if (!record) {
|
|
2103
|
-
if (!synced.ok) return err(
|
|
2104
|
-
return err(
|
|
2297
|
+
if (!synced.ok) return err(`运行 ${wanted} 不在本进程的历史里,且本 home 的运行索引读不出来(${synced.note ?? "原因未知"}):但这不能证明那次运行没发生过。`);
|
|
2298
|
+
return err(`本 home 里没有 ${wanted} 这次运行:用 /evolution maintain status 查看最近的几次。`);
|
|
2105
2299
|
}
|
|
2106
|
-
return ok(synced.ok ? renderRunRecord(record) : `${renderRunRecord(record)}\n(
|
|
2300
|
+
return ok(synced.ok ? renderRunRecord(record) : `${renderRunRecord(record)}\n(运行索引读不出来:${synced.note ?? "原因未知"} — only this process's view is shown)`);
|
|
2107
2301
|
}
|
|
2108
|
-
if (!synced.ok) return err(
|
|
2302
|
+
if (!synced.ok) return err(`本 home 的运行历史读不出来(${synced.note ?? "原因未知"}):这「不是」没有运行。要开始干净的历史,请修好或删除 <evolutionHome>/runs.json。`);
|
|
2109
2303
|
const all = registry.runs();
|
|
2110
|
-
if (all.length === 0) return ok("
|
|
2304
|
+
if (all.length === 0) return ok("本 home 还没有维护运行记录:用 /evolution maintain 启动一次。");
|
|
2111
2305
|
const live = all.filter((record) => record.state === "running");
|
|
2112
2306
|
const recent = all.filter((record) => record.state !== "running").slice(0, 5);
|
|
2113
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"));
|
|
2114
2308
|
}
|
|
2115
|
-
const maintainReport =
|
|
2116
|
-
if (maintainReport) {
|
|
2309
|
+
const maintainReport = subcommandOf(input, "maintain", "report", flagsOf("maintain"));
|
|
2310
|
+
if (maintainReport !== null) {
|
|
2117
2311
|
const registry = runs;
|
|
2118
|
-
if (!registry) return err("
|
|
2312
|
+
if (!registry) return err("未挂载 evolution-io 注册表:无法查看运行结果。");
|
|
2119
2313
|
const synced = await syncRuns(registry);
|
|
2120
|
-
const wanted = maintainReport[1];
|
|
2314
|
+
const wanted = maintainReport.positionals[1];
|
|
2121
2315
|
if (wanted === void 0 && !synced.ok) return err(unreadableIndex(synced.note));
|
|
2122
2316
|
const record = wanted === void 0 ? newestRun(registry) : registry.find(wanted);
|
|
2123
2317
|
if (!record) {
|
|
2124
2318
|
if (wanted !== void 0) {
|
|
2125
2319
|
if (!synced.ok) return err(unreadableForId(wanted, synced.note));
|
|
2126
|
-
return err(
|
|
2320
|
+
return err(`本 home 里没有 ${wanted} 这次运行:用 /evolution maintain status 查看最近的几次。`);
|
|
2127
2321
|
}
|
|
2128
|
-
return err("
|
|
2322
|
+
return err("本 home 还没有维护运行记录:用 /evolution maintain 启动一次。");
|
|
2129
2323
|
}
|
|
2130
|
-
if (wanted === void 0 && record.state === "running") return err(
|
|
2324
|
+
if (wanted === void 0 && record.state === "running") return err(`最新一次运行 ${record.id} 仍在进行:它结束后才会出报告;用 /evolution maintain status ${record.id} 查看进度。`);
|
|
2131
2325
|
const ioRegistry = ctx.get("evolutionIo");
|
|
2132
|
-
if (!ioRegistry) return err("
|
|
2133
|
-
const probed = await readRunReport(ioRegistry
|
|
2134
|
-
if (probed.kind === "missing") return err(
|
|
2135
|
-
if (probed.kind === "unknown") return err(
|
|
2326
|
+
if (!ioRegistry) return err("未挂载 evolution-io 注册表:无法查看运行结果。");
|
|
2327
|
+
const probed = await readRunReport(preferProvider(ioRegistry, ["fs"]), record);
|
|
2328
|
+
if (probed.kind === "missing") return err(`运行 ${record.id}(${record.state})没有结果:没有写出报告${record.failure === void 0 ? "" : `(${record.failure})`}。`);
|
|
2329
|
+
if (probed.kind === "unknown") return err(`运行 ${record.id} 有报告但读不出来(${probed.reason}):这「不是」没有结果。`);
|
|
2136
2330
|
return ok(probed.text);
|
|
2137
2331
|
}
|
|
2138
|
-
const maintainCancel =
|
|
2139
|
-
if (maintainCancel) {
|
|
2332
|
+
const maintainCancel = subcommandOf(input, "maintain", "cancel", flagsOf("maintain"));
|
|
2333
|
+
if (maintainCancel !== null) {
|
|
2140
2334
|
const registry = runs;
|
|
2141
|
-
if (!registry) return err("
|
|
2335
|
+
if (!registry) return err("未挂载 evolution-io 注册表:无法取消运行。");
|
|
2142
2336
|
const synced = await syncRuns(registry);
|
|
2143
|
-
const wanted = maintainCancel[1];
|
|
2337
|
+
const wanted = maintainCancel.positionals[1];
|
|
2144
2338
|
if (wanted === void 0 && !synced.ok) return err(unreadableIndex(synced.note));
|
|
2145
2339
|
const record = wanted === void 0 ? newestRun(registry) : registry.find(wanted);
|
|
2146
2340
|
if (!record) {
|
|
2147
2341
|
if (wanted !== void 0) {
|
|
2148
2342
|
if (!synced.ok) return err(unreadableForId(wanted, synced.note));
|
|
2149
|
-
return err(
|
|
2343
|
+
return err(`本 home 里没有 ${wanted} 这次运行:用 /evolution maintain status 查看最近的几次。`);
|
|
2150
2344
|
}
|
|
2151
|
-
return err("
|
|
2345
|
+
return err("本 home 还没有维护运行记录:用 /evolution maintain 启动一次。");
|
|
2152
2346
|
}
|
|
2153
|
-
const idlessHint = wanted === void 0 ? "
|
|
2347
|
+
const idlessHint = wanted === void 0 ? " 要取消某次运行请带上 id:/evolution maintain status 会列出它们。" : "";
|
|
2154
2348
|
if (!registry.cancel(record.id)) {
|
|
2155
|
-
if (record.state === "running") return err(
|
|
2156
|
-
return err(
|
|
2349
|
+
if (record.state === "running") return err(`运行 ${record.id} 正在共享本 home 的另一个进程里运行:只能在那里取消。`);
|
|
2350
|
+
return err(`运行 ${record.id} 已经是 ${record.state}:没有可取消的东西。${idlessHint}`);
|
|
2157
2351
|
}
|
|
2158
|
-
return ok(
|
|
2352
|
+
return ok(`维护运行 ${record.id} 已取消:扫描会在下一个检查点停止。用 /evolution maintain status ${record.id} 查看终态。`);
|
|
2159
2353
|
}
|
|
2160
|
-
const maintainHandoff =
|
|
2161
|
-
if (maintainHandoff) {
|
|
2354
|
+
const maintainHandoff = subcommandOf(input, "maintain", "handoff", flagsOf("maintain"));
|
|
2355
|
+
if (maintainHandoff !== null) {
|
|
2162
2356
|
const registry = runs;
|
|
2163
|
-
if (!registry) return err("
|
|
2357
|
+
if (!registry) return err("未挂载 evolution-io 注册表:无法交付报告。");
|
|
2164
2358
|
const synced = await syncRuns(registry);
|
|
2165
|
-
const wanted = maintainHandoff[1];
|
|
2359
|
+
const wanted = maintainHandoff.positionals[1];
|
|
2166
2360
|
if (wanted === void 0 && !synced.ok) return err(unreadableIndex(synced.note));
|
|
2167
2361
|
const record = wanted === void 0 ? newestRun(registry) : registry.find(wanted);
|
|
2168
2362
|
if (!record) {
|
|
2169
2363
|
if (wanted !== void 0) {
|
|
2170
2364
|
if (!synced.ok) return err(unreadableForId(wanted, synced.note));
|
|
2171
|
-
return err(
|
|
2365
|
+
return err(`本 home 里没有 ${wanted} 这次运行:用 /evolution maintain status 查看最近的几次。`);
|
|
2172
2366
|
}
|
|
2173
|
-
return err("
|
|
2367
|
+
return err("本 home 还没有维护运行记录:用 /evolution maintain 启动一次。");
|
|
2174
2368
|
}
|
|
2175
|
-
if (wanted === void 0 && record.state === "running") return err(
|
|
2369
|
+
if (wanted === void 0 && record.state === "running") return err(`最新一次运行 ${record.id} 仍在进行:它结束后才会出报告;用 /evolution maintain status ${record.id} 查看进度。`);
|
|
2176
2370
|
const ioRegistry = ctx.get("evolutionIo");
|
|
2177
|
-
if (!ioRegistry) return err("
|
|
2178
|
-
const probed = await readRunReport(ioRegistry
|
|
2179
|
-
if (probed.kind === "missing") return err(
|
|
2180
|
-
if (probed.kind === "unknown") return err(
|
|
2371
|
+
if (!ioRegistry) return err("未挂载 evolution-io 注册表:无法交付报告。");
|
|
2372
|
+
const probed = await readRunReport(preferProvider(ioRegistry, ["fs"]), record);
|
|
2373
|
+
if (probed.kind === "missing") return err(`运行 ${record.id}(${record.state})没有结果:没有写出报告${record.failure === void 0 ? "" : `(${record.failure})`}。`);
|
|
2374
|
+
if (probed.kind === "unknown") return err(`运行 ${record.id} 有报告但读不出来(${probed.reason}):这「不是」没有结果。`);
|
|
2181
2375
|
const missingAgent = agentMissing("handoff");
|
|
2182
2376
|
if (missingAgent) return missingAgent;
|
|
2183
2377
|
const woke = deliverHandoff(invocationAgent, createUserMessage({
|
|
@@ -2198,12 +2392,12 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2198
2392
|
}
|
|
2199
2393
|
}));
|
|
2200
2394
|
if (woke === void 0) return err(errorText("e-305-the-invocation-agent-exposes", { a1: "this maintenance handoff" }));
|
|
2201
|
-
return ok(woke ?
|
|
2395
|
+
return ok(woke ? `已把运行 ${record.id} 交给模型 —— 报告:${reportDigestPath(record)}。它会读取该文件并执行其中的行动项。` : `已把运行 ${record.id} 交给模型并排队 —— 本宿主没有唤醒通道,因此会在你下一条消息时被读取。`);
|
|
2202
2396
|
}
|
|
2203
|
-
if (/^maintain\b/.test(input)) return err("
|
|
2204
|
-
const
|
|
2205
|
-
|
|
2206
|
-
|
|
2397
|
+
if (/^maintain\b/.test(input)) return err("未知的 maintain 参数:应为 `maintain`、`maintain --timeout <ms>` / `maintain --timeout=<ms>`、`maintain status [<id>]`、`maintain report [<id>]`、`maintain handoff [<id>]` 或 `maintain cancel [<id>]`。实际收到:" + input);
|
|
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) {
|
|
2207
2401
|
{
|
|
2208
2402
|
const mounted = presetProfileTarget(ctx);
|
|
2209
2403
|
const exclusion = installExclusionTarget(mounted === void 0 ? void 0 : {
|
|
@@ -2211,77 +2405,84 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2211
2405
|
dir: mounted.profileDir,
|
|
2212
2406
|
bundles: mounted.bundles
|
|
2213
2407
|
});
|
|
2214
|
-
if (exclusion.kind === "undecidable") return err(
|
|
2408
|
+
if (exclusion.kind === "undecidable") return err(`拒绝安装分层 Evolution 预设:${exclusion.reason}:all/preset 双重挂载检查无法执行。请解决该 profile 的访问权限后重试,或改用分层安装器——它会在安装前就执行同一套排除检查。`);
|
|
2215
2409
|
const installedBundles = exclusion.bundles;
|
|
2216
2410
|
const allBundle = installedBundles.find((name) => name === "dsh-evolution-all" || name.endsWith("/dsh-evolution-all"));
|
|
2217
2411
|
const presetBundle = installedBundles.find((name) => name === "dsh-evolution-preset" || name.endsWith("/dsh-evolution-preset"));
|
|
2218
|
-
if (allBundle || presetBundle) return err(
|
|
2412
|
+
if (allBundle || presetBundle) return err(`拒绝安装分层 Evolution 预设:本 profile 里已安装「${allBundle ?? presetBundle}」:它的模型行(tool-memory / tool-skill-manage / tool-session-query / skill-catalog)会与预设的重复挂载,profile 启动时会直接报错。只保留一个:安装分层预设前先移除 ${allBundle ? "evolution-all bundle" : "evolution-preset bundle"}(见 /evolution doctor)。`);
|
|
2219
2413
|
}
|
|
2220
2414
|
try {
|
|
2221
2415
|
const source = resolveAgentPresetDir(import.meta.url);
|
|
2222
2416
|
const table = readAgentPresetBases(source);
|
|
2223
|
-
const
|
|
2417
|
+
const baseFlag = presetInstall.flags.get("base");
|
|
2418
|
+
const requestedRaw = typeof baseFlag === "string" ? baseFlag : table.defaultBase;
|
|
2224
2419
|
const requested = [...new Set(requestedRaw.split(",").map((part) => part.trim()).filter((part) => part !== ""))];
|
|
2225
|
-
if (requested.length === 0) return err(
|
|
2420
|
+
if (requested.length === 0) return err(`没有指定 agent-preset 基名:${join(source, "bases.json")} 里列出的是 ${table.entries.map((entry) => entry.name).join(", ")}`);
|
|
2226
2421
|
const selected = [];
|
|
2227
2422
|
for (const name of requested) {
|
|
2228
2423
|
const entry = table.entries.find((candidate) => candidate.name === name);
|
|
2229
|
-
if (entry === void 0) return err(
|
|
2424
|
+
if (entry === void 0) return err(`未知的 agent-preset 基名「${name}」:${join(source, "bases.json")} 里列出的是 ${table.entries.map((candidate) => candidate.name).join(", ")}`);
|
|
2230
2425
|
selected.push(entry);
|
|
2231
2426
|
}
|
|
2232
2427
|
const deltaPath = join(source, "agent.cordis.yml");
|
|
2233
|
-
if (!existsSync(deltaPath)) return err(
|
|
2428
|
+
if (!existsSync(deltaPath)) return err(`预设文件在 ${source} 里缺失:是否装了 dsh-evolution-agent-preset 包?`);
|
|
2234
2429
|
const profile = presetProfileTarget(ctx);
|
|
2235
|
-
if (profile === void 0) return err("
|
|
2430
|
+
if (profile === void 0) return err("本进程没有挂载任何 profile,因此没有可写入预设行的 profile 补丁:请在由 `dsh --profile <name>` 启动的会话里执行该命令。");
|
|
2236
2431
|
const delta = readFileSync(deltaPath, "utf8");
|
|
2237
|
-
const
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
const
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
rowId
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
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);
|
|
2251
2458
|
});
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
name: PROFILE_PATCH_FILENAME$1,
|
|
2258
|
-
content: presetPatchText(merged)
|
|
2259
|
-
}], void 0, (message) => {
|
|
2260
|
-
ctx.logger.warn(message);
|
|
2261
|
-
});
|
|
2262
|
-
return ok(`Evolution agent preset row installed (${written.join("; ")}). Restart the session switcher to select it.`);
|
|
2459
|
+
return ok(`已写入 evolution agent preset 行(${written.join("; ")})。重启会话切换器后即可选中它。`);
|
|
2460
|
+
};
|
|
2461
|
+
const runExclusive = hmr?.runExclusive;
|
|
2462
|
+
if (runExclusive === void 0) return commit();
|
|
2463
|
+
return await runExclusive.call(hmr, commit);
|
|
2263
2464
|
} catch (error) {
|
|
2264
|
-
return err(
|
|
2465
|
+
return err(`预设安装失败:${error instanceof Error ? error.message : String(error)}`);
|
|
2265
2466
|
}
|
|
2266
2467
|
}
|
|
2267
2468
|
if (input.startsWith("restructure ")) {
|
|
2268
|
-
const
|
|
2269
|
-
|
|
2270
|
-
const
|
|
2271
|
-
const
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
const
|
|
2275
|
-
const
|
|
2276
|
-
if (!toFile.startsWith("references/")) return err("to_file
|
|
2469
|
+
const restParsed = parseSubcommand(rawInputTrimmed);
|
|
2470
|
+
if (isParseFailure(restParsed) || restParsed.name !== "restructure") return err("用法:/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("用法:/evolution restructure <name> \"<## heading>\" <to_file>");
|
|
2475
|
+
const planFlag = restParsed.flags.get("plan");
|
|
2476
|
+
const planRunId = typeof planFlag === "string" ? planFlag : void 0;
|
|
2477
|
+
if (!toFile.startsWith("references/")) return err("to_file 必须位于 references/ 之下(日志或明细的落地位置)。");
|
|
2277
2478
|
const ioRegistry = ctx.get("evolutionIo");
|
|
2278
|
-
if (!ioRegistry) return err("
|
|
2479
|
+
if (!ioRegistry) return err("未挂载 evolution-io 注册表:无法重构。");
|
|
2279
2480
|
if (approval) {
|
|
2280
2481
|
const session = invocationAgent?.session;
|
|
2281
2482
|
const sessionPolicy = effectiveSessionPolicy(ctx, session);
|
|
2282
2483
|
const stagesForeground = approval.isEnabled !== false && sessionPolicy !== "never" && approval.stageForeground !== false;
|
|
2283
2484
|
if (stagesForeground && session === void 0) return err(errorText("e-306-this-deployment-stages-foreground-2"));
|
|
2284
|
-
if (stagesForeground && !approval.hasRunner("skill")) return err("
|
|
2485
|
+
if (stagesForeground && !approval.hasRunner("skill")) return err("无法暂存重构:没有注册技能回放运行器:请挂载 tool-skill-manage 行(evolution-agent 预设或 evolution-all),或禁用 evolution-approval。");
|
|
2285
2486
|
const decision = await approval.request({
|
|
2286
2487
|
kind: "skill",
|
|
2287
2488
|
summary: `/evolution restructure ${name}${planRunId ? ` (plan ${planRunId})` : ""}`,
|
|
@@ -2308,7 +2509,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2308
2509
|
}
|
|
2309
2510
|
const result = await newSkillLibrary({
|
|
2310
2511
|
config: { root: skillsRootValue },
|
|
2311
|
-
io: ioRegistry
|
|
2512
|
+
io: preferProvider(ioRegistry, ["fs"]),
|
|
2312
2513
|
ctx,
|
|
2313
2514
|
threatExemptLabels: config.threatExemptLabels,
|
|
2314
2515
|
limits: {
|
|
@@ -2328,8 +2529,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2328
2529
|
if (!replay) return err(errorText("e-303-replay-service-not-mounted"));
|
|
2329
2530
|
return ok(replay.compare().report);
|
|
2330
2531
|
}
|
|
2331
|
-
const
|
|
2332
|
-
if (
|
|
2532
|
+
const paramsParsed = parseSubcommand(input);
|
|
2533
|
+
if (!isParseFailure(paramsParsed) && paramsParsed.name === "params") {
|
|
2333
2534
|
const provider = ctx.get("settings");
|
|
2334
2535
|
const sections = /* @__PURE__ */ new Map();
|
|
2335
2536
|
if (provider?.describe !== void 0) try {
|
|
@@ -2344,21 +2545,23 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2344
2545
|
return err(errorText("e-307-the-settings-service-could", { a1: error instanceof Error ? error.message : String(error) }));
|
|
2345
2546
|
}
|
|
2346
2547
|
const rows = paramSurfaceRows(sections, PARAM_EXPOSURE);
|
|
2347
|
-
const
|
|
2548
|
+
const groupFlag = paramsParsed.flags.get("group");
|
|
2549
|
+
const group = typeof groupFlag === "string" ? groupFlag : void 0;
|
|
2348
2550
|
const selected = group === void 0 ? rows : rows.filter((row) => row.group === group);
|
|
2349
2551
|
if (group !== void 0 && selected.length === 0) return err(errorText("e-308-unknown-parameter-group-group", {
|
|
2350
2552
|
a1: group,
|
|
2351
2553
|
a2: paramGroups(PARAM_EXPOSURE).join(", ")
|
|
2352
2554
|
}));
|
|
2353
|
-
if (
|
|
2555
|
+
if (paramsParsed.flags.get("json") === true) return ok(renderParamJson(selected));
|
|
2354
2556
|
return ok(renderParamRows(selected, { providerMounted: provider !== void 0 }));
|
|
2355
2557
|
}
|
|
2356
|
-
const
|
|
2357
|
-
if (
|
|
2558
|
+
const policyParsed = parseSubcommand(input);
|
|
2559
|
+
if (isParseFailure(policyParsed) && policyParsed.name === "policy" && policyParsed.kind === "missing-value") return err("/evolution policy set 需要在 --" + (policyParsed.flag ?? "") + ";用法:policy set <id> <value> [--expect <revision>]");
|
|
2560
|
+
if (!isParseFailure(policyParsed) && policyParsed.name === "policy" && policyParsed.positionals[0] === "set") {
|
|
2358
2561
|
const provider = ctx.get("settings");
|
|
2359
2562
|
if (provider?.update === void 0 || provider.describe === void 0) return err(errorText("e-311-no-settings-service-is"));
|
|
2360
|
-
const rawId =
|
|
2361
|
-
const rawValue =
|
|
2563
|
+
const rawId = policyParsed.positionals[1] ?? "";
|
|
2564
|
+
const rawValue = policyParsed.positionals[2] ?? "";
|
|
2362
2565
|
let id;
|
|
2363
2566
|
try {
|
|
2364
2567
|
id = canonicalWriteId(rawId);
|
|
@@ -2389,13 +2592,24 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2389
2592
|
}));
|
|
2390
2593
|
const current = descriptor.value;
|
|
2391
2594
|
const resolved = typeof current === "object" && current !== null ? current : void 0;
|
|
2392
|
-
const
|
|
2595
|
+
const expectFlag = policyParsed.flags.get("expect");
|
|
2596
|
+
const expectedRaw = typeof expectFlag === "string" ? expectFlag : void 0;
|
|
2393
2597
|
const expected = expectedRaw === void 0 ? descriptor.revision : Number(expectedRaw);
|
|
2394
2598
|
if (expectedRaw !== void 0 && descriptor.revision !== void 0 && Number(expectedRaw) !== descriptor.revision) return err(errorText("e-309-revision-conflict-you-sent", {
|
|
2395
2599
|
a1: expectedRaw,
|
|
2396
2600
|
a2: descriptor.revision
|
|
2397
2601
|
}));
|
|
2398
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
|
+
}
|
|
2399
2613
|
try {
|
|
2400
2614
|
await provider.update(namespace, { [id]: value }, expected);
|
|
2401
2615
|
} catch (error) {
|
|
@@ -2437,17 +2651,16 @@ function apply(ctx, rawConfig = {}) {
|
|
|
2437
2651
|
return ok(renderDoctorText(report));
|
|
2438
2652
|
}
|
|
2439
2653
|
if (input === "" || input === "help") return ok(`自进化:记忆、技能、会话回顾、技能整理——状态、待批准写入与维护。\n${renderHelpText()}`);
|
|
2440
|
-
return err(`/evolution ${input}
|
|
2654
|
+
return err(`/evolution ${input} 不是本构建能应答的子命令(已知子命令但缺参数也会落到这里)。\n${renderHint()}`);
|
|
2441
2655
|
} };
|
|
2442
2656
|
/** The registered definition: declared members only (A46). */
|
|
2443
2657
|
const evolutionCommand = {
|
|
2444
2658
|
name: "evolution",
|
|
2445
2659
|
description: "自进化:状态与待批准写入",
|
|
2446
|
-
recordInput: false,
|
|
2447
2660
|
input: { hint: renderHint() },
|
|
2448
2661
|
handler: (invocation) => commandBody.run(invocation).catch((error) => ({
|
|
2449
2662
|
kind: "error",
|
|
2450
|
-
text: `evolution
|
|
2663
|
+
text: `evolution:命令失败:${error instanceof Error ? error.message : String(error)}\n请运行 /evolution doctor 检查服务与状态文件;损坏的状态文件会被隔离为 <file> 以便检查。`
|
|
2451
2664
|
}))
|
|
2452
2665
|
};
|
|
2453
2666
|
commandCtx.effect(() => commands.register(evolutionCommand));
|
|
@@ -2558,121 +2771,6 @@ function resolveAgentPresetDir(importMetaUrl) {
|
|
|
2558
2771
|
return overlayCandidate;
|
|
2559
2772
|
}
|
|
2560
2773
|
}
|
|
2561
|
-
const defaultFs = {
|
|
2562
|
-
writeFileSync: (path, data) => {
|
|
2563
|
-
writeFileSync(path, data);
|
|
2564
|
-
},
|
|
2565
|
-
existsSync: (path) => existsSync(path),
|
|
2566
|
-
copyFileSync: (from, to) => {
|
|
2567
|
-
copyFileSync(from, to);
|
|
2568
|
-
},
|
|
2569
|
-
renameSync: (from, to) => {
|
|
2570
|
-
renameSync(from, to);
|
|
2571
|
-
},
|
|
2572
|
-
rmSync: (path, options) => {
|
|
2573
|
-
rmSync(path, options);
|
|
2574
|
-
},
|
|
2575
|
-
mtime: (path) => {
|
|
2576
|
-
try {
|
|
2577
|
-
return statSync(path).mtimeMs;
|
|
2578
|
-
} catch (error) {
|
|
2579
|
-
if (isMissingPath(error)) return null;
|
|
2580
|
-
throw error;
|
|
2581
|
-
}
|
|
2582
|
-
}
|
|
2583
|
-
};
|
|
2584
|
-
/**
|
|
2585
|
-
* S6.3 (E-40): atomically replace a set of files within one directory.
|
|
2586
|
-
* Each file is staged to a sibling `<name>.tmp`, then renamed into place
|
|
2587
|
-
* (atomic on the same filesystem); a pre-existing file keeps a single
|
|
2588
|
-
* `<name>.bak`. A failure at ANY staging step removes the staged temps and
|
|
2589
|
-
* leaves the previous files untouched — a half-written target is impossible
|
|
2590
|
-
* up to the staging and backup phases.
|
|
2591
|
-
*
|
|
2592
|
-
* F-211: the rename COMMIT phase does not carry the same guarantee. When a
|
|
2593
|
-
* filesystem refuses to overwrite the destination, the code removes it and
|
|
2594
|
-
* retries the rename; if the retry ALSO fails the destination is gone. The
|
|
2595
|
-
* single `.bak` created above still holds the previous content, so the target
|
|
2596
|
-
* is restored from it (best-effort) before the original rename error is
|
|
2597
|
-
* rethrown, so a failed commit never leaves the file missing.
|
|
2598
|
-
*
|
|
2599
|
-
* V4-17: the `.bak` is refreshed to the committed content after every
|
|
2600
|
-
* successful commit, so a later failed commit always recovers to the most
|
|
2601
|
-
* recent good generation (a once-at-first-install `.bak` could otherwise
|
|
2602
|
-
* restore content several installs old). The recovery error names the `.bak`
|
|
2603
|
-
* source (and mtime when available) so the caller knows the generation it
|
|
2604
|
-
* restored from.
|
|
2605
|
-
*/
|
|
2606
|
-
function atomicWriteFiles(targetDir, writes, fs = defaultFs, logger = console.warn) {
|
|
2607
|
-
const stage = (name) => join(targetDir, `${name}.tmp`);
|
|
2608
|
-
const dupes = [...new Set(writes.map((write) => write.name).filter((name, index, all) => all.indexOf(name) !== index))];
|
|
2609
|
-
if (dupes.length > 0) throw new Error(`atomicWriteFiles: duplicate input names: ${dupes.join(", ")}`);
|
|
2610
|
-
try {
|
|
2611
|
-
for (const { name, content } of writes) fs.writeFileSync(stage(name), content);
|
|
2612
|
-
for (const { name } of writes) {
|
|
2613
|
-
const finalPath = join(targetDir, name);
|
|
2614
|
-
const bakPath = join(targetDir, `${name}.bak`);
|
|
2615
|
-
if (fs.existsSync(finalPath) && !fs.existsSync(bakPath)) fs.copyFileSync(finalPath, bakPath);
|
|
2616
|
-
}
|
|
2617
|
-
const committed = [];
|
|
2618
|
-
for (const { name } of writes) {
|
|
2619
|
-
const finalPath = join(targetDir, name);
|
|
2620
|
-
const bakPath = join(targetDir, `${name}.bak`);
|
|
2621
|
-
const tmp = stage(name);
|
|
2622
|
-
try {
|
|
2623
|
-
fs.renameSync(tmp, finalPath);
|
|
2624
|
-
committed.push(name);
|
|
2625
|
-
} catch {
|
|
2626
|
-
try {
|
|
2627
|
-
fs.rmSync(finalPath, { force: true });
|
|
2628
|
-
} catch (removeError) {
|
|
2629
|
-
const already = committed.length > 0 ? `; already committed: ${committed.join(", ")}` : "";
|
|
2630
|
-
throw new Error(`atomicWriteFiles: could not remove "${name}" to replace it (${removeError instanceof Error ? removeError.message : String(removeError)})${already}`);
|
|
2631
|
-
}
|
|
2632
|
-
try {
|
|
2633
|
-
fs.renameSync(tmp, finalPath);
|
|
2634
|
-
committed.push(name);
|
|
2635
|
-
} catch (renameError) {
|
|
2636
|
-
const already = committed.length > 0 ? `; already committed: ${committed.join(", ")}` : "";
|
|
2637
|
-
if (fs.existsSync(bakPath)) {
|
|
2638
|
-
try {
|
|
2639
|
-
fs.copyFileSync(bakPath, finalPath);
|
|
2640
|
-
} catch {
|
|
2641
|
-
throw new Error(`atomicWriteFiles: "${name}" was removed but recovery from ${bakPath} failed (${renameError instanceof Error ? renameError.message : String(renameError)}); verify the file manually${already}`);
|
|
2642
|
-
}
|
|
2643
|
-
const bakMtime = fs.mtime?.(bakPath);
|
|
2644
|
-
const when = bakMtime !== null && bakMtime !== void 0 && Number.isFinite(bakMtime) ? ` (bak mtime ${new Date(bakMtime).toISOString()})` : "";
|
|
2645
|
-
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}`);
|
|
2646
|
-
}
|
|
2647
|
-
throw new Error(`${renameError instanceof Error ? renameError.message : String(renameError)}${already}`);
|
|
2648
|
-
}
|
|
2649
|
-
}
|
|
2650
|
-
}
|
|
2651
|
-
for (const { name } of writes) {
|
|
2652
|
-
const finalPath = join(targetDir, name);
|
|
2653
|
-
try {
|
|
2654
|
-
if (fs.existsSync(finalPath)) fs.copyFileSync(finalPath, join(targetDir, `${name}.bak`));
|
|
2655
|
-
} catch (refreshError) {
|
|
2656
|
-
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`);
|
|
2657
|
-
}
|
|
2658
|
-
}
|
|
2659
|
-
} catch (error) {
|
|
2660
|
-
const cleanupFailures = [];
|
|
2661
|
-
for (const { name } of writes) try {
|
|
2662
|
-
fs.rmSync(stage(name), {
|
|
2663
|
-
force: true,
|
|
2664
|
-
recursive: true
|
|
2665
|
-
});
|
|
2666
|
-
} catch (cleanupError) {
|
|
2667
|
-
cleanupFailures.push(`${name}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
2668
|
-
}
|
|
2669
|
-
if (cleanupFailures.length > 0) {
|
|
2670
|
-
const original = error instanceof Error ? error.message : String(error);
|
|
2671
|
-
throw new Error(`${original} (staged-temp cleanup also failed: ${cleanupFailures.join("; ")})`);
|
|
2672
|
-
}
|
|
2673
|
-
throw error;
|
|
2674
|
-
}
|
|
2675
|
-
}
|
|
2676
2774
|
/** F-328: render staged args for `pending --detail`, truncated to 500 chars.
|
|
2677
2775
|
* args may be non-JSON (a tool produced garbage), so the render is fail-safe.
|
|
2678
2776
|
* V4-19: a truncation is marked with `…(truncated N chars)` so the operator is
|