@lmzhen/dsh-evolution-core 0.1.0-rc.43 → 0.1.0-rc.44
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 +152 -56
- package/lib/types/constants.d.ts +2 -0
- package/lib/types/memory-store.d.ts +7 -0
- package/lib/types/skill-store.d.ts +34 -13
- package/lib/types/usage.d.ts +12 -2
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -149,19 +149,44 @@ function emptyRecord() {
|
|
|
149
149
|
archived_at: null
|
|
150
150
|
};
|
|
151
151
|
}
|
|
152
|
+
const isTimestamp = (value) => value === null || typeof value === "string";
|
|
153
|
+
/**
|
|
154
|
+
* Field-level normalization for one sidecar record (rc.42 audit P2-3): the
|
|
155
|
+
* spread used to copy any junk through verbatim, so a corrupted file could
|
|
156
|
+
* carry `use_count: "3"` into the quality math and lifecycle comparisons as
|
|
157
|
+
* NaN. Every field falls back to its `emptyRecord()` baseline unless it has
|
|
158
|
+
* exactly the declared type; an invalid `created_at` anchors the age clock at
|
|
159
|
+
* now (first-sight defer semantics for a record whose age is unknowable).
|
|
160
|
+
* Pure — exported for unit tests; `loadUsage` is the production caller.
|
|
161
|
+
*/
|
|
162
|
+
function normalizeUsageRecord(record) {
|
|
163
|
+
const base = emptyRecord();
|
|
164
|
+
if (!record || typeof record !== "object" || Array.isArray(record)) return base;
|
|
165
|
+
const raw = record;
|
|
166
|
+
const num = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
167
|
+
const bool = (value, fallback) => typeof value === "boolean" ? value : fallback;
|
|
168
|
+
return {
|
|
169
|
+
created_by: typeof raw.created_by === "string" ? raw.created_by : null,
|
|
170
|
+
use_count: num(raw.use_count, base.use_count),
|
|
171
|
+
view_count: num(raw.view_count, base.view_count),
|
|
172
|
+
patch_count: num(raw.patch_count, base.patch_count),
|
|
173
|
+
last_used_at: isTimestamp(raw.last_used_at) ? raw.last_used_at : base.last_used_at,
|
|
174
|
+
last_viewed_at: isTimestamp(raw.last_viewed_at) ? raw.last_viewed_at : base.last_viewed_at,
|
|
175
|
+
last_patched_at: isTimestamp(raw.last_patched_at) ? raw.last_patched_at : base.last_patched_at,
|
|
176
|
+
created_at: typeof raw.created_at === "string" ? raw.created_at : base.created_at,
|
|
177
|
+
state: raw.state === "stale" || raw.state === "archived" ? raw.state : "active",
|
|
178
|
+
pinned: bool(raw.pinned, base.pinned),
|
|
179
|
+
archived_at: isTimestamp(raw.archived_at) ? raw.archived_at : base.archived_at,
|
|
180
|
+
quality_score: typeof raw.quality_score === "number" && Number.isFinite(raw.quality_score) ? raw.quality_score : void 0,
|
|
181
|
+
quality_warn: typeof raw.quality_warn === "boolean" ? raw.quality_warn : void 0
|
|
182
|
+
};
|
|
183
|
+
}
|
|
152
184
|
async function loadUsage(root, io = nodeEvolutionIo()) {
|
|
153
185
|
const map = /* @__PURE__ */ new Map();
|
|
154
186
|
const raw = await io.readText(usageFile(root));
|
|
155
187
|
if (raw !== null) try {
|
|
156
188
|
const parsed = JSON.parse(raw);
|
|
157
|
-
for (const [name, record] of Object.entries(parsed))
|
|
158
|
-
const base = emptyRecord();
|
|
159
|
-
map.set(name, {
|
|
160
|
-
...base,
|
|
161
|
-
...record,
|
|
162
|
-
state: record.state === "stale" || record.state === "archived" ? record.state : "active"
|
|
163
|
-
});
|
|
164
|
-
}
|
|
189
|
+
for (const [name, record] of Object.entries(parsed)) map.set(name, normalizeUsageRecord(record));
|
|
165
190
|
} catch {}
|
|
166
191
|
return map;
|
|
167
192
|
}
|
|
@@ -287,6 +312,8 @@ const DEFAULT_STALE_AFTER_DAYS = 30;
|
|
|
287
312
|
const DEFAULT_ARCHIVE_AFTER_DAYS = 90;
|
|
288
313
|
const DEFAULT_MEMORY_CHAR_LIMIT = 2200;
|
|
289
314
|
const DEFAULT_USER_CHAR_LIMIT = 1375;
|
|
315
|
+
/** Consolidation-failure backoff cap, shared by MemoryStore and memory-files' Config default. */
|
|
316
|
+
const DEFAULT_CONSOLIDATION_FAILURES = 3;
|
|
290
317
|
const DEFAULT_SKILL_CONTENT_CHARS = 1e5;
|
|
291
318
|
//#endregion
|
|
292
319
|
//#region lib/types/curator.js
|
|
@@ -922,6 +949,15 @@ function scanContentThreats(text, maxScanChars = 65536, options = NO_SCAN_OPTION
|
|
|
922
949
|
* same 10× bound around a file that should never exceed the store limit).
|
|
923
950
|
*/
|
|
924
951
|
const READ_GUARD_FACTOR = 10;
|
|
952
|
+
/**
|
|
953
|
+
* Consolidation-failure backoff window (package-private, rc.42 audit P2-1):
|
|
954
|
+
* only failures inside the window count toward `maxConsolidationFailures`.
|
|
955
|
+
* The store cannot observe turn boundaries, so the model-facing "this turn"
|
|
956
|
+
* phrasing is approximated with ten minutes — generous enough to cover one
|
|
957
|
+
* turn's retry loop, short enough that a failure yesterday never makes today's
|
|
958
|
+
* first refusal say "stop retrying".
|
|
959
|
+
*/
|
|
960
|
+
const FAILURE_WINDOW_MS = 10 * 6e4;
|
|
925
961
|
function memoryRoot(env = process.env) {
|
|
926
962
|
return join(env.DSH_HOME ?? join(homedir(), ".dsh"), "memories");
|
|
927
963
|
}
|
|
@@ -945,6 +981,7 @@ var MemoryStore = class {
|
|
|
945
981
|
maxFailures;
|
|
946
982
|
io;
|
|
947
983
|
failureCount = 0;
|
|
984
|
+
lastFailureAt = 0;
|
|
948
985
|
constructor(options = {}) {
|
|
949
986
|
this.io = options.io ?? nodeEvolutionIo();
|
|
950
987
|
this.memoryLimit = options.memoryCharLimit ?? 2200;
|
|
@@ -984,6 +1021,8 @@ var MemoryStore = class {
|
|
|
984
1021
|
this.failureCount = 0;
|
|
985
1022
|
}
|
|
986
1023
|
failure(target, message, entries) {
|
|
1024
|
+
if (Date.now() - this.lastFailureAt > FAILURE_WINDOW_MS) this.failureCount = 0;
|
|
1025
|
+
this.lastFailureAt = Date.now();
|
|
987
1026
|
this.failureCount += 1;
|
|
988
1027
|
const chars = entries.join(ENTRY_DELIMITER).length;
|
|
989
1028
|
if (this.failureCount > this.maxFailures) return {
|
|
@@ -1313,11 +1352,17 @@ var MemoryStore = class {
|
|
|
1313
1352
|
* blank lines, leading/trailing delimiters) that indicate the file was
|
|
1314
1353
|
* edited outside MemoryStore. Purely single-canonical content reaches the
|
|
1315
1354
|
* same serialization and returns false, so a normal write is never flagged.
|
|
1355
|
+
*
|
|
1356
|
+
* An absent, empty, or whitespace-only file is the "never written" state
|
|
1357
|
+
* (rc.42 audit P1-6): it parses to zero entries, so the canonical form
|
|
1358
|
+
* `'\n'` can never byte-match it and every write path was permanently
|
|
1359
|
+
* refused with "External drift detected" — including the repairs the model
|
|
1360
|
+
* would need to make. Such files are adopted instead of flagged.
|
|
1316
1361
|
*/
|
|
1317
1362
|
async detectDrift(target) {
|
|
1318
1363
|
if (await this.oversizedFile(target)) return true;
|
|
1319
1364
|
const raw = await this.io.readText(fileFor(this.root, target));
|
|
1320
|
-
if (raw === null) return false;
|
|
1365
|
+
if (raw === null || raw.trim() === "") return false;
|
|
1321
1366
|
const entries = normalizeEntries(raw);
|
|
1322
1367
|
const limit = this.limitFor(target);
|
|
1323
1368
|
if (limit > 0 && entries.some((entry) => entry.length > limit)) return true;
|
|
@@ -1346,7 +1391,7 @@ async function loadMutations(root, io = nodeEvolutionIo()) {
|
|
|
1346
1391
|
if (raw === null) return [];
|
|
1347
1392
|
try {
|
|
1348
1393
|
const parsed = JSON.parse(raw);
|
|
1349
|
-
return (Array.isArray(parsed) ? parsed : typeof parsed === "object" && parsed !== null && Array.isArray(parsed.records) ? parsed.records : []).filter((entry) => typeof entry === "object" && entry !== null && typeof entry.skillName === "string" && typeof entry.action === "string");
|
|
1394
|
+
return (Array.isArray(parsed) ? parsed : typeof parsed === "object" && parsed !== null && Array.isArray(parsed.records) ? parsed.records : []).filter((entry) => typeof entry === "object" && entry !== null && typeof entry.skillName === "string" && typeof entry.action === "string" && typeof entry.at === "string");
|
|
1350
1395
|
} catch {
|
|
1351
1396
|
return [];
|
|
1352
1397
|
}
|
|
@@ -1622,6 +1667,26 @@ function parseFrontmatter(content) {
|
|
|
1622
1667
|
body
|
|
1623
1668
|
};
|
|
1624
1669
|
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Skill names referenced by a SKILL.md's `related_skills` frontmatter
|
|
1672
|
+
* (B-line G3, rc.44): the single parsing source for the quality references
|
|
1673
|
+
* factor and the learning-graph edges. The DSH frontmatter parser keeps the
|
|
1674
|
+
* YAML value as a string (`"[a, b]"`), so names are scanned out of it; each
|
|
1675
|
+
* must satisfy the skill-name shape and the referencing skill itself is
|
|
1676
|
+
* excluded. Pure and deduplicated.
|
|
1677
|
+
*/
|
|
1678
|
+
function relatedSkillNames(content, exclude) {
|
|
1679
|
+
const parsed = parseFrontmatter(content);
|
|
1680
|
+
if (!parsed) return [];
|
|
1681
|
+
const raw = parsed.frontmatter["related_skills"];
|
|
1682
|
+
if (typeof raw !== "string") return [];
|
|
1683
|
+
const names = /* @__PURE__ */ new Set();
|
|
1684
|
+
for (const match of Array.from(raw.matchAll(/[a-z0-9][a-z0-9-]*/g))) {
|
|
1685
|
+
const target = match[0];
|
|
1686
|
+
if (target && SKILL_NAME_RE.test(target) && target !== exclude) names.add(target);
|
|
1687
|
+
}
|
|
1688
|
+
return [...names];
|
|
1689
|
+
}
|
|
1625
1690
|
function validateFrontmatter(content, expectedName, limits = DEFAULT_SKILL_LIMITS) {
|
|
1626
1691
|
const parsed = parseFrontmatter(content);
|
|
1627
1692
|
if (!parsed) return "SKILL.md must start and end with YAML frontmatter and include a body.";
|
|
@@ -1748,7 +1813,7 @@ var SkillLibrary = class {
|
|
|
1748
1813
|
async list() {
|
|
1749
1814
|
const summaries = [];
|
|
1750
1815
|
for (const name of await listNames(this.root, this.io)) {
|
|
1751
|
-
const dir =
|
|
1816
|
+
const dir = this.dirOf(name);
|
|
1752
1817
|
const md = await this.io.readText(join(dir, "SKILL.md"));
|
|
1753
1818
|
if (!md) continue;
|
|
1754
1819
|
const parsed = parseFrontmatter(md);
|
|
@@ -1765,9 +1830,24 @@ var SkillLibrary = class {
|
|
|
1765
1830
|
}
|
|
1766
1831
|
return summaries;
|
|
1767
1832
|
}
|
|
1768
|
-
async read(
|
|
1833
|
+
async read(rawName) {
|
|
1834
|
+
const name = rawName.trim();
|
|
1769
1835
|
if (this.badName(name) !== null) return null;
|
|
1770
|
-
return this.io.readText(join(
|
|
1836
|
+
return this.io.readText(join(this.dirOf(name), "SKILL.md"));
|
|
1837
|
+
}
|
|
1838
|
+
/**
|
|
1839
|
+
|
|
1840
|
+
* Single path-building choke point (rc.42 audit P2-5): every directory path
|
|
1841
|
+
|
|
1842
|
+
* is built from the TRIMMED name, so a name that passes `badName` (which
|
|
1843
|
+
|
|
1844
|
+
* trims before validating) can never mint a second, whitespace-padded
|
|
1845
|
+
|
|
1846
|
+
* directory next to the real one. Callers keep passing raw user input.
|
|
1847
|
+
|
|
1848
|
+
*/
|
|
1849
|
+
dirOf(name) {
|
|
1850
|
+
return skillDir(this.root, name.trim());
|
|
1771
1851
|
}
|
|
1772
1852
|
/** Name-format guard shared by every path-building mutator/reader. */
|
|
1773
1853
|
badName(name) {
|
|
@@ -1775,14 +1855,16 @@ var SkillLibrary = class {
|
|
|
1775
1855
|
if (!SKILL_NAME_RE.test(normalized) || normalized.length > this.limits.maxNameLength) return `Invalid skill name "${normalized}". Use lowercase letters, digits, and hyphens (<= ${this.limits.maxNameLength}).`;
|
|
1776
1856
|
return null;
|
|
1777
1857
|
}
|
|
1778
|
-
async writeProtection(
|
|
1779
|
-
const
|
|
1858
|
+
async writeProtection(rawName, origin = "foreground") {
|
|
1859
|
+
const name = rawName.trim();
|
|
1860
|
+
const dir = this.dirOf(name);
|
|
1780
1861
|
for (const marker of ["bundled", "hub-installed"]) if (await this.io.exists(markerPath(dir, marker))) return marker;
|
|
1781
1862
|
if (origin === "background_review" && await this.io.exists(markerPath(dir, "pinned"))) return "pinned";
|
|
1782
1863
|
return null;
|
|
1783
1864
|
}
|
|
1784
|
-
async deleteProtection(
|
|
1785
|
-
const
|
|
1865
|
+
async deleteProtection(rawName, options = {}) {
|
|
1866
|
+
const name = rawName.trim();
|
|
1867
|
+
const dir = this.dirOf(name);
|
|
1786
1868
|
const markers = options.allowBundled ? ["hub-installed", "pinned"] : [
|
|
1787
1869
|
"bundled",
|
|
1788
1870
|
"hub-installed",
|
|
@@ -1791,26 +1873,30 @@ var SkillLibrary = class {
|
|
|
1791
1873
|
for (const marker of markers) if (await this.io.exists(markerPath(dir, marker))) return marker;
|
|
1792
1874
|
return null;
|
|
1793
1875
|
}
|
|
1794
|
-
async isManaged(
|
|
1795
|
-
const
|
|
1876
|
+
async isManaged(rawName) {
|
|
1877
|
+
const name = rawName.trim();
|
|
1878
|
+
const dir = this.dirOf(name);
|
|
1796
1879
|
return await this.io.exists(markerPath(dir, "hermes-managed"));
|
|
1797
1880
|
}
|
|
1798
1881
|
/** Whether the skill carries the bundled marker (curator prune-builtins eligibility). */
|
|
1799
|
-
async isBundled(
|
|
1882
|
+
async isBundled(rawName) {
|
|
1883
|
+
const name = rawName.trim();
|
|
1800
1884
|
if (this.badName(name) !== null) return false;
|
|
1801
|
-
const dir =
|
|
1885
|
+
const dir = this.dirOf(name);
|
|
1802
1886
|
return await this.io.exists(markerPath(dir, "bundled"));
|
|
1803
1887
|
}
|
|
1804
1888
|
/** Whether the skill carries the pinned marker (the marker is the factual source; usage.pinned mirrors it). */
|
|
1805
|
-
async isPinned(
|
|
1889
|
+
async isPinned(rawName) {
|
|
1890
|
+
const name = rawName.trim();
|
|
1806
1891
|
if (this.badName(name) !== null) return false;
|
|
1807
|
-
const dir =
|
|
1892
|
+
const dir = this.dirOf(name);
|
|
1808
1893
|
return await this.io.exists(markerPath(dir, "pinned"));
|
|
1809
1894
|
}
|
|
1810
1895
|
/** Count non-empty support subdirectories (richness input for quality scoring). */
|
|
1811
|
-
async countSupportDirs(
|
|
1896
|
+
async countSupportDirs(rawName) {
|
|
1897
|
+
const name = rawName.trim();
|
|
1812
1898
|
if (this.badName(name) !== null) return 0;
|
|
1813
|
-
const dir =
|
|
1899
|
+
const dir = this.dirOf(name);
|
|
1814
1900
|
let entries;
|
|
1815
1901
|
try {
|
|
1816
1902
|
entries = await this.io.list(dir);
|
|
@@ -1859,7 +1945,7 @@ var SkillLibrary = class {
|
|
|
1859
1945
|
ok: false,
|
|
1860
1946
|
message: "Only the foreground (user or the main agent) may pin or unpin skills."
|
|
1861
1947
|
};
|
|
1862
|
-
const dir =
|
|
1948
|
+
const dir = this.dirOf(normalized);
|
|
1863
1949
|
const marker = markerPath(dir, "pinned");
|
|
1864
1950
|
const existing = await this.io.exists(marker);
|
|
1865
1951
|
if (pinned && existing) return {
|
|
@@ -1901,7 +1987,7 @@ var SkillLibrary = class {
|
|
|
1901
1987
|
ok: false,
|
|
1902
1988
|
message: threat
|
|
1903
1989
|
};
|
|
1904
|
-
const dir =
|
|
1990
|
+
const dir = this.dirOf(normalized);
|
|
1905
1991
|
if (await this.io.exists(join(dir, "SKILL.md"))) return {
|
|
1906
1992
|
ok: false,
|
|
1907
1993
|
message: `Skill "${normalized}" already exists.`
|
|
@@ -1915,13 +2001,14 @@ var SkillLibrary = class {
|
|
|
1915
2001
|
path: dir
|
|
1916
2002
|
};
|
|
1917
2003
|
}
|
|
1918
|
-
async update(
|
|
2004
|
+
async update(rawName, content, origin = "foreground") {
|
|
2005
|
+
const name = rawName.trim();
|
|
1919
2006
|
const badName = this.badName(name);
|
|
1920
2007
|
if (badName) return {
|
|
1921
2008
|
ok: false,
|
|
1922
2009
|
message: badName
|
|
1923
2010
|
};
|
|
1924
|
-
const dir =
|
|
2011
|
+
const dir = this.dirOf(name);
|
|
1925
2012
|
const md = await this.io.readText(join(dir, "SKILL.md"));
|
|
1926
2013
|
if (!md) return {
|
|
1927
2014
|
ok: false,
|
|
@@ -1950,13 +2037,14 @@ var SkillLibrary = class {
|
|
|
1950
2037
|
path: dir
|
|
1951
2038
|
};
|
|
1952
2039
|
}
|
|
1953
|
-
async patch(
|
|
2040
|
+
async patch(rawName, oldString, newString, filePath = "", replaceAll = false, origin = "foreground") {
|
|
2041
|
+
const name = rawName.trim();
|
|
1954
2042
|
const badName = this.badName(name);
|
|
1955
2043
|
if (badName) return {
|
|
1956
2044
|
ok: false,
|
|
1957
2045
|
message: badName
|
|
1958
2046
|
};
|
|
1959
|
-
const dir =
|
|
2047
|
+
const dir = this.dirOf(name);
|
|
1960
2048
|
const skillMd = join(dir, "SKILL.md");
|
|
1961
2049
|
if (!await this.io.exists(skillMd)) return {
|
|
1962
2050
|
ok: false,
|
|
@@ -2016,13 +2104,14 @@ var SkillLibrary = class {
|
|
|
2016
2104
|
path: dir
|
|
2017
2105
|
};
|
|
2018
2106
|
}
|
|
2019
|
-
async archive(
|
|
2107
|
+
async archive(rawName, options = {}) {
|
|
2108
|
+
const name = rawName.trim();
|
|
2020
2109
|
const badName = this.badName(name);
|
|
2021
2110
|
if (badName) return {
|
|
2022
2111
|
ok: false,
|
|
2023
2112
|
message: badName
|
|
2024
2113
|
};
|
|
2025
|
-
const dir =
|
|
2114
|
+
const dir = this.dirOf(name);
|
|
2026
2115
|
const md = await this.io.readText(join(dir, "SKILL.md"));
|
|
2027
2116
|
if (!md) return {
|
|
2028
2117
|
ok: false,
|
|
@@ -2034,14 +2123,17 @@ var SkillLibrary = class {
|
|
|
2034
2123
|
message: `Skill "${name}" is protected (${protection}).`
|
|
2035
2124
|
};
|
|
2036
2125
|
if (options.absorbedInto) {
|
|
2037
|
-
if (!await this.io.readText(join(
|
|
2126
|
+
if (!await this.io.readText(join(this.dirOf(options.absorbedInto), "SKILL.md"))) return {
|
|
2038
2127
|
ok: false,
|
|
2039
2128
|
message: `absorbed_into="${options.absorbedInto}" does not exist.`
|
|
2040
2129
|
};
|
|
2041
2130
|
}
|
|
2042
2131
|
const archiveRoot = join(this.root, ".archive");
|
|
2043
|
-
let dest = join(archiveRoot, name);
|
|
2044
|
-
if (await this.io.exists(dest))
|
|
2132
|
+
let dest = join(archiveRoot, name.trim());
|
|
2133
|
+
if (await this.io.exists(dest)) {
|
|
2134
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
|
2135
|
+
dest = join(archiveRoot, `${name.trim()}-${stamp}`);
|
|
2136
|
+
}
|
|
2045
2137
|
try {
|
|
2046
2138
|
await this.io.rename(dir, dest);
|
|
2047
2139
|
} catch {
|
|
@@ -2063,25 +2155,26 @@ var SkillLibrary = class {
|
|
|
2063
2155
|
* collapse into one, and the originals stay recoverable under `.archive/`.
|
|
2064
2156
|
*/
|
|
2065
2157
|
async consolidate(target, sources, origin = "foreground") {
|
|
2066
|
-
const
|
|
2158
|
+
const targetName = target.trim();
|
|
2159
|
+
const normalizedSources = [...new Set(sources.map((name) => name.trim()))].filter((name) => name !== targetName);
|
|
2067
2160
|
if (normalizedSources.length === 0) return {
|
|
2068
2161
|
ok: false,
|
|
2069
2162
|
message: "Consolidation requires at least one distinct source skill."
|
|
2070
2163
|
};
|
|
2071
|
-
for (const name of [
|
|
2164
|
+
for (const name of [targetName, ...normalizedSources]) if (!SKILL_NAME_RE.test(name)) return {
|
|
2072
2165
|
ok: false,
|
|
2073
2166
|
message: `Invalid skill name "${name}". Use lowercase letters, digits, and hyphens.`
|
|
2074
2167
|
};
|
|
2075
|
-
const targetDir =
|
|
2168
|
+
const targetDir = this.dirOf(targetName);
|
|
2076
2169
|
const targetMd = await this.io.readText(join(targetDir, "SKILL.md"));
|
|
2077
2170
|
if (!targetMd) return {
|
|
2078
2171
|
ok: false,
|
|
2079
|
-
message: `Skill "${
|
|
2172
|
+
message: `Skill "${targetName}" not found.`
|
|
2080
2173
|
};
|
|
2081
|
-
const targetProtection = await this.writeProtection(
|
|
2174
|
+
const targetProtection = await this.writeProtection(targetName, origin);
|
|
2082
2175
|
if (targetProtection) return {
|
|
2083
2176
|
ok: false,
|
|
2084
|
-
message: `Skill "${
|
|
2177
|
+
message: `Skill "${targetName}" is protected (${targetProtection}).`
|
|
2085
2178
|
};
|
|
2086
2179
|
const parts = [];
|
|
2087
2180
|
for (const source of normalizedSources) {
|
|
@@ -2090,7 +2183,7 @@ var SkillLibrary = class {
|
|
|
2090
2183
|
ok: false,
|
|
2091
2184
|
message: `Skill "${source}" is protected (${protection}).`
|
|
2092
2185
|
};
|
|
2093
|
-
const sourceMd = await this.io.readText(join(
|
|
2186
|
+
const sourceMd = await this.io.readText(join(this.dirOf(source), "SKILL.md"));
|
|
2094
2187
|
if (!sourceMd) return {
|
|
2095
2188
|
ok: false,
|
|
2096
2189
|
message: `Skill "${source}" not found.`
|
|
@@ -2103,7 +2196,7 @@ var SkillLibrary = class {
|
|
|
2103
2196
|
parts.push(`\n<!-- consolidated from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}`);
|
|
2104
2197
|
}
|
|
2105
2198
|
const merged = targetMd.trimEnd() + parts.join("\n") + "\n";
|
|
2106
|
-
const validation = validateFrontmatter(merged,
|
|
2199
|
+
const validation = validateFrontmatter(merged, targetName, this.limits);
|
|
2107
2200
|
if (validation) return {
|
|
2108
2201
|
ok: false,
|
|
2109
2202
|
message: `Consolidation rejected: ${validation}`
|
|
@@ -2116,7 +2209,7 @@ var SkillLibrary = class {
|
|
|
2116
2209
|
const archived = [];
|
|
2117
2210
|
try {
|
|
2118
2211
|
for (const source of normalizedSources) {
|
|
2119
|
-
const result = await this.archive(source, { absorbedInto:
|
|
2212
|
+
const result = await this.archive(source, { absorbedInto: targetName });
|
|
2120
2213
|
if (!result.ok) throw new Error(result.message);
|
|
2121
2214
|
archived.push(source);
|
|
2122
2215
|
}
|
|
@@ -2131,7 +2224,7 @@ var SkillLibrary = class {
|
|
|
2131
2224
|
}
|
|
2132
2225
|
return {
|
|
2133
2226
|
ok: true,
|
|
2134
|
-
message: `Consolidated ${normalizedSources.join(", ")} into "${
|
|
2227
|
+
message: `Consolidated ${normalizedSources.join(", ")} into "${targetName}".`,
|
|
2135
2228
|
path: targetDir
|
|
2136
2229
|
};
|
|
2137
2230
|
}
|
|
@@ -2140,12 +2233,13 @@ var SkillLibrary = class {
|
|
|
2140
2233
|
* recoverability: archival never deletes, and this is the control-plane
|
|
2141
2234
|
* path back. The `.archive-reason` marker is dropped on restore.
|
|
2142
2235
|
*/
|
|
2143
|
-
async restoreFromArchive(
|
|
2236
|
+
async restoreFromArchive(rawName) {
|
|
2237
|
+
const name = rawName.trim();
|
|
2144
2238
|
if (!SKILL_NAME_RE.test(name)) return {
|
|
2145
2239
|
ok: false,
|
|
2146
2240
|
message: `Invalid skill name "${name}". Use lowercase letters, digits, and hyphens.`
|
|
2147
2241
|
};
|
|
2148
|
-
if (await this.io.exists(join(
|
|
2242
|
+
if (await this.io.exists(join(this.dirOf(name), "SKILL.md"))) return {
|
|
2149
2243
|
ok: false,
|
|
2150
2244
|
message: `Skill "${name}" already exists in the active root; refusing to overwrite.`
|
|
2151
2245
|
};
|
|
@@ -2165,7 +2259,7 @@ var SkillLibrary = class {
|
|
|
2165
2259
|
message: `Skill "${name}" is not in .archive.`
|
|
2166
2260
|
};
|
|
2167
2261
|
const source = join(archiveRoot, chosen);
|
|
2168
|
-
const dest =
|
|
2262
|
+
const dest = this.dirOf(name);
|
|
2169
2263
|
try {
|
|
2170
2264
|
await this.io.rename(source, dest);
|
|
2171
2265
|
} catch {
|
|
@@ -2179,13 +2273,14 @@ var SkillLibrary = class {
|
|
|
2179
2273
|
path: dest
|
|
2180
2274
|
};
|
|
2181
2275
|
}
|
|
2182
|
-
async writeSupportFile(
|
|
2276
|
+
async writeSupportFile(rawName, filePath, content, origin = "foreground") {
|
|
2277
|
+
const name = rawName.trim();
|
|
2183
2278
|
const badName = this.badName(name);
|
|
2184
2279
|
if (badName) return {
|
|
2185
2280
|
ok: false,
|
|
2186
2281
|
message: badName
|
|
2187
2282
|
};
|
|
2188
|
-
const dir =
|
|
2283
|
+
const dir = this.dirOf(name);
|
|
2189
2284
|
if (!await this.io.exists(join(dir, "SKILL.md"))) return {
|
|
2190
2285
|
ok: false,
|
|
2191
2286
|
message: `Skill "${name}" not found.`
|
|
@@ -2219,13 +2314,14 @@ var SkillLibrary = class {
|
|
|
2219
2314
|
path: target
|
|
2220
2315
|
};
|
|
2221
2316
|
}
|
|
2222
|
-
async removeSupportFile(
|
|
2317
|
+
async removeSupportFile(rawName, filePath, origin = "foreground") {
|
|
2318
|
+
const name = rawName.trim();
|
|
2223
2319
|
const badName = this.badName(name);
|
|
2224
2320
|
if (badName) return {
|
|
2225
2321
|
ok: false,
|
|
2226
2322
|
message: badName
|
|
2227
2323
|
};
|
|
2228
|
-
const dir =
|
|
2324
|
+
const dir = this.dirOf(name);
|
|
2229
2325
|
if (!await this.io.exists(join(dir, "SKILL.md"))) return {
|
|
2230
2326
|
ok: false,
|
|
2231
2327
|
message: `Skill "${name}" not found.`
|
|
@@ -2266,7 +2362,7 @@ var SkillLibrary = class {
|
|
|
2266
2362
|
let dest = join(backupRoot, `skills-${stamp}`);
|
|
2267
2363
|
while (await this.io.exists(dest)) dest = join(backupRoot, `skills-${stamp}-${Math.random().toString(36).slice(2, 8)}`);
|
|
2268
2364
|
const names = await listNames(this.root, this.io);
|
|
2269
|
-
for (const name of names) await this.io.copy(
|
|
2365
|
+
for (const name of names) await this.io.copy(this.dirOf(name), join(dest, name));
|
|
2270
2366
|
const sidecars = [];
|
|
2271
2367
|
for (const sidecar of [usageFile(this.root), suppressedFile(this.root)]) if (await this.io.exists(sidecar)) {
|
|
2272
2368
|
const name = basename(sidecar);
|
|
@@ -2374,7 +2470,7 @@ var SkillLibrary = class {
|
|
|
2374
2470
|
message: "No skill snapshot available."
|
|
2375
2471
|
};
|
|
2376
2472
|
await this.snapshotAll("pre-rollback", extras);
|
|
2377
|
-
for (const name of await listNames(this.root, this.io)) await this.io.remove(
|
|
2473
|
+
for (const name of await listNames(this.root, this.io)) await this.io.remove(this.dirOf(name));
|
|
2378
2474
|
const manifest = await this.readSnapshotManifest(latest.path);
|
|
2379
2475
|
if (manifest === null) for (const entry of await this.io.list(latest.path)) {
|
|
2380
2476
|
if (entry === "manifest.json" || entry === "extras") continue;
|
|
@@ -2464,4 +2560,4 @@ var JsonState = class JsonState {
|
|
|
2464
2560
|
}
|
|
2465
2561
|
};
|
|
2466
2562
|
//#endregion
|
|
2467
|
-
export { COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, JsonState, LOW_QUALITY_THRESHOLD, MAX_DESCRIPTION_LENGTH, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MUTATIONS_FILE_VERSION, MemoryStore, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, SKILL_NAME_RE, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, advanceReview, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, computeDedupGroups, computeLifecycleTransitions, computeQualityScores, computeScopeView, contentHash, emptyRecord, evaluateThreat, evolutionHome, evolutionIoAdapter, foldTurn, getRecord, latestActivityAt, lifecycleCandidate, loadMutations, loadSuppressedNames, loadUsage, markAgentCreated, memoryRoot, mutationsFile, nodeEvolutionIo, observeEvent, parseCuratorNominations, parseFrontmatter, recordMutation, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, usageFile, validateFrontmatter, verifyPromptBundle };
|
|
2563
|
+
export { 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_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, JsonState, LOW_QUALITY_THRESHOLD, MAX_DESCRIPTION_LENGTH, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MUTATIONS_FILE_VERSION, MemoryStore, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, SKILL_NAME_RE, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, advanceReview, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, computeDedupGroups, computeLifecycleTransitions, computeQualityScores, computeScopeView, contentHash, emptyRecord, evaluateThreat, evolutionHome, evolutionIoAdapter, foldTurn, getRecord, latestActivityAt, lifecycleCandidate, loadMutations, loadSuppressedNames, loadUsage, markAgentCreated, memoryRoot, mutationsFile, nodeEvolutionIo, normalizeUsageRecord, observeEvent, parseCuratorNominations, parseFrontmatter, recordMutation, relatedSkillNames, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, usageFile, validateFrontmatter, verifyPromptBundle };
|
package/lib/types/constants.d.ts
CHANGED
|
@@ -47,5 +47,7 @@ export declare const DEFAULT_STALE_AFTER_DAYS = 30;
|
|
|
47
47
|
export declare const DEFAULT_ARCHIVE_AFTER_DAYS = 90;
|
|
48
48
|
export declare const DEFAULT_MEMORY_CHAR_LIMIT = 2200;
|
|
49
49
|
export declare const DEFAULT_USER_CHAR_LIMIT = 1375;
|
|
50
|
+
/** Consolidation-failure backoff cap, shared by MemoryStore and memory-files' Config default. */
|
|
51
|
+
export declare const DEFAULT_CONSOLIDATION_FAILURES = 3;
|
|
50
52
|
export declare const DEFAULT_SKILL_CONTENT_CHARS = 100000;
|
|
51
53
|
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -35,6 +35,7 @@ export declare class MemoryStore {
|
|
|
35
35
|
private readonly maxFailures;
|
|
36
36
|
private readonly io;
|
|
37
37
|
private failureCount;
|
|
38
|
+
private lastFailureAt;
|
|
38
39
|
constructor(options?: MemoryStoreOptions);
|
|
39
40
|
limitFor(target: MemoryTarget): number;
|
|
40
41
|
/**
|
|
@@ -83,6 +84,12 @@ export declare class MemoryStore {
|
|
|
83
84
|
* blank lines, leading/trailing delimiters) that indicate the file was
|
|
84
85
|
* edited outside MemoryStore. Purely single-canonical content reaches the
|
|
85
86
|
* same serialization and returns false, so a normal write is never flagged.
|
|
87
|
+
*
|
|
88
|
+
* An absent, empty, or whitespace-only file is the "never written" state
|
|
89
|
+
* (rc.42 audit P1-6): it parses to zero entries, so the canonical form
|
|
90
|
+
* `'\n'` can never byte-match it and every write path was permanently
|
|
91
|
+
* refused with "External drift detected" — including the repairs the model
|
|
92
|
+
* would need to make. Such files are adopted instead of flagged.
|
|
86
93
|
*/
|
|
87
94
|
detectDrift(target: MemoryTarget): Promise<boolean>;
|
|
88
95
|
}
|
|
@@ -69,6 +69,15 @@ export declare function parseFrontmatter(content: string): {
|
|
|
69
69
|
frontmatter: Frontmatter;
|
|
70
70
|
body: string;
|
|
71
71
|
} | null;
|
|
72
|
+
/**
|
|
73
|
+
* Skill names referenced by a SKILL.md's `related_skills` frontmatter
|
|
74
|
+
* (B-line G3, rc.44): the single parsing source for the quality references
|
|
75
|
+
* factor and the learning-graph edges. The DSH frontmatter parser keeps the
|
|
76
|
+
* YAML value as a string (`"[a, b]"`), so names are scanned out of it; each
|
|
77
|
+
* must satisfy the skill-name shape and the referencing skill itself is
|
|
78
|
+
* excluded. Pure and deduplicated.
|
|
79
|
+
*/
|
|
80
|
+
export declare function relatedSkillNames(content: string, exclude?: string): string[];
|
|
72
81
|
export declare function validateFrontmatter(content: string, expectedName?: string, limits?: SkillLimits): string | null;
|
|
73
82
|
export declare class SkillLibrary {
|
|
74
83
|
readonly root: string;
|
|
@@ -76,20 +85,32 @@ export declare class SkillLibrary {
|
|
|
76
85
|
private readonly io;
|
|
77
86
|
constructor(root?: string, io?: EvolutionIoLike, limits?: SkillLimits);
|
|
78
87
|
list(): Promise<SkillSummary[]>;
|
|
79
|
-
read(
|
|
88
|
+
read(rawName: string): Promise<string | null>;
|
|
89
|
+
/**
|
|
90
|
+
|
|
91
|
+
* Single path-building choke point (rc.42 audit P2-5): every directory path
|
|
92
|
+
|
|
93
|
+
* is built from the TRIMMED name, so a name that passes `badName` (which
|
|
94
|
+
|
|
95
|
+
* trims before validating) can never mint a second, whitespace-padded
|
|
96
|
+
|
|
97
|
+
* directory next to the real one. Callers keep passing raw user input.
|
|
98
|
+
|
|
99
|
+
*/
|
|
100
|
+
private dirOf;
|
|
80
101
|
/** Name-format guard shared by every path-building mutator/reader. */
|
|
81
102
|
private badName;
|
|
82
|
-
writeProtection(
|
|
83
|
-
deleteProtection(
|
|
103
|
+
writeProtection(rawName: string, origin?: WriteOrigin): Promise<string | null>;
|
|
104
|
+
deleteProtection(rawName: string, options?: {
|
|
84
105
|
allowBundled?: boolean;
|
|
85
106
|
}): Promise<string | null>;
|
|
86
|
-
isManaged(
|
|
107
|
+
isManaged(rawName: string): Promise<boolean>;
|
|
87
108
|
/** Whether the skill carries the bundled marker (curator prune-builtins eligibility). */
|
|
88
|
-
isBundled(
|
|
109
|
+
isBundled(rawName: string): Promise<boolean>;
|
|
89
110
|
/** Whether the skill carries the pinned marker (the marker is the factual source; usage.pinned mirrors it). */
|
|
90
|
-
isPinned(
|
|
111
|
+
isPinned(rawName: string): Promise<boolean>;
|
|
91
112
|
/** Count non-empty support subdirectories (richness input for quality scoring). */
|
|
92
|
-
countSupportDirs(
|
|
113
|
+
countSupportDirs(rawName: string): Promise<number>;
|
|
93
114
|
/** Best-effort audit trail entry; never blocks the mutation. */
|
|
94
115
|
private audit;
|
|
95
116
|
/** Recent mutation audit records (read-only inspection surface). */
|
|
@@ -102,9 +123,9 @@ export declare class SkillLibrary {
|
|
|
102
123
|
*/
|
|
103
124
|
setPinned(name: string, pinned: boolean, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
104
125
|
create(name: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
105
|
-
update(
|
|
106
|
-
patch(
|
|
107
|
-
archive(
|
|
126
|
+
update(rawName: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
127
|
+
patch(rawName: string, oldString: string, newString: string, filePath?: string, replaceAll?: boolean, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
128
|
+
archive(rawName: string, options?: ArchiveOptions): Promise<SkillActionResult>;
|
|
108
129
|
/**
|
|
109
130
|
* Merge the bodies of `sources` into `target` and archive the sources with
|
|
110
131
|
* an absorbed-into marker. Hermes-style consolidation: overlapping skills
|
|
@@ -116,9 +137,9 @@ export declare class SkillLibrary {
|
|
|
116
137
|
* recoverability: archival never deletes, and this is the control-plane
|
|
117
138
|
* path back. The `.archive-reason` marker is dropped on restore.
|
|
118
139
|
*/
|
|
119
|
-
restoreFromArchive(
|
|
120
|
-
writeSupportFile(
|
|
121
|
-
removeSupportFile(
|
|
140
|
+
restoreFromArchive(rawName: string): Promise<SkillActionResult>;
|
|
141
|
+
writeSupportFile(rawName: string, filePath: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
142
|
+
removeSupportFile(rawName: string, filePath: string, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
122
143
|
/**
|
|
123
144
|
* Snapshot the recoverable skills state: active tree, usage/suppression
|
|
124
145
|
* sidecars, `.archive/` and caller-supplied extras. `extras` are opaque
|
package/lib/types/usage.d.ts
CHANGED
|
@@ -16,12 +16,22 @@ export interface UsageRecord {
|
|
|
16
16
|
state: SkillState;
|
|
17
17
|
pinned: boolean;
|
|
18
18
|
archived_at: string | null;
|
|
19
|
-
quality_score?: number;
|
|
20
|
-
quality_warn?: boolean;
|
|
19
|
+
quality_score?: number | undefined;
|
|
20
|
+
quality_warn?: boolean | undefined;
|
|
21
21
|
}
|
|
22
22
|
export type UsageMap = Map<string, UsageRecord>;
|
|
23
23
|
export declare function usageFile(root: string): string;
|
|
24
24
|
export declare function emptyRecord(): UsageRecord;
|
|
25
|
+
/**
|
|
26
|
+
* Field-level normalization for one sidecar record (rc.42 audit P2-3): the
|
|
27
|
+
* spread used to copy any junk through verbatim, so a corrupted file could
|
|
28
|
+
* carry `use_count: "3"` into the quality math and lifecycle comparisons as
|
|
29
|
+
* NaN. Every field falls back to its `emptyRecord()` baseline unless it has
|
|
30
|
+
* exactly the declared type; an invalid `created_at` anchors the age clock at
|
|
31
|
+
* now (first-sight defer semantics for a record whose age is unknowable).
|
|
32
|
+
* Pure — exported for unit tests; `loadUsage` is the production caller.
|
|
33
|
+
*/
|
|
34
|
+
export declare function normalizeUsageRecord(record: unknown): UsageRecord;
|
|
25
35
|
export declare function loadUsage(root: string, io?: EvolutionIoLike): Promise<UsageMap>;
|
|
26
36
|
export declare function saveUsage(root: string, map: UsageMap, io?: EvolutionIoLike): Promise<void>;
|
|
27
37
|
export declare function getRecord(map: UsageMap, name: string): UsageRecord;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-core",
|
|
3
3
|
"description": "Shared stores, prompts, signals and lifecycle logic for the dsh-evolution plugin family (community build)",
|
|
4
|
-
"version": "0.1.0-rc.
|
|
4
|
+
"version": "0.1.0-rc.44",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|