@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 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, isMissingPath, isPresent, isUnknown, liveBool, mergePresetRow, newRunRegistry, newSkillLibrary, paramSettingsId, partitionVersions, planNamespaceMigration, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, probeText, reportsSweepLockTarget, resolveBasePresetPatch, resolveParamId, resolveRootConfig, scopedProbeReport, sweepReports, transactIo } from "@lmzhen/dsh-evolution-core";
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 { copyFileSync, existsSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
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 typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
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 `Base "${entry.name}" is registered as UNSUPPORTED: ${entry.unsupported}`;
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 : `Base "${entry.name}" requires the "${service}" service, which this deployment does not provide — the generated preset would refuse to mount.`;
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: "no profile is mounted in this process, so the double-mount question cannot be answered here"
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: "the mounted profile manifest could not be read"
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 ["the platform configuration surface (configEditor.configuration) is not mounted here — parameter divergences were NOT checked"];
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("the platform plugin-row surface (pluginManager.listPlugins) is not mounted here — declared-but-unreachable parameters were NOT checked");
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 typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
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" ? `the home profiles directory could not be enumerated (${detail}) — bundle-based install-form and conflict checks are DEGRADED and may miss installed bundles` : `a profile manifest could not be read or parsed (${detail}) — this profile's bundle rows are UNKNOWN, so the install-form and conflict checks are DEGRADED and may miss installed bundles`);
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} did not answer within ${PROBE_TIMEOUT_MS}ms — the corpus is blocked (a concurrent writer can stall the observation) or the service is wedged; isolate the most recently written session and re-run`
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(SOURCE_LABELS[row.source] ?? row.source, 10) + renderValue(row.value));
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: "把卡在执行中的写入放回待批窗口(先确认效果再批准 or reject)"
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: "通过设置服务写入一个你可改的参数 (E3 only; E1/E2 stay in cordis.yml)"
1501
+ summary: "通过设置服务写入一个你可改的参数 (仅 E3;E1/E2 仍留在 cordis.yml)"
1446
1502
  },
1447
1503
  {
1504
+ readonly: true,
1448
1505
  usage: "params [--group <name>] [--json]",
1449
- summary: "列出全部参数:分组、档位、生效时机、来源与当前值(--json feeds scripts)"
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 `The home's run index is not readable (${note ?? "unknown reason"}) — this is NOT "there is no recent run".`;
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 `Run ${id} is not in this process's history, and the home's run index is not readable (${note ?? "unknown reason"}) — that is NOT proof the run never happened.`;
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.provider(),
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 ?? "unknown reason"}`);
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 pendingMatch = /^pending(?: --detail)?$/.exec(input);
1695
- if (pendingMatch) {
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(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)";
1701
- if (pending.length === 0) return ok("No pending evolution writes." + ttlNote);
1702
- const detailed = pendingMatch[0] === "pending --detail";
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(`Curator run skipped (${result.skipped}): no curation pass was executed.\nrunId=${result.report.runId}`);
1744
- return ok(`Curator run complete: ${result.stale.length} stale, ${result.archived.length} archived, ${result.errors.length} failed.\nrunId=${result.report.runId}${result.report.snapshotPath ? `\nsnapshot=${result.report.snapshotPath}` : ""}`);
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 ? "" : "\nNOTE: no evolution state service is mounted — this setting is NOT persisted and will not survive this process.";
1752
- return ok((paused ? "Curator automatic curation paused. Manual /evolution curator run is unaffected; resume with /evolution curator resume." : "Curator automatic curation resumed. The next scheduled pass waits one interval (first-run defer semantics).") + notPersisted);
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("No curator state yet: the first automatic pass is deferred until the interval elapses.");
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("No mutation records yet.");
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(`Mutations: ${usable.length} recorded (recent 5):\n${recent.join("\n")}`);
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("No curator report available.");
1795
- if (typeof report !== "object" || typeof report.runId !== "string" || typeof report.startedAt !== "string" || !Array.isArray(report.archived) || !Array.isArray(report.failed)) return err("Report file unreadable.");
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\` takes no arguments (got "${tail}"). Use \`restore\` for a whole-tree rollback, or \`skill restore <name>\` for one skill.`);
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("Usage: /evolution consolidate <target> <source...>");
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("Usage: /evolution skill restore <name>");
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("Usage: /evolution skill history <name>");
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(`No content versions are recorded for "${name}" yet — they appear after the next write through skill_manage.`);
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 = [`Content versions for "${name}" (oldest first, ${groups.content.length} of at most the retained count):`, ...groups.content.map(row)];
1859
- if (groups.support.length > 0) lines.push("Support-file versions (history only — undo restores SKILL.md, not these bytes):", ...groups.support.map(row));
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("Usage: /evolution skill undo <name> [--to v<N>]");
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(`Usage: /evolution skill ${pinned ? "pin" : "unpin"} <name>`);
2071
+ if (!name) return err(`用法:/evolution skill ${pinned ? "pin" : "unpin"} <name>`);
1881
2072
  const ioRegistry = ctx.get("evolutionIo");
