@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 +369 -239
- package/lib/types/constants.d.ts +6 -0
- package/lib/types/events.d.ts +5 -1
- package/lib/types/index.d.ts +1 -0
- package/lib/types/io.d.ts +4 -2
- package/lib/types/memory-store.d.ts +0 -1
- package/lib/types/preset-composition.d.ts +18 -0
- 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,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
|
|
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 [
|
|
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,
|
|
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
|
|
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),
|
|
2555
|
-
* evolution `parseFrontmatter` is
|
|
2556
|
-
* silently split family-visibility from
|
|
2557
|
-
* inkos-harness case: the description carried
|
|
2558
|
-
* the whole skill). Already-quoted values and
|
|
2559
|
-
* (`[a, b]` / `{a: b}`) are considered safe.
|
|
2560
|
-
*
|
|
2561
|
-
*
|
|
2562
|
-
*
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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: ${
|
|
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
|
|
3556
|
-
|
|
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 =
|
|
3562
|
-
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);
|
|
3563
3812
|
if ("error" in plan) return {
|
|
3564
3813
|
ok: false,
|
|
3565
3814
|
message: `Restructure rejected: ${plan.error}`
|
|
3566
3815
|
};
|
|
3567
|
-
const newMd = header
|
|
3568
|
-
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);
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3746
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
3979
|
-
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)) {
|
|
3980
4280
|
if (entry === "manifest.json" || entry === "extras") continue;
|
|
3981
|
-
await this.io.copy(join(
|
|
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(
|
|
3985
|
-
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));
|
|
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(
|
|
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
|
|
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 };
|
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/index.d.ts
CHANGED
|
@@ -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;
|
|
@@ -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
|
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