@lmzhen/dsh-evolution-core 0.3.50 → 0.3.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +398 -105
- package/lib/types/evolution-events.d.ts +0 -17
- package/lib/types/index.d.ts +5 -3
- package/lib/types/io.d.ts +41 -8
- package/lib/types/memory-store.d.ts +19 -1
- package/lib/types/prompts.d.ts +14 -2
- package/lib/types/signals.d.ts +1 -1
- package/lib/types/skill-health.d.ts +6 -0
- package/lib/types/skill-store.d.ts +19 -2
- package/lib/types/state-store.d.ts +16 -2
- package/lib/types/threats.d.ts +9 -0
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -13,16 +13,19 @@ import { load } from "js-yaml";
|
|
|
13
13
|
/**
|
|
14
14
|
* Run `task` inside `io.transact` when the backend provides it; otherwise fall
|
|
15
15
|
* back to a plain read → task → write/remove sequence (no cross-process lock —
|
|
16
|
-
* callers keep their single-process serialize chain as the second layer).
|
|
16
|
+
* callers keep their single-process serialize chain as the second layer). A
|
|
17
|
+
* byte-identical task result skips the write (C-07 — parity with the
|
|
18
|
+
* node backend's V5-03 short-circuit).
|
|
17
19
|
*/
|
|
18
20
|
async function transactIo(io, path, task) {
|
|
19
21
|
if (io.transact) {
|
|
20
22
|
await io.transact(path, task);
|
|
21
23
|
return;
|
|
22
24
|
}
|
|
23
|
-
const
|
|
25
|
+
const current = await io.readText(path);
|
|
26
|
+
const next = await task(current);
|
|
24
27
|
if (next === null) await io.remove(path);
|
|
25
|
-
else await io.writeText(path, next);
|
|
28
|
+
else if (next !== current) await io.writeText(path, next);
|
|
26
29
|
}
|
|
27
30
|
/** Lazy adapter over an IO provider registry, shared by every evolution consumer. */
|
|
28
31
|
function evolutionIoAdapter(provider) {
|
|
@@ -64,11 +67,17 @@ function evolutionIoAdapter(provider) {
|
|
|
64
67
|
* so `io.spec.ts` can drive the self-heal path deterministically.
|
|
65
68
|
*/
|
|
66
69
|
const pendingSelfCleanup = /* @__PURE__ */ new Map();
|
|
70
|
+
const RENAME_RETRY_BASE_MS = 50;
|
|
71
|
+
const RENAME_RETRY_MAX_DELAY_MS = 800;
|
|
72
|
+
const RENAME_RETRY_MAX_ATTEMPTS = 6;
|
|
67
73
|
/**
|
|
68
74
|
* Retry a rename that a peer is temporarily holding on Windows (EPERM/EBUSY):
|
|
69
|
-
*
|
|
70
|
-
* write-lock cadence
|
|
71
|
-
*
|
|
75
|
+
* exponential backoff (50ms doubling, capped at 800ms) with a ~2s total
|
|
76
|
+
* budget, matching the write-lock cadence (C-28 — the old 3x50ms
|
|
77
|
+
* budget turned a transient antivirus hold into a permanent write failure).
|
|
78
|
+
* A non-transient code surfaces immediately; a persistent EPERM/EBUSY
|
|
79
|
+
* rethrows with a pointer at the usual causes instead of a bare errno.
|
|
80
|
+
* `fn` is the rename primitive, injectable for deterministic tests.
|
|
72
81
|
*
|
|
73
82
|
* @param tmp - the source path to rename.
|
|
74
83
|
* @param target - the destination path.
|
|
@@ -82,24 +91,85 @@ async function renameWithRetry(tmp, target, fn = rename) {
|
|
|
82
91
|
} catch (error) {
|
|
83
92
|
const code = error?.code;
|
|
84
93
|
if (code !== "EPERM" && code !== "EBUSY") throw error;
|
|
85
|
-
if (retry >=
|
|
86
|
-
|
|
94
|
+
if (retry >= RENAME_RETRY_MAX_ATTEMPTS) {
|
|
95
|
+
const final = error;
|
|
96
|
+
final.message = `${final.message} (persisted after ${retry + 1} rename attempts over ~2s — the target may be held by another process or marked read-only; check antivirus, search indexers and file attributes)`;
|
|
97
|
+
throw final;
|
|
98
|
+
}
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(RENAME_RETRY_BASE_MS * 2 ** retry, RENAME_RETRY_MAX_DELAY_MS)));
|
|
87
100
|
}
|
|
88
101
|
}
|
|
89
102
|
/**
|
|
103
|
+
* V10-06 (P1-1): the crash-durable tmp half of the upstream storage-json
|
|
104
|
+
* `writeAtomic` protocol (storage-json/src/atomic.ts:24-40): exclusive-create
|
|
105
|
+
* a same-directory tmp (`wx` — never clobbers), write, `handle.sync()`,
|
|
106
|
+
* close, then hand the tmp to `commitTmp` for the rename. Without the fsync a
|
|
107
|
+
* power loss could land the rename (metadata) before the data blocks and
|
|
108
|
+
* leave an empty/truncated target — which the state-json E-9 quarantine then
|
|
109
|
+
* amplifies into a permanent fail-loud. The tmp name keeps the
|
|
110
|
+
* `<target>.<pid>.<rand>.tmp` shape so sweepStaleTmps keeps matching.
|
|
111
|
+
* Mode parity note: no explicit mode is passed (upstream uses 0o600) — this
|
|
112
|
+
* seam also writes operator-editable skill/memory files, so the previous
|
|
113
|
+
* `writeFile` default (0o666 & ~umask) is deliberately preserved.
|
|
114
|
+
* `openImpl` is injectable so the sync-failure regression test can drive a
|
|
115
|
+
* failing `handle.sync()` deterministically.
|
|
116
|
+
*/
|
|
117
|
+
async function writeDurableTmp(target, content, openImpl = open) {
|
|
118
|
+
const tmp = `${target}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
119
|
+
try {
|
|
120
|
+
const handle = await openImpl(tmp, "wx");
|
|
121
|
+
try {
|
|
122
|
+
await handle.writeFile(content, "utf8");
|
|
123
|
+
await handle.sync();
|
|
124
|
+
} finally {
|
|
125
|
+
await handle.close();
|
|
126
|
+
}
|
|
127
|
+
return tmp;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
await rm(tmp, { force: true }).catch(() => {});
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** V10-06 (P1-1): fsync a POSIX directory so a just-renamed entry is
|
|
134
|
+
* crash-durable (upstream atomic.ts `fsyncDirectory`). */
|
|
135
|
+
/* v8 ignore start -- Windows rejects O_RDONLY directory opens; POSIX coverage exercises this. */
|
|
136
|
+
async function fsyncDirectory(path) {
|
|
137
|
+
if (process.platform === "win32") return;
|
|
138
|
+
const handle = await open(path, "r");
|
|
139
|
+
try {
|
|
140
|
+
await handle.sync();
|
|
141
|
+
} finally {
|
|
142
|
+
await handle.close();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/* v8 ignore stop */
|
|
146
|
+
/**
|
|
90
147
|
* F-366: commit a freshly-written tmp to its target inside the write lock. On a
|
|
91
148
|
* still-failing rename the tmp is deleted immediately rather than left for the
|
|
92
149
|
* (1h + dead-pid) sweep, so a live writer never leaks a tmp it abandoned.
|
|
150
|
+
* V10-06 (P1-1): after a successful rename the parent directory is fsynced on
|
|
151
|
+
* POSIX so the new directory entry itself is crash-durable (upstream atomic.ts
|
|
152
|
+
* tail; Windows skips — it rejects O_RDONLY directory opens). A dir-fsync
|
|
153
|
+
* failure propagates like upstream: the data is on disk, but the durability
|
|
154
|
+
* contract failed loud.
|
|
93
155
|
*/
|
|
94
156
|
async function commitTmp(tmp, target) {
|
|
95
157
|
try {
|
|
96
158
|
await renameWithRetry(tmp, target);
|
|
159
|
+
await fsyncDirectory(dirname(target));
|
|
97
160
|
} catch (error) {
|
|
98
161
|
await rm(tmp, { force: true }).catch(() => {});
|
|
99
162
|
throw error;
|
|
100
163
|
}
|
|
101
164
|
}
|
|
102
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Build the Node IO backend. `lockAttempts` scales the write-lock retry budget
|
|
167
|
+
* (attempts × 50ms); the default 40 (~2s, rc.69) covers production contention,
|
|
168
|
+
* while contention TESTS on a loaded runner may raise it (e.g. 240 ≈ 12s) —
|
|
169
|
+
* V10-06 integration: full-suite parallel load made 32-way takeover bursts
|
|
170
|
+
* exceed the default budget and fail loud.
|
|
171
|
+
*/
|
|
172
|
+
function nodeEvolutionIo(lockAttempts = 40) {
|
|
103
173
|
const isMissing = (error) => {
|
|
104
174
|
const code = error?.code;
|
|
105
175
|
return code === "ENOENT" || code === "ENOTDIR";
|
|
@@ -114,6 +184,24 @@ function nodeEvolutionIo() {
|
|
|
114
184
|
}
|
|
115
185
|
};
|
|
116
186
|
/**
|
|
187
|
+
* V10-07 (P2-1): age threshold for taking over a lock whose body is TORN
|
|
188
|
+
* (non-empty, but the pid prefix does not parse to a positive integer). 1h:
|
|
189
|
+
* a legal hold (the ~2s lock/rename retry budgets, a slow task) never
|
|
190
|
+
* approaches minutes, so 1h fires only in the "torn write + creator long
|
|
191
|
+
* dead" scenario — far above any legitimate hold, far below "forever".
|
|
192
|
+
*/
|
|
193
|
+
const LOCK_TEAR_TAKEOVER_MS = 36e5;
|
|
194
|
+
/**
|
|
195
|
+
* V10-06 (P1-1 integration fix): a takeover TICKET must never be reclaimed
|
|
196
|
+
* while its (live) holder can still be committing — the C-28 rename retry
|
|
197
|
+
* budget is ~2.35s under AV/indexer pressure, so the former 1s ticket age
|
|
198
|
+
* threshold let a contender steal an in-flight ticket, double-enter the
|
|
199
|
+
* critical section and drop one RMW (observed as 5/6 increments under
|
|
200
|
+
* full-suite load). 5s = retry budget + comfortable margin; a DEAD holder's
|
|
201
|
+
* ticket is still reclaimed immediately via the pid probe.
|
|
202
|
+
*/
|
|
203
|
+
const TICKET_STALE_MS = 5e3;
|
|
204
|
+
/**
|
|
117
205
|
* Cross-process write lock (claw `withFileLock` parity): an O_EXCL lock file
|
|
118
206
|
* guards the atomic write. A >1s-old lock is taken over ONLY after probing
|
|
119
207
|
* the holder pid it carries (rc.66): a LIVE holder is never stolen, so a
|
|
@@ -144,11 +232,15 @@ function nodeEvolutionIo() {
|
|
|
144
232
|
* mtime over 1s is indistinguishable from a long task still executing in this
|
|
145
233
|
* process, and recycling that live lock would double-hold it. A failure to
|
|
146
234
|
* release in finally is recorded so the next write self-heals.
|
|
235
|
+
* V10-07 (P2-1): a third takeover shape — a TORN body (non-empty, no
|
|
236
|
+
* parseable pid) — is taken over after a wide 1h threshold with a
|
|
237
|
+
* console.warn; before this branch such a lock blocked every future writer
|
|
238
|
+
* forever.
|
|
147
239
|
*/
|
|
148
240
|
const withWriteLock = async (path, task) => {
|
|
149
241
|
const lock = `${path}.lock`;
|
|
150
242
|
let myClaim = "";
|
|
151
|
-
for (let attempt = 0; attempt <
|
|
243
|
+
for (let attempt = 0; attempt < lockAttempts; attempt += 1) {
|
|
152
244
|
let lockHandle = null;
|
|
153
245
|
try {
|
|
154
246
|
myClaim = `${process.pid}:${randomBytes(4).toString("hex")}`;
|
|
@@ -179,21 +271,27 @@ function nodeEvolutionIo() {
|
|
|
179
271
|
}
|
|
180
272
|
const staleDead = Number.isInteger(holder) && holder > 0 && Date.now() - st.mtimeMs > 1e3 && !holderAlive;
|
|
181
273
|
const staleEmpty = holderContent === "" && Date.now() - st.mtimeMs > 1e3;
|
|
182
|
-
|
|
274
|
+
const staleCorrupt = holderContent !== "" && !(Number.isInteger(holder) && holder > 0) && Date.now() - st.mtimeMs > LOCK_TEAR_TAKEOVER_MS;
|
|
275
|
+
if (staleDead || staleEmpty || staleCorrupt) {
|
|
276
|
+
if (staleCorrupt) console.warn(`evolution-io: took over a corrupt write lock "${lock}" (body ${JSON.stringify(holderContent)} has no parseable pid, lock older than 1h) — a previous writer likely crashed mid-write`);
|
|
183
277
|
if (await readFile(lock, "utf8").catch(() => "") === holderContent) {
|
|
184
278
|
const ticket = `${lock}.next`;
|
|
185
279
|
try {
|
|
186
280
|
const ticketBody = await readFile(ticket, "utf8").catch(() => "");
|
|
187
281
|
const ticketMtime = await stat(ticket).then((s) => s.mtimeMs, () => 0);
|
|
188
282
|
const ticketHolder = Number(ticketBody.split(":")[0] ?? "");
|
|
189
|
-
if ((!Number.isInteger(ticketHolder) || ticketHolder <= 0 || !isAlive(ticketHolder) || Date.now() - ticketMtime >
|
|
283
|
+
if ((!Number.isInteger(ticketHolder) || ticketHolder <= 0 || !isAlive(ticketHolder) || Date.now() - ticketMtime > TICKET_STALE_MS) && (ticketBody !== "" || Date.now() - ticketMtime > TICKET_STALE_MS)) await rm(ticket, { force: true }).catch(() => {});
|
|
190
284
|
} catch {}
|
|
191
285
|
try {
|
|
192
286
|
await writeFile(ticket, `${process.pid}:${randomBytes(4).toString("hex")}`, { flag: "wx" });
|
|
193
287
|
} catch {
|
|
194
288
|
continue;
|
|
195
289
|
}
|
|
196
|
-
|
|
290
|
+
const verify = await readFile(lock, "utf8").catch(() => "");
|
|
291
|
+
if (verify === holderContent) {
|
|
292
|
+
const verifyHolder = Number(verify.split(":")[0] ?? "");
|
|
293
|
+
if (!(Number.isInteger(verifyHolder) && verifyHolder > 0 && isAlive(verifyHolder))) await rm(lock, { force: true }).catch(() => {});
|
|
294
|
+
}
|
|
197
295
|
await rm(ticket, { force: true }).catch(() => {});
|
|
198
296
|
continue;
|
|
199
297
|
}
|
|
@@ -207,13 +305,17 @@ function nodeEvolutionIo() {
|
|
|
207
305
|
try {
|
|
208
306
|
return await task();
|
|
209
307
|
} finally {
|
|
210
|
-
if (await readFile(lock, "utf8").catch(() => null) === myClaim) await rm(lock, {
|
|
308
|
+
if (await readFile(lock, "utf8").catch(() => null) === myClaim) await rm(lock, {
|
|
309
|
+
force: true,
|
|
310
|
+
maxRetries: 20,
|
|
311
|
+
retryDelay: 100
|
|
312
|
+
}).catch(async () => {
|
|
211
313
|
const body = await readFile(lock, "utf8").catch(() => "");
|
|
212
314
|
pendingSelfCleanup.set(lock, body);
|
|
213
315
|
});
|
|
214
316
|
}
|
|
215
317
|
}
|
|
216
|
-
throw new Error(`could not acquire write lock for ${path} after
|
|
318
|
+
throw new Error(`could not acquire write lock for ${path} after ${lockAttempts} attempts`);
|
|
217
319
|
};
|
|
218
320
|
/** 0.3.17 (E-8b): sweep tmp files a crashed writer left behind — same
|
|
219
321
|
* `<target>.<pid>.<rand>.tmp` shape, older than 1h AND held by a dead pid.
|
|
@@ -231,6 +333,7 @@ function nodeEvolutionIo() {
|
|
|
231
333
|
const prefix = `${base}.`;
|
|
232
334
|
const lockName = `${base}.lock`;
|
|
233
335
|
const ticketName = `${lockName}.next`;
|
|
336
|
+
const CORRUPT_SWEEP_AGE_MS = 168 * 36e5;
|
|
234
337
|
for (const name of entries) {
|
|
235
338
|
if (!name.startsWith(prefix) || name === lockName) continue;
|
|
236
339
|
if (!name.endsWith(".tmp")) {
|
|
@@ -244,6 +347,14 @@ function nodeEvolutionIo() {
|
|
|
244
347
|
const old = Date.now() - st.mtimeMs > 1e3;
|
|
245
348
|
if (dead || old) await rm(ticketPath, { force: true });
|
|
246
349
|
} catch {}
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (name.includes(".corrupt")) {
|
|
353
|
+
const corruptPath = join(dir, name);
|
|
354
|
+
try {
|
|
355
|
+
const st = await stat(corruptPath);
|
|
356
|
+
if (Date.now() - st.mtimeMs > CORRUPT_SWEEP_AGE_MS) await rm(corruptPath, { force: true });
|
|
357
|
+
} catch {}
|
|
247
358
|
}
|
|
248
359
|
continue;
|
|
249
360
|
}
|
|
@@ -269,9 +380,7 @@ function nodeEvolutionIo() {
|
|
|
269
380
|
await mkdir(dirname(path), { recursive: true });
|
|
270
381
|
await withWriteLock(path, async () => {
|
|
271
382
|
await sweepStaleTmps(path);
|
|
272
|
-
|
|
273
|
-
await writeFile(tmp, content, "utf8");
|
|
274
|
-
await commitTmp(tmp, path);
|
|
383
|
+
await commitTmp(await writeDurableTmp(path, content), path);
|
|
275
384
|
});
|
|
276
385
|
},
|
|
277
386
|
async transact(path, task) {
|
|
@@ -291,9 +400,7 @@ function nodeEvolutionIo() {
|
|
|
291
400
|
return;
|
|
292
401
|
}
|
|
293
402
|
if (next === current) return;
|
|
294
|
-
|
|
295
|
-
await writeFile(tmp, next, "utf8");
|
|
296
|
-
await commitTmp(tmp, path);
|
|
403
|
+
await commitTmp(await writeDurableTmp(path, next), path);
|
|
297
404
|
});
|
|
298
405
|
},
|
|
299
406
|
async remove(path) {
|
|
@@ -399,7 +506,7 @@ function normalizeUsageRecord(record) {
|
|
|
399
506
|
const base = emptyRecord();
|
|
400
507
|
if (!record || typeof record !== "object" || Array.isArray(record)) return base;
|
|
401
508
|
const raw = record;
|
|
402
|
-
const num = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
509
|
+
const num = (value, fallback) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
403
510
|
const bool = (value, fallback) => typeof value === "boolean" ? value : fallback;
|
|
404
511
|
return {
|
|
405
512
|
created_by: typeof raw.created_by === "string" ? raw.created_by : null,
|
|
@@ -793,7 +900,7 @@ function renderCuratorReportMarkdown(report) {
|
|
|
793
900
|
""
|
|
794
901
|
].join("\n");
|
|
795
902
|
}
|
|
796
|
-
const NOMINATION_NAME_RE =
|
|
903
|
+
const NOMINATION_NAME_RE = SKILL_NAME_RE;
|
|
797
904
|
/**
|
|
798
905
|
* Parse the curator LLM's YAML nomination block (consolidations + prunings).
|
|
799
906
|
* Line-oriented and lenient by design: the LLM output is advisory, every name
|
|
@@ -1010,7 +1117,8 @@ function eventsFile(home) {
|
|
|
1010
1117
|
return join(home, "evolution", "events.json");
|
|
1011
1118
|
}
|
|
1012
1119
|
function isEventRecord(event) {
|
|
1013
|
-
|
|
1120
|
+
const seq = event?.seq;
|
|
1121
|
+
return typeof event === "object" && event !== null && typeof seq === "number" && Number.isFinite(seq);
|
|
1014
1122
|
}
|
|
1015
1123
|
/**
|
|
1016
1124
|
* Parse an event log body. A missing file, a whitespace-only file (rc.69:
|
|
@@ -1029,13 +1137,22 @@ function isEventRecord(event) {
|
|
|
1029
1137
|
* damaged record is the only loss (self-heal semantics, matching the usage
|
|
1030
1138
|
* sidecar's per-field normalization on read).
|
|
1031
1139
|
*/
|
|
1140
|
+
/**
|
|
1141
|
+
* Post-parse v1 gate shared by parseEvolutionEvents and appendEvolutionEvent
|
|
1142
|
+
* (C-06, v10 audit: the append used to JSON.parse the same body a second time
|
|
1143
|
+
* through parseEvolutionEvents — double parse cost per append). A null parse
|
|
1144
|
+
* (missing/whitespace/unparsable body) reads as an empty timeline.
|
|
1145
|
+
*/
|
|
1146
|
+
function v1EventRecords(parsed) {
|
|
1147
|
+
if (parsed === null) return [];
|
|
1148
|
+
if (parsed.version !== void 0 && parsed.version !== 1) return [];
|
|
1149
|
+
if (!Array.isArray(parsed.events)) return [];
|
|
1150
|
+
return parsed.events.filter(isEventRecord);
|
|
1151
|
+
}
|
|
1032
1152
|
function parseEvolutionEvents(raw) {
|
|
1033
1153
|
if (raw === null || raw.trim() === "") return [];
|
|
1034
1154
|
try {
|
|
1035
|
-
|
|
1036
|
-
if (parsed.version !== void 0 && parsed.version !== 1) return [];
|
|
1037
|
-
if (!Array.isArray(parsed.events)) return [];
|
|
1038
|
-
return parsed.events.filter(isEventRecord);
|
|
1155
|
+
return v1EventRecords(JSON.parse(raw));
|
|
1039
1156
|
} catch {
|
|
1040
1157
|
return [];
|
|
1041
1158
|
}
|
|
@@ -1073,6 +1190,7 @@ async function listEventArchives(io, path) {
|
|
|
1073
1190
|
async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE_AT) {
|
|
1074
1191
|
let assigned = 0;
|
|
1075
1192
|
let refuseMessage = "";
|
|
1193
|
+
let parsedBody = null;
|
|
1076
1194
|
await transactIo(io, path, async (current) => {
|
|
1077
1195
|
if (current !== null && current.trim() !== "") {
|
|
1078
1196
|
let shape;
|
|
@@ -1081,6 +1199,7 @@ async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE
|
|
|
1081
1199
|
} catch {
|
|
1082
1200
|
return current;
|
|
1083
1201
|
}
|
|
1202
|
+
parsedBody = shape;
|
|
1084
1203
|
if (shape.version !== void 0 && shape.version !== 1) {
|
|
1085
1204
|
const found = typeof shape.version === "number" || typeof shape.version === "string" ? String(shape.version) : "unknown";
|
|
1086
1205
|
const kind = typeof shape.version === "number" || typeof shape.version === "string" ? typeof shape.version : "unknown";
|
|
@@ -1088,7 +1207,7 @@ async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE
|
|
|
1088
1207
|
return current;
|
|
1089
1208
|
}
|
|
1090
1209
|
}
|
|
1091
|
-
const nextEvents = await rotateIfDue(io, path,
|
|
1210
|
+
const nextEvents = await rotateIfDue(io, path, await rotateIfDue(io, path, v1EventRecords(parsedBody), rotateAt), rotateAt);
|
|
1092
1211
|
let maxSeq = nextEvents.reduce((max, entry) => Math.max(max, entry.seq), 0);
|
|
1093
1212
|
if (maxSeq === 0) for (const name of await listEventArchives(io, path)) maxSeq = Math.max(maxSeq, Number.parseInt(name.slice(7, name.length - 5), 10));
|
|
1094
1213
|
const record = {
|
|
@@ -1229,8 +1348,8 @@ async function readEvolutionTimeline(io, path) {
|
|
|
1229
1348
|
* changes semantically: the bundle digest is the fail-closed signal for
|
|
1230
1349
|
* review workers, so a stale id across deployments must be distinguishable.
|
|
1231
1350
|
*/
|
|
1232
|
-
const PROMPT_BUNDLE_VERSION =
|
|
1233
|
-
const PROMPT_BUNDLE_ID = `dsh-evolution@
|
|
1351
|
+
const PROMPT_BUNDLE_VERSION = 15;
|
|
1352
|
+
const PROMPT_BUNDLE_ID = `dsh-evolution@15`;
|
|
1234
1353
|
const MEMORY_REVIEW_PROMPT = `[Auto-review — Memory]
|
|
1235
1354
|
Review the conversation above and consider saving to memory if appropriate.
|
|
1236
1355
|
|
|
@@ -1477,6 +1596,18 @@ D. 库·整合纪律(计划形态约束)
|
|
|
1477
1596
|
- 库规模无关:判据是事实与条款,不是库体量印象。
|
|
1478
1597
|
- 信号机制疑问(阈值/检测原理)→ needs_human,不猜测机制。`;
|
|
1479
1598
|
/**
|
|
1599
|
+
* One-line output instruction appended after the facts block in the maintain
|
|
1600
|
+
* subagent's prompt (persona carries the template, the prompt carries facts +
|
|
1601
|
+
* this instruction — one copy of the template in the model input, 011 v11
|
|
1602
|
+
* P3-4). F-16: this text used to be hardcoded in evolution-maintenance
|
|
1603
|
+
* orchestrate — a second model-facing prompt living OUTSIDE the bundle digest.
|
|
1604
|
+
* It now rides PROMPT_BUNDLE so the digest integrity check covers every
|
|
1605
|
+
* maintenance prompt. Adding the entry changes the bundle digest (the intended
|
|
1606
|
+
* fail-closed signal); PROMPT_BUNDLE_VERSION itself is owned by the core test
|
|
1607
|
+
* pin and stays untouched in this batch.
|
|
1608
|
+
*/
|
|
1609
|
+
const MAINTAIN_OUTPUT_INSTRUCTION = "按模板契约输出 JSON 维护计划(verdict/plan/notes);除 skill 工具与维护模板外你无其他工具。";
|
|
1610
|
+
/**
|
|
1480
1611
|
* System-prompt guidance section (Hermes `SKILLS_GUIDANCE`, DSH-adapted).
|
|
1481
1612
|
* Registered as a system-prompt section by tool-skill-manage (it mounts
|
|
1482
1613
|
* exactly when `skill_manage` is available — the DSH analogue of Hermes'
|
|
@@ -1505,12 +1636,12 @@ function sha256(text) {
|
|
|
1505
1636
|
function createPromptBundle(prompts) {
|
|
1506
1637
|
const canonical = JSON.stringify({
|
|
1507
1638
|
id: PROMPT_BUNDLE_ID,
|
|
1508
|
-
version:
|
|
1639
|
+
version: 15,
|
|
1509
1640
|
prompts: Object.fromEntries(Object.entries(prompts).sort())
|
|
1510
1641
|
});
|
|
1511
1642
|
return Object.freeze({
|
|
1512
1643
|
id: PROMPT_BUNDLE_ID,
|
|
1513
|
-
version:
|
|
1644
|
+
version: 15,
|
|
1514
1645
|
prompts: Object.freeze({ ...prompts }),
|
|
1515
1646
|
sha256: sha256(canonical)
|
|
1516
1647
|
});
|
|
@@ -1524,13 +1655,14 @@ const PROMPT_BUNDLE = createPromptBundle({
|
|
|
1524
1655
|
curator: CURATOR_PROMPT,
|
|
1525
1656
|
completion: COMPLETION_SKILL_REVIEW_PROMPT,
|
|
1526
1657
|
maintain: MAINTAIN_PROMPT,
|
|
1658
|
+
maintainOutput: MAINTAIN_OUTPUT_INSTRUCTION,
|
|
1527
1659
|
skillsGuidance: SKILLS_GUIDANCE
|
|
1528
1660
|
});
|
|
1529
1661
|
function verifyPromptBundle(bundle = PROMPT_BUNDLE) {
|
|
1530
|
-
if (bundle.id !== PROMPT_BUNDLE_ID || bundle.version !==
|
|
1662
|
+
if (bundle.id !== PROMPT_BUNDLE_ID || bundle.version !== 15) return false;
|
|
1531
1663
|
const canonical = JSON.stringify({
|
|
1532
1664
|
id: PROMPT_BUNDLE_ID,
|
|
1533
|
-
version:
|
|
1665
|
+
version: 15,
|
|
1534
1666
|
prompts: Object.fromEntries(Object.entries(bundle.prompts).sort())
|
|
1535
1667
|
});
|
|
1536
1668
|
return bundle.sha256 === sha256(canonical);
|
|
@@ -1611,6 +1743,43 @@ function buildLearnPrompt(userRequest) {
|
|
|
1611
1743
|
].join("\n");
|
|
1612
1744
|
}
|
|
1613
1745
|
//#endregion
|
|
1746
|
+
//#region lib/types/state-store.js
|
|
1747
|
+
/**
|
|
1748
|
+
* Evolution home path helpers: the DSH root and `$DSH_HOME/evolution` for
|
|
1749
|
+
* plugin-owned sidecar state (reports, activity store, feedback file,
|
|
1750
|
+
* state-domain data).
|
|
1751
|
+
*
|
|
1752
|
+
* C-10: PATH HELPERS ONLY — despite the file name there is no store
|
|
1753
|
+
* here. Durable evolution state lives in the state stack (evolution-state over
|
|
1754
|
+
* evolution-state-json / -domain); skills and memories live in skill-store.ts
|
|
1755
|
+
* / memory-store.ts. The file name is kept deliberately: renaming it would
|
|
1756
|
+
* touch every family import for zero behavior change, and the audit records
|
|
1757
|
+
* the mismatch as known naming debt.
|
|
1758
|
+
*/
|
|
1759
|
+
/**
|
|
1760
|
+
* DSH home root: `$DSH_HOME` or `~/.dsh`. Single source of the empty-string
|
|
1761
|
+
* fallback — an EMPTY or WHITESPACE-ONLY DSH_HOME resolves to the default
|
|
1762
|
+
* home, never to a CWD-relative path (0.3.19 W1.3, 0.3.22 F-207);
|
|
1763
|
+
* V8-06 (0.3.47) extends the guard to whitespace (upstream home-paths:
|
|
1764
|
+
* `trim().length > 0` is the adoption test — `DSH_HOME=" "` must not produce
|
|
1765
|
+
* a sidecar under a relative "." path).
|
|
1766
|
+
* C-11: the adoption test and the RETURNED value now come from the
|
|
1767
|
+
* SAME trimmed source — the old form tested `trim()` but returned the raw
|
|
1768
|
+
* value, so `DSH_HOME=" /x "` was accepted AND persisted with literal spaces.
|
|
1769
|
+
* Known tradeoff vs upstream `resolveDshHome`: `~` is NOT expanded here —
|
|
1770
|
+
* documented as a deliberate difference in the v10 audit; revisit only if a
|
|
1771
|
+
* real deployment needs it.
|
|
1772
|
+
*/
|
|
1773
|
+
function evolutionRoot(env = process.env) {
|
|
1774
|
+
const home = env.DSH_HOME?.trim();
|
|
1775
|
+
return home ? home : join(homedir(), ".dsh");
|
|
1776
|
+
}
|
|
1777
|
+
/** Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
|
|
1778
|
+
* state (reports, activity store, feedback file, state-domain data). */
|
|
1779
|
+
function evolutionHome(env = process.env) {
|
|
1780
|
+
return join(evolutionRoot(env), "evolution");
|
|
1781
|
+
}
|
|
1782
|
+
//#endregion
|
|
1614
1783
|
//#region lib/types/serial.js
|
|
1615
1784
|
/**
|
|
1616
1785
|
* A process-local serial task queue: each task starts only after the previous
|
|
@@ -1788,7 +1957,7 @@ const PATTERNS = [
|
|
|
1788
1957
|
label: "ssh_backdoor",
|
|
1789
1958
|
category: "persistence",
|
|
1790
1959
|
scope: "strict",
|
|
1791
|
-
regex: /
|
|
1960
|
+
regex: /\bauthorized_keys\b/i
|
|
1792
1961
|
},
|
|
1793
1962
|
{
|
|
1794
1963
|
label: "agent_config_mod",
|
|
@@ -1921,6 +2090,15 @@ function scanContentThreats(text, maxScanChars = 65536, options = NO_SCAN_OPTION
|
|
|
1921
2090
|
if (!blocked) return null;
|
|
1922
2091
|
return `Blocked by security scan (${findings[0]?.label ?? "unknown"}). This content appears to contain potentially malicious instructions.`;
|
|
1923
2092
|
}
|
|
2093
|
+
/**
|
|
2094
|
+
* V10-03 (P2-18): suffix the SkillLibrary/MemoryStore write gates append to a
|
|
2095
|
+
* block message — the hit label is already embedded by scanContentThreats /
|
|
2096
|
+
* scanMemoryThreats, this names the deployable self-heal path so the model
|
|
2097
|
+
* (or operator) can allowlist a known-benign label. The evolution-threat tool
|
|
2098
|
+
* channel deliberately does NOT append it: that channel has no
|
|
2099
|
+
* threatExemptLabels option to advertise.
|
|
2100
|
+
*/
|
|
2101
|
+
const THREAT_EXEMPT_HINT = " If this is a legitimate false positive, the deployment can allow its label via the threatExemptLabels store option.";
|
|
1924
2102
|
//#endregion
|
|
1925
2103
|
//#region lib/types/memory-store.js
|
|
1926
2104
|
/**
|
|
@@ -1961,7 +2139,7 @@ function previewEntries(entries) {
|
|
|
1961
2139
|
return `\n\nCurrent entries (preview):\n${shown.join("\n")}${more}`;
|
|
1962
2140
|
}
|
|
1963
2141
|
function memoryRoot(env = process.env) {
|
|
1964
|
-
return join(env
|
|
2142
|
+
return join(evolutionRoot(env), "memories");
|
|
1965
2143
|
}
|
|
1966
2144
|
function fileFor(root, target) {
|
|
1967
2145
|
return join(root, target === "memory" ? "MEMORY.md" : "USER.md");
|
|
@@ -1990,6 +2168,8 @@ var MemoryStore = class {
|
|
|
1990
2168
|
root;
|
|
1991
2169
|
maxFailures;
|
|
1992
2170
|
io;
|
|
2171
|
+
/** V10-03 (P2-18): see MemoryStoreOptions.threatExemptLabels. */
|
|
2172
|
+
threatExemptLabels;
|
|
1993
2173
|
/** V6-16 (0.3.37): same-process RMW serialization (the SkillLibrary queue) —
|
|
1994
2174
|
* on a backend WITHOUT a transact lock two concurrent callers compute on the
|
|
1995
2175
|
* same old content and the last rename wins, silently dropping one op's
|
|
@@ -2005,6 +2185,18 @@ var MemoryStore = class {
|
|
|
2005
2185
|
this.addDatePrefix = options.addDatePrefix ?? false;
|
|
2006
2186
|
this.root = options.root ?? memoryRoot();
|
|
2007
2187
|
this.maxFailures = options.maxConsolidationFailures ?? 3;
|
|
2188
|
+
this.threatExemptLabels = options.threatExemptLabels ?? [];
|
|
2189
|
+
}
|
|
2190
|
+
/** V10-03 (P2-18): ScanOptions shared by every threat check of this store —
|
|
2191
|
+
* the constructor's exempt labels, empty by default (behavior unchanged). */
|
|
2192
|
+
threatScanOptions() {
|
|
2193
|
+
return this.threatExemptLabels.length > 0 ? { excludeLabels: this.threatExemptLabels } : {};
|
|
2194
|
+
}
|
|
2195
|
+
/** V10-03 (P2-18): the strict-scan write gate. A block message names the hit
|
|
2196
|
+
* label (scanMemoryThreats already embeds it) plus the self-heal hint. */
|
|
2197
|
+
memoryThreatBlock(text) {
|
|
2198
|
+
const threat = scanMemoryThreats(text, void 0, this.threatScanOptions());
|
|
2199
|
+
return threat === null ? null : threat + THREAT_EXEMPT_HINT;
|
|
2008
2200
|
}
|
|
2009
2201
|
limitFor(target) {
|
|
2010
2202
|
return target === "memory" ? this.memoryLimit : this.userLimit;
|
|
@@ -2071,7 +2263,12 @@ var MemoryStore = class {
|
|
|
2071
2263
|
* never loaded just to back it up. Failure to back up does not change the
|
|
2072
2264
|
* refusal semantics. V8-23⑫ (0.3.49): ONE fixed backup name per target —
|
|
2073
2265
|
* a fresh refusal overwrites it (the previous timestamped names accumulated
|
|
2074
|
-
* per drift incident with no retention policy).
|
|
2266
|
+
* per drift incident with no retention policy). V9-08 (0.3.51) declares the
|
|
2267
|
+
* two failure shapes: (1) the pre-copy remove of the previous `.bak` fails —
|
|
2268
|
+
* harmless, because the copy contract is overwrite (`cp force`);
|
|
2269
|
+
* (2) the copy itself fails (disk/backend) — returns `null` and the refusal
|
|
2270
|
+
* message simply carries no backup suffix; the refusal semantics and the
|
|
2271
|
+
* on-disk file are untouched either way.
|
|
2075
2272
|
*/
|
|
2076
2273
|
async backupFile(target) {
|
|
2077
2274
|
const path = fileFor(this.root, target);
|
|
@@ -2124,7 +2321,13 @@ var MemoryStore = class {
|
|
|
2124
2321
|
outcome = core.result;
|
|
2125
2322
|
return core.write ?? current ?? null;
|
|
2126
2323
|
});
|
|
2127
|
-
return outcome
|
|
2324
|
+
return outcome ?? {
|
|
2325
|
+
ok: false,
|
|
2326
|
+
message: "internal error: the memory transaction did not invoke the task; no write was performed",
|
|
2327
|
+
entries: [],
|
|
2328
|
+
chars: 0,
|
|
2329
|
+
limit: this.limitFor(target)
|
|
2330
|
+
};
|
|
2128
2331
|
}
|
|
2129
2332
|
/**
|
|
2130
2333
|
* Single-entry add inside the transaction: shared checks (oversized,
|
|
@@ -2150,7 +2353,7 @@ var MemoryStore = class {
|
|
|
2150
2353
|
write: null
|
|
2151
2354
|
};
|
|
2152
2355
|
}
|
|
2153
|
-
const threat =
|
|
2356
|
+
const threat = this.memoryThreatBlock(content);
|
|
2154
2357
|
if (threat) return {
|
|
2155
2358
|
result: {
|
|
2156
2359
|
ok: false,
|
|
@@ -2161,7 +2364,8 @@ var MemoryStore = class {
|
|
|
2161
2364
|
},
|
|
2162
2365
|
write: null
|
|
2163
2366
|
};
|
|
2164
|
-
|
|
2367
|
+
const prefixed = this.addDatePrefix ? `## ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}\n${content}` : content;
|
|
2368
|
+
if (hasEntryDelimiter(prefixed)) return {
|
|
2165
2369
|
result: {
|
|
2166
2370
|
ok: false,
|
|
2167
2371
|
message: "Fact contains the entry delimiter (§) and would split into multiple entries; rewrite it as separate facts.",
|
|
@@ -2185,7 +2389,7 @@ var MemoryStore = class {
|
|
|
2185
2389
|
write: null
|
|
2186
2390
|
};
|
|
2187
2391
|
}
|
|
2188
|
-
const next = [...entries,
|
|
2392
|
+
const next = [...entries, prefixed];
|
|
2189
2393
|
const total = next.join(ENTRY_DELIMITER).length;
|
|
2190
2394
|
const addLimit = this.limitFor(target);
|
|
2191
2395
|
if (addLimit > 0 && total > addLimit) return {
|
|
@@ -2232,7 +2436,13 @@ var MemoryStore = class {
|
|
|
2232
2436
|
outcome = core.result;
|
|
2233
2437
|
return core.write ?? current ?? null;
|
|
2234
2438
|
});
|
|
2235
|
-
return outcome
|
|
2439
|
+
return outcome ?? {
|
|
2440
|
+
ok: false,
|
|
2441
|
+
message: "internal error: the memory transaction did not invoke the task; no write was performed",
|
|
2442
|
+
entries: [],
|
|
2443
|
+
chars: 0,
|
|
2444
|
+
limit: this.limitFor(target)
|
|
2445
|
+
};
|
|
2236
2446
|
}
|
|
2237
2447
|
/** Batch RMW inside the transaction. `write: null` = failure/no-op, disk untouched. */
|
|
2238
2448
|
async applyBatchCore(target, operations, raw) {
|
|
@@ -2251,6 +2461,7 @@ var MemoryStore = class {
|
|
|
2251
2461
|
}
|
|
2252
2462
|
const entries = [...new Set(normalizeEntries(raw))];
|
|
2253
2463
|
const working = [...entries];
|
|
2464
|
+
const datePrefix = this.addDatePrefix ? `## ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}\n` : "";
|
|
2254
2465
|
for (const [index, op] of operations.entries()) {
|
|
2255
2466
|
const position = index + 1;
|
|
2256
2467
|
if (op.action === "add") {
|
|
@@ -2265,7 +2476,7 @@ var MemoryStore = class {
|
|
|
2265
2476
|
},
|
|
2266
2477
|
write: null
|
|
2267
2478
|
};
|
|
2268
|
-
const threat =
|
|
2479
|
+
const threat = this.memoryThreatBlock(body);
|
|
2269
2480
|
if (threat) return {
|
|
2270
2481
|
result: {
|
|
2271
2482
|
ok: false,
|
|
@@ -2276,7 +2487,7 @@ var MemoryStore = class {
|
|
|
2276
2487
|
},
|
|
2277
2488
|
write: null
|
|
2278
2489
|
};
|
|
2279
|
-
const entryBody =
|
|
2490
|
+
const entryBody = `${datePrefix}${body}`;
|
|
2280
2491
|
if (hasEntryDelimiter(entryBody)) return {
|
|
2281
2492
|
result: {
|
|
2282
2493
|
ok: false,
|
|
@@ -2344,7 +2555,7 @@ var MemoryStore = class {
|
|
|
2344
2555
|
},
|
|
2345
2556
|
write: null
|
|
2346
2557
|
};
|
|
2347
|
-
const threat =
|
|
2558
|
+
const threat = this.memoryThreatBlock(body);
|
|
2348
2559
|
if (threat) return {
|
|
2349
2560
|
result: {
|
|
2350
2561
|
ok: false,
|
|
@@ -2355,7 +2566,8 @@ var MemoryStore = class {
|
|
|
2355
2566
|
},
|
|
2356
2567
|
write: null
|
|
2357
2568
|
};
|
|
2358
|
-
|
|
2569
|
+
const entryBody = `${datePrefix}${body}`;
|
|
2570
|
+
if (hasEntryDelimiter(entryBody)) return {
|
|
2359
2571
|
result: {
|
|
2360
2572
|
ok: false,
|
|
2361
2573
|
message: `Operation ${position} (replace): Fact contains the entry delimiter (§) and would split into multiple entries; rewrite it as separate facts.${previewEntries(entries)}`,
|
|
@@ -2365,7 +2577,7 @@ var MemoryStore = class {
|
|
|
2365
2577
|
},
|
|
2366
2578
|
write: null
|
|
2367
2579
|
};
|
|
2368
|
-
working[matchIndex] =
|
|
2580
|
+
working[matchIndex] = entryBody;
|
|
2369
2581
|
}
|
|
2370
2582
|
}
|
|
2371
2583
|
const total = working.join(ENTRY_DELIMITER).length;
|
|
@@ -2404,13 +2616,13 @@ var MemoryStore = class {
|
|
|
2404
2616
|
parts.push(`## ${label} — file skipped: ${oversized.size} bytes (limit ${oversized.limit * READ_GUARD_FACTOR}); not read`);
|
|
2405
2617
|
continue;
|
|
2406
2618
|
}
|
|
2407
|
-
const safe = entries.filter((entry) => !scanMemoryThreats(entry));
|
|
2619
|
+
const safe = entries.filter((entry) => !scanMemoryThreats(entry, void 0, this.threatScanOptions()));
|
|
2408
2620
|
if (safe.length > 0) {
|
|
2409
2621
|
const body = safe.join(ENTRY_DELIMITER);
|
|
2410
2622
|
const limit = this.limitFor(target);
|
|
2411
|
-
const pct = limit > 0 ? Math.min(100, Math.floor(body.length * 100 / limit)) : 0;
|
|
2412
2623
|
const note = safe.length === entries.length ? "" : ` (${entries.length - safe.length} threat-matched entries filtered)`;
|
|
2413
|
-
|
|
2624
|
+
const usage = limit > 0 ? ` [${Math.min(100, Math.floor(body.length * 100 / limit))}% — ${body.length}/${limit} chars]` : "";
|
|
2625
|
+
parts.push(`## ${label} (${safe.length} entries)${usage}${note}\n${body}`);
|
|
2414
2626
|
}
|
|
2415
2627
|
}
|
|
2416
2628
|
return parts.join("\n\n");
|
|
@@ -2480,6 +2692,7 @@ async function recordMutation(root, io, record, cap = 500) {
|
|
|
2480
2692
|
if (current !== null) try {
|
|
2481
2693
|
JSON.parse(current);
|
|
2482
2694
|
} catch {
|
|
2695
|
+
console.warn(`mutation audit record dropped: ${mutationsFile(root)} is malformed and was not overwritten`);
|
|
2483
2696
|
return current;
|
|
2484
2697
|
}
|
|
2485
2698
|
const existing = parseMutationRecords(current);
|
|
@@ -2522,8 +2735,8 @@ function composePresetComposition(standardComposition, deltaComposition) {
|
|
|
2522
2735
|
function compositionRowIds(composition) {
|
|
2523
2736
|
const ids = /* @__PURE__ */ new Set();
|
|
2524
2737
|
for (const line of composition.split("\n")) {
|
|
2525
|
-
const
|
|
2526
|
-
if (
|
|
2738
|
+
const id = /^- id:\s*(\S+)/.exec(line)?.[1];
|
|
2739
|
+
if (id) ids.add(id);
|
|
2527
2740
|
}
|
|
2528
2741
|
return ids;
|
|
2529
2742
|
}
|
|
@@ -2590,7 +2803,10 @@ function computeQualityScores(input) {
|
|
|
2590
2803
|
function normalize(content) {
|
|
2591
2804
|
return content.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2592
2805
|
}
|
|
2593
|
-
|
|
2806
|
+
/** C-26 (v10 audit): renamed from `contentHash` — mutations.ts exports a
|
|
2807
|
+
* contentHash of RAW bytes, this one hashes the NORMALIZED text for dedup
|
|
2808
|
+
* grouping. Same private helper, unambiguous name. */
|
|
2809
|
+
function normalizedHash(content) {
|
|
2594
2810
|
return createHash("sha256").update(normalize(content)).digest("hex");
|
|
2595
2811
|
}
|
|
2596
2812
|
function tokenize(content) {
|
|
@@ -2612,7 +2828,7 @@ function computeDedupGroups(input) {
|
|
|
2612
2828
|
const names = [...input.contents.keys()];
|
|
2613
2829
|
const hashes = /* @__PURE__ */ new Map();
|
|
2614
2830
|
for (const name of names) {
|
|
2615
|
-
const hash =
|
|
2831
|
+
const hash = normalizedHash(input.contents.get(name) ?? "");
|
|
2616
2832
|
const bucket = hashes.get(hash);
|
|
2617
2833
|
if (bucket) bucket.push(name);
|
|
2618
2834
|
else hashes.set(hash, [name]);
|
|
@@ -2744,6 +2960,12 @@ const DEFAULT_HEALTH_THRESHOLDS = {
|
|
|
2744
2960
|
* (`defaced`, `feedback`) are not counted as commit shas; the ISO branch
|
|
2745
2961
|
* accepts a UTC `Z`, a numeric UTC offset (`+08:00`), or no timezone at all —
|
|
2746
2962
|
* non-UTC timestamps used to escape detection (log-like content missed).
|
|
2963
|
+
*
|
|
2964
|
+
* Known boundary, recorded not fixed (C-22, v10 audit): the hex branch also
|
|
2965
|
+
* matches 7-40 char PURE-DIGIT strings (order numbers etc.), so stamp density
|
|
2966
|
+
* can be over-reported for such content. The misjudgment direction is "extra
|
|
2967
|
+
* stamp hits" only — never a miss — and no real incident exists; revisit only
|
|
2968
|
+
* if curator decisions are actually distorted (v10 deferral ledger).
|
|
2747
2969
|
*/
|
|
2748
2970
|
const HEALTH_STAMP_RE = new RegExp(String.raw`\brc\.\d+\b|\b(?=[0-9a-f]{7,40}\b)[0-9a-f]*[0-9][0-9a-f]*\b|\b\d{4}-\d{2}-\d{2}(?:T[0-9:.]+(?:Z|[+-]\d{2}:?\d{2})?)?\b`, "g");
|
|
2749
2971
|
/**
|
|
@@ -2788,7 +3010,7 @@ function assessStructureHealth(snapshot, thresholds = DEFAULT_HEALTH_THRESHOLDS)
|
|
|
2788
3010
|
* Deterministic review signal gate.
|
|
2789
3011
|
*
|
|
2790
3012
|
* Scans a DSH session event log for durable learning signals before any LLM
|
|
2791
|
-
* is spent. `turn/end` calls `
|
|
3013
|
+
* is spent. `turn/end` calls `observeEvent`; the returned review kind is
|
|
2792
3014
|
* accumulated until a configured interval fires.
|
|
2793
3015
|
*/
|
|
2794
3016
|
const CORRECTION_PATTERNS = [
|
|
@@ -3008,7 +3230,7 @@ function computeDriftSignals(snapshots) {
|
|
|
3008
3230
|
const narrow = narrowNameMatches(snapshot.name);
|
|
3009
3231
|
signals.push(narrow.length === 0 ? sig("narrow_name", "pass", "none") : sig("narrow_name", "over", narrow.join(", "), void 0, `name=${snapshot.name}`));
|
|
3010
3232
|
const description = snapshot.description;
|
|
3011
|
-
signals.push(description === void 0 ? sig("description_chars", "unknown", "missing",
|
|
3233
|
+
signals.push(description === void 0 ? sig("description_chars", "unknown", "missing", `60`) : sig("description_chars", description.length > 60 ? "over" : "pass", `${description.length}`, `60`));
|
|
3012
3234
|
const quality = snapshot.quality;
|
|
3013
3235
|
signals.push(quality === null || quality === void 0 ? sig("quality_low", "unknown", "not-assessed") : sig("quality_low", quality < .3 ? "over" : "pass", quality.toFixed(2), `${LOW_QUALITY_THRESHOLD}`));
|
|
3014
3236
|
return {
|
|
@@ -3056,7 +3278,7 @@ const RESTRUCTURE_TARGET_RE = /^references\/[a-z0-9](?!.*\.\.)[a-z0-9._-]*\.md$/
|
|
|
3056
3278
|
/** Extra file name carried inside a snapshot's `extras/` directory. */
|
|
3057
3279
|
const SNAPSHOT_EXTRA_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
3058
3280
|
function skillsRoot(env = process.env) {
|
|
3059
|
-
return join(env
|
|
3281
|
+
return join(evolutionRoot(env), "skills");
|
|
3060
3282
|
}
|
|
3061
3283
|
/** 0.3.18 (S4.1, E-30): the ONE root resolution for every member that reads
|
|
3062
3284
|
* the skills tree — tool-skill-manage / evolution-skill-catalog / skill-usage
|
|
@@ -3213,6 +3435,13 @@ function frontmatterYamlUnsafeValues(content) {
|
|
|
3213
3435
|
* rewritten block no longer parses, or a rewritten value's parsed content
|
|
3214
3436
|
* differs from the original, the rewrite is rolled back and reported in
|
|
3215
3437
|
* `issues` (fail-loud, never a silent value corruption — P3-4).
|
|
3438
|
+
*
|
|
3439
|
+
* V10-02 (P2-3): the rewrite decision is PER LINE — each entry parses its own
|
|
3440
|
+
* value, so a duplicated key can never route one entry's unsafe value into a
|
|
3441
|
+
* different line's rewrite (the old key→Map lookup rewrote the FIRST (safe)
|
|
3442
|
+
* line with the SECOND line's quoted value, and the last-wins YAML reader
|
|
3443
|
+
* masked the damage). A duplicated key is itself invalid input and is
|
|
3444
|
+
* reported in `issues` (the write path refuses) instead of being rewritten.
|
|
3216
3445
|
*/
|
|
3217
3446
|
function normalizeFrontmatter(content) {
|
|
3218
3447
|
const block = frontmatterBlock(content);
|
|
@@ -3223,10 +3452,10 @@ function normalizeFrontmatter(content) {
|
|
|
3223
3452
|
issues: []
|
|
3224
3453
|
};
|
|
3225
3454
|
const { lines, end, nl } = block;
|
|
3226
|
-
const unsafe = new Map(frontmatterYamlUnsafeValues(content).map((entry) => [entry.key, entry.value]));
|
|
3227
|
-
const originalValues = new Map(unsafe);
|
|
3228
3455
|
const fields = [];
|
|
3229
3456
|
const issues = [];
|
|
3457
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3458
|
+
const originalValues = /* @__PURE__ */ new Map();
|
|
3230
3459
|
let changed = false;
|
|
3231
3460
|
for (let i = 1; i < end; i++) {
|
|
3232
3461
|
const line = lines[i];
|
|
@@ -3234,16 +3463,29 @@ function normalizeFrontmatter(content) {
|
|
|
3234
3463
|
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
|
|
3235
3464
|
if (!match) continue;
|
|
3236
3465
|
const key = match[1];
|
|
3237
|
-
|
|
3238
|
-
|
|
3466
|
+
if (key === void 0) continue;
|
|
3467
|
+
const value = (match[2] ?? "").trim();
|
|
3468
|
+
if (seen.has(key)) {
|
|
3469
|
+
issues.push(`${key}: duplicate frontmatter key — remove the repeated entry and retry`);
|
|
3470
|
+
continue;
|
|
3471
|
+
}
|
|
3472
|
+
seen.add(key);
|
|
3473
|
+
if (!yamlPlainScalarNeedsQuotes(value)) continue;
|
|
3239
3474
|
if (/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/.test(value)) {
|
|
3240
3475
|
issues.push(`${key}: value contains control characters — clean them manually`);
|
|
3241
3476
|
continue;
|
|
3242
3477
|
}
|
|
3243
3478
|
lines[i] = `${key}: ${value.includes("\"") || value.includes("\\") ? `'${value.replace(/'/g, "''")}'` : `"${value}"`}`;
|
|
3479
|
+
originalValues.set(key, value);
|
|
3244
3480
|
fields.push(key);
|
|
3245
3481
|
changed = true;
|
|
3246
3482
|
}
|
|
3483
|
+
if (issues.length > 0) return {
|
|
3484
|
+
content,
|
|
3485
|
+
changed: false,
|
|
3486
|
+
fields: [],
|
|
3487
|
+
issues
|
|
3488
|
+
};
|
|
3247
3489
|
if (!changed) return {
|
|
3248
3490
|
content,
|
|
3249
3491
|
changed: false,
|
|
@@ -3293,7 +3535,7 @@ function validateFrontmatter(content, expectedName, limits = DEFAULT_SKILL_LIMIT
|
|
|
3293
3535
|
const parsed = parseFrontmatter(content);
|
|
3294
3536
|
if (!parsed) return "SKILL.md must start and end with YAML frontmatter and include a body.";
|
|
3295
3537
|
if (!parsed.frontmatter.name) return "Frontmatter must include a name field.";
|
|
3296
|
-
if (
|
|
3538
|
+
if (!SKILL_NAME_RE.test(parsed.frontmatter.name)) return `Invalid skill name "${parsed.frontmatter.name}" — use lowercase letters, digits, and hyphens.`;
|
|
3297
3539
|
if (parsed.frontmatter.name.length > limits.maxNameLength) return `Skill name exceeds ${limits.maxNameLength} characters.`;
|
|
3298
3540
|
if (expectedName && parsed.frontmatter.name !== expectedName) return `Frontmatter name "${parsed.frontmatter.name}" does not match target skill "${expectedName}".`;
|
|
3299
3541
|
if (!parsed.frontmatter.description) return "Frontmatter must include a description field.";
|
|
@@ -3331,12 +3573,51 @@ async function listNames(root, io) {
|
|
|
3331
3573
|
}
|
|
3332
3574
|
return names.sort();
|
|
3333
3575
|
}
|
|
3576
|
+
/** C-18: support file names are restricted to the
|
|
3577
|
+
* RESTRUCTURE_TARGET_RE character class (leading `[a-z0-9]`, then
|
|
3578
|
+
* `[a-z0-9._-]`) — drive-colon / odd-character / uppercase names can no
|
|
3579
|
+
* longer reach the filesystem through writeSupportFile / patch /
|
|
3580
|
+
* removeSupportFile. */
|
|
3581
|
+
const SUPPORT_FILE_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
3582
|
+
/** C-18: win32 reserves these stems with ANY extension (`nul.md` hits the
|
|
3583
|
+
* NUL device), and they are fully inside the charset above — so the reserved
|
|
3584
|
+
* set is checked on the first-dot prefix as well; the charset close alone
|
|
3585
|
+
* cannot refuse them. */
|
|
3586
|
+
const WIN32_RESERVED_DEVICE_NAMES = new Set([
|
|
3587
|
+
"con",
|
|
3588
|
+
"prn",
|
|
3589
|
+
"aux",
|
|
3590
|
+
"nul",
|
|
3591
|
+
"com1",
|
|
3592
|
+
"com2",
|
|
3593
|
+
"com3",
|
|
3594
|
+
"com4",
|
|
3595
|
+
"com5",
|
|
3596
|
+
"com6",
|
|
3597
|
+
"com7",
|
|
3598
|
+
"com8",
|
|
3599
|
+
"com9",
|
|
3600
|
+
"lpt1",
|
|
3601
|
+
"lpt2",
|
|
3602
|
+
"lpt3",
|
|
3603
|
+
"lpt4",
|
|
3604
|
+
"lpt5",
|
|
3605
|
+
"lpt6",
|
|
3606
|
+
"lpt7",
|
|
3607
|
+
"lpt8",
|
|
3608
|
+
"lpt9"
|
|
3609
|
+
]);
|
|
3334
3610
|
function validateSupportPath(filePath) {
|
|
3335
3611
|
const normalized = filePath.replace(/\\/g, "/");
|
|
3336
3612
|
if (normalized.includes("..")) return "Path traversal is not allowed.";
|
|
3337
3613
|
const parts = normalized.split("/").filter(Boolean);
|
|
3338
3614
|
if (parts.length === 0 || !SUPPORT_DIRS.includes(parts[0])) return `file_path must be under one of: ${SUPPORT_DIRS.join(", ")}.`;
|
|
3339
3615
|
if (parts.length < 2) return "Provide a file name, not just a directory.";
|
|
3616
|
+
for (const part of parts.slice(1)) {
|
|
3617
|
+
if (!SUPPORT_FILE_NAME_RE.test(part)) return `Unsupported file name "${part}" — use lowercase letters, digits, dots, hyphens, and underscores (leading letter or digit).`;
|
|
3618
|
+
const stem = part.split(".")[0]?.toLowerCase() ?? "";
|
|
3619
|
+
if (WIN32_RESERVED_DEVICE_NAMES.has(stem)) return `Unsupported file name "${part}" — a Windows reserved device name.`;
|
|
3620
|
+
}
|
|
3340
3621
|
return null;
|
|
3341
3622
|
}
|
|
3342
3623
|
/**
|
|
@@ -3510,11 +3791,15 @@ var SkillLibrary = class {
|
|
|
3510
3791
|
* one skill never interleave their read-modify-write (the cross-process layer
|
|
3511
3792
|
* is the IO backend's transact lock; this chain is the second layer). */
|
|
3512
3793
|
serial;
|
|
3513
|
-
|
|
3794
|
+
/** V10-03 (P2-18): see the constructor's threatExemptLabels. Empty by
|
|
3795
|
+
* default — the strict ANY-hit-blocks policy is unchanged. */
|
|
3796
|
+
threatExemptLabels;
|
|
3797
|
+
constructor(root = skillsRoot(), io = nodeEvolutionIo(), limits = DEFAULT_SKILL_LIMITS, onMutation, transact, threatExemptLabels) {
|
|
3514
3798
|
this.root = root;
|
|
3515
3799
|
this.io = io;
|
|
3516
3800
|
this.limits = limits;
|
|
3517
3801
|
this.onMutation = onMutation;
|
|
3802
|
+
this.threatExemptLabels = threatExemptLabels ?? [];
|
|
3518
3803
|
this.transact = transact ?? (io.transact ? (ioLike, path, task) => {
|
|
3519
3804
|
const t = ioLike.transact;
|
|
3520
3805
|
return t ? t(path, task) : transactIo(ioLike, path, task);
|
|
@@ -3558,12 +3843,24 @@ var SkillLibrary = class {
|
|
|
3558
3843
|
this.onMutation?.(event);
|
|
3559
3844
|
} catch {}
|
|
3560
3845
|
}
|
|
3846
|
+
/** V10-03 (P2-18): ScanOptions shared by every write-path threat check —
|
|
3847
|
+
* the constructor's exempt labels, empty by default (behavior unchanged). */
|
|
3848
|
+
threatScanOptions() {
|
|
3849
|
+
return this.threatExemptLabels.length > 0 ? { excludeLabels: this.threatExemptLabels } : {};
|
|
3850
|
+
}
|
|
3851
|
+
/** V10-03 (P2-18): the strict-scan write gate. A block message names the hit
|
|
3852
|
+
* label (scanContentThreats already embeds it) plus the self-heal hint, so a
|
|
3853
|
+
* false-positive rewrite direction is actionable instead of a dead end. */
|
|
3854
|
+
contentThreatBlock(content) {
|
|
3855
|
+
const threat = scanContentThreats(content, void 0, this.threatScanOptions());
|
|
3856
|
+
return threat === null ? null : threat + THREAT_EXEMPT_HINT;
|
|
3857
|
+
}
|
|
3561
3858
|
async list() {
|
|
3562
3859
|
const summaries = [];
|
|
3563
3860
|
for (const name of await listNames(this.root, this.io)) {
|
|
3564
3861
|
const dir = this.dirOf(name);
|
|
3565
3862
|
const md = await this.io.readText(join(dir, "SKILL.md"));
|
|
3566
|
-
if (
|
|
3863
|
+
if (md === null) continue;
|
|
3567
3864
|
const parsed = parseFrontmatter(md);
|
|
3568
3865
|
let entries = [];
|
|
3569
3866
|
try {
|
|
@@ -3571,9 +3868,10 @@ var SkillLibrary = class {
|
|
|
3571
3868
|
} catch {}
|
|
3572
3869
|
const has = (marker) => entries.includes(markerEntryName(marker));
|
|
3573
3870
|
const protectedBy = has("bundled") ? "bundled" : has("hub-installed") ? "hub-installed" : has("pinned") ? "pinned" : null;
|
|
3871
|
+
const parsedDescription = parsed?.frontmatter.description;
|
|
3574
3872
|
summaries.push({
|
|
3575
3873
|
name,
|
|
3576
|
-
description:
|
|
3874
|
+
description: typeof parsedDescription === "string" ? parsedDescription : "",
|
|
3577
3875
|
path: dir,
|
|
3578
3876
|
protectedBy,
|
|
3579
3877
|
managed: has("hermes-managed"),
|
|
@@ -3739,9 +4037,10 @@ var SkillLibrary = class {
|
|
|
3739
4037
|
*/
|
|
3740
4038
|
async setPinned(name, pinned, origin = "foreground") {
|
|
3741
4039
|
const normalized = name.trim();
|
|
3742
|
-
|
|
4040
|
+
const bad = this.badName(normalized);
|
|
4041
|
+
if (bad) return {
|
|
3743
4042
|
ok: false,
|
|
3744
|
-
message:
|
|
4043
|
+
message: bad
|
|
3745
4044
|
};
|
|
3746
4045
|
if (origin === "background_review") return {
|
|
3747
4046
|
ok: false,
|
|
@@ -3775,9 +4074,10 @@ var SkillLibrary = class {
|
|
|
3775
4074
|
}
|
|
3776
4075
|
async create(name, content, origin = "foreground") {
|
|
3777
4076
|
const normalized = name.trim();
|
|
3778
|
-
|
|
4077
|
+
const bad = this.badName(normalized);
|
|
4078
|
+
if (bad) return {
|
|
3779
4079
|
ok: false,
|
|
3780
|
-
message:
|
|
4080
|
+
message: bad
|
|
3781
4081
|
};
|
|
3782
4082
|
const validation = validateFrontmatter(content, normalized, this.limits);
|
|
3783
4083
|
if (validation) return {
|
|
@@ -3797,7 +4097,7 @@ var SkillLibrary = class {
|
|
|
3797
4097
|
message: revalidated
|
|
3798
4098
|
};
|
|
3799
4099
|
}
|
|
3800
|
-
const threat =
|
|
4100
|
+
const threat = this.contentThreatBlock(finalContent);
|
|
3801
4101
|
if (threat) return {
|
|
3802
4102
|
ok: false,
|
|
3803
4103
|
message: threat
|
|
@@ -3807,6 +4107,11 @@ var SkillLibrary = class {
|
|
|
3807
4107
|
ok: false,
|
|
3808
4108
|
message: `Skill "${normalized}" already exists.`
|
|
3809
4109
|
};
|
|
4110
|
+
const protection = await this.writeProtection(normalized, origin);
|
|
4111
|
+
if (protection) return {
|
|
4112
|
+
ok: false,
|
|
4113
|
+
message: `Skill "${normalized}" is protected (${protection}).`
|
|
4114
|
+
};
|
|
3810
4115
|
const onDisk = finalContent.trimEnd() + "\n";
|
|
3811
4116
|
await this.io.writeText(join(dir, "SKILL.md"), onDisk);
|
|
3812
4117
|
if (origin !== "foreground") await this.io.writeText(markerPath(dir, "hermes-managed"), "");
|
|
@@ -3858,7 +4163,7 @@ var SkillLibrary = class {
|
|
|
3858
4163
|
message: revalidated
|
|
3859
4164
|
};
|
|
3860
4165
|
}
|
|
3861
|
-
const threat =
|
|
4166
|
+
const threat = this.contentThreatBlock(finalContent);
|
|
3862
4167
|
if (threat) return {
|
|
3863
4168
|
ok: false,
|
|
3864
4169
|
message: threat
|
|
@@ -4001,7 +4306,7 @@ var SkillLibrary = class {
|
|
|
4001
4306
|
},
|
|
4002
4307
|
write: null
|
|
4003
4308
|
};
|
|
4004
|
-
const threat =
|
|
4309
|
+
const threat = this.contentThreatBlock(writeContent);
|
|
4005
4310
|
if (threat) return {
|
|
4006
4311
|
result: {
|
|
4007
4312
|
ok: false,
|
|
@@ -4051,11 +4356,11 @@ var SkillLibrary = class {
|
|
|
4051
4356
|
};
|
|
4052
4357
|
const dir = this.dirOf(name);
|
|
4053
4358
|
const md = await this.io.readText(join(dir, "SKILL.md"));
|
|
4054
|
-
if (
|
|
4359
|
+
if (md === null) return {
|
|
4055
4360
|
ok: false,
|
|
4056
4361
|
message: `Skill "${name}" not found.`
|
|
4057
4362
|
};
|
|
4058
|
-
const protection = await this.deleteProtection(name, options
|
|
4363
|
+
const protection = await this.deleteProtection(name, options);
|
|
4059
4364
|
if (protection) return {
|
|
4060
4365
|
ok: false,
|
|
4061
4366
|
message: protection === "pinned" ? `Skill "${name}" is pinned and cannot be archived. Remove the \`.pinned\` marker in its directory, then retry.` : `Skill "${name}" is protected (${protection}).`
|
|
@@ -4152,10 +4457,13 @@ var SkillLibrary = class {
|
|
|
4152
4457
|
ok: false,
|
|
4153
4458
|
message: "Consolidation requires at least one distinct source skill."
|
|
4154
4459
|
};
|
|
4155
|
-
for (const name of [targetName, ...normalizedSources])
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4460
|
+
for (const name of [targetName, ...normalizedSources]) {
|
|
4461
|
+
const bad = this.badName(name);
|
|
4462
|
+
if (bad) return {
|
|
4463
|
+
ok: false,
|
|
4464
|
+
message: bad
|
|
4465
|
+
};
|
|
4466
|
+
}
|
|
4159
4467
|
const targetDir = this.dirOf(targetName);
|
|
4160
4468
|
const targetProtection = await this.writeProtection(targetName, origin);
|
|
4161
4469
|
if (targetProtection) return {
|
|
@@ -4235,7 +4543,14 @@ var SkillLibrary = class {
|
|
|
4235
4543
|
ok: false,
|
|
4236
4544
|
message: `Skill "${targetName}" not found.`
|
|
4237
4545
|
};
|
|
4238
|
-
const writes = [
|
|
4546
|
+
const writes = [];
|
|
4547
|
+
for (const reference of referenceWrites) {
|
|
4548
|
+
const base = (await this.io.readText(reference.target).catch(() => null))?.trimEnd() ?? "";
|
|
4549
|
+
writes.push({
|
|
4550
|
+
target: reference.target,
|
|
4551
|
+
content: base === "" ? reference.content : `${base}\n\n${reference.content}`
|
|
4552
|
+
});
|
|
4553
|
+
}
|
|
4239
4554
|
if (mode === "append") {
|
|
4240
4555
|
const merged = freshTargetMd.trimEnd() + parts.join("\n") + "\n";
|
|
4241
4556
|
const validation = validateFrontmatter(merged, targetName, this.limits);
|
|
@@ -4442,7 +4757,7 @@ var SkillLibrary = class {
|
|
|
4442
4757
|
ok: false,
|
|
4443
4758
|
message: `Write exceeds ${this.limits.maxSkillFileBytes} bytes: ${write.target}`
|
|
4444
4759
|
};
|
|
4445
|
-
const threat =
|
|
4760
|
+
const threat = this.contentThreatBlock(write.content);
|
|
4446
4761
|
if (threat) return {
|
|
4447
4762
|
ok: false,
|
|
4448
4763
|
message: threat
|
|
@@ -4586,7 +4901,7 @@ var SkillLibrary = class {
|
|
|
4586
4901
|
ok: false,
|
|
4587
4902
|
message: `Support file exceeds ${this.limits.maxSkillFileBytes} bytes.`
|
|
4588
4903
|
};
|
|
4589
|
-
const threat =
|
|
4904
|
+
const threat = this.contentThreatBlock(content);
|
|
4590
4905
|
if (threat) return {
|
|
4591
4906
|
ok: false,
|
|
4592
4907
|
message: threat
|
|
@@ -4851,26 +5166,4 @@ var SkillLibrary = class {
|
|
|
4851
5166
|
}
|
|
4852
5167
|
};
|
|
4853
5168
|
//#endregion
|
|
4854
|
-
|
|
4855
|
-
/**
|
|
4856
|
-
* Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
|
|
4857
|
-
* state (reports, activity store, feedback file, state-domain data).
|
|
4858
|
-
*/
|
|
4859
|
-
/**
|
|
4860
|
-
* DSH home root: `$DSH_HOME` or `~/.dsh`. Single source of the empty-string
|
|
4861
|
-
* fallback — an EMPTY or WHITESPACE-ONLY DSH_HOME resolves to the default
|
|
4862
|
-
* home, never to a CWD-relative path (0.3.19 W1.3, 0.3.22 F-207);
|
|
4863
|
-
* V8-06 (0.3.47) extends the guard to whitespace (upstream home-paths:
|
|
4864
|
-
* `trim().length > 0` is the adoption test — `DSH_HOME=" "` must not produce
|
|
4865
|
-
* a sidecar under a relative "." path).
|
|
4866
|
-
*/
|
|
4867
|
-
function evolutionRoot(env = process.env) {
|
|
4868
|
-
return env.DSH_HOME?.trim() ? env.DSH_HOME : join(homedir(), ".dsh");
|
|
4869
|
-
}
|
|
4870
|
-
/** Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
|
|
4871
|
-
* state (reports, activity store, feedback file, state-domain data). */
|
|
4872
|
-
function evolutionHome(env = process.env) {
|
|
4873
|
-
return join(evolutionRoot(env), "evolution");
|
|
4874
|
-
}
|
|
4875
|
-
//#endregion
|
|
4876
|
-
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, PATTERN_OVERLAP, 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, clampedNumber, 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, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, pendingSelfCleanup, 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 };
|
|
5169
|
+
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_OUTPUT_INSTRUCTION, 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, PATTERN_OVERLAP, 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, THREAT_EXEMPT_HINT, advanceReview, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, clampedNumber, 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, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, pendingSelfCleanup, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renameWithRetry, renderCuratorReportMarkdown, resolveOrigins, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, verifyPromptBundle, writeDurableTmp, yamlPlainScalarNeedsQuotes };
|