@lmzhen/dsh-evolution-core 0.1.0-rc.26 → 0.1.0-rc.27

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
@@ -308,6 +308,50 @@ function parseCuratorNominations(text) {
308
308
  consolidations: consolidations.filter((item) => valid(item.from) && valid(item.into))
309
309
  };
310
310
  }
311
+ /**
312
+ * The lifecycle-candidate gate, shared by the transition engine and the scope
313
+ * view so the two can never disagree: records failing ANY of these gates are
314
+ * outside the managed scope.
315
+ */
316
+ function lifecycleCandidate(name, record, config, bundled) {
317
+ if (record.pinned) return false;
318
+ if (config.excludeSkillNames?.has(name)) return false;
319
+ if (config.suppressedNames?.has(name)) return false;
320
+ if (config.referencedSkillNames?.has(name)) return false;
321
+ if (!(record.created_by === "agent" || config.manageUnmanaged === true) && !(config.pruneBuiltins === true && bundled)) return false;
322
+ if (PROTECTED_BUILTIN_SKILLS.has(name)) return false;
323
+ if (record.state === "archived") return false;
324
+ return true;
325
+ }
326
+ /**
327
+ * Read-only scope classification, derived from the SAME gate the transition
328
+ * engine uses (`lifecycleCandidate`), so the view always predicts what a
329
+ * curator pass may touch.
330
+ */
331
+ function computeScopeView(usage, config) {
332
+ const managed = [];
333
+ const watched = [];
334
+ const exempted = [];
335
+ const protectedNames = [];
336
+ for (const [name, record] of usage) {
337
+ if (config.excludeSkillNames?.has(name) || config.referencedSkillNames?.has(name)) {
338
+ exempted.push(name);
339
+ continue;
340
+ }
341
+ const bundled = config.bundledNames?.has(name) === true;
342
+ if (record.pinned || bundled || config.suppressedNames?.has(name) === true) protectedNames.push(name);
343
+ if (lifecycleCandidate(name, record, config, bundled)) {
344
+ managed.push(name);
345
+ if (record.state === "stale" || record.quality_warn === true) watched.push(name);
346
+ }
347
+ }
348
+ return {
349
+ managed: managed.sort(),
350
+ watched: watched.sort(),
351
+ exempted: exempted.sort(),
352
+ protected: protectedNames.sort()
353
+ };
354
+ }
311
355
  function daysSince(iso, created, now) {
312
356
  return (now - new Date(iso ?? created).getTime()) / 864e5;
313
357
  }
@@ -319,14 +363,7 @@ function computeLifecycleTransitions(usage, config, now = /* @__PURE__ */ new Da
319
363
  markStale: []
320
364
  };
321
365
  for (const [name, record] of usage) {
322
- if (record.pinned) continue;
323
- if (config.excludeSkillNames?.has(name)) continue;
324
- if (config.suppressedNames?.has(name)) continue;
325
- if (config.referencedSkillNames?.has(name)) continue;
326
- const bundled = config.bundledNames?.has(name) === true;
327
- if (!(record.created_by === "agent" || config.manageUnmanaged === true) && !(config.pruneBuiltins === true && bundled)) continue;
328
- if (PROTECTED_BUILTIN_SKILLS.has(name)) continue;
329
- if (record.state === "archived") continue;
366
+ if (!lifecycleCandidate(name, record, config, config.bundledNames?.has(name) === true)) continue;
330
367
  const age = daysSince(null, record.created_at, now.getTime());
331
368
  if (record.use_count === 0 && age < config.staleAfterDays) continue;
332
369
  const idle = daysSince(latestActivityAt(record), record.created_at, now.getTime());
@@ -1686,6 +1723,11 @@ var SkillLibrary = class {
1686
1723
  const dir = skillDir(this.root, name);
1687
1724
  return await this.io.exists(markerPath(dir, "bundled"));
1688
1725
  }
1726
+ /** Whether the skill carries the pinned marker (the marker is the factual source; usage.pinned mirrors it). */
1727
+ async isPinned(name) {
1728
+ const dir = skillDir(this.root, name);
1729
+ return await this.io.exists(markerPath(dir, "pinned"));
1730
+ }
1689
1731
  /** Count non-empty support subdirectories (richness input for quality scoring). */
1690
1732
  async countSupportDirs(name) {
1691
1733
  const dir = skillDir(this.root, name);
@@ -1721,6 +1763,48 @@ var SkillLibrary = class {
1721
1763
  async listMutations() {
1722
1764
  return await loadMutations(this.root, this.io);
1723
1765
  }
1766
+ /**
1767
+ * Pin or unpin a skill (`.pinned` marker). Pinned skills are protected from
1768
+ * deletion, from background-review writes, and from the lifecycle — a
1769
+ * protective mutation, so the autonomous pipeline may never call it. The
1770
+ * marker write is the only state change; content is untouched.
1771
+ */
1772
+ async setPinned(name, pinned, origin = "foreground") {
1773
+ const normalized = name.trim();
1774
+ if (!SKILL_NAME_RE.test(normalized) || normalized.length > this.limits.maxNameLength) return {
1775
+ ok: false,
1776
+ message: `Invalid skill name "${normalized}". Use lowercase letters, digits, and hyphens (<= ${this.limits.maxNameLength}).`
1777
+ };
1778
+ if (origin === "background_review") return {
1779
+ ok: false,
1780
+ message: "Only the foreground (user or the main agent) may pin or unpin skills."
1781
+ };
1782
+ const dir = skillDir(this.root, normalized);
1783
+ const marker = markerPath(dir, "pinned");
1784
+ const existing = await this.io.exists(marker);
1785
+ if (pinned && existing) return {
1786
+ ok: true,
1787
+ message: `Skill "${normalized}" is already pinned.`,
1788
+ path: dir
1789
+ };
1790
+ if (!pinned && !existing) return {
1791
+ ok: true,
1792
+ message: `Skill "${normalized}" is not pinned; nothing to do.`,
1793
+ path: dir
1794
+ };
1795
+ if (!await this.io.exists(join(dir, "SKILL.md"))) return {
1796
+ ok: false,
1797
+ message: `Skill "${normalized}" not found.`
1798
+ };
1799
+ if (pinned) await this.io.writeText(marker, "");
1800
+ else await this.io.remove(marker);
1801
+ await this.audit(normalized, pinned ? "pin" : "unpin", null, null, pinned ? "pinned" : "unpinned");
1802
+ return {
1803
+ ok: true,
1804
+ message: pinned ? `Skill "${normalized}" pinned: protected from deletion, background review, and the lifecycle.` : `Skill "${normalized}" unpinned.`,
1805
+ path: dir
1806
+ };
1807
+ }
1724
1808
  async create(name, content, origin) {
1725
1809
  const normalized = name.trim();
1726
1810
  if (!SKILL_NAME_RE.test(normalized) || normalized.length > this.limits.maxNameLength) return {
@@ -2186,4 +2270,4 @@ var JsonState = class JsonState {
2186
2270
  }
2187
2271
  };
2188
2272
  //#endregion
2189
- export { COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, JsonState, LOW_QUALITY_THRESHOLD, MAX_DESCRIPTION_LENGTH, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MUTATIONS_FILE_VERSION, MemoryStore, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, SKILL_NAME_RE, SKILL_REVIEW_PROMPT, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, advanceReview, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, computeDedupGroups, computeLifecycleTransitions, computeQualityScores, contentHash, emptyRecord, evaluateThreat, evolutionHome, evolutionIoAdapter, foldTurn, getRecord, latestActivityAt, loadMutations, loadSuppressedNames, loadUsage, markAgentCreated, memoryRoot, mutationsFile, nodeEvolutionIo, observeEvent, parseCuratorNominations, parseFrontmatter, recordMutation, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, usageFile, validateFrontmatter, verifyPromptBundle };
2273
+ export { COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, JsonState, LOW_QUALITY_THRESHOLD, MAX_DESCRIPTION_LENGTH, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MUTATIONS_FILE_VERSION, MemoryStore, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, SKILL_NAME_RE, SKILL_REVIEW_PROMPT, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, advanceReview, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, computeDedupGroups, computeLifecycleTransitions, computeQualityScores, computeScopeView, contentHash, emptyRecord, evaluateThreat, evolutionHome, evolutionIoAdapter, foldTurn, getRecord, latestActivityAt, lifecycleCandidate, loadMutations, loadSuppressedNames, loadUsage, markAgentCreated, memoryRoot, mutationsFile, nodeEvolutionIo, observeEvent, parseCuratorNominations, parseFrontmatter, recordMutation, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, usageFile, validateFrontmatter, verifyPromptBundle };
@@ -2,7 +2,7 @@
2
2
  * Deterministic skill curator: active → stale → archived transitions.
3
3
  * Pure function; file moves are performed by SkillLibrary.
4
4
  */
5
- import type { UsageMap } from './usage.ts';
5
+ import type { UsageMap, UsageRecord } from './usage.ts';
6
6
  export { PROTECTED_BUILTIN_SKILLS } from './constants.ts';
7
7
  export interface CuratorConfig {
8
8
  staleAfterDays: number;
@@ -87,5 +87,27 @@ export interface CuratorNominations {
87
87
  * is re-validated against the tree before any file move happens downstream.
88
88
  */
89
89
  export declare function parseCuratorNominations(text: string): CuratorNominations;
90
+ /**
91
+ * The lifecycle-candidate gate, shared by the transition engine and the scope
92
+ * view so the two can never disagree: records failing ANY of these gates are
93
+ * outside the managed scope.
94
+ */
95
+ export declare function lifecycleCandidate(name: string, record: UsageRecord, config: CuratorConfig, bundled: boolean): boolean;
96
+ export interface ScopeView {
97
+ /** Skills inside the lifecycle scope right now (candidate gate + active state). */
98
+ managed: string[];
99
+ /** Managed skills already flagged stale or quality-warned — the ones to watch. */
100
+ watched: string[];
101
+ /** Explicitly exempted by excludeSkillNames / referencedSkillNames. */
102
+ exempted: string[];
103
+ /** Carrying a protection marker (pinned / bundled / hub-installed). */
104
+ protected: string[];
105
+ }
106
+ /**
107
+ * Read-only scope classification, derived from the SAME gate the transition
108
+ * engine uses (`lifecycleCandidate`), so the view always predicts what a
109
+ * curator pass may touch.
110
+ */
111
+ export declare function computeScopeView(usage: UsageMap, config: CuratorConfig): ScopeView;
90
112
  export declare function computeLifecycleTransitions(usage: UsageMap, config: CuratorConfig, now?: Date): CuratorResult;
91
113
  //# sourceMappingURL=curator.d.ts.map
@@ -64,12 +64,21 @@ export declare class SkillLibrary {
64
64
  isManaged(name: string): Promise<boolean>;
65
65
  /** Whether the skill carries the bundled marker (curator prune-builtins eligibility). */
66
66
  isBundled(name: string): Promise<boolean>;
67
+ /** Whether the skill carries the pinned marker (the marker is the factual source; usage.pinned mirrors it). */
68
+ isPinned(name: string): Promise<boolean>;
67
69
  /** Count non-empty support subdirectories (richness input for quality scoring). */
68
70
  countSupportDirs(name: string): Promise<number>;
69
71
  /** Best-effort audit trail entry; never blocks the mutation. */
70
72
  private audit;
71
73
  /** Recent mutation audit records (read-only inspection surface). */
72
74
  listMutations(): Promise<MutationRecord[]>;
75
+ /**
76
+ * Pin or unpin a skill (`.pinned` marker). Pinned skills are protected from
77
+ * deletion, from background-review writes, and from the lifecycle — a
78
+ * protective mutation, so the autonomous pipeline may never call it. The
79
+ * marker write is the only state change; content is untouched.
80
+ */
81
+ setPinned(name: string, pinned: boolean, origin?: WriteOrigin): Promise<SkillActionResult>;
73
82
  create(name: string, content: string, origin: 'foreground' | 'background_review'): Promise<SkillActionResult>;
74
83
  update(name: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
75
84
  patch(name: string, oldString: string, newString: string, filePath?: string, replaceAll?: boolean, origin?: WriteOrigin): Promise<SkillActionResult>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-core",
3
3
  "description": "Shared stores, prompts, signals and lifecycle logic for the dsh-evolution plugin family (community build)",
4
- "version": "0.1.0-rc.26",
4
+ "version": "0.1.0-rc.27",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },