@lmzhen/dsh-evolution-core 0.1.0-rc.21 → 0.1.0-rc.23

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
@@ -20,7 +20,11 @@ function evolutionIoAdapter(provider) {
20
20
  list: (path) => provider().list(path),
21
21
  exists: (path) => provider().exists(path),
22
22
  rename: (path, destination) => provider().rename(path, destination),
23
- copy: (path, destination) => provider().copy(path, destination)
23
+ copy: (path, destination) => provider().copy(path, destination),
24
+ size: (path) => {
25
+ const io = provider();
26
+ return io.size ? io.size(path) : Promise.resolve(null);
27
+ }
24
28
  };
25
29
  }
26
30
  function nodeEvolutionIo() {
@@ -69,6 +73,13 @@ function nodeEvolutionIo() {
69
73
  recursive: true,
70
74
  force: true
71
75
  });
76
+ },
77
+ async size(path) {
78
+ try {
79
+ return (await stat(path)).size;
80
+ } catch {
81
+ return null;
82
+ }
72
83
  }
73
84
  };
74
85
  }
@@ -366,6 +377,221 @@ function computeLifecycleTransitions(usage, config, now = /* @__PURE__ */ new Da
366
377
  return result;
367
378
  }
368
379
  //#endregion
380
+ //#region lib/types/prompts.js
381
+ /**
382
+ * Review and curation prompts adapted from Hermes Agent
383
+ * `agent/background_review.py`, `agent/curator.py`, and
384
+ * `agent/learn_prompt.py`, with tool names translated to the DSH-native
385
+ * catalog (`memory`, `skill_manage`, `skill`, `bash`, `str_replace_editor`).
386
+ *
387
+ * Every prompt is pinned in a versioned bundle. Review workers verify the
388
+ * bundle digest before spending a model call, so a partially-patched
389
+ * deployment fails closed instead of silently running a truncated prompt.
390
+ */
391
+ /**
392
+ * Prompt bundle identity. Bump both id and version whenever a prompt's text
393
+ * changes semantically: the bundle digest is the fail-closed signal for
394
+ * review workers, so a stale id across deployments must be distinguishable.
395
+ */
396
+ const PROMPT_BUNDLE_ID = "dsh-evolution@2";
397
+ const PROMPT_BUNDLE_VERSION = 2;
398
+ const MEMORY_REVIEW_PROMPT = `[Auto-review — Memory]
399
+ Review the conversation above and consider saving to memory if appropriate.
400
+
401
+ Focus on:
402
+ 1. Has the user revealed things about themselves — persona, desires, preferences, or personal details worth remembering?
403
+ 2. Has the user expressed expectations about how you should behave, their work style, or ways they want you to operate?
404
+
405
+ If something stands out, save it using the memory tool.
406
+ If nothing is worth saving, just say "Nothing to save." and stop.`;
407
+ const SKILL_REVIEW_PROMPT = `[Auto-review — Skills]
408
+ Review the conversation above and update the skill library. Be ACTIVE — most sessions produce at least one skill update, even if small.
409
+
410
+ Target 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.
411
+
412
+ Signals that warrant action:
413
+ - The user corrected your style, tone, format, verbosity, workflow, or approach.
414
+ - A non-trivial technique, fix, workaround, or debugging path emerged.
415
+ - A loaded skill turned out wrong, missing, or outdated — patch it now.
416
+
417
+ Preference order:
418
+ 1. Patch a skill that was loaded or read this session.
419
+ 2. Patch an existing umbrella skill.
420
+ 3. Add references/, templates/, or scripts/ support under an existing skill.
421
+ 4. Create a new class-level umbrella skill only when nothing fits.
422
+
423
+ Protected skills (bundled/hub-installed) must not be edited. Pinned skills may be patched but not archived.
424
+
425
+ Do NOT capture:
426
+ - Environment-dependent failures (missing binaries, unconfigured credentials).
427
+ - Negative claims about tools ("browser tools do not work").
428
+ - Transient errors that resolved during the session.
429
+ - One-off task narratives.
430
+
431
+ If a tool failed because of setup state, capture the FIX under an existing setup skill — never "this tool does not work" as a standalone constraint.
432
+
433
+ "Nothing to save." is a real option but should NOT be the default.`;
434
+ const COMBINED_REVIEW_PROMPT = `[Auto-review]
435
+ Review the conversation above and update two things.
436
+
437
+ **Memory**: who the user is. Save durable user preferences, personal details, and expectations with the memory tool.
438
+
439
+ **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.
440
+
441
+ 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.`;
442
+ const CURATOR_PROMPT = `You are the skill curator. Maintain a healthy, class-level skill library, not a flat pile of narrow one-session skills.
443
+
444
+ 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.
445
+
446
+ Right target shape: class-level skills with rich SKILL.md + references/, templates/, scripts/ support files for session-specific detail.
447
+
448
+ Hard rules:
449
+ 1. NEVER hard-delete a skill. Archive (moving to .archive/) is the maximum destructive action; archives are recoverable, deletion is not.
450
+ 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.
451
+ 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.
452
+ 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.
453
+ 5. Judge overlap on CONTENT, not on usage counters.
454
+ 6. Before archiving a merged skill, ensure its unique content was preserved in the umbrella.
455
+
456
+ How to work:
457
+ 1. Scan the candidate list. Identify PREFIX CLUSTERS — skills sharing a first word or domain keyword (expect 10-25 clusters).
458
+ 2. For each cluster with 2+ members, ask "what is the UMBRELLA CLASS these skills serve?" and consolidate:
459
+ a. MERGE INTO AN EXISTING UMBRELLA (patch a labeled section for each sibling's unique insight, then archive the siblings).
460
+ b. CREATE A NEW UMBRELLA SKILL.md covering the shared workflow with short labeled subsections, then archive the absorbed siblings.
461
+ c. DEMOTE session-specific detail to references/, templates/, or scripts/ under the umbrella.
462
+ 3. Keep the umbrella body tight and scannable: exact commands, verbatim paths, ~100-200 lines; never invent flags or APIs.
463
+
464
+ Produce a YAML summary with exactly this shape:
465
+ consolidations:
466
+ - from: <old-skill-name>
467
+ into: <umbrella-skill-name>
468
+ reason: <one short sentence>
469
+ prunings:
470
+ - name: <skill-name>
471
+ reason: <one short sentence>
472
+ Nominate a pruning only when archival is clearly safe (stale AND genuinely obsolete or fully absorbed elsewhere).`;
473
+ const CURATOR_DRY_RUN_BANNER = `═══════════════════════════════════════════════════════════════
474
+ DRY-RUN — REPORT ONLY. DO NOT MUTATE THE SKILL LIBRARY.
475
+ ═══════════════════════════════════════════════════════════════
476
+
477
+ This is a PREVIEW pass. Follow every instruction above EXCEPT:
478
+ • Do NOT call skill_manage with action=create, update, patch, delete, write_file, or remove_file.
479
+ • Do NOT move, copy, or rewrite any file under the skills tree.
480
+
481
+ 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.
482
+
483
+ If you accidentally take a mutating action, say so explicitly in the summary.`;
484
+ const COMPLETION_SKILL_REVIEW_PROMPT = `[Auto-review — Skills · task complete]
485
+ Your current task now appears complete. Before wrapping up, review the approach and update the skill library via skill_manage.
486
+
487
+ 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.
488
+
489
+ Do NOT modify output files or re-run the task. If you are still mid-task, ignore this.`;
490
+ function reviewPrompt(kind) {
491
+ if (kind === "memory") return MEMORY_REVIEW_PROMPT;
492
+ if (kind === "skill") return SKILL_REVIEW_PROMPT;
493
+ return COMBINED_REVIEW_PROMPT;
494
+ }
495
+ function sha256(text) {
496
+ return createHash("sha256").update(text).digest("hex");
497
+ }
498
+ function createPromptBundle(prompts) {
499
+ const canonical = JSON.stringify({
500
+ id: PROMPT_BUNDLE_ID,
501
+ version: 2,
502
+ prompts: Object.fromEntries(Object.entries(prompts).sort())
503
+ });
504
+ return Object.freeze({
505
+ id: PROMPT_BUNDLE_ID,
506
+ version: 2,
507
+ prompts: Object.freeze({ ...prompts }),
508
+ sha256: sha256(canonical)
509
+ });
510
+ }
511
+ const PROMPT_BUNDLE = createPromptBundle({
512
+ memory: MEMORY_REVIEW_PROMPT,
513
+ skill: SKILL_REVIEW_PROMPT,
514
+ combined: COMBINED_REVIEW_PROMPT,
515
+ curator: CURATOR_PROMPT,
516
+ completion: COMPLETION_SKILL_REVIEW_PROMPT
517
+ });
518
+ function verifyPromptBundle(bundle = PROMPT_BUNDLE) {
519
+ const canonical = JSON.stringify({
520
+ id: bundle.id,
521
+ version: bundle.version,
522
+ prompts: Object.fromEntries(Object.entries(bundle.prompts).sort())
523
+ });
524
+ return bundle.sha256 === sha256(canonical);
525
+ }
526
+ const DSH_AUTHORING_STANDARDS = `Follow the Hermes skill-authoring standards, translated to DSH tools.
527
+
528
+ Frontmatter:
529
+ - name: lowercase-hyphenated, <=64 chars, no spaces.
530
+ - description: ONE sentence, <=60 characters, ends with a period. State the capability, not the implementation. No marketing words. Do NOT repeat the skill name. Count the characters before saving.
531
+ - version: 0.1.0
532
+ - author: always the literal value "Hermes". NEVER fill it from the environment, git config, or any identity you can probe.
533
+ - platforms: declare [macos], [linux], and/or [windows] only when the skill is genuinely OS-bound; omit for portable skills.
534
+ - metadata.hermes.tags: a few Capitalized, Relevant, Tags.
535
+
536
+ Body section order (omit only when empty):
537
+ 1. "# <Human Title>" then a 2-3 sentence intro: what it does, what it does NOT do, key dependency stance.
538
+ 2. "## When to Use" — concrete trigger phrases.
539
+ 3. "## Prerequisites" — exact env vars, install steps, credentials.
540
+ 4. "## How to Run" — canonical invocation framed through DSH tools.
541
+ 5. "## Quick Reference" — flat command/endpoint list.
542
+ 6. "## Procedure" — numbered steps with copy-paste-exact commands.
543
+ 7. "## Pitfalls" — known limits and rate limits.
544
+ 8. "## Verification" — one check proving the skill worked.
545
+
546
+ DSH-tool framing:
547
+ - Reference DSH tools by name in backticks: \`bash\`, \`str_replace_editor\`, \`write\`, \`skill\`, \`skill_manage\`, \`memory\`.
548
+ - Do not name wrapped shell utilities when a DSH tool already covers them.
549
+ - Larger scripts belong under \`scripts/\` (written with \`skill_manage write_file\`) and are referenced from SKILL.md by relative path.
550
+
551
+ Quality bar:
552
+ - Prefer verbatim flags, paths, and APIs from the source. Never invent them.
553
+ - Keep it tight: ~100 lines simple, ~200 complex.
554
+ - No router/index/hub skills that only point at other skills.
555
+ - References go in \`references/\`, templates in \`templates/\`.`;
556
+ //#endregion
557
+ //#region lib/types/learn-prompt.js
558
+ /**
559
+ * Open-ended `/evolution learn` prompt builder.
560
+ *
561
+ * `learn` is open-ended: the user can name anything they can describe — a
562
+ * directory of code, an API doc URL, a workflow they just walked the agent
563
+ * through, or pasted notes. The prompt instructs the live agent to gather the
564
+ * named sources with its existing tools, then author a single SKILL.md via
565
+ * `skill_manage` following `DSH_AUTHORING_STANDARDS`. There is no separate
566
+ * distillation engine and no model-tool footprint.
567
+ */
568
+ /**
569
+ * Build the agent prompt for an open-ended `/evolution learn` request.
570
+ *
571
+ * @param userRequest free-text the user gave after `/evolution learn`; an
572
+ * empty string falls back to "the workflow we just went through".
573
+ * @returns a complete instruction the agent runs as a normal turn.
574
+ */
575
+ function buildLearnPrompt(userRequest) {
576
+ return [
577
+ "[/learn] The user wants you to learn a reusable skill from the request below, and save it.",
578
+ "",
579
+ "THE REQUEST:",
580
+ userRequest.trim() || "the workflow we just went through in this conversation — review the steps taken and distill them into a reusable skill",
581
+ "",
582
+ "The request is open-ended and may mix two kinds of content, in any order: SOURCES to gather (directories, file paths, URLs, \"what we just did\", pasted notes) AND REQUIREMENTS that shape the skill (what to focus on, what to leave out, scope, naming, the angle to take). Treat EVERY part of the request as load-bearing. In particular, prose that comes after a path or link is NOT incidental — it is the user telling you what they want from that source. A request like `<url> focus on the auth flow, skip the deprecated endpoints` means: gather the URL AND honor \"focus on auth, skip deprecated\" as authoring requirements. Never fetch the first source and ignore the rest.",
583
+ "",
584
+ "Do this:",
585
+ "1. Gather every source the user named, using the tools you already have — reads and searches for local files or directories, web access for URLs, this conversation history if they referred to something you just did, and the text they pasted as-is. If the request is ambiguous about scope, make a reasonable choice and note it; do not stall.",
586
+ "2. Author ONE SKILL.md, applying every requirement, focus, and constraint in the request — these govern what the SKILL.md covers and emphasizes, not just which sources you read.",
587
+ "3. Save it with the `skill_manage` tool (action=\"create\"). Pick a sensible category. If the procedure needs a non-trivial script, add it under the skill's `scripts/` with `skill_manage` write_file and reference it by relative path.",
588
+ "",
589
+ DSH_AUTHORING_STANDARDS,
590
+ "",
591
+ "When done, tell the user the skill name, its category, and a one-line summary of what it captured."
592
+ ].join("\n");
593
+ }
594
+ //#endregion
369
595
  //#region lib/types/threats.js
370
596
  /**
371
597
  * Threat scanning for agent-authored memory and skill content.
@@ -599,6 +825,13 @@ function scanContentThreats(text, maxScanChars = 65536, options = NO_SCAN_OPTION
599
825
  * File-backed durable memory with Hermes-compatible semantics.
600
826
  * Stores are MEMORY.md and USER.md under $DSH_HOME/memories (~/.dsh/memories).
601
827
  */
828
+ /**
829
+ * Read-guard factor: a memory file larger than this multiple of its target's
830
+ * char limit is treated as externally corrupted and skipped instead of being
831
+ * read whole (aligned with claw `tools/memory.ts` size guard, which uses the
832
+ * same 10× bound around a file that should never exceed the store limit).
833
+ */
834
+ const READ_GUARD_FACTOR = 10;
602
835
  function memoryRoot(env = process.env) {
603
836
  return join(env.DSH_HOME ?? join(homedir(), ".dsh"), "memories");
604
837
  }
@@ -633,7 +866,24 @@ var MemoryStore = class {
633
866
  limitFor(target) {
634
867
  return target === "memory" ? this.memoryLimit : this.userLimit;
635
868
  }
869
+ /**
870
+ * Read-guard probe: `{ size, limit }` when the on-disk file exceeds
871
+ * `limit * READ_GUARD_FACTOR` bytes, `null` when it is absent, unknown
872
+ * (backend without a size probe), under the bound, or the target has no
873
+ * limit configured.
874
+ */
875
+ async oversizedFile(target) {
876
+ const size = await this.io.size?.(fileFor(this.root, target));
877
+ if (size === null || size === void 0) return null;
878
+ const limit = this.limitFor(target);
879
+ if (limit <= 0) return null;
880
+ return size > limit * READ_GUARD_FACTOR ? {
881
+ size,
882
+ limit
883
+ } : null;
884
+ }
636
885
  async read(target) {
886
+ if (await this.oversizedFile(target)) return [];
637
887
  const raw = await this.io.readText(fileFor(this.root, target));
638
888
  return raw === null ? [] : [...new Set(normalizeEntries(raw))];
639
889
  }
@@ -669,23 +919,45 @@ var MemoryStore = class {
669
919
  return percent >= 80 ? ` ⚠️ Storage at ${percent}% (${chars}/${limit} chars).` : "";
670
920
  }
671
921
  /**
672
- * Best-effort copy of the drifted on-disk file to `<file>.bak.<stamp>` before
673
- * refusing the write, so an external edit stays recoverable. Failure to back
674
- * up does not change the refusal semantics.
922
+ * Best-effort raw-copy backup of the on-disk file to `<file>.bak.<stamp>`
923
+ * before a refusal, so an externally modified (or oversized) file stays
924
+ * recoverable. Copies bytes instead of reading them so a pathologically
925
+ * large file is never loaded just to back it up. Failure to back up does
926
+ * not change the refusal semantics.
675
927
  */
676
- async backupDrift(target) {
928
+ async backupFile(target) {
677
929
  const path = fileFor(this.root, target);
678
- const raw = await this.io.readText(path);
679
- if (raw === null) return null;
680
930
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 14);
681
931
  try {
682
- await this.io.writeText(`${path}.bak.${stamp}`, raw);
932
+ await this.io.copy(path, `${path}.bak.${stamp}`);
683
933
  return `${path}.bak.${stamp}`;
684
934
  } catch {
685
935
  return null;
686
936
  }
687
937
  }
938
+ /**
939
+ * Read-guard refusal for write paths. Returns the refusal result when the
940
+ * target file is oversized, `null` otherwise. The file is skipped for
941
+ * reading (never loaded), backed up by raw copy, and the model is told to
942
+ * fix it manually — mirroring the drift refusal so corrupted state is never
943
+ * silently overwritten.
944
+ */
945
+ async oversizedRefusal(target) {
946
+ const oversized = await this.oversizedFile(target);
947
+ if (!oversized) return null;
948
+ const backup = await this.backupFile(target);
949
+ const suffix = backup ? ` A backup was saved to ${basename(backup)}.` : "";
950
+ return {
951
+ ok: false,
952
+ message: `Memory file is ${oversized.size} bytes (limit ${oversized.limit * READ_GUARD_FACTOR}) — skipping read.${suffix} Fix the file manually, then retry.`,
953
+ entries: [],
954
+ chars: 0,
955
+ limit: this.limitFor(target)
956
+ };
957
+ }
688
958
  async add(target, facts) {
959
+ const refusal = await this.oversizedRefusal(target);
960
+ if (refusal) return refusal;
689
961
  const content = facts.trim();
690
962
  if (!content) return {
691
963
  ok: false,
@@ -741,6 +1013,8 @@ var MemoryStore = class {
741
1013
  chars: 0,
742
1014
  limit: this.limitFor(target)
743
1015
  };
1016
+ const refusal = await this.oversizedRefusal(target);
1017
+ if (refusal) return refusal;
744
1018
  const content = action === "replace" ? (facts ?? "").trim() : "";
745
1019
  if (action === "replace" && !content) return {
746
1020
  ok: false,
@@ -760,7 +1034,7 @@ var MemoryStore = class {
760
1034
  };
761
1035
  }
762
1036
  if (await this.detectDrift(target)) {
763
- const backup = await this.backupDrift(target);
1037
+ const backup = await this.backupFile(target);
764
1038
  return {
765
1039
  ok: false,
766
1040
  message: `External drift detected in memory file.${backup ? ` A backup was saved to ${basename(backup)}.` : ""} Resolve the drift before retrying.`,
@@ -806,8 +1080,10 @@ var MemoryStore = class {
806
1080
  chars: 0,
807
1081
  limit: this.limitFor(target)
808
1082
  };
1083
+ const refusal = await this.oversizedRefusal(target);
1084
+ if (refusal) return refusal;
809
1085
  if (await this.detectDrift(target)) {
810
- const backup = await this.backupDrift(target);
1086
+ const backup = await this.backupFile(target);
811
1087
  return {
812
1088
  ok: false,
813
1089
  message: `External drift detected in memory file.${backup ? ` A backup was saved to ${basename(backup)}.` : ""} Resolve the drift before retrying.`,
@@ -898,12 +1174,27 @@ var MemoryStore = class {
898
1174
  const memory = await this.read("memory");
899
1175
  const user = await this.read("user");
900
1176
  const parts = [];
901
- for (const [target, entries] of [["Memory", memory], ["User Profile", user]]) {
1177
+ for (const [target, label, entries] of [[
1178
+ "memory",
1179
+ "Memory",
1180
+ memory
1181
+ ], [
1182
+ "user",
1183
+ "User Profile",
1184
+ user
1185
+ ]]) {
1186
+ const oversized = entries.length === 0 ? await this.oversizedFile(target) : null;
1187
+ if (oversized) {
1188
+ parts.push(`## ${label} — file skipped: ${oversized.size} bytes (limit ${oversized.limit * READ_GUARD_FACTOR}); not read`);
1189
+ continue;
1190
+ }
902
1191
  const safe = entries.filter((entry) => !scanMemoryThreats(entry));
903
1192
  if (safe.length > 0) {
904
1193
  const body = safe.join(ENTRY_DELIMITER);
1194
+ const limit = this.limitFor(target);
1195
+ const pct = limit > 0 ? Math.min(100, Math.floor(body.length * 100 / limit)) : 0;
905
1196
  const note = safe.length === entries.length ? "" : ` (${entries.length - safe.length} threat-matched entries filtered)`;
906
- parts.push(`## ${target} (${safe.length} entries)${note}\n${body}`);
1197
+ parts.push(`## ${label} (${safe.length} entries) [${pct}% — ${body.length}/${limit} chars]${note}\n${body}`);
907
1198
  }
908
1199
  }
909
1200
  return parts.join("\n\n");
@@ -928,6 +1219,7 @@ var MemoryStore = class {
928
1219
  * same serialization and returns false, so a normal write is never flagged.
929
1220
  */
930
1221
  async detectDrift(target) {
1222
+ if (await this.oversizedFile(target)) return true;
931
1223
  const raw = await this.io.readText(fileFor(this.root, target));
932
1224
  if (raw === null) return false;
933
1225
  return render(normalizeEntries(raw)) !== raw;
@@ -971,183 +1263,6 @@ async function recordMutation(root, io, record, cap = 500) {
971
1263
  }, null, 2));
972
1264
  }
973
1265
  //#endregion
974
- //#region lib/types/prompts.js
975
- /**
976
- * Review and curation prompts adapted from Hermes Agent
977
- * `agent/background_review.py`, `agent/curator.py`, and
978
- * `agent/learn_prompt.py`, with tool names translated to the DSH-native
979
- * catalog (`memory`, `skill_manage`, `skill`, `bash`, `str_replace_editor`).
980
- *
981
- * Every prompt is pinned in a versioned bundle. Review workers verify the
982
- * bundle digest before spending a model call, so a partially-patched
983
- * deployment fails closed instead of silently running a truncated prompt.
984
- */
985
- /**
986
- * Prompt bundle identity. Bump both id and version whenever a prompt's text
987
- * changes semantically: the bundle digest is the fail-closed signal for
988
- * review workers, so a stale id across deployments must be distinguishable.
989
- */
990
- const PROMPT_BUNDLE_ID = "dsh-evolution@2";
991
- const PROMPT_BUNDLE_VERSION = 2;
992
- const MEMORY_REVIEW_PROMPT = `[Auto-review — Memory]
993
- Review the conversation above and consider saving to memory if appropriate.
994
-
995
- Focus on:
996
- 1. Has the user revealed things about themselves — persona, desires, preferences, or personal details worth remembering?
997
- 2. Has the user expressed expectations about how you should behave, their work style, or ways they want you to operate?
998
-
999
- If something stands out, save it using the memory tool.
1000
- If nothing is worth saving, just say "Nothing to save." and stop.`;
1001
- const SKILL_REVIEW_PROMPT = `[Auto-review — Skills]
1002
- Review the conversation above and update the skill library. Be ACTIVE — most sessions produce at least one skill update, even if small.
1003
-
1004
- Target 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.
1005
-
1006
- Signals that warrant action:
1007
- - The user corrected your style, tone, format, verbosity, workflow, or approach.
1008
- - A non-trivial technique, fix, workaround, or debugging path emerged.
1009
- - A loaded skill turned out wrong, missing, or outdated — patch it now.
1010
-
1011
- Preference order:
1012
- 1. Patch a skill that was loaded or read this session.
1013
- 2. Patch an existing umbrella skill.
1014
- 3. Add references/, templates/, or scripts/ support under an existing skill.
1015
- 4. Create a new class-level umbrella skill only when nothing fits.
1016
-
1017
- Protected skills (bundled/hub-installed) must not be edited. Pinned skills may be patched but not archived.
1018
-
1019
- Do NOT capture:
1020
- - Environment-dependent failures (missing binaries, unconfigured credentials).
1021
- - Negative claims about tools ("browser tools do not work").
1022
- - Transient errors that resolved during the session.
1023
- - One-off task narratives.
1024
-
1025
- If a tool failed because of setup state, capture the FIX under an existing setup skill — never "this tool does not work" as a standalone constraint.
1026
-
1027
- "Nothing to save." is a real option but should NOT be the default.`;
1028
- const COMBINED_REVIEW_PROMPT = `[Auto-review]
1029
- Review the conversation above and update two things.
1030
-
1031
- **Memory**: who the user is. Save durable user preferences, personal details, and expectations with the memory tool.
1032
-
1033
- **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.
1034
-
1035
- 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.`;
1036
- const CURATOR_PROMPT = `You are the skill curator. Maintain a healthy, class-level skill library, not a flat pile of narrow one-session skills.
1037
-
1038
- 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.
1039
-
1040
- Right target shape: class-level skills with rich SKILL.md + references/, templates/, scripts/ support files for session-specific detail.
1041
-
1042
- Hard rules:
1043
- 1. NEVER hard-delete a skill. Archive (moving to .archive/) is the maximum destructive action; archives are recoverable, deletion is not.
1044
- 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.
1045
- 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.
1046
- 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.
1047
- 5. Judge overlap on CONTENT, not on usage counters.
1048
- 6. Before archiving a merged skill, ensure its unique content was preserved in the umbrella.
1049
-
1050
- How to work:
1051
- 1. Scan the candidate list. Identify PREFIX CLUSTERS — skills sharing a first word or domain keyword (expect 10-25 clusters).
1052
- 2. For each cluster with 2+ members, ask "what is the UMBRELLA CLASS these skills serve?" and consolidate:
1053
- a. MERGE INTO AN EXISTING UMBRELLA (patch a labeled section for each sibling's unique insight, then archive the siblings).
1054
- b. CREATE A NEW UMBRELLA SKILL.md covering the shared workflow with short labeled subsections, then archive the absorbed siblings.
1055
- c. DEMOTE session-specific detail to references/, templates/, or scripts/ under the umbrella.
1056
- 3. Keep the umbrella body tight and scannable: exact commands, verbatim paths, ~100-200 lines; never invent flags or APIs.
1057
-
1058
- Produce a YAML summary with exactly this shape:
1059
- consolidations:
1060
- - from: <old-skill-name>
1061
- into: <umbrella-skill-name>
1062
- reason: <one short sentence>
1063
- prunings:
1064
- - name: <skill-name>
1065
- reason: <one short sentence>
1066
- Nominate a pruning only when archival is clearly safe (stale AND genuinely obsolete or fully absorbed elsewhere).`;
1067
- const CURATOR_DRY_RUN_BANNER = `═══════════════════════════════════════════════════════════════
1068
- DRY-RUN — REPORT ONLY. DO NOT MUTATE THE SKILL LIBRARY.
1069
- ═══════════════════════════════════════════════════════════════
1070
-
1071
- This is a PREVIEW pass. Follow every instruction above EXCEPT:
1072
- • Do NOT call skill_manage with action=create, update, patch, delete, write_file, or remove_file.
1073
- • Do NOT move, copy, or rewrite any file under the skills tree.
1074
-
1075
- 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.
1076
-
1077
- If you accidentally take a mutating action, say so explicitly in the summary.`;
1078
- const COMPLETION_SKILL_REVIEW_PROMPT = `[Auto-review — Skills · task complete]
1079
- Your current task now appears complete. Before wrapping up, review the approach and update the skill library via skill_manage.
1080
-
1081
- 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.
1082
-
1083
- Do NOT modify output files or re-run the task. If you are still mid-task, ignore this.`;
1084
- function reviewPrompt(kind) {
1085
- if (kind === "memory") return MEMORY_REVIEW_PROMPT;
1086
- if (kind === "skill") return SKILL_REVIEW_PROMPT;
1087
- return COMBINED_REVIEW_PROMPT;
1088
- }
1089
- function sha256(text) {
1090
- return createHash("sha256").update(text).digest("hex");
1091
- }
1092
- function createPromptBundle(prompts) {
1093
- const canonical = JSON.stringify({
1094
- id: PROMPT_BUNDLE_ID,
1095
- version: 2,
1096
- prompts: Object.fromEntries(Object.entries(prompts).sort())
1097
- });
1098
- return Object.freeze({
1099
- id: PROMPT_BUNDLE_ID,
1100
- version: 2,
1101
- prompts: Object.freeze({ ...prompts }),
1102
- sha256: sha256(canonical)
1103
- });
1104
- }
1105
- const PROMPT_BUNDLE = createPromptBundle({
1106
- memory: MEMORY_REVIEW_PROMPT,
1107
- skill: SKILL_REVIEW_PROMPT,
1108
- combined: COMBINED_REVIEW_PROMPT,
1109
- curator: CURATOR_PROMPT,
1110
- completion: COMPLETION_SKILL_REVIEW_PROMPT
1111
- });
1112
- function verifyPromptBundle(bundle = PROMPT_BUNDLE) {
1113
- const canonical = JSON.stringify({
1114
- id: bundle.id,
1115
- version: bundle.version,
1116
- prompts: Object.fromEntries(Object.entries(bundle.prompts).sort())
1117
- });
1118
- return bundle.sha256 === sha256(canonical);
1119
- }
1120
- const DSH_AUTHORING_STANDARDS = `Follow the Hermes skill-authoring standards, translated to DSH tools.
1121
-
1122
- Frontmatter:
1123
- - name: lowercase-hyphenated, <=64 chars, no spaces.
1124
- - description: ONE sentence, <=60 characters, ends with a period. State the capability, not the implementation. No marketing words. Do NOT repeat the skill name. Count the characters before saving.
1125
- - version: 0.1.0
1126
- - author: always the literal value "Hermes". NEVER fill it from the environment, git config, or any identity you can probe.
1127
- - platforms: declare [macos], [linux], and/or [windows] only when the skill is genuinely OS-bound; omit for portable skills.
1128
- - metadata.hermes.tags: a few Capitalized, Relevant, Tags.
1129
-
1130
- Body section order (omit only when empty):
1131
- 1. "# <Human Title>" then a 2-3 sentence intro: what it does, what it does NOT do, key dependency stance.
1132
- 2. "## When to Use" — concrete trigger phrases.
1133
- 3. "## Prerequisites" — exact env vars, install steps, credentials.
1134
- 4. "## How to Run" — canonical invocation framed through DSH tools.
1135
- 5. "## Quick Reference" — flat command/endpoint list.
1136
- 6. "## Procedure" — numbered steps with copy-paste-exact commands.
1137
- 7. "## Pitfalls" — known limits and rate limits.
1138
- 8. "## Verification" — one check proving the skill worked.
1139
-
1140
- DSH-tool framing:
1141
- - Reference DSH tools by name in backticks: \`bash\`, \`str_replace_editor\`, \`write\`, \`skill\`, \`skill_manage\`, \`memory\`.
1142
- - Do not name wrapped shell utilities when a DSH tool already covers them.
1143
- - Larger scripts belong under \`scripts/\` (written with \`skill_manage write_file\`) and are referenced from SKILL.md by relative path.
1144
-
1145
- Quality bar:
1146
- - Prefer verbatim flags, paths, and APIs from the source. Never invent them.
1147
- - Keep it tight: ~100 lines simple, ~200 complex.
1148
- - No router/index/hub skills that only point at other skills.
1149
- - References go in \`references/\`, templates in \`templates/\`.`;
1150
- //#endregion
1151
1266
  //#region lib/types/quality.js
1152
1267
  /**
1153
1268
  * Quality scoring and near-duplicate detection for the curated skill library.
@@ -2067,4 +2182,4 @@ var JsonState = class JsonState {
2067
2182
  }
2068
2183
  };
2069
2184
  //#endregion
2070
- 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, MUTATIONS_FILE_VERSION, MemoryStore, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, SKILL_NAME_RE, SKILL_REVIEW_PROMPT, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, 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 };
2185
+ 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, MUTATIONS_FILE_VERSION, MemoryStore, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, SKILL_NAME_RE, SKILL_REVIEW_PROMPT, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, advanceReview, buildCuratorRunReport, buildLearnPrompt, 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 };
@@ -10,6 +10,7 @@
10
10
  export * from './curator.ts';
11
11
  export * from './events.ts';
12
12
  export * from './io.ts';
13
+ export * from './learn-prompt.ts';
13
14
  export * from './memory-store.ts';
14
15
  export * from './mutations.ts';
15
16
  export * from './prompts.ts';
package/lib/types/io.d.ts CHANGED
@@ -13,6 +13,13 @@ export interface EvolutionIoLike {
13
13
  exists(path: string): Promise<boolean>;
14
14
  rename(path: string, destination: string): Promise<void>;
15
15
  copy(path: string, destination: string): Promise<void>;
16
+ /**
17
+ * Optional byte-size probe for the read guard. Return the file's size in
18
+ * bytes, or `null`/`undefined` when unknown (unsupported backend, missing
19
+ * file, stat failure). An implementation without this probe gets no guard:
20
+ * consumers treat an unknown size as "guard not applicable".
21
+ */
22
+ size?(path: string): Promise<number | null>;
16
23
  }
17
24
  /** Lazy adapter over an IO provider registry, shared by every evolution consumer. */
18
25
  export declare function evolutionIoAdapter(provider: () => EvolutionIoLike): EvolutionIoLike;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Open-ended `/evolution learn` prompt builder.
3
+ *
4
+ * `learn` is open-ended: the user can name anything they can describe — a
5
+ * directory of code, an API doc URL, a workflow they just walked the agent
6
+ * through, or pasted notes. The prompt instructs the live agent to gather the
7
+ * named sources with its existing tools, then author a single SKILL.md via
8
+ * `skill_manage` following `DSH_AUTHORING_STANDARDS`. There is no separate
9
+ * distillation engine and no model-tool footprint.
10
+ */
11
+ /**
12
+ * Build the agent prompt for an open-ended `/evolution learn` request.
13
+ *
14
+ * @param userRequest free-text the user gave after `/evolution learn`; an
15
+ * empty string falls back to "the workflow we just went through".
16
+ * @returns a complete instruction the agent runs as a normal turn.
17
+ */
18
+ export declare function buildLearnPrompt(userRequest: string): string;
19
+ //# sourceMappingURL=learn-prompt.d.ts.map
@@ -37,6 +37,13 @@ export declare class MemoryStore {
37
37
  private failureCount;
38
38
  constructor(options?: MemoryStoreOptions);
39
39
  limitFor(target: MemoryTarget): number;
40
+ /**
41
+ * Read-guard probe: `{ size, limit }` when the on-disk file exceeds
42
+ * `limit * READ_GUARD_FACTOR` bytes, `null` when it is absent, unknown
43
+ * (backend without a size probe), under the bound, or the target has no
44
+ * limit configured.
45
+ */
46
+ private oversizedFile;
40
47
  read(target: MemoryTarget): Promise<string[]>;
41
48
  write(target: MemoryTarget, entries: string[]): Promise<void>;
42
49
  resetFailures(): void;
@@ -44,11 +51,21 @@ export declare class MemoryStore {
44
51
  /** Percent-based storage hint appended to success message once the target is ≥80% full. */
45
52
  private storageHint;
46
53
  /**
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.
54
+ * Best-effort raw-copy backup of the on-disk file to `<file>.bak.<stamp>`
55
+ * before a refusal, so an externally modified (or oversized) file stays
56
+ * recoverable. Copies bytes instead of reading them so a pathologically
57
+ * large file is never loaded just to back it up. Failure to back up does
58
+ * not change the refusal semantics.
59
+ */
60
+ private backupFile;
61
+ /**
62
+ * Read-guard refusal for write paths. Returns the refusal result when the
63
+ * target file is oversized, `null` otherwise. The file is skipped for
64
+ * reading (never loaded), backed up by raw copy, and the model is told to
65
+ * fix it manually — mirroring the drift refusal so corrupted state is never
66
+ * silently overwritten.
50
67
  */
51
- private backupDrift;
68
+ private oversizedRefusal;
52
69
  add(target: MemoryTarget, facts: string): Promise<MemoryApplyResult>;
53
70
  replace(target: MemoryTarget, oldText: string, facts: string): Promise<MemoryApplyResult>;
54
71
  remove(target: MemoryTarget, oldText: string): Promise<MemoryApplyResult>;
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.21",
4
+ "version": "0.1.0-rc.23",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },