@lmzhen/dsh-evolution-core 0.3.62 → 0.3.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -12
- package/lib/index.js +561 -174
- package/lib/types/constants.d.ts +10 -1
- package/lib/types/events.d.ts +3 -1
- package/lib/types/evolution-events.d.ts +11 -0
- package/lib/types/io.d.ts +15 -0
- package/lib/types/memory-store.d.ts +4 -0
- package/lib/types/quality.d.ts +2 -2
- package/lib/types/skill-store.d.ts +80 -1
- package/lib/types/usage.d.ts +14 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -156,12 +156,41 @@ async function fsyncDirectory(path) {
|
|
|
156
156
|
async function commitTmp(tmp, target) {
|
|
157
157
|
try {
|
|
158
158
|
await renameWithRetry(tmp, target);
|
|
159
|
-
await fsyncDirectory(dirname(target));
|
|
160
159
|
} catch (error) {
|
|
161
160
|
await rm(tmp, { force: true }).catch(() => {});
|
|
162
161
|
throw error;
|
|
163
162
|
}
|
|
163
|
+
try {
|
|
164
|
+
await fsyncDirectory(dirname(target));
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw Object.assign(/* @__PURE__ */ new Error(`commitTmp: ${target} was renamed but the directory fsync failed: ${error instanceof Error ? error.message : String(error)}`), {
|
|
167
|
+
committed: true,
|
|
168
|
+
cause: error
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* True when the pid is alive (EPERM = alive but unowned; ESRCH = gone).
|
|
174
|
+
* V18 single source: the node backend's lock takeover and SkillLibrary's
|
|
175
|
+
* stranded-lock sweep must use the same liveness rule.
|
|
176
|
+
*/
|
|
177
|
+
function isProcessAlive(pid) {
|
|
178
|
+
try {
|
|
179
|
+
process.kill(pid, 0);
|
|
180
|
+
return true;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
return error?.code === "EPERM";
|
|
183
|
+
}
|
|
164
184
|
}
|
|
185
|
+
/** F-17 (v18): the write-lock protocol is a cross-module contract — the lock
|
|
186
|
+
* file is `<target>.lock` and its body is `<pid>:<token>`. This module creates
|
|
187
|
+
* them (`withWriteLock`) and `skill-store`'s probes/sweepers parse them, so both
|
|
188
|
+
* consume these two constants instead of repeating the literals. */
|
|
189
|
+
const LOCK_SUFFIX = ".lock";
|
|
190
|
+
/** Writer-lock body shape: a decimal pid, a colon, then the claim token. A
|
|
191
|
+
* torn body (no parsable pid) still matches the `\\d+:` prefix rule only when
|
|
192
|
+
* the pid part is intact, which is what the takeover probe needs. */
|
|
193
|
+
const LOCK_BODY_RE = /^\d+:[0-9a-f]*$/;
|
|
165
194
|
/**
|
|
166
195
|
* Build the Node IO backend. `lockAttempts` scales the write-lock retry budget
|
|
167
196
|
* (attempts × 50ms); the default 40 (~2s, rc.69) covers production contention,
|
|
@@ -174,15 +203,8 @@ function nodeEvolutionIo(lockAttempts = 40) {
|
|
|
174
203
|
const code = error?.code;
|
|
175
204
|
return code === "ENOENT" || code === "ENOTDIR";
|
|
176
205
|
};
|
|
177
|
-
/** True when the pid is alive (
|
|
178
|
-
const isAlive =
|
|
179
|
-
try {
|
|
180
|
-
process.kill(pid, 0);
|
|
181
|
-
return true;
|
|
182
|
-
} catch (error) {
|
|
183
|
-
return error?.code === "EPERM";
|
|
184
|
-
}
|
|
185
|
-
};
|
|
206
|
+
/** True when the pid is alive (single source: `isProcessAlive`). */
|
|
207
|
+
const isAlive = isProcessAlive;
|
|
186
208
|
/**
|
|
187
209
|
* V10-07 (P2-1): age threshold for taking over a lock whose body is TORN
|
|
188
210
|
* (non-empty, but the pid prefix does not parse to a positive integer). 1h:
|
|
@@ -238,7 +260,7 @@ function nodeEvolutionIo(lockAttempts = 40) {
|
|
|
238
260
|
* forever.
|
|
239
261
|
*/
|
|
240
262
|
const withWriteLock = async (path, task) => {
|
|
241
|
-
const lock = `${path}
|
|
263
|
+
const lock = `${path}${LOCK_SUFFIX}`;
|
|
242
264
|
let myClaim = "";
|
|
243
265
|
for (let attempt = 0; attempt < lockAttempts; attempt += 1) {
|
|
244
266
|
let lockHandle = null;
|
|
@@ -343,40 +365,41 @@ function nodeEvolutionIo(lockAttempts = 40) {
|
|
|
343
365
|
return;
|
|
344
366
|
}
|
|
345
367
|
const prefix = `${base}.`;
|
|
346
|
-
const lockName = `${base}
|
|
368
|
+
const lockName = `${base}${LOCK_SUFFIX}`;
|
|
347
369
|
const ticketName = `${lockName}.next`;
|
|
348
370
|
const CORRUPT_SWEEP_AGE_MS = 168 * 36e5;
|
|
349
371
|
for (const name of entries) {
|
|
350
372
|
if (!name.startsWith(prefix) || name === lockName) continue;
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (dead || old) await rm(ticketPath, { force: true });
|
|
361
|
-
} catch {}
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
if (name.includes(".corrupt")) {
|
|
365
|
-
const corruptPath = join(dir, name);
|
|
366
|
-
try {
|
|
367
|
-
const st = await stat(corruptPath);
|
|
368
|
-
if (Date.now() - st.mtimeMs > CORRUPT_SWEEP_AGE_MS) await rm(corruptPath, { force: true });
|
|
369
|
-
} catch {}
|
|
370
|
-
}
|
|
373
|
+
const tmpMatch = /^(.*)\.(\d+)\.([0-9a-f]+)\.tmp$/.exec(name);
|
|
374
|
+
if (tmpMatch !== null && tmpMatch[1] === base) {
|
|
375
|
+
const tmpPath = join(dir, name);
|
|
376
|
+
const holder = Number(tmpMatch[2]);
|
|
377
|
+
try {
|
|
378
|
+
const st = await stat(tmpPath);
|
|
379
|
+
const deadHolder = !Number.isInteger(holder) || holder <= 0 || !isAlive(holder);
|
|
380
|
+
if (holder === process.pid || Date.now() - st.mtimeMs > 36e5 && deadHolder) await rm(tmpPath, { force: true });
|
|
381
|
+
} catch {}
|
|
371
382
|
continue;
|
|
372
383
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
384
|
+
if (name === ticketName) {
|
|
385
|
+
const ticketPath = join(dir, name);
|
|
386
|
+
try {
|
|
387
|
+
const body = await readFile(ticketPath, "utf8").catch(() => "");
|
|
388
|
+
const holder = Number(body.split(":")[0] ?? "");
|
|
389
|
+
const st = await stat(ticketPath);
|
|
390
|
+
const dead = !Number.isInteger(holder) || holder <= 0 || !isAlive(holder);
|
|
391
|
+
const old = Date.now() - st.mtimeMs > 1e3;
|
|
392
|
+
if (dead || old) await rm(ticketPath, { force: true });
|
|
393
|
+
} catch {}
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (name.includes(".corrupt")) {
|
|
397
|
+
const corruptPath = join(dir, name);
|
|
398
|
+
try {
|
|
399
|
+
const st = await stat(corruptPath);
|
|
400
|
+
if (Date.now() - st.mtimeMs > CORRUPT_SWEEP_AGE_MS) await rm(corruptPath, { force: true });
|
|
401
|
+
} catch {}
|
|
402
|
+
}
|
|
380
403
|
}
|
|
381
404
|
};
|
|
382
405
|
return {
|
|
@@ -534,7 +557,9 @@ function normalizeUsageRecord(record) {
|
|
|
534
557
|
pinned: bool(raw.pinned, base.pinned),
|
|
535
558
|
archived_at: nullableTimestamp(raw.archived_at) ? raw.archived_at : base.archived_at,
|
|
536
559
|
quality_score: typeof raw.quality_score === "number" && Number.isFinite(raw.quality_score) ? raw.quality_score : void 0,
|
|
537
|
-
quality_warn: typeof raw.quality_warn === "boolean" ? raw.quality_warn : void 0
|
|
560
|
+
quality_warn: typeof raw.quality_warn === "boolean" ? raw.quality_warn : void 0,
|
|
561
|
+
feedback_score: typeof raw.feedback_score === "number" && Number.isFinite(raw.feedback_score) ? raw.feedback_score : void 0,
|
|
562
|
+
feedback_warn: typeof raw.feedback_warn === "boolean" ? raw.feedback_warn : void 0
|
|
538
563
|
};
|
|
539
564
|
}
|
|
540
565
|
/** Parse a raw usage sidecar; malformed content reads as empty (best-effort telemetry). */
|
|
@@ -560,11 +585,19 @@ async function loadUsage(root, io = nodeEvolutionIo()) {
|
|
|
560
585
|
*/
|
|
561
586
|
async function mutateUsage(root, io, task) {
|
|
562
587
|
await transactIo(io, usageFile(root), async (current) => {
|
|
588
|
+
let shapePreserved = false;
|
|
563
589
|
if (current !== null) try {
|
|
564
|
-
JSON.parse(current);
|
|
590
|
+
const probe = JSON.parse(current);
|
|
591
|
+
if (probe === null || Array.isArray(probe) || typeof probe !== "object") shapePreserved = true;
|
|
592
|
+
else {
|
|
593
|
+
const record = probe;
|
|
594
|
+
if (Object.values(record).some((value) => value === null || typeof value !== "object" || Array.isArray(value))) shapePreserved = true;
|
|
595
|
+
if (typeof record.version === "number" && record.version > 1) shapePreserved = true;
|
|
596
|
+
}
|
|
565
597
|
} catch {
|
|
566
598
|
return current;
|
|
567
599
|
}
|
|
600
|
+
if (shapePreserved) return current;
|
|
568
601
|
const map = parseUsage(current);
|
|
569
602
|
await task(map);
|
|
570
603
|
return JSON.stringify(Object.fromEntries(map.entries()), null, 2);
|
|
@@ -593,6 +626,9 @@ function applyCuratorLifecycleFields(disk, curated) {
|
|
|
593
626
|
* Copy the recomputed meta pair (quality_score/quality_warn + the
|
|
594
627
|
* marker-mirrored pin flag) — refreshed tree-wide each run by design, so a
|
|
595
628
|
* concurrent curator run's lifecycle changes are never reverted by them.
|
|
629
|
+
* P1-1 (v15): the feedback pair (`feedback_score`/`feedback_warn`) is
|
|
630
|
+
* deliberately NOT copied — it is feedback-owned (see the field-ownership
|
|
631
|
+
* contract on {@link UsageRecord}) and must survive curator runs untouched.
|
|
596
632
|
*/
|
|
597
633
|
function applyCuratorMetaFields(disk, curated) {
|
|
598
634
|
disk.quality_score = curated.quality_score;
|
|
@@ -609,7 +645,8 @@ function applyCuratorMetaFields(disk, curated) {
|
|
|
609
645
|
* transitioned — a concurrent curator run's archive/restore is never reverted
|
|
610
646
|
* by a stale snapshot; without it both pairs apply everywhere.
|
|
611
647
|
*/
|
|
612
|
-
function foldCuratorFields(disk, curated, stateOwned) {
|
|
648
|
+
function foldCuratorFields(disk, curated, stateOwned, runStartStates) {
|
|
649
|
+
const skipped = [];
|
|
613
650
|
for (const [name, record] of curated) {
|
|
614
651
|
const diskRecord = disk.get(name);
|
|
615
652
|
if (!diskRecord) {
|
|
@@ -617,8 +654,16 @@ function foldCuratorFields(disk, curated, stateOwned) {
|
|
|
617
654
|
continue;
|
|
618
655
|
}
|
|
619
656
|
applyCuratorMetaFields(diskRecord, record);
|
|
620
|
-
if (stateOwned === void 0 || stateOwned.has(name))
|
|
657
|
+
if (stateOwned === void 0 || stateOwned.has(name)) {
|
|
658
|
+
const expected = runStartStates?.get(name);
|
|
659
|
+
if (expected !== void 0 && diskRecord.state !== expected) {
|
|
660
|
+
skipped.push(name);
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
applyCuratorLifecycleFields(diskRecord, record);
|
|
664
|
+
}
|
|
621
665
|
}
|
|
666
|
+
return skipped;
|
|
622
667
|
}
|
|
623
668
|
/** Whole-file usage write (V6-37, 0.3.37): this is the ONE path that bypasses
|
|
624
669
|
* the malformed-defense and the transact lock — prefer `mutateUsage` for any
|
|
@@ -749,8 +794,14 @@ async function updateSuppressedNames(root, io, task) {
|
|
|
749
794
|
* threshold, which are intentionally left where they are used.
|
|
750
795
|
* @module @lmzhen/dsh-evolution-core
|
|
751
796
|
*/
|
|
752
|
-
/** Skill frontmatter `name` validated for the file name (lowercase + hyphen).
|
|
753
|
-
|
|
797
|
+
/** Skill frontmatter `name` validated for the file name (lowercase + hyphen).
|
|
798
|
+
* 计划 B-4 (v18): tightened to the UPSTREAM `SKILL_NAME` shape
|
|
799
|
+
* (`/^[a-z0-9]+(?:-[a-z0-9]+)*$/`, packages/skill/skill/src/index.ts:20). The
|
|
800
|
+
* old form admitted trailing/consecutive hyphens, which upstream
|
|
801
|
+
* `validateCandidate` throws on — and that throw aborts the WHOLE `ctx.skills`
|
|
802
|
+
* collection. The catalog provider still filters such legacy tree entries so
|
|
803
|
+
* an existing tree cannot break a session. */
|
|
804
|
+
const SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
754
805
|
/** Allowed skill support-file subdirectories (path-traversal boundary). */
|
|
755
806
|
const SUPPORT_DIRS = [
|
|
756
807
|
"references",
|
|
@@ -788,7 +839,10 @@ const DEFAULT_MEMORY_CHAR_LIMIT = 2200;
|
|
|
788
839
|
const DEFAULT_USER_CHAR_LIMIT = 1375;
|
|
789
840
|
/** Consolidation-failure backoff cap, shared by MemoryStore and memory-files' Config default. */
|
|
790
841
|
const DEFAULT_CONSOLIDATION_FAILURES = 3;
|
|
791
|
-
|
|
842
|
+
/** F-20 (v18): the authored-body budget and the hard ceiling are the same
|
|
843
|
+
* number today. Derive it so a future divergence is one edit, not two names
|
|
844
|
+
* that silently disagree. */
|
|
845
|
+
const DEFAULT_SKILL_CONTENT_CHARS = MAX_SKILL_CONTENT_CHARS;
|
|
792
846
|
/** P3-19 (v14): defaults that were written twice (schema `.default()` AND the
|
|
793
847
|
* clamp fallback literal) now have one home per value. */
|
|
794
848
|
const DEFAULT_REVIEW_TIMEOUT_MS = 12e4;
|
|
@@ -1018,8 +1072,9 @@ function computeScopeView(usage, config, protectedNames, gates) {
|
|
|
1018
1072
|
if (record.pinned || bundled || suppressed || protectedNames?.has(name) === true || isBuiltin) protectedSet.add(name);
|
|
1019
1073
|
if (lifecycleCandidate(name, record, config, bundled, gateSet, protectedNames)) {
|
|
1020
1074
|
managed.push(name);
|
|
1021
|
-
|
|
1022
|
-
if (record.
|
|
1075
|
+
const warned = record.quality_warn === true || record.feedback_warn === true;
|
|
1076
|
+
if (record.state === "stale" || warned) watched.push(name);
|
|
1077
|
+
if (warned) qualityWarned.push(name);
|
|
1023
1078
|
}
|
|
1024
1079
|
}
|
|
1025
1080
|
return {
|
|
@@ -1031,7 +1086,10 @@ function computeScopeView(usage, config, protectedNames, gates) {
|
|
|
1031
1086
|
};
|
|
1032
1087
|
}
|
|
1033
1088
|
function daysSince(iso, created, now) {
|
|
1034
|
-
|
|
1089
|
+
const anchor = iso ?? created;
|
|
1090
|
+
const t = Date.parse(anchor);
|
|
1091
|
+
if (!Number.isFinite(t)) return 0;
|
|
1092
|
+
return (now - t) / 864e5;
|
|
1035
1093
|
}
|
|
1036
1094
|
function computeLifecycleTransitions(usage, config, now = /* @__PURE__ */ new Date(), gates, protectedNames) {
|
|
1037
1095
|
const result = {
|
|
@@ -1044,9 +1102,9 @@ function computeLifecycleTransitions(usage, config, now = /* @__PURE__ */ new Da
|
|
|
1044
1102
|
for (const [name, record] of usage) {
|
|
1045
1103
|
if (!lifecycleCandidate(name, record, config, config.bundledNames?.has(name) === true, gateSet, protectedNames)) continue;
|
|
1046
1104
|
const age = daysSince(null, record.created_at, now.getTime());
|
|
1047
|
-
|
|
1105
|
+
const qualityWarn = record.quality_warn === true || record.feedback_warn === true;
|
|
1106
|
+
if (record.use_count + record.view_count === 0 && !qualityWarn && age < config.staleAfterDays) continue;
|
|
1048
1107
|
const idle = daysSince(latestActivityAt(record), record.created_at, now.getTime());
|
|
1049
|
-
const qualityWarn = record.quality_warn === true;
|
|
1050
1108
|
const staleAfterDays = qualityWarn && config.qualityWarnStaleAfterDays !== void 0 ? config.qualityWarnStaleAfterDays : config.staleAfterDays;
|
|
1051
1109
|
if (record.state === "active") {
|
|
1052
1110
|
if (idle >= config.archiveAfterDays) {
|
|
@@ -1061,7 +1119,8 @@ function computeLifecycleTransitions(usage, config, now = /* @__PURE__ */ new Da
|
|
|
1061
1119
|
result.archive.push(name);
|
|
1062
1120
|
} else if (idle >= staleAfterDays) {
|
|
1063
1121
|
record.state = "stale";
|
|
1064
|
-
const
|
|
1122
|
+
const warnSource = record.feedback_warn === true ? "feedback-warn stale" : "quality-warn stale";
|
|
1123
|
+
const reason = qualityWarn ? `idle ${Math.round(idle)}d >= ${warnSource} ${staleAfterDays}d` : `idle ${Math.round(idle)}d >= ${staleAfterDays}d`;
|
|
1065
1124
|
result.transitions.push({
|
|
1066
1125
|
name,
|
|
1067
1126
|
from: "active",
|
|
@@ -1138,6 +1197,28 @@ const EVENT_ARCHIVE_RE = /^events-(\d+)\.json$/;
|
|
|
1138
1197
|
function eventsFile(home) {
|
|
1139
1198
|
return join(home, "evolution", "events.json");
|
|
1140
1199
|
}
|
|
1200
|
+
/** I-5 (v18): one lightweight description of the durable event payload
|
|
1201
|
+
* contract. The log is a FILE boundary (a host, a script or an older version
|
|
1202
|
+
* can write it), so `appendEvolutionEvent` refuses a record no consumer can
|
|
1203
|
+
* fold instead of persisting it and failing silently later. The process event
|
|
1204
|
+
* bus stays unvalidated — that is a typed same-process boundary.
|
|
1205
|
+
* @param event - the candidate event record.
|
|
1206
|
+
* @returns a human-readable issue, or null when the record is well-formed.
|
|
1207
|
+
*/
|
|
1208
|
+
function evolutionEventPayloadIssue(event) {
|
|
1209
|
+
const type = event.type;
|
|
1210
|
+
if (typeof type !== "string") return `unknown event type "${String(type)}"`;
|
|
1211
|
+
switch (type) {
|
|
1212
|
+
case "feedback":
|
|
1213
|
+
if (event.kind !== "skill" && event.kind !== "session") return "feedback event requires kind skill|session";
|
|
1214
|
+
if (event.rating !== "positive" && event.rating !== "negative") return "feedback event requires rating positive|negative";
|
|
1215
|
+
return null;
|
|
1216
|
+
case "maintain": return typeof event.runId === "string" ? null : "maintain event requires runId";
|
|
1217
|
+
case "learn":
|
|
1218
|
+
case "usage": return null;
|
|
1219
|
+
default: return `unknown event type "${type}"`;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1141
1222
|
function isEventRecord(event) {
|
|
1142
1223
|
const seq = event?.seq;
|
|
1143
1224
|
return typeof event === "object" && event !== null && typeof seq === "number" && Number.isFinite(seq);
|
|
@@ -1210,6 +1291,8 @@ async function listEventArchives(io, path) {
|
|
|
1210
1291
|
* event can never shadow an archived one in the seq-deduped timeline.
|
|
1211
1292
|
*/
|
|
1212
1293
|
async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE_AT) {
|
|
1294
|
+
const issue = evolutionEventPayloadIssue(event);
|
|
1295
|
+
if (issue !== null) throw new Error(`evolution event refused: ${issue}`);
|
|
1213
1296
|
let assigned = 0;
|
|
1214
1297
|
let refuseMessage = "";
|
|
1215
1298
|
let parsedBody = null;
|
|
@@ -1255,7 +1338,7 @@ async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE
|
|
|
1255
1338
|
* one-event rotate would archive everything and restart seqs at 1).
|
|
1256
1339
|
*/
|
|
1257
1340
|
async function rotateIfDue(io, path, events, rotateAt) {
|
|
1258
|
-
if (rotateAt < 2 || events.length < rotateAt) return events;
|
|
1341
|
+
if (!Number.isFinite(rotateAt) || rotateAt < 2 || events.length < rotateAt) return events;
|
|
1259
1342
|
const mid = Math.ceil(events.length / 2);
|
|
1260
1343
|
const head = events.slice(0, mid);
|
|
1261
1344
|
const tail = events.slice(mid);
|
|
@@ -1909,7 +1992,7 @@ const PATTERNS = [
|
|
|
1909
1992
|
label: "disregard_rules",
|
|
1910
1993
|
category: "prompt_injection",
|
|
1911
1994
|
scope: "all",
|
|
1912
|
-
regex:
|
|
1995
|
+
regex: new RegExp(String.raw`disregard\s+${FILLER}(?:your|all|any)\s+${FILLER}(?:instructions|rules|guidelines)`, "i")
|
|
1913
1996
|
},
|
|
1914
1997
|
{
|
|
1915
1998
|
label: "system_prompt_override",
|
|
@@ -2062,7 +2145,7 @@ const PATTERNS = [
|
|
|
2062
2145
|
regex: /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----/
|
|
2063
2146
|
}
|
|
2064
2147
|
];
|
|
2065
|
-
const ZERO_WIDTH_CHARS =
|
|
2148
|
+
const ZERO_WIDTH_CHARS = new RegExp(`[\\u00ad\\u034f\\u061c\\u180e\\u200b\\u200c\\u200d\\u2060\\u2061\\u2062\\u2063\\u2064\\u206a-\\u206f\\ufeff\\ufe00-\\ufe0f]|\\u{e0000}-\\u{e007f}`, "u");
|
|
2066
2149
|
const BIDI_CHARS = /[\u202a-\u202e\u2066-\u2069]/;
|
|
2067
2150
|
const SCOPE_ORDER = {
|
|
2068
2151
|
all: 1,
|
|
@@ -2092,12 +2175,12 @@ function scanThreats(text, scope = "strict", maxScanChars = 65536, options = NO_
|
|
|
2092
2175
|
if (ZERO_WIDTH_CHARS.test(text) && !excluded.has("unicode_zero_width")) findings.push({
|
|
2093
2176
|
label: "unicode_zero_width",
|
|
2094
2177
|
category: "unicode_obfuscation",
|
|
2095
|
-
scope
|
|
2178
|
+
scope: "all"
|
|
2096
2179
|
});
|
|
2097
2180
|
if (BIDI_CHARS.test(text) && !excluded.has("unicode_bidi_override")) findings.push({
|
|
2098
2181
|
label: "unicode_bidi_override",
|
|
2099
2182
|
category: "unicode_obfuscation",
|
|
2100
|
-
scope
|
|
2183
|
+
scope: "all"
|
|
2101
2184
|
});
|
|
2102
2185
|
const normalized = text.normalize("NFKC");
|
|
2103
2186
|
const windows = [];
|
|
@@ -2252,12 +2335,17 @@ var MemoryStore = class {
|
|
|
2252
2335
|
/** V10-03 (P2-18): the strict-scan write gate. A block message names the hit
|
|
2253
2336
|
* label (scanMemoryThreats already embeds it) plus the self-heal hint. */
|
|
2254
2337
|
memoryThreatBlock(text) {
|
|
2255
|
-
|
|
2256
|
-
return threat === null ? null : threat + THREAT_EXEMPT_HINT;
|
|
2338
|
+
return scanMemoryThreats(text, void 0, this.threatScanOptions());
|
|
2257
2339
|
}
|
|
2258
2340
|
limitFor(target) {
|
|
2259
2341
|
return target === "memory" ? this.memoryLimit : this.userLimit;
|
|
2260
2342
|
}
|
|
2343
|
+
/** P2-1 (v18): the generated date prefix participates in duplicate detection
|
|
2344
|
+
* only when THIS store writes it. With addDatePrefix=false a fact's own
|
|
2345
|
+
* leading `## YYYY-MM-DD\n` is content, not a generated prefix. */
|
|
2346
|
+
dedupeKey(entry) {
|
|
2347
|
+
return this.addDatePrefix ? stripDatePrefix(entry) : entry;
|
|
2348
|
+
}
|
|
2261
2349
|
/**
|
|
2262
2350
|
* Read-guard probe: `{ size, limit }` when the on-disk file exceeds
|
|
2263
2351
|
* `limit * READ_GUARD_FACTOR` bytes, `null` when it is absent, unknown
|
|
@@ -2397,7 +2485,13 @@ var MemoryStore = class {
|
|
|
2397
2485
|
async addCore(target, facts, raw) {
|
|
2398
2486
|
const content = facts.trim();
|
|
2399
2487
|
if (!content) return {
|
|
2400
|
-
result:
|
|
2488
|
+
result: {
|
|
2489
|
+
ok: false,
|
|
2490
|
+
message: "Content cannot be empty.",
|
|
2491
|
+
entries: [],
|
|
2492
|
+
chars: 0,
|
|
2493
|
+
limit: this.limitFor(target)
|
|
2494
|
+
},
|
|
2401
2495
|
write: null
|
|
2402
2496
|
};
|
|
2403
2497
|
if (this.driftFromRaw(target, raw)) {
|
|
@@ -2436,7 +2530,7 @@ var MemoryStore = class {
|
|
|
2436
2530
|
write: null
|
|
2437
2531
|
};
|
|
2438
2532
|
const entries = [...new Set(normalizeEntries(raw))];
|
|
2439
|
-
if (entries.some((entry) =>
|
|
2533
|
+
if (entries.some((entry) => this.dedupeKey(entry) === content)) {
|
|
2440
2534
|
this.resetFailures();
|
|
2441
2535
|
return {
|
|
2442
2536
|
result: {
|
|
@@ -2558,7 +2652,7 @@ var MemoryStore = class {
|
|
|
2558
2652
|
},
|
|
2559
2653
|
write: null
|
|
2560
2654
|
};
|
|
2561
|
-
if (!working.some((entry) =>
|
|
2655
|
+
if (!working.some((entry) => this.dedupeKey(entry) === body)) working.push(entryBody);
|
|
2562
2656
|
continue;
|
|
2563
2657
|
}
|
|
2564
2658
|
const rawAction = op.action;
|
|
@@ -2737,25 +2831,38 @@ function contentHash(content) {
|
|
|
2737
2831
|
function parseMutationRecords(raw) {
|
|
2738
2832
|
if (raw === null) return [];
|
|
2739
2833
|
try {
|
|
2740
|
-
|
|
2741
|
-
return (Array.isArray(parsed) ? parsed : typeof parsed === "object" && parsed !== null && Array.isArray(parsed.records) ? parsed.records : []).filter((entry) => typeof entry === "object" && entry !== null && typeof entry.skillName === "string" && typeof entry.action === "string" && typeof entry.at === "string");
|
|
2834
|
+
return recordsFromParsed(JSON.parse(raw));
|
|
2742
2835
|
} catch {
|
|
2743
2836
|
return [];
|
|
2744
2837
|
}
|
|
2745
2838
|
}
|
|
2839
|
+
/** Field-level shape guard shared by the parse and the recordMutation write
|
|
2840
|
+
* path (P3/v15: the guard's JSON.parse and this parse used to run twice over
|
|
2841
|
+
* the same bytes inside one transact). */
|
|
2842
|
+
function recordsFromParsed(parsed) {
|
|
2843
|
+
return (Array.isArray(parsed) ? parsed : typeof parsed === "object" && parsed !== null && Array.isArray(parsed.records) ? parsed.records : []).filter((entry) => typeof entry === "object" && entry !== null && typeof entry.skillName === "string" && typeof entry.action === "string" && typeof entry.at === "string");
|
|
2844
|
+
}
|
|
2746
2845
|
async function loadMutations(root, io = nodeEvolutionIo()) {
|
|
2747
2846
|
return parseMutationRecords(await io.readText(mutationsFile(root)));
|
|
2748
2847
|
}
|
|
2749
2848
|
/** Append one record, trim to `cap`, and write atomically (versioned shape). */
|
|
2750
2849
|
async function recordMutation(root, io, record, cap = 500) {
|
|
2751
2850
|
await transactIo(io, mutationsFile(root), (current) => {
|
|
2851
|
+
let parsed = [];
|
|
2752
2852
|
if (current !== null) try {
|
|
2753
|
-
JSON.parse(current);
|
|
2853
|
+
parsed = JSON.parse(current);
|
|
2754
2854
|
} catch {
|
|
2755
2855
|
console.warn(`mutation audit record dropped: ${mutationsFile(root)} is malformed and was not overwritten`);
|
|
2756
2856
|
return current;
|
|
2757
2857
|
}
|
|
2758
|
-
|
|
2858
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
2859
|
+
const version = parsed.version;
|
|
2860
|
+
if (typeof version === "number" && version > 1) {
|
|
2861
|
+
console.warn(`mutation audit record dropped: ${mutationsFile(root)} declares version ${version} (newer than 1); not overwritten`);
|
|
2862
|
+
return current;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
const existing = recordsFromParsed(parsed);
|
|
2759
2866
|
existing.push(record);
|
|
2760
2867
|
const trimmed = existing.length > cap ? existing.slice(existing.length - cap) : existing;
|
|
2761
2868
|
return JSON.stringify({
|
|
@@ -2875,7 +2982,9 @@ function clamp01(value) {
|
|
|
2875
2982
|
return Math.max(0, Math.min(1, value));
|
|
2876
2983
|
}
|
|
2877
2984
|
function daysBetween(from, now) {
|
|
2878
|
-
|
|
2985
|
+
const t = Date.parse(from);
|
|
2986
|
+
if (!Number.isFinite(t)) return 0;
|
|
2987
|
+
return Math.max(0, (now.getTime() - t) / 864e5);
|
|
2879
2988
|
}
|
|
2880
2989
|
function computeQualityScores(input) {
|
|
2881
2990
|
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
@@ -2884,9 +2993,9 @@ function computeQualityScores(input) {
|
|
|
2884
2993
|
const ageDays = Math.max(1, daysBetween(record.created_at, now));
|
|
2885
2994
|
const idleDays = daysBetween(latestActivityAt(record) ?? record.created_at, now);
|
|
2886
2995
|
const patchCount = record.patch_count;
|
|
2887
|
-
const
|
|
2888
|
-
const usageFrequency = clamp01(
|
|
2889
|
-
const stability =
|
|
2996
|
+
const loadCount = record.use_count + record.view_count;
|
|
2997
|
+
const usageFrequency = clamp01(loadCount / ageDays);
|
|
2998
|
+
const stability = loadCount === 0 ? 1 : clamp01(1 - patchCount / loadCount);
|
|
2890
2999
|
const recency = idleDays < 30 ? 1 : clamp01(1 - (idleDays - 30) / 150);
|
|
2891
3000
|
const references = clamp01((input.referenceCounts?.get(name) ?? 0) / 3);
|
|
2892
3001
|
const mutationMaturity = patchCount === 0 ? .3 : patchCount === 1 ? .4 : clamp01((patchCount - 1) / Math.max(1, ageDays / 30));
|
|
@@ -3031,7 +3140,7 @@ const SECRET_PATTERNS = [
|
|
|
3031
3140
|
["jwt", /eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g],
|
|
3032
3141
|
["bearer credential", /Bearer[\s]+[a-z0-9._~+/=\-]{16,}/gi]
|
|
3033
3142
|
];
|
|
3034
|
-
const INLINE_ASSIGNMENT_PATTERN = /* @__PURE__ */ new RegExp("((
|
|
3143
|
+
const INLINE_ASSIGNMENT_PATTERN = /* @__PURE__ */ new RegExp("(^|[^\\w-])([\\w-]{0,64}[_\\-])?((?:token|api[_-]?key|secret|password|passwd)(?:[_\\-][\\w-]{0,64})?)\\b([\\s]*[:=][\\s]*)(?:\"([^\"\\r\\n]*)\"|'([^'\\r\\n]*)'|([^\\r\\n]+))", "gi");
|
|
3035
3144
|
/**
|
|
3036
3145
|
* Mask credential-shaped text before it crosses a session boundary.
|
|
3037
3146
|
* @param text - the text about to be sent to a model outside this session.
|
|
@@ -3040,7 +3149,7 @@ const INLINE_ASSIGNMENT_PATTERN = /* @__PURE__ */ new RegExp("((?:\\b|[\\w-]+[_\
|
|
|
3040
3149
|
function redactSecrets(text) {
|
|
3041
3150
|
let out = text;
|
|
3042
3151
|
for (const [, pattern] of SECRET_PATTERNS) out = out.replace(pattern, "<redacted>");
|
|
3043
|
-
out = out.replace(INLINE_ASSIGNMENT_PATTERN, (_match,
|
|
3152
|
+
out = out.replace(INLINE_ASSIGNMENT_PATTERN, (_match, lead, prefix, key, separator) => `${lead ?? ""}${prefix ?? ""}${key ?? ""}${separator ?? ""}<redacted>`);
|
|
3044
3153
|
return out;
|
|
3045
3154
|
}
|
|
3046
3155
|
//#endregion
|
|
@@ -3128,6 +3237,22 @@ const CORRECTION_PATTERNS = [
|
|
|
3128
3237
|
/remember\s+(?:this|that|to)/i
|
|
3129
3238
|
];
|
|
3130
3239
|
const FIX_PATTERNS = [/worked after|fixed by|the fix was|root cause/i, /retry(?:ing)? worked|workaround/i];
|
|
3240
|
+
/** Text of one persisted content block, or `''` for any other shape.
|
|
3241
|
+
*
|
|
3242
|
+
* Content blocks cross the durable session-log boundary, so their runtime
|
|
3243
|
+
* shape is `unknown` even where the static event type promises
|
|
3244
|
+
* `{ type, text }`: a persisted `content: [null]` (A2-7, v18) used to throw a
|
|
3245
|
+
* TypeError here and the review catch swallowed the whole turn's remaining
|
|
3246
|
+
* signals. Keeping the guard in one helper also keeps the branches free of
|
|
3247
|
+
* conditions the static type already excludes.
|
|
3248
|
+
* @param block - one element of a persisted message `content` array.
|
|
3249
|
+
* @returns the block's text when it is a text block, otherwise an empty string.
|
|
3250
|
+
*/
|
|
3251
|
+
function textOfBlock(block) {
|
|
3252
|
+
if (block === null || typeof block !== "object") return "";
|
|
3253
|
+
const candidate = block;
|
|
3254
|
+
return candidate.type === "text" && typeof candidate.text === "string" ? candidate.text : "";
|
|
3255
|
+
}
|
|
3131
3256
|
/** Fold one session event into the current turn observation. */
|
|
3132
3257
|
function observeEvent(signal, event) {
|
|
3133
3258
|
const data = event.data;
|
|
@@ -3135,7 +3260,7 @@ function observeEvent(signal, event) {
|
|
|
3135
3260
|
if (event.type === "user/message") {
|
|
3136
3261
|
const content = data.content;
|
|
3137
3262
|
if (!Array.isArray(content)) return;
|
|
3138
|
-
const text = content.map(
|
|
3263
|
+
const text = content.map(textOfBlock).join(" ");
|
|
3139
3264
|
signal.userChars += text.length;
|
|
3140
3265
|
if (CORRECTION_PATTERNS.some((pattern) => pattern.test(text))) signal.memorySignal = true;
|
|
3141
3266
|
if (FIX_PATTERNS.some((pattern) => pattern.test(text))) signal.skillSignal = true;
|
|
@@ -3144,7 +3269,7 @@ function observeEvent(signal, event) {
|
|
|
3144
3269
|
if (event.type === "assistant/message") {
|
|
3145
3270
|
const message = data.message;
|
|
3146
3271
|
if (!message || !Array.isArray(message.content)) return;
|
|
3147
|
-
const text = message.content.map(
|
|
3272
|
+
const text = message.content.map(textOfBlock).join(" ");
|
|
3148
3273
|
signal.assistantChars += text.length;
|
|
3149
3274
|
return;
|
|
3150
3275
|
}
|
|
@@ -3386,8 +3511,12 @@ const MAX_RESTRUCTURE_MOVES = 5;
|
|
|
3386
3511
|
* dots) used to pass here while every later patch/write/remove on it was
|
|
3387
3512
|
* refused as traversal (an orphan file the user could not touch). */
|
|
3388
3513
|
const RESTRUCTURE_TARGET_RE = /^references\/[a-z0-9](?!.*\.\.)[a-z0-9._-]*\.md$/;
|
|
3514
|
+
/** F-20 (v18): the character rule shared by support-file names and snapshot
|
|
3515
|
+
* `extras/` entry names. The two exported names used to carry the same literal
|
|
3516
|
+
* independently; both now derive from this one. */
|
|
3517
|
+
const SUPPORT_ENTRY_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
3389
3518
|
/** Extra file name carried inside a snapshot's `extras/` directory. */
|
|
3390
|
-
const SNAPSHOT_EXTRA_NAME_RE =
|
|
3519
|
+
const SNAPSHOT_EXTRA_NAME_RE = SUPPORT_ENTRY_NAME_RE;
|
|
3391
3520
|
function skillsRoot(env = process.env) {
|
|
3392
3521
|
return join(evolutionRoot(env), "skills");
|
|
3393
3522
|
}
|
|
@@ -3401,6 +3530,29 @@ function skillsRoot(env = process.env) {
|
|
|
3401
3530
|
function resolveSkillsRoot(config = {}) {
|
|
3402
3531
|
return (config.root ?? "").trim() || skillsRoot();
|
|
3403
3532
|
}
|
|
3533
|
+
/** E-7 (v18): every family row reads ONE root key. `root` is canonical;
|
|
3534
|
+
* `skillsRoot` is a deprecated alias honoured only while `root` is empty (so a
|
|
3535
|
+
* deployment that sets both keeps the canonical one) and removed after 0.3.65.
|
|
3536
|
+
* Callers log their own deprecation warning.
|
|
3537
|
+
* @param config - the raw plugin config, carrying `root` and/or `skillsRoot`.
|
|
3538
|
+
* @returns the effective root (empty when neither key is set) and whether the
|
|
3539
|
+
* deprecated alias supplied it.
|
|
3540
|
+
*/
|
|
3541
|
+
function resolveRootConfig(config = {}) {
|
|
3542
|
+
const root = (config.root ?? "").trim();
|
|
3543
|
+
if (root !== "") return {
|
|
3544
|
+
root,
|
|
3545
|
+
usedDeprecatedAlias: false
|
|
3546
|
+
};
|
|
3547
|
+
const alias = (config.skillsRoot ?? "").trim();
|
|
3548
|
+
return alias === "" ? {
|
|
3549
|
+
root: "",
|
|
3550
|
+
usedDeprecatedAlias: false
|
|
3551
|
+
} : {
|
|
3552
|
+
root: alias,
|
|
3553
|
+
usedDeprecatedAlias: true
|
|
3554
|
+
};
|
|
3555
|
+
}
|
|
3404
3556
|
/**
|
|
3405
3557
|
* Map a requesting session onto the two origin surfaces (rc.44 plan M2-2.3):
|
|
3406
3558
|
* the APPROVAL surface treats every delegated subagent as the autonomous
|
|
@@ -3437,6 +3589,15 @@ function skillDir(root, name) {
|
|
|
3437
3589
|
function markerEntryName(marker) {
|
|
3438
3590
|
return `.${marker}`;
|
|
3439
3591
|
}
|
|
3592
|
+
/** F-17 (v18): the root-level lock files a DESTRUCTIVE MOVER must treat as an
|
|
3593
|
+
* active writer (skill body + the two marker writers). Single source with
|
|
3594
|
+
* `markerEntryName`/`LOCK_SUFFIX` so a renamed marker cannot silently drop out
|
|
3595
|
+
* of the ghost-writer probe. */
|
|
3596
|
+
const MARKER_LOCK_NAMES = [
|
|
3597
|
+
`SKILL.md${LOCK_SUFFIX}`,
|
|
3598
|
+
`.pinned${LOCK_SUFFIX}`,
|
|
3599
|
+
`.hermes-managed${LOCK_SUFFIX}`
|
|
3600
|
+
];
|
|
3440
3601
|
function markerPath(dir, marker) {
|
|
3441
3602
|
return join(dir, markerEntryName(marker));
|
|
3442
3603
|
}
|
|
@@ -3689,11 +3850,13 @@ async function listNames(root, io) {
|
|
|
3689
3850
|
* `[a-z0-9._-]`) — drive-colon / odd-character / uppercase names can no
|
|
3690
3851
|
* longer reach the filesystem through writeSupportFile / patch /
|
|
3691
3852
|
* removeSupportFile. */
|
|
3692
|
-
const SUPPORT_FILE_NAME_RE =
|
|
3853
|
+
const SUPPORT_FILE_NAME_RE = SUPPORT_ENTRY_NAME_RE;
|
|
3693
3854
|
/** C-18: win32 reserves these stems with ANY extension (`nul.md` hits the
|
|
3694
3855
|
* NUL device), and they are fully inside the charset above — so the reserved
|
|
3695
3856
|
* set is checked on the first-dot prefix as well; the charset close alone
|
|
3696
|
-
* cannot refuse them.
|
|
3857
|
+
* cannot refuse them. Exported single source: `badName` (skill directories,
|
|
3858
|
+
* P2-11/v15) and `validateSupportPath` (support-file stems, C-18) both
|
|
3859
|
+
* consume this one set — a third copy would drift. */
|
|
3697
3860
|
const WIN32_RESERVED_DEVICE_NAMES = new Set([
|
|
3698
3861
|
"con",
|
|
3699
3862
|
"prn",
|
|
@@ -3718,6 +3881,16 @@ const WIN32_RESERVED_DEVICE_NAMES = new Set([
|
|
|
3718
3881
|
"lpt8",
|
|
3719
3882
|
"lpt9"
|
|
3720
3883
|
]);
|
|
3884
|
+
/** A1-6 (v18): the regex alone admits `references/nul.md` (a Windows device
|
|
3885
|
+
* stem), which the support-file layer refuses. Restructure must use the same
|
|
3886
|
+
* rule, or it creates an orphan the later patch/write/remove paths refuse.
|
|
3887
|
+
* Single source shared with the plan validator. */
|
|
3888
|
+
function validateRestructureTarget(filePath) {
|
|
3889
|
+
if (!RESTRUCTURE_TARGET_RE.test(filePath)) return `toFile must be references/<topic>.md (got "${filePath}").`;
|
|
3890
|
+
const stem = filePath.slice(filePath.lastIndexOf("/") + 1).split(".")[0]?.toLowerCase() ?? "";
|
|
3891
|
+
if (WIN32_RESERVED_DEVICE_NAMES.has(stem)) return `toFile "${filePath}" uses a Windows reserved device name.`;
|
|
3892
|
+
return null;
|
|
3893
|
+
}
|
|
3721
3894
|
function validateSupportPath(filePath) {
|
|
3722
3895
|
const normalized = filePath.replace(/\\/g, "/");
|
|
3723
3896
|
if (normalized.includes("..")) return "Path traversal is not allowed.";
|
|
@@ -3726,6 +3899,8 @@ function validateSupportPath(filePath) {
|
|
|
3726
3899
|
if (parts.length < 2) return "Provide a file name, not just a directory.";
|
|
3727
3900
|
for (const part of parts.slice(1)) {
|
|
3728
3901
|
if (!SUPPORT_FILE_NAME_RE.test(part)) return `Unsupported file name "${part}" — use lowercase letters, digits, dots, hyphens, and underscores (leading letter or digit).`;
|
|
3902
|
+
if (part.toLowerCase().endsWith(".lock")) return `Unsupported file name "${part}" — the .lock suffix is reserved for the writer-lock protocol.`;
|
|
3903
|
+
if (part.toLowerCase().endsWith(".corrupt") || part.toLowerCase().endsWith(".tmp")) return `Unsupported file name "${part}" — the .corrupt/.tmp suffixes are reserved for the state/IO protocols.`;
|
|
3729
3904
|
const stem = part.split(".")[0]?.toLowerCase() ?? "";
|
|
3730
3905
|
if (WIN32_RESERVED_DEVICE_NAMES.has(stem)) return `Unsupported file name "${part}" — a Windows reserved device name.`;
|
|
3731
3906
|
}
|
|
@@ -3933,11 +4108,23 @@ var SkillLibrary = class {
|
|
|
3933
4108
|
outcome = o;
|
|
3934
4109
|
return o.write ?? current ?? null;
|
|
3935
4110
|
};
|
|
3936
|
-
|
|
4111
|
+
let durabilityWarning = "";
|
|
4112
|
+
const committedOnly = (error) => error?.committed === true;
|
|
4113
|
+
if (this.transact) try {
|
|
4114
|
+
await this.transact(this.io, path, run);
|
|
4115
|
+
} catch (error) {
|
|
4116
|
+
if (!committedOnly(error)) throw error;
|
|
4117
|
+
durabilityWarning = error instanceof Error ? error.message : String(error);
|
|
4118
|
+
}
|
|
3937
4119
|
else {
|
|
3938
4120
|
const current = await this.io.readText(path);
|
|
3939
4121
|
const next = await run(current);
|
|
3940
|
-
if (next !== null && next !== current)
|
|
4122
|
+
if (next !== null && next !== current) try {
|
|
4123
|
+
await this.io.writeText(path, next);
|
|
4124
|
+
} catch (error) {
|
|
4125
|
+
if (!committedOnly(error)) throw error;
|
|
4126
|
+
durabilityWarning = error instanceof Error ? error.message : String(error);
|
|
4127
|
+
}
|
|
3941
4128
|
}
|
|
3942
4129
|
const o = outcome;
|
|
3943
4130
|
if (o === void 0 || typeof o !== "object" || !Object.prototype.hasOwnProperty.call(o, "write")) return {
|
|
@@ -3946,7 +4133,10 @@ var SkillLibrary = class {
|
|
|
3946
4133
|
};
|
|
3947
4134
|
if (o.write !== null && o.audit) await this.audit(o.audit.skillName, o.audit.action, o.audit.before, o.audit.after, o.audit.summary);
|
|
3948
4135
|
if (o.write !== null && o.event) this.notifyMutation(o.event);
|
|
3949
|
-
return o.result
|
|
4136
|
+
return durabilityWarning === "" || !o.result.ok ? o.result : {
|
|
4137
|
+
...o.result,
|
|
4138
|
+
message: `${o.result.message} (warning: the write landed but the directory fsync failed — durability unconfirmed: ${durabilityWarning})`
|
|
4139
|
+
};
|
|
3950
4140
|
}
|
|
3951
4141
|
/** Notify the mutation observer after a successful write; observers must never fail the mutation. */
|
|
3952
4142
|
notifyMutation(event) {
|
|
@@ -3963,8 +4153,7 @@ var SkillLibrary = class {
|
|
|
3963
4153
|
* label (scanContentThreats already embeds it) plus the self-heal hint, so a
|
|
3964
4154
|
* false-positive rewrite direction is actionable instead of a dead end. */
|
|
3965
4155
|
contentThreatBlock(content) {
|
|
3966
|
-
|
|
3967
|
-
return threat === null ? null : threat + THREAT_EXEMPT_HINT;
|
|
4156
|
+
return scanContentThreats(content, void 0, this.threatScanOptions());
|
|
3968
4157
|
}
|
|
3969
4158
|
async list() {
|
|
3970
4159
|
const summaries = [];
|
|
@@ -3973,20 +4162,42 @@ var SkillLibrary = class {
|
|
|
3973
4162
|
const md = await this.io.readText(join(dir, "SKILL.md"));
|
|
3974
4163
|
if (md === null) continue;
|
|
3975
4164
|
const parsed = parseFrontmatter(md);
|
|
3976
|
-
let entries =
|
|
4165
|
+
let entries = null;
|
|
3977
4166
|
try {
|
|
3978
4167
|
entries = await this.io.list(dir);
|
|
3979
|
-
} catch {
|
|
3980
|
-
|
|
3981
|
-
|
|
4168
|
+
} catch {
|
|
4169
|
+
entries = null;
|
|
4170
|
+
}
|
|
4171
|
+
const probeMarker = async (marker) => {
|
|
4172
|
+
if (entries !== null) return entries.includes(markerEntryName(marker));
|
|
4173
|
+
try {
|
|
4174
|
+
return await this.io.exists(join(dir, markerEntryName(marker)));
|
|
4175
|
+
} catch {
|
|
4176
|
+
return null;
|
|
4177
|
+
}
|
|
4178
|
+
};
|
|
4179
|
+
const [bundled, hubInstalled, pinned, hermesManaged] = await Promise.all([
|
|
4180
|
+
probeMarker("bundled"),
|
|
4181
|
+
probeMarker("hub-installed"),
|
|
4182
|
+
probeMarker("pinned"),
|
|
4183
|
+
probeMarker("hermes-managed")
|
|
4184
|
+
]);
|
|
4185
|
+
const protectedBy = bundled === true ? "bundled" : hubInstalled === true ? "hub-installed" : pinned === true ? "pinned" : null;
|
|
3982
4186
|
const parsedDescription = parsed?.frontmatter.description;
|
|
4187
|
+
const parsedWhenToUse = parsed?.frontmatter.whenToUse;
|
|
3983
4188
|
summaries.push({
|
|
3984
4189
|
name,
|
|
3985
4190
|
description: typeof parsedDescription === "string" ? parsedDescription : "",
|
|
3986
4191
|
path: dir,
|
|
3987
4192
|
protectedBy,
|
|
3988
|
-
|
|
3989
|
-
|
|
4193
|
+
protectionUnknown: [
|
|
4194
|
+
bundled,
|
|
4195
|
+
hubInstalled,
|
|
4196
|
+
pinned,
|
|
4197
|
+
hermesManaged
|
|
4198
|
+
].some((value) => value === null),
|
|
4199
|
+
managed: hermesManaged === true,
|
|
4200
|
+
...typeof parsedWhenToUse === "string" && parsedWhenToUse.trim() !== "" ? { whenToUse: parsedWhenToUse } : {}
|
|
3990
4201
|
});
|
|
3991
4202
|
}
|
|
3992
4203
|
return summaries;
|
|
@@ -4023,9 +4234,10 @@ var SkillLibrary = class {
|
|
|
4023
4234
|
* are the deliberate exceptions — their names come from `listNames()`, i.e.
|
|
4024
4235
|
* from the tree itself, never from caller input.
|
|
4025
4236
|
*/
|
|
4026
|
-
badName(name) {
|
|
4237
|
+
badName(name, opts = {}) {
|
|
4027
4238
|
const normalized = name.trim();
|
|
4028
4239
|
if (!SKILL_NAME_RE.test(normalized) || normalized.length > this.limits.maxNameLength) return `Invalid skill name "${normalized}". Use lowercase letters, digits, and hyphens (<= ${this.limits.maxNameLength}).`;
|
|
4240
|
+
if (!opts.allowReserved && WIN32_RESERVED_DEVICE_NAMES.has(normalized)) return `"${normalized}" is a Windows reserved device name and cannot be used as a skill name.`;
|
|
4029
4241
|
return null;
|
|
4030
4242
|
}
|
|
4031
4243
|
/**
|
|
@@ -4045,7 +4257,7 @@ var SkillLibrary = class {
|
|
|
4045
4257
|
}
|
|
4046
4258
|
async deleteProtection(rawName, options = {}) {
|
|
4047
4259
|
const name = rawName.trim();
|
|
4048
|
-
const badName = this.badName(name);
|
|
4260
|
+
const badName = this.badName(name, { allowReserved: true });
|
|
4049
4261
|
if (badName) return badName;
|
|
4050
4262
|
const dir = this.dirOf(name);
|
|
4051
4263
|
const markers = options.allowBundled ? ["hub-installed", "pinned"] : [
|
|
@@ -4167,6 +4379,9 @@ var SkillLibrary = class {
|
|
|
4167
4379
|
* marker write is the only state change; content is untouched.
|
|
4168
4380
|
*/
|
|
4169
4381
|
async setPinned(name, pinned, origin = "foreground") {
|
|
4382
|
+
return await this.serial(() => this.setPinnedCore(name, pinned, origin));
|
|
4383
|
+
}
|
|
4384
|
+
async setPinnedCore(name, pinned, origin) {
|
|
4170
4385
|
const normalized = name.trim();
|
|
4171
4386
|
const bad = this.badName(normalized);
|
|
4172
4387
|
if (bad) return {
|
|
@@ -4247,7 +4462,32 @@ var SkillLibrary = class {
|
|
|
4247
4462
|
message: `Skill "${normalized}" is protected (${protection}).`
|
|
4248
4463
|
};
|
|
4249
4464
|
const onDisk = finalContent.trimEnd() + "\n";
|
|
4250
|
-
|
|
4465
|
+
const createPath = join(dir, "SKILL.md");
|
|
4466
|
+
let existsAtCommit = false;
|
|
4467
|
+
let taskRan = false;
|
|
4468
|
+
if (this.transact) await this.transact(this.io, createPath, (current) => {
|
|
4469
|
+
taskRan = true;
|
|
4470
|
+
if (current !== null) {
|
|
4471
|
+
existsAtCommit = true;
|
|
4472
|
+
return current;
|
|
4473
|
+
}
|
|
4474
|
+
return onDisk;
|
|
4475
|
+
});
|
|
4476
|
+
else if (await this.io.exists(createPath)) {
|
|
4477
|
+
taskRan = true;
|
|
4478
|
+
existsAtCommit = true;
|
|
4479
|
+
} else {
|
|
4480
|
+
taskRan = true;
|
|
4481
|
+
await this.io.writeText(createPath, onDisk);
|
|
4482
|
+
}
|
|
4483
|
+
if (!taskRan) return {
|
|
4484
|
+
ok: false,
|
|
4485
|
+
message: "internal error: the create transaction did not invoke the task; no file was written"
|
|
4486
|
+
};
|
|
4487
|
+
if (existsAtCommit) return {
|
|
4488
|
+
ok: false,
|
|
4489
|
+
message: `Skill "${normalized}" already exists.`
|
|
4490
|
+
};
|
|
4251
4491
|
if (origin !== "foreground") await this.io.writeText(markerPath(dir, "hermes-managed"), "");
|
|
4252
4492
|
await this.audit(normalized, "create", null, onDisk, "created");
|
|
4253
4493
|
this.notifyMutation({
|
|
@@ -4267,13 +4507,13 @@ var SkillLibrary = class {
|
|
|
4267
4507
|
return await this.serial(() => this.updateCore(name, content, origin));
|
|
4268
4508
|
}
|
|
4269
4509
|
async updateCore(name, content, origin) {
|
|
4270
|
-
const dir = this.dirOf(name);
|
|
4271
|
-
const path = join(dir, "SKILL.md");
|
|
4272
4510
|
const badName = this.badName(name);
|
|
4273
4511
|
if (badName) return {
|
|
4274
4512
|
ok: false,
|
|
4275
4513
|
message: badName
|
|
4276
4514
|
};
|
|
4515
|
+
const dir = this.dirOf(name);
|
|
4516
|
+
const path = join(dir, "SKILL.md");
|
|
4277
4517
|
const protection = await this.writeProtection(name, origin);
|
|
4278
4518
|
if (protection) return {
|
|
4279
4519
|
ok: false,
|
|
@@ -4493,9 +4733,140 @@ var SkillLibrary = class {
|
|
|
4493
4733
|
};
|
|
4494
4734
|
});
|
|
4495
4735
|
}
|
|
4736
|
+
/**
|
|
4737
|
+
* P2-9 (v15): the destructive directory move shared by archive and
|
|
4738
|
+
* restoreFromArchive — rename first, copy+remove fallback when the backend
|
|
4739
|
+
* cannot rename across media (V5-35), with the E-14 rollback when the
|
|
4740
|
+
* fallback's source removal fails. Returns a failure MESSAGE on a failed
|
|
4741
|
+
* move (caller wraps into a structured result) or undefined on success.
|
|
4742
|
+
*/
|
|
4743
|
+
async moveDir(dir, dest) {
|
|
4744
|
+
try {
|
|
4745
|
+
await this.io.rename(dir, dest);
|
|
4746
|
+
return;
|
|
4747
|
+
} catch {
|
|
4748
|
+
if (await this.io.exists(dest)) return "the destination appeared mid-move (concurrent create or restore); refusing to merge — inspect both trees";
|
|
4749
|
+
try {
|
|
4750
|
+
await this.io.copy(dir, dest);
|
|
4751
|
+
} catch (copyError) {
|
|
4752
|
+
return `the move fell back to copy but failed (${copyError instanceof Error ? copyError.message : String(copyError)}); the tree stays where it is`;
|
|
4753
|
+
}
|
|
4754
|
+
try {
|
|
4755
|
+
await this.io.remove(dir);
|
|
4756
|
+
return;
|
|
4757
|
+
} catch (error) {
|
|
4758
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
4759
|
+
try {
|
|
4760
|
+
await this.io.remove(dest);
|
|
4761
|
+
return `the copy succeeded but the source could not be removed (${reason}); the copied tree was rolled back`;
|
|
4762
|
+
} catch {
|
|
4763
|
+
return `the copy succeeded but the source could not be removed (${reason}) and the copied tree could not be rolled back — the tree now exists in BOTH locations; clean up manually`;
|
|
4764
|
+
}
|
|
4765
|
+
}
|
|
4766
|
+
}
|
|
4767
|
+
}
|
|
4768
|
+
/**
|
|
4769
|
+
* P2 (v16): the write-lock probe for the DESTRUCTIVE MOVERS (archive /
|
|
4770
|
+
* restoreFromArchive). A byte-writer mid-flight is the ghost-generator —
|
|
4771
|
+
* after the move its transact commit re-creates `<dir>/…` (mkdir
|
|
4772
|
+
* recursive) and the tree ends half-archived. The signal is the writer's
|
|
4773
|
+
* own lock file, and its PLACEMENT (inside the moved directory) is why the
|
|
4774
|
+
* mover must PROBE-and-REFUSE instead of acquiring it: an acquired lock
|
|
4775
|
+
* would be renamed into `.archive` with the tree, stranding a phantom live
|
|
4776
|
+
* lock (the v16 audit proved the probe→rename TOCTOU does exactly that,
|
|
4777
|
+
* and restore would later move the residue back into the live root).
|
|
4778
|
+
* Coverage: `SKILL.md.lock` (update/patch of the body) plus one level of
|
|
4779
|
+
* each support dir (write_file's lock sits next to its file). Residual:
|
|
4780
|
+
* NESTED support-subdir locks and the probe→rename TOCTOU itself remain
|
|
4781
|
+
* fail-safe (renameWithRetry rides the write out; the writer's locked
|
|
4782
|
+
* re-read refuses on the moved-away file), and a residue `.lock` from a
|
|
4783
|
+
* CRASHED writer also refuses — correct: inspect, don't archive.
|
|
4784
|
+
*/
|
|
4785
|
+
async hasWriteLock(dir) {
|
|
4786
|
+
const markerLocks = MARKER_LOCK_NAMES.map((name) => join(dir, name));
|
|
4787
|
+
for (const lock of markerLocks) if (await this.isWriterLock(lock)) return true;
|
|
4788
|
+
for (const supportDir of SUPPORT_DIRS) {
|
|
4789
|
+
let entries;
|
|
4790
|
+
try {
|
|
4791
|
+
entries = await this.io.list(join(dir, supportDir));
|
|
4792
|
+
} catch {
|
|
4793
|
+
return true;
|
|
4794
|
+
}
|
|
4795
|
+
for (const entry of entries) {
|
|
4796
|
+
if (!entry.endsWith(".lock")) continue;
|
|
4797
|
+
if (await this.isWriterLock(join(dir, supportDir, entry))) return true;
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
return false;
|
|
4801
|
+
}
|
|
4802
|
+
/** P2 (v17): a file only counts as a writer lock when its body has the
|
|
4803
|
+
* `pid:token` shape the io layer writes. User support files legitimately
|
|
4804
|
+
* named `*.lock` (allowed by SUPPORT_FILE_NAME_RE) must not trip the probe
|
|
4805
|
+
* or be swept as residue — the v16 first cut matched on suffix alone,
|
|
4806
|
+
* which permanently refused archiving and deleted user content on restore. */
|
|
4807
|
+
async isWriterLock(lockPath) {
|
|
4808
|
+
let body;
|
|
4809
|
+
try {
|
|
4810
|
+
body = await this.io.readText(lockPath);
|
|
4811
|
+
} catch {
|
|
4812
|
+
return true;
|
|
4813
|
+
}
|
|
4814
|
+
if (body === null) return false;
|
|
4815
|
+
return LOCK_BODY_RE.test(body.trim());
|
|
4816
|
+
}
|
|
4817
|
+
/** P2 (v16): best-effort removal of lock residue inside a RESTORED tree.
|
|
4818
|
+
* A1-2/A1-7 (v18): the sweep now covers the marker locks the probe checks
|
|
4819
|
+
* (`SKILL.md.lock`/`.pinned.lock`/`.hermes-managed.lock`) and only removes
|
|
4820
|
+
* a lock whose holder pid is NOT alive — a live writer's lock is never
|
|
4821
|
+
* stolen by the sweep. A dead-pid residue would otherwise permanently
|
|
4822
|
+
* refuse archive/restore. */
|
|
4823
|
+
async deleteStrandedLocks(dir) {
|
|
4824
|
+
await this.sweepLockIfStranded(join(dir, "SKILL.md.lock"));
|
|
4825
|
+
await this.sweepLockIfStranded(join(dir, ".pinned.lock"));
|
|
4826
|
+
await this.sweepLockIfStranded(join(dir, ".hermes-managed.lock"));
|
|
4827
|
+
for (const supportDir of SUPPORT_DIRS) {
|
|
4828
|
+
let entries = [];
|
|
4829
|
+
try {
|
|
4830
|
+
entries = await this.io.list(join(dir, supportDir));
|
|
4831
|
+
} catch {
|
|
4832
|
+
continue;
|
|
4833
|
+
}
|
|
4834
|
+
for (const entry of entries) if (entry.endsWith(".lock")) await this.sweepLockIfStranded(join(dir, supportDir, entry));
|
|
4835
|
+
}
|
|
4836
|
+
}
|
|
4837
|
+
/** Remove `lockPath` only when its body has the writer-lock `pid:token`
|
|
4838
|
+
* shape AND the holder pid is not alive; anything else (a user support file
|
|
4839
|
+
* or a live writer's lock) is left untouched. */
|
|
4840
|
+
async sweepLockIfStranded(lockPath) {
|
|
4841
|
+
const body = await this.io.readText(lockPath).catch(() => null);
|
|
4842
|
+
if (body === null) return;
|
|
4843
|
+
const match = /^(\d+):[0-9a-f]*$/.exec(body.trim());
|
|
4844
|
+
if (match === null) return;
|
|
4845
|
+
const pid = Number(match[1]);
|
|
4846
|
+
if (Number.isInteger(pid) && pid > 0 && isProcessAlive(pid)) return;
|
|
4847
|
+
await this.io.remove(lockPath).catch(() => {});
|
|
4848
|
+
}
|
|
4849
|
+
/** A1-4 (v18): a manifest-declared name is copied with `join(root, name)`;
|
|
4850
|
+
* only a single, non-traversing path component is safe. Dotfiles
|
|
4851
|
+
* (`.usage.json`) stay allowed — sidecars are legitimately dot-prefixed. */
|
|
4852
|
+
safeSnapshotEntryName(name) {
|
|
4853
|
+
return name !== "" && name !== "." && name !== ".." && !name.includes("/") && !name.includes("\\") && basename(name) === name;
|
|
4854
|
+
}
|
|
4855
|
+
/** A1-7 (v18): a root-level lock whose holder is alive must refuse the
|
|
4856
|
+
* restore; a dead residue is swept so a crashed writer cannot block
|
|
4857
|
+
* recovery. A non-lock body shape is left alone (user file). */
|
|
4858
|
+
async refuseLiveLockOrSweep(lockPath, label) {
|
|
4859
|
+
const body = await this.io.readText(lockPath).catch(() => null);
|
|
4860
|
+
if (body === null) return;
|
|
4861
|
+
const match = /^(\d+):[0-9a-f]*$/.exec(body.trim());
|
|
4862
|
+
if (match === null) return;
|
|
4863
|
+
const pid = Number(match[1]);
|
|
4864
|
+
if (Number.isInteger(pid) && pid > 0 && isProcessAlive(pid)) throw new Error(`snapshot restore refused: ${label} is being written (write lock present); retry once the write completes`);
|
|
4865
|
+
await this.io.remove(lockPath).catch(() => {});
|
|
4866
|
+
}
|
|
4496
4867
|
async archive(rawName, options = {}) {
|
|
4497
4868
|
const name = rawName.trim();
|
|
4498
|
-
const badName = this.badName(name);
|
|
4869
|
+
const badName = this.badName(name, { allowReserved: true });
|
|
4499
4870
|
if (badName) return {
|
|
4500
4871
|
ok: false,
|
|
4501
4872
|
message: badName
|
|
@@ -4516,6 +4887,11 @@ var SkillLibrary = class {
|
|
|
4516
4887
|
ok: false,
|
|
4517
4888
|
message: "absorbed_into cannot be the skill being archived (cannot absorb into itself)."
|
|
4518
4889
|
};
|
|
4890
|
+
const intoBad = this.badName(options.absorbedInto.trim());
|
|
4891
|
+
if (intoBad) return {
|
|
4892
|
+
ok: false,
|
|
4893
|
+
message: `absorbed_into: ${intoBad}`
|
|
4894
|
+
};
|
|
4519
4895
|
if (!await this.io.readText(join(this.dirOf(options.absorbedInto), "SKILL.md"))) return {
|
|
4520
4896
|
ok: false,
|
|
4521
4897
|
message: `absorbed_into="${options.absorbedInto}" does not exist.`
|
|
@@ -4534,35 +4910,15 @@ var SkillLibrary = class {
|
|
|
4534
4910
|
message: `Skill "${name}" is a symlink; refusing to archive it.`
|
|
4535
4911
|
};
|
|
4536
4912
|
}
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
};
|
|
4547
|
-
}
|
|
4548
|
-
try {
|
|
4549
|
-
await this.io.remove(dir);
|
|
4550
|
-
} catch (error) {
|
|
4551
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
4552
|
-
try {
|
|
4553
|
-
await this.io.remove(dest);
|
|
4554
|
-
return {
|
|
4555
|
-
ok: false,
|
|
4556
|
-
message: `Archive copy succeeded but the source could not be removed (${reason}); the copied archive was rolled back.`
|
|
4557
|
-
};
|
|
4558
|
-
} catch {
|
|
4559
|
-
return {
|
|
4560
|
-
ok: false,
|
|
4561
|
-
message: `Archive copy succeeded but the source could not be removed (${reason}) and the archive copy could not be rolled back — the skill now exists in BOTH the active root and .archive; clean up manually.`
|
|
4562
|
-
};
|
|
4563
|
-
}
|
|
4564
|
-
}
|
|
4565
|
-
}
|
|
4913
|
+
if (await this.hasWriteLock(dir)) return {
|
|
4914
|
+
ok: false,
|
|
4915
|
+
message: `Skill "${name}" is being written (write lock present); retry archiving once the write completes.`
|
|
4916
|
+
};
|
|
4917
|
+
const moveFailure = await this.moveDir(dir, dest);
|
|
4918
|
+
if (moveFailure !== void 0) return {
|
|
4919
|
+
ok: false,
|
|
4920
|
+
message: `Skill "${name}" archive failed: ${moveFailure}.`
|
|
4921
|
+
};
|
|
4566
4922
|
const reason = options.reason ?? (options.absorbedInto ? `Consolidated into ${options.absorbedInto}` : "Archived by self-evolution curator");
|
|
4567
4923
|
try {
|
|
4568
4924
|
await this.io.writeText(join(dest, ".archive-reason"), `${(/* @__PURE__ */ new Date()).toISOString()}: ${reason}\n`);
|
|
@@ -4792,9 +5148,10 @@ var SkillLibrary = class {
|
|
|
4792
5148
|
ok: false,
|
|
4793
5149
|
message: "Every restructure move needs a non-empty heading."
|
|
4794
5150
|
};
|
|
4795
|
-
|
|
5151
|
+
const targetIssue = validateRestructureTarget(move.toFile);
|
|
5152
|
+
if (targetIssue) return {
|
|
4796
5153
|
ok: false,
|
|
4797
|
-
message:
|
|
5154
|
+
message: targetIssue
|
|
4798
5155
|
};
|
|
4799
5156
|
}
|
|
4800
5157
|
const dir = this.dirOf(name);
|
|
@@ -4875,7 +5232,7 @@ var SkillLibrary = class {
|
|
|
4875
5232
|
*/
|
|
4876
5233
|
async applyTreeChange(plan) {
|
|
4877
5234
|
const name = plan.name.trim();
|
|
4878
|
-
const badName = this.badName(name);
|
|
5235
|
+
const badName = this.badName(name, { allowReserved: true });
|
|
4879
5236
|
if (badName) return {
|
|
4880
5237
|
ok: false,
|
|
4881
5238
|
message: badName
|
|
@@ -4891,16 +5248,17 @@ var SkillLibrary = class {
|
|
|
4891
5248
|
ok: false,
|
|
4892
5249
|
message: `Skill "${name}" is protected (${protection}).`
|
|
4893
5250
|
};
|
|
4894
|
-
for (const precondition of plan.preconditions ?? []) {
|
|
4895
|
-
const issue = await precondition({ dir });
|
|
4896
|
-
if (issue) return {
|
|
4897
|
-
ok: false,
|
|
4898
|
-
message: issue
|
|
4899
|
-
};
|
|
4900
|
-
}
|
|
4901
5251
|
const landing = [];
|
|
4902
5252
|
for (const write of plan.writes) {
|
|
4903
|
-
|
|
5253
|
+
let previous;
|
|
5254
|
+
try {
|
|
5255
|
+
previous = await this.io.readText(write.target);
|
|
5256
|
+
} catch (error) {
|
|
5257
|
+
return {
|
|
5258
|
+
ok: false,
|
|
5259
|
+
message: `Tree change refused: cannot safely pre-read ${write.target} (${error instanceof Error ? error.message : String(error)}); no writes were performed`
|
|
5260
|
+
};
|
|
5261
|
+
}
|
|
4904
5262
|
if (Buffer.byteLength(write.content, "utf8") > this.limits.maxSkillFileBytes) return {
|
|
4905
5263
|
ok: false,
|
|
4906
5264
|
message: `Write exceeds ${this.limits.maxSkillFileBytes} bytes: ${write.target}`
|
|
@@ -4916,18 +5274,16 @@ var SkillLibrary = class {
|
|
|
4916
5274
|
previous
|
|
4917
5275
|
});
|
|
4918
5276
|
}
|
|
4919
|
-
const semantic = plan.validate?.({
|
|
4920
|
-
dir,
|
|
4921
|
-
currentMd: md
|
|
4922
|
-
}) ?? null;
|
|
4923
|
-
if (semantic) return {
|
|
4924
|
-
ok: false,
|
|
4925
|
-
message: semantic
|
|
4926
|
-
};
|
|
4927
5277
|
const written = [];
|
|
5278
|
+
let durabilityWarning = "";
|
|
4928
5279
|
try {
|
|
4929
5280
|
for (const entry of landing) {
|
|
4930
|
-
|
|
5281
|
+
try {
|
|
5282
|
+
await this.io.writeText(entry.target, entry.content);
|
|
5283
|
+
} catch (error) {
|
|
5284
|
+
if (error?.committed !== true) throw error;
|
|
5285
|
+
durabilityWarning = error instanceof Error ? error.message : String(error);
|
|
5286
|
+
}
|
|
4931
5287
|
written.push({
|
|
4932
5288
|
target: entry.target,
|
|
4933
5289
|
previous: entry.previous
|
|
@@ -4948,7 +5304,7 @@ var SkillLibrary = class {
|
|
|
4948
5304
|
});
|
|
4949
5305
|
return {
|
|
4950
5306
|
ok: true,
|
|
4951
|
-
message: `${plan.eventAction} "${name}" succeeded
|
|
5307
|
+
message: durabilityWarning === "" ? `${plan.eventAction} "${name}" succeeded.` : `${plan.eventAction} "${name}" succeeded (warning: the write landed but the directory fsync failed — durability unconfirmed: ${durabilityWarning})`,
|
|
4952
5308
|
path: dir
|
|
4953
5309
|
};
|
|
4954
5310
|
}
|
|
@@ -4959,14 +5315,15 @@ var SkillLibrary = class {
|
|
|
4959
5315
|
*/
|
|
4960
5316
|
async restoreFromArchive(rawName) {
|
|
4961
5317
|
const name = rawName.trim();
|
|
4962
|
-
const bad = this.badName(name);
|
|
5318
|
+
const bad = this.badName(name, { allowReserved: true });
|
|
4963
5319
|
if (bad) return {
|
|
4964
5320
|
ok: false,
|
|
4965
5321
|
message: bad
|
|
4966
5322
|
};
|
|
4967
|
-
|
|
5323
|
+
const dest = this.dirOf(name);
|
|
5324
|
+
if (await this.io.exists(dest)) return {
|
|
4968
5325
|
ok: false,
|
|
4969
|
-
message: `Skill "${name}" already exists in the active root; refusing to overwrite.`
|
|
5326
|
+
message: await this.io.exists(join(dest, "SKILL.md")) ? `Skill "${name}" already exists in the active root; refusing to overwrite.` : `Skill directory "${name}" already exists in the active root but carries no SKILL.md; remove or repair it before restoring.`
|
|
4970
5327
|
};
|
|
4971
5328
|
const archiveRoot = join(this.root, ".archive");
|
|
4972
5329
|
let entries;
|
|
@@ -4989,27 +5346,24 @@ var SkillLibrary = class {
|
|
|
4989
5346
|
message: `Skill "${name}" is not in .archive.`
|
|
4990
5347
|
};
|
|
4991
5348
|
const source = join(archiveRoot, chosen);
|
|
4992
|
-
const dest = this.dirOf(name);
|
|
4993
5349
|
if (this.io.isSymlink) {
|
|
4994
5350
|
if (await this.io.isSymlink(source) === true) return {
|
|
4995
5351
|
ok: false,
|
|
4996
5352
|
message: `Archived entry "${chosen}" is a symlink; refusing to restore it.`
|
|
4997
5353
|
};
|
|
4998
5354
|
}
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
};
|
|
5010
|
-
}
|
|
5011
|
-
}
|
|
5355
|
+
if (await this.hasWriteLock(dest)) return {
|
|
5356
|
+
ok: false,
|
|
5357
|
+
message: `Skill "${name}" is being written (write lock present); retry restoring once the write completes.`
|
|
5358
|
+
};
|
|
5359
|
+
const moveFailure = await this.moveDir(source, dest);
|
|
5360
|
+
if (moveFailure !== void 0) return {
|
|
5361
|
+
ok: false,
|
|
5362
|
+
message: `Restore of "${name}" from .archive failed: ${moveFailure}`
|
|
5363
|
+
};
|
|
5364
|
+
await this.deleteStrandedLocks(dest);
|
|
5012
5365
|
if (await this.io.exists(join(dest, ".archive-reason"))) await this.io.remove(join(dest, ".archive-reason"));
|
|
5366
|
+
await this.audit(name, "restore", null, await this.io.readText(join(dest, "SKILL.md")).catch(() => null), `restored from ${source}`);
|
|
5013
5367
|
this.notifyMutation({
|
|
5014
5368
|
action: "restore",
|
|
5015
5369
|
name,
|
|
@@ -5026,12 +5380,12 @@ var SkillLibrary = class {
|
|
|
5026
5380
|
return await this.serial(() => this.writeSupportFileCore(name, filePath, content, origin));
|
|
5027
5381
|
}
|
|
5028
5382
|
async writeSupportFileCore(name, filePath, content, origin) {
|
|
5029
|
-
const dir = this.dirOf(name);
|
|
5030
5383
|
const badName = this.badName(name);
|
|
5031
5384
|
if (badName) return {
|
|
5032
5385
|
ok: false,
|
|
5033
5386
|
message: badName
|
|
5034
5387
|
};
|
|
5388
|
+
const dir = this.dirOf(name);
|
|
5035
5389
|
if (!await this.io.exists(join(dir, "SKILL.md"))) return {
|
|
5036
5390
|
ok: false,
|
|
5037
5391
|
message: `Skill "${name}" not found.`
|
|
@@ -5120,7 +5474,12 @@ var SkillLibrary = class {
|
|
|
5120
5474
|
message: `File "${filePath}" not found in skill "${name}".`
|
|
5121
5475
|
};
|
|
5122
5476
|
const before = await this.io.readText(target).catch(() => null);
|
|
5123
|
-
|
|
5477
|
+
if (before === null) return {
|
|
5478
|
+
ok: false,
|
|
5479
|
+
message: `"${filePath}" is not a readable regular file — remove the files inside it one by one.`
|
|
5480
|
+
};
|
|
5481
|
+
if (this.transact) await this.transact(this.io, target, () => null);
|
|
5482
|
+
else await this.io.remove(target);
|
|
5124
5483
|
await this.audit(name, "remove_file", before, null, `removed ${filePath}`);
|
|
5125
5484
|
this.notifyMutation({
|
|
5126
5485
|
action: "remove_file",
|
|
@@ -5147,9 +5506,10 @@ var SkillLibrary = class {
|
|
|
5147
5506
|
while (await this.io.exists(dest)) dest = join(backupRoot, `skills-${stamp}-${Math.random().toString(36).slice(2, 8)}`);
|
|
5148
5507
|
try {
|
|
5149
5508
|
const names = await listNames(this.root, this.io);
|
|
5150
|
-
await Promise.
|
|
5509
|
+
const copyFailure = (await Promise.allSettled(names.map(async (name) => {
|
|
5151
5510
|
await this.io.copy(this.dirOf(name), join(dest, name));
|
|
5152
|
-
}));
|
|
5511
|
+
}))).find((result) => result.status === "rejected");
|
|
5512
|
+
if (copyFailure) throw copyFailure.reason;
|
|
5153
5513
|
const sidecars = [];
|
|
5154
5514
|
for (const sidecar of [usageFile(this.root), suppressedFile(this.root)]) if (await this.io.exists(sidecar)) {
|
|
5155
5515
|
const name = basename(sidecar);
|
|
@@ -5164,9 +5524,10 @@ var SkillLibrary = class {
|
|
|
5164
5524
|
}
|
|
5165
5525
|
const validExtras = extras.filter((extra) => SNAPSHOT_EXTRA_NAME_RE.test(extra.name));
|
|
5166
5526
|
const extraNames = validExtras.map((extra) => extra.name);
|
|
5167
|
-
await Promise.
|
|
5527
|
+
const extraFailure = (await Promise.allSettled(validExtras.map(async (extra) => {
|
|
5168
5528
|
await this.io.writeText(join(dest, "extras", extra.name), extra.content);
|
|
5169
|
-
}));
|
|
5529
|
+
}))).find((result) => result.status === "rejected");
|
|
5530
|
+
if (extraFailure) throw extraFailure.reason;
|
|
5170
5531
|
await this.io.writeText(join(dest, "manifest.json"), JSON.stringify({
|
|
5171
5532
|
reason,
|
|
5172
5533
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -5188,10 +5549,11 @@ var SkillLibrary = class {
|
|
|
5188
5549
|
if (raw === null) return null;
|
|
5189
5550
|
try {
|
|
5190
5551
|
const manifest = JSON.parse(raw);
|
|
5552
|
+
if (!Array.isArray(manifest.skills)) return null;
|
|
5191
5553
|
return {
|
|
5192
5554
|
reason: typeof manifest.reason === "string" ? manifest.reason : "",
|
|
5193
5555
|
createdAt: typeof manifest.createdAt === "string" ? manifest.createdAt : "",
|
|
5194
|
-
skills:
|
|
5556
|
+
skills: manifest.skills,
|
|
5195
5557
|
sidecars: Array.isArray(manifest.sidecars) ? manifest.sidecars : [],
|
|
5196
5558
|
...typeof manifest.hasArchive === "boolean" ? { hasArchive: manifest.hasArchive } : {},
|
|
5197
5559
|
extras: Array.isArray(manifest.extras) ? manifest.extras : []
|
|
@@ -5226,6 +5588,7 @@ var SkillLibrary = class {
|
|
|
5226
5588
|
reason: manifest.reason
|
|
5227
5589
|
});
|
|
5228
5590
|
}
|
|
5591
|
+
out.sort((a, b) => (b.createdAt || "").localeCompare(a.createdAt || "") || b.path.localeCompare(a.path));
|
|
5229
5592
|
return out;
|
|
5230
5593
|
}
|
|
5231
5594
|
/**
|
|
@@ -5296,6 +5659,15 @@ var SkillLibrary = class {
|
|
|
5296
5659
|
* restoreLatestSnapshot so a failed restore can roll itself back (E-13).
|
|
5297
5660
|
*/
|
|
5298
5661
|
async restoreSnapshotIntoRoot(snapshotPath) {
|
|
5662
|
+
const manifest = await this.readSnapshotManifest(snapshotPath);
|
|
5663
|
+
if (manifest === null) {
|
|
5664
|
+
if (await this.io.exists(join(snapshotPath, "manifest.json"))) throw new Error(`snapshot ${snapshotPath} has an unreadable manifest.json; refusing to clear the active tree`);
|
|
5665
|
+
} else {
|
|
5666
|
+
for (const name of [...manifest.skills, ...manifest.sidecars]) if (!this.safeSnapshotEntryName(name)) throw new Error(`snapshot ${snapshotPath} declares an unsafe entry name ${JSON.stringify(name)}; refusing to restore`);
|
|
5667
|
+
if (manifest.skills.length === 0) {
|
|
5668
|
+
if ((await this.io.list(snapshotPath)).some((entry) => entry !== "manifest.json" && entry !== "extras" && entry !== ".archive")) throw new Error(`snapshot ${snapshotPath} declares no skills but contains entries; refusing to clear the active tree`);
|
|
5669
|
+
}
|
|
5670
|
+
}
|
|
5299
5671
|
let rootEntries;
|
|
5300
5672
|
try {
|
|
5301
5673
|
rootEntries = await this.io.list(this.root);
|
|
@@ -5304,13 +5676,24 @@ var SkillLibrary = class {
|
|
|
5304
5676
|
}
|
|
5305
5677
|
for (const entry of rootEntries) {
|
|
5306
5678
|
if (entry === ".archive" || entry === ".backups" || entry === ".mutations.json" || entry === ".curator-suppressed.json") continue;
|
|
5307
|
-
|
|
5679
|
+
if (entry.endsWith(".lock") || entry.endsWith(`.lock.next`)) {
|
|
5680
|
+
await this.refuseLiveLockOrSweep(join(this.root, entry), entry);
|
|
5681
|
+
continue;
|
|
5682
|
+
}
|
|
5683
|
+
const dir = join(this.root, entry);
|
|
5684
|
+
if (await this.io.exists(join(dir, "SKILL.md"))) {
|
|
5685
|
+
await this.deleteStrandedLocks(dir);
|
|
5686
|
+
if (await this.hasWriteLock(dir)) throw new Error(`snapshot restore refused: skill "${entry}" is being written (write lock present); retry once the write completes`);
|
|
5687
|
+
}
|
|
5308
5688
|
}
|
|
5309
|
-
const
|
|
5310
|
-
|
|
5311
|
-
if (entry === "
|
|
5312
|
-
|
|
5689
|
+
const restoresSuppressed = manifest !== null && manifest.sidecars.includes(".curator-suppressed.json");
|
|
5690
|
+
for (const entry of rootEntries) {
|
|
5691
|
+
if (entry === ".archive" || entry === ".backups" || entry === ".mutations.json") continue;
|
|
5692
|
+
if (entry === ".curator-suppressed.json" && restoresSuppressed) continue;
|
|
5693
|
+
if (entry.endsWith(".lock") || entry.endsWith(`.lock.next`)) continue;
|
|
5694
|
+
await this.io.remove(join(this.root, entry));
|
|
5313
5695
|
}
|
|
5696
|
+
if (manifest === null) throw new Error(`snapshot ${snapshotPath} has no readable manifest.json; refusing to restore`);
|
|
5314
5697
|
else {
|
|
5315
5698
|
for (const name of manifest.skills) await this.io.copy(join(snapshotPath, name), join(this.root, name));
|
|
5316
5699
|
for (const sidecar of manifest.sidecars) await this.io.copy(join(snapshotPath, sidecar), join(this.root, sidecar));
|
|
@@ -5320,7 +5703,11 @@ var SkillLibrary = class {
|
|
|
5320
5703
|
await this.io.copy(join(snapshotPath, ".archive"), archiveRoot);
|
|
5321
5704
|
} else if (manifest.hasArchive === false) await this.io.remove(archiveRoot);
|
|
5322
5705
|
}
|
|
5706
|
+
for (const entry of await this.io.list(this.root)) {
|
|
5707
|
+
if (entry.startsWith(".")) continue;
|
|
5708
|
+
await this.deleteStrandedLocks(join(this.root, entry));
|
|
5709
|
+
}
|
|
5323
5710
|
}
|
|
5324
5711
|
};
|
|
5325
5712
|
//#endregion
|
|
5326
|
-
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_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, 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, allowRowCollisions, 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 };
|
|
5713
|
+
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_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, 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, LOCK_BODY_RE, LOCK_SUFFIX, 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, allowRowCollisions, 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, evolutionEventPayloadIssue, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterYamlUnsafeValues, getRecord, isProcessAlive, 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, resolveRootConfig, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, validateRestructureTarget, verifyPromptBundle, writeDurableTmp, yamlPlainScalarNeedsQuotes };
|