@lmzhen/dsh-evolution-core 0.3.21 → 0.3.22
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/README.md +20 -0
- package/lib/index.js +298 -117
- package/lib/types/evolution-events.d.ts +11 -1
- package/lib/types/skill-store.d.ts +27 -4
- package/lib/types/state-store.d.ts +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,26 @@ Zero direct token effect from this package; consumers add any model-visible toke
|
|
|
18
18
|
|
|
19
19
|
Independent of request-prefix construction. This package does not alter the assembled prompt or tool list.
|
|
20
20
|
|
|
21
|
+
## SkillLibrary concurrency model
|
|
22
|
+
|
|
23
|
+
Skill-library mutations are read-modify-write on one file, so `SkillLibrary`
|
|
24
|
+
serializes them in-process with a `makeSerialQueue` chain: `update`, `patch`,
|
|
25
|
+
`restructure` and `writeSupportFile` run their whole read→validate→write under
|
|
26
|
+
one serial task, so two concurrent mutators on one skill never interleave in
|
|
27
|
+
this process. Single-file writes (`update`, `patch`, `writeSupportFile`)
|
|
28
|
+
additionally run the read and the write inside `transactIo` when a caller
|
|
29
|
+
injects a `transact` into the constructor — that is the cross-process lock, so
|
|
30
|
+
two processes sharing `DSH_HOME` cannot interleave their RMW on one file.
|
|
31
|
+
`create` writes a new file and `archive`/`consolidate` already own a two-phase
|
|
32
|
+
commit, so they deliberately stay outside the serial chain.
|
|
33
|
+
|
|
34
|
+
When no `transact` is injected (the current default callers), only the
|
|
35
|
+
in-process serial chain protects the RMW; same-skill concurrent writes from
|
|
36
|
+
different surfaces (foreground `skill_manage`, the review pipeline, the
|
|
37
|
+
curator, `/evolution restructure`) still resolve **last writer wins**. Wire a
|
|
38
|
+
`transact` at every SkillLibrary instantiation point to extend that guarantee
|
|
39
|
+
across processes.
|
|
40
|
+
|
|
21
41
|
## Known Limitations and Deferred Work
|
|
22
42
|
|
|
23
43
|
- This package is a library, not a Cordis row; do not mount it as a plugin.
|
package/lib/index.js
CHANGED
|
@@ -942,6 +942,13 @@ function isEventRecord(event) {
|
|
|
942
942
|
* rebuildable, NOT malformed) or a corrupt one reads as empty; corrupt content
|
|
943
943
|
* is still refused on append, never overwritten.
|
|
944
944
|
*
|
|
945
|
+
* This reader is **v1-only** (F-338): a body carrying a `version` other than
|
|
946
|
+
* `EVENT_LOG_VERSION` is a future-format log this reader cannot interpret, so
|
|
947
|
+
* it reads as an EMPTY timeline rather than being mis-parsed as v1. The read
|
|
948
|
+
* side never overwrites it on its own — `appendEvolutionEvent` rejects a
|
|
949
|
+
* version mismatch up front and preserves the original bytes, so a newer log
|
|
950
|
+
* is never silently downgraded here.
|
|
951
|
+
*
|
|
945
952
|
* Per-entry normalization (rc.70 F-1): entries without a numeric `seq` are
|
|
946
953
|
* skipped here and dropped at the next append — valid entries survive, the
|
|
947
954
|
* damaged record is the only loss (self-heal semantics, matching the usage
|
|
@@ -951,6 +958,7 @@ function parseEvolutionEvents(raw) {
|
|
|
951
958
|
if (raw === null || raw.trim() === "") return [];
|
|
952
959
|
try {
|
|
953
960
|
const parsed = JSON.parse(raw);
|
|
961
|
+
if (parsed.version !== void 0 && parsed.version !== 1) return [];
|
|
954
962
|
if (!Array.isArray(parsed.events)) return [];
|
|
955
963
|
return parsed.events.filter(isEventRecord);
|
|
956
964
|
} catch {
|
|
@@ -989,11 +997,19 @@ async function listEventArchives(io, path) {
|
|
|
989
997
|
*/
|
|
990
998
|
async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE_AT) {
|
|
991
999
|
let assigned = 0;
|
|
1000
|
+
let refuseMessage = "";
|
|
992
1001
|
await transactIo(io, path, async (current) => {
|
|
993
|
-
if (current !== null && current.trim() !== "")
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1002
|
+
if (current !== null && current.trim() !== "") {
|
|
1003
|
+
let shape;
|
|
1004
|
+
try {
|
|
1005
|
+
shape = JSON.parse(current);
|
|
1006
|
+
} catch {
|
|
1007
|
+
return current;
|
|
1008
|
+
}
|
|
1009
|
+
if (shape.version !== void 0 && shape.version !== 1) {
|
|
1010
|
+
refuseMessage = `evolution event log version mismatch (found ${typeof shape.version === "number" || typeof shape.version === "string" ? String(shape.version) : "unknown"}, expected 1) and was not touched`;
|
|
1011
|
+
return current;
|
|
1012
|
+
}
|
|
997
1013
|
}
|
|
998
1014
|
const nextEvents = await rotateIfDue(io, path, parseEvolutionEvents(current), rotateAt);
|
|
999
1015
|
let maxSeq = nextEvents.reduce((max, entry) => Math.max(max, entry.seq), 0);
|
|
@@ -1009,7 +1025,7 @@ async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE
|
|
|
1009
1025
|
events: [...nextEvents, record]
|
|
1010
1026
|
}, null, 2);
|
|
1011
1027
|
});
|
|
1012
|
-
if (assigned === 0) throw new Error(
|
|
1028
|
+
if (assigned === 0) throw new Error(`${refuseMessage || "evolution event log is malformed and was not touched"}: ${path}`);
|
|
1013
1029
|
return assigned;
|
|
1014
1030
|
}
|
|
1015
1031
|
/**
|
|
@@ -1049,7 +1065,10 @@ async function retainEventArchives(io, path) {
|
|
|
1049
1065
|
for (const name of excess) await io.remove(join(dir, name)).catch(() => {});
|
|
1050
1066
|
}
|
|
1051
1067
|
/** Read the event log; a missing/whitespace-only file reads as empty,
|
|
1052
|
-
* corrupt content is flagged (and refused on append).
|
|
1068
|
+
* corrupt content is flagged (and refused on append). A well-formed future-
|
|
1069
|
+
* version body is v1-incompatible and reads as empty, NOT malformed (F-338:
|
|
1070
|
+
* the reader must never mis-shape a newer format; the append path refuses it
|
|
1071
|
+
* up front so the original bytes survive). */
|
|
1053
1072
|
async function readEvolutionEvents(io, path) {
|
|
1054
1073
|
let raw;
|
|
1055
1074
|
try {
|
|
@@ -1066,6 +1085,10 @@ async function readEvolutionEvents(io, path) {
|
|
|
1066
1085
|
};
|
|
1067
1086
|
try {
|
|
1068
1087
|
const parsed = JSON.parse(raw);
|
|
1088
|
+
if (parsed.version !== void 0 && parsed.version !== 1) return {
|
|
1089
|
+
events: [],
|
|
1090
|
+
malformed: false
|
|
1091
|
+
};
|
|
1069
1092
|
if (!Array.isArray(parsed.events)) return {
|
|
1070
1093
|
events: [],
|
|
1071
1094
|
malformed: false
|
|
@@ -1810,6 +1833,14 @@ function render(entries) {
|
|
|
1810
1833
|
function stripDatePrefix(entry) {
|
|
1811
1834
|
return entry.replace(/^## \d{4}-\d{2}-\d{2}\n/, "");
|
|
1812
1835
|
}
|
|
1836
|
+
/** F-201: does `content` carry the on-disk entry delimiter or a trailing
|
|
1837
|
+
* `\n§` fragment that would combine with the render terminator into a real
|
|
1838
|
+
* delimiter boundary? Both split the fact into multiple entries on read-back
|
|
1839
|
+
* (and a delimiter-ending fact is permanent drift — `render(entries)!==raw`
|
|
1840
|
+
* bricks every later write). A leading/plain `§` is safe and round-trips. */
|
|
1841
|
+
function hasEntryDelimiter(content) {
|
|
1842
|
+
return content.includes("\n§\n") || content.endsWith("\n§");
|
|
1843
|
+
}
|
|
1813
1844
|
var MemoryStore = class {
|
|
1814
1845
|
memoryLimit;
|
|
1815
1846
|
userLimit;
|
|
@@ -1976,6 +2007,16 @@ var MemoryStore = class {
|
|
|
1976
2007
|
},
|
|
1977
2008
|
write: null
|
|
1978
2009
|
};
|
|
2010
|
+
if (hasEntryDelimiter(content)) return {
|
|
2011
|
+
result: {
|
|
2012
|
+
ok: false,
|
|
2013
|
+
message: "Operation 1 (add): Fact contains the entry delimiter (§) and would split into multiple entries; rewrite it as separate facts.",
|
|
2014
|
+
entries: [],
|
|
2015
|
+
chars: 0,
|
|
2016
|
+
limit: this.limitFor(target)
|
|
2017
|
+
},
|
|
2018
|
+
write: null
|
|
2019
|
+
};
|
|
1979
2020
|
const entries = [...new Set(normalizeEntries(raw))];
|
|
1980
2021
|
if (entries.some((entry) => stripDatePrefix(entry) === content)) {
|
|
1981
2022
|
this.resetFailures();
|
|
@@ -2078,6 +2119,16 @@ var MemoryStore = class {
|
|
|
2078
2119
|
},
|
|
2079
2120
|
write: null
|
|
2080
2121
|
};
|
|
2122
|
+
if (hasEntryDelimiter(body)) return {
|
|
2123
|
+
result: {
|
|
2124
|
+
ok: false,
|
|
2125
|
+
message: `Operation ${position} (add): Fact contains the entry delimiter (§) and would split into multiple entries; rewrite it as separate facts.`,
|
|
2126
|
+
entries,
|
|
2127
|
+
chars: entries.join(ENTRY_DELIMITER).length,
|
|
2128
|
+
limit: this.limitFor(target)
|
|
2129
|
+
},
|
|
2130
|
+
write: null
|
|
2131
|
+
};
|
|
2081
2132
|
if (!working.some((entry) => stripDatePrefix(entry) === body)) working.push(this.addDatePrefix ? `## ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}\n${body}` : body);
|
|
2082
2133
|
continue;
|
|
2083
2134
|
}
|
|
@@ -2135,6 +2186,16 @@ var MemoryStore = class {
|
|
|
2135
2186
|
},
|
|
2136
2187
|
write: null
|
|
2137
2188
|
};
|
|
2189
|
+
if (hasEntryDelimiter(body)) return {
|
|
2190
|
+
result: {
|
|
2191
|
+
ok: false,
|
|
2192
|
+
message: `Operation ${position} (replace): Fact contains the entry delimiter (§) and would split into multiple entries; rewrite it as separate facts.`,
|
|
2193
|
+
entries,
|
|
2194
|
+
chars: entries.join(ENTRY_DELIMITER).length,
|
|
2195
|
+
limit: this.limitFor(target)
|
|
2196
|
+
},
|
|
2197
|
+
write: null
|
|
2198
|
+
};
|
|
2138
2199
|
working[matchIndex] = body;
|
|
2139
2200
|
}
|
|
2140
2201
|
}
|
|
@@ -2829,7 +2890,9 @@ function skillsRoot(env = process.env) {
|
|
|
2829
2890
|
* the skills tree — tool-skill-manage / evolution-skill-catalog / skill-usage
|
|
2830
2891
|
* / evolution-learning-graph used to each resolve `config.root || skillsRoot()`
|
|
2831
2892
|
* (and the graph ignored config entirely). Empty/whitespace config falls
|
|
2832
|
-
* through to the default; callers pass their raw Config.
|
|
2893
|
+
* through to the default; callers pass their raw Config. The optional field is
|
|
2894
|
+
* declared `| undefined` so a config object whose root field is explicitly
|
|
2895
|
+
* `string | undefined` still assignable under exactOptionalPropertyTypes. */
|
|
2833
2896
|
function resolveSkillsRoot(config = {}) {
|
|
2834
2897
|
return (config.root ?? "").trim() || skillsRoot();
|
|
2835
2898
|
}
|
|
@@ -3176,15 +3239,9 @@ function fuzzyPatch(content, oldString, newString, replaceAll = false) {
|
|
|
3176
3239
|
const boundary = trimPatternBoundaries(oldString);
|
|
3177
3240
|
if (boundary === "") return null;
|
|
3178
3241
|
if (boundary !== oldString) {
|
|
3179
|
-
if (fuzzyIndexOf(content, boundary) !== null)
|
|
3180
|
-
const patched = fuzzyReplace(content, boundary, newString, replaceAll);
|
|
3181
|
-
return patched === content ? null : patched;
|
|
3182
|
-
}
|
|
3183
|
-
}
|
|
3184
|
-
if (fuzzyIndexOf(content, oldString) !== null) {
|
|
3185
|
-
const patched = fuzzyReplace(content, oldString, newString, replaceAll);
|
|
3186
|
-
return patched === content ? null : patched;
|
|
3242
|
+
if (fuzzyIndexOf(content, boundary) !== null) return fuzzyReplace(content, boundary, newString, replaceAll);
|
|
3187
3243
|
}
|
|
3244
|
+
if (fuzzyIndexOf(content, oldString) !== null) return fuzzyReplace(content, oldString, newString, replaceAll);
|
|
3188
3245
|
return null;
|
|
3189
3246
|
}
|
|
3190
3247
|
/**
|
|
@@ -3255,11 +3312,47 @@ var SkillLibrary = class {
|
|
|
3255
3312
|
limits;
|
|
3256
3313
|
io;
|
|
3257
3314
|
onMutation;
|
|
3258
|
-
|
|
3315
|
+
/** 0.3.21 (F-208): optional cross-process RMW transactor injected by callers.
|
|
3316
|
+
* When unset each single-file write falls back to read→task→write. */
|
|
3317
|
+
transact;
|
|
3318
|
+
/** 0.3.21 (F-208): in-process serialize queue so two concurrent mutators on
|
|
3319
|
+
* one skill never interleave their read-modify-write (the cross-process layer
|
|
3320
|
+
* is the IO backend's transact lock; this chain is the second layer). */
|
|
3321
|
+
serial;
|
|
3322
|
+
constructor(root = skillsRoot(), io = nodeEvolutionIo(), limits = DEFAULT_SKILL_LIMITS, onMutation, transact) {
|
|
3259
3323
|
this.root = root;
|
|
3260
3324
|
this.io = io;
|
|
3261
3325
|
this.limits = limits;
|
|
3262
3326
|
this.onMutation = onMutation;
|
|
3327
|
+
this.transact = transact;
|
|
3328
|
+
this.serial = makeSerialQueue();
|
|
3329
|
+
}
|
|
3330
|
+
/**
|
|
3331
|
+
* Run one single-file read-modify-write for a mutator. When `transact` was
|
|
3332
|
+
* injected the read and the write run inside it (cross-process atomicity);
|
|
3333
|
+
* otherwise a plain read → task → write sequence runs (the process-level
|
|
3334
|
+
* `serial` chain is the second layer). `task` receives the current content
|
|
3335
|
+
* (null when missing) and returns a {@link SingleWriteOutcome}. Audit and the
|
|
3336
|
+
* mutation event are issued ONLY when a write actually lands, so a no-op
|
|
3337
|
+
* never inflates the mutation-maturity counter.
|
|
3338
|
+
*/
|
|
3339
|
+
async runSingleWrite(path, task) {
|
|
3340
|
+
let outcome;
|
|
3341
|
+
const run = async (current) => {
|
|
3342
|
+
const o = await task(current ?? null);
|
|
3343
|
+
outcome = o;
|
|
3344
|
+
return o.write ?? current ?? null;
|
|
3345
|
+
};
|
|
3346
|
+
if (this.transact) await this.transact(this.io, path, run);
|
|
3347
|
+
else {
|
|
3348
|
+
const current = await this.io.readText(path);
|
|
3349
|
+
const next = await run(current);
|
|
3350
|
+
if (next !== null && next !== current) await this.io.writeText(path, next);
|
|
3351
|
+
}
|
|
3352
|
+
const o = outcome;
|
|
3353
|
+
if (o.write !== null && o.audit) await this.audit(o.audit.skillName, o.audit.action, o.audit.before, o.audit.after, o.audit.summary);
|
|
3354
|
+
if (o.write !== null && o.event) this.notifyMutation(o.event);
|
|
3355
|
+
return o.result;
|
|
3263
3356
|
}
|
|
3264
3357
|
/** Notify the mutation observer after a successful write; observers must never fail the mutation. */
|
|
3265
3358
|
notifyMutation(event) {
|
|
@@ -3516,9 +3609,10 @@ var SkillLibrary = class {
|
|
|
3516
3609
|
ok: false,
|
|
3517
3610
|
message: `Skill "${normalized}" already exists.`
|
|
3518
3611
|
};
|
|
3519
|
-
|
|
3612
|
+
const onDisk = finalContent.trimEnd() + "\n";
|
|
3613
|
+
await this.io.writeText(join(dir, "SKILL.md"), onDisk);
|
|
3520
3614
|
if (origin !== "foreground") await this.io.writeText(markerPath(dir, "hermes-managed"), "");
|
|
3521
|
-
await this.audit(normalized, "create", null,
|
|
3615
|
+
await this.audit(normalized, "create", null, onDisk, "created");
|
|
3522
3616
|
this.notifyMutation({
|
|
3523
3617
|
action: "create",
|
|
3524
3618
|
name: normalized,
|
|
@@ -3533,17 +3627,16 @@ var SkillLibrary = class {
|
|
|
3533
3627
|
}
|
|
3534
3628
|
async update(rawName, content, origin = "foreground") {
|
|
3535
3629
|
const name = rawName.trim();
|
|
3630
|
+
return await this.serial(() => this.updateCore(name, content, origin));
|
|
3631
|
+
}
|
|
3632
|
+
async updateCore(name, content, origin) {
|
|
3633
|
+
const dir = this.dirOf(name);
|
|
3634
|
+
const path = join(dir, "SKILL.md");
|
|
3536
3635
|
const badName = this.badName(name);
|
|
3537
3636
|
if (badName) return {
|
|
3538
3637
|
ok: false,
|
|
3539
3638
|
message: badName
|
|
3540
3639
|
};
|
|
3541
|
-
const dir = this.dirOf(name);
|
|
3542
|
-
const md = await this.io.readText(join(dir, "SKILL.md"));
|
|
3543
|
-
if (!md) return {
|
|
3544
|
-
ok: false,
|
|
3545
|
-
message: `Skill "${name}" not found.`
|
|
3546
|
-
};
|
|
3547
3640
|
const protection = await this.writeProtection(name, origin);
|
|
3548
3641
|
if (protection) return {
|
|
3549
3642
|
ok: false,
|
|
@@ -3572,27 +3665,52 @@ var SkillLibrary = class {
|
|
|
3572
3665
|
ok: false,
|
|
3573
3666
|
message: threat
|
|
3574
3667
|
};
|
|
3575
|
-
await this.
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3668
|
+
return await this.runSingleWrite(path, (current) => {
|
|
3669
|
+
if (current === null) return {
|
|
3670
|
+
result: {
|
|
3671
|
+
ok: false,
|
|
3672
|
+
message: `Skill "${name}" not found.`
|
|
3673
|
+
},
|
|
3674
|
+
write: null
|
|
3675
|
+
};
|
|
3676
|
+
if (finalContent.trimEnd() === current.trimEnd()) return {
|
|
3677
|
+
result: {
|
|
3678
|
+
ok: true,
|
|
3679
|
+
message: `Skill "${name}" unchanged: the supplied content already matches the current file; nothing written.`,
|
|
3680
|
+
noop: true,
|
|
3681
|
+
path: dir
|
|
3682
|
+
},
|
|
3683
|
+
write: null
|
|
3684
|
+
};
|
|
3685
|
+
const onDisk = finalContent.trimEnd() + "\n";
|
|
3686
|
+
return {
|
|
3687
|
+
result: {
|
|
3688
|
+
ok: true,
|
|
3689
|
+
message: `Skill "${name}" updated.`,
|
|
3690
|
+
path: dir,
|
|
3691
|
+
...norm.changed ? { normalizedFrontmatterFields: norm.fields } : {}
|
|
3692
|
+
},
|
|
3693
|
+
write: onDisk,
|
|
3694
|
+
audit: {
|
|
3695
|
+
skillName: name,
|
|
3696
|
+
action: "update",
|
|
3697
|
+
before: current,
|
|
3698
|
+
after: onDisk,
|
|
3699
|
+
summary: "updated"
|
|
3700
|
+
},
|
|
3701
|
+
event: {
|
|
3702
|
+
action: "update",
|
|
3703
|
+
name,
|
|
3704
|
+
skillDir: dir
|
|
3705
|
+
}
|
|
3706
|
+
};
|
|
3581
3707
|
});
|
|
3582
|
-
return {
|
|
3583
|
-
ok: true,
|
|
3584
|
-
message: `Skill "${name}" updated.`,
|
|
3585
|
-
path: dir,
|
|
3586
|
-
...norm.changed ? { normalizedFrontmatterFields: norm.fields } : {}
|
|
3587
|
-
};
|
|
3588
3708
|
}
|
|
3589
3709
|
async patch(rawName, oldString, newString, filePath = "", replaceAll = false, origin = "foreground") {
|
|
3590
3710
|
const name = rawName.trim();
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
message: badName
|
|
3595
|
-
};
|
|
3711
|
+
return await this.serial(() => this.patchCore(name, oldString, newString, filePath, replaceAll, origin));
|
|
3712
|
+
}
|
|
3713
|
+
async patchCore(name, oldString, newString, filePath, replaceAll, origin) {
|
|
3596
3714
|
const dir = this.dirOf(name);
|
|
3597
3715
|
const skillMd = join(dir, "SKILL.md");
|
|
3598
3716
|
if (!await this.io.exists(skillMd)) return {
|
|
@@ -3615,71 +3733,109 @@ var SkillLibrary = class {
|
|
|
3615
3733
|
target = join(dir, ...filePath.replace(/\\/g, "/").split("/").filter(Boolean));
|
|
3616
3734
|
patchLabel = filePath;
|
|
3617
3735
|
}
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
message: `Could not find old_string in "${name}/${patchLabel}". Use update for a full rewrite.`
|
|
3627
|
-
};
|
|
3628
|
-
let writeContent = patched;
|
|
3629
|
-
let normalizedFields;
|
|
3630
|
-
if (target === skillMd) {
|
|
3631
|
-
const validation = validateFrontmatter(patched, name, this.limits);
|
|
3632
|
-
if (validation) return {
|
|
3633
|
-
ok: false,
|
|
3634
|
-
message: `Patch rejected: ${validation}`
|
|
3635
|
-
};
|
|
3636
|
-
const norm = normalizeFrontmatter(patched);
|
|
3637
|
-
if (norm.issues.length > 0) return {
|
|
3638
|
-
ok: false,
|
|
3639
|
-
message: `Patch rejected: frontmatter cannot be auto-fixed (${norm.issues[0]}).`
|
|
3736
|
+
return await this.runSingleWrite(target, (current) => {
|
|
3737
|
+
const md = current;
|
|
3738
|
+
if (md === null) return {
|
|
3739
|
+
result: {
|
|
3740
|
+
ok: false,
|
|
3741
|
+
message: `File not found: ${patchLabel}`
|
|
3742
|
+
},
|
|
3743
|
+
write: null
|
|
3640
3744
|
};
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
const revalidated = validateFrontmatter(writeContent, name, this.limits);
|
|
3645
|
-
if (revalidated) return {
|
|
3745
|
+
const patched = fuzzyPatch(md, oldString, newString, replaceAll);
|
|
3746
|
+
if (patched === null) return {
|
|
3747
|
+
result: {
|
|
3646
3748
|
ok: false,
|
|
3647
|
-
message: `
|
|
3749
|
+
message: `Could not find old_string in "${name}/${patchLabel}". Use update for a full rewrite.`
|
|
3750
|
+
},
|
|
3751
|
+
write: null
|
|
3752
|
+
};
|
|
3753
|
+
let writeContent = patched;
|
|
3754
|
+
let normalizedFields;
|
|
3755
|
+
if (target === skillMd) {
|
|
3756
|
+
const validation = validateFrontmatter(patched, name, this.limits);
|
|
3757
|
+
if (validation) return {
|
|
3758
|
+
result: {
|
|
3759
|
+
ok: false,
|
|
3760
|
+
message: `Patch rejected: ${validation}`
|
|
3761
|
+
},
|
|
3762
|
+
write: null
|
|
3648
3763
|
};
|
|
3764
|
+
const norm = normalizeFrontmatter(patched);
|
|
3765
|
+
if (norm.issues.length > 0) return {
|
|
3766
|
+
result: {
|
|
3767
|
+
ok: false,
|
|
3768
|
+
message: `Patch rejected: frontmatter cannot be auto-fixed (${norm.issues[0]}).`
|
|
3769
|
+
},
|
|
3770
|
+
write: null
|
|
3771
|
+
};
|
|
3772
|
+
if (norm.changed) {
|
|
3773
|
+
writeContent = norm.content;
|
|
3774
|
+
normalizedFields = norm.fields;
|
|
3775
|
+
const revalidated = validateFrontmatter(writeContent, name, this.limits);
|
|
3776
|
+
if (revalidated) return {
|
|
3777
|
+
result: {
|
|
3778
|
+
ok: false,
|
|
3779
|
+
message: `Patch rejected: ${revalidated}`
|
|
3780
|
+
},
|
|
3781
|
+
write: null
|
|
3782
|
+
};
|
|
3783
|
+
}
|
|
3649
3784
|
}
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3785
|
+
if (Buffer.byteLength(writeContent, "utf8") > this.limits.maxSkillFileBytes && target !== skillMd) return {
|
|
3786
|
+
result: {
|
|
3787
|
+
ok: false,
|
|
3788
|
+
message: `Patched file exceeds ${this.limits.maxSkillFileBytes} bytes.`
|
|
3789
|
+
},
|
|
3790
|
+
write: null
|
|
3791
|
+
};
|
|
3792
|
+
if (writeContent.length > this.limits.maxSkillContentChars && target === skillMd) return {
|
|
3793
|
+
result: {
|
|
3794
|
+
ok: false,
|
|
3795
|
+
message: `Patched content exceeds ${this.limits.maxSkillContentChars} characters. Consider splitting into a smaller SKILL.md with supporting files.`
|
|
3796
|
+
},
|
|
3797
|
+
write: null
|
|
3798
|
+
};
|
|
3799
|
+
const threat = scanContentThreats(writeContent);
|
|
3800
|
+
if (threat) return {
|
|
3801
|
+
result: {
|
|
3802
|
+
ok: false,
|
|
3803
|
+
message: threat
|
|
3804
|
+
},
|
|
3805
|
+
write: null
|
|
3806
|
+
};
|
|
3807
|
+
if (writeContent.trimEnd() === md.trimEnd()) return {
|
|
3808
|
+
result: {
|
|
3809
|
+
ok: true,
|
|
3810
|
+
message: `Skill "${name}" unchanged: old_string already equals the replacement (${patchLabel}); nothing written.`,
|
|
3811
|
+
noop: true,
|
|
3812
|
+
path: dir
|
|
3813
|
+
},
|
|
3814
|
+
write: null
|
|
3815
|
+
};
|
|
3816
|
+
const onDisk = writeContent.trimEnd() + "\n";
|
|
3817
|
+
return {
|
|
3818
|
+
result: {
|
|
3819
|
+
ok: true,
|
|
3820
|
+
message: `Skill "${name}" patched (${patchLabel}).`,
|
|
3821
|
+
path: dir,
|
|
3822
|
+
...normalizedFields ? { normalizedFrontmatterFields: normalizedFields } : {}
|
|
3823
|
+
},
|
|
3824
|
+
write: onDisk,
|
|
3825
|
+
audit: {
|
|
3826
|
+
skillName: name,
|
|
3827
|
+
action: "patch",
|
|
3828
|
+
before: md,
|
|
3829
|
+
after: onDisk,
|
|
3830
|
+
summary: `patched ${patchLabel}`
|
|
3831
|
+
},
|
|
3832
|
+
event: {
|
|
3833
|
+
action: "patch",
|
|
3834
|
+
name,
|
|
3835
|
+
skillDir: dir
|
|
3836
|
+
}
|
|
3837
|
+
};
|
|
3676
3838
|
});
|
|
3677
|
-
return {
|
|
3678
|
-
ok: true,
|
|
3679
|
-
message: `Skill "${name}" patched (${patchLabel}).`,
|
|
3680
|
-
path: dir,
|
|
3681
|
-
...normalizedFields ? { normalizedFrontmatterFields: normalizedFields } : {}
|
|
3682
|
-
};
|
|
3683
3839
|
}
|
|
3684
3840
|
async archive(rawName, options = {}) {
|
|
3685
3841
|
const name = rawName.trim();
|
|
@@ -3933,6 +4089,9 @@ var SkillLibrary = class {
|
|
|
3933
4089
|
*/
|
|
3934
4090
|
async restructure(rawName, moves, origin = "foreground") {
|
|
3935
4091
|
const name = rawName.trim();
|
|
4092
|
+
return await this.serial(() => this.restructureCore(name, moves, origin));
|
|
4093
|
+
}
|
|
4094
|
+
async restructureCore(name, moves, origin) {
|
|
3936
4095
|
const badName = this.badName(name);
|
|
3937
4096
|
if (badName) return {
|
|
3938
4097
|
ok: false,
|
|
@@ -4181,12 +4340,15 @@ var SkillLibrary = class {
|
|
|
4181
4340
|
}
|
|
4182
4341
|
async writeSupportFile(rawName, filePath, content, origin = "foreground") {
|
|
4183
4342
|
const name = rawName.trim();
|
|
4343
|
+
return await this.serial(() => this.writeSupportFileCore(name, filePath, content, origin));
|
|
4344
|
+
}
|
|
4345
|
+
async writeSupportFileCore(name, filePath, content, origin) {
|
|
4346
|
+
const dir = this.dirOf(name);
|
|
4184
4347
|
const badName = this.badName(name);
|
|
4185
4348
|
if (badName) return {
|
|
4186
4349
|
ok: false,
|
|
4187
4350
|
message: badName
|
|
4188
4351
|
};
|
|
4189
|
-
const dir = this.dirOf(name);
|
|
4190
4352
|
if (!await this.io.exists(join(dir, "SKILL.md"))) return {
|
|
4191
4353
|
ok: false,
|
|
4192
4354
|
message: `Skill "${name}" not found.`
|
|
@@ -4211,20 +4373,29 @@ var SkillLibrary = class {
|
|
|
4211
4373
|
message: threat
|
|
4212
4374
|
};
|
|
4213
4375
|
const target = join(dir, ...filePath.replace(/\\/g, "/").split("/").filter(Boolean));
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4376
|
+
return await this.runSingleWrite(target, (current) => {
|
|
4377
|
+
return {
|
|
4378
|
+
result: {
|
|
4379
|
+
ok: true,
|
|
4380
|
+
message: `Support file "${filePath}" written to "${name}".`,
|
|
4381
|
+
path: target
|
|
4382
|
+
},
|
|
4383
|
+
write: content,
|
|
4384
|
+
audit: {
|
|
4385
|
+
skillName: name,
|
|
4386
|
+
action: "write_file",
|
|
4387
|
+
before: current,
|
|
4388
|
+
after: content,
|
|
4389
|
+
summary: `wrote ${filePath}`
|
|
4390
|
+
},
|
|
4391
|
+
event: {
|
|
4392
|
+
action: "write_file",
|
|
4393
|
+
name,
|
|
4394
|
+
skillDir: dir,
|
|
4395
|
+
file: target
|
|
4396
|
+
}
|
|
4397
|
+
};
|
|
4222
4398
|
});
|
|
4223
|
-
return {
|
|
4224
|
-
ok: true,
|
|
4225
|
-
message: `Support file "${filePath}" written to "${name}".`,
|
|
4226
|
-
path: target
|
|
4227
|
-
};
|
|
4228
4399
|
}
|
|
4229
4400
|
async removeSupportFile(rawName, filePath, origin = "foreground") {
|
|
4230
4401
|
const name = rawName.trim();
|
|
@@ -4457,8 +4628,18 @@ var SkillLibrary = class {
|
|
|
4457
4628
|
* Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
|
|
4458
4629
|
* state (reports, activity store, feedback file, state-domain data).
|
|
4459
4630
|
*/
|
|
4631
|
+
/**
|
|
4632
|
+
* DSH home root: `$DSH_HOME` or `~/.dsh`. Single source of the empty-string
|
|
4633
|
+
* fallback (`||`, not `??`) — an EMPTY DSH_HOME resolves to the default home,
|
|
4634
|
+
* never to a CWD-relative path (0.3.19 W1.3, 0.3.22 F-207).
|
|
4635
|
+
*/
|
|
4636
|
+
function evolutionRoot(env = process.env) {
|
|
4637
|
+
return env.DSH_HOME || join(homedir(), ".dsh");
|
|
4638
|
+
}
|
|
4639
|
+
/** Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
|
|
4640
|
+
* state (reports, activity store, feedback file, state-domain data). */
|
|
4460
4641
|
function evolutionHome(env = process.env) {
|
|
4461
|
-
return join(env
|
|
4642
|
+
return join(evolutionRoot(env), "evolution");
|
|
4462
4643
|
}
|
|
4463
4644
|
//#endregion
|
|
4464
|
-
export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_HEALTH_THRESHOLDS, 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, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, LOW_QUALITY_THRESHOLD, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, advanceReview, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterYamlUnsafeValues, getRecord, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, makeSerialQueue, markAgentCreated, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renameWithRetry, renderCuratorReportMarkdown, resolveOrigins, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, verifyPromptBundle, yamlPlainScalarNeedsQuotes };
|
|
4645
|
+
export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_HEALTH_THRESHOLDS, 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, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, LOW_QUALITY_THRESHOLD, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, advanceReview, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterYamlUnsafeValues, getRecord, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, makeSerialQueue, markAgentCreated, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renameWithRetry, renderCuratorReportMarkdown, resolveOrigins, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, verifyPromptBundle, yamlPlainScalarNeedsQuotes };
|
|
@@ -73,6 +73,13 @@ export declare function eventsFile(home: string): string;
|
|
|
73
73
|
* rebuildable, NOT malformed) or a corrupt one reads as empty; corrupt content
|
|
74
74
|
* is still refused on append, never overwritten.
|
|
75
75
|
*
|
|
76
|
+
* This reader is **v1-only** (F-338): a body carrying a `version` other than
|
|
77
|
+
* `EVENT_LOG_VERSION` is a future-format log this reader cannot interpret, so
|
|
78
|
+
* it reads as an EMPTY timeline rather than being mis-parsed as v1. The read
|
|
79
|
+
* side never overwrites it on its own — `appendEvolutionEvent` rejects a
|
|
80
|
+
* version mismatch up front and preserves the original bytes, so a newer log
|
|
81
|
+
* is never silently downgraded here.
|
|
82
|
+
*
|
|
76
83
|
* Per-entry normalization (rc.70 F-1): entries without a numeric `seq` are
|
|
77
84
|
* skipped here and dropped at the next append — valid entries survive, the
|
|
78
85
|
* damaged record is the only loss (self-heal semantics, matching the usage
|
|
@@ -124,7 +131,10 @@ export interface EventLogRead {
|
|
|
124
131
|
malformed: boolean;
|
|
125
132
|
}
|
|
126
133
|
/** Read the event log; a missing/whitespace-only file reads as empty,
|
|
127
|
-
* corrupt content is flagged (and refused on append).
|
|
134
|
+
* corrupt content is flagged (and refused on append). A well-formed future-
|
|
135
|
+
* version body is v1-incompatible and reads as empty, NOT malformed (F-338:
|
|
136
|
+
* the reader must never mis-shape a newer format; the append path refuses it
|
|
137
|
+
* up front so the original bytes survive). */
|
|
128
138
|
export declare function readEvolutionEvents(io: EvolutionIoLike, path: string): Promise<EventLogRead>;
|
|
129
139
|
/**
|
|
130
140
|
* Read the full timeline (rc.71): active log + all archives, merged by seq
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* it created unless a `.hermes-managed` marker opts a skill in. Archival is a
|
|
7
7
|
* move to `.archive/` — never a hard delete.
|
|
8
8
|
*/
|
|
9
|
-
import { type EvolutionIoLike } from './io.ts';
|
|
9
|
+
import { transactIo, type EvolutionIoLike } from './io.ts';
|
|
10
10
|
import { type MutationRecord } from './mutations.ts';
|
|
11
11
|
import { type SkillHealthAssessment, type SkillHealthThresholds } from './skill-health.ts';
|
|
12
12
|
import type { EvolutionSkillMutatedEvent } from './events.ts';
|
|
@@ -88,9 +88,11 @@ export declare function skillsRoot(env?: NodeJS.ProcessEnv): string;
|
|
|
88
88
|
* the skills tree — tool-skill-manage / evolution-skill-catalog / skill-usage
|
|
89
89
|
* / evolution-learning-graph used to each resolve `config.root || skillsRoot()`
|
|
90
90
|
* (and the graph ignored config entirely). Empty/whitespace config falls
|
|
91
|
-
* through to the default; callers pass their raw Config.
|
|
91
|
+
* through to the default; callers pass their raw Config. The optional field is
|
|
92
|
+
* declared `| undefined` so a config object whose root field is explicitly
|
|
93
|
+
* `string | undefined` still assignable under exactOptionalPropertyTypes. */
|
|
92
94
|
export declare function resolveSkillsRoot(config?: {
|
|
93
|
-
root?: string;
|
|
95
|
+
root?: string | undefined;
|
|
94
96
|
}): string;
|
|
95
97
|
/**
|
|
96
98
|
* Map a requesting session onto the two origin surfaces (rc.44 plan M2-2.3):
|
|
@@ -212,7 +214,24 @@ export declare class SkillLibrary {
|
|
|
212
214
|
readonly limits: SkillLimits;
|
|
213
215
|
private readonly io;
|
|
214
216
|
private readonly onMutation;
|
|
215
|
-
|
|
217
|
+
/** 0.3.21 (F-208): optional cross-process RMW transactor injected by callers.
|
|
218
|
+
* When unset each single-file write falls back to read→task→write. */
|
|
219
|
+
private readonly transact;
|
|
220
|
+
/** 0.3.21 (F-208): in-process serialize queue so two concurrent mutators on
|
|
221
|
+
* one skill never interleave their read-modify-write (the cross-process layer
|
|
222
|
+
* is the IO backend's transact lock; this chain is the second layer). */
|
|
223
|
+
private readonly serial;
|
|
224
|
+
constructor(root?: string, io?: EvolutionIoLike, limits?: SkillLimits, onMutation?: (event: EvolutionSkillMutatedEvent) => void, transact?: typeof transactIo);
|
|
225
|
+
/**
|
|
226
|
+
* Run one single-file read-modify-write for a mutator. When `transact` was
|
|
227
|
+
* injected the read and the write run inside it (cross-process atomicity);
|
|
228
|
+
* otherwise a plain read → task → write sequence runs (the process-level
|
|
229
|
+
* `serial` chain is the second layer). `task` receives the current content
|
|
230
|
+
* (null when missing) and returns a {@link SingleWriteOutcome}. Audit and the
|
|
231
|
+
* mutation event are issued ONLY when a write actually lands, so a no-op
|
|
232
|
+
* never inflates the mutation-maturity counter.
|
|
233
|
+
*/
|
|
234
|
+
private runSingleWrite;
|
|
216
235
|
/** Notify the mutation observer after a successful write; observers must never fail the mutation. */
|
|
217
236
|
private notifyMutation;
|
|
218
237
|
list(): Promise<SkillSummary[]>;
|
|
@@ -271,7 +290,9 @@ export declare class SkillLibrary {
|
|
|
271
290
|
setPinned(name: string, pinned: boolean, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
272
291
|
create(name: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
273
292
|
update(rawName: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
293
|
+
private updateCore;
|
|
274
294
|
patch(rawName: string, oldString: string, newString: string, filePath?: string, replaceAll?: boolean, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
295
|
+
private patchCore;
|
|
275
296
|
archive(rawName: string, options?: ArchiveOptions): Promise<SkillActionResult>;
|
|
276
297
|
/**
|
|
277
298
|
* Merge the bodies of `sources` into `target` and archive the sources with
|
|
@@ -307,6 +328,7 @@ export declare class SkillLibrary {
|
|
|
307
328
|
* same package; the moved text belongs in references/ beside them).
|
|
308
329
|
*/
|
|
309
330
|
restructure(rawName: string, moves: SkillRestructureMove[], origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
331
|
+
private restructureCore;
|
|
310
332
|
/**
|
|
311
333
|
* Unified tree-change commit point (009 kernel): owns validation order,
|
|
312
334
|
* pre-read rollback bytes, two-phase write with byte-level rollback, audit
|
|
@@ -322,6 +344,7 @@ export declare class SkillLibrary {
|
|
|
322
344
|
*/
|
|
323
345
|
restoreFromArchive(rawName: string): Promise<SkillActionResult>;
|
|
324
346
|
writeSupportFile(rawName: string, filePath: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
347
|
+
private writeSupportFileCore;
|
|
325
348
|
removeSupportFile(rawName: string, filePath: string, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
326
349
|
/**
|
|
327
350
|
* Snapshot the recoverable skills state: active tree, usage/suppression
|
|
@@ -2,5 +2,13 @@
|
|
|
2
2
|
* Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
|
|
3
3
|
* state (reports, activity store, feedback file, state-domain data).
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* DSH home root: `$DSH_HOME` or `~/.dsh`. Single source of the empty-string
|
|
7
|
+
* fallback (`||`, not `??`) — an EMPTY DSH_HOME resolves to the default home,
|
|
8
|
+
* never to a CWD-relative path (0.3.19 W1.3, 0.3.22 F-207).
|
|
9
|
+
*/
|
|
10
|
+
export declare function evolutionRoot(env?: NodeJS.ProcessEnv): string;
|
|
11
|
+
/** Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
|
|
12
|
+
* state (reports, activity store, feedback file, state-domain data). */
|
|
5
13
|
export declare function evolutionHome(env?: NodeJS.ProcessEnv): string;
|
|
6
14
|
//# sourceMappingURL=state-store.d.ts.map
|
package/package.json
CHANGED