1882
- if (!ioRegistry) return err("Evolution IO registry not mounted — skill pinning unavailable.");
2073
+ if (!ioRegistry) return err("未挂载 evolution-io 注册表:无法固定或取消固定技能。");
1883
2074
  const result = await newSkillLibrary({
1884
2075
  config: { root: skillsRootValue },
1885
- io: ioRegistry.provider(),
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 ? "" : "Usage observation not yet established — churn (write-ghost) rows are suppressed.";
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("Skill catalog refresh requested: caches dropped, catalog re-read on next lookup.");
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("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.");
2128
+ return woke ? ok("已向本会话发送学习请求,请现在执行。") : ok("学习请求已排入本会话队列:本宿主没有唤醒通道,会在你下一条消息时读取。");
1938
2129
  }
1939
- if (input === "maintain --facts") {
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("Evolution IO registry not mounted — maintenance facts unavailable.");
2133
+ if (!ioRegistry) return err("未挂载 evolution-io 注册表:维护事实不可用。");
1942
2134
  const library = newSkillLibrary({
1943
2135
  config: { root: skillsRootValue },
1944
- io: ioRegistry.provider()
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(`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`);
1956
- if (snapshots.length === 0 && skillsRootValue !== "" && !await ioRegistry.provider().exists(skillsRootValue)) return err(`maintenance scan found an empty skill library, but the configured skill root does not exist (${skillsRootValue}) — point the skillsRoot config at a real directory (a literal \`~\` is not expanded) and run the scan again`);
1957
- if (snapshots.length === 0) return ok("Maintenance scan: empty skill library. Nothing to do.");
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(`Maintenance facts (0-token preview):\n${facts}`);
2151
+ return ok(`维护事实(零 token 预览):\n${facts}`);
1960
2152
  }
1961
- const maintainArgs = /^maintain(?:\s+--timeout[ =](\d+))?\s*$/.exec(input);
1962
- if (maintainArgs) {
1963
- const runTimeoutMs = maintainArgs[1] ? Number(maintainArgs[1]) : config.maintainTimeoutMs ?? 6e5;
1964
- if (!Number.isSafeInteger(runTimeoutMs) || runTimeoutMs <= 0 || runTimeoutMs > MAX_TIMER_DELAY_MS) return err(maintainArgs[1] ? `Invalid --timeout value: expected a positive integer number of milliseconds up to ${MAX_TIMER_DELAY_MS} (e.g. /evolution maintain --timeout 600000).` : `The maintainTimeoutMs config is invalid: expected a positive integer number of milliseconds up to ${MAX_TIMER_DELAY_MS}. Fix the evolution-commands row config (maintainTimeoutMs), then retry.`);
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("Evolution IO registry not mounted — maintenance scan unavailable.");
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("Evolution IO registry not mounted — maintenance scan unavailable.");
1973
- if (!subagents) return err("Subagents service not mounted — maintenance scan unavailable.");
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(`Maintenance run ${running.id} is already running (started ~${elapsed}s ago) — re-submitting now would start a second scan. Status: /evolution maintain status ${running.id}; stop it: /evolution maintain cancel ${running.id}.`);
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(`Maintenance cooldown active (${remaining}s)${latest === void 0 ? "" : ` — latest run ${latest.id}`}; re-running now would spend another model call.`);
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.provider()
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.provider().exists(skillsRootValue),
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.provider(), {
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.provider(), eventsFile(runRoot), {
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 ?? "unknown reason"})`);
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 = /^maintain\s+status(?:\s+(\S+))?\s*$/.exec(input);
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("Evolution IO registry not mounted — run status unavailable.");
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(`Run ${wanted} is not in this process's history, and the home's run index is not readable (${synced.note ?? "unknown reason"}) — that is NOT proof the run never happened.`);
2104
- return err(`No run ${wanted} in this home — /evolution maintain status lists the recent ones.`);
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(index not readable: ${synced.note ?? "unknown reason"} — only this process's view is shown)`);
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(`The run history of this home is not readable (${synced.note ?? "unknown reason"}) — this is NOT "no runs". Fix or remove <evolutionHome>/runs.json to start a clean history.`);
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("No maintenance run recorded in this home yet — /evolution maintain starts one.");
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 = /^maintain\s+report(?:\s+(\S+))?\s*$/.exec(input);
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("Evolution IO registry not mounted — run results unavailable.");
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(`No run ${wanted} in this home — /evolution maintain status lists the recent ones.`);
2320
+ return err(`本 home 里没有 ${wanted} 这次运行:用 /evolution maintain status 查看最近的几次。`);
2127
2321
  }
2128
- return err("No maintenance run recorded in this home yet — /evolution maintain starts one.");
2322
+ return err("本 home 还没有维护运行记录:用 /evolution maintain 启动一次。");
2129
2323
  }
2130
- 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.`);
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("Evolution IO registry not mounted — run results unavailable.");
2133
- const probed = await readRunReport(ioRegistry.provider(), record);
2134
- if (probed.kind === "missing") return err(`Run ${record.id} (${record.state}) has NO result: no report was written${record.failure === void 0 ? "" : ` (${record.failure})`}.`);
2135
- if (probed.kind === "unknown") return err(`Run ${record.id} has a report that could not be read (${probed.reason}) — this is not "no result".`);
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 = /^maintain\s+cancel(?:\s+(\S+))?\s*$/.exec(input);
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("Evolution IO registry not mounted — run cancellation unavailable.");
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(`No run ${wanted} in this home — /evolution maintain status lists the recent ones.`);
2343
+ return err(`本 home 里没有 ${wanted} 这次运行:用 /evolution maintain status 查看最近的几次。`);
2150
2344
  }
2151
- return err("No maintenance run recorded in this home yet — /evolution maintain starts one.");
2345
+ return err("本 home 还没有维护运行记录:用 /evolution maintain 启动一次。");
2152
2346
  }
2153
- const idlessHint = wanted === void 0 ? " Pass an id to cancel a specific run: /evolution maintain status lists them." : "";
2347
+ const idlessHint = wanted === void 0 ? " 要取消某次运行请带上 id:/evolution maintain status 会列出它们。" : "";
2154
2348
  if (!registry.cancel(record.id)) {
2155
- if (record.state === "running") return err(`Run ${record.id} is running in ANOTHER process sharing this home — it can only be cancelled there.`);
2156
- return err(`Run ${record.id} is already ${record.state} — nothing to cancel.${idlessHint}`);
2349
+ if (record.state === "running") return err(`运行 ${record.id} 正在共享本 home 的另一个进程里运行:只能在那里取消。`);
2350
+ return err(`运行 ${record.id} 已经是 ${record.state}:没有可取消的东西。${idlessHint}`);
2157
2351
  }
2158
- return ok(`Maintenance run ${record.id} cancelled — the scan stops at its next checkpoint. /evolution maintain status ${record.id} shows the terminal state.`);
2352
+ return ok(`维护运行 ${record.id} 已取消:扫描会在下一个检查点停止。用 /evolution maintain status ${record.id} 查看终态。`);
2159
2353
  }
2160
- const maintainHandoff = /^maintain\s+handoff(?:\s+(\S+))?\s*$/.exec(input);
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("Evolution IO registry not mounted — the report cannot be handed over.");
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(`No run ${wanted} in this home — /evolution maintain status lists the recent ones.`);
2365
+ return err(`本 home 里没有 ${wanted} 这次运行:用 /evolution maintain status 查看最近的几次。`);
2172
2366
  }
2173
- return err("No maintenance run recorded in this home yet — /evolution maintain starts one.");
2367
+ return err("本 home 还没有维护运行记录:用 /evolution maintain 启动一次。");
2174
2368
  }
2175
- 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.`);
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("Evolution IO registry not mounted — the report cannot be handed over.");
2178
- const probed = await readRunReport(ioRegistry.provider(), record);
2179
- if (probed.kind === "missing") return err(`Run ${record.id} (${record.state}) has NO result: no report was written${record.failure === void 0 ? "" : ` (${record.failure})`}.`);
2180
- if (probed.kind === "unknown") return err(`Run ${record.id} has a report that could not be read (${probed.reason}) — this is not "no result".`);
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 ? `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.`);
2395
+ return ok(woke ? `已把运行 ${record.id} 交给模型 —— 报告:${reportDigestPath(record)}。它会读取该文件并执行其中的行动项。` : `已把运行 ${record.id} 交给模型并排队 —— 本宿主没有唤醒通道,因此会在你下一条消息时被读取。`);
2202
2396
  }
2203
- 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);
2204
- const presetInstall = /^preset install(?: --base (\S+))?$/.exec(input);
2205
- if (presetInstall !== null || /^preset\b/.test(input)) {
2206
- if (presetInstall === null) return err("Unknown preset arguments: expected `preset install` or `preset install --base <name>[,<name>...]` (the names live in the agent package's bases.json). Got: " + input);
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(`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.`);
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(`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).`);
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 requestedRaw = presetInstall[1] ?? table.defaultBase;
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(`No agent-preset base named — ${join(source, "bases.json")} lists ${table.entries.map((entry) => entry.name).join(", ")}`);
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(`Unknown agent-preset base "${name}" — ${join(source, "bases.json")} lists ${table.entries.map((candidate) => candidate.name).join(", ")}`);
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(`Preset file missing from ${source} — is the dsh-evolution-agent-preset package installed?`);
2428
+ if (!existsSync(deltaPath)) return err(`预设文件在 ${source} 里缺失:是否装了 dsh-evolution-agent-preset 包?`);
2234
2429
  const profile = presetProfileTarget(ctx);
2235
- 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>`.");
2430
+ if (profile === void 0) return err("本进程没有挂载任何 profile,因此没有可写入预设行的 profile 补丁:请在由 `dsh --profile <name>` 启动的会话里执行该命令。");
2236
2431
  const delta = readFileSync(deltaPath, "utf8");
2237
- const current = existsSync(profile.patchPath) ? readFileSync(profile.patchPath, "utf8") : "";
2238
- let merged = current;
2239
- const written = [];
2240
- for (const base of selected) {
2241
- const refusal = baseRefusalReason(base, (name) => ctx.get(name) !== void 0);
2242
- if (refusal !== void 0) return err(refusal);
2243
- const basePatch = resolvePresetBasePatch(base.name, profile);
2244
- const rowId = presetRowId(base.id);
2245
- const entryText = composePresetEntry(readFileSync(basePatch, "utf8"), delta, {
2246
- rowId,
2247
- id: base.id,
2248
- name: base.display.name,
2249
- description: base.display.description,
2250
- order: base.display.order
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
- const next = mergePresetRow(merged, entryText, rowId);
2253
- written.push(next === merged ? `${base.name} → ${profile.patchPath} (already current)` : `${base.name} → ${rowId} in ${profile.patchPath}`);
2254
- merged = next;
2255
- }
2256
- if (merged !== current) atomicWriteFiles(dirname(profile.patchPath), [{
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(`Preset install failed: ${error instanceof Error ? error.message : String(error)}`);
2465
+ return err(`预设安装失败:${error instanceof Error ? error.message : String(error)}`);
2265
2466
  }
2266
2467
  }
2267
2468
  if (input.startsWith("restructure ")) {
2268
- const planTail = /\s--plan\s+(\S+)\s*$/.exec(rawInputTrimmed) ?? null;
2269
- const planRunId = planTail?.[1] ?? void 0;
2270
- const rest = planTail ? rawInputTrimmed.slice(0, planTail.index) : rawInputTrimmed;
2271
- const match = /^restructure\s+(\S+)\s+"([^"]+)"\s+(\S+)$/.exec(rest);
2272
- if (!match) return err("Usage: /evolution restructure <name> \"<## heading>\" <to_file>");
2273
- const name = match[1] ?? "";
2274
- const heading = match[2] ?? "";
2275
- const toFile = match[3] ?? "";
2276
- if (!toFile.startsWith("references/")) return err("to_file must live under references/ (log/detail destination).");
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("Evolution IO registry not mounted — restructure unavailable.");
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("Restructure cannot be staged: no skill replay runner is registered — mount the tool-skill-manage row (evolution-agent preset, or evolution-all) or disable evolution-approval.");
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.provider(),
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 paramsMatch = /^params(?: --group ([a-z-]+))?(?: --json)?$/.exec(input);
2332
- if (paramsMatch) {
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 group = paramsMatch[1];
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 (input.endsWith("--json")) return ok(renderParamJson(selected));
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 policySetMatch = /^policy set ([A-Za-z][A-Za-z0-9.-]*) (.+?)(?: --expect (\d+))?$/.exec(input);
2357
- if (policySetMatch) {
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 = policySetMatch[1] ?? "";
2361
- const rawValue = policySetMatch[2] ?? "";
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 expectedRaw = policySetMatch[3];
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} is not a subcommand this build answers (a known subcommand whose argument is missing lands here too).\n${renderHint()}`);
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: command failed: ${error instanceof Error ? error.message : String(error)}\nRun /evolution doctor to inspect services and state files; a corrupted state file is quarantined as <file>.corrupt for inspection.`
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