@lmzhen/dsh-evolution-core 0.3.45 → 0.3.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -3
- package/lib/index.js +129 -116
- package/lib/types/curator.d.ts +2 -2
- package/lib/types/io.d.ts +1 -1
- package/lib/types/skill-store.d.ts +6 -1
- package/lib/types/state-store.d.ts +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,15 +22,24 @@ Independent of request-prefix construction. This package does not alter the asse
|
|
|
22
22
|
|
|
23
23
|
Skill-library mutations are read-modify-write on one file, so `SkillLibrary`
|
|
24
24
|
serializes them in-process with a `makeSerialQueue` chain: `update`, `patch`,
|
|
25
|
-
`restructure
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
`restructure`, `writeSupportFile` and — since 0.3.46 — `consolidate`'s target
|
|
26
|
+
read→merge→commit run their whole read→validate→write under one serial task,
|
|
27
|
+
so two concurrent mutators on one skill never interleave in this process.
|
|
28
|
+
Single-file writes (`update`, `patch`, `writeSupportFile`)
|
|
28
29
|
additionally run the read and the write inside `transactIo` when a caller
|
|
29
30
|
injects a `transact` into the constructor — that is the cross-process lock, so
|
|
30
31
|
two processes sharing `DSH_HOME` cannot interleave their RMW on one file.
|
|
31
32
|
`create` writes a new file and `archive`/`consolidate` already own a two-phase
|
|
32
33
|
commit, so they deliberately stay outside the serial chain.
|
|
33
34
|
|
|
35
|
+
**0.3.46 residual (documented, per G2.5 precedent):** the low-frequency
|
|
36
|
+
single-file entry points `create`, `archive`, `removeSupportFile` and
|
|
37
|
+
`setPinned` still perform an unlocked read→write (their per-file read is not
|
|
38
|
+
inside the serial/transact task). The race needs a same-process concurrent
|
|
39
|
+
mutator on the SAME skill file, which the serialized entry points above make
|
|
40
|
+
unlikely; the exposure is acknowledged and not locked (收益不抵锁面扩大 —
|
|
41
|
+
adding locks to four low-frequency entry points is not worth the surface).
|
|
42
|
+
|
|
34
43
|
When the backend provides `transact` (nodeEvolutionIo and the io adapter do),
|
|
35
44
|
the constructor binds it BY DEFAULT since 0.3.27 — the single-file entry points
|
|
36
45
|
(`update`, `patch`, `writeSupportFile`, and each per-file piece of
|
package/lib/index.js
CHANGED
|
@@ -63,7 +63,7 @@ function evolutionIoAdapter(provider) {
|
|
|
63
63
|
* sole gate for the self-pid recycle branch. Exported (read-only in practice)
|
|
64
64
|
* so `io.spec.ts` can drive the self-heal path deterministically.
|
|
65
65
|
*/
|
|
66
|
-
const pendingSelfCleanup = /* @__PURE__ */ new
|
|
66
|
+
const pendingSelfCleanup = /* @__PURE__ */ new Map();
|
|
67
67
|
/**
|
|
68
68
|
* Retry a rename that a peer is temporarily holding on Windows (EPERM/EBUSY):
|
|
69
69
|
* a short 50ms backoff, at most 3 retries (~150ms budget), matching the
|
|
@@ -170,12 +170,11 @@ function nodeEvolutionIo() {
|
|
|
170
170
|
const holder = Number(holderContent.split(":")[0] ?? "");
|
|
171
171
|
const holderAlive = Number.isInteger(holder) && holder > 0 && isAlive(holder);
|
|
172
172
|
if (holder === process.pid && pendingSelfCleanup.has(lock)) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
} catch {}
|
|
173
|
+
const token = pendingSelfCleanup.get(lock);
|
|
174
|
+
if (await readFile(lock, "utf8").catch(() => "") === token) try {
|
|
175
|
+
await rm(lock, { force: true });
|
|
177
176
|
pendingSelfCleanup.delete(lock);
|
|
178
|
-
}
|
|
177
|
+
} catch {}
|
|
179
178
|
continue;
|
|
180
179
|
}
|
|
181
180
|
const staleDead = Number.isInteger(holder) && holder > 0 && Date.now() - st.mtimeMs > 1e3 && !holderAlive;
|
|
@@ -208,8 +207,9 @@ function nodeEvolutionIo() {
|
|
|
208
207
|
try {
|
|
209
208
|
return await task();
|
|
210
209
|
} finally {
|
|
211
|
-
if (await readFile(lock, "utf8").catch(() => null) === myClaim) await rm(lock, { force: true }).catch(() => {
|
|
212
|
-
|
|
210
|
+
if (await readFile(lock, "utf8").catch(() => null) === myClaim) await rm(lock, { force: true }).catch(async () => {
|
|
211
|
+
const body = await readFile(lock, "utf8").catch(() => "");
|
|
212
|
+
pendingSelfCleanup.set(lock, body);
|
|
213
213
|
});
|
|
214
214
|
}
|
|
215
215
|
}
|
|
@@ -858,8 +858,9 @@ function parseCuratorNominations(text) {
|
|
|
858
858
|
* view so the two can never disagree: records failing ANY of these gates are
|
|
859
859
|
* outside the managed scope.
|
|
860
860
|
*/
|
|
861
|
-
function lifecycleCandidate(name, record, config, bundled, gates = createGateSet(config)) {
|
|
861
|
+
function lifecycleCandidate(name, record, config, bundled, gates = createGateSet(config), protectedNames) {
|
|
862
862
|
if (record.pinned) return false;
|
|
863
|
+
if (protectedNames?.has(name) === true) return false;
|
|
863
864
|
if (gates.isBlocked(name)) return false;
|
|
864
865
|
if (!(record.created_by === "agent" || config.manageUnmanaged === true) && !(config.pruneBuiltins === true && bundled)) return false;
|
|
865
866
|
if (record.state === "archived") return false;
|
|
@@ -887,7 +888,7 @@ function computeScopeView(usage, config, protectedNames, gates) {
|
|
|
887
888
|
const suppressed = gateSet.suppressed.has(name);
|
|
888
889
|
const isBuiltin = PROTECTED_BUILTIN_SKILLS.has(name);
|
|
889
890
|
if (record.pinned || bundled || suppressed || protectedNames?.has(name) === true || isBuiltin) protectedSet.add(name);
|
|
890
|
-
if (lifecycleCandidate(name, record, config, bundled, gateSet)) {
|
|
891
|
+
if (lifecycleCandidate(name, record, config, bundled, gateSet, protectedNames)) {
|
|
891
892
|
managed.push(name);
|
|
892
893
|
if (record.state === "stale" || record.quality_warn === true) watched.push(name);
|
|
893
894
|
if (record.quality_warn === true) qualityWarned.push(name);
|
|
@@ -904,7 +905,7 @@ function computeScopeView(usage, config, protectedNames, gates) {
|
|
|
904
905
|
function daysSince(iso, created, now) {
|
|
905
906
|
return (now - new Date(iso ?? created).getTime()) / 864e5;
|
|
906
907
|
}
|
|
907
|
-
function computeLifecycleTransitions(usage, config, now = /* @__PURE__ */ new Date(), gates) {
|
|
908
|
+
function computeLifecycleTransitions(usage, config, now = /* @__PURE__ */ new Date(), gates, protectedNames) {
|
|
908
909
|
const result = {
|
|
909
910
|
transitions: [],
|
|
910
911
|
archive: [],
|
|
@@ -913,7 +914,7 @@ function computeLifecycleTransitions(usage, config, now = /* @__PURE__ */ new Da
|
|
|
913
914
|
};
|
|
914
915
|
const gateSet = gates ?? createGateSet(config);
|
|
915
916
|
for (const [name, record] of usage) {
|
|
916
|
-
if (!lifecycleCandidate(name, record, config, config.bundledNames?.has(name) === true, gateSet)) continue;
|
|
917
|
+
if (!lifecycleCandidate(name, record, config, config.bundledNames?.has(name) === true, gateSet, protectedNames)) continue;
|
|
917
918
|
const age = daysSince(null, record.created_at, now.getTime());
|
|
918
919
|
if (record.use_count === 0 && age < config.staleAfterDays) continue;
|
|
919
920
|
const idle = daysSince(latestActivityAt(record), record.created_at, now.getTime());
|
|
@@ -1770,19 +1771,19 @@ const PATTERNS = [
|
|
|
1770
1771
|
label: "exfil_curl",
|
|
1771
1772
|
category: "exfiltration",
|
|
1772
1773
|
scope: "all",
|
|
1773
|
-
regex:
|
|
1774
|
+
regex: /\bcurl\s+[^\n]{0,512}\$\{?\w*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i
|
|
1774
1775
|
},
|
|
1775
1776
|
{
|
|
1776
1777
|
label: "exfil_wget",
|
|
1777
1778
|
category: "exfiltration",
|
|
1778
1779
|
scope: "all",
|
|
1779
|
-
regex:
|
|
1780
|
+
regex: /\bwget\s+[^\n]{0,512}\$\{?\w*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i
|
|
1780
1781
|
},
|
|
1781
1782
|
{
|
|
1782
1783
|
label: "read_secrets",
|
|
1783
1784
|
category: "exfiltration",
|
|
1784
1785
|
scope: "all",
|
|
1785
|
-
regex:
|
|
1786
|
+
regex: /\bcat\s+[^\n]{0,512}(?:\.env|credentials|\.netrc|\.pgpass|\.npmrc|\.pypirc)/i
|
|
1786
1787
|
},
|
|
1787
1788
|
{
|
|
1788
1789
|
label: "ssh_backdoor",
|
|
@@ -2158,7 +2159,7 @@ var MemoryStore = class {
|
|
|
2158
2159
|
},
|
|
2159
2160
|
write: null
|
|
2160
2161
|
};
|
|
2161
|
-
if (hasEntryDelimiter(content)) return {
|
|
2162
|
+
if (hasEntryDelimiter(this.addDatePrefix ? `## ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}\n${content}` : content)) return {
|
|
2162
2163
|
result: {
|
|
2163
2164
|
ok: false,
|
|
2164
2165
|
message: "Fact contains the entry delimiter (§) and would split into multiple entries; rewrite it as separate facts.",
|
|
@@ -2273,7 +2274,8 @@ var MemoryStore = class {
|
|
|
2273
2274
|
},
|
|
2274
2275
|
write: null
|
|
2275
2276
|
};
|
|
2276
|
-
|
|
2277
|
+
const entryBody = this.addDatePrefix ? `## ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}\n${body}` : body;
|
|
2278
|
+
if (hasEntryDelimiter(entryBody)) return {
|
|
2277
2279
|
result: {
|
|
2278
2280
|
ok: false,
|
|
2279
2281
|
message: `Operation ${position} (add): Fact contains the entry delimiter (§) and would split into multiple entries; rewrite it as separate facts.${previewEntries(entries)}`,
|
|
@@ -2283,7 +2285,7 @@ var MemoryStore = class {
|
|
|
2283
2285
|
},
|
|
2284
2286
|
write: null
|
|
2285
2287
|
};
|
|
2286
|
-
if (!working.some((entry) => stripDatePrefix(entry) === body)) working.push(
|
|
2288
|
+
if (!working.some((entry) => stripDatePrefix(entry) === body)) working.push(entryBody);
|
|
2287
2289
|
continue;
|
|
2288
2290
|
}
|
|
2289
2291
|
const rawAction = op.action;
|
|
@@ -2695,7 +2697,7 @@ function computePrefixClusters(names) {
|
|
|
2695
2697
|
* Migrated from `evolution-review` so both channels share one implementation.
|
|
2696
2698
|
*/
|
|
2697
2699
|
const SECRET_PATTERNS = [
|
|
2698
|
-
["openai-style key",
|
|
2700
|
+
["openai-style key", /\bsk-[A-Za-z0-9_-]{16,}/g],
|
|
2699
2701
|
["aws access key", /AKIA[0-9A-Z]{16}/g],
|
|
2700
2702
|
["github token", /gh[pousr]_[A-Za-z0-9]{20,}/g],
|
|
2701
2703
|
["gitlab token", /glpat-[A-Za-z0-9_-]{16,}/g],
|
|
@@ -3042,8 +3044,13 @@ const DEFAULT_SKILL_LIMITS = {
|
|
|
3042
3044
|
};
|
|
3043
3045
|
/** Upper bound of moves per restructure proposal (validator and core agree). */
|
|
3044
3046
|
const MAX_RESTRUCTURE_MOVES = 5;
|
|
3045
|
-
/** Restructure targets are plain markdown files under references/ — no
|
|
3046
|
-
|
|
3047
|
+
/** Restructure targets are plain markdown files under references/ — no
|
|
3048
|
+
* subdirectories, no other support kind. V8-10 (0.3.47): the regex-level
|
|
3049
|
+
* `(?!.*\.\.)` keeps the restructure-created set EXACTLY the set
|
|
3050
|
+
* validateSupportPath can reopen — a `references/my..notes.md` target (double
|
|
3051
|
+
* dots) used to pass here while every later patch/write/remove on it was
|
|
3052
|
+
* refused as traversal (an orphan file the user could not touch). */
|
|
3053
|
+
const RESTRUCTURE_TARGET_RE = /^references\/[a-z0-9](?!.*\.\.)[a-z0-9._-]*\.md$/;
|
|
3047
3054
|
/** Extra file name carried inside a snapshot's `extras/` directory. */
|
|
3048
3055
|
const SNAPSHOT_EXTRA_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
3049
3056
|
function skillsRoot(env = process.env) {
|
|
@@ -4148,94 +4155,65 @@ var SkillLibrary = class {
|
|
|
4148
4155
|
message: `Invalid skill name "${name}". Use lowercase letters, digits, and hyphens.`
|
|
4149
4156
|
};
|
|
4150
4157
|
const targetDir = this.dirOf(targetName);
|
|
4151
|
-
const targetMd = await this.io.readText(join(targetDir, "SKILL.md"));
|
|
4152
|
-
if (!targetMd) return {
|
|
4153
|
-
ok: false,
|
|
4154
|
-
message: `Skill "${targetName}" not found.`
|
|
4155
|
-
};
|
|
4156
4158
|
const targetProtection = await this.writeProtection(targetName, origin);
|
|
4157
4159
|
if (targetProtection) return {
|
|
4158
4160
|
ok: false,
|
|
4159
4161
|
message: `Skill "${targetName}" is protected (${targetProtection}).`
|
|
4160
4162
|
};
|
|
4161
|
-
const
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
if (protection) return {
|
|
4167
|
-
ok: false,
|
|
4168
|
-
message: `Skill "${source}" is protected (${protection}).`
|
|
4169
|
-
};
|
|
4170
|
-
const sourceMd = await this.io.readText(join(this.dirOf(source), "SKILL.md"));
|
|
4171
|
-
if (!sourceMd) return {
|
|
4172
|
-
ok: false,
|
|
4173
|
-
message: `Skill "${source}" not found.`
|
|
4174
|
-
};
|
|
4175
|
-
const parsed = parseFrontmatter(sourceMd);
|
|
4176
|
-
if (!parsed) return {
|
|
4177
|
-
ok: false,
|
|
4178
|
-
message: `Skill "${source}" has no valid frontmatter; refusing to merge.`
|
|
4179
|
-
};
|
|
4180
|
-
if (await this.countSupportDirs(source) > 0) return {
|
|
4181
|
-
ok: false,
|
|
4182
|
-
message: `Consolidation rejected: source "${source}" carries support files — use mode:'reference' or archive the whole package instead.`
|
|
4183
|
-
};
|
|
4184
|
-
const refs = supportRefs(parsed.body);
|
|
4185
|
-
if (refs.length > 0) return {
|
|
4186
|
-
ok: false,
|
|
4187
|
-
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.`
|
|
4188
|
-
};
|
|
4189
|
-
parts.push(`\n<!-- consolidated from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}`);
|
|
4190
|
-
}
|
|
4191
|
-
const merged = targetMd.trimEnd() + parts.join("\n") + "\n";
|
|
4192
|
-
const validation = validateFrontmatter(merged, targetName, this.limits);
|
|
4193
|
-
if (validation) return {
|
|
4163
|
+
const referenceWrites = [];
|
|
4164
|
+
const parts = [];
|
|
4165
|
+
if (mode === "append") for (const source of normalizedSources) {
|
|
4166
|
+
const protection = await this.deleteProtection(source);
|
|
4167
|
+
if (protection) return {
|
|
4194
4168
|
ok: false,
|
|
4195
|
-
message: `
|
|
4169
|
+
message: `Skill "${source}" is protected (${protection}).`
|
|
4196
4170
|
};
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
content: merged
|
|
4200
|
-
});
|
|
4201
|
-
} else {
|
|
4202
|
-
for (const source of normalizedSources) {
|
|
4203
|
-
const protection = await this.deleteProtection(source);
|
|
4204
|
-
if (protection) return {
|
|
4205
|
-
ok: false,
|
|
4206
|
-
message: `Skill "${source}" is protected (${protection}).`
|
|
4207
|
-
};
|
|
4208
|
-
const sourceMd = await this.io.readText(join(this.dirOf(source), "SKILL.md"));
|
|
4209
|
-
if (!sourceMd) return {
|
|
4210
|
-
ok: false,
|
|
4211
|
-
message: `Skill "${source}" not found.`
|
|
4212
|
-
};
|
|
4213
|
-
const parsed = parseFrontmatter(sourceMd);
|
|
4214
|
-
if (!parsed) return {
|
|
4215
|
-
ok: false,
|
|
4216
|
-
message: `Skill "${source}" has no valid frontmatter; refusing to demote.`
|
|
4217
|
-
};
|
|
4218
|
-
const refs = supportRefs(parsed.body);
|
|
4219
|
-
if (refs.length > 0) return {
|
|
4220
|
-
ok: false,
|
|
4221
|
-
message: `Consolidation rejected: source "${source}" body references support files (${refs.join(", ")}) that would be left behind — archive the whole package instead.`
|
|
4222
|
-
};
|
|
4223
|
-
const target = join(targetDir, "references", `${source}.md`);
|
|
4224
|
-
writes.push({
|
|
4225
|
-
target,
|
|
4226
|
-
content: `<!-- demoted from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}\n`
|
|
4227
|
-
});
|
|
4228
|
-
}
|
|
4229
|
-
const pointerLines = normalizedSources.map((source) => `\n${POINTER_LINE_PREFIX}${source}.md`).join("");
|
|
4230
|
-
const extended = targetMd.trimEnd() + pointerLines + "\n";
|
|
4231
|
-
const validation = validateFrontmatter(extended, targetName, this.limits);
|
|
4232
|
-
if (validation) return {
|
|
4171
|
+
const sourceMd = await this.io.readText(join(this.dirOf(source), "SKILL.md"));
|
|
4172
|
+
if (!sourceMd) return {
|
|
4233
4173
|
ok: false,
|
|
4234
|
-
message: `
|
|
4174
|
+
message: `Skill "${source}" not found.`
|
|
4235
4175
|
};
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4176
|
+
const parsed = parseFrontmatter(sourceMd);
|
|
4177
|
+
if (!parsed) return {
|
|
4178
|
+
ok: false,
|
|
4179
|
+
message: `Skill "${source}" has no valid frontmatter; refusing to merge.`
|
|
4180
|
+
};
|
|
4181
|
+
if (await this.countSupportDirs(source) > 0) return {
|
|
4182
|
+
ok: false,
|
|
4183
|
+
message: `Consolidation rejected: source "${source}" carries support files — use mode:'reference' or archive the whole package instead.`
|
|
4184
|
+
};
|
|
4185
|
+
const refs = supportRefs(parsed.body);
|
|
4186
|
+
if (refs.length > 0) return {
|
|
4187
|
+
ok: false,
|
|
4188
|
+
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.`
|
|
4189
|
+
};
|
|
4190
|
+
parts.push(`\n<!-- consolidated from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}`);
|
|
4191
|
+
}
|
|
4192
|
+
else for (const source of normalizedSources) {
|
|
4193
|
+
const protection = await this.deleteProtection(source);
|
|
4194
|
+
if (protection) return {
|
|
4195
|
+
ok: false,
|
|
4196
|
+
message: `Skill "${source}" is protected (${protection}).`
|
|
4197
|
+
};
|
|
4198
|
+
const sourceMd = await this.io.readText(join(this.dirOf(source), "SKILL.md"));
|
|
4199
|
+
if (!sourceMd) return {
|
|
4200
|
+
ok: false,
|
|
4201
|
+
message: `Skill "${source}" not found.`
|
|
4202
|
+
};
|
|
4203
|
+
const parsed = parseFrontmatter(sourceMd);
|
|
4204
|
+
if (!parsed) return {
|
|
4205
|
+
ok: false,
|
|
4206
|
+
message: `Skill "${source}" has no valid frontmatter; refusing to demote.`
|
|
4207
|
+
};
|
|
4208
|
+
const refs = supportRefs(parsed.body);
|
|
4209
|
+
if (refs.length > 0) return {
|
|
4210
|
+
ok: false,
|
|
4211
|
+
message: `Consolidation rejected: source "${source}" body references support files (${refs.join(", ")}) that would be left behind — archive the whole package instead.`
|
|
4212
|
+
};
|
|
4213
|
+
const target = join(targetDir, "references", `${source}.md`);
|
|
4214
|
+
referenceWrites.push({
|
|
4215
|
+
target,
|
|
4216
|
+
content: `<!-- demoted from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}\n`
|
|
4239
4217
|
});
|
|
4240
4218
|
}
|
|
4241
4219
|
const archived = [];
|
|
@@ -4245,16 +4223,53 @@ var SkillLibrary = class {
|
|
|
4245
4223
|
if (!result.ok) throw new Error(result.message);
|
|
4246
4224
|
archived.push(source);
|
|
4247
4225
|
}
|
|
4248
|
-
const result = await this.
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4226
|
+
const result = await this.serial(async () => {
|
|
4227
|
+
const freshTargetMd = await this.io.readText(join(targetDir, "SKILL.md"));
|
|
4228
|
+
if (!freshTargetMd) return {
|
|
4229
|
+
ok: false,
|
|
4230
|
+
message: `Skill "${targetName}" not found.`
|
|
4231
|
+
};
|
|
4232
|
+
const writes = [...referenceWrites];
|
|
4233
|
+
if (mode === "append") {
|
|
4234
|
+
const merged = freshTargetMd.trimEnd() + parts.join("\n") + "\n";
|
|
4235
|
+
const validation = validateFrontmatter(merged, targetName, this.limits);
|
|
4236
|
+
if (validation) return {
|
|
4237
|
+
ok: false,
|
|
4238
|
+
message: `Consolidation rejected: ${validation}`
|
|
4239
|
+
};
|
|
4240
|
+
writes.push({
|
|
4241
|
+
target: join(targetDir, "SKILL.md"),
|
|
4242
|
+
content: merged
|
|
4243
|
+
});
|
|
4244
|
+
} else {
|
|
4245
|
+
const pointerLines = normalizedSources.map((source) => `\n${POINTER_LINE_PREFIX}${source}.md`).join("");
|
|
4246
|
+
const extended = freshTargetMd.trimEnd() + pointerLines + "\n";
|
|
4247
|
+
const validation = validateFrontmatter(extended, targetName, this.limits);
|
|
4248
|
+
if (validation) return {
|
|
4249
|
+
ok: false,
|
|
4250
|
+
message: `Consolidation rejected: ${validation}`
|
|
4251
|
+
};
|
|
4252
|
+
writes.push({
|
|
4253
|
+
target: join(targetDir, "SKILL.md"),
|
|
4254
|
+
content: extended
|
|
4255
|
+
});
|
|
4256
|
+
}
|
|
4257
|
+
return await this.applyTreeChange({
|
|
4258
|
+
name: targetName,
|
|
4259
|
+
origin,
|
|
4260
|
+
protection: "write",
|
|
4261
|
+
writes,
|
|
4262
|
+
auditAction: "consolidate",
|
|
4263
|
+
auditSummary: `consolidated ${normalizedSources.join(", ")} (${mode}) into ${targetName}`,
|
|
4264
|
+
eventAction: "consolidate"
|
|
4265
|
+
});
|
|
4256
4266
|
});
|
|
4257
4267
|
if (!result.ok) throw new Error(result.message);
|
|
4268
|
+
return {
|
|
4269
|
+
ok: true,
|
|
4270
|
+
message: `Consolidated ${normalizedSources.join(", ")} into "${targetName}".`,
|
|
4271
|
+
path: targetDir
|
|
4272
|
+
};
|
|
4258
4273
|
} catch (error) {
|
|
4259
4274
|
const reason = error instanceof Error ? error.message : String(error);
|
|
4260
4275
|
const failedRestores = [];
|
|
@@ -4272,11 +4287,6 @@ var SkillLibrary = class {
|
|
|
4272
4287
|
message: `Consolidation failed and was rolled back: ${reason}`
|
|
4273
4288
|
};
|
|
4274
4289
|
}
|
|
4275
|
-
return {
|
|
4276
|
-
ok: true,
|
|
4277
|
-
message: `Consolidated ${normalizedSources.join(", ")} into "${targetName}".`,
|
|
4278
|
-
path: targetDir
|
|
4279
|
-
};
|
|
4280
4290
|
}
|
|
4281
4291
|
/**
|
|
4282
4292
|
* Content-distribution repair (008 batch B, 009-R kernel): move body
|
|
@@ -4842,11 +4852,14 @@ var SkillLibrary = class {
|
|
|
4842
4852
|
*/
|
|
4843
4853
|
/**
|
|
4844
4854
|
* DSH home root: `$DSH_HOME` or `~/.dsh`. Single source of the empty-string
|
|
4845
|
-
* fallback
|
|
4846
|
-
* never to a CWD-relative path (0.3.19 W1.3, 0.3.22 F-207)
|
|
4855
|
+
* fallback — an EMPTY or WHITESPACE-ONLY DSH_HOME resolves to the default
|
|
4856
|
+
* home, never to a CWD-relative path (0.3.19 W1.3, 0.3.22 F-207);
|
|
4857
|
+
* V8-06 (0.3.47) extends the guard to whitespace (upstream home-paths:
|
|
4858
|
+
* `trim().length > 0` is the adoption test — `DSH_HOME=" "` must not produce
|
|
4859
|
+
* a sidecar under a relative "." path).
|
|
4847
4860
|
*/
|
|
4848
4861
|
function evolutionRoot(env = process.env) {
|
|
4849
|
-
return env.DSH_HOME
|
|
4862
|
+
return env.DSH_HOME?.trim() ? env.DSH_HOME : join(homedir(), ".dsh");
|
|
4850
4863
|
}
|
|
4851
4864
|
/** Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
|
|
4852
4865
|
* state (reports, activity store, feedback file, state-domain data). */
|
package/lib/types/curator.d.ts
CHANGED
|
@@ -117,7 +117,7 @@ export declare function parseCuratorNominations(text: string): CuratorNomination
|
|
|
117
117
|
* view so the two can never disagree: records failing ANY of these gates are
|
|
118
118
|
* outside the managed scope.
|
|
119
119
|
*/
|
|
120
|
-
export declare function lifecycleCandidate(name: string, record: UsageRecord, config: CuratorConfig, bundled: boolean, gates?: EvolutionGateSet): boolean;
|
|
120
|
+
export declare function lifecycleCandidate(name: string, record: UsageRecord, config: CuratorConfig, bundled: boolean, gates?: EvolutionGateSet, protectedNames?: ReadonlyMap<string, string>): boolean;
|
|
121
121
|
export interface ScopeView {
|
|
122
122
|
/** Skills inside the lifecycle scope right now (candidate gate + active state). */
|
|
123
123
|
managed: string[];
|
|
@@ -137,5 +137,5 @@ export interface ScopeView {
|
|
|
137
137
|
* records lack (bundled / hub-installed / pinned from `SkillLibrary.list()`).
|
|
138
138
|
*/
|
|
139
139
|
export declare function computeScopeView(usage: UsageMap, config: CuratorConfig, protectedNames?: ReadonlyMap<string, string>, gates?: EvolutionGateSet): ScopeView;
|
|
140
|
-
export declare function computeLifecycleTransitions(usage: UsageMap, config: CuratorConfig, now?: Date, gates?: EvolutionGateSet): CuratorResult;
|
|
140
|
+
export declare function computeLifecycleTransitions(usage: UsageMap, config: CuratorConfig, now?: Date, gates?: EvolutionGateSet, protectedNames?: ReadonlyMap<string, string>): CuratorResult;
|
|
141
141
|
//# sourceMappingURL=curator.d.ts.map
|
package/lib/types/io.d.ts
CHANGED
|
@@ -64,7 +64,7 @@ export declare function evolutionIoAdapter(provider: () => EvolutionIoLike): Evo
|
|
|
64
64
|
* sole gate for the self-pid recycle branch. Exported (read-only in practice)
|
|
65
65
|
* so `io.spec.ts` can drive the self-heal path deterministically.
|
|
66
66
|
*/
|
|
67
|
-
export declare const pendingSelfCleanup:
|
|
67
|
+
export declare const pendingSelfCleanup: Map<string, string>;
|
|
68
68
|
/**
|
|
69
69
|
* Retry a rename that a peer is temporarily holding on Windows (EPERM/EBUSY):
|
|
70
70
|
* a short 50ms backoff, at most 3 retries (~150ms budget), matching the
|
|
@@ -50,7 +50,12 @@ export interface SkillRestructureMove {
|
|
|
50
50
|
}
|
|
51
51
|
/** Upper bound of moves per restructure proposal (validator and core agree). */
|
|
52
52
|
export declare const MAX_RESTRUCTURE_MOVES = 5;
|
|
53
|
-
/** Restructure targets are plain markdown files under references/ — no
|
|
53
|
+
/** Restructure targets are plain markdown files under references/ — no
|
|
54
|
+
* subdirectories, no other support kind. V8-10 (0.3.47): the regex-level
|
|
55
|
+
* `(?!.*\.\.)` keeps the restructure-created set EXACTLY the set
|
|
56
|
+
* validateSupportPath can reopen — a `references/my..notes.md` target (double
|
|
57
|
+
* dots) used to pass here while every later patch/write/remove on it was
|
|
58
|
+
* refused as traversal (an orphan file the user could not touch). */
|
|
54
59
|
export declare const RESTRUCTURE_TARGET_RE: RegExp;
|
|
55
60
|
/** Extra file name carried inside a snapshot's `extras/` directory. */
|
|
56
61
|
export declare const SNAPSHOT_EXTRA_NAME_RE: RegExp;
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* DSH home root: `$DSH_HOME` or `~/.dsh`. Single source of the empty-string
|
|
7
|
-
* fallback
|
|
8
|
-
* never to a CWD-relative path (0.3.19 W1.3, 0.3.22 F-207)
|
|
7
|
+
* fallback — an EMPTY or WHITESPACE-ONLY DSH_HOME resolves to the default
|
|
8
|
+
* home, never to a CWD-relative path (0.3.19 W1.3, 0.3.22 F-207);
|
|
9
|
+
* V8-06 (0.3.47) extends the guard to whitespace (upstream home-paths:
|
|
10
|
+
* `trim().length > 0` is the adoption test — `DSH_HOME=" "` must not produce
|
|
11
|
+
* a sidecar under a relative "." path).
|
|
9
12
|
*/
|
|
10
13
|
export declare function evolutionRoot(env?: NodeJS.ProcessEnv): string;
|
|
11
14
|
/** Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
|
package/package.json
CHANGED