@lmzhen/dsh-evolution-core 0.3.73 → 0.3.76
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 +2 -0
- package/lib/index.js +1301 -640
- package/lib/types/constants.d.ts +5 -2
- package/lib/types/evolution-events.d.ts +12 -1
- package/lib/types/index.d.ts +7 -4
- package/lib/types/memory-store.d.ts +16 -0
- package/lib/types/probe.d.ts +38 -0
- package/lib/types/review-channel.d.ts +41 -0
- package/lib/types/scope.d.ts +27 -0
- package/lib/types/skill-store.d.ts +59 -25
- package/lib/types/tool-dispatch.d.ts +212 -0
- package/lib/types/usage.d.ts +7 -10
- package/package.json +3 -7
- package/lib/invariant.js +0 -8
- package/lib/types/invariant.d.ts +0 -5
package/lib/index.js
CHANGED
|
@@ -2,6 +2,7 @@ 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";
|
|
5
|
+
import { scopeOf } from "@deepseek-ai/dsh-scope";
|
|
5
6
|
import { load } from "js-yaml";
|
|
6
7
|
//#region lib/types/io.js
|
|
7
8
|
/**
|
|
@@ -77,6 +78,25 @@ function evolutionIoAdapter(provider) {
|
|
|
77
78
|
* so `io.spec.ts` can drive the self-heal path deterministically.
|
|
78
79
|
*/
|
|
79
80
|
const pendingSelfCleanup = /* @__PURE__ */ new Map();
|
|
81
|
+
/** P2-7 (v11)/P3-10 (v14): record a failed release so the next write to this
|
|
82
|
+
* path self-heals it, keeping the map's 64-entry cap — a never-re-touched path
|
|
83
|
+
* must not pin it, while a path whose lock still EXISTS is the last to drop
|
|
84
|
+
* (dropping it disables the self-heal for a lock that is really there). */
|
|
85
|
+
const recordPendingSelfCleanup = async (lock, token) => {
|
|
86
|
+
if (pendingSelfCleanup.size >= 64) {
|
|
87
|
+
let droppable;
|
|
88
|
+
for (const candidate of pendingSelfCleanup.keys()) {
|
|
89
|
+
if (candidate === lock) continue;
|
|
90
|
+
if (await readFile(candidate, "utf8").then(() => false, () => true)) {
|
|
91
|
+
droppable = candidate;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const victim = droppable ?? pendingSelfCleanup.keys().next().value;
|
|
96
|
+
if (victim !== void 0) pendingSelfCleanup.delete(victim);
|
|
97
|
+
}
|
|
98
|
+
pendingSelfCleanup.set(lock, token);
|
|
99
|
+
};
|
|
80
100
|
const RENAME_RETRY_BASE_MS = 50;
|
|
81
101
|
const RENAME_RETRY_MAX_DELAY_MS = 800;
|
|
82
102
|
const RENAME_RETRY_MAX_ATTEMPTS = 6;
|
|
@@ -255,6 +275,11 @@ const EMPTY_LOCK_TAKEOVER_MS = 3e4;
|
|
|
255
275
|
/** A body with no parseable pid (crash mid-write): 1h, far above any legal hold
|
|
256
276
|
* and far below "forever". */
|
|
257
277
|
const LOCK_TEAR_TAKEOVER_MS = 36e5;
|
|
278
|
+
/** P2-27 (v37): the commit-point ownership re-read. A transient read failure
|
|
279
|
+
* (EACCES/EMFILE/antivirus hold) must not abort a valid RMW, so the read is
|
|
280
|
+
* retried in place; only a still-unreadable lock fails the attempt. */
|
|
281
|
+
const OWNERSHIP_READ_ATTEMPTS = 3;
|
|
282
|
+
const OWNERSHIP_READ_RETRY_MS = 20;
|
|
258
283
|
/**
|
|
259
284
|
* V27 G1.3: the error `commitTmp` throws when the rename landed but the parent
|
|
260
285
|
* directory fsync failed — the bytes ARE visible, only their durability is
|
|
@@ -438,7 +463,26 @@ function nodeEvolutionIo(lockAttempts = 40) {
|
|
|
438
463
|
continue;
|
|
439
464
|
}
|
|
440
465
|
const assertOwned = async () => {
|
|
441
|
-
|
|
466
|
+
let body = "";
|
|
467
|
+
let readable = false;
|
|
468
|
+
for (let read = 0; read < OWNERSHIP_READ_ATTEMPTS && !readable; read += 1) {
|
|
469
|
+
if (read > 0) await new Promise((resolve) => setTimeout(resolve, OWNERSHIP_READ_RETRY_MS));
|
|
470
|
+
const attempt = await readFile(lock, "utf8").then((value) => ({
|
|
471
|
+
ok: true,
|
|
472
|
+
value
|
|
473
|
+
}), () => ({
|
|
474
|
+
ok: false,
|
|
475
|
+
value: ""
|
|
476
|
+
}));
|
|
477
|
+
if (attempt.ok) {
|
|
478
|
+
body = attempt.value;
|
|
479
|
+
readable = true;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (!readable) {
|
|
483
|
+
console.warn(`evolution-io: write lock ${lock} could not be read at the commit point (ownership unverified, ours: ${JSON.stringify(myClaim)}) — aborting this read-modify-write and retrying under a fresh acquisition`);
|
|
484
|
+
throw new LostWriteLock();
|
|
485
|
+
}
|
|
442
486
|
if (body !== myClaim) {
|
|
443
487
|
console.warn(`evolution-io: write lock ${lock} was reclaimed by another writer before the commit (on disk now: ${JSON.stringify(body)}, ours: ${JSON.stringify(myClaim)}) — aborting this read-modify-write and retrying under a fresh acquisition`);
|
|
444
488
|
throw new LostWriteLock();
|
|
@@ -450,25 +494,21 @@ function nodeEvolutionIo(lockAttempts = 40) {
|
|
|
450
494
|
if (error instanceof LostWriteLock) continue;
|
|
451
495
|
throw error;
|
|
452
496
|
} finally {
|
|
453
|
-
|
|
497
|
+
const mine = await readFile(lock, "utf8").then((body) => ({
|
|
498
|
+
ok: true,
|
|
499
|
+
body
|
|
500
|
+
}), (error) => ({
|
|
501
|
+
ok: false,
|
|
502
|
+
missing: isMissing(error)
|
|
503
|
+
}));
|
|
504
|
+
if (!mine.ok) {
|
|
505
|
+
if (!mine.missing) await recordPendingSelfCleanup(lock, myClaim);
|
|
506
|
+
} else if (mine.body === myClaim) await rm(lock, {
|
|
454
507
|
force: true,
|
|
455
508
|
maxRetries: 20,
|
|
456
509
|
retryDelay: 100
|
|
457
510
|
}).catch(async () => {
|
|
458
|
-
|
|
459
|
-
if (pendingSelfCleanup.size >= 64) {
|
|
460
|
-
let droppable;
|
|
461
|
-
for (const candidate of pendingSelfCleanup.keys()) {
|
|
462
|
-
if (candidate === lock) continue;
|
|
463
|
-
if (await readFile(candidate, "utf8").then(() => false, () => true)) {
|
|
464
|
-
droppable = candidate;
|
|
465
|
-
break;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
const victim = droppable ?? pendingSelfCleanup.keys().next().value;
|
|
469
|
-
if (victim !== void 0) pendingSelfCleanup.delete(victim);
|
|
470
|
-
}
|
|
471
|
-
pendingSelfCleanup.set(lock, body);
|
|
511
|
+
await recordPendingSelfCleanup(lock, await readFile(lock, "utf8").catch(() => ""));
|
|
472
512
|
});
|
|
473
513
|
}
|
|
474
514
|
}
|
|
@@ -750,21 +790,15 @@ async function mutateUsage(root, io, task, options = {}) {
|
|
|
750
790
|
return JSON.stringify(Object.fromEntries(map.entries()), null, 2);
|
|
751
791
|
});
|
|
752
792
|
}
|
|
753
|
-
/**
|
|
754
|
-
* Curator-owned usage fields (rc.67 K-2): the curator writes ONLY this set —
|
|
793
|
+
/** Curator-owned usage fields (rc.67 K-2): the curator writes ONLY this set —
|
|
755
794
|
* lifecycle state, archive stamp, the six-factor quality pair, and the
|
|
756
|
-
* marker-mirrored pin flag
|
|
757
|
-
* tool-telemetry side
|
|
758
|
-
*
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
*/
|
|
762
|
-
function applyCuratorFields(disk, curated) {
|
|
763
|
-
applyCuratorMetaFields(disk, curated);
|
|
764
|
-
applyCuratorLifecycleFields(disk, curated);
|
|
765
|
-
}
|
|
795
|
+
* marker-mirrored pin flag; counters and activity stamps belong to the
|
|
796
|
+
* tool-telemetry side and are never copied by a fold. P2-12 (v39): the
|
|
797
|
+
* combined `applyCuratorFields` wrapper had no production caller (folds go
|
|
798
|
+
* through {@link foldCuratorFields}), so the two field copies below are the
|
|
799
|
+
* whole contract. */
|
|
766
800
|
/** Copy only the lifecycle pair (state/archived_at) — see the ownership split
|
|
767
|
-
* rationale
|
|
801
|
+
* rationale above. */
|
|
768
802
|
function applyCuratorLifecycleFields(disk, curated) {
|
|
769
803
|
disk.state = curated.state;
|
|
770
804
|
disk.archived_at = curated.archived_at;
|
|
@@ -1049,13 +1083,17 @@ const DEFAULT_CURATOR_BOOT_GRACE_SECONDS = 10;
|
|
|
1049
1083
|
const DEFAULT_CURATOR_REVIEW_MAX_TOKENS = 2048;
|
|
1050
1084
|
/** 0.3.17 (S3.10, T-1): control-plane fields a model-facing write call may
|
|
1051
1085
|
* never carry — single source for plan-validator, evolution-policy and the
|
|
1052
|
-
* threat scanner (they used to each hardcode the list).
|
|
1086
|
+
* threat scanner (they used to each hardcode the list).
|
|
1087
|
+
* P2-13 (v37): staged_from_sha256 joins the list — it is the replay's own
|
|
1088
|
+
* staleness anchor, and the tool-arguments root is an OPEN object, so a model
|
|
1089
|
+
* could otherwise choose the anchor that decides the write's outcome. */
|
|
1053
1090
|
const FORBIDDEN_CONTROL_KEYS = [
|
|
1054
1091
|
"policy",
|
|
1055
1092
|
"threshold",
|
|
1056
1093
|
"prompt_hash",
|
|
1057
1094
|
"model_route",
|
|
1058
|
-
"evolution_config"
|
|
1095
|
+
"evolution_config",
|
|
1096
|
+
"staged_from_sha256"
|
|
1059
1097
|
];
|
|
1060
1098
|
/** 0.3.17 (S3.10): the model-facing write tools the policy guard and threat
|
|
1061
1099
|
* scanner cover. */
|
|
@@ -1215,20 +1253,20 @@ function parseCuratorNominations(text) {
|
|
|
1215
1253
|
section = header[1] === "consolidations" ? "consolidations" : "prunings";
|
|
1216
1254
|
continue;
|
|
1217
1255
|
}
|
|
1218
|
-
const consolidated = /^\s*-\s*from:\s*([a-z0-9][a-z0-9-]*)
|
|
1256
|
+
const consolidated = /^\s*-\s*from:\s*([a-z0-9][a-z0-9-]*)(?:\s*#.*)?\s*$/.exec(line);
|
|
1219
1257
|
if (consolidated) {
|
|
1220
1258
|
section = "consolidations";
|
|
1221
1259
|
currentFrom = consolidated[1] ?? "";
|
|
1222
1260
|
currentMode = void 0;
|
|
1223
1261
|
continue;
|
|
1224
1262
|
}
|
|
1225
|
-
const mode = /^\s*mode:\s*(append|reference)
|
|
1263
|
+
const mode = /^\s*mode:\s*(append|reference)(?:\s*#.*)?\s*$/.exec(line);
|
|
1226
1264
|
if (mode) {
|
|
1227
1265
|
if (currentFrom !== "") currentMode = mode[1] === "reference" ? "reference" : "append";
|
|
1228
1266
|
else warnings.push(`mode: ${mode[1]} ignored — no preceding "- from:" entry (the consolidation falls back to append)`);
|
|
1229
1267
|
continue;
|
|
1230
1268
|
}
|
|
1231
|
-
const into = /^\s*into:\s*([a-z0-9][a-z0-9-]*)
|
|
1269
|
+
const into = /^\s*into:\s*([a-z0-9][a-z0-9-]*)(?:\s*#.*)?\s*$/.exec(line);
|
|
1232
1270
|
if (into) {
|
|
1233
1271
|
const intoName = into[1] ?? "";
|
|
1234
1272
|
if (section === "consolidations" && currentFrom !== "" && currentFrom !== intoName) consolidations.push({
|
|
@@ -1240,13 +1278,15 @@ function parseCuratorNominations(text) {
|
|
|
1240
1278
|
currentMode = void 0;
|
|
1241
1279
|
continue;
|
|
1242
1280
|
}
|
|
1243
|
-
const pruned = /^\s*-\s*name:\s*([a-z0-9][a-z0-9-]*)
|
|
1281
|
+
const pruned = /^\s*-\s*name:\s*([a-z0-9][a-z0-9-]*)(?:\s*#.*)?\s*$/.exec(line);
|
|
1244
1282
|
if (pruned) {
|
|
1245
1283
|
if (section === "consolidations") warnings.push("\"- name:\" inside the consolidations section flips the parse to prunings");
|
|
1246
1284
|
section = "prunings";
|
|
1247
1285
|
const name = pruned[1];
|
|
1248
1286
|
if (name) prunings.push(name);
|
|
1287
|
+
continue;
|
|
1249
1288
|
}
|
|
1289
|
+
if (/^\s*(?:-\s*(?:from|name)\s*:|(?:into|mode)\s*:)/.test(line)) warnings.push(`"${line.trim()}" ignored - not a usable nomination line (names are lowercase letters, digits and hyphens; one optional trailing "# comment" is allowed)`);
|
|
1250
1290
|
}
|
|
1251
1291
|
const valid = (name) => NOMINATION_NAME_RE.test(name);
|
|
1252
1292
|
return {
|
|
@@ -1435,6 +1475,7 @@ function evolutionEventPayloadIssue(event) {
|
|
|
1435
1475
|
case "feedback":
|
|
1436
1476
|
if (event.kind !== "skill" && event.kind !== "session") return "feedback event requires kind skill|session";
|
|
1437
1477
|
if (event.rating !== "positive" && event.rating !== "negative") return "feedback event requires rating positive|negative";
|
|
1478
|
+
if (typeof event.target !== "string" || event.target.trim() === "") return "feedback event requires a non-empty target";
|
|
1438
1479
|
return null;
|
|
1439
1480
|
case "maintain": return typeof event.runId === "string" ? null : "maintain event requires runId";
|
|
1440
1481
|
case "learn":
|
|
@@ -1535,7 +1576,12 @@ async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE
|
|
|
1535
1576
|
return current;
|
|
1536
1577
|
}
|
|
1537
1578
|
}
|
|
1538
|
-
const
|
|
1579
|
+
const rotated = await rotateIfDue(io, path, v1EventRecords(parsedBody), rotateAt);
|
|
1580
|
+
if (!rotated.ok) {
|
|
1581
|
+
refuseMessage = rotated.reason;
|
|
1582
|
+
return current;
|
|
1583
|
+
}
|
|
1584
|
+
const events = rotated.events;
|
|
1539
1585
|
let maxSeq = events.reduce((max, entry) => Math.max(max, entry.seq), 0);
|
|
1540
1586
|
for (const name of await listEventArchives(io, path)) maxSeq = Math.max(maxSeq, Number.parseInt(name.slice(7, name.length - 5), 10));
|
|
1541
1587
|
const record = {
|
|
@@ -1552,6 +1598,16 @@ async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE
|
|
|
1552
1598
|
if (assigned === 0) throw new Error(`${refuseMessage || "evolution event log is malformed and was not touched"}: ${path}`);
|
|
1553
1599
|
return assigned;
|
|
1554
1600
|
}
|
|
1601
|
+
/** The collision archive is mergeable only when this v1 writer can read it
|
|
1602
|
+
* (P2-11): a foreign `version` (F-338) or a body without an `events` array is
|
|
1603
|
+
* refused, so a merge can never downgrade it or empty it out.
|
|
1604
|
+
* @param parsed - the parsed collision archive body.
|
|
1605
|
+
* @returns the usable events, or null when the archive must not be rewritten. */
|
|
1606
|
+
function mergeableCollisionEvents(parsed) {
|
|
1607
|
+
if (parsed.version !== void 0 && parsed.version !== 1) return null;
|
|
1608
|
+
if (!Array.isArray(parsed.events)) return null;
|
|
1609
|
+
return parsed.events.filter(isEventRecord);
|
|
1610
|
+
}
|
|
1555
1611
|
/**
|
|
1556
1612
|
* Split the active log at its midpoint when due: the older half is written to
|
|
1557
1613
|
* `events-<lastArchivedSeq>.json` (await — a failed archive write aborts the
|
|
@@ -1561,38 +1617,60 @@ async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE
|
|
|
1561
1617
|
* one-event rotate would archive everything and restart seqs at 1).
|
|
1562
1618
|
*/
|
|
1563
1619
|
async function rotateIfDue(io, path, events, rotateAt) {
|
|
1564
|
-
if (!Number.isFinite(rotateAt) || rotateAt < 2 || events.length < rotateAt) return
|
|
1620
|
+
if (!Number.isFinite(rotateAt) || rotateAt < 2 || events.length < rotateAt) return {
|
|
1621
|
+
ok: true,
|
|
1622
|
+
events
|
|
1623
|
+
};
|
|
1565
1624
|
const mid = Math.ceil(events.length / 2);
|
|
1566
1625
|
const head = events.slice(0, mid);
|
|
1567
1626
|
const tail = events.slice(mid);
|
|
1568
|
-
if (tail.length === 0) return
|
|
1627
|
+
if (tail.length === 0) return {
|
|
1628
|
+
ok: true,
|
|
1629
|
+
events
|
|
1630
|
+
};
|
|
1569
1631
|
const anchor = tail[0]?.seq ?? 0;
|
|
1570
1632
|
const archivePath = join(dirname(path), `${EVENT_ARCHIVE_PREFIX}${anchor - 1}.json`);
|
|
1571
1633
|
let archived = head;
|
|
1572
1634
|
const existing = await io.readText(archivePath).catch(() => null);
|
|
1573
|
-
if (existing !== null)
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1635
|
+
if (existing !== null) {
|
|
1636
|
+
let parsed = null;
|
|
1637
|
+
try {
|
|
1638
|
+
parsed = JSON.parse(existing);
|
|
1639
|
+
} catch {
|
|
1640
|
+
parsed = null;
|
|
1641
|
+
}
|
|
1642
|
+
if (parsed === null) {
|
|
1643
|
+
const shiftPath = `${archivePath}.${Date.now()}.collide`;
|
|
1644
|
+
await io.writeText(shiftPath, JSON.stringify({
|
|
1645
|
+
version: 1,
|
|
1646
|
+
events: head
|
|
1647
|
+
}, null, 2));
|
|
1648
|
+
console.warn(`evolution-events: rotation hit an unparsable archive collision — the rotated head band was preserved at ${shiftPath} but is OUTSIDE the logical timeline; inspect and merge it manually`);
|
|
1649
|
+
await retainEventArchives(io, path);
|
|
1650
|
+
await pruneCollideArchives(io, path);
|
|
1651
|
+
return {
|
|
1652
|
+
ok: true,
|
|
1653
|
+
events: tail
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
const prior = mergeableCollisionEvents(parsed);
|
|
1657
|
+
if (prior === null) return {
|
|
1658
|
+
ok: false,
|
|
1659
|
+
reason: `evolution event archive collision at ${archivePath} is not a readable version-1 archive (future version, or no "events" array) and was not touched`
|
|
1660
|
+
};
|
|
1661
|
+
const bySeq = new Map(prior.map((event) => [event.seq, event]));
|
|
1577
1662
|
for (const event of head) bySeq.set(event.seq, event);
|
|
1578
1663
|
archived = [...bySeq.values()].sort((a, b) => a.seq - b.seq);
|
|
1579
|
-
} catch {
|
|
1580
|
-
const shiftPath = `${archivePath}.${Date.now()}.collide`;
|
|
1581
|
-
await io.writeText(shiftPath, JSON.stringify({
|
|
1582
|
-
version: 1,
|
|
1583
|
-
events: head
|
|
1584
|
-
}, null, 2));
|
|
1585
|
-
console.warn(`evolution-events: rotation hit an unparsable archive collision — the rotated head band was preserved at ${shiftPath} but is OUTSIDE the logical timeline; inspect and merge it manually`);
|
|
1586
|
-
await retainEventArchives(io, path);
|
|
1587
|
-
await pruneCollideArchives(io, path);
|
|
1588
|
-
return tail;
|
|
1589
1664
|
}
|
|
1590
1665
|
await io.writeText(archivePath, JSON.stringify({
|
|
1591
1666
|
version: 1,
|
|
1592
1667
|
events: archived
|
|
1593
1668
|
}, null, 2));
|
|
1594
1669
|
await retainEventArchives(io, path);
|
|
1595
|
-
return
|
|
1670
|
+
return {
|
|
1671
|
+
ok: true,
|
|
1672
|
+
events: tail
|
|
1673
|
+
};
|
|
1596
1674
|
}
|
|
1597
1675
|
/**
|
|
1598
1676
|
* Prune old event archives (rc.71): keep the newest `EVENT_LOG_RETAIN_ARCHIVES`.
|
|
@@ -1726,496 +1804,111 @@ function allowRowCollisions(env = process.env) {
|
|
|
1726
1804
|
return env.DSH_EVOLUTION_ALLOW_ROW_COLLISIONS === ALLOW_ROW_COLLISIONS;
|
|
1727
1805
|
}
|
|
1728
1806
|
//#endregion
|
|
1729
|
-
//#region lib/types/
|
|
1807
|
+
//#region lib/types/learn-prompt.js
|
|
1730
1808
|
/**
|
|
1731
|
-
*
|
|
1732
|
-
* `agent/background_review.py`, `agent/curator.py`, and
|
|
1733
|
-
* `agent/learn_prompt.py`, with tool names translated to the DSH-native
|
|
1734
|
-
* catalog (`memory`, `skill_manage`, `skill`, `bash`, `str_replace_editor`).
|
|
1809
|
+
* Open-ended `/evolution learn` prompt builder.
|
|
1735
1810
|
*
|
|
1736
|
-
*
|
|
1737
|
-
*
|
|
1738
|
-
*
|
|
1739
|
-
*
|
|
1740
|
-
*
|
|
1741
|
-
*
|
|
1811
|
+
* `learn` is open-ended: the user can name anything they can describe — a
|
|
1812
|
+
* directory of code, an API doc URL, a workflow they just walked the agent
|
|
1813
|
+
* through, or pasted notes. The prompt instructs the live agent to gather the
|
|
1814
|
+
* named sources with its existing tools, then author a single SKILL.md via
|
|
1815
|
+
* `skill_manage` following `DSH_AUTHORING_STANDARDS`. There is no separate
|
|
1816
|
+
* distillation engine and no model-tool footprint.
|
|
1817
|
+
*/
|
|
1818
|
+
/**
|
|
1819
|
+
* Build the agent prompt for an open-ended `/evolution learn` request.
|
|
1742
1820
|
*
|
|
1743
|
-
*
|
|
1744
|
-
*
|
|
1745
|
-
*
|
|
1746
|
-
* for a bundle assembled OUT of process and handed to `verifyPromptBundle`
|
|
1747
|
-
* explicitly. It CANNOT detect in-process tampering (the digest is recomputed
|
|
1748
|
-
* from the same module state it verifies) — catching a stale or partially
|
|
1749
|
-
* patched default bundle is CI's version pin (tests/prompts.spec.ts), not
|
|
1750
|
-
* this runtime gate.
|
|
1821
|
+
* @param userRequest free-text the user gave after `/evolution learn`; an
|
|
1822
|
+
* empty string falls back to "the workflow we just went through".
|
|
1823
|
+
* @returns a complete instruction the agent runs as a normal turn.
|
|
1751
1824
|
*/
|
|
1825
|
+
function buildLearnPrompt(userRequest) {
|
|
1826
|
+
return [
|
|
1827
|
+
"[/learn] The user wants you to learn a reusable skill from the request below, and save it.",
|
|
1828
|
+
"",
|
|
1829
|
+
"THE REQUEST:",
|
|
1830
|
+
userRequest.trim() || "the workflow we just went through in this conversation — review the steps taken and distill them into a reusable skill",
|
|
1831
|
+
"",
|
|
1832
|
+
"The request is open-ended and may mix two kinds of content, in any order: SOURCES to gather (directories, file paths, URLs, \"what we just did\", pasted notes) AND REQUIREMENTS that shape the skill (what to focus on, what to leave out, scope, naming, the angle to take). Treat EVERY part of the request as load-bearing. In particular, prose that comes after a path or link is NOT incidental — it is the user telling you what they want from that source. A request like `<url> focus on the auth flow, skip the deprecated endpoints` means: gather the URL AND honor \"focus on auth, skip deprecated\" as authoring requirements. Never fetch the first source and ignore the rest.",
|
|
1833
|
+
"",
|
|
1834
|
+
"Do this:",
|
|
1835
|
+
"1. Gather every source the user named, using the tools you already have — reads and searches for local files or directories, web access for URLs, this conversation history if they referred to something you just did, and the text they pasted as-is. If the request is ambiguous about scope, make a reasonable choice and note it; do not stall.",
|
|
1836
|
+
"2. Author ONE SKILL.md, applying every requirement, focus, and constraint in the request — these govern what the SKILL.md covers and emphasizes, not just which sources you read.",
|
|
1837
|
+
"3. Save it with the `skill_manage` tool (action=\"create\"). Pick a sensible category. If the procedure needs a non-trivial script, add it under the skill's `scripts/` with `skill_manage` write_file and reference it by relative path.",
|
|
1838
|
+
"",
|
|
1839
|
+
"Apply the skill-authoring standards (frontmatter, prose, file layout) carried by the `skill_manage` tool description — that is the single copy; do not restate them here.",
|
|
1840
|
+
"",
|
|
1841
|
+
"When done, tell the user the skill name, its category, and a one-line summary of what it captured."
|
|
1842
|
+
].join("\n");
|
|
1843
|
+
}
|
|
1844
|
+
//#endregion
|
|
1845
|
+
//#region lib/types/state-store.js
|
|
1752
1846
|
/**
|
|
1753
|
-
*
|
|
1754
|
-
*
|
|
1755
|
-
*
|
|
1847
|
+
* Evolution home path helpers: the DSH root and `$DSH_HOME/evolution` for
|
|
1848
|
+
* plugin-owned sidecar state (reports, activity store, feedback file,
|
|
1849
|
+
* state-domain data).
|
|
1850
|
+
*
|
|
1851
|
+
* C-10: PATH HELPERS ONLY — despite the file name there is no store
|
|
1852
|
+
* here. Durable evolution state lives in the state stack (evolution-state over
|
|
1853
|
+
* evolution-state-json / -domain); skills and memories live in skill-store.ts
|
|
1854
|
+
* / memory-store.ts. The file name is kept deliberately: renaming it would
|
|
1855
|
+
* touch every family import for zero behavior change, and the audit records
|
|
1856
|
+
* the mismatch as known naming debt.
|
|
1756
1857
|
*/
|
|
1757
|
-
const PROMPT_BUNDLE_VERSION = 17;
|
|
1758
|
-
const PROMPT_BUNDLE_ID = `dsh-evolution@17`;
|
|
1759
|
-
const MEMORY_REVIEW_PROMPT = `[Auto-review — Memory]
|
|
1760
|
-
Review the conversation above and consider saving to memory if appropriate.
|
|
1761
|
-
|
|
1762
|
-
Focus on:
|
|
1763
|
-
1. Has the user revealed things about themselves — persona, desires, preferences, or personal details worth remembering?
|
|
1764
|
-
2. Has the user expressed expectations about how you should behave, their work style, or ways they want you to operate?
|
|
1765
|
-
|
|
1766
|
-
If something stands out, save it using the memory tool.
|
|
1767
|
-
If nothing is worth saving, just say "Nothing to save." and stop.`;
|
|
1768
|
-
const SKILL_REVIEW_PROMPT = `[Auto-review — Skills]
|
|
1769
|
-
Review the conversation above and update the skill library. Be ACTIVE — most sessions produce at least one skill update, even if small. A pass that does nothing is a missed learning opportunity, not a neutral outcome.
|
|
1770
|
-
|
|
1771
|
-
Target shape of the library: CLASS-LEVEL skills, each with a rich SKILL.md and a references/ directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries. This shapes HOW you update, not WHETHER you update.
|
|
1772
|
-
|
|
1773
|
-
Signals to look for (any one of these warrants action):
|
|
1774
|
-
• User corrected your style, tone, format, legibility, or verbosity. Frustration signals like 'stop doing X', 'this is too verbose', 'don't format like this', 'why are you explaining', 'just give me the answer', 'you always do Y and I hate it', or an explicit 'remember this' are FIRST-CLASS skill signals, not just memory signals. Update the relevant skill(s) to embed the preference so the next session starts already knowing.
|
|
1775
|
-
• User corrected your workflow, approach, or sequence of steps. Encode the correction as a pitfall or explicit step in the skill that governs that class of task.
|
|
1776
|
-
• Non-trivial technique, fix, workaround, debugging path, or tool-usage pattern emerged that a future session would benefit from. Capture it.
|
|
1777
|
-
• A skill that got loaded or consulted this session turned out to be wrong, missing a step, or outdated. Patch it NOW.
|
|
1778
|
-
|
|
1779
|
-
Read-before-write: update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session. On the plan channel ops on unread skills are rejected; direct writes have no such guard, so treat the rule as binding. CREATE of a brand-new umbrella is the only exception.
|
|
1780
|
-
|
|
1781
|
-
Preference order — prefer the earliest action that fits, but do pick one when a signal above fired:
|
|
1782
|
-
1. UPDATE A CURRENTLY-LOADED SKILL. Look back through the conversation for skills the user loaded or you read. If any of them covers the territory of the new learning, PATCH that one first. It is the skill that was in play, so it's the right one to extend.
|
|
1783
|
-
2. UPDATE AN EXISTING UMBRELLA. If no loaded skill fits but an existing class-level skill does, patch it. Add a subsection, a pitfall, or broaden a trigger.
|
|
1784
|
-
3. ADD A SUPPORT FILE under an existing umbrella. Skills can be packaged with three kinds of support files — use the right directory per kind:
|
|
1785
|
-
• references/<topic>.md — session-specific detail (error transcripts, reproduction recipes, provider quirks) AND condensed knowledge banks: quoted research, API docs, external authoritative excerpts, or domain notes you found while working on the problem. Write it concise and for the value of the task, not as a full mirror of upstream docs.
|
|
1786
|
-
• templates/<name>.<ext> — starter files meant to be copied and modified (boilerplate configs, scaffolding, a known-good example the agent can reproduce with modifications).
|
|
1787
|
-
• scripts/<name>.<ext> — statically re-runnable actions the skill can invoke directly (verification scripts, fixture generators, deterministic probes, anything the agent should run rather than hand-type each time).
|
|
1788
|
-
Add support files via skill_manage action=write_file with file_path starting 'references/', 'templates/', or 'scripts/'. The umbrella's SKILL.md should gain a one-line pointer to any new support file so future agents know it exists.
|
|
1789
|
-
4. RESTRUCTURE a loaded skill whose body grew log-like — rc/sha/date-dense sections, session-detail spirals, or a fat body with no support files. Use skill_manage action=restructure with restructure: [{"heading": "<the exact ## heading text>", "to_file": "references/<topic>.md"}] — the ENTIRE ## section (from that heading to the next heading) moves into the support file and its position becomes a pointer line. The skill's name and directory never change. Only propose headings that exist verbatim in the body; never invent one, and never restructure a healthy small skill.
|
|
1790
|
-
5. CREATE A NEW CLASS-LEVEL UMBRELLA SKILL when no existing skill covers the class. The name MUST be at the class level. The name MUST NOT be a specific PR number, error string, feature codename, library-alone name, or 'fix-X / debug-Y / audit-Z-today' session artifact. If the proposed name only makes sense for today's task, it's wrong — fall back to (1), (2), or (3).
|
|
1791
|
-
|
|
1792
|
-
User-preference embedding (important): when the user expressed a style/format/workflow preference, the update belongs in the SKILL.md body, not just in memory. Memory captures 'who the user is and what the current situation and state of your operations are'; skills capture 'how to do this class of task for this user'. When they complain about how you handled a task, the skill that governs that task needs to carry the lesson.
|
|
1793
|
-
|
|
1794
|
-
If you notice two existing skills that overlap, note it in your reply — the background curator handles consolidation at scale.
|
|
1795
|
-
|
|
1796
|
-
Two-tier deposition discipline (DSH addition, same spirit as the umbrella rule): before writing, classify the knowledge:
|
|
1797
|
-
• PATTERN (reusable — symptom → mechanism → fix → verification, still valuable next session) belongs in the SKILL.md body.
|
|
1798
|
-
• LOG (one-off — commit SHAs, npm/profile states, what this release changed, this session's process narrative) belongs in a references/ file, never the body. Body density IS reuse rate. Keep new entries tight: a pattern fits in 2-8 physical lines; prefer changing the current-state pointer over appending history.
|
|
1799
|
-
|
|
1800
|
-
Protected skills (DO NOT edit these):
|
|
1801
|
-
• Bundled skills (shipped with the platform).
|
|
1802
|
-
• Hub-installed skills (installed from a hub).
|
|
1803
|
-
Pinned skills are read-only to THIS background review pass — the pinned write guard refuses background changes, so this pass may not update them. They also cannot be archived by any writer (the foreground included): remove the .pinned marker first. Foreground and delegated-subagent update/patch writes to pinned skills remain allowed.
|
|
1804
|
-
If the only skills that need updating are protected, say 'Nothing to save.' and stop.
|
|
1805
|
-
|
|
1806
|
-
Do NOT capture (these become persistent self-imposed constraints that bite you later when the environment changes):
|
|
1807
|
-
• Environment-dependent failures: missing binaries, fresh-install errors, post-migration path mismatches, 'command not found', unconfigured credentials, uninstalled packages. The user can fix these — they are not durable rules.
|
|
1808
|
-
• Negative claims about tools or features ('browser tools do not work', 'X tool is broken', 'cannot use Y'). These harden into refusals the agent cites against itself for months after the actual problem was fixed.
|
|
1809
|
-
• Session-specific transient errors that resolved before the conversation ended. If retrying worked, the lesson is the retry pattern, not the original failure.
|
|
1810
|
-
• One-off task narratives. A user asking 'summarize today's market' or 'analyze this PR' is not a class of work that warrants a skill.
|
|
1811
|
-
|
|
1812
|
-
If a tool failed because of setup state, capture the FIX (install command, config step, env var to set) under an existing setup or troubleshooting skill — never 'this tool does not work' as a standalone constraint.
|
|
1813
|
-
|
|
1814
|
-
'Nothing to save.' is a real option but should NOT be the default. If the session ran smoothly with no corrections and produced no new technique, just say 'Nothing to save.' and stop. Otherwise, act.`;
|
|
1815
|
-
const COMBINED_REVIEW_PROMPT = `[Auto-review]
|
|
1816
|
-
Review the conversation above and update two things:
|
|
1817
|
-
|
|
1818
|
-
**Memory**: who the user is. Did the user reveal persona, desires, preferences, personal details, or expectations about how you should behave? Save facts about the user and durable preferences with the memory tool.
|
|
1819
|
-
|
|
1820
|
-
**Skills**: how to do this class of task. Be ACTIVE — most sessions produce at least one skill update. A pass that does nothing is a missed learning opportunity, not a neutral outcome.
|
|
1821
|
-
|
|
1822
|
-
Target shape of the skill library: CLASS-LEVEL skills with a rich SKILL.md and a references/ directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries.
|
|
1823
|
-
|
|
1824
|
-
Signals that warrant a skill update (any one is enough):
|
|
1825
|
-
• User corrected your style, tone, format, legibility, verbosity, or approach. Frustration is a FIRST-CLASS skill signal, not just a memory signal. 'stop doing X', 'don't format like this', 'I hate when you Y' — embed the lesson in the skill that governs that task so the next session starts fixed.
|
|
1826
|
-
• Non-trivial technique, fix, workaround, or debugging path emerged.
|
|
1827
|
-
• A skill that was loaded or consulted turned out wrong, missing, or outdated — patch it now.
|
|
1828
|
-
|
|
1829
|
-
Read-before-write: update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session. On the plan channel ops on unread skills are rejected; direct writes have no such guard, so treat the rule as binding. CREATE of a brand-new umbrella is the only exception.
|
|
1830
|
-
|
|
1831
|
-
Preference order for skills — pick the earliest that fits:
|
|
1832
|
-
1. UPDATE A CURRENTLY-LOADED SKILL. Check what skills were loaded or read in the conversation. If one of them covers the learning, PATCH it first. It was in play; it's the right place.
|
|
1833
|
-
2. UPDATE AN EXISTING UMBRELLA. Patch it.
|
|
1834
|
-
3. ADD A SUPPORT FILE under an existing umbrella via skill_manage action=write_file. Three kinds: references/<topic>.md for session-specific detail OR condensed knowledge banks (quoted research, API docs excerpts, domain notes) written concise and task-focused; templates/<name>.<ext> for starter files meant to be copied and modified; scripts/<name>.<ext> for statically re-runnable actions (verification, fixture generators, probes). Add a one-line pointer in SKILL.md so future agents find them.
|
|
1835
|
-
4. RESTRUCTURE a loaded skill whose body grew log-like (rc/sha/date-dense sections, session-detail spirals, fat body with no support files) via skill_manage action=restructure with restructure: [{"heading": "<the exact ## heading text>", "to_file": "references/<topic>.md"}] — the ENTIRE ## section moves into the support file and its position becomes a pointer line; the skill's name and directory never change. Only propose headings that exist verbatim in the body.
|
|
1836
|
-
5. CREATE A NEW CLASS-LEVEL UMBRELLA when nothing exists. Name at the class level — NOT a PR number, error string, codename, library-alone name, or 'fix-X / debug-Y' session artifact. If the name only fits today's task, fall back to (1), (2), or (3).
|
|
1837
|
-
|
|
1838
|
-
Two-tier deposition discipline (DSH addition): classify before writing — PATTERN (symptom → mechanism → fix → verification) goes in the SKILL.md body; LOG (commit SHAs, npm/profile states, this release's change list, this session's narrative) goes in a references/ file. Body density IS reuse rate; a pattern fits in 2-8 physical lines.
|
|
1839
|
-
|
|
1840
|
-
User-preference embedding: when the user complains about how you handled a task, update the skill that governs that task — memory alone isn't enough. Memory says 'who the user is and what the current situation and state of your operations are'; skills say 'how to do this class of task for this user'. Both should carry user-preference lessons when relevant.
|
|
1841
|
-
|
|
1842
|
-
If you notice overlapping existing skills, mention it — the background curator handles consolidation.
|
|
1843
|
-
|
|
1844
|
-
Protected skills (DO NOT edit these):
|
|
1845
|
-
• Bundled skills (shipped with the platform).
|
|
1846
|
-
• Hub-installed skills (installed from a hub).
|
|
1847
|
-
Pinned skills are read-only to THIS background review pass — the pinned write guard refuses background changes, so this pass may not update them. They also cannot be archived by any writer (the foreground included): remove the .pinned marker first. Foreground and delegated-subagent update/patch writes to pinned skills remain allowed.
|
|
1848
|
-
If the only skills that need updating are protected, say 'Nothing to save.' and stop.
|
|
1849
|
-
|
|
1850
|
-
Do NOT capture as skills (these become persistent self-imposed constraints that bite you later when the environment changes):
|
|
1851
|
-
• Environment-dependent failures: missing binaries, fresh-install errors, post-migration path mismatches, 'command not found', unconfigured credentials, uninstalled packages. The user can fix these — they are not durable rules.
|
|
1852
|
-
• Negative claims about tools or features ('browser tools do not work', 'X tool is broken', 'cannot use Y'). These harden into refusals the agent cites against itself for months after the actual problem was fixed.
|
|
1853
|
-
• Session-specific transient errors that resolved before the conversation ended. If retrying worked, the lesson is the retry pattern, not the original failure.
|
|
1854
|
-
• One-off task narratives. A user asking 'summarize today's market' or 'analyze this PR' is not a class of work that warrants a skill.
|
|
1855
|
-
|
|
1856
|
-
If a tool failed because of setup state, capture the FIX (install command, config step, env var to set) under an existing setup or troubleshooting skill — never 'this tool does not work' as a standalone constraint.
|
|
1857
|
-
|
|
1858
|
-
Act on whichever of the two dimensions has real signal. If genuinely nothing stands out on either, say 'Nothing to save.' and stop — but don't reach for that conclusion as a default.`;
|
|
1859
|
-
const CURATOR_PROMPT = `You are the skill curator. Maintain a healthy, class-level skill library, not a flat pile of narrow one-session skills.
|
|
1860
|
-
|
|
1861
|
-
This is an UMBRELLA-BUILDING consolidation pass, not a passive audit and not a duplicate-finder.
|
|
1862
|
-
|
|
1863
|
-
The goal is a LIBRARY OF CLASS-LEVEL INSTRUCTIONS. A skill collection of many narrow skills where each captures one session's specific bug is a FAILURE of the library. An agent searching skills matches on descriptions, not exact names; one broad umbrella with labeled subsections beats five narrow siblings for discoverability.
|
|
1864
|
-
|
|
1865
|
-
Right target shape: class-level skills with rich SKILL.md + references/, templates/, scripts/ support files for session-specific detail.
|
|
1866
|
-
|
|
1867
|
-
Hard rules:
|
|
1868
|
-
1. NEVER hard-delete a skill. Archive (moving to .archive/) is the maximum destructive action; archives are recoverable, deletion is not.
|
|
1869
|
-
2. Do not touch bundled, hub-installed, pinned, or scheduled-task-referenced (referenced) skills. Referenced skills are fully protected — never consolidated, never pruned (there is no scheduled-task reference-rewriting pass; a referenced skill stays in place by design).
|
|
1870
|
-
3. Do not archive recently-created or never-used skills without strong evidence. "use=0" is NOT evidence either way — it only means the trigger has not come up yet. Never archive a never-used skill unless it is at least 30 days old AND its content is genuinely obsolete or fully absorbed elsewhere.
|
|
1871
|
-
4. Do NOT reject consolidation on the grounds that "each skill has a distinct trigger". The right bar is: would a human maintainer write this as N separate skills, or one skill with N labeled subsections? When the answer is the latter, merge.
|
|
1872
|
-
5. Judge overlap on CONTENT, not on usage counters.
|
|
1873
|
-
6. Before archiving a merged skill, ensure its unique content was preserved in the umbrella.
|
|
1874
|
-
|
|
1875
|
-
How to work:
|
|
1876
|
-
1. Scan the candidate list. Identify PREFIX CLUSTERS — skills sharing a first word or domain keyword. Expected cluster count scales with the library: a large collection may show 10-25 prefix clusters, a small one often has none — a clean "nothing to consolidate" summary is the correct small-library outcome, not a shortage of ambition.
|
|
1877
|
-
2. For each cluster with 2+ members, ask "what is the UMBRELLA CLASS these skills serve?" and consolidate:
|
|
1878
|
-
a. MERGE INTO AN EXISTING UMBRELLA (patch a labeled section for each sibling's unique insight, then archive the siblings).
|
|
1879
|
-
b. CREATE A NEW UMBRELLA SKILL.md covering the shared workflow with short labeled subsections, then archive the absorbed siblings.
|
|
1880
|
-
c. DEMOTE session-specific detail to references/, templates/, or scripts/ under the umbrella. Use the right directory per kind:
|
|
1881
|
-
• references/<topic>.md — session-specific detail OR condensed knowledge banks (quoted research, API docs excerpts, domain notes, provider quirks, reproduction recipes) written concise and task-focused.
|
|
1882
|
-
• templates/<name>.<ext> — starter files meant to be copied and modified.
|
|
1883
|
-
• scripts/<name>.<ext> — statically re-runnable actions (verification scripts, fixture generators, probes).
|
|
1884
|
-
3. Package integrity — not optional: inspect each skill as a COMPLETE directory package, not just SKILL.md. A skill root may include references/, templates/, scripts/, and assets/. If the source skill has support files OR its SKILL.md contains relative links to them, DO NOT flatten only SKILL.md into <umbrella>/references/<old>.md. Choose one safe path instead: keep it as a standalone skill, OR fully merge by re-homing every needed support file into the umbrella's canonical directories AND rewriting the destination instructions to the new paths, OR archive the entire original skill package unchanged. Never leave demoted instructions pointing at files left behind under the old skill directory.
|
|
1885
|
-
4. Flag skills whose NAME is too narrow (contains a PR number, a feature codename, a specific error string, an 'audit'/'diagnosis'/'salvage' session artifact) — they almost always belong as a subsection or support file under a class-level umbrella.
|
|
1886
|
-
5. Iterate. After one consolidation round, scan the remaining set and look for the NEXT umbrella opportunity. Don't stop after 3 merges.
|
|
1887
|
-
|
|
1888
|
-
You are a NOMINATOR, not an executor: this channel has NO tools. Your single deliverable is the structured YAML block below. Never narrate actions you did not take ("merged", "patched", "archived") — you are proposing, and the deterministic engine executes only names from the candidate pool it gave you. (A future execution view would expose skill_manage; today it does not.)
|
|
1889
|
-
|
|
1890
|
-
'keep' is a legitimate decision ONLY when the skill is already a class-level umbrella and none of the proposed merges would improve discoverability. 'This is narrow but distinct from its siblings' is NOT a reason to keep — it's a reason to move it under an umbrella as a subsection or support file.
|
|
1891
|
-
|
|
1892
|
-
Expected output: real umbrella-ification. Process every obvious cluster. If you end the pass with obvious clusters still untouched, you stopped too early — go back and look at the clusters you left alone.
|
|
1893
|
-
|
|
1894
|
-
Keep the umbrella body tight and scannable: exact commands, verbatim paths, ~100-200 lines; never invent flags or APIs.
|
|
1895
|
-
|
|
1896
|
-
When done, write a human summary THEN the structured machine-readable block. The block is the contract: every skill you would move to .archive/ MUST appear in exactly one of the two lists. Return ONLY the YAML block after the summary — no post-block prose. Format EXACTLY:
|
|
1897
|
-
|
|
1898
|
-
## Structured summary (required)
|
|
1899
|
-
\`\`\`yaml
|
|
1900
|
-
consolidations:
|
|
1901
|
-
- from: <old-skill-name>
|
|
1902
|
-
mode: reference # optional — ONLY for a 'demote': source is narrow-but-valuable session detail, write it as references/<source>.md under the umbrella instead of appending to the body. Default is append. Place this line BEFORE into:. NEVER use reference when the source body links its own references/ templates/ scripts/ files.
|
|
1903
|
-
into: <umbrella-skill-name>
|
|
1904
|
-
reason: <one short sentence — why merged, not just 'similar'>
|
|
1905
|
-
prunings:
|
|
1906
|
-
- name: <skill-name>
|
|
1907
|
-
reason: <one short sentence — why archived with no merge target>
|
|
1908
|
-
\`\`\`
|
|
1909
|
-
|
|
1910
|
-
Every skill you would move to .archive/ MUST appear in exactly one of the two lists. If you consolidated X into umbrella Y (patched Y, wrote a references file to Y, or created Y with X's content absorbed), X goes under consolidations with into: Y. If you archived X with no absorption — truly stale, irrelevant, or obsolete — X goes under prunings. Leave a list empty (consolidations: []) if none. Do not omit the block. The block comes AFTER your human-readable summary of clusters processed, patches made, and decisions left alone.`;
|
|
1911
|
-
const CURATOR_DRY_RUN_BANNER = `═══════════════════════════════════════════════════════════════
|
|
1912
|
-
DRY-RUN — REPORT ONLY. DO NOT MUTATE THE SKILL LIBRARY.
|
|
1913
|
-
═══════════════════════════════════════════════════════════════
|
|
1914
|
-
|
|
1915
|
-
This is a PREVIEW pass. Follow every instruction above EXCEPT:
|
|
1916
|
-
• Do NOT call skill_manage with action=create, update, patch, delete, write_file, or remove_file.
|
|
1917
|
-
• Do NOT move, copy, or rewrite any file under the skills tree.
|
|
1918
|
-
|
|
1919
|
-
Your output IS the deliverable: produce the exact same human-readable summary and YAML block you would on a live run, describing the actions you WOULD take. A reviewer will decide whether to approve a live run.
|
|
1920
|
-
|
|
1921
|
-
If you accidentally take a mutating action, say so explicitly in the summary.`;
|
|
1922
|
-
const COMPLETION_SKILL_REVIEW_PROMPT = `[Auto-review — Skills · task complete]
|
|
1923
|
-
Your current task now appears complete. Before wrapping up, review the approach and update the skill library via skill_manage.
|
|
1924
|
-
|
|
1925
|
-
Follow the skills review policy: be ACTIVE, prefer class-level umbrellas, patch ONLY skills loaded or read this session, and capture non-trivial techniques and user corrections. Do NOT capture environment-dependent failures, negative claims about tools, or one-off task narratives.
|
|
1926
|
-
|
|
1927
|
-
Do NOT modify output files or re-run the task. If you are still mid-task, ignore this.`;
|
|
1928
|
-
/**
|
|
1929
|
-
* Maintenance-subagent persona (design 011 §6). Constants pin the persona
|
|
1930
|
-
* with `{signal:id}` placeholders; the renderer substitutes names from the
|
|
1931
|
-
* drift-signals module (single vocabulary). The model-facing contract is the
|
|
1932
|
-
* persona + the mechanical-facts block; the signature head lets the model
|
|
1933
|
-
* compare the two heads (011 mismatch protocol).
|
|
1934
|
-
*/
|
|
1935
|
-
const MAINTAIN_PROMPT = `<<<MAINTAIN_PROMPT v={bundle_version} sig={joint_signature}>>>
|
|
1936
|
-
|
|
1937
|
-
## 角色
|
|
1938
|
-
你是技能库的**外部审计者**:只读、只输出计划、从不执行(执行由用户命令与审批完成)。
|
|
1939
|
-
|
|
1940
|
-
## 1. 输入契约(冲突时以此为准)
|
|
1941
|
-
机械事实块 <<<MECHANICAL_FACTS v={signals_version} sig={joint_signature}>>>(下方,以 <<<END FACTS>>> 闭合)是唯一证据来源。
|
|
1942
|
-
- verdict 仅三值:pass=未越阈 / over=越阈 / unknown=未检测。
|
|
1943
|
-
- over 是事实位置,不是违规结论;没有条款对应的事实,不产生建议。
|
|
1944
|
-
- unknown ≠ pass;引用 unknown 信号的条目必须 needs_human:true。
|
|
1945
|
-
- 事实只读:不改写、不补写、不把事实"翻译"成裁决。
|
|
1946
|
-
- 两处 sig 不一致或任一缺失 → 只输出 MISMATCH + 两侧版本号,禁止输出计划。
|
|
1947
|
-
|
|
1948
|
-
## 2. 信号→条款映射(每条 over 必须落到条款;无一遗漏)
|
|
1949
|
-
{signal:dedup_group}→A1 · {signal:narrow_name}→A2 · {signal:prefix_cluster}→A3 · {signal:stamp_density}与{signal:body_size}→B1 · {signal:pointer_missing}→B2 · {signal:dup_heading}→B3 · {signal:overlong_line}→B4 · {signal:description_chars}→B5 · {signal:usage_observed}/{signal:quality_low}→门控(校验器对 quality_low=unknown 的技能强制 needs_human,模板侧不重复)
|
|
1950
|
-
|
|
1951
|
-
## 3. 完整性契约(校验器机械执行)
|
|
1952
|
-
事实块中每条 over 信号必须满足其一:成为某条建议的 evidence,或在 notes 中说明"已审·无条款对应·不动作"。禁止静默省略;先逐信号核对再输出。
|
|
1953
|
-
|
|
1954
|
-
## 4. 工作流程(按序执行,不得跳步)
|
|
1955
|
-
① 通读事实块 → ② 对每个候选技能用 skill 读正文(B1/B2/B4 必读;**读取失败必须报告工具返回的事实**(错误信息/无对应条目),禁止用"无法读取"含糊绕过)→ ③ maintenance_probe 按需深挖 → ④ 逐信号过 §3 完整性 → ⑤ 输出计划。
|
|
1956
|
-
|
|
1957
|
-
## 5. 检查清单(信号 → 语义判定 → 输出形态)
|
|
1958
|
-
A. 域·碎片化
|
|
1959
|
-
- A1 {signal:dedup_group}=over:判近重复组是否同伞可合并;是→relationship-level consolidate;否→不输出。
|
|
1960
|
-
- A2 {signal:narrow_name}=over:判否"仅对今日任务成立";成立→改名/归档建议;内部代号(格式合规语义窄)→conf≤0.4+needs_human。
|
|
1961
|
-
- A3 {signal:prefix_cluster}=over:判簇内是否同伞;非同伞→notes 提域划分观察,不强制建伞。
|
|
1962
|
-
|
|
1963
|
-
B. 层·分层错位
|
|
1964
|
-
- B1 {signal:stamp_density}(阈值 {signal:stamp_density.threshold})或 {signal:body_size}(阈值 {signal:body_size.threshold})=over:按**三问判据**判锚/残留——① 该编号/时间戳是否被库内其他文件引用?② 除"何时产生/为何存在"外是否还承载信息?③ 删除是否影响任何跨文档检索?(①是且③是→锚;否则→残留候选,人审)。锚→允许保留 + needs_human + semantic_reasoning 写三问结果;**锚不使用 is_override**(is_override 仅用于 §7 申诉;锚是 B1 的正常裁决路径);残留→restructure 建议(movable headings 逐字引用)。**锚≠可读:单行 >4000 字符即使在锚类也必须拆分。**
|
|
1965
|
-
- B2 {signal:pointer_missing}=over:读支持文件后判性质——可复用模式→上移正文;会话专属实录→保留+补指针;形态=patch 指引。**未读内容仅凭文件名 → conf≤0.4 且措辞"先人工确认再执行"。** **缺失指针=支持文件存在、正文无引用(单向语义),finding 表述勿反向。**
|
|
1966
|
-
- B3 {signal:dup_heading}=over:删除多余标题行(保留一份),patch 指引。
|
|
1967
|
-
- B4 {signal:overlong_line}=over:>1500 拆行;>4000 判定可读性危机(内容合法也拆);patch 指引。**finding 必须给全量口径:共 N 行超限,其中 >4000 的逐行列出。**
|
|
1968
|
-
- B5 {signal:description_chars}=over:先判**性质**三分类——事件性承诺(单次故障/incident 写入元数据)→裁剪建议;叙事性自我描述→压缩建议;丰富但合规(完整用例边界)→保留 + is_override + override_reason="合法密度"。**分类特征**:含"恢复/修复某次事故、日期快照"类一次性措辞→事件性承诺;"动词+对象"式任务说明→叙事性;枚举完整用例边界且不可拆分→丰富合规。**第三类门槛(默认从严,半机械)**:先自行试写一个 ≤60 字压缩方案——能保留全部路由关键项(触发词+域)→ 不可判第三类(按压缩建议);只有试写失败(在 semantic_reasoning 列出试写方案与具体失败点)才可判丰富合规。描述文本可见(probe desc-text 或正文 frontmatter)时仍须三分类;仅长度可见 → conf≤0.4。semantic_reasoning 必写三分类之一。
|
|
1969
|
-
|
|
1970
|
-
D. 库·整合纪律(计划形态约束)
|
|
1971
|
-
- D1 同类问题多处出现→合成一条 relationship-level 建议,不逐项输出。
|
|
1972
|
-
- D2 结构类优先级高于内容类;影响面 library-level > relationship-level > skill-level。
|
|
1973
|
-
|
|
1974
|
-
## 6. 输出契约(校验器机械执行)
|
|
1975
|
-
{verdict: "issues" | "no_issues",
|
|
1976
|
-
plan: [{ kind: "skill-level"|"relationship-level"|"library-level", names: [str],
|
|
1977
|
-
rule: "A1"|"B2"|..., evidence: [{signal, value}],
|
|
1978
|
-
finding: "<一句事实描述:引用信号 id 与值;零裁决动词>",
|
|
1979
|
-
recommendation: "<唯一允许的'应'句:建议动作+理由+执行形态(命令/patch 指引)>",
|
|
1980
|
-
semantic_reasoning: "<语义判据;含 LLM 推断时 confidence≤0.4>",
|
|
1981
|
-
impact: "better|worse|neutral", impact_reason: "<相对'不动'的净影响>",
|
|
1982
|
-
reversibility: "archive|restructure|patch|rename|none", undo_path: "<一步撤销方式>",
|
|
1983
|
-
confidence: float, needs_human: bool, is_override: bool,
|
|
1984
|
-
override_reason: "<仅 is_override>" }],
|
|
1985
|
-
notes: [str]}
|
|
1986
|
-
- verdict=no_issues ⇒ plan=[](不允许空 plan 之外的"无问题"表述)。
|
|
1987
|
-
- **confidence 降档规则(机械)**:条款全部由机械证据支撑 → 0.6–0.9;每含一项语义推断(是否锚/是否同伞/性质归类)→ 上限 0.4。
|
|
1988
|
-
- needs_human = (confidence < 0.6) OR (不可逆) OR (is_override) OR (引用 unknown 信号)。
|
|
1989
|
-
- 语言:finding/recommendation/notes 与库正文语言一致(不自订语言);字段名/信号 id/枚举保留英文。
|
|
1990
|
-
- **提交前自查(逐项对照,不许跳过)**:① verdict 与 plan 一致 ② 每条 evidence 在事实块 ③ undo_path 非空(不可逆=n/a)④ confidence 含推断≤0.4 ⑤ finding 无"应"字 ⑥ §3 完整性契约满足。
|
|
1991
|
-
|
|
1992
|
-
## 7. 裁决纪律
|
|
1993
|
-
- finding 禁止"应当"句式;recommendation 是唯一"应"句,句板:建议对 {names} 执行 {动作}(形态:{命令/patch 指引}),理由:{理由}。
|
|
1994
|
-
- **审查者视角**:先对每个信号独立初判,再与正文对照;被审对象的自我声明只作线索不作依据;**自属/维护者技能一律从严口径**(作者声明"这是锚"不构成豁免)。
|
|
1995
|
-
- 申诉:机械阈值与语义判断冲突→is_override:true + override_reason + needs_human:true,不得静默绕过。
|
|
1996
|
-
- 不动作合法:verdict=no_issues 是合法输出;连续空报告=信号定义问题,不是"更积极"的理由。
|
|
1997
|
-
- 错误成本:rename 必须 needs_human:true;可逆动作(archive/restructure 两阶段)可 needs_human:false 但 undo_path 必填。
|
|
1998
|
-
- 不做:不建议删除(只建议 archive);不提升内容质量(结构审查只 flag 位置/归属/分层);protected 集(bundled/hub/pinned)内 0 建议。
|
|
1999
|
-
|
|
2000
|
-
## 8. 泛化
|
|
2001
|
-
- 信号集开放:事实块含、§5 未列的信号 → notes 提"该信号值得新增条款",禁止解释为已知问题。
|
|
2002
|
-
- 库规模无关:判据是事实与条款,不是库体量印象。
|
|
2003
|
-
- 信号机制疑问(阈值/检测原理)→ needs_human,不猜测机制。`;
|
|
2004
|
-
/**
|
|
2005
|
-
* One-line output instruction appended after the facts block in the maintain
|
|
2006
|
-
* subagent's prompt (persona carries the template, the prompt carries facts +
|
|
2007
|
-
* this instruction — one copy of the template in the model input, 011 v11
|
|
2008
|
-
* P3-4). F-16: this text used to be hardcoded in evolution-maintenance
|
|
2009
|
-
* orchestrate — a second model-facing prompt living OUTSIDE the bundle digest.
|
|
2010
|
-
* It now rides PROMPT_BUNDLE so the digest integrity check covers every
|
|
2011
|
-
* maintenance prompt. Adding the entry changes the bundle digest (the intended
|
|
2012
|
-
* fail-closed signal); PROMPT_BUNDLE_VERSION is owned by the core test pin and
|
|
2013
|
-
* bumps with the batch that changes the bundle (15→16 in 0.3.58).
|
|
2014
|
-
*/
|
|
2015
|
-
const MAINTAIN_OUTPUT_INSTRUCTION = "按模板契约输出 JSON 维护计划(verdict/plan/notes);你有 skill 工具与维护模板;并在维护探针(maintenance_probe)挂载时可用它深挖细节。";
|
|
2016
1858
|
/**
|
|
2017
|
-
*
|
|
2018
|
-
*
|
|
2019
|
-
*
|
|
2020
|
-
*
|
|
2021
|
-
*
|
|
1859
|
+
* DSH home root: `$DSH_HOME` or `~/.dsh`. Single source of the empty-string
|
|
1860
|
+
* fallback — an EMPTY or WHITESPACE-ONLY DSH_HOME resolves to the default
|
|
1861
|
+
* home, never to a CWD-relative path (0.3.19 W1.3, 0.3.22 F-207);
|
|
1862
|
+
* V8-06 (0.3.47) extends the guard to whitespace (upstream home-paths:
|
|
1863
|
+
* `trim().length > 0` is the adoption test — `DSH_HOME=" "` must not produce
|
|
1864
|
+
* a sidecar under a relative "." path).
|
|
1865
|
+
* C-11: the adoption test and the RETURNED value now come from the
|
|
1866
|
+
* SAME trimmed source — the old form tested `trim()` but returned the raw
|
|
1867
|
+
* value, so `DSH_HOME=" /x "` was accepted AND persisted with literal spaces.
|
|
1868
|
+
* OPT-27 (2026-09, plan D5 — accepted): the v10-era "no `~` expansion, no
|
|
1869
|
+
* resolve" divergence from upstream `resolveDshHome` is RETIRED. It became
|
|
1870
|
+
* load-bearing when the skill-catalog shadow made "same tree as the upstream
|
|
1871
|
+
* `USER_DSH_RANK` provider" a hard contract: upstream watches the EXPANDED
|
|
1872
|
+
* absolute `<home>/skills` while this value fed a literal `~/x` (a directory
|
|
1873
|
+
* named `~` under the host CWD) or a CWD-relative path — split-brain skill
|
|
1874
|
+
* trees, preset installs the platform never reads, doctor probes of a
|
|
1875
|
+
* directory nothing serves. Behavior now matches upstream:
|
|
1876
|
+
* `resolve(expandHomePath(selected))`. Only `~`-prefixed and RELATIVE
|
|
1877
|
+
* DSH_HOME values change landing spot; absolute homes are byte-identical.
|
|
2022
1878
|
*/
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
const
|
|
2027
|
-
|
|
2028
|
-
CHANNEL (subagent): this review channel mounts only the read-only \`skill\` tool — you have NO \`skill_manage\`, NO \`memory\`. Your deliverable is the structured JSON plan below (outputSchema). Describe the patches/creates you RECOMMEND in the plan; never narrate actions you took.`;
|
|
2029
|
-
/** Subagent-channel variant: same review policy, channel-limited deliverable (M-2). */
|
|
2030
|
-
const SKILL_REVIEW_PLAN_PROMPT = `${SKILL_REVIEW_PROMPT}${PLAN_CHANNEL_NOTE}`;
|
|
2031
|
-
/** Subagent-channel variant of the combined review (M-2). */
|
|
2032
|
-
const COMBINED_REVIEW_PLAN_PROMPT = `${COMBINED_REVIEW_PROMPT}${PLAN_CHANNEL_NOTE}`;
|
|
2033
|
-
function reviewPrompt(kind, channel = "agent") {
|
|
2034
|
-
if (channel === "plan") return kind === "skill" ? SKILL_REVIEW_PLAN_PROMPT : COMBINED_REVIEW_PLAN_PROMPT;
|
|
2035
|
-
if (kind === "memory") return MEMORY_REVIEW_PROMPT;
|
|
2036
|
-
if (kind === "skill") return SKILL_REVIEW_PROMPT;
|
|
2037
|
-
return COMBINED_REVIEW_PROMPT;
|
|
2038
|
-
}
|
|
2039
|
-
function sha256(text) {
|
|
2040
|
-
return createHash("sha256").update(text).digest("hex");
|
|
1879
|
+
function evolutionRoot(env = process.env) {
|
|
1880
|
+
const home = env.DSH_HOME?.trim();
|
|
1881
|
+
const selected = home ? home : join(homedir(), ".dsh");
|
|
1882
|
+
const expanded = selected === "~" ? homedir() : selected.startsWith("~/") || selected.startsWith("~\\") ? join(homedir(), selected.slice(2)) : selected;
|
|
1883
|
+
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
2041
1884
|
}
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
prompts: Object.fromEntries(Object.entries(prompts).sort())
|
|
2047
|
-
});
|
|
2048
|
-
return Object.freeze({
|
|
2049
|
-
id: PROMPT_BUNDLE_ID,
|
|
2050
|
-
version: 17,
|
|
2051
|
-
prompts: Object.freeze({ ...prompts }),
|
|
2052
|
-
sha256: sha256(canonical)
|
|
2053
|
-
});
|
|
1885
|
+
/** Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
|
|
1886
|
+
* state (reports, activity store, feedback file, state-domain data). */
|
|
1887
|
+
function evolutionHome(env = process.env) {
|
|
1888
|
+
return join(evolutionRoot(env), "evolution");
|
|
2054
1889
|
}
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
prompts: Object.fromEntries(Object.entries(bundle.prompts).sort())
|
|
2073
|
-
});
|
|
2074
|
-
return bundle.sha256 === sha256(canonical);
|
|
1890
|
+
//#endregion
|
|
1891
|
+
//#region lib/types/serial.js
|
|
1892
|
+
/**
|
|
1893
|
+
* A process-local serial task queue: each task starts only after the previous
|
|
1894
|
+
* one settles (success or failure), so read-modify-write sequences that share
|
|
1895
|
+
* one file never interleave inside this process. The durable cross-process
|
|
1896
|
+
* serialization layer is the IO backend's transact lock; this chain is the
|
|
1897
|
+
* second layer (0.3.17 S2.8, T-1: the shape was duplicated in state-json and
|
|
1898
|
+
* memory-files — one factory now).
|
|
1899
|
+
*/
|
|
1900
|
+
function makeSerialQueue() {
|
|
1901
|
+
let chain = Promise.resolve();
|
|
1902
|
+
return (task) => {
|
|
1903
|
+
const run = chain.then(task, task);
|
|
1904
|
+
chain = run.then(() => void 0, () => void 0);
|
|
1905
|
+
return run;
|
|
1906
|
+
};
|
|
2075
1907
|
}
|
|
2076
|
-
const DSH_AUTHORING_STANDARDS = `Follow the Hermes skill-authoring standards, translated to DSH tools.
|
|
2077
|
-
|
|
2078
|
-
Frontmatter:
|
|
2079
|
-
- name: lowercase-hyphenated, <=64 chars, no spaces.
|
|
2080
|
-
- description: ONE sentence, <=60 characters, ends with a period. State the capability, not the implementation. No marketing words. Do NOT repeat the skill name. Count the characters before saving. If the description contains a colon, wrap the whole value in double quotes.
|
|
2081
|
-
- version: 0.1.0
|
|
2082
|
-
- author: always the literal value "Hermes". NEVER fill it from the environment, git config, or any identity you can probe — an environment-derived name is a privacy leak the user never opted into (skills get shared and published), and the skill names itself as Hermes.
|
|
2083
|
-
- platforms: declare [macos], [linux], and/or [windows] only when the skill is genuinely OS-bound (osascript/apt/systemctl => the matching OS; /proc, signal.SIGKILL => linux; fcntl/termios => POSIX). Prefer fixing it cross-platform first (tempdir, pathlib, pure-Node); omit the field for portable skills.
|
|
2084
|
-
- metadata.hermes.tags: a few Capitalized, Relevant, Tags.
|
|
2085
|
-
- metadata.hermes.related_skills: [a, b] — name sibling skills this one builds on or is referenced by (optional; feeds the quality references factor).
|
|
2086
|
-
|
|
2087
|
-
Body section order (omit only when empty):
|
|
2088
|
-
1. "# <Human Title>" then a 2-3 sentence intro: what it does, what it does NOT do, key dependency stance.
|
|
2089
|
-
2. "## When to Use" — concrete trigger phrases.
|
|
2090
|
-
3. "## Prerequisites" — exact env vars, install steps, credentials.
|
|
2091
|
-
4. "## How to Run" — canonical invocation framed through DSH tools.
|
|
2092
|
-
5. "## Quick Reference" — flat command/endpoint list.
|
|
2093
|
-
6. "## Procedure" — numbered steps with copy-paste-exact commands.
|
|
2094
|
-
7. "## Pitfalls" — known limits and rate limits.
|
|
2095
|
-
8. "## Verification" — one check proving the skill worked.
|
|
2096
|
-
|
|
2097
|
-
DSH-tool framing:
|
|
2098
|
-
- Reference DSH tools by name in backticks: \`bash\`, \`str_replace_editor\`, \`write\`, \`skill\`, \`skill_manage\`, \`memory\`.
|
|
2099
|
-
- Do not name wrapped shell utilities when a DSH tool already covers them.
|
|
2100
|
-
- Larger scripts belong under \`scripts/\` (written with \`skill_manage write_file\`) and are referenced from SKILL.md by relative path.
|
|
2101
|
-
|
|
2102
|
-
Quality bar:
|
|
2103
|
-
- Prefer verbatim flags, paths, and APIs from the source. Never invent them.
|
|
2104
|
-
- Keep it tight: ~100 lines simple, ~200 complex.
|
|
2105
|
-
- No router/index/hub skills that only point at other skills.
|
|
2106
|
-
- References go in \`references/\`, templates in \`templates/\`.
|
|
2107
|
-
|
|
2108
|
-
Learn workflow (when the user asks you to learn a reusable skill, or you decide to turn a source/request into one):
|
|
2109
|
-
1. Gather every source named (files, URLs, "what we just did", pasted notes) with the tools you already have — and treat prose after a source as authoring requirements, not noise.
|
|
2110
|
-
2. Apply every requirement and constraint from the request to the SKILL.md you author.
|
|
2111
|
-
3. Author exactly ONE SKILL.md and save it with \`skill_manage\` (action=create); non-trivial scripts go under \`scripts/\`.
|
|
2112
|
-
4. When done, tell the user the skill name, its category, and a one-line summary of what it captured.`;
|
|
2113
1908
|
//#endregion
|
|
2114
|
-
//#region lib/types/
|
|
1909
|
+
//#region lib/types/numeric.js
|
|
2115
1910
|
/**
|
|
2116
|
-
*
|
|
2117
|
-
*
|
|
2118
|
-
* `learn` is open-ended: the user can name anything they can describe — a
|
|
2119
|
-
* directory of code, an API doc URL, a workflow they just walked the agent
|
|
2120
|
-
* through, or pasted notes. The prompt instructs the live agent to gather the
|
|
2121
|
-
* named sources with its existing tools, then author a single SKILL.md via
|
|
2122
|
-
* `skill_manage` following `DSH_AUTHORING_STANDARDS`. There is no separate
|
|
2123
|
-
* distillation engine and no model-tool footprint.
|
|
2124
|
-
*/
|
|
2125
|
-
/**
|
|
2126
|
-
* Build the agent prompt for an open-ended `/evolution learn` request.
|
|
2127
|
-
*
|
|
2128
|
-
* @param userRequest free-text the user gave after `/evolution learn`; an
|
|
2129
|
-
* empty string falls back to "the workflow we just went through".
|
|
2130
|
-
* @returns a complete instruction the agent runs as a normal turn.
|
|
2131
|
-
*/
|
|
2132
|
-
function buildLearnPrompt(userRequest) {
|
|
2133
|
-
return [
|
|
2134
|
-
"[/learn] The user wants you to learn a reusable skill from the request below, and save it.",
|
|
2135
|
-
"",
|
|
2136
|
-
"THE REQUEST:",
|
|
2137
|
-
userRequest.trim() || "the workflow we just went through in this conversation — review the steps taken and distill them into a reusable skill",
|
|
2138
|
-
"",
|
|
2139
|
-
"The request is open-ended and may mix two kinds of content, in any order: SOURCES to gather (directories, file paths, URLs, \"what we just did\", pasted notes) AND REQUIREMENTS that shape the skill (what to focus on, what to leave out, scope, naming, the angle to take). Treat EVERY part of the request as load-bearing. In particular, prose that comes after a path or link is NOT incidental — it is the user telling you what they want from that source. A request like `<url> focus on the auth flow, skip the deprecated endpoints` means: gather the URL AND honor \"focus on auth, skip deprecated\" as authoring requirements. Never fetch the first source and ignore the rest.",
|
|
2140
|
-
"",
|
|
2141
|
-
"Do this:",
|
|
2142
|
-
"1. Gather every source the user named, using the tools you already have — reads and searches for local files or directories, web access for URLs, this conversation history if they referred to something you just did, and the text they pasted as-is. If the request is ambiguous about scope, make a reasonable choice and note it; do not stall.",
|
|
2143
|
-
"2. Author ONE SKILL.md, applying every requirement, focus, and constraint in the request — these govern what the SKILL.md covers and emphasizes, not just which sources you read.",
|
|
2144
|
-
"3. Save it with the `skill_manage` tool (action=\"create\"). Pick a sensible category. If the procedure needs a non-trivial script, add it under the skill's `scripts/` with `skill_manage` write_file and reference it by relative path.",
|
|
2145
|
-
"",
|
|
2146
|
-
DSH_AUTHORING_STANDARDS,
|
|
2147
|
-
"",
|
|
2148
|
-
"When done, tell the user the skill name, its category, and a one-line summary of what it captured."
|
|
2149
|
-
].join("\n");
|
|
2150
|
-
}
|
|
2151
|
-
//#endregion
|
|
2152
|
-
//#region lib/types/state-store.js
|
|
2153
|
-
/**
|
|
2154
|
-
* Evolution home path helpers: the DSH root and `$DSH_HOME/evolution` for
|
|
2155
|
-
* plugin-owned sidecar state (reports, activity store, feedback file,
|
|
2156
|
-
* state-domain data).
|
|
2157
|
-
*
|
|
2158
|
-
* C-10: PATH HELPERS ONLY — despite the file name there is no store
|
|
2159
|
-
* here. Durable evolution state lives in the state stack (evolution-state over
|
|
2160
|
-
* evolution-state-json / -domain); skills and memories live in skill-store.ts
|
|
2161
|
-
* / memory-store.ts. The file name is kept deliberately: renaming it would
|
|
2162
|
-
* touch every family import for zero behavior change, and the audit records
|
|
2163
|
-
* the mismatch as known naming debt.
|
|
2164
|
-
*/
|
|
2165
|
-
/**
|
|
2166
|
-
* DSH home root: `$DSH_HOME` or `~/.dsh`. Single source of the empty-string
|
|
2167
|
-
* fallback — an EMPTY or WHITESPACE-ONLY DSH_HOME resolves to the default
|
|
2168
|
-
* home, never to a CWD-relative path (0.3.19 W1.3, 0.3.22 F-207);
|
|
2169
|
-
* V8-06 (0.3.47) extends the guard to whitespace (upstream home-paths:
|
|
2170
|
-
* `trim().length > 0` is the adoption test — `DSH_HOME=" "` must not produce
|
|
2171
|
-
* a sidecar under a relative "." path).
|
|
2172
|
-
* C-11: the adoption test and the RETURNED value now come from the
|
|
2173
|
-
* SAME trimmed source — the old form tested `trim()` but returned the raw
|
|
2174
|
-
* value, so `DSH_HOME=" /x "` was accepted AND persisted with literal spaces.
|
|
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.
|
|
2185
|
-
*/
|
|
2186
|
-
function evolutionRoot(env = process.env) {
|
|
2187
|
-
const home = env.DSH_HOME?.trim();
|
|
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);
|
|
2191
|
-
}
|
|
2192
|
-
/** Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
|
|
2193
|
-
* state (reports, activity store, feedback file, state-domain data). */
|
|
2194
|
-
function evolutionHome(env = process.env) {
|
|
2195
|
-
return join(evolutionRoot(env), "evolution");
|
|
2196
|
-
}
|
|
2197
|
-
//#endregion
|
|
2198
|
-
//#region lib/types/serial.js
|
|
2199
|
-
/**
|
|
2200
|
-
* A process-local serial task queue: each task starts only after the previous
|
|
2201
|
-
* one settles (success or failure), so read-modify-write sequences that share
|
|
2202
|
-
* one file never interleave inside this process. The durable cross-process
|
|
2203
|
-
* serialization layer is the IO backend's transact lock; this chain is the
|
|
2204
|
-
* second layer (0.3.17 S2.8, T-1: the shape was duplicated in state-json and
|
|
2205
|
-
* memory-files — one factory now).
|
|
2206
|
-
*/
|
|
2207
|
-
function makeSerialQueue() {
|
|
2208
|
-
let chain = Promise.resolve();
|
|
2209
|
-
return (task) => {
|
|
2210
|
-
const run = chain.then(task, task);
|
|
2211
|
-
chain = run.then(() => void 0, () => void 0);
|
|
2212
|
-
return run;
|
|
2213
|
-
};
|
|
2214
|
-
}
|
|
2215
|
-
//#endregion
|
|
2216
|
-
//#region lib/types/numeric.js
|
|
2217
|
-
/**
|
|
2218
|
-
* Numeric config clamping for the dsh-evolution plugin family.
|
|
1911
|
+
* Numeric config clamping for the dsh-evolution plugin family.
|
|
2219
1912
|
*
|
|
2220
1913
|
* G3.1 (0.3.23): numeric configuration values are normalised through a single
|
|
2221
1914
|
* helper so 0 / negative / NaN / ±Infinity / out-of-range all fall back to the
|
|
@@ -2466,11 +2159,22 @@ const PATTERNS = [
|
|
|
2466
2159
|
regex: /(?:密钥|凭据|口令|密码|环境变量)[\s\S]{0,30}(?:发送|上传|传输|外传|泄露)[\s\S]{0,30}(?:到|至)\s*(?:https?:\/\/|[\w.-]+\.(?:com|net|org|io|cn|dev|xyz|ru)\b)/
|
|
2467
2160
|
}
|
|
2468
2161
|
];
|
|
2469
|
-
const
|
|
2470
|
-
const ZERO_WIDTH_CHARS = new RegExp(`[${INVISIBLE_CHAR_CLASS}]`, "u");
|
|
2162
|
+
const ZERO_WIDTH_CHARS = new RegExp(`[\\u034f\\u200b\\u2060\\u2061\\u2062\\u2063\\u2064\\u206a-\\u206f\\ufeff\\u{e0000}-\\u{e007f}]`, "u");
|
|
2471
2163
|
const ZWJ_OUTSIDE_EMOJI = /(?<!\p{Extended_Pictographic})\u200d(?!\p{Extended_Pictographic})/u;
|
|
2472
2164
|
const TYPOGRAPHY_CHARS = /[\u00ad\u061c\u180e\u200c\ufe00-\ufe0f]/;
|
|
2473
2165
|
const BIDI_CHARS = /[\u202a-\u202e\u2066-\u2069]/;
|
|
2166
|
+
const FORMAT_CONTROL_CLASS = "\\p{Cf}\\p{Zl}\\p{Zp}\\u0000\\u2065\\ufff0-\\ufff8\\u{e0080}-\\u{e00ff}";
|
|
2167
|
+
const FORMAT_CONTROL_TEST = new RegExp(`[${FORMAT_CONTROL_CLASS}]`, "u");
|
|
2168
|
+
/** S0.4 (v37 P0-3): a format control the three finding sets above do NOT report. */
|
|
2169
|
+
function hasUnreportedFormatControl(text) {
|
|
2170
|
+
for (const character of text) {
|
|
2171
|
+
if (!FORMAT_CONTROL_TEST.test(character)) continue;
|
|
2172
|
+
if (ZERO_WIDTH_CHARS.test(character) || TYPOGRAPHY_CHARS.test(character) || BIDI_CHARS.test(character)) continue;
|
|
2173
|
+
if (character === "\\u200d") continue;
|
|
2174
|
+
return true;
|
|
2175
|
+
}
|
|
2176
|
+
return false;
|
|
2177
|
+
}
|
|
2474
2178
|
const SCOPE_ORDER = {
|
|
2475
2179
|
all: 1,
|
|
2476
2180
|
context: 2,
|
|
@@ -2512,9 +2216,15 @@ function scanThreats(text, scope = "strict", maxScanChars = 65536, options = NO_
|
|
|
2512
2216
|
category: "unicode_obfuscation",
|
|
2513
2217
|
scope: "all"
|
|
2514
2218
|
});
|
|
2219
|
+
if (!excluded.has("unicode_format_control") && hasUnreportedFormatControl(text)) findings.push({
|
|
2220
|
+
label: "unicode_format_control",
|
|
2221
|
+
category: "unicode_obfuscation",
|
|
2222
|
+
scope: "all",
|
|
2223
|
+
severity: "report"
|
|
2224
|
+
});
|
|
2515
2225
|
const normalized = text.normalize("NFKC");
|
|
2516
2226
|
const SPACE_SPLITTERS = /[\u00ad\u061c\u180e\u200c\ufe00-\ufe0f]/gu;
|
|
2517
|
-
const OBFUSCATION_SPLITTERS = new RegExp(`[${
|
|
2227
|
+
const OBFUSCATION_SPLITTERS = new RegExp(`[${FORMAT_CONTROL_CLASS}]`, "gu");
|
|
2518
2228
|
const patternTexts = [normalized.replace(SPACE_SPLITTERS, " ").replace(OBFUSCATION_SPLITTERS, " "), normalized.replace(SPACE_SPLITTERS, "").replace(OBFUSCATION_SPLITTERS, "")];
|
|
2519
2229
|
const windows = [];
|
|
2520
2230
|
for (const patternText of patternTexts) if (patternText.length <= windowSize) windows.push(patternText);
|
|
@@ -2629,6 +2339,24 @@ function render(entries) {
|
|
|
2629
2339
|
function stripDatePrefix(entry) {
|
|
2630
2340
|
return entry.replace(/^## \d{4}-\d{2}-\d{2}\n/, "");
|
|
2631
2341
|
}
|
|
2342
|
+
/**
|
|
2343
|
+
* Neutralize the platform's `{{name}}` prompt-variable syntax in text that is
|
|
2344
|
+
* about to be registered as a `systemPrompt.context` contribution.
|
|
2345
|
+
*
|
|
2346
|
+
* The platform interpolates every context/section text once per model step and
|
|
2347
|
+
* THROWS on an unknown or malformed reference — so one stored memory entry
|
|
2348
|
+
* containing `{{...}}` (a CI expression, a Jinja/Helm/Vue template, a doc
|
|
2349
|
+
* placeholder) used to fail `assemble()` on EVERY later pre-step of every
|
|
2350
|
+
* session under this DSH_HOME, while a registered name such as `{{cwd}}` was
|
|
2351
|
+
* silently substituted into the "memory" the model reads. Only the INJECTED
|
|
2352
|
+
* text is neutralized: stored entries keep their original bytes.
|
|
2353
|
+
*
|
|
2354
|
+
* @param text - rendered context text about to leave for the prompt surface.
|
|
2355
|
+
* @returns the same text with every `{{` split so it cannot start a reference.
|
|
2356
|
+
*/
|
|
2357
|
+
function neutralizePromptVariables(text) {
|
|
2358
|
+
return text.includes("{{") ? text.replaceAll("{{", "{ {") : text;
|
|
2359
|
+
}
|
|
2632
2360
|
/** F-201: does `content` carry the on-disk entry delimiter or a trailing
|
|
2633
2361
|
* `\n§` fragment that would combine with the render terminator into a real
|
|
2634
2362
|
* delimiter boundary? Both split the fact into multiple entries on read-back
|
|
@@ -3196,7 +2924,7 @@ var MemoryStore = class {
|
|
|
3196
2924
|
parts.push(`## ${label} (${safe.length} entries)${usage}${note}\n${body}`);
|
|
3197
2925
|
} else if (entries.length > 0) parts.push(`## ${label} — ${entries.length} entries withheld by the security scan; none injected`);
|
|
3198
2926
|
}
|
|
3199
|
-
return parts.join("\n\n");
|
|
2927
|
+
return neutralizePromptVariables(parts.join("\n\n"));
|
|
3200
2928
|
}
|
|
3201
2929
|
/**
|
|
3202
2930
|
* Detect on-disk drift for a caller that holds no locked view: `true` when the
|
|
@@ -3312,56 +3040,452 @@ function composePresetComposition(standardComposition, deltaComposition) {
|
|
|
3312
3040
|
const collisions = [...compositionRowIds(deltaComposition)].filter((id) => standardIds.has(id)).sort();
|
|
3313
3041
|
if (collisions.length > 0 && !allowRowCollisions()) throw new Error(`evolution preset composition: delta rows collide with runtime standard rows: ${collisions.join(", ")}`);
|
|
3314
3042
|
if (collisions.length > 0) console.warn(`evolution preset composition: warning — delta rows collide with standard rows (${collisions.join(", ")}); keeping both (DSH_EVOLUTION_ALLOW_ROW_COLLISIONS=1)`);
|
|
3315
|
-
return
|
|
3043
|
+
return applyRowOverrides(`${standardComposition.replace(/\s+$/, "")}\n\n${deltaComposition.trim()}\n`);
|
|
3044
|
+
}
|
|
3045
|
+
/** The composer-owned overrides, as DATA: one place states what the generated
|
|
3046
|
+
* preset must carry beyond the platform composition. V10-14's cap is the first
|
|
3047
|
+
* entry; install-layered.mjs keeps a byte-identical copy of this table. */
|
|
3048
|
+
const ROW_OVERRIDES = [{
|
|
3049
|
+
row: "tool-skill",
|
|
3050
|
+
key: "config",
|
|
3051
|
+
lines: [
|
|
3052
|
+
" # V10-14: Hermes 60-char catalog cap — injected by the preset composer (P1-2);",
|
|
3053
|
+
" # this preset-scope row is the session-visible instance and no profile",
|
|
3054
|
+
" # patch can reach it. Remove only to run the platform default (500).",
|
|
3055
|
+
" config:",
|
|
3056
|
+
" catalogDescriptionMaxLength: 60"
|
|
3057
|
+
],
|
|
3058
|
+
missing: "evolution preset composition: warning — no `- id: tool-skill` row in the composed preset; the 60-char catalog cap was NOT injected (platform renamed the row? reconcile with the delta)"
|
|
3059
|
+
}];
|
|
3060
|
+
/**
|
|
3061
|
+
* Ensure every {@link RowOverride} inside its target row.
|
|
3062
|
+
*
|
|
3063
|
+
* Key-level by construction: we INSERT the override's own lines and leave a row
|
|
3064
|
+
* that already carries the key byte-identical, so a re-run never doubles a key.
|
|
3065
|
+
* This is the deliberate opposite of the platform's patch layers, where an
|
|
3066
|
+
* override REPLACES `config` wholesale — the difference is why the composer can
|
|
3067
|
+
* add a key without erasing the platform's own config defaults.
|
|
3068
|
+
*/
|
|
3069
|
+
function applyRowOverrides(composition, overrides = ROW_OVERRIDES) {
|
|
3070
|
+
let lines = composition.split("\n");
|
|
3071
|
+
for (const override of overrides) lines = applyOneOverride(lines, override);
|
|
3072
|
+
return lines.join("\n");
|
|
3073
|
+
}
|
|
3074
|
+
function applyOneOverride(lines, override) {
|
|
3075
|
+
const rowRe = new RegExp("^- id:\\s*" + override.row.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s*$");
|
|
3076
|
+
let found = false;
|
|
3077
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
3078
|
+
if (!rowRe.test(lines[i] ?? "")) continue;
|
|
3079
|
+
found = true;
|
|
3080
|
+
let end = i;
|
|
3081
|
+
let hasConfig = false;
|
|
3082
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
3083
|
+
const next = lines[j] ?? "";
|
|
3084
|
+
if (next.trim() === "") break;
|
|
3085
|
+
if (!/^\s/.test(next)) break;
|
|
3086
|
+
if (new RegExp("^ {2}" + override.key + ":(\\s|$)").test(next)) hasConfig = true;
|
|
3087
|
+
end = j;
|
|
3088
|
+
}
|
|
3089
|
+
if (hasConfig) continue;
|
|
3090
|
+
lines.splice(end + 1, 0, ...override.lines);
|
|
3091
|
+
i = end + override.lines.length;
|
|
3092
|
+
}
|
|
3093
|
+
if (!found) console.warn(override.missing);
|
|
3094
|
+
return lines;
|
|
3095
|
+
}
|
|
3096
|
+
function compositionRowIds(composition) {
|
|
3097
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3098
|
+
for (const line of composition.split("\n")) {
|
|
3099
|
+
const id = /^- id:\s*(\S+)/.exec(line)?.[1];
|
|
3100
|
+
if (id) ids.add(id);
|
|
3101
|
+
}
|
|
3102
|
+
return ids;
|
|
3103
|
+
}
|
|
3104
|
+
//#endregion
|
|
3105
|
+
//#region lib/types/prompts.js
|
|
3106
|
+
/**
|
|
3107
|
+
* Review and curation prompts adapted from Hermes Agent
|
|
3108
|
+
* `agent/background_review.py`, `agent/curator.py`, and
|
|
3109
|
+
* `agent/learn_prompt.py`, with tool names translated to the DSH-native
|
|
3110
|
+
* catalog (`memory`, `skill_manage`, `skill`, `bash`, `str_replace_editor`).
|
|
3111
|
+
*
|
|
3112
|
+
* Alignment policy (2026-08-29): the OPERATIONAL steps and instructions the
|
|
3113
|
+
* model follows mirror the Hermes originals structurally (signal list,
|
|
3114
|
+
* preference order, support-file taxonomy, curator package integrity,
|
|
3115
|
+
* consolidated/pruned reporting block). Tool and platform differences are
|
|
3116
|
+
* DSH-adapted (native tool names, pinned-within-review semantics, this
|
|
3117
|
+
* platform's index cap), and DSH-only additions are marked as such.
|
|
3118
|
+
*
|
|
3119
|
+
* Every prompt is pinned in a versioned bundle. Review workers verify the
|
|
3120
|
+
* bundle digest before spending a model call — v31 PROMPT-01, stated
|
|
3121
|
+
* precisely: THAT check proves internal coherence (id/version/digest agree)
|
|
3122
|
+
* for a bundle assembled OUT of process and handed to `verifyPromptBundle`
|
|
3123
|
+
* explicitly. It CANNOT detect in-process tampering (the digest is recomputed
|
|
3124
|
+
* from the same module state it verifies) — catching a stale or partially
|
|
3125
|
+
* patched default bundle is CI's version pin (tests/prompts.spec.ts), not
|
|
3126
|
+
* this runtime gate.
|
|
3127
|
+
*/
|
|
3128
|
+
/**
|
|
3129
|
+
* Prompt bundle identity. Bump both id and version whenever a prompt's text
|
|
3130
|
+
* changes semantically: the bundle digest is the fail-closed signal for
|
|
3131
|
+
* review workers, so a stale id across deployments must be distinguishable.
|
|
3132
|
+
*/
|
|
3133
|
+
const PROMPT_BUNDLE_VERSION = 17;
|
|
3134
|
+
const PROMPT_BUNDLE_ID = `dsh-evolution@17`;
|
|
3135
|
+
const MEMORY_REVIEW_PROMPT = `[Auto-review — Memory]
|
|
3136
|
+
Review the conversation above and consider saving to memory if appropriate.
|
|
3137
|
+
|
|
3138
|
+
Focus on:
|
|
3139
|
+
1. Has the user revealed things about themselves — persona, desires, preferences, or personal details worth remembering?
|
|
3140
|
+
2. Has the user expressed expectations about how you should behave, their work style, or ways they want you to operate?
|
|
3141
|
+
|
|
3142
|
+
If something stands out, save it using the memory tool.
|
|
3143
|
+
If nothing is worth saving, just say "Nothing to save." and stop.`;
|
|
3144
|
+
const SKILL_REVIEW_PROMPT = `[Auto-review — Skills]
|
|
3145
|
+
Review the conversation above and update the skill library. Be ACTIVE — most sessions produce at least one skill update, even if small. A pass that does nothing is a missed learning opportunity, not a neutral outcome.
|
|
3146
|
+
|
|
3147
|
+
Target shape of the library: CLASS-LEVEL skills, each with a rich SKILL.md and a references/ directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries. This shapes HOW you update, not WHETHER you update.
|
|
3148
|
+
|
|
3149
|
+
Signals to look for (any one of these warrants action):
|
|
3150
|
+
• User corrected your style, tone, format, legibility, or verbosity. Frustration signals like 'stop doing X', 'this is too verbose', 'don't format like this', 'why are you explaining', 'just give me the answer', 'you always do Y and I hate it', or an explicit 'remember this' are FIRST-CLASS skill signals, not just memory signals. Update the relevant skill(s) to embed the preference so the next session starts already knowing.
|
|
3151
|
+
• User corrected your workflow, approach, or sequence of steps. Encode the correction as a pitfall or explicit step in the skill that governs that class of task.
|
|
3152
|
+
• Non-trivial technique, fix, workaround, debugging path, or tool-usage pattern emerged that a future session would benefit from. Capture it.
|
|
3153
|
+
• A skill that got loaded or consulted this session turned out to be wrong, missing a step, or outdated. Patch it NOW.
|
|
3154
|
+
|
|
3155
|
+
Read-before-write: update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session. On the plan channel ops on unread skills are rejected; direct writes have no such guard, so treat the rule as binding. CREATE of a brand-new umbrella is the only exception.
|
|
3156
|
+
|
|
3157
|
+
Preference order — prefer the earliest action that fits, but do pick one when a signal above fired:
|
|
3158
|
+
1. UPDATE A CURRENTLY-LOADED SKILL. Look back through the conversation for skills the user loaded or you read. If any of them covers the territory of the new learning, PATCH that one first. It is the skill that was in play, so it's the right one to extend.
|
|
3159
|
+
2. UPDATE AN EXISTING UMBRELLA. If no loaded skill fits but an existing class-level skill does, patch it. Add a subsection, a pitfall, or broaden a trigger.
|
|
3160
|
+
3. ADD A SUPPORT FILE under an existing umbrella. Skills can be packaged with three kinds of support files — use the right directory per kind:
|
|
3161
|
+
• references/<topic>.md — session-specific detail (error transcripts, reproduction recipes, provider quirks) AND condensed knowledge banks: quoted research, API docs, external authoritative excerpts, or domain notes you found while working on the problem. Write it concise and for the value of the task, not as a full mirror of upstream docs.
|
|
3162
|
+
• templates/<name>.<ext> — starter files meant to be copied and modified (boilerplate configs, scaffolding, a known-good example the agent can reproduce with modifications).
|
|
3163
|
+
• scripts/<name>.<ext> — statically re-runnable actions the skill can invoke directly (verification scripts, fixture generators, deterministic probes, anything the agent should run rather than hand-type each time).
|
|
3164
|
+
Add support files via skill_manage action=write_file with file_path starting 'references/', 'templates/', or 'scripts/'. The umbrella's SKILL.md should gain a one-line pointer to any new support file so future agents know it exists.
|
|
3165
|
+
4. RESTRUCTURE a loaded skill whose body grew log-like — rc/sha/date-dense sections, session-detail spirals, or a fat body with no support files. Use skill_manage action=restructure with restructure: [{"heading": "<the exact ## heading text>", "to_file": "references/<topic>.md"}] — the ENTIRE ## section (from that heading to the next heading) moves into the support file and its position becomes a pointer line. The skill's name and directory never change. Only propose headings that exist verbatim in the body; never invent one, and never restructure a healthy small skill.
|
|
3166
|
+
5. CREATE A NEW CLASS-LEVEL UMBRELLA SKILL when no existing skill covers the class. The name MUST be at the class level. The name MUST NOT be a specific PR number, error string, feature codename, library-alone name, or 'fix-X / debug-Y / audit-Z-today' session artifact. If the proposed name only makes sense for today's task, it's wrong — fall back to (1), (2), or (3).
|
|
3167
|
+
|
|
3168
|
+
User-preference embedding (important): when the user expressed a style/format/workflow preference, the update belongs in the SKILL.md body, not just in memory. Memory captures 'who the user is and what the current situation and state of your operations are'; skills capture 'how to do this class of task for this user'. When they complain about how you handled a task, the skill that governs that task needs to carry the lesson.
|
|
3169
|
+
|
|
3170
|
+
If you notice two existing skills that overlap, note it in your reply — the background curator handles consolidation at scale.
|
|
3171
|
+
|
|
3172
|
+
Two-tier deposition discipline (DSH addition, same spirit as the umbrella rule): before writing, classify the knowledge:
|
|
3173
|
+
• PATTERN (reusable — symptom → mechanism → fix → verification, still valuable next session) belongs in the SKILL.md body.
|
|
3174
|
+
• LOG (one-off — commit SHAs, npm/profile states, what this release changed, this session's process narrative) belongs in a references/ file, never the body. Body density IS reuse rate. Keep new entries tight: a pattern fits in 2-8 physical lines; prefer changing the current-state pointer over appending history.
|
|
3175
|
+
|
|
3176
|
+
Protected skills (DO NOT edit these):
|
|
3177
|
+
• Bundled skills (shipped with the platform).
|
|
3178
|
+
• Hub-installed skills (installed from a hub).
|
|
3179
|
+
Pinned skills are read-only to THIS background review pass — the pinned write guard refuses background changes, so this pass may not update them. They also cannot be archived by any writer (the foreground included): remove the .pinned marker first. Foreground and delegated-subagent update/patch writes to pinned skills remain allowed.
|
|
3180
|
+
If the only skills that need updating are protected, say 'Nothing to save.' and stop.
|
|
3181
|
+
|
|
3182
|
+
Do NOT capture (these become persistent self-imposed constraints that bite you later when the environment changes):
|
|
3183
|
+
• Environment-dependent failures: missing binaries, fresh-install errors, post-migration path mismatches, 'command not found', unconfigured credentials, uninstalled packages. The user can fix these — they are not durable rules.
|
|
3184
|
+
• Negative claims about tools or features ('browser tools do not work', 'X tool is broken', 'cannot use Y'). These harden into refusals the agent cites against itself for months after the actual problem was fixed.
|
|
3185
|
+
• Session-specific transient errors that resolved before the conversation ended. If retrying worked, the lesson is the retry pattern, not the original failure.
|
|
3186
|
+
• One-off task narratives. A user asking 'summarize today's market' or 'analyze this PR' is not a class of work that warrants a skill.
|
|
3187
|
+
|
|
3188
|
+
If a tool failed because of setup state, capture the FIX (install command, config step, env var to set) under an existing setup or troubleshooting skill — never 'this tool does not work' as a standalone constraint.
|
|
3189
|
+
|
|
3190
|
+
'Nothing to save.' is a real option but should NOT be the default. If the session ran smoothly with no corrections and produced no new technique, just say 'Nothing to save.' and stop. Otherwise, act.`;
|
|
3191
|
+
const COMBINED_REVIEW_PROMPT = `[Auto-review]
|
|
3192
|
+
Review the conversation above and update two things:
|
|
3193
|
+
|
|
3194
|
+
**Memory**: who the user is. Did the user reveal persona, desires, preferences, personal details, or expectations about how you should behave? Save facts about the user and durable preferences with the memory tool.
|
|
3195
|
+
|
|
3196
|
+
**Skills**: how to do this class of task. Be ACTIVE — most sessions produce at least one skill update. A pass that does nothing is a missed learning opportunity, not a neutral outcome.
|
|
3197
|
+
|
|
3198
|
+
Target shape of the skill library: CLASS-LEVEL skills with a rich SKILL.md and a references/ directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries.
|
|
3199
|
+
|
|
3200
|
+
Signals that warrant a skill update (any one is enough):
|
|
3201
|
+
• User corrected your style, tone, format, legibility, verbosity, or approach. Frustration is a FIRST-CLASS skill signal, not just a memory signal. 'stop doing X', 'don't format like this', 'I hate when you Y' — embed the lesson in the skill that governs that task so the next session starts fixed.
|
|
3202
|
+
• Non-trivial technique, fix, workaround, or debugging path emerged.
|
|
3203
|
+
• A skill that was loaded or consulted turned out wrong, missing, or outdated — patch it now.
|
|
3204
|
+
|
|
3205
|
+
Read-before-write: update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session. On the plan channel ops on unread skills are rejected; direct writes have no such guard, so treat the rule as binding. CREATE of a brand-new umbrella is the only exception.
|
|
3206
|
+
|
|
3207
|
+
Preference order for skills — pick the earliest that fits:
|
|
3208
|
+
1. UPDATE A CURRENTLY-LOADED SKILL. Check what skills were loaded or read in the conversation. If one of them covers the learning, PATCH it first. It was in play; it's the right place.
|
|
3209
|
+
2. UPDATE AN EXISTING UMBRELLA. Patch it.
|
|
3210
|
+
3. ADD A SUPPORT FILE under an existing umbrella via skill_manage action=write_file. Three kinds: references/<topic>.md for session-specific detail OR condensed knowledge banks (quoted research, API docs excerpts, domain notes) written concise and task-focused; templates/<name>.<ext> for starter files meant to be copied and modified; scripts/<name>.<ext> for statically re-runnable actions (verification, fixture generators, probes). Add a one-line pointer in SKILL.md so future agents find them.
|
|
3211
|
+
4. RESTRUCTURE a loaded skill whose body grew log-like (rc/sha/date-dense sections, session-detail spirals, fat body with no support files) via skill_manage action=restructure with restructure: [{"heading": "<the exact ## heading text>", "to_file": "references/<topic>.md"}] — the ENTIRE ## section moves into the support file and its position becomes a pointer line; the skill's name and directory never change. Only propose headings that exist verbatim in the body.
|
|
3212
|
+
5. CREATE A NEW CLASS-LEVEL UMBRELLA when nothing exists. Name at the class level — NOT a PR number, error string, codename, library-alone name, or 'fix-X / debug-Y' session artifact. If the name only fits today's task, fall back to (1), (2), or (3).
|
|
3213
|
+
|
|
3214
|
+
Two-tier deposition discipline (DSH addition): classify before writing — PATTERN (symptom → mechanism → fix → verification) goes in the SKILL.md body; LOG (commit SHAs, npm/profile states, this release's change list, this session's narrative) goes in a references/ file. Body density IS reuse rate; a pattern fits in 2-8 physical lines.
|
|
3215
|
+
|
|
3216
|
+
User-preference embedding: when the user complains about how you handled a task, update the skill that governs that task — memory alone isn't enough. Memory says 'who the user is and what the current situation and state of your operations are'; skills say 'how to do this class of task for this user'. Both should carry user-preference lessons when relevant.
|
|
3217
|
+
|
|
3218
|
+
If you notice overlapping existing skills, mention it — the background curator handles consolidation.
|
|
3219
|
+
|
|
3220
|
+
Protected skills (DO NOT edit these):
|
|
3221
|
+
• Bundled skills (shipped with the platform).
|
|
3222
|
+
• Hub-installed skills (installed from a hub).
|
|
3223
|
+
Pinned skills are read-only to THIS background review pass — the pinned write guard refuses background changes, so this pass may not update them. They also cannot be archived by any writer (the foreground included): remove the .pinned marker first. Foreground and delegated-subagent update/patch writes to pinned skills remain allowed.
|
|
3224
|
+
If the only skills that need updating are protected, say 'Nothing to save.' and stop.
|
|
3225
|
+
|
|
3226
|
+
Do NOT capture as skills (these become persistent self-imposed constraints that bite you later when the environment changes):
|
|
3227
|
+
• Environment-dependent failures: missing binaries, fresh-install errors, post-migration path mismatches, 'command not found', unconfigured credentials, uninstalled packages. The user can fix these — they are not durable rules.
|
|
3228
|
+
• Negative claims about tools or features ('browser tools do not work', 'X tool is broken', 'cannot use Y'). These harden into refusals the agent cites against itself for months after the actual problem was fixed.
|
|
3229
|
+
• Session-specific transient errors that resolved before the conversation ended. If retrying worked, the lesson is the retry pattern, not the original failure.
|
|
3230
|
+
• One-off task narratives. A user asking 'summarize today's market' or 'analyze this PR' is not a class of work that warrants a skill.
|
|
3231
|
+
|
|
3232
|
+
If a tool failed because of setup state, capture the FIX (install command, config step, env var to set) under an existing setup or troubleshooting skill — never 'this tool does not work' as a standalone constraint.
|
|
3233
|
+
|
|
3234
|
+
Act on whichever of the two dimensions has real signal. If genuinely nothing stands out on either, say 'Nothing to save.' and stop — but don't reach for that conclusion as a default.`;
|
|
3235
|
+
const CURATOR_PROMPT = `You are the skill curator. Maintain a healthy, class-level skill library, not a flat pile of narrow one-session skills.
|
|
3236
|
+
|
|
3237
|
+
This is an UMBRELLA-BUILDING consolidation pass, not a passive audit and not a duplicate-finder.
|
|
3238
|
+
|
|
3239
|
+
The goal is a LIBRARY OF CLASS-LEVEL INSTRUCTIONS. A skill collection of many narrow skills where each captures one session's specific bug is a FAILURE of the library. An agent searching skills matches on descriptions, not exact names; one broad umbrella with labeled subsections beats five narrow siblings for discoverability.
|
|
3240
|
+
|
|
3241
|
+
Right target shape: class-level skills with rich SKILL.md + references/, templates/, scripts/ support files for session-specific detail.
|
|
3242
|
+
|
|
3243
|
+
Hard rules:
|
|
3244
|
+
1. NEVER hard-delete a skill. Archive (moving to .archive/) is the maximum destructive action; archives are recoverable, deletion is not.
|
|
3245
|
+
2. Do not touch bundled, hub-installed, pinned, or scheduled-task-referenced (referenced) skills. Referenced skills are fully protected — never consolidated, never pruned (there is no scheduled-task reference-rewriting pass; a referenced skill stays in place by design).
|
|
3246
|
+
3. Do not archive recently-created or never-used skills without strong evidence. "use=0" is NOT evidence either way — it only means the trigger has not come up yet. Never archive a never-used skill unless it is at least 30 days old AND its content is genuinely obsolete or fully absorbed elsewhere.
|
|
3247
|
+
4. Do NOT reject consolidation on the grounds that "each skill has a distinct trigger". The right bar is: would a human maintainer write this as N separate skills, or one skill with N labeled subsections? When the answer is the latter, merge.
|
|
3248
|
+
5. Judge overlap on CONTENT, not on usage counters.
|
|
3249
|
+
6. Before archiving a merged skill, ensure its unique content was preserved in the umbrella.
|
|
3250
|
+
|
|
3251
|
+
How to work:
|
|
3252
|
+
1. Scan the candidate list. Identify PREFIX CLUSTERS — skills sharing a first word or domain keyword. Expected cluster count scales with the library: a large collection may show 10-25 prefix clusters, a small one often has none — a clean "nothing to consolidate" summary is the correct small-library outcome, not a shortage of ambition.
|
|
3253
|
+
2. For each cluster with 2+ members, ask "what is the UMBRELLA CLASS these skills serve?" and consolidate:
|
|
3254
|
+
a. MERGE INTO AN EXISTING UMBRELLA (patch a labeled section for each sibling's unique insight, then archive the siblings).
|
|
3255
|
+
b. CREATE A NEW UMBRELLA SKILL.md covering the shared workflow with short labeled subsections, then archive the absorbed siblings.
|
|
3256
|
+
c. DEMOTE session-specific detail to references/, templates/, or scripts/ under the umbrella. Use the right directory per kind:
|
|
3257
|
+
• references/<topic>.md — session-specific detail OR condensed knowledge banks (quoted research, API docs excerpts, domain notes, provider quirks, reproduction recipes) written concise and task-focused.
|
|
3258
|
+
• templates/<name>.<ext> — starter files meant to be copied and modified.
|
|
3259
|
+
• scripts/<name>.<ext> — statically re-runnable actions (verification scripts, fixture generators, probes).
|
|
3260
|
+
3. Package integrity — not optional: inspect each skill as a COMPLETE directory package, not just SKILL.md. A skill root may include references/, templates/, scripts/, and assets/. If the source skill has support files OR its SKILL.md contains relative links to them, DO NOT flatten only SKILL.md into <umbrella>/references/<old>.md. Choose one safe path instead: keep it as a standalone skill, OR fully merge by re-homing every needed support file into the umbrella's canonical directories AND rewriting the destination instructions to the new paths, OR archive the entire original skill package unchanged. Never leave demoted instructions pointing at files left behind under the old skill directory.
|
|
3261
|
+
4. Flag skills whose NAME is too narrow (contains a PR number, a feature codename, a specific error string, an 'audit'/'diagnosis'/'salvage' session artifact) — they almost always belong as a subsection or support file under a class-level umbrella.
|
|
3262
|
+
5. Iterate. After one consolidation round, scan the remaining set and look for the NEXT umbrella opportunity. Don't stop after 3 merges.
|
|
3263
|
+
|
|
3264
|
+
You are a NOMINATOR, not an executor: this channel has NO tools. Your single deliverable is the structured YAML block below. Never narrate actions you did not take ("merged", "patched", "archived") — you are proposing, and the deterministic engine executes only names from the candidate pool it gave you. (A future execution view would expose skill_manage; today it does not.)
|
|
3265
|
+
|
|
3266
|
+
'keep' is a legitimate decision ONLY when the skill is already a class-level umbrella and none of the proposed merges would improve discoverability. 'This is narrow but distinct from its siblings' is NOT a reason to keep — it's a reason to move it under an umbrella as a subsection or support file.
|
|
3267
|
+
|
|
3268
|
+
Expected output: real umbrella-ification. Process every obvious cluster. If you end the pass with obvious clusters still untouched, you stopped too early — go back and look at the clusters you left alone.
|
|
3269
|
+
|
|
3270
|
+
Keep the umbrella body tight and scannable: exact commands, verbatim paths, ~100-200 lines; never invent flags or APIs.
|
|
3271
|
+
|
|
3272
|
+
When done, write a human summary THEN the structured machine-readable block. The block is the contract: every skill you would move to .archive/ MUST appear in exactly one of the two lists. Return ONLY the YAML block after the summary — no post-block prose. Format EXACTLY:
|
|
3273
|
+
|
|
3274
|
+
## Structured summary (required)
|
|
3275
|
+
\`\`\`yaml
|
|
3276
|
+
consolidations:
|
|
3277
|
+
- from: <old-skill-name>
|
|
3278
|
+
mode: reference # optional — ONLY for a 'demote': source is narrow-but-valuable session detail, write it as references/<source>.md under the umbrella instead of appending to the body. Default is append. Place this line BEFORE into:. NEVER use reference when the source body links its own references/ templates/ scripts/ files.
|
|
3279
|
+
into: <umbrella-skill-name>
|
|
3280
|
+
reason: <one short sentence — why merged, not just 'similar'>
|
|
3281
|
+
prunings:
|
|
3282
|
+
- name: <skill-name>
|
|
3283
|
+
reason: <one short sentence — why archived with no merge target>
|
|
3284
|
+
\`\`\`
|
|
3285
|
+
|
|
3286
|
+
Every skill you would move to .archive/ MUST appear in exactly one of the two lists. If you consolidated X into umbrella Y (patched Y, wrote a references file to Y, or created Y with X's content absorbed), X goes under consolidations with into: Y. If you archived X with no absorption — truly stale, irrelevant, or obsolete — X goes under prunings. Leave a list empty (consolidations: []) if none. Do not omit the block. The block comes AFTER your human-readable summary of clusters processed, patches made, and decisions left alone.`;
|
|
3287
|
+
const CURATOR_DRY_RUN_BANNER = `═══════════════════════════════════════════════════════════════
|
|
3288
|
+
DRY-RUN — REPORT ONLY. DO NOT MUTATE THE SKILL LIBRARY.
|
|
3289
|
+
═══════════════════════════════════════════════════════════════
|
|
3290
|
+
|
|
3291
|
+
This is a PREVIEW pass. Follow every instruction above EXCEPT:
|
|
3292
|
+
• Do NOT call skill_manage with action=create, update, patch, delete, write_file, or remove_file.
|
|
3293
|
+
• Do NOT move, copy, or rewrite any file under the skills tree.
|
|
3294
|
+
|
|
3295
|
+
Your output IS the deliverable: produce the exact same human-readable summary and YAML block you would on a live run, describing the actions you WOULD take. A reviewer will decide whether to approve a live run.
|
|
3296
|
+
|
|
3297
|
+
If you accidentally take a mutating action, say so explicitly in the summary.`;
|
|
3298
|
+
const COMPLETION_SKILL_REVIEW_PROMPT = `[Auto-review — Skills · task complete]
|
|
3299
|
+
Your current task now appears complete. Before wrapping up, review the approach and update the skill library via skill_manage.
|
|
3300
|
+
|
|
3301
|
+
Follow the skills review policy: be ACTIVE, prefer class-level umbrellas, patch ONLY skills loaded or read this session, and capture non-trivial techniques and user corrections. Do NOT capture environment-dependent failures, negative claims about tools, or one-off task narratives.
|
|
3302
|
+
|
|
3303
|
+
Do NOT modify output files or re-run the task. If you are still mid-task, ignore this.`;
|
|
3304
|
+
/**
|
|
3305
|
+
* Maintenance-subagent persona (design 011 §6). Constants pin the persona
|
|
3306
|
+
* with `{signal:id}` placeholders; the renderer substitutes names from the
|
|
3307
|
+
* drift-signals module (single vocabulary). The model-facing contract is the
|
|
3308
|
+
* persona + the mechanical-facts block; the signature head lets the model
|
|
3309
|
+
* compare the two heads (011 mismatch protocol).
|
|
3310
|
+
*/
|
|
3311
|
+
const MAINTAIN_PROMPT = `<<<MAINTAIN_PROMPT v={bundle_version} sig={joint_signature}>>>
|
|
3312
|
+
|
|
3313
|
+
## 角色
|
|
3314
|
+
你是技能库的**外部审计者**:只读、只输出计划、从不执行(执行由用户命令与审批完成)。
|
|
3315
|
+
|
|
3316
|
+
## 1. 输入契约(冲突时以此为准)
|
|
3317
|
+
机械事实块 <<<MECHANICAL_FACTS v={signals_version} sig={joint_signature}>>>(下方,以 <<<END FACTS>>> 闭合)是唯一证据来源。
|
|
3318
|
+
- verdict 仅三值:pass=未越阈 / over=越阈 / unknown=未检测。
|
|
3319
|
+
- over 是事实位置,不是违规结论;没有条款对应的事实,不产生建议。
|
|
3320
|
+
- unknown ≠ pass;引用 unknown 信号的条目必须 needs_human:true。
|
|
3321
|
+
- 事实只读:不改写、不补写、不把事实"翻译"成裁决。
|
|
3322
|
+
- 两处 sig 不一致或任一缺失 → 只输出 MISMATCH + 两侧版本号,禁止输出计划。
|
|
3323
|
+
|
|
3324
|
+
## 2. 信号→条款映射(每条 over 必须落到条款;无一遗漏)
|
|
3325
|
+
{signal:dedup_group}→A1 · {signal:narrow_name}→A2 · {signal:prefix_cluster}→A3 · {signal:stamp_density}与{signal:body_size}→B1 · {signal:pointer_missing}→B2 · {signal:dup_heading}→B3 · {signal:overlong_line}→B4 · {signal:description_chars}→B5 · {signal:usage_observed}/{signal:quality_low}→门控(校验器对 quality_low=unknown 的技能强制 needs_human,模板侧不重复)
|
|
3326
|
+
|
|
3327
|
+
## 3. 完整性契约(校验器机械执行)
|
|
3328
|
+
事实块中每条 over 信号必须满足其一:成为某条建议的 evidence,或在 notes 中说明"已审·无条款对应·不动作"。禁止静默省略;先逐信号核对再输出。
|
|
3329
|
+
|
|
3330
|
+
## 4. 工作流程(按序执行,不得跳步)
|
|
3331
|
+
① 通读事实块 → ② 对每个候选技能用 skill 读正文(B1/B2/B4 必读;**读取失败必须报告工具返回的事实**(错误信息/无对应条目),禁止用"无法读取"含糊绕过)→ ③ maintenance_probe 按需深挖 → ④ 逐信号过 §3 完整性 → ⑤ 输出计划。
|
|
3332
|
+
|
|
3333
|
+
## 5. 检查清单(信号 → 语义判定 → 输出形态)
|
|
3334
|
+
A. 域·碎片化
|
|
3335
|
+
- A1 {signal:dedup_group}=over:判近重复组是否同伞可合并;是→relationship-level consolidate;否→不输出。
|
|
3336
|
+
- A2 {signal:narrow_name}=over:判否"仅对今日任务成立";成立→改名/归档建议;内部代号(格式合规语义窄)→conf≤0.4+needs_human。
|
|
3337
|
+
- A3 {signal:prefix_cluster}=over:判簇内是否同伞;非同伞→notes 提域划分观察,不强制建伞。
|
|
3338
|
+
|
|
3339
|
+
B. 层·分层错位
|
|
3340
|
+
- B1 {signal:stamp_density}(阈值 {signal:stamp_density.threshold})或 {signal:body_size}(阈值 {signal:body_size.threshold})=over:按**三问判据**判锚/残留——① 该编号/时间戳是否被库内其他文件引用?② 除"何时产生/为何存在"外是否还承载信息?③ 删除是否影响任何跨文档检索?(①是且③是→锚;否则→残留候选,人审)。锚→允许保留 + needs_human + semantic_reasoning 写三问结果;**锚不使用 is_override**(is_override 仅用于 §7 申诉;锚是 B1 的正常裁决路径);残留→restructure 建议(movable headings 逐字引用)。**锚≠可读:单行 >4000 字符即使在锚类也必须拆分。**
|
|
3341
|
+
- B2 {signal:pointer_missing}=over:读支持文件后判性质——可复用模式→上移正文;会话专属实录→保留+补指针;形态=patch 指引。**未读内容仅凭文件名 → conf≤0.4 且措辞"先人工确认再执行"。** **缺失指针=支持文件存在、正文无引用(单向语义),finding 表述勿反向。**
|
|
3342
|
+
- B3 {signal:dup_heading}=over:删除多余标题行(保留一份),patch 指引。
|
|
3343
|
+
- B4 {signal:overlong_line}=over:>1500 拆行;>4000 判定可读性危机(内容合法也拆);patch 指引。**finding 必须给全量口径:共 N 行超限,其中 >4000 的逐行列出。**
|
|
3344
|
+
- B5 {signal:description_chars}=over:先判**性质**三分类——事件性承诺(单次故障/incident 写入元数据)→裁剪建议;叙事性自我描述→压缩建议;丰富但合规(完整用例边界)→保留 + is_override + override_reason="合法密度"。**分类特征**:含"恢复/修复某次事故、日期快照"类一次性措辞→事件性承诺;"动词+对象"式任务说明→叙事性;枚举完整用例边界且不可拆分→丰富合规。**第三类门槛(默认从严,半机械)**:先自行试写一个 ≤60 字压缩方案——能保留全部路由关键项(触发词+域)→ 不可判第三类(按压缩建议);只有试写失败(在 semantic_reasoning 列出试写方案与具体失败点)才可判丰富合规。描述文本可见(probe desc-text 或正文 frontmatter)时仍须三分类;仅长度可见 → conf≤0.4。semantic_reasoning 必写三分类之一。
|
|
3345
|
+
|
|
3346
|
+
D. 库·整合纪律(计划形态约束)
|
|
3347
|
+
- D1 同类问题多处出现→合成一条 relationship-level 建议,不逐项输出。
|
|
3348
|
+
- D2 结构类优先级高于内容类;影响面 library-level > relationship-level > skill-level。
|
|
3349
|
+
|
|
3350
|
+
## 6. 输出契约(校验器机械执行)
|
|
3351
|
+
{verdict: "issues" | "no_issues",
|
|
3352
|
+
plan: [{ kind: "skill-level"|"relationship-level"|"library-level", names: [str],
|
|
3353
|
+
rule: "A1"|"B2"|..., evidence: [{signal, value}],
|
|
3354
|
+
finding: "<一句事实描述:引用信号 id 与值;零裁决动词>",
|
|
3355
|
+
recommendation: "<唯一允许的'应'句:建议动作+理由+执行形态(命令/patch 指引)>",
|
|
3356
|
+
semantic_reasoning: "<语义判据;含 LLM 推断时 confidence≤0.4>",
|
|
3357
|
+
impact: "better|worse|neutral", impact_reason: "<相对'不动'的净影响>",
|
|
3358
|
+
reversibility: "archive|restructure|patch|rename|none", undo_path: "<一步撤销方式>",
|
|
3359
|
+
confidence: float, needs_human: bool, is_override: bool,
|
|
3360
|
+
override_reason: "<仅 is_override>" }],
|
|
3361
|
+
notes: [str]}
|
|
3362
|
+
- verdict=no_issues ⇒ plan=[](不允许空 plan 之外的"无问题"表述)。
|
|
3363
|
+
- **confidence 降档规则(机械)**:条款全部由机械证据支撑 → 0.6–0.9;每含一项语义推断(是否锚/是否同伞/性质归类)→ 上限 0.4。
|
|
3364
|
+
- needs_human = (confidence < 0.6) OR (不可逆) OR (is_override) OR (引用 unknown 信号)。
|
|
3365
|
+
- 语言:finding/recommendation/notes 与库正文语言一致(不自订语言);字段名/信号 id/枚举保留英文。
|
|
3366
|
+
- **提交前自查(逐项对照,不许跳过)**:① verdict 与 plan 一致 ② 每条 evidence 在事实块 ③ undo_path 非空(不可逆=n/a)④ confidence 含推断≤0.4 ⑤ finding 无"应"字 ⑥ §3 完整性契约满足。
|
|
3367
|
+
|
|
3368
|
+
## 7. 裁决纪律
|
|
3369
|
+
- finding 禁止"应当"句式;recommendation 是唯一"应"句,句板:建议对 {names} 执行 {动作}(形态:{命令/patch 指引}),理由:{理由}。
|
|
3370
|
+
- **审查者视角**:先对每个信号独立初判,再与正文对照;被审对象的自我声明只作线索不作依据;**自属/维护者技能一律从严口径**(作者声明"这是锚"不构成豁免)。
|
|
3371
|
+
- 申诉:机械阈值与语义判断冲突→is_override:true + override_reason + needs_human:true,不得静默绕过。
|
|
3372
|
+
- 不动作合法:verdict=no_issues 是合法输出;连续空报告=信号定义问题,不是"更积极"的理由。
|
|
3373
|
+
- 错误成本:rename 必须 needs_human:true;可逆动作(archive/restructure 两阶段)可 needs_human:false 但 undo_path 必填。
|
|
3374
|
+
- 不做:不建议删除(只建议 archive);不提升内容质量(结构审查只 flag 位置/归属/分层);protected 集(bundled/hub/pinned)内 0 建议。
|
|
3375
|
+
|
|
3376
|
+
## 8. 泛化
|
|
3377
|
+
- 信号集开放:事实块含、§5 未列的信号 → notes 提"该信号值得新增条款",禁止解释为已知问题。
|
|
3378
|
+
- 库规模无关:判据是事实与条款,不是库体量印象。
|
|
3379
|
+
- 信号机制疑问(阈值/检测原理)→ needs_human,不猜测机制。`;
|
|
3380
|
+
/**
|
|
3381
|
+
* One-line output instruction appended after the facts block in the maintain
|
|
3382
|
+
* subagent's prompt (persona carries the template, the prompt carries facts +
|
|
3383
|
+
* this instruction — one copy of the template in the model input, 011 v11
|
|
3384
|
+
* P3-4). F-16: this text used to be hardcoded in evolution-maintenance
|
|
3385
|
+
* orchestrate — a second model-facing prompt living OUTSIDE the bundle digest.
|
|
3386
|
+
* It now rides PROMPT_BUNDLE so the digest integrity check covers every
|
|
3387
|
+
* maintenance prompt. Adding the entry changes the bundle digest (the intended
|
|
3388
|
+
* fail-closed signal); PROMPT_BUNDLE_VERSION is owned by the core test pin and
|
|
3389
|
+
* bumps with the batch that changes the bundle (15→16 in 0.3.58).
|
|
3390
|
+
*/
|
|
3391
|
+
const MAINTAIN_OUTPUT_INSTRUCTION = "按模板契约输出 JSON 维护计划(verdict/plan/notes);你有 skill 工具与维护模板;并在维护探针(maintenance_probe)挂载时可用它深挖细节。";
|
|
3392
|
+
/**
|
|
3393
|
+
* System-prompt guidance section (Hermes `SKILLS_GUIDANCE`, DSH-adapted).
|
|
3394
|
+
* Registered as a system-prompt section by tool-skill-manage (it mounts
|
|
3395
|
+
* exactly when `skill_manage` is available — the DSH analogue of Hermes'
|
|
3396
|
+
* `if "skill_manage" in agent.valid_tool_names` condition). Instructs the
|
|
3397
|
+
* model to save/repair skills on its own initiative.
|
|
3398
|
+
*/
|
|
3399
|
+
const SKILLS_GUIDANCE = `Skills guidance:
|
|
3400
|
+
• After completing a complex task (5+ tool calls), fixing a tricky error, or discovering a non-trivial workflow, save the approach as a skill with skill_manage so you can reuse it next time.
|
|
3401
|
+
• When using a skill and finding it outdated, incomplete, or wrong, patch it immediately with skill_manage (action='patch') — don't wait to be asked. Skills that aren't maintained become liabilities.`;
|
|
3402
|
+
const PLAN_CHANNEL_NOTE = `
|
|
3403
|
+
|
|
3404
|
+
CHANNEL (subagent): this review channel mounts only the read-only \`skill\` tool — you have NO \`skill_manage\`, NO \`memory\`. Your deliverable is the structured JSON plan below (outputSchema). Describe the patches/creates you RECOMMEND in the plan; never narrate actions you took.`;
|
|
3405
|
+
/** Subagent-channel variant: same review policy, channel-limited deliverable (M-2). */
|
|
3406
|
+
const SKILL_REVIEW_PLAN_PROMPT = `${SKILL_REVIEW_PROMPT}${PLAN_CHANNEL_NOTE}`;
|
|
3407
|
+
/** Subagent-channel variant of the combined review (M-2). */
|
|
3408
|
+
const COMBINED_REVIEW_PLAN_PROMPT = `${COMBINED_REVIEW_PROMPT}${PLAN_CHANNEL_NOTE}`;
|
|
3409
|
+
function reviewPrompt(kind, channel = "agent") {
|
|
3410
|
+
if (channel === "plan") return kind === "skill" ? SKILL_REVIEW_PLAN_PROMPT : COMBINED_REVIEW_PLAN_PROMPT;
|
|
3411
|
+
if (kind === "memory") return MEMORY_REVIEW_PROMPT;
|
|
3412
|
+
if (kind === "skill") return SKILL_REVIEW_PROMPT;
|
|
3413
|
+
return COMBINED_REVIEW_PROMPT;
|
|
3316
3414
|
}
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
* standard-sourced `- id: tool-skill` row of a composed preset.
|
|
3320
|
-
*
|
|
3321
|
-
* The session-visible `tool-skill` instance mounts in the agent preset's own
|
|
3322
|
-
* standing scope; a profile-root patch (evolution-host/cordis.patch.yml)
|
|
3323
|
-
* cannot reach it, so without this injection the catalog's read side runs the
|
|
3324
|
-
* platform default (500). Text-level rewrite in the same line-scan style as
|
|
3325
|
-
* compositionRowIds (no YAML library):
|
|
3326
|
-
* - idempotent: a tool-skill item that already carries a `config:` key is
|
|
3327
|
-
* left byte-identical, so re-running the installer never doubles the key;
|
|
3328
|
-
* - the injected block carries a marker comment so a diff of the generated
|
|
3329
|
-
* preset can tell composer-owned text from platform text;
|
|
3330
|
-
* - a composition WITHOUT a tool-skill row is returned unchanged with a
|
|
3331
|
-
* one-time warning (a renamed platform row must not brick the install,
|
|
3332
|
-
* but the missed cap must be observable).
|
|
3333
|
-
* install-layered.mjs ships the byte-identical `injectToolSkillCap`.
|
|
3334
|
-
*/
|
|
3335
|
-
function injectCatalogDescriptionCap(composition) {
|
|
3336
|
-
const lines = composition.split("\n");
|
|
3337
|
-
let found = false;
|
|
3338
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
3339
|
-
if (!/^- id:\s*tool-skill\s*$/.test(lines[i] ?? "")) continue;
|
|
3340
|
-
found = true;
|
|
3341
|
-
let end = i;
|
|
3342
|
-
let hasConfig = false;
|
|
3343
|
-
for (let j = i + 1; j < lines.length; j += 1) {
|
|
3344
|
-
const next = lines[j] ?? "";
|
|
3345
|
-
if (next.trim() === "") break;
|
|
3346
|
-
if (!/^\s/.test(next)) break;
|
|
3347
|
-
if (/^ {2}config:(\s|$)/.test(next)) hasConfig = true;
|
|
3348
|
-
end = j;
|
|
3349
|
-
}
|
|
3350
|
-
if (hasConfig) continue;
|
|
3351
|
-
lines.splice(end + 1, 0, " # V10-14: Hermes 60-char catalog cap — injected by the preset composer (P1-2);", " # this preset-scope row is the session-visible instance and no profile", " # patch can reach it. Remove only to run the platform default (500).", " config:", " catalogDescriptionMaxLength: 60");
|
|
3352
|
-
i = end + 5;
|
|
3353
|
-
}
|
|
3354
|
-
if (!found) console.warn("evolution preset composition: warning — no `- id: tool-skill` row in the composed preset; the 60-char catalog cap was NOT injected (platform renamed the row? reconcile with the delta)");
|
|
3355
|
-
return lines.join("\n");
|
|
3415
|
+
function sha256(text) {
|
|
3416
|
+
return createHash("sha256").update(text).digest("hex");
|
|
3356
3417
|
}
|
|
3357
|
-
function
|
|
3358
|
-
const
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
}
|
|
3363
|
-
return
|
|
3418
|
+
function createPromptBundle(prompts) {
|
|
3419
|
+
const canonical = JSON.stringify({
|
|
3420
|
+
id: PROMPT_BUNDLE_ID,
|
|
3421
|
+
version: 17,
|
|
3422
|
+
prompts: Object.fromEntries(Object.entries(prompts).sort())
|
|
3423
|
+
});
|
|
3424
|
+
return Object.freeze({
|
|
3425
|
+
id: PROMPT_BUNDLE_ID,
|
|
3426
|
+
version: 17,
|
|
3427
|
+
prompts: Object.freeze({ ...prompts }),
|
|
3428
|
+
sha256: sha256(canonical)
|
|
3429
|
+
});
|
|
3430
|
+
}
|
|
3431
|
+
const PROMPT_BUNDLE = createPromptBundle({
|
|
3432
|
+
memory: MEMORY_REVIEW_PROMPT,
|
|
3433
|
+
skill: SKILL_REVIEW_PROMPT,
|
|
3434
|
+
combined: COMBINED_REVIEW_PROMPT,
|
|
3435
|
+
skillPlan: SKILL_REVIEW_PLAN_PROMPT,
|
|
3436
|
+
combinedPlan: COMBINED_REVIEW_PLAN_PROMPT,
|
|
3437
|
+
curator: CURATOR_PROMPT,
|
|
3438
|
+
completion: COMPLETION_SKILL_REVIEW_PROMPT,
|
|
3439
|
+
maintain: MAINTAIN_PROMPT,
|
|
3440
|
+
maintainOutput: MAINTAIN_OUTPUT_INSTRUCTION,
|
|
3441
|
+
skillsGuidance: SKILLS_GUIDANCE
|
|
3442
|
+
});
|
|
3443
|
+
function verifyPromptBundle(bundle = PROMPT_BUNDLE) {
|
|
3444
|
+
if (bundle.id !== PROMPT_BUNDLE_ID || bundle.version !== 17) return false;
|
|
3445
|
+
const canonical = JSON.stringify({
|
|
3446
|
+
id: PROMPT_BUNDLE_ID,
|
|
3447
|
+
version: 17,
|
|
3448
|
+
prompts: Object.fromEntries(Object.entries(bundle.prompts).sort())
|
|
3449
|
+
});
|
|
3450
|
+
return bundle.sha256 === sha256(canonical);
|
|
3364
3451
|
}
|
|
3452
|
+
const DSH_AUTHORING_STANDARDS = `Follow the Hermes skill-authoring standards, translated to DSH tools.
|
|
3453
|
+
|
|
3454
|
+
Frontmatter:
|
|
3455
|
+
- name: lowercase-hyphenated, <=64 chars, no spaces.
|
|
3456
|
+
- description: ONE sentence, <=60 characters, ends with a period. State the capability, not the implementation. No marketing words. Do NOT repeat the skill name. Count the characters before saving. If the description contains a colon, wrap the whole value in double quotes.
|
|
3457
|
+
- version: 0.1.0
|
|
3458
|
+
- author: always the literal value "Hermes". NEVER fill it from the environment, git config, or any identity you can probe — an environment-derived name is a privacy leak the user never opted into (skills get shared and published), and the skill names itself as Hermes.
|
|
3459
|
+
- platforms: declare [macos], [linux], and/or [windows] only when the skill is genuinely OS-bound (osascript/apt/systemctl => the matching OS; /proc, signal.SIGKILL => linux; fcntl/termios => POSIX). Prefer fixing it cross-platform first (tempdir, pathlib, pure-Node); omit the field for portable skills.
|
|
3460
|
+
- metadata.hermes.tags: a few Capitalized, Relevant, Tags.
|
|
3461
|
+
- metadata.hermes.related_skills: [a, b] — name sibling skills this one builds on or is referenced by (optional; feeds the quality references factor).
|
|
3462
|
+
|
|
3463
|
+
Body section order (omit only when empty):
|
|
3464
|
+
1. "# <Human Title>" then a 2-3 sentence intro: what it does, what it does NOT do, key dependency stance.
|
|
3465
|
+
2. "## When to Use" — concrete trigger phrases.
|
|
3466
|
+
3. "## Prerequisites" — exact env vars, install steps, credentials.
|
|
3467
|
+
4. "## How to Run" — canonical invocation framed through DSH tools.
|
|
3468
|
+
5. "## Quick Reference" — flat command/endpoint list.
|
|
3469
|
+
6. "## Procedure" — numbered steps with copy-paste-exact commands.
|
|
3470
|
+
7. "## Pitfalls" — known limits and rate limits.
|
|
3471
|
+
8. "## Verification" — one check proving the skill worked.
|
|
3472
|
+
|
|
3473
|
+
DSH-tool framing:
|
|
3474
|
+
- Reference DSH tools by name in backticks: \`bash\`, \`str_replace_editor\`, \`write\`, \`skill\`, \`skill_manage\`, \`memory\`.
|
|
3475
|
+
- Do not name wrapped shell utilities when a DSH tool already covers them.
|
|
3476
|
+
- Larger scripts belong under \`scripts/\` (written with \`skill_manage write_file\`) and are referenced from SKILL.md by relative path.
|
|
3477
|
+
|
|
3478
|
+
Quality bar:
|
|
3479
|
+
- Prefer verbatim flags, paths, and APIs from the source. Never invent them.
|
|
3480
|
+
- Keep it tight: ~100 lines simple, ~200 complex.
|
|
3481
|
+
- No router/index/hub skills that only point at other skills.
|
|
3482
|
+
- References go in \`references/\`, templates in \`templates/\`.
|
|
3483
|
+
|
|
3484
|
+
Learn workflow (when the user asks you to learn a reusable skill, or you decide to turn a source/request into one):
|
|
3485
|
+
1. Gather every source named (files, URLs, "what we just did", pasted notes) with the tools you already have — and treat prose after a source as authoring requirements, not noise.
|
|
3486
|
+
2. Apply every requirement and constraint from the request to the SKILL.md you author.
|
|
3487
|
+
3. Author exactly ONE SKILL.md and save it with \`skill_manage\` (action=create); non-trivial scripts go under \`scripts/\`.
|
|
3488
|
+
4. When done, tell the user the skill name, its category, and a one-line summary of what it captured.`;
|
|
3365
3489
|
//#endregion
|
|
3366
3490
|
//#region lib/types/quality.js
|
|
3367
3491
|
/**
|
|
@@ -3551,10 +3675,12 @@ const SECRET_PATTERNS = [
|
|
|
3551
3675
|
["google api key", /AIza[0-9A-Za-z_-]{30,}/g],
|
|
3552
3676
|
["bearer credential", /Bearer[\s]+[a-z0-9._~+/=\-]{16,}/gi]
|
|
3553
3677
|
];
|
|
3554
|
-
const INLINE_ASSIGNMENT_PATTERN = /* @__PURE__ */ new RegExp("(^|[^\\w-])([\\w-]
|
|
3555
|
-
const URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]
|
|
3678
|
+
const INLINE_ASSIGNMENT_PATTERN = /* @__PURE__ */ new RegExp("(^|[^\\w-])([\\w-]*[_\\-])?((?:token|api[_-]?key|secret|password|passwd)(?:[_\\-][\\w-]*)?)\\b([\"\\']?[\\t ]*[:=][\\t ]*)([^\\r\\n]+)", "gi");
|
|
3679
|
+
const URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]{0,63}:\/\/[^\s:/@]+:)([^\s/@]+)@/gi;
|
|
3556
3680
|
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");
|
|
3557
|
-
const BLOCK_KEY_ONLY_LINE = /(?:^|\s)([\w-]
|
|
3681
|
+
const BLOCK_KEY_ONLY_LINE = /(?:^|\s)([\w-]*[_\-])?((?:token|api[_-]?key|secret|password|passwd)(?:[_\-][\w-]*)?)\s*:(?:\r)?$/i;
|
|
3682
|
+
const CREDENTIAL_KEY_RE = /(?:[Tt]oken|[Ss]ecret|[Pp]assword|[Pp]asswd|[Aa]pi[_-]?[Kk]ey)(?![a-z])/;
|
|
3683
|
+
const CANDIDATE_ASSIGNMENT_PATTERN = /(^|[^\w-])([\w-]+)(["']?[\t ]*[:=][\t ]*)([^\r\n]+)/g;
|
|
3558
3684
|
/**
|
|
3559
3685
|
* Mask credential-shaped text before it crosses a session boundary.
|
|
3560
3686
|
* @param text - the text about to be sent to a model outside this session.
|
|
@@ -3566,10 +3692,16 @@ function redactSecrets(text) {
|
|
|
3566
3692
|
for (const [, pattern] of SECRET_PATTERNS) out = out.replace(pattern, "<redacted>");
|
|
3567
3693
|
out = out.replace(URL_CREDENTIALS_PATTERN, (_match, lead) => `${lead ?? ""}<redacted>@`);
|
|
3568
3694
|
out = out.replace(INLINE_ASSIGNMENT_PATTERN, (_match, lead, prefix, key, separator) => `${lead ?? ""}${prefix ?? ""}${key ?? ""}${separator ?? ""}<redacted>`);
|
|
3695
|
+
out = out.replace(CANDIDATE_ASSIGNMENT_PATTERN, (match, lead, key, separator) => {
|
|
3696
|
+
if (typeof key !== "string" || !CREDENTIAL_KEY_RE.test(key)) return match;
|
|
3697
|
+
return `${lead ?? ""}${key}${separator ?? ""}<redacted>`;
|
|
3698
|
+
});
|
|
3569
3699
|
const lines = out.split("\n");
|
|
3570
3700
|
for (let i = 0; i < lines.length - 1; i++) {
|
|
3571
3701
|
const line = lines[i];
|
|
3572
|
-
if (line === void 0
|
|
3702
|
+
if (line === void 0) continue;
|
|
3703
|
+
const camelKey = /^([\w-]+)\s*:(?:\r)?$/.exec(line)?.[1];
|
|
3704
|
+
if (!(BLOCK_KEY_ONLY_LINE.test(line) || camelKey !== void 0 && CREDENTIAL_KEY_RE.test(camelKey))) continue;
|
|
3573
3705
|
const next = lines[i + 1] ?? "";
|
|
3574
3706
|
const [, indent, value, tail] = /^([ \t]+)(\S.*?)([ \t]*)(?:\r)?$/.exec(next) ?? [];
|
|
3575
3707
|
if (indent === void 0 || value === void 0) continue;
|
|
@@ -3584,6 +3716,109 @@ function redactSecrets(text) {
|
|
|
3584
3716
|
return out;
|
|
3585
3717
|
}
|
|
3586
3718
|
//#endregion
|
|
3719
|
+
//#region lib/types/review-channel.js
|
|
3720
|
+
/**
|
|
3721
|
+
* Family-internal mark for the review channel's INJECT delivery (v37 S2.2).
|
|
3722
|
+
*
|
|
3723
|
+
* With the default `reviewMode: 'inject'` the review prompt runs in the PARENT
|
|
3724
|
+
* session, so the parent model's own `skill_manage` calls carry no origin the
|
|
3725
|
+
* platform could attribute: `.pinned` and the `.hermes-managed` authorship mark
|
|
3726
|
+
* were skipped for exactly the autonomous writes they exist for. The delivery
|
|
3727
|
+
* marks the session here and the write path reads the mark.
|
|
3728
|
+
*
|
|
3729
|
+
* Window: after the review prompt, until the next REAL user message (a
|
|
3730
|
+
* `user/message` with `source.kind === 'user'` clears it; plugin-sourced notices
|
|
3731
|
+
* never do). Process-wide and keyed by session on purpose — the same
|
|
3732
|
+
* "restart is a fresh conversation boundary" discipline as the review plugin's
|
|
3733
|
+
* own per-session counters. This module is the ONE owner of the marker.
|
|
3734
|
+
* @module @lmzhen/dsh-evolution-core/review-channel
|
|
3735
|
+
*/
|
|
3736
|
+
/** Session ids currently executing a review prompt, oldest delivery first. */
|
|
3737
|
+
const markedSessions = /* @__PURE__ */ new Set();
|
|
3738
|
+
/**
|
|
3739
|
+
* Mark one session as running the review channel's prompt; idempotent, and
|
|
3740
|
+
* keyed by session so a mark can never leak into another session.
|
|
3741
|
+
* @param sessionId - the session the review prompt was delivered to.
|
|
3742
|
+
*/
|
|
3743
|
+
function markReviewChannel(sessionId) {
|
|
3744
|
+
markedSessions.delete(sessionId);
|
|
3745
|
+
markedSessions.add(sessionId);
|
|
3746
|
+
}
|
|
3747
|
+
/**
|
|
3748
|
+
* Clear the mark: the session's next prompt is human input again.
|
|
3749
|
+
* @param sessionId - the session whose mark is dropped.
|
|
3750
|
+
*/
|
|
3751
|
+
function clearReviewChannel(sessionId) {
|
|
3752
|
+
markedSessions.delete(sessionId);
|
|
3753
|
+
}
|
|
3754
|
+
/**
|
|
3755
|
+
* Whether this session's current prompt came from the review channel. An
|
|
3756
|
+
* execution without a session is never the review channel.
|
|
3757
|
+
* @param sessionId - the executing session id, when the caller has one.
|
|
3758
|
+
* @returns true only for a marked session.
|
|
3759
|
+
*/
|
|
3760
|
+
function isReviewChannelSession(sessionId) {
|
|
3761
|
+
return sessionId !== void 0 && markedSessions.has(sessionId);
|
|
3762
|
+
}
|
|
3763
|
+
/**
|
|
3764
|
+
* Drop marks whose session is gone — a dead session cannot execute a write.
|
|
3765
|
+
* @param isAlive - liveness probe for one session id.
|
|
3766
|
+
* @returns the number of removed marks.
|
|
3767
|
+
*/
|
|
3768
|
+
function sweepReviewChannelSessions(isAlive) {
|
|
3769
|
+
let removed = 0;
|
|
3770
|
+
for (const sessionId of [...markedSessions]) {
|
|
3771
|
+
if (isAlive(sessionId)) continue;
|
|
3772
|
+
markedSessions.delete(sessionId);
|
|
3773
|
+
removed += 1;
|
|
3774
|
+
}
|
|
3775
|
+
return removed;
|
|
3776
|
+
}
|
|
3777
|
+
//#endregion
|
|
3778
|
+
//#region lib/types/probe.js
|
|
3779
|
+
function probePresent(value) {
|
|
3780
|
+
return {
|
|
3781
|
+
kind: "present",
|
|
3782
|
+
value
|
|
3783
|
+
};
|
|
3784
|
+
}
|
|
3785
|
+
function probeAbsent() {
|
|
3786
|
+
return { kind: "absent" };
|
|
3787
|
+
}
|
|
3788
|
+
function probeUnknown(reason) {
|
|
3789
|
+
return {
|
|
3790
|
+
kind: "unknown",
|
|
3791
|
+
reason
|
|
3792
|
+
};
|
|
3793
|
+
}
|
|
3794
|
+
function isPresent(probe) {
|
|
3795
|
+
return probe.kind === "present";
|
|
3796
|
+
}
|
|
3797
|
+
function isAbsent(probe) {
|
|
3798
|
+
return probe.kind === "absent";
|
|
3799
|
+
}
|
|
3800
|
+
function isUnknown(probe) {
|
|
3801
|
+
return probe.kind === "unknown";
|
|
3802
|
+
}
|
|
3803
|
+
/** Only a PRESENT probe yields a value; absent and unknown both fall back. */
|
|
3804
|
+
function valueOr(probe, fallback) {
|
|
3805
|
+
return probe.kind === "present" ? probe.value : fallback;
|
|
3806
|
+
}
|
|
3807
|
+
function mapProbe(probe, transform) {
|
|
3808
|
+
return probe.kind === "present" ? probePresent(transform(probe.value)) : probe;
|
|
3809
|
+
}
|
|
3810
|
+
//#endregion
|
|
3811
|
+
//#region lib/types/scope.js
|
|
3812
|
+
function callingScope(ctx, held) {
|
|
3813
|
+
if (held !== void 0) return held;
|
|
3814
|
+
return scopeOf(ctx);
|
|
3815
|
+
}
|
|
3816
|
+
/** True when a read is deliberately scope-less (global layer only). Callers
|
|
3817
|
+
* pass this to the register so the choice is reviewed, not accidental. */
|
|
3818
|
+
function isGlobalRead(scope) {
|
|
3819
|
+
return scope === void 0;
|
|
3820
|
+
}
|
|
3821
|
+
//#endregion
|
|
3587
3822
|
//#region lib/types/skill-health.js
|
|
3588
3823
|
/**
|
|
3589
3824
|
* Skill structure-health domain (rc.73 A1, 008 design): a SECOND assessment
|
|
@@ -3653,6 +3888,320 @@ function assessStructureHealth(snapshot, thresholds = DEFAULT_HEALTH_THRESHOLDS)
|
|
|
3653
3888
|
};
|
|
3654
3889
|
}
|
|
3655
3890
|
//#endregion
|
|
3891
|
+
//#region lib/types/tool-dispatch.js
|
|
3892
|
+
/**
|
|
3893
|
+
* The family's ONE reader of platform tool-dispatch events.
|
|
3894
|
+
*
|
|
3895
|
+
* The platform records a finished tool call under two different event
|
|
3896
|
+
* vocabularies and only one of them is written per dispatch, chosen by the
|
|
3897
|
+
* mounted tool runtime's mode:
|
|
3898
|
+
*
|
|
3899
|
+
* - native mode: \`tool/call\` (with \`callId\`) settled by \`tool/result\`, the
|
|
3900
|
+
* write sites being core/agent-loop/src/tool-calls.ts:264 and :282.
|
|
3901
|
+
* - PTC mode: \`tool/ptc-dispatch-start\` and \`tool/ptc-dispatch\` (with
|
|
3902
|
+
* \`subCallId\`), the write sites being core/tools/src/ptc.ts:534 and :509.
|
|
3903
|
+
*
|
|
3904
|
+
* A consumer that matches a vocabulary directly therefore goes blind in the
|
|
3905
|
+
* other mode while still appearing to work — the defect this module exists to
|
|
3906
|
+
* make impossible. Every family consumer reads dispatches through
|
|
3907
|
+
* \`ToolDispatchNormalizer\` (or the pure helpers below) and matches on
|
|
3908
|
+
* \`ToolDispatchSignal\` fields only; \`verify-arch-guards\` rule N11 rejects a
|
|
3909
|
+
* dispatch event-type comparison anywhere else.
|
|
3910
|
+
*
|
|
3911
|
+
* ## The one-dispatch invariant
|
|
3912
|
+
*
|
|
3913
|
+
* One dispatch produces exactly one \`ToolDispatchSignal\`, no matter how many
|
|
3914
|
+
* events carry it: a start/settle pair for a PTC sub-dispatch, a call/result
|
|
3915
|
+
* pair for a native call, or both vocabularies for the same call. Deduplication
|
|
3916
|
+
* and outcome folding are keyed by the platform's own per-vocabulary call
|
|
3917
|
+
* identity, which is stable across the pair (the platform's event JSDoc:
|
|
3918
|
+
* "the pairing ids (matching the \`tool/ptc-dispatch-start\` with the same
|
|
3919
|
+
* \`subCallId\`)").
|
|
3920
|
+
*
|
|
3921
|
+
* ## Layer
|
|
3922
|
+
*
|
|
3923
|
+
* Cross-cutting normalization: the reader half of the dispatch vocabulary,
|
|
3924
|
+
* next to \`signals.ts\` (which folds the signals this module emits). Both are
|
|
3925
|
+
* consumers; neither subscribes to the platform bus itself.
|
|
3926
|
+
* @module
|
|
3927
|
+
*/
|
|
3928
|
+
/** The platform event type carrying a PTC sub-dispatch start. */
|
|
3929
|
+
const PTC_DISPATCH_START_EVENT = "tool/ptc-dispatch-start";
|
|
3930
|
+
/** The platform event type carrying a PTC sub-dispatch settle. */
|
|
3931
|
+
const PTC_DISPATCH_EVENT = "tool/ptc-dispatch";
|
|
3932
|
+
/** The platform event type carrying a native tool call. */
|
|
3933
|
+
const NATIVE_CALL_EVENT = "tool/call";
|
|
3934
|
+
/** The platform event type carrying a native tool result. */
|
|
3935
|
+
const NATIVE_RESULT_EVENT = "tool/result";
|
|
3936
|
+
/**
|
|
3937
|
+
* The dispatch vocabulary this module owns. A consumer must never compare
|
|
3938
|
+
* against these literals itself (rule N11); it compares \`kind\` instead.
|
|
3939
|
+
*/
|
|
3940
|
+
const DISPATCH_EVENT_TYPES = [
|
|
3941
|
+
NATIVE_CALL_EVENT,
|
|
3942
|
+
NATIVE_RESULT_EVENT,
|
|
3943
|
+
PTC_DISPATCH_START_EVENT,
|
|
3944
|
+
PTC_DISPATCH_EVENT
|
|
3945
|
+
];
|
|
3946
|
+
/**
|
|
3947
|
+
* \`TypeError\` thrown when a payload that claims to be a dispatch event
|
|
3948
|
+
* violates the platform's event declaration. Named so a deployment can tell this
|
|
3949
|
+
* refusal apart from a generic failure in a log line.
|
|
3950
|
+
*/
|
|
3951
|
+
var ToolDispatchPayloadError = class extends TypeError {
|
|
3952
|
+
name = "ToolDispatchPayloadError";
|
|
3953
|
+
};
|
|
3954
|
+
/** The dispatch event types this module recognizes, as a set. */
|
|
3955
|
+
const DISPATCH_EVENT_TYPE_SET = new Set(DISPATCH_EVENT_TYPES);
|
|
3956
|
+
/** \`name\`-less payloads: an external emitter or a corrupt log can produce these. */
|
|
3957
|
+
const MALFORMED = "malformed";
|
|
3958
|
+
/** One tool-result block of a native result payload, or \`null\` when the payload carries none. */
|
|
3959
|
+
function firstResultBlock(data) {
|
|
3960
|
+
const content = data.message?.content;
|
|
3961
|
+
if (!Array.isArray(content)) return null;
|
|
3962
|
+
for (const entry of content) {
|
|
3963
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
3964
|
+
const block = entry;
|
|
3965
|
+
if (block.type === "tool-result") return block;
|
|
3966
|
+
}
|
|
3967
|
+
return null;
|
|
3968
|
+
}
|
|
3969
|
+
/** Deterministic identity for a dispatch payload that carries no call id. */
|
|
3970
|
+
function payloadIdentity(data) {
|
|
3971
|
+
if (data === void 0) return "";
|
|
3972
|
+
try {
|
|
3973
|
+
return JSON.stringify(data);
|
|
3974
|
+
} catch {
|
|
3975
|
+
return "";
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
/** Read the payload of a recognized dispatch event, or \`null\` for any other event. */
|
|
3979
|
+
function readDispatchRecord(event) {
|
|
3980
|
+
if (!DISPATCH_EVENT_TYPE_SET.has(event.type)) return null;
|
|
3981
|
+
const data = event.data ?? {};
|
|
3982
|
+
if (event.type === "tool/result") {
|
|
3983
|
+
const owner = data.message?.source?.callId;
|
|
3984
|
+
const block = firstResultBlock(data);
|
|
3985
|
+
const callId = typeof owner === "string" && owner !== "" ? owner : typeof block?.toolCallId === "string" && block.toolCallId !== "" ? block.toolCallId : void 0;
|
|
3986
|
+
if (callId === void 0) return null;
|
|
3987
|
+
return {
|
|
3988
|
+
kind: "native",
|
|
3989
|
+
callId,
|
|
3990
|
+
rootCallId: callId,
|
|
3991
|
+
name: "",
|
|
3992
|
+
arguments: void 0,
|
|
3993
|
+
outcome: { ok: data.isError !== true && data.error === void 0 && block?.isError !== true }
|
|
3994
|
+
};
|
|
3995
|
+
}
|
|
3996
|
+
const isPtc = event.type === "tool/ptc-dispatch-start" || event.type === "tool/ptc-dispatch";
|
|
3997
|
+
const identity = isPtc ? data.subCallId : data.callId;
|
|
3998
|
+
const callId = typeof identity === "string" && identity !== "" ? identity : `${event.type}:${payloadIdentity(event.data)}`;
|
|
3999
|
+
const parent = isPtc && typeof data.parentCallId === "string" && data.parentCallId !== "" ? data.parentCallId : void 0;
|
|
4000
|
+
const name = typeof data.name === "string" && data.name !== "" ? data.name : MALFORMED;
|
|
4001
|
+
return {
|
|
4002
|
+
kind: isPtc ? "program" : "native",
|
|
4003
|
+
callId,
|
|
4004
|
+
rootCallId: typeof data.rootCallId === "string" && data.rootCallId !== "" ? data.rootCallId : parent ?? callId,
|
|
4005
|
+
name,
|
|
4006
|
+
arguments: data.arguments,
|
|
4007
|
+
...isPtc && event.type === "tool/ptc-dispatch" ? { outcome: { ok: data.isError !== true } } : {}
|
|
4008
|
+
};
|
|
4009
|
+
}
|
|
4010
|
+
/**
|
|
4011
|
+
* The family's single dispatch ledger: absorbs platform events in log order,
|
|
4012
|
+
* emits one \`ToolDispatchSignal\` per dispatch, and folds later events of the
|
|
4013
|
+
* same dispatch into the record it already emitted.
|
|
4014
|
+
*
|
|
4015
|
+
* Consumers keep the \`ToolDispatchSignal\` object they received and re-read it
|
|
4016
|
+
* later; no consumer needs to correlate events itself.
|
|
4017
|
+
*/
|
|
4018
|
+
var ToolDispatchNormalizer = class {
|
|
4019
|
+
records = /* @__PURE__ */ new Map();
|
|
4020
|
+
/** Call ids of \`run_code\`-class calls, i.e. of dispatches whose sub-dispatches are \`program\`. */
|
|
4021
|
+
programRoots = /* @__PURE__ */ new Set();
|
|
4022
|
+
/**
|
|
4023
|
+
* Absorb one session event.
|
|
4024
|
+
* @param event - the event to absorb; any non-dispatch event is ignored.
|
|
4025
|
+
* @returns the dispatch's signal when this event FIRST reveals the dispatch,
|
|
4026
|
+
* otherwise \`null\` (the paired event of an already-emitted dispatch, or a
|
|
4027
|
+
* non-dispatch event). A \`null\` return is never a dispatch to count again.
|
|
4028
|
+
*/
|
|
4029
|
+
advance(event) {
|
|
4030
|
+
const record = readDispatchRecord(event);
|
|
4031
|
+
if (record === null) return null;
|
|
4032
|
+
if (record.name === "") {
|
|
4033
|
+
const existing = this.records.get(record.callId);
|
|
4034
|
+
if (existing !== void 0 && record.outcome !== void 0) existing.ok = record.outcome.ok;
|
|
4035
|
+
return null;
|
|
4036
|
+
}
|
|
4037
|
+
const existing = this.records.get(record.callId);
|
|
4038
|
+
if (existing !== void 0) {
|
|
4039
|
+
if (record.outcome !== void 0) existing.ok = record.outcome.ok;
|
|
4040
|
+
return null;
|
|
4041
|
+
}
|
|
4042
|
+
const signal = {
|
|
4043
|
+
kind: record.kind === "native" && this.programRoots.has(record.callId) ? "program-root" : record.kind,
|
|
4044
|
+
callId: record.callId,
|
|
4045
|
+
rootCallId: record.rootCallId,
|
|
4046
|
+
name: record.name,
|
|
4047
|
+
arguments: record.arguments,
|
|
4048
|
+
ok: record.outcome?.ok
|
|
4049
|
+
};
|
|
4050
|
+
this.records.set(record.callId, signal);
|
|
4051
|
+
if (record.kind === "program" && record.rootCallId !== record.callId) this.programRoots.add(record.rootCallId);
|
|
4052
|
+
return signal;
|
|
4053
|
+
}
|
|
4054
|
+
/** Every emitted dispatch, in first-seen order. */
|
|
4055
|
+
get signals() {
|
|
4056
|
+
return [...this.records.values()];
|
|
4057
|
+
}
|
|
4058
|
+
/**
|
|
4059
|
+
* Fold every event of a session log, in order.
|
|
4060
|
+
* @param events - the log, oldest first.
|
|
4061
|
+
* @returns one signal per dispatch, in first-seen order.
|
|
4062
|
+
*/
|
|
4063
|
+
foldAll(events) {
|
|
4064
|
+
const emitted = [];
|
|
4065
|
+
for (const event of events) {
|
|
4066
|
+
const signal = this.advance(event);
|
|
4067
|
+
if (signal !== null) emitted.push(signal);
|
|
4068
|
+
}
|
|
4069
|
+
return emitted;
|
|
4070
|
+
}
|
|
4071
|
+
};
|
|
4072
|
+
/**
|
|
4073
|
+
* Read one event into a dispatch record without a ledger (the pure half of the
|
|
4074
|
+
* fold). Unlike the ledger it does NOT dedupe: a caller that walks a log and
|
|
4075
|
+
* emits its own lines needs the call line on the event that opens the result.
|
|
4076
|
+
* @param event - one session event; an absent event (an over-advanced index)
|
|
4077
|
+
* answers \`null\` rather than throwing.
|
|
4078
|
+
* @returns the dispatch this event opens, or \`null\` for any other event.
|
|
4079
|
+
*/
|
|
4080
|
+
function readDispatchSignal(event) {
|
|
4081
|
+
if (event?.type === void 0) return null;
|
|
4082
|
+
const record = readDispatchRecord({
|
|
4083
|
+
type: event.type,
|
|
4084
|
+
...event.data === void 0 ? {} : { data: event.data }
|
|
4085
|
+
});
|
|
4086
|
+
if (record === null || record.name === "") return null;
|
|
4087
|
+
return {
|
|
4088
|
+
kind: record.kind,
|
|
4089
|
+
callId: record.callId,
|
|
4090
|
+
rootCallId: record.rootCallId,
|
|
4091
|
+
name: record.name,
|
|
4092
|
+
arguments: record.arguments
|
|
4093
|
+
};
|
|
4094
|
+
}
|
|
4095
|
+
/** Tool names that READ one skill. The single authority for skill-read detection. */
|
|
4096
|
+
const SKILL_READ_TOOL_NAMES = new Set(["skill"]);
|
|
4097
|
+
/**
|
|
4098
|
+
* Is this dispatched tool name a single-skill read?
|
|
4099
|
+
* @param name - the dispatched tool name.
|
|
4100
|
+
* @returns \`true\` only for the read tool whose arguments name one skill.
|
|
4101
|
+
*/
|
|
4102
|
+
function isSkillReadToolName(name) {
|
|
4103
|
+
return SKILL_READ_TOOL_NAMES.has(name);
|
|
4104
|
+
}
|
|
4105
|
+
/** Tool names whose dispatch opens a code program, i.e. whose sub-dispatches are \`program\`. */
|
|
4106
|
+
const PROGRAM_TOOL_NAMES = new Set(["run_code"]);
|
|
4107
|
+
/**
|
|
4108
|
+
* Does this signal read one skill, and did it not fail?
|
|
4109
|
+
* @param signal - a normalized dispatch.
|
|
4110
|
+
* @returns the skill name the dispatch read, or \`undefined\` when the dispatch
|
|
4111
|
+
* is not a skill read or has failed. A dispatch whose outcome is still pending
|
|
4112
|
+
* counts as a read: the platform settles every started sub-dispatch, so pending
|
|
4113
|
+
* is a live-window state, not a failure.
|
|
4114
|
+
*/
|
|
4115
|
+
function skillReadNameOf(signal) {
|
|
4116
|
+
if (!SKILL_READ_TOOL_NAMES.has(signal.name)) return void 0;
|
|
4117
|
+
if (signal.ok === false) return void 0;
|
|
4118
|
+
let parsed = signal.arguments;
|
|
4119
|
+
if (typeof parsed === "string") try {
|
|
4120
|
+
parsed = JSON.parse(parsed);
|
|
4121
|
+
} catch {
|
|
4122
|
+
return;
|
|
4123
|
+
}
|
|
4124
|
+
if (parsed === null || typeof parsed !== "object") return void 0;
|
|
4125
|
+
const candidate = parsed;
|
|
4126
|
+
const name = typeof candidate.name === "string" ? candidate.name : typeof candidate.skill === "string" ? candidate.skill : "";
|
|
4127
|
+
return name === "" ? void 0 : name;
|
|
4128
|
+
}
|
|
4129
|
+
/**
|
|
4130
|
+
* Fold one session log into its deduplicated dispatches.
|
|
4131
|
+
* @param events - the log, oldest first. Pass an array, not a live snapshot
|
|
4132
|
+
* iterator, when the log can grow while folding.
|
|
4133
|
+
* @returns one signal per dispatch, in first-seen order.
|
|
4134
|
+
*/
|
|
4135
|
+
function foldToolDispatches(events) {
|
|
4136
|
+
return new ToolDispatchNormalizer().foldAll(events);
|
|
4137
|
+
}
|
|
4138
|
+
/**
|
|
4139
|
+
* Every skill name this log read through a dispatch, deduplicated by dispatch.
|
|
4140
|
+
* @param events - the log, oldest first.
|
|
4141
|
+
* @returns the names read by at least one non-failed skill dispatch.
|
|
4142
|
+
*/
|
|
4143
|
+
function collectReadSkillNames(events) {
|
|
4144
|
+
const names = /* @__PURE__ */ new Set();
|
|
4145
|
+
for (const signal of foldToolDispatches(events)) {
|
|
4146
|
+
const name = skillReadNameOf(signal);
|
|
4147
|
+
if (name !== void 0) names.add(name);
|
|
4148
|
+
}
|
|
4149
|
+
return names;
|
|
4150
|
+
}
|
|
4151
|
+
/** Tool names that touch the skill library at all — the review's skill-signal set. */
|
|
4152
|
+
const SKILL_TOOL_NAMES = new Set([...SKILL_READ_TOOL_NAMES, "skill_manage"]);
|
|
4153
|
+
/**
|
|
4154
|
+
* Does this dispatched tool name touch the skill library (read or mutate)?
|
|
4155
|
+
* @param name - the dispatched tool name.
|
|
4156
|
+
* @returns \`true\` for any skill-library tool. This is the review cadence's
|
|
4157
|
+
* skill signal, which has always covered reads and writes alike.
|
|
4158
|
+
*/
|
|
4159
|
+
function isSkillToolName(name) {
|
|
4160
|
+
return SKILL_TOOL_NAMES.has(name);
|
|
4161
|
+
}
|
|
4162
|
+
/**
|
|
4163
|
+
* Does this dispatched tool name open a code program?
|
|
4164
|
+
* @param name - the dispatched tool name.
|
|
4165
|
+
* @returns \`true\` for the tool whose sub-dispatches are \`program\` kind.
|
|
4166
|
+
*/
|
|
4167
|
+
function isProgramToolName(name) {
|
|
4168
|
+
return PROGRAM_TOOL_NAMES.has(name);
|
|
4169
|
+
}
|
|
4170
|
+
/**
|
|
4171
|
+
* Assert that a payload really is the dispatch event it claims to be.
|
|
4172
|
+
*
|
|
4173
|
+
* The normalizer is deliberately lenient (it also folds persisted logs, where a
|
|
4174
|
+
* payload may predate the current declaration); this is the loud gate for the
|
|
4175
|
+
* live path, where a malformed payload means the producer is broken. It never
|
|
4176
|
+
* silently downgrades: an unrecognized event type or a payload missing a
|
|
4177
|
+
* declared field throws a named \`ToolDispatchPayloadError\`.
|
|
4178
|
+
* @param event - the session event to check.
|
|
4179
|
+
* @returns nothing; throws when the payload violates the platform declaration.
|
|
4180
|
+
*/
|
|
4181
|
+
function assertDispatchPayload(event) {
|
|
4182
|
+
const data = event.data ?? {};
|
|
4183
|
+
if (event.type === "tool/call") {
|
|
4184
|
+
if (typeof data.name !== "string" || data.name === "" || typeof data.callId !== "string" || data.callId === "") throw new ToolDispatchPayloadError("tool/call carries no name/callId: the agent-loop write site is broken");
|
|
4185
|
+
return;
|
|
4186
|
+
}
|
|
4187
|
+
if (event.type === "tool/ptc-dispatch-start" || event.type === "tool/ptc-dispatch") {
|
|
4188
|
+
if (typeof data.subCallId !== "string" || data.subCallId === "" || typeof data.name !== "string" || data.name === "") throw new ToolDispatchPayloadError(`${event.type} carries no name/subCallId: the PTC bridge write site is broken`);
|
|
4189
|
+
if (event.type === "tool/ptc-dispatch" && typeof data.isError !== "boolean") throw new ToolDispatchPayloadError("tool/ptc-dispatch carries no boolean isError: the settle outcome is unusable");
|
|
4190
|
+
return;
|
|
4191
|
+
}
|
|
4192
|
+
if (event.type === "tool/result") return;
|
|
4193
|
+
throw new ToolDispatchPayloadError(`${event.type} is not a platform dispatch event; compare signal.kind instead of event types`);
|
|
4194
|
+
}
|
|
4195
|
+
/**
|
|
4196
|
+
* Total dispatches in a log, deduplicated.
|
|
4197
|
+
* @param events - the log, oldest first.
|
|
4198
|
+
* @returns the number of distinct dispatches, one per dispatch regardless of
|
|
4199
|
+
* how many events carried it.
|
|
4200
|
+
*/
|
|
4201
|
+
function countDispatches(events) {
|
|
4202
|
+
return foldToolDispatches(events).length;
|
|
4203
|
+
}
|
|
4204
|
+
//#endregion
|
|
3656
4205
|
//#region lib/types/signals.js
|
|
3657
4206
|
/**
|
|
3658
4207
|
* Deterministic review signal gate.
|
|
@@ -3684,6 +4233,23 @@ function textOfBlock(block) {
|
|
|
3684
4233
|
const candidate = block;
|
|
3685
4234
|
return candidate.type === "text" && typeof candidate.text === "string" ? candidate.text : "";
|
|
3686
4235
|
}
|
|
4236
|
+
/**
|
|
4237
|
+
* Per-turn dispatch ledgers, keyed by the signal object they feed.
|
|
4238
|
+
*
|
|
4239
|
+
* `observeEvent` folds ONE event at a time, so the dedup state has to outlive
|
|
4240
|
+
* the call. One WeakMap entry per `TurnSignals`, so a finished fold's ledger is
|
|
4241
|
+
* collectable with the fold itself and two concurrent turns never share one.
|
|
4242
|
+
*/
|
|
4243
|
+
const dispatchLedgers = /* @__PURE__ */ new WeakMap();
|
|
4244
|
+
/** The ledger folding this signal's turn, created on first use. */
|
|
4245
|
+
function normalizerForSignal(signal) {
|
|
4246
|
+
let normalizer = dispatchLedgers.get(signal);
|
|
4247
|
+
if (normalizer === void 0) {
|
|
4248
|
+
normalizer = new ToolDispatchNormalizer();
|
|
4249
|
+
dispatchLedgers.set(signal, normalizer);
|
|
4250
|
+
}
|
|
4251
|
+
return normalizer;
|
|
4252
|
+
}
|
|
3687
4253
|
/** Fold one session event into the current turn observation. */
|
|
3688
4254
|
function observeEvent(signal, event) {
|
|
3689
4255
|
const data = event.data;
|
|
@@ -3704,11 +4270,10 @@ function observeEvent(signal, event) {
|
|
|
3704
4270
|
signal.assistantChars += text.length;
|
|
3705
4271
|
return;
|
|
3706
4272
|
}
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
}
|
|
4273
|
+
const dispatch = normalizerForSignal(signal).advance(event);
|
|
4274
|
+
if (dispatch === null) return;
|
|
4275
|
+
if (dispatch.kind !== "program") signal.toolCalls += 1;
|
|
4276
|
+
if (isSkillToolName(dispatch.name)) signal.skillSignal = true;
|
|
3712
4277
|
}
|
|
3713
4278
|
/** Compute review cadence after `turn/end`. */
|
|
3714
4279
|
function advanceReview(state, turn, signal, config) {
|
|
@@ -3815,7 +4380,8 @@ function missingSupportPointers(body, supportFiles) {
|
|
|
3815
4380
|
/** Duplicate `## heading` occurrences: singleton results default to head of the file. */
|
|
3816
4381
|
function duplicateHeadings(body) {
|
|
3817
4382
|
const counts = /* @__PURE__ */ new Map();
|
|
3818
|
-
for (const
|
|
4383
|
+
for (const raw of body.split("\n")) {
|
|
4384
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
3819
4385
|
const m = /^##\s+(.+)$/.exec(line);
|
|
3820
4386
|
if (m?.[1]) {
|
|
3821
4387
|
const heading = m[1].trim();
|
|
@@ -3832,7 +4398,8 @@ function overlongLines(body, max = DRIFT_MAX_LINE_CHARS) {
|
|
|
3832
4398
|
const out = [];
|
|
3833
4399
|
const lines = body.split("\n");
|
|
3834
4400
|
for (let index = 0; index < lines.length; index += 1) {
|
|
3835
|
-
const
|
|
4401
|
+
const raw = lines[index] ?? "";
|
|
4402
|
+
const length = raw.endsWith("\r") ? raw.length - 1 : raw.length;
|
|
3836
4403
|
if (length > max) out.push({
|
|
3837
4404
|
lineNo: index + 1,
|
|
3838
4405
|
chars: length
|
|
@@ -3887,7 +4454,7 @@ function computeDriftSignals(snapshots) {
|
|
|
3887
4454
|
bodyText: body,
|
|
3888
4455
|
supportGroups: supportGroupCount(supportFiles)
|
|
3889
4456
|
}, DEFAULT_HEALTH_THRESHOLDS).dims.stampDensityPerKb;
|
|
3890
|
-
signals.push(density === null ? sig("stamp_density", "pass",
|
|
4457
|
+
signals.push(density === null ? sig("stamp_density", "pass", "below-min-body") : sig("stamp_density", density >= DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb ? "over" : "pass", `${density.toFixed(2)}/KB`, `${DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb}/KB`));
|
|
3891
4458
|
signals.push(sig("body_size", body.length >= DEFAULT_HEALTH_THRESHOLDS.softBodyChars ? "over" : "pass", `${body.length}`, `${DEFAULT_HEALTH_THRESHOLDS.softBodyChars}`));
|
|
3892
4459
|
const dupes = duplicateHeadings(body);
|
|
3893
4460
|
signals.push(dupes.length === 0 ? sig("dup_heading", "pass", "none") : sig("dup_heading", "over", dupes.map((d) => `${d.heading}(${d.count})`).join(", "), "count >= 2"));
|
|
@@ -4134,23 +4701,35 @@ function markerPath(dir, marker) {
|
|
|
4134
4701
|
* platform ignored the file: family visibility split from platform visibility,
|
|
4135
4702
|
* which is exactly what a strict-YAML frontmatter is supposed to prevent.
|
|
4136
4703
|
*/
|
|
4704
|
+
/**
|
|
4705
|
+
* S1.1 (v37 P1-4): the frontmatter block is split on LF and every line keeps its
|
|
4706
|
+
* own `\r`, so the byte-preserving rebuild (`lines.join('\n')`) is exact. Line
|
|
4707
|
+
* MATCHERS must therefore look at a CR-stripped view: JS `.` does not match `\r`,
|
|
4708
|
+
* so `(.*)$` never reaches the end of a CRLF line.
|
|
4709
|
+
* @param line - one block line, possibly CR-terminated.
|
|
4710
|
+
* @returns the line without its trailing CR.
|
|
4711
|
+
*/
|
|
4712
|
+
function withoutCr(line) {
|
|
4713
|
+
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
4714
|
+
}
|
|
4137
4715
|
function frontmatterBlock(content) {
|
|
4138
4716
|
if (!content.trimStart().startsWith("---")) return null;
|
|
4139
|
-
const
|
|
4140
|
-
const
|
|
4717
|
+
const firstBreak = content.indexOf("\n");
|
|
4718
|
+
const nl = firstBreak > 0 && content[firstBreak - 1] === "\r" ? "\r\n" : "\n";
|
|
4719
|
+
const lines = content.split("\n");
|
|
4141
4720
|
if ((lines[0] ?? "").replace(/\r$/, "") !== "---") return null;
|
|
4142
4721
|
let end = -1;
|
|
4143
4722
|
for (let i = 1; i < lines.length; i++) {
|
|
4144
4723
|
const line = lines[i];
|
|
4145
4724
|
if (line === void 0) continue;
|
|
4146
|
-
if (line
|
|
4725
|
+
if (line === "---" || line === "---\r") {
|
|
4147
4726
|
end = i;
|
|
4148
4727
|
break;
|
|
4149
4728
|
}
|
|
4150
4729
|
}
|
|
4151
4730
|
if (end < 0) return null;
|
|
4152
4731
|
return {
|
|
4153
|
-
block: lines.slice(1, end).join(
|
|
4732
|
+
block: lines.slice(1, end).join("\n"),
|
|
4154
4733
|
lines,
|
|
4155
4734
|
end,
|
|
4156
4735
|
nl
|
|
@@ -4167,8 +4746,9 @@ function frontmatterBlock(content) {
|
|
|
4167
4746
|
function unsafeFrontmatterEntries(block, nl) {
|
|
4168
4747
|
const found = [];
|
|
4169
4748
|
for (const line of block.split(nl)) {
|
|
4170
|
-
|
|
4171
|
-
|
|
4749
|
+
const clean = withoutCr(line);
|
|
4750
|
+
if (clean.includes("\n") || clean.includes("\r")) continue;
|
|
4751
|
+
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(clean);
|
|
4172
4752
|
if (!match) continue;
|
|
4173
4753
|
const key = match[1];
|
|
4174
4754
|
if (key === void 0) continue;
|
|
@@ -4181,9 +4761,22 @@ function unsafeFrontmatterEntries(block, nl) {
|
|
|
4181
4761
|
return found;
|
|
4182
4762
|
}
|
|
4183
4763
|
/**
|
|
4764
|
+
* Frontmatter fields the platform catalog reads as STRINGS (`stringField` /
|
|
4765
|
+
* `optionalString` in `skill-filesystem`): any other YAML type reads as
|
|
4766
|
+
* ABSENT there, and an absent `name`/`description` makes the catalog ignore the
|
|
4767
|
+
* whole file. The family publishes text for these keys anyway (its lenient
|
|
4768
|
+
* read), which is the one split a write must not create.
|
|
4769
|
+
*/
|
|
4770
|
+
const PLATFORM_STRING_FIELDS = [
|
|
4771
|
+
"name",
|
|
4772
|
+
"description",
|
|
4773
|
+
"whenToUse"
|
|
4774
|
+
];
|
|
4775
|
+
/**
|
|
4184
4776
|
* Frontmatter values as the STRICT platform catalog reads them — js-yaml, the
|
|
4185
4777
|
* parser `normalizeFrontmatter` also verifies rewrites with — or `null` when
|
|
4186
|
-
* the block is not loadable as a YAML mapping.
|
|
4778
|
+
* the block is not loadable as a YAML mapping. Also reports the platform string
|
|
4779
|
+
* fields whose value is not a string ({@link PlatformStringSplit}).
|
|
4187
4780
|
*
|
|
4188
4781
|
* Scalars publish their text (`name`, `description`, `whenToUse` are strings by
|
|
4189
4782
|
* contract; a number/boolean-shaped value keeps the text the family always
|
|
@@ -4195,17 +4788,28 @@ function unsafeFrontmatterEntries(block, nl) {
|
|
|
4195
4788
|
* carried.
|
|
4196
4789
|
*/
|
|
4197
4790
|
function strictFrontmatterValues(block) {
|
|
4198
|
-
if (block.trim() === "") return
|
|
4791
|
+
if (block.trim() === "") return {
|
|
4792
|
+
values: /* @__PURE__ */ new Map(),
|
|
4793
|
+
split: []
|
|
4794
|
+
};
|
|
4199
4795
|
let loaded;
|
|
4200
4796
|
try {
|
|
4201
4797
|
loaded = load(block);
|
|
4202
4798
|
} catch {
|
|
4203
4799
|
return null;
|
|
4204
4800
|
}
|
|
4205
|
-
if (loaded === null || loaded === void 0) return
|
|
4801
|
+
if (loaded === null || loaded === void 0) return {
|
|
4802
|
+
values: /* @__PURE__ */ new Map(),
|
|
4803
|
+
split: []
|
|
4804
|
+
};
|
|
4206
4805
|
if (typeof loaded !== "object" || Array.isArray(loaded)) return null;
|
|
4207
4806
|
const values = /* @__PURE__ */ new Map();
|
|
4807
|
+
const split = [];
|
|
4208
4808
|
for (const [key, value] of Object.entries(loaded)) {
|
|
4809
|
+
if (typeof value !== "string" && PLATFORM_STRING_FIELDS.includes(key)) split.push({
|
|
4810
|
+
key,
|
|
4811
|
+
kind: value === null || value === void 0 ? "null" : Array.isArray(value) ? "sequence" : typeof value === "object" ? "mapping" : "scalar"
|
|
4812
|
+
});
|
|
4209
4813
|
if (typeof value === "string") {
|
|
4210
4814
|
values.set(key, value.trim());
|
|
4211
4815
|
continue;
|
|
@@ -4219,7 +4823,10 @@ function strictFrontmatterValues(block) {
|
|
|
4219
4823
|
continue;
|
|
4220
4824
|
}
|
|
4221
4825
|
}
|
|
4222
|
-
return
|
|
4826
|
+
return {
|
|
4827
|
+
values,
|
|
4828
|
+
split
|
|
4829
|
+
};
|
|
4223
4830
|
}
|
|
4224
4831
|
/**
|
|
4225
4832
|
* Lenient line scan, used only for a block the strict parser rejects: the
|
|
@@ -4231,7 +4838,7 @@ function lenientFrontmatterValues(block, nl) {
|
|
|
4231
4838
|
const values = /* @__PURE__ */ new Map();
|
|
4232
4839
|
const blockLines = block.split(nl);
|
|
4233
4840
|
for (let i = 0; i < blockLines.length; i += 1) {
|
|
4234
|
-
const line = blockLines[i] ?? "";
|
|
4841
|
+
const line = withoutCr(blockLines[i] ?? "");
|
|
4235
4842
|
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
|
|
4236
4843
|
if (!match) continue;
|
|
4237
4844
|
const [, key, value] = match;
|
|
@@ -4280,16 +4887,17 @@ function lenientFrontmatterValues(block, nl) {
|
|
|
4280
4887
|
function readFrontmatterBlock(content) {
|
|
4281
4888
|
const found = frontmatterBlock(content);
|
|
4282
4889
|
if (!found) return null;
|
|
4283
|
-
const body = found.lines.slice(found.end + 1).join(
|
|
4890
|
+
const body = found.lines.slice(found.end + 1).join("\n").trim();
|
|
4284
4891
|
const strict = strictFrontmatterValues(found.block);
|
|
4285
|
-
const values = strict ?? lenientFrontmatterValues(found.block, found.nl);
|
|
4892
|
+
const values = strict?.values ?? lenientFrontmatterValues(found.block, found.nl);
|
|
4286
4893
|
const frontmatter = {};
|
|
4287
4894
|
for (const [key, value] of values) frontmatter[key] = value;
|
|
4288
4895
|
return {
|
|
4289
4896
|
frontmatter,
|
|
4290
4897
|
body,
|
|
4291
4898
|
unsafeValues: unsafeFrontmatterEntries(found.block, found.nl),
|
|
4292
|
-
strictFailed: strict === null
|
|
4899
|
+
strictFailed: strict === null,
|
|
4900
|
+
platformStringSplit: strict?.split ?? []
|
|
4293
4901
|
};
|
|
4294
4902
|
}
|
|
4295
4903
|
/**
|
|
@@ -4309,7 +4917,8 @@ function parseFrontmatter(content) {
|
|
|
4309
4917
|
frontmatter: read.frontmatter,
|
|
4310
4918
|
body: read.body,
|
|
4311
4919
|
unsafeValues: read.unsafeValues,
|
|
4312
|
-
catalogInvalid: read.strictFailed || read.unsafeValues.length > 0
|
|
4920
|
+
catalogInvalid: read.strictFailed || read.unsafeValues.length > 0 || read.platformStringSplit.length > 0,
|
|
4921
|
+
platformStringSplit: read.platformStringSplit
|
|
4313
4922
|
};
|
|
4314
4923
|
}
|
|
4315
4924
|
/**
|
|
@@ -4324,7 +4933,7 @@ function parseFrontmatter(content) {
|
|
|
4324
4933
|
*/
|
|
4325
4934
|
function frontmatterCatalogInvalid(content) {
|
|
4326
4935
|
const read = readFrontmatterBlock(content);
|
|
4327
|
-
if (read !== null) return read.strictFailed || read.unsafeValues.length > 0;
|
|
4936
|
+
if (read !== null) return read.strictFailed || read.unsafeValues.length > 0 || read.platformStringSplit.length > 0;
|
|
4328
4937
|
return frontmatterBlock(content.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n")) !== null;
|
|
4329
4938
|
}
|
|
4330
4939
|
/** YAML plain-scalar hazards that make an UNQUOTED frontmatter value
|
|
@@ -4384,16 +4993,16 @@ function normalizeFrontmatter(content) {
|
|
|
4384
4993
|
fields: [],
|
|
4385
4994
|
issues: []
|
|
4386
4995
|
};
|
|
4387
|
-
const { lines, end
|
|
4996
|
+
const { lines, end } = block;
|
|
4388
4997
|
const fields = [];
|
|
4389
4998
|
const issues = [];
|
|
4390
4999
|
const seen = /* @__PURE__ */ new Set();
|
|
4391
5000
|
const originalValues = /* @__PURE__ */ new Map();
|
|
4392
5001
|
let changed = false;
|
|
4393
5002
|
for (let i = 1; i < end; i++) {
|
|
4394
|
-
const
|
|
4395
|
-
if (
|
|
4396
|
-
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(
|
|
5003
|
+
const raw = lines[i];
|
|
5004
|
+
if (raw === void 0) continue;
|
|
5005
|
+
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(withoutCr(raw));
|
|
4397
5006
|
if (!match) continue;
|
|
4398
5007
|
const key = match[1];
|
|
4399
5008
|
if (key === void 0) continue;
|
|
@@ -4408,7 +5017,7 @@ function normalizeFrontmatter(content) {
|
|
|
4408
5017
|
issues.push(`${key}: value contains control characters — clean them manually`);
|
|
4409
5018
|
continue;
|
|
4410
5019
|
}
|
|
4411
|
-
lines[i] = `${key}: ${value.includes("\"") || value.includes("\\") ? `'${value.replace(/'/g, "''")}'` : `"${value}"`}`;
|
|
5020
|
+
lines[i] = `${key}: ${value.includes("\"") || value.includes("\\") ? `'${value.replace(/'/g, "''")}'` : `"${value}"`}${raw.endsWith("\r") ? "\r" : ""}`;
|
|
4412
5021
|
originalValues.set(key, value);
|
|
4413
5022
|
fields.push(key);
|
|
4414
5023
|
changed = true;
|
|
@@ -4425,12 +5034,12 @@ function normalizeFrontmatter(content) {
|
|
|
4425
5034
|
fields: [],
|
|
4426
5035
|
issues
|
|
4427
5036
|
};
|
|
4428
|
-
const rewrittenBlock = lines.slice(1, end).join(
|
|
5037
|
+
const rewrittenBlock = lines.slice(1, end).join("\n");
|
|
4429
5038
|
try {
|
|
4430
5039
|
const parsed = load(rewrittenBlock);
|
|
4431
5040
|
for (const key of fields) if (String(parsed[key]) !== originalValues.get(key)) throw new Error(`rewritten value for ${key} differs from the original`);
|
|
4432
5041
|
return {
|
|
4433
|
-
content: lines.join(
|
|
5042
|
+
content: lines.join("\n"),
|
|
4434
5043
|
changed: true,
|
|
4435
5044
|
fields,
|
|
4436
5045
|
issues
|
|
@@ -4464,16 +5073,36 @@ function relatedSkillNames(content, exclude) {
|
|
|
4464
5073
|
}
|
|
4465
5074
|
return [...names];
|
|
4466
5075
|
}
|
|
4467
|
-
|
|
5076
|
+
/** S1.2 (v37 P2-1): the content limit applies to the bytes that LAND ON DISK.
|
|
5077
|
+
* Every write normalizes with `trimEnd() + '\n'`, so judging the raw argument let
|
|
5078
|
+
* a 100_000-character body with no trailing newline land as 100_001 bytes — and
|
|
5079
|
+
* every later patch/update of that skill was then refused, which made it
|
|
5080
|
+
* unmaintainable through `skill_manage` with no repair path at all. */
|
|
5081
|
+
function skillMdOnDisk(content) {
|
|
5082
|
+
return content.trimEnd() + "\n";
|
|
5083
|
+
}
|
|
5084
|
+
/** Whether `content` would exceed `limit` once written. */
|
|
5085
|
+
function exceedsContentLimit(content, limit) {
|
|
5086
|
+
return skillMdOnDisk(content).length > limit;
|
|
5087
|
+
}
|
|
5088
|
+
/** S1.2: the repair path — a write that makes an already-over-limit file smaller.
|
|
5089
|
+
* Only a NET SHRINK is exempt; an equal or larger write stays refused. */
|
|
5090
|
+
function shrinksOverLimit(next, current, limit) {
|
|
5091
|
+
if (current === null || current === void 0 || !exceedsContentLimit(current, limit)) return false;
|
|
5092
|
+
return skillMdOnDisk(next).length < skillMdOnDisk(current).length;
|
|
5093
|
+
}
|
|
5094
|
+
function validateFrontmatter(content, expectedName, limits = DEFAULT_SKILL_LIMITS, current) {
|
|
4468
5095
|
const parsed = parseFrontmatter(content);
|
|
4469
5096
|
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.";
|
|
5097
|
+
const unreadable = parsed.platformStringSplit.filter((entry) => entry.kind === "sequence" || entry.kind === "mapping");
|
|
5098
|
+
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.`;
|
|
4470
5099
|
if (!parsed.frontmatter.name) return "Frontmatter must include a name field.";
|
|
4471
5100
|
if (!SKILL_NAME_RE.test(parsed.frontmatter.name)) return `Invalid skill name "${parsed.frontmatter.name}" — use lowercase letters, digits, and hyphens.`;
|
|
4472
5101
|
if (parsed.frontmatter.name.length > limits.maxNameLength) return `Skill name exceeds ${limits.maxNameLength} characters.`;
|
|
4473
5102
|
if (expectedName && parsed.frontmatter.name !== expectedName) return `Frontmatter name "${parsed.frontmatter.name}" does not match target skill "${expectedName}".`;
|
|
4474
5103
|
if (!parsed.frontmatter.description) return "Frontmatter must include a description field.";
|
|
4475
5104
|
if (parsed.frontmatter.description.length > limits.maxDescriptionLength) return `Description exceeds ${limits.maxDescriptionLength} characters.`;
|
|
4476
|
-
if (content.
|
|
5105
|
+
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.`;
|
|
4477
5106
|
return null;
|
|
4478
5107
|
}
|
|
4479
5108
|
/**
|
|
@@ -5310,7 +5939,8 @@ var SkillLibrary = class {
|
|
|
5310
5939
|
ok: false,
|
|
5311
5940
|
message: `Skill "${name}" is protected (${protection}).`
|
|
5312
5941
|
};
|
|
5313
|
-
const
|
|
5942
|
+
const currentForLimit = await this.io.readText(path).catch(() => null);
|
|
5943
|
+
const validation = validateFrontmatter(content, name, this.limits, currentForLimit);
|
|
5314
5944
|
if (validation) return {
|
|
5315
5945
|
ok: false,
|
|
5316
5946
|
message: validation
|
|
@@ -5322,7 +5952,7 @@ var SkillLibrary = class {
|
|
|
5322
5952
|
};
|
|
5323
5953
|
const finalContent = norm.changed ? norm.content : content;
|
|
5324
5954
|
if (norm.changed) {
|
|
5325
|
-
const revalidated = validateFrontmatter(finalContent, name, this.limits);
|
|
5955
|
+
const revalidated = validateFrontmatter(finalContent, name, this.limits, currentForLimit);
|
|
5326
5956
|
if (revalidated) return {
|
|
5327
5957
|
ok: false,
|
|
5328
5958
|
message: revalidated
|
|
@@ -5447,7 +6077,7 @@ var SkillLibrary = class {
|
|
|
5447
6077
|
let writeContent = patched;
|
|
5448
6078
|
let normalizedFields;
|
|
5449
6079
|
if (target === skillMd) {
|
|
5450
|
-
const validation = validateFrontmatter(patched, name, this.limits);
|
|
6080
|
+
const validation = validateFrontmatter(patched, name, this.limits, md);
|
|
5451
6081
|
if (validation) return {
|
|
5452
6082
|
result: {
|
|
5453
6083
|
ok: false,
|
|
@@ -5466,7 +6096,7 @@ var SkillLibrary = class {
|
|
|
5466
6096
|
if (norm.changed) {
|
|
5467
6097
|
writeContent = norm.content;
|
|
5468
6098
|
normalizedFields = norm.fields;
|
|
5469
|
-
const revalidated = validateFrontmatter(writeContent, name, this.limits);
|
|
6099
|
+
const revalidated = validateFrontmatter(writeContent, name, this.limits, md);
|
|
5470
6100
|
if (revalidated) return {
|
|
5471
6101
|
result: {
|
|
5472
6102
|
ok: false,
|
|
@@ -5483,7 +6113,9 @@ var SkillLibrary = class {
|
|
|
5483
6113
|
},
|
|
5484
6114
|
write: null
|
|
5485
6115
|
};
|
|
5486
|
-
|
|
6116
|
+
const overLimit = exceedsContentLimit(writeContent, this.limits.maxSkillContentChars);
|
|
6117
|
+
const repairing = shrinksOverLimit(writeContent, md, this.limits.maxSkillContentChars);
|
|
6118
|
+
if (overLimit && target === skillMd && !repairing) return {
|
|
5487
6119
|
result: {
|
|
5488
6120
|
ok: false,
|
|
5489
6121
|
message: `Patched content exceeds ${this.limits.maxSkillContentChars} characters. Consider splitting into a smaller SKILL.md with supporting files.`
|
|
@@ -5577,8 +6209,8 @@ var SkillLibrary = class {
|
|
|
5577
6209
|
* each support dir (write_file's lock sits next to its file). Residual:
|
|
5578
6210
|
* NESTED support-subdir locks and the probe→rename TOCTOU itself remain
|
|
5579
6211
|
* fail-safe (renameWithRetry rides the write out; the writer's locked
|
|
5580
|
-
* re-read refuses on the moved-away file)
|
|
5581
|
-
*
|
|
6212
|
+
* re-read refuses on the moved-away file). v37 (P2-8): a residue `.lock` whose
|
|
6213
|
+
* holder is GONE is not a writer — `decideTakeover` owns that verdict.
|
|
5582
6214
|
*/
|
|
5583
6215
|
async hasWriteLock(dir) {
|
|
5584
6216
|
const markerLocks = MARKER_LOCK_NAMES.map((name) => join(dir, name));
|
|
@@ -5597,11 +6229,12 @@ var SkillLibrary = class {
|
|
|
5597
6229
|
}
|
|
5598
6230
|
return false;
|
|
5599
6231
|
}
|
|
5600
|
-
/** P2 (v17): a file
|
|
5601
|
-
* `pid:token
|
|
5602
|
-
*
|
|
5603
|
-
*
|
|
5604
|
-
*
|
|
6232
|
+
/** P2 (v17): a `*.lock` file counts as a writer lock only when its body
|
|
6233
|
+
* carries the io layer's protocol — `pid:token`, a bare pid, or an empty body
|
|
6234
|
+
* (a creator between create and body write); anything else is user content
|
|
6235
|
+
* (suffix-only matching refused archiving and deleted user files on restore).
|
|
6236
|
+
* v37 (P2-8): the verdict is `decideTakeover`'s, so the mover refuses exactly
|
|
6237
|
+
* the lock the io layer refuses to reclaim, the 30s empty window included. */
|
|
5605
6238
|
async isWriterLock(lockPath) {
|
|
5606
6239
|
let body;
|
|
5607
6240
|
try {
|
|
@@ -5610,7 +6243,19 @@ var SkillLibrary = class {
|
|
|
5610
6243
|
return true;
|
|
5611
6244
|
}
|
|
5612
6245
|
if (body === null) return false;
|
|
5613
|
-
|
|
6246
|
+
const trimmed = body.trim();
|
|
6247
|
+
if (trimmed !== "" && !/^\d+$/.test(trimmed) && !LOCK_BODY_RE.test(trimmed)) return false;
|
|
6248
|
+
let mtimeMs = null;
|
|
6249
|
+
try {
|
|
6250
|
+
mtimeMs = await this.io.mtime?.(lockPath) ?? null;
|
|
6251
|
+
} catch {
|
|
6252
|
+
mtimeMs = null;
|
|
6253
|
+
}
|
|
6254
|
+
return decideTakeover({
|
|
6255
|
+
body,
|
|
6256
|
+
mtimeMs: mtimeMs ?? Date.now(),
|
|
6257
|
+
alive: isProcessAlive
|
|
6258
|
+
}) === "none";
|
|
5614
6259
|
}
|
|
5615
6260
|
/** P2 (v16): best-effort removal of lock residue inside a RESTORED tree.
|
|
5616
6261
|
* A1-2/A1-7 (v18): the sweep now covers the marker locks the probe checks
|
|
@@ -5984,7 +6629,7 @@ var SkillLibrary = class {
|
|
|
5984
6629
|
ok: false,
|
|
5985
6630
|
message: "SKILL.md has no valid frontmatter; refusing to restructure."
|
|
5986
6631
|
};
|
|
5987
|
-
const header = block.lines.slice(0, block.end + 1).join(
|
|
6632
|
+
const header = block.lines.slice(0, block.end + 1).join("\n");
|
|
5988
6633
|
const plan = planRestructureSections(md.slice(header.length).replace(/\r\n/g, "\n"), moves);
|
|
5989
6634
|
if ("error" in plan) return {
|
|
5990
6635
|
ok: false,
|
|
@@ -6105,13 +6750,16 @@ var SkillLibrary = class {
|
|
|
6105
6750
|
if (this.transact) {
|
|
6106
6751
|
const baseline = entry.expected === void 0 ? entry.previous : entry.expected;
|
|
6107
6752
|
const drift = { seen: false };
|
|
6753
|
+
const ran = { done: false };
|
|
6108
6754
|
await this.transact(this.io, entry.target, (current) => {
|
|
6755
|
+
ran.done = true;
|
|
6109
6756
|
if (current !== baseline) {
|
|
6110
6757
|
drift.seen = true;
|
|
6111
6758
|
return current;
|
|
6112
6759
|
}
|
|
6113
6760
|
return entry.content;
|
|
6114
6761
|
});
|
|
6762
|
+
if (!ran.done) throw new Error(`internal error: the write transaction for ${entry.target} did not invoke the task; nothing was written`);
|
|
6115
6763
|
if (drift.seen) throw new Error(`concurrent modification detected: ${entry.target} changed after the plan was computed (a concurrent writer won the race); no further writes were performed`);
|
|
6116
6764
|
} else await this.io.writeText(entry.target, entry.content);
|
|
6117
6765
|
} catch (error) {
|
|
@@ -6359,11 +7007,18 @@ var SkillLibrary = class {
|
|
|
6359
7007
|
message: `"${filePath}" is not a readable regular file — remove the files inside it one by one.`
|
|
6360
7008
|
};
|
|
6361
7009
|
let verdict = anchorVerdict(anchor, before);
|
|
6362
|
-
if (verdict === "match" && this.transact)
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
7010
|
+
if (verdict === "match" && this.transact) {
|
|
7011
|
+
const ran = { done: false };
|
|
7012
|
+
await this.transact(this.io, target, (current) => {
|
|
7013
|
+
ran.done = true;
|
|
7014
|
+
verdict = anchorVerdict(anchor, current);
|
|
7015
|
+
return verdict === "match" ? null : current;
|
|
7016
|
+
});
|
|
7017
|
+
if (!ran.done) return {
|
|
7018
|
+
ok: false,
|
|
7019
|
+
message: "internal error: the delete transaction did not invoke the task; nothing was removed"
|
|
7020
|
+
};
|
|
7021
|
+
} else if (verdict === "match") await this.io.remove(target);
|
|
6367
7022
|
if (verdict !== "match") return anchorRefusalFile(name, filePath, verdict);
|
|
6368
7023
|
await this.audit(name, "remove_file", before, null, `removed ${filePath}`);
|
|
6369
7024
|
this.notifyMutation({
|
|
@@ -6668,8 +7323,8 @@ var SkillLibrary = class {
|
|
|
6668
7323
|
let rootEntries;
|
|
6669
7324
|
try {
|
|
6670
7325
|
rootEntries = await this.io.list(this.root);
|
|
6671
|
-
} catch {
|
|
6672
|
-
|
|
7326
|
+
} catch (error) {
|
|
7327
|
+
throw new Error(`snapshot restore refused: the skill root could not be listed (${error instanceof Error ? error.message : String(error)}) — nothing was changed`);
|
|
6673
7328
|
}
|
|
6674
7329
|
for (const entry of rootEntries) {
|
|
6675
7330
|
if (entry === ".archive" || entry === ".backups" || entry === ".mutations.json" || entry === ".curator-suppressed.json") continue;
|
|
@@ -6705,5 +7360,11 @@ var SkillLibrary = class {
|
|
|
6705
7360
|
}
|
|
6706
7361
|
}
|
|
6707
7362
|
};
|
|
7363
|
+
function newSkillLibrary(options) {
|
|
7364
|
+
const { config, io, limits, ctx, transact, threatExemptLabels } = options;
|
|
7365
|
+
return new SkillLibrary(resolveSkillsRoot(config ?? {}), io, limits, ctx ? (event) => {
|
|
7366
|
+
ctx.emit("evolution/skill-mutated", event);
|
|
7367
|
+
} : void 0, transact, [...threatExemptLabels ?? []]);
|
|
7368
|
+
}
|
|
6708
7369
|
//#endregion
|
|
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,
|
|
7370
|
+
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, 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, 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, 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, isAbsent, isCommittedWarning, isGlobalRead, isPresent, isProcessAlive, isProgramToolName, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, neutralizePromptVariables, newSkillLibrary, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseLockBody, pendingSelfCleanup, probeAbsent, probePresent, probeUnknown, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renameWithRetry, renderCuratorReportMarkdown, resolveOrigins, resolveRootConfig, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillReadNameOf, skillsRoot, suppressedFile, sweepReviewChannelSessions, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, writeDurableTmp, yamlPlainScalarNeedsQuotes };
|