@lmzhen/dsh-evolution-core 0.3.20 → 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 +367 -125
- package/lib/types/constants.d.ts +1 -1
- package/lib/types/evolution-events.d.ts +11 -1
- package/lib/types/gates.d.ts +1 -1
- package/lib/types/index.d.ts +1 -1
- package/lib/types/io.d.ts +12 -0
- package/lib/types/mutations.d.ts +1 -1
- package/lib/types/quality.d.ts +1 -1
- package/lib/types/skill-store.d.ts +27 -4
- package/lib/types/state-store.d.ts +8 -0
- package/package.json +3 -5
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
|
@@ -52,6 +52,49 @@ function evolutionIoAdapter(provider) {
|
|
|
52
52
|
}
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* F-367 (②): lock paths whose release (the finally `rm`) failed. The next write
|
|
57
|
+
* to the same file proactively recycles our own leftover lock — the holder is
|
|
58
|
+
* us, so a leftover is stale by definition. Module-level by design: it must
|
|
59
|
+
* survive across `nodeEvolutionIo()` instances for the self-heal to be
|
|
60
|
+
* effective. (Not a pure function — the cross-call state is the intent.)
|
|
61
|
+
*/
|
|
62
|
+
const pendingSelfCleanup = /* @__PURE__ */ new Set();
|
|
63
|
+
/**
|
|
64
|
+
* Retry a rename that a peer is temporarily holding on Windows (EPERM/EBUSY):
|
|
65
|
+
* a short 50ms backoff, at most 3 retries (~150ms budget), matching the
|
|
66
|
+
* write-lock cadence. A non-transient code surfaces immediately. `fn` is the
|
|
67
|
+
* rename primitive, injectable for deterministic tests.
|
|
68
|
+
*
|
|
69
|
+
* @param tmp - the source path to rename.
|
|
70
|
+
* @param target - the destination path.
|
|
71
|
+
* @param fn - the rename primitive (defaults to `node:fs/promises.rename`).
|
|
72
|
+
* @returns a promise that resolves once the rename succeeds.
|
|
73
|
+
*/
|
|
74
|
+
async function renameWithRetry(tmp, target, fn = rename) {
|
|
75
|
+
for (let retry = 0;; retry += 1) try {
|
|
76
|
+
await fn(tmp, target);
|
|
77
|
+
return;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
const code = error?.code;
|
|
80
|
+
if (code !== "EPERM" && code !== "EBUSY") throw error;
|
|
81
|
+
if (retry >= 3) throw error;
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* F-366: commit a freshly-written tmp to its target inside the write lock. On a
|
|
87
|
+
* still-failing rename the tmp is deleted immediately rather than left for the
|
|
88
|
+
* (1h + dead-pid) sweep, so a live writer never leaks a tmp it abandoned.
|
|
89
|
+
*/
|
|
90
|
+
async function commitTmp(tmp, target) {
|
|
91
|
+
try {
|
|
92
|
+
await renameWithRetry(tmp, target);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
await rm(tmp, { force: true }).catch(() => {});
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
55
98
|
function nodeEvolutionIo() {
|
|
56
99
|
const isMissing = (error) => {
|
|
57
100
|
const code = error?.code;
|
|
@@ -84,6 +127,13 @@ function nodeEvolutionIo() {
|
|
|
84
127
|
* retry budget (budget >= 2 x threshold), so a dead holder's lock is
|
|
85
128
|
* actually recoverable within one budget instead of being arithmetically
|
|
86
129
|
* unreachable.
|
|
130
|
+
* 0.3.21 (F-101): takeover re-reads the lock right before removing it and
|
|
131
|
+
* only removes it when the content still names the dead pid — a peer that
|
|
132
|
+
* acquired the lock after our stale probe wrote its own pid, and deleting a
|
|
133
|
+
* LIVE lock is the double-hold (concurrent task) the probe must prevent.
|
|
134
|
+
* 0.3.21 (F-367): a self-pid lock is this process's own leftover (a failed
|
|
135
|
+
* release or a crash) and is recycled immediately regardless of age; a
|
|
136
|
+
* failure to release in finally is recorded so the next write self-heals.
|
|
87
137
|
*/
|
|
88
138
|
const withWriteLock = async (path, task) => {
|
|
89
139
|
const lock = `${path}.lock`;
|
|
@@ -95,14 +145,23 @@ function nodeEvolutionIo() {
|
|
|
95
145
|
if (code !== "EEXIST" && code !== "EPERM") throw error;
|
|
96
146
|
try {
|
|
97
147
|
const st = await stat(lock);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
148
|
+
const holderContent = await readFile(lock, "utf8").catch(() => "");
|
|
149
|
+
const holder = Number(holderContent);
|
|
150
|
+
const holderAlive = Number.isInteger(holder) && holder > 0 && isAlive(holder);
|
|
151
|
+
if (holder === process.pid && (pendingSelfCleanup.has(lock) || Date.now() - st.mtimeMs > 1e3)) {
|
|
152
|
+
if (await readFile(lock, "utf8").catch(() => "") === holderContent) {
|
|
101
153
|
try {
|
|
102
154
|
await rm(lock, { force: true });
|
|
103
155
|
} catch {}
|
|
104
|
-
|
|
156
|
+
pendingSelfCleanup.delete(lock);
|
|
105
157
|
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (Date.now() - st.mtimeMs > 1e3 && !holderAlive) {
|
|
161
|
+
if (await readFile(lock, "utf8").catch(() => "") === holderContent) try {
|
|
162
|
+
await rm(lock, { force: true });
|
|
163
|
+
} catch {}
|
|
164
|
+
continue;
|
|
106
165
|
}
|
|
107
166
|
} catch {
|
|
108
167
|
continue;
|
|
@@ -113,7 +172,9 @@ function nodeEvolutionIo() {
|
|
|
113
172
|
try {
|
|
114
173
|
return await task();
|
|
115
174
|
} finally {
|
|
116
|
-
await rm(lock, { force: true }).catch(() => {
|
|
175
|
+
await rm(lock, { force: true }).catch(() => {
|
|
176
|
+
pendingSelfCleanup.add(lock);
|
|
177
|
+
});
|
|
117
178
|
}
|
|
118
179
|
}
|
|
119
180
|
throw new Error(`could not acquire write lock for ${path} after 40 attempts`);
|
|
@@ -139,7 +200,7 @@ function nodeEvolutionIo() {
|
|
|
139
200
|
try {
|
|
140
201
|
const st = await stat(tmpPath);
|
|
141
202
|
const deadHolder = !Number.isInteger(holder) || holder <= 0 || !isAlive(holder);
|
|
142
|
-
if (Date.now() - st.mtimeMs > 36e5 && deadHolder) await rm(tmpPath, { force: true });
|
|
203
|
+
if (holder === process.pid || Date.now() - st.mtimeMs > 36e5 && deadHolder) await rm(tmpPath, { force: true });
|
|
143
204
|
} catch {}
|
|
144
205
|
}
|
|
145
206
|
};
|
|
@@ -158,7 +219,7 @@ function nodeEvolutionIo() {
|
|
|
158
219
|
await sweepStaleTmps(path);
|
|
159
220
|
const tmp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
160
221
|
await writeFile(tmp, content, "utf8");
|
|
161
|
-
await
|
|
222
|
+
await commitTmp(tmp, path);
|
|
162
223
|
});
|
|
163
224
|
},
|
|
164
225
|
async transact(path, task) {
|
|
@@ -179,7 +240,7 @@ function nodeEvolutionIo() {
|
|
|
179
240
|
}
|
|
180
241
|
const tmp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
181
242
|
await writeFile(tmp, next, "utf8");
|
|
182
|
-
await
|
|
243
|
+
await commitTmp(tmp, path);
|
|
183
244
|
});
|
|
184
245
|
},
|
|
185
246
|
async remove(path) {
|
|
@@ -881,6 +942,13 @@ function isEventRecord(event) {
|
|
|
881
942
|
* rebuildable, NOT malformed) or a corrupt one reads as empty; corrupt content
|
|
882
943
|
* is still refused on append, never overwritten.
|
|
883
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
|
+
*
|
|
884
952
|
* Per-entry normalization (rc.70 F-1): entries without a numeric `seq` are
|
|
885
953
|
* skipped here and dropped at the next append — valid entries survive, the
|
|
886
954
|
* damaged record is the only loss (self-heal semantics, matching the usage
|
|
@@ -890,6 +958,7 @@ function parseEvolutionEvents(raw) {
|
|
|
890
958
|
if (raw === null || raw.trim() === "") return [];
|
|
891
959
|
try {
|
|
892
960
|
const parsed = JSON.parse(raw);
|
|
961
|
+
if (parsed.version !== void 0 && parsed.version !== 1) return [];
|
|
893
962
|
if (!Array.isArray(parsed.events)) return [];
|
|
894
963
|
return parsed.events.filter(isEventRecord);
|
|
895
964
|
} catch {
|
|
@@ -928,11 +997,19 @@ async function listEventArchives(io, path) {
|
|
|
928
997
|
*/
|
|
929
998
|
async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE_AT) {
|
|
930
999
|
let assigned = 0;
|
|
1000
|
+
let refuseMessage = "";
|
|
931
1001
|
await transactIo(io, path, async (current) => {
|
|
932
|
-
if (current !== null && current.trim() !== "")
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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
|
+
}
|
|
936
1013
|
}
|
|
937
1014
|
const nextEvents = await rotateIfDue(io, path, parseEvolutionEvents(current), rotateAt);
|
|
938
1015
|
let maxSeq = nextEvents.reduce((max, entry) => Math.max(max, entry.seq), 0);
|
|
@@ -948,7 +1025,7 @@ async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE
|
|
|
948
1025
|
events: [...nextEvents, record]
|
|
949
1026
|
}, null, 2);
|
|
950
1027
|
});
|
|
951
|
-
if (assigned === 0) throw new Error(
|
|
1028
|
+
if (assigned === 0) throw new Error(`${refuseMessage || "evolution event log is malformed and was not touched"}: ${path}`);
|
|
952
1029
|
return assigned;
|
|
953
1030
|
}
|
|
954
1031
|
/**
|
|
@@ -988,7 +1065,10 @@ async function retainEventArchives(io, path) {
|
|
|
988
1065
|
for (const name of excess) await io.remove(join(dir, name)).catch(() => {});
|
|
989
1066
|
}
|
|
990
1067
|
/** Read the event log; a missing/whitespace-only file reads as empty,
|
|
991
|
-
* 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). */
|
|
992
1072
|
async function readEvolutionEvents(io, path) {
|
|
993
1073
|
let raw;
|
|
994
1074
|
try {
|
|
@@ -1005,6 +1085,10 @@ async function readEvolutionEvents(io, path) {
|
|
|
1005
1085
|
};
|
|
1006
1086
|
try {
|
|
1007
1087
|
const parsed = JSON.parse(raw);
|
|
1088
|
+
if (parsed.version !== void 0 && parsed.version !== 1) return {
|
|
1089
|
+
events: [],
|
|
1090
|
+
malformed: false
|
|
1091
|
+
};
|
|
1008
1092
|
if (!Array.isArray(parsed.events)) return {
|
|
1009
1093
|
events: [],
|
|
1010
1094
|
malformed: false
|
|
@@ -1749,6 +1833,14 @@ function render(entries) {
|
|
|
1749
1833
|
function stripDatePrefix(entry) {
|
|
1750
1834
|
return entry.replace(/^## \d{4}-\d{2}-\d{2}\n/, "");
|
|
1751
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
|
+
}
|
|
1752
1844
|
var MemoryStore = class {
|
|
1753
1845
|
memoryLimit;
|
|
1754
1846
|
userLimit;
|
|
@@ -1915,6 +2007,16 @@ var MemoryStore = class {
|
|
|
1915
2007
|
},
|
|
1916
2008
|
write: null
|
|
1917
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
|
+
};
|
|
1918
2020
|
const entries = [...new Set(normalizeEntries(raw))];
|
|
1919
2021
|
if (entries.some((entry) => stripDatePrefix(entry) === content)) {
|
|
1920
2022
|
this.resetFailures();
|
|
@@ -2017,6 +2119,16 @@ var MemoryStore = class {
|
|
|
2017
2119
|
},
|
|
2018
2120
|
write: null
|
|
2019
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
|
+
};
|
|
2020
2132
|
if (!working.some((entry) => stripDatePrefix(entry) === body)) working.push(this.addDatePrefix ? `## ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}\n${body}` : body);
|
|
2021
2133
|
continue;
|
|
2022
2134
|
}
|
|
@@ -2074,6 +2186,16 @@ var MemoryStore = class {
|
|
|
2074
2186
|
},
|
|
2075
2187
|
write: null
|
|
2076
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
|
+
};
|
|
2077
2199
|
working[matchIndex] = body;
|
|
2078
2200
|
}
|
|
2079
2201
|
}
|
|
@@ -2768,7 +2890,9 @@ function skillsRoot(env = process.env) {
|
|
|
2768
2890
|
* the skills tree — tool-skill-manage / evolution-skill-catalog / skill-usage
|
|
2769
2891
|
* / evolution-learning-graph used to each resolve `config.root || skillsRoot()`
|
|
2770
2892
|
* (and the graph ignored config entirely). Empty/whitespace config falls
|
|
2771
|
-
* 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. */
|
|
2772
2896
|
function resolveSkillsRoot(config = {}) {
|
|
2773
2897
|
return (config.root ?? "").trim() || skillsRoot();
|
|
2774
2898
|
}
|
|
@@ -3115,15 +3239,9 @@ function fuzzyPatch(content, oldString, newString, replaceAll = false) {
|
|
|
3115
3239
|
const boundary = trimPatternBoundaries(oldString);
|
|
3116
3240
|
if (boundary === "") return null;
|
|
3117
3241
|
if (boundary !== oldString) {
|
|
3118
|
-
if (fuzzyIndexOf(content, boundary) !== null)
|
|
3119
|
-
const patched = fuzzyReplace(content, boundary, newString, replaceAll);
|
|
3120
|
-
return patched === content ? null : patched;
|
|
3121
|
-
}
|
|
3122
|
-
}
|
|
3123
|
-
if (fuzzyIndexOf(content, oldString) !== null) {
|
|
3124
|
-
const patched = fuzzyReplace(content, oldString, newString, replaceAll);
|
|
3125
|
-
return patched === content ? null : patched;
|
|
3242
|
+
if (fuzzyIndexOf(content, boundary) !== null) return fuzzyReplace(content, boundary, newString, replaceAll);
|
|
3126
3243
|
}
|
|
3244
|
+
if (fuzzyIndexOf(content, oldString) !== null) return fuzzyReplace(content, oldString, newString, replaceAll);
|
|
3127
3245
|
return null;
|
|
3128
3246
|
}
|
|
3129
3247
|
/**
|
|
@@ -3194,11 +3312,47 @@ var SkillLibrary = class {
|
|
|
3194
3312
|
limits;
|
|
3195
3313
|
io;
|
|
3196
3314
|
onMutation;
|
|
3197
|
-
|
|
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) {
|
|
3198
3323
|
this.root = root;
|
|
3199
3324
|
this.io = io;
|
|
3200
3325
|
this.limits = limits;
|
|
3201
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;
|
|
3202
3356
|
}
|
|
3203
3357
|
/** Notify the mutation observer after a successful write; observers must never fail the mutation. */
|
|
3204
3358
|
notifyMutation(event) {
|
|
@@ -3455,9 +3609,10 @@ var SkillLibrary = class {
|
|
|
3455
3609
|
ok: false,
|
|
3456
3610
|
message: `Skill "${normalized}" already exists.`
|
|
3457
3611
|
};
|
|
3458
|
-
|
|
3612
|
+
const onDisk = finalContent.trimEnd() + "\n";
|
|
3613
|
+
await this.io.writeText(join(dir, "SKILL.md"), onDisk);
|
|
3459
3614
|
if (origin !== "foreground") await this.io.writeText(markerPath(dir, "hermes-managed"), "");
|
|
3460
|
-
await this.audit(normalized, "create", null,
|
|
3615
|
+
await this.audit(normalized, "create", null, onDisk, "created");
|
|
3461
3616
|
this.notifyMutation({
|
|
3462
3617
|
action: "create",
|
|
3463
3618
|
name: normalized,
|
|
@@ -3472,17 +3627,16 @@ var SkillLibrary = class {
|
|
|
3472
3627
|
}
|
|
3473
3628
|
async update(rawName, content, origin = "foreground") {
|
|
3474
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");
|
|
3475
3635
|
const badName = this.badName(name);
|
|
3476
3636
|
if (badName) return {
|
|
3477
3637
|
ok: false,
|
|
3478
3638
|
message: badName
|
|
3479
3639
|
};
|
|
3480
|
-
const dir = this.dirOf(name);
|
|
3481
|
-
const md = await this.io.readText(join(dir, "SKILL.md"));
|
|
3482
|
-
if (!md) return {
|
|
3483
|
-
ok: false,
|
|
3484
|
-
message: `Skill "${name}" not found.`
|
|
3485
|
-
};
|
|
3486
3640
|
const protection = await this.writeProtection(name, origin);
|
|
3487
3641
|
if (protection) return {
|
|
3488
3642
|
ok: false,
|
|
@@ -3511,27 +3665,52 @@ var SkillLibrary = class {
|
|
|
3511
3665
|
ok: false,
|
|
3512
3666
|
message: threat
|
|
3513
3667
|
};
|
|
3514
|
-
await this.
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
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
|
+
};
|
|
3520
3707
|
});
|
|
3521
|
-
return {
|
|
3522
|
-
ok: true,
|
|
3523
|
-
message: `Skill "${name}" updated.`,
|
|
3524
|
-
path: dir,
|
|
3525
|
-
...norm.changed ? { normalizedFrontmatterFields: norm.fields } : {}
|
|
3526
|
-
};
|
|
3527
3708
|
}
|
|
3528
3709
|
async patch(rawName, oldString, newString, filePath = "", replaceAll = false, origin = "foreground") {
|
|
3529
3710
|
const name = rawName.trim();
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
message: badName
|
|
3534
|
-
};
|
|
3711
|
+
return await this.serial(() => this.patchCore(name, oldString, newString, filePath, replaceAll, origin));
|
|
3712
|
+
}
|
|
3713
|
+
async patchCore(name, oldString, newString, filePath, replaceAll, origin) {
|
|
3535
3714
|
const dir = this.dirOf(name);
|
|
3536
3715
|
const skillMd = join(dir, "SKILL.md");
|
|
3537
3716
|
if (!await this.io.exists(skillMd)) return {
|
|
@@ -3554,71 +3733,109 @@ var SkillLibrary = class {
|
|
|
3554
3733
|
target = join(dir, ...filePath.replace(/\\/g, "/").split("/").filter(Boolean));
|
|
3555
3734
|
patchLabel = filePath;
|
|
3556
3735
|
}
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
message: `Could not find old_string in "${name}/${patchLabel}". Use update for a full rewrite.`
|
|
3566
|
-
};
|
|
3567
|
-
let writeContent = patched;
|
|
3568
|
-
let normalizedFields;
|
|
3569
|
-
if (target === skillMd) {
|
|
3570
|
-
const validation = validateFrontmatter(patched, name, this.limits);
|
|
3571
|
-
if (validation) return {
|
|
3572
|
-
ok: false,
|
|
3573
|
-
message: `Patch rejected: ${validation}`
|
|
3574
|
-
};
|
|
3575
|
-
const norm = normalizeFrontmatter(patched);
|
|
3576
|
-
if (norm.issues.length > 0) return {
|
|
3577
|
-
ok: false,
|
|
3578
|
-
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
|
|
3579
3744
|
};
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
const revalidated = validateFrontmatter(writeContent, name, this.limits);
|
|
3584
|
-
if (revalidated) return {
|
|
3745
|
+
const patched = fuzzyPatch(md, oldString, newString, replaceAll);
|
|
3746
|
+
if (patched === null) return {
|
|
3747
|
+
result: {
|
|
3585
3748
|
ok: false,
|
|
3586
|
-
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
|
|
3587
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
|
+
}
|
|
3588
3784
|
}
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
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
|
+
};
|
|
3615
3838
|
});
|
|
3616
|
-
return {
|
|
3617
|
-
ok: true,
|
|
3618
|
-
message: `Skill "${name}" patched (${patchLabel}).`,
|
|
3619
|
-
path: dir,
|
|
3620
|
-
...normalizedFields ? { normalizedFrontmatterFields: normalizedFields } : {}
|
|
3621
|
-
};
|
|
3622
3839
|
}
|
|
3623
3840
|
async archive(rawName, options = {}) {
|
|
3624
3841
|
const name = rawName.trim();
|
|
@@ -3872,6 +4089,9 @@ var SkillLibrary = class {
|
|
|
3872
4089
|
*/
|
|
3873
4090
|
async restructure(rawName, moves, origin = "foreground") {
|
|
3874
4091
|
const name = rawName.trim();
|
|
4092
|
+
return await this.serial(() => this.restructureCore(name, moves, origin));
|
|
4093
|
+
}
|
|
4094
|
+
async restructureCore(name, moves, origin) {
|
|
3875
4095
|
const badName = this.badName(name);
|
|
3876
4096
|
if (badName) return {
|
|
3877
4097
|
ok: false,
|
|
@@ -4120,12 +4340,15 @@ var SkillLibrary = class {
|
|
|
4120
4340
|
}
|
|
4121
4341
|
async writeSupportFile(rawName, filePath, content, origin = "foreground") {
|
|
4122
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);
|
|
4123
4347
|
const badName = this.badName(name);
|
|
4124
4348
|
if (badName) return {
|
|
4125
4349
|
ok: false,
|
|
4126
4350
|
message: badName
|
|
4127
4351
|
};
|
|
4128
|
-
const dir = this.dirOf(name);
|
|
4129
4352
|
if (!await this.io.exists(join(dir, "SKILL.md"))) return {
|
|
4130
4353
|
ok: false,
|
|
4131
4354
|
message: `Skill "${name}" not found.`
|
|
@@ -4150,20 +4373,29 @@ var SkillLibrary = class {
|
|
|
4150
4373
|
message: threat
|
|
4151
4374
|
};
|
|
4152
4375
|
const target = join(dir, ...filePath.replace(/\\/g, "/").split("/").filter(Boolean));
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
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
|
+
};
|
|
4161
4398
|
});
|
|
4162
|
-
return {
|
|
4163
|
-
ok: true,
|
|
4164
|
-
message: `Support file "${filePath}" written to "${name}".`,
|
|
4165
|
-
path: target
|
|
4166
|
-
};
|
|
4167
4399
|
}
|
|
4168
4400
|
async removeSupportFile(rawName, filePath, origin = "foreground") {
|
|
4169
4401
|
const name = rawName.trim();
|
|
@@ -4396,8 +4628,18 @@ var SkillLibrary = class {
|
|
|
4396
4628
|
* Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
|
|
4397
4629
|
* state (reports, activity store, feedback file, state-domain data).
|
|
4398
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). */
|
|
4399
4641
|
function evolutionHome(env = process.env) {
|
|
4400
|
-
return join(env
|
|
4642
|
+
return join(evolutionRoot(env), "evolution");
|
|
4401
4643
|
}
|
|
4402
4644
|
//#endregion
|
|
4403
|
-
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, 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 };
|
package/lib/types/constants.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* Package-private tunables (used by exactly one package) stay in that package,
|
|
18
18
|
* not here — see evolution-replay's `DEFAULT_WEIGHTS` and evolution-feedback's
|
|
19
19
|
* threshold, which are intentionally left where they are used.
|
|
20
|
-
* @module @
|
|
20
|
+
* @module @lmzhen/dsh-evolution-core
|
|
21
21
|
*/
|
|
22
22
|
/** Skill frontmatter `name` validated for the file name (lowercase + hyphen). */
|
|
23
23
|
export declare const SKILL_NAME_RE: RegExp;
|
|
@@ -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
|
package/lib/types/gates.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* protections (pinned / bundled / hub-installed) are file markers resolved by
|
|
10
10
|
* `SkillLibrary.writeProtection` / `deleteProtection` — they depend on the
|
|
11
11
|
* filesystem and the write origin, not on a name list.
|
|
12
|
-
* @module @
|
|
12
|
+
* @module @lmzhen/dsh-evolution-core
|
|
13
13
|
*/
|
|
14
14
|
export type GateReason = 'excluded' | 'referenced' | 'suppressed' | 'protected-builtin';
|
|
15
15
|
export interface GateSetInputs {
|
package/lib/types/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* types, and session-event augmentations. This package owns no Cordis plugin
|
|
6
6
|
* entry of its own; consumers import named exports from the package root so
|
|
7
7
|
* published npm bundles never depend on source subpaths.
|
|
8
|
-
* @module @
|
|
8
|
+
* @module @lmzhen/dsh-evolution-core
|
|
9
9
|
*/
|
|
10
10
|
export * from './curator.ts';
|
|
11
11
|
export * from './evolution-events.ts';
|
package/lib/types/io.d.ts
CHANGED
|
@@ -53,5 +53,17 @@ export interface EvolutionIoLike {
|
|
|
53
53
|
export declare function transactIo(io: EvolutionIoLike, path: string, task: (current: string | null) => string | null | Promise<string | null>): Promise<void>;
|
|
54
54
|
/** Lazy adapter over an IO provider registry, shared by every evolution consumer. */
|
|
55
55
|
export declare function evolutionIoAdapter(provider: () => EvolutionIoLike): EvolutionIoLike;
|
|
56
|
+
/**
|
|
57
|
+
* Retry a rename that a peer is temporarily holding on Windows (EPERM/EBUSY):
|
|
58
|
+
* a short 50ms backoff, at most 3 retries (~150ms budget), matching the
|
|
59
|
+
* write-lock cadence. A non-transient code surfaces immediately. `fn` is the
|
|
60
|
+
* rename primitive, injectable for deterministic tests.
|
|
61
|
+
*
|
|
62
|
+
* @param tmp - the source path to rename.
|
|
63
|
+
* @param target - the destination path.
|
|
64
|
+
* @param fn - the rename primitive (defaults to `node:fs/promises.rename`).
|
|
65
|
+
* @returns a promise that resolves once the rename succeeds.
|
|
66
|
+
*/
|
|
67
|
+
export declare function renameWithRetry(tmp: string, target: string, fn?: (from: string, to: string) => Promise<void>): Promise<void>;
|
|
56
68
|
export declare function nodeEvolutionIo(): EvolutionIoLike;
|
|
57
69
|
//# sourceMappingURL=io.d.ts.map
|
package/lib/types/mutations.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Curator/author audit trail: `.mutations.json` records every skill mutation
|
|
3
3
|
* with before/after content hashes so any automated edit is reviewable and
|
|
4
4
|
* replayable. Best-effort persistence, mirroring the usage sidecar posture.
|
|
5
|
-
* @module @
|
|
5
|
+
* @module @lmzhen/dsh-evolution-core
|
|
6
6
|
*/
|
|
7
7
|
import { type EvolutionIoLike } from './io.ts';
|
|
8
8
|
export interface MutationRecord {
|
package/lib/types/quality.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* mutation maturity is a documented DSH approximation (single per-month patch
|
|
8
8
|
* trend ratio replaces the claw timestamp-trend formula, since DSH usage
|
|
9
9
|
* records only carry the last patched timestamp).
|
|
10
|
-
* @module @
|
|
10
|
+
* @module @lmzhen/dsh-evolution-core
|
|
11
11
|
*/
|
|
12
12
|
import type { UsageMap } from './usage.ts';
|
|
13
13
|
export interface QualityFactors {
|
|
@@ -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
|
@@ -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.
|
|
4
|
+
"version": "0.3.22",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -25,10 +25,8 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
|
-
"lib
|
|
29
|
-
"lib/
|
|
30
|
-
"lib/types/**/*.d.ts",
|
|
31
|
-
"lib/types/invariant.d.ts"
|
|
28
|
+
"lib/*.js",
|
|
29
|
+
"lib/types/**/*.d.ts"
|
|
32
30
|
],
|
|
33
31
|
"license": "MIT",
|
|
34
32
|
"dependencies": {
|