@lmzhen/dsh-evolution-curator 0.1.0-rc.2 → 0.1.0-rc.20
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/README.md +8 -1
- package/lib/index.js +235 -50
- package/lib/types/index.d.ts +72 -13
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -21,4 +21,11 @@ Independent of request-prefix construction. This package does not alter the asse
|
|
|
21
21
|
## Known Limitations and Deferred Work
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
-
|
|
24
|
+
- LLM nomination pass is advisory and disabled by default; deterministic lifecycle remains authoritative.
|
|
25
|
+
- Consolidation is control-plane only (`/evolution consolidate <target> <source...>`): no LLM pass proposes merge groups yet, and merged source bodies are appended verbatim rather than rewritten into a synthesized skill.
|
|
26
|
+
|
|
27
|
+
## Recovery and consolidation
|
|
28
|
+
|
|
29
|
+
- `archive` never deletes: skills move to `.archive/` with a `.archive-reason` marker.
|
|
30
|
+
- `restore(name)` (service) / `/evolution skill restore <name>` brings one archived skill back to the active root and resets its usage state.
|
|
31
|
+
- `consolidate(target, sources)` (service) / `/evolution consolidate` merges source bodies into the target, archives the sources with an absorbed-into marker, and folds their usage records into `archived` state. Both operations snapshot the skill tree first (`pre-consolidate` / `pre-restore`).
|
package/lib/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
5
5
|
import z from "@deepseek-ai/schemastery";
|
|
6
|
-
import { CURATOR_PROMPT, SkillLibrary, buildCuratorRunReport, computeLifecycleTransitions, evolutionHome, evolutionIoAdapter, loadUsage, saveUsage } from "@lmzhen/dsh-evolution-core";
|
|
6
|
+
import { CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_MIN_IDLE_HOURS, DEFAULT_STALE_AFTER_DAYS, SkillLibrary, buildCuratorRunReport, computeLifecycleTransitions, computeQualityScores, emptyRecord, evolutionHome, evolutionIoAdapter, loadSuppressedNames, loadUsage, parseCuratorNominations, saveSuppressedNames, saveUsage } from "@lmzhen/dsh-evolution-core";
|
|
7
7
|
//#region lib/types/index.js
|
|
8
8
|
/**
|
|
9
9
|
* Deterministic skill lifecycle curator with interval gate and archive.
|
|
@@ -13,15 +13,18 @@ var EvolutionCurator = class extends Service {
|
|
|
13
13
|
static inject = ["evolutionIo"];
|
|
14
14
|
static Config = z.object({
|
|
15
15
|
enabled: z.boolean().default(true),
|
|
16
|
-
intervalHours: z.number().default(
|
|
17
|
-
staleAfterDays: z.number().default(
|
|
18
|
-
archiveAfterDays: z.number().default(
|
|
16
|
+
intervalHours: z.number().default(DEFAULT_CURATOR_INTERVAL_HOURS),
|
|
17
|
+
staleAfterDays: z.number().default(DEFAULT_STALE_AFTER_DAYS),
|
|
18
|
+
archiveAfterDays: z.number().default(DEFAULT_ARCHIVE_AFTER_DAYS),
|
|
19
19
|
llmReview: z.boolean().default(false),
|
|
20
20
|
curatorProvider: z.string().default("deepseek-official"),
|
|
21
21
|
qualityWarnStaleAfterDays: z.number().default(7),
|
|
22
|
-
minIdleHours: z.number().default(
|
|
22
|
+
minIdleHours: z.number().default(DEFAULT_MIN_IDLE_HOURS),
|
|
23
23
|
excludeSkillNames: z.array(z.string()).default([]),
|
|
24
24
|
manageUnmanaged: z.boolean().default(false),
|
|
25
|
+
pruneBuiltins: z.boolean().default(false),
|
|
26
|
+
referencedSkillNames: z.array(z.string()).default([]),
|
|
27
|
+
autoStart: z.boolean().default(true),
|
|
25
28
|
curatorReviewMaxTokens: z.number().default(2048)
|
|
26
29
|
});
|
|
27
30
|
skills;
|
|
@@ -36,6 +39,8 @@ var EvolutionCurator = class extends Service {
|
|
|
36
39
|
minIdleHours;
|
|
37
40
|
excludeSkillNames;
|
|
38
41
|
manageUnmanaged;
|
|
42
|
+
pruneBuiltins;
|
|
43
|
+
referencedSkillNames;
|
|
39
44
|
curatorReviewMaxTokens;
|
|
40
45
|
lastRun = 0;
|
|
41
46
|
timer;
|
|
@@ -50,9 +55,11 @@ var EvolutionCurator = class extends Service {
|
|
|
50
55
|
this.llmReview = config.llmReview ?? false;
|
|
51
56
|
this.curatorProvider = config.curatorProvider ?? "deepseek-official";
|
|
52
57
|
this.qualityWarnStaleAfterDays = config.qualityWarnStaleAfterDays ?? 7;
|
|
53
|
-
this.minIdleHours = config.minIdleHours ??
|
|
58
|
+
this.minIdleHours = config.minIdleHours ?? DEFAULT_MIN_IDLE_HOURS;
|
|
54
59
|
this.excludeSkillNames = new Set(config.excludeSkillNames ?? []);
|
|
55
60
|
this.manageUnmanaged = config.manageUnmanaged ?? false;
|
|
61
|
+
this.pruneBuiltins = config.pruneBuiltins ?? false;
|
|
62
|
+
this.referencedSkillNames = new Set(config.referencedSkillNames ?? []);
|
|
56
63
|
this.curatorReviewMaxTokens = config.curatorReviewMaxTokens ?? 2048;
|
|
57
64
|
this.lastRun = Date.now();
|
|
58
65
|
this.ctx.effect(() => {
|
|
@@ -60,6 +67,7 @@ var EvolutionCurator = class extends Service {
|
|
|
60
67
|
this.stop();
|
|
61
68
|
};
|
|
62
69
|
}, "evolution-curator.stop");
|
|
70
|
+
if (config.autoStart ?? true) this.start();
|
|
63
71
|
}
|
|
64
72
|
lifecycle() {
|
|
65
73
|
const snapshot = this.ctx.get("evolutionPolicy")?.get();
|
|
@@ -81,23 +89,28 @@ var EvolutionCurator = class extends Service {
|
|
|
81
89
|
this.timer = void 0;
|
|
82
90
|
}
|
|
83
91
|
/**
|
|
84
|
-
* Optional Hermes-curator LLM pass. The model may only NOMINATE pruning
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
92
|
+
* Optional Hermes-curator LLM pass. The model may only NOMINATE pruning and
|
|
93
|
+
* consolidation; every move stays a control-plane operation and each
|
|
94
|
+
* nomination is re-validated against the tree and protected markers before
|
|
95
|
+
* any file move. `dryRun` prepends the report-only banner.
|
|
88
96
|
*/
|
|
89
|
-
async recommend(candidates) {
|
|
90
|
-
|
|
97
|
+
async recommend(candidates, options = {}) {
|
|
98
|
+
const empty = {
|
|
99
|
+
prunings: [],
|
|
100
|
+
consolidations: []
|
|
101
|
+
};
|
|
102
|
+
if (candidates.length === 0) return empty;
|
|
91
103
|
const llm = this.ctx.get("llm");
|
|
92
|
-
if (!llm) return
|
|
104
|
+
if (!llm) return empty;
|
|
93
105
|
const model = this.ctx.get("evolutionPolicy")?.get().curatorModel ?? "deepseek-v4-pro";
|
|
94
106
|
const prompt = [
|
|
107
|
+
options.dryRun ? CURATOR_DRY_RUN_BANNER : "",
|
|
95
108
|
CURATOR_PROMPT,
|
|
96
109
|
"",
|
|
97
|
-
|
|
110
|
+
`Stale candidates observed by the deterministic lifecycle scanner:${candidates.length === 0 ? " (none)" : ""}`,
|
|
98
111
|
...candidates.map((name) => `- ${name}`),
|
|
99
112
|
"",
|
|
100
|
-
"Return a YAML summary with
|
|
113
|
+
"Return a YAML summary with consolidations and prunings lists. Nominate only actions whose archival/merge is clearly safe."
|
|
101
114
|
].join("\n");
|
|
102
115
|
try {
|
|
103
116
|
const assembler = new BlockAssembler();
|
|
@@ -119,13 +132,13 @@ var EvolutionCurator = class extends Service {
|
|
|
119
132
|
maxTokens: this.curatorReviewMaxTokens,
|
|
120
133
|
purpose: "evolution-curator"
|
|
121
134
|
})) assembler.push(chunk);
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
135
|
+
const parsed = parseCuratorNominations(assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("\n"));
|
|
136
|
+
return {
|
|
137
|
+
prunings: parsed.prunings.filter((name) => candidates.includes(name)),
|
|
138
|
+
consolidations: parsed.consolidations
|
|
139
|
+
};
|
|
127
140
|
} catch {
|
|
128
|
-
return
|
|
141
|
+
return empty;
|
|
129
142
|
}
|
|
130
143
|
}
|
|
131
144
|
skippedReport(runId, startedAt) {
|
|
@@ -140,20 +153,27 @@ var EvolutionCurator = class extends Service {
|
|
|
140
153
|
failed: []
|
|
141
154
|
});
|
|
142
155
|
}
|
|
143
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Run one curator pass. `ignoreGates` skips the interval and idle gates so an
|
|
158
|
+
* explicit `/evolution curator run` always executes (manual-run semantics):
|
|
159
|
+
* `dryRun` computes the lifecycle and the LLM nominations but performs no
|
|
160
|
+
* mutation, reports what WOULD happen, and does not push out the next run.
|
|
161
|
+
*/
|
|
162
|
+
async run(options = {}) {
|
|
163
|
+
const { ignoreGates = false, dryRun = false } = options;
|
|
144
164
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
145
165
|
const runId = randomUUID();
|
|
146
166
|
const stateService = this.ctx.get("evolutionState");
|
|
147
167
|
const lifecycle = this.lifecycle();
|
|
148
168
|
const persisted = await stateService?.loadCuratorState();
|
|
149
|
-
if (persisted && Date.now() - persisted.lastRunAt < lifecycle.intervalHours * 36e5) return {
|
|
169
|
+
if (!ignoreGates && persisted && Date.now() - persisted.lastRunAt < lifecycle.intervalHours * 36e5) return {
|
|
150
170
|
stale: [],
|
|
151
171
|
archived: [],
|
|
152
172
|
errors: [],
|
|
153
173
|
report: this.skippedReport(runId, startedAt),
|
|
154
174
|
skipped: "interval"
|
|
155
175
|
};
|
|
156
|
-
if (this.minIdleHours > 0 && this.recentSessionActive()) return {
|
|
176
|
+
if (!ignoreGates && this.minIdleHours > 0 && this.recentSessionActive()) return {
|
|
157
177
|
stale: [],
|
|
158
178
|
archived: [],
|
|
159
179
|
errors: [],
|
|
@@ -161,33 +181,40 @@ var EvolutionCurator = class extends Service {
|
|
|
161
181
|
skipped: "active-session"
|
|
162
182
|
};
|
|
163
183
|
const root = this.skills.root;
|
|
164
|
-
const
|
|
165
|
-
const usage =
|
|
184
|
+
const rawUsage = await loadUsage(root, this.io);
|
|
185
|
+
const usage = dryRun ? new Map([...rawUsage].map(([name, record]) => [name, { ...record }])) : rawUsage;
|
|
186
|
+
const snapshotPath = dryRun ? void 0 : await this.skills.snapshotAll("pre-curator-run");
|
|
187
|
+
const suppressedNames = new Set(await loadSuppressedNames(root, this.io));
|
|
188
|
+
const { bundledNames, treeNames } = await this.seedBaseline(usage);
|
|
166
189
|
const result = computeLifecycleTransitions(usage, {
|
|
167
190
|
staleAfterDays: lifecycle.staleAfterDays,
|
|
168
191
|
archiveAfterDays: lifecycle.archiveAfterDays,
|
|
169
192
|
qualityWarnStaleAfterDays: this.qualityWarnStaleAfterDays,
|
|
170
193
|
excludeSkillNames: this.excludeSkillNames,
|
|
171
|
-
manageUnmanaged: this.manageUnmanaged
|
|
194
|
+
manageUnmanaged: this.manageUnmanaged,
|
|
195
|
+
pruneBuiltins: this.pruneBuiltins,
|
|
196
|
+
bundledNames,
|
|
197
|
+
suppressedNames,
|
|
198
|
+
referencedSkillNames: this.referencedSkillNames
|
|
172
199
|
});
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
200
|
+
await this.scoreTree(usage, treeNames);
|
|
201
|
+
const nominations = this.llmReview ? await this.recommend(result.markStale, { dryRun }) : {
|
|
202
|
+
prunings: [],
|
|
203
|
+
consolidations: []
|
|
204
|
+
};
|
|
205
|
+
const llmNominations = nominations.prunings;
|
|
176
206
|
const archiveCandidates = [...new Set([...result.archive, ...llmNominations])];
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
await saveUsage(root, usage, this.io);
|
|
190
|
-
this.lastRun = Date.now();
|
|
207
|
+
const { archivedSkills, errors } = await this.applyMutations({
|
|
208
|
+
dryRun,
|
|
209
|
+
archiveCandidates,
|
|
210
|
+
nominations,
|
|
211
|
+
treeNames,
|
|
212
|
+
usage,
|
|
213
|
+
bundledNames,
|
|
214
|
+
suppressedNames,
|
|
215
|
+
root
|
|
216
|
+
});
|
|
217
|
+
if (!dryRun) this.lastRun = Date.now();
|
|
191
218
|
const report = buildCuratorRunReport({
|
|
192
219
|
runId,
|
|
193
220
|
startedAt,
|
|
@@ -196,13 +223,13 @@ var EvolutionCurator = class extends Service {
|
|
|
196
223
|
llmNominations,
|
|
197
224
|
archiveCandidates,
|
|
198
225
|
archived: archivedSkills,
|
|
199
|
-
failed: archiveCandidates.filter((name) => errors.some((error) => error.startsWith(`${name}:`))).map((name) => {
|
|
226
|
+
failed: [...new Set([...archiveCandidates, ...nominations.consolidations.map((item) => item.from)])].filter((name) => errors.some((error) => error.startsWith(`${name}:`))).map((name) => {
|
|
200
227
|
return {
|
|
201
228
|
name,
|
|
202
229
|
reason: errors.find((item) => item.startsWith(`${name}:`))?.slice(name.length + 2) ?? "unknown"
|
|
203
230
|
};
|
|
204
231
|
}),
|
|
205
|
-
snapshotPath
|
|
232
|
+
...snapshotPath === void 0 ? {} : { snapshotPath }
|
|
206
233
|
});
|
|
207
234
|
const reportsRoot = join(evolutionHome(), "reports");
|
|
208
235
|
try {
|
|
@@ -211,17 +238,133 @@ var EvolutionCurator = class extends Service {
|
|
|
211
238
|
this.ctx.logger.warn(`evolution-curator: failed to persist report ${runId}`);
|
|
212
239
|
this.ctx.logger.warn(error);
|
|
213
240
|
}
|
|
241
|
+
const summary = `${dryRun ? "dry-run" : "auto"}: stale:${result.markStale.length} archived:${archivedSkills.length} consolidated:${nominations.consolidations.length}`;
|
|
214
242
|
await stateService?.saveCuratorState({
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
243
|
+
schemaVersion: 1,
|
|
244
|
+
lastRunAt: dryRun ? persisted?.lastRunAt ?? this.lastRun : this.lastRun,
|
|
245
|
+
runCount: dryRun ? persisted?.runCount ?? 0 : (persisted?.runCount ?? 0) + 1,
|
|
246
|
+
lastSummary: summary,
|
|
218
247
|
paused: false
|
|
219
248
|
});
|
|
220
249
|
return {
|
|
221
250
|
stale: result.markStale,
|
|
222
251
|
archived: archivedSkills.map((item) => item.name),
|
|
223
252
|
errors,
|
|
224
|
-
report
|
|
253
|
+
report,
|
|
254
|
+
...this.llmReview ? { nominations } : {}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Seed baseline records for tree skills the sidecar has not seen yet, so
|
|
259
|
+
* their inactivity clock starts now (first-sight defer) and bundled skills
|
|
260
|
+
* become known candidates only when prune-builtins opts them in. Also
|
|
261
|
+
* returns the full active tree names for nomination validation.
|
|
262
|
+
*/
|
|
263
|
+
async seedBaseline(usage) {
|
|
264
|
+
const bundledNames = /* @__PURE__ */ new Set();
|
|
265
|
+
const treeNames = /* @__PURE__ */ new Set();
|
|
266
|
+
for (const summary of await this.skills.list()) {
|
|
267
|
+
treeNames.add(summary.name);
|
|
268
|
+
if (!usage.has(summary.name)) usage.set(summary.name, emptyRecord());
|
|
269
|
+
if (await this.skills.isBundled(summary.name)) bundledNames.add(summary.name);
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
bundledNames,
|
|
273
|
+
treeNames
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* F13 six-factor quality scoring, persisted onto the usage records.
|
|
278
|
+
*/
|
|
279
|
+
async scoreTree(usage, treeNames) {
|
|
280
|
+
const supportDirs = /* @__PURE__ */ new Map();
|
|
281
|
+
for (const name of treeNames) supportDirs.set(name, await this.skills.countSupportDirs(name));
|
|
282
|
+
const quality = computeQualityScores({
|
|
283
|
+
usage,
|
|
284
|
+
supportDirs
|
|
285
|
+
});
|
|
286
|
+
for (const [name, score] of quality) {
|
|
287
|
+
const record = usage.get(name);
|
|
288
|
+
if (record) {
|
|
289
|
+
record.quality_score = score.score;
|
|
290
|
+
record.quality_warn = score.warn;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Execute lifecycle archives and consolidation nominations, then persist the
|
|
296
|
+
* suppression and usage sidecars best-effort. A dry-run short-circuits: no
|
|
297
|
+
* file moves and no state persistence — the caller still writes the report.
|
|
298
|
+
*/
|
|
299
|
+
async applyMutations(input) {
|
|
300
|
+
if (input.dryRun) return {
|
|
301
|
+
archivedSkills: [],
|
|
302
|
+
errors: [],
|
|
303
|
+
suppressedChanged: false
|
|
304
|
+
};
|
|
305
|
+
const { archiveCandidates, nominations, treeNames, usage, bundledNames, suppressedNames, root } = input;
|
|
306
|
+
const errors = [];
|
|
307
|
+
const archivedSkills = [];
|
|
308
|
+
let suppressedChanged = false;
|
|
309
|
+
for (const name of archiveCandidates) {
|
|
310
|
+
const archived = await this.skills.archive(name, {
|
|
311
|
+
reason: "Lifecycle: reached archive threshold",
|
|
312
|
+
allowBundled: this.pruneBuiltins
|
|
313
|
+
});
|
|
314
|
+
if (!archived.ok) {
|
|
315
|
+
const record = usage.get(name);
|
|
316
|
+
if (record) record.state = "active";
|
|
317
|
+
errors.push(`${name}: ${archived.message}`);
|
|
318
|
+
} else {
|
|
319
|
+
archivedSkills.push({
|
|
320
|
+
name,
|
|
321
|
+
path: archived.path ?? "",
|
|
322
|
+
reason: "Lifecycle: reached archive threshold"
|
|
323
|
+
});
|
|
324
|
+
if (bundledNames.has(name)) {
|
|
325
|
+
suppressedNames.add(name);
|
|
326
|
+
suppressedChanged = true;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const alreadyArchived = new Set(archiveCandidates);
|
|
331
|
+
for (const nomination of nominations.consolidations) {
|
|
332
|
+
if (alreadyArchived.has(nomination.from)) continue;
|
|
333
|
+
if (!treeNames.has(nomination.from) || !treeNames.has(nomination.into)) {
|
|
334
|
+
errors.push(`${nomination.from}: consolidation target or source missing from the skill tree`);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const consolidated = await this.skills.consolidate(nomination.into, [nomination.from], "background_review");
|
|
338
|
+
if (!consolidated.ok) {
|
|
339
|
+
errors.push(`${nomination.from}: ${consolidated.message}`);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const record = usage.get(nomination.from);
|
|
343
|
+
if (record) {
|
|
344
|
+
record.state = "archived";
|
|
345
|
+
record.archived_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
346
|
+
}
|
|
347
|
+
alreadyArchived.add(nomination.from);
|
|
348
|
+
archivedSkills.push({
|
|
349
|
+
name: nomination.from,
|
|
350
|
+
path: consolidated.path ?? "",
|
|
351
|
+
reason: `Consolidated into ${nomination.into}`
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (suppressedChanged) try {
|
|
355
|
+
await saveSuppressedNames(root, suppressedNames, this.io);
|
|
356
|
+
} catch {
|
|
357
|
+
this.ctx.logger.warn("evolution-curator: failed to persist suppressed names; archived built-ins may re-enter the lifecycle");
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
await saveUsage(root, usage, this.io);
|
|
361
|
+
} catch {
|
|
362
|
+
this.ctx.logger.warn("evolution-curator: failed to persist usage sidecar");
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
archivedSkills,
|
|
366
|
+
errors,
|
|
367
|
+
suppressedChanged
|
|
225
368
|
};
|
|
226
369
|
}
|
|
227
370
|
recentSessionActive() {
|
|
@@ -247,6 +390,48 @@ var EvolutionCurator = class extends Service {
|
|
|
247
390
|
return null;
|
|
248
391
|
}
|
|
249
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Control-plane consolidation: merge source skill bodies into `target`,
|
|
395
|
+
* archive the sources with an absorbed-into marker, and fold their usage
|
|
396
|
+
* records into `archived` state. Snapshot-then-mutate, never a hard delete.
|
|
397
|
+
*/
|
|
398
|
+
async consolidate(target, sources) {
|
|
399
|
+
const blocked = [...this.excludeSkillNames].filter((name) => name === target || sources.includes(name));
|
|
400
|
+
if (blocked.length > 0) return {
|
|
401
|
+
ok: false,
|
|
402
|
+
message: `Skill(s) excluded from lifecycle management: ${blocked.join(", ")}`
|
|
403
|
+
};
|
|
404
|
+
await this.skills.snapshotAll("pre-consolidate");
|
|
405
|
+
const result = await this.skills.consolidate(target, sources);
|
|
406
|
+
if (!result.ok) return result;
|
|
407
|
+
const usage = await loadUsage(this.skills.root, this.io);
|
|
408
|
+
for (const source of sources) {
|
|
409
|
+
const record = usage.get(source);
|
|
410
|
+
if (record) record.state = "archived";
|
|
411
|
+
}
|
|
412
|
+
await saveUsage(this.skills.root, usage, this.io);
|
|
413
|
+
return result;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Control-plane restore: bring one archived skill back to the active root
|
|
417
|
+
* and reset its usage state, keeping the recoverable-archive invariant.
|
|
418
|
+
*/
|
|
419
|
+
async restore(name) {
|
|
420
|
+
await this.skills.snapshotAll("pre-restore");
|
|
421
|
+
const result = await this.skills.restoreFromArchive(name);
|
|
422
|
+
if (!result.ok) return result;
|
|
423
|
+
const usage = await loadUsage(this.skills.root, this.io);
|
|
424
|
+
const record = usage.get(name);
|
|
425
|
+
if (record) record.state = "active";
|
|
426
|
+
await saveUsage(this.skills.root, usage, this.io);
|
|
427
|
+
const suppressed = new Set(await loadSuppressedNames(this.skills.root, this.io));
|
|
428
|
+
if (suppressed.delete(name)) try {
|
|
429
|
+
await saveSuppressedNames(this.skills.root, suppressed, this.io);
|
|
430
|
+
} catch {
|
|
431
|
+
this.ctx.logger.warn(`evolution-curator: failed to persist suppressed names after restoring ${name}`);
|
|
432
|
+
}
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
250
435
|
};
|
|
251
436
|
//#endregion
|
|
252
437
|
export { EvolutionCurator, EvolutionCurator as default };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
6
6
|
import type Schema from '@deepseek-ai/schemastery';
|
|
7
7
|
import { SkillLibrary } from '@deepseek-ai/dsh-evolution-core';
|
|
8
|
-
import { type CuratorRunReport } from '@deepseek-ai/dsh-evolution-core';
|
|
8
|
+
import { type CuratorNominations, type CuratorRunReport, type SkillActionResult } from '@deepseek-ai/dsh-evolution-core';
|
|
9
9
|
declare module '@deepseek-ai/cordis' {
|
|
10
10
|
interface Context {
|
|
11
11
|
evolutionCurator: EvolutionCurator;
|
|
@@ -27,9 +27,33 @@ export interface Config {
|
|
|
27
27
|
excludeSkillNames?: string[];
|
|
28
28
|
/** Include usage records whose created_by is not 'agent' in lifecycle decisions. */
|
|
29
29
|
manageUnmanaged?: boolean;
|
|
30
|
+
/** Archive long-unused bundled skills too (with suppression against re-seeds). */
|
|
31
|
+
pruneBuiltins?: boolean;
|
|
32
|
+
/** Static scheduled-task skill references; such skills never auto-transition. */
|
|
33
|
+
referencedSkillNames?: string[];
|
|
34
|
+
/** Start the interval timer on context ready (auto-curation). Default true. */
|
|
35
|
+
autoStart?: boolean;
|
|
30
36
|
/** Max tokens for the optional LLM nomination pass. */
|
|
31
37
|
curatorReviewMaxTokens?: number;
|
|
32
38
|
}
|
|
39
|
+
/** Outcome of one curator run pass. */
|
|
40
|
+
export interface CuratorRunOutcome {
|
|
41
|
+
stale: string[];
|
|
42
|
+
archived: string[];
|
|
43
|
+
errors: string[];
|
|
44
|
+
report: CuratorRunReport;
|
|
45
|
+
skipped?: string;
|
|
46
|
+
/** LLM nominations when the optional review pass is enabled (audit visibility). */
|
|
47
|
+
nominations?: CuratorNominations;
|
|
48
|
+
}
|
|
49
|
+
/** Persisted curator-state record shape (schemaVersion optional for legacy reads). */
|
|
50
|
+
export interface CuratorStateRecordShape {
|
|
51
|
+
schemaVersion?: number;
|
|
52
|
+
lastRunAt: number;
|
|
53
|
+
runCount: number;
|
|
54
|
+
lastSummary: string;
|
|
55
|
+
paused: boolean;
|
|
56
|
+
}
|
|
33
57
|
export declare class EvolutionCurator extends Service {
|
|
34
58
|
static inject: string[];
|
|
35
59
|
static Config: Schema<Config>;
|
|
@@ -45,6 +69,8 @@ export declare class EvolutionCurator extends Service {
|
|
|
45
69
|
private readonly minIdleHours;
|
|
46
70
|
private readonly excludeSkillNames;
|
|
47
71
|
private readonly manageUnmanaged;
|
|
72
|
+
private readonly pruneBuiltins;
|
|
73
|
+
private readonly referencedSkillNames;
|
|
48
74
|
private readonly curatorReviewMaxTokens;
|
|
49
75
|
private lastRun;
|
|
50
76
|
private timer;
|
|
@@ -53,22 +79,55 @@ export declare class EvolutionCurator extends Service {
|
|
|
53
79
|
start(): void;
|
|
54
80
|
stop(): void;
|
|
55
81
|
/**
|
|
56
|
-
* Optional Hermes-curator LLM pass. The model may only NOMINATE pruning
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
82
|
+
* Optional Hermes-curator LLM pass. The model may only NOMINATE pruning and
|
|
83
|
+
* consolidation; every move stays a control-plane operation and each
|
|
84
|
+
* nomination is re-validated against the tree and protected markers before
|
|
85
|
+
* any file move. `dryRun` prepends the report-only banner.
|
|
60
86
|
*/
|
|
61
|
-
recommend(candidates: string[]
|
|
87
|
+
recommend(candidates: string[], options?: {
|
|
88
|
+
dryRun?: boolean;
|
|
89
|
+
}): Promise<CuratorNominations>;
|
|
62
90
|
private skippedReport;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Run one curator pass. `ignoreGates` skips the interval and idle gates so an
|
|
93
|
+
* explicit `/evolution curator run` always executes (manual-run semantics):
|
|
94
|
+
* `dryRun` computes the lifecycle and the LLM nominations but performs no
|
|
95
|
+
* mutation, reports what WOULD happen, and does not push out the next run.
|
|
96
|
+
*/
|
|
97
|
+
run(options?: {
|
|
98
|
+
ignoreGates?: boolean;
|
|
99
|
+
dryRun?: boolean;
|
|
100
|
+
}): Promise<CuratorRunOutcome>;
|
|
101
|
+
/**
|
|
102
|
+
* Seed baseline records for tree skills the sidecar has not seen yet, so
|
|
103
|
+
* their inactivity clock starts now (first-sight defer) and bundled skills
|
|
104
|
+
* become known candidates only when prune-builtins opts them in. Also
|
|
105
|
+
* returns the full active tree names for nomination validation.
|
|
106
|
+
*/
|
|
107
|
+
private seedBaseline;
|
|
108
|
+
/**
|
|
109
|
+
* F13 six-factor quality scoring, persisted onto the usage records.
|
|
110
|
+
*/
|
|
111
|
+
private scoreTree;
|
|
112
|
+
/**
|
|
113
|
+
* Execute lifecycle archives and consolidation nominations, then persist the
|
|
114
|
+
* suppression and usage sidecars best-effort. A dry-run short-circuits: no
|
|
115
|
+
* file moves and no state persistence — the caller still writes the report.
|
|
116
|
+
*/
|
|
117
|
+
private applyMutations;
|
|
70
118
|
private recentSessionActive;
|
|
71
119
|
latestReport(): Promise<CuratorRunReport | null>;
|
|
120
|
+
/**
|
|
121
|
+
* Control-plane consolidation: merge source skill bodies into `target`,
|
|
122
|
+
* archive the sources with an absorbed-into marker, and fold their usage
|
|
123
|
+
* records into `archived` state. Snapshot-then-mutate, never a hard delete.
|
|
124
|
+
*/
|
|
125
|
+
consolidate(target: string, sources: string[]): Promise<SkillActionResult>;
|
|
126
|
+
/**
|
|
127
|
+
* Control-plane restore: bring one archived skill back to the active root
|
|
128
|
+
* and reset its usage state, keeping the recoverable-archive invariant.
|
|
129
|
+
*/
|
|
130
|
+
restore(name: string): Promise<SkillActionResult>;
|
|
72
131
|
}
|
|
73
132
|
export default EvolutionCurator;
|
|
74
133
|
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-curator",
|
|
3
3
|
"description": "Deterministic skill lifecycle and recovery (community build)",
|
|
4
|
-
"version": "0.1.0-rc.
|
|
4
|
+
"version": "0.1.0-rc.20",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -33,20 +33,20 @@
|
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
36
|
-
"@lmzhen/dsh-evolution-core": "^0.1.0-rc.
|
|
36
|
+
"@lmzhen/dsh-evolution-core": "^0.1.0-rc.20"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
40
40
|
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
|
|
41
41
|
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
42
|
-
"@lmzhen/dsh-evolution-io": "^0.1.0-rc.
|
|
43
|
-
"@lmzhen/dsh-evolution-state": "^0.1.0-rc.
|
|
42
|
+
"@lmzhen/dsh-evolution-io": "^0.1.0-rc.20",
|
|
43
|
+
"@lmzhen/dsh-evolution-state": "^0.1.0-rc.20"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
|
|
47
47
|
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
48
|
-
"@lmzhen/dsh-evolution-core": "^0.1.0-rc.
|
|
49
|
-
"@lmzhen/dsh-evolution-io": "^0.1.0-rc.
|
|
50
|
-
"@lmzhen/dsh-evolution-state": "^0.1.0-rc.
|
|
48
|
+
"@lmzhen/dsh-evolution-core": "^0.1.0-rc.20",
|
|
49
|
+
"@lmzhen/dsh-evolution-io": "^0.1.0-rc.20",
|
|
50
|
+
"@lmzhen/dsh-evolution-state": "^0.1.0-rc.20"
|
|
51
51
|
}
|
|
52
52
|
}
|