@lmzhen/dsh-evolution-curator 0.1.0-rc.3 → 0.1.0-rc.30
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 +333 -55
- package/lib/types/index.d.ts +99 -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,25 +3,38 @@ 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, SKILL_NAME_RE, SkillLibrary, buildCuratorRunReport, computeLifecycleTransitions, computeQualityScores, computeScopeView, emptyRecord, evolutionHome, evolutionIoAdapter, loadSuppressedNames, loadUsage, parseCuratorNominations, parseFrontmatter, 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.
|
|
10
10
|
* @module @lmzhen/dsh-evolution-curator
|
|
11
11
|
*/
|
|
12
|
+
/**
|
|
13
|
+
* Block LLM-nominated consolidations that would touch a gate-protected name:
|
|
14
|
+
* exclude / referenced / suppressed skills must never merge (neither as the
|
|
15
|
+
* source being archived nor as the umbrella being edited). Mirrors the control
|
|
16
|
+
* plane's `consolidate()` guard; automatic nominations must pass the same gate.
|
|
17
|
+
*/
|
|
18
|
+
function gateConsolidations(consolidations, gates) {
|
|
19
|
+
const blocked = (name) => gates.exclude?.has(name) === true || gates.referenced?.has(name) === true || gates.suppressed?.has(name) === true;
|
|
20
|
+
return consolidations.filter((n) => !blocked(n.from) && !blocked(n.into));
|
|
21
|
+
}
|
|
12
22
|
var EvolutionCurator = class extends Service {
|
|
13
23
|
static inject = ["evolutionIo"];
|
|
14
24
|
static Config = z.object({
|
|
15
25
|
enabled: z.boolean().default(true),
|
|
16
|
-
intervalHours: z.number().default(
|
|
17
|
-
staleAfterDays: z.number().default(
|
|
18
|
-
archiveAfterDays: z.number().default(
|
|
26
|
+
intervalHours: z.number().default(DEFAULT_CURATOR_INTERVAL_HOURS),
|
|
27
|
+
staleAfterDays: z.number().default(DEFAULT_STALE_AFTER_DAYS),
|
|
28
|
+
archiveAfterDays: z.number().default(DEFAULT_ARCHIVE_AFTER_DAYS),
|
|
19
29
|
llmReview: z.boolean().default(false),
|
|
20
30
|
curatorProvider: z.string().default("deepseek-official"),
|
|
21
31
|
qualityWarnStaleAfterDays: z.number().default(7),
|
|
22
|
-
minIdleHours: z.number().default(
|
|
32
|
+
minIdleHours: z.number().default(DEFAULT_MIN_IDLE_HOURS),
|
|
23
33
|
excludeSkillNames: z.array(z.string()).default([]),
|
|
24
34
|
manageUnmanaged: z.boolean().default(false),
|
|
35
|
+
pruneBuiltins: z.boolean().default(false),
|
|
36
|
+
referencedSkillNames: z.array(z.string()).default([]),
|
|
37
|
+
autoStart: z.boolean().default(true),
|
|
25
38
|
curatorReviewMaxTokens: z.number().default(2048)
|
|
26
39
|
});
|
|
27
40
|
skills;
|
|
@@ -36,6 +49,8 @@ var EvolutionCurator = class extends Service {
|
|
|
36
49
|
minIdleHours;
|
|
37
50
|
excludeSkillNames;
|
|
38
51
|
manageUnmanaged;
|
|
52
|
+
pruneBuiltins;
|
|
53
|
+
referencedSkillNames;
|
|
39
54
|
curatorReviewMaxTokens;
|
|
40
55
|
lastRun = 0;
|
|
41
56
|
timer;
|
|
@@ -44,15 +59,17 @@ var EvolutionCurator = class extends Service {
|
|
|
44
59
|
this.io = evolutionIoAdapter(() => ctx.evolutionIo.provider());
|
|
45
60
|
this.skills = new SkillLibrary(void 0, this.io);
|
|
46
61
|
this.enabled = config.enabled ?? true;
|
|
47
|
-
this.intervalHours = config.intervalHours ??
|
|
48
|
-
this.staleAfterDays = config.staleAfterDays ??
|
|
49
|
-
this.archiveAfterDays = config.archiveAfterDays ??
|
|
62
|
+
this.intervalHours = config.intervalHours ?? DEFAULT_CURATOR_INTERVAL_HOURS;
|
|
63
|
+
this.staleAfterDays = config.staleAfterDays ?? DEFAULT_STALE_AFTER_DAYS;
|
|
64
|
+
this.archiveAfterDays = config.archiveAfterDays ?? DEFAULT_ARCHIVE_AFTER_DAYS;
|
|
50
65
|
this.llmReview = config.llmReview ?? false;
|
|
51
66
|
this.curatorProvider = config.curatorProvider ?? "deepseek-official";
|
|
52
67
|
this.qualityWarnStaleAfterDays = config.qualityWarnStaleAfterDays ?? 7;
|
|
53
|
-
this.minIdleHours = config.minIdleHours ??
|
|
68
|
+
this.minIdleHours = config.minIdleHours ?? DEFAULT_MIN_IDLE_HOURS;
|
|
54
69
|
this.excludeSkillNames = new Set(config.excludeSkillNames ?? []);
|
|
55
70
|
this.manageUnmanaged = config.manageUnmanaged ?? false;
|
|
71
|
+
this.pruneBuiltins = config.pruneBuiltins ?? false;
|
|
72
|
+
this.referencedSkillNames = new Set(config.referencedSkillNames ?? []);
|
|
56
73
|
this.curatorReviewMaxTokens = config.curatorReviewMaxTokens ?? 2048;
|
|
57
74
|
this.lastRun = Date.now();
|
|
58
75
|
this.ctx.effect(() => {
|
|
@@ -60,6 +77,7 @@ var EvolutionCurator = class extends Service {
|
|
|
60
77
|
this.stop();
|
|
61
78
|
};
|
|
62
79
|
}, "evolution-curator.stop");
|
|
80
|
+
if (config.autoStart ?? true) this.start();
|
|
63
81
|
}
|
|
64
82
|
lifecycle() {
|
|
65
83
|
const snapshot = this.ctx.get("evolutionPolicy")?.get();
|
|
@@ -81,23 +99,28 @@ var EvolutionCurator = class extends Service {
|
|
|
81
99
|
this.timer = void 0;
|
|
82
100
|
}
|
|
83
101
|
/**
|
|
84
|
-
* Optional Hermes-curator LLM pass. The model may only NOMINATE pruning
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
102
|
+
* Optional Hermes-curator LLM pass. The model may only NOMINATE pruning and
|
|
103
|
+
* consolidation; every move stays a control-plane operation and each
|
|
104
|
+
* nomination is re-validated against the tree and protected markers before
|
|
105
|
+
* any file move. `dryRun` prepends the report-only banner.
|
|
88
106
|
*/
|
|
89
|
-
async recommend(candidates) {
|
|
90
|
-
|
|
107
|
+
async recommend(candidates, options = {}) {
|
|
108
|
+
const empty = {
|
|
109
|
+
prunings: [],
|
|
110
|
+
consolidations: []
|
|
111
|
+
};
|
|
112
|
+
if (candidates.length === 0) return empty;
|
|
91
113
|
const llm = this.ctx.get("llm");
|
|
92
|
-
if (!llm) return
|
|
114
|
+
if (!llm) return empty;
|
|
93
115
|
const model = this.ctx.get("evolutionPolicy")?.get().curatorModel ?? "deepseek-v4-pro";
|
|
94
116
|
const prompt = [
|
|
117
|
+
options.dryRun ? CURATOR_DRY_RUN_BANNER : "",
|
|
95
118
|
CURATOR_PROMPT,
|
|
96
119
|
"",
|
|
97
|
-
|
|
120
|
+
`Stale candidates observed by the deterministic lifecycle scanner:${candidates.length === 0 ? " (none)" : ""}`,
|
|
98
121
|
...candidates.map((name) => `- ${name}`),
|
|
99
122
|
"",
|
|
100
|
-
"Return a YAML summary with
|
|
123
|
+
"Return a YAML summary with consolidations and prunings lists. Nominate only actions whose archival/merge is clearly safe."
|
|
101
124
|
].join("\n");
|
|
102
125
|
try {
|
|
103
126
|
const assembler = new BlockAssembler();
|
|
@@ -119,13 +142,13 @@ var EvolutionCurator = class extends Service {
|
|
|
119
142
|
maxTokens: this.curatorReviewMaxTokens,
|
|
120
143
|
purpose: "evolution-curator"
|
|
121
144
|
})) assembler.push(chunk);
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
145
|
+
const parsed = parseCuratorNominations(assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("\n"));
|
|
146
|
+
return {
|
|
147
|
+
prunings: parsed.prunings.filter((name) => candidates.includes(name)),
|
|
148
|
+
consolidations: parsed.consolidations
|
|
149
|
+
};
|
|
127
150
|
} catch {
|
|
128
|
-
return
|
|
151
|
+
return empty;
|
|
129
152
|
}
|
|
130
153
|
}
|
|
131
154
|
skippedReport(runId, startedAt) {
|
|
@@ -137,57 +160,97 @@ var EvolutionCurator = class extends Service {
|
|
|
137
160
|
llmNominations: [],
|
|
138
161
|
archiveCandidates: [],
|
|
139
162
|
archived: [],
|
|
140
|
-
failed: []
|
|
163
|
+
failed: [],
|
|
164
|
+
llmReviewEnabled: this.llmReview
|
|
141
165
|
});
|
|
142
166
|
}
|
|
143
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Run one curator pass. `ignoreGates` skips the interval and idle gates so an
|
|
169
|
+
* explicit `/evolution curator run` always executes (manual-run semantics):
|
|
170
|
+
* `dryRun` computes the lifecycle and the LLM nominations but performs no
|
|
171
|
+
* mutation, reports what WOULD happen, and does not push out the next run.
|
|
172
|
+
*/
|
|
173
|
+
async run(options = {}) {
|
|
174
|
+
const { ignoreGates = false, dryRun = false } = options;
|
|
144
175
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
145
176
|
const runId = randomUUID();
|
|
146
177
|
const stateService = this.ctx.get("evolutionState");
|
|
147
178
|
const lifecycle = this.lifecycle();
|
|
148
179
|
const persisted = await stateService?.loadCuratorState();
|
|
149
|
-
if (persisted && Date.now() - persisted.lastRunAt < lifecycle.intervalHours * 36e5) return {
|
|
180
|
+
if (!ignoreGates && persisted && Date.now() - persisted.lastRunAt < lifecycle.intervalHours * 36e5) return {
|
|
150
181
|
stale: [],
|
|
151
182
|
archived: [],
|
|
152
183
|
errors: [],
|
|
153
184
|
report: this.skippedReport(runId, startedAt),
|
|
154
185
|
skipped: "interval"
|
|
155
186
|
};
|
|
156
|
-
if (this.minIdleHours > 0 && this.recentSessionActive()) return {
|
|
187
|
+
if (!ignoreGates && this.minIdleHours > 0 && this.recentSessionActive()) return {
|
|
157
188
|
stale: [],
|
|
158
189
|
archived: [],
|
|
159
190
|
errors: [],
|
|
160
191
|
report: this.skippedReport(runId, startedAt),
|
|
161
192
|
skipped: "active-session"
|
|
162
193
|
};
|
|
194
|
+
if (!ignoreGates && persisted === null) {
|
|
195
|
+
await stateService?.saveCuratorState({
|
|
196
|
+
schemaVersion: 1,
|
|
197
|
+
lastRunAt: Date.now(),
|
|
198
|
+
runCount: 0,
|
|
199
|
+
lastSummary: "first-run-deferred",
|
|
200
|
+
paused: false
|
|
201
|
+
});
|
|
202
|
+
return {
|
|
203
|
+
stale: [],
|
|
204
|
+
archived: [],
|
|
205
|
+
errors: [],
|
|
206
|
+
report: this.skippedReport(runId, startedAt),
|
|
207
|
+
skipped: "first-run-deferred"
|
|
208
|
+
};
|
|
209
|
+
}
|
|
163
210
|
const root = this.skills.root;
|
|
164
|
-
const
|
|
165
|
-
const usage =
|
|
211
|
+
const rawUsage = await loadUsage(root, this.io);
|
|
212
|
+
const usage = dryRun ? new Map([...rawUsage].map(([name, record]) => [name, { ...record }])) : rawUsage;
|
|
213
|
+
const snapshotPath = dryRun ? void 0 : await this.skills.snapshotAll("pre-curator-run");
|
|
214
|
+
const suppressedNames = new Set(await loadSuppressedNames(root, this.io));
|
|
215
|
+
const { bundledNames, treeNames } = await this.seedBaseline(usage);
|
|
166
216
|
const result = computeLifecycleTransitions(usage, {
|
|
167
217
|
staleAfterDays: lifecycle.staleAfterDays,
|
|
168
218
|
archiveAfterDays: lifecycle.archiveAfterDays,
|
|
169
219
|
qualityWarnStaleAfterDays: this.qualityWarnStaleAfterDays,
|
|
170
220
|
excludeSkillNames: this.excludeSkillNames,
|
|
171
|
-
manageUnmanaged: this.manageUnmanaged
|
|
221
|
+
manageUnmanaged: this.manageUnmanaged,
|
|
222
|
+
pruneBuiltins: this.pruneBuiltins,
|
|
223
|
+
bundledNames,
|
|
224
|
+
suppressedNames,
|
|
225
|
+
referencedSkillNames: this.referencedSkillNames
|
|
172
226
|
});
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
227
|
+
await this.scoreTree(usage, treeNames);
|
|
228
|
+
const nominations = this.llmReview ? await this.recommend(result.markStale, { dryRun }) : {
|
|
229
|
+
prunings: [],
|
|
230
|
+
consolidations: []
|
|
231
|
+
};
|
|
232
|
+
const gatedNominations = {
|
|
233
|
+
...nominations,
|
|
234
|
+
consolidations: gateConsolidations(nominations.consolidations, {
|
|
235
|
+
exclude: this.excludeSkillNames,
|
|
236
|
+
referenced: this.referencedSkillNames,
|
|
237
|
+
suppressed: suppressedNames
|
|
238
|
+
})
|
|
239
|
+
};
|
|
240
|
+
const llmNominations = gatedNominations.prunings;
|
|
176
241
|
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();
|
|
242
|
+
const { archivedSkills, errors } = await this.applyMutations({
|
|
243
|
+
dryRun,
|
|
244
|
+
archiveCandidates,
|
|
245
|
+
nominations: gatedNominations,
|
|
246
|
+
treeNames,
|
|
247
|
+
usage,
|
|
248
|
+
bundledNames,
|
|
249
|
+
suppressedNames,
|
|
250
|
+
root,
|
|
251
|
+
failedFrom: new Map(result.transitions.filter((t) => t.to === "archived").map((t) => [t.name, t.from]))
|
|
252
|
+
});
|
|
253
|
+
if (!dryRun) this.lastRun = Date.now();
|
|
191
254
|
const report = buildCuratorRunReport({
|
|
192
255
|
runId,
|
|
193
256
|
startedAt,
|
|
@@ -196,13 +259,14 @@ var EvolutionCurator = class extends Service {
|
|
|
196
259
|
llmNominations,
|
|
197
260
|
archiveCandidates,
|
|
198
261
|
archived: archivedSkills,
|
|
199
|
-
failed: archiveCandidates.filter((name) => errors.some((error) => error.startsWith(`${name}:`))).map((name) => {
|
|
262
|
+
failed: [...new Set([...archiveCandidates, ...gatedNominations.consolidations.map((item) => item.from)])].filter((name) => errors.some((error) => error.startsWith(`${name}:`))).map((name) => {
|
|
200
263
|
return {
|
|
201
264
|
name,
|
|
202
265
|
reason: errors.find((item) => item.startsWith(`${name}:`))?.slice(name.length + 2) ?? "unknown"
|
|
203
266
|
};
|
|
204
267
|
}),
|
|
205
|
-
snapshotPath
|
|
268
|
+
...snapshotPath === void 0 ? {} : { snapshotPath },
|
|
269
|
+
llmReviewEnabled: this.llmReview
|
|
206
270
|
});
|
|
207
271
|
const reportsRoot = join(evolutionHome(), "reports");
|
|
208
272
|
try {
|
|
@@ -211,17 +275,160 @@ var EvolutionCurator = class extends Service {
|
|
|
211
275
|
this.ctx.logger.warn(`evolution-curator: failed to persist report ${runId}`);
|
|
212
276
|
this.ctx.logger.warn(error);
|
|
213
277
|
}
|
|
278
|
+
const llmHint = !this.llmReview && result.markStale.length > 0 ? " (llmReview: off - deterministic archive only; set llmReview: true for the LLM merge channel)" : "";
|
|
279
|
+
const summary = `${dryRun ? "dry-run" : "auto"}: stale:${result.markStale.length} archived:${archivedSkills.length} consolidated:${gatedNominations.consolidations.length}${llmHint}`;
|
|
214
280
|
await stateService?.saveCuratorState({
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
281
|
+
schemaVersion: 1,
|
|
282
|
+
lastRunAt: dryRun ? persisted?.lastRunAt ?? this.lastRun : this.lastRun,
|
|
283
|
+
runCount: dryRun ? persisted?.runCount ?? 0 : (persisted?.runCount ?? 0) + 1,
|
|
284
|
+
lastSummary: summary,
|
|
218
285
|
paused: false
|
|
219
286
|
});
|
|
220
287
|
return {
|
|
221
288
|
stale: result.markStale,
|
|
222
289
|
archived: archivedSkills.map((item) => item.name),
|
|
223
290
|
errors,
|
|
224
|
-
report
|
|
291
|
+
report,
|
|
292
|
+
...this.llmReview ? { nominations: gatedNominations } : {}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Seed baseline records for tree skills the sidecar has not seen yet, so
|
|
297
|
+
* their inactivity clock starts now (first-sight defer) and bundled skills
|
|
298
|
+
* become known candidates only when prune-builtins opts them in. Also
|
|
299
|
+
* returns the full active tree names for nomination validation.
|
|
300
|
+
*/
|
|
301
|
+
async seedBaseline(usage) {
|
|
302
|
+
const bundledNames = /* @__PURE__ */ new Set();
|
|
303
|
+
const treeNames = /* @__PURE__ */ new Set();
|
|
304
|
+
for (const summary of await this.skills.list()) {
|
|
305
|
+
treeNames.add(summary.name);
|
|
306
|
+
if (!usage.has(summary.name)) usage.set(summary.name, emptyRecord());
|
|
307
|
+
if (await this.skills.isBundled(summary.name)) bundledNames.add(summary.name);
|
|
308
|
+
const record = usage.get(summary.name);
|
|
309
|
+
if (record) record.pinned = await this.skills.isPinned(summary.name);
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
bundledNames,
|
|
313
|
+
treeNames
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* F13 six-factor quality scoring, persisted onto the usage records.
|
|
318
|
+
*/
|
|
319
|
+
async scoreTree(usage, treeNames) {
|
|
320
|
+
const supportDirs = /* @__PURE__ */ new Map();
|
|
321
|
+
for (const name of treeNames) supportDirs.set(name, await this.skills.countSupportDirs(name));
|
|
322
|
+
const quality = computeQualityScores({
|
|
323
|
+
usage,
|
|
324
|
+
supportDirs,
|
|
325
|
+
referenceCounts: await this.referenceCounts(treeNames)
|
|
326
|
+
});
|
|
327
|
+
for (const [name, score] of quality) {
|
|
328
|
+
const record = usage.get(name);
|
|
329
|
+
if (record) {
|
|
330
|
+
record.quality_score = score.score;
|
|
331
|
+
record.quality_warn = score.warn;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* In-degree over explicit `related_skills` frontmatter references (the DSH
|
|
337
|
+
* equivalent of the graph-in-degree references factor): a skill listing
|
|
338
|
+
* other skill names counts as one reference to each of them, so hub skills
|
|
339
|
+
* that are explicitly named by peers get a non-zero references factor.
|
|
340
|
+
*/
|
|
341
|
+
async referenceCounts(treeNames) {
|
|
342
|
+
const counts = /* @__PURE__ */ new Map();
|
|
343
|
+
for (const name of treeNames) {
|
|
344
|
+
const content = await this.skills.read(name);
|
|
345
|
+
if (!content) continue;
|
|
346
|
+
const parsed = parseFrontmatter(content);
|
|
347
|
+
if (!parsed) continue;
|
|
348
|
+
const raw = parsed.frontmatter["related_skills"];
|
|
349
|
+
if (typeof raw !== "string") continue;
|
|
350
|
+
for (const match of Array.from(raw.matchAll(/[a-z0-9][a-z0-9-]*/g))) {
|
|
351
|
+
const target = match[0];
|
|
352
|
+
if (target && SKILL_NAME_RE.test(target) && target !== name) counts.set(target, (counts.get(target) ?? 0) + 1);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return counts;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Execute lifecycle archives and consolidation nominations, then persist the
|
|
359
|
+
* suppression and usage sidecars best-effort. A dry-run short-circuits: no
|
|
360
|
+
* file moves and no state persistence — the caller still writes the report.
|
|
361
|
+
*/
|
|
362
|
+
async applyMutations(input) {
|
|
363
|
+
if (input.dryRun) return {
|
|
364
|
+
archivedSkills: [],
|
|
365
|
+
errors: [],
|
|
366
|
+
suppressedChanged: false
|
|
367
|
+
};
|
|
368
|
+
const { archiveCandidates, nominations, treeNames, usage, bundledNames, suppressedNames, root, failedFrom } = input;
|
|
369
|
+
const errors = [];
|
|
370
|
+
const archivedSkills = [];
|
|
371
|
+
let suppressedChanged = false;
|
|
372
|
+
for (const name of archiveCandidates) {
|
|
373
|
+
const archived = await this.skills.archive(name, {
|
|
374
|
+
reason: "Lifecycle: reached archive threshold",
|
|
375
|
+
allowBundled: this.pruneBuiltins
|
|
376
|
+
});
|
|
377
|
+
if (!archived.ok) {
|
|
378
|
+
const record = usage.get(name);
|
|
379
|
+
const from = failedFrom?.get(name);
|
|
380
|
+
if (record && (from === "stale" || from === "active")) record.state = from;
|
|
381
|
+
errors.push(`${name}: ${archived.message}`);
|
|
382
|
+
} else {
|
|
383
|
+
archivedSkills.push({
|
|
384
|
+
name,
|
|
385
|
+
path: archived.path ?? "",
|
|
386
|
+
reason: "Lifecycle: reached archive threshold"
|
|
387
|
+
});
|
|
388
|
+
if (bundledNames.has(name)) {
|
|
389
|
+
suppressedNames.add(name);
|
|
390
|
+
suppressedChanged = true;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const alreadyArchived = new Set(archiveCandidates);
|
|
395
|
+
for (const nomination of nominations.consolidations) {
|
|
396
|
+
if (alreadyArchived.has(nomination.from)) continue;
|
|
397
|
+
if (!treeNames.has(nomination.from) || !treeNames.has(nomination.into)) {
|
|
398
|
+
errors.push(`${nomination.from}: consolidation target or source missing from the skill tree`);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const consolidated = await this.skills.consolidate(nomination.into, [nomination.from], "background_review");
|
|
402
|
+
if (!consolidated.ok) {
|
|
403
|
+
errors.push(`${nomination.from}: ${consolidated.message}`);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
const record = usage.get(nomination.from);
|
|
407
|
+
if (record) {
|
|
408
|
+
record.state = "archived";
|
|
409
|
+
record.archived_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
410
|
+
}
|
|
411
|
+
alreadyArchived.add(nomination.from);
|
|
412
|
+
archivedSkills.push({
|
|
413
|
+
name: nomination.from,
|
|
414
|
+
path: join(this.skills.root, ".archive", nomination.from),
|
|
415
|
+
reason: `Consolidated into ${nomination.into}`
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
if (suppressedChanged) try {
|
|
419
|
+
await saveSuppressedNames(root, suppressedNames, this.io);
|
|
420
|
+
} catch {
|
|
421
|
+
this.ctx.logger.warn("evolution-curator: failed to persist suppressed names; archived built-ins may re-enter the lifecycle");
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
await saveUsage(root, usage, this.io);
|
|
425
|
+
} catch {
|
|
426
|
+
this.ctx.logger.warn("evolution-curator: failed to persist usage sidecar");
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
archivedSkills,
|
|
430
|
+
errors,
|
|
431
|
+
suppressedChanged
|
|
225
432
|
};
|
|
226
433
|
}
|
|
227
434
|
recentSessionActive() {
|
|
@@ -247,6 +454,77 @@ var EvolutionCurator = class extends Service {
|
|
|
247
454
|
return null;
|
|
248
455
|
}
|
|
249
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Read-only lifecycle scope classification: which skills are in scope,
|
|
459
|
+
* watched (stale/quality-warned), exempted, or protected. Uses the same
|
|
460
|
+
* candidate gate as `run()` (`computeScopeView` / `lifecycleCandidate`),
|
|
461
|
+
* so the view always predicts what a curator pass may touch.
|
|
462
|
+
*/
|
|
463
|
+
async scopeView() {
|
|
464
|
+
const root = this.skills.root;
|
|
465
|
+
const usage = await loadUsage(root, this.io);
|
|
466
|
+
const { bundledNames } = await this.seedBaseline(usage);
|
|
467
|
+
return computeScopeView(usage, {
|
|
468
|
+
staleAfterDays: this.lifecycle().staleAfterDays,
|
|
469
|
+
archiveAfterDays: this.lifecycle().archiveAfterDays,
|
|
470
|
+
excludeSkillNames: this.excludeSkillNames,
|
|
471
|
+
referencedSkillNames: this.referencedSkillNames,
|
|
472
|
+
suppressedNames: new Set(await loadSuppressedNames(root, this.io)),
|
|
473
|
+
manageUnmanaged: this.manageUnmanaged,
|
|
474
|
+
pruneBuiltins: this.pruneBuiltins,
|
|
475
|
+
bundledNames
|
|
476
|
+
}, await this.protectedNameMap());
|
|
477
|
+
}
|
|
478
|
+
/** marker info (pinned/bundled/hub-installed) per skill, from the library list. */
|
|
479
|
+
async protectedNameMap() {
|
|
480
|
+
const map = /* @__PURE__ */ new Map();
|
|
481
|
+
for (const summary of await this.skills.list()) if (summary.protectedBy !== null) map.set(summary.name, summary.protectedBy);
|
|
482
|
+
return map;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Control-plane consolidation: merge source skill bodies into `target`,
|
|
486
|
+
* archive the sources with an absorbed-into marker, and fold their usage
|
|
487
|
+
* records into `archived` state. Snapshot-then-mutate, never a hard delete.
|
|
488
|
+
*/
|
|
489
|
+
async consolidate(target, sources) {
|
|
490
|
+
const blocked = [...this.excludeSkillNames].filter((name) => name === target || sources.includes(name));
|
|
491
|
+
if (blocked.length > 0) return {
|
|
492
|
+
ok: false,
|
|
493
|
+
message: `Skill(s) excluded from lifecycle management: ${blocked.join(", ")}`
|
|
494
|
+
};
|
|
495
|
+
await this.skills.snapshotAll("pre-consolidate");
|
|
496
|
+
const result = await this.skills.consolidate(target, sources);
|
|
497
|
+
if (!result.ok) return result;
|
|
498
|
+
const usage = await loadUsage(this.skills.root, this.io);
|
|
499
|
+
for (const source of sources) {
|
|
500
|
+
const record = usage.get(source);
|
|
501
|
+
if (record) record.state = "archived";
|
|
502
|
+
if (record) record.archived_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
503
|
+
}
|
|
504
|
+
await saveUsage(this.skills.root, usage, this.io);
|
|
505
|
+
return result;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Control-plane restore: bring one archived skill back to the active root
|
|
509
|
+
* and reset its usage state, keeping the recoverable-archive invariant.
|
|
510
|
+
*/
|
|
511
|
+
async restore(name) {
|
|
512
|
+
await this.skills.snapshotAll("pre-restore");
|
|
513
|
+
const result = await this.skills.restoreFromArchive(name);
|
|
514
|
+
if (!result.ok) return result;
|
|
515
|
+
const usage = await loadUsage(this.skills.root, this.io);
|
|
516
|
+
const record = usage.get(name);
|
|
517
|
+
if (record) record.state = "active";
|
|
518
|
+
if (record) record.archived_at = null;
|
|
519
|
+
await saveUsage(this.skills.root, usage, this.io);
|
|
520
|
+
const suppressed = new Set(await loadSuppressedNames(this.skills.root, this.io));
|
|
521
|
+
if (suppressed.delete(name)) try {
|
|
522
|
+
await saveSuppressedNames(this.skills.root, suppressed, this.io);
|
|
523
|
+
} catch {
|
|
524
|
+
this.ctx.logger.warn(`evolution-curator: failed to persist suppressed names after restoring ${name}`);
|
|
525
|
+
}
|
|
526
|
+
return result;
|
|
527
|
+
}
|
|
250
528
|
};
|
|
251
529
|
//#endregion
|
|
252
|
-
export { EvolutionCurator, EvolutionCurator as default };
|
|
530
|
+
export { EvolutionCurator, EvolutionCurator as default, gateConsolidations };
|
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 CuratorConsolidation, type CuratorNominations, type CuratorRunReport, type ScopeView, 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,44 @@ 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
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Block LLM-nominated consolidations that would touch a gate-protected name:
|
|
59
|
+
* exclude / referenced / suppressed skills must never merge (neither as the
|
|
60
|
+
* source being archived nor as the umbrella being edited). Mirrors the control
|
|
61
|
+
* plane's `consolidate()` guard; automatic nominations must pass the same gate.
|
|
62
|
+
*/
|
|
63
|
+
export declare function gateConsolidations(consolidations: CuratorConsolidation[], gates: {
|
|
64
|
+
exclude?: ReadonlySet<string>;
|
|
65
|
+
referenced?: ReadonlySet<string>;
|
|
66
|
+
suppressed?: ReadonlySet<string>;
|
|
67
|
+
}): CuratorConsolidation[];
|
|
33
68
|
export declare class EvolutionCurator extends Service {
|
|
34
69
|
static inject: string[];
|
|
35
70
|
static Config: Schema<Config>;
|
|
@@ -45,6 +80,8 @@ export declare class EvolutionCurator extends Service {
|
|
|
45
80
|
private readonly minIdleHours;
|
|
46
81
|
private readonly excludeSkillNames;
|
|
47
82
|
private readonly manageUnmanaged;
|
|
83
|
+
private readonly pruneBuiltins;
|
|
84
|
+
private readonly referencedSkillNames;
|
|
48
85
|
private readonly curatorReviewMaxTokens;
|
|
49
86
|
private lastRun;
|
|
50
87
|
private timer;
|
|
@@ -53,22 +90,71 @@ export declare class EvolutionCurator extends Service {
|
|
|
53
90
|
start(): void;
|
|
54
91
|
stop(): void;
|
|
55
92
|
/**
|
|
56
|
-
* Optional Hermes-curator LLM pass. The model may only NOMINATE pruning
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
93
|
+
* Optional Hermes-curator LLM pass. The model may only NOMINATE pruning and
|
|
94
|
+
* consolidation; every move stays a control-plane operation and each
|
|
95
|
+
* nomination is re-validated against the tree and protected markers before
|
|
96
|
+
* any file move. `dryRun` prepends the report-only banner.
|
|
60
97
|
*/
|
|
61
|
-
recommend(candidates: string[]
|
|
98
|
+
recommend(candidates: string[], options?: {
|
|
99
|
+
dryRun?: boolean;
|
|
100
|
+
}): Promise<CuratorNominations>;
|
|
62
101
|
private skippedReport;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Run one curator pass. `ignoreGates` skips the interval and idle gates so an
|
|
104
|
+
* explicit `/evolution curator run` always executes (manual-run semantics):
|
|
105
|
+
* `dryRun` computes the lifecycle and the LLM nominations but performs no
|
|
106
|
+
* mutation, reports what WOULD happen, and does not push out the next run.
|
|
107
|
+
*/
|
|
108
|
+
run(options?: {
|
|
109
|
+
ignoreGates?: boolean;
|
|
110
|
+
dryRun?: boolean;
|
|
111
|
+
}): Promise<CuratorRunOutcome>;
|
|
112
|
+
/**
|
|
113
|
+
* Seed baseline records for tree skills the sidecar has not seen yet, so
|
|
114
|
+
* their inactivity clock starts now (first-sight defer) and bundled skills
|
|
115
|
+
* become known candidates only when prune-builtins opts them in. Also
|
|
116
|
+
* returns the full active tree names for nomination validation.
|
|
117
|
+
*/
|
|
118
|
+
private seedBaseline;
|
|
119
|
+
/**
|
|
120
|
+
* F13 six-factor quality scoring, persisted onto the usage records.
|
|
121
|
+
*/
|
|
122
|
+
private scoreTree;
|
|
123
|
+
/**
|
|
124
|
+
* In-degree over explicit `related_skills` frontmatter references (the DSH
|
|
125
|
+
* equivalent of the graph-in-degree references factor): a skill listing
|
|
126
|
+
* other skill names counts as one reference to each of them, so hub skills
|
|
127
|
+
* that are explicitly named by peers get a non-zero references factor.
|
|
128
|
+
*/
|
|
129
|
+
private referenceCounts;
|
|
130
|
+
/**
|
|
131
|
+
* Execute lifecycle archives and consolidation nominations, then persist the
|
|
132
|
+
* suppression and usage sidecars best-effort. A dry-run short-circuits: no
|
|
133
|
+
* file moves and no state persistence — the caller still writes the report.
|
|
134
|
+
*/
|
|
135
|
+
private applyMutations;
|
|
70
136
|
private recentSessionActive;
|
|
71
137
|
latestReport(): Promise<CuratorRunReport | null>;
|
|
138
|
+
/**
|
|
139
|
+
* Read-only lifecycle scope classification: which skills are in scope,
|
|
140
|
+
* watched (stale/quality-warned), exempted, or protected. Uses the same
|
|
141
|
+
* candidate gate as `run()` (`computeScopeView` / `lifecycleCandidate`),
|
|
142
|
+
* so the view always predicts what a curator pass may touch.
|
|
143
|
+
*/
|
|
144
|
+
scopeView(): Promise<ScopeView>;
|
|
145
|
+
/** marker info (pinned/bundled/hub-installed) per skill, from the library list. */
|
|
146
|
+
private protectedNameMap;
|
|
147
|
+
/**
|
|
148
|
+
* Control-plane consolidation: merge source skill bodies into `target`,
|
|
149
|
+
* archive the sources with an absorbed-into marker, and fold their usage
|
|
150
|
+
* records into `archived` state. Snapshot-then-mutate, never a hard delete.
|
|
151
|
+
*/
|
|
152
|
+
consolidate(target: string, sources: string[]): Promise<SkillActionResult>;
|
|
153
|
+
/**
|
|
154
|
+
* Control-plane restore: bring one archived skill back to the active root
|
|
155
|
+
* and reset its usage state, keeping the recoverable-archive invariant.
|
|
156
|
+
*/
|
|
157
|
+
restore(name: string): Promise<SkillActionResult>;
|
|
72
158
|
}
|
|
73
159
|
export default EvolutionCurator;
|
|
74
160
|
//# 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.30",
|
|
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.30"
|
|
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.30",
|
|
43
|
+
"@lmzhen/dsh-evolution-state": "^0.1.0-rc.30"
|
|
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.30",
|
|
49
|
+
"@lmzhen/dsh-evolution-io": "^0.1.0-rc.30",
|
|
50
|
+
"@lmzhen/dsh-evolution-state": "^0.1.0-rc.30"
|
|
51
51
|
}
|
|
52
52
|
}
|