@lmzhen/dsh-evolution-core 0.3.15 → 0.3.17
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 +428 -261
- package/lib/types/constants.d.ts +13 -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/prompts.d.ts +1 -1
- package/lib/types/serial.d.ts +10 -0
- 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
|
@@ -64,45 +64,81 @@ function nodeEvolutionIo() {
|
|
|
64
64
|
};
|
|
65
65
|
/**
|
|
66
66
|
* Cross-process write lock (claw `withFileLock` parity): an O_EXCL lock file
|
|
67
|
-
* guards the atomic write. A >
|
|
67
|
+
* guards the atomic write. A >1s-old lock is taken over ONLY after probing
|
|
68
68
|
* the holder pid it carries (rc.66): a LIVE holder is never stolen, so a
|
|
69
|
-
* slow writer no longer loses its lock to a peer at the
|
|
69
|
+
* slow writer no longer loses its lock to a peer at the threshold (the
|
|
70
70
|
* takeover is the only best-effort surface; the retry budget fails loud —
|
|
71
71
|
* rc.65 — instead of ever proceeding unlocked). Budget = 40 * 50ms (~2s,
|
|
72
72
|
* rc.69): 8-writer contention bursts on a loaded CI runner exceed 10
|
|
73
73
|
* attempts (500ms), and a fail-loud throw was observed instead of a clean
|
|
74
74
|
* serialization.
|
|
75
|
+
* 0.3.17 (E-8): acquisition and the task are now SEPARATE try blocks — a
|
|
76
|
+
* task error (win32 rename/EBUSY surfaces as EPERM) used to be mistaken for
|
|
77
|
+
* lock contention, retried up to 40x and finally reported as
|
|
78
|
+
* "could not acquire write lock" while the real cause was hidden.
|
|
79
|
+
* 0.3.17 (E-8a): the takeover threshold (1000ms) now fits INSIDE the ~2s
|
|
80
|
+
* retry budget (budget >= 2 x threshold), so a dead holder's lock is
|
|
81
|
+
* actually recoverable within one budget instead of being arithmetically
|
|
82
|
+
* unreachable.
|
|
75
83
|
*/
|
|
76
84
|
const withWriteLock = async (path, task) => {
|
|
77
85
|
const lock = `${path}.lock`;
|
|
78
|
-
for (let attempt = 0; attempt < 40; attempt += 1)
|
|
79
|
-
await writeFile(lock, String(process.pid), { flag: "wx" });
|
|
86
|
+
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
80
87
|
try {
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
} catch {}
|
|
96
|
-
continue;
|
|
88
|
+
await writeFile(lock, String(process.pid), { flag: "wx" });
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const code = error?.code;
|
|
91
|
+
if (code !== "EEXIST" && code !== "EPERM") throw error;
|
|
92
|
+
try {
|
|
93
|
+
const st = await stat(lock);
|
|
94
|
+
if (Date.now() - st.mtimeMs > 1e3) {
|
|
95
|
+
const holder = Number(await readFile(lock, "utf8").catch(() => ""));
|
|
96
|
+
if (!(Number.isInteger(holder) && holder > 0 && isAlive(holder))) {
|
|
97
|
+
try {
|
|
98
|
+
await rm(lock, { force: true });
|
|
99
|
+
} catch {}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
97
102
|
}
|
|
103
|
+
} catch {
|
|
104
|
+
continue;
|
|
98
105
|
}
|
|
99
|
-
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
100
107
|
continue;
|
|
101
108
|
}
|
|
102
|
-
|
|
109
|
+
try {
|
|
110
|
+
return await task();
|
|
111
|
+
} finally {
|
|
112
|
+
await rm(lock, { force: true }).catch(() => {});
|
|
113
|
+
}
|
|
103
114
|
}
|
|
104
115
|
throw new Error(`could not acquire write lock for ${path} after 40 attempts`);
|
|
105
116
|
};
|
|
117
|
+
/** 0.3.17 (E-8b): sweep tmp files a crashed writer left behind — same
|
|
118
|
+
* `<target>.<pid>.<rand>.tmp` shape, older than 1h AND held by a dead pid.
|
|
119
|
+
* Lazy: only the directory a write is about to touch gets swept, once per
|
|
120
|
+
* write, inside the write lock. */
|
|
121
|
+
const sweepStaleTmps = async (path) => {
|
|
122
|
+
const dir = dirname(path);
|
|
123
|
+
const base = basename(path);
|
|
124
|
+
let entries;
|
|
125
|
+
try {
|
|
126
|
+
entries = await readdir(dir);
|
|
127
|
+
} catch {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const prefix = `${base}.`;
|
|
131
|
+
for (const name of entries) {
|
|
132
|
+
if (!name.startsWith(prefix) || !name.endsWith(".tmp")) continue;
|
|
133
|
+
const tmpPath = join(dir, name);
|
|
134
|
+
const holder = Number(name.slice(prefix.length, name.length - 4).split(".")[0] ?? "");
|
|
135
|
+
try {
|
|
136
|
+
const st = await stat(tmpPath);
|
|
137
|
+
const deadHolder = !Number.isInteger(holder) || holder <= 0 || !isAlive(holder);
|
|
138
|
+
if (Date.now() - st.mtimeMs > 36e5 && deadHolder) await rm(tmpPath, { force: true });
|
|
139
|
+
} catch {}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
106
142
|
return {
|
|
107
143
|
async readText(path) {
|
|
108
144
|
try {
|
|
@@ -115,6 +151,7 @@ function nodeEvolutionIo() {
|
|
|
115
151
|
async writeText(path, content) {
|
|
116
152
|
await mkdir(dirname(path), { recursive: true });
|
|
117
153
|
await withWriteLock(path, async () => {
|
|
154
|
+
await sweepStaleTmps(path);
|
|
118
155
|
const tmp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
119
156
|
await writeFile(tmp, content, "utf8");
|
|
120
157
|
await rename(tmp, path);
|
|
@@ -123,6 +160,7 @@ function nodeEvolutionIo() {
|
|
|
123
160
|
async transact(path, task) {
|
|
124
161
|
await mkdir(dirname(path), { recursive: true });
|
|
125
162
|
await withWriteLock(path, async () => {
|
|
163
|
+
await sweepStaleTmps(path);
|
|
126
164
|
let current;
|
|
127
165
|
try {
|
|
128
166
|
current = await readFile(path, "utf8");
|
|
@@ -370,9 +408,9 @@ function latestActivityAt(record) {
|
|
|
370
408
|
record.last_used_at,
|
|
371
409
|
record.last_viewed_at,
|
|
372
410
|
record.last_patched_at
|
|
373
|
-
].filter((value) => typeof value === "string");
|
|
411
|
+
].filter((value) => typeof value === "string" && Number.isFinite(Date.parse(value)));
|
|
374
412
|
if (values.length === 0) return null;
|
|
375
|
-
return values.
|
|
413
|
+
return values.reduce((latest, value) => Date.parse(value) > Date.parse(latest) ? value : latest);
|
|
376
414
|
}
|
|
377
415
|
/**
|
|
378
416
|
* Whether the library has ANY observed read evidence (C observation window):
|
|
@@ -494,6 +532,25 @@ const DEFAULT_USER_CHAR_LIMIT = 1375;
|
|
|
494
532
|
/** Consolidation-failure backoff cap, shared by MemoryStore and memory-files' Config default. */
|
|
495
533
|
const DEFAULT_CONSOLIDATION_FAILURES = 3;
|
|
496
534
|
const DEFAULT_SKILL_CONTENT_CHARS = 1e5;
|
|
535
|
+
/** 0.3.17 (S3.10, T-1): control-plane fields a model-facing write call may
|
|
536
|
+
* never carry — single source for plan-validator, evolution-policy and the
|
|
537
|
+
* threat scanner (they used to each hardcode the list). */
|
|
538
|
+
const FORBIDDEN_CONTROL_KEYS = [
|
|
539
|
+
"policy",
|
|
540
|
+
"threshold",
|
|
541
|
+
"prompt_hash",
|
|
542
|
+
"model_route",
|
|
543
|
+
"evolution_config"
|
|
544
|
+
];
|
|
545
|
+
/** 0.3.17 (S3.10): the model-facing write tools the policy guard and threat
|
|
546
|
+
* scanner cover. */
|
|
547
|
+
const EVOLUTION_WRITE_TOOLS = ["memory", "skill_manage"];
|
|
548
|
+
/** Hermes authoring quality bar for descriptions (the 60-char Rule). The
|
|
549
|
+
* platform's own index limit stays in validateFrontmatter; this bar is the
|
|
550
|
+
* target the authoring standard names, enforced as ADVISORY feedback.
|
|
551
|
+
* 0.3.16 (T-4): moved here from skill-store.ts so drift-signals (pure, no IO)
|
|
552
|
+
* can reference it without importing the skill-store module. */
|
|
553
|
+
const AUTHORING_DESCRIPTION_BAR = 60;
|
|
497
554
|
//#endregion
|
|
498
555
|
//#region lib/types/gates.js
|
|
499
556
|
/**
|
|
@@ -999,8 +1056,8 @@ async function readEvolutionTimeline(io, path) {
|
|
|
999
1056
|
* changes semantically: the bundle digest is the fail-closed signal for
|
|
1000
1057
|
* review workers, so a stale id across deployments must be distinguishable.
|
|
1001
1058
|
*/
|
|
1002
|
-
const PROMPT_BUNDLE_ID = "dsh-evolution@13";
|
|
1003
1059
|
const PROMPT_BUNDLE_VERSION = 13;
|
|
1060
|
+
const PROMPT_BUNDLE_ID = `dsh-evolution@13`;
|
|
1004
1061
|
const MEMORY_REVIEW_PROMPT = `[Auto-review — Memory]
|
|
1005
1062
|
Review the conversation above and consider saving to memory if appropriate.
|
|
1006
1063
|
|
|
@@ -1264,8 +1321,8 @@ const SKILL_REVIEW_PLAN_PROMPT = `${SKILL_REVIEW_PROMPT}${PLAN_CHANNEL_NOTE}`;
|
|
|
1264
1321
|
/** Subagent-channel variant of the combined review (M-2). */
|
|
1265
1322
|
const COMBINED_REVIEW_PLAN_PROMPT = `${COMBINED_REVIEW_PROMPT}${PLAN_CHANNEL_NOTE}`;
|
|
1266
1323
|
function reviewPrompt(kind, channel = "agent") {
|
|
1267
|
-
if (kind === "memory") return MEMORY_REVIEW_PROMPT;
|
|
1268
1324
|
if (channel === "plan") return kind === "skill" ? SKILL_REVIEW_PLAN_PROMPT : COMBINED_REVIEW_PLAN_PROMPT;
|
|
1325
|
+
if (kind === "memory") return MEMORY_REVIEW_PROMPT;
|
|
1269
1326
|
if (kind === "skill") return SKILL_REVIEW_PROMPT;
|
|
1270
1327
|
return COMBINED_REVIEW_PROMPT;
|
|
1271
1328
|
}
|
|
@@ -1297,7 +1354,7 @@ const PROMPT_BUNDLE = createPromptBundle({
|
|
|
1297
1354
|
skillsGuidance: SKILLS_GUIDANCE
|
|
1298
1355
|
});
|
|
1299
1356
|
function verifyPromptBundle(bundle = PROMPT_BUNDLE) {
|
|
1300
|
-
if (bundle.id !==
|
|
1357
|
+
if (bundle.id !== PROMPT_BUNDLE_ID || bundle.version !== 13) return false;
|
|
1301
1358
|
const canonical = JSON.stringify({
|
|
1302
1359
|
id: PROMPT_BUNDLE_ID,
|
|
1303
1360
|
version: 13,
|
|
@@ -1510,7 +1567,7 @@ const PATTERNS = [
|
|
|
1510
1567
|
label: "hermes_env",
|
|
1511
1568
|
category: "persistence",
|
|
1512
1569
|
scope: "strict",
|
|
1513
|
-
regex: /\$?HOME\/\.hermes|~\/\.hermes|\.hermes\/\.env/i
|
|
1570
|
+
regex: /\$?HOME\/\.hermes|~\/\.hermes|\.hermes\/\.env|%USERPROFILE%[\\/]\.hermes/i
|
|
1514
1571
|
},
|
|
1515
1572
|
{
|
|
1516
1573
|
label: "c2_node_registration",
|
|
@@ -1557,9 +1614,16 @@ const SCOPE_ORDER = {
|
|
|
1557
1614
|
strict: 3
|
|
1558
1615
|
};
|
|
1559
1616
|
const NO_SCAN_OPTIONS = {};
|
|
1617
|
+
/** Window overlap for the full-coverage scan: far larger than the longest
|
|
1618
|
+
* pattern span (~530 chars: `curl [^\n]{0,512} ...`), so a match straddling a
|
|
1619
|
+
* window boundary is fully inside at least one window (E-12, 0.3.16). */
|
|
1620
|
+
const PATTERN_OVERLAP = 4096;
|
|
1560
1621
|
/**
|
|
1561
1622
|
* Scan text at `scope`. Patterns are cumulative: `strict` includes all scopes.
|
|
1562
1623
|
* `options.excludeLabels` removes matching patterns without changing `scope`.
|
|
1624
|
+
* `maxScanChars` is the WINDOW SIZE, not a total cap (E-12, 0.3.16): the whole
|
|
1625
|
+
* text is always scanned in overlapping windows, so content beyond 65,536
|
|
1626
|
+
* characters (skill files may run to 100,000) is no longer a blind zone.
|
|
1563
1627
|
*/
|
|
1564
1628
|
function scanThreats(text, scope = "strict", maxScanChars = 65536, options = NO_SCAN_OPTIONS) {
|
|
1565
1629
|
const findings = [];
|
|
@@ -1573,12 +1637,23 @@ function scanThreats(text, scope = "strict", maxScanChars = 65536, options = NO_
|
|
|
1573
1637
|
category: "unicode_obfuscation",
|
|
1574
1638
|
scope
|
|
1575
1639
|
});
|
|
1576
|
-
const normalized = text.normalize("NFKC")
|
|
1640
|
+
const normalized = text.normalize("NFKC");
|
|
1641
|
+
const windows = [];
|
|
1642
|
+
if (normalized.length <= maxScanChars) windows.push(normalized);
|
|
1643
|
+
else {
|
|
1644
|
+
const step = Math.max(1, maxScanChars - PATTERN_OVERLAP);
|
|
1645
|
+
for (let start = 0; start < normalized.length; start += step) windows.push(normalized.slice(start, start + maxScanChars));
|
|
1646
|
+
}
|
|
1577
1647
|
const excluded = new Set(options.excludeLabels ?? []);
|
|
1578
|
-
|
|
1648
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1649
|
+
for (const window of windows) for (const pattern of PATTERNS) {
|
|
1579
1650
|
if (SCOPE_ORDER[pattern.scope] > SCOPE_ORDER[scope]) continue;
|
|
1580
1651
|
if (excluded.has(pattern.label)) continue;
|
|
1581
|
-
if (pattern.regex.test(
|
|
1652
|
+
if (!pattern.regex.test(window)) continue;
|
|
1653
|
+
const key = `${pattern.label}|${pattern.scope}`;
|
|
1654
|
+
if (seen.has(key)) continue;
|
|
1655
|
+
seen.add(key);
|
|
1656
|
+
findings.push({
|
|
1582
1657
|
label: pattern.label,
|
|
1583
1658
|
category: pattern.category,
|
|
1584
1659
|
scope: pattern.scope
|
|
@@ -1648,7 +1723,7 @@ function previewEntries(entries) {
|
|
|
1648
1723
|
return `\n\nCurrent entries (preview):\n${shown.join("\n")}${more}`;
|
|
1649
1724
|
}
|
|
1650
1725
|
function memoryRoot(env = process.env) {
|
|
1651
|
-
return join(env.DSH_HOME
|
|
1726
|
+
return join(env.DSH_HOME || join(homedir(), ".dsh"), "memories");
|
|
1652
1727
|
}
|
|
1653
1728
|
function fileFor(root, target) {
|
|
1654
1729
|
return join(root, target === "memory" ? "MEMORY.md" : "USER.md");
|
|
@@ -1703,9 +1778,6 @@ var MemoryStore = class {
|
|
|
1703
1778
|
const raw = await this.io.readText(fileFor(this.root, target));
|
|
1704
1779
|
return raw === null ? [] : [...new Set(normalizeEntries(raw))];
|
|
1705
1780
|
}
|
|
1706
|
-
async write(target, entries) {
|
|
1707
|
-
await this.io.writeText(fileFor(this.root, target), render(entries));
|
|
1708
|
-
}
|
|
1709
1781
|
resetFailures() {
|
|
1710
1782
|
this.failureCount = 0;
|
|
1711
1783
|
}
|
|
@@ -2101,7 +2173,7 @@ async function loadMutations(root, io = nodeEvolutionIo()) {
|
|
|
2101
2173
|
}
|
|
2102
2174
|
/** Append one record, trim to `cap`, and write atomically (versioned shape). */
|
|
2103
2175
|
async function recordMutation(root, io, record, cap = 500) {
|
|
2104
|
-
await transactIo(io, mutationsFile(root),
|
|
2176
|
+
await transactIo(io, mutationsFile(root), (current) => {
|
|
2105
2177
|
if (current !== null) try {
|
|
2106
2178
|
JSON.parse(current);
|
|
2107
2179
|
} catch {
|
|
@@ -2110,10 +2182,10 @@ async function recordMutation(root, io, record, cap = 500) {
|
|
|
2110
2182
|
const existing = parseMutationRecords(current);
|
|
2111
2183
|
existing.push(record);
|
|
2112
2184
|
const trimmed = existing.length > cap ? existing.slice(existing.length - cap) : existing;
|
|
2113
|
-
return
|
|
2185
|
+
return JSON.stringify({
|
|
2114
2186
|
version: 1,
|
|
2115
2187
|
records: trimmed
|
|
2116
|
-
}, null, 2)
|
|
2188
|
+
}, null, 2);
|
|
2117
2189
|
});
|
|
2118
2190
|
}
|
|
2119
2191
|
//#endregion
|
|
@@ -2248,7 +2320,7 @@ function computeDedupGroups(input) {
|
|
|
2248
2320
|
const [ra, rb] = [find(a), find(b)];
|
|
2249
2321
|
if (ra !== rb) parent.set(rb, ra);
|
|
2250
2322
|
};
|
|
2251
|
-
for (const [
|
|
2323
|
+
for (const [, bucketNames] of hashes) {
|
|
2252
2324
|
const first = bucketNames[0];
|
|
2253
2325
|
if (first === void 0 || bucketNames.length === 1) continue;
|
|
2254
2326
|
for (let index = 1; index < bucketNames.length; index += 1) {
|
|
@@ -2326,9 +2398,9 @@ const SECRET_PATTERNS = [
|
|
|
2326
2398
|
["gitlab token", /glpat-[A-Za-z0-9_-]{16,}/g],
|
|
2327
2399
|
["slack token", /xox[baprs]-[A-Za-z0-9-]{10,}/g],
|
|
2328
2400
|
["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]
|
|
2401
|
+
["bearer credential", /Bearer [A-Za-z0-9._~+/=\-]{16,}/g]
|
|
2331
2402
|
];
|
|
2403
|
+
const INLINE_ASSIGNMENT_PATTERN = /(\b(?:token|api[_-]?key|secret|password|passwd)\b[\s]*[:=][\s]*["']?)([A-Z0-9._~+/=\-]{12,})/gi;
|
|
2332
2404
|
/**
|
|
2333
2405
|
* Mask credential-shaped text before it crosses a session boundary.
|
|
2334
2406
|
* @param text - the text about to be sent to a model outside this session.
|
|
@@ -2336,10 +2408,29 @@ const SECRET_PATTERNS = [
|
|
|
2336
2408
|
*/
|
|
2337
2409
|
function redactSecrets(text) {
|
|
2338
2410
|
let out = text;
|
|
2339
|
-
for (const [, pattern] of SECRET_PATTERNS) out = out.replace(pattern,
|
|
2411
|
+
for (const [, pattern] of SECRET_PATTERNS) out = out.replace(pattern, "<redacted>");
|
|
2412
|
+
out = out.replace(INLINE_ASSIGNMENT_PATTERN, (_match, p1) => `${p1 ?? ""}<redacted>`);
|
|
2340
2413
|
return out;
|
|
2341
2414
|
}
|
|
2342
2415
|
//#endregion
|
|
2416
|
+
//#region lib/types/serial.js
|
|
2417
|
+
/**
|
|
2418
|
+
* A process-local serial task queue: each task starts only after the previous
|
|
2419
|
+
* one settles (success or failure), so read-modify-write sequences that share
|
|
2420
|
+
* one file never interleave inside this process. The durable cross-process
|
|
2421
|
+
* serialization layer is the IO backend's transact lock; this chain is the
|
|
2422
|
+
* second layer (0.3.17 S2.8, T-1: the shape was duplicated in state-json and
|
|
2423
|
+
* memory-files — one factory now).
|
|
2424
|
+
*/
|
|
2425
|
+
function makeSerialQueue() {
|
|
2426
|
+
let chain = Promise.resolve();
|
|
2427
|
+
return (task) => {
|
|
2428
|
+
const run = chain.then(task, task);
|
|
2429
|
+
chain = run.then(() => void 0, () => void 0);
|
|
2430
|
+
return run;
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
//#endregion
|
|
2343
2434
|
//#region lib/types/skill-health.js
|
|
2344
2435
|
/**
|
|
2345
2436
|
* Skill structure-health domain (rc.73 A1, 008 design): a SECOND assessment
|
|
@@ -2414,6 +2505,7 @@ const FIX_PATTERNS = [/worked after|fixed by|the fix was|root cause/i, /retry(?:
|
|
|
2414
2505
|
/** Fold one session event into the current turn observation. */
|
|
2415
2506
|
function observeEvent(signal, event) {
|
|
2416
2507
|
if (event.type === "user/message") {
|
|
2508
|
+
if (!Array.isArray(event.data.content)) return;
|
|
2417
2509
|
const text = event.data.content.map((block) => block.type === "text" ? block.text : "").join(" ");
|
|
2418
2510
|
signal.userChars += text.length;
|
|
2419
2511
|
if (CORRECTION_PATTERNS.some((pattern) => pattern.test(text))) signal.memorySignal = true;
|
|
@@ -2473,6 +2565,165 @@ function foldTurn(session, fromSeq) {
|
|
|
2473
2565
|
return signal;
|
|
2474
2566
|
}
|
|
2475
2567
|
//#endregion
|
|
2568
|
+
//#region lib/types/drift-signals.js
|
|
2569
|
+
/**
|
|
2570
|
+
* Library-level drift signals for the maintenance subagent (design 011).
|
|
2571
|
+
*
|
|
2572
|
+
* Deterministic fact checks over a skill-library snapshot: domain drift
|
|
2573
|
+
* (narrow names, near-duplicate groups, prefix clusters) and layer drift
|
|
2574
|
+
* (log-like bodies, duplicate headings, overlong lines, missing support-file
|
|
2575
|
+
* pointers, description over the authoring bar). Pure functions only — no IO,
|
|
2576
|
+
* no LLM, no services. Thresholds are imported from their owning modules
|
|
2577
|
+
* (skill-health / quality / skill-store), never duplicated.
|
|
2578
|
+
*
|
|
2579
|
+
* Distinct from `signals.ts` — the session-level review signal gate.
|
|
2580
|
+
*/
|
|
2581
|
+
/** Physical line length at/above which a body line is reported overlong (011 §4). */
|
|
2582
|
+
const DRIFT_MAX_LINE_CHARS = 1500;
|
|
2583
|
+
/** Signal-set version: bump whenever ids/thresholds change (011 §7 version coupling). */
|
|
2584
|
+
const DRIFT_SIGNALS_VERSION = "1";
|
|
2585
|
+
/** Render-time nouns for the MAINTAIN_PROMPT placeholders (single vocabulary with the facts block). */
|
|
2586
|
+
const DRIFT_SIGNAL_NOUNS = {
|
|
2587
|
+
dedup_group: "近重复组",
|
|
2588
|
+
prefix_cluster: "前缀聚类",
|
|
2589
|
+
stamp_density: "stamp 密度",
|
|
2590
|
+
body_size: "正文体量",
|
|
2591
|
+
dup_heading: "重复标题",
|
|
2592
|
+
overlong_line: "超长行",
|
|
2593
|
+
pointer_missing: "缺失指针",
|
|
2594
|
+
description_chars: "描述长度",
|
|
2595
|
+
narrow_name: "窄名",
|
|
2596
|
+
usage_observed: "使用观察",
|
|
2597
|
+
quality_low: "质量分"
|
|
2598
|
+
};
|
|
2599
|
+
const NARROW_NAME_PATTERNS = [
|
|
2600
|
+
{
|
|
2601
|
+
label: "error-string",
|
|
2602
|
+
re: /^(?:err|error|exception|traceback|warn|fail)(?:[-_][a-z0-9]+)+$/i
|
|
2603
|
+
},
|
|
2604
|
+
{
|
|
2605
|
+
label: "pr-number",
|
|
2606
|
+
re: /^(?:pr|issue)[-_]?\d{2,}$/i
|
|
2607
|
+
},
|
|
2608
|
+
{
|
|
2609
|
+
label: "dated",
|
|
2610
|
+
re: /\d{4}-\d{2}-\d{2}/
|
|
2611
|
+
},
|
|
2612
|
+
{
|
|
2613
|
+
label: "session-verb",
|
|
2614
|
+
re: /^(?:fix|debug|audit|salvage|diagnose|investigate)[-_][a-z0-9-]+$/i
|
|
2615
|
+
}
|
|
2616
|
+
];
|
|
2617
|
+
/** Detect support files the body never references (by basename or relative path). */
|
|
2618
|
+
function missingSupportPointers(body, supportFiles) {
|
|
2619
|
+
return supportFiles.filter((path) => {
|
|
2620
|
+
const base = path.split("/").pop() ?? path;
|
|
2621
|
+
return base.length > 0 && !body.includes(base) && !body.includes(path);
|
|
2622
|
+
});
|
|
2623
|
+
}
|
|
2624
|
+
/** Duplicate `## heading` occurrences: singleton results default to head of the file. */
|
|
2625
|
+
function duplicateHeadings(body) {
|
|
2626
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2627
|
+
for (const line of body.split("\n")) {
|
|
2628
|
+
const m = /^##\s+(.+)$/.exec(line);
|
|
2629
|
+
if (m?.[1]) {
|
|
2630
|
+
const heading = m[1].trim();
|
|
2631
|
+
if (heading) counts.set(heading, (counts.get(heading) ?? 0) + 1);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
return [...counts.entries()].filter(([, count]) => count > 1).map(([heading, count]) => ({
|
|
2635
|
+
heading,
|
|
2636
|
+
count
|
|
2637
|
+
}));
|
|
2638
|
+
}
|
|
2639
|
+
/** Physical lines over `max` characters: `{ lineNo, chars }`, 1-based line numbers. */
|
|
2640
|
+
function overlongLines(body, max = DRIFT_MAX_LINE_CHARS) {
|
|
2641
|
+
const out = [];
|
|
2642
|
+
const lines = body.split("\n");
|
|
2643
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
2644
|
+
const length = (lines[index] ?? "").length;
|
|
2645
|
+
if (length > max) out.push({
|
|
2646
|
+
lineNo: index + 1,
|
|
2647
|
+
chars: length
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
return out;
|
|
2651
|
+
}
|
|
2652
|
+
/** Narrow-name shapes detected in a skill name (empty = none). */
|
|
2653
|
+
function narrowNameMatches(name) {
|
|
2654
|
+
return NARROW_NAME_PATTERNS.filter(({ re }) => re.test(name)).map(({ label }) => label);
|
|
2655
|
+
}
|
|
2656
|
+
function supportGroupCount(supportFiles) {
|
|
2657
|
+
const groups = /* @__PURE__ */ new Set();
|
|
2658
|
+
for (const path of supportFiles ?? []) {
|
|
2659
|
+
const head = path.split("/")[0];
|
|
2660
|
+
if (head) groups.add(head);
|
|
2661
|
+
}
|
|
2662
|
+
return groups.size;
|
|
2663
|
+
}
|
|
2664
|
+
function sig(id, verdict, value, threshold, detail) {
|
|
2665
|
+
return {
|
|
2666
|
+
id,
|
|
2667
|
+
verdict,
|
|
2668
|
+
value,
|
|
2669
|
+
threshold,
|
|
2670
|
+
detail
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2673
|
+
/**
|
|
2674
|
+
* Compute all drift signals for a snapshot. Missing inputs (quality score,
|
|
2675
|
+
* usage window) yield `unknown` — never a fabricated verdict.
|
|
2676
|
+
*/
|
|
2677
|
+
function computeDriftSignals(snapshots) {
|
|
2678
|
+
const library = [];
|
|
2679
|
+
const names = snapshots.map((s) => s.name);
|
|
2680
|
+
const dedup = computeDedupGroups({ contents: new Map(snapshots.map((s) => [s.name, s.body])) });
|
|
2681
|
+
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(";")}`));
|
|
2682
|
+
const clusters = computePrefixClusters(names);
|
|
2683
|
+
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("|")}`));
|
|
2684
|
+
const allProvided = snapshots.length > 0 && snapshots.every((s) => s.usageObserved !== null && s.usageObserved !== void 0);
|
|
2685
|
+
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"));
|
|
2686
|
+
return {
|
|
2687
|
+
library,
|
|
2688
|
+
skills: snapshots.map((snapshot) => {
|
|
2689
|
+
const signals = [];
|
|
2690
|
+
const body = snapshot.body;
|
|
2691
|
+
const supportFiles = snapshot.supportFiles ?? [];
|
|
2692
|
+
const supportEnumerated = snapshot.supportFiles !== void 0;
|
|
2693
|
+
const density = assessStructureHealth({
|
|
2694
|
+
skillName: snapshot.name,
|
|
2695
|
+
bodyChars: body.length,
|
|
2696
|
+
bodyText: body,
|
|
2697
|
+
supportGroups: supportGroupCount(supportFiles)
|
|
2698
|
+
}, DEFAULT_HEALTH_THRESHOLDS).dims.stampDensityPerKb;
|
|
2699
|
+
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`));
|
|
2700
|
+
signals.push(sig("body_size", body.length >= DEFAULT_HEALTH_THRESHOLDS.softBodyChars ? "over" : "pass", `${body.length}`, `${DEFAULT_HEALTH_THRESHOLDS.softBodyChars}`));
|
|
2701
|
+
const dupes = duplicateHeadings(body);
|
|
2702
|
+
signals.push(dupes.length === 0 ? sig("dup_heading", "pass", "none") : sig("dup_heading", "over", dupes.map((d) => `${d.heading}(${d.count})`).join(", "), "count >= 2"));
|
|
2703
|
+
const long = overlongLines(body);
|
|
2704
|
+
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}`));
|
|
2705
|
+
const missing = supportEnumerated ? missingSupportPointers(body, supportFiles) : void 0;
|
|
2706
|
+
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(", ") ?? ""));
|
|
2707
|
+
const narrow = narrowNameMatches(snapshot.name);
|
|
2708
|
+
signals.push(narrow.length === 0 ? sig("narrow_name", "pass", "none") : sig("narrow_name", "over", narrow.join(", "), void 0, `name=${snapshot.name}`));
|
|
2709
|
+
const description = snapshot.description;
|
|
2710
|
+
signals.push(description === void 0 ? sig("description_chars", "unknown", "missing", "60") : sig("description_chars", description.length > 60 ? "over" : "pass", `${description.length}`, `60`));
|
|
2711
|
+
const quality = snapshot.quality;
|
|
2712
|
+
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}`));
|
|
2713
|
+
return {
|
|
2714
|
+
name: snapshot.name,
|
|
2715
|
+
signals,
|
|
2716
|
+
...snapshot.protected !== void 0 && snapshot.protected !== null ? { protected: snapshot.protected } : {},
|
|
2717
|
+
...snapshot.catalogInvalid !== void 0 ? { catalogInvalid: snapshot.catalogInvalid } : {}
|
|
2718
|
+
};
|
|
2719
|
+
})
|
|
2720
|
+
};
|
|
2721
|
+
}
|
|
2722
|
+
/** Convenience: fetch one signal from an assessment or library list. */
|
|
2723
|
+
function findDriftSignal(signals, id) {
|
|
2724
|
+
return signals.find((signal) => signal.id === id);
|
|
2725
|
+
}
|
|
2726
|
+
//#endregion
|
|
2476
2727
|
//#region lib/types/skill-store.js
|
|
2477
2728
|
/**
|
|
2478
2729
|
* Skill library management for the self-evolution plugin.
|
|
@@ -2482,6 +2733,10 @@ function foldTurn(session, fromSeq) {
|
|
|
2482
2733
|
* it created unless a `.hermes-managed` marker opts a skill in. Archival is a
|
|
2483
2734
|
* move to `.archive/` — never a hard delete.
|
|
2484
2735
|
*/
|
|
2736
|
+
/** 0.3.16 (S1.13, T-6): the pointer-line prefix written into a body when a
|
|
2737
|
+
* section is moved to references/ — single literal, both restructure and
|
|
2738
|
+
* append-mode consolidation emit the same discoverability line. */
|
|
2739
|
+
const POINTER_LINE_PREFIX = "> 详见 references/";
|
|
2485
2740
|
const DEFAULT_SKILL_LIMITS = {
|
|
2486
2741
|
maxNameLength: 64,
|
|
2487
2742
|
maxDescriptionLength: MAX_DESCRIPTION_LENGTH,
|
|
@@ -2495,7 +2750,7 @@ const RESTRUCTURE_TARGET_RE = /^references\/[a-z0-9][a-z0-9._-]*\.md$/;
|
|
|
2495
2750
|
/** Extra file name carried inside a snapshot's `extras/` directory. */
|
|
2496
2751
|
const SNAPSHOT_EXTRA_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
2497
2752
|
function skillsRoot(env = process.env) {
|
|
2498
|
-
return join(env.DSH_HOME
|
|
2753
|
+
return join(env.DSH_HOME || join(homedir(), ".dsh"), "skills");
|
|
2499
2754
|
}
|
|
2500
2755
|
/**
|
|
2501
2756
|
* Map a requesting session onto the two origin surfaces (rc.44 plan M2-2.3):
|
|
@@ -2583,21 +2838,26 @@ function parseFrontmatter(content) {
|
|
|
2583
2838
|
}
|
|
2584
2839
|
/** YAML plain-scalar hazards that make an UNQUOTED frontmatter value
|
|
2585
2840
|
* 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
|
-
*
|
|
2841
|
+
* separator), ` #` (comment start), a trailing `:` (a mapping marker),
|
|
2842
|
+
* or a leading YAML indicator. The evolution `parseFrontmatter` is
|
|
2843
|
+
* deliberately lenient, so violations silently split family-visibility from
|
|
2844
|
+
* platform-visibility (0.3.11 inkos-harness case: the description carried
|
|
2845
|
+
* "…: " and the catalog dropped the whole skill). Already-quoted values and
|
|
2846
|
+
* well-formed flow collections (`[a, b]` / `{a: b}`) are considered safe.
|
|
2847
|
+
* 0.3.16 (E-47): null/bool/number-shaped plain scalars are flagged too — they
|
|
2848
|
+
* parse as booleans/numbers on the platform while the family keeps the string
|
|
2849
|
+
* (a `description: true` split-brain).
|
|
2850
|
+
* This rule is only the FAST PATH — the write path re-verifies every rewrite
|
|
2851
|
+
* with the real YAML parser (see normalizeFrontmatter), so an incomplete
|
|
2852
|
+
* approximation can never corrupt a multiline flow value (P3-4). */
|
|
2595
2853
|
function yamlPlainScalarNeedsQuotes(value) {
|
|
2596
2854
|
if (value.length === 0) return false;
|
|
2597
2855
|
if (value.length >= 2 && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'"))) return false;
|
|
2598
2856
|
if (/^\[.*\]$/.test(value) || /^\{.*\}$/.test(value)) return false;
|
|
2599
2857
|
if (value.includes(": ")) return true;
|
|
2600
2858
|
if (value.includes(" #")) return true;
|
|
2859
|
+
if (value.endsWith(":")) return true;
|
|
2860
|
+
if (/^(?:null|true|false|~|[-+]?\d+(?:\.\d+)?)$/i.test(value)) return true;
|
|
2601
2861
|
if (/^[-?:,[\]{}#&*!|>'\"%@`\s]/.test(value)) return true;
|
|
2602
2862
|
return false;
|
|
2603
2863
|
}
|
|
@@ -2723,10 +2983,6 @@ function validateFrontmatter(content, expectedName, limits = DEFAULT_SKILL_LIMIT
|
|
|
2723
2983
|
if (content.length > limits.maxSkillContentChars) return `SKILL.md content exceeds ${limits.maxSkillContentChars} characters. Consider splitting into a smaller SKILL.md with supporting files.`;
|
|
2724
2984
|
return null;
|
|
2725
2985
|
}
|
|
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
2986
|
/**
|
|
2731
2987
|
* Advisory authoring feedback (P0): evaluate frontmatter against the
|
|
2732
2988
|
* authoring bar WITHOUT changing platform validation semantics. The bar is
|
|
@@ -2835,7 +3091,7 @@ function fuzzyReplace(content, oldString, newString, replaceAll) {
|
|
|
2835
3091
|
}
|
|
2836
3092
|
function fuzzyPatch(content, oldString, newString, replaceAll = false) {
|
|
2837
3093
|
if (oldString === "") return null;
|
|
2838
|
-
if (content.includes(oldString)) return replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, newString);
|
|
3094
|
+
if (content.includes(oldString)) return replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, () => newString);
|
|
2839
3095
|
const boundary = trimPatternBoundaries(oldString);
|
|
2840
3096
|
if (boundary === "") return null;
|
|
2841
3097
|
if (boundary !== oldString) {
|
|
@@ -2900,7 +3156,7 @@ function planRestructureSections(body, moves) {
|
|
|
2900
3156
|
for (let i = 0; i < lines.length; i += 1) {
|
|
2901
3157
|
const span = byStart.get(i);
|
|
2902
3158
|
if (span) {
|
|
2903
|
-
rebuilt.push(
|
|
3159
|
+
rebuilt.push(`${POINTER_LINE_PREFIX}${span.rel.split("/").at(-1)}`);
|
|
2904
3160
|
i = span.end - 1;
|
|
2905
3161
|
} else rebuilt.push(lines[i] ?? "");
|
|
2906
3162
|
}
|
|
@@ -2957,7 +3213,12 @@ var SkillLibrary = class {
|
|
|
2957
3213
|
async read(rawName) {
|
|
2958
3214
|
const name = rawName.trim();
|
|
2959
3215
|
if (this.badName(name) !== null) return null;
|
|
2960
|
-
|
|
3216
|
+
try {
|
|
3217
|
+
return await this.io.readText(join(this.dirOf(name), "SKILL.md"));
|
|
3218
|
+
} catch (error) {
|
|
3219
|
+
if (error?.code === "EISDIR") return null;
|
|
3220
|
+
throw error;
|
|
3221
|
+
}
|
|
2961
3222
|
}
|
|
2962
3223
|
/**
|
|
2963
3224
|
|
|
@@ -3180,7 +3441,7 @@ var SkillLibrary = class {
|
|
|
3180
3441
|
this.notifyMutation({
|
|
3181
3442
|
action: "create",
|
|
3182
3443
|
name: normalized,
|
|
3183
|
-
|
|
3444
|
+
skillDir: dir
|
|
3184
3445
|
});
|
|
3185
3446
|
return {
|
|
3186
3447
|
ok: true,
|
|
@@ -3235,7 +3496,7 @@ var SkillLibrary = class {
|
|
|
3235
3496
|
this.notifyMutation({
|
|
3236
3497
|
action: "update",
|
|
3237
3498
|
name,
|
|
3238
|
-
|
|
3499
|
+
skillDir: dir
|
|
3239
3500
|
});
|
|
3240
3501
|
return {
|
|
3241
3502
|
ok: true,
|
|
@@ -3324,7 +3585,7 @@ var SkillLibrary = class {
|
|
|
3324
3585
|
this.notifyMutation({
|
|
3325
3586
|
action: "patch",
|
|
3326
3587
|
name,
|
|
3327
|
-
|
|
3588
|
+
skillDir: dir
|
|
3328
3589
|
});
|
|
3329
3590
|
return {
|
|
3330
3591
|
ok: true,
|
|
@@ -3374,7 +3635,23 @@ var SkillLibrary = class {
|
|
|
3374
3635
|
await this.io.rename(dir, dest);
|
|
3375
3636
|
} catch {
|
|
3376
3637
|
await this.io.copy(dir, dest);
|
|
3377
|
-
|
|
3638
|
+
try {
|
|
3639
|
+
await this.io.remove(dir);
|
|
3640
|
+
} catch (error) {
|
|
3641
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
3642
|
+
try {
|
|
3643
|
+
await this.io.remove(dest);
|
|
3644
|
+
return {
|
|
3645
|
+
ok: false,
|
|
3646
|
+
message: `Archive copy succeeded but the source could not be removed (${reason}); the copied archive was rolled back.`
|
|
3647
|
+
};
|
|
3648
|
+
} catch {
|
|
3649
|
+
return {
|
|
3650
|
+
ok: false,
|
|
3651
|
+
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.`
|
|
3652
|
+
};
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3378
3655
|
}
|
|
3379
3656
|
const reason = options.reason ?? (options.absorbedInto ? `Consolidated into ${options.absorbedInto}` : "Archived by self-evolution curator");
|
|
3380
3657
|
await this.io.writeText(join(dest, ".archive-reason"), `${(/* @__PURE__ */ new Date()).toISOString()}: ${reason}\n`);
|
|
@@ -3499,7 +3776,7 @@ var SkillLibrary = class {
|
|
|
3499
3776
|
content: `<!-- demoted from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}\n`
|
|
3500
3777
|
});
|
|
3501
3778
|
}
|
|
3502
|
-
const pointerLines = normalizedSources.map((source) => `\n
|
|
3779
|
+
const pointerLines = normalizedSources.map((source) => `\n${POINTER_LINE_PREFIX}${source}.md`).join("");
|
|
3503
3780
|
const extended = targetMd.trimEnd() + pointerLines + "\n";
|
|
3504
3781
|
const validation = validateFrontmatter(extended, targetName, this.limits);
|
|
3505
3782
|
if (validation) return {
|
|
@@ -3529,10 +3806,20 @@ var SkillLibrary = class {
|
|
|
3529
3806
|
});
|
|
3530
3807
|
if (!result.ok) throw new Error(result.message);
|
|
3531
3808
|
} catch (error) {
|
|
3532
|
-
|
|
3809
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
3810
|
+
const failedRestores = [];
|
|
3811
|
+
for (const source of archived.reverse()) try {
|
|
3812
|
+
if (!(await this.restoreFromArchive(source)).ok) failedRestores.push(source);
|
|
3813
|
+
} catch {
|
|
3814
|
+
failedRestores.push(source);
|
|
3815
|
+
}
|
|
3816
|
+
if (failedRestores.length > 0) return {
|
|
3817
|
+
ok: false,
|
|
3818
|
+
message: `Consolidation failed (${reason}); rolled back EXCEPT ${failedRestores.join(", ")} — still in .archive, restore them with /evolution skill restore.`
|
|
3819
|
+
};
|
|
3533
3820
|
return {
|
|
3534
3821
|
ok: false,
|
|
3535
|
-
message: `Consolidation failed and was rolled back: ${
|
|
3822
|
+
message: `Consolidation failed and was rolled back: ${reason}`
|
|
3536
3823
|
};
|
|
3537
3824
|
}
|
|
3538
3825
|
return {
|
|
@@ -3584,20 +3871,20 @@ var SkillLibrary = class {
|
|
|
3584
3871
|
ok: false,
|
|
3585
3872
|
message: `Skill "${name}" not found.`
|
|
3586
3873
|
};
|
|
3587
|
-
const
|
|
3588
|
-
|
|
3589
|
-
if (frontmatterEnd < 0) return {
|
|
3874
|
+
const block = frontmatterBlock(md);
|
|
3875
|
+
if (!block) return {
|
|
3590
3876
|
ok: false,
|
|
3591
3877
|
message: "SKILL.md has no valid frontmatter; refusing to restructure."
|
|
3592
3878
|
};
|
|
3593
|
-
const header =
|
|
3594
|
-
const plan = planRestructureSections(
|
|
3879
|
+
const header = block.lines.slice(0, block.end + 1).join(block.nl);
|
|
3880
|
+
const plan = planRestructureSections(md.slice(header.length).replace(/\r\n/g, "\n"), moves);
|
|
3595
3881
|
if ("error" in plan) return {
|
|
3596
3882
|
ok: false,
|
|
3597
3883
|
message: `Restructure rejected: ${plan.error}`
|
|
3598
3884
|
};
|
|
3599
|
-
const newMd = header
|
|
3600
|
-
const
|
|
3885
|
+
const newMd = `${header}${plan.body}`.replace(/\r\n/g, "\n");
|
|
3886
|
+
const finalMd = block.nl === "\r\n" ? newMd.replace(/\n/g, "\r\n") : newMd;
|
|
3887
|
+
const newMdCheck = validateFrontmatter(finalMd, name, this.limits);
|
|
3601
3888
|
if (newMdCheck) return {
|
|
3602
3889
|
ok: false,
|
|
3603
3890
|
message: `Restructure rejected: ${newMdCheck}`
|
|
@@ -3629,7 +3916,7 @@ var SkillLibrary = class {
|
|
|
3629
3916
|
}
|
|
3630
3917
|
writes.push({
|
|
3631
3918
|
target: join(dir, "SKILL.md"),
|
|
3632
|
-
content:
|
|
3919
|
+
content: finalMd
|
|
3633
3920
|
});
|
|
3634
3921
|
const result = await this.applyTreeChange({
|
|
3635
3922
|
name,
|
|
@@ -3721,11 +4008,11 @@ var SkillLibrary = class {
|
|
|
3721
4008
|
message: `Tree change failed and was rolled back: ${error instanceof Error ? error.message : String(error)}`
|
|
3722
4009
|
};
|
|
3723
4010
|
}
|
|
3724
|
-
await this.audit(name, plan.auditAction, md, landing.find((entry) => entry.target.
|
|
4011
|
+
await this.audit(name, plan.auditAction, md, landing.find((entry) => entry.target.split(/[\\/]/).pop() === "SKILL.md")?.content ?? md, plan.auditSummary);
|
|
3725
4012
|
this.notifyMutation({
|
|
3726
4013
|
action: plan.eventAction,
|
|
3727
4014
|
name,
|
|
3728
|
-
|
|
4015
|
+
skillDir: dir
|
|
3729
4016
|
});
|
|
3730
4017
|
return {
|
|
3731
4018
|
ok: true,
|
|
@@ -3758,7 +4045,12 @@ var SkillLibrary = class {
|
|
|
3758
4045
|
message: "No skill archive available."
|
|
3759
4046
|
};
|
|
3760
4047
|
}
|
|
3761
|
-
const
|
|
4048
|
+
const candidates = entries.filter((entry) => entry === name || entry.startsWith(`${name}-`)).sort().reverse();
|
|
4049
|
+
let chosen;
|
|
4050
|
+
for (const candidate of candidates) if (parseFrontmatter(await this.io.readText(join(archiveRoot, candidate, "SKILL.md")).catch(() => null) ?? "")?.frontmatter.name === name) {
|
|
4051
|
+
chosen = candidate;
|
|
4052
|
+
break;
|
|
4053
|
+
}
|
|
3762
4054
|
if (!chosen) return {
|
|
3763
4055
|
ok: false,
|
|
3764
4056
|
message: `Skill "${name}" is not in .archive.`
|
|
@@ -3774,14 +4066,21 @@ var SkillLibrary = class {
|
|
|
3774
4066
|
try {
|
|
3775
4067
|
await this.io.rename(source, dest);
|
|
3776
4068
|
} catch {
|
|
3777
|
-
|
|
3778
|
-
|
|
4069
|
+
try {
|
|
4070
|
+
await this.io.copy(source, dest);
|
|
4071
|
+
await this.io.remove(source);
|
|
4072
|
+
} catch (error) {
|
|
4073
|
+
return {
|
|
4074
|
+
ok: false,
|
|
4075
|
+
message: `Restore of "${name}" from .archive failed: ${error instanceof Error ? error.message : String(error)}`
|
|
4076
|
+
};
|
|
4077
|
+
}
|
|
3779
4078
|
}
|
|
3780
4079
|
if (await this.io.exists(join(dest, ".archive-reason"))) await this.io.remove(join(dest, ".archive-reason"));
|
|
3781
4080
|
this.notifyMutation({
|
|
3782
4081
|
action: "restore",
|
|
3783
4082
|
name,
|
|
3784
|
-
|
|
4083
|
+
skillDir: dest
|
|
3785
4084
|
});
|
|
3786
4085
|
return {
|
|
3787
4086
|
ok: true,
|
|
@@ -3827,7 +4126,8 @@ var SkillLibrary = class {
|
|
|
3827
4126
|
this.notifyMutation({
|
|
3828
4127
|
action: "write_file",
|
|
3829
4128
|
name,
|
|
3830
|
-
|
|
4129
|
+
skillDir: dir,
|
|
4130
|
+
file: target
|
|
3831
4131
|
});
|
|
3832
4132
|
return {
|
|
3833
4133
|
ok: true,
|
|
@@ -3868,7 +4168,8 @@ var SkillLibrary = class {
|
|
|
3868
4168
|
this.notifyMutation({
|
|
3869
4169
|
action: "remove_file",
|
|
3870
4170
|
name,
|
|
3871
|
-
|
|
4171
|
+
skillDir: dir,
|
|
4172
|
+
file: target
|
|
3872
4173
|
});
|
|
3873
4174
|
return {
|
|
3874
4175
|
ok: true,
|
|
@@ -3996,7 +4297,43 @@ var SkillLibrary = class {
|
|
|
3996
4297
|
ok: false,
|
|
3997
4298
|
message: "No skill snapshot available."
|
|
3998
4299
|
};
|
|
3999
|
-
await this.snapshotAll("pre-rollback", extras);
|
|
4300
|
+
const preRollbackPath = await this.snapshotAll("pre-rollback", extras);
|
|
4301
|
+
try {
|
|
4302
|
+
await this.restoreSnapshotIntoRoot(latest.path);
|
|
4303
|
+
} catch (error) {
|
|
4304
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
4305
|
+
try {
|
|
4306
|
+
await this.restoreSnapshotIntoRoot(preRollbackPath);
|
|
4307
|
+
return {
|
|
4308
|
+
ok: false,
|
|
4309
|
+
message: `Snapshot restore failed (${reason}); the active tree was rolled back to the pre-rollback snapshot.`
|
|
4310
|
+
};
|
|
4311
|
+
} catch (rollbackError) {
|
|
4312
|
+
return {
|
|
4313
|
+
ok: false,
|
|
4314
|
+
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).`
|
|
4315
|
+
};
|
|
4316
|
+
}
|
|
4317
|
+
}
|
|
4318
|
+
const snapshotExtras = await this.readSnapshotExtras(latest.path);
|
|
4319
|
+
this.notifyMutation({
|
|
4320
|
+
action: "restore",
|
|
4321
|
+
name: "snapshot"
|
|
4322
|
+
});
|
|
4323
|
+
return {
|
|
4324
|
+
ok: true,
|
|
4325
|
+
message: `Restored skill tree from ${latest.path}`,
|
|
4326
|
+
path: latest.path,
|
|
4327
|
+
...snapshotExtras.length === 0 ? {} : { extras: snapshotExtras }
|
|
4328
|
+
};
|
|
4329
|
+
}
|
|
4330
|
+
/**
|
|
4331
|
+
* Whole-tree replacement from one snapshot path (rc.50 P2-14): every
|
|
4332
|
+
* NON-system entry in the active root is cleared first, then the manifest
|
|
4333
|
+
* drives the repopulation (skills, sidecars, `.archive`). Extracted from
|
|
4334
|
+
* restoreLatestSnapshot so a failed restore can roll itself back (E-13).
|
|
4335
|
+
*/
|
|
4336
|
+
async restoreSnapshotIntoRoot(snapshotPath) {
|
|
4000
4337
|
let rootEntries;
|
|
4001
4338
|
try {
|
|
4002
4339
|
rootEntries = await this.io.list(this.root);
|
|
@@ -4007,200 +4344,30 @@ var SkillLibrary = class {
|
|
|
4007
4344
|
if (entry.startsWith(".")) continue;
|
|
4008
4345
|
await this.io.remove(join(this.root, entry));
|
|
4009
4346
|
}
|
|
4010
|
-
const manifest = await this.readSnapshotManifest(
|
|
4011
|
-
if (manifest === null) for (const entry of await this.io.list(
|
|
4347
|
+
const manifest = await this.readSnapshotManifest(snapshotPath);
|
|
4348
|
+
if (manifest === null) for (const entry of await this.io.list(snapshotPath)) {
|
|
4012
4349
|
if (entry === "manifest.json" || entry === "extras") continue;
|
|
4013
|
-
await this.io.copy(join(
|
|
4350
|
+
await this.io.copy(join(snapshotPath, entry), join(this.root, entry));
|
|
4014
4351
|
}
|
|
4015
4352
|
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(
|
|
4353
|
+
for (const name of manifest.skills) await this.io.copy(join(snapshotPath, name), join(this.root, name));
|
|
4354
|
+
for (const sidecar of manifest.sidecars) await this.io.copy(join(snapshotPath, sidecar), join(this.root, sidecar));
|
|
4018
4355
|
const archiveRoot = join(this.root, ".archive");
|
|
4019
4356
|
if (manifest.hasArchive === true) {
|
|
4020
4357
|
await this.io.remove(archiveRoot);
|
|
4021
|
-
await this.io.copy(join(
|
|
4358
|
+
await this.io.copy(join(snapshotPath, ".archive"), archiveRoot);
|
|
4022
4359
|
} else if (manifest.hasArchive === false) await this.io.remove(archiveRoot);
|
|
4023
4360
|
}
|
|
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
4361
|
}
|
|
4036
4362
|
};
|
|
4037
4363
|
//#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
4364
|
//#region lib/types/state-store.js
|
|
4198
4365
|
/**
|
|
4199
4366
|
* Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
|
|
4200
4367
|
* state (reports, activity store, feedback file, state-domain data).
|
|
4201
4368
|
*/
|
|
4202
4369
|
function evolutionHome(env = process.env) {
|
|
4203
|
-
return join(env.DSH_HOME
|
|
4370
|
+
return join(env.DSH_HOME || join(homedir(), ".dsh"), "evolution");
|
|
4204
4371
|
}
|
|
4205
4372
|
//#endregion
|
|
4206
|
-
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 };
|
|
4373
|
+
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, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FORBIDDEN_CONTROL_KEYS, 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, makeSerialQueue, 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,17 @@ 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
|
+
/** 0.3.17 (S3.10, T-1): control-plane fields a model-facing write call may
|
|
54
|
+
* never carry — single source for plan-validator, evolution-policy and the
|
|
55
|
+
* threat scanner (they used to each hardcode the list). */
|
|
56
|
+
export declare const FORBIDDEN_CONTROL_KEYS: readonly ["policy", "threshold", "prompt_hash", "model_route", "evolution_config"];
|
|
57
|
+
/** 0.3.17 (S3.10): the model-facing write tools the policy guard and threat
|
|
58
|
+
* scanner cover. */
|
|
59
|
+
export declare const EVOLUTION_WRITE_TOOLS: readonly ["memory", "skill_manage"];
|
|
60
|
+
/** Hermes authoring quality bar for descriptions (the 60-char Rule). The
|
|
61
|
+
* platform's own index limit stays in validateFrontmatter; this bar is the
|
|
62
|
+
* target the authoring standard names, enforced as ADVISORY feedback.
|
|
63
|
+
* 0.3.16 (T-4): moved here from skill-store.ts so drift-signals (pure, no IO)
|
|
64
|
+
* can reference it without importing the skill-store module. */
|
|
65
|
+
export declare const AUTHORING_DESCRIPTION_BAR = 60;
|
|
53
66
|
//# 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
|
@@ -19,6 +19,7 @@ export * from './preset-composition.ts';
|
|
|
19
19
|
export * from './prompts.ts';
|
|
20
20
|
export * from './quality.ts';
|
|
21
21
|
export * from './redact.ts';
|
|
22
|
+
export * from './serial.ts';
|
|
22
23
|
export * from './skill-health.ts';
|
|
23
24
|
export * from './signals.ts';
|
|
24
25
|
export * from './drift-signals.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;
|
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.";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A process-local serial task queue: each task starts only after the previous
|
|
3
|
+
* one settles (success or failure), so read-modify-write sequences that share
|
|
4
|
+
* one file never interleave inside this process. The durable cross-process
|
|
5
|
+
* serialization layer is the IO backend's transact lock; this chain is the
|
|
6
|
+
* second layer (0.3.17 S2.8, T-1: the shape was duplicated in state-json and
|
|
7
|
+
* memory-files — one factory now).
|
|
8
|
+
*/
|
|
9
|
+
export declare function makeSerialQueue(): <T>(task: () => Promise<T>) => Promise<T>;
|
|
10
|
+
//# sourceMappingURL=serial.d.ts.map
|
|
@@ -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