@lmzhen/dsh-evolution-core 0.3.68 → 0.3.70

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
@@ -511,7 +511,7 @@ function nodeEvolutionIo(lockAttempts = 40) {
511
511
  const holder = Number(body.split(":")[0] ?? "");
512
512
  const st = await stat(ticketPath);
513
513
  const dead = !Number.isInteger(holder) || holder <= 0 || !isAlive(holder);
514
- const old = Date.now() - st.mtimeMs > 1e3;
514
+ const old = Date.now() - st.mtimeMs > TICKET_STALE_MS;
515
515
  if (dead || old) await rm(ticketPath, { force: true });
516
516
  } catch {}
517
517
  continue;
@@ -673,6 +673,7 @@ function normalizeUsageRecord(record) {
673
673
  const num = (value, fallback) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
674
674
  const bool = (value, fallback) => typeof value === "boolean" ? value : fallback;
675
675
  return {
676
+ ...raw,
676
677
  created_by: typeof raw.created_by === "string" ? raw.created_by : null,
677
678
  use_count: num(raw.use_count, base.use_count),
678
679
  view_count: num(raw.view_count, base.view_count),
@@ -1044,6 +1045,10 @@ const MAX_TIMER_DELAY_MS = 2147483647;
1044
1045
  const DEFAULT_MEMORY_REVIEW_MODEL = "deepseek-v4-flash";
1045
1046
  const DEFAULT_SKILL_REVIEW_MODEL = "deepseek-v4-pro";
1046
1047
  const DEFAULT_CURATOR_MODEL = "deepseek-v4-pro";
1048
+ /** Order of the `evolution:memory-guidance` section (before the skills one). */
1049
+ const MEMORY_GUIDANCE_SECTION_ORDER = 11e3;
1050
+ /** Order of the `evolution-skills-guidance` section (last of the two). */
1051
+ const SKILLS_GUIDANCE_SECTION_ORDER = 11100;
1047
1052
  //#endregion
1048
1053
  //#region lib/types/gates.js
1049
1054
  /**
@@ -1528,9 +1533,28 @@ async function rotateIfDue(io, path, events, rotateAt) {
1528
1533
  if (tail.length === 0) return events;
1529
1534
  const anchor = tail[0]?.seq ?? 0;
1530
1535
  const archivePath = join(dirname(path), `${EVENT_ARCHIVE_PREFIX}${anchor - 1}.json`);
1536
+ let archived = head;
1537
+ const existing = await io.readText(archivePath).catch(() => null);
1538
+ if (existing !== null) try {
1539
+ const parsed = JSON.parse(existing);
1540
+ const usablePrior = (Array.isArray(parsed.events) ? parsed.events : []).filter(isEventRecord);
1541
+ const bySeq = new Map(usablePrior.map((event) => [event.seq, event]));
1542
+ for (const event of head) bySeq.set(event.seq, event);
1543
+ archived = [...bySeq.values()].sort((a, b) => a.seq - b.seq);
1544
+ } catch {
1545
+ const shiftPath = `${archivePath}.${Date.now()}.collide`;
1546
+ await io.writeText(shiftPath, JSON.stringify({
1547
+ version: 1,
1548
+ events: head
1549
+ }, null, 2));
1550
+ console.warn(`evolution-events: rotation hit an unparsable archive collision — the rotated head band was preserved at ${shiftPath} but is OUTSIDE the logical timeline; inspect and merge it manually`);
1551
+ await retainEventArchives(io, path);
1552
+ await pruneCollideArchives(io, path);
1553
+ return tail;
1554
+ }
1531
1555
  await io.writeText(archivePath, JSON.stringify({
1532
1556
  version: 1,
1533
- events: head
1557
+ events: archived
1534
1558
  }, null, 2));
1535
1559
  await retainEventArchives(io, path);
1536
1560
  return tail;
@@ -1548,6 +1572,34 @@ async function retainEventArchives(io, path) {
1548
1572
  const excess = names.slice(0, Math.max(0, names.length - 10));
1549
1573
  for (const name of excess) await io.remove(join(dir, name)).catch(() => {});
1550
1574
  }
1575
+ /** v31 EVENTS-02: prune shift-aside collision files (`events-*.json.<ts>.collide`)
1576
+ * after the same 7-day window the `.corrupt` sweep uses. They are write-once
1577
+ * recovery artifacts no reader accepts; without a sweep they accumulated
1578
+ * without bound across rollback episodes. No mtime probe → keep (fail-safe). */
1579
+ const COLLIDE_AGE_MS = 10080 * 60 * 1e3;
1580
+ async function pruneCollideArchives(io, path) {
1581
+ const dir = dirname(path);
1582
+ let names;
1583
+ try {
1584
+ names = await io.list(dir);
1585
+ } catch {
1586
+ return;
1587
+ }
1588
+ const now = Date.now();
1589
+ for (const name of names) {
1590
+ if (!name.endsWith(".collide") || !name.startsWith("events-")) continue;
1591
+ const full = join(dir, name);
1592
+ const stamp = name.match(/\.(\d{13})\.collide$/);
1593
+ if (stamp && now - Number(stamp[1]) < COLLIDE_AGE_MS) continue;
1594
+ if (!stamp) try {
1595
+ const mtime = await io.mtime?.(full);
1596
+ if (typeof mtime === "number" && now - mtime < COLLIDE_AGE_MS) continue;
1597
+ } catch {
1598
+ continue;
1599
+ }
1600
+ await io.remove(full).catch(() => {});
1601
+ }
1602
+ }
1551
1603
  /** Read the event log; a missing/whitespace-only file reads as empty,
1552
1604
  * corrupt content is flagged (and refused on append). A well-formed future-
1553
1605
  * version body is v1-incompatible and reads as empty, NOT malformed (F-338:
@@ -1654,16 +1706,21 @@ function allowRowCollisions(env = process.env) {
1654
1706
  * platform's index cap), and DSH-only additions are marked as such.
1655
1707
  *
1656
1708
  * Every prompt is pinned in a versioned bundle. Review workers verify the
1657
- * bundle digest before spending a model call, so a partially-patched
1658
- * deployment fails closed instead of silently running a truncated prompt.
1709
+ * bundle digest before spending a model call v31 PROMPT-01, stated
1710
+ * precisely: THAT check proves internal coherence (id/version/digest agree)
1711
+ * for a bundle assembled OUT of process and handed to `verifyPromptBundle`
1712
+ * explicitly. It CANNOT detect in-process tampering (the digest is recomputed
1713
+ * from the same module state it verifies) — catching a stale or partially
1714
+ * patched default bundle is CI's version pin (tests/prompts.spec.ts), not
1715
+ * this runtime gate.
1659
1716
  */
1660
1717
  /**
1661
1718
  * Prompt bundle identity. Bump both id and version whenever a prompt's text
1662
1719
  * changes semantically: the bundle digest is the fail-closed signal for
1663
1720
  * review workers, so a stale id across deployments must be distinguishable.
1664
1721
  */
1665
- const PROMPT_BUNDLE_VERSION = 16;
1666
- const PROMPT_BUNDLE_ID = `dsh-evolution@16`;
1722
+ const PROMPT_BUNDLE_VERSION = 17;
1723
+ const PROMPT_BUNDLE_ID = `dsh-evolution@17`;
1667
1724
  const MEMORY_REVIEW_PROMPT = `[Auto-review — Memory]
1668
1725
  Review the conversation above and consider saving to memory if appropriate.
1669
1726
 
@@ -1684,7 +1741,7 @@ Signals to look for (any one of these warrants action):
1684
1741
  • Non-trivial technique, fix, workaround, debugging path, or tool-usage pattern emerged that a future session would benefit from. Capture it.
1685
1742
  • A skill that got loaded or consulted this session turned out to be wrong, missing a step, or outdated. Patch it NOW.
1686
1743
 
1687
- Read-before-write (enforced by this channel): update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session ops on unread skills are dropped; CREATE of a brand-new umbrella is the only exception.
1744
+ Read-before-write: update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session. On the plan channel ops on unread skills are rejected; direct writes have no such guard, so treat the rule as binding. CREATE of a brand-new umbrella is the only exception.
1688
1745
 
1689
1746
  Preference order — prefer the earliest action that fits, but do pick one when a signal above fired:
1690
1747
  1. UPDATE A CURRENTLY-LOADED SKILL. Look back through the conversation for skills the user loaded or you read. If any of them covers the territory of the new learning, PATCH that one first. It is the skill that was in play, so it's the right one to extend.
@@ -1734,7 +1791,7 @@ Signals that warrant a skill update (any one is enough):
1734
1791
  • Non-trivial technique, fix, workaround, or debugging path emerged.
1735
1792
  • A skill that was loaded or consulted turned out wrong, missing, or outdated — patch it now.
1736
1793
 
1737
- Read-before-write (enforced by this channel): update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session ops on unread skills are dropped; CREATE of a brand-new umbrella is the only exception.
1794
+ Read-before-write: update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session. On the plan channel ops on unread skills are rejected; direct writes have no such guard, so treat the rule as binding. CREATE of a brand-new umbrella is the only exception.
1738
1795
 
1739
1796
  Preference order for skills — pick the earliest that fits:
1740
1797
  1. UPDATE A CURRENTLY-LOADED SKILL. Check what skills were loaded or read in the conversation. If one of them covers the learning, PATCH it first. It was in play; it's the right place.
@@ -1950,12 +2007,12 @@ function sha256(text) {
1950
2007
  function createPromptBundle(prompts) {
1951
2008
  const canonical = JSON.stringify({
1952
2009
  id: PROMPT_BUNDLE_ID,
1953
- version: 16,
2010
+ version: 17,
1954
2011
  prompts: Object.fromEntries(Object.entries(prompts).sort())
1955
2012
  });
1956
2013
  return Object.freeze({
1957
2014
  id: PROMPT_BUNDLE_ID,
1958
- version: 16,
2015
+ version: 17,
1959
2016
  prompts: Object.freeze({ ...prompts }),
1960
2017
  sha256: sha256(canonical)
1961
2018
  });
@@ -1973,10 +2030,10 @@ const PROMPT_BUNDLE = createPromptBundle({
1973
2030
  skillsGuidance: SKILLS_GUIDANCE
1974
2031
  });
1975
2032
  function verifyPromptBundle(bundle = PROMPT_BUNDLE) {
1976
- if (bundle.id !== PROMPT_BUNDLE_ID || bundle.version !== 16) return false;
2033
+ if (bundle.id !== PROMPT_BUNDLE_ID || bundle.version !== 17) return false;
1977
2034
  const canonical = JSON.stringify({
1978
2035
  id: PROMPT_BUNDLE_ID,
1979
- version: 16,
2036
+ version: 17,
1980
2037
  prompts: Object.fromEntries(Object.entries(bundle.prompts).sort())
1981
2038
  });
1982
2039
  return bundle.sha256 === sha256(canonical);
@@ -2691,11 +2748,30 @@ var MemoryStore = class {
2691
2748
  *
2692
2749
  * @param target - memory target being written
2693
2750
  * @param core - the in-transaction read-modify-write for the locked body
2751
+ * @param shrinkOnly - v29 MEM-02: every queued operation REMOVES an entry
2752
+ * (the recovery path for a limit lowered under existing content). The
2753
+ * oversized read-guard is skipped for these batches: a canonical file
2754
+ * written under a ≥10× higher limit trips the `limit × 10` byte bound, and
2755
+ * the guard's "fix the file manually" refusal would pre-empt exactly the
2756
+ * shrink recovery MEM-01 advertises. The load is bounded by what the store
2757
+ * itself wrote under the old limit.
2694
2758
  * @returns the core's result, or the oversized / contract-violation refusal
2695
2759
  */
2696
- async chainedWrite(target, core) {
2697
- const refusal = await this.oversizedRefusal(target);
2698
- if (refusal) return refusal;
2760
+ async chainedWrite(target, core, shrinkOnly = false) {
2761
+ const SHRINK_LOAD_CEILING = 64 * 1024 * 1024;
2762
+ if (!shrinkOnly) {
2763
+ const refusal = await this.oversizedRefusal(target);
2764
+ if (refusal) return refusal;
2765
+ } else {
2766
+ const size = await this.io.size?.(fileFor(this.root, target));
2767
+ if (typeof size === "number" && size > SHRINK_LOAD_CEILING) return {
2768
+ ok: false,
2769
+ message: `Memory file is ${size} bytes - too large to load even for a remove-only batch. Fix the file manually, then retry.`,
2770
+ entries: [],
2771
+ chars: 0,
2772
+ limit: this.limitFor(target)
2773
+ };
2774
+ }
2699
2775
  let outcome;
2700
2776
  await transactIo(this.io, fileFor(this.root, target), async (current) => {
2701
2777
  const step = await core(current ?? "");
@@ -2801,7 +2877,7 @@ var MemoryStore = class {
2801
2877
  };
2802
2878
  }
2803
2879
  /**
2804
- * The single drift predicate. `raw` is in canonical form when it byte-matches
2880
+ * The single drift evaluation. `raw` is in canonical form when it byte-matches
2805
2881
  * `render(normalizeEntries(raw))`; anything else means it was edited outside
2806
2882
  * MemoryStore (empty/`§`-only entries, stray blank lines, leading or trailing
2807
2883
  * delimiters — structural anomalies the writer would quietly normalize away).
@@ -2815,28 +2891,69 @@ var MemoryStore = class {
2815
2891
  * every write path — including the repairs the model would need to make.
2816
2892
  * Such files are adopted instead of flagged.
2817
2893
  *
2894
+ * Returns one of:
2895
+ * - `'external'` — non-canonical body, i.e. real external modification. This
2896
+ * includes the Hermes-parity signal #2 (an entry larger than the whole-file
2897
+ * limit): that shape is only meaningful as external evidence on a
2898
+ * NON-canonical body, because free-form external appends never render
2899
+ * canonically.
2900
+ * - `'over-limit'` — v28 MEM-01: a CANONICAL body whose entries exceed the
2901
+ * CURRENT configured limit. Those bytes were written by this store under a
2902
+ * previous (higher) limit, so they are not external drift; treating them as
2903
+ * such misattributed a config change to an "external editor" and bricked
2904
+ * every write path (the advertised recovery — remove/consolidate — is
2905
+ * exactly what the drift gate refused). Callers route this state to a
2906
+ * config-naming refusal and let shrink-only batches through.
2907
+ * - `null` — no drift: writable as-is.
2908
+ *
2818
2909
  * @param target - memory target whose char limit bounds one parsed entry
2819
2910
  * @param raw - on-disk body, or `null` when the file does not exist
2820
- * @returns whether these bytes count as externally drifted
2821
2911
  */
2822
- drifted(target, raw) {
2823
- if (raw === null || raw.trim() === "") return false;
2912
+ driftKind(target, raw) {
2913
+ if (raw === null || raw.trim() === "") return null;
2824
2914
  const entries = normalizeEntries(raw);
2915
+ if (render(entries) !== raw) return "external";
2825
2916
  const limit = this.limitFor(target);
2826
- if (limit > 0 && entries.some((entry) => entry.length > limit)) return true;
2827
- return render(entries) !== raw;
2917
+ if (limit > 0 && entries.some((entry) => entry.length > limit)) return "over-limit";
2918
+ return null;
2919
+ }
2920
+ /** External-drift predicate: canonical-form violations only (see
2921
+ * {@link driftKind}). `detectDrift` and the write paths share it, so a write
2922
+ * and a later read never disagree about the same bytes. */
2923
+ drifted(target, raw) {
2924
+ return this.driftKind(target, raw) === "external";
2828
2925
  }
2829
2926
  /**
2830
2927
  * Drift refusal for a body already read under the write lock, or `null` when
2831
- * the body is canonical. Both write paths return this unchanged, so their
2928
+ * writing may proceed. Both write paths return this unchanged, so their
2832
2929
  * refusals stay byte-identical and each carries the same backup.
2833
2930
  *
2931
+ * The `'over-limit'` state never refuses shrink-only batches (`shrinkOnly`):
2932
+ * removing entries is the advertised recovery for a limit lowered under
2933
+ * existing content, and the batch's own final limit check still gates the
2934
+ * result.
2935
+ *
2834
2936
  * @param target - memory target that owns the drifted file
2835
2937
  * @param raw - locked file body
2938
+ * @param shrinkOnly - every queued operation removes an entry (no growth)
2836
2939
  * @returns the refusal to hand back, or `null` to continue writing
2837
2940
  */
2838
- async driftRefusal(target, raw) {
2839
- if (!this.drifted(target, raw)) return null;
2941
+ async driftRefusal(target, raw, shrinkOnly = false) {
2942
+ const kind = this.driftKind(target, raw);
2943
+ if (kind === null) return null;
2944
+ if (kind === "over-limit") {
2945
+ if (shrinkOnly) return null;
2946
+ const limit = this.limitFor(target);
2947
+ const entries = normalizeEntries(raw);
2948
+ const over = entries.filter((entry) => entry.length > limit).length;
2949
+ return {
2950
+ ok: false,
2951
+ message: `${over} memory ${over === 1 ? "entry exceeds" : "entries exceed"} the configured ${target}CharLimit (${limit}); they were written under a higher limit. Raise the limit or remove entries — remove operations stay available.`,
2952
+ entries,
2953
+ chars: entries.join(ENTRY_DELIMITER).length,
2954
+ limit
2955
+ };
2956
+ }
2840
2957
  const backup = await this.backupFile(target);
2841
2958
  return {
2842
2959
  ok: false,
@@ -2857,11 +2974,12 @@ var MemoryStore = class {
2857
2974
  chars: 0,
2858
2975
  limit: this.limitFor(target)
2859
2976
  };
2860
- return await this.chainedWrite(target, async (raw) => await this.applyBatchCore(target, operations, raw));
2977
+ const shrinkOnly = operations.every((op) => op.action === "remove");
2978
+ return await this.chainedWrite(target, async (raw) => await this.applyBatchCore(target, operations, raw, shrinkOnly), shrinkOnly);
2861
2979
  }
2862
2980
  /** Batch RMW inside the transaction. `write: null` = failure/no-op, disk untouched. */
2863
- async applyBatchCore(target, operations, raw) {
2864
- const refusal = await this.driftRefusal(target, raw);
2981
+ async applyBatchCore(target, operations, raw, shrinkOnly = false) {
2982
+ const refusal = await this.driftRefusal(target, raw, shrinkOnly);
2865
2983
  if (refusal) return {
2866
2984
  result: refusal,
2867
2985
  write: null
@@ -3389,6 +3507,8 @@ const SECRET_PATTERNS = [
3389
3507
  ];
3390
3508
  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");
3391
3509
  const URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:)([^\s/@]+)@/gi;
3510
+ const PEM_PRIVATE_KEY_PATTERN = new RegExp(`-----BEGIN\\s+(?:(?:RSA|EC|DSA|OPENSSH|ENCRYPTED|PGP)\\s+)?PRIVATE\\s+KEY(?:\\s+BLOCK)?-----[\\s\\S]*?-----END\\s+(?:(?:RSA|EC|DSA|OPENSSH|ENCRYPTED|PGP)\\s+)?PRIVATE\\s+KEY(?:\\s+BLOCK)?-----`, "g");
3511
+ const BLOCK_KEY_ONLY_LINE = /(?:^|\s)([\w-]{0,64}[_\-])?((?:token|api[_-]?key|secret|password|passwd)(?:[_\-][\w-]{0,64})?)\s*:\s*$/i;
3392
3512
  /**
3393
3513
  * Mask credential-shaped text before it crosses a session boundary.
3394
3514
  * @param text - the text about to be sent to a model outside this session.
@@ -3396,9 +3516,21 @@ const URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:)([^\s/@]+)@/g
3396
3516
  */
3397
3517
  function redactSecrets(text) {
3398
3518
  let out = text;
3519
+ out = out.replace(PEM_PRIVATE_KEY_PATTERN, "<redacted-private-key>");
3399
3520
  for (const [, pattern] of SECRET_PATTERNS) out = out.replace(pattern, "<redacted>");
3400
3521
  out = out.replace(URL_CREDENTIALS_PATTERN, (_match, lead) => `${lead ?? ""}<redacted>@`);
3401
3522
  out = out.replace(INLINE_ASSIGNMENT_PATTERN, (_match, lead, prefix, key, separator) => `${lead ?? ""}${prefix ?? ""}${key ?? ""}${separator ?? ""}<redacted>`);
3523
+ const lines = out.split("\n");
3524
+ for (let i = 0; i < lines.length - 1; i++) {
3525
+ const line = lines[i];
3526
+ if (line === void 0 || !BLOCK_KEY_ONLY_LINE.test(line)) continue;
3527
+ const next = lines[i + 1] ?? "";
3528
+ const [, indent, value, tail] = /^([ \t]+)(\S.*?)([ \t]*)$/.exec(next) ?? [];
3529
+ if (indent === void 0 || value === void 0) continue;
3530
+ if (value.includes("<redacted>")) continue;
3531
+ lines[i + 1] = `${indent}<redacted>${tail ?? ""}`;
3532
+ }
3533
+ out = lines.join("\n");
3402
3534
  out = out.split("\n").map((line) => {
3403
3535
  if (!/<redacted>/.test(line) && !/\baws\b|\bAKIA\b|\bsecret\b/i.test(line)) return line;
3404
3536
  return line.replace(/(?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=])/g, "<redacted>");
@@ -3570,8 +3702,9 @@ function foldTurn(session, fromSeq) {
3570
3702
  memorySignal: false,
3571
3703
  skillSignal: false
3572
3704
  };
3573
- for (let index = Math.max(0, fromSeq); index < session.events.length; index += 1) {
3574
- const event = session.events[index];
3705
+ const events = session.snapshotEvents();
3706
+ for (let index = Math.max(0, fromSeq); index < events.length; index += 1) {
3707
+ const event = events[index];
3575
3708
  if (event) observeEvent(signal, event);
3576
3709
  }
3577
3710
  return signal;
@@ -3859,7 +3992,7 @@ function markerPath(dir, marker) {
3859
3992
  /**
3860
3993
  * Shared frontmatter block detection (P3-3 single owner): opening line `---`
3861
3994
  * and closing line exactly `---`. Used by `parseFrontmatter`,
3862
- * `frontmatterYamlUnsafeValues` and `normalizeFrontmatter` so the three can
3995
+ * `frontmatterCatalogInvalid` and `normalizeFrontmatter` so the three can
3863
3996
  * never disagree about where the block ends (the loose `indexOf('\n---')`
3864
3997
  * form matched `\n----` and was replaced by this strict line rule).
3865
3998
  *
@@ -3894,7 +4027,7 @@ function frontmatterBlock(content) {
3894
4027
  }
3895
4028
  /**
3896
4029
  * Raw-line scan of a frontmatter block: the single owner of "which entries the
3897
- * strict catalog cannot load". `frontmatterYamlUnsafeValues` publishes it and
4030
+ * strict catalog cannot load". `frontmatterCatalogInvalid` publishes it and
3898
4031
  * `normalizeFrontmatter` decides each rewrite with the same predicate
3899
4032
  * (`yamlPlainScalarNeedsQuotes`), so the audit verdict and the write path can
3900
4033
  * never disagree. Only single-line `key: value` entries are judged; a line with
@@ -4060,7 +4193,8 @@ function parseFrontmatter(content) {
4060
4193
  */
4061
4194
  function frontmatterCatalogInvalid(content) {
4062
4195
  const read = readFrontmatterBlock(content);
4063
- return read === null ? false : read.strictFailed || read.unsafeValues.length > 0;
4196
+ if (read !== null) return read.strictFailed || read.unsafeValues.length > 0;
4197
+ return frontmatterBlock(content.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n")) !== null;
4064
4198
  }
4065
4199
  /** YAML plain-scalar hazards that make an UNQUOTED frontmatter value
4066
4200
  * unloadable to the platform catalog (strict YAML parser): `: ` (mapping
@@ -4084,22 +4218,14 @@ function yamlPlainScalarNeedsQuotes(value) {
4084
4218
  if (value.includes(": ")) return true;
4085
4219
  if (value.includes(" #")) return true;
4086
4220
  if (value.endsWith(":")) return true;
4087
- if (/^(?:null|true|false|~|[-+]?\d+(?:\.\d+)?)$/i.test(value)) return true;
4221
+ if (/^(?:null|true|false|~)$/i.test(value)) return true;
4222
+ if (/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?$/.test(value)) return true;
4223
+ if (/^0x[0-9a-f]+$/i.test(value)) return true;
4224
+ if (/^0o[0-7]+$/.test(value)) return true;
4225
+ if (/^\.(?:inf|nan)$/i.test(value)) return true;
4088
4226
  if (/^[-?:,[\]{}#&*!|>'\"%@`\s]/.test(value)) return true;
4089
4227
  return false;
4090
4228
  }
4091
- /** Raw-line scan of the frontmatter block: entries whose UNQUOTED value is
4092
- * YAML-unsafe for the strict platform catalog. Operates on the ORIGINAL line
4093
- * value (quotes included), so a value already wrapped by
4094
- * `normalizeFrontmatter` is never re-flagged — one source with the write
4095
- * path. V27 G2.1: delegates to the shared scan, which
4096
- * `parseFrontmatter(...).catalogInvalid` also uses, so the audit view and the
4097
- * read view of one file can never disagree. Independent of the body: a
4098
- * body-less file is still reported here. */
4099
- function frontmatterYamlUnsafeValues(content) {
4100
- const block = frontmatterBlock(content);
4101
- return block === null ? [] : unsafeFrontmatterEntries(block.block, block.nl);
4102
- }
4103
4229
  /**
4104
4230
  * Normalize a SKILL.md frontmatter block into catalog-loadable YAML: values
4105
4231
  * that YAML forbids unquoted get quotes — double quotes normally, single
@@ -4243,9 +4369,18 @@ function authoringFeedback(frontmatter) {
4243
4369
  /** A1-15 (v18) / P2-2 (v19): the io layer marks an error `committed: true` when
4244
4370
  * the rename landed and only the directory fsync failed. Every single-file
4245
4371
  * writer must treat that as "written, durability unconfirmed" — never as a
4246
- * plain failure (which a caller would retry, or a two-phase caller roll back). */
4372
+ * plain failure (which a caller would retry, or a two-phase caller roll back).
4373
+ * v28 G2.1 (EVO-IO-05): this is a delegation to the seam's own
4374
+ * `isCommittedWarning` — the marker predicate has exactly one definition. */
4247
4375
  function isCommittedOnly(error) {
4248
- return error?.committed === true;
4376
+ return isCommittedWarning(error);
4377
+ }
4378
+ /** v28 G2.5 (CORE-SK-03): every pre-clear refusal in restoreSnapshotIntoRoot
4379
+ * says "refus…" (manifest / traversal / live-writer gates) or "is incomplete"
4380
+ * (completeness gate). Anything else thrown from that method means the
4381
+ * destructive clear already happened and a rollback is load-bearing. */
4382
+ function isPreClearRefusal(message) {
4383
+ return message.includes("refus") || message.includes("is incomplete");
4249
4384
  }
4250
4385
  async function listNames(root, io) {
4251
4386
  const entries = await io.list(root);
@@ -4516,11 +4651,14 @@ var SkillLibrary = class {
4516
4651
  let outcome;
4517
4652
  const run = async (current) => {
4518
4653
  const o = await task(current ?? null);
4519
- outcome = o;
4654
+ outcome = {
4655
+ ...o,
4656
+ ghostDir: current === null && o.write === null
4657
+ };
4520
4658
  return o.write ?? current ?? null;
4521
4659
  };
4522
4660
  let durabilityWarning = "";
4523
- const committedOnly = (error) => error?.committed === true;
4661
+ const committedOnly = isCommittedOnly;
4524
4662
  if (this.transact) try {
4525
4663
  await this.transact(this.io, path, run);
4526
4664
  } catch (error) {
@@ -4542,6 +4680,7 @@ var SkillLibrary = class {
4542
4680
  ok: false,
4543
4681
  message: "internal error: the write transaction did not invoke the task; no write was performed"
4544
4682
  };
4683
+ if (o.ghostDir === true) await this.cleanupGhostDir(path);
4545
4684
  if (o.write !== null && o.audit) await this.audit(o.audit.skillName, o.audit.action, o.audit.before, o.audit.after, o.audit.summary);
4546
4685
  if (o.write !== null && o.event) this.notifyMutation(o.event);
4547
4686
  return durabilityWarning === "" || !o.result.ok ? o.result : {
@@ -4549,6 +4688,24 @@ var SkillLibrary = class {
4549
4688
  message: `${o.result.message} (warning: the write landed but the directory fsync failed — durability unconfirmed: ${durabilityWarning})`
4550
4689
  };
4551
4690
  }
4691
+ /**
4692
+ * v28 G1.1 (EVO-IO-02): shared compensating cleanup for a locked write that
4693
+ * found no SKILL.md — remove the possibly-resurrected directory ONLY when it
4694
+ * holds nothing but this path's own write-lock file. The BR-5 rule from
4695
+ * setPinnedCore/createCore applies unchanged: a concurrent mover can land a
4696
+ * full directory between the list probe and the remove, so anything beyond
4697
+ * the lock file (support files, a fresh restore) must never be recursed
4698
+ * away. Best-effort: a failed cleanup leaves the "not found" result
4699
+ * unchanged (the operator-facing ghost-dir refusal is fail-loud already).
4700
+ */
4701
+ async cleanupGhostDir(skillFilePath) {
4702
+ const dir = dirname(skillFilePath);
4703
+ const lockName = `${basename(skillFilePath)}${LOCK_SUFFIX}`;
4704
+ try {
4705
+ if ((await this.io.list(dir)).some((entry) => entry !== lockName)) return;
4706
+ await this.io.remove(dir);
4707
+ } catch {}
4708
+ }
4552
4709
  /** Notify the mutation observer after a successful write; observers must never fail the mutation. */
4553
4710
  notifyMutation(event) {
4554
4711
  try {
@@ -4566,11 +4723,39 @@ var SkillLibrary = class {
4566
4723
  contentThreatBlock(content) {
4567
4724
  return scanContentThreats(content, void 0, this.threatScanOptions());
4568
4725
  }
4726
+ /**
4727
+ * v30 REV-03: read a support file's bytes for staleness anchoring (the
4728
+ * write/remove replay guard). Same validation as writeSupportFile; a
4729
+ * missing file (or a directory squatting on the path) reads as `null`, any
4730
+ * other failure RETHROWS — the callers are the staging/replay anchors, and
4731
+ * a swallowed error would silently downgrade the anchor to
4732
+ * last-writer-wins.
4733
+ */
4734
+ async readSupportFile(name, filePath) {
4735
+ const bad = this.badName(name, { allowReserved: true });
4736
+ if (bad) throw new Error(bad);
4737
+ const validation = validateSupportPath(filePath);
4738
+ if (validation) throw new Error(validation);
4739
+ const target = join(this.dirOf(name), ...filePath.replace(/\\/g, "/").split("/").filter(Boolean));
4740
+ try {
4741
+ return await this.io.readText(target);
4742
+ } catch (error) {
4743
+ const code = error?.code;
4744
+ if (code === "ENOENT" || code === "EISDIR") return null;
4745
+ throw error;
4746
+ }
4747
+ }
4569
4748
  async list() {
4570
4749
  const summaries = [];
4571
4750
  for (const name of await listNames(this.root, this.io)) {
4572
4751
  const dir = this.dirOf(name);
4573
- const md = await this.io.readText(join(dir, "SKILL.md"));
4752
+ let md;
4753
+ try {
4754
+ md = await this.io.readText(join(dir, "SKILL.md"));
4755
+ } catch (error) {
4756
+ if (error?.code === "EISDIR") continue;
4757
+ throw error;
4758
+ }
4574
4759
  if (md === null) continue;
4575
4760
  const parsed = parseFrontmatter(md);
4576
4761
  let entries = null;
@@ -4830,7 +5015,7 @@ var SkillLibrary = class {
4830
5015
  }
4831
5016
  if (!await this.io.exists(join(dir, "SKILL.md"))) {
4832
5017
  await this.io.remove(marker).catch(() => {});
4833
- if ((await this.io.list(dir).catch(() => [])).length === 0) await this.io.remove(dir).catch(() => {});
5018
+ await this.cleanupGhostDir(join(dir, "SKILL.md"));
4834
5019
  return {
4835
5020
  ok: false,
4836
5021
  message: `Skill "${normalized}" was archived concurrently while pinning; the partial marker was removed — retry after the mover settles.`
@@ -4930,15 +5115,22 @@ var SkillLibrary = class {
4930
5115
  message: `Skill "${normalized}" already exists.`
4931
5116
  };
4932
5117
  if (origin !== "foreground") {
4933
- await this.io.writeText(markerPath(dir, "hermes-managed"), "");
5118
+ let markerDurabilityWarning = "";
5119
+ try {
5120
+ await this.io.writeText(markerPath(dir, "hermes-managed"), "");
5121
+ } catch (error) {
5122
+ if (!isCommittedOnly(error)) throw error;
5123
+ markerDurabilityWarning = error instanceof Error ? error.message : String(error);
5124
+ }
4934
5125
  if (!await this.io.exists(createPath)) {
4935
5126
  await this.io.remove(markerPath(dir, "hermes-managed")).catch(() => {});
4936
- if ((await this.io.list(dir).catch(() => [])).length === 0) await this.io.remove(dir).catch(() => {});
5127
+ await this.cleanupGhostDir(createPath);
4937
5128
  return {
4938
5129
  ok: false,
4939
5130
  message: `Skill "${normalized}" was archived concurrently while being created; the partial marker was removed — retry once the mover settles.`
4940
5131
  };
4941
5132
  }
5133
+ if (markerDurabilityWarning !== "") createDurabilityWarning = createDurabilityWarning === "" ? markerDurabilityWarning : `${createDurabilityWarning}; marker: ${markerDurabilityWarning}`;
4942
5134
  }
4943
5135
  await this.audit(normalized, "create", null, onDisk, "created");
4944
5136
  this.notifyMutation({
@@ -5765,7 +5957,7 @@ var SkillLibrary = class {
5765
5957
  if (drift.seen) throw new Error(`concurrent modification detected: ${entry.target} changed after the plan was computed (a concurrent writer won the race); no further writes were performed`);
5766
5958
  } else await this.io.writeText(entry.target, entry.content);
5767
5959
  } catch (error) {
5768
- if (error?.committed !== true) throw error;
5960
+ if (!isCommittedOnly(error)) throw error;
5769
5961
  durabilityWarning = error instanceof Error ? error.message : String(error);
5770
5962
  }
5771
5963
  written.push({
@@ -6071,7 +6263,9 @@ var SkillLibrary = class {
6071
6263
  await this.io.copy(archiveRoot, join(dest, ".archive"));
6072
6264
  hasArchive = true;
6073
6265
  }
6074
- const validExtras = extras.filter((extra) => SNAPSHOT_EXTRA_NAME_RE.test(extra.name));
6266
+ const rejectedExtras = extras.filter((extra) => typeof extra?.name !== "string" || !SNAPSHOT_EXTRA_NAME_RE.test(extra.name)).map((extra) => JSON.stringify(extra?.name ?? extra));
6267
+ if (rejectedExtras.length > 0) throw new Error(`snapshotAll: refusing invalid snapshot extras (name must match ${SNAPSHOT_EXTRA_NAME_RE.source}): ${rejectedExtras.join(", ")}`);
6268
+ const validExtras = extras;
6075
6269
  const extraNames = validExtras.map((extra) => extra.name);
6076
6270
  const extraFailure = (await Promise.allSettled(validExtras.map(async (extra) => {
6077
6271
  await this.io.writeText(join(dest, "extras", extra.name), extra.content);
@@ -6219,7 +6413,15 @@ var SkillLibrary = class {
6219
6413
  ok: false,
6220
6414
  message: "No skill snapshot available."
6221
6415
  };
6222
- const preRollbackPath = await this.snapshotAll("pre-rollback", extras);
6416
+ let preRollbackPath;
6417
+ try {
6418
+ preRollbackPath = await this.snapshotAll("pre-rollback", extras);
6419
+ } catch (error) {
6420
+ return {
6421
+ ok: false,
6422
+ message: `Snapshot restore was refused before anything was cleared: taking the pre-rollback snapshot failed (${error instanceof Error ? error.message : String(error)}) — the active tree is UNCHANGED. Resolve the snapshot write failure and retry.`
6423
+ };
6424
+ }
6223
6425
  const snapshotExtras = await this.readSnapshotExtras(latest.path);
6224
6426
  try {
6225
6427
  await this.restoreSnapshotIntoRoot(latest.path);
@@ -6232,9 +6434,14 @@ var SkillLibrary = class {
6232
6434
  message: `Snapshot restore failed (${reason}); the active tree was rolled back to the pre-rollback snapshot.`
6233
6435
  };
6234
6436
  } catch (rollbackError) {
6437
+ const rb = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
6438
+ if (isPreClearRefusal(reason) && isPreClearRefusal(rb)) return {
6439
+ ok: false,
6440
+ message: `Snapshot restore was refused before anything was cleared; the active tree is UNCHANGED.\n- target: ${reason}\n- pre-rollback: ${rb}\nResolve the cause (a live skill write, or an incomplete snapshot) and retry — no manual rescue is needed.`
6441
+ };
6235
6442
  return {
6236
6443
  ok: false,
6237
- message: `Snapshot restore failed (${reason}) AND pre-rollback restore failed (${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}). Rescue manually from: ${preRollbackPath} (pre-rollback), ${latest.path} (target).`
6444
+ message: `Snapshot restore failed (${reason}) AND pre-rollback restore failed (${rb}). Rescue manually from: ${preRollbackPath} (pre-rollback), ${latest.path} (target).`
6238
6445
  };
6239
6446
  }
6240
6447
  }
@@ -6313,4 +6520,4 @@ var SkillLibrary = class {
6313
6520
  }
6314
6521
  };
6315
6522
  //#endregion
6316
- export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEFAULT_ARCHIVE_AFTER_DAYS, 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_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_MODEL, 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, EMPTY_LOCK_TAKEOVER_MS, 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, LOCK_TEAR_TAKEOVER_MS, LOW_QUALITY_THRESHOLD, LostWriteLock, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_TIMER_DELAY_MS, 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_EXEMPTION_HINT, advanceReview, allowRowCollisions, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, clampedNumber, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, decideTakeover, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionEventPayloadIssue, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterCatalogInvalid, frontmatterYamlUnsafeValues, getRecord, isCommittedWarning, isProcessAlive, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, makeSerialQueue, markAgentCreated, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseLockBody, 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 };
6523
+ export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEFAULT_ARCHIVE_AFTER_DAYS, 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_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_MODEL, 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, EMPTY_LOCK_TAKEOVER_MS, 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, LOCK_TEAR_TAKEOVER_MS, LOW_QUALITY_THRESHOLD, LostWriteLock, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, 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, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, THREAT_EXEMPTION_HINT, advanceReview, allowRowCollisions, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, clampedNumber, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, decideTakeover, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionEventPayloadIssue, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterCatalogInvalid, getRecord, isCommittedWarning, isProcessAlive, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, makeSerialQueue, markAgentCreated, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseLockBody, 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 };
@@ -97,4 +97,8 @@ export declare const MAX_TIMER_DELAY_MS = 2147483647;
97
97
  export declare const DEFAULT_MEMORY_REVIEW_MODEL = "deepseek-v4-flash";
98
98
  export declare const DEFAULT_SKILL_REVIEW_MODEL = "deepseek-v4-pro";
99
99
  export declare const DEFAULT_CURATOR_MODEL = "deepseek-v4-pro";
100
+ /** Order of the `evolution:memory-guidance` section (before the skills one). */
101
+ export declare const MEMORY_GUIDANCE_SECTION_ORDER = 11000;
102
+ /** Order of the `evolution-skills-guidance` section (last of the two). */
103
+ export declare const SKILLS_GUIDANCE_SECTION_ORDER = 11100;
100
104
  //# sourceMappingURL=constants.d.ts.map
@@ -40,7 +40,13 @@ export interface CuratorResult {
40
40
  }
41
41
  export interface CuratorArchivedSkill {
42
42
  name: string;
43
- path: string;
43
+ /** v28 G4.3 (CUR-03): the on-disk archive destination. OMITTED when the run
44
+ * only knows the nominal `.archive/<name>` location — consolidation sources
45
+ * archive through SkillLibrary.archive(), which stamps a
46
+ * `<name>-<stamp>[-<rand>]` suffix on collision. A synthesized path sent
47
+ * operators to a directory that may not exist; the real destination is on
48
+ * the `evolution/skill-mutated` event (`archivedPath`). */
49
+ path?: string;
44
50
  reason: string;
45
51
  }
46
52
  export interface CuratorFailedSkill {
@@ -109,6 +109,13 @@ export declare class MemoryStore {
109
109
  *
110
110
  * @param target - memory target being written
111
111
  * @param core - the in-transaction read-modify-write for the locked body
112
+ * @param shrinkOnly - v29 MEM-02: every queued operation REMOVES an entry
113
+ * (the recovery path for a limit lowered under existing content). The
114
+ * oversized read-guard is skipped for these batches: a canonical file
115
+ * written under a ≥10× higher limit trips the `limit × 10` byte bound, and
116
+ * the guard's "fix the file manually" refusal would pre-empt exactly the
117
+ * shrink recovery MEM-01 advertises. The load is bounded by what the store
118
+ * itself wrote under the old limit.
112
119
  * @returns the core's result, or the oversized / contract-violation refusal
113
120
  */
114
121
  private chainedWrite;
@@ -121,7 +128,7 @@ export declare class MemoryStore {
121
128
  */
122
129
  private addCore;
123
130
  /**
124
- * The single drift predicate. `raw` is in canonical form when it byte-matches
131
+ * The single drift evaluation. `raw` is in canonical form when it byte-matches
125
132
  * `render(normalizeEntries(raw))`; anything else means it was edited outside
126
133
  * MemoryStore (empty/`§`-only entries, stray blank lines, leading or trailing
127
134
  * delimiters — structural anomalies the writer would quietly normalize away).
@@ -135,18 +142,42 @@ export declare class MemoryStore {
135
142
  * every write path — including the repairs the model would need to make.
136
143
  * Such files are adopted instead of flagged.
137
144
  *
145
+ * Returns one of:
146
+ * - `'external'` — non-canonical body, i.e. real external modification. This
147
+ * includes the Hermes-parity signal #2 (an entry larger than the whole-file
148
+ * limit): that shape is only meaningful as external evidence on a
149
+ * NON-canonical body, because free-form external appends never render
150
+ * canonically.
151
+ * - `'over-limit'` — v28 MEM-01: a CANONICAL body whose entries exceed the
152
+ * CURRENT configured limit. Those bytes were written by this store under a
153
+ * previous (higher) limit, so they are not external drift; treating them as
154
+ * such misattributed a config change to an "external editor" and bricked
155
+ * every write path (the advertised recovery — remove/consolidate — is
156
+ * exactly what the drift gate refused). Callers route this state to a
157
+ * config-naming refusal and let shrink-only batches through.
158
+ * - `null` — no drift: writable as-is.
159
+ *
138
160
  * @param target - memory target whose char limit bounds one parsed entry
139
161
  * @param raw - on-disk body, or `null` when the file does not exist
140
- * @returns whether these bytes count as externally drifted
141
162
  */
163
+ private driftKind;
164
+ /** External-drift predicate: canonical-form violations only (see
165
+ * {@link driftKind}). `detectDrift` and the write paths share it, so a write
166
+ * and a later read never disagree about the same bytes. */
142
167
  private drifted;
143
168
  /**
144
169
  * Drift refusal for a body already read under the write lock, or `null` when
145
- * the body is canonical. Both write paths return this unchanged, so their
170
+ * writing may proceed. Both write paths return this unchanged, so their
146
171
  * refusals stay byte-identical and each carries the same backup.
147
172
  *
173
+ * The `'over-limit'` state never refuses shrink-only batches (`shrinkOnly`):
174
+ * removing entries is the advertised recovery for a limit lowered under
175
+ * existing content, and the batch's own final limit check still gates the
176
+ * result.
177
+ *
148
178
  * @param target - memory target that owns the drifted file
149
179
  * @param raw - locked file body
180
+ * @param shrinkOnly - every queued operation removes an entry (no growth)
150
181
  * @returns the refusal to hand back, or `null` to continue writing
151
182
  */
152
183
  private driftRefusal;
@@ -3,11 +3,11 @@
3
3
  * changes semantically: the bundle digest is the fail-closed signal for
4
4
  * review workers, so a stale id across deployments must be distinguishable.
5
5
  */
6
- export declare const PROMPT_BUNDLE_VERSION = 16;
7
- export declare const PROMPT_BUNDLE_ID = "dsh-evolution@16";
6
+ export declare const PROMPT_BUNDLE_VERSION = 17;
7
+ export declare const PROMPT_BUNDLE_ID = "dsh-evolution@17";
8
8
  export declare const MEMORY_REVIEW_PROMPT = "[Auto-review \u2014 Memory]\nReview the conversation above and consider saving to memory if appropriate.\n\nFocus on:\n1. Has the user revealed things about themselves \u2014 persona, desires, preferences, or personal details worth remembering?\n2. Has the user expressed expectations about how you should behave, their work style, or ways they want you to operate?\n\nIf something stands out, save it using the memory tool.\nIf nothing is worth saving, just say \"Nothing to save.\" and stop.";
9
- export declare const SKILL_REVIEW_PROMPT = "[Auto-review \u2014 Skills]\nReview the conversation above and update the skill library. Be ACTIVE \u2014 most sessions produce at least one skill update, even if small. A pass that does nothing is a missed learning opportunity, not a neutral outcome.\n\nTarget shape of the library: CLASS-LEVEL skills, each with a rich SKILL.md and a references/ directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries. This shapes HOW you update, not WHETHER you update.\n\nSignals to look for (any one of these warrants action):\n \u2022 User corrected your style, tone, format, legibility, or verbosity. Frustration signals like 'stop doing X', 'this is too verbose', 'don't format like this', 'why are you explaining', 'just give me the answer', 'you always do Y and I hate it', or an explicit 'remember this' are FIRST-CLASS skill signals, not just memory signals. Update the relevant skill(s) to embed the preference so the next session starts already knowing.\n \u2022 User corrected your workflow, approach, or sequence of steps. Encode the correction as a pitfall or explicit step in the skill that governs that class of task.\n \u2022 Non-trivial technique, fix, workaround, debugging path, or tool-usage pattern emerged that a future session would benefit from. Capture it.\n \u2022 A skill that got loaded or consulted this session turned out to be wrong, missing a step, or outdated. Patch it NOW.\n\nRead-before-write (enforced by this channel): update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session \u2014 ops on unread skills are dropped; CREATE of a brand-new umbrella is the only exception.\n\nPreference order \u2014 prefer the earliest action that fits, but do pick one when a signal above fired:\n 1. UPDATE A CURRENTLY-LOADED SKILL. Look back through the conversation for skills the user loaded or you read. If any of them covers the territory of the new learning, PATCH that one first. It is the skill that was in play, so it's the right one to extend.\n 2. UPDATE AN EXISTING UMBRELLA. If no loaded skill fits but an existing class-level skill does, patch it. Add a subsection, a pitfall, or broaden a trigger.\n 3. ADD A SUPPORT FILE under an existing umbrella. Skills can be packaged with three kinds of support files \u2014 use the right directory per kind:\n \u2022 references/<topic>.md \u2014 session-specific detail (error transcripts, reproduction recipes, provider quirks) AND condensed knowledge banks: quoted research, API docs, external authoritative excerpts, or domain notes you found while working on the problem. Write it concise and for the value of the task, not as a full mirror of upstream docs.\n \u2022 templates/<name>.<ext> \u2014 starter files meant to be copied and modified (boilerplate configs, scaffolding, a known-good example the agent can reproduce with modifications).\n \u2022 scripts/<name>.<ext> \u2014 statically re-runnable actions the skill can invoke directly (verification scripts, fixture generators, deterministic probes, anything the agent should run rather than hand-type each time).\n Add support files via skill_manage action=write_file with file_path starting 'references/', 'templates/', or 'scripts/'. The umbrella's SKILL.md should gain a one-line pointer to any new support file so future agents know it exists.\n 4. RESTRUCTURE a loaded skill whose body grew log-like \u2014 rc/sha/date-dense sections, session-detail spirals, or a fat body with no support files. Use skill_manage action=restructure with restructure: [{\"heading\": \"<the exact ## heading text>\", \"to_file\": \"references/<topic>.md\"}] \u2014 the ENTIRE ## section (from that heading to the next heading) moves into the support file and its position becomes a pointer line. The skill's name and directory never change. Only propose headings that exist verbatim in the body; never invent one, and never restructure a healthy small skill.\n 5. CREATE A NEW CLASS-LEVEL UMBRELLA SKILL when no existing skill covers the class. The name MUST be at the class level. The name MUST NOT be a specific PR number, error string, feature codename, library-alone name, or 'fix-X / debug-Y / audit-Z-today' session artifact. If the proposed name only makes sense for today's task, it's wrong \u2014 fall back to (1), (2), or (3).\n\nUser-preference embedding (important): when the user expressed a style/format/workflow preference, the update belongs in the SKILL.md body, not just in memory. Memory captures 'who the user is and what the current situation and state of your operations are'; skills capture 'how to do this class of task for this user'. When they complain about how you handled a task, the skill that governs that task needs to carry the lesson.\n\nIf you notice two existing skills that overlap, note it in your reply \u2014 the background curator handles consolidation at scale.\n\nTwo-tier deposition discipline (DSH addition, same spirit as the umbrella rule): before writing, classify the knowledge:\n \u2022 PATTERN (reusable \u2014 symptom \u2192 mechanism \u2192 fix \u2192 verification, still valuable next session) belongs in the SKILL.md body.\n \u2022 LOG (one-off \u2014 commit SHAs, npm/profile states, what this release changed, this session's process narrative) belongs in a references/ file, never the body. Body density IS reuse rate. Keep new entries tight: a pattern fits in 2-8 physical lines; prefer changing the current-state pointer over appending history.\n\nProtected skills (DO NOT edit these):\n \u2022 Bundled skills (shipped with the platform).\n \u2022 Hub-installed skills (installed from a hub).\nPinned skills are read-only to THIS background review pass \u2014 the pinned write guard refuses background changes, so this pass may not update them. They also cannot be archived by any writer (the foreground included): remove the .pinned marker first. Foreground and delegated-subagent update/patch writes to pinned skills remain allowed.\nIf the only skills that need updating are protected, say 'Nothing to save.' and stop.\n\nDo NOT capture (these become persistent self-imposed constraints that bite you later when the environment changes):\n \u2022 Environment-dependent failures: missing binaries, fresh-install errors, post-migration path mismatches, 'command not found', unconfigured credentials, uninstalled packages. The user can fix these \u2014 they are not durable rules.\n \u2022 Negative claims about tools or features ('browser tools do not work', 'X tool is broken', 'cannot use Y'). These harden into refusals the agent cites against itself for months after the actual problem was fixed.\n \u2022 Session-specific transient errors that resolved before the conversation ended. If retrying worked, the lesson is the retry pattern, not the original failure.\n \u2022 One-off task narratives. A user asking 'summarize today's market' or 'analyze this PR' is not a class of work that warrants a skill.\n\nIf a tool failed because of setup state, capture the FIX (install command, config step, env var to set) under an existing setup or troubleshooting skill \u2014 never 'this tool does not work' as a standalone constraint.\n\n'Nothing to save.' is a real option but should NOT be the default. If the session ran smoothly with no corrections and produced no new technique, just say 'Nothing to save.' and stop. Otherwise, act.";
10
- export declare const COMBINED_REVIEW_PROMPT = "[Auto-review]\nReview the conversation above and update two things:\n\n**Memory**: who the user is. Did the user reveal persona, desires, preferences, personal details, or expectations about how you should behave? Save facts about the user and durable preferences with the memory tool.\n\n**Skills**: how to do this class of task. Be ACTIVE \u2014 most sessions produce at least one skill update. A pass that does nothing is a missed learning opportunity, not a neutral outcome.\n\nTarget shape of the skill library: CLASS-LEVEL skills with a rich SKILL.md and a references/ directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries.\n\nSignals that warrant a skill update (any one is enough):\n \u2022 User corrected your style, tone, format, legibility, verbosity, or approach. Frustration is a FIRST-CLASS skill signal, not just a memory signal. 'stop doing X', 'don't format like this', 'I hate when you Y' \u2014 embed the lesson in the skill that governs that task so the next session starts fixed.\n \u2022 Non-trivial technique, fix, workaround, or debugging path emerged.\n \u2022 A skill that was loaded or consulted turned out wrong, missing, or outdated \u2014 patch it now.\n\nRead-before-write (enforced by this channel): update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session \u2014 ops on unread skills are dropped; CREATE of a brand-new umbrella is the only exception.\n\nPreference order for skills \u2014 pick the earliest that fits:\n 1. UPDATE A CURRENTLY-LOADED SKILL. Check what skills were loaded or read in the conversation. If one of them covers the learning, PATCH it first. It was in play; it's the right place.\n 2. UPDATE AN EXISTING UMBRELLA. Patch it.\n 3. ADD A SUPPORT FILE under an existing umbrella via skill_manage action=write_file. Three kinds: references/<topic>.md for session-specific detail OR condensed knowledge banks (quoted research, API docs excerpts, domain notes) written concise and task-focused; templates/<name>.<ext> for starter files meant to be copied and modified; scripts/<name>.<ext> for statically re-runnable actions (verification, fixture generators, probes). Add a one-line pointer in SKILL.md so future agents find them.\n 4. RESTRUCTURE a loaded skill whose body grew log-like (rc/sha/date-dense sections, session-detail spirals, fat body with no support files) via skill_manage action=restructure with restructure: [{\"heading\": \"<the exact ## heading text>\", \"to_file\": \"references/<topic>.md\"}] \u2014 the ENTIRE ## section moves into the support file and its position becomes a pointer line; the skill's name and directory never change. Only propose headings that exist verbatim in the body.\n 5. CREATE A NEW CLASS-LEVEL UMBRELLA when nothing exists. Name at the class level \u2014 NOT a PR number, error string, codename, library-alone name, or 'fix-X / debug-Y' session artifact. If the name only fits today's task, fall back to (1), (2), or (3).\n\nTwo-tier deposition discipline (DSH addition): classify before writing \u2014 PATTERN (symptom \u2192 mechanism \u2192 fix \u2192 verification) goes in the SKILL.md body; LOG (commit SHAs, npm/profile states, this release's change list, this session's narrative) goes in a references/ file. Body density IS reuse rate; a pattern fits in 2-8 physical lines.\n\nUser-preference embedding: when the user complains about how you handled a task, update the skill that governs that task \u2014 memory alone isn't enough. Memory says 'who the user is and what the current situation and state of your operations are'; skills say 'how to do this class of task for this user'. Both should carry user-preference lessons when relevant.\n\nIf you notice overlapping existing skills, mention it \u2014 the background curator handles consolidation.\n\nProtected skills (DO NOT edit these):\n \u2022 Bundled skills (shipped with the platform).\n \u2022 Hub-installed skills (installed from a hub).\nPinned skills are read-only to THIS background review pass \u2014 the pinned write guard refuses background changes, so this pass may not update them. They also cannot be archived by any writer (the foreground included): remove the .pinned marker first. Foreground and delegated-subagent update/patch writes to pinned skills remain allowed.\nIf the only skills that need updating are protected, say 'Nothing to save.' and stop.\n\nDo NOT capture as skills (these become persistent self-imposed constraints that bite you later when the environment changes):\n \u2022 Environment-dependent failures: missing binaries, fresh-install errors, post-migration path mismatches, 'command not found', unconfigured credentials, uninstalled packages. The user can fix these \u2014 they are not durable rules.\n \u2022 Negative claims about tools or features ('browser tools do not work', 'X tool is broken', 'cannot use Y'). These harden into refusals the agent cites against itself for months after the actual problem was fixed.\n \u2022 Session-specific transient errors that resolved before the conversation ended. If retrying worked, the lesson is the retry pattern, not the original failure.\n \u2022 One-off task narratives. A user asking 'summarize today's market' or 'analyze this PR' is not a class of work that warrants a skill.\n\nIf a tool failed because of setup state, capture the FIX (install command, config step, env var to set) under an existing setup or troubleshooting skill \u2014 never 'this tool does not work' as a standalone constraint.\n\nAct on whichever of the two dimensions has real signal. If genuinely nothing stands out on either, say 'Nothing to save.' and stop \u2014 but don't reach for that conclusion as a default.";
9
+ export declare const SKILL_REVIEW_PROMPT = "[Auto-review \u2014 Skills]\nReview the conversation above and update the skill library. Be ACTIVE \u2014 most sessions produce at least one skill update, even if small. A pass that does nothing is a missed learning opportunity, not a neutral outcome.\n\nTarget shape of the library: CLASS-LEVEL skills, each with a rich SKILL.md and a references/ directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries. This shapes HOW you update, not WHETHER you update.\n\nSignals to look for (any one of these warrants action):\n \u2022 User corrected your style, tone, format, legibility, or verbosity. Frustration signals like 'stop doing X', 'this is too verbose', 'don't format like this', 'why are you explaining', 'just give me the answer', 'you always do Y and I hate it', or an explicit 'remember this' are FIRST-CLASS skill signals, not just memory signals. Update the relevant skill(s) to embed the preference so the next session starts already knowing.\n \u2022 User corrected your workflow, approach, or sequence of steps. Encode the correction as a pitfall or explicit step in the skill that governs that class of task.\n \u2022 Non-trivial technique, fix, workaround, debugging path, or tool-usage pattern emerged that a future session would benefit from. Capture it.\n \u2022 A skill that got loaded or consulted this session turned out to be wrong, missing a step, or outdated. Patch it NOW.\n\nRead-before-write: update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session. On the plan channel ops on unread skills are rejected; direct writes have no such guard, so treat the rule as binding. CREATE of a brand-new umbrella is the only exception.\n\nPreference order \u2014 prefer the earliest action that fits, but do pick one when a signal above fired:\n 1. UPDATE A CURRENTLY-LOADED SKILL. Look back through the conversation for skills the user loaded or you read. If any of them covers the territory of the new learning, PATCH that one first. It is the skill that was in play, so it's the right one to extend.\n 2. UPDATE AN EXISTING UMBRELLA. If no loaded skill fits but an existing class-level skill does, patch it. Add a subsection, a pitfall, or broaden a trigger.\n 3. ADD A SUPPORT FILE under an existing umbrella. Skills can be packaged with three kinds of support files \u2014 use the right directory per kind:\n \u2022 references/<topic>.md \u2014 session-specific detail (error transcripts, reproduction recipes, provider quirks) AND condensed knowledge banks: quoted research, API docs, external authoritative excerpts, or domain notes you found while working on the problem. Write it concise and for the value of the task, not as a full mirror of upstream docs.\n \u2022 templates/<name>.<ext> \u2014 starter files meant to be copied and modified (boilerplate configs, scaffolding, a known-good example the agent can reproduce with modifications).\n \u2022 scripts/<name>.<ext> \u2014 statically re-runnable actions the skill can invoke directly (verification scripts, fixture generators, deterministic probes, anything the agent should run rather than hand-type each time).\n Add support files via skill_manage action=write_file with file_path starting 'references/', 'templates/', or 'scripts/'. The umbrella's SKILL.md should gain a one-line pointer to any new support file so future agents know it exists.\n 4. RESTRUCTURE a loaded skill whose body grew log-like \u2014 rc/sha/date-dense sections, session-detail spirals, or a fat body with no support files. Use skill_manage action=restructure with restructure: [{\"heading\": \"<the exact ## heading text>\", \"to_file\": \"references/<topic>.md\"}] \u2014 the ENTIRE ## section (from that heading to the next heading) moves into the support file and its position becomes a pointer line. The skill's name and directory never change. Only propose headings that exist verbatim in the body; never invent one, and never restructure a healthy small skill.\n 5. CREATE A NEW CLASS-LEVEL UMBRELLA SKILL when no existing skill covers the class. The name MUST be at the class level. The name MUST NOT be a specific PR number, error string, feature codename, library-alone name, or 'fix-X / debug-Y / audit-Z-today' session artifact. If the proposed name only makes sense for today's task, it's wrong \u2014 fall back to (1), (2), or (3).\n\nUser-preference embedding (important): when the user expressed a style/format/workflow preference, the update belongs in the SKILL.md body, not just in memory. Memory captures 'who the user is and what the current situation and state of your operations are'; skills capture 'how to do this class of task for this user'. When they complain about how you handled a task, the skill that governs that task needs to carry the lesson.\n\nIf you notice two existing skills that overlap, note it in your reply \u2014 the background curator handles consolidation at scale.\n\nTwo-tier deposition discipline (DSH addition, same spirit as the umbrella rule): before writing, classify the knowledge:\n \u2022 PATTERN (reusable \u2014 symptom \u2192 mechanism \u2192 fix \u2192 verification, still valuable next session) belongs in the SKILL.md body.\n \u2022 LOG (one-off \u2014 commit SHAs, npm/profile states, what this release changed, this session's process narrative) belongs in a references/ file, never the body. Body density IS reuse rate. Keep new entries tight: a pattern fits in 2-8 physical lines; prefer changing the current-state pointer over appending history.\n\nProtected skills (DO NOT edit these):\n \u2022 Bundled skills (shipped with the platform).\n \u2022 Hub-installed skills (installed from a hub).\nPinned skills are read-only to THIS background review pass \u2014 the pinned write guard refuses background changes, so this pass may not update them. They also cannot be archived by any writer (the foreground included): remove the .pinned marker first. Foreground and delegated-subagent update/patch writes to pinned skills remain allowed.\nIf the only skills that need updating are protected, say 'Nothing to save.' and stop.\n\nDo NOT capture (these become persistent self-imposed constraints that bite you later when the environment changes):\n \u2022 Environment-dependent failures: missing binaries, fresh-install errors, post-migration path mismatches, 'command not found', unconfigured credentials, uninstalled packages. The user can fix these \u2014 they are not durable rules.\n \u2022 Negative claims about tools or features ('browser tools do not work', 'X tool is broken', 'cannot use Y'). These harden into refusals the agent cites against itself for months after the actual problem was fixed.\n \u2022 Session-specific transient errors that resolved before the conversation ended. If retrying worked, the lesson is the retry pattern, not the original failure.\n \u2022 One-off task narratives. A user asking 'summarize today's market' or 'analyze this PR' is not a class of work that warrants a skill.\n\nIf a tool failed because of setup state, capture the FIX (install command, config step, env var to set) under an existing setup or troubleshooting skill \u2014 never 'this tool does not work' as a standalone constraint.\n\n'Nothing to save.' is a real option but should NOT be the default. If the session ran smoothly with no corrections and produced no new technique, just say 'Nothing to save.' and stop. Otherwise, act.";
10
+ export declare const COMBINED_REVIEW_PROMPT = "[Auto-review]\nReview the conversation above and update two things:\n\n**Memory**: who the user is. Did the user reveal persona, desires, preferences, personal details, or expectations about how you should behave? Save facts about the user and durable preferences with the memory tool.\n\n**Skills**: how to do this class of task. Be ACTIVE \u2014 most sessions produce at least one skill update. A pass that does nothing is a missed learning opportunity, not a neutral outcome.\n\nTarget shape of the skill library: CLASS-LEVEL skills with a rich SKILL.md and a references/ directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries.\n\nSignals that warrant a skill update (any one is enough):\n \u2022 User corrected your style, tone, format, legibility, verbosity, or approach. Frustration is a FIRST-CLASS skill signal, not just a memory signal. 'stop doing X', 'don't format like this', 'I hate when you Y' \u2014 embed the lesson in the skill that governs that task so the next session starts fixed.\n \u2022 Non-trivial technique, fix, workaround, or debugging path emerged.\n \u2022 A skill that was loaded or consulted turned out wrong, missing, or outdated \u2014 patch it now.\n\nRead-before-write: update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session. On the plan channel ops on unread skills are rejected; direct writes have no such guard, so treat the rule as binding. CREATE of a brand-new umbrella is the only exception.\n\nPreference order for skills \u2014 pick the earliest that fits:\n 1. UPDATE A CURRENTLY-LOADED SKILL. Check what skills were loaded or read in the conversation. If one of them covers the learning, PATCH it first. It was in play; it's the right place.\n 2. UPDATE AN EXISTING UMBRELLA. Patch it.\n 3. ADD A SUPPORT FILE under an existing umbrella via skill_manage action=write_file. Three kinds: references/<topic>.md for session-specific detail OR condensed knowledge banks (quoted research, API docs excerpts, domain notes) written concise and task-focused; templates/<name>.<ext> for starter files meant to be copied and modified; scripts/<name>.<ext> for statically re-runnable actions (verification, fixture generators, probes). Add a one-line pointer in SKILL.md so future agents find them.\n 4. RESTRUCTURE a loaded skill whose body grew log-like (rc/sha/date-dense sections, session-detail spirals, fat body with no support files) via skill_manage action=restructure with restructure: [{\"heading\": \"<the exact ## heading text>\", \"to_file\": \"references/<topic>.md\"}] \u2014 the ENTIRE ## section moves into the support file and its position becomes a pointer line; the skill's name and directory never change. Only propose headings that exist verbatim in the body.\n 5. CREATE A NEW CLASS-LEVEL UMBRELLA when nothing exists. Name at the class level \u2014 NOT a PR number, error string, codename, library-alone name, or 'fix-X / debug-Y' session artifact. If the name only fits today's task, fall back to (1), (2), or (3).\n\nTwo-tier deposition discipline (DSH addition): classify before writing \u2014 PATTERN (symptom \u2192 mechanism \u2192 fix \u2192 verification) goes in the SKILL.md body; LOG (commit SHAs, npm/profile states, this release's change list, this session's narrative) goes in a references/ file. Body density IS reuse rate; a pattern fits in 2-8 physical lines.\n\nUser-preference embedding: when the user complains about how you handled a task, update the skill that governs that task \u2014 memory alone isn't enough. Memory says 'who the user is and what the current situation and state of your operations are'; skills say 'how to do this class of task for this user'. Both should carry user-preference lessons when relevant.\n\nIf you notice overlapping existing skills, mention it \u2014 the background curator handles consolidation.\n\nProtected skills (DO NOT edit these):\n \u2022 Bundled skills (shipped with the platform).\n \u2022 Hub-installed skills (installed from a hub).\nPinned skills are read-only to THIS background review pass \u2014 the pinned write guard refuses background changes, so this pass may not update them. They also cannot be archived by any writer (the foreground included): remove the .pinned marker first. Foreground and delegated-subagent update/patch writes to pinned skills remain allowed.\nIf the only skills that need updating are protected, say 'Nothing to save.' and stop.\n\nDo NOT capture as skills (these become persistent self-imposed constraints that bite you later when the environment changes):\n \u2022 Environment-dependent failures: missing binaries, fresh-install errors, post-migration path mismatches, 'command not found', unconfigured credentials, uninstalled packages. The user can fix these \u2014 they are not durable rules.\n \u2022 Negative claims about tools or features ('browser tools do not work', 'X tool is broken', 'cannot use Y'). These harden into refusals the agent cites against itself for months after the actual problem was fixed.\n \u2022 Session-specific transient errors that resolved before the conversation ended. If retrying worked, the lesson is the retry pattern, not the original failure.\n \u2022 One-off task narratives. A user asking 'summarize today's market' or 'analyze this PR' is not a class of work that warrants a skill.\n\nIf a tool failed because of setup state, capture the FIX (install command, config step, env var to set) under an existing setup or troubleshooting skill \u2014 never 'this tool does not work' as a standalone constraint.\n\nAct on whichever of the two dimensions has real signal. If genuinely nothing stands out on either, say 'Nothing to save.' and stop \u2014 but don't reach for that conclusion as a default.";
11
11
  export declare const CURATOR_PROMPT = "You are the skill curator. Maintain a healthy, class-level skill library, not a flat pile of narrow one-session skills.\n\nThis is an UMBRELLA-BUILDING consolidation pass, not a passive audit and not a duplicate-finder.\n\nThe goal is a LIBRARY OF CLASS-LEVEL INSTRUCTIONS. A skill collection of many narrow skills where each captures one session's specific bug is a FAILURE of the library. An agent searching skills matches on descriptions, not exact names; one broad umbrella with labeled subsections beats five narrow siblings for discoverability.\n\nRight target shape: class-level skills with rich SKILL.md + references/, templates/, scripts/ support files for session-specific detail.\n\nHard rules:\n1. NEVER hard-delete a skill. Archive (moving to .archive/) is the maximum destructive action; archives are recoverable, deletion is not.\n2. Do not touch bundled, hub-installed, pinned, or scheduled-task-referenced (referenced) skills. Referenced skills are fully protected \u2014 never consolidated, never pruned (there is no scheduled-task reference-rewriting pass; a referenced skill stays in place by design).\n3. Do not archive recently-created or never-used skills without strong evidence. \"use=0\" is NOT evidence either way \u2014 it only means the trigger has not come up yet. Never archive a never-used skill unless it is at least 30 days old AND its content is genuinely obsolete or fully absorbed elsewhere.\n4. Do NOT reject consolidation on the grounds that \"each skill has a distinct trigger\". The right bar is: would a human maintainer write this as N separate skills, or one skill with N labeled subsections? When the answer is the latter, merge.\n5. Judge overlap on CONTENT, not on usage counters.\n6. Before archiving a merged skill, ensure its unique content was preserved in the umbrella.\n\nHow to work:\n1. Scan the candidate list. Identify PREFIX CLUSTERS \u2014 skills sharing a first word or domain keyword. Expected cluster count scales with the library: a large collection may show 10-25 prefix clusters, a small one often has none \u2014 a clean \"nothing to consolidate\" summary is the correct small-library outcome, not a shortage of ambition.\n2. For each cluster with 2+ members, ask \"what is the UMBRELLA CLASS these skills serve?\" and consolidate:\n a. MERGE INTO AN EXISTING UMBRELLA (patch a labeled section for each sibling's unique insight, then archive the siblings).\n b. CREATE A NEW UMBRELLA SKILL.md covering the shared workflow with short labeled subsections, then archive the absorbed siblings.\n c. DEMOTE session-specific detail to references/, templates/, or scripts/ under the umbrella. Use the right directory per kind:\n \u2022 references/<topic>.md \u2014 session-specific detail OR condensed knowledge banks (quoted research, API docs excerpts, domain notes, provider quirks, reproduction recipes) written concise and task-focused.\n \u2022 templates/<name>.<ext> \u2014 starter files meant to be copied and modified.\n \u2022 scripts/<name>.<ext> \u2014 statically re-runnable actions (verification scripts, fixture generators, probes).\n3. Package integrity \u2014 not optional: inspect each skill as a COMPLETE directory package, not just SKILL.md. A skill root may include references/, templates/, scripts/, and assets/. If the source skill has support files OR its SKILL.md contains relative links to them, DO NOT flatten only SKILL.md into <umbrella>/references/<old>.md. Choose one safe path instead: keep it as a standalone skill, OR fully merge by re-homing every needed support file into the umbrella's canonical directories AND rewriting the destination instructions to the new paths, OR archive the entire original skill package unchanged. Never leave demoted instructions pointing at files left behind under the old skill directory.\n4. Flag skills whose NAME is too narrow (contains a PR number, a feature codename, a specific error string, an 'audit'/'diagnosis'/'salvage' session artifact) \u2014 they almost always belong as a subsection or support file under a class-level umbrella.\n5. Iterate. After one consolidation round, scan the remaining set and look for the NEXT umbrella opportunity. Don't stop after 3 merges.\n\nYou are a NOMINATOR, not an executor: this channel has NO tools. Your single deliverable is the structured YAML block below. Never narrate actions you did not take (\"merged\", \"patched\", \"archived\") \u2014 you are proposing, and the deterministic engine executes only names from the candidate pool it gave you. (A future execution view would expose skill_manage; today it does not.)\n\n'keep' is a legitimate decision ONLY when the skill is already a class-level umbrella and none of the proposed merges would improve discoverability. 'This is narrow but distinct from its siblings' is NOT a reason to keep \u2014 it's a reason to move it under an umbrella as a subsection or support file.\n\nExpected output: real umbrella-ification. Process every obvious cluster. If you end the pass with obvious clusters still untouched, you stopped too early \u2014 go back and look at the clusters you left alone.\n\nKeep the umbrella body tight and scannable: exact commands, verbatim paths, ~100-200 lines; never invent flags or APIs.\n\nWhen done, write a human summary THEN the structured machine-readable block. The block is the contract: every skill you would move to .archive/ MUST appear in exactly one of the two lists. Return ONLY the YAML block after the summary \u2014 no post-block prose. Format EXACTLY:\n\n## Structured summary (required)\n```yaml\nconsolidations:\n - from: <old-skill-name>\n mode: reference # optional \u2014 ONLY for a 'demote': source is narrow-but-valuable session detail, write it as references/<source>.md under the umbrella instead of appending to the body. Default is append. Place this line BEFORE into:. NEVER use reference when the source body links its own references/ templates/ scripts/ files.\n into: <umbrella-skill-name>\n reason: <one short sentence \u2014 why merged, not just 'similar'>\nprunings:\n - name: <skill-name>\n reason: <one short sentence \u2014 why archived with no merge target>\n```\n\nEvery skill you would move to .archive/ MUST appear in exactly one of the two lists. If you consolidated X into umbrella Y (patched Y, wrote a references file to Y, or created Y with X's content absorbed), X goes under consolidations with into: Y. If you archived X with no absorption \u2014 truly stale, irrelevant, or obsolete \u2014 X goes under prunings. Leave a list empty (consolidations: []) if none. Do not omit the block. The block comes AFTER your human-readable summary of clusters processed, patches made, and decisions left alone.";
12
12
  export declare const CURATOR_DRY_RUN_BANNER = "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nDRY-RUN \u2014 REPORT ONLY. DO NOT MUTATE THE SKILL LIBRARY.\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nThis is a PREVIEW pass. Follow every instruction above EXCEPT:\n \u2022 Do NOT call skill_manage with action=create, update, patch, delete, write_file, or remove_file.\n \u2022 Do NOT move, copy, or rewrite any file under the skills tree.\n\nYour output IS the deliverable: produce the exact same human-readable summary and YAML block you would on a live run, describing the actions you WOULD take. A reviewer will decide whether to approve a live run.\n\nIf you accidentally take a mutating action, say so explicitly in the summary.";
13
13
  export declare const COMPLETION_SKILL_REVIEW_PROMPT = "[Auto-review \u2014 Skills \u00B7 task complete]\nYour current task now appears complete. Before wrapping up, review the approach and update the skill library via skill_manage.\n\nFollow the skills review policy: be ACTIVE, prefer class-level umbrellas, patch ONLY skills loaded or read this session, and capture non-trivial techniques and user corrections. Do NOT capture environment-dependent failures, negative claims about tools, or one-off task narratives.\n\nDo NOT modify output files or re-run the task. If you are still mid-task, ignore this.";
@@ -40,9 +40,9 @@ export declare const MAINTAIN_OUTPUT_INSTRUCTION = "\u6309\u6A21\u677F\u5951\u7E
40
40
  */
41
41
  export declare const SKILLS_GUIDANCE = "Skills guidance:\n\u2022 After completing a complex task (5+ tool calls), fixing a tricky error, or discovering a non-trivial workflow, save the approach as a skill with skill_manage so you can reuse it next time.\n\u2022 When using a skill and finding it outdated, incomplete, or wrong, patch it immediately with skill_manage (action='patch') \u2014 don't wait to be asked. Skills that aren't maintained become liabilities.";
42
42
  /** Subagent-channel variant: same review policy, channel-limited deliverable (M-2). */
43
- export declare const SKILL_REVIEW_PLAN_PROMPT = "[Auto-review \u2014 Skills]\nReview the conversation above and update the skill library. Be ACTIVE \u2014 most sessions produce at least one skill update, even if small. A pass that does nothing is a missed learning opportunity, not a neutral outcome.\n\nTarget shape of the library: CLASS-LEVEL skills, each with a rich SKILL.md and a references/ directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries. This shapes HOW you update, not WHETHER you update.\n\nSignals to look for (any one of these warrants action):\n \u2022 User corrected your style, tone, format, legibility, or verbosity. Frustration signals like 'stop doing X', 'this is too verbose', 'don't format like this', 'why are you explaining', 'just give me the answer', 'you always do Y and I hate it', or an explicit 'remember this' are FIRST-CLASS skill signals, not just memory signals. Update the relevant skill(s) to embed the preference so the next session starts already knowing.\n \u2022 User corrected your workflow, approach, or sequence of steps. Encode the correction as a pitfall or explicit step in the skill that governs that class of task.\n \u2022 Non-trivial technique, fix, workaround, debugging path, or tool-usage pattern emerged that a future session would benefit from. Capture it.\n \u2022 A skill that got loaded or consulted this session turned out to be wrong, missing a step, or outdated. Patch it NOW.\n\nRead-before-write (enforced by this channel): update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session \u2014 ops on unread skills are dropped; CREATE of a brand-new umbrella is the only exception.\n\nPreference order \u2014 prefer the earliest action that fits, but do pick one when a signal above fired:\n 1. UPDATE A CURRENTLY-LOADED SKILL. Look back through the conversation for skills the user loaded or you read. If any of them covers the territory of the new learning, PATCH that one first. It is the skill that was in play, so it's the right one to extend.\n 2. UPDATE AN EXISTING UMBRELLA. If no loaded skill fits but an existing class-level skill does, patch it. Add a subsection, a pitfall, or broaden a trigger.\n 3. ADD A SUPPORT FILE under an existing umbrella. Skills can be packaged with three kinds of support files \u2014 use the right directory per kind:\n \u2022 references/<topic>.md \u2014 session-specific detail (error transcripts, reproduction recipes, provider quirks) AND condensed knowledge banks: quoted research, API docs, external authoritative excerpts, or domain notes you found while working on the problem. Write it concise and for the value of the task, not as a full mirror of upstream docs.\n \u2022 templates/<name>.<ext> \u2014 starter files meant to be copied and modified (boilerplate configs, scaffolding, a known-good example the agent can reproduce with modifications).\n \u2022 scripts/<name>.<ext> \u2014 statically re-runnable actions the skill can invoke directly (verification scripts, fixture generators, deterministic probes, anything the agent should run rather than hand-type each time).\n Add support files via skill_manage action=write_file with file_path starting 'references/', 'templates/', or 'scripts/'. The umbrella's SKILL.md should gain a one-line pointer to any new support file so future agents know it exists.\n 4. RESTRUCTURE a loaded skill whose body grew log-like \u2014 rc/sha/date-dense sections, session-detail spirals, or a fat body with no support files. Use skill_manage action=restructure with restructure: [{\"heading\": \"<the exact ## heading text>\", \"to_file\": \"references/<topic>.md\"}] \u2014 the ENTIRE ## section (from that heading to the next heading) moves into the support file and its position becomes a pointer line. The skill's name and directory never change. Only propose headings that exist verbatim in the body; never invent one, and never restructure a healthy small skill.\n 5. CREATE A NEW CLASS-LEVEL UMBRELLA SKILL when no existing skill covers the class. The name MUST be at the class level. The name MUST NOT be a specific PR number, error string, feature codename, library-alone name, or 'fix-X / debug-Y / audit-Z-today' session artifact. If the proposed name only makes sense for today's task, it's wrong \u2014 fall back to (1), (2), or (3).\n\nUser-preference embedding (important): when the user expressed a style/format/workflow preference, the update belongs in the SKILL.md body, not just in memory. Memory captures 'who the user is and what the current situation and state of your operations are'; skills capture 'how to do this class of task for this user'. When they complain about how you handled a task, the skill that governs that task needs to carry the lesson.\n\nIf you notice two existing skills that overlap, note it in your reply \u2014 the background curator handles consolidation at scale.\n\nTwo-tier deposition discipline (DSH addition, same spirit as the umbrella rule): before writing, classify the knowledge:\n \u2022 PATTERN (reusable \u2014 symptom \u2192 mechanism \u2192 fix \u2192 verification, still valuable next session) belongs in the SKILL.md body.\n \u2022 LOG (one-off \u2014 commit SHAs, npm/profile states, what this release changed, this session's process narrative) belongs in a references/ file, never the body. Body density IS reuse rate. Keep new entries tight: a pattern fits in 2-8 physical lines; prefer changing the current-state pointer over appending history.\n\nProtected skills (DO NOT edit these):\n \u2022 Bundled skills (shipped with the platform).\n \u2022 Hub-installed skills (installed from a hub).\nPinned skills are read-only to THIS background review pass \u2014 the pinned write guard refuses background changes, so this pass may not update them. They also cannot be archived by any writer (the foreground included): remove the .pinned marker first. Foreground and delegated-subagent update/patch writes to pinned skills remain allowed.\nIf the only skills that need updating are protected, say 'Nothing to save.' and stop.\n\nDo NOT capture (these become persistent self-imposed constraints that bite you later when the environment changes):\n \u2022 Environment-dependent failures: missing binaries, fresh-install errors, post-migration path mismatches, 'command not found', unconfigured credentials, uninstalled packages. The user can fix these \u2014 they are not durable rules.\n \u2022 Negative claims about tools or features ('browser tools do not work', 'X tool is broken', 'cannot use Y'). These harden into refusals the agent cites against itself for months after the actual problem was fixed.\n \u2022 Session-specific transient errors that resolved before the conversation ended. If retrying worked, the lesson is the retry pattern, not the original failure.\n \u2022 One-off task narratives. A user asking 'summarize today's market' or 'analyze this PR' is not a class of work that warrants a skill.\n\nIf a tool failed because of setup state, capture the FIX (install command, config step, env var to set) under an existing setup or troubleshooting skill \u2014 never 'this tool does not work' as a standalone constraint.\n\n'Nothing to save.' is a real option but should NOT be the default. If the session ran smoothly with no corrections and produced no new technique, just say 'Nothing to save.' and stop. Otherwise, act.\n\nCHANNEL (subagent): this review channel mounts only the read-only `skill` tool \u2014 you have NO `skill_manage`, NO `memory`. Your deliverable is the structured JSON plan below (outputSchema). Describe the patches/creates you RECOMMEND in the plan; never narrate actions you took.";
43
+ export declare const SKILL_REVIEW_PLAN_PROMPT = "[Auto-review \u2014 Skills]\nReview the conversation above and update the skill library. Be ACTIVE \u2014 most sessions produce at least one skill update, even if small. A pass that does nothing is a missed learning opportunity, not a neutral outcome.\n\nTarget shape of the library: CLASS-LEVEL skills, each with a rich SKILL.md and a references/ directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries. This shapes HOW you update, not WHETHER you update.\n\nSignals to look for (any one of these warrants action):\n \u2022 User corrected your style, tone, format, legibility, or verbosity. Frustration signals like 'stop doing X', 'this is too verbose', 'don't format like this', 'why are you explaining', 'just give me the answer', 'you always do Y and I hate it', or an explicit 'remember this' are FIRST-CLASS skill signals, not just memory signals. Update the relevant skill(s) to embed the preference so the next session starts already knowing.\n \u2022 User corrected your workflow, approach, or sequence of steps. Encode the correction as a pitfall or explicit step in the skill that governs that class of task.\n \u2022 Non-trivial technique, fix, workaround, debugging path, or tool-usage pattern emerged that a future session would benefit from. Capture it.\n \u2022 A skill that got loaded or consulted this session turned out to be wrong, missing a step, or outdated. Patch it NOW.\n\nRead-before-write: update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session. On the plan channel ops on unread skills are rejected; direct writes have no such guard, so treat the rule as binding. CREATE of a brand-new umbrella is the only exception.\n\nPreference order \u2014 prefer the earliest action that fits, but do pick one when a signal above fired:\n 1. UPDATE A CURRENTLY-LOADED SKILL. Look back through the conversation for skills the user loaded or you read. If any of them covers the territory of the new learning, PATCH that one first. It is the skill that was in play, so it's the right one to extend.\n 2. UPDATE AN EXISTING UMBRELLA. If no loaded skill fits but an existing class-level skill does, patch it. Add a subsection, a pitfall, or broaden a trigger.\n 3. ADD A SUPPORT FILE under an existing umbrella. Skills can be packaged with three kinds of support files \u2014 use the right directory per kind:\n \u2022 references/<topic>.md \u2014 session-specific detail (error transcripts, reproduction recipes, provider quirks) AND condensed knowledge banks: quoted research, API docs, external authoritative excerpts, or domain notes you found while working on the problem. Write it concise and for the value of the task, not as a full mirror of upstream docs.\n \u2022 templates/<name>.<ext> \u2014 starter files meant to be copied and modified (boilerplate configs, scaffolding, a known-good example the agent can reproduce with modifications).\n \u2022 scripts/<name>.<ext> \u2014 statically re-runnable actions the skill can invoke directly (verification scripts, fixture generators, deterministic probes, anything the agent should run rather than hand-type each time).\n Add support files via skill_manage action=write_file with file_path starting 'references/', 'templates/', or 'scripts/'. The umbrella's SKILL.md should gain a one-line pointer to any new support file so future agents know it exists.\n 4. RESTRUCTURE a loaded skill whose body grew log-like \u2014 rc/sha/date-dense sections, session-detail spirals, or a fat body with no support files. Use skill_manage action=restructure with restructure: [{\"heading\": \"<the exact ## heading text>\", \"to_file\": \"references/<topic>.md\"}] \u2014 the ENTIRE ## section (from that heading to the next heading) moves into the support file and its position becomes a pointer line. The skill's name and directory never change. Only propose headings that exist verbatim in the body; never invent one, and never restructure a healthy small skill.\n 5. CREATE A NEW CLASS-LEVEL UMBRELLA SKILL when no existing skill covers the class. The name MUST be at the class level. The name MUST NOT be a specific PR number, error string, feature codename, library-alone name, or 'fix-X / debug-Y / audit-Z-today' session artifact. If the proposed name only makes sense for today's task, it's wrong \u2014 fall back to (1), (2), or (3).\n\nUser-preference embedding (important): when the user expressed a style/format/workflow preference, the update belongs in the SKILL.md body, not just in memory. Memory captures 'who the user is and what the current situation and state of your operations are'; skills capture 'how to do this class of task for this user'. When they complain about how you handled a task, the skill that governs that task needs to carry the lesson.\n\nIf you notice two existing skills that overlap, note it in your reply \u2014 the background curator handles consolidation at scale.\n\nTwo-tier deposition discipline (DSH addition, same spirit as the umbrella rule): before writing, classify the knowledge:\n \u2022 PATTERN (reusable \u2014 symptom \u2192 mechanism \u2192 fix \u2192 verification, still valuable next session) belongs in the SKILL.md body.\n \u2022 LOG (one-off \u2014 commit SHAs, npm/profile states, what this release changed, this session's process narrative) belongs in a references/ file, never the body. Body density IS reuse rate. Keep new entries tight: a pattern fits in 2-8 physical lines; prefer changing the current-state pointer over appending history.\n\nProtected skills (DO NOT edit these):\n \u2022 Bundled skills (shipped with the platform).\n \u2022 Hub-installed skills (installed from a hub).\nPinned skills are read-only to THIS background review pass \u2014 the pinned write guard refuses background changes, so this pass may not update them. They also cannot be archived by any writer (the foreground included): remove the .pinned marker first. Foreground and delegated-subagent update/patch writes to pinned skills remain allowed.\nIf the only skills that need updating are protected, say 'Nothing to save.' and stop.\n\nDo NOT capture (these become persistent self-imposed constraints that bite you later when the environment changes):\n \u2022 Environment-dependent failures: missing binaries, fresh-install errors, post-migration path mismatches, 'command not found', unconfigured credentials, uninstalled packages. The user can fix these \u2014 they are not durable rules.\n \u2022 Negative claims about tools or features ('browser tools do not work', 'X tool is broken', 'cannot use Y'). These harden into refusals the agent cites against itself for months after the actual problem was fixed.\n \u2022 Session-specific transient errors that resolved before the conversation ended. If retrying worked, the lesson is the retry pattern, not the original failure.\n \u2022 One-off task narratives. A user asking 'summarize today's market' or 'analyze this PR' is not a class of work that warrants a skill.\n\nIf a tool failed because of setup state, capture the FIX (install command, config step, env var to set) under an existing setup or troubleshooting skill \u2014 never 'this tool does not work' as a standalone constraint.\n\n'Nothing to save.' is a real option but should NOT be the default. If the session ran smoothly with no corrections and produced no new technique, just say 'Nothing to save.' and stop. Otherwise, act.\n\nCHANNEL (subagent): this review channel mounts only the read-only `skill` tool \u2014 you have NO `skill_manage`, NO `memory`. Your deliverable is the structured JSON plan below (outputSchema). Describe the patches/creates you RECOMMEND in the plan; never narrate actions you took.";
44
44
  /** Subagent-channel variant of the combined review (M-2). */
45
- export declare const COMBINED_REVIEW_PLAN_PROMPT = "[Auto-review]\nReview the conversation above and update two things:\n\n**Memory**: who the user is. Did the user reveal persona, desires, preferences, personal details, or expectations about how you should behave? Save facts about the user and durable preferences with the memory tool.\n\n**Skills**: how to do this class of task. Be ACTIVE \u2014 most sessions produce at least one skill update. A pass that does nothing is a missed learning opportunity, not a neutral outcome.\n\nTarget shape of the skill library: CLASS-LEVEL skills with a rich SKILL.md and a references/ directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries.\n\nSignals that warrant a skill update (any one is enough):\n \u2022 User corrected your style, tone, format, legibility, verbosity, or approach. Frustration is a FIRST-CLASS skill signal, not just a memory signal. 'stop doing X', 'don't format like this', 'I hate when you Y' \u2014 embed the lesson in the skill that governs that task so the next session starts fixed.\n \u2022 Non-trivial technique, fix, workaround, or debugging path emerged.\n \u2022 A skill that was loaded or consulted turned out wrong, missing, or outdated \u2014 patch it now.\n\nRead-before-write (enforced by this channel): update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session \u2014 ops on unread skills are dropped; CREATE of a brand-new umbrella is the only exception.\n\nPreference order for skills \u2014 pick the earliest that fits:\n 1. UPDATE A CURRENTLY-LOADED SKILL. Check what skills were loaded or read in the conversation. If one of them covers the learning, PATCH it first. It was in play; it's the right place.\n 2. UPDATE AN EXISTING UMBRELLA. Patch it.\n 3. ADD A SUPPORT FILE under an existing umbrella via skill_manage action=write_file. Three kinds: references/<topic>.md for session-specific detail OR condensed knowledge banks (quoted research, API docs excerpts, domain notes) written concise and task-focused; templates/<name>.<ext> for starter files meant to be copied and modified; scripts/<name>.<ext> for statically re-runnable actions (verification, fixture generators, probes). Add a one-line pointer in SKILL.md so future agents find them.\n 4. RESTRUCTURE a loaded skill whose body grew log-like (rc/sha/date-dense sections, session-detail spirals, fat body with no support files) via skill_manage action=restructure with restructure: [{\"heading\": \"<the exact ## heading text>\", \"to_file\": \"references/<topic>.md\"}] \u2014 the ENTIRE ## section moves into the support file and its position becomes a pointer line; the skill's name and directory never change. Only propose headings that exist verbatim in the body.\n 5. CREATE A NEW CLASS-LEVEL UMBRELLA when nothing exists. Name at the class level \u2014 NOT a PR number, error string, codename, library-alone name, or 'fix-X / debug-Y' session artifact. If the name only fits today's task, fall back to (1), (2), or (3).\n\nTwo-tier deposition discipline (DSH addition): classify before writing \u2014 PATTERN (symptom \u2192 mechanism \u2192 fix \u2192 verification) goes in the SKILL.md body; LOG (commit SHAs, npm/profile states, this release's change list, this session's narrative) goes in a references/ file. Body density IS reuse rate; a pattern fits in 2-8 physical lines.\n\nUser-preference embedding: when the user complains about how you handled a task, update the skill that governs that task \u2014 memory alone isn't enough. Memory says 'who the user is and what the current situation and state of your operations are'; skills say 'how to do this class of task for this user'. Both should carry user-preference lessons when relevant.\n\nIf you notice overlapping existing skills, mention it \u2014 the background curator handles consolidation.\n\nProtected skills (DO NOT edit these):\n \u2022 Bundled skills (shipped with the platform).\n \u2022 Hub-installed skills (installed from a hub).\nPinned skills are read-only to THIS background review pass \u2014 the pinned write guard refuses background changes, so this pass may not update them. They also cannot be archived by any writer (the foreground included): remove the .pinned marker first. Foreground and delegated-subagent update/patch writes to pinned skills remain allowed.\nIf the only skills that need updating are protected, say 'Nothing to save.' and stop.\n\nDo NOT capture as skills (these become persistent self-imposed constraints that bite you later when the environment changes):\n \u2022 Environment-dependent failures: missing binaries, fresh-install errors, post-migration path mismatches, 'command not found', unconfigured credentials, uninstalled packages. The user can fix these \u2014 they are not durable rules.\n \u2022 Negative claims about tools or features ('browser tools do not work', 'X tool is broken', 'cannot use Y'). These harden into refusals the agent cites against itself for months after the actual problem was fixed.\n \u2022 Session-specific transient errors that resolved before the conversation ended. If retrying worked, the lesson is the retry pattern, not the original failure.\n \u2022 One-off task narratives. A user asking 'summarize today's market' or 'analyze this PR' is not a class of work that warrants a skill.\n\nIf a tool failed because of setup state, capture the FIX (install command, config step, env var to set) under an existing setup or troubleshooting skill \u2014 never 'this tool does not work' as a standalone constraint.\n\nAct on whichever of the two dimensions has real signal. If genuinely nothing stands out on either, say 'Nothing to save.' and stop \u2014 but don't reach for that conclusion as a default.\n\nCHANNEL (subagent): this review channel mounts only the read-only `skill` tool \u2014 you have NO `skill_manage`, NO `memory`. Your deliverable is the structured JSON plan below (outputSchema). Describe the patches/creates you RECOMMEND in the plan; never narrate actions you took.";
45
+ export declare const COMBINED_REVIEW_PLAN_PROMPT = "[Auto-review]\nReview the conversation above and update two things:\n\n**Memory**: who the user is. Did the user reveal persona, desires, preferences, personal details, or expectations about how you should behave? Save facts about the user and durable preferences with the memory tool.\n\n**Skills**: how to do this class of task. Be ACTIVE \u2014 most sessions produce at least one skill update. A pass that does nothing is a missed learning opportunity, not a neutral outcome.\n\nTarget shape of the skill library: CLASS-LEVEL skills with a rich SKILL.md and a references/ directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries.\n\nSignals that warrant a skill update (any one is enough):\n \u2022 User corrected your style, tone, format, legibility, verbosity, or approach. Frustration is a FIRST-CLASS skill signal, not just a memory signal. 'stop doing X', 'don't format like this', 'I hate when you Y' \u2014 embed the lesson in the skill that governs that task so the next session starts fixed.\n \u2022 Non-trivial technique, fix, workaround, or debugging path emerged.\n \u2022 A skill that was loaded or consulted turned out wrong, missing, or outdated \u2014 patch it now.\n\nRead-before-write: update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session. On the plan channel ops on unread skills are rejected; direct writes have no such guard, so treat the rule as binding. CREATE of a brand-new umbrella is the only exception.\n\nPreference order for skills \u2014 pick the earliest that fits:\n 1. UPDATE A CURRENTLY-LOADED SKILL. Check what skills were loaded or read in the conversation. If one of them covers the learning, PATCH it first. It was in play; it's the right place.\n 2. UPDATE AN EXISTING UMBRELLA. Patch it.\n 3. ADD A SUPPORT FILE under an existing umbrella via skill_manage action=write_file. Three kinds: references/<topic>.md for session-specific detail OR condensed knowledge banks (quoted research, API docs excerpts, domain notes) written concise and task-focused; templates/<name>.<ext> for starter files meant to be copied and modified; scripts/<name>.<ext> for statically re-runnable actions (verification, fixture generators, probes). Add a one-line pointer in SKILL.md so future agents find them.\n 4. RESTRUCTURE a loaded skill whose body grew log-like (rc/sha/date-dense sections, session-detail spirals, fat body with no support files) via skill_manage action=restructure with restructure: [{\"heading\": \"<the exact ## heading text>\", \"to_file\": \"references/<topic>.md\"}] \u2014 the ENTIRE ## section moves into the support file and its position becomes a pointer line; the skill's name and directory never change. Only propose headings that exist verbatim in the body.\n 5. CREATE A NEW CLASS-LEVEL UMBRELLA when nothing exists. Name at the class level \u2014 NOT a PR number, error string, codename, library-alone name, or 'fix-X / debug-Y' session artifact. If the name only fits today's task, fall back to (1), (2), or (3).\n\nTwo-tier deposition discipline (DSH addition): classify before writing \u2014 PATTERN (symptom \u2192 mechanism \u2192 fix \u2192 verification) goes in the SKILL.md body; LOG (commit SHAs, npm/profile states, this release's change list, this session's narrative) goes in a references/ file. Body density IS reuse rate; a pattern fits in 2-8 physical lines.\n\nUser-preference embedding: when the user complains about how you handled a task, update the skill that governs that task \u2014 memory alone isn't enough. Memory says 'who the user is and what the current situation and state of your operations are'; skills say 'how to do this class of task for this user'. Both should carry user-preference lessons when relevant.\n\nIf you notice overlapping existing skills, mention it \u2014 the background curator handles consolidation.\n\nProtected skills (DO NOT edit these):\n \u2022 Bundled skills (shipped with the platform).\n \u2022 Hub-installed skills (installed from a hub).\nPinned skills are read-only to THIS background review pass \u2014 the pinned write guard refuses background changes, so this pass may not update them. They also cannot be archived by any writer (the foreground included): remove the .pinned marker first. Foreground and delegated-subagent update/patch writes to pinned skills remain allowed.\nIf the only skills that need updating are protected, say 'Nothing to save.' and stop.\n\nDo NOT capture as skills (these become persistent self-imposed constraints that bite you later when the environment changes):\n \u2022 Environment-dependent failures: missing binaries, fresh-install errors, post-migration path mismatches, 'command not found', unconfigured credentials, uninstalled packages. The user can fix these \u2014 they are not durable rules.\n \u2022 Negative claims about tools or features ('browser tools do not work', 'X tool is broken', 'cannot use Y'). These harden into refusals the agent cites against itself for months after the actual problem was fixed.\n \u2022 Session-specific transient errors that resolved before the conversation ended. If retrying worked, the lesson is the retry pattern, not the original failure.\n \u2022 One-off task narratives. A user asking 'summarize today's market' or 'analyze this PR' is not a class of work that warrants a skill.\n\nIf a tool failed because of setup state, capture the FIX (install command, config step, env var to set) under an existing setup or troubleshooting skill \u2014 never 'this tool does not work' as a standalone constraint.\n\nAct on whichever of the two dimensions has real signal. If genuinely nothing stands out on either, say 'Nothing to save.' and stop \u2014 but don't reach for that conclusion as a default.\n\nCHANNEL (subagent): this review channel mounts only the read-only `skill` tool \u2014 you have NO `skill_manage`, NO `memory`. Your deliverable is the structured JSON plan below (outputSchema). Describe the patches/creates you RECOMMEND in the plan; never narrate actions you took.";
46
46
  export declare function reviewPrompt(kind: 'memory' | 'skill' | 'combined', channel?: 'agent' | 'plan'): string;
47
47
  export interface PromptBundle {
48
48
  id: string;
@@ -167,7 +167,7 @@ export interface Frontmatter {
167
167
  /**
168
168
  * Shared frontmatter block detection (P3-3 single owner): opening line `---`
169
169
  * and closing line exactly `---`. Used by `parseFrontmatter`,
170
- * `frontmatterYamlUnsafeValues` and `normalizeFrontmatter` so the three can
170
+ * `frontmatterCatalogInvalid` and `normalizeFrontmatter` so the three can
171
171
  * never disagree about where the block ends (the loose `indexOf('\n---')`
172
172
  * form matched `\n----` and was replaced by this strict line rule).
173
173
  *
@@ -244,18 +244,6 @@ export declare function frontmatterCatalogInvalid(content: string): boolean;
244
244
  * with the real YAML parser (see normalizeFrontmatter), so an incomplete
245
245
  * approximation can never corrupt a multiline flow value (P3-4). */
246
246
  export declare function yamlPlainScalarNeedsQuotes(value: string): boolean;
247
- /** Raw-line scan of the frontmatter block: entries whose UNQUOTED value is
248
- * YAML-unsafe for the strict platform catalog. Operates on the ORIGINAL line
249
- * value (quotes included), so a value already wrapped by
250
- * `normalizeFrontmatter` is never re-flagged — one source with the write
251
- * path. V27 G2.1: delegates to the shared scan, which
252
- * `parseFrontmatter(...).catalogInvalid` also uses, so the audit view and the
253
- * read view of one file can never disagree. Independent of the body: a
254
- * body-less file is still reported here. */
255
- export declare function frontmatterYamlUnsafeValues(content: string): Array<{
256
- key: string;
257
- value: string;
258
- }>;
259
247
  export interface FrontmatterNormalizeResult {
260
248
  content: string;
261
249
  changed: boolean;
@@ -352,6 +340,17 @@ export declare class SkillLibrary {
352
340
  * never inflates the mutation-maturity counter.
353
341
  */
354
342
  private runSingleWrite;
343
+ /**
344
+ * v28 G1.1 (EVO-IO-02): shared compensating cleanup for a locked write that
345
+ * found no SKILL.md — remove the possibly-resurrected directory ONLY when it
346
+ * holds nothing but this path's own write-lock file. The BR-5 rule from
347
+ * setPinnedCore/createCore applies unchanged: a concurrent mover can land a
348
+ * full directory between the list probe and the remove, so anything beyond
349
+ * the lock file (support files, a fresh restore) must never be recursed
350
+ * away. Best-effort: a failed cleanup leaves the "not found" result
351
+ * unchanged (the operator-facing ghost-dir refusal is fail-loud already).
352
+ */
353
+ private cleanupGhostDir;
355
354
  /** Notify the mutation observer after a successful write; observers must never fail the mutation. */
356
355
  private notifyMutation;
357
356
  /** V10-03 (P2-18): ScanOptions shared by every write-path threat check —
@@ -361,6 +360,15 @@ export declare class SkillLibrary {
361
360
  * label (scanContentThreats already embeds it) plus the self-heal hint, so a
362
361
  * false-positive rewrite direction is actionable instead of a dead end. */
363
362
  private contentThreatBlock;
363
+ /**
364
+ * v30 REV-03: read a support file's bytes for staleness anchoring (the
365
+ * write/remove replay guard). Same validation as writeSupportFile; a
366
+ * missing file (or a directory squatting on the path) reads as `null`, any
367
+ * other failure RETHROWS — the callers are the staging/replay anchors, and
368
+ * a swallowed error would silently downgrade the anchor to
369
+ * last-writer-wins.
370
+ */
371
+ readSupportFile(name: string, filePath: string): Promise<string | null>;
364
372
  list(): Promise<SkillSummary[]>;
365
373
  read(rawName: string): Promise<string | null>;
366
374
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-core",
3
3
  "description": "Shared stores, prompts, signals and lifecycle logic for the dsh-evolution plugin family (community build)",
4
- "version": "0.3.68",
4
+ "version": "0.3.70",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -33,14 +33,14 @@
33
33
  "js-yaml": "^4.2.0"
34
34
  },
35
35
  "peerDependencies": {
36
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
36
+ "@deepseek-ai/dsh-invariants": "^0.1.5-rc.2",
37
37
  "@deepseek-ai/cordis": "^4.0.1",
38
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2"
38
+ "@deepseek-ai/dsh-session": "^0.1.5-rc.2"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/js-yaml": "^4.0.9",
42
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
42
+ "@deepseek-ai/dsh-invariants": "^0.1.5-rc.2",
43
43
  "@deepseek-ai/cordis": "^4.0.1",
44
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2"
44
+ "@deepseek-ai/dsh-session": "^0.1.5-rc.2"
45
45
  }
46
46
  }