@lmzhen/dsh-evolution-core 0.3.51 → 0.3.53
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 +419 -83
- package/lib/types/evolution-events.d.ts +0 -17
- package/lib/types/index.d.ts +5 -3
- package/lib/types/io.d.ts +34 -5
- package/lib/types/memory-store.d.ts +13 -0
- package/lib/types/preset-composition.d.ts +8 -0
- 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);
|
|
@@ -1613,8 +1745,16 @@ function buildLearnPrompt(userRequest) {
|
|
|
1613
1745
|
//#endregion
|
|
1614
1746
|
//#region lib/types/state-store.js
|
|
1615
1747
|
/**
|
|
1616
|
-
* Evolution home path
|
|
1617
|
-
* state (reports, activity store, feedback file,
|
|
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.
|
|
1618
1758
|
*/
|
|
1619
1759
|
/**
|
|
1620
1760
|
* DSH home root: `$DSH_HOME` or `~/.dsh`. Single source of the empty-string
|
|
@@ -1623,9 +1763,16 @@ function buildLearnPrompt(userRequest) {
|
|
|
1623
1763
|
* V8-06 (0.3.47) extends the guard to whitespace (upstream home-paths:
|
|
1624
1764
|
* `trim().length > 0` is the adoption test — `DSH_HOME=" "` must not produce
|
|
1625
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.
|
|
1626
1772
|
*/
|
|
1627
1773
|
function evolutionRoot(env = process.env) {
|
|
1628
|
-
|
|
1774
|
+
const home = env.DSH_HOME?.trim();
|
|
1775
|
+
return home ? home : join(homedir(), ".dsh");
|
|
1629
1776
|
}
|
|
1630
1777
|
/** Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
|
|
1631
1778
|
* state (reports, activity store, feedback file, state-domain data). */
|
|
@@ -1943,6 +2090,15 @@ function scanContentThreats(text, maxScanChars = 65536, options = NO_SCAN_OPTION
|
|
|
1943
2090
|
if (!blocked) return null;
|
|
1944
2091
|
return `Blocked by security scan (${findings[0]?.label ?? "unknown"}). This content appears to contain potentially malicious instructions.`;
|
|
1945
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.";
|
|
1946
2102
|
//#endregion
|
|
1947
2103
|
//#region lib/types/memory-store.js
|
|
1948
2104
|
/**
|
|
@@ -2012,6 +2168,8 @@ var MemoryStore = class {
|
|
|
2012
2168
|
root;
|
|
2013
2169
|
maxFailures;
|
|
2014
2170
|
io;
|
|
2171
|
+
/** V10-03 (P2-18): see MemoryStoreOptions.threatExemptLabels. */
|
|
2172
|
+
threatExemptLabels;
|
|
2015
2173
|
/** V6-16 (0.3.37): same-process RMW serialization (the SkillLibrary queue) —
|
|
2016
2174
|
* on a backend WITHOUT a transact lock two concurrent callers compute on the
|
|
2017
2175
|
* same old content and the last rename wins, silently dropping one op's
|
|
@@ -2027,6 +2185,18 @@ var MemoryStore = class {
|
|
|
2027
2185
|
this.addDatePrefix = options.addDatePrefix ?? false;
|
|
2028
2186
|
this.root = options.root ?? memoryRoot();
|
|
2029
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;
|
|
2030
2200
|
}
|
|
2031
2201
|
limitFor(target) {
|
|
2032
2202
|
return target === "memory" ? this.memoryLimit : this.userLimit;
|
|
@@ -2151,7 +2321,13 @@ var MemoryStore = class {
|
|
|
2151
2321
|
outcome = core.result;
|
|
2152
2322
|
return core.write ?? current ?? null;
|
|
2153
2323
|
});
|
|
2154
|
-
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
|
+
};
|
|
2155
2331
|
}
|
|
2156
2332
|
/**
|
|
2157
2333
|
* Single-entry add inside the transaction: shared checks (oversized,
|
|
@@ -2177,7 +2353,7 @@ var MemoryStore = class {
|
|
|
2177
2353
|
write: null
|
|
2178
2354
|
};
|
|
2179
2355
|
}
|
|
2180
|
-
const threat =
|
|
2356
|
+
const threat = this.memoryThreatBlock(content);
|
|
2181
2357
|
if (threat) return {
|
|
2182
2358
|
result: {
|
|
2183
2359
|
ok: false,
|
|
@@ -2188,7 +2364,8 @@ var MemoryStore = class {
|
|
|
2188
2364
|
},
|
|
2189
2365
|
write: null
|
|
2190
2366
|
};
|
|
2191
|
-
|
|
2367
|
+
const prefixed = this.addDatePrefix ? `## ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}\n${content}` : content;
|
|
2368
|
+
if (hasEntryDelimiter(prefixed)) return {
|
|
2192
2369
|
result: {
|
|
2193
2370
|
ok: false,
|
|
2194
2371
|
message: "Fact contains the entry delimiter (§) and would split into multiple entries; rewrite it as separate facts.",
|
|
@@ -2212,7 +2389,7 @@ var MemoryStore = class {
|
|
|
2212
2389
|
write: null
|
|
2213
2390
|
};
|
|
2214
2391
|
}
|
|
2215
|
-
const next = [...entries,
|
|
2392
|
+
const next = [...entries, prefixed];
|
|
2216
2393
|
const total = next.join(ENTRY_DELIMITER).length;
|
|
2217
2394
|
const addLimit = this.limitFor(target);
|
|
2218
2395
|
if (addLimit > 0 && total > addLimit) return {
|
|
@@ -2259,7 +2436,13 @@ var MemoryStore = class {
|
|
|
2259
2436
|
outcome = core.result;
|
|
2260
2437
|
return core.write ?? current ?? null;
|
|
2261
2438
|
});
|
|
2262
|
-
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
|
+
};
|
|
2263
2446
|
}
|
|
2264
2447
|
/** Batch RMW inside the transaction. `write: null` = failure/no-op, disk untouched. */
|
|
2265
2448
|
async applyBatchCore(target, operations, raw) {
|
|
@@ -2278,6 +2461,7 @@ var MemoryStore = class {
|
|
|
2278
2461
|
}
|
|
2279
2462
|
const entries = [...new Set(normalizeEntries(raw))];
|
|
2280
2463
|
const working = [...entries];
|
|
2464
|
+
const datePrefix = this.addDatePrefix ? `## ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}\n` : "";
|
|
2281
2465
|
for (const [index, op] of operations.entries()) {
|
|
2282
2466
|
const position = index + 1;
|
|
2283
2467
|
if (op.action === "add") {
|
|
@@ -2292,7 +2476,7 @@ var MemoryStore = class {
|
|
|
2292
2476
|
},
|
|
2293
2477
|
write: null
|
|
2294
2478
|
};
|
|
2295
|
-
const threat =
|
|
2479
|
+
const threat = this.memoryThreatBlock(body);
|
|
2296
2480
|
if (threat) return {
|
|
2297
2481
|
result: {
|
|
2298
2482
|
ok: false,
|
|
@@ -2303,7 +2487,7 @@ var MemoryStore = class {
|
|
|
2303
2487
|
},
|
|
2304
2488
|
write: null
|
|
2305
2489
|
};
|
|
2306
|
-
const entryBody =
|
|
2490
|
+
const entryBody = `${datePrefix}${body}`;
|
|
2307
2491
|
if (hasEntryDelimiter(entryBody)) return {
|
|
2308
2492
|
result: {
|
|
2309
2493
|
ok: false,
|
|
@@ -2371,7 +2555,7 @@ var MemoryStore = class {
|
|
|
2371
2555
|
},
|
|
2372
2556
|
write: null
|
|
2373
2557
|
};
|
|
2374
|
-
const threat =
|
|
2558
|
+
const threat = this.memoryThreatBlock(body);
|
|
2375
2559
|
if (threat) return {
|
|
2376
2560
|
result: {
|
|
2377
2561
|
ok: false,
|
|
@@ -2382,7 +2566,8 @@ var MemoryStore = class {
|
|
|
2382
2566
|
},
|
|
2383
2567
|
write: null
|
|
2384
2568
|
};
|
|
2385
|
-
|
|
2569
|
+
const entryBody = `${datePrefix}${body}`;
|
|
2570
|
+
if (hasEntryDelimiter(entryBody)) return {
|
|
2386
2571
|
result: {
|
|
2387
2572
|
ok: false,
|
|
2388
2573
|
message: `Operation ${position} (replace): Fact contains the entry delimiter (§) and would split into multiple entries; rewrite it as separate facts.${previewEntries(entries)}`,
|
|
@@ -2392,7 +2577,7 @@ var MemoryStore = class {
|
|
|
2392
2577
|
},
|
|
2393
2578
|
write: null
|
|
2394
2579
|
};
|
|
2395
|
-
working[matchIndex] =
|
|
2580
|
+
working[matchIndex] = entryBody;
|
|
2396
2581
|
}
|
|
2397
2582
|
}
|
|
2398
2583
|
const total = working.join(ENTRY_DELIMITER).length;
|
|
@@ -2431,13 +2616,13 @@ var MemoryStore = class {
|
|
|
2431
2616
|
parts.push(`## ${label} — file skipped: ${oversized.size} bytes (limit ${oversized.limit * READ_GUARD_FACTOR}); not read`);
|
|
2432
2617
|
continue;
|
|
2433
2618
|
}
|
|
2434
|
-
const safe = entries.filter((entry) => !scanMemoryThreats(entry));
|
|
2619
|
+
const safe = entries.filter((entry) => !scanMemoryThreats(entry, void 0, this.threatScanOptions()));
|
|
2435
2620
|
if (safe.length > 0) {
|
|
2436
2621
|
const body = safe.join(ENTRY_DELIMITER);
|
|
2437
2622
|
const limit = this.limitFor(target);
|
|
2438
|
-
const pct = limit > 0 ? Math.min(100, Math.floor(body.length * 100 / limit)) : 0;
|
|
2439
2623
|
const note = safe.length === entries.length ? "" : ` (${entries.length - safe.length} threat-matched entries filtered)`;
|
|
2440
|
-
|
|
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}`);
|
|
2441
2626
|
}
|
|
2442
2627
|
}
|
|
2443
2628
|
return parts.join("\n\n");
|
|
@@ -2507,6 +2692,7 @@ async function recordMutation(root, io, record, cap = 500) {
|
|
|
2507
2692
|
if (current !== null) try {
|
|
2508
2693
|
JSON.parse(current);
|
|
2509
2694
|
} catch {
|
|
2695
|
+
console.warn(`mutation audit record dropped: ${mutationsFile(root)} is malformed and was not overwritten`);
|
|
2510
2696
|
return current;
|
|
2511
2697
|
}
|
|
2512
2698
|
const existing = parseMutationRecords(current);
|
|
@@ -2533,6 +2719,14 @@ async function recordMutation(root, io, record, cap = 500) {
|
|
|
2533
2719
|
* standard row id fails loud by default, and `DSH_EVOLUTION_ALLOW_ROW_COLLISIONS=1`
|
|
2534
2720
|
* downgrades it to a warning that keeps both (the row mounts twice).
|
|
2535
2721
|
*
|
|
2722
|
+
* V10-14 / 0.3.53 (P1-2): BOTH composers now inject the Hermes 60-char catalog
|
|
2723
|
+
* cap onto the standard-sourced `- id: tool-skill` row of the composed preset
|
|
2724
|
+
* (see injectCatalogDescriptionCap) — the session-visible tool-skill instance
|
|
2725
|
+
* mounts in the preset's own standing scope, where no profile-root patch can
|
|
2726
|
+
* reach it. 0.3.53 moved the injection INTO the composer so
|
|
2727
|
+
* `/evolution preset install` (the npm user's only preset path) gets it too;
|
|
2728
|
+
* install-layered applies the byte-identical rule, pinned by installer.spec.
|
|
2729
|
+
*
|
|
2536
2730
|
* Row ids are read from `- id:` lines; an id present in both fragments would
|
|
2537
2731
|
* mount twice and could shadow the platform row, so it fails loud.
|
|
2538
2732
|
* @param standardComposition - the runtime `standard` preset composition.
|
|
@@ -2544,13 +2738,53 @@ function composePresetComposition(standardComposition, deltaComposition) {
|
|
|
2544
2738
|
const collisions = [...compositionRowIds(deltaComposition)].filter((id) => standardIds.has(id)).sort();
|
|
2545
2739
|
if (collisions.length > 0 && process.env.DSH_EVOLUTION_ALLOW_ROW_COLLISIONS !== "1") throw new Error(`evolution preset composition: delta rows collide with runtime standard rows: ${collisions.join(", ")}`);
|
|
2546
2740
|
if (collisions.length > 0) console.warn(`evolution preset composition: warning — delta rows collide with standard rows (${collisions.join(", ")}); keeping both (DSH_EVOLUTION_ALLOW_ROW_COLLISIONS=1)`);
|
|
2547
|
-
return `${standardComposition.replace(/\s+$/, "")}\n\n${deltaComposition.trim()}\n
|
|
2741
|
+
return injectCatalogDescriptionCap(`${standardComposition.replace(/\s+$/, "")}\n\n${deltaComposition.trim()}\n`);
|
|
2742
|
+
}
|
|
2743
|
+
/**
|
|
2744
|
+
* V10-14 (P1-2), 0.3.53: inject the Hermes 60-char catalog cap onto the
|
|
2745
|
+
* standard-sourced `- id: tool-skill` row of a composed preset.
|
|
2746
|
+
*
|
|
2747
|
+
* The session-visible `tool-skill` instance mounts in the agent preset's own
|
|
2748
|
+
* standing scope; a profile-root patch (evolution-host/cordis.patch.yml)
|
|
2749
|
+
* cannot reach it, so without this injection the catalog's read side runs the
|
|
2750
|
+
* platform default (500). Text-level rewrite in the same line-scan style as
|
|
2751
|
+
* compositionRowIds (no YAML library):
|
|
2752
|
+
* - idempotent: a tool-skill item that already carries a `config:` key is
|
|
2753
|
+
* left byte-identical, so re-running the installer never doubles the key;
|
|
2754
|
+
* - the injected block carries a marker comment so a diff of the generated
|
|
2755
|
+
* preset can tell composer-owned text from platform text;
|
|
2756
|
+
* - a composition WITHOUT a tool-skill row is returned unchanged with a
|
|
2757
|
+
* one-time warning (a renamed platform row must not brick the install,
|
|
2758
|
+
* but the missed cap must be observable).
|
|
2759
|
+
* install-layered.mjs ships the byte-identical `injectToolSkillCap`.
|
|
2760
|
+
*/
|
|
2761
|
+
function injectCatalogDescriptionCap(composition) {
|
|
2762
|
+
const lines = composition.split("\n");
|
|
2763
|
+
let found = false;
|
|
2764
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
2765
|
+
if (!/^- id:\s*tool-skill\s*$/.test(lines[i] ?? "")) continue;
|
|
2766
|
+
found = true;
|
|
2767
|
+
let end = i;
|
|
2768
|
+
let hasConfig = false;
|
|
2769
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
2770
|
+
const next = lines[j] ?? "";
|
|
2771
|
+
if (next.trim() === "") break;
|
|
2772
|
+
if (!/^\s/.test(next)) break;
|
|
2773
|
+
if (/^\s+config:(\s|$)/.test(next)) hasConfig = true;
|
|
2774
|
+
end = j;
|
|
2775
|
+
}
|
|
2776
|
+
if (hasConfig) continue;
|
|
2777
|
+
lines.splice(end + 1, 0, " # V10-14: Hermes 60-char catalog cap — injected by the preset composer (P1-2);", " # this preset-scope row is the session-visible instance and no profile", " # patch can reach it. Remove only to run the platform default (500).", " config:", " catalogDescriptionMaxLength: 60");
|
|
2778
|
+
i = end + 5;
|
|
2779
|
+
}
|
|
2780
|
+
if (!found) console.warn("evolution preset composition: warning — no `- id: tool-skill` row in the composed preset; the 60-char catalog cap was NOT injected (platform renamed the row? reconcile with the delta)");
|
|
2781
|
+
return lines.join("\n");
|
|
2548
2782
|
}
|
|
2549
2783
|
function compositionRowIds(composition) {
|
|
2550
2784
|
const ids = /* @__PURE__ */ new Set();
|
|
2551
2785
|
for (const line of composition.split("\n")) {
|
|
2552
|
-
const
|
|
2553
|
-
if (
|
|
2786
|
+
const id = /^- id:\s*(\S+)/.exec(line)?.[1];
|
|
2787
|
+
if (id) ids.add(id);
|
|
2554
2788
|
}
|
|
2555
2789
|
return ids;
|
|
2556
2790
|
}
|
|
@@ -2617,7 +2851,10 @@ function computeQualityScores(input) {
|
|
|
2617
2851
|
function normalize(content) {
|
|
2618
2852
|
return content.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2619
2853
|
}
|
|
2620
|
-
|
|
2854
|
+
/** C-26 (v10 audit): renamed from `contentHash` — mutations.ts exports a
|
|
2855
|
+
* contentHash of RAW bytes, this one hashes the NORMALIZED text for dedup
|
|
2856
|
+
* grouping. Same private helper, unambiguous name. */
|
|
2857
|
+
function normalizedHash(content) {
|
|
2621
2858
|
return createHash("sha256").update(normalize(content)).digest("hex");
|
|
2622
2859
|
}
|
|
2623
2860
|
function tokenize(content) {
|
|
@@ -2639,7 +2876,7 @@ function computeDedupGroups(input) {
|
|
|
2639
2876
|
const names = [...input.contents.keys()];
|
|
2640
2877
|
const hashes = /* @__PURE__ */ new Map();
|
|
2641
2878
|
for (const name of names) {
|
|
2642
|
-
const hash =
|
|
2879
|
+
const hash = normalizedHash(input.contents.get(name) ?? "");
|
|
2643
2880
|
const bucket = hashes.get(hash);
|
|
2644
2881
|
if (bucket) bucket.push(name);
|
|
2645
2882
|
else hashes.set(hash, [name]);
|
|
@@ -2771,6 +3008,12 @@ const DEFAULT_HEALTH_THRESHOLDS = {
|
|
|
2771
3008
|
* (`defaced`, `feedback`) are not counted as commit shas; the ISO branch
|
|
2772
3009
|
* accepts a UTC `Z`, a numeric UTC offset (`+08:00`), or no timezone at all —
|
|
2773
3010
|
* non-UTC timestamps used to escape detection (log-like content missed).
|
|
3011
|
+
*
|
|
3012
|
+
* Known boundary, recorded not fixed (C-22, v10 audit): the hex branch also
|
|
3013
|
+
* matches 7-40 char PURE-DIGIT strings (order numbers etc.), so stamp density
|
|
3014
|
+
* can be over-reported for such content. The misjudgment direction is "extra
|
|
3015
|
+
* stamp hits" only — never a miss — and no real incident exists; revisit only
|
|
3016
|
+
* if curator decisions are actually distorted (v10 deferral ledger).
|
|
2774
3017
|
*/
|
|
2775
3018
|
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");
|
|
2776
3019
|
/**
|
|
@@ -2815,7 +3058,7 @@ function assessStructureHealth(snapshot, thresholds = DEFAULT_HEALTH_THRESHOLDS)
|
|
|
2815
3058
|
* Deterministic review signal gate.
|
|
2816
3059
|
*
|
|
2817
3060
|
* Scans a DSH session event log for durable learning signals before any LLM
|
|
2818
|
-
* is spent. `turn/end` calls `
|
|
3061
|
+
* is spent. `turn/end` calls `observeEvent`; the returned review kind is
|
|
2819
3062
|
* accumulated until a configured interval fires.
|
|
2820
3063
|
*/
|
|
2821
3064
|
const CORRECTION_PATTERNS = [
|
|
@@ -3035,7 +3278,7 @@ function computeDriftSignals(snapshots) {
|
|
|
3035
3278
|
const narrow = narrowNameMatches(snapshot.name);
|
|
3036
3279
|
signals.push(narrow.length === 0 ? sig("narrow_name", "pass", "none") : sig("narrow_name", "over", narrow.join(", "), void 0, `name=${snapshot.name}`));
|
|
3037
3280
|
const description = snapshot.description;
|
|
3038
|
-
signals.push(description === void 0 ? sig("description_chars", "unknown", "missing",
|
|
3281
|
+
signals.push(description === void 0 ? sig("description_chars", "unknown", "missing", `60`) : sig("description_chars", description.length > 60 ? "over" : "pass", `${description.length}`, `60`));
|
|
3039
3282
|
const quality = snapshot.quality;
|
|
3040
3283
|
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}`));
|
|
3041
3284
|
return {
|
|
@@ -3240,6 +3483,13 @@ function frontmatterYamlUnsafeValues(content) {
|
|
|
3240
3483
|
* rewritten block no longer parses, or a rewritten value's parsed content
|
|
3241
3484
|
* differs from the original, the rewrite is rolled back and reported in
|
|
3242
3485
|
* `issues` (fail-loud, never a silent value corruption — P3-4).
|
|
3486
|
+
*
|
|
3487
|
+
* V10-02 (P2-3): the rewrite decision is PER LINE — each entry parses its own
|
|
3488
|
+
* value, so a duplicated key can never route one entry's unsafe value into a
|
|
3489
|
+
* different line's rewrite (the old key→Map lookup rewrote the FIRST (safe)
|
|
3490
|
+
* line with the SECOND line's quoted value, and the last-wins YAML reader
|
|
3491
|
+
* masked the damage). A duplicated key is itself invalid input and is
|
|
3492
|
+
* reported in `issues` (the write path refuses) instead of being rewritten.
|
|
3243
3493
|
*/
|
|
3244
3494
|
function normalizeFrontmatter(content) {
|
|
3245
3495
|
const block = frontmatterBlock(content);
|
|
@@ -3250,10 +3500,10 @@ function normalizeFrontmatter(content) {
|
|
|
3250
3500
|
issues: []
|
|
3251
3501
|
};
|
|
3252
3502
|
const { lines, end, nl } = block;
|
|
3253
|
-
const unsafe = new Map(frontmatterYamlUnsafeValues(content).map((entry) => [entry.key, entry.value]));
|
|
3254
|
-
const originalValues = new Map(unsafe);
|
|
3255
3503
|
const fields = [];
|
|
3256
3504
|
const issues = [];
|
|
3505
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3506
|
+
const originalValues = /* @__PURE__ */ new Map();
|
|
3257
3507
|
let changed = false;
|
|
3258
3508
|
for (let i = 1; i < end; i++) {
|
|
3259
3509
|
const line = lines[i];
|
|
@@ -3261,16 +3511,29 @@ function normalizeFrontmatter(content) {
|
|
|
3261
3511
|
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
|
|
3262
3512
|
if (!match) continue;
|
|
3263
3513
|
const key = match[1];
|
|
3264
|
-
|
|
3265
|
-
|
|
3514
|
+
if (key === void 0) continue;
|
|
3515
|
+
const value = (match[2] ?? "").trim();
|
|
3516
|
+
if (seen.has(key)) {
|
|
3517
|
+
issues.push(`${key}: duplicate frontmatter key — remove the repeated entry and retry`);
|
|
3518
|
+
continue;
|
|
3519
|
+
}
|
|
3520
|
+
seen.add(key);
|
|
3521
|
+
if (!yamlPlainScalarNeedsQuotes(value)) continue;
|
|
3266
3522
|
if (/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/.test(value)) {
|
|
3267
3523
|
issues.push(`${key}: value contains control characters — clean them manually`);
|
|
3268
3524
|
continue;
|
|
3269
3525
|
}
|
|
3270
3526
|
lines[i] = `${key}: ${value.includes("\"") || value.includes("\\") ? `'${value.replace(/'/g, "''")}'` : `"${value}"`}`;
|
|
3527
|
+
originalValues.set(key, value);
|
|
3271
3528
|
fields.push(key);
|
|
3272
3529
|
changed = true;
|
|
3273
3530
|
}
|
|
3531
|
+
if (issues.length > 0) return {
|
|
3532
|
+
content,
|
|
3533
|
+
changed: false,
|
|
3534
|
+
fields: [],
|
|
3535
|
+
issues
|
|
3536
|
+
};
|
|
3274
3537
|
if (!changed) return {
|
|
3275
3538
|
content,
|
|
3276
3539
|
changed: false,
|
|
@@ -3320,7 +3583,7 @@ function validateFrontmatter(content, expectedName, limits = DEFAULT_SKILL_LIMIT
|
|
|
3320
3583
|
const parsed = parseFrontmatter(content);
|
|
3321
3584
|
if (!parsed) return "SKILL.md must start and end with YAML frontmatter and include a body.";
|
|
3322
3585
|
if (!parsed.frontmatter.name) return "Frontmatter must include a name field.";
|
|
3323
|
-
if (
|
|
3586
|
+
if (!SKILL_NAME_RE.test(parsed.frontmatter.name)) return `Invalid skill name "${parsed.frontmatter.name}" — use lowercase letters, digits, and hyphens.`;
|
|
3324
3587
|
if (parsed.frontmatter.name.length > limits.maxNameLength) return `Skill name exceeds ${limits.maxNameLength} characters.`;
|
|
3325
3588
|
if (expectedName && parsed.frontmatter.name !== expectedName) return `Frontmatter name "${parsed.frontmatter.name}" does not match target skill "${expectedName}".`;
|
|
3326
3589
|
if (!parsed.frontmatter.description) return "Frontmatter must include a description field.";
|
|
@@ -3358,12 +3621,51 @@ async function listNames(root, io) {
|
|
|
3358
3621
|
}
|
|
3359
3622
|
return names.sort();
|
|
3360
3623
|
}
|
|
3624
|
+
/** C-18: support file names are restricted to the
|
|
3625
|
+
* RESTRUCTURE_TARGET_RE character class (leading `[a-z0-9]`, then
|
|
3626
|
+
* `[a-z0-9._-]`) — drive-colon / odd-character / uppercase names can no
|
|
3627
|
+
* longer reach the filesystem through writeSupportFile / patch /
|
|
3628
|
+
* removeSupportFile. */
|
|
3629
|
+
const SUPPORT_FILE_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
3630
|
+
/** C-18: win32 reserves these stems with ANY extension (`nul.md` hits the
|
|
3631
|
+
* NUL device), and they are fully inside the charset above — so the reserved
|
|
3632
|
+
* set is checked on the first-dot prefix as well; the charset close alone
|
|
3633
|
+
* cannot refuse them. */
|
|
3634
|
+
const WIN32_RESERVED_DEVICE_NAMES = new Set([
|
|
3635
|
+
"con",
|
|
3636
|
+
"prn",
|
|
3637
|
+
"aux",
|
|
3638
|
+
"nul",
|
|
3639
|
+
"com1",
|
|
3640
|
+
"com2",
|
|
3641
|
+
"com3",
|
|
3642
|
+
"com4",
|
|
3643
|
+
"com5",
|
|
3644
|
+
"com6",
|
|
3645
|
+
"com7",
|
|
3646
|
+
"com8",
|
|
3647
|
+
"com9",
|
|
3648
|
+
"lpt1",
|
|
3649
|
+
"lpt2",
|
|
3650
|
+
"lpt3",
|
|
3651
|
+
"lpt4",
|
|
3652
|
+
"lpt5",
|
|
3653
|
+
"lpt6",
|
|
3654
|
+
"lpt7",
|
|
3655
|
+
"lpt8",
|
|
3656
|
+
"lpt9"
|
|
3657
|
+
]);
|
|
3361
3658
|
function validateSupportPath(filePath) {
|
|
3362
3659
|
const normalized = filePath.replace(/\\/g, "/");
|
|
3363
3660
|
if (normalized.includes("..")) return "Path traversal is not allowed.";
|
|
3364
3661
|
const parts = normalized.split("/").filter(Boolean);
|
|
3365
3662
|
if (parts.length === 0 || !SUPPORT_DIRS.includes(parts[0])) return `file_path must be under one of: ${SUPPORT_DIRS.join(", ")}.`;
|
|
3366
3663
|
if (parts.length < 2) return "Provide a file name, not just a directory.";
|
|
3664
|
+
for (const part of parts.slice(1)) {
|
|
3665
|
+
if (!SUPPORT_FILE_NAME_RE.test(part)) return `Unsupported file name "${part}" — use lowercase letters, digits, dots, hyphens, and underscores (leading letter or digit).`;
|
|
3666
|
+
const stem = part.split(".")[0]?.toLowerCase() ?? "";
|
|
3667
|
+
if (WIN32_RESERVED_DEVICE_NAMES.has(stem)) return `Unsupported file name "${part}" — a Windows reserved device name.`;
|
|
3668
|
+
}
|
|
3367
3669
|
return null;
|
|
3368
3670
|
}
|
|
3369
3671
|
/**
|
|
@@ -3537,11 +3839,15 @@ var SkillLibrary = class {
|
|
|
3537
3839
|
* one skill never interleave their read-modify-write (the cross-process layer
|
|
3538
3840
|
* is the IO backend's transact lock; this chain is the second layer). */
|
|
3539
3841
|
serial;
|
|
3540
|
-
|
|
3842
|
+
/** V10-03 (P2-18): see the constructor's threatExemptLabels. Empty by
|
|
3843
|
+
* default — the strict ANY-hit-blocks policy is unchanged. */
|
|
3844
|
+
threatExemptLabels;
|
|
3845
|
+
constructor(root = skillsRoot(), io = nodeEvolutionIo(), limits = DEFAULT_SKILL_LIMITS, onMutation, transact, threatExemptLabels) {
|
|
3541
3846
|
this.root = root;
|
|
3542
3847
|
this.io = io;
|
|
3543
3848
|
this.limits = limits;
|
|
3544
3849
|
this.onMutation = onMutation;
|
|
3850
|
+
this.threatExemptLabels = threatExemptLabels ?? [];
|
|
3545
3851
|
this.transact = transact ?? (io.transact ? (ioLike, path, task) => {
|
|
3546
3852
|
const t = ioLike.transact;
|
|
3547
3853
|
return t ? t(path, task) : transactIo(ioLike, path, task);
|
|
@@ -3585,12 +3891,24 @@ var SkillLibrary = class {
|
|
|
3585
3891
|
this.onMutation?.(event);
|
|
3586
3892
|
} catch {}
|
|
3587
3893
|
}
|
|
3894
|
+
/** V10-03 (P2-18): ScanOptions shared by every write-path threat check —
|
|
3895
|
+
* the constructor's exempt labels, empty by default (behavior unchanged). */
|
|
3896
|
+
threatScanOptions() {
|
|
3897
|
+
return this.threatExemptLabels.length > 0 ? { excludeLabels: this.threatExemptLabels } : {};
|
|
3898
|
+
}
|
|
3899
|
+
/** V10-03 (P2-18): the strict-scan write gate. A block message names the hit
|
|
3900
|
+
* label (scanContentThreats already embeds it) plus the self-heal hint, so a
|
|
3901
|
+
* false-positive rewrite direction is actionable instead of a dead end. */
|
|
3902
|
+
contentThreatBlock(content) {
|
|
3903
|
+
const threat = scanContentThreats(content, void 0, this.threatScanOptions());
|
|
3904
|
+
return threat === null ? null : threat + THREAT_EXEMPT_HINT;
|
|
3905
|
+
}
|
|
3588
3906
|
async list() {
|
|
3589
3907
|
const summaries = [];
|
|
3590
3908
|
for (const name of await listNames(this.root, this.io)) {
|
|
3591
3909
|
const dir = this.dirOf(name);
|
|
3592
3910
|
const md = await this.io.readText(join(dir, "SKILL.md"));
|
|
3593
|
-
if (
|
|
3911
|
+
if (md === null) continue;
|
|
3594
3912
|
const parsed = parseFrontmatter(md);
|
|
3595
3913
|
let entries = [];
|
|
3596
3914
|
try {
|
|
@@ -3598,9 +3916,10 @@ var SkillLibrary = class {
|
|
|
3598
3916
|
} catch {}
|
|
3599
3917
|
const has = (marker) => entries.includes(markerEntryName(marker));
|
|
3600
3918
|
const protectedBy = has("bundled") ? "bundled" : has("hub-installed") ? "hub-installed" : has("pinned") ? "pinned" : null;
|
|
3919
|
+
const parsedDescription = parsed?.frontmatter.description;
|
|
3601
3920
|
summaries.push({
|
|
3602
3921
|
name,
|
|
3603
|
-
description:
|
|
3922
|
+
description: typeof parsedDescription === "string" ? parsedDescription : "",
|
|
3604
3923
|
path: dir,
|
|
3605
3924
|
protectedBy,
|
|
3606
3925
|
managed: has("hermes-managed"),
|
|
@@ -3766,9 +4085,10 @@ var SkillLibrary = class {
|
|
|
3766
4085
|
*/
|
|
3767
4086
|
async setPinned(name, pinned, origin = "foreground") {
|
|
3768
4087
|
const normalized = name.trim();
|
|
3769
|
-
|
|
4088
|
+
const bad = this.badName(normalized);
|
|
4089
|
+
if (bad) return {
|
|
3770
4090
|
ok: false,
|
|
3771
|
-
message:
|
|
4091
|
+
message: bad
|
|
3772
4092
|
};
|
|
3773
4093
|
if (origin === "background_review") return {
|
|
3774
4094
|
ok: false,
|
|
@@ -3802,9 +4122,10 @@ var SkillLibrary = class {
|
|
|
3802
4122
|
}
|
|
3803
4123
|
async create(name, content, origin = "foreground") {
|
|
3804
4124
|
const normalized = name.trim();
|
|
3805
|
-
|
|
4125
|
+
const bad = this.badName(normalized);
|
|
4126
|
+
if (bad) return {
|
|
3806
4127
|
ok: false,
|
|
3807
|
-
message:
|
|
4128
|
+
message: bad
|
|
3808
4129
|
};
|
|
3809
4130
|
const validation = validateFrontmatter(content, normalized, this.limits);
|
|
3810
4131
|
if (validation) return {
|
|
@@ -3824,7 +4145,7 @@ var SkillLibrary = class {
|
|
|
3824
4145
|
message: revalidated
|
|
3825
4146
|
};
|
|
3826
4147
|
}
|
|
3827
|
-
const threat =
|
|
4148
|
+
const threat = this.contentThreatBlock(finalContent);
|
|
3828
4149
|
if (threat) return {
|
|
3829
4150
|
ok: false,
|
|
3830
4151
|
message: threat
|
|
@@ -3834,6 +4155,11 @@ var SkillLibrary = class {
|
|
|
3834
4155
|
ok: false,
|
|
3835
4156
|
message: `Skill "${normalized}" already exists.`
|
|
3836
4157
|
};
|
|
4158
|
+
const protection = await this.writeProtection(normalized, origin);
|
|
4159
|
+
if (protection) return {
|
|
4160
|
+
ok: false,
|
|
4161
|
+
message: `Skill "${normalized}" is protected (${protection}).`
|
|
4162
|
+
};
|
|
3837
4163
|
const onDisk = finalContent.trimEnd() + "\n";
|
|
3838
4164
|
await this.io.writeText(join(dir, "SKILL.md"), onDisk);
|
|
3839
4165
|
if (origin !== "foreground") await this.io.writeText(markerPath(dir, "hermes-managed"), "");
|
|
@@ -3885,7 +4211,7 @@ var SkillLibrary = class {
|
|
|
3885
4211
|
message: revalidated
|
|
3886
4212
|
};
|
|
3887
4213
|
}
|
|
3888
|
-
const threat =
|
|
4214
|
+
const threat = this.contentThreatBlock(finalContent);
|
|
3889
4215
|
if (threat) return {
|
|
3890
4216
|
ok: false,
|
|
3891
4217
|
message: threat
|
|
@@ -4028,7 +4354,7 @@ var SkillLibrary = class {
|
|
|
4028
4354
|
},
|
|
4029
4355
|
write: null
|
|
4030
4356
|
};
|
|
4031
|
-
const threat =
|
|
4357
|
+
const threat = this.contentThreatBlock(writeContent);
|
|
4032
4358
|
if (threat) return {
|
|
4033
4359
|
result: {
|
|
4034
4360
|
ok: false,
|
|
@@ -4078,11 +4404,11 @@ var SkillLibrary = class {
|
|
|
4078
4404
|
};
|
|
4079
4405
|
const dir = this.dirOf(name);
|
|
4080
4406
|
const md = await this.io.readText(join(dir, "SKILL.md"));
|
|
4081
|
-
if (
|
|
4407
|
+
if (md === null) return {
|
|
4082
4408
|
ok: false,
|
|
4083
4409
|
message: `Skill "${name}" not found.`
|
|
4084
4410
|
};
|
|
4085
|
-
const protection = await this.deleteProtection(name, options
|
|
4411
|
+
const protection = await this.deleteProtection(name, options);
|
|
4086
4412
|
if (protection) return {
|
|
4087
4413
|
ok: false,
|
|
4088
4414
|
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}).`
|
|
@@ -4179,10 +4505,13 @@ var SkillLibrary = class {
|
|
|
4179
4505
|
ok: false,
|
|
4180
4506
|
message: "Consolidation requires at least one distinct source skill."
|
|
4181
4507
|
};
|
|
4182
|
-
for (const name of [targetName, ...normalizedSources])
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4508
|
+
for (const name of [targetName, ...normalizedSources]) {
|
|
4509
|
+
const bad = this.badName(name);
|
|
4510
|
+
if (bad) return {
|
|
4511
|
+
ok: false,
|
|
4512
|
+
message: bad
|
|
4513
|
+
};
|
|
4514
|
+
}
|
|
4186
4515
|
const targetDir = this.dirOf(targetName);
|
|
4187
4516
|
const targetProtection = await this.writeProtection(targetName, origin);
|
|
4188
4517
|
if (targetProtection) return {
|
|
@@ -4262,7 +4591,14 @@ var SkillLibrary = class {
|
|
|
4262
4591
|
ok: false,
|
|
4263
4592
|
message: `Skill "${targetName}" not found.`
|
|
4264
4593
|
};
|
|
4265
|
-
const writes = [
|
|
4594
|
+
const writes = [];
|
|
4595
|
+
for (const reference of referenceWrites) {
|
|
4596
|
+
const base = (await this.io.readText(reference.target).catch(() => null))?.trimEnd() ?? "";
|
|
4597
|
+
writes.push({
|
|
4598
|
+
target: reference.target,
|
|
4599
|
+
content: base === "" ? reference.content : `${base}\n\n${reference.content}`
|
|
4600
|
+
});
|
|
4601
|
+
}
|
|
4266
4602
|
if (mode === "append") {
|
|
4267
4603
|
const merged = freshTargetMd.trimEnd() + parts.join("\n") + "\n";
|
|
4268
4604
|
const validation = validateFrontmatter(merged, targetName, this.limits);
|
|
@@ -4469,7 +4805,7 @@ var SkillLibrary = class {
|
|
|
4469
4805
|
ok: false,
|
|
4470
4806
|
message: `Write exceeds ${this.limits.maxSkillFileBytes} bytes: ${write.target}`
|
|
4471
4807
|
};
|
|
4472
|
-
const threat =
|
|
4808
|
+
const threat = this.contentThreatBlock(write.content);
|
|
4473
4809
|
if (threat) return {
|
|
4474
4810
|
ok: false,
|
|
4475
4811
|
message: threat
|
|
@@ -4613,7 +4949,7 @@ var SkillLibrary = class {
|
|
|
4613
4949
|
ok: false,
|
|
4614
4950
|
message: `Support file exceeds ${this.limits.maxSkillFileBytes} bytes.`
|
|
4615
4951
|
};
|
|
4616
|
-
const threat =
|
|
4952
|
+
const threat = this.contentThreatBlock(content);
|
|
4617
4953
|
if (threat) return {
|
|
4618
4954
|
ok: false,
|
|
4619
4955
|
message: threat
|
|
@@ -4878,4 +5214,4 @@ var SkillLibrary = class {
|
|
|
4878
5214
|
}
|
|
4879
5215
|
};
|
|
4880
5216
|
//#endregion
|
|
4881
|
-
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 };
|
|
5217
|
+
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 };
|