@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 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
- - - LLM nomination pass is advisory and disabled by default; deterministic lifecycle remains authoritative.
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(168),
17
- staleAfterDays: z.number().default(30),
18
- archiveAfterDays: z.number().default(90),
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(0),
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 ?? 0;
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
- * candidates; archive/restore remains a control-plane operation and every
86
- * nominated name is still checked against lifecycle thresholds and
87
- * protected markers before any file move.
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
- if (candidates.length === 0) return [];
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
- "Stale candidates observed by the deterministic lifecycle scanner:",
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 a prunings list. Nominate only candidates whose archival is clearly safe."
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 text = assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("\n");
123
- const names = /* @__PURE__ */ new Set();
124
- const section = text.slice(text.indexOf("prunings:"));
125
- for (const [, name] of section.matchAll(/^\s*-\s*name:\s*([a-z0-9][a-z0-9-]*)\s*$/gm)) if (name) names.add(name);
126
- return [...names].filter((name) => candidates.includes(name));
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
- async run() {
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 snapshotPath = await this.skills.snapshotAll("pre-curator-run");
165
- const usage = await loadUsage(root, this.io);
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
- const errors = [];
174
- const archivedSkills = [];
175
- const llmNominations = this.llmReview ? await this.recommend(result.markStale) : [];
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
- for (const name of archiveCandidates) {
178
- const archived = await this.skills.archive(name, "Lifecycle: reached archive threshold");
179
- if (!archived.ok) {
180
- const record = usage.get(name);
181
- if (record) record.state = "active";
182
- errors.push(`${name}: ${archived.message}`);
183
- } else archivedSkills.push({
184
- name,
185
- path: archived.path ?? "",
186
- reason: "Lifecycle: reached archive threshold"
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
- lastRunAt: this.lastRun,
216
- runCount: (persisted?.runCount ?? 0) + 1,
217
- lastSummary: `stale:${result.markStale.length} archived:${archivedSkills.length}`,
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 };
@@ -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
- * candidates; archive/restore remains a control-plane operation and every
58
- * nominated name is still checked against lifecycle thresholds and
59
- * protected markers before any file move.
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[]): Promise<string[]>;
87
+ recommend(candidates: string[], options?: {
88
+ dryRun?: boolean;
89
+ }): Promise<CuratorNominations>;
62
90
  private skippedReport;
63
- run(): Promise<{
64
- stale: string[];
65
- archived: string[];
66
- errors: string[];
67
- report: CuratorRunReport;
68
- skipped?: string;
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.2",
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.2"
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.2",
43
- "@lmzhen/dsh-evolution-state": "^0.1.0-rc.2"
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.2",
49
- "@lmzhen/dsh-evolution-io": "^0.1.0-rc.2",
50
- "@lmzhen/dsh-evolution-state": "^0.1.0-rc.2"
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
  }