@lmzhen/dsh-evolution-core 0.3.14 → 0.3.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
@@ -370,9 +370,9 @@ function latestActivityAt(record) {
370
370
  record.last_used_at,
371
371
  record.last_viewed_at,
372
372
  record.last_patched_at
373
- ].filter((value) => typeof value === "string");
373
+ ].filter((value) => typeof value === "string" && Number.isFinite(Date.parse(value)));
374
374
  if (values.length === 0) return null;
375
- return values.sort().reverse()[0] ?? null;
375
+ return values.reduce((latest, value) => Date.parse(value) > Date.parse(latest) ? value : latest);
376
376
  }
377
377
  /**
378
378
  * Whether the library has ANY observed read evidence (C observation window):
@@ -494,6 +494,12 @@ const DEFAULT_USER_CHAR_LIMIT = 1375;
494
494
  /** Consolidation-failure backoff cap, shared by MemoryStore and memory-files' Config default. */
495
495
  const DEFAULT_CONSOLIDATION_FAILURES = 3;
496
496
  const DEFAULT_SKILL_CONTENT_CHARS = 1e5;
497
+ /** Hermes authoring quality bar for descriptions (the 60-char Rule). The
498
+ * platform's own index limit stays in validateFrontmatter; this bar is the
499
+ * target the authoring standard names, enforced as ADVISORY feedback.
500
+ * 0.3.16 (T-4): moved here from skill-store.ts so drift-signals (pure, no IO)
501
+ * can reference it without importing the skill-store module. */
502
+ const AUTHORING_DESCRIPTION_BAR = 60;
497
503
  //#endregion
498
504
  //#region lib/types/gates.js
499
505
  /**
@@ -999,8 +1005,8 @@ async function readEvolutionTimeline(io, path) {
999
1005
  * changes semantically: the bundle digest is the fail-closed signal for
1000
1006
  * review workers, so a stale id across deployments must be distinguishable.
1001
1007
  */
1002
- const PROMPT_BUNDLE_ID = "dsh-evolution@13";
1003
1008
  const PROMPT_BUNDLE_VERSION = 13;
1009
+ const PROMPT_BUNDLE_ID = `dsh-evolution@13`;
1004
1010
  const MEMORY_REVIEW_PROMPT = `[Auto-review — Memory]
1005
1011
  Review the conversation above and consider saving to memory if appropriate.
1006
1012
 
@@ -1264,8 +1270,8 @@ const SKILL_REVIEW_PLAN_PROMPT = `${SKILL_REVIEW_PROMPT}${PLAN_CHANNEL_NOTE}`;
1264
1270
  /** Subagent-channel variant of the combined review (M-2). */
1265
1271
  const COMBINED_REVIEW_PLAN_PROMPT = `${COMBINED_REVIEW_PROMPT}${PLAN_CHANNEL_NOTE}`;
1266
1272
  function reviewPrompt(kind, channel = "agent") {
1267
- if (kind === "memory") return MEMORY_REVIEW_PROMPT;
1268
1273
  if (channel === "plan") return kind === "skill" ? SKILL_REVIEW_PLAN_PROMPT : COMBINED_REVIEW_PLAN_PROMPT;
1274
+ if (kind === "memory") return MEMORY_REVIEW_PROMPT;
1269
1275
  if (kind === "skill") return SKILL_REVIEW_PROMPT;
1270
1276
  return COMBINED_REVIEW_PROMPT;
1271
1277
  }
@@ -1297,7 +1303,7 @@ const PROMPT_BUNDLE = createPromptBundle({
1297
1303
  skillsGuidance: SKILLS_GUIDANCE
1298
1304
  });
1299
1305
  function verifyPromptBundle(bundle = PROMPT_BUNDLE) {
1300
- if (bundle.id !== "dsh-evolution@13" || bundle.version !== 13) return false;
1306
+ if (bundle.id !== PROMPT_BUNDLE_ID || bundle.version !== 13) return false;
1301
1307
  const canonical = JSON.stringify({
1302
1308
  id: PROMPT_BUNDLE_ID,
1303
1309
  version: 13,
@@ -1510,7 +1516,7 @@ const PATTERNS = [
1510
1516
  label: "hermes_env",
1511
1517
  category: "persistence",
1512
1518
  scope: "strict",
1513
- regex: /\$?HOME\/\.hermes|~\/\.hermes|\.hermes\/\.env/i
1519
+ regex: /\$?HOME\/\.hermes|~\/\.hermes|\.hermes\/\.env|%USERPROFILE%[\\/]\.hermes/i
1514
1520
  },
1515
1521
  {
1516
1522
  label: "c2_node_registration",
@@ -1557,9 +1563,16 @@ const SCOPE_ORDER = {
1557
1563
  strict: 3
1558
1564
  };
1559
1565
  const NO_SCAN_OPTIONS = {};
1566
+ /** Window overlap for the full-coverage scan: far larger than the longest
1567
+ * pattern span (~530 chars: `curl [^\n]{0,512} ...`), so a match straddling a
1568
+ * window boundary is fully inside at least one window (E-12, 0.3.16). */
1569
+ const PATTERN_OVERLAP = 4096;
1560
1570
  /**
1561
1571
  * Scan text at `scope`. Patterns are cumulative: `strict` includes all scopes.
1562
1572
  * `options.excludeLabels` removes matching patterns without changing `scope`.
1573
+ * `maxScanChars` is the WINDOW SIZE, not a total cap (E-12, 0.3.16): the whole
1574
+ * text is always scanned in overlapping windows, so content beyond 65,536
1575
+ * characters (skill files may run to 100,000) is no longer a blind zone.
1563
1576
  */
1564
1577
  function scanThreats(text, scope = "strict", maxScanChars = 65536, options = NO_SCAN_OPTIONS) {
1565
1578
  const findings = [];
@@ -1573,12 +1586,23 @@ function scanThreats(text, scope = "strict", maxScanChars = 65536, options = NO_
1573
1586
  category: "unicode_obfuscation",
1574
1587
  scope
1575
1588
  });
1576
- const normalized = text.normalize("NFKC").slice(0, maxScanChars);
1589
+ const normalized = text.normalize("NFKC");
1590
+ const windows = [];
1591
+ if (normalized.length <= maxScanChars) windows.push(normalized);
1592
+ else {
1593
+ const step = Math.max(1, maxScanChars - PATTERN_OVERLAP);
1594
+ for (let start = 0; start < normalized.length; start += step) windows.push(normalized.slice(start, start + maxScanChars));
1595
+ }
1577
1596
  const excluded = new Set(options.excludeLabels ?? []);
1578
- for (const pattern of PATTERNS) {
1597
+ const seen = /* @__PURE__ */ new Set();
1598
+ for (const window of windows) for (const pattern of PATTERNS) {
1579
1599
  if (SCOPE_ORDER[pattern.scope] > SCOPE_ORDER[scope]) continue;
1580
1600
  if (excluded.has(pattern.label)) continue;
1581
- if (pattern.regex.test(normalized)) findings.push({
1601
+ if (!pattern.regex.test(window)) continue;
1602
+ const key = `${pattern.label}|${pattern.scope}`;
1603
+ if (seen.has(key)) continue;
1604
+ seen.add(key);
1605
+ findings.push({
1582
1606
  label: pattern.label,
1583
1607
  category: pattern.category,
1584
1608
  scope: pattern.scope
@@ -1648,7 +1672,7 @@ function previewEntries(entries) {
1648
1672
  return `\n\nCurrent entries (preview):\n${shown.join("\n")}${more}`;
1649
1673
  }
1650
1674
  function memoryRoot(env = process.env) {
1651
- return join(env.DSH_HOME ?? join(homedir(), ".dsh"), "memories");
1675
+ return join(env.DSH_HOME || join(homedir(), ".dsh"), "memories");
1652
1676
  }
1653
1677
  function fileFor(root, target) {
1654
1678
  return join(root, target === "memory" ? "MEMORY.md" : "USER.md");
@@ -1703,9 +1727,6 @@ var MemoryStore = class {
1703
1727
  const raw = await this.io.readText(fileFor(this.root, target));
1704
1728
  return raw === null ? [] : [...new Set(normalizeEntries(raw))];
1705
1729
  }
1706
- async write(target, entries) {
1707
- await this.io.writeText(fileFor(this.root, target), render(entries));
1708
- }
1709
1730
  resetFailures() {
1710
1731
  this.failureCount = 0;
1711
1732
  }
@@ -2101,7 +2122,7 @@ async function loadMutations(root, io = nodeEvolutionIo()) {
2101
2122
  }
2102
2123
  /** Append one record, trim to `cap`, and write atomically (versioned shape). */
2103
2124
  async function recordMutation(root, io, record, cap = 500) {
2104
- await transactIo(io, mutationsFile(root), async (current) => {
2125
+ await transactIo(io, mutationsFile(root), (current) => {
2105
2126
  if (current !== null) try {
2106
2127
  JSON.parse(current);
2107
2128
  } catch {
@@ -2110,13 +2131,45 @@ async function recordMutation(root, io, record, cap = 500) {
2110
2131
  const existing = parseMutationRecords(current);
2111
2132
  existing.push(record);
2112
2133
  const trimmed = existing.length > cap ? existing.slice(existing.length - cap) : existing;
2113
- return Promise.resolve(JSON.stringify({
2134
+ return JSON.stringify({
2114
2135
  version: 1,
2115
2136
  records: trimmed
2116
- }, null, 2));
2137
+ }, null, 2);
2117
2138
  });
2118
2139
  }
2119
2140
  //#endregion
2141
+ //#region lib/types/preset-composition.js
2142
+ /**
2143
+ * Build the user-root Evolution preset composition from the RUNTIME platform's
2144
+ * `standard` preset rows plus the evolution delta rows (P1-1 follow-up,
2145
+ * 0.3.15): the agent-preset registry mounts ONE composition file verbatim, so
2146
+ * a delta-only `agent.cordis.yml` would produce an agent carrying only the
2147
+ * delta rows.
2148
+ *
2149
+ * Same contract as `install-layered.mjs` `generateAgentPreset` (the source
2150
+ * install path) — installer.spec pins byte parity between the two.
2151
+ *
2152
+ * Row ids are read from `- id:` lines; an id present in both fragments would
2153
+ * mount twice and could shadow the platform row, so it fails loud.
2154
+ * @param standardComposition - the runtime `standard` preset composition.
2155
+ * @param deltaComposition - the evolution delta fragment.
2156
+ * @returns the composed preset composition (standard rows first, then delta).
2157
+ */
2158
+ function composePresetComposition(standardComposition, deltaComposition) {
2159
+ const standardIds = compositionRowIds(standardComposition);
2160
+ const collisions = [...compositionRowIds(deltaComposition)].filter((id) => standardIds.has(id)).sort();
2161
+ if (collisions.length > 0) throw new Error(`evolution preset composition: delta rows collide with runtime standard rows: ${collisions.join(", ")}`);
2162
+ return `${standardComposition.replace(/\s+$/, "")}\n\n${deltaComposition.trim()}\n`;
2163
+ }
2164
+ function compositionRowIds(composition) {
2165
+ const ids = /* @__PURE__ */ new Set();
2166
+ for (const line of composition.split("\n")) {
2167
+ const match = /^- id:\s*(\S+)/.exec(line);
2168
+ if (match) ids.add(match[1] ?? "");
2169
+ }
2170
+ return ids;
2171
+ }
2172
+ //#endregion
2120
2173
  //#region lib/types/quality.js
2121
2174
  /**
2122
2175
  * Quality scoring and near-duplicate detection for the curated skill library.
@@ -2216,7 +2269,7 @@ function computeDedupGroups(input) {
2216
2269
  const [ra, rb] = [find(a), find(b)];
2217
2270
  if (ra !== rb) parent.set(rb, ra);
2218
2271
  };
2219
- for (const [hash, bucketNames] of hashes) {
2272
+ for (const [, bucketNames] of hashes) {
2220
2273
  const first = bucketNames[0];
2221
2274
  if (first === void 0 || bucketNames.length === 1) continue;
2222
2275
  for (let index = 1; index < bucketNames.length; index += 1) {
@@ -2294,9 +2347,9 @@ const SECRET_PATTERNS = [
2294
2347
  ["gitlab token", /glpat-[A-Za-z0-9_-]{16,}/g],
2295
2348
  ["slack token", /xox[baprs]-[A-Za-z0-9-]{10,}/g],
2296
2349
  ["jwt", /eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g],
2297
- ["bearer credential", /Bearer [A-Za-z0-9._~+/=\-]{16,}/g],
2298
- ["inline assignment", /(\b(?:token|api[_-]?key|secret|password|passwd)\b[\s]*[:=][\s]*["']?)([A-Z0-9._~+/=\-]{12,})/gi]
2350
+ ["bearer credential", /Bearer [A-Za-z0-9._~+/=\-]{16,}/g]
2299
2351
  ];
2352
+ const INLINE_ASSIGNMENT_PATTERN = /(\b(?:token|api[_-]?key|secret|password|passwd)\b[\s]*[:=][\s]*["']?)([A-Z0-9._~+/=\-]{12,})/gi;
2300
2353
  /**
2301
2354
  * Mask credential-shaped text before it crosses a session boundary.
2302
2355
  * @param text - the text about to be sent to a model outside this session.
@@ -2304,7 +2357,8 @@ const SECRET_PATTERNS = [
2304
2357
  */
2305
2358
  function redactSecrets(text) {
2306
2359
  let out = text;
2307
- for (const [, pattern] of SECRET_PATTERNS) out = out.replace(pattern, (_match, p1) => p1 === void 0 ? "<redacted>" : `${p1}<redacted>`);
2360
+ for (const [, pattern] of SECRET_PATTERNS) out = out.replace(pattern, "<redacted>");
2361
+ out = out.replace(INLINE_ASSIGNMENT_PATTERN, (_match, p1) => `${p1 ?? ""}<redacted>`);
2308
2362
  return out;
2309
2363
  }
2310
2364
  //#endregion
@@ -2382,6 +2436,7 @@ const FIX_PATTERNS = [/worked after|fixed by|the fix was|root cause/i, /retry(?:
2382
2436
  /** Fold one session event into the current turn observation. */
2383
2437
  function observeEvent(signal, event) {
2384
2438
  if (event.type === "user/message") {
2439
+ if (!Array.isArray(event.data.content)) return;
2385
2440
  const text = event.data.content.map((block) => block.type === "text" ? block.text : "").join(" ");
2386
2441
  signal.userChars += text.length;
2387
2442
  if (CORRECTION_PATTERNS.some((pattern) => pattern.test(text))) signal.memorySignal = true;
@@ -2441,6 +2496,165 @@ function foldTurn(session, fromSeq) {
2441
2496
  return signal;
2442
2497
  }
2443
2498
  //#endregion
2499
+ //#region lib/types/drift-signals.js
2500
+ /**
2501
+ * Library-level drift signals for the maintenance subagent (design 011).
2502
+ *
2503
+ * Deterministic fact checks over a skill-library snapshot: domain drift
2504
+ * (narrow names, near-duplicate groups, prefix clusters) and layer drift
2505
+ * (log-like bodies, duplicate headings, overlong lines, missing support-file
2506
+ * pointers, description over the authoring bar). Pure functions only — no IO,
2507
+ * no LLM, no services. Thresholds are imported from their owning modules
2508
+ * (skill-health / quality / skill-store), never duplicated.
2509
+ *
2510
+ * Distinct from `signals.ts` — the session-level review signal gate.
2511
+ */
2512
+ /** Physical line length at/above which a body line is reported overlong (011 §4). */
2513
+ const DRIFT_MAX_LINE_CHARS = 1500;
2514
+ /** Signal-set version: bump whenever ids/thresholds change (011 §7 version coupling). */
2515
+ const DRIFT_SIGNALS_VERSION = "1";
2516
+ /** Render-time nouns for the MAINTAIN_PROMPT placeholders (single vocabulary with the facts block). */
2517
+ const DRIFT_SIGNAL_NOUNS = {
2518
+ dedup_group: "近重复组",
2519
+ prefix_cluster: "前缀聚类",
2520
+ stamp_density: "stamp 密度",
2521
+ body_size: "正文体量",
2522
+ dup_heading: "重复标题",
2523
+ overlong_line: "超长行",
2524
+ pointer_missing: "缺失指针",
2525
+ description_chars: "描述长度",
2526
+ narrow_name: "窄名",
2527
+ usage_observed: "使用观察",
2528
+ quality_low: "质量分"
2529
+ };
2530
+ const NARROW_NAME_PATTERNS = [
2531
+ {
2532
+ label: "error-string",
2533
+ re: /^(?:err|error|exception|traceback|warn|fail)(?:[-_][a-z0-9]+)+$/i
2534
+ },
2535
+ {
2536
+ label: "pr-number",
2537
+ re: /^(?:pr|issue)[-_]?\d{2,}$/i
2538
+ },
2539
+ {
2540
+ label: "dated",
2541
+ re: /\d{4}-\d{2}-\d{2}/
2542
+ },
2543
+ {
2544
+ label: "session-verb",
2545
+ re: /^(?:fix|debug|audit|salvage|diagnose|investigate)[-_][a-z0-9-]+$/i
2546
+ }
2547
+ ];
2548
+ /** Detect support files the body never references (by basename or relative path). */
2549
+ function missingSupportPointers(body, supportFiles) {
2550
+ return supportFiles.filter((path) => {
2551
+ const base = path.split("/").pop() ?? path;
2552
+ return base.length > 0 && !body.includes(base) && !body.includes(path);
2553
+ });
2554
+ }
2555
+ /** Duplicate `## heading` occurrences: singleton results default to head of the file. */
2556
+ function duplicateHeadings(body) {
2557
+ const counts = /* @__PURE__ */ new Map();
2558
+ for (const line of body.split("\n")) {
2559
+ const m = /^##\s+(.+)$/.exec(line);
2560
+ if (m?.[1]) {
2561
+ const heading = m[1].trim();
2562
+ if (heading) counts.set(heading, (counts.get(heading) ?? 0) + 1);
2563
+ }
2564
+ }
2565
+ return [...counts.entries()].filter(([, count]) => count > 1).map(([heading, count]) => ({
2566
+ heading,
2567
+ count
2568
+ }));
2569
+ }
2570
+ /** Physical lines over `max` characters: `{ lineNo, chars }`, 1-based line numbers. */
2571
+ function overlongLines(body, max = DRIFT_MAX_LINE_CHARS) {
2572
+ const out = [];
2573
+ const lines = body.split("\n");
2574
+ for (let index = 0; index < lines.length; index += 1) {
2575
+ const length = (lines[index] ?? "").length;
2576
+ if (length > max) out.push({
2577
+ lineNo: index + 1,
2578
+ chars: length
2579
+ });
2580
+ }
2581
+ return out;
2582
+ }
2583
+ /** Narrow-name shapes detected in a skill name (empty = none). */
2584
+ function narrowNameMatches(name) {
2585
+ return NARROW_NAME_PATTERNS.filter(({ re }) => re.test(name)).map(({ label }) => label);
2586
+ }
2587
+ function supportGroupCount(supportFiles) {
2588
+ const groups = /* @__PURE__ */ new Set();
2589
+ for (const path of supportFiles ?? []) {
2590
+ const head = path.split("/")[0];
2591
+ if (head) groups.add(head);
2592
+ }
2593
+ return groups.size;
2594
+ }
2595
+ function sig(id, verdict, value, threshold, detail) {
2596
+ return {
2597
+ id,
2598
+ verdict,
2599
+ value,
2600
+ threshold,
2601
+ detail
2602
+ };
2603
+ }
2604
+ /**
2605
+ * Compute all drift signals for a snapshot. Missing inputs (quality score,
2606
+ * usage window) yield `unknown` — never a fabricated verdict.
2607
+ */
2608
+ function computeDriftSignals(snapshots) {
2609
+ const library = [];
2610
+ const names = snapshots.map((s) => s.name);
2611
+ const dedup = computeDedupGroups({ contents: new Map(snapshots.map((s) => [s.name, s.body])) });
2612
+ library.push(dedup.length === 0 ? sig("dedup_group", "pass", "none", "size >= 2") : sig("dedup_group", "over", dedup.map((group) => group.join(", ")).join(" | "), "size >= 2", `members=${dedup.map((group) => group.join("|")).join(";")}`));
2613
+ const clusters = computePrefixClusters(names);
2614
+ library.push(clusters.length === 0 ? sig("prefix_cluster", "pass", "none", "size >= 2") : sig("prefix_cluster", "over", clusters.map((cluster) => cluster.members.join(", ")).join(" | "), "size >= 2", `key=${clusters.map((cluster) => cluster.key).join("|")}`));
2615
+ const allProvided = snapshots.length > 0 && snapshots.every((s) => s.usageObserved !== null && s.usageObserved !== void 0);
2616
+ library.push(!allProvided ? sig("usage_observed", "unknown", "not-observed", void 0, "usage window status missing") : snapshots.every((s) => s.usageObserved === true) ? sig("usage_observed", "pass", "observed") : sig("usage_observed", "pass", "unobserved"));
2617
+ return {
2618
+ library,
2619
+ skills: snapshots.map((snapshot) => {
2620
+ const signals = [];
2621
+ const body = snapshot.body;
2622
+ const supportFiles = snapshot.supportFiles ?? [];
2623
+ const supportEnumerated = snapshot.supportFiles !== void 0;
2624
+ const density = assessStructureHealth({
2625
+ skillName: snapshot.name,
2626
+ bodyChars: body.length,
2627
+ bodyText: body,
2628
+ supportGroups: supportGroupCount(supportFiles)
2629
+ }, DEFAULT_HEALTH_THRESHOLDS).dims.stampDensityPerKb;
2630
+ signals.push(density === null ? sig("stamp_density", "pass", body.length < 2e3 ? "below-min-body" : "not-assessed") : sig("stamp_density", density >= DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb ? "over" : "pass", `${density.toFixed(2)}/KB`, `${DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb}/KB`));
2631
+ signals.push(sig("body_size", body.length >= DEFAULT_HEALTH_THRESHOLDS.softBodyChars ? "over" : "pass", `${body.length}`, `${DEFAULT_HEALTH_THRESHOLDS.softBodyChars}`));
2632
+ const dupes = duplicateHeadings(body);
2633
+ signals.push(dupes.length === 0 ? sig("dup_heading", "pass", "none") : sig("dup_heading", "over", dupes.map((d) => `${d.heading}(${d.count})`).join(", "), "count >= 2"));
2634
+ const long = overlongLines(body);
2635
+ signals.push(long.length === 0 ? sig("overlong_line", "pass", "none") : sig("overlong_line", "over", long.map((l) => `${l.lineNo}:${l.chars}`).join(", "), `${DRIFT_MAX_LINE_CHARS}`));
2636
+ const missing = supportEnumerated ? missingSupportPointers(body, supportFiles) : void 0;
2637
+ signals.push(!supportEnumerated ? sig("pointer_missing", "unknown", "not-enumerated", void 0, "support files not enumerated") : (missing ?? []).length === 0 ? sig("pointer_missing", "pass", "none") : sig("pointer_missing", "over", missing?.join(", ") ?? ""));
2638
+ const narrow = narrowNameMatches(snapshot.name);
2639
+ signals.push(narrow.length === 0 ? sig("narrow_name", "pass", "none") : sig("narrow_name", "over", narrow.join(", "), void 0, `name=${snapshot.name}`));
2640
+ const description = snapshot.description;
2641
+ signals.push(description === void 0 ? sig("description_chars", "unknown", "missing", "60") : sig("description_chars", description.length > 60 ? "over" : "pass", `${description.length}`, `60`));
2642
+ const quality = snapshot.quality;
2643
+ signals.push(quality === null || quality === void 0 ? sig("quality_low", "unknown", "not-assessed") : sig("quality_low", quality < .3 ? "over" : "pass", quality.toFixed(2), `${LOW_QUALITY_THRESHOLD}`));
2644
+ return {
2645
+ name: snapshot.name,
2646
+ signals,
2647
+ ...snapshot.protected !== void 0 && snapshot.protected !== null ? { protected: snapshot.protected } : {},
2648
+ ...snapshot.catalogInvalid !== void 0 ? { catalogInvalid: snapshot.catalogInvalid } : {}
2649
+ };
2650
+ })
2651
+ };
2652
+ }
2653
+ /** Convenience: fetch one signal from an assessment or library list. */
2654
+ function findDriftSignal(signals, id) {
2655
+ return signals.find((signal) => signal.id === id);
2656
+ }
2657
+ //#endregion
2444
2658
  //#region lib/types/skill-store.js
2445
2659
  /**
2446
2660
  * Skill library management for the self-evolution plugin.
@@ -2450,6 +2664,10 @@ function foldTurn(session, fromSeq) {
2450
2664
  * it created unless a `.hermes-managed` marker opts a skill in. Archival is a
2451
2665
  * move to `.archive/` — never a hard delete.
2452
2666
  */
2667
+ /** 0.3.16 (S1.13, T-6): the pointer-line prefix written into a body when a
2668
+ * section is moved to references/ — single literal, both restructure and
2669
+ * append-mode consolidation emit the same discoverability line. */
2670
+ const POINTER_LINE_PREFIX = "> 详见 references/";
2453
2671
  const DEFAULT_SKILL_LIMITS = {
2454
2672
  maxNameLength: 64,
2455
2673
  maxDescriptionLength: MAX_DESCRIPTION_LENGTH,
@@ -2463,7 +2681,7 @@ const RESTRUCTURE_TARGET_RE = /^references\/[a-z0-9][a-z0-9._-]*\.md$/;
2463
2681
  /** Extra file name carried inside a snapshot's `extras/` directory. */
2464
2682
  const SNAPSHOT_EXTRA_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
2465
2683
  function skillsRoot(env = process.env) {
2466
- return join(env.DSH_HOME ?? join(homedir(), ".dsh"), "skills");
2684
+ return join(env.DSH_HOME || join(homedir(), ".dsh"), "skills");
2467
2685
  }
2468
2686
  /**
2469
2687
  * Map a requesting session onto the two origin surfaces (rc.44 plan M2-2.3):
@@ -2551,21 +2769,26 @@ function parseFrontmatter(content) {
2551
2769
  }
2552
2770
  /** YAML plain-scalar hazards that make an UNQUOTED frontmatter value
2553
2771
  * unloadable to the platform catalog (strict YAML parser): `: ` (mapping
2554
- * separator), ` #` (comment start), or a leading YAML indicator. The
2555
- * evolution `parseFrontmatter` is deliberately lenient, so violations
2556
- * silently split family-visibility from platform-visibility (0.3.11
2557
- * inkos-harness case: the description carried "…: " and the catalog dropped
2558
- * the whole skill). Already-quoted values and well-formed flow collections
2559
- * (`[a, b]` / `{a: b}`) are considered safe. This rule is only the FAST
2560
- * PATH — the write path re-verifies every rewrite with the real YAML parser
2561
- * (see normalizeFrontmatter), so an incomplete approximation can never
2562
- * corrupt a multiline flow value (P3-4). */
2772
+ * separator), ` #` (comment start), a trailing `:` (a mapping marker),
2773
+ * or a leading YAML indicator. The evolution `parseFrontmatter` is
2774
+ * deliberately lenient, so violations silently split family-visibility from
2775
+ * platform-visibility (0.3.11 inkos-harness case: the description carried
2776
+ * "…: " and the catalog dropped the whole skill). Already-quoted values and
2777
+ * well-formed flow collections (`[a, b]` / `{a: b}`) are considered safe.
2778
+ * 0.3.16 (E-47): null/bool/number-shaped plain scalars are flagged too — they
2779
+ * parse as booleans/numbers on the platform while the family keeps the string
2780
+ * (a `description: true` split-brain).
2781
+ * This rule is only the FAST PATH — the write path re-verifies every rewrite
2782
+ * with the real YAML parser (see normalizeFrontmatter), so an incomplete
2783
+ * approximation can never corrupt a multiline flow value (P3-4). */
2563
2784
  function yamlPlainScalarNeedsQuotes(value) {
2564
2785
  if (value.length === 0) return false;
2565
2786
  if (value.length >= 2 && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'"))) return false;
2566
2787
  if (/^\[.*\]$/.test(value) || /^\{.*\}$/.test(value)) return false;
2567
2788
  if (value.includes(": ")) return true;
2568
2789
  if (value.includes(" #")) return true;
2790
+ if (value.endsWith(":")) return true;
2791
+ if (/^(?:null|true|false|~|[-+]?\d+(?:\.\d+)?)$/i.test(value)) return true;
2569
2792
  if (/^[-?:,[\]{}#&*!|>'\"%@`\s]/.test(value)) return true;
2570
2793
  return false;
2571
2794
  }
@@ -2691,10 +2914,6 @@ function validateFrontmatter(content, expectedName, limits = DEFAULT_SKILL_LIMIT
2691
2914
  if (content.length > limits.maxSkillContentChars) return `SKILL.md content exceeds ${limits.maxSkillContentChars} characters. Consider splitting into a smaller SKILL.md with supporting files.`;
2692
2915
  return null;
2693
2916
  }
2694
- /** Hermes authoring quality bar for descriptions (the 60-char Rule). The
2695
- * platform's own index limit stays in `validateFrontmatter`; this bar is the
2696
- * target the authoring standard names, enforced as ADVISORY feedback. */
2697
- const AUTHORING_DESCRIPTION_BAR = 60;
2698
2917
  /**
2699
2918
  * Advisory authoring feedback (P0): evaluate frontmatter against the
2700
2919
  * authoring bar WITHOUT changing platform validation semantics. The bar is
@@ -2803,7 +3022,7 @@ function fuzzyReplace(content, oldString, newString, replaceAll) {
2803
3022
  }
2804
3023
  function fuzzyPatch(content, oldString, newString, replaceAll = false) {
2805
3024
  if (oldString === "") return null;
2806
- if (content.includes(oldString)) return replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, newString);
3025
+ if (content.includes(oldString)) return replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, () => newString);
2807
3026
  const boundary = trimPatternBoundaries(oldString);
2808
3027
  if (boundary === "") return null;
2809
3028
  if (boundary !== oldString) {
@@ -2868,7 +3087,7 @@ function planRestructureSections(body, moves) {
2868
3087
  for (let i = 0; i < lines.length; i += 1) {
2869
3088
  const span = byStart.get(i);
2870
3089
  if (span) {
2871
- rebuilt.push(`> 详见 references/${span.rel.split("/").at(-1)}`);
3090
+ rebuilt.push(`${POINTER_LINE_PREFIX}${span.rel.split("/").at(-1)}`);
2872
3091
  i = span.end - 1;
2873
3092
  } else rebuilt.push(lines[i] ?? "");
2874
3093
  }
@@ -2925,7 +3144,12 @@ var SkillLibrary = class {
2925
3144
  async read(rawName) {
2926
3145
  const name = rawName.trim();
2927
3146
  if (this.badName(name) !== null) return null;
2928
- return this.io.readText(join(this.dirOf(name), "SKILL.md"));
3147
+ try {
3148
+ return await this.io.readText(join(this.dirOf(name), "SKILL.md"));
3149
+ } catch (error) {
3150
+ if (error?.code === "EISDIR") return null;
3151
+ throw error;
3152
+ }
2929
3153
  }
2930
3154
  /**
2931
3155
 
@@ -3148,7 +3372,7 @@ var SkillLibrary = class {
3148
3372
  this.notifyMutation({
3149
3373
  action: "create",
3150
3374
  name: normalized,
3151
- filePath: dir
3375
+ skillDir: dir
3152
3376
  });
3153
3377
  return {
3154
3378
  ok: true,
@@ -3203,7 +3427,7 @@ var SkillLibrary = class {
3203
3427
  this.notifyMutation({
3204
3428
  action: "update",
3205
3429
  name,
3206
- filePath: dir
3430
+ skillDir: dir
3207
3431
  });
3208
3432
  return {
3209
3433
  ok: true,
@@ -3292,7 +3516,7 @@ var SkillLibrary = class {
3292
3516
  this.notifyMutation({
3293
3517
  action: "patch",
3294
3518
  name,
3295
- filePath: dir
3519
+ skillDir: dir
3296
3520
  });
3297
3521
  return {
3298
3522
  ok: true,
@@ -3342,7 +3566,23 @@ var SkillLibrary = class {
3342
3566
  await this.io.rename(dir, dest);
3343
3567
  } catch {
3344
3568
  await this.io.copy(dir, dest);
3345
- await this.io.remove(dir);
3569
+ try {
3570
+ await this.io.remove(dir);
3571
+ } catch (error) {
3572
+ const reason = error instanceof Error ? error.message : String(error);
3573
+ try {
3574
+ await this.io.remove(dest);
3575
+ return {
3576
+ ok: false,
3577
+ message: `Archive copy succeeded but the source could not be removed (${reason}); the copied archive was rolled back.`
3578
+ };
3579
+ } catch {
3580
+ return {
3581
+ ok: false,
3582
+ message: `Archive copy succeeded but the source could not be removed (${reason}) and the archive copy could not be rolled back — the skill now exists in BOTH the active root and .archive; clean up manually.`
3583
+ };
3584
+ }
3585
+ }
3346
3586
  }
3347
3587
  const reason = options.reason ?? (options.absorbedInto ? `Consolidated into ${options.absorbedInto}` : "Archived by self-evolution curator");
3348
3588
  await this.io.writeText(join(dest, ".archive-reason"), `${(/* @__PURE__ */ new Date()).toISOString()}: ${reason}\n`);
@@ -3467,7 +3707,7 @@ var SkillLibrary = class {
3467
3707
  content: `<!-- demoted from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}\n`
3468
3708
  });
3469
3709
  }
3470
- const pointerLines = normalizedSources.map((source) => `\n> 详见 references/${source}.md`).join("");
3710
+ const pointerLines = normalizedSources.map((source) => `\n${POINTER_LINE_PREFIX}${source}.md`).join("");
3471
3711
  const extended = targetMd.trimEnd() + pointerLines + "\n";
3472
3712
  const validation = validateFrontmatter(extended, targetName, this.limits);
3473
3713
  if (validation) return {
@@ -3497,10 +3737,20 @@ var SkillLibrary = class {
3497
3737
  });
3498
3738
  if (!result.ok) throw new Error(result.message);
3499
3739
  } catch (error) {
3500
- for (const source of archived.reverse()) await this.restoreFromArchive(source).catch(() => {});
3740
+ const reason = error instanceof Error ? error.message : String(error);
3741
+ const failedRestores = [];
3742
+ for (const source of archived.reverse()) try {
3743
+ if (!(await this.restoreFromArchive(source)).ok) failedRestores.push(source);
3744
+ } catch {
3745
+ failedRestores.push(source);
3746
+ }
3747
+ if (failedRestores.length > 0) return {
3748
+ ok: false,
3749
+ message: `Consolidation failed (${reason}); rolled back EXCEPT ${failedRestores.join(", ")} — still in .archive, restore them with /evolution skill restore.`
3750
+ };
3501
3751
  return {
3502
3752
  ok: false,
3503
- message: `Consolidation failed and was rolled back: ${error instanceof Error ? error.message : String(error)}`
3753
+ message: `Consolidation failed and was rolled back: ${reason}`
3504
3754
  };
3505
3755
  }
3506
3756
  return {
@@ -3552,20 +3802,20 @@ var SkillLibrary = class {
3552
3802
  ok: false,
3553
3803
  message: `Skill "${name}" not found.`
3554
3804
  };
3555
- const normalized = md.replace(/\r\n/g, "\n");
3556
- const frontmatterEnd = normalized.indexOf("\n---", 3);
3557
- if (frontmatterEnd < 0) return {
3805
+ const block = frontmatterBlock(md);
3806
+ if (!block) return {
3558
3807
  ok: false,
3559
3808
  message: "SKILL.md has no valid frontmatter; refusing to restructure."
3560
3809
  };
3561
- const header = normalized.slice(0, frontmatterEnd + 4);
3562
- const plan = planRestructureSections(normalized.slice(frontmatterEnd + 4), moves);
3810
+ const header = block.lines.slice(0, block.end + 1).join(block.nl);
3811
+ const plan = planRestructureSections(md.slice(header.length).replace(/\r\n/g, "\n"), moves);
3563
3812
  if ("error" in plan) return {
3564
3813
  ok: false,
3565
3814
  message: `Restructure rejected: ${plan.error}`
3566
3815
  };
3567
- const newMd = header + plan.body;
3568
- const newMdCheck = validateFrontmatter(newMd, name, this.limits);
3816
+ const newMd = `${header}${plan.body}`.replace(/\r\n/g, "\n");
3817
+ const finalMd = block.nl === "\r\n" ? newMd.replace(/\n/g, "\r\n") : newMd;
3818
+ const newMdCheck = validateFrontmatter(finalMd, name, this.limits);
3569
3819
  if (newMdCheck) return {
3570
3820
  ok: false,
3571
3821
  message: `Restructure rejected: ${newMdCheck}`
@@ -3597,7 +3847,7 @@ var SkillLibrary = class {
3597
3847
  }
3598
3848
  writes.push({
3599
3849
  target: join(dir, "SKILL.md"),
3600
- content: newMd
3850
+ content: finalMd
3601
3851
  });
3602
3852
  const result = await this.applyTreeChange({
3603
3853
  name,
@@ -3689,11 +3939,11 @@ var SkillLibrary = class {
3689
3939
  message: `Tree change failed and was rolled back: ${error instanceof Error ? error.message : String(error)}`
3690
3940
  };
3691
3941
  }
3692
- await this.audit(name, plan.auditAction, md, landing.find((entry) => entry.target.endsWith("SKILL.md"))?.content ?? md, plan.auditSummary);
3942
+ await this.audit(name, plan.auditAction, md, landing.find((entry) => entry.target.split(/[\\/]/).pop() === "SKILL.md")?.content ?? md, plan.auditSummary);
3693
3943
  this.notifyMutation({
3694
3944
  action: plan.eventAction,
3695
3945
  name,
3696
- filePath: dir
3946
+ skillDir: dir
3697
3947
  });
3698
3948
  return {
3699
3949
  ok: true,
@@ -3726,7 +3976,12 @@ var SkillLibrary = class {
3726
3976
  message: "No skill archive available."
3727
3977
  };
3728
3978
  }
3729
- const chosen = entries.filter((entry) => entry === name || entry.startsWith(`${name}-`)).sort().reverse()[0];
3979
+ const candidates = entries.filter((entry) => entry === name || entry.startsWith(`${name}-`)).sort().reverse();
3980
+ let chosen;
3981
+ for (const candidate of candidates) if (parseFrontmatter(await this.io.readText(join(archiveRoot, candidate, "SKILL.md")).catch(() => null) ?? "")?.frontmatter.name === name) {
3982
+ chosen = candidate;
3983
+ break;
3984
+ }
3730
3985
  if (!chosen) return {
3731
3986
  ok: false,
3732
3987
  message: `Skill "${name}" is not in .archive.`
@@ -3742,14 +3997,21 @@ var SkillLibrary = class {
3742
3997
  try {
3743
3998
  await this.io.rename(source, dest);
3744
3999
  } catch {
3745
- await this.io.copy(source, dest);
3746
- await this.io.remove(source);
4000
+ try {
4001
+ await this.io.copy(source, dest);
4002
+ await this.io.remove(source);
4003
+ } catch (error) {
4004
+ return {
4005
+ ok: false,
4006
+ message: `Restore of "${name}" from .archive failed: ${error instanceof Error ? error.message : String(error)}`
4007
+ };
4008
+ }
3747
4009
  }
3748
4010
  if (await this.io.exists(join(dest, ".archive-reason"))) await this.io.remove(join(dest, ".archive-reason"));
3749
4011
  this.notifyMutation({
3750
4012
  action: "restore",
3751
4013
  name,
3752
- filePath: dest
4014
+ skillDir: dest
3753
4015
  });
3754
4016
  return {
3755
4017
  ok: true,
@@ -3795,7 +4057,8 @@ var SkillLibrary = class {
3795
4057
  this.notifyMutation({
3796
4058
  action: "write_file",
3797
4059
  name,
3798
- filePath: target
4060
+ skillDir: dir,
4061
+ file: target
3799
4062
  });
3800
4063
  return {
3801
4064
  ok: true,
@@ -3836,7 +4099,8 @@ var SkillLibrary = class {
3836
4099
  this.notifyMutation({
3837
4100
  action: "remove_file",
3838
4101
  name,
3839
- filePath: target
4102
+ skillDir: dir,
4103
+ file: target
3840
4104
  });
3841
4105
  return {
3842
4106
  ok: true,
@@ -3964,7 +4228,43 @@ var SkillLibrary = class {
3964
4228
  ok: false,
3965
4229
  message: "No skill snapshot available."
3966
4230
  };
3967
- await this.snapshotAll("pre-rollback", extras);
4231
+ const preRollbackPath = await this.snapshotAll("pre-rollback", extras);
4232
+ try {
4233
+ await this.restoreSnapshotIntoRoot(latest.path);
4234
+ } catch (error) {
4235
+ const reason = error instanceof Error ? error.message : String(error);
4236
+ try {
4237
+ await this.restoreSnapshotIntoRoot(preRollbackPath);
4238
+ return {
4239
+ ok: false,
4240
+ message: `Snapshot restore failed (${reason}); the active tree was rolled back to the pre-rollback snapshot.`
4241
+ };
4242
+ } catch (rollbackError) {
4243
+ return {
4244
+ ok: false,
4245
+ message: `Snapshot restore failed (${reason}) AND pre-rollback restore failed (${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}). Rescue manually from: ${preRollbackPath} (pre-rollback), ${latest.path} (target).`
4246
+ };
4247
+ }
4248
+ }
4249
+ const snapshotExtras = await this.readSnapshotExtras(latest.path);
4250
+ this.notifyMutation({
4251
+ action: "restore",
4252
+ name: "snapshot"
4253
+ });
4254
+ return {
4255
+ ok: true,
4256
+ message: `Restored skill tree from ${latest.path}`,
4257
+ path: latest.path,
4258
+ ...snapshotExtras.length === 0 ? {} : { extras: snapshotExtras }
4259
+ };
4260
+ }
4261
+ /**
4262
+ * Whole-tree replacement from one snapshot path (rc.50 P2-14): every
4263
+ * NON-system entry in the active root is cleared first, then the manifest
4264
+ * drives the repopulation (skills, sidecars, `.archive`). Extracted from
4265
+ * restoreLatestSnapshot so a failed restore can roll itself back (E-13).
4266
+ */
4267
+ async restoreSnapshotIntoRoot(snapshotPath) {
3968
4268
  let rootEntries;
3969
4269
  try {
3970
4270
  rootEntries = await this.io.list(this.root);
@@ -3975,200 +4275,30 @@ var SkillLibrary = class {
3975
4275
  if (entry.startsWith(".")) continue;
3976
4276
  await this.io.remove(join(this.root, entry));
3977
4277
  }
3978
- const manifest = await this.readSnapshotManifest(latest.path);
3979
- if (manifest === null) for (const entry of await this.io.list(latest.path)) {
4278
+ const manifest = await this.readSnapshotManifest(snapshotPath);
4279
+ if (manifest === null) for (const entry of await this.io.list(snapshotPath)) {
3980
4280
  if (entry === "manifest.json" || entry === "extras") continue;
3981
- await this.io.copy(join(latest.path, entry), join(this.root, entry));
4281
+ await this.io.copy(join(snapshotPath, entry), join(this.root, entry));
3982
4282
  }
3983
4283
  else {
3984
- for (const name of manifest.skills) await this.io.copy(join(latest.path, name), join(this.root, name));
3985
- for (const sidecar of manifest.sidecars) await this.io.copy(join(latest.path, sidecar), join(this.root, sidecar));
4284
+ for (const name of manifest.skills) await this.io.copy(join(snapshotPath, name), join(this.root, name));
4285
+ for (const sidecar of manifest.sidecars) await this.io.copy(join(snapshotPath, sidecar), join(this.root, sidecar));
3986
4286
  const archiveRoot = join(this.root, ".archive");
3987
4287
  if (manifest.hasArchive === true) {
3988
4288
  await this.io.remove(archiveRoot);
3989
- await this.io.copy(join(latest.path, ".archive"), archiveRoot);
4289
+ await this.io.copy(join(snapshotPath, ".archive"), archiveRoot);
3990
4290
  } else if (manifest.hasArchive === false) await this.io.remove(archiveRoot);
3991
4291
  }
3992
- const snapshotExtras = await this.readSnapshotExtras(latest.path);
3993
- this.notifyMutation({
3994
- action: "restore",
3995
- name: "snapshot"
3996
- });
3997
- return {
3998
- ok: true,
3999
- message: `Restored skill tree from ${latest.path}`,
4000
- path: latest.path,
4001
- ...snapshotExtras.length === 0 ? {} : { extras: snapshotExtras }
4002
- };
4003
4292
  }
4004
4293
  };
4005
4294
  //#endregion
4006
- //#region lib/types/drift-signals.js
4007
- /**
4008
- * Library-level drift signals for the maintenance subagent (design 011).
4009
- *
4010
- * Deterministic fact checks over a skill-library snapshot: domain drift
4011
- * (narrow names, near-duplicate groups, prefix clusters) and layer drift
4012
- * (log-like bodies, duplicate headings, overlong lines, missing support-file
4013
- * pointers, description over the authoring bar). Pure functions only — no IO,
4014
- * no LLM, no services. Thresholds are imported from their owning modules
4015
- * (skill-health / quality / skill-store), never duplicated.
4016
- *
4017
- * Distinct from `signals.ts` — the session-level review signal gate.
4018
- */
4019
- /** Physical line length at/above which a body line is reported overlong (011 §4). */
4020
- const DRIFT_MAX_LINE_CHARS = 1500;
4021
- /** Signal-set version: bump whenever ids/thresholds change (011 §7 version coupling). */
4022
- const DRIFT_SIGNALS_VERSION = "1";
4023
- /** Render-time nouns for the MAINTAIN_PROMPT placeholders (single vocabulary with the facts block). */
4024
- const DRIFT_SIGNAL_NOUNS = {
4025
- dedup_group: "近重复组",
4026
- prefix_cluster: "前缀聚类",
4027
- stamp_density: "stamp 密度",
4028
- body_size: "正文体量",
4029
- dup_heading: "重复标题",
4030
- overlong_line: "超长行",
4031
- pointer_missing: "缺失指针",
4032
- description_chars: "描述长度",
4033
- narrow_name: "窄名",
4034
- usage_observed: "使用观察",
4035
- quality_low: "质量分"
4036
- };
4037
- const NARROW_NAME_PATTERNS = [
4038
- {
4039
- label: "error-string",
4040
- re: /^(?:err|error|exception|traceback|warn|fail)(?:[-_][a-z0-9]+)+$/i
4041
- },
4042
- {
4043
- label: "pr-number",
4044
- re: /^(?:pr|issue)[-_]?\d{2,}$/i
4045
- },
4046
- {
4047
- label: "dated",
4048
- re: /\d{4}-\d{2}-\d{2}/
4049
- },
4050
- {
4051
- label: "session-verb",
4052
- re: /^(?:fix|debug|audit|salvage|diagnose|investigate)[-_][a-z0-9-]+$/i
4053
- }
4054
- ];
4055
- /** Detect support files the body never references (by basename or relative path). */
4056
- function missingSupportPointers(body, supportFiles) {
4057
- return supportFiles.filter((path) => {
4058
- const base = path.split("/").pop() ?? path;
4059
- return base.length > 0 && !body.includes(base) && !body.includes(path);
4060
- });
4061
- }
4062
- /** Duplicate `## heading` occurrences: singleton results default to head of the file. */
4063
- function duplicateHeadings(body) {
4064
- const counts = /* @__PURE__ */ new Map();
4065
- for (const line of body.split("\n")) {
4066
- const m = /^##\s+(.+)$/.exec(line);
4067
- if (m?.[1]) {
4068
- const heading = m[1].trim();
4069
- if (heading) counts.set(heading, (counts.get(heading) ?? 0) + 1);
4070
- }
4071
- }
4072
- return [...counts.entries()].filter(([, count]) => count > 1).map(([heading, count]) => ({
4073
- heading,
4074
- count
4075
- }));
4076
- }
4077
- /** Physical lines over `max` characters: `{ lineNo, chars }`, 1-based line numbers. */
4078
- function overlongLines(body, max = DRIFT_MAX_LINE_CHARS) {
4079
- const out = [];
4080
- const lines = body.split("\n");
4081
- for (let index = 0; index < lines.length; index += 1) {
4082
- const length = (lines[index] ?? "").length;
4083
- if (length > max) out.push({
4084
- lineNo: index + 1,
4085
- chars: length
4086
- });
4087
- }
4088
- return out;
4089
- }
4090
- /** Narrow-name shapes detected in a skill name (empty = none). */
4091
- function narrowNameMatches(name) {
4092
- return NARROW_NAME_PATTERNS.filter(({ re }) => re.test(name)).map(({ label }) => label);
4093
- }
4094
- function supportGroupCount(supportFiles) {
4095
- const groups = /* @__PURE__ */ new Set();
4096
- for (const path of supportFiles ?? []) {
4097
- const head = path.split("/")[0];
4098
- if (head) groups.add(head);
4099
- }
4100
- return groups.size;
4101
- }
4102
- function sig(id, verdict, value, threshold, detail) {
4103
- return {
4104
- id,
4105
- verdict,
4106
- value,
4107
- threshold,
4108
- detail
4109
- };
4110
- }
4111
- /**
4112
- * Compute all drift signals for a snapshot. Missing inputs (quality score,
4113
- * usage window) yield `unknown` — never a fabricated verdict.
4114
- */
4115
- function computeDriftSignals(snapshots) {
4116
- const library = [];
4117
- const names = snapshots.map((s) => s.name);
4118
- const dedup = computeDedupGroups({ contents: new Map(snapshots.map((s) => [s.name, s.body])) });
4119
- library.push(dedup.length === 0 ? sig("dedup_group", "pass", "none", "size >= 2") : sig("dedup_group", "over", dedup.map((group) => group.join(", ")).join(" | "), "size >= 2", `members=${dedup.map((group) => group.join("|")).join(";")}`));
4120
- const clusters = computePrefixClusters(names);
4121
- library.push(clusters.length === 0 ? sig("prefix_cluster", "pass", "none", "size >= 2") : sig("prefix_cluster", "over", clusters.map((cluster) => cluster.members.join(", ")).join(" | "), "size >= 2", `key=${clusters.map((cluster) => cluster.key).join("|")}`));
4122
- const allProvided = snapshots.length > 0 && snapshots.every((s) => s.usageObserved !== null && s.usageObserved !== void 0);
4123
- library.push(!allProvided ? sig("usage_observed", "unknown", "not-observed", void 0, "usage window status missing") : snapshots.every((s) => s.usageObserved === true) ? sig("usage_observed", "pass", "observed") : sig("usage_observed", "pass", "unobserved"));
4124
- return {
4125
- library,
4126
- skills: snapshots.map((snapshot) => {
4127
- const signals = [];
4128
- const body = snapshot.body;
4129
- const supportFiles = snapshot.supportFiles ?? [];
4130
- const supportEnumerated = snapshot.supportFiles !== void 0;
4131
- const density = assessStructureHealth({
4132
- skillName: snapshot.name,
4133
- bodyChars: body.length,
4134
- bodyText: body,
4135
- supportGroups: supportGroupCount(supportFiles)
4136
- }, DEFAULT_HEALTH_THRESHOLDS).dims.stampDensityPerKb;
4137
- signals.push(density === null ? sig("stamp_density", "pass", body.length < 2e3 ? "below-min-body" : "not-assessed") : sig("stamp_density", density >= DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb ? "over" : "pass", `${density.toFixed(2)}/KB`, `${DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb}/KB`));
4138
- signals.push(sig("body_size", body.length >= DEFAULT_HEALTH_THRESHOLDS.softBodyChars ? "over" : "pass", `${body.length}`, `${DEFAULT_HEALTH_THRESHOLDS.softBodyChars}`));
4139
- const dupes = duplicateHeadings(body);
4140
- signals.push(dupes.length === 0 ? sig("dup_heading", "pass", "none") : sig("dup_heading", "over", dupes.map((d) => `${d.heading}(${d.count})`).join(", "), "count >= 2"));
4141
- const long = overlongLines(body);
4142
- signals.push(long.length === 0 ? sig("overlong_line", "pass", "none") : sig("overlong_line", "over", long.map((l) => `${l.lineNo}:${l.chars}`).join(", "), `${DRIFT_MAX_LINE_CHARS}`));
4143
- const missing = supportEnumerated ? missingSupportPointers(body, supportFiles) : void 0;
4144
- signals.push(!supportEnumerated ? sig("pointer_missing", "unknown", "not-enumerated", void 0, "support files not enumerated") : (missing ?? []).length === 0 ? sig("pointer_missing", "pass", "none") : sig("pointer_missing", "over", missing?.join(", ") ?? ""));
4145
- const narrow = narrowNameMatches(snapshot.name);
4146
- signals.push(narrow.length === 0 ? sig("narrow_name", "pass", "none") : sig("narrow_name", "over", narrow.join(", "), void 0, `name=${snapshot.name}`));
4147
- const description = snapshot.description;
4148
- signals.push(description === void 0 ? sig("description_chars", "unknown", "missing", "60") : sig("description_chars", description.length > 60 ? "over" : "pass", `${description.length}`, `60`));
4149
- const quality = snapshot.quality;
4150
- signals.push(quality === null || quality === void 0 ? sig("quality_low", "unknown", "not-assessed") : sig("quality_low", quality < .3 ? "over" : "pass", quality.toFixed(2), `${LOW_QUALITY_THRESHOLD}`));
4151
- return {
4152
- name: snapshot.name,
4153
- signals,
4154
- ...snapshot.protected !== void 0 && snapshot.protected !== null ? { protected: snapshot.protected } : {},
4155
- ...snapshot.catalogInvalid !== void 0 ? { catalogInvalid: snapshot.catalogInvalid } : {}
4156
- };
4157
- })
4158
- };
4159
- }
4160
- /** Convenience: fetch one signal from an assessment or library list. */
4161
- function findDriftSignal(signals, id) {
4162
- return signals.find((signal) => signal.id === id);
4163
- }
4164
- //#endregion
4165
4295
  //#region lib/types/state-store.js
4166
4296
  /**
4167
4297
  * Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
4168
4298
  * state (reports, activity store, feedback file, state-domain data).
4169
4299
  */
4170
4300
  function evolutionHome(env = process.env) {
4171
- return join(env.DSH_HOME ?? join(homedir(), ".dsh"), "evolution");
4301
+ return join(env.DSH_HOME || join(homedir(), ".dsh"), "evolution");
4172
4302
  }
4173
4303
  //#endregion
4174
- export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_HEALTH_THRESHOLDS, 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, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EvolutionGateSet, HEALTH_STAMP_RE, LOW_QUALITY_THRESHOLD, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, 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, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, advanceReview, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterYamlUnsafeValues, getRecord, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, markAgentCreated, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renderCuratorReportMarkdown, resolveOrigins, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, verifyPromptBundle, yamlPlainScalarNeedsQuotes };
4304
+ export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_HEALTH_THRESHOLDS, 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, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EvolutionGateSet, HEALTH_STAMP_RE, LOW_QUALITY_THRESHOLD, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, 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, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, advanceReview, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterYamlUnsafeValues, getRecord, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, markAgentCreated, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renderCuratorReportMarkdown, resolveOrigins, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, verifyPromptBundle, yamlPlainScalarNeedsQuotes };
@@ -50,4 +50,10 @@ export declare const DEFAULT_USER_CHAR_LIMIT = 1375;
50
50
  /** Consolidation-failure backoff cap, shared by MemoryStore and memory-files' Config default. */
51
51
  export declare const DEFAULT_CONSOLIDATION_FAILURES = 3;
52
52
  export declare const DEFAULT_SKILL_CONTENT_CHARS = 100000;
53
+ /** Hermes authoring quality bar for descriptions (the 60-char Rule). The
54
+ * platform's own index limit stays in validateFrontmatter; this bar is the
55
+ * target the authoring standard names, enforced as ADVISORY feedback.
56
+ * 0.3.16 (T-4): moved here from skill-store.ts so drift-signals (pure, no IO)
57
+ * can reference it without importing the skill-store module. */
58
+ export declare const AUTHORING_DESCRIPTION_BAR = 60;
53
59
  //# sourceMappingURL=constants.d.ts.map
@@ -35,7 +35,11 @@ export interface EvolutionPlanAppliedEvent {
35
35
  export interface EvolutionSkillMutatedEvent {
36
36
  action: string;
37
37
  name: string;
38
- filePath?: string;
38
+ /** 0.3.16 (E-50): was `filePath` with mixed semantics — skill-directory ops
39
+ * carried the DIRECTORY while file ops (write_file/remove_file) carried the
40
+ * FILE path. Split into explicit fields so a subscriber can distinguish. */
41
+ skillDir?: string;
42
+ file?: string;
39
43
  archivedPath?: string;
40
44
  }
41
45
  declare module '@deepseek-ai/cordis' {
@@ -15,6 +15,7 @@ export * from './io.ts';
15
15
  export * from './learn-prompt.ts';
16
16
  export * from './memory-store.ts';
17
17
  export * from './mutations.ts';
18
+ export * from './preset-composition.ts';
18
19
  export * from './prompts.ts';
19
20
  export * from './quality.ts';
20
21
  export * from './redact.ts';
package/lib/types/io.d.ts CHANGED
@@ -26,8 +26,10 @@ export interface EvolutionIoLike {
26
26
  * (`null` when missing) and returns the next content; returning `null`
27
27
  * deletes the file. A backend without it falls back to plain read+write and
28
28
  * the caller keeps its single-process chain as the second layer.
29
+ * 0.3.16: sync returns are allowed (0.3.16 S1.14 X-1 — mutated callers with
30
+ * no await in the task need no Promise residue).
29
31
  */
30
- transact?(this: void, path: string, task: (current: string | null) => Promise<string | null>): Promise<void>;
32
+ transact?(this: void, path: string, task: (current: string | null) => string | null | Promise<string | null>): Promise<void>;
31
33
  /**
32
34
  * Optional symlink probe (G7). `true` = the path is a symlink, `false` = a
33
35
  * real entry, `null` = guard not applicable (backend without the probe or
@@ -40,7 +42,7 @@ export interface EvolutionIoLike {
40
42
  * back to a plain read → task → write/remove sequence (no cross-process lock —
41
43
  * callers keep their single-process serialize chain as the second layer).
42
44
  */
43
- export declare function transactIo(io: EvolutionIoLike, path: string, task: (current: string | null) => Promise<string | null>): Promise<void>;
45
+ export declare function transactIo(io: EvolutionIoLike, path: string, task: (current: string | null) => string | null | Promise<string | null>): Promise<void>;
44
46
  /** Lazy adapter over an IO provider registry, shared by every evolution consumer. */
45
47
  export declare function evolutionIoAdapter(provider: () => EvolutionIoLike): EvolutionIoLike;
46
48
  export declare function nodeEvolutionIo(): EvolutionIoLike;
@@ -46,7 +46,6 @@ export declare class MemoryStore {
46
46
  */
47
47
  private oversizedFile;
48
48
  read(target: MemoryTarget): Promise<string[]>;
49
- write(target: MemoryTarget, entries: string[]): Promise<void>;
50
49
  resetFailures(): void;
51
50
  private failure;
52
51
  /**
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Build the user-root Evolution preset composition from the RUNTIME platform's
3
+ * `standard` preset rows plus the evolution delta rows (P1-1 follow-up,
4
+ * 0.3.15): the agent-preset registry mounts ONE composition file verbatim, so
5
+ * a delta-only `agent.cordis.yml` would produce an agent carrying only the
6
+ * delta rows.
7
+ *
8
+ * Same contract as `install-layered.mjs` `generateAgentPreset` (the source
9
+ * install path) — installer.spec pins byte parity between the two.
10
+ *
11
+ * Row ids are read from `- id:` lines; an id present in both fragments would
12
+ * mount twice and could shadow the platform row, so it fails loud.
13
+ * @param standardComposition - the runtime `standard` preset composition.
14
+ * @param deltaComposition - the evolution delta fragment.
15
+ * @returns the composed preset composition (standard rows first, then delta).
16
+ */
17
+ export declare function composePresetComposition(standardComposition: string, deltaComposition: string): string;
18
+ //# sourceMappingURL=preset-composition.d.ts.map
@@ -3,8 +3,8 @@
3
3
  * changes semantically: the bundle digest is the fail-closed signal for
4
4
  * review workers, so a stale id across deployments must be distinguishable.
5
5
  */
6
- export declare const PROMPT_BUNDLE_ID = "dsh-evolution@13";
7
6
  export declare const PROMPT_BUNDLE_VERSION = 13;
7
+ export declare const PROMPT_BUNDLE_ID = "dsh-evolution@13";
8
8
  export declare const MEMORY_REVIEW_PROMPT = "[Auto-review \u2014 Memory]\nReview the conversation above and consider saving to memory if appropriate.\n\nFocus on:\n1. Has the user revealed things about themselves \u2014 persona, desires, preferences, or personal details worth remembering?\n2. Has the user expressed expectations about how you should behave, their work style, or ways they want you to operate?\n\nIf something stands out, save it using the memory tool.\nIf nothing is worth saving, just say \"Nothing to save.\" and stop.";
9
9
  export declare const SKILL_REVIEW_PROMPT = "[Auto-review \u2014 Skills]\nReview the conversation above and update the skill library. Be ACTIVE \u2014 most sessions produce at least one skill update, even if small. A pass that does nothing is a missed learning opportunity, not a neutral outcome.\n\nTarget shape of the library: CLASS-LEVEL skills, each with a rich SKILL.md and a references/ directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries. This shapes HOW you update, not WHETHER you update.\n\nSignals to look for (any one of these warrants action):\n \u2022 User corrected your style, tone, format, legibility, or verbosity. Frustration signals like 'stop doing X', 'this is too verbose', 'don't format like this', 'why are you explaining', 'just give me the answer', 'you always do Y and I hate it', or an explicit 'remember this' are FIRST-CLASS skill signals, not just memory signals. Update the relevant skill(s) to embed the preference so the next session starts already knowing.\n \u2022 User corrected your workflow, approach, or sequence of steps. Encode the correction as a pitfall or explicit step in the skill that governs that class of task.\n \u2022 Non-trivial technique, fix, workaround, debugging path, or tool-usage pattern emerged that a future session would benefit from. Capture it.\n \u2022 A skill that got loaded or consulted this session turned out to be wrong, missing a step, or outdated. Patch it NOW.\n\nRead-before-write (enforced by this channel): update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session \u2014 ops on unread skills are dropped; CREATE of a brand-new umbrella is the only exception.\n\nPreference order \u2014 prefer the earliest action that fits, but do pick one when a signal above fired:\n 1. UPDATE A CURRENTLY-LOADED SKILL. Look back through the conversation for skills the user loaded or you read. If any of them covers the territory of the new learning, PATCH that one first. It is the skill that was in play, so it's the right one to extend.\n 2. UPDATE AN EXISTING UMBRELLA. If no loaded skill fits but an existing class-level skill does, patch it. Add a subsection, a pitfall, or broaden a trigger.\n 3. ADD A SUPPORT FILE under an existing umbrella. Skills can be packaged with three kinds of support files \u2014 use the right directory per kind:\n \u2022 references/<topic>.md \u2014 session-specific detail (error transcripts, reproduction recipes, provider quirks) AND condensed knowledge banks: quoted research, API docs, external authoritative excerpts, or domain notes you found while working on the problem. Write it concise and for the value of the task, not as a full mirror of upstream docs.\n \u2022 templates/<name>.<ext> \u2014 starter files meant to be copied and modified (boilerplate configs, scaffolding, a known-good example the agent can reproduce with modifications).\n \u2022 scripts/<name>.<ext> \u2014 statically re-runnable actions the skill can invoke directly (verification scripts, fixture generators, deterministic probes, anything the agent should run rather than hand-type each time).\n Add support files via skill_manage action=write_file with file_path starting 'references/', 'templates/', or 'scripts/'. The umbrella's SKILL.md should gain a one-line pointer to any new support file so future agents know it exists.\n 4. RESTRUCTURE a loaded skill whose body grew log-like \u2014 rc/sha/date-dense sections, session-detail spirals, or a fat body with no support files. Use skill_manage action=restructure with restructure: [{\"heading\": \"<the exact ## heading text>\", \"to_file\": \"references/<topic>.md\"}] \u2014 the ENTIRE ## section (from that heading to the next heading) moves into the support file and its position becomes a pointer line. The skill's name and directory never change. Only propose headings that exist verbatim in the body; never invent one, and never restructure a healthy small skill.\n 5. CREATE A NEW CLASS-LEVEL UMBRELLA SKILL when no existing skill covers the class. The name MUST be at the class level. The name MUST NOT be a specific PR number, error string, feature codename, library-alone name, or 'fix-X / debug-Y / audit-Z-today' session artifact. If the proposed name only makes sense for today's task, it's wrong \u2014 fall back to (1), (2), or (3).\n\nUser-preference embedding (important): when the user expressed a style/format/workflow preference, the update belongs in the SKILL.md body, not just in memory. Memory captures 'who the user is and what the current situation and state of your operations are'; skills capture 'how to do this class of task for this user'. When they complain about how you handled a task, the skill that governs that task needs to carry the lesson.\n\nIf you notice two existing skills that overlap, note it in your reply \u2014 the background curator handles consolidation at scale.\n\nTwo-tier deposition discipline (DSH addition, same spirit as the umbrella rule): before writing, classify the knowledge:\n \u2022 PATTERN (reusable \u2014 symptom \u2192 mechanism \u2192 fix \u2192 verification, still valuable next session) belongs in the SKILL.md body.\n \u2022 LOG (one-off \u2014 commit SHAs, npm/profile states, what this release changed, this session's process narrative) belongs in a references/ file, never the body. Body density IS reuse rate. Keep new entries tight: a pattern fits in 2-8 physical lines; prefer changing the current-state pointer over appending history.\n\nProtected skills (DO NOT edit these):\n \u2022 Bundled skills (shipped with the platform).\n \u2022 Hub-installed skills (installed from a hub).\nPinned skills are read-only to THIS background review pass \u2014 the pinned write guard refuses background changes, so only the foreground may update or archive them. Foreground and delegated-subagent writes to pinned skills remain allowed.\nIf the only skills that need updating are protected, say 'Nothing to save.' and stop.\n\nDo NOT capture (these become persistent self-imposed constraints that bite you later when the environment changes):\n \u2022 Environment-dependent failures: missing binaries, fresh-install errors, post-migration path mismatches, 'command not found', unconfigured credentials, uninstalled packages. The user can fix these \u2014 they are not durable rules.\n \u2022 Negative claims about tools or features ('browser tools do not work', 'X tool is broken', 'cannot use Y'). These harden into refusals the agent cites against itself for months after the actual problem was fixed.\n \u2022 Session-specific transient errors that resolved before the conversation ended. If retrying worked, the lesson is the retry pattern, not the original failure.\n \u2022 One-off task narratives. A user asking 'summarize today's market' or 'analyze this PR' is not a class of work that warrants a skill.\n\nIf a tool failed because of setup state, capture the FIX (install command, config step, env var to set) under an existing setup or troubleshooting skill \u2014 never 'this tool does not work' as a standalone constraint.\n\n'Nothing to save.' is a real option but should NOT be the default. If the session ran smoothly with no corrections and produced no new technique, just say 'Nothing to save.' and stop. Otherwise, act.";
10
10
  export declare const COMBINED_REVIEW_PROMPT = "[Auto-review]\nReview the conversation above and update two things:\n\n**Memory**: who the user is. Did the user reveal persona, desires, preferences, personal details, or expectations about how you should behave? Save facts about the user and durable preferences with the memory tool.\n\n**Skills**: how to do this class of task. Be ACTIVE \u2014 most sessions produce at least one skill update. A pass that does nothing is a missed learning opportunity, not a neutral outcome.\n\nTarget shape of the skill library: CLASS-LEVEL skills with a rich SKILL.md and a references/ directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries.\n\nSignals that warrant a skill update (any one is enough):\n \u2022 User corrected your style, tone, format, legibility, verbosity, or approach. Frustration is a FIRST-CLASS skill signal, not just a memory signal. 'stop doing X', 'don't format like this', 'I hate when you Y' \u2014 embed the lesson in the skill that governs that task so the next session starts fixed.\n \u2022 Non-trivial technique, fix, workaround, or debugging path emerged.\n \u2022 A skill that was loaded or consulted turned out wrong, missing, or outdated \u2014 patch it now.\n\nRead-before-write (enforced by this channel): update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session \u2014 ops on unread skills are dropped; CREATE of a brand-new umbrella is the only exception.\n\nPreference order for skills \u2014 pick the earliest that fits:\n 1. UPDATE A CURRENTLY-LOADED SKILL. Check what skills were loaded or read in the conversation. If one of them covers the learning, PATCH it first. It was in play; it's the right place.\n 2. UPDATE AN EXISTING UMBRELLA. Patch it.\n 3. ADD A SUPPORT FILE under an existing umbrella via skill_manage action=write_file. Three kinds: references/<topic>.md for session-specific detail OR condensed knowledge banks (quoted research, API docs excerpts, domain notes) written concise and task-focused; templates/<name>.<ext> for starter files meant to be copied and modified; scripts/<name>.<ext> for statically re-runnable actions (verification, fixture generators, probes). Add a one-line pointer in SKILL.md so future agents find them.\n 4. RESTRUCTURE a loaded skill whose body grew log-like (rc/sha/date-dense sections, session-detail spirals, fat body with no support files) via skill_manage action=restructure with restructure: [{\"heading\": \"<the exact ## heading text>\", \"to_file\": \"references/<topic>.md\"}] \u2014 the ENTIRE ## section moves into the support file and its position becomes a pointer line; the skill's name and directory never change. Only propose headings that exist verbatim in the body.\n 5. CREATE A NEW CLASS-LEVEL UMBRELLA when nothing exists. Name at the class level \u2014 NOT a PR number, error string, codename, library-alone name, or 'fix-X / debug-Y' session artifact. If the name only fits today's task, fall back to (1), (2), or (3).\n\nTwo-tier deposition discipline (DSH addition): classify before writing \u2014 PATTERN (symptom \u2192 mechanism \u2192 fix \u2192 verification) goes in the SKILL.md body; LOG (commit SHAs, npm/profile states, this release's change list, this session's narrative) goes in a references/ file. Body density IS reuse rate; a pattern fits in 2-8 physical lines.\n\nUser-preference embedding: when the user complains about how you handled a task, update the skill that governs that task \u2014 memory alone isn't enough. Memory says 'who the user is and what the current situation and state of your operations are'; skills say 'how to do this class of task for this user'. Both should carry user-preference lessons when relevant.\n\nIf you notice overlapping existing skills, mention it \u2014 the background curator handles consolidation.\n\nProtected skills (DO NOT edit these):\n \u2022 Bundled skills (shipped with the platform).\n \u2022 Hub-installed skills (installed from a hub).\nPinned skills are read-only to THIS background review pass \u2014 the pinned write guard refuses background changes, so only the foreground may update or archive them. Foreground and delegated-subagent writes to pinned skills remain allowed.\nIf the only skills that need updating are protected, say 'Nothing to save.' and stop.\n\nDo NOT capture as skills (these become persistent self-imposed constraints that bite you later when the environment changes):\n \u2022 Environment-dependent failures: missing binaries, fresh-install errors, post-migration path mismatches, 'command not found', unconfigured credentials, uninstalled packages. The user can fix these \u2014 they are not durable rules.\n \u2022 Negative claims about tools or features ('browser tools do not work', 'X tool is broken', 'cannot use Y'). These harden into refusals the agent cites against itself for months after the actual problem was fixed.\n \u2022 Session-specific transient errors that resolved before the conversation ended. If retrying worked, the lesson is the retry pattern, not the original failure.\n \u2022 One-off task narratives. A user asking 'summarize today's market' or 'analyze this PR' is not a class of work that warrants a skill.\n\nIf a tool failed because of setup state, capture the FIX (install command, config step, env var to set) under an existing setup or troubleshooting skill \u2014 never 'this tool does not work' as a standalone constraint.\n\nAct on whichever of the two dimensions has real signal. If genuinely nothing stands out on either, say 'Nothing to save.' and stop \u2014 but don't reach for that conclusion as a default.";
@@ -119,15 +119,18 @@ export declare function parseFrontmatter(content: string): {
119
119
  } | null;
120
120
  /** YAML plain-scalar hazards that make an UNQUOTED frontmatter value
121
121
  * unloadable to the platform catalog (strict YAML parser): `: ` (mapping
122
- * separator), ` #` (comment start), or a leading YAML indicator. The
123
- * evolution `parseFrontmatter` is deliberately lenient, so violations
124
- * silently split family-visibility from platform-visibility (0.3.11
125
- * inkos-harness case: the description carried "…: " and the catalog dropped
126
- * the whole skill). Already-quoted values and well-formed flow collections
127
- * (`[a, b]` / `{a: b}`) are considered safe. This rule is only the FAST
128
- * PATH — the write path re-verifies every rewrite with the real YAML parser
129
- * (see normalizeFrontmatter), so an incomplete approximation can never
130
- * corrupt a multiline flow value (P3-4). */
122
+ * separator), ` #` (comment start), a trailing `:` (a mapping marker),
123
+ * or a leading YAML indicator. The evolution `parseFrontmatter` is
124
+ * deliberately lenient, so violations silently split family-visibility from
125
+ * platform-visibility (0.3.11 inkos-harness case: the description carried
126
+ * "…: " and the catalog dropped the whole skill). Already-quoted values and
127
+ * well-formed flow collections (`[a, b]` / `{a: b}`) are considered safe.
128
+ * 0.3.16 (E-47): null/bool/number-shaped plain scalars are flagged too — they
129
+ * parse as booleans/numbers on the platform while the family keeps the string
130
+ * (a `description: true` split-brain).
131
+ * This rule is only the FAST PATH — the write path re-verifies every rewrite
132
+ * with the real YAML parser (see normalizeFrontmatter), so an incomplete
133
+ * approximation can never corrupt a multiline flow value (P3-4). */
131
134
  export declare function yamlPlainScalarNeedsQuotes(value: string): boolean;
132
135
  /** Raw-line scan of the frontmatter block: entries whose UNQUOTED value is
133
136
  * YAML-unsafe for the strict platform catalog. Operates on the ORIGINAL line
@@ -172,10 +175,9 @@ export declare function normalizeFrontmatter(content: string): FrontmatterNormal
172
175
  */
173
176
  export declare function relatedSkillNames(content: string, exclude?: string): string[];
174
177
  export declare function validateFrontmatter(content: string, expectedName?: string, limits?: SkillLimits): string | null;
175
- /** Hermes authoring quality bar for descriptions (the 60-char Rule). The
176
- * platform's own index limit stays in `validateFrontmatter`; this bar is the
177
- * target the authoring standard names, enforced as ADVISORY feedback. */
178
- export declare const AUTHORING_DESCRIPTION_BAR = 60;
178
+ /** Hermes authoring quality bar for descriptions — see constants.ts
179
+ * (0.3.16 T-4 moved the single source there; the public re-export sits behind
180
+ * the package root, which re-exports constants anyway). */
179
181
  export interface AuthoringFeedback {
180
182
  /** Frontmatter description length in characters (0 when absent). */
181
183
  descriptionChars: number;
@@ -341,5 +343,12 @@ export declare class SkillLibrary {
341
343
  restoreLatestSnapshot(extras?: SnapshotExtra[]): Promise<SkillActionResult & {
342
344
  extras?: SnapshotExtra[];
343
345
  }>;
346
+ /**
347
+ * Whole-tree replacement from one snapshot path (rc.50 P2-14): every
348
+ * NON-system entry in the active root is cleared first, then the manifest
349
+ * drives the repopulation (skills, sidecars, `.archive`). Extracted from
350
+ * restoreLatestSnapshot so a failed restore can roll itself back (E-13).
351
+ */
352
+ private restoreSnapshotIntoRoot;
344
353
  }
345
354
  //# sourceMappingURL=skill-store.d.ts.map
@@ -26,6 +26,9 @@ export interface ScanOptions {
26
26
  /**
27
27
  * Scan text at `scope`. Patterns are cumulative: `strict` includes all scopes.
28
28
  * `options.excludeLabels` removes matching patterns without changing `scope`.
29
+ * `maxScanChars` is the WINDOW SIZE, not a total cap (E-12, 0.3.16): the whole
30
+ * text is always scanned in overlapping windows, so content beyond 65,536
31
+ * characters (skill files may run to 100,000) is no longer a blind zone.
29
32
  */
30
33
  export declare function scanThreats(text: string, scope?: ThreatScope, maxScanChars?: number, options?: ScanOptions): ThreatFinding[];
31
34
  /** Blocking policy: any hit blocks. `severity` is deliberately not a gate. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-core",
3
3
  "description": "Shared stores, prompts, signals and lifecycle logic for the dsh-evolution plugin family (community build)",
4
- "version": "0.3.14",
4
+ "version": "0.3.16",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },