@lmzhen/dsh-evolution-core 0.3.81 → 0.3.82
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 +673 -480
- package/lib/types/drift-signals.d.ts +3 -2
- package/lib/types/events.d.ts +2 -1
- package/lib/types/evolution-events.d.ts +24 -15
- package/lib/types/frontmatter.d.ts +169 -0
- package/lib/types/fuzzy-match.d.ts +18 -0
- package/lib/types/gates.d.ts +3 -2
- package/lib/types/instance-scope.d.ts +21 -2
- package/lib/types/io.d.ts +31 -2
- package/lib/types/limits.d.ts +15 -0
- package/lib/types/opt-in.d.ts +34 -4
- package/lib/types/skill-health.d.ts +3 -2
- package/lib/types/skill-store.d.ts +5 -156
- package/lib/types/threats.d.ts +3 -2
- package/lib/types/tool-dispatch.d.ts +12 -2
- package/lib/types/usage.d.ts +2 -1
- package/lib/types/write-inventory.d.ts +15 -3
- package/package.json +1 -1
- package/persisted-write-inventory.json +3 -4
package/lib/index.js
CHANGED
|
@@ -6,6 +6,79 @@ import { readFileSync } from "node:fs";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { scopeOf } from "@deepseek-ai/dsh-scope";
|
|
8
8
|
import { load } from "js-yaml";
|
|
9
|
+
//#region lib/types/probe.js
|
|
10
|
+
function probePresent(value) {
|
|
11
|
+
return {
|
|
12
|
+
kind: "present",
|
|
13
|
+
value
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function probeAbsent() {
|
|
17
|
+
return { kind: "absent" };
|
|
18
|
+
}
|
|
19
|
+
function probeUnknown(reason) {
|
|
20
|
+
return {
|
|
21
|
+
kind: "unknown",
|
|
22
|
+
reason
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function isPresent(probe) {
|
|
26
|
+
return probe.kind === "present";
|
|
27
|
+
}
|
|
28
|
+
function isAbsent(probe) {
|
|
29
|
+
return probe.kind === "absent";
|
|
30
|
+
}
|
|
31
|
+
function isUnknown(probe) {
|
|
32
|
+
return probe.kind === "unknown";
|
|
33
|
+
}
|
|
34
|
+
/** Only a PRESENT probe yields a value; absent and unknown both fall back. */
|
|
35
|
+
function valueOr(probe, fallback) {
|
|
36
|
+
return probe.kind === "present" ? probe.value : fallback;
|
|
37
|
+
}
|
|
38
|
+
function mapProbe(probe, transform) {
|
|
39
|
+
return probe.kind === "present" ? probePresent(transform(probe.value)) : probe;
|
|
40
|
+
}
|
|
41
|
+
/** The reason an `unknown` probe carries (message, never a bare String(object)). */
|
|
42
|
+
function probeReason(error) {
|
|
43
|
+
return error instanceof Error ? error.message : String(error);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* ENOENT/ENOTDIR are the ONE read failure that means "it is not there"; every
|
|
47
|
+
* other failure is an IO error and stays unknown (same split as the node
|
|
48
|
+
* backend's own isMissing, V8-23⑨ for the size probe).
|
|
49
|
+
*/
|
|
50
|
+
function isMissingPath(error) {
|
|
51
|
+
const code = error?.code;
|
|
52
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Three-state directory listing. The node backend already answers `[]` for a
|
|
56
|
+
* MISSING directory (its own rc.50 P2-4 contract) and THROWS for an unreadable
|
|
57
|
+
* one, so the value this adds is the second half: a backend that throws ENOENT
|
|
58
|
+
* reads as absent, an EACCES/EIO reads as unknown — where a bare
|
|
59
|
+
* `catch { return [] }` served a broken store as an empty one (the N14 class).
|
|
60
|
+
*/
|
|
61
|
+
async function probeList(io, dir) {
|
|
62
|
+
try {
|
|
63
|
+
return probePresent(await io.list(dir));
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return isMissingPath(error) ? probeAbsent() : probeUnknown(probeReason(error));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Three-state mtime read. Absent covers both "the path is missing" and "this
|
|
70
|
+
* backend has no mtime probe" — the seam cannot tell those apart, so consumers
|
|
71
|
+
* that must know say so in their own log line. A stat that FAILS is unknown.
|
|
72
|
+
*/
|
|
73
|
+
async function probeMtime(io, path) {
|
|
74
|
+
try {
|
|
75
|
+
const value = await io.mtime?.(path) ?? null;
|
|
76
|
+
return value === null ? probeAbsent() : probePresent(value);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return probeUnknown(probeReason(error));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
9
82
|
//#region lib/types/io.js
|
|
10
83
|
/**
|
|
11
84
|
* Structural IO seam for the evolution plugin family.
|
|
@@ -40,6 +113,21 @@ async function transactIo(io, path, task) {
|
|
|
40
113
|
console.warn(`evolution-io: ${path} was written but its directory fsync failed — the bytes are visible, durability is unconfirmed: ${error instanceof Error ? error.message : String(error)}`);
|
|
41
114
|
}
|
|
42
115
|
}
|
|
116
|
+
/** Build a {@link TransactTaskGuard} for one write path. See its doc. */
|
|
117
|
+
function transactTaskGuard(what) {
|
|
118
|
+
let invoked = false;
|
|
119
|
+
return {
|
|
120
|
+
wrap: (task) => (current) => {
|
|
121
|
+
invoked = true;
|
|
122
|
+
return task(current);
|
|
123
|
+
},
|
|
124
|
+
invoked: () => invoked,
|
|
125
|
+
assertInvoked: () => {
|
|
126
|
+
if (invoked) return;
|
|
127
|
+
throw new Error(`internal error: the write transaction for ${what} did not invoke the task; no write was performed`);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
43
131
|
/** Lazy adapter over an IO provider registry, shared by every evolution consumer. */
|
|
44
132
|
function evolutionIoAdapter(provider) {
|
|
45
133
|
return {
|
|
@@ -324,10 +412,17 @@ function decideTakeover(probe) {
|
|
|
324
412
|
* exceed the default budget and fail loud.
|
|
325
413
|
*/
|
|
326
414
|
function nodeEvolutionIo(lockAttempts = 40) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
415
|
+
/**
|
|
416
|
+
* v43 S1-4: delegates to the canonical `isMissingPath` (probe.ts) — one
|
|
417
|
+
* definition for the whole family instead of a second copy of the same two
|
|
418
|
+
* codes. EISDIR deliberately stays OUT of that predicate: a directory
|
|
419
|
+
* squatting on a file path is not "absent" — rotation and event reads must
|
|
420
|
+
* still see it as malformed (rc.72 G-2), while the SkillLibrary.read boundary
|
|
421
|
+
* absorbs EISDIR into "absent" for its own surface (E-43).
|
|
422
|
+
* @param error - the caught read failure.
|
|
423
|
+
* @returns true only for ENOENT/ENOTDIR.
|
|
424
|
+
*/
|
|
425
|
+
const isMissing = (error) => isMissingPath(error);
|
|
331
426
|
/** True when the pid is alive (single source: `isProcessAlive`). */
|
|
332
427
|
const isAlive = isProcessAlive;
|
|
333
428
|
/**
|
|
@@ -425,7 +520,15 @@ function nodeEvolutionIo(lockAttempts = 40) {
|
|
|
425
520
|
});
|
|
426
521
|
if (decision !== "none") {
|
|
427
522
|
console.warn(`evolution-io: taking over write lock ${lock} (branch=stale${decision[0]?.toUpperCase()}${decision.slice(1)}, body=${JSON.stringify(holderContent)}, ageMs=${Date.now() - st.mtimeMs}, holderPid=${Number.isInteger(holder) && holder > 0 ? holder : "none"})`);
|
|
428
|
-
|
|
523
|
+
const currentRead = await readFile(lock, "utf8").then((body) => ({
|
|
524
|
+
ok: true,
|
|
525
|
+
body
|
|
526
|
+
}), () => ({
|
|
527
|
+
ok: false,
|
|
528
|
+
body: ""
|
|
529
|
+
}));
|
|
530
|
+
if (!currentRead.ok) continue;
|
|
531
|
+
if (currentRead.body === holderContent) {
|
|
429
532
|
const ticket = `${lock}.next`;
|
|
430
533
|
try {
|
|
431
534
|
const ticketBody = await readFile(ticket, "utf8").catch(() => "");
|
|
@@ -509,9 +612,7 @@ function nodeEvolutionIo(lockAttempts = 40) {
|
|
|
509
612
|
force: true,
|
|
510
613
|
maxRetries: 20,
|
|
511
614
|
retryDelay: 100
|
|
512
|
-
}).catch(
|
|
513
|
-
await recordPendingSelfCleanup(lock, await readFile(lock, "utf8").catch(() => ""));
|
|
514
|
-
});
|
|
615
|
+
}).catch(() => recordPendingSelfCleanup(lock, myClaim));
|
|
515
616
|
}
|
|
516
617
|
}
|
|
517
618
|
throw new Error(`could not acquire write lock for ${path} after ${lockAttempts} attempts`);
|
|
@@ -773,7 +874,9 @@ async function mutateUsage(root, io, task, options = {}) {
|
|
|
773
874
|
const record = parsed;
|
|
774
875
|
const isMalformed = (value) => value === null || typeof value !== "object" || Array.isArray(value);
|
|
775
876
|
if (typeof record.version === "number" && record.version > 1) {
|
|
776
|
-
|
|
877
|
+
const message = `usage sidecar ${usageFile(root)} carries schema version ${String(record.version)} (> this runtime) — writes stay frozen and the bytes preserved until the runtime is upgraded`;
|
|
878
|
+
if (options.onQuarantine !== void 0) options.onQuarantine(message);
|
|
879
|
+
else console.warn(message);
|
|
777
880
|
shapePreserved = true;
|
|
778
881
|
} else if (Object.values(record).some(isMalformed)) {
|
|
779
882
|
const bad = Object.keys(record).filter((key) => isMalformed(record[key]));
|
|
@@ -928,7 +1031,7 @@ function parseSuppressed(raw) {
|
|
|
928
1031
|
}
|
|
929
1032
|
}
|
|
930
1033
|
async function saveSuppressedNames(root, names, io = nodeEvolutionIo()) {
|
|
931
|
-
const current = await io.readText(suppressedFile(root))
|
|
1034
|
+
const current = await io.readText(suppressedFile(root));
|
|
932
1035
|
if (current !== null) try {
|
|
933
1036
|
const parsed = JSON.parse(current);
|
|
934
1037
|
if (parsed !== null && typeof parsed.version === "number" && parsed.version > 1) {
|
|
@@ -1641,7 +1744,15 @@ async function rotateIfDue(io, path, events, rotateAt) {
|
|
|
1641
1744
|
const anchor = tail[0]?.seq ?? 0;
|
|
1642
1745
|
const archivePath = join(dirname(path), `${EVENT_ARCHIVE_PREFIX}${anchor - 1}.json`);
|
|
1643
1746
|
let archived = head;
|
|
1644
|
-
|
|
1747
|
+
let existing;
|
|
1748
|
+
try {
|
|
1749
|
+
existing = await io.readText(archivePath);
|
|
1750
|
+
} catch (error) {
|
|
1751
|
+
return {
|
|
1752
|
+
ok: false,
|
|
1753
|
+
reason: `evolution event archive collision at ${archivePath} could not be read (${error instanceof Error ? error.message : String(error)}) and was not touched`
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1645
1756
|
if (existing !== null) {
|
|
1646
1757
|
let parsed = null;
|
|
1647
1758
|
try {
|
|
@@ -1716,18 +1827,20 @@ async function pruneCollideArchives(io, path) {
|
|
|
1716
1827
|
if (stamp && now - Number(stamp[1]) < COLLIDE_AGE_MS) continue;
|
|
1717
1828
|
if (!stamp) try {
|
|
1718
1829
|
const mtime = await io.mtime?.(full);
|
|
1719
|
-
if (typeof mtime
|
|
1830
|
+
if (typeof mtime !== "number" || now - mtime < COLLIDE_AGE_MS) continue;
|
|
1720
1831
|
} catch {
|
|
1721
1832
|
continue;
|
|
1722
1833
|
}
|
|
1723
1834
|
await io.remove(full).catch(() => {});
|
|
1724
1835
|
}
|
|
1725
1836
|
}
|
|
1726
|
-
/** Read the event log; a missing/whitespace-only file reads as empty,
|
|
1727
|
-
*
|
|
1728
|
-
*
|
|
1729
|
-
*
|
|
1730
|
-
*
|
|
1837
|
+
/** Read the event log; a missing/whitespace-only file reads as empty, corrupt
|
|
1838
|
+
* content is flagged (and refused on append). A well-formed future-version body
|
|
1839
|
+
* is v1-incompatible: it reads as EMPTY and is now flagged malformed as well
|
|
1840
|
+
* (C-events-dispatch-1, v43). F-338's own guarantees are untouched — the reader
|
|
1841
|
+
* never mis-shapes a newer format and the append path refuses it up front, so
|
|
1842
|
+
* the original bytes survive — while the flag reports what the old reader hid:
|
|
1843
|
+
* every record that body holds is dropped from this read. */
|
|
1731
1844
|
async function readEvolutionEvents(io, path) {
|
|
1732
1845
|
let raw;
|
|
1733
1846
|
try {
|
|
@@ -1746,7 +1859,7 @@ async function readEvolutionEvents(io, path) {
|
|
|
1746
1859
|
const parsed = JSON.parse(raw);
|
|
1747
1860
|
if (parsed.version !== void 0 && parsed.version !== 1) return {
|
|
1748
1861
|
events: [],
|
|
1749
|
-
malformed:
|
|
1862
|
+
malformed: true
|
|
1750
1863
|
};
|
|
1751
1864
|
if (!Array.isArray(parsed.events)) return {
|
|
1752
1865
|
events: [],
|
|
@@ -1767,8 +1880,11 @@ async function readEvolutionEvents(io, path) {
|
|
|
1767
1880
|
* Read the full timeline (rc.71): active log + all archives, merged by seq
|
|
1768
1881
|
* (active copy wins, duplicates only arise from the rotation crash window),
|
|
1769
1882
|
* sorted ascending. Per-file malformed flag as in `readEvolutionEvents`; a
|
|
1770
|
-
*
|
|
1771
|
-
*
|
|
1883
|
+
* flagged ARCHIVE (unreadable, damaged, or a future-version body this reader
|
|
1884
|
+
* cannot interpret) is SKIPPED — it never bricks the boot, the returned events
|
|
1885
|
+
* simply LACK that seq band, and `malformed` is the only signal that they do
|
|
1886
|
+
* (C-events-dispatch-1, v43: the flag is the consumer's contract; a truncated
|
|
1887
|
+
* timeline must never be folded back as if it were complete).
|
|
1772
1888
|
*/
|
|
1773
1889
|
async function readEvolutionTimeline(io, path, archives) {
|
|
1774
1890
|
const dir = dirname(path);
|
|
@@ -3811,79 +3927,6 @@ function sweepReviewChannelSessions(isAlive) {
|
|
|
3811
3927
|
return removed;
|
|
3812
3928
|
}
|
|
3813
3929
|
//#endregion
|
|
3814
|
-
//#region lib/types/probe.js
|
|
3815
|
-
function probePresent(value) {
|
|
3816
|
-
return {
|
|
3817
|
-
kind: "present",
|
|
3818
|
-
value
|
|
3819
|
-
};
|
|
3820
|
-
}
|
|
3821
|
-
function probeAbsent() {
|
|
3822
|
-
return { kind: "absent" };
|
|
3823
|
-
}
|
|
3824
|
-
function probeUnknown(reason) {
|
|
3825
|
-
return {
|
|
3826
|
-
kind: "unknown",
|
|
3827
|
-
reason
|
|
3828
|
-
};
|
|
3829
|
-
}
|
|
3830
|
-
function isPresent(probe) {
|
|
3831
|
-
return probe.kind === "present";
|
|
3832
|
-
}
|
|
3833
|
-
function isAbsent(probe) {
|
|
3834
|
-
return probe.kind === "absent";
|
|
3835
|
-
}
|
|
3836
|
-
function isUnknown(probe) {
|
|
3837
|
-
return probe.kind === "unknown";
|
|
3838
|
-
}
|
|
3839
|
-
/** Only a PRESENT probe yields a value; absent and unknown both fall back. */
|
|
3840
|
-
function valueOr(probe, fallback) {
|
|
3841
|
-
return probe.kind === "present" ? probe.value : fallback;
|
|
3842
|
-
}
|
|
3843
|
-
function mapProbe(probe, transform) {
|
|
3844
|
-
return probe.kind === "present" ? probePresent(transform(probe.value)) : probe;
|
|
3845
|
-
}
|
|
3846
|
-
/** The reason an `unknown` probe carries (message, never a bare String(object)). */
|
|
3847
|
-
function probeReason(error) {
|
|
3848
|
-
return error instanceof Error ? error.message : String(error);
|
|
3849
|
-
}
|
|
3850
|
-
/**
|
|
3851
|
-
* ENOENT/ENOTDIR are the ONE read failure that means "it is not there"; every
|
|
3852
|
-
* other failure is an IO error and stays unknown (same split as the node
|
|
3853
|
-
* backend's own isMissing, V8-23⑨ for the size probe).
|
|
3854
|
-
*/
|
|
3855
|
-
function isMissingPath(error) {
|
|
3856
|
-
const code = error?.code;
|
|
3857
|
-
return code === "ENOENT" || code === "ENOTDIR";
|
|
3858
|
-
}
|
|
3859
|
-
/**
|
|
3860
|
-
* Three-state directory listing. The node backend already answers `[]` for a
|
|
3861
|
-
* MISSING directory (its own rc.50 P2-4 contract) and THROWS for an unreadable
|
|
3862
|
-
* one, so the value this adds is the second half: a backend that throws ENOENT
|
|
3863
|
-
* reads as absent, an EACCES/EIO reads as unknown — where a bare
|
|
3864
|
-
* `catch { return [] }` served a broken store as an empty one (the N14 class).
|
|
3865
|
-
*/
|
|
3866
|
-
async function probeList(io, dir) {
|
|
3867
|
-
try {
|
|
3868
|
-
return probePresent(await io.list(dir));
|
|
3869
|
-
} catch (error) {
|
|
3870
|
-
return isMissingPath(error) ? probeAbsent() : probeUnknown(probeReason(error));
|
|
3871
|
-
}
|
|
3872
|
-
}
|
|
3873
|
-
/**
|
|
3874
|
-
* Three-state mtime read. Absent covers both "the path is missing" and "this
|
|
3875
|
-
* backend has no mtime probe" — the seam cannot tell those apart, so consumers
|
|
3876
|
-
* that must know say so in their own log line. A stat that FAILS is unknown.
|
|
3877
|
-
*/
|
|
3878
|
-
async function probeMtime(io, path) {
|
|
3879
|
-
try {
|
|
3880
|
-
const value = await io.mtime?.(path) ?? null;
|
|
3881
|
-
return value === null ? probeAbsent() : probePresent(value);
|
|
3882
|
-
} catch (error) {
|
|
3883
|
-
return probeUnknown(probeReason(error));
|
|
3884
|
-
}
|
|
3885
|
-
}
|
|
3886
|
-
//#endregion
|
|
3887
3930
|
//#region lib/types/instance-scope.js
|
|
3888
3931
|
/**
|
|
3889
3932
|
* B3 / G4 (0.3.78): the family's single-instance contract, made explicit.
|
|
@@ -3901,11 +3944,30 @@ async function probeMtime(io, path) {
|
|
|
3901
3944
|
* the sidecar directory, so two instances resolving different homes (an
|
|
3902
3945
|
* isolated test fixture, a second DSH_HOME) do not contend, while two rows on
|
|
3903
3946
|
* one profile do.
|
|
3947
|
+
*
|
|
3948
|
+
* ## Scope (v43 FLOW2-1) — this registry is PER PROCESS
|
|
3949
|
+
*
|
|
3950
|
+
* `claims` below is a module-scope Map: two ROWS over one home in ONE process
|
|
3951
|
+
* contend, while the SAME home in another process gets its own Map and is
|
|
3952
|
+
* granted the key. That is by construction, not a gap to close here — the
|
|
3953
|
+
* cross-process half of the contract is the IO backend's write lock
|
|
3954
|
+
* (`transactIo`, core/io.ts), which serializes a per-target read-modify-write.
|
|
3955
|
+
* The FLOW2-1 finding was three call sites reading a GRANTED claim as "no other
|
|
3956
|
+
* process can be doing this work", so the caller contract is stated here:
|
|
3957
|
+
* - granted means "no other row OF THIS PROCESS owns the key";
|
|
3958
|
+
* - `instanceHolder()` answers "who holds it HERE"; `undefined` also covers
|
|
3959
|
+
* "held by another process";
|
|
3960
|
+
* - a foreign holder's LIVENESS cannot be decided from a claim at all (no pid
|
|
3961
|
+
* is recorded here): a consumer that needs that decision must carry a pid in
|
|
3962
|
+
* its own credential and probe it (`isProcessAlive`, core/io.ts), or state
|
|
3963
|
+
* that its action is destructive.
|
|
3904
3964
|
* @module @lmzhen/dsh-evolution-core/src/instance-scope
|
|
3905
3965
|
*/
|
|
3906
|
-
/** home+key -> holder.
|
|
3907
|
-
*
|
|
3908
|
-
* cross-process half is the write lock, see
|
|
3966
|
+
/** home+key -> holder, IN THIS PROCESS ONLY. The Map is module-scope, so two
|
|
3967
|
+
* processes never share it: this half cannot exclude another process (v43
|
|
3968
|
+
* FLOW2-1) — the cross-process half is the write lock, see
|
|
3969
|
+
* persisted-write-inventory.json. It is still the whole point for the case it
|
|
3970
|
+
* was written for: two ROWS of one process writing one home. */
|
|
3909
3971
|
const claims = /* @__PURE__ */ new Map();
|
|
3910
3972
|
/** `<home> :: <key>` — the registry key, exported so diagnostics name the
|
|
3911
3973
|
* same unit the claim does. */
|
|
@@ -3936,7 +3998,8 @@ function releaseInstance(home, key, owner) {
|
|
|
3936
3998
|
const id = instanceClaimKey(home, key);
|
|
3937
3999
|
if (claims.get(id) === owner) claims.delete(id);
|
|
3938
4000
|
}
|
|
3939
|
-
/** The current holder of `key` at `home`, or undefined
|
|
4001
|
+
/** The current holder of `key` at `home`, IN THIS PROCESS, or undefined
|
|
4002
|
+
* (which also covers "another process holds it" — v43 FLOW2-1). */
|
|
3940
4003
|
function instanceHolder(home, key) {
|
|
3941
4004
|
return claims.get(instanceClaimKey(home, key));
|
|
3942
4005
|
}
|
|
@@ -4001,16 +4064,39 @@ function parseSites(raw) {
|
|
|
4001
4064
|
const INSTANCE_KEYS = {
|
|
4002
4065
|
/** The per-home curator: report writing + the retention sweep. */
|
|
4003
4066
|
curator: "evolution-curator" };
|
|
4004
|
-
|
|
4005
|
-
|
|
4067
|
+
let cachedSites;
|
|
4068
|
+
/**
|
|
4069
|
+
* The declared persisted write sites, in file order.
|
|
4070
|
+
*
|
|
4071
|
+
* v43 audit (S2-3 / P1-10): this used to be a module-scope readFileSync plus
|
|
4072
|
+
* parse, so an unshipped asset threw AT IMPORT — one absent file took the whole
|
|
4073
|
+
* family's load down (0.3.79 shipped a tarball without this asset and every
|
|
4074
|
+
* package failed to load). The read is lazy now: importing the package never
|
|
4075
|
+
* fails on this asset, while the first caller still gets a loud, descriptive
|
|
4076
|
+
* failure instead of an empty table ("no declared write sites" would silently
|
|
4077
|
+
* disable rule N20).
|
|
4078
|
+
* @returns the sites, in file order.
|
|
4079
|
+
*/
|
|
4080
|
+
function persistedWriteSites() {
|
|
4081
|
+
if (cachedSites !== void 0) return cachedSites;
|
|
4082
|
+
let raw;
|
|
4083
|
+
try {
|
|
4084
|
+
raw = readFileSync(fileURLToPath(SITES_URL), "utf8");
|
|
4085
|
+
} catch (error) {
|
|
4086
|
+
const cause = error instanceof Error ? error.message : String(error);
|
|
4087
|
+
throw new Error(`evolution-core: persisted-write-inventory.json is unreadable (${cause}) — the package asset is missing or was not shipped; the write-inventory rules cannot be evaluated without it`);
|
|
4088
|
+
}
|
|
4089
|
+
cachedSites = parseSites(JSON.parse(raw));
|
|
4090
|
+
return cachedSites;
|
|
4091
|
+
}
|
|
4006
4092
|
/** Sites serialized by the per-home instance claim, with their instance keys. */
|
|
4007
4093
|
function instanceClaimedWriteSites() {
|
|
4008
|
-
return
|
|
4094
|
+
return persistedWriteSites().filter((site) => site.serializedBy === "instance-claim");
|
|
4009
4095
|
}
|
|
4010
4096
|
/** One declared site by id. An undeclared id throws — a stale caller must fail
|
|
4011
4097
|
* loud rather than read "nothing is declared". */
|
|
4012
4098
|
function persistedWriteSite(id) {
|
|
4013
|
-
const site =
|
|
4099
|
+
const site = persistedWriteSites().find((candidate) => candidate.id === id);
|
|
4014
4100
|
if (site === void 0) throw new Error(`evolution-core: no persisted write site "${id}" in persisted-write-inventory.json`);
|
|
4015
4101
|
return site;
|
|
4016
4102
|
}
|
|
@@ -4238,21 +4324,34 @@ var ToolDispatchNormalizer = class {
|
|
|
4238
4324
|
this.maxTracked = options.maxTracked ?? Number.POSITIVE_INFINITY;
|
|
4239
4325
|
}
|
|
4240
4326
|
/**
|
|
4327
|
+
* v43 audit (FLOW4-4): the ledger key. A normalizer shared by every session
|
|
4328
|
+
* (skill-usage's live listener holds ONE process-wide instance) used the bare
|
|
4329
|
+
* call id, so two sessions that produced the same id — PTC sub-call ids are
|
|
4330
|
+
* short, and an id-less payload falls back to a type+payload key that is not
|
|
4331
|
+
* unique by construction — collided: the second session's read was absorbed as
|
|
4332
|
+
* "already seen" and never counted, and a settle in one session flipped the
|
|
4333
|
+
* other's `ok`. Callers that span sessions pass the session id as `scope`.
|
|
4334
|
+
*/
|
|
4335
|
+
keyOf(callId, scope) {
|
|
4336
|
+
return scope === "" ? callId : `${scope}:${callId}`;
|
|
4337
|
+
}
|
|
4338
|
+
/**
|
|
4241
4339
|
* Absorb one session event.
|
|
4242
4340
|
* @param event - the event to absorb; any non-dispatch event is ignored.
|
|
4243
4341
|
* @returns the dispatch's signal when this event FIRST reveals the dispatch,
|
|
4244
4342
|
* otherwise \`null\` (the paired event of an already-emitted dispatch, or a
|
|
4245
4343
|
* non-dispatch event). A \`null\` return is never a dispatch to count again.
|
|
4246
4344
|
*/
|
|
4247
|
-
advance(event) {
|
|
4345
|
+
advance(event, scope = "") {
|
|
4248
4346
|
const record = readDispatchRecord(event);
|
|
4249
4347
|
if (record === null) return null;
|
|
4348
|
+
const key = this.keyOf(record.callId, scope);
|
|
4250
4349
|
if (record.name === "") {
|
|
4251
|
-
const existing = this.records.get(
|
|
4350
|
+
const existing = this.records.get(key);
|
|
4252
4351
|
if (existing !== void 0 && record.outcome !== void 0) existing.ok = record.outcome.ok;
|
|
4253
4352
|
return null;
|
|
4254
4353
|
}
|
|
4255
|
-
const existing = this.records.get(
|
|
4354
|
+
const existing = this.records.get(key);
|
|
4256
4355
|
if (existing !== void 0) {
|
|
4257
4356
|
if (record.outcome !== void 0) existing.ok = record.outcome.ok;
|
|
4258
4357
|
return null;
|
|
@@ -4265,7 +4364,7 @@ var ToolDispatchNormalizer = class {
|
|
|
4265
4364
|
arguments: record.arguments,
|
|
4266
4365
|
ok: record.outcome?.ok
|
|
4267
4366
|
};
|
|
4268
|
-
this.records.set(
|
|
4367
|
+
this.records.set(key, signal);
|
|
4269
4368
|
this.evict();
|
|
4270
4369
|
return signal;
|
|
4271
4370
|
}
|
|
@@ -4279,13 +4378,14 @@ var ToolDispatchNormalizer = class {
|
|
|
4279
4378
|
* @param event - the event already absorbed by \`advance\`.
|
|
4280
4379
|
* @returns the dispatch this event settled, or \`null\`.
|
|
4281
4380
|
*/
|
|
4282
|
-
settledSignalOf(event) {
|
|
4381
|
+
settledSignalOf(event, scope = "") {
|
|
4283
4382
|
const record = readDispatchRecord(event);
|
|
4284
4383
|
if (record === null || record.outcome === void 0) return null;
|
|
4285
|
-
|
|
4286
|
-
|
|
4384
|
+
const key = this.keyOf(record.callId, scope);
|
|
4385
|
+
if (this.settledIds.has(key)) return null;
|
|
4386
|
+
const signal = this.records.get(key);
|
|
4287
4387
|
if (signal === void 0) return null;
|
|
4288
|
-
this.settledIds.add(
|
|
4388
|
+
this.settledIds.add(key);
|
|
4289
4389
|
this.evict();
|
|
4290
4390
|
return signal;
|
|
4291
4391
|
}
|
|
@@ -4738,236 +4838,42 @@ function findDriftSignal(signals, id) {
|
|
|
4738
4838
|
return signals.find((signal) => signal.id === id);
|
|
4739
4839
|
}
|
|
4740
4840
|
//#endregion
|
|
4741
|
-
//#region lib/types/
|
|
4841
|
+
//#region lib/types/limits.js
|
|
4742
4842
|
/**
|
|
4743
|
-
* Skill
|
|
4744
|
-
*
|
|
4745
|
-
* Skills live under `$DSH_HOME/skills` (`~/.dsh/skills` by default), matching
|
|
4746
|
-
* the default dsh skill-filesystem user root. The plugin only manages skills
|
|
4747
|
-
* it created unless a `.hermes-managed` marker opts a skill in. Archival is a
|
|
4748
|
-
* move to `.archive/` — never a hard delete.
|
|
4749
|
-
*
|
|
4750
|
-
* ## Concurrency discipline (OPT-09, 2026-09) — read before adding a mutator
|
|
4751
|
-
*
|
|
4752
|
-
* Three primitives, three distinct jobs (they compose, they do not replace
|
|
4753
|
-
* each other):
|
|
4754
|
-
*
|
|
4755
|
-
* 1. **In-process serial queue** (`this.serial`, makeSerialQueue) — orders the
|
|
4756
|
-
* read→plan→commit phases of one skill's mutation against OTHER mutators
|
|
4757
|
-
* in this process. Used by: create/update/patch/setPinned/restructure/
|
|
4758
|
-
* writeSupportFile/removeSupportFile and (whole-mutation) consolidate.
|
|
4759
|
-
* NON-reentrant: a callback must never call a public method that wraps
|
|
4760
|
-
* itself in `this.serial` (archive/restoreFromArchive deliberately do not).
|
|
4761
|
-
* 2. **Per-directory write lock** (io.ts LOCK_*) — cross-process mutual
|
|
4762
|
-
* exclusion plus in-process crash ownership (tickets, takeover). Checked
|
|
4763
|
-
* with `hasWriteLock` before any destructive move (archive/restore/
|
|
4764
|
-
* snapshot); held inside transactIo by byte writers.
|
|
4765
|
-
* 3. **CAS baseline (`expected:`)** — any read whose bytes feed a later write
|
|
4766
|
-
* must either live inside the serial section that commits the write, or
|
|
4767
|
-
* carry its plan-time bytes as `expected` so the commit fails closed on
|
|
4768
|
-
* drift (V8-11 / V24-01). A read outside the serial section WITHOUT a
|
|
4769
|
-
* baseline is a lost-update bug; this file's history is the test suite.
|
|
4843
|
+
* Skill content limits: the byte/char budgets every write path validates against.
|
|
4770
4844
|
*
|
|
4771
|
-
*
|
|
4772
|
-
*
|
|
4773
|
-
*
|
|
4774
|
-
* (OPT-07); the movers' probe→rename window is owned by the io.ts protocol.
|
|
4845
|
+
* Split out of skill-store.ts (S2-1) so the frontmatter validators and the
|
|
4846
|
+
* store share one declaration site. Re-exported by skill-store.ts: the package
|
|
4847
|
+
* export surface is unchanged.
|
|
4775
4848
|
*/
|
|
4776
|
-
/** 0.3.16 (S1.13, T-6): the pointer-line prefix written into a body when a
|
|
4777
|
-
* section is moved to references/ — single literal, both restructure and
|
|
4778
|
-
* append-mode consolidation emit the same discoverability line. */
|
|
4779
|
-
const POINTER_LINE_PREFIX = "> 详见 references/";
|
|
4780
4849
|
const DEFAULT_SKILL_LIMITS = {
|
|
4781
4850
|
maxNameLength: 64,
|
|
4782
4851
|
maxDescriptionLength: MAX_DESCRIPTION_LENGTH,
|
|
4783
4852
|
maxSkillContentChars: MAX_SKILL_CONTENT_CHARS,
|
|
4784
4853
|
maxSkillFileBytes: MAX_SKILL_FILE_BYTES
|
|
4785
4854
|
};
|
|
4855
|
+
//#endregion
|
|
4856
|
+
//#region lib/types/frontmatter.js
|
|
4786
4857
|
/**
|
|
4787
|
-
*
|
|
4788
|
-
*
|
|
4789
|
-
*
|
|
4790
|
-
*
|
|
4858
|
+
* Frontmatter parsing, normalization and validation for skill Markdown files.
|
|
4859
|
+
*
|
|
4860
|
+
* Split out of skill-store.ts (S2-1): pure functions over file text, no store
|
|
4861
|
+
* state. skill-store.ts re-exports the same names it exported before the split,
|
|
4862
|
+
* so the package export surface is unchanged.
|
|
4791
4863
|
*/
|
|
4792
|
-
function anchorVerdict(anchor, current) {
|
|
4793
|
-
if (anchor === void 0) return "match";
|
|
4794
|
-
if ("absent" in anchor) return current === null ? "match" : "drift";
|
|
4795
|
-
if (current === null) return "missing";
|
|
4796
|
-
return contentHash(current) === anchor.sha256 ? "match" : "drift";
|
|
4797
|
-
}
|
|
4798
4864
|
/**
|
|
4799
|
-
*
|
|
4800
|
-
*
|
|
4801
|
-
*
|
|
4802
|
-
*
|
|
4803
|
-
*
|
|
4804
|
-
*
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
message: verdict === "missing" ? `Skill "${name}" not found.` : `Skill "${name}" changed since it was read; the write was refused to avoid overwriting newer content.`
|
|
4812
|
-
};
|
|
4813
|
-
}
|
|
4814
|
-
/**
|
|
4815
|
-
* Build the refusal for a support-file write/remove whose anchor did not hold.
|
|
4816
|
-
* @param name - the owning skill name.
|
|
4817
|
-
* @param filePath - the support-file path inside the skill.
|
|
4818
|
-
* @param verdict - the non-matching verdict.
|
|
4819
|
-
* @returns the refusal result (nothing was written or removed).
|
|
4820
|
-
*/
|
|
4821
|
-
/**
|
|
4822
|
-
* Refusal for a target the locked read could not verify at all (EISDIR, an
|
|
4823
|
-
* unreadable file). A staged replay reports "could not be verified" instead of
|
|
4824
|
-
* propagating an exception: nothing was read, so nothing can have been written.
|
|
4825
|
-
* @param name - the owning skill name.
|
|
4826
|
-
* @param filePath - the support-file path, or `null` for the skill body.
|
|
4827
|
-
* @returns the refusal result.
|
|
4828
|
-
*/
|
|
4829
|
-
function anchorUnverifiable(name, filePath) {
|
|
4830
|
-
return {
|
|
4831
|
-
ok: false,
|
|
4832
|
-
stale: true,
|
|
4833
|
-
anchor: "drift",
|
|
4834
|
-
message: filePath === null ? `Skill "${name}" could not be read to verify the staged content.` : `Support file "${filePath}" of "${name}" could not be read to verify the staged content.`
|
|
4835
|
-
};
|
|
4836
|
-
}
|
|
4837
|
-
function anchorRefusalFile(name, filePath, verdict) {
|
|
4838
|
-
return {
|
|
4839
|
-
ok: false,
|
|
4840
|
-
stale: true,
|
|
4841
|
-
anchor: verdict,
|
|
4842
|
-
message: verdict === "missing" ? `File "${filePath}" not found in skill "${name}".` : `Support file "${filePath}" of "${name}" changed since it was read; the write was refused to avoid overwriting newer content.`
|
|
4843
|
-
};
|
|
4844
|
-
}
|
|
4845
|
-
/** Upper bound of moves per restructure proposal (validator and core agree). */
|
|
4846
|
-
const MAX_RESTRUCTURE_MOVES = 5;
|
|
4847
|
-
/** Restructure targets are plain markdown files under references/ — no
|
|
4848
|
-
* subdirectories, no other support kind. V8-10 (0.3.47): the regex-level
|
|
4849
|
-
* `(?!.*\.\.)` keeps the restructure-created set EXACTLY the set
|
|
4850
|
-
* validateSupportPath can reopen — a `references/my..notes.md` target (double
|
|
4851
|
-
* dots) used to pass here while every later patch/write/remove on it was
|
|
4852
|
-
* refused as traversal (an orphan file the user could not touch). */
|
|
4853
|
-
const RESTRUCTURE_TARGET_RE = /^references\/[a-z0-9](?!.*\.\.)[a-z0-9._-]*\.md$/;
|
|
4854
|
-
/** F-20 (v18): the character rule shared by support-file names and snapshot
|
|
4855
|
-
* `extras/` entry names. The two exported names used to carry the same literal
|
|
4856
|
-
* independently; both now derive from this one. */
|
|
4857
|
-
const SUPPORT_ENTRY_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
4858
|
-
/** Extra file name carried inside a snapshot's `extras/` directory. */
|
|
4859
|
-
const SNAPSHOT_EXTRA_NAME_RE = SUPPORT_ENTRY_NAME_RE;
|
|
4860
|
-
function skillsRoot(env = process.env) {
|
|
4861
|
-
return join(evolutionRoot(env), "skills");
|
|
4862
|
-
}
|
|
4863
|
-
/** 0.3.18 (S4.1, E-30): the ONE root resolution for every member that reads
|
|
4864
|
-
* the skills tree — tool-skill-manage / evolution-skill-catalog / skill-usage
|
|
4865
|
-
* / evolution-learning-graph used to each resolve `config.root || skillsRoot()`
|
|
4866
|
-
* (and the graph ignored config entirely). Empty/whitespace config falls
|
|
4867
|
-
* through to the default; callers pass their raw Config. The optional field is
|
|
4868
|
-
* declared `| undefined` so a config object whose root field is explicitly
|
|
4869
|
-
* `string | undefined` still assignable under exactOptionalPropertyTypes. */
|
|
4870
|
-
function resolveSkillsRoot(config = {}) {
|
|
4871
|
-
return (config.root ?? "").trim() || skillsRoot();
|
|
4872
|
-
}
|
|
4873
|
-
/** E-7 (v18) → V27 G2.4 (M-08): every family row reads ONE root key. `root` is
|
|
4874
|
-
* canonical; the `skillsRoot` alias was honoured for one minor version and its
|
|
4875
|
-
* window closed at 0.3.65 — it is now two releases past expiry, so this
|
|
4876
|
-
* resolver no longer reads it at all. A deployment that still sets the alias
|
|
4877
|
-
* must fail LOUDLY at load (see {@link assertSkillsRootAliasRetired}): silently
|
|
4878
|
-
* ignoring a config key leaves the deployment pointing at a root nobody reads,
|
|
4879
|
-
* which is the worst form of compatibility.
|
|
4880
|
-
* @param config - the raw plugin config.
|
|
4881
|
-
* @returns the effective root (empty when the key is unset or blank).
|
|
4882
|
-
*/
|
|
4883
|
-
function resolveRootConfig(config = {}) {
|
|
4884
|
-
return { root: (config.root ?? "").trim() };
|
|
4885
|
-
}
|
|
4886
|
-
/** V27 G2.4 (M-08): the retirement gate for the expired `skillsRoot` alias.
|
|
4887
|
-
* Called at each plugin's load boundary (before the root is resolved), it turns
|
|
4888
|
-
* a stale key into an explicit load error naming the replacement — the
|
|
4889
|
-
* fail-loud form the plan requires instead of a silent no-op.
|
|
4890
|
-
* @param config - the raw plugin config (the alias field stays DECLARED in each
|
|
4891
|
-
* schema so the loader can hand it here instead of dropping it).
|
|
4892
|
-
*/
|
|
4893
|
-
function assertSkillsRootAliasRetired(config = {}) {
|
|
4894
|
-
if ((config.skillsRoot ?? "").trim() !== "") throw new Error("evolution: config \"skillsRoot\" was removed after 0.3.65 — rename the key to \"root\" (the alias is no longer honoured)");
|
|
4895
|
-
}
|
|
4896
|
-
/**
|
|
4897
|
-
* Map a requesting session onto the two origin surfaces (rc.44 plan M2-2.3):
|
|
4898
|
-
* the APPROVAL surface treats every delegated subagent as the autonomous
|
|
4899
|
-
* review channel, while the LIBRARY surface keeps the Hermes distinction -
|
|
4900
|
-
* the review fork is 'background_review' (the pinned guard blocks its
|
|
4901
|
-
* writes) and any other subagent is 'subagent' (agent-authored, not
|
|
4902
|
-
* review-channel). `isReview` marks the caller as the background review
|
|
4903
|
-
* pipeline itself. Single source: the two tools and the review executor all
|
|
4904
|
-
* read this table instead of re-deriving it.
|
|
4905
|
-
*/
|
|
4906
|
-
function resolveOrigins(headerOrigin, isReview = false) {
|
|
4907
|
-
if (isReview) return {
|
|
4908
|
-
approval: "background_review",
|
|
4909
|
-
library: "background_review"
|
|
4910
|
-
};
|
|
4911
|
-
if (headerOrigin === "subagent") return {
|
|
4912
|
-
approval: "background_review",
|
|
4913
|
-
library: "subagent"
|
|
4914
|
-
};
|
|
4915
|
-
return {
|
|
4916
|
-
approval: "foreground",
|
|
4917
|
-
library: "foreground"
|
|
4918
|
-
};
|
|
4919
|
-
}
|
|
4920
|
-
/**
|
|
4921
|
-
* S1-E8 (0.3.80): ONE exec→origins resolution for the two write tools — reads
|
|
4922
|
-
* the session header origin AND the v37 S2.2 review-channel session mark, so a
|
|
4923
|
-
* tool cannot forget the mark half (tool-memory shipped without it, which
|
|
4924
|
-
* mislabeled every inject-mode review memory write as `foreground` and let it
|
|
4925
|
-
* bypass staging under `stageForeground: false`).
|
|
4926
|
-
* Single source: both tools call this instead of re-deriving the pair.
|
|
4927
|
-
*/
|
|
4928
|
-
function resolveExecOrigins(exec) {
|
|
4929
|
-
const session = exec?.agent?.session;
|
|
4930
|
-
return resolveOrigins(session?.header?.origin, isReviewChannelSession(typeof session?.id === "string" ? session.id : void 0));
|
|
4931
|
-
}
|
|
4932
|
-
function skillDir(root, name) {
|
|
4933
|
-
return join(root, name);
|
|
4934
|
-
}
|
|
4935
|
-
/** Dot-prefixed on-disk marker name. SINGLE source: `list()` matches directory
|
|
4936
|
-
* entries against this name, and path builders must never hardcode a marker
|
|
4937
|
-
* literal (N-1: the rc.49 exists()-probe convergence dropped the dot,
|
|
4938
|
-
* poisoning every protectedBy/managed report). Exported for cross-package
|
|
4939
|
-
* consumers that must probe markers without re-deriving the name (curator's
|
|
4940
|
-
* archive-copy bundled probe, 0.3.26 V4-02). */
|
|
4941
|
-
function markerEntryName(marker) {
|
|
4942
|
-
return `.${marker}`;
|
|
4943
|
-
}
|
|
4944
|
-
/** F-17 (v18): the root-level lock files a DESTRUCTIVE MOVER must treat as an
|
|
4945
|
-
* active writer (skill body + the two marker writers). Single source with
|
|
4946
|
-
* `markerEntryName`/`LOCK_SUFFIX` so a renamed marker cannot silently drop out
|
|
4947
|
-
* of the ghost-writer probe. */
|
|
4948
|
-
const MARKER_LOCK_NAMES = [
|
|
4949
|
-
`SKILL.md${LOCK_SUFFIX}`,
|
|
4950
|
-
`.pinned${LOCK_SUFFIX}`,
|
|
4951
|
-
`.hermes-managed${LOCK_SUFFIX}`
|
|
4952
|
-
];
|
|
4953
|
-
/** v23 (ML-1): `.archive` retention window (see pruneExpiredArchives). */
|
|
4954
|
-
const ARCHIVE_RETENTION_DAYS = 365;
|
|
4955
|
-
function markerPath(dir, marker) {
|
|
4956
|
-
return join(dir, markerEntryName(marker));
|
|
4957
|
-
}
|
|
4958
|
-
/**
|
|
4959
|
-
* Shared frontmatter block detection (P3-3 single owner): opening line `---`
|
|
4960
|
-
* and closing line exactly `---`. Used by `parseFrontmatter`,
|
|
4961
|
-
* `frontmatterCatalogInvalid` and `normalizeFrontmatter` so the three can
|
|
4962
|
-
* never disagree about where the block ends (the loose `indexOf('\n---')`
|
|
4963
|
-
* form matched `\n----` and was replaced by this strict line rule).
|
|
4964
|
-
*
|
|
4965
|
-
* V27 G2.1: both fence lines are matched EXACTLY, tolerating only a trailing
|
|
4966
|
-
* `\r` — the same rule the upstream filesystem catalog uses
|
|
4967
|
-
* (`skill-filesystem.parseFrontmatter`). The former `.trim()` comparison
|
|
4968
|
-
* accepted ` --- `, so an indented fence loaded in the family while the
|
|
4969
|
-
* platform ignored the file: family visibility split from platform visibility,
|
|
4970
|
-
* which is exactly what a strict-YAML frontmatter is supposed to prevent.
|
|
4865
|
+
* Shared frontmatter block detection (P3-3 single owner): opening line `---`
|
|
4866
|
+
* and closing line exactly `---`. Used by `parseFrontmatter`,
|
|
4867
|
+
* `frontmatterCatalogInvalid` and `normalizeFrontmatter` so the three can
|
|
4868
|
+
* never disagree about where the block ends (the loose `indexOf('\n---')`
|
|
4869
|
+
* form matched `\n----` and was replaced by this strict line rule).
|
|
4870
|
+
*
|
|
4871
|
+
* V27 G2.1: both fence lines are matched EXACTLY, tolerating only a trailing
|
|
4872
|
+
* `\r` — the same rule the upstream filesystem catalog uses
|
|
4873
|
+
* (`skill-filesystem.parseFrontmatter`). The former `.trim()` comparison
|
|
4874
|
+
* accepted ` --- `, so an indented fence loaded in the family while the
|
|
4875
|
+
* platform ignored the file: family visibility split from platform visibility,
|
|
4876
|
+
* which is exactly what a strict-YAML frontmatter is supposed to prevent.
|
|
4971
4877
|
*/
|
|
4972
4878
|
/**
|
|
4973
4879
|
* S1.1 (v37 P1-4): the frontmatter block is split on LF and every line keeps its
|
|
@@ -5341,65 +5247,381 @@ function relatedSkillNames(content, exclude) {
|
|
|
5341
5247
|
}
|
|
5342
5248
|
return [...names];
|
|
5343
5249
|
}
|
|
5344
|
-
/** S1.2 (v37 P2-1): the content limit applies to the bytes that LAND ON DISK.
|
|
5345
|
-
* Every write normalizes with `trimEnd() + '\n'`, so judging the raw argument let
|
|
5346
|
-
* a 100_000-character body with no trailing newline land as 100_001 bytes — and
|
|
5347
|
-
* every later patch/update of that skill was then refused, which made it
|
|
5348
|
-
* unmaintainable through `skill_manage` with no repair path at all. */
|
|
5349
|
-
function skillMdOnDisk(content) {
|
|
5350
|
-
return content.trimEnd() + "\n";
|
|
5250
|
+
/** S1.2 (v37 P2-1): the content limit applies to the bytes that LAND ON DISK.
|
|
5251
|
+
* Every write normalizes with `trimEnd() + '\n'`, so judging the raw argument let
|
|
5252
|
+
* a 100_000-character body with no trailing newline land as 100_001 bytes — and
|
|
5253
|
+
* every later patch/update of that skill was then refused, which made it
|
|
5254
|
+
* unmaintainable through `skill_manage` with no repair path at all. */
|
|
5255
|
+
function skillMdOnDisk(content) {
|
|
5256
|
+
return content.trimEnd() + "\n";
|
|
5257
|
+
}
|
|
5258
|
+
/** Whether `content` would exceed `limit` once written. */
|
|
5259
|
+
function exceedsContentLimit(content, limit) {
|
|
5260
|
+
return skillMdOnDisk(content).length > limit;
|
|
5261
|
+
}
|
|
5262
|
+
/** S1.2: the repair path — a write that makes an already-over-limit file smaller.
|
|
5263
|
+
* Only a NET SHRINK is exempt; an equal or larger write stays refused. */
|
|
5264
|
+
function shrinksOverLimit(next, current, limit) {
|
|
5265
|
+
if (current === null || current === void 0 || !exceedsContentLimit(current, limit)) return false;
|
|
5266
|
+
return skillMdOnDisk(next).length < skillMdOnDisk(current).length;
|
|
5267
|
+
}
|
|
5268
|
+
function validateFrontmatter(content, expectedName, limits = DEFAULT_SKILL_LIMITS, current) {
|
|
5269
|
+
const parsed = parseFrontmatter(content);
|
|
5270
|
+
if (!parsed) return "SKILL.md must start with a `---` line, close the frontmatter with another exact `---` line (only a trailing `\\r` is tolerated), and include a body below it.";
|
|
5271
|
+
const unreadable = parsed.platformStringSplit.filter((entry) => entry.kind === "sequence" || entry.kind === "mapping");
|
|
5272
|
+
if (unreadable.length > 0) return `Frontmatter field ${unreadable.map((entry) => `"${entry.key}" (a YAML ${entry.kind})`).join(", ")} must be a string: the platform skill catalog reads such a field as absent and ignores the whole file. Write plain text, or wrap the value in double quotes to keep it literally.`;
|
|
5273
|
+
if (!parsed.frontmatter.name) return "Frontmatter must include a name field.";
|
|
5274
|
+
if (!SKILL_NAME_RE.test(parsed.frontmatter.name)) return `Invalid skill name "${parsed.frontmatter.name}" — use lowercase letters, digits, and hyphens.`;
|
|
5275
|
+
if (parsed.frontmatter.name.length > limits.maxNameLength) return `Skill name exceeds ${limits.maxNameLength} characters.`;
|
|
5276
|
+
if (expectedName && parsed.frontmatter.name !== expectedName) return `Frontmatter name "${parsed.frontmatter.name}" does not match target skill "${expectedName}".`;
|
|
5277
|
+
if (!parsed.frontmatter.description) return "Frontmatter must include a description field.";
|
|
5278
|
+
if (parsed.frontmatter.description.length > limits.maxDescriptionLength) return `Description exceeds ${limits.maxDescriptionLength} characters.`;
|
|
5279
|
+
if (exceedsContentLimit(content, limits.maxSkillContentChars) && !shrinksOverLimit(content, current, limits.maxSkillContentChars)) return `SKILL.md content exceeds ${limits.maxSkillContentChars} characters. Consider splitting into a smaller SKILL.md with supporting files.`;
|
|
5280
|
+
return null;
|
|
5281
|
+
}
|
|
5282
|
+
/**
|
|
5283
|
+
* Advisory authoring feedback (P0): evaluate frontmatter against the
|
|
5284
|
+
* authoring bar WITHOUT changing platform validation semantics. The bar is
|
|
5285
|
+
* the quality target, `validateFrontmatter`'s limits are the compatibility
|
|
5286
|
+
* floor, and this bridge layer tells the model when its text would be
|
|
5287
|
+
* truncated or route-poor instead of silently shipping it.
|
|
5288
|
+
*/
|
|
5289
|
+
function authoringFeedback(frontmatter) {
|
|
5290
|
+
const description = frontmatter.description ?? "";
|
|
5291
|
+
const over60 = description.length > 60;
|
|
5292
|
+
const hasColon = description.includes(":");
|
|
5293
|
+
const lines = [];
|
|
5294
|
+
lines.push(over60 ? `Description is ${description.length}/60 characters — exceeds the 60-char authoring bar (Hermes standard; the catalog truncates at the configured platform cap).` : `Description ${description.length}/60 characters — within the authoring bar.`);
|
|
5295
|
+
if (hasColon) lines.push("Description contains a colon — wrap the whole value in double quotes.");
|
|
5296
|
+
return {
|
|
5297
|
+
descriptionChars: description.length,
|
|
5298
|
+
over60,
|
|
5299
|
+
hasColon,
|
|
5300
|
+
lines
|
|
5301
|
+
};
|
|
5302
|
+
}
|
|
5303
|
+
/** A1-15 (v18) / P2-2 (v19): the io layer marks an error `committed: true` when
|
|
5304
|
+
* the rename landed and only the directory fsync failed. Every single-file
|
|
5305
|
+
* writer must treat that as "written, durability unconfirmed" — never as a
|
|
5306
|
+
* plain failure (which a caller would retry, or a two-phase caller roll back).
|
|
5307
|
+
* v28 G2.1 (EVO-IO-05): this is a delegation to the seam's own
|
|
5308
|
+
* `isCommittedWarning` — the marker predicate has exactly one definition. */
|
|
5309
|
+
//#endregion
|
|
5310
|
+
//#region lib/types/fuzzy-match.js
|
|
5311
|
+
/**
|
|
5312
|
+
* Fuzzy string matching and replacement for patch/restructure edits of skill files.
|
|
5313
|
+
*
|
|
5314
|
+
* Split out of skill-store.ts (S2-1): pure text functions with no store state.
|
|
5315
|
+
* The store imports the scan, the budgets and the replace entry points directly;
|
|
5316
|
+
* none of them is re-exported, so the package export surface is unchanged.
|
|
5317
|
+
*/
|
|
5318
|
+
function fuzzyIndexOf(content, pattern, from = 0) {
|
|
5319
|
+
const isSpace = (char) => char !== void 0 && /[ \t]/.test(char);
|
|
5320
|
+
const escaped = (char) => {
|
|
5321
|
+
if (char === "n") return "\n";
|
|
5322
|
+
if (char === "t") return " ";
|
|
5323
|
+
if (char === "r") return "\r";
|
|
5324
|
+
return null;
|
|
5325
|
+
};
|
|
5326
|
+
for (let start = from; start < content.length; start += 1) {
|
|
5327
|
+
let contentIndex = start;
|
|
5328
|
+
let patternIndex = 0;
|
|
5329
|
+
while (patternIndex < pattern.length && contentIndex < content.length) {
|
|
5330
|
+
const patternChar = pattern[patternIndex];
|
|
5331
|
+
const contentChar = content[contentIndex];
|
|
5332
|
+
if (isSpace(patternChar)) {
|
|
5333
|
+
while (patternIndex < pattern.length && isSpace(pattern[patternIndex])) patternIndex += 1;
|
|
5334
|
+
while (contentIndex < content.length && isSpace(content[contentIndex])) contentIndex += 1;
|
|
5335
|
+
continue;
|
|
5336
|
+
}
|
|
5337
|
+
const escapedChar = patternChar === "\\" ? escaped(pattern[patternIndex + 1]) : null;
|
|
5338
|
+
if (escapedChar !== null && contentChar === escapedChar) {
|
|
5339
|
+
patternIndex += 2;
|
|
5340
|
+
contentIndex += 1;
|
|
5341
|
+
continue;
|
|
5342
|
+
}
|
|
5343
|
+
if (patternChar === contentChar) {
|
|
5344
|
+
contentIndex += 1;
|
|
5345
|
+
patternIndex += 1;
|
|
5346
|
+
continue;
|
|
5347
|
+
}
|
|
5348
|
+
break;
|
|
5349
|
+
}
|
|
5350
|
+
if (patternIndex === pattern.length) return [start, contentIndex];
|
|
5351
|
+
}
|
|
5352
|
+
return null;
|
|
5353
|
+
}
|
|
5354
|
+
/** Trim leading whitespace of the first line and trailing whitespace of the last line. */
|
|
5355
|
+
function trimPatternBoundaries(pattern) {
|
|
5356
|
+
const from = pattern.search(/\S/);
|
|
5357
|
+
const trimmed = from < 0 ? pattern : pattern.slice(from);
|
|
5358
|
+
const trailing = trimmed.search(/\s+$/);
|
|
5359
|
+
return trailing < 0 ? trimmed : trimmed.slice(0, trailing);
|
|
5360
|
+
}
|
|
5361
|
+
/** Replace only the fuzzy-matched span, preserving all surrounding bytes.
|
|
5362
|
+
* V7-11 (0.3.44): the replaceAll loop accumulates the per-scan cost — the
|
|
5363
|
+
* single-scan budget at the caller only bounded ONE fuzzyIndexOf, while an
|
|
5364
|
+
* unbounded number of matches × O(n·m) each could still stall the loop.
|
|
5365
|
+
* When the accumulated cost exceeds the budget the whole replace fails
|
|
5366
|
+
* (null) instead of partially applying an arbitrary prefix. */
|
|
5367
|
+
function fuzzyReplace(content, oldString, newString, replaceAll) {
|
|
5368
|
+
let current = content;
|
|
5369
|
+
let scanFrom = 0;
|
|
5370
|
+
let totalWork = 0;
|
|
5371
|
+
for (;;) {
|
|
5372
|
+
totalWork += current.length * oldString.length;
|
|
5373
|
+
if (totalWork > 8e6) return null;
|
|
5374
|
+
const match = fuzzyIndexOf(current, oldString, scanFrom);
|
|
5375
|
+
if (match === null) return current;
|
|
5376
|
+
const [start, end] = match;
|
|
5377
|
+
const next = current.slice(0, start) + newString + current.slice(end);
|
|
5378
|
+
if (!replaceAll) return next;
|
|
5379
|
+
current = next;
|
|
5380
|
+
scanFrom = start + newString.length;
|
|
5381
|
+
}
|
|
5382
|
+
}
|
|
5383
|
+
function fuzzyPatch(content, oldString, newString, replaceAll = false) {
|
|
5384
|
+
if (oldString === "") return null;
|
|
5385
|
+
if (content.includes(oldString)) return replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, () => newString);
|
|
5386
|
+
const boundary = trimPatternBoundaries(oldString);
|
|
5387
|
+
if (boundary === "") return null;
|
|
5388
|
+
if (boundary !== oldString) {
|
|
5389
|
+
if (fuzzyIndexOf(content, boundary) !== null) return fuzzyReplace(content, boundary, newString, replaceAll);
|
|
5390
|
+
}
|
|
5391
|
+
if (fuzzyIndexOf(content, oldString) !== null) return fuzzyReplace(content, oldString, newString, replaceAll);
|
|
5392
|
+
return null;
|
|
5393
|
+
}
|
|
5394
|
+
/** Deterministic section-extraction plan facts; the caller owns the IO and the append semantics. */
|
|
5395
|
+
//#endregion
|
|
5396
|
+
//#region lib/types/skill-store.js
|
|
5397
|
+
/**
|
|
5398
|
+
* Skill library management for the self-evolution plugin.
|
|
5399
|
+
*
|
|
5400
|
+
* Skills live under `$DSH_HOME/skills` (`~/.dsh/skills` by default), matching
|
|
5401
|
+
* the default dsh skill-filesystem user root. The plugin only manages skills
|
|
5402
|
+
* it created unless a `.hermes-managed` marker opts a skill in. Archival is a
|
|
5403
|
+
* move to `.archive/` — never a hard delete.
|
|
5404
|
+
*
|
|
5405
|
+
* ## Concurrency discipline (OPT-09, 2026-09) — read before adding a mutator
|
|
5406
|
+
*
|
|
5407
|
+
* Three primitives, three distinct jobs (they compose, they do not replace
|
|
5408
|
+
* each other):
|
|
5409
|
+
*
|
|
5410
|
+
* 1. **In-process serial queue** (`this.serial`, makeSerialQueue) — orders the
|
|
5411
|
+
* read→plan→commit phases of one skill's mutation against OTHER mutators
|
|
5412
|
+
* in this process. Used by: create/update/patch/setPinned/restructure/
|
|
5413
|
+
* writeSupportFile/removeSupportFile and (whole-mutation) consolidate.
|
|
5414
|
+
* NON-reentrant: a callback must never call a public method that wraps
|
|
5415
|
+
* itself in `this.serial` (archive/restoreFromArchive deliberately do not).
|
|
5416
|
+
* 2. **Per-directory write lock** (io.ts LOCK_*) — cross-process mutual
|
|
5417
|
+
* exclusion plus in-process crash ownership (tickets, takeover). Checked
|
|
5418
|
+
* with `hasWriteLock` before any destructive move (archive/restore/
|
|
5419
|
+
* snapshot); held inside transactIo by byte writers.
|
|
5420
|
+
* 3. **CAS baseline (`expected:`)** — any read whose bytes feed a later write
|
|
5421
|
+
* must either live inside the serial section that commits the write, or
|
|
5422
|
+
* carry its plan-time bytes as `expected` so the commit fails closed on
|
|
5423
|
+
* drift (V8-11 / V24-01). A read outside the serial section WITHOUT a
|
|
5424
|
+
* baseline is a lost-update bug; this file's history is the test suite.
|
|
5425
|
+
*
|
|
5426
|
+
* Known residuals (deliberate, documented at their sites): the archive commit
|
|
5427
|
+
* re-check narrows but does not close the pin race (OPT-06); snapshotAll
|
|
5428
|
+
* re-probes after its copies so a mid-copy writer demotes to `skipped`
|
|
5429
|
+
* (OPT-07); the movers' probe→rename window is owned by the io.ts protocol.
|
|
5430
|
+
*/
|
|
5431
|
+
/** 0.3.16 (S1.13, T-6): the pointer-line prefix written into a body when a
|
|
5432
|
+
* section is moved to references/ — single literal, both restructure and
|
|
5433
|
+
* append-mode consolidation emit the same discoverability line. */
|
|
5434
|
+
const POINTER_LINE_PREFIX = "> 详见 references/";
|
|
5435
|
+
/**
|
|
5436
|
+
* Evaluate a stage-time anchor against the bytes a locked read observed.
|
|
5437
|
+
* @param anchor - the caller's anchor, or `undefined` for an unanchored write.
|
|
5438
|
+
* @param current - the bytes the write lock read (`null` = the target is absent).
|
|
5439
|
+
* @returns `match` when the write may proceed, otherwise the refusal verdict.
|
|
5440
|
+
*/
|
|
5441
|
+
function anchorVerdict(anchor, current) {
|
|
5442
|
+
if (anchor === void 0) return "match";
|
|
5443
|
+
if ("absent" in anchor) return current === null ? "match" : "drift";
|
|
5444
|
+
if (current === null) return "missing";
|
|
5445
|
+
return contentHash(current) === anchor.sha256 ? "match" : "drift";
|
|
5446
|
+
}
|
|
5447
|
+
/**
|
|
5448
|
+
* Build the refusal for a skill write whose anchor did not hold. The wording is
|
|
5449
|
+
* the library's own; a caller with staged-replay wording (the skill tool, the
|
|
5450
|
+
* review plan) re-words it from {@link SkillActionResult.anchor}.
|
|
5451
|
+
* @param name - the skill name the refusal names.
|
|
5452
|
+
* @param verdict - the non-matching verdict.
|
|
5453
|
+
* @returns the refusal result (nothing was written).
|
|
5454
|
+
*/
|
|
5455
|
+
function anchorRefusal(name, verdict) {
|
|
5456
|
+
return {
|
|
5457
|
+
ok: false,
|
|
5458
|
+
stale: true,
|
|
5459
|
+
anchor: verdict,
|
|
5460
|
+
message: verdict === "missing" ? `Skill "${name}" not found.` : `Skill "${name}" changed since it was read; the write was refused to avoid overwriting newer content.`
|
|
5461
|
+
};
|
|
5462
|
+
}
|
|
5463
|
+
/**
|
|
5464
|
+
* Build the refusal for a support-file write/remove whose anchor did not hold.
|
|
5465
|
+
* @param name - the owning skill name.
|
|
5466
|
+
* @param filePath - the support-file path inside the skill.
|
|
5467
|
+
* @param verdict - the non-matching verdict.
|
|
5468
|
+
* @returns the refusal result (nothing was written or removed).
|
|
5469
|
+
*/
|
|
5470
|
+
/**
|
|
5471
|
+
* Refusal for a target the locked read could not verify at all (EISDIR, an
|
|
5472
|
+
* unreadable file). A staged replay reports "could not be verified" instead of
|
|
5473
|
+
* propagating an exception: nothing was read, so nothing can have been written.
|
|
5474
|
+
* @param name - the owning skill name.
|
|
5475
|
+
* @param filePath - the support-file path, or `null` for the skill body.
|
|
5476
|
+
* @returns the refusal result.
|
|
5477
|
+
*/
|
|
5478
|
+
function anchorUnverifiable(name, filePath) {
|
|
5479
|
+
return {
|
|
5480
|
+
ok: false,
|
|
5481
|
+
stale: true,
|
|
5482
|
+
anchor: "drift",
|
|
5483
|
+
message: filePath === null ? `Skill "${name}" could not be read to verify the staged content.` : `Support file "${filePath}" of "${name}" could not be read to verify the staged content.`
|
|
5484
|
+
};
|
|
5485
|
+
}
|
|
5486
|
+
function anchorRefusalFile(name, filePath, verdict) {
|
|
5487
|
+
return {
|
|
5488
|
+
ok: false,
|
|
5489
|
+
stale: true,
|
|
5490
|
+
anchor: verdict,
|
|
5491
|
+
message: verdict === "missing" ? `File "${filePath}" not found in skill "${name}".` : `Support file "${filePath}" of "${name}" changed since it was read; the write was refused to avoid overwriting newer content.`
|
|
5492
|
+
};
|
|
5493
|
+
}
|
|
5494
|
+
/**
|
|
5495
|
+
* v43 S2-14 (FLOW2-1/flow-3): the target could not be READ at all — EISDIR, an
|
|
5496
|
+
* unreadable file, a failing backend. `io.readText` returns null only for a
|
|
5497
|
+
* genuinely missing path and throws for everything else, so a raw errno used to
|
|
5498
|
+
* escape `update` / `patch` / `write_file` to the model while the remove path had
|
|
5499
|
+
* classified the same condition since A-5 (v15). Structured refusal, nothing
|
|
5500
|
+
* written.
|
|
5501
|
+
*
|
|
5502
|
+
* @param label - `name` or `name/filePath`, as the caller's surface names it.
|
|
5503
|
+
* @param error - the read failure.
|
|
5504
|
+
* @returns the refusal result.
|
|
5505
|
+
*/
|
|
5506
|
+
function unreadableTarget(label, error) {
|
|
5507
|
+
return {
|
|
5508
|
+
ok: false,
|
|
5509
|
+
message: `Could not read "${label}": ${error?.code === "EISDIR" ? "the path is a DIRECTORY, not a file" : error instanceof Error ? error.message : String(error)}. Nothing was written.`
|
|
5510
|
+
};
|
|
5511
|
+
}
|
|
5512
|
+
/** Upper bound of moves per restructure proposal (validator and core agree). */
|
|
5513
|
+
const MAX_RESTRUCTURE_MOVES = 5;
|
|
5514
|
+
/** Restructure targets are plain markdown files under references/ — no
|
|
5515
|
+
* subdirectories, no other support kind. V8-10 (0.3.47): the regex-level
|
|
5516
|
+
* `(?!.*\.\.)` keeps the restructure-created set EXACTLY the set
|
|
5517
|
+
* validateSupportPath can reopen — a `references/my..notes.md` target (double
|
|
5518
|
+
* dots) used to pass here while every later patch/write/remove on it was
|
|
5519
|
+
* refused as traversal (an orphan file the user could not touch). */
|
|
5520
|
+
const RESTRUCTURE_TARGET_RE = /^references\/[a-z0-9](?!.*\.\.)[a-z0-9._-]*\.md$/;
|
|
5521
|
+
/** F-20 (v18): the character rule shared by support-file names and snapshot
|
|
5522
|
+
* `extras/` entry names. The two exported names used to carry the same literal
|
|
5523
|
+
* independently; both now derive from this one. */
|
|
5524
|
+
const SUPPORT_ENTRY_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
5525
|
+
/** Extra file name carried inside a snapshot's `extras/` directory. */
|
|
5526
|
+
const SNAPSHOT_EXTRA_NAME_RE = SUPPORT_ENTRY_NAME_RE;
|
|
5527
|
+
function skillsRoot(env = process.env) {
|
|
5528
|
+
return join(evolutionRoot(env), "skills");
|
|
5351
5529
|
}
|
|
5352
|
-
/**
|
|
5353
|
-
|
|
5354
|
-
|
|
5530
|
+
/** 0.3.18 (S4.1, E-30): the ONE root resolution for every member that reads
|
|
5531
|
+
* the skills tree — tool-skill-manage / evolution-skill-catalog / skill-usage
|
|
5532
|
+
* / evolution-learning-graph used to each resolve `config.root || skillsRoot()`
|
|
5533
|
+
* (and the graph ignored config entirely). Empty/whitespace config falls
|
|
5534
|
+
* through to the default; callers pass their raw Config. The optional field is
|
|
5535
|
+
* declared `| undefined` so a config object whose root field is explicitly
|
|
5536
|
+
* `string | undefined` still assignable under exactOptionalPropertyTypes. */
|
|
5537
|
+
function resolveSkillsRoot(config = {}) {
|
|
5538
|
+
return (config.root ?? "").trim() || skillsRoot();
|
|
5355
5539
|
}
|
|
5356
|
-
/**
|
|
5357
|
-
*
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5540
|
+
/** E-7 (v18) → V27 G2.4 (M-08): every family row reads ONE root key. `root` is
|
|
5541
|
+
* canonical; the `skillsRoot` alias was honoured for one minor version and its
|
|
5542
|
+
* window closed at 0.3.65 — it is now two releases past expiry, so this
|
|
5543
|
+
* resolver no longer reads it at all. A deployment that still sets the alias
|
|
5544
|
+
* must fail LOUDLY at load (see {@link assertSkillsRootAliasRetired}): silently
|
|
5545
|
+
* ignoring a config key leaves the deployment pointing at a root nobody reads,
|
|
5546
|
+
* which is the worst form of compatibility.
|
|
5547
|
+
* @param config - the raw plugin config.
|
|
5548
|
+
* @returns the effective root (empty when the key is unset or blank).
|
|
5549
|
+
*/
|
|
5550
|
+
function resolveRootConfig(config = {}) {
|
|
5551
|
+
return { root: (config.root ?? "").trim() };
|
|
5361
5552
|
}
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
if (
|
|
5371
|
-
if (!parsed.frontmatter.description) return "Frontmatter must include a description field.";
|
|
5372
|
-
if (parsed.frontmatter.description.length > limits.maxDescriptionLength) return `Description exceeds ${limits.maxDescriptionLength} characters.`;
|
|
5373
|
-
if (exceedsContentLimit(content, limits.maxSkillContentChars) && !shrinksOverLimit(content, current, limits.maxSkillContentChars)) return `SKILL.md content exceeds ${limits.maxSkillContentChars} characters. Consider splitting into a smaller SKILL.md with supporting files.`;
|
|
5374
|
-
return null;
|
|
5553
|
+
/** V27 G2.4 (M-08): the retirement gate for the expired `skillsRoot` alias.
|
|
5554
|
+
* Called at each plugin's load boundary (before the root is resolved), it turns
|
|
5555
|
+
* a stale key into an explicit load error naming the replacement — the
|
|
5556
|
+
* fail-loud form the plan requires instead of a silent no-op.
|
|
5557
|
+
* @param config - the raw plugin config (the alias field stays DECLARED in each
|
|
5558
|
+
* schema so the loader can hand it here instead of dropping it).
|
|
5559
|
+
*/
|
|
5560
|
+
function assertSkillsRootAliasRetired(config = {}) {
|
|
5561
|
+
if ((config.skillsRoot ?? "").trim() !== "") throw new Error("evolution: config \"skillsRoot\" was removed after 0.3.65 — rename the key to \"root\" (the alias is no longer honoured)");
|
|
5375
5562
|
}
|
|
5376
5563
|
/**
|
|
5377
|
-
*
|
|
5378
|
-
*
|
|
5379
|
-
*
|
|
5380
|
-
*
|
|
5381
|
-
*
|
|
5564
|
+
* Map a requesting session onto the two origin surfaces (rc.44 plan M2-2.3):
|
|
5565
|
+
* the APPROVAL surface treats every delegated subagent as the autonomous
|
|
5566
|
+
* review channel, while the LIBRARY surface keeps the Hermes distinction -
|
|
5567
|
+
* the review fork is 'background_review' (the pinned guard blocks its
|
|
5568
|
+
* writes) and any other subagent is 'subagent' (agent-authored, not
|
|
5569
|
+
* review-channel). `isReview` marks the caller as the background review
|
|
5570
|
+
* pipeline itself. Single source: the two tools and the review executor all
|
|
5571
|
+
* read this table instead of re-deriving it.
|
|
5382
5572
|
*/
|
|
5383
|
-
function
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5573
|
+
function resolveOrigins(headerOrigin, isReview = false) {
|
|
5574
|
+
if (isReview) return {
|
|
5575
|
+
approval: "background_review",
|
|
5576
|
+
library: "background_review"
|
|
5577
|
+
};
|
|
5578
|
+
if (headerOrigin === "subagent") return {
|
|
5579
|
+
approval: "background_review",
|
|
5580
|
+
library: "subagent"
|
|
5581
|
+
};
|
|
5390
5582
|
return {
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
hasColon,
|
|
5394
|
-
lines
|
|
5583
|
+
approval: "foreground",
|
|
5584
|
+
library: "foreground"
|
|
5395
5585
|
};
|
|
5396
5586
|
}
|
|
5397
|
-
/**
|
|
5398
|
-
*
|
|
5399
|
-
*
|
|
5400
|
-
*
|
|
5401
|
-
*
|
|
5402
|
-
*
|
|
5587
|
+
/**
|
|
5588
|
+
* S1-E8 (0.3.80): ONE exec→origins resolution for the two write tools — reads
|
|
5589
|
+
* the session header origin AND the v37 S2.2 review-channel session mark, so a
|
|
5590
|
+
* tool cannot forget the mark half (tool-memory shipped without it, which
|
|
5591
|
+
* mislabeled every inject-mode review memory write as `foreground` and let it
|
|
5592
|
+
* bypass staging under `stageForeground: false`).
|
|
5593
|
+
* Single source: both tools call this instead of re-deriving the pair.
|
|
5594
|
+
*/
|
|
5595
|
+
function resolveExecOrigins(exec) {
|
|
5596
|
+
const session = exec?.agent?.session;
|
|
5597
|
+
return resolveOrigins(session?.header?.origin, isReviewChannelSession(typeof session?.id === "string" ? session.id : void 0));
|
|
5598
|
+
}
|
|
5599
|
+
function skillDir(root, name) {
|
|
5600
|
+
return join(root, name);
|
|
5601
|
+
}
|
|
5602
|
+
/** Dot-prefixed on-disk marker name. SINGLE source: `list()` matches directory
|
|
5603
|
+
* entries against this name, and path builders must never hardcode a marker
|
|
5604
|
+
* literal (N-1: the rc.49 exists()-probe convergence dropped the dot,
|
|
5605
|
+
* poisoning every protectedBy/managed report). Exported for cross-package
|
|
5606
|
+
* consumers that must probe markers without re-deriving the name (curator's
|
|
5607
|
+
* archive-copy bundled probe, 0.3.26 V4-02). */
|
|
5608
|
+
function markerEntryName(marker) {
|
|
5609
|
+
return `.${marker}`;
|
|
5610
|
+
}
|
|
5611
|
+
/** F-17 (v18): the root-level lock files a DESTRUCTIVE MOVER must treat as an
|
|
5612
|
+
* active writer (skill body + the two marker writers). Single source with
|
|
5613
|
+
* `markerEntryName`/`LOCK_SUFFIX` so a renamed marker cannot silently drop out
|
|
5614
|
+
* of the ghost-writer probe. */
|
|
5615
|
+
const MARKER_LOCK_NAMES = [
|
|
5616
|
+
`SKILL.md${LOCK_SUFFIX}`,
|
|
5617
|
+
`.pinned${LOCK_SUFFIX}`,
|
|
5618
|
+
`.hermes-managed${LOCK_SUFFIX}`
|
|
5619
|
+
];
|
|
5620
|
+
/** v23 (ML-1): `.archive` retention window (see pruneExpiredArchives). */
|
|
5621
|
+
const ARCHIVE_RETENTION_DAYS = 365;
|
|
5622
|
+
function markerPath(dir, marker) {
|
|
5623
|
+
return join(dir, markerEntryName(marker));
|
|
5624
|
+
}
|
|
5403
5625
|
function isCommittedOnly(error) {
|
|
5404
5626
|
return isCommittedWarning(error);
|
|
5405
5627
|
}
|
|
@@ -5481,99 +5703,6 @@ function validateSupportPath(filePath) {
|
|
|
5481
5703
|
return null;
|
|
5482
5704
|
}
|
|
5483
5705
|
/**
|
|
5484
|
-
* Index of `pattern` inside `content`, treating whitespace runs (spaces/tabs)
|
|
5485
|
-
* as flexible and literal escape sequences (`\n`, `\t`, `\r`) as their real
|
|
5486
|
-
* characters: a PATTERN whitespace run matches any content run of any length
|
|
5487
|
-
* (even empty), while extra whitespace that only exists in the content is not
|
|
5488
|
-
* skipped — the flexibility is one-sided on the pattern, and a backslash-
|
|
5489
|
-
* escaped char in the pattern matches the real char in the content
|
|
5490
|
-
* (model-copy drift). Returns the [start, end) range in the ORIGINAL content
|
|
5491
|
-
* so a patch can replace exactly the matched span and keep every other byte
|
|
5492
|
-
* intact. Returns null when no fuzzy match exists.
|
|
5493
|
-
*/
|
|
5494
|
-
function fuzzyIndexOf(content, pattern, from = 0) {
|
|
5495
|
-
const isSpace = (char) => char !== void 0 && /[ \t]/.test(char);
|
|
5496
|
-
const escaped = (char) => {
|
|
5497
|
-
if (char === "n") return "\n";
|
|
5498
|
-
if (char === "t") return " ";
|
|
5499
|
-
if (char === "r") return "\r";
|
|
5500
|
-
return null;
|
|
5501
|
-
};
|
|
5502
|
-
for (let start = from; start < content.length; start += 1) {
|
|
5503
|
-
let contentIndex = start;
|
|
5504
|
-
let patternIndex = 0;
|
|
5505
|
-
while (patternIndex < pattern.length && contentIndex < content.length) {
|
|
5506
|
-
const patternChar = pattern[patternIndex];
|
|
5507
|
-
const contentChar = content[contentIndex];
|
|
5508
|
-
if (isSpace(patternChar)) {
|
|
5509
|
-
while (patternIndex < pattern.length && isSpace(pattern[patternIndex])) patternIndex += 1;
|
|
5510
|
-
while (contentIndex < content.length && isSpace(content[contentIndex])) contentIndex += 1;
|
|
5511
|
-
continue;
|
|
5512
|
-
}
|
|
5513
|
-
const escapedChar = patternChar === "\\" ? escaped(pattern[patternIndex + 1]) : null;
|
|
5514
|
-
if (escapedChar !== null && contentChar === escapedChar) {
|
|
5515
|
-
patternIndex += 2;
|
|
5516
|
-
contentIndex += 1;
|
|
5517
|
-
continue;
|
|
5518
|
-
}
|
|
5519
|
-
if (patternChar === contentChar) {
|
|
5520
|
-
contentIndex += 1;
|
|
5521
|
-
patternIndex += 1;
|
|
5522
|
-
continue;
|
|
5523
|
-
}
|
|
5524
|
-
break;
|
|
5525
|
-
}
|
|
5526
|
-
if (patternIndex === pattern.length) return [start, contentIndex];
|
|
5527
|
-
}
|
|
5528
|
-
return null;
|
|
5529
|
-
}
|
|
5530
|
-
/** V6-17 (0.3.37): the fuzzy-patch scan is O(n·m) with no input bound; a
|
|
5531
|
-
* non-exact anchor past these budgets would block the event loop (measured
|
|
5532
|
-
* ~6s at 20k×20k). Exact matches go through the fast `includes` path and stay
|
|
5533
|
-
* allowed regardless of size. */
|
|
5534
|
-
const FUZZY_MAX_PATTERN_CHARS = 4096;
|
|
5535
|
-
const FUZZY_MAX_WORK = 8e6;
|
|
5536
|
-
/** Trim leading whitespace of the first line and trailing whitespace of the last line. */
|
|
5537
|
-
function trimPatternBoundaries(pattern) {
|
|
5538
|
-
const from = pattern.search(/\S/);
|
|
5539
|
-
const trimmed = from < 0 ? pattern : pattern.slice(from);
|
|
5540
|
-
const trailing = trimmed.search(/\s+$/);
|
|
5541
|
-
return trailing < 0 ? trimmed : trimmed.slice(0, trailing);
|
|
5542
|
-
}
|
|
5543
|
-
/** Replace only the fuzzy-matched span, preserving all surrounding bytes.
|
|
5544
|
-
* V7-11 (0.3.44): the replaceAll loop accumulates the per-scan cost — the
|
|
5545
|
-
* single-scan budget at the caller only bounded ONE fuzzyIndexOf, while an
|
|
5546
|
-
* unbounded number of matches × O(n·m) each could still stall the loop.
|
|
5547
|
-
* When the accumulated cost exceeds the budget the whole replace fails
|
|
5548
|
-
* (null) instead of partially applying an arbitrary prefix. */
|
|
5549
|
-
function fuzzyReplace(content, oldString, newString, replaceAll) {
|
|
5550
|
-
let current = content;
|
|
5551
|
-
let scanFrom = 0;
|
|
5552
|
-
let totalWork = 0;
|
|
5553
|
-
for (;;) {
|
|
5554
|
-
totalWork += current.length * oldString.length;
|
|
5555
|
-
if (totalWork > FUZZY_MAX_WORK) return null;
|
|
5556
|
-
const match = fuzzyIndexOf(current, oldString, scanFrom);
|
|
5557
|
-
if (match === null) return current;
|
|
5558
|
-
const [start, end] = match;
|
|
5559
|
-
const next = current.slice(0, start) + newString + current.slice(end);
|
|
5560
|
-
if (!replaceAll) return next;
|
|
5561
|
-
current = next;
|
|
5562
|
-
scanFrom = start + newString.length;
|
|
5563
|
-
}
|
|
5564
|
-
}
|
|
5565
|
-
function fuzzyPatch(content, oldString, newString, replaceAll = false) {
|
|
5566
|
-
if (oldString === "") return null;
|
|
5567
|
-
if (content.includes(oldString)) return replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, () => newString);
|
|
5568
|
-
const boundary = trimPatternBoundaries(oldString);
|
|
5569
|
-
if (boundary === "") return null;
|
|
5570
|
-
if (boundary !== oldString) {
|
|
5571
|
-
if (fuzzyIndexOf(content, boundary) !== null) return fuzzyReplace(content, boundary, newString, replaceAll);
|
|
5572
|
-
}
|
|
5573
|
-
if (fuzzyIndexOf(content, oldString) !== null) return fuzzyReplace(content, oldString, newString, replaceAll);
|
|
5574
|
-
return null;
|
|
5575
|
-
}
|
|
5576
|
-
/**
|
|
5577
5706
|
* Support-directory references in a markdown body (009 kernel): `references/…`,
|
|
5578
5707
|
* `templates/…`, `scripts/…`, `assets/…` relative links — any extension and
|
|
5579
5708
|
* nested paths (v7 audit P3-1: `.md`-only matching missed `scripts/run.sh` and
|
|
@@ -5692,7 +5821,7 @@ var SkillLibrary = class {
|
|
|
5692
5821
|
if (this.transact) try {
|
|
5693
5822
|
await this.transact(this.io, path, run);
|
|
5694
5823
|
} catch (error) {
|
|
5695
|
-
if (!progress.entered && readFailure !== void 0) return readFailure;
|
|
5824
|
+
if (!progress.entered && readFailure !== void 0) return readFailure(error);
|
|
5696
5825
|
if (!committedOnly(error)) throw error;
|
|
5697
5826
|
durabilityWarning = error instanceof Error ? error.message : String(error);
|
|
5698
5827
|
}
|
|
@@ -5701,7 +5830,7 @@ var SkillLibrary = class {
|
|
|
5701
5830
|
try {
|
|
5702
5831
|
current = await this.io.readText(path);
|
|
5703
5832
|
} catch (error) {
|
|
5704
|
-
if (readFailure !== void 0) return readFailure;
|
|
5833
|
+
if (readFailure !== void 0) return readFailure(error);
|
|
5705
5834
|
throw error;
|
|
5706
5835
|
}
|
|
5707
5836
|
const next = await run(current);
|
|
@@ -6104,7 +6233,17 @@ var SkillLibrary = class {
|
|
|
6104
6233
|
ok: false,
|
|
6105
6234
|
message: `Skill "${normalized}" already exists.`
|
|
6106
6235
|
};
|
|
6107
|
-
|
|
6236
|
+
let rootEntries;
|
|
6237
|
+
try {
|
|
6238
|
+
rootEntries = await this.io.list(this.root);
|
|
6239
|
+
} catch (error) {
|
|
6240
|
+
const cause = error instanceof Error ? error.message : String(error);
|
|
6241
|
+
return {
|
|
6242
|
+
ok: false,
|
|
6243
|
+
message: `Skill "${normalized}" could not be checked for a case-variant collision: listing ${this.root} failed (${cause}). Refusing the create — a second directory differing only in case cannot be undone across platforms.`
|
|
6244
|
+
};
|
|
6245
|
+
}
|
|
6246
|
+
for (const entry of rootEntries) if (typeof entry === "string" && entry !== normalized && entry.toLowerCase() === normalized.toLowerCase()) return {
|
|
6108
6247
|
ok: false,
|
|
6109
6248
|
message: `Skill "${normalized}" collides with the existing case-variant directory "${entry}" (skill names are lowercase-only); rename one of them.`
|
|
6110
6249
|
};
|
|
@@ -6120,6 +6259,7 @@ var SkillLibrary = class {
|
|
|
6120
6259
|
let createDurabilityWarning = "";
|
|
6121
6260
|
if (this.transact) try {
|
|
6122
6261
|
await this.transact(this.io, createPath, (current) => {
|
|
6262
|
+
existsAtCommit = false;
|
|
6123
6263
|
taskRan = true;
|
|
6124
6264
|
if (current !== null) {
|
|
6125
6265
|
existsAtCommit = true;
|
|
@@ -6267,7 +6407,7 @@ var SkillLibrary = class {
|
|
|
6267
6407
|
skillDir: dir
|
|
6268
6408
|
}
|
|
6269
6409
|
};
|
|
6270
|
-
}, anchor !== void 0 ? anchorUnverifiable(name, null) :
|
|
6410
|
+
}, anchor !== void 0 ? () => anchorUnverifiable(name, null) : (error) => unreadableTarget(name, error));
|
|
6271
6411
|
}
|
|
6272
6412
|
async patch(rawName, oldString, newString, filePath = "", replaceAll = false, origin = "foreground") {
|
|
6273
6413
|
const name = rawName.trim();
|
|
@@ -6310,7 +6450,7 @@ var SkillLibrary = class {
|
|
|
6310
6450
|
},
|
|
6311
6451
|
write: null
|
|
6312
6452
|
};
|
|
6313
|
-
if (!md.includes(oldString) && (oldString.length >
|
|
6453
|
+
if (!md.includes(oldString) && (oldString.length > 4096 || md.length * oldString.length > 8e6)) return {
|
|
6314
6454
|
result: {
|
|
6315
6455
|
ok: false,
|
|
6316
6456
|
message: `old_string too large for fuzzy match (${oldString.length} chars in ${patchLabel}); use update for a full rewrite or a narrower anchor.`
|
|
@@ -6421,7 +6561,7 @@ var SkillLibrary = class {
|
|
|
6421
6561
|
skillDir: dir
|
|
6422
6562
|
}
|
|
6423
6563
|
};
|
|
6424
|
-
});
|
|
6564
|
+
}, (error) => unreadableTarget(name + "/" + patchLabel, error));
|
|
6425
6565
|
}
|
|
6426
6566
|
/**
|
|
6427
6567
|
* P2-9 (v15): the destructive directory move shared by archive and
|
|
@@ -7009,6 +7149,7 @@ var SkillLibrary = class {
|
|
|
7009
7149
|
const drift = { seen: false };
|
|
7010
7150
|
const ran = { done: false };
|
|
7011
7151
|
await this.transact(this.io, entry.target, (current) => {
|
|
7152
|
+
drift.seen = false;
|
|
7012
7153
|
ran.done = true;
|
|
7013
7154
|
if (current !== baseline) {
|
|
7014
7155
|
drift.seen = true;
|
|
@@ -7223,7 +7364,7 @@ var SkillLibrary = class {
|
|
|
7223
7364
|
file: target
|
|
7224
7365
|
}
|
|
7225
7366
|
};
|
|
7226
|
-
}, anchor !== void 0 ? anchorUnverifiable(name, filePath) :
|
|
7367
|
+
}, anchor !== void 0 ? () => anchorUnverifiable(name, filePath) : (error) => unreadableTarget(name + "/" + filePath, error));
|
|
7227
7368
|
}
|
|
7228
7369
|
async removeSupportFile(rawName, filePath, origin = "foreground", anchor) {
|
|
7229
7370
|
const name = rawName.trim();
|
|
@@ -7651,6 +7792,35 @@ function newSkillLibrary(options) {
|
|
|
7651
7792
|
*/
|
|
7652
7793
|
const FAMILY_SESSION_TOOL_NAMES = ["skill_manage", "memory"];
|
|
7653
7794
|
/**
|
|
7795
|
+
* S0-4 (v43 J-1 / G-1): the process-wide witness behind the deployment
|
|
7796
|
+
* diagnostic. A scoped false is the CORRECT answer for a session that did not
|
|
7797
|
+
* opt in, so the miss alone proves nothing — "no session in this process ever
|
|
7798
|
+
* matched" is the shape both real faults share, and only a witness that outlives
|
|
7799
|
+
* one session can tell it from a healthy per-session skip.
|
|
7800
|
+
*
|
|
7801
|
+
* Module scope on purpose: the claim spans every session and every row of this
|
|
7802
|
+
* process, so no single row's fiber owns it. Monotone scalars with no per-key
|
|
7803
|
+
* lifecycle — the N12 registry covers module-scope Set/Map/WeakMap stores, which
|
|
7804
|
+
* carry entries that do need one.
|
|
7805
|
+
*/
|
|
7806
|
+
let scopedProbeHits = 0;
|
|
7807
|
+
let scopedProbeMisses = 0;
|
|
7808
|
+
let scopedProbeWarned = false;
|
|
7809
|
+
/**
|
|
7810
|
+
* Read the witness for a diagnostic surface (`/evolution doctor`). Read-only: it
|
|
7811
|
+
* neither evaluates the probe nor consumes the one-time warn, so a report run
|
|
7812
|
+
* cannot change what the next miss would have logged.
|
|
7813
|
+
* @returns the verdict with both counts, zeroed in a process where the gate has
|
|
7814
|
+
* not run.
|
|
7815
|
+
*/
|
|
7816
|
+
function scopedProbeReport() {
|
|
7817
|
+
return {
|
|
7818
|
+
verdict: scopedProbeHits > 0 ? "hit" : scopedProbeMisses > 0 ? "never-hit" : "idle",
|
|
7819
|
+
hits: scopedProbeHits,
|
|
7820
|
+
misses: scopedProbeMisses
|
|
7821
|
+
};
|
|
7822
|
+
}
|
|
7823
|
+
/**
|
|
7654
7824
|
* Does this session's scope see the family's model tools?
|
|
7655
7825
|
*
|
|
7656
7826
|
* The scope is the live agent's — the platform's own addressing for "what does
|
|
@@ -7671,6 +7841,23 @@ function sessionSeesFamilyTools(ctx, sessionId) {
|
|
|
7671
7841
|
return FAMILY_SESSION_TOOL_NAMES.some((name) => tools.get(name, scope) !== void 0);
|
|
7672
7842
|
}
|
|
7673
7843
|
/**
|
|
7844
|
+
* S0-4 (v43 J-1 / G-1): record a scoped rejection and leave the one-time
|
|
7845
|
+
* deployment diagnostic.
|
|
7846
|
+
*
|
|
7847
|
+
* Fires at most once per process, on the first miss, and only while nothing has
|
|
7848
|
+
* ever matched. The message names the check rather than guessing the deployment
|
|
7849
|
+
* form: a host-only install and a disabled model row are the two shapes that can
|
|
7850
|
+
* never match, while the layered variant form legitimately waits for a session
|
|
7851
|
+
* that selected the Evolution preset.
|
|
7852
|
+
* @param ctx - the asking row's context; the warn rides that row's own logger.
|
|
7853
|
+
*/
|
|
7854
|
+
function noteScopedProbeMiss(ctx) {
|
|
7855
|
+
scopedProbeMisses += 1;
|
|
7856
|
+
if (scopedProbeHits > 0 || scopedProbeWarned) return;
|
|
7857
|
+
scopedProbeWarned = true;
|
|
7858
|
+
ctx.logger.warn("dsh-evolution: sessionScoped is on but the family-tool probe has never matched a session in this process — review injection and skill-usage telemetry are inert for every session seen so far. Check whether the family's model rows are mounted at all: tool-memory / tool-skill-manage are devDependencies of @lmzhen/dsh-evolution-host, so a HOST-ONLY install can never match, and a profile overlay that disables either row has the same effect. Under the layered VARIANT form only a session on the Evolution preset matches, so a miss there just means none has run yet. Next: /evolution doctor reports the scoped rows and this verdict (see INSTALL.md).");
|
|
7859
|
+
}
|
|
7860
|
+
/**
|
|
7674
7861
|
* The one decision every cross-session consumer calls before it acts.
|
|
7675
7862
|
* @param ctx - a context of the runtime.
|
|
7676
7863
|
* @param sessionId - the session the event belongs to.
|
|
@@ -7678,11 +7865,17 @@ function sessionSeesFamilyTools(ctx, sessionId) {
|
|
|
7678
7865
|
* @returns true when the consumer may act on this session. A deployment that did
|
|
7679
7866
|
* not declare session scoping always answers true (the historical behavior);
|
|
7680
7867
|
* a scoped one answers true only for a session that carries the family's model
|
|
7681
|
-
* tools.
|
|
7868
|
+
* tools. Each scoped answer updates the process witness, and the first miss with
|
|
7869
|
+
* no match ever leaves one warn (see {@link noteScopedProbeMiss}).
|
|
7682
7870
|
*/
|
|
7683
7871
|
function sessionAudited(ctx, sessionId, sessionScoped) {
|
|
7684
7872
|
if (sessionScoped !== true) return true;
|
|
7685
|
-
|
|
7873
|
+
if (sessionSeesFamilyTools(ctx, sessionId)) {
|
|
7874
|
+
scopedProbeHits += 1;
|
|
7875
|
+
return true;
|
|
7876
|
+
}
|
|
7877
|
+
noteScopedProbeMiss(ctx);
|
|
7878
|
+
return false;
|
|
7686
7879
|
}
|
|
7687
7880
|
//#endregion
|
|
7688
|
-
export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, 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_MODEL, 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, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, EMPTY_LOCK_TAKEOVER_MS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, INSTANCE_KEYS, LOCK_BODY_RE, LOCK_SUFFIX, LOCK_TEAR_TAKEOVER_MS, LOW_QUALITY_THRESHOLD, LostWriteLock, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PATTERN_OVERLAP,
|
|
7881
|
+
export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, 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_MODEL, 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, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, EMPTY_LOCK_TAKEOVER_MS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, INSTANCE_KEYS, LOCK_BODY_RE, LOCK_SUFFIX, LOCK_TEAR_TAKEOVER_MS, LOW_QUALITY_THRESHOLD, LostWriteLock, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PATTERN_OVERLAP, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, THREAT_EXEMPTION_HINT, ToolDispatchNormalizer, ToolDispatchPayloadError, advanceReview, allowRowCollisions, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, callingScope, claimInstance, clampedNumber, clearReviewChannel, collectReadSkillNames, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, countDispatches, createGateSet, decideTakeover, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionEventPayloadIssue, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurn, frontmatterBlock, frontmatterCatalogInvalid, getRecord, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isCommittedWarning, isGlobalRead, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadRowOverrides, loadSuppressedNames, loadUsage, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, neutralizePromptVariables, newSkillLibrary, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseLockBody, pendingSelfCleanup, persistedWriteSite, persistedWriteSites, probeAbsent, probeList, probeMtime, probePresent, probeReason, probeUnknown, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, releaseInstance, renameWithRetry, renderCuratorReportMarkdown, resolveExecOrigins, resolveOrigins, resolveRootConfig, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, scopedProbeReport, sessionAudited, sessionSeesFamilyTools, skillReadNameOf, skillsRoot, suppressedFile, sweepReviewChannelSessions, transactIo, transactTaskGuard, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, writeDurableTmp, yamlPlainScalarNeedsQuotes };
|