@lmzhen/dsh-evolution-core 0.3.66 → 0.3.67

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
@@ -796,6 +796,14 @@ function parseSuppressed(raw) {
796
796
  }
797
797
  }
798
798
  async function saveSuppressedNames(root, names, io = nodeEvolutionIo()) {
799
+ const current = await io.readText(suppressedFile(root)).catch(() => null);
800
+ if (current !== null) try {
801
+ const parsed = JSON.parse(current);
802
+ if (parsed !== null && typeof parsed.version === "number" && parsed.version > 1) {
803
+ console.warn(`suppression sidecar ${suppressedFile(root)} declares version ${String(parsed.version)} (newer than 1); not overwritten`);
804
+ return;
805
+ }
806
+ } catch {}
799
807
  await io.writeText(suppressedFile(root), JSON.stringify({
800
808
  version: 1,
801
809
  names: [...names].sort()
@@ -809,10 +817,17 @@ async function saveSuppressedNames(root, names, io = nodeEvolutionIo()) {
809
817
  */
810
818
  async function updateSuppressedNames(root, io, task) {
811
819
  await transactIo(io, suppressedFile(root), async (current) => {
812
- if (current !== null) try {
813
- JSON.parse(current);
814
- } catch {
815
- return current;
820
+ if (current !== null) {
821
+ let parsed = null;
822
+ try {
823
+ parsed = JSON.parse(current);
824
+ } catch {
825
+ return current;
826
+ }
827
+ if (parsed !== null && typeof parsed.version === "number" && parsed.version > 1) {
828
+ console.warn(`suppression sidecar ${suppressedFile(root)} declares version ${String(parsed.version)} (newer than 1); not overwritten`);
829
+ return current;
830
+ }
816
831
  }
817
832
  const names = parseSuppressed(current);
818
833
  await task(names);
@@ -5211,10 +5226,12 @@ var SkillLibrary = class {
5211
5226
  };
5212
5227
  const writes = [];
5213
5228
  for (const reference of referenceWrites) {
5214
- const base = (await this.io.readText(reference.target).catch(() => null))?.trimEnd() ?? "";
5229
+ const previous = await this.io.readText(reference.target).catch(() => null);
5230
+ const base = previous?.trimEnd() ?? "";
5215
5231
  writes.push({
5216
5232
  target: reference.target,
5217
- content: base === "" ? reference.content : `${base}\n\n${reference.content}`
5233
+ content: base === "" ? reference.content : `${base}\n\n${reference.content}`,
5234
+ expected: previous
5218
5235
  });
5219
5236
  }
5220
5237
  if (mode === "append") {
@@ -5226,7 +5243,8 @@ var SkillLibrary = class {
5226
5243
  };
5227
5244
  writes.push({
5228
5245
  target: join(targetDir, "SKILL.md"),
5229
- content: merged
5246
+ content: merged,
5247
+ expected: freshTargetMd
5230
5248
  });
5231
5249
  } else {
5232
5250
  const pointerLines = normalizedSources.map((source) => `\n${POINTER_LINE_PREFIX}${source}.md`).join("");
@@ -5238,7 +5256,8 @@ var SkillLibrary = class {
5238
5256
  };
5239
5257
  writes.push({
5240
5258
  target: join(targetDir, "SKILL.md"),
5241
- content: extended
5259
+ content: extended,
5260
+ expected: freshTargetMd
5242
5261
  });
5243
5262
  }
5244
5263
  return await this.applyTreeChange({
@@ -5306,6 +5325,11 @@ var SkillLibrary = class {
5306
5325
  message: `Restructure exceeds 5 moves.`
5307
5326
  };
5308
5327
  for (const move of moves) {
5328
+ const raw = move;
5329
+ if (raw === null || typeof raw !== "object") return {
5330
+ ok: false,
5331
+ message: "Every restructure move must be an object with a heading."
5332
+ };
5309
5333
  if (typeof move.heading !== "string" || !move.heading.trim()) return {
5310
5334
  ok: false,
5311
5335
  message: "Every restructure move needs a non-empty heading."
@@ -5359,15 +5383,18 @@ var SkillLibrary = class {
5359
5383
  const writes = [];
5360
5384
  for (const entry of byRel.values()) {
5361
5385
  const target = join(dir, ...entry.rel.split("/"));
5362
- const base = (await this.io.readText(target).catch(() => null))?.trimEnd() ?? "";
5386
+ const previous = await this.io.readText(target).catch(() => null);
5387
+ const base = previous?.trimEnd() ?? "";
5363
5388
  writes.push({
5364
5389
  target,
5365
- content: base === "" ? entry.texts.join("\n\n") : `${base}\n\n${entry.texts.join("\n\n")}`
5390
+ content: base === "" ? entry.texts.join("\n\n") : `${base}\n\n${entry.texts.join("\n\n")}`,
5391
+ expected: previous
5366
5392
  });
5367
5393
  }
5368
5394
  writes.push({
5369
5395
  target: join(dir, "SKILL.md"),
5370
- content: finalMd
5396
+ content: finalMd,
5397
+ expected: md
5371
5398
  });
5372
5399
  const result = await this.applyTreeChange({
5373
5400
  name,
@@ -5433,7 +5460,8 @@ var SkillLibrary = class {
5433
5460
  landing.push({
5434
5461
  target: write.target,
5435
5462
  content: write.content,
5436
- previous
5463
+ previous,
5464
+ expected: write.expected
5437
5465
  });
5438
5466
  }
5439
5467
  const written = [];
@@ -5442,9 +5470,10 @@ var SkillLibrary = class {
5442
5470
  for (const entry of landing) {
5443
5471
  try {
5444
5472
  if (this.transact) {
5473
+ const baseline = entry.expected === void 0 ? entry.previous : entry.expected;
5445
5474
  const drift = { seen: false };
5446
5475
  await this.transact(this.io, entry.target, (current) => {
5447
- if (current !== entry.previous) {
5476
+ if (current !== baseline) {
5448
5477
  drift.seen = true;
5449
5478
  return current;
5450
5479
  }
@@ -5707,7 +5736,13 @@ var SkillLibrary = class {
5707
5736
  while (await this.io.exists(dest)) dest = join(backupRoot, `skills-${stamp}-${Math.random().toString(36).slice(2, 8)}`);
5708
5737
  try {
5709
5738
  const names = await listNames(this.root, this.io);
5710
- const copyFailure = (await Promise.allSettled(names.map(async (name) => {
5739
+ const skipped = [];
5740
+ const copyable = [];
5741
+ for (const name of names) if (await this.hasWriteLock(this.dirOf(name))) {
5742
+ console.warn(`skill-store: snapshot skipped "${name}" — a byte-writer holds its write lock; the skill is recorded as skipped in the manifest`);
5743
+ skipped.push(name);
5744
+ } else copyable.push(name);
5745
+ const copyFailure = (await Promise.allSettled(copyable.map(async (name) => {
5711
5746
  await this.io.copy(this.dirOf(name), join(dest, name));
5712
5747
  }))).find((result) => result.status === "rejected");
5713
5748
  if (copyFailure) throw copyFailure.reason;
@@ -5732,7 +5767,8 @@ var SkillLibrary = class {
5732
5767
  await this.io.writeText(join(dest, "manifest.json"), JSON.stringify({
5733
5768
  reason,
5734
5769
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5735
- skills: names,
5770
+ skills: copyable,
5771
+ skipped,
5736
5772
  sidecars,
5737
5773
  hasArchive,
5738
5774
  extras: extraNames
@@ -5745,6 +5781,24 @@ var SkillLibrary = class {
5745
5781
  return dest;
5746
5782
  }
5747
5783
  /** Read and normalize a snapshot manifest; null when the file is missing or unparsable. */
5784
+ /**
5785
+ * V26-03 (v25/v26): sanitize the manifest's `skipped` list before it can
5786
+ * reach a user-visible restore message. Entries must pass the same name
5787
+ * gate as snapshot entries (a corrupted or hand-edited manifest cannot
5788
+ * inject arbitrary text into the result), bounded to 50 entries of at most
5789
+ * 64 chars each (the name-rule maximum — real skill names always fit).
5790
+ */
5791
+ sanitizeSkippedNames(raw) {
5792
+ if (!Array.isArray(raw)) return [];
5793
+ const out = [];
5794
+ for (const entry of raw) {
5795
+ if (typeof entry !== "string") continue;
5796
+ if (!this.safeSnapshotEntryName(entry)) continue;
5797
+ out.push(entry.length > 64 ? entry.slice(0, 64) : entry);
5798
+ if (out.length >= 50) break;
5799
+ }
5800
+ return out;
5801
+ }
5748
5802
  async readSnapshotManifest(path) {
5749
5803
  const raw = await this.io.readText(join(path, "manifest.json"));
5750
5804
  if (raw === null) return null;
@@ -5755,6 +5809,7 @@ var SkillLibrary = class {
5755
5809
  reason: typeof manifest.reason === "string" ? manifest.reason : "",
5756
5810
  createdAt: typeof manifest.createdAt === "string" ? manifest.createdAt : "",
5757
5811
  skills: manifest.skills,
5812
+ skipped: this.sanitizeSkippedNames(manifest.skipped),
5758
5813
  sidecars: Array.isArray(manifest.sidecars) ? manifest.sidecars : [],
5759
5814
  ...typeof manifest.hasArchive === "boolean" ? { hasArchive: manifest.hasArchive } : {},
5760
5815
  extras: Array.isArray(manifest.extras) ? manifest.extras : []
@@ -5831,8 +5886,9 @@ var SkillLibrary = class {
5831
5886
  message: "No skill snapshot available."
5832
5887
  };
5833
5888
  const preRollbackPath = await this.snapshotAll("pre-rollback", extras);
5889
+ let skipped = [];
5834
5890
  try {
5835
- await this.restoreSnapshotIntoRoot(latest.path);
5891
+ skipped = await this.restoreSnapshotIntoRoot(latest.path);
5836
5892
  } catch (error) {
5837
5893
  const reason = error instanceof Error ? error.message : String(error);
5838
5894
  try {
@@ -5853,9 +5909,10 @@ var SkillLibrary = class {
5853
5909
  action: "restore",
5854
5910
  name: "snapshot"
5855
5911
  });
5912
+ const skippedNote = skipped.length === 0 ? "" : ` NOTE: ${skipped.length} skill(s) were skipped when this snapshot was taken (a live writer held their lock) and are NOT restored: ${skipped.join(", ")} — recover them from .backups if a copy exists.`;
5856
5913
  return {
5857
5914
  ok: true,
5858
- message: `Restored skill tree from ${latest.path}`,
5915
+ message: `Restored skill tree from ${latest.path}.${skippedNote}`,
5859
5916
  path: latest.path,
5860
5917
  ...snapshotExtras.length === 0 ? {} : { extras: snapshotExtras }
5861
5918
  };
@@ -5920,6 +5977,7 @@ var SkillLibrary = class {
5920
5977
  if (entry.startsWith(".")) continue;
5921
5978
  await this.deleteStrandedLocks(join(this.root, entry));
5922
5979
  }
5980
+ return manifest.skipped;
5923
5981
  }
5924
5982
  };
5925
5983
  //#endregion
@@ -21,6 +21,15 @@ export interface EvolutionReviewScheduledEvent {
21
21
  toolCalls: number;
22
22
  userChars: number;
23
23
  assistantChars: number;
24
+ /** V24-15 (v24): which delivery channel actually sent the review. The
25
+ * emission point was previously covered on only two of the four delivery
26
+ * paths (subagent success + completion inject), so a consumer on the
27
+ * default inject-mode deployment would have silently missed every cadence
28
+ * review. Every delivery path now emits with its channel:
29
+ * `'subagent'` (spawned review run), `'inject'` (prompt injected into the
30
+ * parent — direct, fallback, or deferred-drain), `'completion'`
31
+ * (completion-trigger prompt, direct or deferred-drain). */
32
+ channel?: 'subagent' | 'inject' | 'completion' | undefined;
24
33
  }
25
34
  export interface EvolutionPlanAppliedEvent {
26
35
  /** Owning session (payload v2): process events carry no session envelope. */
@@ -77,6 +77,13 @@ export interface SnapshotManifest {
77
77
  createdAt: string;
78
78
  /** Active skill names at snapshot time. */
79
79
  skills: string[];
80
+ /** V25-05 (v25): skills that were SKIPPED by the write-lock probe at
81
+ * snapshot time (a live byte-writer held them) — they are NOT in the
82
+ * snapshot directory and are NOT restored by a whole-tree restore, which
83
+ * clears the live tree and copies back only `skills`. Consumers must
84
+ * surface this list; restore reports it in its result message. Absent
85
+ * (empty) on pre-0.3.67 manifests. */
86
+ skipped: string[];
80
87
  /** Co-copied sidecar file names (usage/suppression). */
81
88
  sidecars: string[];
82
89
  /** Whether `.archive/` was co-copied; absent on legacy manifests (do not touch archive on restore). */
@@ -493,6 +500,14 @@ export declare class SkillLibrary {
493
500
  */
494
501
  snapshotAll(reason?: string, extras?: SnapshotExtra[]): Promise<string>;
495
502
  /** Read and normalize a snapshot manifest; null when the file is missing or unparsable. */
503
+ /**
504
+ * V26-03 (v25/v26): sanitize the manifest's `skipped` list before it can
505
+ * reach a user-visible restore message. Entries must pass the same name
506
+ * gate as snapshot entries (a corrupted or hand-edited manifest cannot
507
+ * inject arbitrary text into the result), bounded to 50 entries of at most
508
+ * 64 chars each (the name-rule maximum — real skill names always fit).
509
+ */
510
+ private sanitizeSkippedNames;
496
511
  readSnapshotManifest(path: string): Promise<SnapshotManifest | null>;
497
512
  /** Keep only the newest N snapshots (Hermes keep=5 parity); older ones are removed outright. */
498
513
  private retainSnapshots;
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.3.66",
4
+ "version": "0.3.67",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },