@lmzhen/dsh-evolution-curator 0.5.0 → 0.6.1

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_SKILL_LIMITS, 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, policyStageLimits, 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
@@ -130,7 +164,7 @@ var EvolutionCurator = class extends Service {
130
164
  if (value !== void 0 && result !== value) clamped.push(name);
131
165
  return result;
132
166
  };
133
- this.intervalHours = field("intervalHours", config.intervalHours, DEFAULT_CURATOR_INTERVAL_HOURS, 1);
167
+ this.intervalHours = field("intervalHours", readNumberParam(config, "curatorIntervalHours"), DEFAULT_CURATOR_INTERVAL_HOURS, 1);
134
168
  this.staleAfterDays = field("staleAfterDays", config.staleAfterDays, DEFAULT_STALE_AFTER_DAYS, 1);
135
169
  this.archiveAfterDays = field("archiveAfterDays", config.archiveAfterDays, DEFAULT_ARCHIVE_AFTER_DAYS, 1);
136
170
  if (this.archiveAfterDays < this.staleAfterDays) {
@@ -153,6 +187,26 @@ var EvolutionCurator = class extends Service {
153
187
  this.healthStampDensityPerKb = field("healthStampDensityPerKb", config.healthStampDensityPerKb, DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb, 1);
154
188
  this.healthChurnMinPatches = field("healthChurnMinPatches", config.healthChurnMinPatches, DEFAULT_HEALTH_THRESHOLDS.churnMinPatches, 1);
155
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
+ });
156
210
  this.lastRun = Date.now();
157
211
  this.ctx.effect(() => {
158
212
  return () => {
@@ -163,17 +217,58 @@ var EvolutionCurator = class extends Service {
163
217
  }, "evolution-curator.stop");
164
218
  if (config.autoStart ?? true) this.start();
165
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
+ }
166
262
  lifecycle() {
167
- const snapshot = this.ctx.get("evolutionPolicy")?.get();
168
- const staleAfterDays = snapshot?.staleAfterDays ?? this.staleAfterDays;
169
- let archiveAfterDays = snapshot?.archiveAfterDays ?? this.archiveAfterDays;
170
- if (archiveAfterDays < staleAfterDays) {
171
- this.ctx.logger.warn(`evolution-curator: policy archiveAfterDays (${archiveAfterDays}) < policy staleAfterDays (${staleAfterDays}); using the stale threshold as the archive threshold`);
172
- 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;
173
268
  }
174
269
  return {
175
- intervalHours: snapshot?.curatorIntervalHours ?? this.intervalHours,
176
- staleAfterDays,
270
+ intervalHours: settings.curatorIntervalHours,
271
+ staleAfterDays: settings.staleAfterDays,
177
272
  archiveAfterDays
178
273
  };
179
274
  }
@@ -307,6 +402,7 @@ var EvolutionCurator = class extends Service {
307
402
  "",
308
403
  "Return a YAML summary with consolidations and prunings lists. Nominate only actions whose archival/merge is clearly safe."
309
404
  ].join("\n");
405
+ const settings = this.settings();
310
406
  try {
311
407
  const assembler = new BlockAssembler();
312
408
  for await (const chunk of llm.stream({
@@ -324,8 +420,8 @@ var EvolutionCurator = class extends Service {
324
420
  summary: "curator review"
325
421
  }
326
422
  })],
327
- maxTokens: this.curatorReviewMaxTokens,
328
- signal: AbortSignal.timeout(this.curatorReviewTimeoutMs)
423
+ maxTokens: settings.curatorReviewMaxTokens,
424
+ signal: AbortSignal.timeout(settings.curatorReviewTimeoutMs)
329
425
  })) assembler.push(chunk);
330
426
  const parsed = parseCuratorNominations(assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("\n"));
331
427
  return {
@@ -398,7 +494,7 @@ var EvolutionCurator = class extends Service {
398
494
  archiveCandidates: [],
399
495
  archived: [],
400
496
  failed: [],
401
- llmReviewEnabled: this.llmReview
497
+ llmReviewEnabled: this.settings().llmReview
402
498
  });
403
499
  }
404
500
  /**
@@ -506,6 +602,7 @@ var EvolutionCurator = class extends Service {
506
602
  const runId = randomUUID();
507
603
  const stateService = this.curatorStateService();
508
604
  const lifecycle = this.lifecycle();
605
+ const settings = this.settings();
509
606
  const persisted = await stateService?.loadCuratorState() ?? null;
510
607
  if (!ignoreGates && persisted?.paused === true) return {
511
608
  stale: [],
@@ -521,7 +618,7 @@ var EvolutionCurator = class extends Service {
521
618
  report: this.skippedReport(runId, startedAt),
522
619
  skipped: "interval"
523
620
  };
524
- if (!ignoreGates && this.minIdleHours > 0 && this.recentSessionActive()) return {
621
+ if (!ignoreGates && settings.minIdleHours > 0 && this.recentSessionActive()) return {
525
622
  stale: [],
526
623
  archived: [],
527
624
  errors: [],
@@ -584,7 +681,7 @@ var EvolutionCurator = class extends Service {
584
681
  const result = computeLifecycleTransitions(usage, {
585
682
  staleAfterDays: lifecycle.staleAfterDays,
586
683
  archiveAfterDays: lifecycle.archiveAfterDays,
587
- qualityWarnStaleAfterDays: this.qualityWarnStaleAfterDays,
684
+ qualityWarnStaleAfterDays: settings.qualityWarnStaleAfterDays,
588
685
  excludeSkillNames: this.excludeSkillNames,
589
686
  manageUnmanaged: this.manageUnmanaged,
590
687
  pruneBuiltins: this.pruneBuiltins,
@@ -593,7 +690,7 @@ var EvolutionCurator = class extends Service {
593
690
  referencedSkillNames: this.referencedSkillNames
594
691
  }, /* @__PURE__ */ new Date(), gates, protectedNames);
595
692
  const recommendPool = [...new Set([...result.markStale, ...dedupMembers])].filter((name) => SKILL_NAME_RE.test(name));
596
- const nominations = this.llmReview ? await this.recommend(recommendPool, { dryRun }) : {
693
+ const nominations = settings.llmReview ? await this.recommend(recommendPool, { dryRun }) : {
597
694
  prunings: [],
598
695
  consolidations: [],
599
696
  warnings: []
@@ -605,7 +702,7 @@ var EvolutionCurator = class extends Service {
605
702
  };
606
703
  const llmNominations = gatedNominations.prunings;
607
704
  const archiveCandidates = [...new Set([...result.archive, ...llmNominations])];
608
- if (!ignoreGates && this.minIdleHours > 0 && this.recentSessionActive()) {
705
+ if (!ignoreGates && settings.minIdleHours > 0 && this.recentSessionActive()) {
609
706
  if (!dryRun) try {
610
707
  await mutateUsage(root, this.io, (map) => {
611
708
  for (const [name, record] of usage) if (!map.has(name)) map.set(name, { ...record });
@@ -622,7 +719,7 @@ var EvolutionCurator = class extends Service {
622
719
  };
623
720
  }
624
721
  const snapshotPath = dryRun ? void 0 : await this.snapshotFull("pre-curator-run");
625
- if (!ignoreGates && this.minIdleHours > 0 && this.recentSessionActive()) {
722
+ if (!ignoreGates && settings.minIdleHours > 0 && this.recentSessionActive()) {
626
723
  if (!dryRun) try {
627
724
  await mutateUsage(root, this.io, (map) => {
628
725
  for (const [name, record] of usage) if (!map.has(name)) map.set(name, { ...record });
@@ -686,7 +783,7 @@ var EvolutionCurator = class extends Service {
686
783
  })(),
687
784
  consolidated,
688
785
  ...snapshotPath === void 0 ? {} : { snapshotPath },
689
- llmReviewEnabled: this.llmReview,
786
+ llmReviewEnabled: settings.llmReview,
690
787
  ...gatedNominations.warnings.length > 0 ? { nominationsWarnings: gatedNominations.warnings } : {}
691
788
  });
692
789
  const reportsRoot = join(evolutionHome(), "reports");
@@ -697,7 +794,7 @@ var EvolutionCurator = class extends Service {
697
794
  this.ctx.logger.warn(`evolution-curator: failed to persist report ${runId}`);
698
795
  this.ctx.logger.warn(error);
699
796
  }
700
- 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)" : "";
701
798
  const summary = `${dryRun ? "dry-run" : "auto"}: stale:${result.markStale.length} archived:${archivedSkills.length} consolidated:${consolidated.length}${llmHint}`;
702
799
  try {
703
800
  await stateService?.transactCuratorState((current) => {
@@ -718,7 +815,7 @@ var EvolutionCurator = class extends Service {
718
815
  archived: archivedSkills.map((item) => item.name),
719
816
  errors,
720
817
  report,
721
- ...this.llmReview ? { nominations: gatedNominations } : {}
818
+ ...settings.llmReview ? { nominations: gatedNominations } : {}
722
819
  };
723
820
  }
724
821
  /**
@@ -950,15 +1047,16 @@ var EvolutionCurator = class extends Service {
950
1047
  };
951
1048
  }
952
1049
  recentSessionActive() {
1050
+ const settings = this.settings();
953
1051
  const agents = this.ctx.get("agents");
954
- if (!agents) return !this.minIdleFailOpen;
1052
+ if (!agents) return !settings.minIdleFailOpen;
955
1053
  let latest = 0;
956
1054
  for (const agent of agents.list()) {
957
1055
  const events = agent.session.snapshotEvents();
958
1056
  const last = events.length === 0 ? 0 : events[events.length - 1]?.time ?? 0;
959
1057
  latest = Math.max(latest, last);
960
1058
  }
961
- return latest > 0 && Date.now() - latest < this.minIdleHours * 36e5;
1059
+ return latest > 0 && Date.now() - latest < settings.minIdleHours * 36e5;
962
1060
  }
963
1061
  /**
964
1062
  * Keep only the newest N curator run reports plus at most `errorKeep` error
@@ -1105,10 +1203,11 @@ var EvolutionCurator = class extends Service {
1105
1203
  * layer.
1106
1204
  */
1107
1205
  async healthView() {
1206
+ const settings = this.settings();
1108
1207
  const thresholds = {
1109
- softBodyChars: this.healthSoftBodyChars,
1110
- stampDensityPerKb: this.healthStampDensityPerKb,
1111
- churnMinPatches: this.healthChurnMinPatches
1208
+ softBodyChars: settings.healthSoftBodyChars,
1209
+ stampDensityPerKb: settings.healthStampDensityPerKb,
1210
+ churnMinPatches: settings.healthChurnMinPatches
1112
1211
  };
1113
1212
  const usage = await loadUsage(this.skills.root, this.io);
1114
1213
  const observed = usageObserved(usage);
@@ -1237,4 +1336,4 @@ function reportsSweepLockTarget() {
1237
1336
  return join(evolutionHome(), "reports", ".retention");
1238
1337
  }
1239
1338
  //#endregion
1240
- 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.5.0",
4
+ "version": "0.6.1",
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.5.0"
30
+ "@lmzhen/dsh-evolution-core": "^0.6.1"
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.5.0",
37
- "@lmzhen/dsh-evolution-state": "^0.5.0"
36
+ "@lmzhen/dsh-evolution-io": "^0.6.1",
37
+ "@lmzhen/dsh-evolution-state": "^0.6.1"
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.5.0",
43
- "@lmzhen/dsh-evolution-io": "^0.5.0",
44
- "@lmzhen/dsh-evolution-state": "^0.5.0"
42
+ "@lmzhen/dsh-evolution-core": "^0.6.1",
43
+ "@lmzhen/dsh-evolution-io": "^0.6.1",
44
+ "@lmzhen/dsh-evolution-state": "^0.6.1"
45
45
  }
46
46
  }