@lmzhen/dsh-evolution-core 0.1.0-rc.37 → 0.1.0-rc.39

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
@@ -377,6 +377,7 @@ function lifecycleCandidate(name, record, config, bundled) {
377
377
  function computeScopeView(usage, config, protectedNames) {
378
378
  const managed = [];
379
379
  const watched = [];
380
+ const qualityWarned = [];
380
381
  const exempted = [];
381
382
  const protectedSet = /* @__PURE__ */ new Set();
382
383
  for (const [name, record] of usage) {
@@ -390,11 +391,13 @@ function computeScopeView(usage, config, protectedNames) {
390
391
  if (lifecycleCandidate(name, record, config, bundled)) {
391
392
  managed.push(name);
392
393
  if (record.state === "stale" || record.quality_warn === true) watched.push(name);
394
+ if (record.quality_warn === true) qualityWarned.push(name);
393
395
  }
394
396
  }
395
397
  return {
396
398
  managed: managed.sort(),
397
399
  watched: watched.sort(),
400
+ qualityWarned: qualityWarned.sort(),
398
401
  exempted: exempted.sort(),
399
402
  protected: [...protectedSet].sort()
400
403
  };
@@ -1588,6 +1591,8 @@ const DEFAULT_SKILL_LIMITS = {
1588
1591
  maxSkillContentChars: MAX_SKILL_CONTENT_CHARS,
1589
1592
  maxSkillFileBytes: MAX_SKILL_FILE_BYTES
1590
1593
  };
1594
+ /** Extra file name carried inside a snapshot's `extras/` directory. */
1595
+ const SNAPSHOT_EXTRA_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
1591
1596
  function skillsRoot(env = process.env) {
1592
1597
  return join(env.DSH_HOME ?? join(homedir(), ".dsh"), "skills");
1593
1598
  }
@@ -2241,7 +2246,13 @@ var SkillLibrary = class {
2241
2246
  path: target
2242
2247
  };
2243
2248
  }
2244
- async snapshotAll(reason = "pre-mutation") {
2249
+ /**
2250
+ * Snapshot the recoverable skills state: active tree, usage/suppression
2251
+ * sidecars, `.archive/` and caller-supplied extras. `extras` are opaque
2252
+ * side files the Snapshot owner cares about (curator state); they are
2253
+ * listed in the manifest and only those names are ever read back.
2254
+ */
2255
+ async snapshotAll(reason = "pre-mutation", extras = []) {
2245
2256
  const dest = join(join(this.root, ".backups"), `skills-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
2246
2257
  const names = await listNames(this.root, this.io);
2247
2258
  for (const name of names) await this.io.copy(skillDir(this.root, name), join(dest, name));
@@ -2251,15 +2262,47 @@ var SkillLibrary = class {
2251
2262
  await this.io.copy(sidecar, join(dest, name));
2252
2263
  sidecars.push(name);
2253
2264
  }
2265
+ const archiveRoot = join(this.root, ".archive");
2266
+ let hasArchive = false;
2267
+ if (await this.io.exists(archiveRoot)) {
2268
+ await this.io.copy(archiveRoot, join(dest, ".archive"));
2269
+ hasArchive = true;
2270
+ }
2271
+ const extraNames = [];
2272
+ for (const extra of extras) {
2273
+ if (!SNAPSHOT_EXTRA_NAME_RE.test(extra.name)) continue;
2274
+ await this.io.writeText(join(dest, "extras", extra.name), extra.content);
2275
+ extraNames.push(extra.name);
2276
+ }
2254
2277
  await this.io.writeText(join(dest, "manifest.json"), JSON.stringify({
2255
2278
  reason,
2256
2279
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2257
2280
  skills: names,
2258
- sidecars
2281
+ sidecars,
2282
+ hasArchive,
2283
+ extras: extraNames
2259
2284
  }, null, 2));
2260
2285
  await this.retainSnapshots(5);
2261
2286
  return dest;
2262
2287
  }
2288
+ /** Read and normalize a snapshot manifest; null when the file is missing or unparsable. */
2289
+ async readSnapshotManifest(path) {
2290
+ const raw = await this.io.readText(join(path, "manifest.json"));
2291
+ if (raw === null) return null;
2292
+ try {
2293
+ const manifest = JSON.parse(raw);
2294
+ return {
2295
+ reason: typeof manifest.reason === "string" ? manifest.reason : "",
2296
+ createdAt: typeof manifest.createdAt === "string" ? manifest.createdAt : "",
2297
+ skills: Array.isArray(manifest.skills) ? manifest.skills : [],
2298
+ sidecars: Array.isArray(manifest.sidecars) ? manifest.sidecars : [],
2299
+ ...typeof manifest.hasArchive === "boolean" ? { hasArchive: manifest.hasArchive } : {},
2300
+ extras: Array.isArray(manifest.extras) ? manifest.extras : []
2301
+ };
2302
+ } catch {
2303
+ return null;
2304
+ }
2305
+ }
2263
2306
  /** Keep only the newest N snapshots (Hermes keep=5 parity); oldest folded into .backups history. */
2264
2307
  async retainSnapshots(keep) {
2265
2308
  const snapshots = await this.listSnapshots();
@@ -2278,36 +2321,69 @@ var SkillLibrary = class {
2278
2321
  const out = [];
2279
2322
  for (const name of entries.sort().reverse()) {
2280
2323
  if (!name.startsWith("skills-")) continue;
2281
- try {
2282
- const raw = await this.io.readText(join(backupRoot, name, "manifest.json"));
2283
- if (raw === null) continue;
2284
- const manifest = JSON.parse(raw);
2285
- out.push({
2286
- path: join(backupRoot, name),
2287
- createdAt: manifest.createdAt ?? "",
2288
- reason: manifest.reason ?? ""
2289
- });
2290
- } catch {}
2324
+ const manifest = await this.readSnapshotManifest(join(backupRoot, name));
2325
+ if (manifest === null) continue;
2326
+ out.push({
2327
+ path: join(backupRoot, name),
2328
+ createdAt: manifest.createdAt,
2329
+ reason: manifest.reason
2330
+ });
2291
2331
  }
2292
2332
  return out;
2293
2333
  }
2294
- async restoreLatestSnapshot() {
2334
+ /**
2335
+ * Read the extras of a snapshot, restricted to the names declared in the
2336
+ * manifest — an `extras/` directory is never listed directly, so unknown
2337
+ * files cannot leak back as state on the next restore.
2338
+ */
2339
+ async readSnapshotExtras(path) {
2340
+ const manifest = await this.readSnapshotManifest(path);
2341
+ if (manifest === null) return [];
2342
+ const extras = [];
2343
+ for (const name of manifest.extras) {
2344
+ if (!SNAPSHOT_EXTRA_NAME_RE.test(name)) continue;
2345
+ const content = await this.io.readText(join(path, "extras", name));
2346
+ if (content !== null) extras.push({
2347
+ name,
2348
+ content
2349
+ });
2350
+ }
2351
+ return extras;
2352
+ }
2353
+ /**
2354
+ * Manifest-driven restore of the latest snapshot: active tree, sidecars,
2355
+ * `.archive/` and (for full-state snapshots) the extras read back by the
2356
+ * caller. `extras` are additionally written into the pre-rollback safety
2357
+ * snapshot so the rollback itself is undoable with the same state.
2358
+ */
2359
+ async restoreLatestSnapshot(extras = []) {
2295
2360
  const latest = (await this.listSnapshots())[0];
2296
2361
  if (!latest) return {
2297
2362
  ok: false,
2298
2363
  message: "No skill snapshot available."
2299
2364
  };
2300
- await this.snapshotAll("pre-rollback");
2365
+ await this.snapshotAll("pre-rollback", extras);
2301
2366
  for (const name of await listNames(this.root, this.io)) await this.io.remove(skillDir(this.root, name));
2302
- const entries = await this.io.list(latest.path);
2303
- for (const entry of entries) {
2304
- if (entry === "manifest.json") continue;
2367
+ const manifest = await this.readSnapshotManifest(latest.path);
2368
+ if (manifest === null) for (const entry of await this.io.list(latest.path)) {
2369
+ if (entry === "manifest.json" || entry === "extras") continue;
2305
2370
  await this.io.copy(join(latest.path, entry), join(this.root, entry));
2306
2371
  }
2372
+ else {
2373
+ for (const name of manifest.skills) await this.io.copy(join(latest.path, name), join(this.root, name));
2374
+ for (const sidecar of manifest.sidecars) await this.io.copy(join(latest.path, sidecar), join(this.root, sidecar));
2375
+ const archiveRoot = join(this.root, ".archive");
2376
+ if (manifest.hasArchive === true) {
2377
+ await this.io.remove(archiveRoot);
2378
+ await this.io.copy(join(latest.path, ".archive"), archiveRoot);
2379
+ } else if (manifest.hasArchive === false) await this.io.remove(archiveRoot);
2380
+ }
2381
+ const snapshotExtras = await this.readSnapshotExtras(latest.path);
2307
2382
  return {
2308
2383
  ok: true,
2309
2384
  message: `Restored skill tree from ${latest.path}`,
2310
- path: latest.path
2385
+ path: latest.path,
2386
+ ...snapshotExtras.length === 0 ? {} : { extras: snapshotExtras }
2311
2387
  };
2312
2388
  }
2313
2389
  };
@@ -2377,4 +2453,4 @@ var JsonState = class JsonState {
2377
2453
  }
2378
2454
  };
2379
2455
  //#endregion
2380
- 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 };
2456
+ 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, SNAPSHOT_EXTRA_NAME_RE, 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 };
@@ -101,6 +101,8 @@ export interface ScopeView {
101
101
  managed: string[];
102
102
  /** Managed skills already flagged stale or quality-warned — the ones to watch. */
103
103
  watched: string[];
104
+ /** Managed skills flagged low quality (subset of `watched`) — consolidation candidates. */
105
+ qualityWarned: string[];
104
106
  /** Explicitly exempted by excludeSkillNames / referencedSkillNames. */
105
107
  exempted: string[];
106
108
  /** Carrying a protection marker (pinned / bundled / hub-installed). */
@@ -28,6 +28,26 @@ export interface SkillActionResult {
28
28
  message: string;
29
29
  path?: string;
30
30
  }
31
+ /** Extra file name carried inside a snapshot's `extras/` directory. */
32
+ export declare const SNAPSHOT_EXTRA_NAME_RE: RegExp;
33
+ /** An opaque side file stored under a snapshot's `extras/` (curator state etc.). */
34
+ export interface SnapshotExtra {
35
+ name: string;
36
+ content: string;
37
+ }
38
+ /** Normalized manifest of a skills snapshot. */
39
+ export interface SnapshotManifest {
40
+ reason: string;
41
+ createdAt: string;
42
+ /** Active skill names at snapshot time. */
43
+ skills: string[];
44
+ /** Co-copied sidecar file names (usage/suppression). */
45
+ sidecars: string[];
46
+ /** Whether `.archive/` was co-copied; absent on legacy manifests (do not touch archive on restore). */
47
+ hasArchive?: boolean;
48
+ /** Extras declared under `extras/`; only these names are ever read back. */
49
+ extras: string[];
50
+ }
31
51
  /** Who is writing: a foreground user-directed tool call, or the autonomous review/curator pipeline. */
32
52
  export type WriteOrigin = 'foreground' | 'subagent' | 'background_review';
33
53
  /** Options for `SkillLibrary.archive`. The absorbed-into name and the archival reason are distinct fields. */
@@ -99,7 +119,15 @@ export declare class SkillLibrary {
99
119
  restoreFromArchive(name: string): Promise<SkillActionResult>;
100
120
  writeSupportFile(name: string, filePath: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
101
121
  removeSupportFile(name: string, filePath: string, origin?: WriteOrigin): Promise<SkillActionResult>;
102
- snapshotAll(reason?: string): Promise<string>;
122
+ /**
123
+ * Snapshot the recoverable skills state: active tree, usage/suppression
124
+ * sidecars, `.archive/` and caller-supplied extras. `extras` are opaque
125
+ * side files the Snapshot owner cares about (curator state); they are
126
+ * listed in the manifest and only those names are ever read back.
127
+ */
128
+ snapshotAll(reason?: string, extras?: SnapshotExtra[]): Promise<string>;
129
+ /** Read and normalize a snapshot manifest; null when the file is missing or unparsable. */
130
+ readSnapshotManifest(path: string): Promise<SnapshotManifest | null>;
103
131
  /** Keep only the newest N snapshots (Hermes keep=5 parity); oldest folded into .backups history. */
104
132
  private retainSnapshots;
105
133
  listSnapshots(): Promise<Array<{
@@ -107,6 +135,20 @@ export declare class SkillLibrary {
107
135
  createdAt: string;
108
136
  reason: string;
109
137
  }>>;
110
- restoreLatestSnapshot(): Promise<SkillActionResult>;
138
+ /**
139
+ * Read the extras of a snapshot, restricted to the names declared in the
140
+ * manifest — an `extras/` directory is never listed directly, so unknown
141
+ * files cannot leak back as state on the next restore.
142
+ */
143
+ readSnapshotExtras(path: string): Promise<SnapshotExtra[]>;
144
+ /**
145
+ * Manifest-driven restore of the latest snapshot: active tree, sidecars,
146
+ * `.archive/` and (for full-state snapshots) the extras read back by the
147
+ * caller. `extras` are additionally written into the pre-rollback safety
148
+ * snapshot so the rollback itself is undoable with the same state.
149
+ */
150
+ restoreLatestSnapshot(extras?: SnapshotExtra[]): Promise<SkillActionResult & {
151
+ extras?: SnapshotExtra[];
152
+ }>;
111
153
  }
112
154
  //# sourceMappingURL=skill-store.d.ts.map
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.37",
4
+ "version": "0.1.0-rc.39",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },