@lmzhen/dsh-evolution-curator 0.1.0-rc.6 → 0.1.0-rc.60
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 +7 -1
- package/lib/index.js +495 -71
- package/lib/types/index.d.ts +148 -14
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -28,4 +28,10 @@ Independent of request-prefix construction. This package does not alter the asse
|
|
|
28
28
|
|
|
29
29
|
- `archive` never deletes: skills move to `.archive/` with a `.archive-reason` marker.
|
|
30
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
|
|
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 full state first (`pre-consolidate` / `pre-restore`).
|
|
32
|
+
- `restoreSnapshot` (service) / `/evolution restore` rolls the FULL state back to the latest snapshot: active tree, usage/suppression sidecars, `.archive/` and the curator state carried in the snapshot (`curator-state.json`), so the interval gate does not immediately re-fire after a rollback. The restore itself is undoable — the pre-rollback safety snapshot preserves the current tree plus its state.
|
|
33
|
+
|
|
34
|
+
## Automatic scheduling
|
|
35
|
+
|
|
36
|
+
- `autoStart` (default true) arms an hourly interval check plus a deferred catch-up check `bootGraceSeconds` (default 10) after host boot. Both decide due-ness from the **persisted** `lastRunAt`, so a restart with an overdue schedule runs the first pass within the boot grace instead of waiting a full interval. `bootGraceSeconds: 0` disables the deferral (not recommended: the check may run against a half-mounted host). All scheduling gates — interval, idle, first-run deferral, and the reentrancy guard — remain inside `run()`.
|
|
37
|
+
- `autoStart: false` disables both automatic checks; `/evolution curator run` (manual, gate-skipping) still works.
|
package/lib/index.js
CHANGED
|
@@ -3,25 +3,41 @@ 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, EvolutionGateSet, SkillLibrary, buildCuratorRunReport, computeLifecycleTransitions, computeQualityScores, computeScopeView, emptyRecord, evolutionHome, evolutionIoAdapter, loadSuppressedNames, loadUsage, parseCuratorNominations, relatedSkillNames, renderCuratorReportMarkdown, saveUsage, updateSuppressedNames } 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
|
+
/** Quality-warned skills may turn stale after this many idle days (package-private tunable, P2-8). */
|
|
13
|
+
const DEFAULT_QUALITY_WARN_STALE_AFTER_DAYS = 7;
|
|
14
|
+
/**
|
|
15
|
+
* Block LLM-nominated consolidations that would touch a gate-protected name:
|
|
16
|
+
* exclude / referenced / suppressed skills must never merge (neither as the
|
|
17
|
+
* source being archived nor as the umbrella being edited). Mirrors the control
|
|
18
|
+
* plane's `consolidate()` guard; automatic nominations must pass the same gate.
|
|
19
|
+
*/
|
|
20
|
+
function gateConsolidations(consolidations, gates) {
|
|
21
|
+
const gateSet = gates instanceof EvolutionGateSet ? gates : new EvolutionGateSet(gates);
|
|
22
|
+
return consolidations.filter((n) => !gateSet.isBlocked(n.from) && !gateSet.isBlocked(n.into));
|
|
23
|
+
}
|
|
12
24
|
var EvolutionCurator = class extends Service {
|
|
13
25
|
static inject = ["evolutionIo"];
|
|
14
26
|
static Config = z.object({
|
|
15
27
|
enabled: z.boolean().default(true),
|
|
16
|
-
intervalHours: z.number().default(
|
|
17
|
-
staleAfterDays: z.number().default(
|
|
18
|
-
archiveAfterDays: z.number().default(
|
|
28
|
+
intervalHours: z.number().default(DEFAULT_CURATOR_INTERVAL_HOURS),
|
|
29
|
+
staleAfterDays: z.number().default(DEFAULT_STALE_AFTER_DAYS),
|
|
30
|
+
archiveAfterDays: z.number().default(DEFAULT_ARCHIVE_AFTER_DAYS),
|
|
19
31
|
llmReview: z.boolean().default(false),
|
|
20
32
|
curatorProvider: z.string().default("deepseek-official"),
|
|
21
|
-
qualityWarnStaleAfterDays: z.number().default(
|
|
22
|
-
minIdleHours: z.number().default(
|
|
33
|
+
qualityWarnStaleAfterDays: z.number().default(DEFAULT_QUALITY_WARN_STALE_AFTER_DAYS),
|
|
34
|
+
minIdleHours: z.number().default(DEFAULT_MIN_IDLE_HOURS),
|
|
23
35
|
excludeSkillNames: z.array(z.string()).default([]),
|
|
24
36
|
manageUnmanaged: z.boolean().default(false),
|
|
37
|
+
pruneBuiltins: z.boolean().default(false),
|
|
38
|
+
referencedSkillNames: z.array(z.string()).default([]),
|
|
39
|
+
autoStart: z.boolean().default(true),
|
|
40
|
+
bootGraceSeconds: z.number().default(10),
|
|
25
41
|
curatorReviewMaxTokens: z.number().default(2048)
|
|
26
42
|
});
|
|
27
43
|
skills;
|
|
@@ -36,23 +52,33 @@ var EvolutionCurator = class extends Service {
|
|
|
36
52
|
minIdleHours;
|
|
37
53
|
excludeSkillNames;
|
|
38
54
|
manageUnmanaged;
|
|
55
|
+
pruneBuiltins;
|
|
56
|
+
referencedSkillNames;
|
|
57
|
+
bootGraceSeconds;
|
|
39
58
|
curatorReviewMaxTokens;
|
|
40
59
|
lastRun = 0;
|
|
41
60
|
timer;
|
|
61
|
+
bootCheck;
|
|
62
|
+
running = false;
|
|
42
63
|
constructor(ctx, config = {}) {
|
|
43
64
|
super(ctx, "evolutionCurator");
|
|
44
65
|
this.io = evolutionIoAdapter(() => ctx.evolutionIo.provider());
|
|
45
|
-
this.skills = new SkillLibrary(void 0, this.io)
|
|
66
|
+
this.skills = new SkillLibrary(void 0, this.io, void 0, (event) => {
|
|
67
|
+
this.ctx.emit("evolution/skill-mutated", event);
|
|
68
|
+
});
|
|
46
69
|
this.enabled = config.enabled ?? true;
|
|
47
|
-
this.intervalHours = config.intervalHours ??
|
|
48
|
-
this.staleAfterDays = config.staleAfterDays ??
|
|
49
|
-
this.archiveAfterDays = config.archiveAfterDays ??
|
|
70
|
+
this.intervalHours = config.intervalHours ?? DEFAULT_CURATOR_INTERVAL_HOURS;
|
|
71
|
+
this.staleAfterDays = config.staleAfterDays ?? DEFAULT_STALE_AFTER_DAYS;
|
|
72
|
+
this.archiveAfterDays = config.archiveAfterDays ?? DEFAULT_ARCHIVE_AFTER_DAYS;
|
|
50
73
|
this.llmReview = config.llmReview ?? false;
|
|
51
74
|
this.curatorProvider = config.curatorProvider ?? "deepseek-official";
|
|
52
|
-
this.qualityWarnStaleAfterDays = config.qualityWarnStaleAfterDays ??
|
|
53
|
-
this.minIdleHours = config.minIdleHours ??
|
|
75
|
+
this.qualityWarnStaleAfterDays = config.qualityWarnStaleAfterDays ?? DEFAULT_QUALITY_WARN_STALE_AFTER_DAYS;
|
|
76
|
+
this.minIdleHours = config.minIdleHours ?? DEFAULT_MIN_IDLE_HOURS;
|
|
54
77
|
this.excludeSkillNames = new Set(config.excludeSkillNames ?? []);
|
|
55
78
|
this.manageUnmanaged = config.manageUnmanaged ?? false;
|
|
79
|
+
this.pruneBuiltins = config.pruneBuiltins ?? false;
|
|
80
|
+
this.referencedSkillNames = new Set(config.referencedSkillNames ?? []);
|
|
81
|
+
this.bootGraceSeconds = config.bootGraceSeconds ?? 10;
|
|
56
82
|
this.curatorReviewMaxTokens = config.curatorReviewMaxTokens ?? 2048;
|
|
57
83
|
this.lastRun = Date.now();
|
|
58
84
|
this.ctx.effect(() => {
|
|
@@ -60,6 +86,7 @@ var EvolutionCurator = class extends Service {
|
|
|
60
86
|
this.stop();
|
|
61
87
|
};
|
|
62
88
|
}, "evolution-curator.stop");
|
|
89
|
+
if (config.autoStart ?? true) this.start();
|
|
63
90
|
}
|
|
64
91
|
lifecycle() {
|
|
65
92
|
const snapshot = this.ctx.get("evolutionPolicy")?.get();
|
|
@@ -71,33 +98,81 @@ var EvolutionCurator = class extends Service {
|
|
|
71
98
|
}
|
|
72
99
|
start() {
|
|
73
100
|
if (!this.enabled || this.timer) return;
|
|
74
|
-
this.
|
|
75
|
-
|
|
76
|
-
|
|
101
|
+
this.bootCheck = setTimeout(() => {
|
|
102
|
+
this.bootCheck = void 0;
|
|
103
|
+
this.autoCheck();
|
|
104
|
+
}, this.bootGraceSeconds * 1e3);
|
|
105
|
+
this.bootCheck.unref();
|
|
106
|
+
this.timer = setInterval(() => void this.autoCheck(), 3600 * 1e3);
|
|
77
107
|
this.timer.unref();
|
|
78
108
|
}
|
|
79
109
|
stop() {
|
|
80
110
|
if (this.timer) clearInterval(this.timer);
|
|
81
111
|
this.timer = void 0;
|
|
112
|
+
if (this.bootCheck) clearTimeout(this.bootCheck);
|
|
113
|
+
this.bootCheck = void 0;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Pause or resume automatic curation (B-line G2, Hermes `set_paused`
|
|
117
|
+
* parity): the flag is persisted on the curator state record and the
|
|
118
|
+
* `run()` paused gate skips automatic passes while it holds. Manual runs
|
|
119
|
+
* (`ignoreGates`) are unaffected — pause is a soft stop for the scheduler,
|
|
120
|
+
* not a lock on the operator.
|
|
121
|
+
*
|
|
122
|
+
* Pausing on a state-less curator state seeds the record with `lastRunAt:
|
|
123
|
+
* now`, so a later resume re-enters through the interval gate and defers a
|
|
124
|
+
* full cycle instead of firing immediately (first-run defer interaction,
|
|
125
|
+
* kept deliberately: an unattended resume must not auto-run mid-boot).
|
|
126
|
+
*/
|
|
127
|
+
async setPaused(paused) {
|
|
128
|
+
const stateService = this.curatorStateService();
|
|
129
|
+
const persisted = await stateService?.loadCuratorState() ?? null;
|
|
130
|
+
await stateService?.saveCuratorState({
|
|
131
|
+
schemaVersion: 1,
|
|
132
|
+
lastRunAt: persisted?.lastRunAt ?? Date.now(),
|
|
133
|
+
runCount: persisted?.runCount ?? 0,
|
|
134
|
+
lastSummary: persisted?.lastSummary ?? (paused ? "paused" : "resumed"),
|
|
135
|
+
paused
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/** Current persisted curator state (read-only view for /evolution curator status). */
|
|
139
|
+
async status() {
|
|
140
|
+
return await this.curatorStateService()?.loadCuratorState() ?? null;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* One automatic schedule check: run a pass when the persisted curator state
|
|
144
|
+
* (falling back to the in-memory clock for state-less compositions) is at
|
|
145
|
+
* least one interval old. All gates — interval, idle, first-run defer,
|
|
146
|
+
* reentrancy — stay inside `run()`, so this method only decides whether to
|
|
147
|
+
* wake it, and never duplicates gate logic.
|
|
148
|
+
*/
|
|
149
|
+
async autoCheck() {
|
|
150
|
+
const last = (await this.curatorStateService()?.loadCuratorState())?.lastRunAt ?? this.lastRun;
|
|
151
|
+
if (Date.now() - last >= this.lifecycle().intervalHours * 36e5) await this.run();
|
|
82
152
|
}
|
|
83
153
|
/**
|
|
84
|
-
* Optional Hermes-curator LLM pass. The model may only NOMINATE pruning
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
154
|
+
* Optional Hermes-curator LLM pass. The model may only NOMINATE pruning and
|
|
155
|
+
* consolidation; every move stays a control-plane operation and each
|
|
156
|
+
* nomination is re-validated against the tree and protected markers before
|
|
157
|
+
* any file move. `dryRun` prepends the report-only banner.
|
|
88
158
|
*/
|
|
89
|
-
async recommend(candidates) {
|
|
90
|
-
|
|
159
|
+
async recommend(candidates, options = {}) {
|
|
160
|
+
const empty = {
|
|
161
|
+
prunings: [],
|
|
162
|
+
consolidations: []
|
|
163
|
+
};
|
|
164
|
+
if (candidates.length === 0) return empty;
|
|
91
165
|
const llm = this.ctx.get("llm");
|
|
92
|
-
if (!llm) return
|
|
166
|
+
if (!llm) return empty;
|
|
93
167
|
const model = this.ctx.get("evolutionPolicy")?.get().curatorModel ?? "deepseek-v4-pro";
|
|
94
168
|
const prompt = [
|
|
169
|
+
options.dryRun ? CURATOR_DRY_RUN_BANNER : "",
|
|
95
170
|
CURATOR_PROMPT,
|
|
96
171
|
"",
|
|
97
|
-
|
|
172
|
+
`Stale candidates observed by the deterministic lifecycle scanner:${candidates.length === 0 ? " (none)" : ""}`,
|
|
98
173
|
...candidates.map((name) => `- ${name}`),
|
|
99
174
|
"",
|
|
100
|
-
"Return a YAML summary with
|
|
175
|
+
"Return a YAML summary with consolidations and prunings lists. Nominate only actions whose archival/merge is clearly safe."
|
|
101
176
|
].join("\n");
|
|
102
177
|
try {
|
|
103
178
|
const assembler = new BlockAssembler();
|
|
@@ -116,18 +191,58 @@ var EvolutionCurator = class extends Service {
|
|
|
116
191
|
summary: "curator review"
|
|
117
192
|
}
|
|
118
193
|
})],
|
|
119
|
-
maxTokens: this.curatorReviewMaxTokens
|
|
120
|
-
purpose: "evolution-curator"
|
|
194
|
+
maxTokens: this.curatorReviewMaxTokens
|
|
121
195
|
})) assembler.push(chunk);
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
196
|
+
const parsed = parseCuratorNominations(assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("\n"));
|
|
197
|
+
return {
|
|
198
|
+
prunings: parsed.prunings.filter((name) => candidates.includes(name)),
|
|
199
|
+
consolidations: parsed.consolidations
|
|
200
|
+
};
|
|
127
201
|
} catch {
|
|
128
|
-
return
|
|
202
|
+
return empty;
|
|
129
203
|
}
|
|
130
204
|
}
|
|
205
|
+
/** Optional curator-state service (evolution-state-json / storage-domain). */
|
|
206
|
+
curatorStateService() {
|
|
207
|
+
return this.ctx.get("evolutionState");
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Full-state snapshot: the skills tree plus the current curator state as an
|
|
211
|
+
* `extras/curator-state.json` side file. Every pre-mutation snapshot in the
|
|
212
|
+
* curator goes through here so a later `restoreSnapshot()` can rewind both
|
|
213
|
+
* the tree and the state (Hermes curator_backup backs up `.curator_state`).
|
|
214
|
+
*/
|
|
215
|
+
async snapshotFull(reason = "pre-mutation") {
|
|
216
|
+
const state = await this.curatorStateService()?.loadCuratorState();
|
|
217
|
+
const extras = state === null || state === void 0 ? [] : [{
|
|
218
|
+
name: "curator-state.json",
|
|
219
|
+
content: JSON.stringify(state, null, 2)
|
|
220
|
+
}];
|
|
221
|
+
return await this.skills.snapshotAll(reason, extras);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Full-state rollback: restore the latest snapshot's tree/sidecars/archive
|
|
225
|
+
* AND the curator state it carried. The pre-rollback safety snapshot keeps
|
|
226
|
+
* the current tree plus current state (as extras), so the rollback itself
|
|
227
|
+
* is reversible.
|
|
228
|
+
*/
|
|
229
|
+
async restoreSnapshot() {
|
|
230
|
+
const stateService = this.curatorStateService();
|
|
231
|
+
const currentState = await stateService?.loadCuratorState();
|
|
232
|
+
const extras = currentState === null || currentState === void 0 ? [] : [{
|
|
233
|
+
name: "curator-state.json",
|
|
234
|
+
content: JSON.stringify(currentState, null, 2)
|
|
235
|
+
}];
|
|
236
|
+
const result = await this.skills.restoreLatestSnapshot(extras);
|
|
237
|
+
if (!result.ok) return result;
|
|
238
|
+
const stateExtra = result.extras?.find((extra) => extra.name === "curator-state.json");
|
|
239
|
+
if (stateExtra && stateService) try {
|
|
240
|
+
await stateService.saveCuratorState(JSON.parse(stateExtra.content));
|
|
241
|
+
} catch (error) {
|
|
242
|
+
this.ctx.logger.warn(`evolution-curator: failed to restore curator state: ${error instanceof Error ? error.message : String(error)}`);
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
131
246
|
skippedReport(runId, startedAt) {
|
|
132
247
|
return buildCuratorRunReport({
|
|
133
248
|
runId,
|
|
@@ -137,57 +252,122 @@ var EvolutionCurator = class extends Service {
|
|
|
137
252
|
llmNominations: [],
|
|
138
253
|
archiveCandidates: [],
|
|
139
254
|
archived: [],
|
|
140
|
-
failed: []
|
|
255
|
+
failed: [],
|
|
256
|
+
llmReviewEnabled: this.llmReview
|
|
141
257
|
});
|
|
142
258
|
}
|
|
143
|
-
|
|
259
|
+
/**
|
|
260
|
+
* Run one curator pass. `ignoreGates` skips the interval and idle gates so an
|
|
261
|
+
* explicit `/evolution curator run` always executes (manual-run semantics):
|
|
262
|
+
* `dryRun` computes the lifecycle and the LLM nominations but performs no
|
|
263
|
+
* mutation, reports what WOULD happen, and does not push out the next run.
|
|
264
|
+
* Reentrant calls (autoStart timer + manual command at the same instant) are
|
|
265
|
+
* skipped with an explicit `already-running` outcome.
|
|
266
|
+
*/
|
|
267
|
+
async run(options = {}) {
|
|
268
|
+
if (this.running) return {
|
|
269
|
+
stale: [],
|
|
270
|
+
archived: [],
|
|
271
|
+
errors: [],
|
|
272
|
+
report: this.skippedReport("already-running", (/* @__PURE__ */ new Date()).toISOString()),
|
|
273
|
+
skipped: "already-running"
|
|
274
|
+
};
|
|
275
|
+
this.running = true;
|
|
276
|
+
try {
|
|
277
|
+
return await this.runCore(options);
|
|
278
|
+
} finally {
|
|
279
|
+
this.running = false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async runCore(options = {}) {
|
|
283
|
+
const { ignoreGates = false, dryRun = false } = options;
|
|
144
284
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
145
285
|
const runId = randomUUID();
|
|
146
|
-
const stateService = this.
|
|
286
|
+
const stateService = this.curatorStateService();
|
|
147
287
|
const lifecycle = this.lifecycle();
|
|
148
|
-
const persisted = await stateService?.loadCuratorState();
|
|
149
|
-
if (
|
|
288
|
+
const persisted = await stateService?.loadCuratorState() ?? null;
|
|
289
|
+
if (!ignoreGates && persisted?.paused === true) return {
|
|
290
|
+
stale: [],
|
|
291
|
+
archived: [],
|
|
292
|
+
errors: [],
|
|
293
|
+
report: this.skippedReport(runId, startedAt),
|
|
294
|
+
skipped: "paused"
|
|
295
|
+
};
|
|
296
|
+
if (!ignoreGates && persisted && Date.now() - persisted.lastRunAt < lifecycle.intervalHours * 36e5) return {
|
|
150
297
|
stale: [],
|
|
151
298
|
archived: [],
|
|
152
299
|
errors: [],
|
|
153
300
|
report: this.skippedReport(runId, startedAt),
|
|
154
301
|
skipped: "interval"
|
|
155
302
|
};
|
|
156
|
-
if (this.minIdleHours > 0 && this.recentSessionActive()) return {
|
|
303
|
+
if (!ignoreGates && this.minIdleHours > 0 && this.recentSessionActive()) return {
|
|
157
304
|
stale: [],
|
|
158
305
|
archived: [],
|
|
159
306
|
errors: [],
|
|
160
307
|
report: this.skippedReport(runId, startedAt),
|
|
161
308
|
skipped: "active-session"
|
|
162
309
|
};
|
|
310
|
+
if (!ignoreGates && persisted === null) {
|
|
311
|
+
await stateService?.saveCuratorState({
|
|
312
|
+
schemaVersion: 1,
|
|
313
|
+
lastRunAt: Date.now(),
|
|
314
|
+
runCount: 0,
|
|
315
|
+
lastSummary: "first-run-deferred",
|
|
316
|
+
paused: false
|
|
317
|
+
});
|
|
318
|
+
return {
|
|
319
|
+
stale: [],
|
|
320
|
+
archived: [],
|
|
321
|
+
errors: [],
|
|
322
|
+
report: this.skippedReport(runId, startedAt),
|
|
323
|
+
skipped: "first-run-deferred"
|
|
324
|
+
};
|
|
325
|
+
}
|
|
163
326
|
const root = this.skills.root;
|
|
164
|
-
const
|
|
165
|
-
const usage =
|
|
327
|
+
const rawUsage = await loadUsage(root, this.io);
|
|
328
|
+
const usage = dryRun ? new Map([...rawUsage].map(([name, record]) => [name, { ...record }])) : rawUsage;
|
|
329
|
+
const snapshotPath = dryRun ? void 0 : await this.snapshotFull("pre-curator-run");
|
|
330
|
+
const suppressedNames = new Set(await loadSuppressedNames(root, this.io));
|
|
331
|
+
const gates = new EvolutionGateSet({
|
|
332
|
+
exclude: this.excludeSkillNames,
|
|
333
|
+
referenced: this.referencedSkillNames,
|
|
334
|
+
suppressed: suppressedNames
|
|
335
|
+
});
|
|
336
|
+
const { bundledNames, treeNames } = await this.seedBaseline(usage);
|
|
337
|
+
await this.scoreTree(usage, treeNames);
|
|
166
338
|
const result = computeLifecycleTransitions(usage, {
|
|
167
339
|
staleAfterDays: lifecycle.staleAfterDays,
|
|
168
340
|
archiveAfterDays: lifecycle.archiveAfterDays,
|
|
169
341
|
qualityWarnStaleAfterDays: this.qualityWarnStaleAfterDays,
|
|
170
342
|
excludeSkillNames: this.excludeSkillNames,
|
|
171
|
-
manageUnmanaged: this.manageUnmanaged
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
343
|
+
manageUnmanaged: this.manageUnmanaged,
|
|
344
|
+
pruneBuiltins: this.pruneBuiltins,
|
|
345
|
+
bundledNames,
|
|
346
|
+
suppressedNames,
|
|
347
|
+
referencedSkillNames: this.referencedSkillNames
|
|
348
|
+
}, /* @__PURE__ */ new Date(), gates);
|
|
349
|
+
const nominations = this.llmReview ? await this.recommend(result.markStale, { dryRun }) : {
|
|
350
|
+
prunings: [],
|
|
351
|
+
consolidations: []
|
|
352
|
+
};
|
|
353
|
+
const gatedNominations = {
|
|
354
|
+
...nominations,
|
|
355
|
+
consolidations: gateConsolidations(nominations.consolidations, gates)
|
|
356
|
+
};
|
|
357
|
+
const llmNominations = gatedNominations.prunings;
|
|
176
358
|
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();
|
|
359
|
+
const { archivedSkills, errors, consolidated } = await this.applyMutations({
|
|
360
|
+
dryRun,
|
|
361
|
+
archiveCandidates,
|
|
362
|
+
nominations: gatedNominations,
|
|
363
|
+
treeNames,
|
|
364
|
+
usage,
|
|
365
|
+
bundledNames,
|
|
366
|
+
suppressedNames,
|
|
367
|
+
root,
|
|
368
|
+
failedFrom: new Map(result.transitions.filter((t) => t.to === "archived").map((t) => [t.name, t.from]))
|
|
369
|
+
});
|
|
370
|
+
if (!dryRun) this.lastRun = Date.now();
|
|
191
371
|
const report = buildCuratorRunReport({
|
|
192
372
|
runId,
|
|
193
373
|
startedAt,
|
|
@@ -196,32 +376,193 @@ var EvolutionCurator = class extends Service {
|
|
|
196
376
|
llmNominations,
|
|
197
377
|
archiveCandidates,
|
|
198
378
|
archived: archivedSkills,
|
|
199
|
-
failed: archiveCandidates.filter((name) => errors.some((error) => error.startsWith(`${name}:`))).map((name) => {
|
|
379
|
+
failed: [...new Set([...archiveCandidates, ...gatedNominations.consolidations.map((item) => item.from)])].filter((name) => errors.some((error) => error.startsWith(`${name}:`))).map((name) => {
|
|
200
380
|
return {
|
|
201
381
|
name,
|
|
202
382
|
reason: errors.find((item) => item.startsWith(`${name}:`))?.slice(name.length + 2) ?? "unknown"
|
|
203
383
|
};
|
|
204
384
|
}),
|
|
205
|
-
|
|
385
|
+
consolidated,
|
|
386
|
+
...snapshotPath === void 0 ? {} : { snapshotPath },
|
|
387
|
+
llmReviewEnabled: this.llmReview
|
|
206
388
|
});
|
|
207
389
|
const reportsRoot = join(evolutionHome(), "reports");
|
|
208
390
|
try {
|
|
209
391
|
await this.io.writeText(join(reportsRoot, `curator-${runId}.json`), JSON.stringify(report, null, 2));
|
|
392
|
+
await this.io.writeText(join(reportsRoot, `curator-${runId}.md`), renderCuratorReportMarkdown(report));
|
|
393
|
+
await this.retainReports(20);
|
|
210
394
|
} catch (error) {
|
|
211
395
|
this.ctx.logger.warn(`evolution-curator: failed to persist report ${runId}`);
|
|
212
396
|
this.ctx.logger.warn(error);
|
|
213
397
|
}
|
|
398
|
+
const llmHint = !this.llmReview && result.markStale.length > 0 ? " (llmReview: off - deterministic archive only; set llmReview: true for the LLM merge channel)" : "";
|
|
399
|
+
const summary = `${dryRun ? "dry-run" : "auto"}: stale:${result.markStale.length} archived:${archivedSkills.length} consolidated:${gatedNominations.consolidations.length}${llmHint}`;
|
|
400
|
+
const pausedNow = (await stateService?.loadCuratorState())?.paused ?? false;
|
|
214
401
|
await stateService?.saveCuratorState({
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
402
|
+
schemaVersion: 1,
|
|
403
|
+
lastRunAt: dryRun ? persisted?.lastRunAt ?? this.lastRun : this.lastRun,
|
|
404
|
+
runCount: dryRun ? persisted?.runCount ?? 0 : (persisted?.runCount ?? 0) + 1,
|
|
405
|
+
lastSummary: summary,
|
|
406
|
+
paused: pausedNow
|
|
219
407
|
});
|
|
220
408
|
return {
|
|
221
409
|
stale: result.markStale,
|
|
222
410
|
archived: archivedSkills.map((item) => item.name),
|
|
223
411
|
errors,
|
|
224
|
-
report
|
|
412
|
+
report,
|
|
413
|
+
...this.llmReview ? { nominations: gatedNominations } : {}
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Seed baseline records for tree skills the sidecar has not seen yet, so
|
|
418
|
+
* their inactivity clock starts now (first-sight defer) and bundled skills
|
|
419
|
+
* become known candidates only when prune-builtins opts them in. Also
|
|
420
|
+
* returns the full active tree names for nomination validation.
|
|
421
|
+
*/
|
|
422
|
+
async seedBaseline(usage) {
|
|
423
|
+
const bundledNames = /* @__PURE__ */ new Set();
|
|
424
|
+
const treeNames = /* @__PURE__ */ new Set();
|
|
425
|
+
for (const summary of await this.skills.list()) {
|
|
426
|
+
treeNames.add(summary.name);
|
|
427
|
+
if (!usage.has(summary.name)) usage.set(summary.name, emptyRecord());
|
|
428
|
+
if (await this.skills.isBundled(summary.name)) bundledNames.add(summary.name);
|
|
429
|
+
const record = usage.get(summary.name);
|
|
430
|
+
if (record) record.pinned = await this.skills.isPinned(summary.name);
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
bundledNames,
|
|
434
|
+
treeNames
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* F13 six-factor quality scoring, persisted onto the usage records.
|
|
439
|
+
*/
|
|
440
|
+
async scoreTree(usage, treeNames) {
|
|
441
|
+
const supportDirs = /* @__PURE__ */ new Map();
|
|
442
|
+
for (const name of treeNames) supportDirs.set(name, await this.skills.countSupportDirs(name));
|
|
443
|
+
const quality = computeQualityScores({
|
|
444
|
+
usage,
|
|
445
|
+
supportDirs,
|
|
446
|
+
referenceCounts: await this.referenceCounts(treeNames)
|
|
447
|
+
});
|
|
448
|
+
for (const [name, score] of quality) {
|
|
449
|
+
const record = usage.get(name);
|
|
450
|
+
if (record) {
|
|
451
|
+
record.quality_score = score.score;
|
|
452
|
+
record.quality_warn = score.warn;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* In-degree over explicit `related_skills` frontmatter references (the DSH
|
|
458
|
+
* equivalent of the graph-in-degree references factor): a skill listing
|
|
459
|
+
* other skill names counts as one reference to each of them, so hub skills
|
|
460
|
+
* that are explicitly named by peers get a non-zero references factor.
|
|
461
|
+
*/
|
|
462
|
+
async referenceCounts(treeNames) {
|
|
463
|
+
const counts = /* @__PURE__ */ new Map();
|
|
464
|
+
for (const name of treeNames) {
|
|
465
|
+
const content = await this.skills.read(name);
|
|
466
|
+
if (!content) continue;
|
|
467
|
+
for (const target of relatedSkillNames(content, name)) counts.set(target, (counts.get(target) ?? 0) + 1);
|
|
468
|
+
}
|
|
469
|
+
return counts;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Execute lifecycle archives and consolidation nominations, then persist the
|
|
473
|
+
* suppression and usage sidecars best-effort. A dry-run short-circuits: no
|
|
474
|
+
* file moves and no state persistence — the caller still writes the report.
|
|
475
|
+
*/
|
|
476
|
+
async applyMutations(input) {
|
|
477
|
+
if (input.dryRun) return {
|
|
478
|
+
archivedSkills: [],
|
|
479
|
+
errors: [],
|
|
480
|
+
suppressedChanged: false,
|
|
481
|
+
consolidated: []
|
|
482
|
+
};
|
|
483
|
+
const { archiveCandidates, nominations, treeNames, usage, bundledNames, suppressedNames, root, failedFrom } = input;
|
|
484
|
+
const errors = [];
|
|
485
|
+
const archivedSkills = [];
|
|
486
|
+
const executedConsolidations = [];
|
|
487
|
+
let suppressedChanged = false;
|
|
488
|
+
const suppressedAdded = /* @__PURE__ */ new Set();
|
|
489
|
+
for (const name of archiveCandidates) {
|
|
490
|
+
const archived = await this.skills.archive(name, {
|
|
491
|
+
reason: "Lifecycle: reached archive threshold",
|
|
492
|
+
allowBundled: this.pruneBuiltins
|
|
493
|
+
});
|
|
494
|
+
if (!archived.ok) {
|
|
495
|
+
const record = usage.get(name);
|
|
496
|
+
const from = failedFrom?.get(name);
|
|
497
|
+
if (record && (from === "stale" || from === "active")) record.state = from;
|
|
498
|
+
errors.push(`${name}: ${archived.message}`);
|
|
499
|
+
} else {
|
|
500
|
+
const record = usage.get(name);
|
|
501
|
+
if (record) {
|
|
502
|
+
record.state = "archived";
|
|
503
|
+
record.archived_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
504
|
+
}
|
|
505
|
+
archivedSkills.push({
|
|
506
|
+
name,
|
|
507
|
+
path: archived.path ?? "",
|
|
508
|
+
reason: "Lifecycle: reached archive threshold"
|
|
509
|
+
});
|
|
510
|
+
if (bundledNames.has(name)) {
|
|
511
|
+
suppressedNames.add(name);
|
|
512
|
+
suppressedAdded.add(name);
|
|
513
|
+
suppressedChanged = true;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const alreadyArchived = new Set(archiveCandidates);
|
|
518
|
+
for (const nomination of nominations.consolidations) {
|
|
519
|
+
if (alreadyArchived.has(nomination.from)) continue;
|
|
520
|
+
if (!treeNames.has(nomination.from) || !treeNames.has(nomination.into)) {
|
|
521
|
+
errors.push(`${nomination.from}: consolidation target or source missing from the skill tree`);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
const consolidated = await this.skills.consolidate(nomination.into, [nomination.from], "background_review");
|
|
525
|
+
if (!consolidated.ok) {
|
|
526
|
+
errors.push(`${nomination.from}: ${consolidated.message}`);
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const record = usage.get(nomination.from);
|
|
530
|
+
if (record) {
|
|
531
|
+
record.state = "archived";
|
|
532
|
+
record.archived_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
533
|
+
}
|
|
534
|
+
alreadyArchived.add(nomination.from);
|
|
535
|
+
executedConsolidations.push({
|
|
536
|
+
from: nomination.from,
|
|
537
|
+
into: nomination.into
|
|
538
|
+
});
|
|
539
|
+
archivedSkills.push({
|
|
540
|
+
name: nomination.from,
|
|
541
|
+
path: join(this.skills.root, ".archive", nomination.from),
|
|
542
|
+
reason: `Consolidated into ${nomination.into}`
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
if (suppressedChanged) try {
|
|
546
|
+
await updateSuppressedNames(root, this.io, (current) => {
|
|
547
|
+
for (const name of suppressedAdded) current.add(name);
|
|
548
|
+
});
|
|
549
|
+
} catch {
|
|
550
|
+
this.ctx.logger.warn("evolution-curator: failed to persist suppressed names; archived built-ins may re-enter the lifecycle");
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
await saveUsage(root, usage, this.io);
|
|
554
|
+
} catch {
|
|
555
|
+
this.ctx.logger.warn("evolution-curator: failed to persist usage sidecar");
|
|
556
|
+
}
|
|
557
|
+
const usageRegistry = this.ctx.get("skillUsage");
|
|
558
|
+
try {
|
|
559
|
+
await usageRegistry?.invalidate?.();
|
|
560
|
+
} catch {}
|
|
561
|
+
return {
|
|
562
|
+
archivedSkills,
|
|
563
|
+
errors,
|
|
564
|
+
suppressedChanged,
|
|
565
|
+
consolidated: executedConsolidations
|
|
225
566
|
};
|
|
226
567
|
}
|
|
227
568
|
recentSessionActive() {
|
|
@@ -235,6 +576,42 @@ var EvolutionCurator = class extends Service {
|
|
|
235
576
|
}
|
|
236
577
|
return latest > 0 && Date.now() - latest < this.minIdleHours * 36e5;
|
|
237
578
|
}
|
|
579
|
+
/**
|
|
580
|
+
* Keep only the newest N curator reports, ordered by the report's own
|
|
581
|
+
* `startedAt` (the runId is a UUID and cannot order history). Best-effort
|
|
582
|
+
* like `retainSnapshots`: a failed removal must not fail the run that just
|
|
583
|
+
* persisted its report. The paired `.md` digest is pruned with its JSON.
|
|
584
|
+
*/
|
|
585
|
+
async retainReports(keep) {
|
|
586
|
+
const reportsRoot = join(evolutionHome(), "reports");
|
|
587
|
+
let entries;
|
|
588
|
+
try {
|
|
589
|
+
entries = await this.io.list(reportsRoot);
|
|
590
|
+
} catch {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const dated = [];
|
|
594
|
+
for (const name of entries.filter((entry) => entry.startsWith("curator-") && entry.endsWith(".json"))) try {
|
|
595
|
+
const raw = await this.io.readText(join(reportsRoot, name));
|
|
596
|
+
if (raw === null) continue;
|
|
597
|
+
const parsed = JSON.parse(raw);
|
|
598
|
+
const startedAt = typeof parsed.startedAt === "string" ? Date.parse(parsed.startedAt) : NaN;
|
|
599
|
+
if (Number.isFinite(startedAt)) dated.push({
|
|
600
|
+
name,
|
|
601
|
+
startedAt
|
|
602
|
+
});
|
|
603
|
+
} catch {}
|
|
604
|
+
dated.sort((a, b) => b.startedAt - a.startedAt);
|
|
605
|
+
for (const oldReport of dated.slice(keep)) {
|
|
606
|
+
const stem = oldReport.name.replace(/\.json$/, "");
|
|
607
|
+
try {
|
|
608
|
+
await this.io.remove(join(reportsRoot, oldReport.name));
|
|
609
|
+
} catch {}
|
|
610
|
+
try {
|
|
611
|
+
await this.io.remove(join(reportsRoot, `${stem}.md`));
|
|
612
|
+
} catch {}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
238
615
|
async latestReport() {
|
|
239
616
|
const reportsRoot = join(evolutionHome(), "reports");
|
|
240
617
|
const latest = (await this.io.list(reportsRoot)).filter((name) => name.startsWith("curator-") && name.endsWith(".json")).sort().reverse()[0];
|
|
@@ -248,23 +625,62 @@ var EvolutionCurator = class extends Service {
|
|
|
248
625
|
}
|
|
249
626
|
}
|
|
250
627
|
/**
|
|
628
|
+
* Read-only lifecycle scope classification: which skills are in scope,
|
|
629
|
+
* watched (stale/quality-warned), exempted, or protected. Uses the same
|
|
630
|
+
* candidate gate as `run()` (`computeScopeView` / `lifecycleCandidate`),
|
|
631
|
+
* so the view always predicts what a curator pass may touch.
|
|
632
|
+
*/
|
|
633
|
+
async scopeView() {
|
|
634
|
+
const root = this.skills.root;
|
|
635
|
+
const usage = await loadUsage(root, this.io);
|
|
636
|
+
const { bundledNames } = await this.seedBaseline(usage);
|
|
637
|
+
const gates = new EvolutionGateSet({
|
|
638
|
+
exclude: this.excludeSkillNames,
|
|
639
|
+
referenced: this.referencedSkillNames,
|
|
640
|
+
suppressed: new Set(await loadSuppressedNames(root, this.io))
|
|
641
|
+
});
|
|
642
|
+
return computeScopeView(usage, {
|
|
643
|
+
staleAfterDays: this.lifecycle().staleAfterDays,
|
|
644
|
+
archiveAfterDays: this.lifecycle().archiveAfterDays,
|
|
645
|
+
excludeSkillNames: this.excludeSkillNames,
|
|
646
|
+
referencedSkillNames: this.referencedSkillNames,
|
|
647
|
+
suppressedNames: new Set(gates.suppressed),
|
|
648
|
+
manageUnmanaged: this.manageUnmanaged,
|
|
649
|
+
pruneBuiltins: this.pruneBuiltins,
|
|
650
|
+
bundledNames
|
|
651
|
+
}, await this.protectedNameMap(), gates);
|
|
652
|
+
}
|
|
653
|
+
/** marker info (pinned/bundled/hub-installed) per skill, from the library list. */
|
|
654
|
+
async protectedNameMap() {
|
|
655
|
+
const map = /* @__PURE__ */ new Map();
|
|
656
|
+
for (const summary of await this.skills.list()) if (summary.protectedBy !== null) map.set(summary.name, summary.protectedBy);
|
|
657
|
+
return map;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
251
660
|
* Control-plane consolidation: merge source skill bodies into `target`,
|
|
252
661
|
* archive the sources with an absorbed-into marker, and fold their usage
|
|
253
662
|
* records into `archived` state. Snapshot-then-mutate, never a hard delete.
|
|
254
663
|
*/
|
|
255
664
|
async consolidate(target, sources) {
|
|
256
|
-
const
|
|
665
|
+
const suppressedNames = new Set(await loadSuppressedNames(this.skills.root, this.io));
|
|
666
|
+
const gates = new EvolutionGateSet({
|
|
667
|
+
exclude: this.excludeSkillNames,
|
|
668
|
+
referenced: this.referencedSkillNames,
|
|
669
|
+
suppressed: suppressedNames
|
|
670
|
+
});
|
|
671
|
+
const blocked = [...new Set([target, ...sources])].filter((name) => gates.isBlocked(name));
|
|
257
672
|
if (blocked.length > 0) return {
|
|
258
673
|
ok: false,
|
|
259
|
-
message: `Skill(s)
|
|
674
|
+
message: `Skill(s) protected from consolidation (excluded / referenced / suppressed / protected builtin): ${blocked.join(", ")}`
|
|
260
675
|
};
|
|
261
|
-
await this.
|
|
676
|
+
await this.snapshotFull("pre-consolidate");
|
|
262
677
|
const result = await this.skills.consolidate(target, sources);
|
|
263
678
|
if (!result.ok) return result;
|
|
264
679
|
const usage = await loadUsage(this.skills.root, this.io);
|
|
265
680
|
for (const source of sources) {
|
|
266
681
|
const record = usage.get(source);
|
|
267
682
|
if (record) record.state = "archived";
|
|
683
|
+
if (record) record.archived_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
268
684
|
}
|
|
269
685
|
await saveUsage(this.skills.root, usage, this.io);
|
|
270
686
|
return result;
|
|
@@ -274,15 +690,23 @@ var EvolutionCurator = class extends Service {
|
|
|
274
690
|
* and reset its usage state, keeping the recoverable-archive invariant.
|
|
275
691
|
*/
|
|
276
692
|
async restore(name) {
|
|
277
|
-
await this.
|
|
693
|
+
await this.snapshotFull("pre-restore");
|
|
278
694
|
const result = await this.skills.restoreFromArchive(name);
|
|
279
695
|
if (!result.ok) return result;
|
|
280
696
|
const usage = await loadUsage(this.skills.root, this.io);
|
|
281
697
|
const record = usage.get(name);
|
|
282
698
|
if (record) record.state = "active";
|
|
699
|
+
if (record) record.archived_at = null;
|
|
283
700
|
await saveUsage(this.skills.root, usage, this.io);
|
|
701
|
+
if (new Set(await loadSuppressedNames(this.skills.root, this.io)).has(name)) try {
|
|
702
|
+
await updateSuppressedNames(this.skills.root, this.io, (current) => {
|
|
703
|
+
current.delete(name);
|
|
704
|
+
});
|
|
705
|
+
} catch {
|
|
706
|
+
this.ctx.logger.warn(`evolution-curator: failed to persist suppressed names after restoring ${name}`);
|
|
707
|
+
}
|
|
284
708
|
return result;
|
|
285
709
|
}
|
|
286
710
|
};
|
|
287
711
|
//#endregion
|
|
288
|
-
export { EvolutionCurator, EvolutionCurator as default };
|
|
712
|
+
export { EvolutionCurator, EvolutionCurator as default, gateConsolidations };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
6
6
|
import type Schema from '@deepseek-ai/schemastery';
|
|
7
|
-
import { SkillLibrary } from '@deepseek-ai/dsh-evolution-core';
|
|
8
|
-
import { type CuratorRunReport, type SkillActionResult } from '@deepseek-ai/dsh-evolution-core';
|
|
7
|
+
import { EvolutionGateSet, SkillLibrary } 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,46 @@ 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;
|
|
36
|
+
/** Seconds between host boot and the first automatic schedule check (restart catch-up). */
|
|
37
|
+
bootGraceSeconds?: number;
|
|
30
38
|
/** Max tokens for the optional LLM nomination pass. */
|
|
31
39
|
curatorReviewMaxTokens?: number;
|
|
32
40
|
}
|
|
41
|
+
/** Outcome of one curator run pass. */
|
|
42
|
+
export interface CuratorRunOutcome {
|
|
43
|
+
stale: string[];
|
|
44
|
+
archived: string[];
|
|
45
|
+
errors: string[];
|
|
46
|
+
report: CuratorRunReport;
|
|
47
|
+
skipped?: string;
|
|
48
|
+
/** LLM nominations when the optional review pass is enabled (audit visibility). */
|
|
49
|
+
nominations?: CuratorNominations;
|
|
50
|
+
}
|
|
51
|
+
/** Persisted curator-state record shape (schemaVersion optional for legacy reads). */
|
|
52
|
+
export interface CuratorStateRecordShape {
|
|
53
|
+
schemaVersion?: number;
|
|
54
|
+
lastRunAt: number;
|
|
55
|
+
runCount: number;
|
|
56
|
+
lastSummary: string;
|
|
57
|
+
paused: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Block LLM-nominated consolidations that would touch a gate-protected name:
|
|
61
|
+
* exclude / referenced / suppressed skills must never merge (neither as the
|
|
62
|
+
* source being archived nor as the umbrella being edited). Mirrors the control
|
|
63
|
+
* plane's `consolidate()` guard; automatic nominations must pass the same gate.
|
|
64
|
+
*/
|
|
65
|
+
export declare function gateConsolidations(consolidations: CuratorConsolidation[], gates: EvolutionGateSet | {
|
|
66
|
+
exclude?: ReadonlySet<string>;
|
|
67
|
+
referenced?: ReadonlySet<string>;
|
|
68
|
+
suppressed?: ReadonlySet<string>;
|
|
69
|
+
}): CuratorConsolidation[];
|
|
33
70
|
export declare class EvolutionCurator extends Service {
|
|
34
71
|
static inject: string[];
|
|
35
72
|
static Config: Schema<Config>;
|
|
@@ -45,30 +82,127 @@ export declare class EvolutionCurator extends Service {
|
|
|
45
82
|
private readonly minIdleHours;
|
|
46
83
|
private readonly excludeSkillNames;
|
|
47
84
|
private readonly manageUnmanaged;
|
|
85
|
+
private readonly pruneBuiltins;
|
|
86
|
+
private readonly referencedSkillNames;
|
|
87
|
+
private readonly bootGraceSeconds;
|
|
48
88
|
private readonly curatorReviewMaxTokens;
|
|
49
89
|
private lastRun;
|
|
50
90
|
private timer;
|
|
91
|
+
private bootCheck;
|
|
92
|
+
private running;
|
|
51
93
|
constructor(ctx: Context, config?: Config);
|
|
52
94
|
private lifecycle;
|
|
53
95
|
start(): void;
|
|
54
96
|
stop(): void;
|
|
55
97
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
98
|
+
* Pause or resume automatic curation (B-line G2, Hermes `set_paused`
|
|
99
|
+
* parity): the flag is persisted on the curator state record and the
|
|
100
|
+
* `run()` paused gate skips automatic passes while it holds. Manual runs
|
|
101
|
+
* (`ignoreGates`) are unaffected — pause is a soft stop for the scheduler,
|
|
102
|
+
* not a lock on the operator.
|
|
103
|
+
*
|
|
104
|
+
* Pausing on a state-less curator state seeds the record with `lastRunAt:
|
|
105
|
+
* now`, so a later resume re-enters through the interval gate and defers a
|
|
106
|
+
* full cycle instead of firing immediately (first-run defer interaction,
|
|
107
|
+
* kept deliberately: an unattended resume must not auto-run mid-boot).
|
|
60
108
|
*/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
109
|
+
setPaused(paused: boolean): Promise<void>;
|
|
110
|
+
/** Current persisted curator state (read-only view for /evolution curator status). */
|
|
111
|
+
status(): Promise<CuratorStateRecordShape | null>;
|
|
112
|
+
/**
|
|
113
|
+
* One automatic schedule check: run a pass when the persisted curator state
|
|
114
|
+
* (falling back to the in-memory clock for state-less compositions) is at
|
|
115
|
+
* least one interval old. All gates — interval, idle, first-run defer,
|
|
116
|
+
* reentrancy — stay inside `run()`, so this method only decides whether to
|
|
117
|
+
* wake it, and never duplicates gate logic.
|
|
118
|
+
*/
|
|
119
|
+
private autoCheck;
|
|
120
|
+
/**
|
|
121
|
+
* Optional Hermes-curator LLM pass. The model may only NOMINATE pruning and
|
|
122
|
+
* consolidation; every move stays a control-plane operation and each
|
|
123
|
+
* nomination is re-validated against the tree and protected markers before
|
|
124
|
+
* any file move. `dryRun` prepends the report-only banner.
|
|
125
|
+
*/
|
|
126
|
+
recommend(candidates: string[], options?: {
|
|
127
|
+
dryRun?: boolean;
|
|
128
|
+
}): Promise<CuratorNominations>;
|
|
129
|
+
/** Optional curator-state service (evolution-state-json / storage-domain). */
|
|
130
|
+
private curatorStateService;
|
|
131
|
+
/**
|
|
132
|
+
* Full-state snapshot: the skills tree plus the current curator state as an
|
|
133
|
+
* `extras/curator-state.json` side file. Every pre-mutation snapshot in the
|
|
134
|
+
* curator goes through here so a later `restoreSnapshot()` can rewind both
|
|
135
|
+
* the tree and the state (Hermes curator_backup backs up `.curator_state`).
|
|
136
|
+
*/
|
|
137
|
+
snapshotFull(reason?: string): Promise<string>;
|
|
138
|
+
/**
|
|
139
|
+
* Full-state rollback: restore the latest snapshot's tree/sidecars/archive
|
|
140
|
+
* AND the curator state it carried. The pre-rollback safety snapshot keeps
|
|
141
|
+
* the current tree plus current state (as extras), so the rollback itself
|
|
142
|
+
* is reversible.
|
|
143
|
+
*/
|
|
144
|
+
restoreSnapshot(): Promise<SkillActionResult & {
|
|
145
|
+
extras?: Array<{
|
|
146
|
+
name: string;
|
|
147
|
+
content: string;
|
|
148
|
+
}>;
|
|
69
149
|
}>;
|
|
150
|
+
private skippedReport;
|
|
151
|
+
/**
|
|
152
|
+
* Run one curator pass. `ignoreGates` skips the interval and idle gates so an
|
|
153
|
+
* explicit `/evolution curator run` always executes (manual-run semantics):
|
|
154
|
+
* `dryRun` computes the lifecycle and the LLM nominations but performs no
|
|
155
|
+
* mutation, reports what WOULD happen, and does not push out the next run.
|
|
156
|
+
* Reentrant calls (autoStart timer + manual command at the same instant) are
|
|
157
|
+
* skipped with an explicit `already-running` outcome.
|
|
158
|
+
*/
|
|
159
|
+
run(options?: {
|
|
160
|
+
ignoreGates?: boolean;
|
|
161
|
+
dryRun?: boolean;
|
|
162
|
+
}): Promise<CuratorRunOutcome>;
|
|
163
|
+
private runCore;
|
|
164
|
+
/**
|
|
165
|
+
* Seed baseline records for tree skills the sidecar has not seen yet, so
|
|
166
|
+
* their inactivity clock starts now (first-sight defer) and bundled skills
|
|
167
|
+
* become known candidates only when prune-builtins opts them in. Also
|
|
168
|
+
* returns the full active tree names for nomination validation.
|
|
169
|
+
*/
|
|
170
|
+
private seedBaseline;
|
|
171
|
+
/**
|
|
172
|
+
* F13 six-factor quality scoring, persisted onto the usage records.
|
|
173
|
+
*/
|
|
174
|
+
private scoreTree;
|
|
175
|
+
/**
|
|
176
|
+
* In-degree over explicit `related_skills` frontmatter references (the DSH
|
|
177
|
+
* equivalent of the graph-in-degree references factor): a skill listing
|
|
178
|
+
* other skill names counts as one reference to each of them, so hub skills
|
|
179
|
+
* that are explicitly named by peers get a non-zero references factor.
|
|
180
|
+
*/
|
|
181
|
+
private referenceCounts;
|
|
182
|
+
/**
|
|
183
|
+
* Execute lifecycle archives and consolidation nominations, then persist the
|
|
184
|
+
* suppression and usage sidecars best-effort. A dry-run short-circuits: no
|
|
185
|
+
* file moves and no state persistence — the caller still writes the report.
|
|
186
|
+
*/
|
|
187
|
+
private applyMutations;
|
|
70
188
|
private recentSessionActive;
|
|
189
|
+
/**
|
|
190
|
+
* Keep only the newest N curator reports, ordered by the report's own
|
|
191
|
+
* `startedAt` (the runId is a UUID and cannot order history). Best-effort
|
|
192
|
+
* like `retainSnapshots`: a failed removal must not fail the run that just
|
|
193
|
+
* persisted its report. The paired `.md` digest is pruned with its JSON.
|
|
194
|
+
*/
|
|
195
|
+
private retainReports;
|
|
71
196
|
latestReport(): Promise<CuratorRunReport | null>;
|
|
197
|
+
/**
|
|
198
|
+
* Read-only lifecycle scope classification: which skills are in scope,
|
|
199
|
+
* watched (stale/quality-warned), exempted, or protected. Uses the same
|
|
200
|
+
* candidate gate as `run()` (`computeScopeView` / `lifecycleCandidate`),
|
|
201
|
+
* so the view always predicts what a curator pass may touch.
|
|
202
|
+
*/
|
|
203
|
+
scopeView(): Promise<ScopeView>;
|
|
204
|
+
/** marker info (pinned/bundled/hub-installed) per skill, from the library list. */
|
|
205
|
+
private protectedNameMap;
|
|
72
206
|
/**
|
|
73
207
|
* Control-plane consolidation: merge source skill bodies into `target`,
|
|
74
208
|
* archive the sources with an absorbed-into marker, and fold their usage
|
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.60",
|
|
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.60"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
40
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
41
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
42
|
-
"@lmzhen/dsh-evolution-io": "^0.1.0-rc.
|
|
43
|
-
"@lmzhen/dsh-evolution-state": "^0.1.0-rc.
|
|
40
|
+
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
41
|
+
"@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
|
|
42
|
+
"@lmzhen/dsh-evolution-io": "^0.1.0-rc.60",
|
|
43
|
+
"@lmzhen/dsh-evolution-state": "^0.1.0-rc.60"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
47
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
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.
|
|
46
|
+
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
|
|
48
|
+
"@lmzhen/dsh-evolution-core": "^0.1.0-rc.60",
|
|
49
|
+
"@lmzhen/dsh-evolution-io": "^0.1.0-rc.60",
|
|
50
|
+
"@lmzhen/dsh-evolution-state": "^0.1.0-rc.60"
|
|
51
51
|
}
|
|
52
52
|
}
|