@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 +75 -17
- package/lib/types/events.d.ts +9 -0
- package/lib/types/skill-store.d.ts +15 -0
- package/package.json +1 -1
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)
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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
|
|
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
|
|
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 !==
|
|
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
|
|
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:
|
|
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
|
package/lib/types/events.d.ts
CHANGED
|
@@ -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