@lmzhen/dsh-evolution-core 0.3.15 → 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 +336 -238
- package/lib/types/constants.d.ts +6 -0
- package/lib/types/events.d.ts +5 -1
- package/lib/types/io.d.ts +4 -2
- package/lib/types/memory-store.d.ts +0 -1
- package/lib/types/prompts.d.ts +1 -1
- package/lib/types/skill-store.d.ts +22 -13
- package/lib/types/threats.d.ts +3 -0
- package/package.json +1 -1
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.
|
|
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 !==
|
|
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")
|
|
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
|
-
|
|
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(
|
|
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
|
|
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),
|
|
2125
|
+
await transactIo(io, mutationsFile(root), (current) => {
|
|
2105
2126
|
if (current !== null) try {
|
|
2106
2127
|
JSON.parse(current);
|
|
2107
2128
|
} catch {
|
|
@@ -2110,10 +2131,10 @@ 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
|
|
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
|
|
@@ -2248,7 +2269,7 @@ function computeDedupGroups(input) {
|
|
|
2248
2269
|
const [ra, rb] = [find(a), find(b)];
|
|
2249
2270
|
if (ra !== rb) parent.set(rb, ra);
|
|
2250
2271
|
};
|
|
2251
|
-
for (const [
|
|
2272
|
+
for (const [, bucketNames] of hashes) {
|
|
2252
2273
|
const first = bucketNames[0];
|
|
2253
2274
|
if (first === void 0 || bucketNames.length === 1) continue;
|
|
2254
2275
|
for (let index = 1; index < bucketNames.length; index += 1) {
|
|
@@ -2326,9 +2347,9 @@ const SECRET_PATTERNS = [
|
|
|
2326
2347
|
["gitlab token", /glpat-[A-Za-z0-9_-]{16,}/g],
|
|
2327
2348
|
["slack token", /xox[baprs]-[A-Za-z0-9-]{10,}/g],
|
|
2328
2349
|
["jwt", /eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g],
|
|
2329
|
-
["bearer credential", /Bearer [A-Za-z0-9._~+/=\-]{16,}/g]
|
|
2330
|
-
["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]
|
|
2331
2351
|
];
|
|
2352
|
+
const INLINE_ASSIGNMENT_PATTERN = /(\b(?:token|api[_-]?key|secret|password|passwd)\b[\s]*[:=][\s]*["']?)([A-Z0-9._~+/=\-]{12,})/gi;
|
|
2332
2353
|
/**
|
|
2333
2354
|
* Mask credential-shaped text before it crosses a session boundary.
|
|
2334
2355
|
* @param text - the text about to be sent to a model outside this session.
|
|
@@ -2336,7 +2357,8 @@ const SECRET_PATTERNS = [
|
|
|
2336
2357
|
*/
|
|
2337
2358
|
function redactSecrets(text) {
|
|
2338
2359
|
let out = text;
|
|
2339
|
-
for (const [, pattern] of SECRET_PATTERNS) out = out.replace(pattern,
|
|
2360
|
+
for (const [, pattern] of SECRET_PATTERNS) out = out.replace(pattern, "<redacted>");
|
|
2361
|
+
out = out.replace(INLINE_ASSIGNMENT_PATTERN, (_match, p1) => `${p1 ?? ""}<redacted>`);
|
|
2340
2362
|
return out;
|
|
2341
2363
|
}
|
|
2342
2364
|
//#endregion
|
|
@@ -2414,6 +2436,7 @@ const FIX_PATTERNS = [/worked after|fixed by|the fix was|root cause/i, /retry(?:
|
|
|
2414
2436
|
/** Fold one session event into the current turn observation. */
|
|
2415
2437
|
function observeEvent(signal, event) {
|
|
2416
2438
|
if (event.type === "user/message") {
|
|
2439
|
+
if (!Array.isArray(event.data.content)) return;
|
|
2417
2440
|
const text = event.data.content.map((block) => block.type === "text" ? block.text : "").join(" ");
|
|
2418
2441
|
signal.userChars += text.length;
|
|
2419
2442
|
if (CORRECTION_PATTERNS.some((pattern) => pattern.test(text))) signal.memorySignal = true;
|
|
@@ -2473,6 +2496,165 @@ function foldTurn(session, fromSeq) {
|
|
|
2473
2496
|
return signal;
|
|
2474
2497
|
}
|
|
2475
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
|
|
2476
2658
|
//#region lib/types/skill-store.js
|
|
2477
2659
|
/**
|
|
2478
2660
|
* Skill library management for the self-evolution plugin.
|
|
@@ -2482,6 +2664,10 @@ function foldTurn(session, fromSeq) {
|
|
|
2482
2664
|
* it created unless a `.hermes-managed` marker opts a skill in. Archival is a
|
|
2483
2665
|
* move to `.archive/` — never a hard delete.
|
|
2484
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/";
|
|
2485
2671
|
const DEFAULT_SKILL_LIMITS = {
|
|
2486
2672
|
maxNameLength: 64,
|
|
2487
2673
|
maxDescriptionLength: MAX_DESCRIPTION_LENGTH,
|
|
@@ -2495,7 +2681,7 @@ const RESTRUCTURE_TARGET_RE = /^references\/[a-z0-9][a-z0-9._-]*\.md$/;
|
|
|
2495
2681
|
/** Extra file name carried inside a snapshot's `extras/` directory. */
|
|
2496
2682
|
const SNAPSHOT_EXTRA_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
2497
2683
|
function skillsRoot(env = process.env) {
|
|
2498
|
-
return join(env.DSH_HOME
|
|
2684
|
+
return join(env.DSH_HOME || join(homedir(), ".dsh"), "skills");
|
|
2499
2685
|
}
|
|
2500
2686
|
/**
|
|
2501
2687
|
* Map a requesting session onto the two origin surfaces (rc.44 plan M2-2.3):
|
|
@@ -2583,21 +2769,26 @@ function parseFrontmatter(content) {
|
|
|
2583
2769
|
}
|
|
2584
2770
|
/** YAML plain-scalar hazards that make an UNQUOTED frontmatter value
|
|
2585
2771
|
* unloadable to the platform catalog (strict YAML parser): `: ` (mapping
|
|
2586
|
-
* separator), ` #` (comment start),
|
|
2587
|
-
* evolution `parseFrontmatter` is
|
|
2588
|
-
* silently split family-visibility from
|
|
2589
|
-
* inkos-harness case: the description carried
|
|
2590
|
-
* the whole skill). Already-quoted values and
|
|
2591
|
-
* (`[a, b]` / `{a: b}`) are considered safe.
|
|
2592
|
-
*
|
|
2593
|
-
*
|
|
2594
|
-
*
|
|
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). */
|
|
2595
2784
|
function yamlPlainScalarNeedsQuotes(value) {
|
|
2596
2785
|
if (value.length === 0) return false;
|
|
2597
2786
|
if (value.length >= 2 && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'"))) return false;
|
|
2598
2787
|
if (/^\[.*\]$/.test(value) || /^\{.*\}$/.test(value)) return false;
|
|
2599
2788
|
if (value.includes(": ")) return true;
|
|
2600
2789
|
if (value.includes(" #")) return true;
|
|
2790
|
+
if (value.endsWith(":")) return true;
|
|
2791
|
+
if (/^(?:null|true|false|~|[-+]?\d+(?:\.\d+)?)$/i.test(value)) return true;
|
|
2601
2792
|
if (/^[-?:,[\]{}#&*!|>'\"%@`\s]/.test(value)) return true;
|
|
2602
2793
|
return false;
|
|
2603
2794
|
}
|
|
@@ -2723,10 +2914,6 @@ function validateFrontmatter(content, expectedName, limits = DEFAULT_SKILL_LIMIT
|
|
|
2723
2914
|
if (content.length > limits.maxSkillContentChars) return `SKILL.md content exceeds ${limits.maxSkillContentChars} characters. Consider splitting into a smaller SKILL.md with supporting files.`;
|
|
2724
2915
|
return null;
|
|
2725
2916
|
}
|
|
2726
|
-
/** Hermes authoring quality bar for descriptions (the 60-char Rule). The
|
|
2727
|
-
* platform's own index limit stays in `validateFrontmatter`; this bar is the
|
|
2728
|
-
* target the authoring standard names, enforced as ADVISORY feedback. */
|
|
2729
|
-
const AUTHORING_DESCRIPTION_BAR = 60;
|
|
2730
2917
|
/**
|
|
2731
2918
|
* Advisory authoring feedback (P0): evaluate frontmatter against the
|
|
2732
2919
|
* authoring bar WITHOUT changing platform validation semantics. The bar is
|
|
@@ -2835,7 +3022,7 @@ function fuzzyReplace(content, oldString, newString, replaceAll) {
|
|
|
2835
3022
|
}
|
|
2836
3023
|
function fuzzyPatch(content, oldString, newString, replaceAll = false) {
|
|
2837
3024
|
if (oldString === "") return null;
|
|
2838
|
-
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);
|
|
2839
3026
|
const boundary = trimPatternBoundaries(oldString);
|
|
2840
3027
|
if (boundary === "") return null;
|
|
2841
3028
|
if (boundary !== oldString) {
|
|
@@ -2900,7 +3087,7 @@ function planRestructureSections(body, moves) {
|
|
|
2900
3087
|
for (let i = 0; i < lines.length; i += 1) {
|
|
2901
3088
|
const span = byStart.get(i);
|
|
2902
3089
|
if (span) {
|
|
2903
|
-
rebuilt.push(
|
|
3090
|
+
rebuilt.push(`${POINTER_LINE_PREFIX}${span.rel.split("/").at(-1)}`);
|
|
2904
3091
|
i = span.end - 1;
|
|
2905
3092
|
} else rebuilt.push(lines[i] ?? "");
|
|
2906
3093
|
}
|
|
@@ -2957,7 +3144,12 @@ var SkillLibrary = class {
|
|
|
2957
3144
|
async read(rawName) {
|
|
2958
3145
|
const name = rawName.trim();
|
|
2959
3146
|
if (this.badName(name) !== null) return null;
|
|
2960
|
-
|
|
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
|
+
}
|
|
2961
3153
|
}
|
|
2962
3154
|
/**
|
|
2963
3155
|
|
|
@@ -3180,7 +3372,7 @@ var SkillLibrary = class {
|
|
|
3180
3372
|
this.notifyMutation({
|
|
3181
3373
|
action: "create",
|
|
3182
3374
|
name: normalized,
|
|
3183
|
-
|
|
3375
|
+
skillDir: dir
|
|
3184
3376
|
});
|
|
3185
3377
|
return {
|
|
3186
3378
|
ok: true,
|
|
@@ -3235,7 +3427,7 @@ var SkillLibrary = class {
|
|
|
3235
3427
|
this.notifyMutation({
|
|
3236
3428
|
action: "update",
|
|
3237
3429
|
name,
|
|
3238
|
-
|
|
3430
|
+
skillDir: dir
|
|
3239
3431
|
});
|
|
3240
3432
|
return {
|
|
3241
3433
|
ok: true,
|
|
@@ -3324,7 +3516,7 @@ var SkillLibrary = class {
|
|
|
3324
3516
|
this.notifyMutation({
|
|
3325
3517
|
action: "patch",
|
|
3326
3518
|
name,
|
|
3327
|
-
|
|
3519
|
+
skillDir: dir
|
|
3328
3520
|
});
|
|
3329
3521
|
return {
|
|
3330
3522
|
ok: true,
|
|
@@ -3374,7 +3566,23 @@ var SkillLibrary = class {
|
|
|
3374
3566
|
await this.io.rename(dir, dest);
|
|
3375
3567
|
} catch {
|
|
3376
3568
|
await this.io.copy(dir, dest);
|
|
3377
|
-
|
|
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
|
+
}
|
|
3378
3586
|
}
|
|
3379
3587
|
const reason = options.reason ?? (options.absorbedInto ? `Consolidated into ${options.absorbedInto}` : "Archived by self-evolution curator");
|
|
3380
3588
|
await this.io.writeText(join(dest, ".archive-reason"), `${(/* @__PURE__ */ new Date()).toISOString()}: ${reason}\n`);
|
|
@@ -3499,7 +3707,7 @@ var SkillLibrary = class {
|
|
|
3499
3707
|
content: `<!-- demoted from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}\n`
|
|
3500
3708
|
});
|
|
3501
3709
|
}
|
|
3502
|
-
const pointerLines = normalizedSources.map((source) => `\n
|
|
3710
|
+
const pointerLines = normalizedSources.map((source) => `\n${POINTER_LINE_PREFIX}${source}.md`).join("");
|
|
3503
3711
|
const extended = targetMd.trimEnd() + pointerLines + "\n";
|
|
3504
3712
|
const validation = validateFrontmatter(extended, targetName, this.limits);
|
|
3505
3713
|
if (validation) return {
|
|
@@ -3529,10 +3737,20 @@ var SkillLibrary = class {
|
|
|
3529
3737
|
});
|
|
3530
3738
|
if (!result.ok) throw new Error(result.message);
|
|
3531
3739
|
} catch (error) {
|
|
3532
|
-
|
|
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
|
+
};
|
|
3533
3751
|
return {
|
|
3534
3752
|
ok: false,
|
|
3535
|
-
message: `Consolidation failed and was rolled back: ${
|
|
3753
|
+
message: `Consolidation failed and was rolled back: ${reason}`
|
|
3536
3754
|
};
|
|
3537
3755
|
}
|
|
3538
3756
|
return {
|
|
@@ -3584,20 +3802,20 @@ var SkillLibrary = class {
|
|
|
3584
3802
|
ok: false,
|
|
3585
3803
|
message: `Skill "${name}" not found.`
|
|
3586
3804
|
};
|
|
3587
|
-
const
|
|
3588
|
-
|
|
3589
|
-
if (frontmatterEnd < 0) return {
|
|
3805
|
+
const block = frontmatterBlock(md);
|
|
3806
|
+
if (!block) return {
|
|
3590
3807
|
ok: false,
|
|
3591
3808
|
message: "SKILL.md has no valid frontmatter; refusing to restructure."
|
|
3592
3809
|
};
|
|
3593
|
-
const header =
|
|
3594
|
-
const plan = planRestructureSections(
|
|
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);
|
|
3595
3812
|
if ("error" in plan) return {
|
|
3596
3813
|
ok: false,
|
|
3597
3814
|
message: `Restructure rejected: ${plan.error}`
|
|
3598
3815
|
};
|
|
3599
|
-
const newMd = header
|
|
3600
|
-
const
|
|
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);
|
|
3601
3819
|
if (newMdCheck) return {
|
|
3602
3820
|
ok: false,
|
|
3603
3821
|
message: `Restructure rejected: ${newMdCheck}`
|
|
@@ -3629,7 +3847,7 @@ var SkillLibrary = class {
|
|
|
3629
3847
|
}
|
|
3630
3848
|
writes.push({
|
|
3631
3849
|
target: join(dir, "SKILL.md"),
|
|
3632
|
-
content:
|
|
3850
|
+
content: finalMd
|
|
3633
3851
|
});
|
|
3634
3852
|
const result = await this.applyTreeChange({
|
|
3635
3853
|
name,
|
|
@@ -3721,11 +3939,11 @@ var SkillLibrary = class {
|
|
|
3721
3939
|
message: `Tree change failed and was rolled back: ${error instanceof Error ? error.message : String(error)}`
|
|
3722
3940
|
};
|
|
3723
3941
|
}
|
|
3724
|
-
await this.audit(name, plan.auditAction, md, landing.find((entry) => entry.target.
|
|
3942
|
+
await this.audit(name, plan.auditAction, md, landing.find((entry) => entry.target.split(/[\\/]/).pop() === "SKILL.md")?.content ?? md, plan.auditSummary);
|
|
3725
3943
|
this.notifyMutation({
|
|
3726
3944
|
action: plan.eventAction,
|
|
3727
3945
|
name,
|
|
3728
|
-
|
|
3946
|
+
skillDir: dir
|
|
3729
3947
|
});
|
|
3730
3948
|
return {
|
|
3731
3949
|
ok: true,
|
|
@@ -3758,7 +3976,12 @@ var SkillLibrary = class {
|
|
|
3758
3976
|
message: "No skill archive available."
|
|
3759
3977
|
};
|
|
3760
3978
|
}
|
|
3761
|
-
const
|
|
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
|
+
}
|
|
3762
3985
|
if (!chosen) return {
|
|
3763
3986
|
ok: false,
|
|
3764
3987
|
message: `Skill "${name}" is not in .archive.`
|
|
@@ -3774,14 +3997,21 @@ var SkillLibrary = class {
|
|
|
3774
3997
|
try {
|
|
3775
3998
|
await this.io.rename(source, dest);
|
|
3776
3999
|
} catch {
|
|
3777
|
-
|
|
3778
|
-
|
|
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
|
+
}
|
|
3779
4009
|
}
|
|
3780
4010
|
if (await this.io.exists(join(dest, ".archive-reason"))) await this.io.remove(join(dest, ".archive-reason"));
|
|
3781
4011
|
this.notifyMutation({
|
|
3782
4012
|
action: "restore",
|
|
3783
4013
|
name,
|
|
3784
|
-
|
|
4014
|
+
skillDir: dest
|
|
3785
4015
|
});
|
|
3786
4016
|
return {
|
|
3787
4017
|
ok: true,
|
|
@@ -3827,7 +4057,8 @@ var SkillLibrary = class {
|
|
|
3827
4057
|
this.notifyMutation({
|
|
3828
4058
|
action: "write_file",
|
|
3829
4059
|
name,
|
|
3830
|
-
|
|
4060
|
+
skillDir: dir,
|
|
4061
|
+
file: target
|
|
3831
4062
|
});
|
|
3832
4063
|
return {
|
|
3833
4064
|
ok: true,
|
|
@@ -3868,7 +4099,8 @@ var SkillLibrary = class {
|
|
|
3868
4099
|
this.notifyMutation({
|
|
3869
4100
|
action: "remove_file",
|
|
3870
4101
|
name,
|
|
3871
|
-
|
|
4102
|
+
skillDir: dir,
|
|
4103
|
+
file: target
|
|
3872
4104
|
});
|
|
3873
4105
|
return {
|
|
3874
4106
|
ok: true,
|
|
@@ -3996,7 +4228,43 @@ var SkillLibrary = class {
|
|
|
3996
4228
|
ok: false,
|
|
3997
4229
|
message: "No skill snapshot available."
|
|
3998
4230
|
};
|
|
3999
|
-
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) {
|
|
4000
4268
|
let rootEntries;
|
|
4001
4269
|
try {
|
|
4002
4270
|
rootEntries = await this.io.list(this.root);
|
|
@@ -4007,200 +4275,30 @@ var SkillLibrary = class {
|
|
|
4007
4275
|
if (entry.startsWith(".")) continue;
|
|
4008
4276
|
await this.io.remove(join(this.root, entry));
|
|
4009
4277
|
}
|
|
4010
|
-
const manifest = await this.readSnapshotManifest(
|
|
4011
|
-
if (manifest === null) for (const entry of await this.io.list(
|
|
4278
|
+
const manifest = await this.readSnapshotManifest(snapshotPath);
|
|
4279
|
+
if (manifest === null) for (const entry of await this.io.list(snapshotPath)) {
|
|
4012
4280
|
if (entry === "manifest.json" || entry === "extras") continue;
|
|
4013
|
-
await this.io.copy(join(
|
|
4281
|
+
await this.io.copy(join(snapshotPath, entry), join(this.root, entry));
|
|
4014
4282
|
}
|
|
4015
4283
|
else {
|
|
4016
|
-
for (const name of manifest.skills) await this.io.copy(join(
|
|
4017
|
-
for (const sidecar of manifest.sidecars) await this.io.copy(join(
|
|
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));
|
|
4018
4286
|
const archiveRoot = join(this.root, ".archive");
|
|
4019
4287
|
if (manifest.hasArchive === true) {
|
|
4020
4288
|
await this.io.remove(archiveRoot);
|
|
4021
|
-
await this.io.copy(join(
|
|
4289
|
+
await this.io.copy(join(snapshotPath, ".archive"), archiveRoot);
|
|
4022
4290
|
} else if (manifest.hasArchive === false) await this.io.remove(archiveRoot);
|
|
4023
4291
|
}
|
|
4024
|
-
const snapshotExtras = await this.readSnapshotExtras(latest.path);
|
|
4025
|
-
this.notifyMutation({
|
|
4026
|
-
action: "restore",
|
|
4027
|
-
name: "snapshot"
|
|
4028
|
-
});
|
|
4029
|
-
return {
|
|
4030
|
-
ok: true,
|
|
4031
|
-
message: `Restored skill tree from ${latest.path}`,
|
|
4032
|
-
path: latest.path,
|
|
4033
|
-
...snapshotExtras.length === 0 ? {} : { extras: snapshotExtras }
|
|
4034
|
-
};
|
|
4035
4292
|
}
|
|
4036
4293
|
};
|
|
4037
4294
|
//#endregion
|
|
4038
|
-
//#region lib/types/drift-signals.js
|
|
4039
|
-
/**
|
|
4040
|
-
* Library-level drift signals for the maintenance subagent (design 011).
|
|
4041
|
-
*
|
|
4042
|
-
* Deterministic fact checks over a skill-library snapshot: domain drift
|
|
4043
|
-
* (narrow names, near-duplicate groups, prefix clusters) and layer drift
|
|
4044
|
-
* (log-like bodies, duplicate headings, overlong lines, missing support-file
|
|
4045
|
-
* pointers, description over the authoring bar). Pure functions only — no IO,
|
|
4046
|
-
* no LLM, no services. Thresholds are imported from their owning modules
|
|
4047
|
-
* (skill-health / quality / skill-store), never duplicated.
|
|
4048
|
-
*
|
|
4049
|
-
* Distinct from `signals.ts` — the session-level review signal gate.
|
|
4050
|
-
*/
|
|
4051
|
-
/** Physical line length at/above which a body line is reported overlong (011 §4). */
|
|
4052
|
-
const DRIFT_MAX_LINE_CHARS = 1500;
|
|
4053
|
-
/** Signal-set version: bump whenever ids/thresholds change (011 §7 version coupling). */
|
|
4054
|
-
const DRIFT_SIGNALS_VERSION = "1";
|
|
4055
|
-
/** Render-time nouns for the MAINTAIN_PROMPT placeholders (single vocabulary with the facts block). */
|
|
4056
|
-
const DRIFT_SIGNAL_NOUNS = {
|
|
4057
|
-
dedup_group: "近重复组",
|
|
4058
|
-
prefix_cluster: "前缀聚类",
|
|
4059
|
-
stamp_density: "stamp 密度",
|
|
4060
|
-
body_size: "正文体量",
|
|
4061
|
-
dup_heading: "重复标题",
|
|
4062
|
-
overlong_line: "超长行",
|
|
4063
|
-
pointer_missing: "缺失指针",
|
|
4064
|
-
description_chars: "描述长度",
|
|
4065
|
-
narrow_name: "窄名",
|
|
4066
|
-
usage_observed: "使用观察",
|
|
4067
|
-
quality_low: "质量分"
|
|
4068
|
-
};
|
|
4069
|
-
const NARROW_NAME_PATTERNS = [
|
|
4070
|
-
{
|
|
4071
|
-
label: "error-string",
|
|
4072
|
-
re: /^(?:err|error|exception|traceback|warn|fail)(?:[-_][a-z0-9]+)+$/i
|
|
4073
|
-
},
|
|
4074
|
-
{
|
|
4075
|
-
label: "pr-number",
|
|
4076
|
-
re: /^(?:pr|issue)[-_]?\d{2,}$/i
|
|
4077
|
-
},
|
|
4078
|
-
{
|
|
4079
|
-
label: "dated",
|
|
4080
|
-
re: /\d{4}-\d{2}-\d{2}/
|
|
4081
|
-
},
|
|
4082
|
-
{
|
|
4083
|
-
label: "session-verb",
|
|
4084
|
-
re: /^(?:fix|debug|audit|salvage|diagnose|investigate)[-_][a-z0-9-]+$/i
|
|
4085
|
-
}
|
|
4086
|
-
];
|
|
4087
|
-
/** Detect support files the body never references (by basename or relative path). */
|
|
4088
|
-
function missingSupportPointers(body, supportFiles) {
|
|
4089
|
-
return supportFiles.filter((path) => {
|
|
4090
|
-
const base = path.split("/").pop() ?? path;
|
|
4091
|
-
return base.length > 0 && !body.includes(base) && !body.includes(path);
|
|
4092
|
-
});
|
|
4093
|
-
}
|
|
4094
|
-
/** Duplicate `## heading` occurrences: singleton results default to head of the file. */
|
|
4095
|
-
function duplicateHeadings(body) {
|
|
4096
|
-
const counts = /* @__PURE__ */ new Map();
|
|
4097
|
-
for (const line of body.split("\n")) {
|
|
4098
|
-
const m = /^##\s+(.+)$/.exec(line);
|
|
4099
|
-
if (m?.[1]) {
|
|
4100
|
-
const heading = m[1].trim();
|
|
4101
|
-
if (heading) counts.set(heading, (counts.get(heading) ?? 0) + 1);
|
|
4102
|
-
}
|
|
4103
|
-
}
|
|
4104
|
-
return [...counts.entries()].filter(([, count]) => count > 1).map(([heading, count]) => ({
|
|
4105
|
-
heading,
|
|
4106
|
-
count
|
|
4107
|
-
}));
|
|
4108
|
-
}
|
|
4109
|
-
/** Physical lines over `max` characters: `{ lineNo, chars }`, 1-based line numbers. */
|
|
4110
|
-
function overlongLines(body, max = DRIFT_MAX_LINE_CHARS) {
|
|
4111
|
-
const out = [];
|
|
4112
|
-
const lines = body.split("\n");
|
|
4113
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
4114
|
-
const length = (lines[index] ?? "").length;
|
|
4115
|
-
if (length > max) out.push({
|
|
4116
|
-
lineNo: index + 1,
|
|
4117
|
-
chars: length
|
|
4118
|
-
});
|
|
4119
|
-
}
|
|
4120
|
-
return out;
|
|
4121
|
-
}
|
|
4122
|
-
/** Narrow-name shapes detected in a skill name (empty = none). */
|
|
4123
|
-
function narrowNameMatches(name) {
|
|
4124
|
-
return NARROW_NAME_PATTERNS.filter(({ re }) => re.test(name)).map(({ label }) => label);
|
|
4125
|
-
}
|
|
4126
|
-
function supportGroupCount(supportFiles) {
|
|
4127
|
-
const groups = /* @__PURE__ */ new Set();
|
|
4128
|
-
for (const path of supportFiles ?? []) {
|
|
4129
|
-
const head = path.split("/")[0];
|
|
4130
|
-
if (head) groups.add(head);
|
|
4131
|
-
}
|
|
4132
|
-
return groups.size;
|
|
4133
|
-
}
|
|
4134
|
-
function sig(id, verdict, value, threshold, detail) {
|
|
4135
|
-
return {
|
|
4136
|
-
id,
|
|
4137
|
-
verdict,
|
|
4138
|
-
value,
|
|
4139
|
-
threshold,
|
|
4140
|
-
detail
|
|
4141
|
-
};
|
|
4142
|
-
}
|
|
4143
|
-
/**
|
|
4144
|
-
* Compute all drift signals for a snapshot. Missing inputs (quality score,
|
|
4145
|
-
* usage window) yield `unknown` — never a fabricated verdict.
|
|
4146
|
-
*/
|
|
4147
|
-
function computeDriftSignals(snapshots) {
|
|
4148
|
-
const library = [];
|
|
4149
|
-
const names = snapshots.map((s) => s.name);
|
|
4150
|
-
const dedup = computeDedupGroups({ contents: new Map(snapshots.map((s) => [s.name, s.body])) });
|
|
4151
|
-
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(";")}`));
|
|
4152
|
-
const clusters = computePrefixClusters(names);
|
|
4153
|
-
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("|")}`));
|
|
4154
|
-
const allProvided = snapshots.length > 0 && snapshots.every((s) => s.usageObserved !== null && s.usageObserved !== void 0);
|
|
4155
|
-
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"));
|
|
4156
|
-
return {
|
|
4157
|
-
library,
|
|
4158
|
-
skills: snapshots.map((snapshot) => {
|
|
4159
|
-
const signals = [];
|
|
4160
|
-
const body = snapshot.body;
|
|
4161
|
-
const supportFiles = snapshot.supportFiles ?? [];
|
|
4162
|
-
const supportEnumerated = snapshot.supportFiles !== void 0;
|
|
4163
|
-
const density = assessStructureHealth({
|
|
4164
|
-
skillName: snapshot.name,
|
|
4165
|
-
bodyChars: body.length,
|
|
4166
|
-
bodyText: body,
|
|
4167
|
-
supportGroups: supportGroupCount(supportFiles)
|
|
4168
|
-
}, DEFAULT_HEALTH_THRESHOLDS).dims.stampDensityPerKb;
|
|
4169
|
-
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`));
|
|
4170
|
-
signals.push(sig("body_size", body.length >= DEFAULT_HEALTH_THRESHOLDS.softBodyChars ? "over" : "pass", `${body.length}`, `${DEFAULT_HEALTH_THRESHOLDS.softBodyChars}`));
|
|
4171
|
-
const dupes = duplicateHeadings(body);
|
|
4172
|
-
signals.push(dupes.length === 0 ? sig("dup_heading", "pass", "none") : sig("dup_heading", "over", dupes.map((d) => `${d.heading}(${d.count})`).join(", "), "count >= 2"));
|
|
4173
|
-
const long = overlongLines(body);
|
|
4174
|
-
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}`));
|
|
4175
|
-
const missing = supportEnumerated ? missingSupportPointers(body, supportFiles) : void 0;
|
|
4176
|
-
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(", ") ?? ""));
|
|
4177
|
-
const narrow = narrowNameMatches(snapshot.name);
|
|
4178
|
-
signals.push(narrow.length === 0 ? sig("narrow_name", "pass", "none") : sig("narrow_name", "over", narrow.join(", "), void 0, `name=${snapshot.name}`));
|
|
4179
|
-
const description = snapshot.description;
|
|
4180
|
-
signals.push(description === void 0 ? sig("description_chars", "unknown", "missing", "60") : sig("description_chars", description.length > 60 ? "over" : "pass", `${description.length}`, `60`));
|
|
4181
|
-
const quality = snapshot.quality;
|
|
4182
|
-
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}`));
|
|
4183
|
-
return {
|
|
4184
|
-
name: snapshot.name,
|
|
4185
|
-
signals,
|
|
4186
|
-
...snapshot.protected !== void 0 && snapshot.protected !== null ? { protected: snapshot.protected } : {},
|
|
4187
|
-
...snapshot.catalogInvalid !== void 0 ? { catalogInvalid: snapshot.catalogInvalid } : {}
|
|
4188
|
-
};
|
|
4189
|
-
})
|
|
4190
|
-
};
|
|
4191
|
-
}
|
|
4192
|
-
/** Convenience: fetch one signal from an assessment or library list. */
|
|
4193
|
-
function findDriftSignal(signals, id) {
|
|
4194
|
-
return signals.find((signal) => signal.id === id);
|
|
4195
|
-
}
|
|
4196
|
-
//#endregion
|
|
4197
4295
|
//#region lib/types/state-store.js
|
|
4198
4296
|
/**
|
|
4199
4297
|
* Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
|
|
4200
4298
|
* state (reports, activity store, feedback file, state-domain data).
|
|
4201
4299
|
*/
|
|
4202
4300
|
function evolutionHome(env = process.env) {
|
|
4203
|
-
return join(env.DSH_HOME
|
|
4301
|
+
return join(env.DSH_HOME || join(homedir(), ".dsh"), "evolution");
|
|
4204
4302
|
}
|
|
4205
4303
|
//#endregion
|
|
4206
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 };
|
package/lib/types/constants.d.ts
CHANGED
|
@@ -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
|
package/lib/types/events.d.ts
CHANGED
|
@@ -35,7 +35,11 @@ export interface EvolutionPlanAppliedEvent {
|
|
|
35
35
|
export interface EvolutionSkillMutatedEvent {
|
|
36
36
|
action: string;
|
|
37
37
|
name: string;
|
|
38
|
-
filePath
|
|
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' {
|
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;
|
package/lib/types/prompts.d.ts
CHANGED
|
@@ -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),
|
|
123
|
-
* evolution `parseFrontmatter` is
|
|
124
|
-
* silently split family-visibility from
|
|
125
|
-
* inkos-harness case: the description carried
|
|
126
|
-
* the whole skill). Already-quoted values and
|
|
127
|
-
* (`[a, b]` / `{a: b}`) are considered safe.
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
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
|
|
176
|
-
*
|
|
177
|
-
*
|
|
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
|
package/lib/types/threats.d.ts
CHANGED
|
@@ -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