@lmzhen/dsh-evolution-core 0.1.0-rc.14 → 0.1.0-rc.16

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
@@ -215,6 +215,10 @@ const MAX_SKILL_CONTENT_CHARS = 1e5;
215
215
  const MAX_SKILL_FILE_BYTES = 1048576;
216
216
  const DEFAULT_REVIEW_MEMORY_INTERVAL = 10;
217
217
  const DEFAULT_REVIEW_SKILL_INTERVAL = 10;
218
+ /** Skill-review completion channel trigger mode: 'cadence' | 'completion' | 'both'. */
219
+ const DEFAULT_SKILL_REVIEW_TRIGGER = "both";
220
+ /** Cumulative session tool calls before a session counts as "proven long" for the completion channel. */
221
+ const DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS = 20;
218
222
  const DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS = 3;
219
223
  const DEFAULT_SUBSTANTIVE_MIN_USER_CHARS = 200;
220
224
  const DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS = 500;
@@ -245,6 +249,47 @@ function buildCuratorRunReport(input) {
245
249
  ...input.snapshotPath === void 0 ? {} : { snapshotPath: input.snapshotPath }
246
250
  };
247
251
  }
252
+ const NOMINATION_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
253
+ /**
254
+ * Parse the curator LLM's YAML nomination block (consolidations + prunings).
255
+ * Line-oriented and lenient by design: the LLM output is advisory, every name
256
+ * is re-validated against the tree before any file move happens downstream.
257
+ */
258
+ function parseCuratorNominations(text) {
259
+ const prunings = [];
260
+ const consolidations = [];
261
+ let section = null;
262
+ let currentFrom = "";
263
+ for (const line of text.split("\n")) {
264
+ const consolidated = /^\s*-\s*from:\s*([a-z0-9][a-z0-9-]*)\s*$/.exec(line);
265
+ if (consolidated) {
266
+ section = "consolidations";
267
+ currentFrom = consolidated[1] ?? "";
268
+ continue;
269
+ }
270
+ const into = /^\s*into:\s*([a-z0-9][a-z0-9-]*)\s*$/.exec(line);
271
+ if (into) {
272
+ const intoName = into[1] ?? "";
273
+ if (section === "consolidations" && currentFrom !== "" && currentFrom !== intoName) consolidations.push({
274
+ from: currentFrom,
275
+ into: intoName
276
+ });
277
+ currentFrom = "";
278
+ continue;
279
+ }
280
+ const pruned = /^\s*-\s*name:\s*([a-z0-9][a-z0-9-]*)\s*$/.exec(line);
281
+ if (pruned) {
282
+ section = "prunings";
283
+ const name = pruned[1];
284
+ if (name) prunings.push(name);
285
+ }
286
+ }
287
+ const valid = (name) => NOMINATION_NAME_RE.test(name);
288
+ return {
289
+ prunings: prunings.filter(valid),
290
+ consolidations: consolidations.filter((item) => valid(item.from) && valid(item.into))
291
+ };
292
+ }
248
293
  function daysSince(iso, created, now) {
249
294
  return (now - new Date(iso ?? created).getTime()) / 864e5;
250
295
  }
@@ -259,6 +304,7 @@ function computeLifecycleTransitions(usage, config, now = /* @__PURE__ */ new Da
259
304
  if (record.pinned) continue;
260
305
  if (config.excludeSkillNames?.has(name)) continue;
261
306
  if (config.suppressedNames?.has(name)) continue;
307
+ if (config.referencedSkillNames?.has(name)) continue;
262
308
  const bundled = config.bundledNames?.has(name) === true;
263
309
  if (!(record.created_by === "agent" || config.manageUnmanaged === true) && !(config.pruneBuiltins === true && bundled)) continue;
264
310
  if (PROTECTED_BUILTIN_SKILLS.has(name)) continue;
@@ -882,6 +928,39 @@ var MemoryStore = class {
882
928
  }
883
929
  };
884
930
  //#endregion
931
+ //#region lib/types/mutations.js
932
+ /**
933
+ * Curator/author audit trail: `.mutations.json` records every skill mutation
934
+ * with before/after content hashes so any automated edit is reviewable and
935
+ * replayable. Best-effort persistence, mirroring the usage sidecar posture.
936
+ * @module @lmzhen/dsh-evolution-core
937
+ */
938
+ const DEFAULT_MUTATION_CAP = 500;
939
+ function mutationsFile(root) {
940
+ return join(root, ".mutations.json");
941
+ }
942
+ function contentHash(content) {
943
+ return createHash("sha256").update(content).digest("hex");
944
+ }
945
+ async function loadMutations(root, io = nodeEvolutionIo()) {
946
+ const raw = await io.readText(mutationsFile(root));
947
+ if (raw === null) return [];
948
+ try {
949
+ const parsed = JSON.parse(raw);
950
+ if (!Array.isArray(parsed)) return [];
951
+ return parsed.filter((entry) => typeof entry === "object" && entry !== null && typeof entry.skillName === "string" && typeof entry.action === "string");
952
+ } catch {
953
+ return [];
954
+ }
955
+ }
956
+ /** Append one record, trim to `cap`, and write atomically. */
957
+ async function recordMutation(root, io, record, cap = 500) {
958
+ const existing = await loadMutations(root, io);
959
+ existing.push(record);
960
+ const trimmed = existing.length > cap ? existing.slice(existing.length - cap) : existing;
961
+ await io.writeText(mutationsFile(root), JSON.stringify(trimmed, null, 2));
962
+ }
963
+ //#endregion
885
964
  //#region lib/types/prompts.js
886
965
  /**
887
966
  * Review and curation prompts adapted from Hermes Agent
@@ -938,23 +1017,54 @@ Review the conversation above and update two things.
938
1017
  **Skills**: how to do this class of task. Be ACTIVE. Follow the same class-level umbrella policy, preference order, protected-skill rules, and do-not-capture list as a skill review.
939
1018
 
940
1019
  Act on whichever dimension has real signal. If genuinely nothing stands out on either, say "Nothing to save." and stop — but don't reach for that conclusion as a default.`;
941
- const CURATOR_PROMPT = `You are the skill curator. Maintain a healthy, class-level skill library.
1020
+ const CURATOR_PROMPT = `You are the skill curator. Maintain a healthy, class-level skill library, not a flat pile of narrow one-session skills.
1021
+
1022
+ The 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.
1023
+
1024
+ Right target shape: class-level skills with rich SKILL.md + references/, templates/, scripts/ support files for session-specific detail.
942
1025
 
943
- Rules:
944
- 1. NEVER hard-delete a skill. Archive is the maximum destructive action.
945
- 2. Do not touch bundled, hub-installed, or pinned skills.
946
- 3. Do not archive recently-created or never-used skills without strong evidence.
947
- 4. Prefer merging narrow skills into class-level umbrellas.
948
- 5. Before archiving a merged skill, ensure its unique content was preserved.
1026
+ Hard rules:
1027
+ 1. NEVER hard-delete a skill. Archive (moving to .archive/) is the maximum destructive action; archives are recoverable, deletion is not.
1028
+ 2. Do not touch bundled, hub-installed, pinned, or scheduled-task-referenced (\`referenced\`) skills. Referenced skills MAY be consolidated into an umbrella, but never simply pruned.
1029
+ 3. Do not archive recently-created or never-used skills without strong evidence. "use=0" is NOT evidence either way — it only means the trigger has not come up yet.
1030
+ 4. 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.
1031
+ 5. Judge overlap on CONTENT, not on usage counters.
1032
+ 6. Before archiving a merged skill, ensure its unique content was preserved in the umbrella.
949
1033
 
950
- Produce a YAML summary:
1034
+ How to work:
1035
+ 1. Scan the candidate list. Identify PREFIX CLUSTERS — skills sharing a first word or domain keyword (expect 10-25 clusters).
1036
+ 2. For each cluster with 2+ members, ask "what is the UMBRELLA CLASS these skills serve?" and consolidate:
1037
+ a. MERGE INTO AN EXISTING UMBRELLA (patch a labeled section for each sibling's unique insight, then archive the siblings).
1038
+ b. CREATE A NEW UMBRELLA SKILL.md covering the shared workflow with short labeled subsections, then archive the absorbed siblings.
1039
+ c. DEMOTE session-specific detail to references/, templates/, or scripts/ under the umbrella.
1040
+ 3. Keep the umbrella body tight and scannable: exact commands, verbatim paths, ~100-200 lines; never invent flags or APIs.
1041
+
1042
+ Produce a YAML summary with exactly this shape:
951
1043
  consolidations:
952
1044
  - from: <old-skill-name>
953
1045
  into: <umbrella-skill-name>
954
1046
  reason: <one short sentence>
955
1047
  prunings:
956
1048
  - name: <skill-name>
957
- reason: <one short sentence>`;
1049
+ reason: <one short sentence>
1050
+ Nominate a pruning only when archival is clearly safe (stale AND genuinely obsolete or fully absorbed elsewhere).`;
1051
+ const CURATOR_DRY_RUN_BANNER = `═══════════════════════════════════════════════════════════════
1052
+ DRY-RUN — REPORT ONLY. DO NOT MUTATE THE SKILL LIBRARY.
1053
+ ═══════════════════════════════════════════════════════════════
1054
+
1055
+ This is a PREVIEW pass. Follow every instruction above EXCEPT:
1056
+ • Do NOT call skill_manage with action=create, update, patch, delete, write_file, or remove_file.
1057
+ • Do NOT move, copy, or rewrite any file under the skills tree.
1058
+
1059
+ Your 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.
1060
+
1061
+ If you accidentally take a mutating action, say so explicitly in the summary.`;
1062
+ const COMPLETION_SKILL_REVIEW_PROMPT = `[Auto-review — Skills · task complete]
1063
+ Your current task now appears complete. Before wrapping up, review the approach and update the skill library via skill_manage.
1064
+
1065
+ Follow the skills review policy: be ACTIVE, prefer class-level umbrellas, patch skills loaded 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.
1066
+
1067
+ Do NOT modify output files or re-run the task. If you are still mid-task, ignore this.`;
958
1068
  function reviewPrompt(kind) {
959
1069
  if (kind === "memory") return MEMORY_REVIEW_PROMPT;
960
1070
  if (kind === "skill") return SKILL_REVIEW_PROMPT;
@@ -980,7 +1090,8 @@ const PROMPT_BUNDLE = createPromptBundle({
980
1090
  memory: MEMORY_REVIEW_PROMPT,
981
1091
  skill: SKILL_REVIEW_PROMPT,
982
1092
  combined: COMBINED_REVIEW_PROMPT,
983
- curator: CURATOR_PROMPT
1093
+ curator: CURATOR_PROMPT,
1094
+ completion: COMPLETION_SKILL_REVIEW_PROMPT
984
1095
  });
985
1096
  function verifyPromptBundle(bundle = PROMPT_BUNDLE) {
986
1097
  const canonical = JSON.stringify({
@@ -1021,6 +1132,143 @@ Quality bar:
1021
1132
  - No router/index/hub skills that only point at other skills.
1022
1133
  - References go in \`references/\`, templates in \`templates/\`.`;
1023
1134
  //#endregion
1135
+ //#region lib/types/quality.js
1136
+ /**
1137
+ * Quality scoring and near-duplicate detection for the curated skill library.
1138
+ *
1139
+ * Pure functions over data inputs so the scoring policy is unit-testable and
1140
+ * the same math feeds the usage sidecar, the `skill_manage review` surface and
1141
+ * the learning graph. Weights follow the Hermes/hermes-claw six-factor model;
1142
+ * mutation maturity is a documented DSH approximation (single per-month patch
1143
+ * trend ratio replaces the claw timestamp-trend formula, since DSH usage
1144
+ * records only carry the last patched timestamp).
1145
+ * @module @lmzhen/dsh-evolution-core
1146
+ */
1147
+ const QUALITY_WEIGHTS = {
1148
+ usageFrequency: .25,
1149
+ stability: .2,
1150
+ recency: .2,
1151
+ references: .1,
1152
+ mutationMaturity: .2,
1153
+ richness: .05
1154
+ };
1155
+ /** Score below which a skill is flagged for review. */
1156
+ const LOW_QUALITY_THRESHOLD = .3;
1157
+ function clamp01(value) {
1158
+ return Math.max(0, Math.min(1, value));
1159
+ }
1160
+ function daysBetween(from, now) {
1161
+ return Math.max(0, (now.getTime() - new Date(from).getTime()) / 864e5);
1162
+ }
1163
+ function computeQualityScores(input) {
1164
+ const now = input.now ?? /* @__PURE__ */ new Date();
1165
+ const scores = /* @__PURE__ */ new Map();
1166
+ for (const [name, record] of input.usage) {
1167
+ const ageDays = Math.max(1, daysBetween(record.created_at, now));
1168
+ const idleDays = daysBetween(latestActivityAt(record) ?? record.created_at, now);
1169
+ const patchCount = record.patch_count;
1170
+ const useCount = record.use_count;
1171
+ const usageFrequency = clamp01(useCount / ageDays);
1172
+ const stability = useCount === 0 ? 1 : clamp01(1 - patchCount / useCount);
1173
+ const recency = idleDays < 30 ? 1 : clamp01(1 - (idleDays - 30) / 150);
1174
+ const references = clamp01((input.referenceCounts?.get(name) ?? 0) / 3);
1175
+ const mutationMaturity = patchCount === 0 ? .3 : patchCount === 1 ? .4 : clamp01((patchCount - 1) / Math.max(1, ageDays / 30));
1176
+ const richness = clamp01((input.supportDirs?.get(name) ?? 0) * .175);
1177
+ const factors = {
1178
+ usageFrequency,
1179
+ stability,
1180
+ recency,
1181
+ references,
1182
+ mutationMaturity,
1183
+ richness
1184
+ };
1185
+ const score = usageFrequency * QUALITY_WEIGHTS.usageFrequency + stability * QUALITY_WEIGHTS.stability + recency * QUALITY_WEIGHTS.recency + references * QUALITY_WEIGHTS.references + mutationMaturity * QUALITY_WEIGHTS.mutationMaturity + richness * QUALITY_WEIGHTS.richness;
1186
+ scores.set(name, {
1187
+ score,
1188
+ factors,
1189
+ warn: score < LOW_QUALITY_THRESHOLD
1190
+ });
1191
+ }
1192
+ return scores;
1193
+ }
1194
+ function normalize(content) {
1195
+ return content.toLowerCase().replace(/\s+/g, " ").trim();
1196
+ }
1197
+ function contentHash$1(content) {
1198
+ return createHash("sha256").update(normalize(content)).digest("hex");
1199
+ }
1200
+ function tokenize(content) {
1201
+ return new Set(normalize(content).split(/[^a-z0-9\u4e00-\u9fff]+/).filter(Boolean));
1202
+ }
1203
+ function jaccard(a, b) {
1204
+ if (a.size === 0 || b.size === 0) return 0;
1205
+ let intersection = 0;
1206
+ for (const token of a) if (b.has(token)) intersection += 1;
1207
+ return intersection / (a.size + b.size - intersection);
1208
+ }
1209
+ /**
1210
+ * Two-phase near-duplicate clustering: exact normalized-hash groups first,
1211
+ * then token-Jaccard edges at {@link DEDUP_SIMILARITY_THRESHOLD} with a token
1212
+ * ratio guard, union-find across the whole set.
1213
+ */
1214
+ function computeDedupGroups(input) {
1215
+ const threshold = input.threshold ?? .95;
1216
+ const names = [...input.contents.keys()];
1217
+ const hashes = /* @__PURE__ */ new Map();
1218
+ for (const name of names) {
1219
+ const hash = contentHash$1(input.contents.get(name) ?? "");
1220
+ const bucket = hashes.get(hash);
1221
+ if (bucket) bucket.push(name);
1222
+ else hashes.set(hash, [name]);
1223
+ }
1224
+ const parent = /* @__PURE__ */ new Map();
1225
+ const find = (x) => {
1226
+ const root = parent.get(x) ?? x;
1227
+ if (root !== x) parent.set(x, find(root));
1228
+ return parent.get(x) ?? x;
1229
+ };
1230
+ const union = (a, b) => {
1231
+ const [ra, rb] = [find(a), find(b)];
1232
+ if (ra !== rb) parent.set(rb, ra);
1233
+ };
1234
+ for (const [hash, bucketNames] of hashes) {
1235
+ const first = bucketNames[0];
1236
+ if (first === void 0 || bucketNames.length === 1) continue;
1237
+ for (let index = 1; index < bucketNames.length; index += 1) {
1238
+ const peer = bucketNames[index];
1239
+ if (peer) union(first, peer);
1240
+ }
1241
+ }
1242
+ const tokens = /* @__PURE__ */ new Map();
1243
+ const tokenSet = (name) => {
1244
+ let set = tokens.get(name);
1245
+ if (!set) {
1246
+ set = tokenize(input.contents.get(name) ?? "");
1247
+ tokens.set(name, set);
1248
+ }
1249
+ return set;
1250
+ };
1251
+ for (let index = 0; index < names.length; index += 1) {
1252
+ const a = names[index];
1253
+ if (a === void 0) continue;
1254
+ for (let other = index + 1; other < names.length; other += 1) {
1255
+ const b = names[other];
1256
+ if (b === void 0) continue;
1257
+ const [ta, tb] = [tokenSet(a), tokenSet(b)];
1258
+ if (Math.max(ta.size, tb.size) / Math.max(1, Math.min(ta.size, tb.size)) > 5) continue;
1259
+ if (jaccard(ta, tb) >= threshold) union(a, b);
1260
+ }
1261
+ }
1262
+ const groups = /* @__PURE__ */ new Map();
1263
+ for (const name of names) {
1264
+ const root = find(name);
1265
+ const group = groups.get(root);
1266
+ if (group) group.push(name);
1267
+ else groups.set(root, [name]);
1268
+ }
1269
+ return [...groups.values()].filter((group) => group.length > 1);
1270
+ }
1271
+ //#endregion
1024
1272
  //#region lib/types/signals.js
1025
1273
  /**
1026
1274
  * Deterministic review signal gate.
@@ -1236,6 +1484,41 @@ var SkillLibrary = class {
1236
1484
  const dir = skillDir(this.root, name);
1237
1485
  return await this.io.exists(markerPath(dir, "bundled"));
1238
1486
  }
1487
+ /** Count non-empty support subdirectories (richness input for quality scoring). */
1488
+ async countSupportDirs(name) {
1489
+ const dir = skillDir(this.root, name);
1490
+ let entries;
1491
+ try {
1492
+ entries = await this.io.list(dir);
1493
+ } catch {
1494
+ return 0;
1495
+ }
1496
+ let count = 0;
1497
+ for (const subdir of SUPPORT_DIRS) {
1498
+ if (!entries.includes(subdir)) continue;
1499
+ try {
1500
+ if ((await this.io.list(join(dir, subdir))).some((file) => file !== ".gitkeep")) count += 1;
1501
+ } catch {}
1502
+ }
1503
+ return count;
1504
+ }
1505
+ /** Best-effort audit trail entry; never blocks the mutation. */
1506
+ async audit(skillName, action, before, after, summary) {
1507
+ try {
1508
+ await recordMutation(this.root, this.io, {
1509
+ skillName,
1510
+ action,
1511
+ ...before === null ? {} : { beforeHash: contentHash(before) },
1512
+ ...after === null ? {} : { afterHash: contentHash(after) },
1513
+ summary,
1514
+ at: (/* @__PURE__ */ new Date()).toISOString()
1515
+ });
1516
+ } catch {}
1517
+ }
1518
+ /** Recent mutation audit records (read-only inspection surface). */
1519
+ async listMutations() {
1520
+ return await loadMutations(this.root, this.io);
1521
+ }
1239
1522
  async create(name, content, origin) {
1240
1523
  const normalized = name.trim();
1241
1524
  if (!SKILL_NAME_RE.test(normalized) || normalized.length > this.limits.maxNameLength) return {
@@ -1259,6 +1542,7 @@ var SkillLibrary = class {
1259
1542
  };
1260
1543
  await this.io.writeText(join(dir, "SKILL.md"), content.trimEnd() + "\n");
1261
1544
  if (origin === "background_review") await this.io.writeText(markerPath(dir, "hermes-managed"), "");
1545
+ await this.audit(normalized, "create", null, content, "created");
1262
1546
  return {
1263
1547
  ok: true,
1264
1548
  message: `Skill "${normalized}" created.`,
@@ -1267,7 +1551,8 @@ var SkillLibrary = class {
1267
1551
  }
1268
1552
  async update(name, content, origin = "foreground") {
1269
1553
  const dir = skillDir(this.root, name);
1270
- if (!await this.io.readText(join(dir, "SKILL.md"))) return {
1554
+ const md = await this.io.readText(join(dir, "SKILL.md"));
1555
+ if (!md) return {
1271
1556
  ok: false,
1272
1557
  message: `Skill "${name}" not found.`
1273
1558
  };
@@ -1287,6 +1572,7 @@ var SkillLibrary = class {
1287
1572
  message: threat
1288
1573
  };
1289
1574
  await this.io.writeText(join(dir, "SKILL.md"), content.trimEnd() + "\n");
1575
+ await this.audit(name, "update", md, content, "updated");
1290
1576
  return {
1291
1577
  ok: true,
1292
1578
  message: `Skill "${name}" updated.`,
@@ -1347,6 +1633,7 @@ var SkillLibrary = class {
1347
1633
  message: threat
1348
1634
  };
1349
1635
  await this.io.writeText(target, patched.trimEnd() + "\n");
1636
+ await this.audit(name, "patch", md, patched, `patched ${patchLabel}`);
1350
1637
  return {
1351
1638
  ok: true,
1352
1639
  message: `Skill "${name}" patched (${patchLabel}).`,
@@ -1355,7 +1642,8 @@ var SkillLibrary = class {
1355
1642
  }
1356
1643
  async archive(name, options = {}) {
1357
1644
  const dir = skillDir(this.root, name);
1358
- if (!await this.io.readText(join(dir, "SKILL.md"))) return {
1645
+ const md = await this.io.readText(join(dir, "SKILL.md"));
1646
+ if (!md) return {
1359
1647
  ok: false,
1360
1648
  message: `Skill "${name}" not found.`
1361
1649
  };
@@ -1381,6 +1669,7 @@ var SkillLibrary = class {
1381
1669
  }
1382
1670
  const reason = options.reason ?? (options.absorbedInto ? `Consolidated into ${options.absorbedInto}` : "Archived by self-evolution curator");
1383
1671
  await this.io.writeText(join(dest, ".archive-reason"), `${(/* @__PURE__ */ new Date()).toISOString()}: ${reason}\n`);
1672
+ await this.audit(name, "archive", md, null, reason);
1384
1673
  return {
1385
1674
  ok: true,
1386
1675
  message: `Skill "${name}" archived to .archive.`,
@@ -1535,7 +1824,9 @@ var SkillLibrary = class {
1535
1824
  message: threat
1536
1825
  };
1537
1826
  const target = join(dir, ...filePath.replace(/\\/g, "/").split("/").filter(Boolean));
1827
+ const existing = await this.io.readText(target).catch(() => null);
1538
1828
  await this.io.writeText(target, content);
1829
+ await this.audit(name, "write_file", existing, content, `wrote ${filePath}`);
1539
1830
  return {
1540
1831
  ok: true,
1541
1832
  message: `Support file "${filePath}" written to "${name}".`,
@@ -1563,7 +1854,9 @@ var SkillLibrary = class {
1563
1854
  ok: false,
1564
1855
  message: `File "${filePath}" not found in skill "${name}".`
1565
1856
  };
1857
+ const before = await this.io.readText(target).catch(() => null);
1566
1858
  await this.io.remove(target);
1859
+ await this.audit(name, "remove_file", before, null, `removed ${filePath}`);
1567
1860
  return {
1568
1861
  ok: true,
1569
1862
  message: `Support file "${filePath}" removed from "${name}".`,
@@ -1691,4 +1984,4 @@ var JsonState = class JsonState {
1691
1984
  }
1692
1985
  };
1693
1986
  //#endregion
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 };
1987
+ export { COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, JsonState, LOW_QUALITY_THRESHOLD, 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, QUALITY_WEIGHTS, SKILL_NAME_RE, SKILL_REVIEW_PROMPT, SUPPORT_DIRS, SkillLibrary, advanceReview, buildCuratorRunReport, bumpPatch, bumpUse, bumpView, computeDedupGroups, computeLifecycleTransitions, computeQualityScores, contentHash, emptyRecord, evaluateThreat, evolutionHome, evolutionIoAdapter, foldTurn, getRecord, latestActivityAt, loadMutations, loadSuppressedNames, loadUsage, markAgentCreated, memoryRoot, mutationsFile, nodeEvolutionIo, observeEvent, parseCuratorNominations, parseFrontmatter, recordMutation, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, usageFile, validateFrontmatter, verifyPromptBundle };
@@ -33,6 +33,10 @@ export declare const MAX_SKILL_CONTENT_CHARS = 100000;
33
33
  export declare const MAX_SKILL_FILE_BYTES = 1048576;
34
34
  export declare const DEFAULT_REVIEW_MEMORY_INTERVAL = 10;
35
35
  export declare const DEFAULT_REVIEW_SKILL_INTERVAL = 10;
36
+ /** Skill-review completion channel trigger mode: 'cadence' | 'completion' | 'both'. */
37
+ export declare const DEFAULT_SKILL_REVIEW_TRIGGER: "both";
38
+ /** Cumulative session tool calls before a session counts as "proven long" for the completion channel. */
39
+ export declare const DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS = 20;
36
40
  export declare const DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS = 3;
37
41
  export declare const DEFAULT_SUBSTANTIVE_MIN_USER_CHARS = 200;
38
42
  export declare const DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS = 500;
@@ -19,6 +19,8 @@ export interface CuratorConfig {
19
19
  bundledNames?: ReadonlySet<string>;
20
20
  /** Skill names the curator archived once and must not fight across re-seeds. */
21
21
  suppressedNames?: ReadonlySet<string>;
22
+ /** Skills referenced by scheduled/automated jobs: never auto-transitioned (idle clocks mislead for rarely-firing tasks). */
23
+ referencedSkillNames?: ReadonlySet<string>;
22
24
  }
23
25
  export interface CuratorTransition {
24
26
  name: string;
@@ -64,5 +66,21 @@ export interface CuratorReportInput {
64
66
  snapshotPath?: string;
65
67
  }
66
68
  export declare function buildCuratorRunReport(input: CuratorReportInput): CuratorRunReport;
69
+ /** One LLM-nominated consolidation: `from` merges into the umbrella `into`. */
70
+ export interface CuratorConsolidation {
71
+ from: string;
72
+ into: string;
73
+ }
74
+ /** Structured result of the optional curator LLM nomination pass. */
75
+ export interface CuratorNominations {
76
+ prunings: string[];
77
+ consolidations: CuratorConsolidation[];
78
+ }
79
+ /**
80
+ * Parse the curator LLM's YAML nomination block (consolidations + prunings).
81
+ * Line-oriented and lenient by design: the LLM output is advisory, every name
82
+ * is re-validated against the tree before any file move happens downstream.
83
+ */
84
+ export declare function parseCuratorNominations(text: string): CuratorNominations;
67
85
  export declare function computeLifecycleTransitions(usage: UsageMap, config: CuratorConfig, now?: Date): CuratorResult;
68
86
  //# sourceMappingURL=curator.d.ts.map
@@ -11,7 +11,9 @@ export * from './curator.ts';
11
11
  export * from './events.ts';
12
12
  export * from './io.ts';
13
13
  export * from './memory-store.ts';
14
+ export * from './mutations.ts';
14
15
  export * from './prompts.ts';
16
+ export * from './quality.ts';
15
17
  export * from './signals.ts';
16
18
  export * from './skill-store.ts';
17
19
  export * from './state-store.ts';
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Curator/author audit trail: `.mutations.json` records every skill mutation
3
+ * with before/after content hashes so any automated edit is reviewable and
4
+ * replayable. Best-effort persistence, mirroring the usage sidecar posture.
5
+ * @module @deepseek-ai/dsh-evolution-core
6
+ */
7
+ import { type EvolutionIoLike } from './io.ts';
8
+ export interface MutationRecord {
9
+ skillName: string;
10
+ action: string;
11
+ beforeHash?: string;
12
+ afterHash?: string;
13
+ summary: string;
14
+ at: string;
15
+ }
16
+ export declare const DEFAULT_MUTATION_CAP = 500;
17
+ export declare function mutationsFile(root: string): string;
18
+ export declare function contentHash(content: string): string;
19
+ export declare function loadMutations(root: string, io?: EvolutionIoLike): Promise<MutationRecord[]>;
20
+ /** Append one record, trim to `cap`, and write atomically. */
21
+ export declare function recordMutation(root: string, io: EvolutionIoLike, record: MutationRecord, cap?: number): Promise<void>;
22
+ //# sourceMappingURL=mutations.d.ts.map
@@ -2,7 +2,9 @@ export declare const PROMPT_BUNDLE_ID = "dsh-evolution@1";
2
2
  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.";
3
3
  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.\n\nTarget shape: CLASS-LEVEL skills with a rich SKILL.md and a references/ directory for session-specific detail. Not a flat list of narrow one-session skills.\n\nSignals that warrant action:\n- The user corrected your style, tone, format, verbosity, workflow, or approach.\n- A non-trivial technique, fix, workaround, or debugging path emerged.\n- A loaded skill turned out wrong, missing, or outdated \u2014 patch it now.\n\nPreference order:\n1. Patch a skill that was loaded or read this session.\n2. Patch an existing umbrella skill.\n3. Add references/, templates/, or scripts/ support under an existing skill.\n4. Create a new class-level umbrella skill only when nothing fits.\n\nProtected skills (bundled/hub-installed) must not be edited. Pinned skills may be patched but not archived.\n\nDo NOT capture:\n- Environment-dependent failures (missing binaries, unconfigured credentials).\n- Negative claims about tools (\"browser tools do not work\").\n- Transient errors that resolved during the session.\n- One-off task narratives.\n\nIf a tool failed because of setup state, capture the FIX under an existing setup 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.";
4
4
  export declare const COMBINED_REVIEW_PROMPT = "[Auto-review]\nReview the conversation above and update two things.\n\n**Memory**: who the user is. Save durable user preferences, personal details, and expectations with the memory tool.\n\n**Skills**: how to do this class of task. Be ACTIVE. Follow the same class-level umbrella policy, preference order, protected-skill rules, and do-not-capture list as a skill review.\n\nAct on whichever dimension 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.";
5
- export declare const CURATOR_PROMPT = "You are the skill curator. Maintain a healthy, class-level skill library.\n\nRules:\n1. NEVER hard-delete a skill. Archive is the maximum destructive action.\n2. Do not touch bundled, hub-installed, or pinned skills.\n3. Do not archive recently-created or never-used skills without strong evidence.\n4. Prefer merging narrow skills into class-level umbrellas.\n5. Before archiving a merged skill, ensure its unique content was preserved.\n\nProduce a YAML summary:\nconsolidations:\n - from: <old-skill-name>\n into: <umbrella-skill-name>\n reason: <one short sentence>\nprunings:\n - name: <skill-name>\n reason: <one short sentence>";
5
+ 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\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 MAY be consolidated into an umbrella, but never simply pruned.\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.\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 (expect 10-25 clusters).\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.\n3. Keep the umbrella body tight and scannable: exact commands, verbatim paths, ~100-200 lines; never invent flags or APIs.\n\nProduce a YAML summary with exactly this shape:\nconsolidations:\n - from: <old-skill-name>\n into: <umbrella-skill-name>\n reason: <one short sentence>\nprunings:\n - name: <skill-name>\n reason: <one short sentence>\nNominate a pruning only when archival is clearly safe (stale AND genuinely obsolete or fully absorbed elsewhere).";
6
+ 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.";
7
+ 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 skills loaded 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.";
6
8
  export declare function reviewPrompt(kind: 'memory' | 'skill' | 'combined'): string;
7
9
  export interface PromptBundle {
8
10
  id: string;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Quality scoring and near-duplicate detection for the curated skill library.
3
+ *
4
+ * Pure functions over data inputs so the scoring policy is unit-testable and
5
+ * the same math feeds the usage sidecar, the `skill_manage review` surface and
6
+ * the learning graph. Weights follow the Hermes/hermes-claw six-factor model;
7
+ * mutation maturity is a documented DSH approximation (single per-month patch
8
+ * trend ratio replaces the claw timestamp-trend formula, since DSH usage
9
+ * records only carry the last patched timestamp).
10
+ * @module @deepseek-ai/dsh-evolution-core
11
+ */
12
+ import type { UsageMap } from './usage.ts';
13
+ export interface QualityFactors {
14
+ /** 0.25 — use_count per day of age, capped at 1. */
15
+ usageFrequency: number;
16
+ /** 0.20 — 1 − patch/use (zero use = stable). */
17
+ stability: number;
18
+ /** 0.20 — 1 under 30 idle days, linear decay to 0 at 180. */
19
+ recency: number;
20
+ /** 0.10 — in-degree / 3 (graph references), capped at 1. */
21
+ references: number;
22
+ /** 0.20 — patch cadence maturity (DSH approximation of the trend formula). */
23
+ mutationMaturity: number;
24
+ /** 0.05 — non-empty support subdirectories × 0.175, capped at 1. */
25
+ richness: number;
26
+ }
27
+ export interface QualityScore {
28
+ score: number;
29
+ factors: QualityFactors;
30
+ warn: boolean;
31
+ }
32
+ export declare const QUALITY_WEIGHTS: {
33
+ readonly usageFrequency: 0.25;
34
+ readonly stability: 0.2;
35
+ readonly recency: 0.2;
36
+ readonly references: 0.1;
37
+ readonly mutationMaturity: 0.2;
38
+ readonly richness: 0.05;
39
+ };
40
+ /** Score below which a skill is flagged for review. */
41
+ export declare const LOW_QUALITY_THRESHOLD = 0.3;
42
+ export declare function computeQualityScores(input: {
43
+ usage: UsageMap;
44
+ referenceCounts?: ReadonlyMap<string, number>;
45
+ supportDirs?: ReadonlyMap<string, number>;
46
+ now?: Date;
47
+ }): Map<string, QualityScore>;
48
+ /**
49
+ * Two-phase near-duplicate clustering: exact normalized-hash groups first,
50
+ * then token-Jaccard edges at {@link DEDUP_SIMILARITY_THRESHOLD} with a token
51
+ * ratio guard, union-find across the whole set.
52
+ */
53
+ export declare function computeDedupGroups(input: {
54
+ contents: ReadonlyMap<string, string>;
55
+ threshold?: number;
56
+ }): string[][];
57
+ //# sourceMappingURL=quality.d.ts.map
@@ -7,6 +7,7 @@
7
7
  * move to `.archive/` — never a hard delete.
8
8
  */
9
9
  import { type EvolutionIoLike } from './io.ts';
10
+ import { type MutationRecord } from './mutations.ts';
10
11
  export interface SkillLimits {
11
12
  maxNameLength: number;
12
13
  maxDescriptionLength: number;
@@ -63,6 +64,12 @@ export declare class SkillLibrary {
63
64
  isManaged(name: string): Promise<boolean>;
64
65
  /** Whether the skill carries the bundled marker (curator prune-builtins eligibility). */
65
66
  isBundled(name: string): Promise<boolean>;
67
+ /** Count non-empty support subdirectories (richness input for quality scoring). */
68
+ countSupportDirs(name: string): Promise<number>;
69
+ /** Best-effort audit trail entry; never blocks the mutation. */
70
+ private audit;
71
+ /** Recent mutation audit records (read-only inspection surface). */
72
+ listMutations(): Promise<MutationRecord[]>;
66
73
  create(name: string, content: string, origin: 'foreground' | 'background_review'): Promise<SkillActionResult>;
67
74
  update(name: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
68
75
  patch(name: string, oldString: string, newString: string, filePath?: string, replaceAll?: boolean, origin?: WriteOrigin): Promise<SkillActionResult>;
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.14",
4
+ "version": "0.1.0-rc.16",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },