@lmzhen/dsh-evolution-core 0.3.63 → 0.3.65

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
@@ -156,12 +156,41 @@ async function fsyncDirectory(path) {
156
156
  async function commitTmp(tmp, target) {
157
157
  try {
158
158
  await renameWithRetry(tmp, target);
159
- await fsyncDirectory(dirname(target));
160
159
  } catch (error) {
161
160
  await rm(tmp, { force: true }).catch(() => {});
162
161
  throw error;
163
162
  }
163
+ try {
164
+ await fsyncDirectory(dirname(target));
165
+ } catch (error) {
166
+ throw Object.assign(/* @__PURE__ */ new Error(`commitTmp: ${target} was renamed but the directory fsync failed: ${error instanceof Error ? error.message : String(error)}`), {
167
+ committed: true,
168
+ cause: error
169
+ });
170
+ }
171
+ }
172
+ /**
173
+ * True when the pid is alive (EPERM = alive but unowned; ESRCH = gone).
174
+ * V18 single source: the node backend's lock takeover and SkillLibrary's
175
+ * stranded-lock sweep must use the same liveness rule.
176
+ */
177
+ function isProcessAlive(pid) {
178
+ try {
179
+ process.kill(pid, 0);
180
+ return true;
181
+ } catch (error) {
182
+ return error?.code === "EPERM";
183
+ }
164
184
  }
185
+ /** F-17 (v18): the write-lock protocol is a cross-module contract — the lock
186
+ * file is `<target>.lock` and its body is `<pid>:<token>`. This module creates
187
+ * them (`withWriteLock`) and `skill-store`'s probes/sweepers parse them, so both
188
+ * consume these two constants instead of repeating the literals. */
189
+ const LOCK_SUFFIX = ".lock";
190
+ /** Writer-lock body shape: a decimal pid, a colon, then the claim token. A
191
+ * torn body (no parsable pid) still matches the `\\d+:` prefix rule only when
192
+ * the pid part is intact, which is what the takeover probe needs. */
193
+ const LOCK_BODY_RE = /^\d+:[0-9a-f]*$/;
165
194
  /**
166
195
  * Build the Node IO backend. `lockAttempts` scales the write-lock retry budget
167
196
  * (attempts × 50ms); the default 40 (~2s, rc.69) covers production contention,
@@ -174,15 +203,8 @@ function nodeEvolutionIo(lockAttempts = 40) {
174
203
  const code = error?.code;
175
204
  return code === "ENOENT" || code === "ENOTDIR";
176
205
  };
177
- /** True when the pid is alive (EPERM = alive but unowned; ESRCH = gone). */
178
- const isAlive = (pid) => {
179
- try {
180
- process.kill(pid, 0);
181
- return true;
182
- } catch (error) {
183
- return error?.code === "EPERM";
184
- }
185
- };
206
+ /** True when the pid is alive (single source: `isProcessAlive`). */
207
+ const isAlive = isProcessAlive;
186
208
  /**
187
209
  * V10-07 (P2-1): age threshold for taking over a lock whose body is TORN
188
210
  * (non-empty, but the pid prefix does not parse to a positive integer). 1h:
@@ -238,7 +260,7 @@ function nodeEvolutionIo(lockAttempts = 40) {
238
260
  * forever.
239
261
  */
240
262
  const withWriteLock = async (path, task) => {
241
- const lock = `${path}.lock`;
263
+ const lock = `${path}${LOCK_SUFFIX}`;
242
264
  let myClaim = "";
243
265
  for (let attempt = 0; attempt < lockAttempts; attempt += 1) {
244
266
  let lockHandle = null;
@@ -343,40 +365,41 @@ function nodeEvolutionIo(lockAttempts = 40) {
343
365
  return;
344
366
  }
345
367
  const prefix = `${base}.`;
346
- const lockName = `${base}.lock`;
368
+ const lockName = `${base}${LOCK_SUFFIX}`;
347
369
  const ticketName = `${lockName}.next`;
348
370
  const CORRUPT_SWEEP_AGE_MS = 168 * 36e5;
349
371
  for (const name of entries) {
350
372
  if (!name.startsWith(prefix) || name === lockName) continue;
351
- if (!name.endsWith(".tmp")) {
352
- if (name === ticketName) {
353
- const ticketPath = join(dir, name);
354
- try {
355
- const body = await readFile(ticketPath, "utf8").catch(() => "");
356
- const holder = Number(body.split(":")[0] ?? "");
357
- const st = await stat(ticketPath);
358
- const dead = !Number.isInteger(holder) || holder <= 0 || !isAlive(holder);
359
- const old = Date.now() - st.mtimeMs > 1e3;
360
- if (dead || old) await rm(ticketPath, { force: true });
361
- } catch {}
362
- continue;
363
- }
364
- if (name.includes(".corrupt")) {
365
- const corruptPath = join(dir, name);
366
- try {
367
- const st = await stat(corruptPath);
368
- if (Date.now() - st.mtimeMs > CORRUPT_SWEEP_AGE_MS) await rm(corruptPath, { force: true });
369
- } catch {}
370
- }
373
+ const tmpMatch = /^(.*)\.(\d+)\.([0-9a-f]+)\.tmp$/.exec(name);
374
+ if (tmpMatch !== null && tmpMatch[1] === base) {
375
+ const tmpPath = join(dir, name);
376
+ const holder = Number(tmpMatch[2]);
377
+ try {
378
+ const st = await stat(tmpPath);
379
+ const deadHolder = !Number.isInteger(holder) || holder <= 0 || !isAlive(holder);
380
+ if (holder === process.pid || Date.now() - st.mtimeMs > 36e5 && deadHolder) await rm(tmpPath, { force: true });
381
+ } catch {}
371
382
  continue;
372
383
  }
373
- const tmpPath = join(dir, name);
374
- const holder = Number(name.slice(prefix.length, name.length - 4).split(".")[0] ?? "");
375
- try {
376
- const st = await stat(tmpPath);
377
- const deadHolder = !Number.isInteger(holder) || holder <= 0 || !isAlive(holder);
378
- if (holder === process.pid || Date.now() - st.mtimeMs > 36e5 && deadHolder) await rm(tmpPath, { force: true });
379
- } catch {}
384
+ if (name === ticketName) {
385
+ const ticketPath = join(dir, name);
386
+ try {
387
+ const body = await readFile(ticketPath, "utf8").catch(() => "");
388
+ const holder = Number(body.split(":")[0] ?? "");
389
+ const st = await stat(ticketPath);
390
+ const dead = !Number.isInteger(holder) || holder <= 0 || !isAlive(holder);
391
+ const old = Date.now() - st.mtimeMs > 1e3;
392
+ if (dead || old) await rm(ticketPath, { force: true });
393
+ } catch {}
394
+ continue;
395
+ }
396
+ if (/\.corrupt(\.\d+)?$/.test(name)) {
397
+ const corruptPath = join(dir, name);
398
+ try {
399
+ const st = await stat(corruptPath);
400
+ if (Date.now() - st.mtimeMs > CORRUPT_SWEEP_AGE_MS) await rm(corruptPath, { force: true });
401
+ } catch {}
402
+ }
380
403
  }
381
404
  };
382
405
  return {
@@ -553,24 +576,30 @@ function parseUsage(raw) {
553
576
  async function loadUsage(root, io = nodeEvolutionIo()) {
554
577
  return parseUsage(await io.readText(usageFile(root)));
555
578
  }
556
- /**
557
- * Atomic read-modify-write on the usage sidecar (rc.50 P2-2): `task` receives
558
- * the map parsed from the current on-disk state and may mutate it; the result
559
- * is persisted inside the same transact so a second process sharing DSH_HOME
560
- * cannot interleave its RMW and lose a counter update. Callers keep their own
561
- * single-process serialize chain as the second layer.
562
- */
563
- async function mutateUsage(root, io, task) {
579
+ async function mutateUsage(root, io, task, options = {}) {
564
580
  await transactIo(io, usageFile(root), async (current) => {
565
581
  let shapePreserved = false;
582
+ let recovered = null;
566
583
  if (current !== null) try {
567
584
  const probe = JSON.parse(current);
568
585
  if (probe === null || Array.isArray(probe) || typeof probe !== "object") shapePreserved = true;
586
+ else {
587
+ const record = probe;
588
+ const isMalformed = (value) => value === null || typeof value !== "object" || Array.isArray(value);
589
+ if (typeof record.version === "number" && record.version > 1) shapePreserved = true;
590
+ else if (Object.values(record).some(isMalformed)) {
591
+ const bad = Object.keys(record).filter((key) => isMalformed(record[key]));
592
+ const corruptPath = `${usageFile(root)}.corrupt`;
593
+ await io.writeText(corruptPath, current).catch(() => {});
594
+ recovered = JSON.stringify(Object.fromEntries(Object.entries(record).filter(([, value]) => !isMalformed(value))));
595
+ options.onQuarantine?.(`usage sidecar ${usageFile(root)} carried ${bad.length} malformed entr${bad.length === 1 ? "y" : "ies"} (${bad.slice(0, 5).join(", ")}); the original bytes were copied to ${corruptPath} and the remaining entries continue to be served`);
596
+ }
597
+ }
569
598
  } catch {
570
599
  return current;
571
600
  }
572
601
  if (shapePreserved) return current;
573
- const map = parseUsage(current);
602
+ const map = parseUsage(recovered ?? current);
574
603
  await task(map);
575
604
  return JSON.stringify(Object.fromEntries(map.entries()), null, 2);
576
605
  });
@@ -617,7 +646,8 @@ function applyCuratorMetaFields(disk, curated) {
617
646
  * transitioned — a concurrent curator run's archive/restore is never reverted
618
647
  * by a stale snapshot; without it both pairs apply everywhere.
619
648
  */
620
- function foldCuratorFields(disk, curated, stateOwned) {
649
+ function foldCuratorFields(disk, curated, stateOwned, runStartStates) {
650
+ const skipped = [];
621
651
  for (const [name, record] of curated) {
622
652
  const diskRecord = disk.get(name);
623
653
  if (!diskRecord) {
@@ -625,8 +655,16 @@ function foldCuratorFields(disk, curated, stateOwned) {
625
655
  continue;
626
656
  }
627
657
  applyCuratorMetaFields(diskRecord, record);
628
- if (stateOwned === void 0 || stateOwned.has(name)) applyCuratorLifecycleFields(diskRecord, record);
658
+ if (stateOwned === void 0 || stateOwned.has(name)) {
659
+ const expected = runStartStates?.get(name);
660
+ if (expected !== void 0 && diskRecord.state !== expected) {
661
+ skipped.push(name);
662
+ continue;
663
+ }
664
+ applyCuratorLifecycleFields(diskRecord, record);
665
+ }
629
666
  }
667
+ return skipped;
630
668
  }
631
669
  /** Whole-file usage write (V6-37, 0.3.37): this is the ONE path that bypasses
632
670
  * the malformed-defense and the transact lock — prefer `mutateUsage` for any
@@ -757,8 +795,14 @@ async function updateSuppressedNames(root, io, task) {
757
795
  * threshold, which are intentionally left where they are used.
758
796
  * @module @lmzhen/dsh-evolution-core
759
797
  */
760
- /** Skill frontmatter `name` validated for the file name (lowercase + hyphen). */
761
- const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
798
+ /** Skill frontmatter `name` validated for the file name (lowercase + hyphen).
799
+ * 计划 B-4 (v18): tightened to the UPSTREAM `SKILL_NAME` shape
800
+ * (`/^[a-z0-9]+(?:-[a-z0-9]+)*$/`, packages/skill/skill/src/index.ts:20). The
801
+ * old form admitted trailing/consecutive hyphens, which upstream
802
+ * `validateCandidate` throws on — and that throw aborts the WHOLE `ctx.skills`
803
+ * collection. The catalog provider still filters such legacy tree entries so
804
+ * an existing tree cannot break a session. */
805
+ const SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
762
806
  /** Allowed skill support-file subdirectories (path-traversal boundary). */
763
807
  const SUPPORT_DIRS = [
764
808
  "references",
@@ -796,7 +840,10 @@ const DEFAULT_MEMORY_CHAR_LIMIT = 2200;
796
840
  const DEFAULT_USER_CHAR_LIMIT = 1375;
797
841
  /** Consolidation-failure backoff cap, shared by MemoryStore and memory-files' Config default. */
798
842
  const DEFAULT_CONSOLIDATION_FAILURES = 3;
799
- const DEFAULT_SKILL_CONTENT_CHARS = 1e5;
843
+ /** F-20 (v18): the authored-body budget and the hard ceiling are the same
844
+ * number today. Derive it so a future divergence is one edit, not two names
845
+ * that silently disagree. */
846
+ const DEFAULT_SKILL_CONTENT_CHARS = MAX_SKILL_CONTENT_CHARS;
800
847
  /** P3-19 (v14): defaults that were written twice (schema `.default()` AND the
801
848
  * clamp fallback literal) now have one home per value. */
802
849
  const DEFAULT_REVIEW_TIMEOUT_MS = 12e4;
@@ -1040,7 +1087,10 @@ function computeScopeView(usage, config, protectedNames, gates) {
1040
1087
  };
1041
1088
  }
1042
1089
  function daysSince(iso, created, now) {
1043
- return (now - new Date(iso ?? created).getTime()) / 864e5;
1090
+ const anchor = iso ?? created;
1091
+ const t = Date.parse(anchor);
1092
+ if (!Number.isFinite(t)) return 0;
1093
+ return (now - t) / 864e5;
1044
1094
  }
1045
1095
  function computeLifecycleTransitions(usage, config, now = /* @__PURE__ */ new Date(), gates, protectedNames) {
1046
1096
  const result = {
@@ -1053,9 +1103,9 @@ function computeLifecycleTransitions(usage, config, now = /* @__PURE__ */ new Da
1053
1103
  for (const [name, record] of usage) {
1054
1104
  if (!lifecycleCandidate(name, record, config, config.bundledNames?.has(name) === true, gateSet, protectedNames)) continue;
1055
1105
  const age = daysSince(null, record.created_at, now.getTime());
1056
- if (record.use_count === 0 && age < config.staleAfterDays) continue;
1057
- const idle = daysSince(latestActivityAt(record), record.created_at, now.getTime());
1058
1106
  const qualityWarn = record.quality_warn === true || record.feedback_warn === true;
1107
+ if (record.use_count + record.view_count === 0 && !qualityWarn && age < config.staleAfterDays) continue;
1108
+ const idle = daysSince(latestActivityAt(record), record.created_at, now.getTime());
1059
1109
  const staleAfterDays = qualityWarn && config.qualityWarnStaleAfterDays !== void 0 ? config.qualityWarnStaleAfterDays : config.staleAfterDays;
1060
1110
  if (record.state === "active") {
1061
1111
  if (idle >= config.archiveAfterDays) {
@@ -1148,6 +1198,28 @@ const EVENT_ARCHIVE_RE = /^events-(\d+)\.json$/;
1148
1198
  function eventsFile(home) {
1149
1199
  return join(home, "evolution", "events.json");
1150
1200
  }
1201
+ /** I-5 (v18): one lightweight description of the durable event payload
1202
+ * contract. The log is a FILE boundary (a host, a script or an older version
1203
+ * can write it), so `appendEvolutionEvent` refuses a record no consumer can
1204
+ * fold instead of persisting it and failing silently later. The process event
1205
+ * bus stays unvalidated — that is a typed same-process boundary.
1206
+ * @param event - the candidate event record.
1207
+ * @returns a human-readable issue, or null when the record is well-formed.
1208
+ */
1209
+ function evolutionEventPayloadIssue(event) {
1210
+ const type = event.type;
1211
+ if (typeof type !== "string") return `unknown event type "${String(type)}"`;
1212
+ switch (type) {
1213
+ case "feedback":
1214
+ if (event.kind !== "skill" && event.kind !== "session") return "feedback event requires kind skill|session";
1215
+ if (event.rating !== "positive" && event.rating !== "negative") return "feedback event requires rating positive|negative";
1216
+ return null;
1217
+ case "maintain": return typeof event.runId === "string" ? null : "maintain event requires runId";
1218
+ case "learn":
1219
+ case "usage": return null;
1220
+ default: return `unknown event type "${type}"`;
1221
+ }
1222
+ }
1151
1223
  function isEventRecord(event) {
1152
1224
  const seq = event?.seq;
1153
1225
  return typeof event === "object" && event !== null && typeof seq === "number" && Number.isFinite(seq);
@@ -1220,6 +1292,8 @@ async function listEventArchives(io, path) {
1220
1292
  * event can never shadow an archived one in the seq-deduped timeline.
1221
1293
  */
1222
1294
  async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE_AT) {
1295
+ const issue = evolutionEventPayloadIssue(event);
1296
+ if (issue !== null) throw new Error(`evolution event refused: ${issue}`);
1223
1297
  let assigned = 0;
1224
1298
  let refuseMessage = "";
1225
1299
  let parsedBody = null;
@@ -1265,7 +1339,7 @@ async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE
1265
1339
  * one-event rotate would archive everything and restart seqs at 1).
1266
1340
  */
1267
1341
  async function rotateIfDue(io, path, events, rotateAt) {
1268
- if (rotateAt < 2 || events.length < rotateAt) return events;
1342
+ if (!Number.isFinite(rotateAt) || rotateAt < 2 || events.length < rotateAt) return events;
1269
1343
  const mid = Math.ceil(events.length / 2);
1270
1344
  const head = events.slice(0, mid);
1271
1345
  const tail = events.slice(mid);
@@ -1903,9 +1977,16 @@ function clampedNumber(value, fallback, opts) {
1903
1977
  * Threat scanning for agent-authored memory and skill content.
1904
1978
  *
1905
1979
  * Ported as a small, dependency-free subset of Hermes Agent's
1906
- * `tools/threat_patterns.py` + hermes-claw `threats.ts`. The policy is the
1907
- * load-bearing part: ANY in-scope hit blocks. Severity and category are
1908
- * metadata for diagnostics only.
1980
+ * `tools/threat_patterns.py` + hermes-claw `threats.ts`.
1981
+ *
1982
+ * Policy (P1-1, v19): a finding either BLOCKS the write or only REPORTS.
1983
+ * Blocking is reserved for shapes with no legitimate use in stored knowledge
1984
+ * (prompt-injection phrasing, credential exfiltration, the invisible-character
1985
+ * smuggling core). Typography and presentation characters that are legitimate
1986
+ * in ordinary prose — variation selectors (❤️), zero-width non-joiner, soft
1987
+ * hyphen from PDF paste, Arabic letter mark, Mongolian vowel separator — are
1988
+ * REPORT findings: they stay visible to operators and tests but never reject a
1989
+ * write. Blocking them turned every emoji into a security event.
1909
1990
  */
1910
1991
  const FILLER = String.raw`(?:\w+\s+){0,8}`;
1911
1992
  const PATTERNS = [
@@ -1919,7 +2000,7 @@ const PATTERNS = [
1919
2000
  label: "disregard_rules",
1920
2001
  category: "prompt_injection",
1921
2002
  scope: "all",
1922
- regex: /disregard\s+(?:your|all|any)\s+(?:instructions|rules|guidelines)/i
2003
+ regex: new RegExp(String.raw`disregard\s+${FILLER}(?:your|all|any)\s+${FILLER}(?:instructions|rules|guidelines)`, "i")
1923
2004
  },
1924
2005
  {
1925
2006
  label: "system_prompt_override",
@@ -2072,7 +2153,9 @@ const PATTERNS = [
2072
2153
  regex: /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----/
2073
2154
  }
2074
2155
  ];
2075
- const ZERO_WIDTH_CHARS = /[\u200b\u200c\u200d\u2060\u2062\u2063\u2064\ufeff]/;
2156
+ const ZERO_WIDTH_CHARS = new RegExp(`[\\u034f\\u200b\\u2060\\u2061\\u2062\\u2063\\u2064\\u206a-\\u206f\\ufeff\\u{e0000}-\\u{e007f}]`, "u");
2157
+ const ZWJ_OUTSIDE_EMOJI = /(?<!\p{Extended_Pictographic})\u200d(?!\p{Extended_Pictographic})/u;
2158
+ const TYPOGRAPHY_CHARS = /[\u00ad\u061c\u180e\u200c\ufe00-\ufe0f]/;
2076
2159
  const BIDI_CHARS = /[\u202a-\u202e\u2066-\u2069]/;
2077
2160
  const SCOPE_ORDER = {
2078
2161
  all: 1,
@@ -2099,11 +2182,17 @@ function scanThreats(text, scope = "strict", maxScanChars = 65536, options = NO_
2099
2182
  const windowSize = clampedNumber(maxScanChars, 65536, { min: 4097 });
2100
2183
  const findings = [];
2101
2184
  const excluded = new Set(options.excludeLabels ?? []);
2102
- if (ZERO_WIDTH_CHARS.test(text) && !excluded.has("unicode_zero_width")) findings.push({
2185
+ if ((ZERO_WIDTH_CHARS.test(text) || ZWJ_OUTSIDE_EMOJI.test(text)) && !excluded.has("unicode_zero_width")) findings.push({
2103
2186
  label: "unicode_zero_width",
2104
2187
  category: "unicode_obfuscation",
2105
2188
  scope: "all"
2106
2189
  });
2190
+ if (TYPOGRAPHY_CHARS.test(text) && !excluded.has("unicode_typography")) findings.push({
2191
+ label: "unicode_typography",
2192
+ category: "unicode_obfuscation",
2193
+ scope: "all",
2194
+ severity: "report"
2195
+ });
2107
2196
  if (BIDI_CHARS.test(text) && !excluded.has("unicode_bidi_override")) findings.push({
2108
2197
  label: "unicode_bidi_override",
2109
2198
  category: "unicode_obfuscation",
@@ -2132,11 +2221,13 @@ function scanThreats(text, scope = "strict", maxScanChars = 65536, options = NO_
2132
2221
  }
2133
2222
  return findings;
2134
2223
  }
2135
- /** Blocking policy: any hit blocks. `severity` is deliberately not a gate. */
2224
+ /** Blocking policy (P1-1, v19): a finding blocks unless it is explicitly
2225
+ * `report`-only. Pattern findings carry no severity and therefore block as
2226
+ * before. */
2136
2227
  function evaluateThreat(text, scope = "strict", maxScanChars = 65536, options = NO_SCAN_OPTIONS) {
2137
2228
  const findings = scanThreats(text, scope, maxScanChars, options);
2138
2229
  return {
2139
- blocked: findings.length > 0,
2230
+ blocked: findings.some((finding) => finding.severity !== "report"),
2140
2231
  findings
2141
2232
  };
2142
2233
  }
@@ -2262,12 +2353,17 @@ var MemoryStore = class {
2262
2353
  /** V10-03 (P2-18): the strict-scan write gate. A block message names the hit
2263
2354
  * label (scanMemoryThreats already embeds it) plus the self-heal hint. */
2264
2355
  memoryThreatBlock(text) {
2265
- const threat = scanMemoryThreats(text, void 0, this.threatScanOptions());
2266
- return threat === null ? null : threat + THREAT_EXEMPT_HINT;
2356
+ return scanMemoryThreats(text, void 0, this.threatScanOptions());
2267
2357
  }
2268
2358
  limitFor(target) {
2269
2359
  return target === "memory" ? this.memoryLimit : this.userLimit;
2270
2360
  }
2361
+ /** P2-1 (v18): the generated date prefix participates in duplicate detection
2362
+ * only when THIS store writes it. With addDatePrefix=false a fact's own
2363
+ * leading `## YYYY-MM-DD\n` is content, not a generated prefix. */
2364
+ dedupeKey(entry) {
2365
+ return this.addDatePrefix ? stripDatePrefix(entry) : entry;
2366
+ }
2271
2367
  /**
2272
2368
  * Read-guard probe: `{ size, limit }` when the on-disk file exceeds
2273
2369
  * `limit * READ_GUARD_FACTOR` bytes, `null` when it is absent, unknown
@@ -2452,7 +2548,7 @@ var MemoryStore = class {
2452
2548
  write: null
2453
2549
  };
2454
2550
  const entries = [...new Set(normalizeEntries(raw))];
2455
- if (entries.some((entry) => stripDatePrefix(entry) === content)) {
2551
+ if (entries.some((entry) => this.dedupeKey(entry) === content)) {
2456
2552
  this.resetFailures();
2457
2553
  return {
2458
2554
  result: {
@@ -2574,7 +2670,7 @@ var MemoryStore = class {
2574
2670
  },
2575
2671
  write: null
2576
2672
  };
2577
- if (!working.some((entry) => stripDatePrefix(entry) === body)) working.push(entryBody);
2673
+ if (!working.some((entry) => this.dedupeKey(entry) === body)) working.push(entryBody);
2578
2674
  continue;
2579
2675
  }
2580
2676
  const rawAction = op.action;
@@ -2777,6 +2873,13 @@ async function recordMutation(root, io, record, cap = 500) {
2777
2873
  console.warn(`mutation audit record dropped: ${mutationsFile(root)} is malformed and was not overwritten`);
2778
2874
  return current;
2779
2875
  }
2876
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
2877
+ const version = parsed.version;
2878
+ if (typeof version === "number" && version > 1) {
2879
+ console.warn(`mutation audit record dropped: ${mutationsFile(root)} declares version ${version} (newer than 1); not overwritten`);
2880
+ return current;
2881
+ }
2882
+ }
2780
2883
  const existing = recordsFromParsed(parsed);
2781
2884
  existing.push(record);
2782
2885
  const trimmed = existing.length > cap ? existing.slice(existing.length - cap) : existing;
@@ -2897,7 +3000,9 @@ function clamp01(value) {
2897
3000
  return Math.max(0, Math.min(1, value));
2898
3001
  }
2899
3002
  function daysBetween(from, now) {
2900
- return Math.max(0, (now.getTime() - new Date(from).getTime()) / 864e5);
3003
+ const t = Date.parse(from);
3004
+ if (!Number.isFinite(t)) return 0;
3005
+ return Math.max(0, (now.getTime() - t) / 864e5);
2901
3006
  }
2902
3007
  function computeQualityScores(input) {
2903
3008
  const now = input.now ?? /* @__PURE__ */ new Date();
@@ -2906,9 +3011,9 @@ function computeQualityScores(input) {
2906
3011
  const ageDays = Math.max(1, daysBetween(record.created_at, now));
2907
3012
  const idleDays = daysBetween(latestActivityAt(record) ?? record.created_at, now);
2908
3013
  const patchCount = record.patch_count;
2909
- const useCount = record.use_count;
2910
- const usageFrequency = clamp01(useCount / ageDays);
2911
- const stability = useCount === 0 ? 1 : clamp01(1 - patchCount / useCount);
3014
+ const loadCount = record.use_count + record.view_count;
3015
+ const usageFrequency = clamp01(loadCount / ageDays);
3016
+ const stability = loadCount === 0 ? 1 : clamp01(1 - patchCount / loadCount);
2912
3017
  const recency = idleDays < 30 ? 1 : clamp01(1 - (idleDays - 30) / 150);
2913
3018
  const references = clamp01((input.referenceCounts?.get(name) ?? 0) / 3);
2914
3019
  const mutationMaturity = patchCount === 0 ? .3 : patchCount === 1 ? .4 : clamp01((patchCount - 1) / Math.max(1, ageDays / 30));
@@ -3053,7 +3158,7 @@ const SECRET_PATTERNS = [
3053
3158
  ["jwt", /eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g],
3054
3159
  ["bearer credential", /Bearer[\s]+[a-z0-9._~+/=\-]{16,}/gi]
3055
3160
  ];
3056
- const INLINE_ASSIGNMENT_PATTERN = /* @__PURE__ */ new RegExp("((?:\\b|[\\w-]+[_\\-])(?:token|api[_-]?key|secret|password|passwd)(?:[_\\-][\\w-]+)?\\b[\\s]*[:=][\\s]*[\"']?)([A-Z0-9._~+/=\\-]{12,})", "gi");
3161
+ const INLINE_ASSIGNMENT_PATTERN = /* @__PURE__ */ new RegExp("(^|[^\\w-])([\\w-]{0,64}[_\\-])?((?:token|api[_-]?key|secret|password|passwd)(?:[_\\-][\\w-]{0,64})?)\\b([\\t ]*[:=][\\t ]*)([^\\r\\n]+)", "gi");
3057
3162
  /**
3058
3163
  * Mask credential-shaped text before it crosses a session boundary.
3059
3164
  * @param text - the text about to be sent to a model outside this session.
@@ -3062,7 +3167,7 @@ const INLINE_ASSIGNMENT_PATTERN = /* @__PURE__ */ new RegExp("((?:\\b|[\\w-]+[_\
3062
3167
  function redactSecrets(text) {
3063
3168
  let out = text;
3064
3169
  for (const [, pattern] of SECRET_PATTERNS) out = out.replace(pattern, "<redacted>");
3065
- out = out.replace(INLINE_ASSIGNMENT_PATTERN, (_match, p1) => `${p1 ?? ""}<redacted>`);
3170
+ out = out.replace(INLINE_ASSIGNMENT_PATTERN, (_match, lead, prefix, key, separator) => `${lead ?? ""}${prefix ?? ""}${key ?? ""}${separator ?? ""}<redacted>`);
3066
3171
  return out;
3067
3172
  }
3068
3173
  //#endregion
@@ -3150,6 +3255,22 @@ const CORRECTION_PATTERNS = [
3150
3255
  /remember\s+(?:this|that|to)/i
3151
3256
  ];
3152
3257
  const FIX_PATTERNS = [/worked after|fixed by|the fix was|root cause/i, /retry(?:ing)? worked|workaround/i];
3258
+ /** Text of one persisted content block, or `''` for any other shape.
3259
+ *
3260
+ * Content blocks cross the durable session-log boundary, so their runtime
3261
+ * shape is `unknown` even where the static event type promises
3262
+ * `{ type, text }`: a persisted `content: [null]` (A2-7, v18) used to throw a
3263
+ * TypeError here and the review catch swallowed the whole turn's remaining
3264
+ * signals. Keeping the guard in one helper also keeps the branches free of
3265
+ * conditions the static type already excludes.
3266
+ * @param block - one element of a persisted message `content` array.
3267
+ * @returns the block's text when it is a text block, otherwise an empty string.
3268
+ */
3269
+ function textOfBlock(block) {
3270
+ if (block === null || typeof block !== "object") return "";
3271
+ const candidate = block;
3272
+ return candidate.type === "text" && typeof candidate.text === "string" ? candidate.text : "";
3273
+ }
3153
3274
  /** Fold one session event into the current turn observation. */
3154
3275
  function observeEvent(signal, event) {
3155
3276
  const data = event.data;
@@ -3157,7 +3278,7 @@ function observeEvent(signal, event) {
3157
3278
  if (event.type === "user/message") {
3158
3279
  const content = data.content;
3159
3280
  if (!Array.isArray(content)) return;
3160
- const text = content.map((block) => block.type === "text" ? block.text ?? "" : "").join(" ");
3281
+ const text = content.map(textOfBlock).join(" ");
3161
3282
  signal.userChars += text.length;
3162
3283
  if (CORRECTION_PATTERNS.some((pattern) => pattern.test(text))) signal.memorySignal = true;
3163
3284
  if (FIX_PATTERNS.some((pattern) => pattern.test(text))) signal.skillSignal = true;
@@ -3166,7 +3287,7 @@ function observeEvent(signal, event) {
3166
3287
  if (event.type === "assistant/message") {
3167
3288
  const message = data.message;
3168
3289
  if (!message || !Array.isArray(message.content)) return;
3169
- const text = message.content.map((block) => block.type === "text" ? block.text ?? "" : "").join(" ");
3290
+ const text = message.content.map(textOfBlock).join(" ");
3170
3291
  signal.assistantChars += text.length;
3171
3292
  return;
3172
3293
  }
@@ -3408,8 +3529,12 @@ const MAX_RESTRUCTURE_MOVES = 5;
3408
3529
  * dots) used to pass here while every later patch/write/remove on it was
3409
3530
  * refused as traversal (an orphan file the user could not touch). */
3410
3531
  const RESTRUCTURE_TARGET_RE = /^references\/[a-z0-9](?!.*\.\.)[a-z0-9._-]*\.md$/;
3532
+ /** F-20 (v18): the character rule shared by support-file names and snapshot
3533
+ * `extras/` entry names. The two exported names used to carry the same literal
3534
+ * independently; both now derive from this one. */
3535
+ const SUPPORT_ENTRY_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
3411
3536
  /** Extra file name carried inside a snapshot's `extras/` directory. */
3412
- const SNAPSHOT_EXTRA_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
3537
+ const SNAPSHOT_EXTRA_NAME_RE = SUPPORT_ENTRY_NAME_RE;
3413
3538
  function skillsRoot(env = process.env) {
3414
3539
  return join(evolutionRoot(env), "skills");
3415
3540
  }
@@ -3423,6 +3548,29 @@ function skillsRoot(env = process.env) {
3423
3548
  function resolveSkillsRoot(config = {}) {
3424
3549
  return (config.root ?? "").trim() || skillsRoot();
3425
3550
  }
3551
+ /** E-7 (v18): every family row reads ONE root key. `root` is canonical;
3552
+ * `skillsRoot` is a deprecated alias honoured only while `root` is empty (so a
3553
+ * deployment that sets both keeps the canonical one) and removed after 0.3.65.
3554
+ * Callers log their own deprecation warning.
3555
+ * @param config - the raw plugin config, carrying `root` and/or `skillsRoot`.
3556
+ * @returns the effective root (empty when neither key is set) and whether the
3557
+ * deprecated alias supplied it.
3558
+ */
3559
+ function resolveRootConfig(config = {}) {
3560
+ const root = (config.root ?? "").trim();
3561
+ if (root !== "") return {
3562
+ root,
3563
+ usedDeprecatedAlias: false
3564
+ };
3565
+ const alias = (config.skillsRoot ?? "").trim();
3566
+ return alias === "" ? {
3567
+ root: "",
3568
+ usedDeprecatedAlias: false
3569
+ } : {
3570
+ root: alias,
3571
+ usedDeprecatedAlias: true
3572
+ };
3573
+ }
3426
3574
  /**
3427
3575
  * Map a requesting session onto the two origin surfaces (rc.44 plan M2-2.3):
3428
3576
  * the APPROVAL surface treats every delegated subagent as the autonomous
@@ -3459,6 +3607,15 @@ function skillDir(root, name) {
3459
3607
  function markerEntryName(marker) {
3460
3608
  return `.${marker}`;
3461
3609
  }
3610
+ /** F-17 (v18): the root-level lock files a DESTRUCTIVE MOVER must treat as an
3611
+ * active writer (skill body + the two marker writers). Single source with
3612
+ * `markerEntryName`/`LOCK_SUFFIX` so a renamed marker cannot silently drop out
3613
+ * of the ghost-writer probe. */
3614
+ const MARKER_LOCK_NAMES = [
3615
+ `SKILL.md${LOCK_SUFFIX}`,
3616
+ `.pinned${LOCK_SUFFIX}`,
3617
+ `.hermes-managed${LOCK_SUFFIX}`
3618
+ ];
3462
3619
  function markerPath(dir, marker) {
3463
3620
  return join(dir, markerEntryName(marker));
3464
3621
  }
@@ -3697,6 +3854,13 @@ function authoringFeedback(frontmatter) {
3697
3854
  lines
3698
3855
  };
3699
3856
  }
3857
+ /** A1-15 (v18) / P2-2 (v19): the io layer marks an error `committed: true` when
3858
+ * the rename landed and only the directory fsync failed. Every single-file
3859
+ * writer must treat that as "written, durability unconfirmed" — never as a
3860
+ * plain failure (which a caller would retry, or a two-phase caller roll back). */
3861
+ function isCommittedOnly(error) {
3862
+ return error?.committed === true;
3863
+ }
3700
3864
  async function listNames(root, io) {
3701
3865
  const entries = await io.list(root);
3702
3866
  const names = [];
@@ -3711,7 +3875,7 @@ async function listNames(root, io) {
3711
3875
  * `[a-z0-9._-]`) — drive-colon / odd-character / uppercase names can no
3712
3876
  * longer reach the filesystem through writeSupportFile / patch /
3713
3877
  * removeSupportFile. */
3714
- const SUPPORT_FILE_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
3878
+ const SUPPORT_FILE_NAME_RE = SUPPORT_ENTRY_NAME_RE;
3715
3879
  /** C-18: win32 reserves these stems with ANY extension (`nul.md` hits the
3716
3880
  * NUL device), and they are fully inside the charset above — so the reserved
3717
3881
  * set is checked on the first-dot prefix as well; the charset close alone
@@ -3742,6 +3906,16 @@ const WIN32_RESERVED_DEVICE_NAMES = new Set([
3742
3906
  "lpt8",
3743
3907
  "lpt9"
3744
3908
  ]);
3909
+ /** A1-6 (v18): the regex alone admits `references/nul.md` (a Windows device
3910
+ * stem), which the support-file layer refuses. Restructure must use the same
3911
+ * rule, or it creates an orphan the later patch/write/remove paths refuse.
3912
+ * Single source shared with the plan validator. */
3913
+ function validateRestructureTarget(filePath) {
3914
+ if (!RESTRUCTURE_TARGET_RE.test(filePath)) return `toFile must be references/<topic>.md (got "${filePath}").`;
3915
+ const stem = filePath.slice(filePath.lastIndexOf("/") + 1).split(".")[0]?.toLowerCase() ?? "";
3916
+ if (WIN32_RESERVED_DEVICE_NAMES.has(stem)) return `toFile "${filePath}" uses a Windows reserved device name.`;
3917
+ return null;
3918
+ }
3745
3919
  function validateSupportPath(filePath) {
3746
3920
  const normalized = filePath.replace(/\\/g, "/");
3747
3921
  if (normalized.includes("..")) return "Path traversal is not allowed.";
@@ -3751,6 +3925,7 @@ function validateSupportPath(filePath) {
3751
3925
  for (const part of parts.slice(1)) {
3752
3926
  if (!SUPPORT_FILE_NAME_RE.test(part)) return `Unsupported file name "${part}" — use lowercase letters, digits, dots, hyphens, and underscores (leading letter or digit).`;
3753
3927
  if (part.toLowerCase().endsWith(".lock")) return `Unsupported file name "${part}" — the .lock suffix is reserved for the writer-lock protocol.`;
3928
+ 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.`;
3754
3929
  const stem = part.split(".")[0]?.toLowerCase() ?? "";
3755
3930
  if (WIN32_RESERVED_DEVICE_NAMES.has(stem)) return `Unsupported file name "${part}" — a Windows reserved device name.`;
3756
3931
  }
@@ -3958,11 +4133,23 @@ var SkillLibrary = class {
3958
4133
  outcome = o;
3959
4134
  return o.write ?? current ?? null;
3960
4135
  };
3961
- if (this.transact) await this.transact(this.io, path, run);
4136
+ let durabilityWarning = "";
4137
+ const committedOnly = (error) => error?.committed === true;
4138
+ if (this.transact) try {
4139
+ await this.transact(this.io, path, run);
4140
+ } catch (error) {
4141
+ if (!committedOnly(error)) throw error;
4142
+ durabilityWarning = error instanceof Error ? error.message : String(error);
4143
+ }
3962
4144
  else {
3963
4145
  const current = await this.io.readText(path);
3964
4146
  const next = await run(current);
3965
- if (next !== null && next !== current) await this.io.writeText(path, next);
4147
+ if (next !== null && next !== current) try {
4148
+ await this.io.writeText(path, next);
4149
+ } catch (error) {
4150
+ if (!committedOnly(error)) throw error;
4151
+ durabilityWarning = error instanceof Error ? error.message : String(error);
4152
+ }
3966
4153
  }
3967
4154
  const o = outcome;
3968
4155
  if (o === void 0 || typeof o !== "object" || !Object.prototype.hasOwnProperty.call(o, "write")) return {
@@ -3971,7 +4158,10 @@ var SkillLibrary = class {
3971
4158
  };
3972
4159
  if (o.write !== null && o.audit) await this.audit(o.audit.skillName, o.audit.action, o.audit.before, o.audit.after, o.audit.summary);
3973
4160
  if (o.write !== null && o.event) this.notifyMutation(o.event);
3974
- return o.result;
4161
+ return durabilityWarning === "" || !o.result.ok ? o.result : {
4162
+ ...o.result,
4163
+ message: `${o.result.message} (warning: the write landed but the directory fsync failed — durability unconfirmed: ${durabilityWarning})`
4164
+ };
3975
4165
  }
3976
4166
  /** Notify the mutation observer after a successful write; observers must never fail the mutation. */
3977
4167
  notifyMutation(event) {
@@ -3988,8 +4178,7 @@ var SkillLibrary = class {
3988
4178
  * label (scanContentThreats already embeds it) plus the self-heal hint, so a
3989
4179
  * false-positive rewrite direction is actionable instead of a dead end. */
3990
4180
  contentThreatBlock(content) {
3991
- const threat = scanContentThreats(content, void 0, this.threatScanOptions());
3992
- return threat === null ? null : threat + THREAT_EXEMPT_HINT;
4181
+ return scanContentThreats(content, void 0, this.threatScanOptions());
3993
4182
  }
3994
4183
  async list() {
3995
4184
  const summaries = [];
@@ -3998,20 +4187,42 @@ var SkillLibrary = class {
3998
4187
  const md = await this.io.readText(join(dir, "SKILL.md"));
3999
4188
  if (md === null) continue;
4000
4189
  const parsed = parseFrontmatter(md);
4001
- let entries = [];
4190
+ let entries = null;
4002
4191
  try {
4003
4192
  entries = await this.io.list(dir);
4004
- } catch {}
4005
- const has = (marker) => entries.includes(markerEntryName(marker));
4006
- const protectedBy = has("bundled") ? "bundled" : has("hub-installed") ? "hub-installed" : has("pinned") ? "pinned" : null;
4193
+ } catch {
4194
+ entries = null;
4195
+ }
4196
+ const probeMarker = async (marker) => {
4197
+ if (entries !== null) return entries.includes(markerEntryName(marker));
4198
+ try {
4199
+ return await this.io.exists(join(dir, markerEntryName(marker)));
4200
+ } catch {
4201
+ return null;
4202
+ }
4203
+ };
4204
+ const [bundled, hubInstalled, pinned, hermesManaged] = await Promise.all([
4205
+ probeMarker("bundled"),
4206
+ probeMarker("hub-installed"),
4207
+ probeMarker("pinned"),
4208
+ probeMarker("hermes-managed")
4209
+ ]);
4210
+ const protectedBy = bundled === true ? "bundled" : hubInstalled === true ? "hub-installed" : pinned === true ? "pinned" : null;
4007
4211
  const parsedDescription = parsed?.frontmatter.description;
4212
+ const parsedWhenToUse = parsed?.frontmatter.whenToUse;
4008
4213
  summaries.push({
4009
4214
  name,
4010
4215
  description: typeof parsedDescription === "string" ? parsedDescription : "",
4011
4216
  path: dir,
4012
4217
  protectedBy,
4013
- managed: has("hermes-managed"),
4014
- archived: false
4218
+ protectionUnknown: [
4219
+ bundled,
4220
+ hubInstalled,
4221
+ pinned,
4222
+ hermesManaged
4223
+ ].some((value) => value === null),
4224
+ managed: hermesManaged === true,
4225
+ ...typeof parsedWhenToUse === "string" && parsedWhenToUse.trim() !== "" ? { whenToUse: parsedWhenToUse } : {}
4015
4226
  });
4016
4227
  }
4017
4228
  return summaries;
@@ -4193,6 +4404,9 @@ var SkillLibrary = class {
4193
4404
  * marker write is the only state change; content is untouched.
4194
4405
  */
4195
4406
  async setPinned(name, pinned, origin = "foreground") {
4407
+ return await this.serial(() => this.setPinnedCore(name, pinned, origin));
4408
+ }
4409
+ async setPinnedCore(name, pinned, origin) {
4196
4410
  const normalized = name.trim();
4197
4411
  const bad = this.badName(normalized);
4198
4412
  if (bad) return {
@@ -4203,6 +4417,7 @@ var SkillLibrary = class {
4203
4417
  ok: false,
4204
4418
  message: "Only the foreground (user or the main agent) may pin or unpin skills."
4205
4419
  };
4420
+ let durabilityWarning = "";
4206
4421
  const dir = this.dirOf(normalized);
4207
4422
  const marker = markerPath(dir, "pinned");
4208
4423
  const existing = await this.io.exists(marker);
@@ -4220,12 +4435,17 @@ var SkillLibrary = class {
4220
4435
  ok: false,
4221
4436
  message: `Skill "${normalized}" not found.`
4222
4437
  };
4223
- if (pinned) await this.io.writeText(marker, "");
4438
+ if (pinned) try {
4439
+ await this.io.writeText(marker, "");
4440
+ } catch (error) {
4441
+ if (!isCommittedOnly(error)) throw error;
4442
+ durabilityWarning = error instanceof Error ? error.message : String(error);
4443
+ }
4224
4444
  else await this.io.remove(marker);
4225
4445
  await this.audit(normalized, pinned ? "pin" : "unpin", null, null, pinned ? "pinned" : "unpinned");
4226
4446
  return {
4227
4447
  ok: true,
4228
- message: pinned ? `Skill "${normalized}" pinned: protected from deletion, background review, and the lifecycle.` : `Skill "${normalized}" unpinned.`,
4448
+ 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})`}`,
4229
4449
  path: dir
4230
4450
  };
4231
4451
  }
@@ -4275,15 +4495,37 @@ var SkillLibrary = class {
4275
4495
  const onDisk = finalContent.trimEnd() + "\n";
4276
4496
  const createPath = join(dir, "SKILL.md");
4277
4497
  let existsAtCommit = false;
4278
- if (this.transact) await this.transact(this.io, createPath, (current) => {
4279
- if (current !== null) {
4280
- existsAtCommit = true;
4281
- return current;
4498
+ let taskRan = false;
4499
+ let createDurabilityWarning = "";
4500
+ if (this.transact) try {
4501
+ await this.transact(this.io, createPath, (current) => {
4502
+ taskRan = true;
4503
+ if (current !== null) {
4504
+ existsAtCommit = true;
4505
+ return current;
4506
+ }
4507
+ return onDisk;
4508
+ });
4509
+ } catch (error) {
4510
+ if (!isCommittedOnly(error)) throw error;
4511
+ createDurabilityWarning = error instanceof Error ? error.message : String(error);
4512
+ }
4513
+ else if (await this.io.exists(createPath)) {
4514
+ taskRan = true;
4515
+ existsAtCommit = true;
4516
+ } else {
4517
+ taskRan = true;
4518
+ try {
4519
+ await this.io.writeText(createPath, onDisk);
4520
+ } catch (error) {
4521
+ if (!isCommittedOnly(error)) throw error;
4522
+ createDurabilityWarning = error instanceof Error ? error.message : String(error);
4282
4523
  }
4283
- return onDisk;
4284
- });
4285
- else if (await this.io.exists(createPath)) existsAtCommit = true;
4286
- else await this.io.writeText(createPath, onDisk);
4524
+ }
4525
+ if (!taskRan) return {
4526
+ ok: false,
4527
+ message: "internal error: the create transaction did not invoke the task; no file was written"
4528
+ };
4287
4529
  if (existsAtCommit) return {
4288
4530
  ok: false,
4289
4531
  message: `Skill "${normalized}" already exists.`
@@ -4297,7 +4539,7 @@ var SkillLibrary = class {
4297
4539
  });
4298
4540
  return {
4299
4541
  ok: true,
4300
- message: `Skill "${normalized}" created.`,
4542
+ message: `Skill "${normalized}" created.${createDurabilityWarning === "" ? "" : ` (warning: the write landed but the directory fsync failed — durability unconfirmed: ${createDurabilityWarning})`}`,
4301
4543
  path: dir,
4302
4544
  ...norm.changed ? { normalizedFrontmatterFields: norm.fields } : {}
4303
4545
  };
@@ -4583,11 +4825,7 @@ var SkillLibrary = class {
4583
4825
  * CRASHED writer also refuses — correct: inspect, don't archive.
4584
4826
  */
4585
4827
  async hasWriteLock(dir) {
4586
- const markerLocks = [
4587
- join(dir, "SKILL.md.lock"),
4588
- join(dir, ".pinned.lock"),
4589
- join(dir, ".hermes-managed.lock")
4590
- ];
4828
+ const markerLocks = MARKER_LOCK_NAMES.map((name) => join(dir, name));
4591
4829
  for (const lock of markerLocks) if (await this.isWriterLock(lock)) return true;
4592
4830
  for (const supportDir of SUPPORT_DIRS) {
4593
4831
  let entries;
@@ -4609,17 +4847,25 @@ var SkillLibrary = class {
4609
4847
  * or be swept as residue — the v16 first cut matched on suffix alone,
4610
4848
  * which permanently refused archiving and deleted user content on restore. */
4611
4849
  async isWriterLock(lockPath) {
4612
- const body = await this.io.readText(lockPath).catch(() => null);
4850
+ let body;
4851
+ try {
4852
+ body = await this.io.readText(lockPath);
4853
+ } catch {
4854
+ return true;
4855
+ }
4613
4856
  if (body === null) return false;
4614
- return /^\d+:[0-9a-f]*$/.test(body.trim());
4857
+ return LOCK_BODY_RE.test(body.trim());
4615
4858
  }
4616
- /** P2 (v16): best-effort removal of lock residue inside a RESTORED tree
4617
- * a `.lock` that a pre-restore crash or TOCTOU stranded in `.archive`
4618
- * cannot have a live writer (restore refuses when the live root is
4619
- * locked), and if left in place its body (a live pid on a single-host
4620
- * deployment) structurally closes the writer's self-heal path. */
4859
+ /** P2 (v16): best-effort removal of lock residue inside a RESTORED tree.
4860
+ * A1-2/A1-7 (v18): the sweep now covers the marker locks the probe checks
4861
+ * (`SKILL.md.lock`/`.pinned.lock`/`.hermes-managed.lock`) and only removes
4862
+ * a lock whose holder pid is NOT alive a live writer's lock is never
4863
+ * stolen by the sweep. A dead-pid residue would otherwise permanently
4864
+ * refuse archive/restore. */
4621
4865
  async deleteStrandedLocks(dir) {
4622
4866
  await this.sweepLockIfStranded(join(dir, "SKILL.md.lock"));
4867
+ await this.sweepLockIfStranded(join(dir, ".pinned.lock"));
4868
+ await this.sweepLockIfStranded(join(dir, ".hermes-managed.lock"));
4623
4869
  for (const supportDir of SUPPORT_DIRS) {
4624
4870
  let entries = [];
4625
4871
  try {
@@ -4631,10 +4877,41 @@ var SkillLibrary = class {
4631
4877
  }
4632
4878
  }
4633
4879
  /** Remove `lockPath` only when its body has the writer-lock `pid:token`
4634
- * shape; anything else (a user support file) is left untouched. */
4880
+ * shape AND the holder pid is not alive; anything else (a user support file
4881
+ * or a live writer's lock) is left untouched. */
4635
4882
  async sweepLockIfStranded(lockPath) {
4636
4883
  const body = await this.io.readText(lockPath).catch(() => null);
4637
- if (body === null || !/^\d+:[0-9a-f]*$/.test(body.trim())) return;
4884
+ if (body === null) return;
4885
+ const match = /^(\d+):[0-9a-f]*$/.exec(body.trim());
4886
+ if (match === null) return;
4887
+ const pid = Number(match[1]);
4888
+ if (Number.isInteger(pid) && pid > 0 && isProcessAlive(pid)) return;
4889
+ await this.io.remove(lockPath).catch(() => {});
4890
+ }
4891
+ /** A1-4 (v18): a manifest-declared name is copied with `join(root, name)`;
4892
+ * only a single, non-traversing path component is safe. Dotfiles
4893
+ * (`.usage.json`) stay allowed — sidecars are legitimately dot-prefixed.
4894
+ * P2-4 (v19): a non-string entry (`skills: [123]`) is refused structurally
4895
+ * instead of throwing `name.includes is not a function`. */
4896
+ safeSnapshotEntryName(name) {
4897
+ if (typeof name !== "string") return false;
4898
+ return name !== "" && name !== "." && name !== ".." && !name.includes("/") && !name.includes("\\") && basename(name) === name;
4899
+ }
4900
+ /** A1-7 (v18): a root-level lock whose holder is alive must refuse the
4901
+ * restore; a dead residue is swept so a crashed writer cannot block
4902
+ * recovery. A non-lock body shape is left alone (user file). */
4903
+ async refuseLiveLockOrSweep(lockPath, label) {
4904
+ let body;
4905
+ try {
4906
+ body = await this.io.readText(lockPath);
4907
+ } catch (error) {
4908
+ throw new Error(`snapshot restore refused: cannot verify ${label} (${error instanceof Error ? error.message : String(error)}); a live writer may hold it`);
4909
+ }
4910
+ if (body === null) return;
4911
+ const match = /^(\d+):[0-9a-f]*$/.exec(body.trim());
4912
+ if (match === null) return;
4913
+ const pid = Number(match[1]);
4914
+ if (Number.isInteger(pid) && pid > 0 && isProcessAlive(pid)) throw new Error(`snapshot restore refused: ${label} is being written (write lock present); retry once the write completes`);
4638
4915
  await this.io.remove(lockPath).catch(() => {});
4639
4916
  }
4640
4917
  async archive(rawName, options = {}) {
@@ -4921,9 +5198,10 @@ var SkillLibrary = class {
4921
5198
  ok: false,
4922
5199
  message: "Every restructure move needs a non-empty heading."
4923
5200
  };
4924
- if (!RESTRUCTURE_TARGET_RE.test(move.toFile)) return {
5201
+ const targetIssue = validateRestructureTarget(move.toFile);
5202
+ if (targetIssue) return {
4925
5203
  ok: false,
4926
- message: `toFile must be references/<topic>.md (got "${move.toFile}").`
5204
+ message: targetIssue
4927
5205
  };
4928
5206
  }
4929
5207
  const dir = this.dirOf(name);
@@ -5020,16 +5298,17 @@ var SkillLibrary = class {
5020
5298
  ok: false,
5021
5299
  message: `Skill "${name}" is protected (${protection}).`
5022
5300
  };
5023
- for (const precondition of plan.preconditions ?? []) {
5024
- const issue = await precondition({ dir });
5025
- if (issue) return {
5026
- ok: false,
5027
- message: issue
5028
- };
5029
- }
5030
5301
  const landing = [];
5031
5302
  for (const write of plan.writes) {
5032
- const previous = await this.io.readText(write.target).catch(() => null);
5303
+ let previous;
5304
+ try {
5305
+ previous = await this.io.readText(write.target);
5306
+ } catch (error) {
5307
+ return {
5308
+ ok: false,
5309
+ message: `Tree change refused: cannot safely pre-read ${write.target} (${error instanceof Error ? error.message : String(error)}); no writes were performed`
5310
+ };
5311
+ }
5033
5312
  if (Buffer.byteLength(write.content, "utf8") > this.limits.maxSkillFileBytes) return {
5034
5313
  ok: false,
5035
5314
  message: `Write exceeds ${this.limits.maxSkillFileBytes} bytes: ${write.target}`
@@ -5045,18 +5324,16 @@ var SkillLibrary = class {
5045
5324
  previous
5046
5325
  });
5047
5326
  }
5048
- const semantic = plan.validate?.({
5049
- dir,
5050
- currentMd: md
5051
- }) ?? null;
5052
- if (semantic) return {
5053
- ok: false,
5054
- message: semantic
5055
- };
5056
5327
  const written = [];
5328
+ let durabilityWarning = "";
5057
5329
  try {
5058
5330
  for (const entry of landing) {
5059
- await this.io.writeText(entry.target, entry.content);
5331
+ try {
5332
+ await this.io.writeText(entry.target, entry.content);
5333
+ } catch (error) {
5334
+ if (error?.committed !== true) throw error;
5335
+ durabilityWarning = error instanceof Error ? error.message : String(error);
5336
+ }
5060
5337
  written.push({
5061
5338
  target: entry.target,
5062
5339
  previous: entry.previous
@@ -5077,7 +5354,7 @@ var SkillLibrary = class {
5077
5354
  });
5078
5355
  return {
5079
5356
  ok: true,
5080
- message: `${plan.eventAction} "${name}" succeeded.`,
5357
+ message: durabilityWarning === "" ? `${plan.eventAction} "${name}" succeeded.` : `${plan.eventAction} "${name}" succeeded (warning: the write landed but the directory fsync failed — durability unconfirmed: ${durabilityWarning})`,
5081
5358
  path: dir
5082
5359
  };
5083
5360
  }
@@ -5093,9 +5370,10 @@ var SkillLibrary = class {
5093
5370
  ok: false,
5094
5371
  message: bad
5095
5372
  };
5096
- if (await this.io.exists(join(this.dirOf(name), "SKILL.md"))) return {
5373
+ const dest = this.dirOf(name);
5374
+ if (await this.io.exists(dest)) return {
5097
5375
  ok: false,
5098
- message: `Skill "${name}" already exists in the active root; refusing to overwrite.`
5376
+ 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.`
5099
5377
  };
5100
5378
  const archiveRoot = join(this.root, ".archive");
5101
5379
  let entries;
@@ -5118,7 +5396,6 @@ var SkillLibrary = class {
5118
5396
  message: `Skill "${name}" is not in .archive.`
5119
5397
  };
5120
5398
  const source = join(archiveRoot, chosen);
5121
- const dest = this.dirOf(name);
5122
5399
  if (this.io.isSymlink) {
5123
5400
  if (await this.io.isSymlink(source) === true) return {
5124
5401
  ok: false,
@@ -5251,7 +5528,8 @@ var SkillLibrary = class {
5251
5528
  ok: false,
5252
5529
  message: `"${filePath}" is not a readable regular file — remove the files inside it one by one.`
5253
5530
  };
5254
- await this.io.remove(target);
5531
+ if (this.transact) await this.transact(this.io, target, () => null);
5532
+ else await this.io.remove(target);
5255
5533
  await this.audit(name, "remove_file", before, null, `removed ${filePath}`);
5256
5534
  this.notifyMutation({
5257
5535
  action: "remove_file",
@@ -5278,9 +5556,10 @@ var SkillLibrary = class {
5278
5556
  while (await this.io.exists(dest)) dest = join(backupRoot, `skills-${stamp}-${Math.random().toString(36).slice(2, 8)}`);
5279
5557
  try {
5280
5558
  const names = await listNames(this.root, this.io);
5281
- await Promise.all(names.map(async (name) => {
5559
+ const copyFailure = (await Promise.allSettled(names.map(async (name) => {
5282
5560
  await this.io.copy(this.dirOf(name), join(dest, name));
5283
- }));
5561
+ }))).find((result) => result.status === "rejected");
5562
+ if (copyFailure) throw copyFailure.reason;
5284
5563
  const sidecars = [];
5285
5564
  for (const sidecar of [usageFile(this.root), suppressedFile(this.root)]) if (await this.io.exists(sidecar)) {
5286
5565
  const name = basename(sidecar);
@@ -5295,9 +5574,10 @@ var SkillLibrary = class {
5295
5574
  }
5296
5575
  const validExtras = extras.filter((extra) => SNAPSHOT_EXTRA_NAME_RE.test(extra.name));
5297
5576
  const extraNames = validExtras.map((extra) => extra.name);
5298
- await Promise.all(validExtras.map(async (extra) => {
5577
+ const extraFailure = (await Promise.allSettled(validExtras.map(async (extra) => {
5299
5578
  await this.io.writeText(join(dest, "extras", extra.name), extra.content);
5300
- }));
5579
+ }))).find((result) => result.status === "rejected");
5580
+ if (extraFailure) throw extraFailure.reason;
5301
5581
  await this.io.writeText(join(dest, "manifest.json"), JSON.stringify({
5302
5582
  reason,
5303
5583
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -5319,10 +5599,11 @@ var SkillLibrary = class {
5319
5599
  if (raw === null) return null;
5320
5600
  try {
5321
5601
  const manifest = JSON.parse(raw);
5602
+ if (!Array.isArray(manifest.skills)) return null;
5322
5603
  return {
5323
5604
  reason: typeof manifest.reason === "string" ? manifest.reason : "",
5324
5605
  createdAt: typeof manifest.createdAt === "string" ? manifest.createdAt : "",
5325
- skills: Array.isArray(manifest.skills) ? manifest.skills : [],
5606
+ skills: manifest.skills,
5326
5607
  sidecars: Array.isArray(manifest.sidecars) ? manifest.sidecars : [],
5327
5608
  ...typeof manifest.hasArchive === "boolean" ? { hasArchive: manifest.hasArchive } : {},
5328
5609
  extras: Array.isArray(manifest.extras) ? manifest.extras : []
@@ -5357,6 +5638,7 @@ var SkillLibrary = class {
5357
5638
  reason: manifest.reason
5358
5639
  });
5359
5640
  }
5641
+ out.sort((a, b) => (b.createdAt || "").localeCompare(a.createdAt || "") || b.path.localeCompare(a.path));
5360
5642
  return out;
5361
5643
  }
5362
5644
  /**
@@ -5427,6 +5709,21 @@ var SkillLibrary = class {
5427
5709
  * restoreLatestSnapshot so a failed restore can roll itself back (E-13).
5428
5710
  */
5429
5711
  async restoreSnapshotIntoRoot(snapshotPath) {
5712
+ const manifest = await this.readSnapshotManifest(snapshotPath);
5713
+ if (manifest === null) throw new Error(await this.io.exists(join(snapshotPath, "manifest.json")) ? `snapshot ${snapshotPath} has an unreadable manifest.json; refusing to clear the active tree` : `snapshot ${snapshotPath} has no readable manifest.json; refusing to restore`);
5714
+ for (const name of [...manifest.skills, ...manifest.sidecars]) if (!this.safeSnapshotEntryName(name)) throw new Error(`snapshot ${snapshotPath} declares an unsafe entry name ${JSON.stringify(name)}; refusing to restore`);
5715
+ if (manifest.skills.length === 0) {
5716
+ const snapshotEntries = await this.io.list(snapshotPath);
5717
+ const declared = new Set([
5718
+ "manifest.json",
5719
+ "extras",
5720
+ ".archive",
5721
+ ...manifest.skills,
5722
+ ...manifest.sidecars
5723
+ ]);
5724
+ const undeclared = snapshotEntries.filter((entry) => !declared.has(entry));
5725
+ if (undeclared.length > 0) throw new Error(`snapshot ${snapshotPath} declares no skills but contains undeclared entries (${undeclared.join(", ")}); refusing to clear the active tree`);
5726
+ }
5430
5727
  let rootEntries;
5431
5728
  try {
5432
5729
  rootEntries = await this.io.list(this.root);
@@ -5435,16 +5732,26 @@ var SkillLibrary = class {
5435
5732
  }
5436
5733
  for (const entry of rootEntries) {
5437
5734
  if (entry === ".archive" || entry === ".backups" || entry === ".mutations.json" || entry === ".curator-suppressed.json") continue;
5438
- await this.io.remove(join(this.root, entry));
5735
+ if (entry.endsWith(".lock") || entry.endsWith(`.lock.next`)) {
5736
+ await this.refuseLiveLockOrSweep(join(this.root, entry), entry);
5737
+ continue;
5738
+ }
5739
+ const dir = join(this.root, entry);
5740
+ if (await this.io.exists(join(dir, "SKILL.md"))) {
5741
+ await this.deleteStrandedLocks(dir);
5742
+ if (await this.hasWriteLock(dir)) throw new Error(`snapshot restore refused: skill "${entry}" is being written (write lock present); retry once the write completes`);
5743
+ }
5439
5744
  }
5440
- const manifest = await this.readSnapshotManifest(snapshotPath);
5441
- if (manifest === null) for (const entry of await this.io.list(snapshotPath)) {
5442
- if (entry === "manifest.json" || entry === "extras") continue;
5443
- await this.io.copy(join(snapshotPath, entry), join(this.root, entry));
5745
+ const restoresSuppressed = manifest.sidecars.includes(".curator-suppressed.json");
5746
+ for (const entry of rootEntries) {
5747
+ if (entry === ".archive" || entry === ".backups" || entry === ".mutations.json") continue;
5748
+ if (entry === ".curator-suppressed.json" && restoresSuppressed) continue;
5749
+ if (entry.endsWith(".lock") || entry.endsWith(`.lock.next`)) continue;
5750
+ await this.io.remove(join(this.root, entry));
5444
5751
  }
5445
- else {
5446
- for (const name of manifest.skills) await this.io.copy(join(snapshotPath, name), join(this.root, name));
5447
- for (const sidecar of manifest.sidecars) await this.io.copy(join(snapshotPath, sidecar), join(this.root, sidecar));
5752
+ for (const name of manifest.skills) await this.io.copy(join(snapshotPath, name), join(this.root, name));
5753
+ for (const sidecar of manifest.sidecars) await this.io.copy(join(snapshotPath, sidecar), join(this.root, sidecar));
5754
+ {
5448
5755
  const archiveRoot = join(this.root, ".archive");
5449
5756
  if (manifest.hasArchive === true) {
5450
5757
  await this.io.remove(archiveRoot);
@@ -5458,4 +5765,4 @@ var SkillLibrary = class {
5458
5765
  }
5459
5766
  };
5460
5767
  //#endregion
5461
- export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, LOW_QUALITY_THRESHOLD, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, PATTERN_OVERLAP, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, THREAT_EXEMPT_HINT, WIN32_RESERVED_DEVICE_NAMES, advanceReview, allowRowCollisions, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, clampedNumber, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterYamlUnsafeValues, getRecord, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, makeSerialQueue, markAgentCreated, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, pendingSelfCleanup, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renameWithRetry, renderCuratorReportMarkdown, resolveOrigins, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, verifyPromptBundle, writeDurableTmp, yamlPlainScalarNeedsQuotes };
5768
+ export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, LOCK_BODY_RE, LOCK_SUFFIX, LOW_QUALITY_THRESHOLD, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, PATTERN_OVERLAP, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, THREAT_EXEMPT_HINT, advanceReview, allowRowCollisions, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, clampedNumber, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionEventPayloadIssue, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterYamlUnsafeValues, getRecord, isProcessAlive, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, makeSerialQueue, markAgentCreated, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, pendingSelfCleanup, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renameWithRetry, renderCuratorReportMarkdown, resolveOrigins, resolveRootConfig, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, validateRestructureTarget, verifyPromptBundle, writeDurableTmp, yamlPlainScalarNeedsQuotes };