@lmzhen/dsh-evolution-curator 0.3.67 → 0.3.69

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/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_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MIN_IDLE_HOURS, DEFAULT_STALE_AFTER_DAYS, EvolutionGateSet, SKILL_NAME_RE, SkillLibrary, buildCuratorRunReport, clampedNumber, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, emptyRecord, evolutionHome, evolutionIoAdapter, foldCuratorFields, loadSuppressedNames, loadUsage, markerEntryName, mutateUsage, parseCuratorNominations, parseFrontmatter, relatedSkillNames, renderCuratorReportMarkdown, resolveSkillsRoot, 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_STALE_AFTER_DAYS, EvolutionGateSet, MAX_TIMER_DELAY_MS, SKILL_NAME_RE, SkillLibrary, buildCuratorRunReport, clampedNumber, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, emptyRecord, evolutionHome, evolutionIoAdapter, foldCuratorFields, loadSuppressedNames, loadUsage, markerEntryName, mutateUsage, parseCuratorNominations, parseFrontmatter, relatedSkillNames, renderCuratorReportMarkdown, resolveSkillsRoot, 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,7 +17,6 @@ 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
- const MAX_TIMER_DELAY_MS = 2147483647;
21
20
  /**
22
21
  * Block LLM-nominated consolidations that would touch a gate-protected name:
23
22
  * exclude / referenced / suppressed skills must never merge (neither as the
@@ -255,7 +254,7 @@ var EvolutionCurator = class extends Service {
255
254
  if (candidates.length === 0) return empty;
256
255
  const llm = this.ctx.get("llm");
257
256
  if (!llm) return empty;
258
- const model = this.ctx.get("evolutionPolicy")?.get()?.curatorModel ?? "deepseek-v4-pro";
257
+ const model = this.ctx.get("evolutionPolicy")?.get()?.curatorModel ?? DEFAULT_CURATOR_MODEL;
259
258
  const clusters = computePrefixClusters(candidates);
260
259
  const clusterLines = clusters.length === 0 ? ["Prefix clusters observed in the candidate list: (none)"] : ["Prefix clusters observed in the candidate list (orientation only — verify against the names above; you may also flag additional clusters):", ...clusters.map((cluster) => `- '${cluster.key}': ${cluster.members.join(", ")}`)];
261
260
  const prompt = [
@@ -409,7 +408,9 @@ var EvolutionCurator = class extends Service {
409
408
  * mover refuses a directory whose writer lock is alive), so a collision
410
409
  * degrades to a recorded failed op + snapshot rollback, never a torn
411
410
  * write. Automatic passes are kept out of the session-active window by the
412
- * min-idle gate; a manual run (`ignoreGates`) bypasses that gate and is
411
+ * min-idle gate checked pre-run AND re-checked at the commit boundary
412
+ * (v28 G4.2: a session activating mid-run no longer slips past it); a
413
+ * manual run (`ignoreGates`) bypasses both checks and is
413
414
  * the one realistic interleave window — documented, accepted (plan v20
414
415
  * C-3①). The same statement lives on evolution-review's `reviewInFlight`.
415
416
  */
@@ -497,14 +498,13 @@ var EvolutionCurator = class extends Service {
497
498
  skipped: "disposed"
498
499
  };
499
500
  }
500
- const snapshotPath = dryRun ? void 0 : await this.snapshotFull("pre-curator-run");
501
501
  const suppressedNames = new Set(await loadSuppressedNames(root, this.io));
502
502
  const gates = new EvolutionGateSet({
503
503
  exclude: this.excludeSkillNames,
504
504
  referenced: this.referencedSkillNames,
505
505
  suppressed: suppressedNames
506
506
  });
507
- const { bundledNames, treeNames } = await this.seedBaseline(usage);
507
+ const { bundledNames, treeNames, resetToActive } = await this.seedBaseline(usage);
508
508
  const contents = /* @__PURE__ */ new Map();
509
509
  for (const name of treeNames) {
510
510
  const text = await this.skills.read(name);
@@ -537,6 +537,23 @@ var EvolutionCurator = class extends Service {
537
537
  };
538
538
  const llmNominations = gatedNominations.prunings;
539
539
  const archiveCandidates = [...new Set([...result.archive, ...llmNominations])];
540
+ if (!ignoreGates && this.minIdleHours > 0 && this.recentSessionActive()) {
541
+ if (!dryRun) try {
542
+ await mutateUsage(root, this.io, (map) => {
543
+ for (const [name, record] of usage) if (!map.has(name)) map.set(name, { ...record });
544
+ });
545
+ } catch (error) {
546
+ this.ctx.logger.warn(`evolution-curator: failed to persist the seeded usage baseline on a session-blocked run (${error instanceof Error ? error.message : String(error)})`);
547
+ }
548
+ return {
549
+ stale: [],
550
+ archived: [],
551
+ errors: [],
552
+ report: this.skippedReport(runId, startedAt),
553
+ skipped: "active-session"
554
+ };
555
+ }
556
+ const snapshotPath = dryRun ? void 0 : await this.snapshotFull("pre-curator-run");
540
557
  const { archivedSkills, errors, consolidated } = await this.applyMutations({
541
558
  dryRun,
542
559
  archiveCandidates,
@@ -547,11 +564,16 @@ var EvolutionCurator = class extends Service {
547
564
  suppressedNames,
548
565
  root,
549
566
  recommendPool: new Set(recommendPool),
550
- stateOwned: new Set([...result.transitions.map((t) => t.name), ...archiveCandidates]),
567
+ stateOwned: new Set([
568
+ ...result.transitions.map((t) => t.name),
569
+ ...archiveCandidates,
570
+ ...resetToActive
571
+ ]),
551
572
  runStartStates,
552
573
  failedFrom: new Map(result.transitions.filter((t) => t.to === "archived").map((t) => [t.name, t.from]))
553
574
  });
554
- if (!dryRun) this.lastRun = Date.now();
575
+ const runAborted = errors.some((error) => error.startsWith("run aborted"));
576
+ if (!dryRun && !runAborted) this.lastRun = Date.now();
555
577
  const report = buildCuratorRunReport({
556
578
  runId,
557
579
  startedAt,
@@ -566,6 +588,15 @@ var EvolutionCurator = class extends Service {
566
588
  reason: errors.find((item) => item.startsWith(`${name}:`))?.slice(name.length + 2) ?? "unknown"
567
589
  };
568
590
  }),
591
+ ...(() => {
592
+ const attributed = new Set([...archiveCandidates, ...gatedNominations.consolidations.map((item) => item.from)]);
593
+ const loose = errors.filter((error) => ![...attributed].some((name) => error.startsWith(`${name}:`)));
594
+ const abort = loose.find((error) => error.startsWith("run aborted"));
595
+ return {
596
+ ...abort === void 0 ? {} : { aborted: abort.slice(13) },
597
+ ...loose.length === 0 ? {} : { unattributed: loose }
598
+ };
599
+ })(),
569
600
  consolidated,
570
601
  ...snapshotPath === void 0 ? {} : { snapshotPath },
571
602
  llmReviewEnabled: this.llmReview,
@@ -586,8 +617,8 @@ var EvolutionCurator = class extends Service {
586
617
  const pausedNow = current?.paused ?? false;
587
618
  return {
588
619
  schemaVersion: 1,
589
- lastRunAt: dryRun ? persisted?.lastRunAt ?? this.lastRun : Date.now(),
590
- runCount: dryRun ? persisted?.runCount ?? 0 : (current?.runCount ?? 0) + 1,
620
+ lastRunAt: dryRun || runAborted ? persisted?.lastRunAt ?? this.lastRun : Date.now(),
621
+ runCount: dryRun || runAborted ? persisted?.runCount ?? 0 : (current?.runCount ?? 0) + 1,
591
622
  lastSummary: summary,
592
623
  paused: pausedNow
593
624
  };
@@ -612,16 +643,23 @@ var EvolutionCurator = class extends Service {
612
643
  async seedBaseline(usage) {
613
644
  const bundledNames = /* @__PURE__ */ new Set();
614
645
  const treeNames = /* @__PURE__ */ new Set();
646
+ const resetToActive = /* @__PURE__ */ new Set();
615
647
  for (const summary of await this.skills.list()) {
616
648
  treeNames.add(summary.name);
617
649
  if (!usage.has(summary.name)) usage.set(summary.name, emptyRecord());
618
650
  if (await this.skills.isBundled(summary.name)) bundledNames.add(summary.name);
619
651
  const record = usage.get(summary.name);
652
+ if (record?.state === "archived") {
653
+ record.state = "active";
654
+ record.archived_at = null;
655
+ resetToActive.add(summary.name);
656
+ }
620
657
  if (record) record.pinned = await this.skills.isPinned(summary.name);
621
658
  }
622
659
  return {
623
660
  bundledNames,
624
- treeNames
661
+ treeNames,
662
+ resetToActive
625
663
  };
626
664
  }
627
665
  /**
@@ -699,6 +737,7 @@ var EvolutionCurator = class extends Service {
699
737
  record.state = "archived";
700
738
  record.archived_at = record.archived_at ?? (/* @__PURE__ */ new Date()).toISOString();
701
739
  stateOwned.add(name);
740
+ this.ctx.logger.warn(`evolution-curator: skill "${name}" left the tree without an archive move - its usage record was folded to archived; if the directory is still on disk, repair or remove it manually (it will not re-enter the lifecycle while the record is archived)`);
702
741
  }
703
742
  let wasBundled = false;
704
743
  try {
@@ -793,8 +832,7 @@ var EvolutionCurator = class extends Service {
793
832
  });
794
833
  archivedSkills.push({
795
834
  name: nomination.from,
796
- path: join(this.skills.root, ".archive", nomination.from),
797
- reason: `Consolidated into ${nomination.into}`
835
+ reason: `Consolidated into ${nomination.into} (exact archive path: the evolution/skill-mutated event, archivedPath)`
798
836
  });
799
837
  }
800
838
  if (suppressedChanged) try {
@@ -222,7 +222,9 @@ export declare class EvolutionCurator extends Service {
222
222
  * mover refuses a directory whose writer lock is alive), so a collision
223
223
  * degrades to a recorded failed op + snapshot rollback, never a torn
224
224
  * write. Automatic passes are kept out of the session-active window by the
225
- * min-idle gate; a manual run (`ignoreGates`) bypasses that gate and is
225
+ * min-idle gate checked pre-run AND re-checked at the commit boundary
226
+ * (v28 G4.2: a session activating mid-run no longer slips past it); a
227
+ * manual run (`ignoreGates`) bypasses both checks and is
226
228
  * the one realistic interleave window — documented, accepted (plan v20
227
229
  * C-3①). The same statement lives on evolution-review's `reviewInFlight`.
228
230
  */
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.3.67",
4
+ "version": "0.3.69",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -31,20 +31,20 @@
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
33
  "@deepseek-ai/schemastery": "^3.18.1",
34
- "@lmzhen/dsh-evolution-core": "^0.3.67"
34
+ "@lmzhen/dsh-evolution-core": "^0.3.69"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@deepseek-ai/cordis": "^4.0.1",
38
38
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
39
39
  "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
40
- "@lmzhen/dsh-evolution-io": "^0.3.67",
41
- "@lmzhen/dsh-evolution-state": "^0.3.67"
40
+ "@lmzhen/dsh-evolution-io": "^0.3.69",
41
+ "@lmzhen/dsh-evolution-state": "^0.3.69"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
45
45
  "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
46
- "@lmzhen/dsh-evolution-core": "^0.3.67",
47
- "@lmzhen/dsh-evolution-io": "^0.3.67",
48
- "@lmzhen/dsh-evolution-state": "^0.3.67"
46
+ "@lmzhen/dsh-evolution-core": "^0.3.69",
47
+ "@lmzhen/dsh-evolution-io": "^0.3.69",
48
+ "@lmzhen/dsh-evolution-state": "^0.3.69"
49
49
  }
50
50
  }