@lmzhen/dsh-evolution-core 0.1.0-rc.11 → 0.1.0-rc.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { dirname, join } from "node:path";
1
+ import { basename, dirname, join } from "node:path";
2
2
  import { cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
4
  import { homedir } from "node:os";
@@ -151,13 +151,87 @@ function latestActivityAt(record) {
151
151
  if (values.length === 0) return null;
152
152
  return values.sort().reverse()[0] ?? null;
153
153
  }
154
+ /**
155
+ * Curator suppression sidecar: built-in skills the curator has archived stay
156
+ * suppressed across re-seeds, so the lifecycle never fights a re-created
157
+ * bundled skill. Best-effort load/save, mirroring the usage sidecar posture.
158
+ */
159
+ function suppressedFile(root) {
160
+ return join(root, ".curator-suppressed.json");
161
+ }
162
+ async function loadSuppressedNames(root, io = nodeEvolutionIo()) {
163
+ const raw = await io.readText(suppressedFile(root));
164
+ if (raw === null) return /* @__PURE__ */ new Set();
165
+ try {
166
+ const parsed = JSON.parse(raw);
167
+ if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
168
+ return new Set(parsed.filter((entry) => typeof entry === "string"));
169
+ } catch {
170
+ return /* @__PURE__ */ new Set();
171
+ }
172
+ }
173
+ async function saveSuppressedNames(root, names, io = nodeEvolutionIo()) {
174
+ await io.writeText(suppressedFile(root), JSON.stringify([...names].sort(), null, 2));
175
+ }
176
+ //#endregion
177
+ //#region lib/types/constants.js
178
+ /**
179
+ * Shared constants for the dsh-evolution plugin family.
180
+ *
181
+ * Two classes of value live here, deliberately separated by section so future
182
+ * edits do not blur the semantic boundary:
183
+ *
184
+ * 1. **Fixed protocol/format/security invariants** — changing these breaks an
185
+ * on-disk format, a naming/format contract, a path-security boundary, or a
186
+ * cross-component invariant. They are NOT exposed as deployment config.
187
+ *
188
+ * 2. **Cross-package shared tunable defaults** — the same semantic default is
189
+ * read (with a config override path) by more than one package (e.g.
190
+ * `evolution-policy` and `evolution-curator` both default `staleAfterDays`
191
+ * to 30). Centralizing them here means one authoritative default: a config
192
+ * override still applies per package, but the fallback is single-sourced.
193
+ *
194
+ * Package-private tunables (used by exactly one package) stay in that package,
195
+ * not here — see evolution-replay's `DEFAULT_WEIGHTS` and evolution-feedback's
196
+ * threshold, which are intentionally left where they are used.
197
+ * @module @lmzhen/dsh-evolution-core
198
+ */
199
+ /** Skill frontmatter `name` validated for the file name (lowercase + hyphen). */
200
+ const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
201
+ /** Allowed skill support-file subdirectories (path-traversal boundary). */
202
+ const SUPPORT_DIRS = [
203
+ "references",
204
+ "templates",
205
+ "scripts",
206
+ "assets"
207
+ ];
208
+ /** Delimiter between durable memory entries (on-disk storage format). */
209
+ const ENTRY_DELIMITER = "\n§\n";
210
+ /** Built-in skill names the curator must never lifecycle-manage. */
211
+ const PROTECTED_BUILTIN_SKILLS = new Set(["plan"]);
212
+ const MAX_SKILL_NAME_LENGTH = 64;
213
+ const MAX_DESCRIPTION_LENGTH = 1024;
214
+ const MAX_SKILL_CONTENT_CHARS = 1e5;
215
+ const MAX_SKILL_FILE_BYTES = 1048576;
216
+ const DEFAULT_REVIEW_MEMORY_INTERVAL = 10;
217
+ const DEFAULT_REVIEW_SKILL_INTERVAL = 10;
218
+ const DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS = 3;
219
+ const DEFAULT_SUBSTANTIVE_MIN_USER_CHARS = 200;
220
+ const DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS = 500;
221
+ const DEFAULT_MAX_OPS_PER_PLAN = 32;
222
+ const DEFAULT_CURATOR_INTERVAL_HOURS = 168;
223
+ const DEFAULT_MIN_IDLE_HOURS = 2;
224
+ const DEFAULT_STALE_AFTER_DAYS = 30;
225
+ const DEFAULT_ARCHIVE_AFTER_DAYS = 90;
226
+ const DEFAULT_MEMORY_CHAR_LIMIT = 2200;
227
+ const DEFAULT_USER_CHAR_LIMIT = 1375;
228
+ const DEFAULT_SKILL_CONTENT_CHARS = 1e5;
154
229
  //#endregion
155
230
  //#region lib/types/curator.js
156
231
  /**
157
232
  * Deterministic skill curator: active → stale → archived transitions.
158
233
  * Pure function; file moves are performed by SkillLibrary.
159
234
  */
160
- const PROTECTED_BUILTIN_SKILLS = new Set(["plan"]);
161
235
  function buildCuratorRunReport(input) {
162
236
  return {
163
237
  runId: input.runId,
@@ -184,7 +258,9 @@ function computeLifecycleTransitions(usage, config, now = /* @__PURE__ */ new Da
184
258
  for (const [name, record] of usage) {
185
259
  if (record.pinned) continue;
186
260
  if (config.excludeSkillNames?.has(name)) continue;
187
- if (record.created_by !== "agent" && config.manageUnmanaged !== true) continue;
261
+ if (config.suppressedNames?.has(name)) continue;
262
+ const bundled = config.bundledNames?.has(name) === true;
263
+ if (!(record.created_by === "agent" || config.manageUnmanaged === true) && !(config.pruneBuiltins === true && bundled)) continue;
188
264
  if (PROTECTED_BUILTIN_SKILLS.has(name)) continue;
189
265
  if (record.state === "archived") continue;
190
266
  const age = daysSince(null, record.created_at, now.getTime());
@@ -471,7 +547,6 @@ function scanContentThreats(text, maxScanChars = 65536, options = NO_SCAN_OPTION
471
547
  * File-backed durable memory with Hermes-compatible semantics.
472
548
  * Stores are MEMORY.md and USER.md under $DSH_HOME/memories (~/.dsh/memories).
473
549
  */
474
- const ENTRY_DELIMITER = "\n§\n";
475
550
  function memoryRoot(env = process.env) {
476
551
  return join(env.DSH_HOME ?? join(homedir(), ".dsh"), "memories");
477
552
  }
@@ -534,6 +609,30 @@ var MemoryStore = class {
534
609
  limit: this.limitFor(target)
535
610
  };
536
611
  }
612
+ /** Percent-based storage hint appended to success message once the target is ≥80% full. */
613
+ storageHint(target, chars) {
614
+ const limit = this.limitFor(target);
615
+ if (limit <= 0) return "";
616
+ const percent = Math.floor(chars * 100 / limit);
617
+ return percent >= 80 ? ` ⚠️ Storage at ${percent}% (${chars}/${limit} chars).` : "";
618
+ }
619
+ /**
620
+ * Best-effort copy of the drifted on-disk file to `<file>.bak.<stamp>` before
621
+ * refusing the write, so an external edit stays recoverable. Failure to back
622
+ * up does not change the refusal semantics.
623
+ */
624
+ async backupDrift(target) {
625
+ const path = fileFor(this.root, target);
626
+ const raw = await this.io.readText(path);
627
+ if (raw === null) return null;
628
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 14);
629
+ try {
630
+ await this.io.writeText(`${path}.bak.${stamp}`, raw);
631
+ return `${path}.bak.${stamp}`;
632
+ } catch {
633
+ return null;
634
+ }
635
+ }
537
636
  async add(target, facts) {
538
637
  const content = facts.trim();
539
638
  if (!content) return {
@@ -556,7 +655,7 @@ var MemoryStore = class {
556
655
  this.resetFailures();
557
656
  return {
558
657
  ok: true,
559
- message: "Entry already exists (no duplicate added).",
658
+ message: `Entry already exists (no duplicate added).${this.storageHint(target, entries.join(ENTRY_DELIMITER).length)}`,
560
659
  entries,
561
660
  chars: entries.join(ENTRY_DELIMITER).length,
562
661
  limit: this.limitFor(target)
@@ -569,7 +668,7 @@ var MemoryStore = class {
569
668
  this.resetFailures();
570
669
  return {
571
670
  ok: true,
572
- message: "Entry added.",
671
+ message: `Entry added.${this.storageHint(target, total)}`,
573
672
  entries: next,
574
673
  chars: total,
575
674
  limit: this.limitFor(target)
@@ -608,13 +707,16 @@ var MemoryStore = class {
608
707
  limit: this.limitFor(target)
609
708
  };
610
709
  }
611
- if (await this.detectDrift(target)) return {
612
- ok: false,
613
- message: "External drift detected in memory file. Resolve the drift before retrying.",
614
- entries: [],
615
- chars: 0,
616
- limit: this.limitFor(target)
617
- };
710
+ if (await this.detectDrift(target)) {
711
+ const backup = await this.backupDrift(target);
712
+ return {
713
+ ok: false,
714
+ message: `External drift detected in memory file.${backup ? ` A backup was saved to ${basename(backup)}.` : ""} Resolve the drift before retrying.`,
715
+ entries: [],
716
+ chars: 0,
717
+ limit: this.limitFor(target)
718
+ };
719
+ }
618
720
  const entries = await this.read(target);
619
721
  const matches = entries.map((entry, index) => ({
620
722
  entry,
@@ -638,7 +740,7 @@ var MemoryStore = class {
638
740
  this.resetFailures();
639
741
  return {
640
742
  ok: true,
641
- message: `Entry ${action === "remove" ? "removed" : "replaced"}.`,
743
+ message: `Entry ${action === "remove" ? "removed" : "replaced"}.${this.storageHint(target, total)}`,
642
744
  entries: next,
643
745
  chars: total,
644
746
  limit: this.limitFor(target)
@@ -652,13 +754,16 @@ var MemoryStore = class {
652
754
  chars: 0,
653
755
  limit: this.limitFor(target)
654
756
  };
655
- if (await this.detectDrift(target)) return {
656
- ok: false,
657
- message: "External drift detected in memory file. Resolve the drift before retrying.",
658
- entries: [],
659
- chars: 0,
660
- limit: this.limitFor(target)
661
- };
757
+ if (await this.detectDrift(target)) {
758
+ const backup = await this.backupDrift(target);
759
+ return {
760
+ ok: false,
761
+ message: `External drift detected in memory file.${backup ? ` A backup was saved to ${basename(backup)}.` : ""} Resolve the drift before retrying.`,
762
+ entries: [],
763
+ chars: 0,
764
+ limit: this.limitFor(target)
765
+ };
766
+ }
662
767
  const entries = await this.read(target);
663
768
  const working = [...entries];
664
769
  for (const [index, op] of operations.entries()) {
@@ -731,7 +836,7 @@ var MemoryStore = class {
731
836
  this.resetFailures();
732
837
  return {
733
838
  ok: true,
734
- message: `Applied ${operations.length} operation(s).`,
839
+ message: `Applied ${operations.length} operation(s).${this.storageHint(target, total)}`,
735
840
  entries: working,
736
841
  chars: total,
737
842
  limit: this.limitFor(target)
@@ -1002,23 +1107,12 @@ function foldTurn(session, fromSeq) {
1002
1107
  * it created unless a `.hermes-managed` marker opts a skill in. Archival is a
1003
1108
  * move to `.archive/` — never a hard delete.
1004
1109
  */
1005
- const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
1006
- const MAX_SKILL_NAME_LENGTH = 64;
1007
- const MAX_DESCRIPTION_LENGTH = 1024;
1008
- const MAX_SKILL_CONTENT_CHARS = 1e5;
1009
- const MAX_SKILL_FILE_BYTES = 1048576;
1010
1110
  const DEFAULT_SKILL_LIMITS = {
1011
1111
  maxNameLength: 64,
1012
1112
  maxDescriptionLength: MAX_DESCRIPTION_LENGTH,
1013
1113
  maxSkillContentChars: MAX_SKILL_CONTENT_CHARS,
1014
1114
  maxSkillFileBytes: MAX_SKILL_FILE_BYTES
1015
1115
  };
1016
- const SUPPORT_DIRS = [
1017
- "references",
1018
- "templates",
1019
- "scripts",
1020
- "assets"
1021
- ];
1022
1116
  function skillsRoot(env = process.env) {
1023
1117
  return join(env.DSH_HOME ?? join(homedir(), ".dsh"), "skills");
1024
1118
  }
@@ -1117,24 +1211,31 @@ var SkillLibrary = class {
1117
1211
  async read(name) {
1118
1212
  return this.io.readText(join(skillDir(this.root, name), "SKILL.md"));
1119
1213
  }
1120
- async writeProtection(name) {
1214
+ async writeProtection(name, origin = "foreground") {
1121
1215
  const dir = skillDir(this.root, name);
1122
1216
  for (const marker of ["bundled", "hub-installed"]) if (await this.io.exists(markerPath(dir, marker))) return marker;
1217
+ if (origin === "background_review" && await this.io.exists(markerPath(dir, "pinned"))) return "pinned";
1123
1218
  return null;
1124
1219
  }
1125
- async deleteProtection(name) {
1220
+ async deleteProtection(name, options = {}) {
1126
1221
  const dir = skillDir(this.root, name);
1127
- for (const marker of [
1222
+ const markers = options.allowBundled ? ["hub-installed", "pinned"] : [
1128
1223
  "bundled",
1129
1224
  "hub-installed",
1130
1225
  "pinned"
1131
- ]) if (await this.io.exists(markerPath(dir, marker))) return marker;
1226
+ ];
1227
+ for (const marker of markers) if (await this.io.exists(markerPath(dir, marker))) return marker;
1132
1228
  return null;
1133
1229
  }
1134
1230
  async isManaged(name) {
1135
1231
  const dir = skillDir(this.root, name);
1136
1232
  return await this.io.exists(markerPath(dir, "hermes-managed"));
1137
1233
  }
1234
+ /** Whether the skill carries the bundled marker (curator prune-builtins eligibility). */
1235
+ async isBundled(name) {
1236
+ const dir = skillDir(this.root, name);
1237
+ return await this.io.exists(markerPath(dir, "bundled"));
1238
+ }
1138
1239
  async create(name, content, origin) {
1139
1240
  const normalized = name.trim();
1140
1241
  if (!SKILL_NAME_RE.test(normalized) || normalized.length > this.limits.maxNameLength) return {
@@ -1164,13 +1265,13 @@ var SkillLibrary = class {
1164
1265
  path: dir
1165
1266
  };
1166
1267
  }
1167
- async update(name, content) {
1268
+ async update(name, content, origin = "foreground") {
1168
1269
  const dir = skillDir(this.root, name);
1169
1270
  if (!await this.io.readText(join(dir, "SKILL.md"))) return {
1170
1271
  ok: false,
1171
1272
  message: `Skill "${name}" not found.`
1172
1273
  };
1173
- const protection = await this.writeProtection(name);
1274
+ const protection = await this.writeProtection(name, origin);
1174
1275
  if (protection) return {
1175
1276
  ok: false,
1176
1277
  message: `Skill "${name}" is protected (${protection}).`
@@ -1192,14 +1293,14 @@ var SkillLibrary = class {
1192
1293
  path: dir
1193
1294
  };
1194
1295
  }
1195
- async patch(name, oldString, newString, filePath = "", replaceAll = false) {
1296
+ async patch(name, oldString, newString, filePath = "", replaceAll = false, origin = "foreground") {
1196
1297
  const dir = skillDir(this.root, name);
1197
1298
  const skillMd = join(dir, "SKILL.md");
1198
1299
  if (!await this.io.exists(skillMd)) return {
1199
1300
  ok: false,
1200
1301
  message: `Skill "${name}" not found.`
1201
1302
  };
1202
- const protection = await this.writeProtection(name);
1303
+ const protection = await this.writeProtection(name, origin);
1203
1304
  if (protection) return {
1204
1305
  ok: false,
1205
1306
  message: `Skill "${name}" is protected (${protection}).`
@@ -1252,21 +1353,21 @@ var SkillLibrary = class {
1252
1353
  path: dir
1253
1354
  };
1254
1355
  }
1255
- async archive(name, absorbedInto = "") {
1356
+ async archive(name, options = {}) {
1256
1357
  const dir = skillDir(this.root, name);
1257
1358
  if (!await this.io.readText(join(dir, "SKILL.md"))) return {
1258
1359
  ok: false,
1259
1360
  message: `Skill "${name}" not found.`
1260
1361
  };
1261
- const protection = await this.deleteProtection(name);
1362
+ const protection = await this.deleteProtection(name, options.allowBundled === void 0 ? {} : { allowBundled: options.allowBundled });
1262
1363
  if (protection) return {
1263
1364
  ok: false,
1264
1365
  message: `Skill "${name}" is protected (${protection}).`
1265
1366
  };
1266
- if (absorbedInto) {
1267
- if (!await this.io.readText(join(skillDir(this.root, absorbedInto), "SKILL.md"))) return {
1367
+ if (options.absorbedInto) {
1368
+ if (!await this.io.readText(join(skillDir(this.root, options.absorbedInto), "SKILL.md"))) return {
1268
1369
  ok: false,
1269
- message: `absorbed_into="${absorbedInto}" does not exist.`
1370
+ message: `absorbed_into="${options.absorbedInto}" does not exist.`
1270
1371
  };
1271
1372
  }
1272
1373
  const archiveRoot = join(this.root, ".archive");
@@ -1278,7 +1379,7 @@ var SkillLibrary = class {
1278
1379
  await this.io.copy(dir, dest);
1279
1380
  await this.io.remove(dir);
1280
1381
  }
1281
- const reason = absorbedInto ? `Consolidated into ${absorbedInto}` : "Archived by self-evolution curator";
1382
+ const reason = options.reason ?? (options.absorbedInto ? `Consolidated into ${options.absorbedInto}` : "Archived by self-evolution curator");
1282
1383
  await this.io.writeText(join(dest, ".archive-reason"), `${(/* @__PURE__ */ new Date()).toISOString()}: ${reason}\n`);
1283
1384
  return {
1284
1385
  ok: true,
@@ -1291,7 +1392,7 @@ var SkillLibrary = class {
1291
1392
  * an absorbed-into marker. Hermes-style consolidation: overlapping skills
1292
1393
  * collapse into one, and the originals stay recoverable under `.archive/`.
1293
1394
  */
1294
- async consolidate(target, sources) {
1395
+ async consolidate(target, sources, origin = "foreground") {
1295
1396
  const normalizedSources = [...new Set(sources)].filter((name) => name !== target);
1296
1397
  if (normalizedSources.length === 0) return {
1297
1398
  ok: false,
@@ -1307,7 +1408,7 @@ var SkillLibrary = class {
1307
1408
  ok: false,
1308
1409
  message: `Skill "${target}" not found.`
1309
1410
  };
1310
- const targetProtection = await this.writeProtection(target);
1411
+ const targetProtection = await this.writeProtection(target, origin);
1311
1412
  if (targetProtection) return {
1312
1413
  ok: false,
1313
1414
  message: `Skill "${target}" is protected (${targetProtection}).`
@@ -1345,7 +1446,7 @@ var SkillLibrary = class {
1345
1446
  const archived = [];
1346
1447
  try {
1347
1448
  for (const source of normalizedSources) {
1348
- const result = await this.archive(source, target);
1449
+ const result = await this.archive(source, { absorbedInto: target });
1349
1450
  if (!result.ok) return result;
1350
1451
  archived.push(source);
1351
1452
  }
@@ -1408,13 +1509,13 @@ var SkillLibrary = class {
1408
1509
  path: dest
1409
1510
  };
1410
1511
  }
1411
- async writeSupportFile(name, filePath, content) {
1512
+ async writeSupportFile(name, filePath, content, origin = "foreground") {
1412
1513
  const dir = skillDir(this.root, name);
1413
1514
  if (!await this.io.exists(join(dir, "SKILL.md"))) return {
1414
1515
  ok: false,
1415
1516
  message: `Skill "${name}" not found.`
1416
1517
  };
1417
- const protection = await this.writeProtection(name);
1518
+ const protection = await this.writeProtection(name, origin);
1418
1519
  if (protection) return {
1419
1520
  ok: false,
1420
1521
  message: `Skill "${name}" is protected (${protection}).`
@@ -1441,13 +1542,13 @@ var SkillLibrary = class {
1441
1542
  path: target
1442
1543
  };
1443
1544
  }
1444
- async removeSupportFile(name, filePath) {
1545
+ async removeSupportFile(name, filePath, origin = "foreground") {
1445
1546
  const dir = skillDir(this.root, name);
1446
1547
  if (!await this.io.exists(join(dir, "SKILL.md"))) return {
1447
1548
  ok: false,
1448
1549
  message: `Skill "${name}" not found.`
1449
1550
  };
1450
- const protection = await this.writeProtection(name);
1551
+ const protection = await this.writeProtection(name, origin);
1451
1552
  if (protection) return {
1452
1553
  ok: false,
1453
1554
  message: `Skill "${name}" is protected (${protection}).`
@@ -1590,4 +1691,4 @@ var JsonState = class JsonState {
1590
1691
  }
1591
1692
  };
1592
1693
  //#endregion
1593
- export { COMBINED_REVIEW_PROMPT, CURATOR_PROMPT, DEFAULT_SKILL_LIMITS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, JsonState, MAX_DESCRIPTION_LENGTH, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MemoryStore, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROTECTED_BUILTIN_SKILLS, SKILL_NAME_RE, SKILL_REVIEW_PROMPT, SUPPORT_DIRS, SkillLibrary, advanceReview, buildCuratorRunReport, bumpPatch, bumpUse, bumpView, computeLifecycleTransitions, emptyRecord, evaluateThreat, evolutionHome, evolutionIoAdapter, foldTurn, getRecord, latestActivityAt, loadUsage, markAgentCreated, memoryRoot, nodeEvolutionIo, observeEvent, parseFrontmatter, reviewPrompt, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, usageFile, validateFrontmatter, verifyPromptBundle };
1694
+ export { COMBINED_REVIEW_PROMPT, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, JsonState, MAX_DESCRIPTION_LENGTH, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MemoryStore, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROTECTED_BUILTIN_SKILLS, SKILL_NAME_RE, SKILL_REVIEW_PROMPT, SUPPORT_DIRS, SkillLibrary, advanceReview, buildCuratorRunReport, bumpPatch, bumpUse, bumpView, computeLifecycleTransitions, emptyRecord, evaluateThreat, evolutionHome, evolutionIoAdapter, foldTurn, getRecord, latestActivityAt, loadSuppressedNames, loadUsage, markAgentCreated, memoryRoot, nodeEvolutionIo, observeEvent, parseFrontmatter, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, usageFile, validateFrontmatter, verifyPromptBundle };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Shared constants for the dsh-evolution plugin family.
3
+ *
4
+ * Two classes of value live here, deliberately separated by section so future
5
+ * edits do not blur the semantic boundary:
6
+ *
7
+ * 1. **Fixed protocol/format/security invariants** — changing these breaks an
8
+ * on-disk format, a naming/format contract, a path-security boundary, or a
9
+ * cross-component invariant. They are NOT exposed as deployment config.
10
+ *
11
+ * 2. **Cross-package shared tunable defaults** — the same semantic default is
12
+ * read (with a config override path) by more than one package (e.g.
13
+ * `evolution-policy` and `evolution-curator` both default `staleAfterDays`
14
+ * to 30). Centralizing them here means one authoritative default: a config
15
+ * override still applies per package, but the fallback is single-sourced.
16
+ *
17
+ * Package-private tunables (used by exactly one package) stay in that package,
18
+ * not here — see evolution-replay's `DEFAULT_WEIGHTS` and evolution-feedback's
19
+ * threshold, which are intentionally left where they are used.
20
+ * @module @deepseek-ai/dsh-evolution-core
21
+ */
22
+ /** Skill frontmatter `name` validated for the file name (lowercase + hyphen). */
23
+ export declare const SKILL_NAME_RE: RegExp;
24
+ /** Allowed skill support-file subdirectories (path-traversal boundary). */
25
+ export declare const SUPPORT_DIRS: readonly ["references", "templates", "scripts", "assets"];
26
+ /** Delimiter between durable memory entries (on-disk storage format). */
27
+ export declare const ENTRY_DELIMITER = "\n\u00A7\n";
28
+ /** Built-in skill names the curator must never lifecycle-manage. */
29
+ export declare const PROTECTED_BUILTIN_SKILLS: ReadonlySet<string>;
30
+ export declare const MAX_SKILL_NAME_LENGTH = 64;
31
+ export declare const MAX_DESCRIPTION_LENGTH = 1024;
32
+ export declare const MAX_SKILL_CONTENT_CHARS = 100000;
33
+ export declare const MAX_SKILL_FILE_BYTES = 1048576;
34
+ export declare const DEFAULT_REVIEW_MEMORY_INTERVAL = 10;
35
+ export declare const DEFAULT_REVIEW_SKILL_INTERVAL = 10;
36
+ export declare const DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS = 3;
37
+ export declare const DEFAULT_SUBSTANTIVE_MIN_USER_CHARS = 200;
38
+ export declare const DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS = 500;
39
+ export declare const DEFAULT_MAX_OPS_PER_PLAN = 32;
40
+ export declare const DEFAULT_CURATOR_INTERVAL_HOURS = 168;
41
+ export declare const DEFAULT_MIN_IDLE_HOURS = 2;
42
+ export declare const DEFAULT_STALE_AFTER_DAYS = 30;
43
+ export declare const DEFAULT_ARCHIVE_AFTER_DAYS = 90;
44
+ export declare const DEFAULT_MEMORY_CHAR_LIMIT = 2200;
45
+ export declare const DEFAULT_USER_CHAR_LIMIT = 1375;
46
+ export declare const DEFAULT_SKILL_CONTENT_CHARS = 100000;
47
+ //# sourceMappingURL=constants.d.ts.map
@@ -3,6 +3,7 @@
3
3
  * Pure function; file moves are performed by SkillLibrary.
4
4
  */
5
5
  import type { UsageMap } from './usage.ts';
6
+ export { PROTECTED_BUILTIN_SKILLS } from './constants.ts';
6
7
  export interface CuratorConfig {
7
8
  staleAfterDays: number;
8
9
  archiveAfterDays: number;
@@ -12,6 +13,12 @@ export interface CuratorConfig {
12
13
  excludeSkillNames?: ReadonlySet<string>;
13
14
  /** When true, usage records without created_by='agent' also enter the lifecycle. */
14
15
  manageUnmanaged?: boolean;
16
+ /** When true, bundled skills (in `bundledNames`) are curation candidates like agent-created ones. */
17
+ pruneBuiltins?: boolean;
18
+ /** Skill names carrying the bundled marker; only read when `pruneBuiltins` is true. */
19
+ bundledNames?: ReadonlySet<string>;
20
+ /** Skill names the curator archived once and must not fight across re-seeds. */
21
+ suppressedNames?: ReadonlySet<string>;
15
22
  }
16
23
  export interface CuratorTransition {
17
24
  name: string;
@@ -25,7 +32,6 @@ export interface CuratorResult {
25
32
  reactivate: string[];
26
33
  markStale: string[];
27
34
  }
28
- export declare const PROTECTED_BUILTIN_SKILLS: ReadonlySet<string>;
29
35
  export interface CuratorArchivedSkill {
30
36
  name: string;
31
37
  path: string;
@@ -17,4 +17,5 @@ export * from './skill-store.ts';
17
17
  export * from './state-store.ts';
18
18
  export * from './threats.ts';
19
19
  export * from './usage.ts';
20
+ export * from './constants.ts';
20
21
  //# sourceMappingURL=index.d.ts.map
@@ -3,7 +3,7 @@
3
3
  * Stores are MEMORY.md and USER.md under $DSH_HOME/memories (~/.dsh/memories).
4
4
  */
5
5
  import { type EvolutionIoLike } from './io.ts';
6
- export declare const ENTRY_DELIMITER = "\n\u00A7\n";
6
+ export { ENTRY_DELIMITER } from './constants.ts';
7
7
  export type MemoryTarget = 'memory' | 'user';
8
8
  export interface MemoryOperation {
9
9
  action: 'add' | 'replace' | 'remove';
@@ -41,6 +41,14 @@ export declare class MemoryStore {
41
41
  write(target: MemoryTarget, entries: string[]): Promise<void>;
42
42
  resetFailures(): void;
43
43
  private failure;
44
+ /** Percent-based storage hint appended to success message once the target is ≥80% full. */
45
+ private storageHint;
46
+ /**
47
+ * Best-effort copy of the drifted on-disk file to `<file>.bak.<stamp>` before
48
+ * refusing the write, so an external edit stays recoverable. Failure to back
49
+ * up does not change the refusal semantics.
50
+ */
51
+ private backupDrift;
44
52
  add(target: MemoryTarget, facts: string): Promise<MemoryApplyResult>;
45
53
  replace(target: MemoryTarget, oldText: string, facts: string): Promise<MemoryApplyResult>;
46
54
  remove(target: MemoryTarget, oldText: string): Promise<MemoryApplyResult>;
@@ -7,11 +7,6 @@
7
7
  * move to `.archive/` — never a hard delete.
8
8
  */
9
9
  import { type EvolutionIoLike } from './io.ts';
10
- export declare const SKILL_NAME_RE: RegExp;
11
- export declare const MAX_SKILL_NAME_LENGTH = 64;
12
- export declare const MAX_DESCRIPTION_LENGTH = 1024;
13
- export declare const MAX_SKILL_CONTENT_CHARS = 100000;
14
- export declare const MAX_SKILL_FILE_BYTES = 1048576;
15
10
  export interface SkillLimits {
16
11
  maxNameLength: number;
17
12
  maxDescriptionLength: number;
@@ -19,7 +14,6 @@ export interface SkillLimits {
19
14
  maxSkillFileBytes: number;
20
15
  }
21
16
  export declare const DEFAULT_SKILL_LIMITS: SkillLimits;
22
- export declare const SUPPORT_DIRS: readonly ["references", "templates", "scripts", "assets"];
23
17
  export interface SkillSummary {
24
18
  name: string;
25
19
  description: string;
@@ -33,6 +27,17 @@ export interface SkillActionResult {
33
27
  message: string;
34
28
  path?: string;
35
29
  }
30
+ /** Who is writing: a foreground user-directed tool call, or the autonomous review/curator pipeline. */
31
+ export type WriteOrigin = 'foreground' | 'background_review';
32
+ /** Options for `SkillLibrary.archive`. The absorbed-into name and the archival reason are distinct fields. */
33
+ export interface ArchiveOptions {
34
+ /** Umbrella skill this one was consolidated into; when set it must exist (consolidate semantics). */
35
+ absorbedInto?: string;
36
+ /** Human-readable reason written to `.archive-reason`; default derives from `absorbedInto`. */
37
+ reason?: string;
38
+ /** Permit archiving a bundled skill (curator prune-builtins only; hub-installed and pinned stay protected). */
39
+ allowBundled?: boolean;
40
+ }
36
41
  export declare function skillsRoot(env?: NodeJS.ProcessEnv): string;
37
42
  export interface Frontmatter {
38
43
  name?: string;
@@ -51,27 +56,31 @@ export declare class SkillLibrary {
51
56
  constructor(root?: string, io?: EvolutionIoLike, limits?: SkillLimits);
52
57
  list(): Promise<SkillSummary[]>;
53
58
  read(name: string): Promise<string | null>;
54
- writeProtection(name: string): Promise<string | null>;
55
- deleteProtection(name: string): Promise<string | null>;
59
+ writeProtection(name: string, origin?: WriteOrigin): Promise<string | null>;
60
+ deleteProtection(name: string, options?: {
61
+ allowBundled?: boolean;
62
+ }): Promise<string | null>;
56
63
  isManaged(name: string): Promise<boolean>;
64
+ /** Whether the skill carries the bundled marker (curator prune-builtins eligibility). */
65
+ isBundled(name: string): Promise<boolean>;
57
66
  create(name: string, content: string, origin: 'foreground' | 'background_review'): Promise<SkillActionResult>;
58
- update(name: string, content: string): Promise<SkillActionResult>;
59
- patch(name: string, oldString: string, newString: string, filePath?: string, replaceAll?: boolean): Promise<SkillActionResult>;
60
- archive(name: string, absorbedInto?: string): Promise<SkillActionResult>;
67
+ update(name: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
68
+ patch(name: string, oldString: string, newString: string, filePath?: string, replaceAll?: boolean, origin?: WriteOrigin): Promise<SkillActionResult>;
69
+ archive(name: string, options?: ArchiveOptions): Promise<SkillActionResult>;
61
70
  /**
62
71
  * Merge the bodies of `sources` into `target` and archive the sources with
63
72
  * an absorbed-into marker. Hermes-style consolidation: overlapping skills
64
73
  * collapse into one, and the originals stay recoverable under `.archive/`.
65
74
  */
66
- consolidate(target: string, sources: string[]): Promise<SkillActionResult>;
75
+ consolidate(target: string, sources: string[], origin?: WriteOrigin): Promise<SkillActionResult>;
67
76
  /**
68
77
  * Restore one skill from `.archive/` back to the active root. Hermes-style
69
78
  * recoverability: archival never deletes, and this is the control-plane
70
79
  * path back. The `.archive-reason` marker is dropped on restore.
71
80
  */
72
81
  restoreFromArchive(name: string): Promise<SkillActionResult>;
73
- writeSupportFile(name: string, filePath: string, content: string): Promise<SkillActionResult>;
74
- removeSupportFile(name: string, filePath: string): Promise<SkillActionResult>;
82
+ writeSupportFile(name: string, filePath: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
83
+ removeSupportFile(name: string, filePath: string, origin?: WriteOrigin): Promise<SkillActionResult>;
75
84
  snapshotAll(reason?: string): Promise<string>;
76
85
  listSnapshots(): Promise<Array<{
77
86
  path: string;
@@ -30,4 +30,12 @@ export declare function bumpUse(map: UsageMap, name: string, when?: Date): void;
30
30
  export declare function bumpPatch(map: UsageMap, name: string, when?: Date): void;
31
31
  export declare function markAgentCreated(map: UsageMap, name: string): void;
32
32
  export declare function latestActivityAt(record: UsageRecord): string | null;
33
+ /**
34
+ * Curator suppression sidecar: built-in skills the curator has archived stay
35
+ * suppressed across re-seeds, so the lifecycle never fights a re-created
36
+ * bundled skill. Best-effort load/save, mirroring the usage sidecar posture.
37
+ */
38
+ export declare function suppressedFile(root: string): string;
39
+ export declare function loadSuppressedNames(root: string, io?: EvolutionIoLike): Promise<ReadonlySet<string>>;
40
+ export declare function saveSuppressedNames(root: string, names: ReadonlySet<string>, io?: EvolutionIoLike): Promise<void>;
33
41
  //# sourceMappingURL=usage.d.ts.map
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.1.0-rc.11",
4
+ "version": "0.1.0-rc.13",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },