@lmzhen/dsh-evolution-core 0.3.70 → 0.3.72
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 +309 -123
- package/lib/types/constants.d.ts +16 -0
- package/lib/types/evolution-events.d.ts +1 -1
- package/lib/types/index.d.ts +17 -0
- package/lib/types/skill-store.d.ts +70 -4
- package/lib/types/state-store.d.ts +10 -3
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { basename, dirname, join } from "node:path";
|
|
1
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
2
2
|
import { cp, lstat, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { createHash, randomBytes } from "node:crypto";
|
|
4
4
|
import { homedir } from "node:os";
|
|
@@ -956,6 +956,41 @@ async function updateSuppressedNames(root, io, task) {
|
|
|
956
956
|
* threshold, which are intentionally left where they are used.
|
|
957
957
|
* @module @lmzhen/dsh-evolution-core
|
|
958
958
|
*/
|
|
959
|
+
/**
|
|
960
|
+
* Required argument names per `skill_manage` action — the SINGLE SOURCE read
|
|
961
|
+
* by the tool's argument gate (tool-skill-manage executeCore) and the plan
|
|
962
|
+
* validator (evolution-plan-validator), so the two can never drift.
|
|
963
|
+
* OPT-05 (2026-09): the plan validator used to accept a `write_file`/
|
|
964
|
+
* `remove_file` op without `file_path` while the executor required it — the
|
|
965
|
+
* staged write then failed at EVERY approve until rejected.
|
|
966
|
+
* Rows here are the op-level requirements only: `delete` additionally
|
|
967
|
+
* requires `absorbed_into` at the PLAN layer (review passes may only delete
|
|
968
|
+
* into an umbrella) and `pin`/`unpin` are tool-only actions — each consumer
|
|
969
|
+
* adds its own extras on top of this table. An empty-string argument is NOT
|
|
970
|
+
* caught here (the tool's gate deliberately lets it reach the library for a
|
|
971
|
+
* more specific remedy message); the validator adds its own `.trim()`
|
|
972
|
+
* emptiness checks for payload fields.
|
|
973
|
+
*/
|
|
974
|
+
const SKILL_ACTION_REQUIRED_FIELDS = {
|
|
975
|
+
create: ["name", "content"],
|
|
976
|
+
edit: ["name", "content"],
|
|
977
|
+
update: ["name", "content"],
|
|
978
|
+
patch: [
|
|
979
|
+
"name",
|
|
980
|
+
"old_string",
|
|
981
|
+
"new_string"
|
|
982
|
+
],
|
|
983
|
+
delete: ["name"],
|
|
984
|
+
write_file: [
|
|
985
|
+
"name",
|
|
986
|
+
"file_path",
|
|
987
|
+
"file_content"
|
|
988
|
+
],
|
|
989
|
+
remove_file: ["name", "file_path"],
|
|
990
|
+
restructure: ["name"],
|
|
991
|
+
pin: ["name"],
|
|
992
|
+
unpin: ["name"]
|
|
993
|
+
};
|
|
959
994
|
/** Skill frontmatter `name` validated for the file name (lowercase + hyphen).
|
|
960
995
|
* 计划 B-4 (v18): tightened to the UPSTREAM `SKILL_NAME` shape
|
|
961
996
|
* (`/^[a-z0-9]+(?:-[a-z0-9]+)*$/`, packages/skill/skill/src/index.ts:20). The
|
|
@@ -1647,11 +1682,11 @@ async function readEvolutionEvents(io, path) {
|
|
|
1647
1682
|
* malformed (or unreadable) ARCHIVE is skipped — it never bricks the boot and
|
|
1648
1683
|
* it is still flagged.
|
|
1649
1684
|
*/
|
|
1650
|
-
async function readEvolutionTimeline(io, path) {
|
|
1685
|
+
async function readEvolutionTimeline(io, path, archives) {
|
|
1651
1686
|
const dir = dirname(path);
|
|
1652
1687
|
let malformed = false;
|
|
1653
1688
|
const bySeq = /* @__PURE__ */ new Map();
|
|
1654
|
-
for (const name of await listEventArchives(io, path)) {
|
|
1689
|
+
for (const name of archives ?? await listEventArchives(io, path)) {
|
|
1655
1690
|
const read = await readEvolutionEvents(io, join(dir, name));
|
|
1656
1691
|
if (read.malformed) malformed = true;
|
|
1657
1692
|
for (const event of read.events) bySeq.set(event.seq, event);
|
|
@@ -2137,13 +2172,22 @@ function buildLearnPrompt(userRequest) {
|
|
|
2137
2172
|
* C-11: the adoption test and the RETURNED value now come from the
|
|
2138
2173
|
* SAME trimmed source — the old form tested `trim()` but returned the raw
|
|
2139
2174
|
* value, so `DSH_HOME=" /x "` was accepted AND persisted with literal spaces.
|
|
2140
|
-
*
|
|
2141
|
-
*
|
|
2142
|
-
*
|
|
2175
|
+
* OPT-27 (2026-09, plan D5 — accepted): the v10-era "no `~` expansion, no
|
|
2176
|
+
* resolve" divergence from upstream `resolveDshHome` is RETIRED. It became
|
|
2177
|
+
* load-bearing when the skill-catalog shadow made "same tree as the upstream
|
|
2178
|
+
* `USER_DSH_RANK` provider" a hard contract: upstream watches the EXPANDED
|
|
2179
|
+
* absolute `<home>/skills` while this value fed a literal `~/x` (a directory
|
|
2180
|
+
* named `~` under the host CWD) or a CWD-relative path — split-brain skill
|
|
2181
|
+
* trees, preset installs the platform never reads, doctor probes of a
|
|
2182
|
+
* directory nothing serves. Behavior now matches upstream:
|
|
2183
|
+
* `resolve(expandHomePath(selected))`. Only `~`-prefixed and RELATIVE
|
|
2184
|
+
* DSH_HOME values change landing spot; absolute homes are byte-identical.
|
|
2143
2185
|
*/
|
|
2144
2186
|
function evolutionRoot(env = process.env) {
|
|
2145
2187
|
const home = env.DSH_HOME?.trim();
|
|
2146
|
-
|
|
2188
|
+
const selected = home ? home : join(homedir(), ".dsh");
|
|
2189
|
+
const expanded = selected === "~" ? homedir() : selected.startsWith("~/") || selected.startsWith("~\\") ? join(homedir(), selected.slice(2)) : selected;
|
|
2190
|
+
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
2147
2191
|
}
|
|
2148
2192
|
/** Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
|
|
2149
2193
|
* state (reports, activity store, feedback file, state-domain data). */
|
|
@@ -2516,7 +2560,9 @@ function scanMemoryThreats(text, maxScanChars = 65536, options = NO_SCAN_OPTIONS
|
|
|
2516
2560
|
function scanContentThreats(text, maxScanChars = 65536, options = NO_SCAN_OPTIONS) {
|
|
2517
2561
|
const { blocked, findings } = evaluateThreat(text, "strict", maxScanChars, options);
|
|
2518
2562
|
if (!blocked) return null;
|
|
2519
|
-
|
|
2563
|
+
const pattern = findings.find((f) => f.category !== "unicode_obfuscation");
|
|
2564
|
+
if (pattern) return `Blocked by security scan (${pattern.label}). This content appears to contain potentially malicious instructions.${THREAT_EXEMPTION_HINT}`;
|
|
2565
|
+
return `Blocked by security scan: invisible or potentially malicious Unicode detected. This content appears to contain potentially malicious instructions.${THREAT_EXEMPTION_HINT}`;
|
|
2520
2566
|
}
|
|
2521
2567
|
/**
|
|
2522
2568
|
* WD2 (0.3.56): the shared tail of every user-facing threat block — names the
|
|
@@ -3508,7 +3554,7 @@ const SECRET_PATTERNS = [
|
|
|
3508
3554
|
const INLINE_ASSIGNMENT_PATTERN = /* @__PURE__ */ new RegExp("(^|[^\\w-])([\\w-]{0,64}[_\\-])?((?:token|api[_-]?key|secret|password|passwd)(?:[_\\-][\\w-]{0,64})?)\\b([\"\\']?[\\t ]*[:=][\\t ]*)([^\\r\\n]+)", "gi");
|
|
3509
3555
|
const URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:)([^\s/@]+)@/gi;
|
|
3510
3556
|
const PEM_PRIVATE_KEY_PATTERN = new RegExp(`-----BEGIN\\s+(?:(?:RSA|EC|DSA|OPENSSH|ENCRYPTED|PGP)\\s+)?PRIVATE\\s+KEY(?:\\s+BLOCK)?-----[\\s\\S]*?-----END\\s+(?:(?:RSA|EC|DSA|OPENSSH|ENCRYPTED|PGP)\\s+)?PRIVATE\\s+KEY(?:\\s+BLOCK)?-----`, "g");
|
|
3511
|
-
const BLOCK_KEY_ONLY_LINE = /(?:^|\s)([\w-]{0,64}[_\-])?((?:token|api[_-]?key|secret|password|passwd)(?:[_\-][\w-]{0,64})?)\s
|
|
3557
|
+
const BLOCK_KEY_ONLY_LINE = /(?:^|\s)([\w-]{0,64}[_\-])?((?:token|api[_-]?key|secret|password|passwd)(?:[_\-][\w-]{0,64})?)\s*:(?:\r)?$/i;
|
|
3512
3558
|
/**
|
|
3513
3559
|
* Mask credential-shaped text before it crosses a session boundary.
|
|
3514
3560
|
* @param text - the text about to be sent to a model outside this session.
|
|
@@ -3525,7 +3571,7 @@ function redactSecrets(text) {
|
|
|
3525
3571
|
const line = lines[i];
|
|
3526
3572
|
if (line === void 0 || !BLOCK_KEY_ONLY_LINE.test(line)) continue;
|
|
3527
3573
|
const next = lines[i + 1] ?? "";
|
|
3528
|
-
const [, indent, value, tail] = /^([ \t]+)(\S.*?)([ \t]*)
|
|
3574
|
+
const [, indent, value, tail] = /^([ \t]+)(\S.*?)([ \t]*)(?:\r)?$/.exec(next) ?? [];
|
|
3529
3575
|
if (indent === void 0 || value === void 0) continue;
|
|
3530
3576
|
if (value.includes("<redacted>")) continue;
|
|
3531
3577
|
lines[i + 1] = `${indent}<redacted>${tail ?? ""}`;
|
|
@@ -3877,6 +3923,32 @@ function findDriftSignal(signals, id) {
|
|
|
3877
3923
|
* the default dsh skill-filesystem user root. The plugin only manages skills
|
|
3878
3924
|
* it created unless a `.hermes-managed` marker opts a skill in. Archival is a
|
|
3879
3925
|
* move to `.archive/` — never a hard delete.
|
|
3926
|
+
*
|
|
3927
|
+
* ## Concurrency discipline (OPT-09, 2026-09) — read before adding a mutator
|
|
3928
|
+
*
|
|
3929
|
+
* Three primitives, three distinct jobs (they compose, they do not replace
|
|
3930
|
+
* each other):
|
|
3931
|
+
*
|
|
3932
|
+
* 1. **In-process serial queue** (`this.serial`, makeSerialQueue) — orders the
|
|
3933
|
+
* read→plan→commit phases of one skill's mutation against OTHER mutators
|
|
3934
|
+
* in this process. Used by: create/update/patch/setPinned/restructure/
|
|
3935
|
+
* writeSupportFile/removeSupportFile and (whole-mutation) consolidate.
|
|
3936
|
+
* NON-reentrant: a callback must never call a public method that wraps
|
|
3937
|
+
* itself in `this.serial` (archive/restoreFromArchive deliberately do not).
|
|
3938
|
+
* 2. **Per-directory write lock** (io.ts LOCK_*) — cross-process mutual
|
|
3939
|
+
* exclusion plus in-process crash ownership (tickets, takeover). Checked
|
|
3940
|
+
* with `hasWriteLock` before any destructive move (archive/restore/
|
|
3941
|
+
* snapshot); held inside transactIo by byte writers.
|
|
3942
|
+
* 3. **CAS baseline (`expected:`)** — any read whose bytes feed a later write
|
|
3943
|
+
* must either live inside the serial section that commits the write, or
|
|
3944
|
+
* carry its plan-time bytes as `expected` so the commit fails closed on
|
|
3945
|
+
* drift (V8-11 / V24-01). A read outside the serial section WITHOUT a
|
|
3946
|
+
* baseline is a lost-update bug; this file's history is the test suite.
|
|
3947
|
+
*
|
|
3948
|
+
* Known residuals (deliberate, documented at their sites): the archive commit
|
|
3949
|
+
* re-check narrows but does not close the pin race (OPT-06); snapshotAll
|
|
3950
|
+
* re-probes after its copies so a mid-copy writer demotes to `skipped`
|
|
3951
|
+
* (OPT-07); the movers' probe→rename window is owned by the io.ts protocol.
|
|
3880
3952
|
*/
|
|
3881
3953
|
/** 0.3.16 (S1.13, T-6): the pointer-line prefix written into a body when a
|
|
3882
3954
|
* section is moved to references/ — single literal, both restructure and
|
|
@@ -3888,6 +3960,65 @@ const DEFAULT_SKILL_LIMITS = {
|
|
|
3888
3960
|
maxSkillContentChars: MAX_SKILL_CONTENT_CHARS,
|
|
3889
3961
|
maxSkillFileBytes: MAX_SKILL_FILE_BYTES
|
|
3890
3962
|
};
|
|
3963
|
+
/**
|
|
3964
|
+
* Evaluate a stage-time anchor against the bytes a locked read observed.
|
|
3965
|
+
* @param anchor - the caller's anchor, or `undefined` for an unanchored write.
|
|
3966
|
+
* @param current - the bytes the write lock read (`null` = the target is absent).
|
|
3967
|
+
* @returns `match` when the write may proceed, otherwise the refusal verdict.
|
|
3968
|
+
*/
|
|
3969
|
+
function anchorVerdict(anchor, current) {
|
|
3970
|
+
if (anchor === void 0) return "match";
|
|
3971
|
+
if ("absent" in anchor) return current === null ? "match" : "drift";
|
|
3972
|
+
if (current === null) return "missing";
|
|
3973
|
+
return contentHash(current) === anchor.sha256 ? "match" : "drift";
|
|
3974
|
+
}
|
|
3975
|
+
/**
|
|
3976
|
+
* Build the refusal for a skill write whose anchor did not hold. The wording is
|
|
3977
|
+
* the library's own; a caller with staged-replay wording (the skill tool, the
|
|
3978
|
+
* review plan) re-words it from {@link SkillActionResult.anchor}.
|
|
3979
|
+
* @param name - the skill name the refusal names.
|
|
3980
|
+
* @param verdict - the non-matching verdict.
|
|
3981
|
+
* @returns the refusal result (nothing was written).
|
|
3982
|
+
*/
|
|
3983
|
+
function anchorRefusal(name, verdict) {
|
|
3984
|
+
return {
|
|
3985
|
+
ok: false,
|
|
3986
|
+
stale: true,
|
|
3987
|
+
anchor: verdict,
|
|
3988
|
+
message: verdict === "missing" ? `Skill "${name}" not found.` : `Skill "${name}" changed since it was read; the write was refused to avoid overwriting newer content.`
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3991
|
+
/**
|
|
3992
|
+
* Build the refusal for a support-file write/remove whose anchor did not hold.
|
|
3993
|
+
* @param name - the owning skill name.
|
|
3994
|
+
* @param filePath - the support-file path inside the skill.
|
|
3995
|
+
* @param verdict - the non-matching verdict.
|
|
3996
|
+
* @returns the refusal result (nothing was written or removed).
|
|
3997
|
+
*/
|
|
3998
|
+
/**
|
|
3999
|
+
* Refusal for a target the locked read could not verify at all (EISDIR, an
|
|
4000
|
+
* unreadable file). A staged replay reports "could not be verified" instead of
|
|
4001
|
+
* propagating an exception: nothing was read, so nothing can have been written.
|
|
4002
|
+
* @param name - the owning skill name.
|
|
4003
|
+
* @param filePath - the support-file path, or `null` for the skill body.
|
|
4004
|
+
* @returns the refusal result.
|
|
4005
|
+
*/
|
|
4006
|
+
function anchorUnverifiable(name, filePath) {
|
|
4007
|
+
return {
|
|
4008
|
+
ok: false,
|
|
4009
|
+
stale: true,
|
|
4010
|
+
anchor: "drift",
|
|
4011
|
+
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.`
|
|
4012
|
+
};
|
|
4013
|
+
}
|
|
4014
|
+
function anchorRefusalFile(name, filePath, verdict) {
|
|
4015
|
+
return {
|
|
4016
|
+
ok: false,
|
|
4017
|
+
stale: true,
|
|
4018
|
+
anchor: verdict,
|
|
4019
|
+
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.`
|
|
4020
|
+
};
|
|
4021
|
+
}
|
|
3891
4022
|
/** Upper bound of moves per restructure proposal (validator and core agree). */
|
|
3892
4023
|
const MAX_RESTRUCTURE_MOVES = 5;
|
|
3893
4024
|
/** Restructure targets are plain markdown files under references/ — no
|
|
@@ -4647,9 +4778,11 @@ var SkillLibrary = class {
|
|
|
4647
4778
|
* mutation event are issued ONLY when a write actually lands, so a no-op
|
|
4648
4779
|
* never inflates the mutation-maturity counter.
|
|
4649
4780
|
*/
|
|
4650
|
-
async runSingleWrite(path, task) {
|
|
4781
|
+
async runSingleWrite(path, task, readFailure) {
|
|
4651
4782
|
let outcome;
|
|
4783
|
+
const progress = { entered: false };
|
|
4652
4784
|
const run = async (current) => {
|
|
4785
|
+
progress.entered = true;
|
|
4653
4786
|
const o = await task(current ?? null);
|
|
4654
4787
|
outcome = {
|
|
4655
4788
|
...o,
|
|
@@ -4662,11 +4795,18 @@ var SkillLibrary = class {
|
|
|
4662
4795
|
if (this.transact) try {
|
|
4663
4796
|
await this.transact(this.io, path, run);
|
|
4664
4797
|
} catch (error) {
|
|
4798
|
+
if (!progress.entered && readFailure !== void 0) return readFailure;
|
|
4665
4799
|
if (!committedOnly(error)) throw error;
|
|
4666
4800
|
durabilityWarning = error instanceof Error ? error.message : String(error);
|
|
4667
4801
|
}
|
|
4668
4802
|
else {
|
|
4669
|
-
|
|
4803
|
+
let current;
|
|
4804
|
+
try {
|
|
4805
|
+
current = await this.io.readText(path);
|
|
4806
|
+
} catch (error) {
|
|
4807
|
+
if (readFailure !== void 0) return readFailure;
|
|
4808
|
+
throw error;
|
|
4809
|
+
}
|
|
4670
4810
|
const next = await run(current);
|
|
4671
4811
|
if (next !== null && next !== current) try {
|
|
4672
4812
|
await this.io.writeText(path, next);
|
|
@@ -4745,7 +4885,14 @@ var SkillLibrary = class {
|
|
|
4745
4885
|
throw error;
|
|
4746
4886
|
}
|
|
4747
4887
|
}
|
|
4748
|
-
|
|
4888
|
+
/**
|
|
4889
|
+
* Summarize the skill tree.
|
|
4890
|
+
* @param options - `withContent` attaches each skill's whole SKILL.md body to
|
|
4891
|
+
* its summary (v35 C11): the read this listing already performs is the one the
|
|
4892
|
+
* body would cost again, so a content-consuming caller pays no second pass.
|
|
4893
|
+
* @returns one summary per readable skill directory.
|
|
4894
|
+
*/
|
|
4895
|
+
async list(options = {}) {
|
|
4749
4896
|
const summaries = [];
|
|
4750
4897
|
for (const name of await listNames(this.root, this.io)) {
|
|
4751
4898
|
const dir = this.dirOf(name);
|
|
@@ -4793,7 +4940,8 @@ var SkillLibrary = class {
|
|
|
4793
4940
|
hermesManaged
|
|
4794
4941
|
].some((value) => value === null),
|
|
4795
4942
|
managed: hermesManaged === true,
|
|
4796
|
-
...typeof parsedWhenToUse === "string" && parsedWhenToUse.trim() !== "" ? { whenToUse: parsedWhenToUse } : {}
|
|
4943
|
+
...typeof parsedWhenToUse === "string" && parsedWhenToUse.trim() !== "" ? { whenToUse: parsedWhenToUse } : {},
|
|
4944
|
+
...options.withContent === true ? { content: md } : {}
|
|
4797
4945
|
});
|
|
4798
4946
|
}
|
|
4799
4947
|
return summaries;
|
|
@@ -5145,11 +5293,11 @@ var SkillLibrary = class {
|
|
|
5145
5293
|
...norm.changed ? { normalizedFrontmatterFields: norm.fields } : {}
|
|
5146
5294
|
};
|
|
5147
5295
|
}
|
|
5148
|
-
async update(rawName, content, origin = "foreground") {
|
|
5296
|
+
async update(rawName, content, origin = "foreground", anchor) {
|
|
5149
5297
|
const name = rawName.trim();
|
|
5150
|
-
return await this.serial(() => this.updateCore(name, content, origin));
|
|
5298
|
+
return await this.serial(() => this.updateCore(name, content, origin, anchor));
|
|
5151
5299
|
}
|
|
5152
|
-
async updateCore(name, content, origin) {
|
|
5300
|
+
async updateCore(name, content, origin, anchor) {
|
|
5153
5301
|
const badName = this.badName(name);
|
|
5154
5302
|
if (badName) return {
|
|
5155
5303
|
ok: false,
|
|
@@ -5186,6 +5334,11 @@ var SkillLibrary = class {
|
|
|
5186
5334
|
message: threat
|
|
5187
5335
|
};
|
|
5188
5336
|
return await this.runSingleWrite(path, (current) => {
|
|
5337
|
+
const verdict = anchorVerdict(anchor, current);
|
|
5338
|
+
if (verdict !== "match") return {
|
|
5339
|
+
result: anchorRefusal(name, verdict),
|
|
5340
|
+
write: null
|
|
5341
|
+
};
|
|
5189
5342
|
if (current === null) return {
|
|
5190
5343
|
result: {
|
|
5191
5344
|
ok: false,
|
|
@@ -5224,7 +5377,7 @@ var SkillLibrary = class {
|
|
|
5224
5377
|
skillDir: dir
|
|
5225
5378
|
}
|
|
5226
5379
|
};
|
|
5227
|
-
});
|
|
5380
|
+
}, anchor !== void 0 ? anchorRefusal(name, "missing") : void 0);
|
|
5228
5381
|
}
|
|
5229
5382
|
async patch(rawName, oldString, newString, filePath = "", replaceAll = false, origin = "foreground") {
|
|
5230
5383
|
const name = rawName.trim();
|
|
@@ -5563,6 +5716,11 @@ var SkillLibrary = class {
|
|
|
5563
5716
|
ok: false,
|
|
5564
5717
|
message: `Skill "${name}" is being written (write lock present); retry archiving once the write completes.`
|
|
5565
5718
|
};
|
|
5719
|
+
const commitProtection = await this.deleteProtection(name, options);
|
|
5720
|
+
if (commitProtection) return {
|
|
5721
|
+
ok: false,
|
|
5722
|
+
message: commitProtection === "pinned" ? `Skill "${name}" is pinned and cannot be archived. Remove the \`.pinned\` marker in its directory, then retry.` : `Skill "${name}" is protected (${commitProtection}).`
|
|
5723
|
+
};
|
|
5566
5724
|
const moveFailure = await this.moveDir(dir, dest);
|
|
5567
5725
|
if (moveFailure !== void 0) return {
|
|
5568
5726
|
ok: false,
|
|
@@ -5626,74 +5784,74 @@ var SkillLibrary = class {
|
|
|
5626
5784
|
ok: false,
|
|
5627
5785
|
message: `Skill "${targetName}" is protected (${targetProtection}).`
|
|
5628
5786
|
};
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
const
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
const archived = [];
|
|
5686
|
-
try {
|
|
5687
|
-
if (!await this.io.readText(join(targetDir, "SKILL.md"))) return {
|
|
5688
|
-
ok: false,
|
|
5689
|
-
message: `Skill "${targetName}" not found.`
|
|
5690
|
-
};
|
|
5691
|
-
for (const source of normalizedSources) {
|
|
5692
|
-
const result = await this.archive(source, { absorbedInto: targetName });
|
|
5693
|
-
if (!result.ok) throw new Error(result.message);
|
|
5694
|
-
archived.push(source);
|
|
5787
|
+
return await this.serial(async () => {
|
|
5788
|
+
const referenceWrites = [];
|
|
5789
|
+
const parts = [];
|
|
5790
|
+
if (mode === "append") for (const source of normalizedSources) {
|
|
5791
|
+
const protection = await this.deleteProtection(source);
|
|
5792
|
+
if (protection) return {
|
|
5793
|
+
ok: false,
|
|
5794
|
+
message: `Skill "${source}" is protected (${protection}).`
|
|
5795
|
+
};
|
|
5796
|
+
const sourceMd = await this.io.readText(join(this.dirOf(source), "SKILL.md"));
|
|
5797
|
+
if (!sourceMd) return {
|
|
5798
|
+
ok: false,
|
|
5799
|
+
message: `Skill "${source}" not found.`
|
|
5800
|
+
};
|
|
5801
|
+
const parsed = parseFrontmatter(sourceMd);
|
|
5802
|
+
if (!parsed) return {
|
|
5803
|
+
ok: false,
|
|
5804
|
+
message: `Skill "${source}" has no valid frontmatter; refusing to merge.`
|
|
5805
|
+
};
|
|
5806
|
+
if (await this.countSupportDirs(source) > 0) return {
|
|
5807
|
+
ok: false,
|
|
5808
|
+
message: `Consolidation rejected: source "${source}" carries support files — use mode:'reference' or archive the whole package instead.`
|
|
5809
|
+
};
|
|
5810
|
+
const refs = supportRefs(parsed.body);
|
|
5811
|
+
if (refs.length > 0) return {
|
|
5812
|
+
ok: false,
|
|
5813
|
+
message: `Consolidation rejected: source "${source}" body references support files (${refs.join(", ")}) that would be left behind — use mode:'reference' or archive the whole package instead.`
|
|
5814
|
+
};
|
|
5815
|
+
parts.push(`\n<!-- consolidated from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}`);
|
|
5816
|
+
}
|
|
5817
|
+
else for (const source of normalizedSources) {
|
|
5818
|
+
const protection = await this.deleteProtection(source);
|
|
5819
|
+
if (protection) return {
|
|
5820
|
+
ok: false,
|
|
5821
|
+
message: `Skill "${source}" is protected (${protection}).`
|
|
5822
|
+
};
|
|
5823
|
+
const sourceMd = await this.io.readText(join(this.dirOf(source), "SKILL.md"));
|
|
5824
|
+
if (!sourceMd) return {
|
|
5825
|
+
ok: false,
|
|
5826
|
+
message: `Skill "${source}" not found.`
|
|
5827
|
+
};
|
|
5828
|
+
const parsed = parseFrontmatter(sourceMd);
|
|
5829
|
+
if (!parsed) return {
|
|
5830
|
+
ok: false,
|
|
5831
|
+
message: `Skill "${source}" has no valid frontmatter; refusing to demote.`
|
|
5832
|
+
};
|
|
5833
|
+
const refs = supportRefs(parsed.body);
|
|
5834
|
+
if (refs.length > 0) return {
|
|
5835
|
+
ok: false,
|
|
5836
|
+
message: `Consolidation rejected: source "${source}" body references support files (${refs.join(", ")}) that would be left behind — archive the whole package instead.`
|
|
5837
|
+
};
|
|
5838
|
+
const target = join(targetDir, "references", `${source}.md`);
|
|
5839
|
+
referenceWrites.push({
|
|
5840
|
+
target,
|
|
5841
|
+
content: `<!-- demoted from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}\n`
|
|
5842
|
+
});
|
|
5695
5843
|
}
|
|
5696
|
-
const
|
|
5844
|
+
const archived = [];
|
|
5845
|
+
try {
|
|
5846
|
+
if (!await this.io.readText(join(targetDir, "SKILL.md"))) return {
|
|
5847
|
+
ok: false,
|
|
5848
|
+
message: `Skill "${targetName}" not found.`
|
|
5849
|
+
};
|
|
5850
|
+
for (const source of normalizedSources) {
|
|
5851
|
+
const result = await this.archive(source, { absorbedInto: targetName });
|
|
5852
|
+
if (!result.ok) throw new Error(result.message);
|
|
5853
|
+
archived.push(source);
|
|
5854
|
+
}
|
|
5697
5855
|
const freshTargetMd = await this.io.readText(join(targetDir, "SKILL.md"));
|
|
5698
5856
|
if (!freshTargetMd) return {
|
|
5699
5857
|
ok: false,
|
|
@@ -5735,7 +5893,7 @@ var SkillLibrary = class {
|
|
|
5735
5893
|
expected: freshTargetMd
|
|
5736
5894
|
});
|
|
5737
5895
|
}
|
|
5738
|
-
|
|
5896
|
+
const result = await this.applyTreeChange({
|
|
5739
5897
|
name: targetName,
|
|
5740
5898
|
origin,
|
|
5741
5899
|
protection: "write",
|
|
@@ -5744,30 +5902,30 @@ var SkillLibrary = class {
|
|
|
5744
5902
|
auditSummary: `consolidated ${normalizedSources.join(", ")} (${mode}) into ${targetName}`,
|
|
5745
5903
|
eventAction: "consolidate"
|
|
5746
5904
|
});
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
}
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5905
|
+
if (!result.ok) throw new Error(result.message);
|
|
5906
|
+
return {
|
|
5907
|
+
ok: true,
|
|
5908
|
+
message: `Consolidated ${normalizedSources.join(", ")} into "${targetName}".`,
|
|
5909
|
+
path: targetDir
|
|
5910
|
+
};
|
|
5911
|
+
} catch (error) {
|
|
5912
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
5913
|
+
const failedRestores = [];
|
|
5914
|
+
for (const source of archived.reverse()) try {
|
|
5915
|
+
if (!(await this.restoreFromArchive(source)).ok) failedRestores.push(source);
|
|
5916
|
+
} catch {
|
|
5917
|
+
failedRestores.push(source);
|
|
5918
|
+
}
|
|
5919
|
+
if (failedRestores.length > 0) return {
|
|
5920
|
+
ok: false,
|
|
5921
|
+
message: `Consolidation failed (${reason}); rolled back EXCEPT ${failedRestores.join(", ")} — still in .archive, restore them with /evolution skill restore.`
|
|
5922
|
+
};
|
|
5923
|
+
return {
|
|
5924
|
+
ok: false,
|
|
5925
|
+
message: `Consolidation failed and was rolled back: ${reason}`
|
|
5926
|
+
};
|
|
5761
5927
|
}
|
|
5762
|
-
|
|
5763
|
-
ok: false,
|
|
5764
|
-
message: `Consolidation failed (${reason}); rolled back EXCEPT ${failedRestores.join(", ")} — still in .archive, restore them with /evolution skill restore.`
|
|
5765
|
-
};
|
|
5766
|
-
return {
|
|
5767
|
-
ok: false,
|
|
5768
|
-
message: `Consolidation failed and was rolled back: ${reason}`
|
|
5769
|
-
};
|
|
5770
|
-
}
|
|
5928
|
+
});
|
|
5771
5929
|
}
|
|
5772
5930
|
/**
|
|
5773
5931
|
* Content-distribution repair (008 batch B, 009-R kernel): move body
|
|
@@ -6012,6 +6170,14 @@ var SkillLibrary = class {
|
|
|
6012
6170
|
ok: false,
|
|
6013
6171
|
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.`
|
|
6014
6172
|
};
|
|
6173
|
+
let caseVariant;
|
|
6174
|
+
try {
|
|
6175
|
+
caseVariant = (await this.io.list(this.root)).find((entry) => entry !== name && entry.toLowerCase() === name.toLowerCase());
|
|
6176
|
+
} catch {}
|
|
6177
|
+
if (caseVariant !== void 0) return {
|
|
6178
|
+
ok: false,
|
|
6179
|
+
message: `Skill "${caseVariant}" (same name, different letter case) already exists in the active root; restoring "${name}" beside it would create ambiguous duplicates — restore as "${caseVariant}" or remove the variant first.`
|
|
6180
|
+
};
|
|
6015
6181
|
const archiveRoot = join(this.root, ".archive");
|
|
6016
6182
|
let entries;
|
|
6017
6183
|
try {
|
|
@@ -6081,11 +6247,11 @@ var SkillLibrary = class {
|
|
|
6081
6247
|
path: dest
|
|
6082
6248
|
};
|
|
6083
6249
|
}
|
|
6084
|
-
async writeSupportFile(rawName, filePath, content, origin = "foreground") {
|
|
6250
|
+
async writeSupportFile(rawName, filePath, content, origin = "foreground", anchor) {
|
|
6085
6251
|
const name = rawName.trim();
|
|
6086
|
-
return await this.serial(() => this.writeSupportFileCore(name, filePath, content, origin));
|
|
6252
|
+
return await this.serial(() => this.writeSupportFileCore(name, filePath, content, origin, anchor));
|
|
6087
6253
|
}
|
|
6088
|
-
async writeSupportFileCore(name, filePath, content, origin) {
|
|
6254
|
+
async writeSupportFileCore(name, filePath, content, origin, anchor) {
|
|
6089
6255
|
const badName = this.badName(name);
|
|
6090
6256
|
if (badName) return {
|
|
6091
6257
|
ok: false,
|
|
@@ -6117,6 +6283,11 @@ var SkillLibrary = class {
|
|
|
6117
6283
|
};
|
|
6118
6284
|
const target = join(dir, ...filePath.replace(/\\/g, "/").split("/").filter(Boolean));
|
|
6119
6285
|
return await this.runSingleWrite(target, (current) => {
|
|
6286
|
+
const verdict = anchorVerdict(anchor, current);
|
|
6287
|
+
if (verdict !== "match") return {
|
|
6288
|
+
result: anchorRefusalFile(name, filePath, verdict),
|
|
6289
|
+
write: null
|
|
6290
|
+
};
|
|
6120
6291
|
if (current !== null && content.trimEnd() === current.trimEnd()) return {
|
|
6121
6292
|
result: {
|
|
6122
6293
|
ok: true,
|
|
@@ -6147,13 +6318,13 @@ var SkillLibrary = class {
|
|
|
6147
6318
|
file: target
|
|
6148
6319
|
}
|
|
6149
6320
|
};
|
|
6150
|
-
});
|
|
6321
|
+
}, anchor !== void 0 ? anchorUnverifiable(name, filePath) : void 0);
|
|
6151
6322
|
}
|
|
6152
|
-
async removeSupportFile(rawName, filePath, origin = "foreground") {
|
|
6323
|
+
async removeSupportFile(rawName, filePath, origin = "foreground", anchor) {
|
|
6153
6324
|
const name = rawName.trim();
|
|
6154
|
-
return await this.serial(() => this.removeSupportFileCore(name, filePath, origin));
|
|
6325
|
+
return await this.serial(() => this.removeSupportFileCore(name, filePath, origin, anchor));
|
|
6155
6326
|
}
|
|
6156
|
-
async removeSupportFileCore(name, filePath, origin) {
|
|
6327
|
+
async removeSupportFileCore(name, filePath, origin, anchor) {
|
|
6157
6328
|
const badName = this.badName(name);
|
|
6158
6329
|
if (badName) return {
|
|
6159
6330
|
ok: false,
|
|
@@ -6175,17 +6346,25 @@ var SkillLibrary = class {
|
|
|
6175
6346
|
message: validation
|
|
6176
6347
|
};
|
|
6177
6348
|
const target = join(dir, ...filePath.replace(/\\/g, "/").split("/").filter(Boolean));
|
|
6178
|
-
if (!await this.io.exists(target))
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6349
|
+
if (!await this.io.exists(target)) {
|
|
6350
|
+
if (anchor !== void 0) return anchorRefusalFile(name, filePath, "missing");
|
|
6351
|
+
return {
|
|
6352
|
+
ok: false,
|
|
6353
|
+
message: `File "${filePath}" not found in skill "${name}".`
|
|
6354
|
+
};
|
|
6355
|
+
}
|
|
6182
6356
|
const before = await this.io.readText(target).catch(() => null);
|
|
6183
6357
|
if (before === null) return {
|
|
6184
6358
|
ok: false,
|
|
6185
6359
|
message: `"${filePath}" is not a readable regular file — remove the files inside it one by one.`
|
|
6186
6360
|
};
|
|
6187
|
-
|
|
6188
|
-
|
|
6361
|
+
let verdict = anchorVerdict(anchor, before);
|
|
6362
|
+
if (verdict === "match" && this.transact) await this.transact(this.io, target, (current) => {
|
|
6363
|
+
verdict = anchorVerdict(anchor, current);
|
|
6364
|
+
return verdict === "match" ? null : current;
|
|
6365
|
+
});
|
|
6366
|
+
else if (verdict === "match") await this.io.remove(target);
|
|
6367
|
+
if (verdict !== "match") return anchorRefusalFile(name, filePath, verdict);
|
|
6189
6368
|
await this.audit(name, "remove_file", before, null, `removed ${filePath}`);
|
|
6190
6369
|
this.notifyMutation({
|
|
6191
6370
|
action: "remove_file",
|
|
@@ -6251,6 +6430,13 @@ var SkillLibrary = class {
|
|
|
6251
6430
|
await this.io.copy(this.dirOf(name), join(dest, name));
|
|
6252
6431
|
}))).find((result) => result.status === "rejected");
|
|
6253
6432
|
if (copyFailure) throw copyFailure.reason;
|
|
6433
|
+
const suspect = [];
|
|
6434
|
+
for (const name of copyable) if (await this.hasWriteLock(this.dirOf(name))) suspect.push(name);
|
|
6435
|
+
if (suspect.length > 0) {
|
|
6436
|
+
for (const name of suspect) console.warn(`skill-store: snapshot demoted "${name}" to skipped — a write lock appeared while its copy ran; the copied bytes are suspect`);
|
|
6437
|
+
for (const name of suspect) copyable.splice(copyable.indexOf(name), 1);
|
|
6438
|
+
skipped.push(...suspect);
|
|
6439
|
+
}
|
|
6254
6440
|
const sidecars = [];
|
|
6255
6441
|
for (const sidecar of [usageFile(this.root), suppressedFile(this.root)]) if (await this.io.exists(sidecar)) {
|
|
6256
6442
|
const name = basename(sidecar);
|
|
@@ -6520,4 +6706,4 @@ var SkillLibrary = class {
|
|
|
6520
6706
|
}
|
|
6521
6707
|
};
|
|
6522
6708
|
//#endregion
|
|
6523
|
-
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, 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, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, 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, PATTERN_OVERLAP, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, THREAT_EXEMPTION_HINT, advanceReview, allowRowCollisions, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, clampedNumber, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, decideTakeover, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionEventPayloadIssue, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterCatalogInvalid, getRecord, isCommittedWarning, isProcessAlive, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, makeSerialQueue, markAgentCreated, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseLockBody, 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 };
|
|
6709
|
+
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, 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, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, 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, PATTERN_OVERLAP, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, 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, advanceReview, allowRowCollisions, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, clampedNumber, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, decideTakeover, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionEventPayloadIssue, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterCatalogInvalid, getRecord, isCommittedWarning, isProcessAlive, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, makeSerialQueue, markAgentCreated, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseLockBody, 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 };
|
package/lib/types/constants.d.ts
CHANGED
|
@@ -19,6 +19,22 @@
|
|
|
19
19
|
* threshold, which are intentionally left where they are used.
|
|
20
20
|
* @module @lmzhen/dsh-evolution-core
|
|
21
21
|
*/
|
|
22
|
+
/**
|
|
23
|
+
* Required argument names per `skill_manage` action — the SINGLE SOURCE read
|
|
24
|
+
* by the tool's argument gate (tool-skill-manage executeCore) and the plan
|
|
25
|
+
* validator (evolution-plan-validator), so the two can never drift.
|
|
26
|
+
* OPT-05 (2026-09): the plan validator used to accept a `write_file`/
|
|
27
|
+
* `remove_file` op without `file_path` while the executor required it — the
|
|
28
|
+
* staged write then failed at EVERY approve until rejected.
|
|
29
|
+
* Rows here are the op-level requirements only: `delete` additionally
|
|
30
|
+
* requires `absorbed_into` at the PLAN layer (review passes may only delete
|
|
31
|
+
* into an umbrella) and `pin`/`unpin` are tool-only actions — each consumer
|
|
32
|
+
* adds its own extras on top of this table. An empty-string argument is NOT
|
|
33
|
+
* caught here (the tool's gate deliberately lets it reach the library for a
|
|
34
|
+
* more specific remedy message); the validator adds its own `.trim()`
|
|
35
|
+
* emptiness checks for payload fields.
|
|
36
|
+
*/
|
|
37
|
+
export declare const SKILL_ACTION_REQUIRED_FIELDS: Readonly<Record<string, readonly string[]>>;
|
|
22
38
|
/** Skill frontmatter `name` validated for the file name (lowercase + hyphen).
|
|
23
39
|
* 计划 B-4 (v18): tightened to the UPSTREAM `SKILL_NAME` shape
|
|
24
40
|
* (`/^[a-z0-9]+(?:-[a-z0-9]+)*$/`, packages/skill/skill/src/index.ts:20). The
|
|
@@ -140,5 +140,5 @@ export declare function readEvolutionEvents(io: EvolutionIoLike, path: string):
|
|
|
140
140
|
* malformed (or unreadable) ARCHIVE is skipped — it never bricks the boot and
|
|
141
141
|
* it is still flagged.
|
|
142
142
|
*/
|
|
143
|
-
export declare function readEvolutionTimeline(io: EvolutionIoLike, path: string): Promise<EventLogRead>;
|
|
143
|
+
export declare function readEvolutionTimeline(io: EvolutionIoLike, path: string, archives?: readonly string[]): Promise<EventLogRead>;
|
|
144
144
|
//# sourceMappingURL=evolution-events.d.ts.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -7,6 +7,23 @@
|
|
|
7
7
|
* the host auto-assembles to register this package's no-op invariant);
|
|
8
8
|
* consumers import named exports from the package root so published npm
|
|
9
9
|
* bundles never depend on source subpaths.
|
|
10
|
+
*
|
|
11
|
+
* ## Layer map (OPT-28, 2026-09) — locate code by LAYER, not by directory
|
|
12
|
+
*
|
|
13
|
+
* This one physical package carries THREE architecture layers of the family;
|
|
14
|
+
* when adding or looking for something, go by the export's layer:
|
|
15
|
+
*
|
|
16
|
+
* - **Cross-cutting basics** — `state-store.ts` (env roots — the single
|
|
17
|
+
* source of DSH-home semantics), `serial.ts`, `numeric.ts`, `constants.ts`,
|
|
18
|
+
* `mutations.ts`, `events.ts`, `gates.ts`.
|
|
19
|
+
* - **Security primitives** — `threats.ts` (content threat scanner),
|
|
20
|
+
* `redact.ts` (credential masking at model boundaries). Consumers:
|
|
21
|
+
* evolution-policy/threat, both stores, review, maintenance.
|
|
22
|
+
* - **Core domain stores/logic** — `skill-store.ts` (skill tree engine +
|
|
23
|
+
* IO-seam consumer), `memory-store.ts`, `usage.ts`, `curator.ts`,
|
|
24
|
+
* `quality.ts`, `signals.ts`, `drift-signals.ts`, `skill-health.ts`,
|
|
25
|
+
* `preset-composition.ts`, `prompts.ts`, `learn-prompt.ts`,
|
|
26
|
+
* `evolution-events.ts`, `io.ts` (the ctx.evolutionIo seam itself).
|
|
10
27
|
* @module @lmzhen/dsh-evolution-core
|
|
11
28
|
*/
|
|
12
29
|
export * from './curator.ts';
|
|
@@ -5,6 +5,32 @@
|
|
|
5
5
|
* the default dsh skill-filesystem user root. The plugin only manages skills
|
|
6
6
|
* it created unless a `.hermes-managed` marker opts a skill in. Archival is a
|
|
7
7
|
* move to `.archive/` — never a hard delete.
|
|
8
|
+
*
|
|
9
|
+
* ## Concurrency discipline (OPT-09, 2026-09) — read before adding a mutator
|
|
10
|
+
*
|
|
11
|
+
* Three primitives, three distinct jobs (they compose, they do not replace
|
|
12
|
+
* each other):
|
|
13
|
+
*
|
|
14
|
+
* 1. **In-process serial queue** (`this.serial`, makeSerialQueue) — orders the
|
|
15
|
+
* read→plan→commit phases of one skill's mutation against OTHER mutators
|
|
16
|
+
* in this process. Used by: create/update/patch/setPinned/restructure/
|
|
17
|
+
* writeSupportFile/removeSupportFile and (whole-mutation) consolidate.
|
|
18
|
+
* NON-reentrant: a callback must never call a public method that wraps
|
|
19
|
+
* itself in `this.serial` (archive/restoreFromArchive deliberately do not).
|
|
20
|
+
* 2. **Per-directory write lock** (io.ts LOCK_*) — cross-process mutual
|
|
21
|
+
* exclusion plus in-process crash ownership (tickets, takeover). Checked
|
|
22
|
+
* with `hasWriteLock` before any destructive move (archive/restore/
|
|
23
|
+
* snapshot); held inside transactIo by byte writers.
|
|
24
|
+
* 3. **CAS baseline (`expected:`)** — any read whose bytes feed a later write
|
|
25
|
+
* must either live inside the serial section that commits the write, or
|
|
26
|
+
* carry its plan-time bytes as `expected` so the commit fails closed on
|
|
27
|
+
* drift (V8-11 / V24-01). A read outside the serial section WITHOUT a
|
|
28
|
+
* baseline is a lost-update bug; this file's history is the test suite.
|
|
29
|
+
*
|
|
30
|
+
* Known residuals (deliberate, documented at their sites): the archive commit
|
|
31
|
+
* re-check narrows but does not close the pin race (OPT-06); snapshotAll
|
|
32
|
+
* re-probes after its copies so a mid-copy writer demotes to `skipped`
|
|
33
|
+
* (OPT-07); the movers' probe→rename window is owned by the io.ts protocol.
|
|
8
34
|
*/
|
|
9
35
|
import { transactIo, type EvolutionIoLike } from './io.ts';
|
|
10
36
|
import { type MutationRecord } from './mutations.ts';
|
|
@@ -31,7 +57,32 @@ export interface SkillSummary {
|
|
|
31
57
|
* platform catalog keeps it while this provider shadows the upstream
|
|
32
58
|
* filesystem provider. Absent when the frontmatter has none. */
|
|
33
59
|
whenToUse?: string;
|
|
60
|
+
/** v35 C11: the whole SKILL.md body, present only for
|
|
61
|
+
* {@link SkillLibrary.list} calls that pass `{ withContent: true }`. A consumer
|
|
62
|
+
* that needs both the summary fields and the body (tree hashing, enrichment,
|
|
63
|
+
* drift scans) saves the second per-skill read; the default stays body-free so
|
|
64
|
+
* the common listing does not hold a whole tree in memory. */
|
|
65
|
+
content?: string;
|
|
34
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Stage-time anchor for a full-content write (v35 C9): the sha256 the caller read
|
|
69
|
+
* when it staged the plan, or `absent` when the target did not exist then. The
|
|
70
|
+
* library compares it against the bytes it reads INSIDE the write's own lock — the
|
|
71
|
+
* same read the write commits — so a concurrent writer landing between staging and
|
|
72
|
+
* commit is refused instead of silently overwritten.
|
|
73
|
+
*/
|
|
74
|
+
export type WriteAnchor = {
|
|
75
|
+
readonly sha256: string;
|
|
76
|
+
} | {
|
|
77
|
+
readonly absent: true;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* What the write lock observed about an anchor target.
|
|
81
|
+
* - `match`: the anchor holds; the write proceeds.
|
|
82
|
+
* - `drift`: the target exists with different bytes.
|
|
83
|
+
* - `missing`: the target does not exist (an `absent` anchor `match`es this).
|
|
84
|
+
*/
|
|
85
|
+
export type AnchorVerdict = 'match' | 'drift' | 'missing';
|
|
35
86
|
export interface SkillActionResult {
|
|
36
87
|
ok: boolean;
|
|
37
88
|
message: string;
|
|
@@ -42,6 +93,12 @@ export interface SkillActionResult {
|
|
|
42
93
|
/** 0.3.18 (E-68): patch produced byte-identical content (old===new) — no
|
|
43
94
|
* write, no audit, no mutation event; callers must not count a patch. */
|
|
44
95
|
noop?: boolean;
|
|
96
|
+
/** Set when the caller passed a {@link WriteAnchor} that the locked read did
|
|
97
|
+
* not satisfy: nothing was written. Carries {@link SkillActionResult.anchor}
|
|
98
|
+
* so a caller with staged-replay wording can translate it (v35 C9). */
|
|
99
|
+
stale?: true;
|
|
100
|
+
/** The locked read's verdict for the caller's anchor. */
|
|
101
|
+
anchor?: AnchorVerdict;
|
|
45
102
|
}
|
|
46
103
|
/**
|
|
47
104
|
* One section move of a restructure proposal (008 batch B): a body section
|
|
@@ -369,7 +426,16 @@ export declare class SkillLibrary {
|
|
|
369
426
|
* last-writer-wins.
|
|
370
427
|
*/
|
|
371
428
|
readSupportFile(name: string, filePath: string): Promise<string | null>;
|
|
372
|
-
|
|
429
|
+
/**
|
|
430
|
+
* Summarize the skill tree.
|
|
431
|
+
* @param options - `withContent` attaches each skill's whole SKILL.md body to
|
|
432
|
+
* its summary (v35 C11): the read this listing already performs is the one the
|
|
433
|
+
* body would cost again, so a content-consuming caller pays no second pass.
|
|
434
|
+
* @returns one summary per readable skill directory.
|
|
435
|
+
*/
|
|
436
|
+
list(options?: {
|
|
437
|
+
withContent?: boolean;
|
|
438
|
+
}): Promise<SkillSummary[]>;
|
|
373
439
|
read(rawName: string): Promise<string | null>;
|
|
374
440
|
/**
|
|
375
441
|
|
|
@@ -439,7 +505,7 @@ export declare class SkillLibrary {
|
|
|
439
505
|
private setPinnedCore;
|
|
440
506
|
create(name: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
441
507
|
private createCore;
|
|
442
|
-
update(rawName: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
508
|
+
update(rawName: string, content: string, origin?: WriteOrigin, anchor?: WriteAnchor): Promise<SkillActionResult>;
|
|
443
509
|
private updateCore;
|
|
444
510
|
patch(rawName: string, oldString: string, newString: string, filePath?: string, replaceAll?: boolean, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
445
511
|
private patchCore;
|
|
@@ -546,9 +612,9 @@ export declare class SkillLibrary {
|
|
|
546
612
|
* path back. The `.archive-reason` marker is dropped on restore.
|
|
547
613
|
*/
|
|
548
614
|
restoreFromArchive(rawName: string): Promise<SkillActionResult>;
|
|
549
|
-
writeSupportFile(rawName: string, filePath: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
615
|
+
writeSupportFile(rawName: string, filePath: string, content: string, origin?: WriteOrigin, anchor?: WriteAnchor): Promise<SkillActionResult>;
|
|
550
616
|
private writeSupportFileCore;
|
|
551
|
-
removeSupportFile(rawName: string, filePath: string, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
617
|
+
removeSupportFile(rawName: string, filePath: string, origin?: WriteOrigin, anchor?: WriteAnchor): Promise<SkillActionResult>;
|
|
552
618
|
private removeSupportFileCore;
|
|
553
619
|
/**
|
|
554
620
|
* v23 (ML-1): `.archive` retention. Archived skills are recoverable history,
|
|
@@ -20,9 +20,16 @@
|
|
|
20
20
|
* C-11: the adoption test and the RETURNED value now come from the
|
|
21
21
|
* SAME trimmed source — the old form tested `trim()` but returned the raw
|
|
22
22
|
* value, so `DSH_HOME=" /x "` was accepted AND persisted with literal spaces.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* OPT-27 (2026-09, plan D5 — accepted): the v10-era "no `~` expansion, no
|
|
24
|
+
* resolve" divergence from upstream `resolveDshHome` is RETIRED. It became
|
|
25
|
+
* load-bearing when the skill-catalog shadow made "same tree as the upstream
|
|
26
|
+
* `USER_DSH_RANK` provider" a hard contract: upstream watches the EXPANDED
|
|
27
|
+
* absolute `<home>/skills` while this value fed a literal `~/x` (a directory
|
|
28
|
+
* named `~` under the host CWD) or a CWD-relative path — split-brain skill
|
|
29
|
+
* trees, preset installs the platform never reads, doctor probes of a
|
|
30
|
+
* directory nothing serves. Behavior now matches upstream:
|
|
31
|
+
* `resolve(expandHomePath(selected))`. Only `~`-prefixed and RELATIVE
|
|
32
|
+
* DSH_HOME values change landing spot; absolute homes are byte-identical.
|
|
26
33
|
*/
|
|
27
34
|
export declare function evolutionRoot(env?: NodeJS.ProcessEnv): string;
|
|
28
35
|
/** Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
|
package/package.json
CHANGED