@lmzhen/dsh-evolution-core 0.1.0-rc.11 → 0.1.0-rc.13
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 +156 -55
- package/lib/types/constants.d.ts +47 -0
- package/lib/types/curator.d.ts +7 -1
- package/lib/types/index.d.ts +1 -0
- package/lib/types/memory-store.d.ts +9 -1
- package/lib/types/skill-store.d.ts +23 -14
- package/lib/types/usage.d.ts +8 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { dirname, join } from "node:path";
|
|
1
|
+
import { basename, dirname, join } from "node:path";
|
|
2
2
|
import { cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { createHash, randomBytes } from "node:crypto";
|
|
4
4
|
import { homedir } from "node:os";
|
|
@@ -151,13 +151,87 @@ function latestActivityAt(record) {
|
|
|
151
151
|
if (values.length === 0) return null;
|
|
152
152
|
return values.sort().reverse()[0] ?? null;
|
|
153
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Curator suppression sidecar: built-in skills the curator has archived stay
|
|
156
|
+
* suppressed across re-seeds, so the lifecycle never fights a re-created
|
|
157
|
+
* bundled skill. Best-effort load/save, mirroring the usage sidecar posture.
|
|
158
|
+
*/
|
|
159
|
+
function suppressedFile(root) {
|
|
160
|
+
return join(root, ".curator-suppressed.json");
|
|
161
|
+
}
|
|
162
|
+
async function loadSuppressedNames(root, io = nodeEvolutionIo()) {
|
|
163
|
+
const raw = await io.readText(suppressedFile(root));
|
|
164
|
+
if (raw === null) return /* @__PURE__ */ new Set();
|
|
165
|
+
try {
|
|
166
|
+
const parsed = JSON.parse(raw);
|
|
167
|
+
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
|
|
168
|
+
return new Set(parsed.filter((entry) => typeof entry === "string"));
|
|
169
|
+
} catch {
|
|
170
|
+
return /* @__PURE__ */ new Set();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function saveSuppressedNames(root, names, io = nodeEvolutionIo()) {
|
|
174
|
+
await io.writeText(suppressedFile(root), JSON.stringify([...names].sort(), null, 2));
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region lib/types/constants.js
|
|
178
|
+
/**
|
|
179
|
+
* Shared constants for the dsh-evolution plugin family.
|
|
180
|
+
*
|
|
181
|
+
* Two classes of value live here, deliberately separated by section so future
|
|
182
|
+
* edits do not blur the semantic boundary:
|
|
183
|
+
*
|
|
184
|
+
* 1. **Fixed protocol/format/security invariants** — changing these breaks an
|
|
185
|
+
* on-disk format, a naming/format contract, a path-security boundary, or a
|
|
186
|
+
* cross-component invariant. They are NOT exposed as deployment config.
|
|
187
|
+
*
|
|
188
|
+
* 2. **Cross-package shared tunable defaults** — the same semantic default is
|
|
189
|
+
* read (with a config override path) by more than one package (e.g.
|
|
190
|
+
* `evolution-policy` and `evolution-curator` both default `staleAfterDays`
|
|
191
|
+
* to 30). Centralizing them here means one authoritative default: a config
|
|
192
|
+
* override still applies per package, but the fallback is single-sourced.
|
|
193
|
+
*
|
|
194
|
+
* Package-private tunables (used by exactly one package) stay in that package,
|
|
195
|
+
* not here — see evolution-replay's `DEFAULT_WEIGHTS` and evolution-feedback's
|
|
196
|
+
* threshold, which are intentionally left where they are used.
|
|
197
|
+
* @module @lmzhen/dsh-evolution-core
|
|
198
|
+
*/
|
|
199
|
+
/** Skill frontmatter `name` validated for the file name (lowercase + hyphen). */
|
|
200
|
+
const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
201
|
+
/** Allowed skill support-file subdirectories (path-traversal boundary). */
|
|
202
|
+
const SUPPORT_DIRS = [
|
|
203
|
+
"references",
|
|
204
|
+
"templates",
|
|
205
|
+
"scripts",
|
|
206
|
+
"assets"
|
|
207
|
+
];
|
|
208
|
+
/** Delimiter between durable memory entries (on-disk storage format). */
|
|
209
|
+
const ENTRY_DELIMITER = "\n§\n";
|
|
210
|
+
/** Built-in skill names the curator must never lifecycle-manage. */
|
|
211
|
+
const PROTECTED_BUILTIN_SKILLS = new Set(["plan"]);
|
|
212
|
+
const MAX_SKILL_NAME_LENGTH = 64;
|
|
213
|
+
const MAX_DESCRIPTION_LENGTH = 1024;
|
|
214
|
+
const MAX_SKILL_CONTENT_CHARS = 1e5;
|
|
215
|
+
const MAX_SKILL_FILE_BYTES = 1048576;
|
|
216
|
+
const DEFAULT_REVIEW_MEMORY_INTERVAL = 10;
|
|
217
|
+
const DEFAULT_REVIEW_SKILL_INTERVAL = 10;
|
|
218
|
+
const DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS = 3;
|
|
219
|
+
const DEFAULT_SUBSTANTIVE_MIN_USER_CHARS = 200;
|
|
220
|
+
const DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS = 500;
|
|
221
|
+
const DEFAULT_MAX_OPS_PER_PLAN = 32;
|
|
222
|
+
const DEFAULT_CURATOR_INTERVAL_HOURS = 168;
|
|
223
|
+
const DEFAULT_MIN_IDLE_HOURS = 2;
|
|
224
|
+
const DEFAULT_STALE_AFTER_DAYS = 30;
|
|
225
|
+
const DEFAULT_ARCHIVE_AFTER_DAYS = 90;
|
|
226
|
+
const DEFAULT_MEMORY_CHAR_LIMIT = 2200;
|
|
227
|
+
const DEFAULT_USER_CHAR_LIMIT = 1375;
|
|
228
|
+
const DEFAULT_SKILL_CONTENT_CHARS = 1e5;
|
|
154
229
|
//#endregion
|
|
155
230
|
//#region lib/types/curator.js
|
|
156
231
|
/**
|
|
157
232
|
* Deterministic skill curator: active → stale → archived transitions.
|
|
158
233
|
* Pure function; file moves are performed by SkillLibrary.
|
|
159
234
|
*/
|
|
160
|
-
const PROTECTED_BUILTIN_SKILLS = new Set(["plan"]);
|
|
161
235
|
function buildCuratorRunReport(input) {
|
|
162
236
|
return {
|
|
163
237
|
runId: input.runId,
|
|
@@ -184,7 +258,9 @@ function computeLifecycleTransitions(usage, config, now = /* @__PURE__ */ new Da
|
|
|
184
258
|
for (const [name, record] of usage) {
|
|
185
259
|
if (record.pinned) continue;
|
|
186
260
|
if (config.excludeSkillNames?.has(name)) continue;
|
|
187
|
-
if (
|
|
261
|
+
if (config.suppressedNames?.has(name)) continue;
|
|
262
|
+
const bundled = config.bundledNames?.has(name) === true;
|
|
263
|
+
if (!(record.created_by === "agent" || config.manageUnmanaged === true) && !(config.pruneBuiltins === true && bundled)) continue;
|
|
188
264
|
if (PROTECTED_BUILTIN_SKILLS.has(name)) continue;
|
|
189
265
|
if (record.state === "archived") continue;
|
|
190
266
|
const age = daysSince(null, record.created_at, now.getTime());
|
|
@@ -471,7 +547,6 @@ function scanContentThreats(text, maxScanChars = 65536, options = NO_SCAN_OPTION
|
|
|
471
547
|
* File-backed durable memory with Hermes-compatible semantics.
|
|
472
548
|
* Stores are MEMORY.md and USER.md under $DSH_HOME/memories (~/.dsh/memories).
|
|
473
549
|
*/
|
|
474
|
-
const ENTRY_DELIMITER = "\n§\n";
|
|
475
550
|
function memoryRoot(env = process.env) {
|
|
476
551
|
return join(env.DSH_HOME ?? join(homedir(), ".dsh"), "memories");
|
|
477
552
|
}
|
|
@@ -534,6 +609,30 @@ var MemoryStore = class {
|
|
|
534
609
|
limit: this.limitFor(target)
|
|
535
610
|
};
|
|
536
611
|
}
|
|
612
|
+
/** Percent-based storage hint appended to success message once the target is ≥80% full. */
|
|
613
|
+
storageHint(target, chars) {
|
|
614
|
+
const limit = this.limitFor(target);
|
|
615
|
+
if (limit <= 0) return "";
|
|
616
|
+
const percent = Math.floor(chars * 100 / limit);
|
|
617
|
+
return percent >= 80 ? ` ⚠️ Storage at ${percent}% (${chars}/${limit} chars).` : "";
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Best-effort copy of the drifted on-disk file to `<file>.bak.<stamp>` before
|
|
621
|
+
* refusing the write, so an external edit stays recoverable. Failure to back
|
|
622
|
+
* up does not change the refusal semantics.
|
|
623
|
+
*/
|
|
624
|
+
async backupDrift(target) {
|
|
625
|
+
const path = fileFor(this.root, target);
|
|
626
|
+
const raw = await this.io.readText(path);
|
|
627
|
+
if (raw === null) return null;
|
|
628
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
|
629
|
+
try {
|
|
630
|
+
await this.io.writeText(`${path}.bak.${stamp}`, raw);
|
|
631
|
+
return `${path}.bak.${stamp}`;
|
|
632
|
+
} catch {
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
537
636
|
async add(target, facts) {
|
|
538
637
|
const content = facts.trim();
|
|
539
638
|
if (!content) return {
|
|
@@ -556,7 +655,7 @@ var MemoryStore = class {
|
|
|
556
655
|
this.resetFailures();
|
|
557
656
|
return {
|
|
558
657
|
ok: true,
|
|
559
|
-
message:
|
|
658
|
+
message: `Entry already exists (no duplicate added).${this.storageHint(target, entries.join(ENTRY_DELIMITER).length)}`,
|
|
560
659
|
entries,
|
|
561
660
|
chars: entries.join(ENTRY_DELIMITER).length,
|
|
562
661
|
limit: this.limitFor(target)
|
|
@@ -569,7 +668,7 @@ var MemoryStore = class {
|
|
|
569
668
|
this.resetFailures();
|
|
570
669
|
return {
|
|
571
670
|
ok: true,
|
|
572
|
-
message:
|
|
671
|
+
message: `Entry added.${this.storageHint(target, total)}`,
|
|
573
672
|
entries: next,
|
|
574
673
|
chars: total,
|
|
575
674
|
limit: this.limitFor(target)
|
|
@@ -608,13 +707,16 @@ var MemoryStore = class {
|
|
|
608
707
|
limit: this.limitFor(target)
|
|
609
708
|
};
|
|
610
709
|
}
|
|
611
|
-
if (await this.detectDrift(target))
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
710
|
+
if (await this.detectDrift(target)) {
|
|
711
|
+
const backup = await this.backupDrift(target);
|
|
712
|
+
return {
|
|
713
|
+
ok: false,
|
|
714
|
+
message: `External drift detected in memory file.${backup ? ` A backup was saved to ${basename(backup)}.` : ""} Resolve the drift before retrying.`,
|
|
715
|
+
entries: [],
|
|
716
|
+
chars: 0,
|
|
717
|
+
limit: this.limitFor(target)
|
|
718
|
+
};
|
|
719
|
+
}
|
|
618
720
|
const entries = await this.read(target);
|
|
619
721
|
const matches = entries.map((entry, index) => ({
|
|
620
722
|
entry,
|
|
@@ -638,7 +740,7 @@ var MemoryStore = class {
|
|
|
638
740
|
this.resetFailures();
|
|
639
741
|
return {
|
|
640
742
|
ok: true,
|
|
641
|
-
message: `Entry ${action === "remove" ? "removed" : "replaced"}
|
|
743
|
+
message: `Entry ${action === "remove" ? "removed" : "replaced"}.${this.storageHint(target, total)}`,
|
|
642
744
|
entries: next,
|
|
643
745
|
chars: total,
|
|
644
746
|
limit: this.limitFor(target)
|
|
@@ -652,13 +754,16 @@ var MemoryStore = class {
|
|
|
652
754
|
chars: 0,
|
|
653
755
|
limit: this.limitFor(target)
|
|
654
756
|
};
|
|
655
|
-
if (await this.detectDrift(target))
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
757
|
+
if (await this.detectDrift(target)) {
|
|
758
|
+
const backup = await this.backupDrift(target);
|
|
759
|
+
return {
|
|
760
|
+
ok: false,
|
|
761
|
+
message: `External drift detected in memory file.${backup ? ` A backup was saved to ${basename(backup)}.` : ""} Resolve the drift before retrying.`,
|
|
762
|
+
entries: [],
|
|
763
|
+
chars: 0,
|
|
764
|
+
limit: this.limitFor(target)
|
|
765
|
+
};
|
|
766
|
+
}
|
|
662
767
|
const entries = await this.read(target);
|
|
663
768
|
const working = [...entries];
|
|
664
769
|
for (const [index, op] of operations.entries()) {
|
|
@@ -731,7 +836,7 @@ var MemoryStore = class {
|
|
|
731
836
|
this.resetFailures();
|
|
732
837
|
return {
|
|
733
838
|
ok: true,
|
|
734
|
-
message: `Applied ${operations.length} operation(s)
|
|
839
|
+
message: `Applied ${operations.length} operation(s).${this.storageHint(target, total)}`,
|
|
735
840
|
entries: working,
|
|
736
841
|
chars: total,
|
|
737
842
|
limit: this.limitFor(target)
|
|
@@ -1002,23 +1107,12 @@ function foldTurn(session, fromSeq) {
|
|
|
1002
1107
|
* it created unless a `.hermes-managed` marker opts a skill in. Archival is a
|
|
1003
1108
|
* move to `.archive/` — never a hard delete.
|
|
1004
1109
|
*/
|
|
1005
|
-
const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
1006
|
-
const MAX_SKILL_NAME_LENGTH = 64;
|
|
1007
|
-
const MAX_DESCRIPTION_LENGTH = 1024;
|
|
1008
|
-
const MAX_SKILL_CONTENT_CHARS = 1e5;
|
|
1009
|
-
const MAX_SKILL_FILE_BYTES = 1048576;
|
|
1010
1110
|
const DEFAULT_SKILL_LIMITS = {
|
|
1011
1111
|
maxNameLength: 64,
|
|
1012
1112
|
maxDescriptionLength: MAX_DESCRIPTION_LENGTH,
|
|
1013
1113
|
maxSkillContentChars: MAX_SKILL_CONTENT_CHARS,
|
|
1014
1114
|
maxSkillFileBytes: MAX_SKILL_FILE_BYTES
|
|
1015
1115
|
};
|
|
1016
|
-
const SUPPORT_DIRS = [
|
|
1017
|
-
"references",
|
|
1018
|
-
"templates",
|
|
1019
|
-
"scripts",
|
|
1020
|
-
"assets"
|
|
1021
|
-
];
|
|
1022
1116
|
function skillsRoot(env = process.env) {
|
|
1023
1117
|
return join(env.DSH_HOME ?? join(homedir(), ".dsh"), "skills");
|
|
1024
1118
|
}
|
|
@@ -1117,24 +1211,31 @@ var SkillLibrary = class {
|
|
|
1117
1211
|
async read(name) {
|
|
1118
1212
|
return this.io.readText(join(skillDir(this.root, name), "SKILL.md"));
|
|
1119
1213
|
}
|
|
1120
|
-
async writeProtection(name) {
|
|
1214
|
+
async writeProtection(name, origin = "foreground") {
|
|
1121
1215
|
const dir = skillDir(this.root, name);
|
|
1122
1216
|
for (const marker of ["bundled", "hub-installed"]) if (await this.io.exists(markerPath(dir, marker))) return marker;
|
|
1217
|
+
if (origin === "background_review" && await this.io.exists(markerPath(dir, "pinned"))) return "pinned";
|
|
1123
1218
|
return null;
|
|
1124
1219
|
}
|
|
1125
|
-
async deleteProtection(name) {
|
|
1220
|
+
async deleteProtection(name, options = {}) {
|
|
1126
1221
|
const dir = skillDir(this.root, name);
|
|
1127
|
-
|
|
1222
|
+
const markers = options.allowBundled ? ["hub-installed", "pinned"] : [
|
|
1128
1223
|
"bundled",
|
|
1129
1224
|
"hub-installed",
|
|
1130
1225
|
"pinned"
|
|
1131
|
-
]
|
|
1226
|
+
];
|
|
1227
|
+
for (const marker of markers) if (await this.io.exists(markerPath(dir, marker))) return marker;
|
|
1132
1228
|
return null;
|
|
1133
1229
|
}
|
|
1134
1230
|
async isManaged(name) {
|
|
1135
1231
|
const dir = skillDir(this.root, name);
|
|
1136
1232
|
return await this.io.exists(markerPath(dir, "hermes-managed"));
|
|
1137
1233
|
}
|
|
1234
|
+
/** Whether the skill carries the bundled marker (curator prune-builtins eligibility). */
|
|
1235
|
+
async isBundled(name) {
|
|
1236
|
+
const dir = skillDir(this.root, name);
|
|
1237
|
+
return await this.io.exists(markerPath(dir, "bundled"));
|
|
1238
|
+
}
|
|
1138
1239
|
async create(name, content, origin) {
|
|
1139
1240
|
const normalized = name.trim();
|
|
1140
1241
|
if (!SKILL_NAME_RE.test(normalized) || normalized.length > this.limits.maxNameLength) return {
|
|
@@ -1164,13 +1265,13 @@ var SkillLibrary = class {
|
|
|
1164
1265
|
path: dir
|
|
1165
1266
|
};
|
|
1166
1267
|
}
|
|
1167
|
-
async update(name, content) {
|
|
1268
|
+
async update(name, content, origin = "foreground") {
|
|
1168
1269
|
const dir = skillDir(this.root, name);
|
|
1169
1270
|
if (!await this.io.readText(join(dir, "SKILL.md"))) return {
|
|
1170
1271
|
ok: false,
|
|
1171
1272
|
message: `Skill "${name}" not found.`
|
|
1172
1273
|
};
|
|
1173
|
-
const protection = await this.writeProtection(name);
|
|
1274
|
+
const protection = await this.writeProtection(name, origin);
|
|
1174
1275
|
if (protection) return {
|
|
1175
1276
|
ok: false,
|
|
1176
1277
|
message: `Skill "${name}" is protected (${protection}).`
|
|
@@ -1192,14 +1293,14 @@ var SkillLibrary = class {
|
|
|
1192
1293
|
path: dir
|
|
1193
1294
|
};
|
|
1194
1295
|
}
|
|
1195
|
-
async patch(name, oldString, newString, filePath = "", replaceAll = false) {
|
|
1296
|
+
async patch(name, oldString, newString, filePath = "", replaceAll = false, origin = "foreground") {
|
|
1196
1297
|
const dir = skillDir(this.root, name);
|
|
1197
1298
|
const skillMd = join(dir, "SKILL.md");
|
|
1198
1299
|
if (!await this.io.exists(skillMd)) return {
|
|
1199
1300
|
ok: false,
|
|
1200
1301
|
message: `Skill "${name}" not found.`
|
|
1201
1302
|
};
|
|
1202
|
-
const protection = await this.writeProtection(name);
|
|
1303
|
+
const protection = await this.writeProtection(name, origin);
|
|
1203
1304
|
if (protection) return {
|
|
1204
1305
|
ok: false,
|
|
1205
1306
|
message: `Skill "${name}" is protected (${protection}).`
|
|
@@ -1252,21 +1353,21 @@ var SkillLibrary = class {
|
|
|
1252
1353
|
path: dir
|
|
1253
1354
|
};
|
|
1254
1355
|
}
|
|
1255
|
-
async archive(name,
|
|
1356
|
+
async archive(name, options = {}) {
|
|
1256
1357
|
const dir = skillDir(this.root, name);
|
|
1257
1358
|
if (!await this.io.readText(join(dir, "SKILL.md"))) return {
|
|
1258
1359
|
ok: false,
|
|
1259
1360
|
message: `Skill "${name}" not found.`
|
|
1260
1361
|
};
|
|
1261
|
-
const protection = await this.deleteProtection(name);
|
|
1362
|
+
const protection = await this.deleteProtection(name, options.allowBundled === void 0 ? {} : { allowBundled: options.allowBundled });
|
|
1262
1363
|
if (protection) return {
|
|
1263
1364
|
ok: false,
|
|
1264
1365
|
message: `Skill "${name}" is protected (${protection}).`
|
|
1265
1366
|
};
|
|
1266
|
-
if (absorbedInto) {
|
|
1267
|
-
if (!await this.io.readText(join(skillDir(this.root, absorbedInto), "SKILL.md"))) return {
|
|
1367
|
+
if (options.absorbedInto) {
|
|
1368
|
+
if (!await this.io.readText(join(skillDir(this.root, options.absorbedInto), "SKILL.md"))) return {
|
|
1268
1369
|
ok: false,
|
|
1269
|
-
message: `absorbed_into="${absorbedInto}" does not exist.`
|
|
1370
|
+
message: `absorbed_into="${options.absorbedInto}" does not exist.`
|
|
1270
1371
|
};
|
|
1271
1372
|
}
|
|
1272
1373
|
const archiveRoot = join(this.root, ".archive");
|
|
@@ -1278,7 +1379,7 @@ var SkillLibrary = class {
|
|
|
1278
1379
|
await this.io.copy(dir, dest);
|
|
1279
1380
|
await this.io.remove(dir);
|
|
1280
1381
|
}
|
|
1281
|
-
const reason = absorbedInto ? `Consolidated into ${absorbedInto}` : "Archived by self-evolution curator";
|
|
1382
|
+
const reason = options.reason ?? (options.absorbedInto ? `Consolidated into ${options.absorbedInto}` : "Archived by self-evolution curator");
|
|
1282
1383
|
await this.io.writeText(join(dest, ".archive-reason"), `${(/* @__PURE__ */ new Date()).toISOString()}: ${reason}\n`);
|
|
1283
1384
|
return {
|
|
1284
1385
|
ok: true,
|
|
@@ -1291,7 +1392,7 @@ var SkillLibrary = class {
|
|
|
1291
1392
|
* an absorbed-into marker. Hermes-style consolidation: overlapping skills
|
|
1292
1393
|
* collapse into one, and the originals stay recoverable under `.archive/`.
|
|
1293
1394
|
*/
|
|
1294
|
-
async consolidate(target, sources) {
|
|
1395
|
+
async consolidate(target, sources, origin = "foreground") {
|
|
1295
1396
|
const normalizedSources = [...new Set(sources)].filter((name) => name !== target);
|
|
1296
1397
|
if (normalizedSources.length === 0) return {
|
|
1297
1398
|
ok: false,
|
|
@@ -1307,7 +1408,7 @@ var SkillLibrary = class {
|
|
|
1307
1408
|
ok: false,
|
|
1308
1409
|
message: `Skill "${target}" not found.`
|
|
1309
1410
|
};
|
|
1310
|
-
const targetProtection = await this.writeProtection(target);
|
|
1411
|
+
const targetProtection = await this.writeProtection(target, origin);
|
|
1311
1412
|
if (targetProtection) return {
|
|
1312
1413
|
ok: false,
|
|
1313
1414
|
message: `Skill "${target}" is protected (${targetProtection}).`
|
|
@@ -1345,7 +1446,7 @@ var SkillLibrary = class {
|
|
|
1345
1446
|
const archived = [];
|
|
1346
1447
|
try {
|
|
1347
1448
|
for (const source of normalizedSources) {
|
|
1348
|
-
const result = await this.archive(source, target);
|
|
1449
|
+
const result = await this.archive(source, { absorbedInto: target });
|
|
1349
1450
|
if (!result.ok) return result;
|
|
1350
1451
|
archived.push(source);
|
|
1351
1452
|
}
|
|
@@ -1408,13 +1509,13 @@ var SkillLibrary = class {
|
|
|
1408
1509
|
path: dest
|
|
1409
1510
|
};
|
|
1410
1511
|
}
|
|
1411
|
-
async writeSupportFile(name, filePath, content) {
|
|
1512
|
+
async writeSupportFile(name, filePath, content, origin = "foreground") {
|
|
1412
1513
|
const dir = skillDir(this.root, name);
|
|
1413
1514
|
if (!await this.io.exists(join(dir, "SKILL.md"))) return {
|
|
1414
1515
|
ok: false,
|
|
1415
1516
|
message: `Skill "${name}" not found.`
|
|
1416
1517
|
};
|
|
1417
|
-
const protection = await this.writeProtection(name);
|
|
1518
|
+
const protection = await this.writeProtection(name, origin);
|
|
1418
1519
|
if (protection) return {
|
|
1419
1520
|
ok: false,
|
|
1420
1521
|
message: `Skill "${name}" is protected (${protection}).`
|
|
@@ -1441,13 +1542,13 @@ var SkillLibrary = class {
|
|
|
1441
1542
|
path: target
|
|
1442
1543
|
};
|
|
1443
1544
|
}
|
|
1444
|
-
async removeSupportFile(name, filePath) {
|
|
1545
|
+
async removeSupportFile(name, filePath, origin = "foreground") {
|
|
1445
1546
|
const dir = skillDir(this.root, name);
|
|
1446
1547
|
if (!await this.io.exists(join(dir, "SKILL.md"))) return {
|
|
1447
1548
|
ok: false,
|
|
1448
1549
|
message: `Skill "${name}" not found.`
|
|
1449
1550
|
};
|
|
1450
|
-
const protection = await this.writeProtection(name);
|
|
1551
|
+
const protection = await this.writeProtection(name, origin);
|
|
1451
1552
|
if (protection) return {
|
|
1452
1553
|
ok: false,
|
|
1453
1554
|
message: `Skill "${name}" is protected (${protection}).`
|
|
@@ -1590,4 +1691,4 @@ var JsonState = class JsonState {
|
|
|
1590
1691
|
}
|
|
1591
1692
|
};
|
|
1592
1693
|
//#endregion
|
|
1593
|
-
export { COMBINED_REVIEW_PROMPT, CURATOR_PROMPT, DEFAULT_SKILL_LIMITS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, JsonState, MAX_DESCRIPTION_LENGTH, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MemoryStore, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROTECTED_BUILTIN_SKILLS, SKILL_NAME_RE, SKILL_REVIEW_PROMPT, SUPPORT_DIRS, SkillLibrary, advanceReview, buildCuratorRunReport, bumpPatch, bumpUse, bumpView, computeLifecycleTransitions, emptyRecord, evaluateThreat, evolutionHome, evolutionIoAdapter, foldTurn, getRecord, latestActivityAt, loadUsage, markAgentCreated, memoryRoot, nodeEvolutionIo, observeEvent, parseFrontmatter, reviewPrompt, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, usageFile, validateFrontmatter, verifyPromptBundle };
|
|
1694
|
+
export { COMBINED_REVIEW_PROMPT, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, 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, MAX_DESCRIPTION_LENGTH, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MemoryStore, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROTECTED_BUILTIN_SKILLS, SKILL_NAME_RE, SKILL_REVIEW_PROMPT, SUPPORT_DIRS, SkillLibrary, advanceReview, buildCuratorRunReport, bumpPatch, bumpUse, bumpView, computeLifecycleTransitions, emptyRecord, evaluateThreat, evolutionHome, evolutionIoAdapter, foldTurn, getRecord, latestActivityAt, loadSuppressedNames, loadUsage, markAgentCreated, memoryRoot, nodeEvolutionIo, observeEvent, parseFrontmatter, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, usageFile, validateFrontmatter, verifyPromptBundle };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants for the dsh-evolution plugin family.
|
|
3
|
+
*
|
|
4
|
+
* Two classes of value live here, deliberately separated by section so future
|
|
5
|
+
* edits do not blur the semantic boundary:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Fixed protocol/format/security invariants** — changing these breaks an
|
|
8
|
+
* on-disk format, a naming/format contract, a path-security boundary, or a
|
|
9
|
+
* cross-component invariant. They are NOT exposed as deployment config.
|
|
10
|
+
*
|
|
11
|
+
* 2. **Cross-package shared tunable defaults** — the same semantic default is
|
|
12
|
+
* read (with a config override path) by more than one package (e.g.
|
|
13
|
+
* `evolution-policy` and `evolution-curator` both default `staleAfterDays`
|
|
14
|
+
* to 30). Centralizing them here means one authoritative default: a config
|
|
15
|
+
* override still applies per package, but the fallback is single-sourced.
|
|
16
|
+
*
|
|
17
|
+
* Package-private tunables (used by exactly one package) stay in that package,
|
|
18
|
+
* not here — see evolution-replay's `DEFAULT_WEIGHTS` and evolution-feedback's
|
|
19
|
+
* threshold, which are intentionally left where they are used.
|
|
20
|
+
* @module @deepseek-ai/dsh-evolution-core
|
|
21
|
+
*/
|
|
22
|
+
/** Skill frontmatter `name` validated for the file name (lowercase + hyphen). */
|
|
23
|
+
export declare const SKILL_NAME_RE: RegExp;
|
|
24
|
+
/** Allowed skill support-file subdirectories (path-traversal boundary). */
|
|
25
|
+
export declare const SUPPORT_DIRS: readonly ["references", "templates", "scripts", "assets"];
|
|
26
|
+
/** Delimiter between durable memory entries (on-disk storage format). */
|
|
27
|
+
export declare const ENTRY_DELIMITER = "\n\u00A7\n";
|
|
28
|
+
/** Built-in skill names the curator must never lifecycle-manage. */
|
|
29
|
+
export declare const PROTECTED_BUILTIN_SKILLS: ReadonlySet<string>;
|
|
30
|
+
export declare const MAX_SKILL_NAME_LENGTH = 64;
|
|
31
|
+
export declare const MAX_DESCRIPTION_LENGTH = 1024;
|
|
32
|
+
export declare const MAX_SKILL_CONTENT_CHARS = 100000;
|
|
33
|
+
export declare const MAX_SKILL_FILE_BYTES = 1048576;
|
|
34
|
+
export declare const DEFAULT_REVIEW_MEMORY_INTERVAL = 10;
|
|
35
|
+
export declare const DEFAULT_REVIEW_SKILL_INTERVAL = 10;
|
|
36
|
+
export declare const DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS = 3;
|
|
37
|
+
export declare const DEFAULT_SUBSTANTIVE_MIN_USER_CHARS = 200;
|
|
38
|
+
export declare const DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS = 500;
|
|
39
|
+
export declare const DEFAULT_MAX_OPS_PER_PLAN = 32;
|
|
40
|
+
export declare const DEFAULT_CURATOR_INTERVAL_HOURS = 168;
|
|
41
|
+
export declare const DEFAULT_MIN_IDLE_HOURS = 2;
|
|
42
|
+
export declare const DEFAULT_STALE_AFTER_DAYS = 30;
|
|
43
|
+
export declare const DEFAULT_ARCHIVE_AFTER_DAYS = 90;
|
|
44
|
+
export declare const DEFAULT_MEMORY_CHAR_LIMIT = 2200;
|
|
45
|
+
export declare const DEFAULT_USER_CHAR_LIMIT = 1375;
|
|
46
|
+
export declare const DEFAULT_SKILL_CONTENT_CHARS = 100000;
|
|
47
|
+
//# sourceMappingURL=constants.d.ts.map
|
package/lib/types/curator.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Pure function; file moves are performed by SkillLibrary.
|
|
4
4
|
*/
|
|
5
5
|
import type { UsageMap } from './usage.ts';
|
|
6
|
+
export { PROTECTED_BUILTIN_SKILLS } from './constants.ts';
|
|
6
7
|
export interface CuratorConfig {
|
|
7
8
|
staleAfterDays: number;
|
|
8
9
|
archiveAfterDays: number;
|
|
@@ -12,6 +13,12 @@ export interface CuratorConfig {
|
|
|
12
13
|
excludeSkillNames?: ReadonlySet<string>;
|
|
13
14
|
/** When true, usage records without created_by='agent' also enter the lifecycle. */
|
|
14
15
|
manageUnmanaged?: boolean;
|
|
16
|
+
/** When true, bundled skills (in `bundledNames`) are curation candidates like agent-created ones. */
|
|
17
|
+
pruneBuiltins?: boolean;
|
|
18
|
+
/** Skill names carrying the bundled marker; only read when `pruneBuiltins` is true. */
|
|
19
|
+
bundledNames?: ReadonlySet<string>;
|
|
20
|
+
/** Skill names the curator archived once and must not fight across re-seeds. */
|
|
21
|
+
suppressedNames?: ReadonlySet<string>;
|
|
15
22
|
}
|
|
16
23
|
export interface CuratorTransition {
|
|
17
24
|
name: string;
|
|
@@ -25,7 +32,6 @@ export interface CuratorResult {
|
|
|
25
32
|
reactivate: string[];
|
|
26
33
|
markStale: string[];
|
|
27
34
|
}
|
|
28
|
-
export declare const PROTECTED_BUILTIN_SKILLS: ReadonlySet<string>;
|
|
29
35
|
export interface CuratorArchivedSkill {
|
|
30
36
|
name: string;
|
|
31
37
|
path: string;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Stores are MEMORY.md and USER.md under $DSH_HOME/memories (~/.dsh/memories).
|
|
4
4
|
*/
|
|
5
5
|
import { type EvolutionIoLike } from './io.ts';
|
|
6
|
-
export
|
|
6
|
+
export { ENTRY_DELIMITER } from './constants.ts';
|
|
7
7
|
export type MemoryTarget = 'memory' | 'user';
|
|
8
8
|
export interface MemoryOperation {
|
|
9
9
|
action: 'add' | 'replace' | 'remove';
|
|
@@ -41,6 +41,14 @@ export declare class MemoryStore {
|
|
|
41
41
|
write(target: MemoryTarget, entries: string[]): Promise<void>;
|
|
42
42
|
resetFailures(): void;
|
|
43
43
|
private failure;
|
|
44
|
+
/** Percent-based storage hint appended to success message once the target is ≥80% full. */
|
|
45
|
+
private storageHint;
|
|
46
|
+
/**
|
|
47
|
+
* Best-effort copy of the drifted on-disk file to `<file>.bak.<stamp>` before
|
|
48
|
+
* refusing the write, so an external edit stays recoverable. Failure to back
|
|
49
|
+
* up does not change the refusal semantics.
|
|
50
|
+
*/
|
|
51
|
+
private backupDrift;
|
|
44
52
|
add(target: MemoryTarget, facts: string): Promise<MemoryApplyResult>;
|
|
45
53
|
replace(target: MemoryTarget, oldText: string, facts: string): Promise<MemoryApplyResult>;
|
|
46
54
|
remove(target: MemoryTarget, oldText: string): Promise<MemoryApplyResult>;
|
|
@@ -7,11 +7,6 @@
|
|
|
7
7
|
* move to `.archive/` — never a hard delete.
|
|
8
8
|
*/
|
|
9
9
|
import { type EvolutionIoLike } from './io.ts';
|
|
10
|
-
export declare const SKILL_NAME_RE: RegExp;
|
|
11
|
-
export declare const MAX_SKILL_NAME_LENGTH = 64;
|
|
12
|
-
export declare const MAX_DESCRIPTION_LENGTH = 1024;
|
|
13
|
-
export declare const MAX_SKILL_CONTENT_CHARS = 100000;
|
|
14
|
-
export declare const MAX_SKILL_FILE_BYTES = 1048576;
|
|
15
10
|
export interface SkillLimits {
|
|
16
11
|
maxNameLength: number;
|
|
17
12
|
maxDescriptionLength: number;
|
|
@@ -19,7 +14,6 @@ export interface SkillLimits {
|
|
|
19
14
|
maxSkillFileBytes: number;
|
|
20
15
|
}
|
|
21
16
|
export declare const DEFAULT_SKILL_LIMITS: SkillLimits;
|
|
22
|
-
export declare const SUPPORT_DIRS: readonly ["references", "templates", "scripts", "assets"];
|
|
23
17
|
export interface SkillSummary {
|
|
24
18
|
name: string;
|
|
25
19
|
description: string;
|
|
@@ -33,6 +27,17 @@ export interface SkillActionResult {
|
|
|
33
27
|
message: string;
|
|
34
28
|
path?: string;
|
|
35
29
|
}
|
|
30
|
+
/** Who is writing: a foreground user-directed tool call, or the autonomous review/curator pipeline. */
|
|
31
|
+
export type WriteOrigin = 'foreground' | 'background_review';
|
|
32
|
+
/** Options for `SkillLibrary.archive`. The absorbed-into name and the archival reason are distinct fields. */
|
|
33
|
+
export interface ArchiveOptions {
|
|
34
|
+
/** Umbrella skill this one was consolidated into; when set it must exist (consolidate semantics). */
|
|
35
|
+
absorbedInto?: string;
|
|
36
|
+
/** Human-readable reason written to `.archive-reason`; default derives from `absorbedInto`. */
|
|
37
|
+
reason?: string;
|
|
38
|
+
/** Permit archiving a bundled skill (curator prune-builtins only; hub-installed and pinned stay protected). */
|
|
39
|
+
allowBundled?: boolean;
|
|
40
|
+
}
|
|
36
41
|
export declare function skillsRoot(env?: NodeJS.ProcessEnv): string;
|
|
37
42
|
export interface Frontmatter {
|
|
38
43
|
name?: string;
|
|
@@ -51,27 +56,31 @@ export declare class SkillLibrary {
|
|
|
51
56
|
constructor(root?: string, io?: EvolutionIoLike, limits?: SkillLimits);
|
|
52
57
|
list(): Promise<SkillSummary[]>;
|
|
53
58
|
read(name: string): Promise<string | null>;
|
|
54
|
-
writeProtection(name: string): Promise<string | null>;
|
|
55
|
-
deleteProtection(name: string
|
|
59
|
+
writeProtection(name: string, origin?: WriteOrigin): Promise<string | null>;
|
|
60
|
+
deleteProtection(name: string, options?: {
|
|
61
|
+
allowBundled?: boolean;
|
|
62
|
+
}): Promise<string | null>;
|
|
56
63
|
isManaged(name: string): Promise<boolean>;
|
|
64
|
+
/** Whether the skill carries the bundled marker (curator prune-builtins eligibility). */
|
|
65
|
+
isBundled(name: string): Promise<boolean>;
|
|
57
66
|
create(name: string, content: string, origin: 'foreground' | 'background_review'): Promise<SkillActionResult>;
|
|
58
|
-
update(name: string, content: string): Promise<SkillActionResult>;
|
|
59
|
-
patch(name: string, oldString: string, newString: string, filePath?: string, replaceAll?: boolean): Promise<SkillActionResult>;
|
|
60
|
-
archive(name: string,
|
|
67
|
+
update(name: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
68
|
+
patch(name: string, oldString: string, newString: string, filePath?: string, replaceAll?: boolean, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
69
|
+
archive(name: string, options?: ArchiveOptions): Promise<SkillActionResult>;
|
|
61
70
|
/**
|
|
62
71
|
* Merge the bodies of `sources` into `target` and archive the sources with
|
|
63
72
|
* an absorbed-into marker. Hermes-style consolidation: overlapping skills
|
|
64
73
|
* collapse into one, and the originals stay recoverable under `.archive/`.
|
|
65
74
|
*/
|
|
66
|
-
consolidate(target: string, sources: string[]): Promise<SkillActionResult>;
|
|
75
|
+
consolidate(target: string, sources: string[], origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
67
76
|
/**
|
|
68
77
|
* Restore one skill from `.archive/` back to the active root. Hermes-style
|
|
69
78
|
* recoverability: archival never deletes, and this is the control-plane
|
|
70
79
|
* path back. The `.archive-reason` marker is dropped on restore.
|
|
71
80
|
*/
|
|
72
81
|
restoreFromArchive(name: string): Promise<SkillActionResult>;
|
|
73
|
-
writeSupportFile(name: string, filePath: string, content: string): Promise<SkillActionResult>;
|
|
74
|
-
removeSupportFile(name: string, filePath: string): Promise<SkillActionResult>;
|
|
82
|
+
writeSupportFile(name: string, filePath: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
83
|
+
removeSupportFile(name: string, filePath: string, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
75
84
|
snapshotAll(reason?: string): Promise<string>;
|
|
76
85
|
listSnapshots(): Promise<Array<{
|
|
77
86
|
path: string;
|
package/lib/types/usage.d.ts
CHANGED
|
@@ -30,4 +30,12 @@ export declare function bumpUse(map: UsageMap, name: string, when?: Date): void;
|
|
|
30
30
|
export declare function bumpPatch(map: UsageMap, name: string, when?: Date): void;
|
|
31
31
|
export declare function markAgentCreated(map: UsageMap, name: string): void;
|
|
32
32
|
export declare function latestActivityAt(record: UsageRecord): string | null;
|
|
33
|
+
/**
|
|
34
|
+
* Curator suppression sidecar: built-in skills the curator has archived stay
|
|
35
|
+
* suppressed across re-seeds, so the lifecycle never fights a re-created
|
|
36
|
+
* bundled skill. Best-effort load/save, mirroring the usage sidecar posture.
|
|
37
|
+
*/
|
|
38
|
+
export declare function suppressedFile(root: string): string;
|
|
39
|
+
export declare function loadSuppressedNames(root: string, io?: EvolutionIoLike): Promise<ReadonlySet<string>>;
|
|
40
|
+
export declare function saveSuppressedNames(root: string, names: ReadonlySet<string>, io?: EvolutionIoLike): Promise<void>;
|
|
33
41
|
//# sourceMappingURL=usage.d.ts.map
|
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.13",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|