@lmzhen/dsh-evolution-core 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js 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";
@@ -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
@@ -3289,6 +3126,7 @@ const EVOLUTION_ERRORS = Object.freeze({
3289
3126
  "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
3127
  "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
3128
  "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.",
3129
+ "e-322-a-parameter-would-break-a": "E-322: %a1% must be at least %a2%, but this write sets it to %a3%. Nothing was written.",
3292
3130
  "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
3131
  "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`."
3294
3132
  });
@@ -4532,7 +4370,7 @@ async function recordMutation(root, io, record, cap = 500) {
4532
4370
  console.warn(`mutation audit record dropped: ${mutationsFile(root)} is malformed and was not overwritten`);
4533
4371
  return current;
4534
4372
  }
4535
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
4373
+ if (isRecord(parsed)) {
4536
4374
  const version = parsed.version;
4537
4375
  if (typeof version === "number" && version > 1) {
4538
4376
  console.warn(`mutation audit record dropped: ${mutationsFile(root)} declares version ${version} (newer than 1); not overwritten`);
@@ -4596,16 +4434,23 @@ function composePresetComposition(standardComposition, deltaComposition) {
4596
4434
  * test can keep — the entries are now data, and a divergence is impossible.
4597
4435
  */
4598
4436
  const ROW_OVERRIDES_URL = new URL("../row-overrides.json", import.meta.url);
4599
- let cachedOverrides;
4437
+ /** One cache per PATH: the installer reads the same file through its own root, and a single-slot
4438
+ * cache would hand the second caller the first caller's bytes. */
4439
+ const overridesCache = /* @__PURE__ */ new Map();
4600
4440
  /**
4601
4441
  * Read and validate the shared override table.
4442
+ * G6.1/M1: the path is a PARAMETER (default: the table beside this module). The source installer
4443
+ * runs the generated port from `scripts/`, so its own root has to be passed in — a module-relative
4444
+ * read would look for `scripts/../row-overrides.json` and fail.
4445
+ * @param path - the table to read.
4602
4446
  * @returns the entries, in file order. A malformed table fails LOUD: a composer
4603
4447
  * that silently skipped an entry would ship a preset running the platform
4604
4448
  * default while nothing downstream reported it.
4605
4449
  */
4606
- function loadRowOverrides() {
4607
- if (cachedOverrides !== void 0) return cachedOverrides;
4608
- const parsed = JSON.parse(readFileSync(fileURLToPath(ROW_OVERRIDES_URL), "utf8"));
4450
+ function loadRowOverrides(path = fileURLToPath(ROW_OVERRIDES_URL)) {
4451
+ const cached = overridesCache.get(path);
4452
+ if (cached !== void 0) return cached;
4453
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
4609
4454
  if (!Array.isArray(parsed)) throw new Error("evolution-core: row-overrides.json must be an array of override entries");
4610
4455
  const entries = [];
4611
4456
  for (const [index, entry] of parsed.entries()) {
@@ -4618,7 +4463,7 @@ function loadRowOverrides() {
4618
4463
  missingReason: candidate.missingReason
4619
4464
  });
4620
4465
  }
4621
- cachedOverrides = entries;
4466
+ overridesCache.set(path, entries);
4622
4467
  return entries;
4623
4468
  }
4624
4469
  /**
@@ -4737,7 +4582,7 @@ function composePresetRow(baseComposition, deltaComposition, identity) {
4737
4582
  }
4738
4583
  /** The patch-entry key that ADDS a row: a plain entry overrides the row with the same id, so a
4739
4584
  * 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`). */
4585
+ * (the platform's own `user-patches.spec.ts` proves the override-vs-insert split). */
4741
4586
  const PATCH_INSERT_KEY = "insert";
4742
4587
  /**
4743
4588
  * Wrap a composed row as the patch entry a profile patch needs.
@@ -6411,7 +6256,7 @@ function renderLongRunTable() {
6411
6256
  /**
6412
6257
  * Every platform service the family reaches for with `ctx.get`, and what degrades without it.
6413
6258
  *
6414
- * O-2 (v46 audit): the family probes 26 platform services optionally — the platform has no
6259
+ * O-2 (v46 audit): the family probes 28 platform services optionally — the platform has no
6415
6260
  * formalized "optional dependency", so an ABSENT service is a silent downgrade and the diagnostic
6416
6261
  * surface is the only place a reader can learn about it. `/evolution doctor` used to judge six of
6417
6262
  * them; this table is the single source it judges the rest from, and the arch guard cross-checks it
@@ -6421,7 +6266,7 @@ function renderLongRunTable() {
6421
6266
  * `client` probes run in the browser half, where the host has no view of the seats.
6422
6267
  * @module @lmzhen/dsh-evolution-core
6423
6268
  */
6424
- /** The 26 probes, in service-name order (the guard checks this list against the tree). */
6269
+ /** The 28 probes, in service-name order (the guard checks this list against the tree). */
6425
6270
  const PLATFORM_SERVICE_PROBES = Object.freeze([
6426
6271
  {
6427
6272
  service: "agents",
@@ -6458,6 +6303,16 @@ const PLATFORM_SERVICE_PROBES = Object.freeze([
6458
6303
  feature: "every durable read and write (the IO seam)",
6459
6304
  side: "host"
6460
6305
  },
6306
+ {
6307
+ service: "fs",
6308
+ feature: "the platform filesystem the evolution-io-fs backend maps the seam onto (locks + writeText(expected) version CAS)",
6309
+ side: "host"
6310
+ },
6311
+ {
6312
+ service: "hmr",
6313
+ feature: "the preset install read-merge-write commit (the platform HMR lock serializes it)",
6314
+ side: "host"
6315
+ },
6461
6316
  {
6462
6317
  service: "evolutionMemoryBudget",
6463
6318
  feature: "the memory-budget divergence section of this report",
@@ -8007,6 +7862,13 @@ function versionRowCells(row) {
8007
7862
  tone: "tertiary",
8008
7863
  slot: "meta"
8009
7864
  });
7865
+ if (row.hash !== void 0 && row.hash !== "") title.push({
7866
+ key: "hash",
7867
+ literal: row.hash.slice(0, 7),
7868
+ tone: "tertiary",
7869
+ slot: "meta",
7870
+ note: row.hash
7871
+ });
8010
7872
  const meta = [];
8011
7873
  if (row.summary !== void 0 && row.summary !== "") meta.push({
8012
7874
  key: "summary",
@@ -8055,6 +7917,13 @@ function skillRowCells(skill) {
8055
7917
  tone: "tertiary",
8056
7918
  slot: "meta"
8057
7919
  }];
7920
+ if (skill.protectedBy !== null) meta.push({
7921
+ key: "protectedBy",
7922
+ literal: skill.protectedBy,
7923
+ tone: "tertiary",
7924
+ slot: "meta",
7925
+ note: skill.protectedBy
7926
+ });
8058
7927
  if (skill.age !== null) meta.push({
8059
7928
  key: "age",
8060
7929
  copy: ageCopyKey(skill.age),
@@ -9002,7 +8871,7 @@ function elapsedSince(at, nowMs) {
9002
8871
  * itself in `this.serial` (archive/restoreFromArchive deliberately do not).
9003
8872
  * 2. **Per-directory write lock** (io.ts LOCK_*) — cross-process mutual
9004
8873
  * exclusion plus in-process crash ownership (tickets, takeover). Checked
9005
- * with `hasWriteLock` before any destructive move (archive/restore/
8874
+ * with `supportDirsUnlistable` before any destructive move (archive/restore/
9006
8875
  * snapshot); held inside transactIo by byte writers.
9007
8876
  * 3. **CAS baseline (`expected:`)** — any read whose bytes feed a later write
9008
8877
  * must either live inside the serial section that commits the write, or
@@ -9239,15 +9108,6 @@ function skillDir(root, name) {
9239
9108
  function markerEntryName(marker) {
9240
9109
  return `.${marker}`;
9241
9110
  }
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
9111
  /** v23 (ML-1): `.archive` retention window (see pruneExpiredArchives). */
9252
9112
  const ARCHIVE_RETENTION_DAYS = 365;
9253
9113
  function markerPath(dir, marker) {
@@ -9326,7 +9186,6 @@ function validateSupportPath(filePath) {
9326
9186
  if (parts.length < 2) return "Provide a file name, not just a directory.";
9327
9187
  for (const part of parts.slice(1)) {
9328
9188
  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
9189
  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.`;
9331
9190
  const stem = part.split(".")[0]?.toLowerCase() ?? "";
9332
9191
  if (WIN32_RESERVED_DEVICE_NAMES.has(stem)) return `Unsupported file name "${part}" — a Windows reserved device name.`;
@@ -9506,16 +9365,14 @@ var SkillLibrary = class {
9506
9365
  * found no SKILL.md — remove the possibly-resurrected directory ONLY when it
9507
9366
  * holds nothing but this path's own write-lock file. The BR-5 rule from
9508
9367
  * 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
9368
+ * full directory between the list probe and the remove, so ANY content at all (support files, a
9369
+ * fresh restore) must never be recursed away. Best-effort: a failed cleanup leaves the "not found" result
9512
9370
  * unchanged (the operator-facing ghost-dir refusal is fail-loud already).
9513
9371
  */
9514
9372
  async cleanupGhostDir(skillFilePath) {
9515
9373
  const dir = dirname(skillFilePath);
9516
- const lockName = `${basename(skillFilePath)}${LOCK_SUFFIX}`;
9517
9374
  try {
9518
- if ((await this.io.list(dir)).some((entry) => entry !== lockName)) return;
9375
+ if ((await this.io.list(dir)).length > 0) return;
9519
9376
  await this.io.remove(dir);
9520
9377
  } catch {}
9521
9378
  }
@@ -10380,110 +10237,19 @@ var SkillLibrary = class {
10380
10237
  * P2 (v16): the write-lock probe for the DESTRUCTIVE MOVERS (archive /
10381
10238
  * restoreFromArchive). A byte-writer mid-flight is the ghost-generator —
10382
10239
  * 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.
10240
+ * recursive) and the tree ends half-archived.
10241
+ * G2 S4: the writer-lock probe that used to answer this question is retired — writes go through
10242
+ * the provider's versioned writes, so a writer that raced the move fails its own write loudly
10243
+ * instead of landing on a moved-away path. What REMAINS load-bearing here is the fail-closed
10244
+ * listing rule, which is about the filesystem misbehaving rather than about writers.
10395
10245
  */
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);
10246
+ async supportDirsUnlistable(dir) {
10247
+ for (const supportDir of SUPPORT_DIRS) try {
10248
+ await this.io.list(join(dir, supportDir));
10423
10249
  } catch {
10424
10250
  return true;
10425
10251
  }
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(() => {});
10252
+ return false;
10487
10253
  }
10488
10254
  /** A1-4 (v18): a manifest-declared name is copied with `join(root, name)`;
10489
10255
  * only a single, non-traversing path component is safe. Dotfiles
@@ -10494,22 +10260,6 @@ var SkillLibrary = class {
10494
10260
  if (typeof name !== "string") return false;
10495
10261
  return name !== "" && name !== "." && name !== ".." && !name.includes("/") && !name.includes("\\") && basename(name) === name;
10496
10262
  }
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
10263
  async archive(rawName, options = {}) {
10514
10264
  const name = rawName.trim();
10515
10265
  const badName = this.badName(name, { allowReserved: true });
@@ -10556,9 +10306,9 @@ var SkillLibrary = class {
10556
10306
  message: `Skill "${name}" is a symlink; refusing to archive it.`
10557
10307
  };
10558
10308
  }
10559
- if (await this.hasWriteLock(dir)) return {
10309
+ if (await this.supportDirsUnlistable(dir)) return {
10560
10310
  ok: false,
10561
- message: `Skill "${name}" is being written (write lock present); retry archiving once the write completes.`
10311
+ message: `Skill "${name}" could not be checked for writers (a support directory could not be listed); retry archiving.`
10562
10312
  };
10563
10313
  const commitProtection = await this.deleteProtection(name, options);
10564
10314
  if (commitProtection) return {
@@ -11188,16 +10938,15 @@ var SkillLibrary = class {
11188
10938
  message: `Archived entry "${chosen}" is a symlink; refusing to restore it.`
11189
10939
  };
11190
10940
  }
11191
- if (await this.hasWriteLock(dest)) return {
10941
+ if (await this.supportDirsUnlistable(dest)) return {
11192
10942
  ok: false,
11193
- message: `Skill "${name}" is being written (write lock present); retry restoring once the write completes.`
10943
+ message: `Skill "${name}" could not be checked for writers (a support directory could not be listed); retry restoring.`
11194
10944
  };
11195
10945
  const moveFailure = await this.moveDir(source, dest);
11196
10946
  if (moveFailure !== void 0) return {
11197
10947
  ok: false,
11198
10948
  message: `Restore of "${name}" from .archive failed: ${moveFailure}`
11199
10949
  };
11200
- await this.deleteStrandedLocks(dest);
11201
10950
  for (const marker of [".archive-reason", ".archive-name"]) if (await this.io.exists(join(dest, marker))) await this.io.remove(join(dest, marker));
11202
10951
  const restoredProbe = await probeText(this.io, join(dest, "SKILL.md"));
11203
10952
  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`);
@@ -11445,8 +11194,8 @@ var SkillLibrary = class {
11445
11194
  const names = await listNames(this.root, this.io);
11446
11195
  const skipped = [];
11447
11196
  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`);
11197
+ for (const name of names) if (await this.supportDirsUnlistable(this.dirOf(name))) {
11198
+ console.warn(`skill-store: snapshot skipped "${name}" — its support directory could not be listed; the skill is recorded as skipped in the manifest`);
11450
11199
  skipped.push(name);
11451
11200
  } else copyable.push(name);
11452
11201
  const copyFailure = (await Promise.allSettled(copyable.map(async (name) => {
@@ -11454,7 +11203,7 @@ var SkillLibrary = class {
11454
11203
  }))).find((result) => result.status === "rejected");
11455
11204
  if (copyFailure) throw copyFailure.reason;
11456
11205
  const suspect = [];
11457
- for (const name of copyable) if (await this.hasWriteLock(this.dirOf(name))) suspect.push(name);
11206
+ for (const name of copyable) if (await this.supportDirsUnlistable(this.dirOf(name))) suspect.push(name);
11458
11207
  if (suspect.length > 0) {
11459
11208
  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
11209
  for (const name of suspect) copyable.splice(copyable.indexOf(name), 1);
@@ -11713,14 +11462,9 @@ var SkillLibrary = class {
11713
11462
  }
11714
11463
  for (const entry of rootEntries) {
11715
11464
  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
11465
  const dir = join(this.root, entry);
11721
11466
  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`);
11467
+ 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
11468
  }
11725
11469
  }
11726
11470
  const restoresSuppressed = manifest.sidecars.includes(".curator-suppressed.json");
@@ -11728,7 +11472,6 @@ var SkillLibrary = class {
11728
11472
  if (entry === ".archive" || entry === ".backups" || entry === ".mutations.json") continue;
11729
11473
  if (entry === ".history") continue;
11730
11474
  if (entry === ".curator-suppressed.json" && restoresSuppressed) continue;
11731
- if (entry.endsWith(".lock") || entry.endsWith(`.lock.next`)) continue;
11732
11475
  await this.io.remove(join(this.root, entry));
11733
11476
  }
11734
11477
  for (const name of manifest.skills) await this.io.copy(join(snapshotPath, name), join(this.root, name));
@@ -11740,10 +11483,7 @@ var SkillLibrary = class {
11740
11483
  await this.io.copy(join(snapshotPath, ".archive"), archiveRoot);
11741
11484
  } else if (manifest.hasArchive === false) await this.io.remove(archiveRoot);
11742
11485
  }
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
- }
11486
+ for (const entry of await this.io.list(this.root)) if (entry.startsWith(".")) continue;
11747
11487
  }
11748
11488
  };
11749
11489
  function newSkillLibrary(options) {
@@ -12358,6 +12098,7 @@ const PARAM_EXPOSURE = Object.freeze([
12358
12098
  control: "number",
12359
12099
  unit: "天",
12360
12100
  values: "",
12101
+ relation: { atLeast: "staleAfterDays" },
12361
12102
  card: "evolution-curator"
12362
12103
  },
12363
12104
  {
@@ -12999,6 +12740,23 @@ const PARAM_EXPOSURE = Object.freeze([
12999
12740
  values: "",
13000
12741
  card: "evolution-approval"
13001
12742
  },
12743
+ {
12744
+ id: "approvalConfirm",
12745
+ group: "write-caps",
12746
+ tier: "E3",
12747
+ authority: "cordis",
12748
+ owner: "evolution-approval",
12749
+ applies: "live",
12750
+ docAnchor: "PARAMETERS.md#write-caps",
12751
+ 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.",
12752
+ label: "批准前先确认",
12753
+ hint: "点「批准」时要不要再确认一次:直接执行 / 先确认 / 等待超时即取消。默认直接执行(不打断批量批准)。",
12754
+ control: "select",
12755
+ unit: "",
12756
+ values: "auto|ask|timeout",
12757
+ valueLabels: "直接执行|先确认|等待超时即取消",
12758
+ card: "evolution-approval"
12759
+ },
13002
12760
  {
13003
12761
  id: "qualityWarnThreshold",
13004
12762
  group: "deployment",
@@ -13269,14 +13027,6 @@ function readNumberParam(carrier, id) {
13269
13027
  * @module @lmzhen/dsh-evolution-core
13270
13028
  */
13271
13029
  /**
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
13030
  * The key names one row may be written with: its own E3 rows, plus the deprecated
13281
13031
  * spellings that still resolve to them. An unknown key is refused instead of written —
13282
13032
  * the platform validates a write against the row's schema, so writing it would fail the
@@ -13307,7 +13057,7 @@ function planNamespaceMigration(documentText) {
13307
13057
  foreign: [],
13308
13058
  skipped: []
13309
13059
  };
13310
- if (!isSection(parsed)) throw new Error("the settings document is not a mapping; refusing to migrate it");
13060
+ if (!isRecord(parsed)) throw new Error("the settings document is not a mapping; refusing to migrate it");
13311
13061
  const legacy = new Map(NAMESPACE_MIGRATIONS.map((entry) => [entry.namespace, entry.rowId]));
13312
13062
  const entries = [];
13313
13063
  const empty = [];
@@ -13315,7 +13065,7 @@ function planNamespaceMigration(documentText) {
13315
13065
  for (const [namespace, section] of Object.entries(parsed)) {
13316
13066
  const rowId = legacy.get(namespace);
13317
13067
  if (rowId === void 0) continue;
13318
- if (!isSection(section)) throw new Error("settings section `" + namespace + "` is not a mapping; refusing to migrate it");
13068
+ if (!isRecord(section)) throw new Error("settings section `" + namespace + "` is not a mapping; refusing to migrate it");
13319
13069
  const allowed = writableKeys(rowId);
13320
13070
  const values = [];
13321
13071
  for (const [key, value] of Object.entries(section)) {
@@ -13689,4 +13439,4 @@ function foldUntrustedLines(text, limit) {
13689
13439
  return limit > 0 ? marked.slice(0, limit) : marked;
13690
13440
  }
13691
13441
  //#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 };
13442
+ 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 };