@lmzhen/dsh-evolution-core 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
@@ -1,9 +1,9 @@
1
1
  import { parse } from "yaml";
2
2
  import { basename, dirname, join, resolve } from "node:path";
3
- import { cp, lstat, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
3
+ import { copyFileSync, existsSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import { cp, lstat, mkdir, open, readFile, readdir, rename, rm, stat } from "node:fs/promises";
4
5
  import { createHash, randomBytes, randomUUID } from "node:crypto";
5
6
  import { homedir } from "node:os";
6
- import { existsSync, readFileSync, readdirSync } from "node:fs";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { scopeOf } from "@deepseek-ai/dsh-scope";
9
9
  import { z } from "zod";
@@ -531,7 +531,7 @@ const PLATFORM_CATALOG_DESCRIPTION_DEFAULT = 500;
531
531
  * be copied into validateFrontmatter AND the patch path, and neither copy named
532
532
  * where the content should go — the upstream cap message names the destination
533
533
  * directories, and that is the part a model actually acts on. */
534
- const CONTENT_SPLIT_HINT = "Consider splitting into a smaller SKILL.md with supporting files in references/ or templates.";
534
+ const CONTENT_SPLIT_HINT = "建议拆成更小的 SKILL.md,把支撑文件放进 references/ 或 templates/。";
535
535
  /** V27 G2.4: the largest millisecond delay a timer accepts. `AbortSignal.timeout`
536
536
  * (and `setTimeout`) coerce anything larger to 1ms after a Node warning, so a
537
537
  * timeout configured above this ceiling silently collapses to "immediately
@@ -1052,16 +1052,16 @@ function shrinksOverLimit(next, current, limit) {
1052
1052
  }
1053
1053
  function validateFrontmatter(content, expectedName, limits = DEFAULT_SKILL_LIMITS, current) {
1054
1054
  const parsed = parseFrontmatter(content);
1055
- if (!parsed) return "SKILL.md must start with a `---` line, close the frontmatter with another exact `---` line (only a trailing `\\r` is tolerated), and include a body below it.";
1055
+ if (!parsed) return "SKILL.md 必须以 `---` 行开头,用另一个完全相同的 `---` 行结束 frontmatter(只容忍结尾的 `\\r`),并在其下包含正文。";
1056
1056
  const unreadable = parsed.platformStringSplit.filter((entry) => entry.kind === "sequence" || entry.kind === "mapping");
1057
- if (unreadable.length > 0) return `Frontmatter field ${unreadable.map((entry) => `"${entry.key}" (a YAML ${entry.kind})`).join(", ")} must be a string: the platform skill catalog reads such a field as absent and ignores the whole file. Write plain text, or wrap the value in double quotes to keep it literally.`;
1058
- if (!parsed.frontmatter.name) return "Frontmatter must include a name field.";
1059
- if (!SKILL_NAME_RE.test(parsed.frontmatter.name)) return `Invalid skill name "${parsed.frontmatter.name}" — use lowercase letters, digits, and hyphens.`;
1060
- if (parsed.frontmatter.name.length > limits.maxNameLength) return `Skill name exceeds ${limits.maxNameLength} characters.`;
1061
- if (expectedName && parsed.frontmatter.name !== expectedName) return `Frontmatter name "${parsed.frontmatter.name}" does not match target skill "${expectedName}".`;
1062
- if (!parsed.frontmatter.description) return "Frontmatter must include a description field.";
1063
- if (parsed.frontmatter.description.length > limits.maxDescriptionLength) return `Description exceeds ${limits.maxDescriptionLength} characters.`;
1064
- if (exceedsContentLimit(content, limits.maxSkillContentChars) && !shrinksOverLimit(content, current, limits.maxSkillContentChars)) return `SKILL.md content exceeds ${limits.maxSkillContentChars} characters. ${CONTENT_SPLIT_HINT}`;
1057
+ if (unreadable.length > 0) return `Frontmatter 字段 ${unreadable.map((entry) => `"${entry.key}" (a YAML ${entry.kind})`).join(", ")} 必须是字符串:平台技能目录会把这种字段当作缺失,并忽略整个文件。请写成纯文本,或用双引号包住取值以保持字面内容。`;
1058
+ if (!parsed.frontmatter.name) return "Frontmatter 必须包含 name 字段。";
1059
+ if (!SKILL_NAME_RE.test(parsed.frontmatter.name)) return `技能名「${parsed.frontmatter.name}」不合法:请只用小写字母、数字和连字符。`;
1060
+ if (parsed.frontmatter.name.length > limits.maxNameLength) return `技能名超过 ${limits.maxNameLength} 个字符。`;
1061
+ if (expectedName && parsed.frontmatter.name !== expectedName) return `Frontmatter 里的 name「${parsed.frontmatter.name}」与目标技能「${expectedName}」不一致。`;
1062
+ if (!parsed.frontmatter.description) return "Frontmatter 必须包含 description 字段。";
1063
+ if (parsed.frontmatter.description.length > limits.maxDescriptionLength) return `description 超过 ${limits.maxDescriptionLength} 个字符。`;
1064
+ if (exceedsContentLimit(content, limits.maxSkillContentChars) && !shrinksOverLimit(content, current, limits.maxSkillContentChars)) return `SKILL.md 内容超过 ${limits.maxSkillContentChars} 个字符。${CONTENT_SPLIT_HINT}`;
1065
1065
  return null;
1066
1066
  }
1067
1067
  /**
@@ -1076,8 +1076,8 @@ function authoringFeedback(frontmatter) {
1076
1076
  const over60 = description.length > 60;
1077
1077
  const hasColon = description.includes(":");
1078
1078
  const lines = [];
1079
- lines.push(over60 ? `Description is ${description.length}/60 characters — exceeds the 60-char authoring bar (Hermes standard; the catalog truncates at the configured platform cap).` : `Description ${description.length}/60 characters — within the authoring bar.`);
1080
- if (hasColon) lines.push("Description contains a colon — wrap the whole value in double quotes.");
1079
+ lines.push(over60 ? `描述为 ${description.length}/60 个字符 —— 超过 60 字符的作者规范上限(Hermes 标准;技能目录会按平台配置的上限截断)。` : `描述为 ${description.length}/60 个字符 —— 在作者规范上限之内。`);
1080
+ if (hasColon) lines.push("描述里含冒号 —— 请用双引号把整个取值包起来。");
1081
1081
  return {
1082
1082
  descriptionChars: description.length,
1083
1083
  over60,
@@ -1157,6 +1157,34 @@ function bodyCost(content) {
1157
1157
  };
1158
1158
  }
1159
1159
  //#endregion
1160
+ //#region lib/types/guards.js
1161
+ /**
1162
+ * The family's plain-object predicate, in ONE place (v46 G7.1 / audit A8).
1163
+ *
1164
+ * The audit found the same guard — "a non-null object that is not an array" — rewritten fifteen
1165
+ * times across nine packages, four times inside core itself, and with two byte-identical `asRecord`
1166
+ * copies in evolution-policy and evolution-threat. This module is the single definition; callers
1167
+ * import it instead of spelling the clause again.
1168
+ *
1169
+ * @module @lmzhen/dsh-evolution-core
1170
+ */
1171
+ /**
1172
+ * Is this a non-null, non-array object (what JSON calls an object)?
1173
+ * @param value - the value to test.
1174
+ * @returns whether it is a plain object.
1175
+ */
1176
+ function isRecord(value) {
1177
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1178
+ }
1179
+ /**
1180
+ * Read a value as a record, or undefined when it is not one.
1181
+ * @param value - the value to read.
1182
+ * @returns the value typed as a record, or undefined.
1183
+ */
1184
+ function asRecord(value) {
1185
+ return isRecord(value) ? value : void 0;
1186
+ }
1187
+ //#endregion
1160
1188
  //#region lib/types/probe.js
1161
1189
  function probePresent(value) {
1162
1190
  return {
@@ -1356,37 +1384,6 @@ function evolutionIoAdapter(provider) {
1356
1384
  }
1357
1385
  };
1358
1386
  }
1359
- /**
1360
- * F-367 (②): lock paths whose release (the finally `rm`) failed. The next write
1361
- * to the same file proactively recycles our own leftover lock — the holder is
1362
- * us, so a leftover is stale by definition. Module-level by design: it must
1363
- * survive across `nodeEvolutionIo()` instances for the self-heal to be
1364
- * effective. (Not a pure function — the cross-call state is the intent.)
1365
- * V4-05: this set — not an mtime heuristic — is the ONLY signal that a
1366
- * same-pid lock is a leftover rather than a live in-process task, so it is the
1367
- * sole gate for the self-pid recycle branch. Exported (read-only in practice)
1368
- * so `io.spec.ts` can drive the self-heal path deterministically.
1369
- */
1370
- const pendingSelfCleanup = /* @__PURE__ */ new Map();
1371
- /** P2-7 (v11)/P3-10 (v14): record a failed release so the next write to this
1372
- * path self-heals it, keeping the map's 64-entry cap — a never-re-touched path
1373
- * must not pin it, while a path whose lock still EXISTS is the last to drop
1374
- * (dropping it disables the self-heal for a lock that is really there). */
1375
- const recordPendingSelfCleanup = async (lock, token) => {
1376
- if (pendingSelfCleanup.size >= 64) {
1377
- let droppable;
1378
- for (const candidate of pendingSelfCleanup.keys()) {
1379
- if (candidate === lock) continue;
1380
- if (await readFile(candidate, "utf8").then(() => false, () => true)) {
1381
- droppable = candidate;
1382
- break;
1383
- }
1384
- }
1385
- const victim = droppable ?? pendingSelfCleanup.keys().next().value;
1386
- if (victim !== void 0) pendingSelfCleanup.delete(victim);
1387
- }
1388
- pendingSelfCleanup.set(lock, token);
1389
- };
1390
1387
  const RENAME_RETRY_BASE_MS = 50;
1391
1388
  const RENAME_RETRY_MAX_DELAY_MS = 800;
1392
1389
  const RENAME_RETRY_MAX_ATTEMPTS = 6;
@@ -1514,73 +1511,14 @@ function isProcessAlive(pid) {
1514
1511
  return error?.code === "EPERM";
1515
1512
  }
1516
1513
  }
1517
- /** F-17 (v18): the write-lock protocol is a cross-module contract — the lock
1518
- * file is `<target>.lock` and its body is `<pid>:<token>`. This module creates
1519
- * them (`withWriteLock`) and `skill-store`'s probes/sweepers parse them, so both
1520
- * consume these two constants instead of repeating the literals. */
1521
- const LOCK_SUFFIX = ".lock";
1522
- /** Writer-lock body shape: a decimal pid, a colon, then the claim token. A
1523
- * torn body (no parsable pid) still matches the `\\d+:` prefix rule only when
1524
- * the pid part is intact, which is what the takeover probe needs. The capture
1525
- * group feeds `parseLockBody` (v20 A-1: skill-store's sweepers consume this
1526
- * helper instead of re-inlining a third regex copy — F-17 single-source). */
1527
- const LOCK_BODY_RE = /^(\d+):[0-9a-f]*$/;
1528
- /** Parse a writer-lock body into its holder pid. `null` when the body does
1529
- * not have the `pid:token` shape at all (e.g. a user support file named
1530
- * `*.lock`) — callers leave such files alone. A shape-matching body always
1531
- * yields a number, possibly `0` (state corruption / hand edit): there is no
1532
- * pid 0, so a `0` holder must be treated as DEAD by the caller —
1533
- * `isProcessAlive(0)` signals the caller's own process group on POSIX and
1534
- * answers true, so probe sites must guard `holder > 0` before probing. */
1535
- function parseLockBody(body) {
1536
- const match = LOCK_BODY_RE.exec(body.trim());
1537
- return match === null ? null : Number(match[1]);
1538
- }
1539
- /**
1540
- * V27 G0.2 (EVO-IO-01): the write lock named by this claim is no longer ours
1541
- * at the commit point — a takeover reclaimed it while we were inside the
1542
- * critical section. The lock layer converts this into a retry of the whole
1543
- * read-modify-write; it must never reach a caller as a successful write.
1544
- */
1545
- var LostWriteLock = class extends Error {
1546
- constructor() {
1547
- super("evolution-io: the write lock was reclaimed before the commit");
1548
- this.name = "LostWriteLock";
1549
- }
1550
- };
1514
+ /** How long an abandoned quarantine copy is kept for operator rescue. G2 S4: hoisted to module scope
1515
+ * (it used to live inside `nodeEvolutionIo`, in the region the lock protocol occupied). */
1516
+ const CORRUPT_SWEEP_AGE_MS = 168 * 36e5;
1551
1517
  /** V27 G1.4: every quarantine-copy name this family has ever produced —
1552
1518
  * `<file>.corrupt` (current fixed name), `<file>.corrupt.<epoch>`, and the
1553
1519
  * v10-05 legacy `<file>.corrupt-<epoch>-<rand>` series. A user support file
1554
1520
  * keeps a final extension (`.corrupt-backup.md`) and therefore stays. */
1555
1521
  const CORRUPT_COPY_RE = /\.corrupt(\.\d+|-\d+-[0-9a-z]+)?$/;
1556
- /** V27 G1.1: the three takeover windows, as protocol constants at ONE place
1557
- * (the inline copies that used to live in the acquisition loop, plus the dead
1558
- * branch's bare `1000`, are gone). */
1559
- /** A named holder that is gone, past this age, is reclaimed. Fits inside the
1560
- * `lockAttempts * 50ms` retry budget so a dead holder's lock is reachable
1561
- * within one budget (v19 gate arithmetic: budget >= 2 x threshold). */
1562
- const DEAD_LOCK_TAKEOVER_MS = 1e3;
1563
- /** No body at all: nothing attributes the lock to a holder, so this is the one
1564
- * branch that cannot probe liveness — it must outlast any plausible stall
1565
- * between create and body write (V27 G0.2: 1s was below what a loaded machine
1566
- * actually took, which let a peer delete a live holder's lock). */
1567
- const EMPTY_LOCK_TAKEOVER_MS = 3e4;
1568
- /** A body with no parseable pid (crash mid-write): 1h, far above any legal hold
1569
- * and far below "forever". */
1570
- const LOCK_TEAR_TAKEOVER_MS = 36e5;
1571
- /** A2 (audit P1-2): even a lock whose holder pid probes ALIVE is reclaimable
1572
- * past this age. Liveness-by-pid cannot distinguish the original holder from
1573
- * an unrelated process the OS later assigned the same pid, so the plain alive
1574
- * probe let one recycled pid brick every writer of one state file forever.
1575
- * No write in this family holds a lock for more than minutes (the longest is
1576
- * the ~120s review window), so a day-old "alive" lock is a recycled pid, not
1577
- * a live writer. */
1578
- const ALIVE_LOCK_TAKEOVER_MS = 864e5;
1579
- /** P2-27 (v37): the commit-point ownership re-read. A transient read failure
1580
- * (EACCES/EMFILE/antivirus hold) must not abort a valid RMW, so the read is
1581
- * retried in place; only a still-unreadable lock fails the attempt. */
1582
- const OWNERSHIP_READ_ATTEMPTS = 3;
1583
- const OWNERSHIP_READ_RETRY_MS = 20;
1584
1522
  /**
1585
1523
  * V27 G1.3: the error `commitTmp` throws when the rename landed but the parent
1586
1524
  * directory fsync failed — the bytes ARE visible, only their durability is
@@ -1593,39 +1531,12 @@ function isCommittedWarning(error) {
1593
1531
  return error?.committed === true;
1594
1532
  }
1595
1533
  /**
1596
- * V27 G1.1: the lock-takeover decision as ONE pure function, so the protocol is
1597
- * testable and exhaustive instead of being an inline expression inside the
1598
- * acquisition loop:
1599
- * - `none` the lock is fresh, or its holder is alive within the alive
1600
- * window → wait, never steal;
1601
- * - `dead` a named holder that is gone past the dead threshold, OR whose
1602
- * pid still probes alive but whose lock is older than the alive
1603
- * window (A2: a recycled pid must not hold the file forever);
1604
- * - `empty` no body at all: nothing attributes it to a holder, so only the
1605
- * wide `emptyAfterMs` window may reclaim it;
1606
- * - `corrupt` a body with no parseable pid (a crash mid-write), past the 1h
1607
- * tear threshold.
1608
- * The age thresholds compare against `nowMs - mtimeMs` with `>` so a lock whose
1609
- * age EQUALS the threshold is not yet reclaimed (the boundary the v19 gate fix
1610
- * pinned).
1611
- */
1612
- function decideTakeover(probe) {
1613
- const age = (probe.nowMs ?? Date.now()) - probe.mtimeMs;
1614
- const holder = Number(probe.body.split(":")[0] ?? "");
1615
- const namedHolder = Number.isInteger(holder) && holder > 0;
1616
- if (probe.body === "") return age > (probe.emptyAfterMs ?? 3e4) ? "empty" : "none";
1617
- if (!namedHolder) return age > (probe.corruptAfterMs ?? 36e5) ? "corrupt" : "none";
1618
- if (probe.alive(holder)) return age > (probe.aliveAfterMs ?? 864e5) ? "dead" : "none";
1619
- return age > (probe.deadAfterMs ?? 1e3) ? "dead" : "none";
1620
- }
1621
- /**
1622
- * Build the Node IO backend. `lockAttempts` scales the write-lock retry budget
1623
- * (attempts × 50ms); the default 40 (~2s, rc.69) covers production contention,
1624
- * while contention TESTS on a loaded runner may raise it (e.g. 240 ≈ 12s) —
1625
- * V10-06 integration: full-suite parallel load made 32-way takeover bursts
1626
- * exceed the default budget and fail loud.
1627
- */
1628
- function nodeEvolutionIo(lockAttempts = 40) {
1534
+ * Build the Node IO backend: plain fs promises, durable tmp + rename commits (crash atomicity) and
1535
+ * ONE concurrency guarantee of its own — writes to the same path are serialized in-process (see
1536
+ * `inProcessWrite`). G2 S4 removed the cross-process write lock and its takeover protocol; the
1537
+ * plugin path gets cross-process safety from the platform provider (`ctx.fs`).
1538
+ */
1539
+ function nodeEvolutionIo() {
1629
1540
  /**
1630
1541
  * v43 S1-4: delegates to the canonical `isMissingPath` (probe.ts) — one
1631
1542
  * definition for the whole family instead of a second copy of the same two
@@ -1640,201 +1551,34 @@ function nodeEvolutionIo(lockAttempts = 40) {
1640
1551
  /** True when the pid is alive (single source: `isProcessAlive`). */
1641
1552
  const isAlive = isProcessAlive;
1642
1553
  /**
1643
- * V10-06 (P1-1 integration fix): a takeover TICKET must never be reclaimed
1644
- * while its (live) holder can still be committing — the C-28 rename retry
1645
- * budget is ~2.35s under AV/indexer pressure, so the former 1s ticket age
1646
- * threshold let a contender steal an in-flight ticket, double-enter the
1647
- * critical section and drop one RMW (observed as 5/6 increments under
1648
- * full-suite load). 5s = retry budget + comfortable margin; a DEAD holder's
1649
- * ticket is still reclaimed immediately via the pid probe.
1650
- */
1651
- const TICKET_STALE_MS = 5e3;
1652
- /**
1653
- * Cross-process write lock (claw `withFileLock` parity): an O_EXCL lock file
1654
- * guards the atomic write. A >1s-old lock is taken over ONLY after probing
1655
- * the holder pid it carries (rc.66): a LIVE holder is never stolen, so a
1656
- * slow writer no longer loses its lock to a peer at the threshold (the
1657
- * takeover is the only best-effort surface; the retry budget fails loud —
1658
- * rc.65 — instead of ever proceeding unlocked). Budget = 40 * 50ms (~2s,
1659
- * rc.69): 8-writer contention bursts on a loaded CI runner exceed 10
1660
- * attempts (500ms), and a fail-loud throw was observed instead of a clean
1661
- * serialization.
1662
- * 0.3.17 (E-8): acquisition and the task are now SEPARATE try blocks — a
1663
- * task error (win32 rename/EBUSY surfaces as EPERM) used to be mistaken for
1664
- * lock contention, retried up to 40x and finally reported as
1665
- * "could not acquire write lock" while the real cause was hidden.
1666
- * 0.3.17 (E-8a): the takeover threshold (1000ms) now fits INSIDE the ~2s
1667
- * retry budget (budget >= 2 x threshold), so a dead holder's lock is
1668
- * actually recoverable within one budget instead of being arithmetically
1669
- * unreachable.
1670
- * 0.3.21 (F-101), V4-04: takeover re-reads the lock right before acting and
1671
- * only proceeds when the content still names the dead pid — a peer that
1672
- * acquired the lock after our stale probe wrote its own pid, and deleting a
1673
- * LIVE lock is the double-hold (concurrent task) the probe must prevent. The
1674
- * re-read is necessary but not sufficient: two peers can both pass it, so the
1675
- * commit itself is an atomic rename to a unique name (only one peer's rename
1676
- * can succeed; the loser's source is gone). A naive rm lets the later peer
1677
- * delete the winner's freshly re-acquired live lock.
1678
- * 0.3.21 (F-367), V4-05: a self-pid lock is recycled ONLY when the failed
1679
- * release was recorded in pendingSelfCleanup — never on age alone, because an
1680
- * mtime over 1s is indistinguishable from a long task still executing in this
1681
- * process, and recycling that live lock would double-hold it. A failure to
1682
- * release in finally is recorded so the next write self-heals.
1683
- * V10-07 (P2-1): a third takeover shape — a TORN body (non-empty, no
1684
- * parseable pid) — is taken over after a wide 1h threshold with a
1685
- * console.warn; before this branch such a lock blocked every future writer
1686
- * forever.
1687
- * V27 G0.2 (EVO-IO-01): the CONTRACT for `task` is therefore: commit only
1688
- * after calling the `assertOwned` callback it receives, immediately before the
1689
- * rename (or delete) that publishes the result. A takeover may reclaim a lock
1690
- * whose holder is alive — an empty body cannot be attributed to a pid, and a
1691
- * peer can read a stale stat — and the holder cannot see it from the handle it
1692
- * opened (on POSIX its write lands on the UNLINKED inode). With the guard, a
1693
- * reclaimed claim aborts the RMW (`LostWriteLock`, converted into a retry
1694
- * here) instead of overwriting the current holder's result; without it, two
1695
- * writers commit and one update is silently lost.
1554
+ * G2 S4: the family write-lock protocol is retired. Concurrency is the PLATFORM’s job now — the
1555
+ * plugin path requires `ctx.fs`, whose writes are versioned — so this backend keeps exactly one
1556
+ * guarantee of its own: writes to the SAME path inside THIS process are serialized (a promise
1557
+ * chain per path: no timers, no lock files, no takeover windows). Cross-process RMW safety comes
1558
+ * from the provider; a standalone caller sharing a directory across processes must not rely on
1559
+ * this backend alone. The durable tmp + rename commit stays — crash atomicity and mutual
1560
+ * exclusion are different concerns.
1696
1561
  */
1697
- const withWriteLock = async (path, task) => {
1698
- const lock = `${path}${LOCK_SUFFIX}`;
1699
- let myClaim = "";
1700
- for (let attempt = 0; attempt < lockAttempts; attempt += 1) {
1701
- try {
1702
- myClaim = `${process.pid}:${randomBytes(4).toString("hex")}`;
1703
- await writeFile(lock, myClaim, { flag: "wx" });
1704
- } catch (error) {
1705
- const code = error?.code;
1706
- if (code === "EEXIST" || code === "EPERM") {} else {
1707
- await rm(lock, { force: true }).catch(() => {});
1708
- throw error;
1709
- }
1710
- try {
1711
- const st = await stat(lock);
1712
- const holderRead = await readFile(lock, "utf8").then((body) => ({
1713
- ok: true,
1714
- body
1715
- }), () => ({
1716
- ok: false,
1717
- body: ""
1718
- }));
1719
- if (!holderRead.ok) continue;
1720
- const holderContent = holderRead.body;
1721
- const holder = Number(holderContent.split(":")[0] ?? "");
1722
- if (holder === process.pid && pendingSelfCleanup.has(lock)) {
1723
- const token = pendingSelfCleanup.get(lock);
1724
- if (await readFile(lock, "utf8").catch(() => "") === token) try {
1725
- await rm(lock, { force: true });
1726
- pendingSelfCleanup.delete(lock);
1727
- } catch {}
1728
- continue;
1729
- }
1730
- const decision = decideTakeover({
1731
- body: holderContent,
1732
- mtimeMs: st.mtimeMs,
1733
- alive: isAlive
1734
- });
1735
- if (decision !== "none") {
1736
- console.warn(`evolution-io: taking over write lock ${lock} (branch=stale${decision[0]?.toUpperCase()}${decision.slice(1)}, body=${JSON.stringify(holderContent)}, ageMs=${Date.now() - st.mtimeMs}, holderPid=${Number.isInteger(holder) && holder > 0 ? holder : "none"})`);
1737
- const currentRead = await readFile(lock, "utf8").then((body) => ({
1738
- ok: true,
1739
- body
1740
- }), () => ({
1741
- ok: false,
1742
- body: ""
1743
- }));
1744
- if (!currentRead.ok) continue;
1745
- if (currentRead.body === holderContent) {
1746
- const ticket = `${lock}.next`;
1747
- try {
1748
- const ticketBody = await readFile(ticket, "utf8").catch(() => "");
1749
- const ticketMtime = await stat(ticket).then((s) => s.mtimeMs, () => 0);
1750
- const ticketHolder = Number(ticketBody.split(":")[0] ?? "");
1751
- if ((!Number.isInteger(ticketHolder) || ticketHolder <= 0 || !isAlive(ticketHolder) || Date.now() - ticketMtime > TICKET_STALE_MS) && (ticketBody !== "" || Date.now() - ticketMtime > TICKET_STALE_MS)) await rm(ticket, { force: true }).catch(() => {});
1752
- } catch {}
1753
- try {
1754
- await writeFile(ticket, `${process.pid}:${randomBytes(4).toString("hex")}`, { flag: "wx" });
1755
- } catch {
1756
- continue;
1757
- }
1758
- const verifyRead = await readFile(lock, "utf8").then((body) => ({
1759
- ok: true,
1760
- body
1761
- }), () => ({
1762
- ok: false,
1763
- body: ""
1764
- }));
1765
- if (!verifyRead.ok) {
1766
- await rm(ticket, { force: true }).catch(() => {});
1767
- continue;
1768
- }
1769
- const verify = verifyRead.body;
1770
- if (verify === holderContent) {
1771
- const verifyHolder = Number(verify.split(":")[0] ?? "");
1772
- if (!(Number.isInteger(verifyHolder) && verifyHolder > 0 && isAlive(verifyHolder))) await rm(lock, { force: true }).catch(() => {});
1773
- }
1774
- await rm(ticket, { force: true }).catch(() => {});
1775
- continue;
1776
- }
1777
- }
1778
- } catch {
1779
- continue;
1780
- }
1781
- await new Promise((resolve) => setTimeout(resolve, 50));
1782
- continue;
1783
- }
1784
- const assertOwned = async () => {
1785
- let body = "";
1786
- let readable = false;
1787
- for (let read = 0; read < OWNERSHIP_READ_ATTEMPTS && !readable; read += 1) {
1788
- if (read > 0) await new Promise((resolve) => setTimeout(resolve, OWNERSHIP_READ_RETRY_MS));
1789
- const attempt = await readFile(lock, "utf8").then((value) => ({
1790
- ok: true,
1791
- value
1792
- }), () => ({
1793
- ok: false,
1794
- value: ""
1795
- }));
1796
- if (attempt.ok) {
1797
- body = attempt.value;
1798
- readable = true;
1799
- }
1800
- }
1801
- if (!readable) {
1802
- console.warn(`evolution-io: write lock ${lock} could not be read at the commit point (ownership unverified, ours: ${JSON.stringify(myClaim)}) — aborting this read-modify-write and retrying under a fresh acquisition`);
1803
- throw new LostWriteLock();
1804
- }
1805
- if (body !== myClaim) {
1806
- console.warn(`evolution-io: write lock ${lock} was reclaimed by another writer before the commit (on disk now: ${JSON.stringify(body)}, ours: ${JSON.stringify(myClaim)}) — aborting this read-modify-write and retrying under a fresh acquisition`);
1807
- throw new LostWriteLock();
1808
- }
1809
- };
1810
- try {
1811
- return await task(assertOwned);
1812
- } catch (error) {
1813
- if (error instanceof LostWriteLock) continue;
1814
- throw error;
1815
- } finally {
1816
- const mine = await readFile(lock, "utf8").then((body) => ({
1817
- ok: true,
1818
- body
1819
- }), (error) => ({
1820
- ok: false,
1821
- missing: isMissing(error)
1822
- }));
1823
- if (!mine.ok) {
1824
- if (!mine.missing) await recordPendingSelfCleanup(lock, myClaim);
1825
- } else if (mine.body === myClaim) await rm(lock, {
1826
- force: true,
1827
- maxRetries: 20,
1828
- retryDelay: 100
1829
- }).catch(() => recordPendingSelfCleanup(lock, myClaim));
1830
- }
1562
+ const queues = /* @__PURE__ */ new Map();
1563
+ const inProcessWrite = async (path, run) => {
1564
+ const result = (queues.get(path) ?? Promise.resolve()).then(run, run);
1565
+ const settled = result.then(() => {}, () => {});
1566
+ queues.set(path, settled);
1567
+ try {
1568
+ return await result;
1569
+ } finally {
1570
+ if (queues.get(path) === settled) queues.delete(path);
1831
1571
  }
1832
- throw new Error(`could not acquire write lock for ${path} after ${lockAttempts} attempts`);
1833
1572
  };
1834
- /** 0.3.17 (E-8b): sweep tmp files a crashed writer left behind — same
1835
- * `<target>.<pid>.<rand>.tmp` shape, older than 1h AND held by a dead pid.
1836
- * Lazy: only the directory a write is about to touch gets swept, once per
1837
- * write, inside the write lock. */
1573
+ /**
1574
+ * Sweep transient files a crashed writer left behind, lazily: once per write, for the directory
1575
+ * that write is about to touch.
1576
+ * `<target>.<pid>.<rand>.tmp` — the durable-write tmp. This process's own leftover is recycled
1577
+ * immediately (a live writer's tmp is a current write, not a crash artifact); a foreign tmp
1578
+ * needs an age past an hour AND a dead holder.
1579
+ * `.corrupt*` — quarantine copies past their rescue window.
1580
+ * G2 S4: the lock and takeover-ticket branches are gone with the write-lock protocol.
1581
+ */
1838
1582
  const sweepStaleTmps = async (path) => {
1839
1583
  const dir = dirname(path);
1840
1584
  const base = basename(path);
@@ -1844,12 +1588,7 @@ function nodeEvolutionIo(lockAttempts = 40) {
1844
1588
  } catch {
1845
1589
  return;
1846
1590
  }
1847
- const prefix = `${base}.`;
1848
- const lockName = `${base}${LOCK_SUFFIX}`;
1849
- const ticketName = `${lockName}.next`;
1850
- const CORRUPT_SWEEP_AGE_MS = 168 * 36e5;
1851
1591
  for (const name of entries) {
1852
- if (!name.startsWith(prefix) || name === lockName) continue;
1853
1592
  const tmpMatch = /^(.*)\.(\d+)\.([0-9a-f]+)\.tmp$/.exec(name);
1854
1593
  if (tmpMatch !== null && tmpMatch[1] === base) {
1855
1594
  const tmpPath = join(dir, name);
@@ -1861,19 +1600,7 @@ function nodeEvolutionIo(lockAttempts = 40) {
1861
1600
  } catch {}
1862
1601
  continue;
1863
1602
  }
1864
- if (name === ticketName) {
1865
- const ticketPath = join(dir, name);
1866
- try {
1867
- const body = await readFile(ticketPath, "utf8").catch(() => "");
1868
- const holder = Number(body.split(":")[0] ?? "");
1869
- const st = await stat(ticketPath);
1870
- const dead = !Number.isInteger(holder) || holder <= 0 || !isAlive(holder);
1871
- const old = Date.now() - st.mtimeMs > TICKET_STALE_MS;
1872
- if (dead || old) await rm(ticketPath, { force: true });
1873
- } catch {}
1874
- continue;
1875
- }
1876
- if (CORRUPT_COPY_RE.test(name)) {
1603
+ if (name.startsWith(base + ".") && CORRUPT_COPY_RE.test(name)) {
1877
1604
  const corruptPath = join(dir, name);
1878
1605
  try {
1879
1606
  const st = await stat(corruptPath);
@@ -1893,16 +1620,14 @@ function nodeEvolutionIo(lockAttempts = 40) {
1893
1620
  },
1894
1621
  async writeText(path, content) {
1895
1622
  await mkdir(dirname(path), { recursive: true });
1896
- await withWriteLock(path, async (assertOwned) => {
1623
+ await inProcessWrite(path, async () => {
1897
1624
  await sweepStaleTmps(path);
1898
- const tmp = await writeDurableTmp(path, content);
1899
- await assertOwned();
1900
- await commitTmp(tmp, path);
1625
+ await commitTmp(await writeDurableTmp(path, content), path);
1901
1626
  });
1902
1627
  },
1903
1628
  async transact(path, task) {
1904
1629
  await mkdir(dirname(path), { recursive: true });
1905
- await withWriteLock(path, async (assertOwned) => {
1630
+ await inProcessWrite(path, async () => {
1906
1631
  await sweepStaleTmps(path);
1907
1632
  let current;
1908
1633
  try {
@@ -1913,14 +1638,11 @@ function nodeEvolutionIo(lockAttempts = 40) {
1913
1638
  }
1914
1639
  const next = await task(current);
1915
1640
  if (next === null) {
1916
- await assertOwned();
1917
1641
  await rm(path, { force: true });
1918
1642
  return;
1919
1643
  }
1920
1644
  if (next === current) return;
1921
- const tmp = await writeDurableTmp(path, next);
1922
- await assertOwned();
1923
- await commitTmp(tmp, path);
1645
+ await commitTmp(await writeDurableTmp(path, next), path);
1924
1646
  });
1925
1647
  },
1926
1648
  async remove(path) {
@@ -1983,6 +1705,121 @@ function nodeEvolutionIo(lockAttempts = 40) {
1983
1705
  }
1984
1706
  };
1985
1707
  }
1708
+ const defaultFs = {
1709
+ writeFileSync: (path, data) => {
1710
+ writeFileSync(path, data);
1711
+ },
1712
+ existsSync: (path) => existsSync(path),
1713
+ copyFileSync: (from, to) => {
1714
+ copyFileSync(from, to);
1715
+ },
1716
+ renameSync: (from, to) => {
1717
+ renameSync(from, to);
1718
+ },
1719
+ rmSync: (path, options) => {
1720
+ rmSync(path, options);
1721
+ },
1722
+ mtime: (path) => {
1723
+ try {
1724
+ return statSync(path).mtimeMs;
1725
+ } catch (error) {
1726
+ if (isMissingPath(error)) return null;
1727
+ throw error;
1728
+ }
1729
+ }
1730
+ };
1731
+ /**
1732
+ * S6.3 (E-40): atomically replace a set of files within one directory.
1733
+ * Each file is staged to a sibling `<name>.tmp`, then renamed into place
1734
+ * (atomic on the same filesystem); a pre-existing file keeps a single
1735
+ * `<name>.bak`. A failure at ANY staging step removes the staged temps and
1736
+ * leaves the previous files untouched — a half-written target is impossible
1737
+ * up to the staging and backup phases.
1738
+ *
1739
+ * F-211: the rename COMMIT phase does not carry the same guarantee. When a
1740
+ * filesystem refuses to overwrite the destination, the code removes it and
1741
+ * retries the rename; if the retry ALSO fails the destination is gone. The
1742
+ * single `.bak` created above still holds the previous content, so the target
1743
+ * is restored from it (best-effort) before the original rename error is
1744
+ * rethrown, so a failed commit never leaves the file missing.
1745
+ *
1746
+ * V4-17: the `.bak` is refreshed to the committed content after every
1747
+ * successful commit, so a later failed commit always recovers to the most
1748
+ * recent good generation (a once-at-first-install `.bak` could otherwise
1749
+ * restore content several installs old). The recovery error names the `.bak`
1750
+ * source (and mtime when available) so the caller knows the generation it
1751
+ * restored from.
1752
+ */
1753
+ function atomicWriteFiles(targetDir, writes, fs = defaultFs, logger = console.warn) {
1754
+ const stage = (name) => join(targetDir, `${name}.tmp`);
1755
+ const dupes = [...new Set(writes.map((write) => write.name).filter((name, index, all) => all.indexOf(name) !== index))];
1756
+ if (dupes.length > 0) throw new Error(`atomicWriteFiles: duplicate input names: ${dupes.join(", ")}`);
1757
+ try {
1758
+ for (const { name, content } of writes) fs.writeFileSync(stage(name), content);
1759
+ for (const { name } of writes) {
1760
+ const finalPath = join(targetDir, name);
1761
+ const bakPath = join(targetDir, `${name}.bak`);
1762
+ if (fs.existsSync(finalPath) && !fs.existsSync(bakPath)) fs.copyFileSync(finalPath, bakPath);
1763
+ }
1764
+ const committed = [];
1765
+ for (const { name } of writes) {
1766
+ const finalPath = join(targetDir, name);
1767
+ const bakPath = join(targetDir, `${name}.bak`);
1768
+ const tmp = stage(name);
1769
+ try {
1770
+ fs.renameSync(tmp, finalPath);
1771
+ committed.push(name);
1772
+ } catch {
1773
+ try {
1774
+ fs.rmSync(finalPath, { force: true });
1775
+ } catch (removeError) {
1776
+ const already = committed.length > 0 ? `; already committed: ${committed.join(", ")}` : "";
1777
+ throw new Error(`atomicWriteFiles: could not remove "${name}" to replace it (${removeError instanceof Error ? removeError.message : String(removeError)})${already}`);
1778
+ }
1779
+ try {
1780
+ fs.renameSync(tmp, finalPath);
1781
+ committed.push(name);
1782
+ } catch (renameError) {
1783
+ const already = committed.length > 0 ? `; already committed: ${committed.join(", ")}` : "";
1784
+ if (fs.existsSync(bakPath)) {
1785
+ try {
1786
+ fs.copyFileSync(bakPath, finalPath);
1787
+ } catch {
1788
+ throw new Error(`atomicWriteFiles: "${name}" was removed but recovery from ${bakPath} failed (${renameError instanceof Error ? renameError.message : String(renameError)}); verify the file manually${already}`);
1789
+ }
1790
+ const bakMtime = fs.mtime?.(bakPath);
1791
+ const when = bakMtime !== null && bakMtime !== void 0 && Number.isFinite(bakMtime) ? ` (bak mtime ${new Date(bakMtime).toISOString()})` : "";
1792
+ 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}`);
1793
+ }
1794
+ throw new Error(`${renameError instanceof Error ? renameError.message : String(renameError)}${already}`);
1795
+ }
1796
+ }
1797
+ }
1798
+ for (const { name } of writes) {
1799
+ const finalPath = join(targetDir, name);
1800
+ try {
1801
+ if (fs.existsSync(finalPath)) fs.copyFileSync(finalPath, join(targetDir, `${name}.bak`));
1802
+ } catch (refreshError) {
1803
+ 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`);
1804
+ }
1805
+ }
1806
+ } catch (error) {
1807
+ const cleanupFailures = [];
1808
+ for (const { name } of writes) try {
1809
+ fs.rmSync(stage(name), {
1810
+ force: true,
1811
+ recursive: true
1812
+ });
1813
+ } catch (cleanupError) {
1814
+ cleanupFailures.push(`${name}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
1815
+ }
1816
+ if (cleanupFailures.length > 0) {
1817
+ const original = error instanceof Error ? error.message : String(error);
1818
+ throw new Error(`${original} (staged-temp cleanup also failed: ${cleanupFailures.join("; ")})`);
1819
+ }
1820
+ throw error;
1821
+ }
1822
+ }
1986
1823
  //#endregion
1987
1824
  //#region lib/types/usage.js
1988
1825
  /**
@@ -2093,7 +1930,7 @@ async function mutateUsage(root, io, task, options = {}) {
2093
1930
  let parsed = null;
2094
1931
  try {
2095
1932
  const probe = JSON.parse(current);
2096
- if (probe !== null && typeof probe === "object" && !Array.isArray(probe)) parsed = probe;
1933
+ if (isRecord(probe)) parsed = probe;
2097
1934
  } catch {
2098
1935
  parsed = null;
2099
1936
  }
@@ -3221,7 +3058,7 @@ async function readJsonBody(req) {
3221
3058
  */
3222
3059
  async function readJsonObject(req) {
3223
3060
  const body = await readJsonBody(req);
3224
- return body !== null && typeof body === "object" && !Array.isArray(body) ? body : null;
3061
+ return isRecord(body) ? body : null;
3225
3062
  }
3226
3063
  //#endregion
3227
3064
  //#region lib/types/env.js
@@ -3252,7 +3089,7 @@ function allowRowCollisions(env = process.env) {
3252
3089
  //#endregion
3253
3090
  //#region lib/types/errors.js
3254
3091
  /**
3255
- * THE error-code table: E-301…E-319 with the exact text each one emits (E-317…E-319 are the
3092
+ * THE error-code table: E-301…E-322 with the exact text each one emits (E-317…E-319 are the
3256
3093
  * write-admission gates' own refusals).
3257
3094
  *
3258
3095
  * The codes used to be spelled out at 33 call sites across two packages, so one
@@ -3265,32 +3102,33 @@ function allowRowCollisions(env = process.env) {
3265
3102
  * @module @lmzhen/dsh-evolution-core
3266
3103
  */
3267
3104
  const EVOLUTION_ERRORS = Object.freeze({
3268
- "e-305-this-invocation-carries-no": "E-305: this invocation carries no agent — `%a1%` needs a session-backed call (run it from a session in the GUI or the CLI).",
3269
- "e-306-this-deployment-stages-foreground": "E-306: this deployment stages foreground writes, but `/evolution %a1%` is not replayable through the skill runner — there is nothing to stage. Run it from a session whose approval policy is 'never', or set `stageForeground: false` on the evolution-approval row, then repeat the command.",
3270
- "e-301-approval-service-not-mounted": "E-301: approval service not mounted — pending writes cannot be listed or replayed. Next: the evolution-approval row ships with evolution-host/evolution-all — run /evolution doctor to see which services are mounted.",
3271
- "e-301-approval-service-not-mounted-2": "E-301: approval service not mounted. Next: the evolution-approval row ships with evolution-host/evolution-all — run /evolution doctor to see which services are mounted.",
3272
- "e-304-the-mounted-approval-service": "E-304: the mounted approval service predates the release capability — reject the record instead.",
3273
- "e-302-curator-service-not-mounted": "E-302: curator service not mounted. Next: mount the evolution-curator row (evolution-host/evolution-all) and run /evolution doctor.",
3274
- "e-305-the-invocation-agent-exposes": "E-305: the invocation agent exposes neither `followup` nor `inject` — %a1% has no delivery channel.",
3275
- "e-306-this-deployment-stages-foreground-2": "E-306: this deployment stages foreground writes, but this invocation carries no agent session — `/evolution restructure` would stage a record with no session attribution. Run it from a session in the GUI or the CLI, or set `stageForeground: false` on the evolution-approval row, then repeat the command.",
3276
- "e-303-replay-service-not-mounted": "E-303: replay service not mounted. Next: mount the evolution-replay row (evolution-host/evolution-all) and run /evolution doctor.",
3277
- "e-307-the-settings-service-could": "E-307: the settings service could not report its sections (%a1%). /evolution params needs the user layer to tell an override from a deployment value; run /evolution doctor to see which services are mounted.",
3278
- "e-308-unknown-parameter-group-group": "E-308: unknown parameter group \"%a1%\" — known groups: %a2%.",
3279
- "e-311-no-settings-service-is": "E-311: no settings service is mounted, so there is no user layer to write. Mount the settings row (packages/settings/settings-file) and retry; this command never edits cordis.yml — a deployment value belongs to the deployment. /evolution doctor lists the mounted services.",
3280
- "e-312-error-instanceof-Error-error": "E-312: %a1% — /evolution params lists the canonical ids.",
3281
- "e-313-unknown-parameter-id-evolution": "E-313: unknown parameter \"%a1%\" — /evolution params lists every registered id with its tier and user layer.",
3282
- "e-314-id-is-a-deployment": "E-314: \"%a1%\" is a deployment parameter (tier %a2%, owner %a3%) — it is written in cordis.yml on its plugin or policy row, not from a session. /evolution params shows who may write each row.",
3283
- "e-315-id-has-no-user": "E-315: \"%a1%\" has no user layer — owner %a2% publishes no settings namespace, so the value stays with the deployment.",
3284
- "e-307-the-settings-service-could-2": "E-307: the settings service could not report its sections (%a1%). /evolution policy set needs the current revision to write safely; run /evolution doctor to see which services are mounted.",
3285
- "e-316-namespace-namespace-is-not": "E-316: namespace \"%a1%\" is not registered — owner %a2% is not mounted in this composition, so its parameters cannot be written from here. /evolution params marks those rows 'unregistered'.",
3286
- "e-309-revision-conflict-you-sent": "E-309: revision conflict — you sent %a1%, the document stands at %a2%. Re-read with /evolution params --json and retry.",
3287
- "e-309-revision-conflict-message-Re": "E-309: revision conflict — %a1%. Re-read with /evolution params --json and retry.",
3288
- "e-310-the-settings-service-refused": "E-310: the settings service refused the write: %a1% — the owning plugin's rule stands (a cross-field pair, or a cap that may only be tightened). /evolution params shows the current value.",
3289
- "e-317-skill-write-not-confirmed": "E-317: skill \"%a1%\" was not %a2% — the confirmation prompt was declined or dismissed, so nothing was written. The prompt appears on every create and bare delete; repeat the call only if the operator asks for it.",
3290
- "e-318-skill-write-without-a-read": "E-318: skill \"%a1%\" was not read in this session, so this write is refused. Read it with the `skill` tool first (a read that failed does not count), then repeat the write.",
3291
- "e-320-the-profile-home-is-not": "E-320: the profile home is not reported by this host, so the legacy settings document cannot be located. /evolution migrate reads `<profile home>/settings.yaml.imported`; run it from a profile-launched session.",
3292
- "e-321-no-io-provider-is": "E-321: no evolution-io provider is mounted, so the legacy settings document cannot be read. Mount the evolution-io row (evolution-host/evolution-all) and retry.",
3293
- "e-319-skill-write-confirm-timed-out": "E-319: skill \"%a1%\" was not %a2% — the confirmation prompt went unanswered for %a3% second(s), so the write was cancelled. Nobody was there to decide: set `skillWriteConfirm` to `auto` to write without a prompt, keep `ask` to wait until someone answers, or raise `skillWriteConfirmTimeoutSeconds`."
3105
+ "e-305-this-invocation-carries-no": "E-305: 本次调用没有携带 agent —— `%a1%` 需要由会话支撑的调用(请在 GUI 或 CLI 的会话里运行)。",
3106
+ "e-306-this-deployment-stages-foreground": "E-306: 本部署会暂存前台写入,但 `/evolution %a1%` 无法通过技能运行器回放 —— 没有可暂存的内容。请在批准策略为 'never' 的会话里运行,或在 evolution-approval 行上设置 `stageForeground: false`,然后重试该命令。",
3107
+ "e-301-approval-service-not-mounted": "E-301: 批准服务未挂载:待批准写入无法列出或回放。挂载 evolution-approval 行(随 evolution-host/evolution-all 提供)后,运行 /evolution doctor 可查看已挂载的服务。",
3108
+ "e-301-approval-service-not-mounted-2": "E-301: 批准服务未挂载。下一步:evolution-approval 行随 evolution-host/evolution-all 一起提供 —— 运行 /evolution doctor 查看已挂载的服务。",
3109
+ "e-304-the-mounted-approval-service": "E-304: 已挂载的批准服务早于本能力发布 —— 请改为驳回该记录。",
3110
+ "e-302-curator-service-not-mounted": "E-302: 策展服务未挂载。下一步:挂载 evolution-curator 行(evolution-host/evolution-all),并运行 /evolution doctor。",
3111
+ "e-305-the-invocation-agent-exposes": "E-305: 调用 agent 既没有暴露 `followup` 也没有 `inject` —— %a1% 没有投递通道。",
3112
+ "e-306-this-deployment-stages-foreground-2": "E-306: 本部署会暂存前台写入,但本次调用没有 agent 会话 —— `/evolution restructure` 会暂存一条没有会话归属的记录。请在 GUI 或 CLI 的会话里运行,或在 `stageForeground: false` 行上设置,然后重试该命令。",
3113
+ "e-303-replay-service-not-mounted": "E-303: 回放服务未挂载。下一步:挂载 evolution-replay 行(evolution-host/evolution-all),并运行 /evolution doctor。",
3114
+ "e-307-the-settings-service-could": "E-307: 设置服务无法报告它的分区(%a1%)。/evolution params 需要用户层来区分覆盖值与部署值;运行 /evolution doctor 查看已挂载的服务。",
3115
+ "e-308-unknown-parameter-group-group": "E-308: 未知的参数分组「%a1%」—— 已知分组:%a2%。",
3116
+ "e-311-no-settings-service-is": "E-311: 没有挂载设置服务,因此没有可写入的用户层。请挂载 settings 行(packages/settings/settings-file)后重试;本命令从不修改 cordis.yml —— 部署值属于部署。/evolution doctor 会列出已挂载的服务。",
3117
+ "e-312-error-instanceof-Error-error": "E-312: %a1% —— /evolution params 会列出规范 id。",
3118
+ "e-313-unknown-parameter-id-evolution": "E-313: 未知参数「%a1%」—— /evolution params 会列出每个已注册 id 及其层级与用户层。",
3119
+ "e-314-id-is-a-deployment": "E-314: 「%a1%」是部署参数(层级 %a2%,归属 %a3%)—— 它写在 cordis.yml 的插件行或策略行上,而不是从会话写入。/evolution params 会显示每一行由谁可写。",
3120
+ "e-315-id-has-no-user": "E-315: 「%a1%」没有用户层 —— 归属 %a2% 没有发布设置命名空间,因此该值留在部署层。",
3121
+ "e-307-the-settings-service-could-2": "E-307: 设置服务无法报告它的分区(%a1%)。/evolution policy set 需要当前修订号才能安全写入;运行 /evolution doctor 查看已挂载的服务。",
3122
+ "e-316-namespace-namespace-is-not": "E-316: namespace \"%a1%\" 未注册 —— 归属 %a2% 在本组合中未挂载,因此无法从这里写入它的参数。 /evolution params 会把那些行标为 'unregistered'.",
3123
+ "e-309-revision-conflict-you-sent": "E-309: 修订冲突 —— 你提交的是 %a1%,文档当前为 %a2%。请用 /evolution params --json 重新读取后重试。",
3124
+ "e-309-revision-conflict-message-Re": "E-309: 修订冲突 —— %a1%。请用 /evolution params --json 重新读取后重试。",
3125
+ "e-310-the-settings-service-refused": "E-310: 设置服务拒绝了这次写入:%a1% —— 归属插件的规则优先(跨字段成对约束,或只能收紧的上限)。/evolution params 会显示当前值。",
3126
+ "e-317-skill-write-not-confirmed": "E-317: 技能 \"%a1%\" 未被%a2% —— 确认提示被拒绝或关闭,因此没有写入任何内容。每次创建与裸删除都会出现该提示;只有操作者要求时才重复调用。",
3127
+ "e-318-skill-write-without-a-read": "E-318: 本会话没有读取技能 \"%a1%\",因此拒绝这次写入。请先用 `skill` 工具读取它(失败的读取不算),然后重复该写入。",
3128
+ "e-320-the-profile-home-is-not": "E-320: 本宿主没有报告 profile home,因此无法定位旧设置文档。/evolution migrate 读取 `<profile home>/settings.yaml.imported`;请在由 profile 启动的会话里运行。",
3129
+ "e-322-a-parameter-would-break-a": "E-322: %a1% 至少应为 %a2%,但这次写入把它设成 %a3%。未写入任何内容。",
3130
+ "e-321-no-io-provider-is": "E-321: 没有挂载 evolution-io 提供方,因此无法读取旧设置文档。请挂载 evolution-io 行(evolution-host/evolution-all)后重试。",
3131
+ "e-319-skill-write-confirm-timed-out": "E-319: 技能 \"%a1%\" 未被%a2% —— 确认提示在 %a3% 秒内无人应答,因此该写入被取消。没有人在场决定:把 `skillWriteConfirm` 设为 `auto` 可免提示直接写入,保留 `ask` 则一直等到有人应答,或提高 `skillWriteConfirmTimeoutSeconds`。"
3294
3132
  });
3295
3133
  /**
3296
3134
  * Render one table entry, substituting its interpolation slots.
@@ -3983,7 +3821,7 @@ var MemoryStore = class {
3983
3821
  const chars = entries.join(ENTRY_DELIMITER).length;
3984
3822
  if (this.failureCount > this.maxFailures) return {
3985
3823
  ok: false,
3986
- message: `Memory consolidation failed ${this.failureCount} times this turn. Stop retrying memory calls and continue with the user's task.${previewEntries(entries)}`,
3824
+ message: `本轮记忆整理已失败 ${this.failureCount} 次。请停止重试记忆调用,继续处理用户的任务。${previewEntries(entries)}`,
3987
3825
  entries,
3988
3826
  chars,
3989
3827
  limit: this.limitFor(target)
@@ -4046,10 +3884,10 @@ var MemoryStore = class {
4046
3884
  const oversized = await this.oversizedFile(target);
4047
3885
  if (!oversized) return null;
4048
3886
  const backup = await this.backupFile(target);
4049
- const suffix = backup ? ` A backup was saved to ${basename(backup)}.` : "";
3887
+ const suffix = backup ? ` 已把备份保存到 ${basename(backup)}。` : "";
4050
3888
  return {
4051
3889
  ok: false,
4052
- message: `Memory file is ${oversized.size} bytes (limit ${oversized.limit * READ_GUARD_FACTOR}) — skipping read.${suffix} Fix the file manually, then retry.`,
3890
+ message: `记忆文件为 ${oversized.size} 字节(上限 ${oversized.limit * READ_GUARD_FACTOR})—— 跳过读取。${suffix} 请手动修复该文件后重试。`,
4053
3891
  entries: [],
4054
3892
  chars: 0,
4055
3893
  limit: this.limitFor(target)
@@ -4082,7 +3920,7 @@ var MemoryStore = class {
4082
3920
  const size = await this.io.size?.(fileFor(this.root, target));
4083
3921
  if (typeof size === "number" && size > SHRINK_LOAD_CEILING) return {
4084
3922
  ok: false,
4085
- message: `Memory file is ${size} bytes - too large to load even for a remove-only batch. Fix the file manually, then retry.`,
3923
+ message: `记忆文件为 ${size} 字节 —— 即使只做删除批处理也过大、无法加载。请手动修复该文件后重试。`,
4086
3924
  entries: [],
4087
3925
  chars: 0,
4088
3926
  limit: this.limitFor(target)
@@ -4096,7 +3934,7 @@ var MemoryStore = class {
4096
3934
  });
4097
3935
  return outcome ?? {
4098
3936
  ok: false,
4099
- message: "internal error: the memory transaction did not invoke the task; no write was performed",
3937
+ message: "内部错误:记忆写事务没有调用任务;未执行任何写入",
4100
3938
  entries: [],
4101
3939
  chars: 0,
4102
3940
  limit: this.limitFor(target)
@@ -4108,7 +3946,7 @@ var MemoryStore = class {
4108
3946
  async addChained(target, facts) {
4109
3947
  if (!facts.trim()) return {
4110
3948
  ok: false,
4111
- message: "Content cannot be empty.",
3949
+ message: "内容不能为空。",
4112
3950
  entries: [],
4113
3951
  chars: 0,
4114
3952
  limit: this.limitFor(target)
@@ -4125,7 +3963,7 @@ var MemoryStore = class {
4125
3963
  if (!content) return {
4126
3964
  result: {
4127
3965
  ok: false,
4128
- message: "Content cannot be empty.",
3966
+ message: "内容不能为空。",
4129
3967
  entries: [],
4130
3968
  chars: 0,
4131
3969
  limit: this.limitFor(target)
@@ -4152,7 +3990,7 @@ var MemoryStore = class {
4152
3990
  if (hasEntryDelimiter(prefixed)) return {
4153
3991
  result: {
4154
3992
  ok: false,
4155
- message: "Fact contains the entry delimiter (§) and would split into multiple entries; rewrite it as separate facts.",
3993
+ message: "该条目含有分隔符(§),会被拆成多条;请改写为多条独立的事实。",
4156
3994
  entries: [],
4157
3995
  chars: 0,
4158
3996
  limit: this.limitFor(target)
@@ -4165,7 +4003,7 @@ var MemoryStore = class {
4165
4003
  return {
4166
4004
  result: {
4167
4005
  ok: true,
4168
- message: `Entry already exists (no duplicate added).${this.storageHint(target, entries.join(ENTRY_DELIMITER).length)}`,
4006
+ message: `该条目已存在(未重复添加)。${this.storageHint(target, entries.join(ENTRY_DELIMITER).length)}`,
4169
4007
  entries,
4170
4008
  chars: entries.join(ENTRY_DELIMITER).length,
4171
4009
  limit: this.limitFor(target)
@@ -4177,14 +4015,14 @@ var MemoryStore = class {
4177
4015
  const total = next.join(ENTRY_DELIMITER).length;
4178
4016
  const addLimit = this.limitFor(target);
4179
4017
  if (addLimit > 0 && total > addLimit) return {
4180
- result: this.failure(target, `Adding this entry would exceed the ${addLimit} char limit. Consolidate or remove stale entries, then retry.`, entries),
4018
+ result: this.failure(target, `添加该条目会超过 ${addLimit} 字符上限。请先合并或删除过期条目,再重试。`, entries),
4181
4019
  write: null
4182
4020
  };
4183
4021
  this.resetFailures();
4184
4022
  return {
4185
4023
  result: {
4186
4024
  ok: true,
4187
- message: `Entry added.${this.storageHint(target, total)}`,
4025
+ message: `已添加条目。${this.storageHint(target, total)}`,
4188
4026
  entries: next,
4189
4027
  chars: total,
4190
4028
  limit: this.limitFor(target)
@@ -4261,10 +4099,9 @@ var MemoryStore = class {
4261
4099
  if (shrinkOnly) return null;
4262
4100
  const limit = this.limitFor(target);
4263
4101
  const entries = normalizeEntries(raw);
4264
- const over = entries.filter((entry) => entry.length > limit).length;
4265
4102
  return {
4266
4103
  ok: false,
4267
- message: `${over} memory ${over === 1 ? "entry exceeds" : "entries exceed"} the configured ${target}CharLimit (${limit}); they were written under a higher limit. Raise the limit or remove entries — remove operations stay available.`,
4104
+ message: `${entries.filter((entry) => entry.length > limit).length} 条记忆超过了配置的 ${target}CharLimit(${limit});它们是在更高上限下写入的。请提高上限或删除条目 —— 删除类操作仍然可用。`,
4268
4105
  entries,
4269
4106
  chars: entries.join(ENTRY_DELIMITER).length,
4270
4107
  limit
@@ -4273,7 +4110,7 @@ var MemoryStore = class {
4273
4110
  const backup = await this.backupFile(target);
4274
4111
  return {
4275
4112
  ok: false,
4276
- message: `External drift detected in memory file.${backup ? ` A backup was saved to ${basename(backup)}.` : ""} Resolve the drift before retrying.`,
4113
+ message: `检测到记忆文件被外部改动。${backup ? ` 已把备份保存到 ${basename(backup)}。` : ""} 请先处理该漂移再重试。`,
4277
4114
  entries: [],
4278
4115
  chars: 0,
4279
4116
  limit: this.limitFor(target)
@@ -4285,7 +4122,7 @@ var MemoryStore = class {
4285
4122
  async applyBatchChained(target, operations) {
4286
4123
  if (operations.length === 0) return {
4287
4124
  ok: false,
4288
- message: "operations list is empty.",
4125
+ message: "operations 列表为空。",
4289
4126
  entries: [],
4290
4127
  chars: 0,
4291
4128
  limit: this.limitFor(target)
@@ -4310,7 +4147,7 @@ var MemoryStore = class {
4310
4147
  if (!body) return {
4311
4148
  result: {
4312
4149
  ok: false,
4313
- message: `Operation ${position} (add): facts is required. No operations were applied.${previewEntries(entries)}`,
4150
+ message: `第 ${position} 个操作(add):必须提供 facts。未应用任何操作。${previewEntries(entries)}`,
4314
4151
  entries,
4315
4152
  chars: entries.join(ENTRY_DELIMITER).length,
4316
4153
  limit: this.limitFor(target)
@@ -4321,7 +4158,7 @@ var MemoryStore = class {
4321
4158
  if (threat) return {
4322
4159
  result: {
4323
4160
  ok: false,
4324
- message: `Operation ${position}: ${threat}${previewEntries(entries)}`,
4161
+ message: `第 ${position} 个操作:${threat}${previewEntries(entries)}`,
4325
4162
  entries,
4326
4163
  chars: entries.join(ENTRY_DELIMITER).length,
4327
4164
  limit: this.limitFor(target)
@@ -4332,7 +4169,7 @@ var MemoryStore = class {
4332
4169
  if (hasEntryDelimiter(entryBody)) return {
4333
4170
  result: {
4334
4171
  ok: false,
4335
- message: `Operation ${position} (add): Fact contains the entry delimiter (§) and would split into multiple entries; rewrite it as separate facts.${previewEntries(entries)}`,
4172
+ message: `第 ${position} 个操作(add):该条目含有分隔符(§),会被拆成多条;请改写为多条独立的事实。${previewEntries(entries)}`,
4336
4173
  entries,
4337
4174
  chars: entries.join(ENTRY_DELIMITER).length,
4338
4175
  limit: this.limitFor(target)
@@ -4346,7 +4183,7 @@ var MemoryStore = class {
4346
4183
  if (rawAction !== "remove" && rawAction !== "replace") return {
4347
4184
  result: {
4348
4185
  ok: false,
4349
- message: `Operation ${position}: unknown action "${String(rawAction)}" (expected add/remove/replace). No operations were applied.${previewEntries(entries)}`,
4186
+ message: `第 ${position} 个操作:未知动作「${String(rawAction)}」(应为 add/remove/replace)。未应用任何操作。${previewEntries(entries)}`,
4350
4187
  entries,
4351
4188
  chars: entries.join(ENTRY_DELIMITER).length,
4352
4189
  limit: this.limitFor(target)
@@ -4357,7 +4194,7 @@ var MemoryStore = class {
4357
4194
  if (!needle) return {
4358
4195
  result: {
4359
4196
  ok: false,
4360
- message: `Operation ${position} (${op.action}): old_text is required. No operations were applied.${previewEntries(entries)}`,
4197
+ message: `第 ${position} 个操作(${op.action}):必须提供 old_text。未应用任何操作。${previewEntries(entries)}`,
4361
4198
  entries,
4362
4199
  chars: entries.join(ENTRY_DELIMITER).length,
4363
4200
  limit: this.limitFor(target)
@@ -4369,13 +4206,13 @@ var MemoryStore = class {
4369
4206
  matchIndex
4370
4207
  })).filter(({ entry }) => entry.includes(needle));
4371
4208
  if (matches.length === 0) return {
4372
- result: this.failure(target, `Operation ${position}: no entry matching "${needle}" found. No operations were applied.`, entries),
4209
+ result: this.failure(target, `第 ${position} 个操作:找不到匹配 "${needle}" 的条目。未应用任何操作。`, entries),
4373
4210
  write: null
4374
4211
  };
4375
4212
  if (new Set(matches.map((m) => m.entry)).size > 1) return {
4376
4213
  result: {
4377
4214
  ok: false,
4378
- message: `Operation ${position}: "${needle}" matched multiple distinct entries. No operations were applied.${previewEntries(entries)}`,
4215
+ message: `第 ${position} 个操作:「${needle}」匹配到多条不同条目。未应用任何操作。${previewEntries(entries)}`,
4379
4216
  entries,
4380
4217
  chars: entries.join(ENTRY_DELIMITER).length,
4381
4218
  limit: this.limitFor(target)
@@ -4389,7 +4226,7 @@ var MemoryStore = class {
4389
4226
  if (!body) return {
4390
4227
  result: {
4391
4228
  ok: false,
4392
- message: `Operation ${position} (replace): facts is required.${previewEntries(entries)}`,
4229
+ message: `第 ${position} 个操作(replace):必须提供 facts。${previewEntries(entries)}`,
4393
4230
  entries,
4394
4231
  chars: entries.join(ENTRY_DELIMITER).length,
4395
4232
  limit: this.limitFor(target)
@@ -4400,7 +4237,7 @@ var MemoryStore = class {
4400
4237
  if (threat) return {
4401
4238
  result: {
4402
4239
  ok: false,
4403
- message: `Operation ${position}: ${threat}${previewEntries(entries)}`,
4240
+ message: `第 ${position} 个操作:${threat}${previewEntries(entries)}`,
4404
4241
  entries,
4405
4242
  chars: entries.join(ENTRY_DELIMITER).length,
4406
4243
  limit: this.limitFor(target)
@@ -4411,7 +4248,7 @@ var MemoryStore = class {
4411
4248
  if (hasEntryDelimiter(entryBody)) return {
4412
4249
  result: {
4413
4250
  ok: false,
4414
- message: `Operation ${position} (replace): Fact contains the entry delimiter (§) and would split into multiple entries; rewrite it as separate facts.${previewEntries(entries)}`,
4251
+ message: `第 ${position} 个操作(replace):该条目含有分隔符(§),会被拆成多条;请改写为多条独立的事实。${previewEntries(entries)}`,
4415
4252
  entries,
4416
4253
  chars: entries.join(ENTRY_DELIMITER).length,
4417
4254
  limit: this.limitFor(target)
@@ -4424,14 +4261,14 @@ var MemoryStore = class {
4424
4261
  const total = working.join(ENTRY_DELIMITER).length;
4425
4262
  const batchLimit = this.limitFor(target);
4426
4263
  if (batchLimit > 0 && total > batchLimit) return {
4427
- result: this.failure(target, `Batch result (${total} chars) exceeds the ${batchLimit} limit. Remove or shorten more entries in the same batch.`, entries),
4264
+ result: this.failure(target, `批处理结果(${total} 字符)超过 ${batchLimit} 上限。请在同一个批里再删除或缩短一些条目。`, entries),
4428
4265
  write: null
4429
4266
  };
4430
4267
  this.resetFailures();
4431
4268
  return {
4432
4269
  result: {
4433
4270
  ok: true,
4434
- message: `Applied ${operations.length} operation(s).${this.storageHint(target, total)}`,
4271
+ message: `已应用 ${operations.length} 个操作。${this.storageHint(target, total)}`,
4435
4272
  entries: working,
4436
4273
  chars: total,
4437
4274
  limit: this.limitFor(target)
@@ -4532,7 +4369,7 @@ async function recordMutation(root, io, record, cap = 500) {
4532
4369
  console.warn(`mutation audit record dropped: ${mutationsFile(root)} is malformed and was not overwritten`);
4533
4370
  return current;
4534
4371
  }
4535
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
4372
+ if (isRecord(parsed)) {
4536
4373
  const version = parsed.version;
4537
4374
  if (typeof version === "number" && version > 1) {
4538
4375
  console.warn(`mutation audit record dropped: ${mutationsFile(root)} declares version ${version} (newer than 1); not overwritten`);
@@ -4596,16 +4433,23 @@ function composePresetComposition(standardComposition, deltaComposition) {
4596
4433
  * test can keep — the entries are now data, and a divergence is impossible.
4597
4434
  */
4598
4435
  const ROW_OVERRIDES_URL = new URL("../row-overrides.json", import.meta.url);
4599
- let cachedOverrides;
4436
+ /** One cache per PATH: the installer reads the same file through its own root, and a single-slot
4437
+ * cache would hand the second caller the first caller's bytes. */
4438
+ const overridesCache = /* @__PURE__ */ new Map();
4600
4439
  /**
4601
4440
  * Read and validate the shared override table.
4441
+ * G6.1/M1: the path is a PARAMETER (default: the table beside this module). The source installer
4442
+ * runs the generated port from `scripts/`, so its own root has to be passed in — a module-relative
4443
+ * read would look for `scripts/../row-overrides.json` and fail.
4444
+ * @param path - the table to read.
4602
4445
  * @returns the entries, in file order. A malformed table fails LOUD: a composer
4603
4446
  * that silently skipped an entry would ship a preset running the platform
4604
4447
  * default while nothing downstream reported it.
4605
4448
  */
4606
- function loadRowOverrides() {
4607
- if (cachedOverrides !== void 0) return cachedOverrides;
4608
- const parsed = JSON.parse(readFileSync(fileURLToPath(ROW_OVERRIDES_URL), "utf8"));
4449
+ function loadRowOverrides(path = fileURLToPath(ROW_OVERRIDES_URL)) {
4450
+ const cached = overridesCache.get(path);
4451
+ if (cached !== void 0) return cached;
4452
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
4609
4453
  if (!Array.isArray(parsed)) throw new Error("evolution-core: row-overrides.json must be an array of override entries");
4610
4454
  const entries = [];
4611
4455
  for (const [index, entry] of parsed.entries()) {
@@ -4618,7 +4462,7 @@ function loadRowOverrides() {
4618
4462
  missingReason: candidate.missingReason
4619
4463
  });
4620
4464
  }
4621
- cachedOverrides = entries;
4465
+ overridesCache.set(path, entries);
4622
4466
  return entries;
4623
4467
  }
4624
4468
  /**
@@ -4737,7 +4581,7 @@ function composePresetRow(baseComposition, deltaComposition, identity) {
4737
4581
  }
4738
4582
  /** The patch-entry key that ADDS a row: a plain entry overrides the row with the same id, so a
4739
4583
  * preset the platform does not ship has to arrive inside an `insert` entry
4740
- * (`packages/boot/app-boot/tests/user-patches.spec.ts:47-64`). */
4584
+ * (the platform's own `user-patches.spec.ts` proves the override-vs-insert split). */
4741
4585
  const PATCH_INSERT_KEY = "insert";
4742
4586
  /**
4743
4587
  * Wrap a composed row as the patch entry a profile patch needs.
@@ -6116,7 +5960,7 @@ function newRunRegistry(options) {
6116
5960
  const parsed = parseIndex(raw);
6117
5961
  if (!parsed.ok) return {
6118
5962
  ok: false,
6119
- note: "run index could not be read as an index (not JSON, or no runs array) — the previous run history is unknown (the reports are unaffected)"
5963
+ note: "运行索引读不出来(不是 JSON,或没有 runs 数组)—— 上一次的运行历史未知(报告不受影响)"
6120
5964
  };
6121
5965
  const wasRunning = new Set([...foreign.values()].filter((row) => row.state === "running").map((row) => row.id));
6122
5966
  const seen = /* @__PURE__ */ new Set();
@@ -6411,7 +6255,7 @@ function renderLongRunTable() {
6411
6255
  /**
6412
6256
  * Every platform service the family reaches for with `ctx.get`, and what degrades without it.
6413
6257
  *
6414
- * O-2 (v46 audit): the family probes 26 platform services optionally — the platform has no
6258
+ * O-2 (v46 audit): the family probes 28 platform services optionally — the platform has no
6415
6259
  * formalized "optional dependency", so an ABSENT service is a silent downgrade and the diagnostic
6416
6260
  * surface is the only place a reader can learn about it. `/evolution doctor` used to judge six of
6417
6261
  * them; this table is the single source it judges the rest from, and the arch guard cross-checks it
@@ -6421,7 +6265,7 @@ function renderLongRunTable() {
6421
6265
  * `client` probes run in the browser half, where the host has no view of the seats.
6422
6266
  * @module @lmzhen/dsh-evolution-core
6423
6267
  */
6424
- /** The 26 probes, in service-name order (the guard checks this list against the tree). */
6268
+ /** The 28 probes, in service-name order (the guard checks this list against the tree). */
6425
6269
  const PLATFORM_SERVICE_PROBES = Object.freeze([
6426
6270
  {
6427
6271
  service: "agents",
@@ -6458,6 +6302,16 @@ const PLATFORM_SERVICE_PROBES = Object.freeze([
6458
6302
  feature: "every durable read and write (the IO seam)",
6459
6303
  side: "host"
6460
6304
  },
6305
+ {
6306
+ service: "fs",
6307
+ feature: "the platform filesystem the evolution-io-fs backend maps the seam onto (locks + writeText(expected) version CAS)",
6308
+ side: "host"
6309
+ },
6310
+ {
6311
+ service: "hmr",
6312
+ feature: "the preset install read-merge-write commit (the platform HMR lock serializes it)",
6313
+ side: "host"
6314
+ },
6461
6315
  {
6462
6316
  service: "evolutionMemoryBudget",
6463
6317
  feature: "the memory-budget divergence section of this report",
@@ -8007,6 +7861,13 @@ function versionRowCells(row) {
8007
7861
  tone: "tertiary",
8008
7862
  slot: "meta"
8009
7863
  });
7864
+ if (row.hash !== void 0 && row.hash !== "") title.push({
7865
+ key: "hash",
7866
+ literal: row.hash.slice(0, 7),
7867
+ tone: "tertiary",
7868
+ slot: "meta",
7869
+ note: row.hash
7870
+ });
8010
7871
  const meta = [];
8011
7872
  if (row.summary !== void 0 && row.summary !== "") meta.push({
8012
7873
  key: "summary",
@@ -8055,6 +7916,13 @@ function skillRowCells(skill) {
8055
7916
  tone: "tertiary",
8056
7917
  slot: "meta"
8057
7918
  }];
7919
+ if (skill.protectedBy !== null) meta.push({
7920
+ key: "protectedBy",
7921
+ literal: skill.protectedBy,
7922
+ tone: "tertiary",
7923
+ slot: "meta",
7924
+ note: skill.protectedBy
7925
+ });
8058
7926
  if (skill.age !== null) meta.push({
8059
7927
  key: "age",
8060
7928
  copy: ageCopyKey(skill.age),
@@ -8710,7 +8578,7 @@ function contentRetentionFeedback(input) {
8710
8578
  if (before === null || after === null) return null;
8711
8579
  if (before.length < 200) return null;
8712
8580
  if (after.length >= before.length * .5) return null;
8713
- return `Content kept ${Math.round(after.length / before.length * 100)}% of the previous body (${after.length} of ${before.length} characters); the replaced version is preserved in this skill's history.`;
8581
+ return `新内容保留了上一版正文的 ${Math.round(after.length / before.length * 100)}%(${after.length}/${before.length} 字符);被替换的版本已保留在该技能的历史里。`;
8714
8582
  }
8715
8583
  /**
8716
8584
  * Read one skill's versions, oldest first.
@@ -9002,7 +8870,7 @@ function elapsedSince(at, nowMs) {
9002
8870
  * itself in `this.serial` (archive/restoreFromArchive deliberately do not).
9003
8871
  * 2. **Per-directory write lock** (io.ts LOCK_*) — cross-process mutual
9004
8872
  * exclusion plus in-process crash ownership (tickets, takeover). Checked
9005
- * with `hasWriteLock` before any destructive move (archive/restore/
8873
+ * with `supportDirsUnlistable` before any destructive move (archive/restore/
9006
8874
  * snapshot); held inside transactIo by byte writers.
9007
8875
  * 3. **CAS baseline (`expected:`)** — any read whose bytes feed a later write
9008
8876
  * must either live inside the serial section that commits the write, or
@@ -9078,7 +8946,7 @@ function anchorRefusal(name, verdict) {
9078
8946
  ok: false,
9079
8947
  stale: true,
9080
8948
  anchor: verdict,
9081
- message: verdict === "missing" ? `Skill "${name}" not found.` : `Skill "${name}" changed since it was read; the write was refused to avoid overwriting newer content.`
8949
+ message: verdict === "missing" ? `技能「${name}」不存在。` : `技能 "${name}" 在读取之后被改动过;为避免覆盖更新的内容,本次写入被拒绝。`
9082
8950
  };
9083
8951
  }
9084
8952
  /**
@@ -9101,7 +8969,7 @@ function anchorUnverifiable(name, filePath) {
9101
8969
  ok: false,
9102
8970
  stale: true,
9103
8971
  anchor: "drift",
9104
- message: filePath === null ? `Skill "${name}" could not be read to verify the staged content.` : `Support file "${filePath}" of "${name}" could not be read to verify the staged content.`
8972
+ message: filePath === null ? `无法读取技能 "${name}" 以校验暂存内容。` : `无法读取技能 "${name}" 的支撑文件 "${filePath}" 以校验暂存内容。`
9105
8973
  };
9106
8974
  }
9107
8975
  function anchorRefusalFile(name, filePath, verdict) {
@@ -9109,7 +8977,7 @@ function anchorRefusalFile(name, filePath, verdict) {
9109
8977
  ok: false,
9110
8978
  stale: true,
9111
8979
  anchor: verdict,
9112
- message: verdict === "missing" ? `File "${filePath}" not found in skill "${name}".` : `Support file "${filePath}" of "${name}" changed since it was read; the write was refused to avoid overwriting newer content.`
8980
+ message: verdict === "missing" ? `文件「${filePath}」不在技能「${name}」里。` : `技能 "${name}" 的支撑文件 "${filePath}" 在读取之后被改动过;为避免覆盖更新的内容,本次写入被拒绝。`
9113
8981
  };
9114
8982
  }
9115
8983
  /**
@@ -9127,7 +8995,7 @@ function anchorRefusalFile(name, filePath, verdict) {
9127
8995
  function unreadableTarget(label, error) {
9128
8996
  return {
9129
8997
  ok: false,
9130
- message: `Could not read "${label}": ${error?.code === "EISDIR" ? "the path is a DIRECTORY, not a file" : error instanceof Error ? error.message : String(error)}. Nothing was written.`
8998
+ message: `读不出「${label}」:${error?.code === "EISDIR" ? "the path is a DIRECTORY, not a file" : error instanceof Error ? error.message : String(error)}。没有写入任何内容。`
9131
8999
  };
9132
9000
  }
9133
9001
  /** Upper bound of moves per restructure proposal (validator and core agree). */
@@ -9239,15 +9107,6 @@ function skillDir(root, name) {
9239
9107
  function markerEntryName(marker) {
9240
9108
  return `.${marker}`;
9241
9109
  }
9242
- /** F-17 (v18): the root-level lock files a DESTRUCTIVE MOVER must treat as an
9243
- * active writer (skill body + the two marker writers). Single source with
9244
- * `markerEntryName`/`LOCK_SUFFIX` so a renamed marker cannot silently drop out
9245
- * of the ghost-writer probe. */
9246
- const MARKER_LOCK_NAMES = [
9247
- `SKILL.md${LOCK_SUFFIX}`,
9248
- `.pinned${LOCK_SUFFIX}`,
9249
- `.hermes-managed${LOCK_SUFFIX}`
9250
- ];
9251
9110
  /** v23 (ML-1): `.archive` retention window (see pruneExpiredArchives). */
9252
9111
  const ARCHIVE_RETENTION_DAYS = 365;
9253
9112
  function markerPath(dir, marker) {
@@ -9320,16 +9179,15 @@ function validateRestructureTarget(filePath) {
9320
9179
  }
9321
9180
  function validateSupportPath(filePath) {
9322
9181
  const normalized = filePath.replace(/\\/g, "/");
9323
- if (normalized.includes("..")) return "Path traversal is not allowed.";
9182
+ if (normalized.includes("..")) return "不允许路径穿越。";
9324
9183
  const parts = normalized.split("/").filter(Boolean);
9325
9184
  if (parts.length === 0 || !SUPPORT_DIRS.includes(parts[0])) return `file_path must be under one of: ${SUPPORT_DIRS.join(", ")}.`;
9326
- if (parts.length < 2) return "Provide a file name, not just a directory.";
9185
+ if (parts.length < 2) return "请给出文件名,不能只给目录。";
9327
9186
  for (const part of parts.slice(1)) {
9328
- if (!SUPPORT_FILE_NAME_RE.test(part)) return `Unsupported file name "${part}" — use lowercase letters, digits, dots, hyphens, and underscores (leading letter or digit).`;
9329
- if (part.toLowerCase().endsWith(".lock")) return `Unsupported file name "${part}" — the .lock suffix is reserved for the writer-lock protocol.`;
9330
- if (part.toLowerCase().endsWith(".corrupt") || part.toLowerCase().endsWith(".tmp")) return `Unsupported file name "${part}" — the .corrupt/.tmp suffixes are reserved for the state/IO protocols.`;
9187
+ if (!SUPPORT_FILE_NAME_RE.test(part)) return `不支持的文件名「${part}」:请只用小写字母、数字、点、连字符和下划线(首字符须为字母或数字)。`;
9188
+ if (part.toLowerCase().endsWith(".corrupt") || part.toLowerCase().endsWith(".tmp")) return `不支持的文件名「${part}」:.corrupt/.tmp 后缀由状态与 IO 协议保留。`;
9331
9189
  const stem = part.split(".")[0]?.toLowerCase() ?? "";
9332
- if (WIN32_RESERVED_DEVICE_NAMES.has(stem)) return `Unsupported file name "${part}" — a Windows reserved device name.`;
9190
+ if (WIN32_RESERVED_DEVICE_NAMES.has(stem)) return `不支持的文件名「${part}」:这是 Windows 保留设备名。`;
9333
9191
  }
9334
9192
  return null;
9335
9193
  }
@@ -9478,7 +9336,7 @@ var SkillLibrary = class {
9478
9336
  const o = outcome;
9479
9337
  if (o === void 0 || typeof o !== "object" || !Object.prototype.hasOwnProperty.call(o, "write")) return {
9480
9338
  ok: false,
9481
- message: "internal error: the write transaction did not invoke the task; no write was performed"
9339
+ message: "内部错误:写事务没有调用任务;未执行任何写入"
9482
9340
  };
9483
9341
  if (o.ghostDir === true) await this.cleanupGhostDir(path);
9484
9342
  let result = o.result;
@@ -9498,7 +9356,7 @@ var SkillLibrary = class {
9498
9356
  if (o.write !== null && o.event) this.notifyMutation(o.event);
9499
9357
  return durabilityWarning === "" || !result.ok ? result : {
9500
9358
  ...result,
9501
- message: `${result.message} (warning: the write landed but the directory fsync failed — durability unconfirmed: ${durabilityWarning})`
9359
+ message: `${result.message} (警告:写入已落盘,但目录 fsync 失败 —— 持久性未确认:${durabilityWarning})`
9502
9360
  };
9503
9361
  }
9504
9362
  /**
@@ -9506,16 +9364,14 @@ var SkillLibrary = class {
9506
9364
  * found no SKILL.md — remove the possibly-resurrected directory ONLY when it
9507
9365
  * holds nothing but this path's own write-lock file. The BR-5 rule from
9508
9366
  * setPinnedCore/createCore applies unchanged: a concurrent mover can land a
9509
- * full directory between the list probe and the remove, so anything beyond
9510
- * the lock file (support files, a fresh restore) must never be recursed
9511
- * away. Best-effort: a failed cleanup leaves the "not found" result
9367
+ * full directory between the list probe and the remove, so ANY content at all (support files, a
9368
+ * fresh restore) must never be recursed away. Best-effort: a failed cleanup leaves the "not found" result
9512
9369
  * unchanged (the operator-facing ghost-dir refusal is fail-loud already).
9513
9370
  */
9514
9371
  async cleanupGhostDir(skillFilePath) {
9515
9372
  const dir = dirname(skillFilePath);
9516
- const lockName = `${basename(skillFilePath)}${LOCK_SUFFIX}`;
9517
9373
  try {
9518
- if ((await this.io.list(dir)).some((entry) => entry !== lockName)) return;
9374
+ if ((await this.io.list(dir)).length > 0) return;
9519
9375
  await this.io.remove(dir);
9520
9376
  } catch {}
9521
9377
  }
@@ -9663,7 +9519,7 @@ var SkillLibrary = class {
9663
9519
  */
9664
9520
  badName(name, opts = {}) {
9665
9521
  const normalized = name.trim();
9666
- if (!SKILL_NAME_RE.test(normalized) || normalized.length > this.limits.maxNameLength) return `Invalid skill name "${normalized}". Use lowercase letters, digits, and hyphens (<= ${this.limits.maxNameLength}).`;
9522
+ if (!SKILL_NAME_RE.test(normalized) || normalized.length > this.limits.maxNameLength) return `技能名「${normalized}」不合法:请只用小写字母、数字和连字符(不超过 ${this.limits.maxNameLength} 个)。`;
9667
9523
  if (!opts.allowReserved && WIN32_RESERVED_DEVICE_NAMES.has(normalized)) return `"${normalized}" is a Windows reserved device name and cannot be used as a skill name.`;
9668
9524
  return null;
9669
9525
  }
@@ -9922,7 +9778,7 @@ var SkillLibrary = class {
9922
9778
  };
9923
9779
  if (origin === "background_review") return {
9924
9780
  ok: false,
9925
- message: "Only the foreground (user or the main agent) may pin or unpin skills."
9781
+ message: "只有前台(用户或主智能体)可以固定或取消固定技能。"
9926
9782
  };
9927
9783
  let durabilityWarning = "";
9928
9784
  const dir = this.dirOf(normalized);
@@ -9930,17 +9786,17 @@ var SkillLibrary = class {
9930
9786
  const existing = await this.io.exists(marker);
9931
9787
  if (pinned && existing) return {
9932
9788
  ok: true,
9933
- message: `Skill "${normalized}" is already pinned.`,
9789
+ message: `技能「${normalized}」已被固定。`,
9934
9790
  path: dir
9935
9791
  };
9936
9792
  if (!pinned && !existing) return {
9937
9793
  ok: true,
9938
- message: `Skill "${normalized}" is not pinned; nothing to do.`,
9794
+ message: `技能「${normalized}」未被固定,无需处理。`,
9939
9795
  path: dir
9940
9796
  };
9941
9797
  if (!await this.io.exists(join(dir, "SKILL.md"))) return {
9942
9798
  ok: false,
9943
- message: `Skill "${normalized}" not found.`
9799
+ message: `技能「${normalized}」不存在。`
9944
9800
  };
9945
9801
  if (pinned) {
9946
9802
  try {
@@ -9954,14 +9810,14 @@ var SkillLibrary = class {
9954
9810
  await this.cleanupGhostDir(join(dir, "SKILL.md"));
9955
9811
  return {
9956
9812
  ok: false,
9957
- message: `Skill "${normalized}" was archived concurrently while pinning; the partial marker was removed — retry after the mover settles.`
9813
+ message: `固定过程中技能「${normalized}」被并发归档;残留标记已移除:等搬运完成后重试。`
9958
9814
  };
9959
9815
  }
9960
9816
  } else await this.io.remove(marker);
9961
9817
  await this.audit(normalized, pinned ? "pin" : "unpin", null, null, pinned ? "pinned" : "unpinned");
9962
9818
  return {
9963
9819
  ok: true,
9964
- message: `${pinned ? `Skill "${normalized}" pinned: protected from deletion, background review, and the lifecycle.` : `Skill "${normalized}" unpinned.`}${durabilityWarning === "" ? "" : ` (warning: the write landed but the directory fsync failed — durability unconfirmed: ${durabilityWarning})`}`,
9820
+ message: `${pinned ? `技能 "${normalized}" 已固定:免受删除、后台复核与生命周期影响。` : `技能 "${normalized}" 已取消固定。`}${durabilityWarning === "" ? "" : ` (警告:写入已落盘,但目录 fsync 失败 —— 持久性未确认:${durabilityWarning})`}`,
9965
9821
  path: dir
9966
9822
  };
9967
9823
  }
@@ -9983,7 +9839,7 @@ var SkillLibrary = class {
9983
9839
  const norm = normalizeFrontmatter(content);
9984
9840
  if (norm.issues.length > 0) return {
9985
9841
  ok: false,
9986
- message: `Skill "${normalized}" frontmatter cannot be auto-fixed: ${norm.issues[0]} — wrap the value in double quotes and retry.`
9842
+ message: `技能「${normalized}」的 frontmatter 无法自动修复:${norm.issues[0]}:请用双引号包住取值后重试。`
9987
9843
  };
9988
9844
  const finalContent = norm.changed ? norm.content : content;
9989
9845
  if (norm.changed) {
@@ -10001,7 +9857,7 @@ var SkillLibrary = class {
10001
9857
  const dir = this.dirOf(normalized);
10002
9858
  if (await this.io.exists(join(dir, "SKILL.md"))) return {
10003
9859
  ok: false,
10004
- message: `Skill "${normalized}" already exists.`
9860
+ message: `技能「${normalized}」已存在。`
10005
9861
  };
10006
9862
  let rootEntries;
10007
9863
  try {
@@ -10010,17 +9866,17 @@ var SkillLibrary = class {
10010
9866
  const cause = error instanceof Error ? error.message : String(error);
10011
9867
  return {
10012
9868
  ok: false,
10013
- message: `Skill "${normalized}" could not be checked for a case-variant collision: listing ${this.root} failed (${cause}). Refusing the create — a second directory differing only in case cannot be undone across platforms.`
9869
+ message: `无法检查技能「${normalized}」是否与大小写变体冲突:列出 ${this.root} 失败(${cause})。拒绝创建 —— 仅大小写不同的第二个目录在跨平台时无法撤销。`
10014
9870
  };
10015
9871
  }
10016
9872
  for (const entry of rootEntries) if (typeof entry === "string" && entry !== normalized && entry.toLowerCase() === normalized.toLowerCase()) return {
10017
9873
  ok: false,
10018
- message: `Skill "${normalized}" collides with the existing case-variant directory "${entry}" (skill names are lowercase-only); rename one of them.`
9874
+ message: `技能「${normalized}」与已存在的大小写变体目录「${entry}」冲突(技能名只允许小写):请重命名其中一个。`
10019
9875
  };
10020
9876
  const protection = await this.writeProtection(normalized, origin);
10021
9877
  if (protection) return {
10022
9878
  ok: false,
10023
- message: `Skill "${normalized}" is protected (${protection}).`
9879
+ message: `技能「${normalized}」受保护(${protection})。`
10024
9880
  };
10025
9881
  const onDisk = finalContent.trimEnd() + "\n";
10026
9882
  const createPath = join(dir, "SKILL.md");
@@ -10055,18 +9911,18 @@ var SkillLibrary = class {
10055
9911
  }
10056
9912
  if (!taskRan) return {
10057
9913
  ok: false,
10058
- message: "internal error: the create transaction did not invoke the task; no file was written"
9914
+ message: "内部错误:创建事务没有调用任务;未写入任何文件"
10059
9915
  };
10060
9916
  if (existsAtCommit) return {
10061
9917
  ok: false,
10062
- message: `Skill "${normalized}" already exists.`
9918
+ message: `技能「${normalized}」已存在。`
10063
9919
  };
10064
9920
  let markerWarning = "";
10065
9921
  if (origin !== "foreground") {
10066
9922
  try {
10067
9923
  await this.io.writeText(markerPath(dir, "hermes-managed"), "");
10068
9924
  } catch (error) {
10069
- if (isCommittedOnly(error)) markerWarning = `the hermes-managed marker landed but its directory fsync failed — durability unconfirmed: ${error instanceof Error ? error.message : String(error)}`;
9925
+ if (isCommittedOnly(error)) markerWarning = `hermes-managed 标记已落盘,但其目录 fsync 失败 —— 持久性未确认:${error instanceof Error ? error.message : String(error)}`;
10070
9926
  else markerWarning = `hermes-managed marker write failed (${error instanceof Error ? error.message : String(error)}); the skill landed but is NOT lifecycle-managed — recreate or pin it manually`;
10071
9927
  }
10072
9928
  if (!await this.io.exists(createPath)) {
@@ -10074,7 +9930,7 @@ var SkillLibrary = class {
10074
9930
  await this.cleanupGhostDir(createPath);
10075
9931
  return {
10076
9932
  ok: false,
10077
- message: `Skill "${normalized}" was archived concurrently while being created; the partial marker was removed — retry once the mover settles.`
9933
+ message: `创建过程中技能「${normalized}」被并发归档;残留标记已移除:等搬运完成后重试一次。`
10078
9934
  };
10079
9935
  }
10080
9936
  }
@@ -10084,10 +9940,10 @@ var SkillLibrary = class {
10084
9940
  name: normalized,
10085
9941
  skillDir: dir
10086
9942
  });
10087
- const warnings = [createDurabilityWarning !== "" ? `the write landed but the directory fsync failed — durability unconfirmed: ${createDurabilityWarning}` : "", markerWarning].filter((warning) => warning !== "");
9943
+ const warnings = [createDurabilityWarning !== "" ? `写入已落盘,但目录 fsync 失败 —— 持久性未确认:${createDurabilityWarning}` : "", markerWarning].filter((warning) => warning !== "");
10088
9944
  return {
10089
9945
  ok: true,
10090
- message: `Skill "${normalized}" created.${warnings.length === 0 ? "" : ` (warning: ${warnings.join("; ")})`}`,
9946
+ message: `已创建技能「${normalized}」。${warnings.length === 0 ? "" : ` (警告:${warnings.join(";")})`}`,
10091
9947
  path: dir,
10092
9948
  ...norm.changed ? { normalizedFrontmatterFields: norm.fields } : {}
10093
9949
  };
@@ -10107,7 +9963,7 @@ var SkillLibrary = class {
10107
9963
  const protection = await this.writeProtection(name, origin);
10108
9964
  if (protection) return {
10109
9965
  ok: false,
10110
- message: `Skill "${name}" is protected (${protection}).`
9966
+ message: `技能「${name}」受保护(${protection})。`
10111
9967
  };
10112
9968
  const limitProbe = await probeText(this.io, path);
10113
9969
  if (isUnknown(limitProbe)) console.warn(`skill-store: could not re-read ${path} for the shrink exemption (${limitProbe.reason}); validating without it — an over-limit repair is refused, not accepted`);
@@ -10120,7 +9976,7 @@ var SkillLibrary = class {
10120
9976
  const norm = normalizeFrontmatter(content);
10121
9977
  if (norm.issues.length > 0) return {
10122
9978
  ok: false,
10123
- message: `Skill "${name}" frontmatter cannot be auto-fixed: ${norm.issues[0]} — wrap the value in double quotes and retry.`
9979
+ message: `技能「${name}」的 frontmatter 无法自动修复:${norm.issues[0]}:请用双引号包住取值后重试。`
10124
9980
  };
10125
9981
  const finalContent = norm.changed ? norm.content : content;
10126
9982
  if (norm.changed) {
@@ -10144,14 +10000,14 @@ var SkillLibrary = class {
10144
10000
  if (current === null) return {
10145
10001
  result: {
10146
10002
  ok: false,
10147
- message: `Skill "${name}" not found.`
10003
+ message: `技能「${name}」不存在。`
10148
10004
  },
10149
10005
  write: null
10150
10006
  };
10151
10007
  if (finalContent.trimEnd() === current.trimEnd()) return {
10152
10008
  result: {
10153
10009
  ok: true,
10154
- message: `Skill "${name}" unchanged: the supplied content already matches the current file; nothing written.`,
10010
+ message: `技能「${name}」未变更:提供的内容与当前文件一致,未写入。`,
10155
10011
  noop: true,
10156
10012
  path: dir
10157
10013
  },
@@ -10161,7 +10017,7 @@ var SkillLibrary = class {
10161
10017
  return {
10162
10018
  result: {
10163
10019
  ok: true,
10164
- message: `Skill "${name}" updated.`,
10020
+ message: `技能「${name}」已更新。`,
10165
10021
  path: dir,
10166
10022
  ...norm.changed ? { normalizedFrontmatterFields: norm.fields } : {}
10167
10023
  },
@@ -10195,12 +10051,12 @@ var SkillLibrary = class {
10195
10051
  const skillMd = join(dir, "SKILL.md");
10196
10052
  if (!await this.io.exists(skillMd)) return {
10197
10053
  ok: false,
10198
- message: `Skill "${name}" not found.`
10054
+ message: `技能「${name}」不存在。`
10199
10055
  };
10200
10056
  const protection = await this.writeProtection(name, origin);
10201
10057
  if (protection) return {
10202
10058
  ok: false,
10203
- message: `Skill "${name}" is protected (${protection}).`
10059
+ message: `技能「${name}」受保护(${protection})。`
10204
10060
  };
10205
10061
  let target = skillMd;
10206
10062
  let patchLabel = "SKILL.md";
@@ -10218,7 +10074,7 @@ var SkillLibrary = class {
10218
10074
  if (md === null) return {
10219
10075
  result: {
10220
10076
  ok: false,
10221
- message: `File not found: ${patchLabel}`
10077
+ message: `文件不存在:${patchLabel}`
10222
10078
  },
10223
10079
  write: null
10224
10080
  };
@@ -10240,7 +10096,7 @@ var SkillLibrary = class {
10240
10096
  if (patched === null) return {
10241
10097
  result: {
10242
10098
  ok: false,
10243
- message: `Could not find old_string in "${name}/${patchLabel}" (or the replaceAll fuzzy budget was exceeded). Use update for a full rewrite.`
10099
+ message: `在「${name}/${patchLabel}」里找不到 old_string(或 replaceAll 的模糊匹配预算已用尽)。整篇重写请改用 update。`
10244
10100
  },
10245
10101
  write: null
10246
10102
  };
@@ -10253,7 +10109,7 @@ var SkillLibrary = class {
10253
10109
  if (validation) return {
10254
10110
  result: {
10255
10111
  ok: false,
10256
- message: `Patch rejected: ${validation}`
10112
+ message: `补丁被拒绝:${validation}`
10257
10113
  },
10258
10114
  write: null
10259
10115
  };
@@ -10261,7 +10117,7 @@ var SkillLibrary = class {
10261
10117
  if (norm.issues.length > 0) return {
10262
10118
  result: {
10263
10119
  ok: false,
10264
- message: `Patch rejected: frontmatter cannot be auto-fixed (${norm.issues[0]}).`
10120
+ message: `补丁被拒绝:frontmatter 无法自动修复(${norm.issues[0]})。`
10265
10121
  },
10266
10122
  write: null
10267
10123
  };
@@ -10272,7 +10128,7 @@ var SkillLibrary = class {
10272
10128
  if (revalidated) return {
10273
10129
  result: {
10274
10130
  ok: false,
10275
- message: `Patch rejected: ${revalidated}`
10131
+ message: `补丁被拒绝:${revalidated}`
10276
10132
  },
10277
10133
  write: null
10278
10134
  };
@@ -10281,7 +10137,7 @@ var SkillLibrary = class {
10281
10137
  if (Buffer.byteLength(writeContent, "utf8") > this.limits.maxSkillFileBytes && target !== skillMd) return {
10282
10138
  result: {
10283
10139
  ok: false,
10284
- message: `Patched file exceeds ${this.limits.maxSkillFileBytes} bytes.`
10140
+ message: `修补后的文件超过 ${this.limits.maxSkillFileBytes} 字节。`
10285
10141
  },
10286
10142
  write: null
10287
10143
  };
@@ -10290,7 +10146,7 @@ var SkillLibrary = class {
10290
10146
  if (overLimit && target === skillMd && !repairing) return {
10291
10147
  result: {
10292
10148
  ok: false,
10293
- message: `Patched content exceeds ${this.limits.maxSkillContentChars} characters. ${CONTENT_SPLIT_HINT}`
10149
+ message: `修补后的内容超过 ${this.limits.maxSkillContentChars} 个字符。${CONTENT_SPLIT_HINT}`
10294
10150
  },
10295
10151
  write: null
10296
10152
  };
@@ -10299,7 +10155,7 @@ var SkillLibrary = class {
10299
10155
  if (supportOverCap && this.supportFileCharPolicy() === "enforce") return {
10300
10156
  result: {
10301
10157
  ok: false,
10302
- message: `Patched file content exceeds ${this.limits.maxSkillContentChars} characters. ${CONTENT_SPLIT_HINT}`
10158
+ message: `修补后的文件内容超过 ${this.limits.maxSkillContentChars} 个字符。${CONTENT_SPLIT_HINT}`
10303
10159
  },
10304
10160
  write: null
10305
10161
  };
@@ -10314,7 +10170,7 @@ var SkillLibrary = class {
10314
10170
  if (writeContent.trimEnd() === md.trimEnd()) return {
10315
10171
  result: {
10316
10172
  ok: true,
10317
- message: `Skill "${name}" unchanged: old_string already equals the replacement (${patchLabel}); nothing written.`,
10173
+ message: `技能「${name}」未变更:old_string 与替换内容相同(${patchLabel}),未写入。`,
10318
10174
  noop: true,
10319
10175
  path: dir
10320
10176
  },
@@ -10324,7 +10180,7 @@ var SkillLibrary = class {
10324
10180
  return {
10325
10181
  result: {
10326
10182
  ok: true,
10327
- message: `Skill "${name}" patched (${patchLabel}).${patchNote}${capAdvisory}`,
10183
+ message: `技能「${name}」已修补(${patchLabel})。${patchNote}${capAdvisory}`,
10328
10184
  path: dir,
10329
10185
  ...normalizedFields ? { normalizedFrontmatterFields: normalizedFields } : {}
10330
10186
  },
@@ -10369,9 +10225,9 @@ var SkillLibrary = class {
10369
10225
  const reason = error instanceof Error ? error.message : String(error);
10370
10226
  try {
10371
10227
  await this.io.remove(dest);
10372
- return `the copy succeeded but the source could not be removed (${reason}); the copied tree was rolled back`;
10228
+ return `复制成功,但源目录删不掉(${reason});已回滚复制出的目录树`;
10373
10229
  } catch {
10374
- return `the copy succeeded but the source could not be removed (${reason}) and the copied tree could not be rolled back — the tree now exists in BOTH locations; clean up manually`;
10230
+ return `复制成功,但源目录删不掉(${reason}),且复制出的目录树也无法回滚 —— 该目录树现在同时存在于两处;请手动清理`;
10375
10231
  }
10376
10232
  }
10377
10233
  }
@@ -10380,110 +10236,19 @@ var SkillLibrary = class {
10380
10236
  * P2 (v16): the write-lock probe for the DESTRUCTIVE MOVERS (archive /
10381
10237
  * restoreFromArchive). A byte-writer mid-flight is the ghost-generator —
10382
10238
  * after the move its transact commit re-creates `<dir>/…` (mkdir
10383
- * recursive) and the tree ends half-archived. The signal is the writer's
10384
- * own lock file, and its PLACEMENT (inside the moved directory) is why the
10385
- * mover must PROBE-and-REFUSE instead of acquiring it: an acquired lock
10386
- * would be renamed into `.archive` with the tree, stranding a phantom live
10387
- * lock (the v16 audit proved the probe→rename TOCTOU does exactly that,
10388
- * and restore would later move the residue back into the live root).
10389
- * Coverage: `SKILL.md.lock` (update/patch of the body) plus one level of
10390
- * each support dir (write_file's lock sits next to its file). Residual:
10391
- * NESTED support-subdir locks and the probe→rename TOCTOU itself remain
10392
- * fail-safe (renameWithRetry rides the write out; the writer's locked
10393
- * re-read refuses on the moved-away file). v37 (P2-8): a residue `.lock` whose
10394
- * holder is GONE is not a writer — `decideTakeover` owns that verdict.
10239
+ * recursive) and the tree ends half-archived.
10240
+ * G2 S4: the writer-lock probe that used to answer this question is retired — writes go through
10241
+ * the provider's versioned writes, so a writer that raced the move fails its own write loudly
10242
+ * instead of landing on a moved-away path. What REMAINS load-bearing here is the fail-closed
10243
+ * listing rule, which is about the filesystem misbehaving rather than about writers.
10395
10244
  */
10396
- async hasWriteLock(dir) {
10397
- const markerLocks = MARKER_LOCK_NAMES.map((name) => join(dir, name));
10398
- for (const lock of markerLocks) if (await this.isWriterLock(lock)) return true;
10399
- for (const supportDir of SUPPORT_DIRS) {
10400
- let entries;
10401
- try {
10402
- entries = await this.io.list(join(dir, supportDir));
10403
- } catch {
10404
- return true;
10405
- }
10406
- for (const entry of entries) {
10407
- if (!entry.endsWith(".lock")) continue;
10408
- if (await this.isWriterLock(join(dir, supportDir, entry))) return true;
10409
- }
10410
- }
10411
- return false;
10412
- }
10413
- /** P2 (v17): a `*.lock` file counts as a writer lock only when its body
10414
- * carries the io layer's protocol — `pid:token`, a bare pid, or an empty body
10415
- * (a creator between create and body write); anything else is user content
10416
- * (suffix-only matching refused archiving and deleted user files on restore).
10417
- * v37 (P2-8): the verdict is `decideTakeover`'s, so the mover refuses exactly
10418
- * the lock the io layer refuses to reclaim, the 30s empty window included. */
10419
- async isWriterLock(lockPath) {
10420
- let body;
10421
- try {
10422
- body = await this.io.readText(lockPath);
10245
+ async supportDirsUnlistable(dir) {
10246
+ for (const supportDir of SUPPORT_DIRS) try {
10247
+ await this.io.list(join(dir, supportDir));
10423
10248
  } catch {
10424
10249
  return true;
10425
10250
  }
10426
- if (body === null) return false;
10427
- const trimmed = body.trim();
10428
- if (trimmed !== "" && !/^\d+$/.test(trimmed) && !LOCK_BODY_RE.test(trimmed)) return false;
10429
- let mtimeMs = null;
10430
- try {
10431
- mtimeMs = await this.io.mtime?.(lockPath) ?? null;
10432
- } catch {
10433
- mtimeMs = null;
10434
- }
10435
- return decideTakeover({
10436
- body,
10437
- mtimeMs: mtimeMs ?? Date.now(),
10438
- alive: isProcessAlive
10439
- }) === "none";
10440
- }
10441
- /** P2 (v16): best-effort removal of lock residue inside a RESTORED tree.
10442
- * A1-2/A1-7 (v18): the sweep now covers the marker locks the probe checks
10443
- * (`SKILL.md.lock`/`.pinned.lock`/`.hermes-managed.lock`) and only removes
10444
- * a lock whose holder pid is NOT alive — a live writer's lock is never
10445
- * stolen by the sweep. A dead-pid residue would otherwise permanently
10446
- * refuse archive/restore. */
10447
- async deleteStrandedLocks(dir) {
10448
- for (const markerLock of MARKER_LOCK_NAMES) await this.sweepLockIfStranded(join(dir, markerLock));
10449
- for (const supportDir of SUPPORT_DIRS) {
10450
- let entries = [];
10451
- try {
10452
- entries = await this.io.list(join(dir, supportDir));
10453
- } catch {
10454
- continue;
10455
- }
10456
- for (const entry of entries) if (entry.endsWith(".lock")) await this.sweepLockIfStranded(join(dir, supportDir, entry));
10457
- }
10458
- }
10459
- /** R2 follow-up (audit of A2): the recycled-pid window from io's
10460
- * `ALIVE_LOCK_TAKEOVER_MS`, applied to the stranded-lock sweepers. A lock
10461
- * whose holder pid probes alive but whose mtime is older than the alive
10462
- * window is a recycled pid, not a live writer (no write in this family
10463
- * holds a lock for more than minutes) — without this, one recycled pid
10464
- * blocked whole-tree snapshot recovery forever with a "retry once the
10465
- * write completes" message that could never become true. A backend without
10466
- * `mtime` keeps the conservative refuse-on-alive posture. */
10467
- async lockHolderAgedOut(lockPath) {
10468
- const mtime = await this.io.mtime?.(lockPath).catch(() => null);
10469
- return typeof mtime === "number" && Date.now() - mtime > 864e5;
10470
- }
10471
- /** Remove `lockPath` only when its body has the writer-lock `pid:token`
10472
- * shape AND the holder pid is not alive (or is an aged-out recycled pid —
10473
- * R2 follow-up); anything else (a user support file or a live writer's
10474
- * lock) is left untouched. */
10475
- async sweepLockIfStranded(lockPath) {
10476
- const lockProbe = await probeText(this.io, lockPath);
10477
- if (isUnknown(lockProbe)) {
10478
- console.warn(`skill-store: could not read ${lockPath} to check whether its holder is alive (${lockProbe.reason}); leaving the lock in place`);
10479
- return;
10480
- }
10481
- if (!isPresent(lockProbe)) return;
10482
- const body = lockProbe.value;
10483
- const pid = parseLockBody(body);
10484
- if (pid === null) return;
10485
- if (Number.isInteger(pid) && pid > 0 && isProcessAlive(pid) && !await this.lockHolderAgedOut(lockPath)) return;
10486
- await this.io.remove(lockPath).catch(() => {});
10251
+ return false;
10487
10252
  }
10488
10253
  /** A1-4 (v18): a manifest-declared name is copied with `join(root, name)`;
10489
10254
  * only a single, non-traversing path component is safe. Dotfiles
@@ -10494,22 +10259,6 @@ var SkillLibrary = class {
10494
10259
  if (typeof name !== "string") return false;
10495
10260
  return name !== "" && name !== "." && name !== ".." && !name.includes("/") && !name.includes("\\") && basename(name) === name;
10496
10261
  }
10497
- /** A1-7 (v18): a root-level lock whose holder is alive must refuse the
10498
- * restore; a dead residue is swept so a crashed writer cannot block
10499
- * recovery. A non-lock body shape is left alone (user file). */
10500
- async refuseLiveLockOrSweep(lockPath, label) {
10501
- let body;
10502
- try {
10503
- body = await this.io.readText(lockPath);
10504
- } catch (error) {
10505
- throw new Error(`snapshot restore refused: cannot verify ${label} (${error instanceof Error ? error.message : String(error)}); a live writer may hold it`);
10506
- }
10507
- if (body === null) return;
10508
- const pid = parseLockBody(body);
10509
- if (pid === null) return;
10510
- if (Number.isInteger(pid) && pid > 0 && isProcessAlive(pid) && !await this.lockHolderAgedOut(lockPath)) throw new Error(`snapshot restore refused: ${label} is being written (write lock present); retry once the write completes`);
10511
- await this.io.remove(lockPath).catch(() => {});
10512
- }
10513
10262
  async archive(rawName, options = {}) {
10514
10263
  const name = rawName.trim();
10515
10264
  const badName = this.badName(name, { allowReserved: true });
@@ -10521,17 +10270,17 @@ var SkillLibrary = class {
10521
10270
  const md = await this.io.readText(join(dir, "SKILL.md"));
10522
10271
  if (md === null) return {
10523
10272
  ok: false,
10524
- message: `Skill "${name}" not found.`
10273
+ message: `技能「${name}」不存在。`
10525
10274
  };
10526
10275
  const protection = await this.deleteProtection(name, options);
10527
10276
  if (protection) return {
10528
10277
  ok: false,
10529
- message: protection === "pinned" ? `Skill "${name}" is pinned and cannot be archived. Remove the \`.pinned\` marker in its directory, then retry.` : `Skill "${name}" is protected (${protection}).`
10278
+ message: protection === "pinned" ? `Skill "${name}" is pinned and cannot be archived. Remove the \`.pinned\` marker in its directory, then retry.` : `技能「${name}」受保护(${protection})。`
10530
10279
  };
10531
10280
  if (options.absorbedInto) {
10532
10281
  if (options.absorbedInto.trim() === name) return {
10533
10282
  ok: false,
10534
- message: "absorbed_into cannot be the skill being archived (cannot absorb into itself)."
10283
+ message: "absorbed_into 不能是正在归档的技能本身(不能吸收到自己)。"
10535
10284
  };
10536
10285
  const intoBad = this.badName(options.absorbedInto.trim());
10537
10286
  if (intoBad) return {
@@ -10553,22 +10302,22 @@ var SkillLibrary = class {
10553
10302
  if (this.io.isSymlink) {
10554
10303
  if (await this.io.isSymlink(dir) === true) return {
10555
10304
  ok: false,
10556
- message: `Skill "${name}" is a symlink; refusing to archive it.`
10305
+ message: `技能「${name}」是符号链接;拒绝归档。`
10557
10306
  };
10558
10307
  }
10559
- if (await this.hasWriteLock(dir)) return {
10308
+ if (await this.supportDirsUnlistable(dir)) return {
10560
10309
  ok: false,
10561
- message: `Skill "${name}" is being written (write lock present); retry archiving once the write completes.`
10310
+ message: `无法检查技能「${name}」是否有写入方(有一个支撑目录列不出来):请重试归档。`
10562
10311
  };
10563
10312
  const commitProtection = await this.deleteProtection(name, options);
10564
10313
  if (commitProtection) return {
10565
10314
  ok: false,
10566
- message: commitProtection === "pinned" ? `Skill "${name}" is pinned and cannot be archived. Remove the \`.pinned\` marker in its directory, then retry.` : `Skill "${name}" is protected (${commitProtection}).`
10315
+ message: commitProtection === "pinned" ? `Skill "${name}" is pinned and cannot be archived. Remove the \`.pinned\` marker in its directory, then retry.` : `技能「${name}」受保护(${commitProtection})。`
10567
10316
  };
10568
10317
  const moveFailure = await this.moveDir(dir, dest);
10569
10318
  if (moveFailure !== void 0) return {
10570
10319
  ok: false,
10571
- message: `Skill "${name}" archive failed: ${moveFailure}.`
10320
+ message: `技能「${name}」归档失败:${moveFailure}。`
10572
10321
  };
10573
10322
  const reason = options.reason ?? (options.absorbedInto ? `Consolidated into ${options.absorbedInto}` : "Archived by self-evolution curator");
10574
10323
  try {
@@ -10585,7 +10334,7 @@ var SkillLibrary = class {
10585
10334
  });
10586
10335
  return {
10587
10336
  ok: true,
10588
- message: `Skill "${name}" archived to .archive.`,
10337
+ message: `技能「${name}」已归档到 .archive。`,
10589
10338
  path: dest
10590
10339
  };
10591
10340
  }
@@ -10683,7 +10432,7 @@ var SkillLibrary = class {
10683
10432
  const mode = options.mode ?? "append";
10684
10433
  if (normalizedSources.length === 0) return {
10685
10434
  ok: false,
10686
- message: "Consolidation requires at least one distinct source skill."
10435
+ message: "合并至少需要一个互不相同的来源技能。"
10687
10436
  };
10688
10437
  for (const name of [targetName, ...normalizedSources]) {
10689
10438
  const bad = this.badName(name);
@@ -10696,7 +10445,7 @@ var SkillLibrary = class {
10696
10445
  const targetProtection = await this.writeProtection(targetName, origin);
10697
10446
  if (targetProtection) return {
10698
10447
  ok: false,
10699
- message: `Skill "${targetName}" is protected (${targetProtection}).`
10448
+ message: `技能「${targetName}」受保护(${targetProtection})。`
10700
10449
  };
10701
10450
  return await this.serial(async () => {
10702
10451
  const referenceWrites = [];
@@ -10708,17 +10457,17 @@ var SkillLibrary = class {
10708
10457
  const protection = await this.deleteProtection(source);
10709
10458
  if (protection) return {
10710
10459
  ok: false,
10711
- message: `Skill "${source}" is protected (${protection}).`
10460
+ message: `技能「${source}」受保护(${protection})。`
10712
10461
  };
10713
10462
  const sourceMd = await this.io.readText(join(this.dirOf(source), "SKILL.md"));
10714
10463
  if (!sourceMd) return {
10715
10464
  ok: false,
10716
- message: `Skill "${source}" not found.`
10465
+ message: `技能「${source}」不存在。`
10717
10466
  };
10718
10467
  const parsed = parseFrontmatter(sourceMd);
10719
10468
  if (!parsed) return {
10720
10469
  ok: false,
10721
- message: `Skill "${source}" has no valid frontmatter; refusing to merge.`
10470
+ message: `技能「${source}」没有有效的 frontmatter;拒绝合并。`
10722
10471
  };
10723
10472
  const carriesSupport = await this.countSupportDirs(source) > 0;
10724
10473
  const refs = supportRefs(parsed.body);
@@ -10727,7 +10476,7 @@ var SkillLibrary = class {
10727
10476
  const decision = await this.planSourceRehoming(source, targetName, parsed.body, rehomed.map((file) => file.to));
10728
10477
  if (decision.kind === "blocked") return {
10729
10478
  ok: false,
10730
- message: `Consolidation rejected: ${carriesSupport ? `source "${source}" carries support files` : `source "${source}" body references support files (${refs.join(", ")})`} that would be left behind — use mode:'reference' or archive the whole package instead.${decision.note}`
10479
+ message: `合并被拒绝:${carriesSupport ? `source "${source}" carries support files` : `source "${source}" body references support files (${refs.join(", ")})`} 会被落下。请改用 mode:'reference',或直接归档整个包。${decision.note}`
10731
10480
  };
10732
10481
  rehomed.push(...decision.files);
10733
10482
  appendedBody = decision.rewritten;
@@ -10739,17 +10488,17 @@ var SkillLibrary = class {
10739
10488
  const protection = await this.deleteProtection(source);
10740
10489
  if (protection) return {
10741
10490
  ok: false,
10742
- message: `Skill "${source}" is protected (${protection}).`
10491
+ message: `技能「${source}」受保护(${protection})。`
10743
10492
  };
10744
10493
  const sourceMd = await this.io.readText(join(this.dirOf(source), "SKILL.md"));
10745
10494
  if (!sourceMd) return {
10746
10495
  ok: false,
10747
- message: `Skill "${source}" not found.`
10496
+ message: `技能「${source}」不存在。`
10748
10497
  };
10749
10498
  const parsed = parseFrontmatter(sourceMd);
10750
10499
  if (!parsed) return {
10751
10500
  ok: false,
10752
- message: `Skill "${source}" has no valid frontmatter; refusing to demote.`
10501
+ message: `技能「${source}」没有有效的 frontmatter;拒绝降级。`
10753
10502
  };
10754
10503
  const refs = supportRefs(parsed.body);
10755
10504
  let demotedBody = parsed.body;
@@ -10757,7 +10506,7 @@ var SkillLibrary = class {
10757
10506
  const decision = await this.planSourceRehoming(source, targetName, parsed.body, rehomed.map((file) => file.to));
10758
10507
  if (decision.kind === "blocked") return {
10759
10508
  ok: false,
10760
- message: `Consolidation rejected: source "${source}" body references support files (${refs.join(", ")}) that would be left behind — archive the whole package instead.${decision.note}`
10509
+ message: `合并被拒绝:来源技能「${source}」的正文引用了会被落下的支撑文件(${refs.join(", ")}):请改为归档整个包。${decision.note}`
10761
10510
  };
10762
10511
  rehomed.push(...decision.files);
10763
10512
  demotedBody = decision.rewritten;
@@ -10774,7 +10523,7 @@ var SkillLibrary = class {
10774
10523
  try {
10775
10524
  if (!await this.io.readText(join(targetDir, "SKILL.md"))) return {
10776
10525
  ok: false,
10777
- message: `Skill "${targetName}" not found.`
10526
+ message: `技能「${targetName}」不存在。`
10778
10527
  };
10779
10528
  for (const source of normalizedSources) {
10780
10529
  if (plannedSourceBytes.has(source)) {
@@ -10837,7 +10586,7 @@ var SkillLibrary = class {
10837
10586
  if (!result.ok) throw new Error(result.message);
10838
10587
  return {
10839
10588
  ok: true,
10840
- message: `Consolidated ${normalizedSources.join(", ")} into "${targetName}".`,
10589
+ message: `已把 ${normalizedSources.join(", ")} 合并进「${targetName}」。`,
10841
10590
  path: targetDir
10842
10591
  };
10843
10592
  } catch (error) {
@@ -10850,11 +10599,11 @@ var SkillLibrary = class {
10850
10599
  }
10851
10600
  if (failedRestores.length > 0) return {
10852
10601
  ok: false,
10853
- message: `Consolidation failed (${reason}); rolled back EXCEPT ${failedRestores.join(", ")} — still in .archive, restore them with /evolution skill restore.`
10602
+ message: `合并失败(${reason});已回滚,但以下未回滚:${failedRestores.join(", ")}:它们仍在 .archive 里,请用 /evolution skill restore 恢复。`
10854
10603
  };
10855
10604
  return {
10856
10605
  ok: false,
10857
- message: `Consolidation failed and was rolled back: ${reason}`
10606
+ message: `合并失败,已回滚:${reason}`
10858
10607
  };
10859
10608
  }
10860
10609
  });
@@ -10883,21 +10632,21 @@ var SkillLibrary = class {
10883
10632
  };
10884
10633
  if (moves.length === 0) return {
10885
10634
  ok: false,
10886
- message: "Restructure requires at least one section move."
10635
+ message: "重构至少需要一次章节搬移。"
10887
10636
  };
10888
10637
  if (moves.length > 5) return {
10889
10638
  ok: false,
10890
- message: `Restructure exceeds 5 moves.`
10639
+ message: `重构超过 5 次搬移。`
10891
10640
  };
10892
10641
  for (const move of moves) {
10893
10642
  const raw = move;
10894
10643
  if (raw === null || typeof raw !== "object") return {
10895
10644
  ok: false,
10896
- message: "Every restructure move must be an object with a heading."
10645
+ message: "每次重构搬移都必须是带 heading 的对象。"
10897
10646
  };
10898
10647
  if (typeof move.heading !== "string" || !move.heading.trim()) return {
10899
10648
  ok: false,
10900
- message: "Every restructure move needs a non-empty heading."
10649
+ message: "每次重构搬移都需要非空的 heading。"
10901
10650
  };
10902
10651
  const targetIssue = validateRestructureTarget(move.toFile);
10903
10652
  if (targetIssue) return {
@@ -10909,25 +10658,25 @@ var SkillLibrary = class {
10909
10658
  const md = await this.io.readText(join(dir, "SKILL.md"));
10910
10659
  if (!md) return {
10911
10660
  ok: false,
10912
- message: `Skill "${name}" not found.`
10661
+ message: `技能「${name}」不存在。`
10913
10662
  };
10914
10663
  const block = frontmatterBlock(md);
10915
10664
  if (!block) return {
10916
10665
  ok: false,
10917
- message: "SKILL.md has no valid frontmatter; refusing to restructure."
10666
+ message: "SKILL.md 没有有效的 frontmatter;拒绝重构。"
10918
10667
  };
10919
10668
  const header = block.lines.slice(0, block.end + 1).join("\n");
10920
10669
  const plan = planRestructureSections(md.slice(header.length).replace(/\r\n/g, "\n"), moves);
10921
10670
  if ("error" in plan) return {
10922
10671
  ok: false,
10923
- message: `Restructure rejected: ${plan.error}`
10672
+ message: `重构被拒绝:${plan.error}`
10924
10673
  };
10925
10674
  const newMd = `${header}${plan.body}`.replace(/\r\n/g, "\n");
10926
10675
  const finalMd = block.nl === "\r\n" ? newMd.replace(/\n/g, "\r\n") : newMd;
10927
10676
  const newMdCheck = validateFrontmatter(finalMd, name, this.limits);
10928
10677
  if (newMdCheck) return {
10929
10678
  ok: false,
10930
- message: `Restructure rejected: ${newMdCheck}`
10679
+ message: `重构被拒绝:${newMdCheck}`
10931
10680
  };
10932
10681
  const listing = (this.limits.citationPolicy ?? "verify") === "refuse" ? null : await this.listSupportFiles(name);
10933
10682
  const files = listing !== null && isPresent(listing) ? listing.value : null;
@@ -10936,7 +10685,7 @@ var SkillLibrary = class {
10936
10685
  if (refs.length === 0) continue;
10937
10686
  if (files === null) return {
10938
10687
  ok: false,
10939
- message: `Restructure rejected: section "## ${section.heading}" references support files (${refs.join(", ")}) that stay behind — split the section or move it with its files.`
10688
+ message: `重构被拒绝:章节「## ${section.heading}」引用了会留在原地的支撑文件(${refs.join(", ")}):请拆开该章节,或连同它的文件一起搬。`
10940
10689
  };
10941
10690
  const report = resolveCitations({
10942
10691
  content: section.text,
@@ -10947,7 +10696,7 @@ var SkillLibrary = class {
10947
10696
  const missing = report.dangling.map((ref) => ref.target ?? ref.raw).join(", ");
10948
10697
  return {
10949
10698
  ok: false,
10950
- message: `Restructure rejected: section "## ${section.heading}" cites support file(s) that do not exist (${missing}) — fix the citation or create the file first.`
10699
+ message: `重构被拒绝:章节「## ${section.heading}」引用的支撑文件不存在(${missing}):请修正引用,或先创建该文件。`
10951
10700
  };
10952
10701
  }
10953
10702
  }
@@ -10988,7 +10737,7 @@ var SkillLibrary = class {
10988
10737
  if (!result.ok) return result;
10989
10738
  return {
10990
10739
  ok: true,
10991
- message: `Restructured "${name}": moved ${plan.sections.length} section(s) to references/.`,
10740
+ message: `已重构「${name}」:把 ${plan.sections.length} 个章节搬到 references/。`,
10992
10741
  path: dir
10993
10742
  };
10994
10743
  }
@@ -11010,12 +10759,12 @@ var SkillLibrary = class {
11010
10759
  const md = await this.io.readText(join(dir, "SKILL.md"));
11011
10760
  if (!md) return {
11012
10761
  ok: false,
11013
- message: `Skill "${name}" not found.`
10762
+ message: `技能「${name}」不存在。`
11014
10763
  };
11015
10764
  const protection = plan.protection === "write" ? await this.writeProtection(name, plan.origin) : plan.protection === "delete" ? await this.deleteProtection(name) : null;
11016
10765
  if (protection) return {
11017
10766
  ok: false,
11018
- message: `Skill "${name}" is protected (${protection}).`
10767
+ message: `技能「${name}」受保护(${protection})。`
11019
10768
  };
11020
10769
  const landing = [];
11021
10770
  for (const write of plan.writes) {
@@ -11025,12 +10774,12 @@ var SkillLibrary = class {
11025
10774
  } catch (error) {
11026
10775
  return {
11027
10776
  ok: false,
11028
- message: `Tree change refused: cannot safely pre-read ${write.target} (${error instanceof Error ? error.message : String(error)}); no writes were performed`
10777
+ message: `拒绝目录树变更:无法安全预读 ${write.target}(${error instanceof Error ? error.message : String(error)});未执行任何写入`
11029
10778
  };
11030
10779
  }
11031
10780
  if (Buffer.byteLength(write.content, "utf8") > this.limits.maxSkillFileBytes) return {
11032
10781
  ok: false,
11033
- message: `Write exceeds ${this.limits.maxSkillFileBytes} bytes: ${write.target}`
10782
+ message: `写入超过 ${this.limits.maxSkillFileBytes} 字节:${write.target}`
11034
10783
  };
11035
10784
  const threat = this.contentThreatBlock(write.content);
11036
10785
  if (threat) return {
@@ -11085,11 +10834,11 @@ var SkillLibrary = class {
11085
10834
  const reason = error instanceof Error ? error.message : String(error);
11086
10835
  if (stuck.length === 0) return {
11087
10836
  ok: false,
11088
- message: `Tree change failed and was rolled back: ${reason}`
10837
+ message: `目录树变更失败,已回滚:${reason}`
11089
10838
  };
11090
10839
  return {
11091
10840
  ok: false,
11092
- message: `Tree change failed: ${reason}. The rollback could not restore ${stuck.join(", ")} — recover those targets from the .backups snapshot (or re-apply the change).`
10841
+ message: `目录树变更失败:${reason}。回滚无法恢复 ${stuck.join(", ")}:请从 .backups 快照恢复这些目标(或重新应用这次变更)。`
11093
10842
  };
11094
10843
  }
11095
10844
  const skillMdWrite = landing.find((entry) => entry.target.split(/[\\/]/).pop() === "SKILL.md");
@@ -11103,7 +10852,7 @@ var SkillLibrary = class {
11103
10852
  });
11104
10853
  return {
11105
10854
  ok: true,
11106
- message: durabilityWarning === "" ? `${plan.eventAction} "${name}" succeeded.` : `${plan.eventAction} "${name}" succeeded (warning: the write landed but the directory fsync failed — durability unconfirmed: ${durabilityWarning})`,
10855
+ message: durabilityWarning === "" ? `${plan.eventAction} "${name}" succeeded.` : `${plan.eventAction} "${name}" succeeded (警告:写入已落盘,但目录 fsync 失败 —— 持久性未确认:${durabilityWarning})`,
11107
10856
  path: dir
11108
10857
  };
11109
10858
  }
@@ -11122,7 +10871,7 @@ var SkillLibrary = class {
11122
10871
  const dest = this.dirOf(name);
11123
10872
  if (await this.io.exists(dest)) return {
11124
10873
  ok: false,
11125
- message: await this.io.exists(join(dest, "SKILL.md")) ? `Skill "${name}" already exists in the active root; refusing to overwrite.` : `Skill directory "${name}" already exists in the active root but carries no SKILL.md; remove or repair it before restoring.`
10874
+ message: await this.io.exists(join(dest, "SKILL.md")) ? `技能「${name}」已存在于当前根目录;拒绝覆盖。` : `技能目录「${name}」已存在于当前根目录,但没有 SKILL.md;恢复前请先移除或修复它。`
11126
10875
  };
11127
10876
  let caseVariant;
11128
10877
  try {
@@ -11130,7 +10879,7 @@ var SkillLibrary = class {
11130
10879
  } catch {}
11131
10880
  if (caseVariant !== void 0) return {
11132
10881
  ok: false,
11133
- message: `Skill "${caseVariant}" (same name, different letter case) already exists in the active root; restoring "${name}" beside it would create ambiguous duplicates — restore as "${caseVariant}" or remove the variant first.`
10882
+ message: `技能「${caseVariant}」(同名、仅大小写不同)已存在于当前根目录;把「${name}」恢复到它旁边会造成含义不清的重复:请恢复为「${caseVariant}」,或先移除该变体。`
11134
10883
  };
11135
10884
  const archiveRoot = join(this.root, ".archive");
11136
10885
  let entries;
@@ -11139,7 +10888,7 @@ var SkillLibrary = class {
11139
10888
  } catch {
11140
10889
  return {
11141
10890
  ok: false,
11142
- message: "No skill archive available."
10891
+ message: "没有可用的技能归档。"
11143
10892
  };
11144
10893
  }
11145
10894
  const candidates = entries.filter((entry) => entry === name || entry.startsWith(`${name}-`)).sort().reverse();
@@ -11174,30 +10923,29 @@ var SkillLibrary = class {
11174
10923
  if (!chosen) {
11175
10924
  if (unreadable.length === 0) return {
11176
10925
  ok: false,
11177
- message: `Skill "${name}" is not in .archive.`
10926
+ message: `技能「${name}」不在 .archive 里。`
11178
10927
  };
11179
10928
  return {
11180
10929
  ok: false,
11181
- message: `No archived entry for "${name}" carries a matching frontmatter name, but .archive holds ${unreadable.length} entr(y/ies) named like it: ${unreadable.join(", ")}. Those entries have no readable SKILL.md (or none matching the name), so restoring by name cannot tell them apart — rename the directory to the skill name and restore again, or restore from a snapshot.`
10930
+ message: `.archive 里没有「${name}」对应的 frontmatter 名,但有 ${unreadable.length} 个名字相似的归档项:${unreadable.join(", ")}。这些归档项没有可读的 SKILL.md(或名字对不上),按名字恢复无法区分它们:请把目录改成技能名后再次恢复,或改用快照恢复。`
11182
10931
  };
11183
10932
  }
11184
10933
  const source = join(archiveRoot, chosen);
11185
10934
  if (this.io.isSymlink) {
11186
10935
  if (await this.io.isSymlink(source) === true) return {
11187
10936
  ok: false,
11188
- message: `Archived entry "${chosen}" is a symlink; refusing to restore it.`
10937
+ message: `归档项「${chosen}」是符号链接;拒绝恢复。`
11189
10938
  };
11190
10939
  }
11191
- if (await this.hasWriteLock(dest)) return {
10940
+ if (await this.supportDirsUnlistable(dest)) return {
11192
10941
  ok: false,
11193
- message: `Skill "${name}" is being written (write lock present); retry restoring once the write completes.`
10942
+ message: `无法检查技能「${name}」是否有写入方(有一个支撑目录列不出来):请重试恢复。`
11194
10943
  };
11195
10944
  const moveFailure = await this.moveDir(source, dest);
11196
10945
  if (moveFailure !== void 0) return {
11197
10946
  ok: false,
11198
- message: `Restore of "${name}" from .archive failed: ${moveFailure}`
10947
+ message: `从 .archive 恢复「${name}」失败:${moveFailure}`
11199
10948
  };
11200
- await this.deleteStrandedLocks(dest);
11201
10949
  for (const marker of [".archive-reason", ".archive-name"]) if (await this.io.exists(join(dest, marker))) await this.io.remove(join(dest, marker));
11202
10950
  const restoredProbe = await probeText(this.io, join(dest, "SKILL.md"));
11203
10951
  if (isUnknown(restoredProbe)) console.warn(`evolution-skill-store: the restored SKILL.md of "${name}" could not be read back (${restoredProbe.reason}); the restore is recorded without after-bytes`);
@@ -11209,7 +10957,7 @@ var SkillLibrary = class {
11209
10957
  });
11210
10958
  return {
11211
10959
  ok: true,
11212
- message: `Skill "${name}" restored from .archive.`,
10960
+ message: `技能「${name}」已从 .archive 恢复。`,
11213
10961
  path: dest
11214
10962
  };
11215
10963
  }
@@ -11226,12 +10974,12 @@ var SkillLibrary = class {
11226
10974
  const dir = this.dirOf(name);
11227
10975
  if (!await this.io.exists(join(dir, "SKILL.md"))) return {
11228
10976
  ok: false,
11229
- message: `Skill "${name}" not found.`
10977
+ message: `技能「${name}」不存在。`
11230
10978
  };
11231
10979
  const protection = await this.writeProtection(name, origin);
11232
10980
  if (protection) return {
11233
10981
  ok: false,
11234
- message: `Skill "${name}" is protected (${protection}).`
10982
+ message: `技能「${name}」受保护(${protection})。`
11235
10983
  };
11236
10984
  const validation = validateSupportPath(filePath);
11237
10985
  if (validation) return {
@@ -11240,7 +10988,7 @@ var SkillLibrary = class {
11240
10988
  };
11241
10989
  if (Buffer.byteLength(content, "utf8") > this.limits.maxSkillFileBytes) return {
11242
10990
  ok: false,
11243
- message: `Support file exceeds ${this.limits.maxSkillFileBytes} bytes.`
10991
+ message: `支撑文件超过 ${this.limits.maxSkillFileBytes} 字节。`
11244
10992
  };
11245
10993
  const threat = this.contentThreatBlock(content);
11246
10994
  if (threat) return {
@@ -11260,14 +11008,14 @@ var SkillLibrary = class {
11260
11008
  if (overCap && !repairing && this.supportFileCharPolicy() === "enforce") return {
11261
11009
  result: {
11262
11010
  ok: false,
11263
- message: `Support file "${filePath}" content exceeds ${this.limits.maxSkillContentChars} characters. ${CONTENT_SPLIT_HINT}`
11011
+ message: `支撑文件「${filePath}」的内容超过 ${this.limits.maxSkillContentChars} 个字符。${CONTENT_SPLIT_HINT}`
11264
11012
  },
11265
11013
  write: null
11266
11014
  };
11267
11015
  if (current !== null && content.trimEnd() === current.trimEnd()) return {
11268
11016
  result: {
11269
11017
  ok: true,
11270
- message: `Support file "${filePath}" unchanged: the supplied content already matches the current file; nothing written.`,
11018
+ message: `支撑文件「${filePath}」未变更:提供的内容与当前文件一致,未写入。`,
11271
11019
  noop: true,
11272
11020
  path: target
11273
11021
  },
@@ -11276,7 +11024,7 @@ var SkillLibrary = class {
11276
11024
  return {
11277
11025
  result: {
11278
11026
  ok: true,
11279
- message: `Support file "${filePath}" written to "${name}".${capAdvisory}`,
11027
+ message: `支撑文件「${filePath}」已写入「${name}」。${capAdvisory}`,
11280
11028
  path: target
11281
11029
  },
11282
11030
  write: content,
@@ -11310,12 +11058,12 @@ var SkillLibrary = class {
11310
11058
  const dir = this.dirOf(name);
11311
11059
  if (!await this.io.exists(join(dir, "SKILL.md"))) return {
11312
11060
  ok: false,
11313
- message: `Skill "${name}" not found.`
11061
+ message: `技能「${name}」不存在。`
11314
11062
  };
11315
11063
  const protection = await this.writeProtection(name, origin);
11316
11064
  if (protection) return {
11317
11065
  ok: false,
11318
- message: `Skill "${name}" is protected (${protection}).`
11066
+ message: `技能「${name}」受保护(${protection})。`
11319
11067
  };
11320
11068
  const validation = validateSupportPath(filePath);
11321
11069
  if (validation) return {
@@ -11327,7 +11075,7 @@ var SkillLibrary = class {
11327
11075
  if (anchor !== void 0) return anchorRefusalFile(name, filePath, "missing");
11328
11076
  return {
11329
11077
  ok: false,
11330
- message: `File "${filePath}" not found in skill "${name}".`
11078
+ message: `文件「${filePath}」不在技能「${name}」里。`
11331
11079
  };
11332
11080
  }
11333
11081
  const beforeProbe = await probeText(this.io, target);
@@ -11351,7 +11099,7 @@ var SkillLibrary = class {
11351
11099
  });
11352
11100
  if (!ran.done) return {
11353
11101
  ok: false,
11354
- message: "internal error: the delete transaction did not invoke the task; nothing was removed"
11102
+ message: "内部错误:删除事务没有调用任务;没有删除任何东西"
11355
11103
  };
11356
11104
  } else if (verdict === "match") await this.io.remove(target);
11357
11105
  if (verdict !== "match") return anchorRefusalFile(name, filePath, verdict);
@@ -11364,7 +11112,7 @@ var SkillLibrary = class {
11364
11112
  });
11365
11113
  return {
11366
11114
  ok: true,
11367
- message: `Support file "${filePath}" removed from "${name}".`,
11115
+ message: `支撑文件「${filePath}」已从「${name}」移除。`,
11368
11116
  path: target
11369
11117
  };
11370
11118
  }
@@ -11445,8 +11193,8 @@ var SkillLibrary = class {
11445
11193
  const names = await listNames(this.root, this.io);
11446
11194
  const skipped = [];
11447
11195
  const copyable = [];
11448
- for (const name of names) if (await this.hasWriteLock(this.dirOf(name))) {
11449
- console.warn(`skill-store: snapshot skipped "${name}" — a byte-writer holds its write lock; the skill is recorded as skipped in the manifest`);
11196
+ for (const name of names) if (await this.supportDirsUnlistable(this.dirOf(name))) {
11197
+ console.warn(`skill-store: snapshot skipped "${name}" — its support directory could not be listed; the skill is recorded as skipped in the manifest`);
11450
11198
  skipped.push(name);
11451
11199
  } else copyable.push(name);
11452
11200
  const copyFailure = (await Promise.allSettled(copyable.map(async (name) => {
@@ -11454,7 +11202,7 @@ var SkillLibrary = class {
11454
11202
  }))).find((result) => result.status === "rejected");
11455
11203
  if (copyFailure) throw copyFailure.reason;
11456
11204
  const suspect = [];
11457
- for (const name of copyable) if (await this.hasWriteLock(this.dirOf(name))) suspect.push(name);
11205
+ for (const name of copyable) if (await this.supportDirsUnlistable(this.dirOf(name))) suspect.push(name);
11458
11206
  if (suspect.length > 0) {
11459
11207
  for (const name of suspect) console.warn(`skill-store: snapshot demoted "${name}" to skipped — a write lock appeared while its copy ran; the copied bytes are suspect`);
11460
11208
  for (const name of suspect) copyable.splice(copyable.indexOf(name), 1);
@@ -11582,7 +11330,7 @@ var SkillLibrary = class {
11582
11330
  out.push({
11583
11331
  path: join(backupRoot, name),
11584
11332
  createdAt: "",
11585
- reason: "unreadable or missing manifest (orphan snapshot)"
11333
+ reason: "清单读不出来或缺失(孤儿快照)"
11586
11334
  });
11587
11335
  continue;
11588
11336
  }
@@ -11624,12 +11372,12 @@ var SkillLibrary = class {
11624
11372
  const listed = await this.listSnapshots();
11625
11373
  if (isUnknown(listed)) return {
11626
11374
  ok: false,
11627
- message: `Snapshot restore refused: the snapshot directory could not be read (${listed.reason}) — nothing was changed.`
11375
+ message: `拒绝快照恢复:快照目录读不出来(${listed.reason})—— 未做任何改动。`
11628
11376
  };
11629
11377
  const latest = isPresent(listed) ? listed.value[0] : void 0;
11630
11378
  if (!latest) return {
11631
11379
  ok: false,
11632
- message: "No skill snapshot available."
11380
+ message: "没有可用的技能快照。"
11633
11381
  };
11634
11382
  let preRollbackPath;
11635
11383
  try {
@@ -11637,7 +11385,7 @@ var SkillLibrary = class {
11637
11385
  } catch (error) {
11638
11386
  return {
11639
11387
  ok: false,
11640
- message: `Snapshot restore was refused before anything was cleared: taking the pre-rollback snapshot failed (${error instanceof Error ? error.message : String(error)}) — the active tree is UNCHANGED. Resolve the snapshot write failure and retry.`
11388
+ message: `在任何清理之前就拒绝了快照恢复:生成回滚前快照失败(${error instanceof Error ? error.message : String(error)})—— 活动目录树未改变。请解决快照写入失败后重试。`
11641
11389
  };
11642
11390
  }
11643
11391
  let snapshotExtras;
@@ -11646,7 +11394,7 @@ var SkillLibrary = class {
11646
11394
  } catch (error) {
11647
11395
  return {
11648
11396
  ok: false,
11649
- message: `Snapshot restore was refused before anything was cleared: reading the snapshot manifest/extras failed (${error instanceof Error ? error.message : String(error)}) — the active tree is UNCHANGED. Resolve the read failure and retry.`
11397
+ message: `在任何清理之前就拒绝了快照恢复:读取快照清单或附属文件失败(${error instanceof Error ? error.message : String(error)})—— 活动目录树未改变。请解决读取失败后重试。`
11650
11398
  };
11651
11399
  }
11652
11400
  try {
@@ -11657,17 +11405,17 @@ var SkillLibrary = class {
11657
11405
  await this.restoreSnapshotIntoRoot(preRollbackPath);
11658
11406
  return {
11659
11407
  ok: false,
11660
- message: `Snapshot restore failed (${reason}); the active tree was rolled back to the pre-rollback snapshot.`
11408
+ message: `快照恢复失败(${reason});活动目录树已回滚到回滚前的快照。`
11661
11409
  };
11662
11410
  } catch (rollbackError) {
11663
11411
  const rb = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
11664
11412
  if (isPreClearRefusal(reason) && isPreClearRefusal(rb)) return {
11665
11413
  ok: false,
11666
- message: `Snapshot restore was refused before anything was cleared; the active tree is UNCHANGED.\n- target: ${reason}\n- pre-rollback: ${rb}\nResolve the cause (a live skill write, or an incomplete snapshot) and retry — no manual rescue is needed.`
11414
+ message: `在任何清理之前就拒绝了快照恢复; 活动目录树未改变。\n- 目标:${reason}\n- 回滚前快照:${rb}\n请解决原因(有存活的技能写入,或快照不完整)后重试 —— 无需手动抢救。`
11667
11415
  };
11668
11416
  return {
11669
11417
  ok: false,
11670
- message: `Snapshot restore failed (${reason}) AND pre-rollback restore failed (${rb}). Rescue manually from: ${preRollbackPath} (pre-rollback), ${latest.path} (target).`
11418
+ message: `快照恢复失败(${reason}),且回滚前快照的恢复也失败(${rb})。请手动抢救:${preRollbackPath}(回滚前)、${latest.path}(目标)。`
11671
11419
  };
11672
11420
  }
11673
11421
  }
@@ -11677,7 +11425,7 @@ var SkillLibrary = class {
11677
11425
  });
11678
11426
  return {
11679
11427
  ok: true,
11680
- message: `Restored skill tree from ${latest.path}`,
11428
+ message: `已从 ${latest.path} 恢复技能目录树`,
11681
11429
  path: latest.path,
11682
11430
  ...snapshotExtras.length === 0 ? {} : { extras: snapshotExtras }
11683
11431
  };
@@ -11713,14 +11461,9 @@ var SkillLibrary = class {
11713
11461
  }
11714
11462
  for (const entry of rootEntries) {
11715
11463
  if (entry === ".archive" || entry === ".backups" || entry === ".mutations.json" || entry === ".curator-suppressed.json" || entry === ".history") continue;
11716
- if (entry.endsWith(".lock") || entry.endsWith(`.lock.next`)) {
11717
- await this.refuseLiveLockOrSweep(join(this.root, entry), entry);
11718
- continue;
11719
- }
11720
11464
  const dir = join(this.root, entry);
11721
11465
  if (await this.io.exists(join(dir, "SKILL.md"))) {
11722
- await this.deleteStrandedLocks(dir);
11723
- if (await this.hasWriteLock(dir)) throw new Error(`snapshot restore refused: skill "${entry}" is being written (write lock present); retry once the write completes`);
11466
+ if (await this.supportDirsUnlistable(dir)) throw new Error(`snapshot restore refused: skill "${entry}" is being written (write lock present); retry once the write completes`);
11724
11467
  }
11725
11468
  }
11726
11469
  const restoresSuppressed = manifest.sidecars.includes(".curator-suppressed.json");
@@ -11728,7 +11471,6 @@ var SkillLibrary = class {
11728
11471
  if (entry === ".archive" || entry === ".backups" || entry === ".mutations.json") continue;
11729
11472
  if (entry === ".history") continue;
11730
11473
  if (entry === ".curator-suppressed.json" && restoresSuppressed) continue;
11731
- if (entry.endsWith(".lock") || entry.endsWith(`.lock.next`)) continue;
11732
11474
  await this.io.remove(join(this.root, entry));
11733
11475
  }
11734
11476
  for (const name of manifest.skills) await this.io.copy(join(snapshotPath, name), join(this.root, name));
@@ -11740,10 +11482,7 @@ var SkillLibrary = class {
11740
11482
  await this.io.copy(join(snapshotPath, ".archive"), archiveRoot);
11741
11483
  } else if (manifest.hasArchive === false) await this.io.remove(archiveRoot);
11742
11484
  }
11743
- for (const entry of await this.io.list(this.root)) {
11744
- if (entry.startsWith(".")) continue;
11745
- await this.deleteStrandedLocks(join(this.root, entry));
11746
- }
11485
+ for (const entry of await this.io.list(this.root)) if (entry.startsWith(".")) continue;
11747
11486
  }
11748
11487
  };
11749
11488
  function newSkillLibrary(options) {
@@ -12358,6 +12097,7 @@ const PARAM_EXPOSURE = Object.freeze([
12358
12097
  control: "number",
12359
12098
  unit: "天",
12360
12099
  values: "",
12100
+ relation: { atLeast: "staleAfterDays" },
12361
12101
  card: "evolution-curator"
12362
12102
  },
12363
12103
  {
@@ -12999,6 +12739,23 @@ const PARAM_EXPOSURE = Object.freeze([
12999
12739
  values: "",
13000
12740
  card: "evolution-approval"
13001
12741
  },
12742
+ {
12743
+ id: "approvalConfirm",
12744
+ group: "write-caps",
12745
+ tier: "E3",
12746
+ authority: "cordis",
12747
+ owner: "evolution-approval",
12748
+ applies: "live",
12749
+ docAnchor: "PARAMETERS.md#write-caps",
12750
+ summary: "What the confirmation before an APPROVE does: write straight through, ask first, or treat an unanswered prompt as a cancel. The same three words skillWriteConfirm uses for a skill write.",
12751
+ label: "批准前先确认",
12752
+ hint: "点「批准」时要不要再确认一次:直接执行 / 先确认 / 等待超时即取消。默认直接执行(不打断批量批准)。",
12753
+ control: "select",
12754
+ unit: "",
12755
+ values: "auto|ask|timeout",
12756
+ valueLabels: "直接执行|先确认|等待超时即取消",
12757
+ card: "evolution-approval"
12758
+ },
13002
12759
  {
13003
12760
  id: "qualityWarnThreshold",
13004
12761
  group: "deployment",
@@ -13269,14 +13026,6 @@ function readNumberParam(carrier, id) {
13269
13026
  * @module @lmzhen/dsh-evolution-core
13270
13027
  */
13271
13028
  /**
13272
- * Whether a parsed value is a plain mapping (a settings section).
13273
- * @param value - the parsed value.
13274
- * @returns true for a non-null, non-array object.
13275
- */
13276
- function isSection(value) {
13277
- return typeof value === "object" && value !== null && !Array.isArray(value);
13278
- }
13279
- /**
13280
13029
  * The key names one row may be written with: its own E3 rows, plus the deprecated
13281
13030
  * spellings that still resolve to them. An unknown key is refused instead of written —
13282
13031
  * the platform validates a write against the row's schema, so writing it would fail the
@@ -13307,7 +13056,7 @@ function planNamespaceMigration(documentText) {
13307
13056
  foreign: [],
13308
13057
  skipped: []
13309
13058
  };
13310
- if (!isSection(parsed)) throw new Error("the settings document is not a mapping; refusing to migrate it");
13059
+ if (!isRecord(parsed)) throw new Error("the settings document is not a mapping; refusing to migrate it");
13311
13060
  const legacy = new Map(NAMESPACE_MIGRATIONS.map((entry) => [entry.namespace, entry.rowId]));
13312
13061
  const entries = [];
13313
13062
  const empty = [];
@@ -13315,7 +13064,7 @@ function planNamespaceMigration(documentText) {
13315
13064
  for (const [namespace, section] of Object.entries(parsed)) {
13316
13065
  const rowId = legacy.get(namespace);
13317
13066
  if (rowId === void 0) continue;
13318
- if (!isSection(section)) throw new Error("settings section `" + namespace + "` is not a mapping; refusing to migrate it");
13067
+ if (!isRecord(section)) throw new Error("settings section `" + namespace + "` is not a mapping; refusing to migrate it");
13319
13068
  const allowed = writableKeys(rowId);
13320
13069
  const values = [];
13321
13070
  for (const [key, value] of Object.entries(section)) {
@@ -13324,7 +13073,7 @@ function planNamespaceMigration(documentText) {
13324
13073
  skipped.push({
13325
13074
  namespace,
13326
13075
  key,
13327
- reason: "row `" + rowId + "` has no such parameter"
13076
+ reason: "row `" + rowId + "` 没有这个参数"
13328
13077
  });
13329
13078
  continue;
13330
13079
  }
@@ -13376,7 +13125,7 @@ function classifyNamespaceMigration(plan, read) {
13376
13125
  for (const value of entry.values) skipped.push({
13377
13126
  namespace: entry.namespace,
13378
13127
  key: value.source,
13379
- reason: "row `" + entry.rowId + "` is not served in this composition"
13128
+ reason: "row `" + entry.rowId + "` 在本组合中不提供服务"
13380
13129
  });
13381
13130
  continue;
13382
13131
  }
@@ -13689,4 +13438,4 @@ function foldUntrustedLines(text, limit) {
13689
13438
  return limit > 0 ? marked.slice(0, limit) : marked;
13690
13439
  }
13691
13440
  //#endregion
13692
- export { AGENT_PRESET_PACKAGE, AGENT_PRESET_ROOT_HINT, ALIVE_LOCK_TAKEOVER_MS, AUTHORING_DESCRIPTION_BAR, AUTHORING_SPLIT_LINE_CHARS, BASELINE_ACTION, CITATION_SUPPORT_DIRS, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CONTENT_SPLIT_HINT, COST_ASCII_TOKEN_HIGH, COST_ASCII_TOKEN_LOW, COST_CJK_TOKEN_HIGH, COST_CJK_TOKEN_LOW, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEDUP_MAX_PAIR_COMPARISONS, DEDUP_SIMILARITY_THRESHOLD, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_ARCHIVE_RETENTION_POLICY, DEFAULT_CITATION_POLICY, DEFAULT_CITATION_REF_BUDGET, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_PROBE_TIMEOUT_MS, DEFAULT_REFERENCE_REWRITE_POLICY, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_RUN_RECORDS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_SKILL_VERSION_KEEP, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_SUPPORT_FILE_CHAR_POLICY, DEFAULT_SUPPORT_READ_TOOL_NAMES, DEFAULT_THREAT_MAX_SCAN_CHARS, DEFAULT_USER_CHAR_LIMIT, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, EMPTY_LOCK_TAKEOVER_MS, EMPTY_PATCH_SEED, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_ERRORS, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, FUZZY_MAX_PATTERN_CHARS, FUZZY_MAX_WORK, HEALTH_STAMP_RE, HISTORY_DIR, HISTORY_INDEX_VERSION, HOOK_LINE_RE, INSTANCE_KEYS, KEEP_LINE_RE, LOCK_BODY_RE, LOCK_SUFFIX, LOCK_TEAR_TAKEOVER_MS, LONG_RUNS, LOW_QUALITY_THRESHOLD, LostWriteLock, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_REQUEST_BODY_BYTES, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_SUPPORT_READ_PATHS, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NAMESPACE_MIGRATIONS, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PARAM_ALIASES, PARAM_EXPOSURE, PARAM_GROUP_LABELS, PARAM_NAMESPACES, PATTERN_OVERLAP, PLATFORM_CATALOG_DESCRIPTION_DEFAULT, PLATFORM_ESTIMATE_CHARS_PER_TOKEN, PLATFORM_SERVICE_PROBES, POLICY_STAGE_DEFAULTS, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, READ_REQUIRED_ACTIONS, RESTRUCTURE_TARGET_RE, RETENTION_FEEDBACK_KEEP_RATIO, RETENTION_FEEDBACK_MIN_CHARS, RUNS_SCHEMA_VERSION, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUMMARY_DUPLICATE_HINT_THRESHOLD, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, TEXT_DIFF_MAX_CHARS, TEXT_DIFF_MAX_LINES, THREAT_EXEMPTION_HINT, TOKEN_ASCII_WEIGHT, TOKEN_CJK_WEIGHT, ToolDispatchNormalizer, ToolDispatchPayloadError, UNTRUSTED_LINE_BREAK, UNTRUSTED_LINE_MARK, UPSTREAM_LIMIT_CHARS_PER_TOKEN, VERSION_SUMMARY_MAX_CHARS, absentHostServices, advanceReview, affinity, ageCopyKey, allowRowCollisions, anchorVerdict, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, applyNamespaceMigration, applyReferenceRewrite, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, basePresetPlugins, blobPath, bodyCost, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpSupportRead, bumpUse, bumpView, callingScope, canonicalWriteId, claimInstance, clampOnce, clampedNumber, classifyNamespaceMigration, clearReviewChannel, collectReadSkillNames, composePresetComposition, composePresetEntry, composePresetInsert, composePresetRow, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, contentRetentionFeedback, countDispatches, createGateSet, daysSinceIso, decideTakeover, declaredReportStamp, describeReferenceRewrite, dispatchKeyOf, dispatchStepOf, displayBodyOf, duplicateHeadings, elapsedSince, emptyRecord, entryTarget, errorText, evaluateThreat, eventsFile, evidenceKindIndex, evolutionActivityProjection, evolutionEventPayloadIssue, evolutionEvidenceProjection, evolutionHome, evolutionIoAdapter, evolutionReadsProjection, evolutionRoot, evolutionSignalsProjection, filterUnreadSkillOps, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurnEvent, foldTurnEvents, foldUntrustedLines, frontmatterBlock, frontmatterCatalogInvalid, fuzzyPatch, getRecord, historyIndexFile, historyRoot, identity, idleDays, initialTurnFold, installEvolutionProjections, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isBookkeepingFrame, isCommittedWarning, isDeprecatedParamId, isFileShapedPath, isLoopbackRequest, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, isUnreadWrite, latestActivityAt, latestVersionAt, lifecycleCandidate, listEventArchives, liveBool, liveEnum, liveNumber, liveString, loadMutations, loadRowOverrides, loadSuppressedNames, loadUsage, loadVersionContent, loadVersions, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, mergePresetRow, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nearDuplicateSummaries, neutralizePromptVariables, newRunRegistry, newSkillLibrary, nextHistoryIndex, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, normalizeVersionSummary, observeEvent, orderVersions, overlongLines, paramNamespace, paramRowId, paramSettingsId, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseHistoryIndex, parseLockBody, partitionVersions, payloadText, pendingSelfCleanup, persistedWriteSite, persistedWriteSites, pickWithPolicy, planNamespaceMigration, planReferenceRewrite, planRehoming, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, probeAbsent, probeList, probeMtime, probePresent, probeReason, probeText, probeUnknown, projectionText, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, readHistoryIndex, readJsonBody, readJsonObject, readNumberParam, readParam, readingStats, recordMutation, recordVersions, redactSecrets, relatedSkillNames, releaseInstance, removePresetRow, renameWithRetry, renderCuratorReportMarkdown, renderLongRunTable, reportTime, reportsSweepLockTarget, resolveBasePresetPatch, resolveCitations, resolveExecOrigins, resolveOrigins, resolveParamId, resolveRootConfig, resolveSkillsRoot, retainEventArchives, retirementReport, reviewPrompt, runsFile, saveSuppressedNames, saveUsage, scanBodyHooks, scanContentThreats, scanMemoryThreats, scanThreats, scopedProbeReport, sessionAudited, sessionEvidenceIndex, sessionLastEventTime, sessionReadNames, sessionReadSkillNames, sessionSeesFamilyTools, sessionTurnSignals, skillReadNameOf, skillRowCells, skillStateKey, skillsRoot, supportFileReadOf, suppressedFile, sweepReports, sweepReviewChannelSessions, textDiffFacts, tokenLineFor, transactIo, transactTaskGuard, trimPatternBoundaries, unhookedSupportPointers, unwrapLive, updateSuppressedNames, usageFile, usageObserved, userSetKeys, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, versionActionKind, versionRowCells, versionSummaryPrompt, versionTarget, vocabulary, writeDurableTmp, writeJson, writePathSymbols, yamlPlainScalarNeedsQuotes };
13441
+ export { AGENT_PRESET_PACKAGE, AGENT_PRESET_ROOT_HINT, AUTHORING_DESCRIPTION_BAR, AUTHORING_SPLIT_LINE_CHARS, BASELINE_ACTION, CITATION_SUPPORT_DIRS, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CONTENT_SPLIT_HINT, COST_ASCII_TOKEN_HIGH, COST_ASCII_TOKEN_LOW, COST_CJK_TOKEN_HIGH, COST_CJK_TOKEN_LOW, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEDUP_MAX_PAIR_COMPARISONS, DEDUP_SIMILARITY_THRESHOLD, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_ARCHIVE_RETENTION_POLICY, DEFAULT_CITATION_POLICY, DEFAULT_CITATION_REF_BUDGET, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_PROBE_TIMEOUT_MS, DEFAULT_REFERENCE_REWRITE_POLICY, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_RUN_RECORDS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_SKILL_VERSION_KEEP, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_SUPPORT_FILE_CHAR_POLICY, DEFAULT_SUPPORT_READ_TOOL_NAMES, DEFAULT_THREAT_MAX_SCAN_CHARS, DEFAULT_USER_CHAR_LIMIT, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, EMPTY_PATCH_SEED, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_ERRORS, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, FUZZY_MAX_PATTERN_CHARS, FUZZY_MAX_WORK, HEALTH_STAMP_RE, HISTORY_DIR, HISTORY_INDEX_VERSION, HOOK_LINE_RE, INSTANCE_KEYS, KEEP_LINE_RE, LONG_RUNS, LOW_QUALITY_THRESHOLD, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_REQUEST_BODY_BYTES, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_SUPPORT_READ_PATHS, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NAMESPACE_MIGRATIONS, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PARAM_ALIASES, PARAM_EXPOSURE, PARAM_GROUP_LABELS, PARAM_NAMESPACES, PATTERN_OVERLAP, PLATFORM_CATALOG_DESCRIPTION_DEFAULT, PLATFORM_ESTIMATE_CHARS_PER_TOKEN, PLATFORM_SERVICE_PROBES, POLICY_STAGE_DEFAULTS, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, READ_REQUIRED_ACTIONS, RESTRUCTURE_TARGET_RE, RETENTION_FEEDBACK_KEEP_RATIO, RETENTION_FEEDBACK_MIN_CHARS, RUNS_SCHEMA_VERSION, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUMMARY_DUPLICATE_HINT_THRESHOLD, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, TEXT_DIFF_MAX_CHARS, TEXT_DIFF_MAX_LINES, THREAT_EXEMPTION_HINT, TOKEN_ASCII_WEIGHT, TOKEN_CJK_WEIGHT, ToolDispatchNormalizer, ToolDispatchPayloadError, UNTRUSTED_LINE_BREAK, UNTRUSTED_LINE_MARK, UPSTREAM_LIMIT_CHARS_PER_TOKEN, VERSION_SUMMARY_MAX_CHARS, absentHostServices, advanceReview, affinity, ageCopyKey, allowRowCollisions, anchorVerdict, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, applyNamespaceMigration, applyReferenceRewrite, asRecord, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, atomicWriteFiles, authoringFeedback, basePresetPlugins, blobPath, bodyCost, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpSupportRead, bumpUse, bumpView, callingScope, canonicalWriteId, claimInstance, clampOnce, clampedNumber, classifyNamespaceMigration, clearReviewChannel, collectReadSkillNames, composePresetComposition, composePresetEntry, composePresetInsert, composePresetRow, compositionRowIds, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, contentRetentionFeedback, countDispatches, createGateSet, daysSinceIso, declaredReportStamp, denseBlockSequence, describeReferenceRewrite, dispatchKeyOf, dispatchStepOf, displayBodyOf, duplicateHeadings, elapsedSince, emptyRecord, entryTarget, errorText, evaluateThreat, eventsFile, evidenceKindIndex, evolutionActivityProjection, evolutionEventPayloadIssue, evolutionEvidenceProjection, evolutionHome, evolutionIoAdapter, evolutionReadsProjection, evolutionRoot, evolutionSignalsProjection, filterUnreadSkillOps, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurnEvent, foldTurnEvents, foldUntrustedLines, frontmatterBlock, frontmatterCatalogInvalid, fuzzyPatch, getRecord, historyIndexFile, historyRoot, identity, idleDays, initialTurnFold, installEvolutionProjections, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isBookkeepingFrame, isCommittedWarning, isDeprecatedParamId, isFileShapedPath, isLoopbackRequest, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isRecord, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, isUnreadWrite, latestActivityAt, latestVersionAt, lifecycleCandidate, listDirectories, listEventArchives, liveBool, liveEnum, liveNumber, liveString, loadMutations, loadRowOverrides, loadSuppressedNames, loadUsage, loadVersionContent, loadVersions, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, mergePresetRow, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nearDuplicateSummaries, neutralizePromptVariables, newRunRegistry, newSkillLibrary, nextHistoryIndex, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, normalizeVersionSummary, observeEvent, orderVersions, overlongLines, paramNamespace, paramRowId, paramSettingsId, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseHistoryIndex, partitionVersions, payloadText, persistedWriteSite, persistedWriteSites, pickWithPolicy, planNamespaceMigration, planReferenceRewrite, planRehoming, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, probeAbsent, probeDesktopScope, probeList, probeMtime, probePresent, probeReason, probeText, probeUnknown, projectionText, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, readHistoryIndex, readJsonBody, readJsonObject, readNumberParam, readParam, readingStats, recordMutation, recordVersions, redactSecrets, relatedSkillNames, releaseInstance, removePresetRow, renameWithRetry, renderCuratorReportMarkdown, renderLongRunTable, reportTime, reportsSweepLockTarget, resolveBasePresetPatch, resolveCitations, resolveExecOrigins, resolveOrigins, resolveParamId, resolveRootConfig, resolveSkillsRoot, retainEventArchives, retirementReport, reviewPrompt, rowBlockSpan, rowSiblingEnd, runsFile, saveSuppressedNames, saveUsage, scanBodyHooks, scanContentThreats, scanMemoryThreats, scanThreats, scopedProbeReport, sessionAudited, sessionEvidenceIndex, sessionLastEventTime, sessionReadNames, sessionReadSkillNames, sessionSeesFamilyTools, sessionTurnSignals, skillReadNameOf, skillRowCells, skillStateKey, skillsRoot, supportFileReadOf, suppressedFile, sweepReports, sweepReviewChannelSessions, textDiffFacts, tokenLineFor, transactIo, transactTaskGuard, trimPatternBoundaries, trimTrailingBlankLines, unhookedSupportPointers, unwrapLive, updateSuppressedNames, usageFile, usageObserved, userSetKeys, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, versionActionKind, versionRowCells, versionSummaryPrompt, versionTarget, vocabulary, writeDurableTmp, writeJson, writePathSymbols, yamlPlainScalarNeedsQuotes };