@lmzhen/dsh-evolution-curator 0.4.1 → 0.6.0

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
@@ -26,6 +26,9 @@ Deterministic skill lifecycle and recovery
26
26
 
27
27
  - `autoStart` (default true) arms an HOURLY tick that only asks whether the due-ness interval (`intervalHours`, default 168 h) has elapsed — the tick is not the interval — 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()`.
28
28
  - `autoStart: false` disables both automatic checks; `/evolution curator run` (manual, gate-skipping) still works.
29
+ - Deprecated name (G0/S0.3): `intervalHours` is the legacy spelling of the policy
30
+ row's `curatorIntervalHours`, whose value the snapshot shadows. Reading it still
31
+ works; writing it is refused, and it is removed in 0.7.0.
29
32
 
30
33
  **Runtime invariant:** No companion is published. The platform auto-assembles nothing and the family mounts no `<pkg>/invariant` cordis row, so a companion here would never execute (v37 S2.1 / I-3).
31
34
  ## Notes and history
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_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MIN_IDLE_HOURS, DEFAULT_STALE_AFTER_DAYS, EvolutionGateSet, INSTANCE_KEYS, MAX_TIMER_DELAY_MS, SKILL_NAME_RE, buildCuratorRunReport, claimInstance, clampedNumber, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, emptyRecord, evolutionHome, evolutionIoAdapter, foldCuratorFields, isPresent, isUnknown, loadSuppressedNames, loadUsage, markerEntryName, mutateUsage, newSkillLibrary, parseCuratorNominations, parseFrontmatter, probeList, probeMtime, relatedSkillNames, releaseInstance, renderCuratorReportMarkdown, transactIo, updateSuppressedNames, usageObserved } from "@lmzhen/dsh-evolution-core";
6
+ import { CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MIN_IDLE_HOURS, DEFAULT_SKILL_LIMITS, DEFAULT_STALE_AFTER_DAYS, EvolutionGateSet, INSTANCE_KEYS, MAX_TIMER_DELAY_MS, PARAM_NAMESPACES, SKILL_NAME_RE, buildCuratorRunReport, claimInstance, clampedNumber, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, emptyRecord, evolutionHome, evolutionIoAdapter, foldCuratorFields, installParamSection, isPresent, isUnknown, loadSuppressedNames, loadUsage, markerEntryName, mutateUsage, newSkillLibrary, parseCuratorNominations, parseFrontmatter, policyStageLimits, probeList, probeMtime, readNumberParam, relatedSkillNames, releaseInstance, renderCuratorReportMarkdown, transactIo, updateSuppressedNames, usageObserved } 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.
@@ -17,6 +17,36 @@ const DEFAULT_QUALITY_WARN_STALE_AFTER_DAYS = 7;
17
17
  * 120s matches the review subagent default; the 32-bit ceiling is Node's
18
18
  * timer-delay limit (`AbortSignal.timeout` throws above it). */
19
19
  const DEFAULT_CURATOR_REVIEW_TIMEOUT_MS = 12e4;
20
+ /** Namespace the curator group's user-writable knobs live in (core's PARAM_NAMESPACES). */
21
+ const CURATOR_SETTINGS_NAMESPACE = "evolution-curator";
22
+ /** Schema the platform validates the user layer against; defaults mirror the core
23
+ * constants and the bounds mirror the row schema, so an empty document resolves to
24
+ * today's behaviour and an out-of-range value is refused instead of clamped. */
25
+ const CURATOR_SETTINGS_SCHEMA = z.object({
26
+ curatorIntervalHours: z.number().min(1).default(DEFAULT_CURATOR_INTERVAL_HOURS),
27
+ staleAfterDays: z.number().min(1).default(DEFAULT_STALE_AFTER_DAYS),
28
+ archiveAfterDays: z.number().min(1).default(DEFAULT_ARCHIVE_AFTER_DAYS),
29
+ qualityWarnStaleAfterDays: z.number().min(1).default(DEFAULT_QUALITY_WARN_STALE_AFTER_DAYS),
30
+ minIdleHours: z.number().min(0).default(DEFAULT_MIN_IDLE_HOURS),
31
+ minIdleFailOpen: z.boolean().default(true),
32
+ llmReview: z.boolean().default(false),
33
+ curatorReviewMaxTokens: z.number().min(1).default(DEFAULT_CURATOR_REVIEW_MAX_TOKENS),
34
+ curatorReviewTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS).default(DEFAULT_CURATOR_REVIEW_TIMEOUT_MS),
35
+ healthSoftBodyChars: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.softBodyChars),
36
+ healthStampDensityPerKb: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb),
37
+ healthChurnMinPatches: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.churnMinPatches)
38
+ });
39
+ /**
40
+ * The cross-field rule the schema cannot express (A2-17): an archive threshold
41
+ * below the stale threshold makes the engine reactivate stale records instead of
42
+ * archiving them, so a resolved pair that violates it is refused at WRITE time.
43
+ * The runtime keeps its clamp for the carriers this hook never sees (the policy
44
+ * snapshot and the plugin row).
45
+ * @param value - the resolved section the platform hands the owner.
46
+ */
47
+ function validateCuratorSettings(value) {
48
+ if (value.archiveAfterDays < value.staleAfterDays) throw new Error(`archiveAfterDays (${value.archiveAfterDays}) must be >= staleAfterDays (${value.staleAfterDays})`);
49
+ }
20
50
  /**
21
51
  * Block LLM-nominated consolidations that would touch a gate-protected name:
22
52
  * exclude / referenced / suppressed skills must never merge (neither as the
@@ -76,6 +106,10 @@ var EvolutionCurator = class extends Service {
76
106
  healthSoftBodyChars;
77
107
  healthStampDensityPerKb;
78
108
  healthChurnMinPatches;
109
+ /** G3/S3.3: this row's resolved values — the base layer of the settings section. */
110
+ settingsBase;
111
+ /** The user layer for this row's namespace (absent provider = deployment values). */
112
+ overrides;
79
113
  lastRun = 0;
80
114
  timer;
81
115
  /** B-8 (v18): set by the fiber disposer; a triggered autoCheck must not
@@ -110,7 +144,11 @@ var EvolutionCurator = class extends Service {
110
144
  this.skills = newSkillLibrary({
111
145
  config,
112
146
  io: this.io,
113
- ctx: this.ctx
147
+ ctx: this.ctx,
148
+ limits: {
149
+ ...DEFAULT_SKILL_LIMITS,
150
+ ...policyStageLimits(this.ctx.get("evolutionPolicy")?.get?.())
151
+ }
114
152
  });
115
153
  this.enabled = config.enabled ?? true;
116
154
  this.instanceHome = evolutionHome();
@@ -126,7 +164,7 @@ var EvolutionCurator = class extends Service {
126
164
  if (value !== void 0 && result !== value) clamped.push(name);
127
165
  return result;
128
166
  };
129
- this.intervalHours = field("intervalHours", config.intervalHours, DEFAULT_CURATOR_INTERVAL_HOURS, 1);
167
+ this.intervalHours = field("intervalHours", readNumberParam(config, "curatorIntervalHours"), DEFAULT_CURATOR_INTERVAL_HOURS, 1);
130
168
  this.staleAfterDays = field("staleAfterDays", config.staleAfterDays, DEFAULT_STALE_AFTER_DAYS, 1);
131
169
  this.archiveAfterDays = field("archiveAfterDays", config.archiveAfterDays, DEFAULT_ARCHIVE_AFTER_DAYS, 1);
132
170
  if (this.archiveAfterDays < this.staleAfterDays) {
@@ -149,6 +187,26 @@ var EvolutionCurator = class extends Service {
149
187
  this.healthStampDensityPerKb = field("healthStampDensityPerKb", config.healthStampDensityPerKb, DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb, 1);
150
188
  this.healthChurnMinPatches = field("healthChurnMinPatches", config.healthChurnMinPatches, DEFAULT_HEALTH_THRESHOLDS.churnMinPatches, 1);
151
189
  if (clamped.length > 0) this.ctx.logger.warn(`evolution-curator: ${clamped.join(", ")} provided an invalid value; falling back to the default`);
190
+ this.settingsBase = {
191
+ curatorIntervalHours: this.intervalHours,
192
+ staleAfterDays: this.staleAfterDays,
193
+ archiveAfterDays: this.archiveAfterDays,
194
+ qualityWarnStaleAfterDays: this.qualityWarnStaleAfterDays,
195
+ minIdleHours: this.minIdleHours,
196
+ minIdleFailOpen: this.minIdleFailOpen,
197
+ llmReview: this.llmReview,
198
+ curatorReviewMaxTokens: this.curatorReviewMaxTokens,
199
+ curatorReviewTimeoutMs: this.curatorReviewTimeoutMs,
200
+ healthSoftBodyChars: this.healthSoftBodyChars,
201
+ healthStampDensityPerKb: this.healthStampDensityPerKb,
202
+ healthChurnMinPatches: this.healthChurnMinPatches
203
+ };
204
+ this.overrides = installParamSection(ctx, PARAM_NAMESPACES["evolution-curator"] ?? "evolution-curator", CURATOR_SETTINGS_SCHEMA, this.settingsBase, {
205
+ warn: (message) => {
206
+ this.ctx.logger.warn("dsh-evolution-curator: " + message);
207
+ },
208
+ validate: validateCuratorSettings
209
+ });
152
210
  this.lastRun = Date.now();
153
211
  this.ctx.effect(() => {
154
212
  return () => {
@@ -159,17 +217,58 @@ var EvolutionCurator = class extends Service {
159
217
  }, "evolution-curator.stop");
160
218
  if (config.autoStart ?? true) this.start();
161
219
  }
220
+ /** The policy-snapshot carrier for this group (unmounted service = no policy). */
221
+ policyFields() {
222
+ return this.ctx.get("evolutionPolicy")?.get() ?? {};
223
+ }
224
+ /**
225
+ * G3/S3.3: the ONE reader of the curator's behaviour knobs. Precedence is
226
+ * user > policy snapshot > this row, and {@link ParamOverrides.get} returns
227
+ * undefined for a key the user never set, so the deployment carriers keep
228
+ * winning exactly as before. Read at USE time: a committed settings change is
229
+ * live without a restart and without a watcher.
230
+ * @returns the resolved section, every numeric field sanitized.
231
+ */
232
+ settings() {
233
+ const snapshot = this.policyFields();
234
+ const row = this.settingsBase;
235
+ const pick = (key) => {
236
+ const user = this.overrides.get(key);
237
+ if (user !== void 0) return user;
238
+ return snapshot[key] ?? row[key];
239
+ };
240
+ const number = (key, min, max) => {
241
+ const lower = row[key];
242
+ return clampedNumber(pick(key), lower, max === void 0 ? { min } : {
243
+ min,
244
+ max
245
+ });
246
+ };
247
+ return {
248
+ curatorIntervalHours: number("curatorIntervalHours", 1),
249
+ staleAfterDays: number("staleAfterDays", 1),
250
+ archiveAfterDays: number("archiveAfterDays", 1),
251
+ qualityWarnStaleAfterDays: number("qualityWarnStaleAfterDays", 1),
252
+ minIdleHours: number("minIdleHours", 0),
253
+ minIdleFailOpen: pick("minIdleFailOpen"),
254
+ llmReview: pick("llmReview"),
255
+ curatorReviewMaxTokens: number("curatorReviewMaxTokens", 1),
256
+ curatorReviewTimeoutMs: number("curatorReviewTimeoutMs", 1, MAX_TIMER_DELAY_MS),
257
+ healthSoftBodyChars: number("healthSoftBodyChars", 1),
258
+ healthStampDensityPerKb: number("healthStampDensityPerKb", 1),
259
+ healthChurnMinPatches: number("healthChurnMinPatches", 1)
260
+ };
261
+ }
162
262
  lifecycle() {
163
- const snapshot = this.ctx.get("evolutionPolicy")?.get();
164
- const staleAfterDays = snapshot?.staleAfterDays ?? this.staleAfterDays;
165
- let archiveAfterDays = snapshot?.archiveAfterDays ?? this.archiveAfterDays;
166
- if (archiveAfterDays < staleAfterDays) {
167
- this.ctx.logger.warn(`evolution-curator: policy archiveAfterDays (${archiveAfterDays}) < policy staleAfterDays (${staleAfterDays}); using the stale threshold as the archive threshold`);
168
- archiveAfterDays = staleAfterDays;
263
+ const settings = this.settings();
264
+ let archiveAfterDays = settings.archiveAfterDays;
265
+ if (archiveAfterDays < settings.staleAfterDays) {
266
+ this.ctx.logger.warn(`evolution-curator: archiveAfterDays (${archiveAfterDays}) < staleAfterDays (${settings.staleAfterDays}); using the stale threshold as the archive threshold`);
267
+ archiveAfterDays = settings.staleAfterDays;
169
268
  }
170
269
  return {
171
- intervalHours: snapshot?.curatorIntervalHours ?? this.intervalHours,
172
- staleAfterDays,
270
+ intervalHours: settings.curatorIntervalHours,
271
+ staleAfterDays: settings.staleAfterDays,
173
272
  archiveAfterDays
174
273
  };
175
274
  }
@@ -303,6 +402,7 @@ var EvolutionCurator = class extends Service {
303
402
  "",
304
403
  "Return a YAML summary with consolidations and prunings lists. Nominate only actions whose archival/merge is clearly safe."
305
404
  ].join("\n");
405
+ const settings = this.settings();
306
406
  try {
307
407
  const assembler = new BlockAssembler();
308
408
  for await (const chunk of llm.stream({
@@ -320,8 +420,8 @@ var EvolutionCurator = class extends Service {
320
420
  summary: "curator review"
321
421
  }
322
422
  })],
323
- maxTokens: this.curatorReviewMaxTokens,
324
- signal: AbortSignal.timeout(this.curatorReviewTimeoutMs)
423
+ maxTokens: settings.curatorReviewMaxTokens,
424
+ signal: AbortSignal.timeout(settings.curatorReviewTimeoutMs)
325
425
  })) assembler.push(chunk);
326
426
  const parsed = parseCuratorNominations(assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("\n"));
327
427
  return {
@@ -394,7 +494,7 @@ var EvolutionCurator = class extends Service {
394
494
  archiveCandidates: [],
395
495
  archived: [],
396
496
  failed: [],
397
- llmReviewEnabled: this.llmReview
497
+ llmReviewEnabled: this.settings().llmReview
398
498
  });
399
499
  }
400
500
  /**
@@ -502,6 +602,7 @@ var EvolutionCurator = class extends Service {
502
602
  const runId = randomUUID();
503
603
  const stateService = this.curatorStateService();
504
604
  const lifecycle = this.lifecycle();
605
+ const settings = this.settings();
505
606
  const persisted = await stateService?.loadCuratorState() ?? null;
506
607
  if (!ignoreGates && persisted?.paused === true) return {
507
608
  stale: [],
@@ -517,7 +618,7 @@ var EvolutionCurator = class extends Service {
517
618
  report: this.skippedReport(runId, startedAt),
518
619
  skipped: "interval"
519
620
  };
520
- if (!ignoreGates && this.minIdleHours > 0 && this.recentSessionActive()) return {
621
+ if (!ignoreGates && settings.minIdleHours > 0 && this.recentSessionActive()) return {
521
622
  stale: [],
522
623
  archived: [],
523
624
  errors: [],
@@ -580,7 +681,7 @@ var EvolutionCurator = class extends Service {
580
681
  const result = computeLifecycleTransitions(usage, {
581
682
  staleAfterDays: lifecycle.staleAfterDays,
582
683
  archiveAfterDays: lifecycle.archiveAfterDays,
583
- qualityWarnStaleAfterDays: this.qualityWarnStaleAfterDays,
684
+ qualityWarnStaleAfterDays: settings.qualityWarnStaleAfterDays,
584
685
  excludeSkillNames: this.excludeSkillNames,
585
686
  manageUnmanaged: this.manageUnmanaged,
586
687
  pruneBuiltins: this.pruneBuiltins,
@@ -589,7 +690,7 @@ var EvolutionCurator = class extends Service {
589
690
  referencedSkillNames: this.referencedSkillNames
590
691
  }, /* @__PURE__ */ new Date(), gates, protectedNames);
591
692
  const recommendPool = [...new Set([...result.markStale, ...dedupMembers])].filter((name) => SKILL_NAME_RE.test(name));
592
- const nominations = this.llmReview ? await this.recommend(recommendPool, { dryRun }) : {
693
+ const nominations = settings.llmReview ? await this.recommend(recommendPool, { dryRun }) : {
593
694
  prunings: [],
594
695
  consolidations: [],
595
696
  warnings: []
@@ -601,7 +702,7 @@ var EvolutionCurator = class extends Service {
601
702
  };
602
703
  const llmNominations = gatedNominations.prunings;
603
704
  const archiveCandidates = [...new Set([...result.archive, ...llmNominations])];
604
- if (!ignoreGates && this.minIdleHours > 0 && this.recentSessionActive()) {
705
+ if (!ignoreGates && settings.minIdleHours > 0 && this.recentSessionActive()) {
605
706
  if (!dryRun) try {
606
707
  await mutateUsage(root, this.io, (map) => {
607
708
  for (const [name, record] of usage) if (!map.has(name)) map.set(name, { ...record });
@@ -618,7 +719,7 @@ var EvolutionCurator = class extends Service {
618
719
  };
619
720
  }
620
721
  const snapshotPath = dryRun ? void 0 : await this.snapshotFull("pre-curator-run");
621
- if (!ignoreGates && this.minIdleHours > 0 && this.recentSessionActive()) {
722
+ if (!ignoreGates && settings.minIdleHours > 0 && this.recentSessionActive()) {
622
723
  if (!dryRun) try {
623
724
  await mutateUsage(root, this.io, (map) => {
624
725
  for (const [name, record] of usage) if (!map.has(name)) map.set(name, { ...record });
@@ -654,10 +755,13 @@ var EvolutionCurator = class extends Service {
654
755
  });
655
756
  const runAborted = errors.some((error) => error.startsWith("run aborted"));
656
757
  if (!dryRun && !runAborted) this.lastRun = Date.now();
758
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
759
+ const wouldPrune = this.skills.archiveRetentionPolicy() === "report" ? await this.skills.expiredArchives() : void 0;
657
760
  const report = buildCuratorRunReport({
658
761
  runId,
659
762
  startedAt,
660
- finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
763
+ finishedAt,
764
+ ...wouldPrune === void 0 ? {} : { wouldPrune },
661
765
  staleCandidates: result.markStale,
662
766
  llmNominations,
663
767
  archiveCandidates,
@@ -679,7 +783,7 @@ var EvolutionCurator = class extends Service {
679
783
  })(),
680
784
  consolidated,
681
785
  ...snapshotPath === void 0 ? {} : { snapshotPath },
682
- llmReviewEnabled: this.llmReview,
786
+ llmReviewEnabled: settings.llmReview,
683
787
  ...gatedNominations.warnings.length > 0 ? { nominationsWarnings: gatedNominations.warnings } : {}
684
788
  });
685
789
  const reportsRoot = join(evolutionHome(), "reports");
@@ -690,7 +794,7 @@ var EvolutionCurator = class extends Service {
690
794
  this.ctx.logger.warn(`evolution-curator: failed to persist report ${runId}`);
691
795
  this.ctx.logger.warn(error);
692
796
  }
693
- const llmHint = !this.llmReview && result.markStale.length > 0 ? " (llmReview: off - deterministic archive only; set llmReview: true for the LLM merge channel)" : "";
797
+ const llmHint = !settings.llmReview && result.markStale.length > 0 ? " (llmReview: off - deterministic archive only; set llmReview: true for the LLM merge channel)" : "";
694
798
  const summary = `${dryRun ? "dry-run" : "auto"}: stale:${result.markStale.length} archived:${archivedSkills.length} consolidated:${consolidated.length}${llmHint}`;
695
799
  try {
696
800
  await stateService?.transactCuratorState((current) => {
@@ -711,7 +815,7 @@ var EvolutionCurator = class extends Service {
711
815
  archived: archivedSkills.map((item) => item.name),
712
816
  errors,
713
817
  report,
714
- ...this.llmReview ? { nominations: gatedNominations } : {}
818
+ ...settings.llmReview ? { nominations: gatedNominations } : {}
715
819
  };
716
820
  }
717
821
  /**
@@ -943,15 +1047,16 @@ var EvolutionCurator = class extends Service {
943
1047
  };
944
1048
  }
945
1049
  recentSessionActive() {
1050
+ const settings = this.settings();
946
1051
  const agents = this.ctx.get("agents");
947
- if (!agents) return !this.minIdleFailOpen;
1052
+ if (!agents) return !settings.minIdleFailOpen;
948
1053
  let latest = 0;
949
1054
  for (const agent of agents.list()) {
950
1055
  const events = agent.session.snapshotEvents();
951
1056
  const last = events.length === 0 ? 0 : events[events.length - 1]?.time ?? 0;
952
1057
  latest = Math.max(latest, last);
953
1058
  }
954
- return latest > 0 && Date.now() - latest < this.minIdleHours * 36e5;
1059
+ return latest > 0 && Date.now() - latest < settings.minIdleHours * 36e5;
955
1060
  }
956
1061
  /**
957
1062
  * Keep only the newest N curator run reports plus at most `errorKeep` error
@@ -1098,10 +1203,11 @@ var EvolutionCurator = class extends Service {
1098
1203
  * layer.
1099
1204
  */
1100
1205
  async healthView() {
1206
+ const settings = this.settings();
1101
1207
  const thresholds = {
1102
- softBodyChars: this.healthSoftBodyChars,
1103
- stampDensityPerKb: this.healthStampDensityPerKb,
1104
- churnMinPatches: this.healthChurnMinPatches
1208
+ softBodyChars: settings.healthSoftBodyChars,
1209
+ stampDensityPerKb: settings.healthStampDensityPerKb,
1210
+ churnMinPatches: settings.healthChurnMinPatches
1105
1211
  };
1106
1212
  const usage = await loadUsage(this.skills.root, this.io);
1107
1213
  const observed = usageObserved(usage);
@@ -1230,4 +1336,4 @@ function reportsSweepLockTarget() {
1230
1336
  return join(evolutionHome(), "reports", ".retention");
1231
1337
  }
1232
1338
  //#endregion
1233
- export { EvolutionCurator, EvolutionCurator as default, gateConsolidations };
1339
+ export { CURATOR_SETTINGS_NAMESPACE, CURATOR_SETTINGS_SCHEMA, EvolutionCurator, EvolutionCurator as default, gateConsolidations, validateCuratorSettings };
@@ -3,6 +3,7 @@
3
3
  * @module @lmzhen/dsh-evolution-curator
4
4
  */
5
5
  import { Context, Service } from '@deepseek-ai/cordis';
6
+ import z from '@deepseek-ai/schemastery';
6
7
  import type Schema from '@deepseek-ai/schemastery';
7
8
  import { EvolutionGateSet, type SkillLibrary } from '@lmzhen/dsh-evolution-core';
8
9
  import { type CuratorConsolidation, type CuratorNominations, type CuratorRunReport, type ScopeView, type SkillActionResult, type SkillHealthVerdict } from '@lmzhen/dsh-evolution-core';
@@ -14,6 +15,10 @@ declare module '@deepseek-ai/cordis' {
14
15
  }
15
16
  export interface Config {
16
17
  enabled?: boolean;
18
+ /** The due-ness interval. The policy snapshot shadows it in every shipped
19
+ * composition — configure `curatorIntervalHours` there; that name is the
20
+ * canonical id. Deprecated alias (G0/S0.3): still readable, refused by
21
+ * writes; removed 0.7.0. */
17
22
  intervalHours?: number;
18
23
  staleAfterDays?: number;
19
24
  archiveAfterDays?: number;
@@ -56,6 +61,50 @@ export interface Config {
56
61
  /** Structure-health write-ghost floor: patches at/above with zero reads (A2). */
57
62
  healthChurnMinPatches?: number;
58
63
  }
64
+ /** Namespace the curator group's user-writable knobs live in (core's PARAM_NAMESPACES). */
65
+ export declare const CURATOR_SETTINGS_NAMESPACE = "evolution-curator";
66
+ /** Curator behaviour a user may change (G3/S3.3). Field names are the CANONICAL
67
+ * parameter ids from the registry, so the settings document, the params output,
68
+ * the doctor report and the cards all spell one name. */
69
+ export interface CuratorSettings {
70
+ /** Minimum hours between deterministic curation passes. */
71
+ curatorIntervalHours: number;
72
+ /** Inactive days before a skill counts as stale. */
73
+ staleAfterDays: number;
74
+ /** Inactive days before a stale skill is archived (>= staleAfterDays). */
75
+ archiveAfterDays: number;
76
+ /** Age at which a low quality score starts warning. */
77
+ qualityWarnStaleAfterDays: number;
78
+ /** Idle hours required before an automatic pass runs (0 disables the gate). */
79
+ minIdleHours: number;
80
+ /** Let the idle gate open when the activity probe is unavailable. */
81
+ minIdleFailOpen: boolean;
82
+ /** Enable the LLM nomination pass on top of the deterministic lifecycle. */
83
+ llmReview: boolean;
84
+ /** Token budget of the curator LLM review. */
85
+ curatorReviewMaxTokens: number;
86
+ /** Wall-clock bound of the curator LLM review. */
87
+ curatorReviewTimeoutMs: number;
88
+ /** Body character line the health view judges against. */
89
+ healthSoftBodyChars: number;
90
+ /** Stamp density per KB that flags log-like content in a body. */
91
+ healthStampDensityPerKb: number;
92
+ /** Patches without a read that flag a write-ghost skill. */
93
+ healthChurnMinPatches: number;
94
+ }
95
+ /** Schema the platform validates the user layer against; defaults mirror the core
96
+ * constants and the bounds mirror the row schema, so an empty document resolves to
97
+ * today's behaviour and an out-of-range value is refused instead of clamped. */
98
+ export declare const CURATOR_SETTINGS_SCHEMA: z<CuratorSettings>;
99
+ /**
100
+ * The cross-field rule the schema cannot express (A2-17): an archive threshold
101
+ * below the stale threshold makes the engine reactivate stale records instead of
102
+ * archiving them, so a resolved pair that violates it is refused at WRITE time.
103
+ * The runtime keeps its clamp for the carriers this hook never sees (the policy
104
+ * snapshot and the plugin row).
105
+ * @param value - the resolved section the platform hands the owner.
106
+ */
107
+ export declare function validateCuratorSettings(value: CuratorSettings): void;
59
108
  /** Outcome of one curator run pass. */
60
109
  export interface CuratorRunOutcome {
61
110
  stale: string[];
@@ -113,6 +162,10 @@ export declare class EvolutionCurator extends Service {
113
162
  private readonly healthSoftBodyChars;
114
163
  private readonly healthStampDensityPerKb;
115
164
  private readonly healthChurnMinPatches;
165
+ /** G3/S3.3: this row's resolved values — the base layer of the settings section. */
166
+ private readonly settingsBase;
167
+ /** The user layer for this row's namespace (absent provider = deployment values). */
168
+ private readonly overrides;
116
169
  private lastRun;
117
170
  private timer;
118
171
  /** B-8 (v18): set by the fiber disposer; a triggered autoCheck must not
@@ -140,6 +193,17 @@ export declare class EvolutionCurator extends Service {
140
193
  * yielding instance's log line names a concrete holder. */
141
194
  private readonly instanceOwner;
142
195
  constructor(ctx: Context, config?: Config);
196
+ /** The policy-snapshot carrier for this group (unmounted service = no policy). */
197
+ private policyFields;
198
+ /**
199
+ * G3/S3.3: the ONE reader of the curator's behaviour knobs. Precedence is
200
+ * user > policy snapshot > this row, and {@link ParamOverrides.get} returns
201
+ * undefined for a key the user never set, so the deployment carriers keep
202
+ * winning exactly as before. Read at USE time: a committed settings change is
203
+ * live without a restart and without a watcher.
204
+ * @returns the resolved section, every numeric field sanitized.
205
+ */
206
+ private settings;
143
207
  private lifecycle;
144
208
  start(): void;
145
209
  stop(): void;
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.4.1",
4
+ "version": "0.6.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,20 +27,20 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@deepseek-ai/schemastery": "^3.18.1",
30
- "@lmzhen/dsh-evolution-core": "^0.4.1"
30
+ "@lmzhen/dsh-evolution-core": "^0.6.0"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@deepseek-ai/cordis": "^4.0.1",
34
34
  "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
35
35
  "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
36
- "@lmzhen/dsh-evolution-io": "^0.4.1",
37
- "@lmzhen/dsh-evolution-state": "^0.4.1"
36
+ "@lmzhen/dsh-evolution-io": "^0.6.0",
37
+ "@lmzhen/dsh-evolution-state": "^0.6.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
41
41
  "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
42
- "@lmzhen/dsh-evolution-core": "^0.4.1",
43
- "@lmzhen/dsh-evolution-io": "^0.4.1",
44
- "@lmzhen/dsh-evolution-state": "^0.4.1"
42
+ "@lmzhen/dsh-evolution-core": "^0.6.0",
43
+ "@lmzhen/dsh-evolution-io": "^0.6.0",
44
+ "@lmzhen/dsh-evolution-state": "^0.6.0"
45
45
  }
46
46
  }