@lmzhen/dsh-evolution-core 0.17.1 → 0.18.1
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 +29 -0
- package/lib/index.js +540 -43
- package/lib/types/constants.d.ts +19 -0
- package/lib/types/errors.d.ts +0 -1
- package/lib/types/events.d.ts +26 -1
- package/lib/types/fold-untrusted.d.ts +35 -0
- package/lib/types/index.d.ts +7 -2
- package/lib/types/memory-store.d.ts +3 -2
- package/lib/types/namespace-migration.d.ts +0 -1
- package/lib/types/numeric.d.ts +20 -0
- package/lib/types/params.d.ts +28 -2
- package/lib/types/payload.d.ts +32 -0
- package/lib/types/platform-services.d.ts +33 -0
- package/lib/types/probe.d.ts +28 -0
- package/lib/types/prompts.d.ts +2 -2
- package/lib/types/skill-reads.d.ts +13 -4
- package/lib/types/skill-store.d.ts +24 -2
- package/package.json +1 -1
- package/persisted-write-inventory.json +47 -6
package/lib/index.js
CHANGED
|
@@ -539,6 +539,25 @@ const CONTENT_SPLIT_HINT = "Consider splitting into a smaller SKILL.md with supp
|
|
|
539
539
|
* their own copy of the literal; the bound is one protocol constant.
|
|
540
540
|
* (v19 P2-10 corrected the value from 2^32-1 to Node's real 2^31-1 ceiling.) */
|
|
541
541
|
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
542
|
+
/**
|
|
543
|
+
* Model-facing budget of one `maintenance_probe` call (ms) — a HANG GUARD, not a
|
|
544
|
+
* performance knob.
|
|
545
|
+
*
|
|
546
|
+
* Measured (2026-10-10): one probe costs 188/220/225 ms on the deployed library
|
|
547
|
+
* (28 skills / 418 KB) and scales linearly at ~5.5 ms per skill (100 skills
|
|
548
|
+
* 0.4-0.6 s, 400 skills 1.9-2.5 s), so this default is ~50x the worst measured
|
|
549
|
+
* case and ~600x the deployed one — it will not fire in normal use, and the
|
|
550
|
+
* ~305 s desktop interaction wall is ~5e4 skills away.
|
|
551
|
+
*
|
|
552
|
+
* It exists for the state the probe cannot report itself: a provider that never
|
|
553
|
+
* answers. The platform's tool-timeout guard reads this budget off the tool
|
|
554
|
+
* definition and turns the hang into a diagnosable `tool call timed out after
|
|
555
|
+
* <ms>ms` error result, instead of a request that ends with nothing to read.
|
|
556
|
+
* Reopen the default when the library approaches ~1e4 skills, when the
|
|
557
|
+
* `evolutionIo` provider stops being local, or when a model leg enters a tool's
|
|
558
|
+
* execute (the same conditions the probe's own note lists).
|
|
559
|
+
*/
|
|
560
|
+
const DEFAULT_PROBE_TIMEOUT_MS = 12e4;
|
|
542
561
|
/** V27 G2.4: the model each review/curation leg defaults to. The policy schema,
|
|
543
562
|
* the policy resolver and the curator's LLM nomination pass each carried their
|
|
544
563
|
* own copy of these strings — a deployment that changed the policy default used
|
|
@@ -1198,6 +1217,24 @@ async function probeList(io, dir) {
|
|
|
1198
1217
|
}
|
|
1199
1218
|
}
|
|
1200
1219
|
/**
|
|
1220
|
+
* Three-state TEXT read of one file. A missing path (ENOENT/ENOTDIR, or a backend whose read
|
|
1221
|
+
* answers null) is `absent`; every other failure is `unknown` with its reason. The distinction is
|
|
1222
|
+
* the whole point (N14/O-7): "I could not read this file" and "this file is not there" lead to
|
|
1223
|
+
* opposite decisions once a write is about to replace the bytes — treating the first as the
|
|
1224
|
+
* second is how a broken store reads as empty and a rescue copy never happens.
|
|
1225
|
+
* @param io - the IO seam.
|
|
1226
|
+
* @param path - the file to read.
|
|
1227
|
+
* @returns the probe.
|
|
1228
|
+
*/
|
|
1229
|
+
async function probeText(io, path) {
|
|
1230
|
+
try {
|
|
1231
|
+
const value = await io.readText(path);
|
|
1232
|
+
return value === null ? probeAbsent() : probePresent(value);
|
|
1233
|
+
} catch (error) {
|
|
1234
|
+
return isMissingPath(error) ? probeAbsent() : probeUnknown(probeReason(error));
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1201
1238
|
* Three-state mtime read. Absent covers both "the path is missing" and "this
|
|
1202
1239
|
* backend has no mtime probe" — the seam cannot tell those apart, so consumers
|
|
1203
1240
|
* that must know say so in their own log line. A stat that FAILS is unknown.
|
|
@@ -1210,6 +1247,37 @@ async function probeMtime(io, path) {
|
|
|
1210
1247
|
return probeUnknown(probeReason(error));
|
|
1211
1248
|
}
|
|
1212
1249
|
}
|
|
1250
|
+
/** The timestamp a curator run report DECLARES: `startedAt` (the run's own start), then `at` (the
|
|
1251
|
+
* stamp the error writer sets on every report — F-327/P2-4).
|
|
1252
|
+
* @param parsed - the parsed report, or anything else.
|
|
1253
|
+
* @returns epoch milliseconds, or null when neither field carries a parseable time.
|
|
1254
|
+
*/
|
|
1255
|
+
function declaredReportStamp(parsed) {
|
|
1256
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1257
|
+
const record = parsed;
|
|
1258
|
+
for (const value of [record.startedAt, record.at]) {
|
|
1259
|
+
if (typeof value !== "string") continue;
|
|
1260
|
+
const ms = Date.parse(value);
|
|
1261
|
+
if (Number.isFinite(ms)) return ms;
|
|
1262
|
+
}
|
|
1263
|
+
return null;
|
|
1264
|
+
}
|
|
1265
|
+
/** The time one curator run report is ordered by — ONE口径 for every reader (S2.9/T3-12): the
|
|
1266
|
+
* declared stamp above, then the optional mtime probe as the fallback. `null` means "no usable
|
|
1267
|
+
* time", and that single answer serves both consumers: such a report is never the LATEST (the
|
|
1268
|
+
* panel) and is never DELETED (the retention sweep), which is exactly the pair of answers the two
|
|
1269
|
+
* readers disagreed on when one ordered by the declared stamp and the other by mtime alone.
|
|
1270
|
+
* @param io - the IO seam; a backend without `mtime` simply has no fallback.
|
|
1271
|
+
* @param path - the report file the probe reads.
|
|
1272
|
+
* @param parsed - the already-parsed report.
|
|
1273
|
+
* @returns epoch milliseconds, or null.
|
|
1274
|
+
*/
|
|
1275
|
+
async function reportTime(io, path, parsed) {
|
|
1276
|
+
const declared = declaredReportStamp(parsed);
|
|
1277
|
+
if (declared !== null) return declared;
|
|
1278
|
+
const probe = await probeMtime(io, path);
|
|
1279
|
+
return isPresent(probe) ? probe.value : null;
|
|
1280
|
+
}
|
|
1213
1281
|
//#endregion
|
|
1214
1282
|
//#region lib/types/io.js
|
|
1215
1283
|
/**
|
|
@@ -3218,7 +3286,6 @@ const EVOLUTION_ERRORS = Object.freeze({
|
|
|
3218
3286
|
"e-309-revision-conflict-you-sent": "E-309: revision conflict — you sent %a1%, the document stands at %a2%. Re-read with /evolution params --json and retry.",
|
|
3219
3287
|
"e-309-revision-conflict-message-Re": "E-309: revision conflict — %a1%. Re-read with /evolution params --json and retry.",
|
|
3220
3288
|
"e-310-the-settings-service-refused": "E-310: the settings service refused the write: %a1% — the owning plugin's rule stands (a cross-field pair, or a cap that may only be tightened). /evolution params shows the current value.",
|
|
3221
|
-
"e-305-this-invocation-carries-no-2": "E-305: this invocation carries no agent — `%a1%` needs a session-backed call (run it from a session in the GUI or the CLI).",
|
|
3222
3289
|
"e-317-skill-write-not-confirmed": "E-317: skill \"%a1%\" was not %a2% — the confirmation prompt was declined or dismissed, so nothing was written. The prompt appears on every create and bare delete; repeat the call only if the operator asks for it.",
|
|
3223
3290
|
"e-318-skill-write-without-a-read": "E-318: skill \"%a1%\" was not read in this session, so this write is refused. Read it with the `skill` tool first (a read that failed does not count), then repeat the write.",
|
|
3224
3291
|
"e-320-the-profile-home-is-not": "E-320: the profile home is not reported by this host, so the legacy settings document cannot be located. /evolution migrate reads `<profile home>/settings.yaml.imported`; run it from a profile-launched session.",
|
|
@@ -3342,6 +3409,41 @@ function makeSerialQueue() {
|
|
|
3342
3409
|
};
|
|
3343
3410
|
}
|
|
3344
3411
|
//#endregion
|
|
3412
|
+
//#region lib/types/payload.js
|
|
3413
|
+
/**
|
|
3414
|
+
* The memory op's payload field, resolved in ONE place (T3-06/A34).
|
|
3415
|
+
*
|
|
3416
|
+
* A memory op carries its body under two names — `facts` (the original) and
|
|
3417
|
+
* `content` (the schema alias) — and every reader used to resolve the pair with
|
|
3418
|
+
* `op.facts ?? op.content`. `??` only skips null/undefined, so
|
|
3419
|
+
* `{ facts: '', content: '正文' }` counted as "has facts": the plan validator
|
|
3420
|
+
* refused the op as empty (a FALSE refusal of a payload that is right there),
|
|
3421
|
+
* and the writers that normalized the pair dropped the body to `''`.
|
|
3422
|
+
*
|
|
3423
|
+
* The payload is the first field whose TRIMMED value is non-empty — a blank
|
|
3424
|
+
* string carries no payload, so it yields to its alias. Both blank (or absent)
|
|
3425
|
+
* ⇒ `''`, the one shape every caller already reads as "no payload".
|
|
3426
|
+
*
|
|
3427
|
+
* Non-string values are SKIPPED, never trimmed: a truthy non-string `facts`
|
|
3428
|
+
* used to reach `.trim()` and throw a TypeError out of callers that promise to
|
|
3429
|
+
* be deterministic (the E-60 / P2-1 class). The field-type gates reject such an
|
|
3430
|
+
* op on their own; this helper must not be the one that throws.
|
|
3431
|
+
*
|
|
3432
|
+
* The union posture matches evolution-threat's E-28a scan (BOTH fields are
|
|
3433
|
+
* scanned, not only the first truthy one); the two agree on what counts as
|
|
3434
|
+
* provided.
|
|
3435
|
+
* @module @lmzhen/dsh-evolution-core
|
|
3436
|
+
*/
|
|
3437
|
+
/** The op's payload text: the first of `facts`/`content` whose trim is non-empty, trimmed; `''` when neither carries one. */
|
|
3438
|
+
function payloadText(op) {
|
|
3439
|
+
for (const value of [op?.facts, op?.content]) {
|
|
3440
|
+
if (typeof value !== "string") continue;
|
|
3441
|
+
const text = value.trim();
|
|
3442
|
+
if (text !== "") return text;
|
|
3443
|
+
}
|
|
3444
|
+
return "";
|
|
3445
|
+
}
|
|
3446
|
+
//#endregion
|
|
3345
3447
|
//#region lib/types/numeric.js
|
|
3346
3448
|
/**
|
|
3347
3449
|
* Numeric config clamping for the dsh-evolution plugin family.
|
|
@@ -3383,6 +3485,35 @@ function clampedNumber(value, fallback, opts) {
|
|
|
3383
3485
|
if (max !== void 0 && value > max) return fallback;
|
|
3384
3486
|
return value;
|
|
3385
3487
|
}
|
|
3488
|
+
/**
|
|
3489
|
+
* Clamp one numeric knob and warn ONCE per key.
|
|
3490
|
+
*
|
|
3491
|
+
* S2.9 (T3-10): `evolution-review` and `evolution-curator` each carried their own copy of this
|
|
3492
|
+
* helper — their own once-per-key set and their own message — so the two settings readers of ONE
|
|
3493
|
+
* policy row could drift apart. The logic lives here; each caller passes its own sink (and its own
|
|
3494
|
+
* package prefix). A live field is read at every use, so the warning cannot be per-read.
|
|
3495
|
+
*
|
|
3496
|
+
* @param warned - the keys this caller already warned about.
|
|
3497
|
+
* @param warn - the warning sink (each package prefixes its own name).
|
|
3498
|
+
* @param name - the config key, for the warning.
|
|
3499
|
+
* @param value - the value as supplied, or its volatile reference.
|
|
3500
|
+
* @param fallback - the package default a corrected value falls back to.
|
|
3501
|
+
* @param min - the smallest value the engine can act on.
|
|
3502
|
+
* @param max - the largest value the carrier can hold, when bounded.
|
|
3503
|
+
* @returns the value the engine may use.
|
|
3504
|
+
*/
|
|
3505
|
+
function clampOnce(warned, warn, name, value, fallback, min, max) {
|
|
3506
|
+
const current = typeof value === "object" ? value.get() : value;
|
|
3507
|
+
const result = clampedNumber(current, fallback, max === void 0 ? { min } : {
|
|
3508
|
+
min,
|
|
3509
|
+
max
|
|
3510
|
+
});
|
|
3511
|
+
if (current !== void 0 && result !== current && !warned.has(name)) {
|
|
3512
|
+
warned.add(name);
|
|
3513
|
+
warn(`${name} provided an invalid value; falling back to the default`);
|
|
3514
|
+
}
|
|
3515
|
+
return result;
|
|
3516
|
+
}
|
|
3386
3517
|
//#endregion
|
|
3387
3518
|
//#region lib/types/threats.js
|
|
3388
3519
|
/**
|
|
@@ -4204,7 +4335,7 @@ var MemoryStore = class {
|
|
|
4204
4335
|
for (const [index, op] of operations.entries()) {
|
|
4205
4336
|
const position = index + 1;
|
|
4206
4337
|
if (op.action === "add") {
|
|
4207
|
-
const body = (op
|
|
4338
|
+
const body = payloadText(op);
|
|
4208
4339
|
if (!body) return {
|
|
4209
4340
|
result: {
|
|
4210
4341
|
ok: false,
|
|
@@ -4283,7 +4414,7 @@ var MemoryStore = class {
|
|
|
4283
4414
|
const matchIndex = matches[0]?.matchIndex ?? -1;
|
|
4284
4415
|
if (op.action === "remove") working.splice(matchIndex, 1);
|
|
4285
4416
|
else {
|
|
4286
|
-
const body = (op
|
|
4417
|
+
const body = payloadText(op);
|
|
4287
4418
|
if (!body) return {
|
|
4288
4419
|
result: {
|
|
4289
4420
|
ok: false,
|
|
@@ -5025,8 +5156,8 @@ function presetPatchText(patchText) {
|
|
|
5025
5156
|
* changes semantically: the bundle digest is the fail-closed signal for
|
|
5026
5157
|
* review workers, so a stale id across deployments must be distinguishable.
|
|
5027
5158
|
*/
|
|
5028
|
-
const PROMPT_BUNDLE_VERSION =
|
|
5029
|
-
const PROMPT_BUNDLE_ID = `dsh-evolution@
|
|
5159
|
+
const PROMPT_BUNDLE_VERSION = 21;
|
|
5160
|
+
const PROMPT_BUNDLE_ID = `dsh-evolution@21`;
|
|
5030
5161
|
const MEMORY_REVIEW_PROMPT = `[Auto-review — Memory]
|
|
5031
5162
|
Review the conversation above and consider saving to memory if appropriate.
|
|
5032
5163
|
|
|
@@ -5317,12 +5448,12 @@ function sha256(text) {
|
|
|
5317
5448
|
function createPromptBundle(prompts) {
|
|
5318
5449
|
const canonical = JSON.stringify({
|
|
5319
5450
|
id: PROMPT_BUNDLE_ID,
|
|
5320
|
-
version:
|
|
5451
|
+
version: 21,
|
|
5321
5452
|
prompts: Object.fromEntries(Object.entries(prompts).sort())
|
|
5322
5453
|
});
|
|
5323
5454
|
return Object.freeze({
|
|
5324
5455
|
id: PROMPT_BUNDLE_ID,
|
|
5325
|
-
version:
|
|
5456
|
+
version: 21,
|
|
5326
5457
|
prompts: Object.freeze({ ...prompts }),
|
|
5327
5458
|
sha256: sha256(canonical)
|
|
5328
5459
|
});
|
|
@@ -5334,16 +5465,17 @@ const PROMPT_BUNDLE = createPromptBundle({
|
|
|
5334
5465
|
skillPlan: SKILL_REVIEW_PLAN_PROMPT,
|
|
5335
5466
|
combinedPlan: COMBINED_REVIEW_PLAN_PROMPT,
|
|
5336
5467
|
curator: CURATOR_PROMPT,
|
|
5468
|
+
curatorDryRun: CURATOR_DRY_RUN_BANNER,
|
|
5337
5469
|
completion: COMPLETION_SKILL_REVIEW_PROMPT,
|
|
5338
5470
|
maintain: MAINTAIN_PROMPT,
|
|
5339
5471
|
maintainOutput: MAINTAIN_OUTPUT_INSTRUCTION,
|
|
5340
5472
|
skillsGuidance: SKILLS_GUIDANCE
|
|
5341
5473
|
});
|
|
5342
5474
|
function verifyPromptBundle(bundle = PROMPT_BUNDLE) {
|
|
5343
|
-
if (bundle.id !== PROMPT_BUNDLE_ID || bundle.version !==
|
|
5475
|
+
if (bundle.id !== PROMPT_BUNDLE_ID || bundle.version !== 21) return false;
|
|
5344
5476
|
const canonical = JSON.stringify({
|
|
5345
5477
|
id: PROMPT_BUNDLE_ID,
|
|
5346
|
-
version:
|
|
5478
|
+
version: 21,
|
|
5347
5479
|
prompts: Object.fromEntries(Object.entries(bundle.prompts).sort())
|
|
5348
5480
|
});
|
|
5349
5481
|
return bundle.sha256 === sha256(canonical);
|
|
@@ -5793,6 +5925,164 @@ function sweepReviewChannelSessions(isAlive) {
|
|
|
5793
5925
|
return removed;
|
|
5794
5926
|
}
|
|
5795
5927
|
//#endregion
|
|
5928
|
+
//#region lib/types/platform-services.js
|
|
5929
|
+
/**
|
|
5930
|
+
* Every platform service the family reaches for with `ctx.get`, and what degrades without it.
|
|
5931
|
+
*
|
|
5932
|
+
* O-2 (v46 audit): the family probes 26 platform services optionally — the platform has no
|
|
5933
|
+
* formalized "optional dependency", so an ABSENT service is a silent downgrade and the diagnostic
|
|
5934
|
+
* surface is the only place a reader can learn about it. `/evolution doctor` used to judge six of
|
|
5935
|
+
* them; this table is the single source it judges the rest from, and the arch guard cross-checks it
|
|
5936
|
+
* against the actual probe sites so a new `ctx.get` cannot stay undeclared.
|
|
5937
|
+
*
|
|
5938
|
+
* `side` decides who can judge it: `host` probes run in the plugin process (doctor judges those),
|
|
5939
|
+
* `client` probes run in the browser half, where the host has no view of the seats.
|
|
5940
|
+
* @module @lmzhen/dsh-evolution-core
|
|
5941
|
+
*/
|
|
5942
|
+
/** The 26 probes, in service-name order (the guard checks this list against the tree). */
|
|
5943
|
+
const PLATFORM_SERVICE_PROBES = Object.freeze([
|
|
5944
|
+
{
|
|
5945
|
+
service: "agents",
|
|
5946
|
+
feature: "review and curator subagent wake-ups",
|
|
5947
|
+
side: "host"
|
|
5948
|
+
},
|
|
5949
|
+
{
|
|
5950
|
+
service: "approval",
|
|
5951
|
+
feature: "the platform approval policy the approval row shadows",
|
|
5952
|
+
side: "host"
|
|
5953
|
+
},
|
|
5954
|
+
{
|
|
5955
|
+
service: "configEditor",
|
|
5956
|
+
feature: "the params view inherited-vs-override column",
|
|
5957
|
+
side: "host"
|
|
5958
|
+
},
|
|
5959
|
+
{
|
|
5960
|
+
service: "configForms",
|
|
5961
|
+
feature: "the settings card seat (its section renders read-only without it)",
|
|
5962
|
+
side: "client"
|
|
5963
|
+
},
|
|
5964
|
+
{
|
|
5965
|
+
service: "evolutionApproval",
|
|
5966
|
+
feature: "pending writes: list, approve and reject from the command surface",
|
|
5967
|
+
side: "host"
|
|
5968
|
+
},
|
|
5969
|
+
{
|
|
5970
|
+
service: "evolutionCurator",
|
|
5971
|
+
feature: "/evolution curator subcommands, skill undo and history",
|
|
5972
|
+
side: "host"
|
|
5973
|
+
},
|
|
5974
|
+
{
|
|
5975
|
+
service: "evolutionIo",
|
|
5976
|
+
feature: "every durable read and write (the IO seam)",
|
|
5977
|
+
side: "host"
|
|
5978
|
+
},
|
|
5979
|
+
{
|
|
5980
|
+
service: "evolutionMemoryBudget",
|
|
5981
|
+
feature: "the memory-budget divergence section of this report",
|
|
5982
|
+
side: "host"
|
|
5983
|
+
},
|
|
5984
|
+
{
|
|
5985
|
+
service: "evolutionPolicy",
|
|
5986
|
+
feature: "the policy snapshot that shadows row knobs",
|
|
5987
|
+
side: "host"
|
|
5988
|
+
},
|
|
5989
|
+
{
|
|
5990
|
+
service: "evolutionReplay",
|
|
5991
|
+
feature: "/evolution replay",
|
|
5992
|
+
side: "host"
|
|
5993
|
+
},
|
|
5994
|
+
{
|
|
5995
|
+
service: "evolutionState",
|
|
5996
|
+
feature: "the state facade behind activity and replay",
|
|
5997
|
+
side: "host"
|
|
5998
|
+
},
|
|
5999
|
+
{
|
|
6000
|
+
service: "llm",
|
|
6001
|
+
feature: "the curator one-shot review model call",
|
|
6002
|
+
side: "host"
|
|
6003
|
+
},
|
|
6004
|
+
{
|
|
6005
|
+
service: "memory",
|
|
6006
|
+
feature: "the cross-session memory store (review and learning-graph reads)",
|
|
6007
|
+
side: "host"
|
|
6008
|
+
},
|
|
6009
|
+
{
|
|
6010
|
+
service: "pluginInventory",
|
|
6011
|
+
feature: "the plugin inventory view",
|
|
6012
|
+
side: "host"
|
|
6013
|
+
},
|
|
6014
|
+
{
|
|
6015
|
+
service: "pluginManager",
|
|
6016
|
+
feature: "the platform plugin and preset surfaces this report reads",
|
|
6017
|
+
side: "host"
|
|
6018
|
+
},
|
|
6019
|
+
{
|
|
6020
|
+
service: "profileContext",
|
|
6021
|
+
feature: "writing the agent-preset row into a profile patch",
|
|
6022
|
+
side: "host"
|
|
6023
|
+
},
|
|
6024
|
+
{
|
|
6025
|
+
service: "sessionProjections",
|
|
6026
|
+
feature: "the domain-owned session units",
|
|
6027
|
+
side: "host"
|
|
6028
|
+
},
|
|
6029
|
+
{
|
|
6030
|
+
service: "sessionQuery",
|
|
6031
|
+
feature: "the session corpus behind /evolution sessions",
|
|
6032
|
+
side: "host"
|
|
6033
|
+
},
|
|
6034
|
+
{
|
|
6035
|
+
service: "settings",
|
|
6036
|
+
feature: "the settings user layer and the params view",
|
|
6037
|
+
side: "host"
|
|
6038
|
+
},
|
|
6039
|
+
{
|
|
6040
|
+
service: "skillUsage",
|
|
6041
|
+
feature: "usage counters feeding the curator and the review planner",
|
|
6042
|
+
side: "host"
|
|
6043
|
+
},
|
|
6044
|
+
{
|
|
6045
|
+
service: "skills",
|
|
6046
|
+
feature: "the platform skill catalog the skill tool writes through",
|
|
6047
|
+
side: "host"
|
|
6048
|
+
},
|
|
6049
|
+
{
|
|
6050
|
+
service: "storageDomain",
|
|
6051
|
+
feature: "the storage-domain state provider",
|
|
6052
|
+
side: "host"
|
|
6053
|
+
},
|
|
6054
|
+
{
|
|
6055
|
+
service: "subagents",
|
|
6056
|
+
feature: "spawning review and maintenance subagents",
|
|
6057
|
+
side: "host"
|
|
6058
|
+
},
|
|
6059
|
+
{
|
|
6060
|
+
service: "systemPrompt",
|
|
6061
|
+
feature: "the protected-skills prompt variable",
|
|
6062
|
+
side: "host"
|
|
6063
|
+
},
|
|
6064
|
+
{
|
|
6065
|
+
service: "tools",
|
|
6066
|
+
feature: "tool registration and the guard seam",
|
|
6067
|
+
side: "host"
|
|
6068
|
+
},
|
|
6069
|
+
{
|
|
6070
|
+
service: "webServer",
|
|
6071
|
+
feature: "the skill-history panel routes",
|
|
6072
|
+
side: "host"
|
|
6073
|
+
}
|
|
6074
|
+
]);
|
|
6075
|
+
/**
|
|
6076
|
+
* The HOST-side probes that do not resolve in this runtime — what `/evolution doctor` prints as
|
|
6077
|
+
* "capability absence". Empty is the normal, healthy answer: every capability the family can use
|
|
6078
|
+
* is mounted, and the section stays away.
|
|
6079
|
+
* @param has - a `ctx.get(name) !== undefined` probe of the running context.
|
|
6080
|
+
* @returns the absent host-side probes, in declaration order.
|
|
6081
|
+
*/
|
|
6082
|
+
function absentHostServices(has) {
|
|
6083
|
+
return PLATFORM_SERVICE_PROBES.filter((entry) => entry.side === "host" && !has(entry.service));
|
|
6084
|
+
}
|
|
6085
|
+
//#endregion
|
|
5796
6086
|
//#region lib/types/instance-scope.js
|
|
5797
6087
|
/**
|
|
5798
6088
|
* B3 / G4 (0.3.78): the family's single-instance contract, made explicit.
|
|
@@ -7061,7 +7351,7 @@ function sessionReadSkillNames(ctx, session) {
|
|
|
7061
7351
|
if (session === void 0) return void 0;
|
|
7062
7352
|
const projected = sessionReadNames(ctx, session);
|
|
7063
7353
|
if (projected !== void 0) return projected;
|
|
7064
|
-
const events = session.
|
|
7354
|
+
const events = session.readEvents?.();
|
|
7065
7355
|
return events === void 0 ? void 0 : collectReadSkillNames(events);
|
|
7066
7356
|
}
|
|
7067
7357
|
/** Actions whose target must have been read first (create is authorship, so it is exempt). */
|
|
@@ -7921,7 +8211,13 @@ function contentRetentionFeedback(input) {
|
|
|
7921
8211
|
* @returns the versions; an absent or unreadable index reads as empty (never a throw).
|
|
7922
8212
|
*/
|
|
7923
8213
|
async function loadVersions(root, io, name) {
|
|
7924
|
-
|
|
8214
|
+
const file = historyIndexFile(root, name);
|
|
8215
|
+
const probe = await probeText(io, file);
|
|
8216
|
+
if (isUnknown(probe)) {
|
|
8217
|
+
console.warn(`skill-store: ${file} could not be read (${probe.reason}); reporting no versions for "${name}" — the bytes are untouched`);
|
|
8218
|
+
return [];
|
|
8219
|
+
}
|
|
8220
|
+
return parseHistoryIndex(isPresent(probe) ? probe.value : null);
|
|
7925
8221
|
}
|
|
7926
8222
|
/**
|
|
7927
8223
|
* One version's content.
|
|
@@ -7934,7 +8230,12 @@ async function loadVersions(root, io, name) {
|
|
|
7934
8230
|
async function loadVersionContent(root, io, name, v) {
|
|
7935
8231
|
const entry = (await loadVersions(root, io, name)).find((candidate) => candidate.v === v);
|
|
7936
8232
|
if (entry === void 0) return null;
|
|
7937
|
-
|
|
8233
|
+
const probe = await probeText(io, blobPath(root, entry.hash));
|
|
8234
|
+
if (isUnknown(probe)) {
|
|
8235
|
+
console.warn(`skill-store: version ${v} of "${name}" could not be read (${probe.reason}); reporting no content`);
|
|
8236
|
+
return null;
|
|
8237
|
+
}
|
|
8238
|
+
return isPresent(probe) ? probe.value : null;
|
|
7938
8239
|
}
|
|
7939
8240
|
/**
|
|
7940
8241
|
* Record the content one mutation produced, best-effort.
|
|
@@ -7952,17 +8253,39 @@ async function recordVersions(root, io, input, keep) {
|
|
|
7952
8253
|
for (const side of [input.before, input.after]) {
|
|
7953
8254
|
if (side === null) continue;
|
|
7954
8255
|
const path = blobPath(root, contentHash(side));
|
|
7955
|
-
|
|
8256
|
+
let blobPresent;
|
|
8257
|
+
try {
|
|
8258
|
+
blobPresent = await io.exists(path);
|
|
8259
|
+
} catch (error) {
|
|
8260
|
+
blobPresent = false;
|
|
8261
|
+
console.warn(`skill-store: could not check whether the history blob ${path} exists (${error instanceof Error ? error.message : String(error)}); writing it — content-addressed bytes are the same file`);
|
|
8262
|
+
}
|
|
8263
|
+
if (!blobPresent) await io.writeText(path, side);
|
|
7956
8264
|
}
|
|
7957
8265
|
const file = historyIndexFile(root, input.skillName);
|
|
7958
|
-
|
|
8266
|
+
const fileProbe = await probeText(io, file);
|
|
8267
|
+
if (isUnknown(fileProbe)) {
|
|
8268
|
+
const quarantine = `${file}.corrupt`;
|
|
8269
|
+
try {
|
|
8270
|
+
await io.copy(file, quarantine);
|
|
8271
|
+
} catch {}
|
|
8272
|
+
console.warn(`skill-store: ${file} could not be read (${fileProbe.reason}); its bytes were copied to ${quarantine} where possible and the version record for this write was abandoned — the mutation stands and the file is untouched`);
|
|
8273
|
+
return null;
|
|
8274
|
+
}
|
|
8275
|
+
const probe = readHistoryIndex(isPresent(fileProbe) ? fileProbe.value : null);
|
|
8276
|
+
if (probe.kind === "unreadable") {
|
|
7959
8277
|
const quarantine = `${file}.corrupt`;
|
|
7960
8278
|
await io.copy(file, quarantine);
|
|
7961
8279
|
console.warn(`skill-store: ${file} could not be understood (malformed, foreign, or written by a newer reader); its bytes were copied to ${quarantine} and a fresh index starts from this write`);
|
|
7962
8280
|
}
|
|
7963
8281
|
const recorded = {};
|
|
8282
|
+
let raceRefused = false;
|
|
7964
8283
|
await transactIo(io, file, (current) => {
|
|
7965
8284
|
const state = readHistoryIndex(current);
|
|
8285
|
+
if (state.kind === "unreadable" && probe.kind !== "unreadable") {
|
|
8286
|
+
raceRefused = true;
|
|
8287
|
+
return current;
|
|
8288
|
+
}
|
|
7966
8289
|
const { versions, recorded: numbers } = nextHistoryIndex(state.kind === "ok" ? state.versions : [], input, keep);
|
|
7967
8290
|
recorded.beforeVersion = numbers.beforeVersion;
|
|
7968
8291
|
recorded.afterVersion = numbers.afterVersion;
|
|
@@ -7971,6 +8294,14 @@ async function recordVersions(root, io, input, keep) {
|
|
|
7971
8294
|
versions
|
|
7972
8295
|
}, null, 2);
|
|
7973
8296
|
});
|
|
8297
|
+
if (raceRefused) {
|
|
8298
|
+
const quarantine = `${file}.corrupt`;
|
|
8299
|
+
try {
|
|
8300
|
+
await io.copy(file, quarantine);
|
|
8301
|
+
} catch {}
|
|
8302
|
+
console.warn(`skill-store: ${file} became unreadable between the probe and the locked read; its bytes were copied to ${quarantine} where possible and the version record for this write was abandoned — nothing was overwritten`);
|
|
8303
|
+
return null;
|
|
8304
|
+
}
|
|
7974
8305
|
return recorded;
|
|
7975
8306
|
}
|
|
7976
8307
|
/**
|
|
@@ -8721,10 +9052,19 @@ var SkillLibrary = class {
|
|
|
8721
9052
|
/**
|
|
8722
9053
|
* Summarize the skill tree.
|
|
8723
9054
|
* @param options - `withContent` attaches each skill's whole SKILL.md body to
|
|
8724
|
-
* its summary (v35 C11)
|
|
8725
|
-
*
|
|
9055
|
+
* its summary (v35 C11), `withFrontmatter` its parsed frontmatter record
|
|
9056
|
+
* (T2-07/A22): the read this listing already performs is the one either would
|
|
9057
|
+
* cost again, so a caller pays no second pass over the tree.
|
|
8726
9058
|
* @returns one summary per readable skill directory.
|
|
8727
9059
|
*/
|
|
9060
|
+
/** T2-V2/A27: the skill names the TREE holds — the same listing rule {@link list} applies (dot
|
|
9061
|
+
* entries skipped, a SKILL.md must exist), WITHOUT reading any SKILL.md. A consumer that needs the
|
|
9062
|
+
* member set rather than the summaries (the learning graph's node set) pays one directory listing
|
|
9063
|
+
* instead of a whole-tree read pass.
|
|
9064
|
+
* @returns the sorted skill directory names. */
|
|
9065
|
+
async treeNames() {
|
|
9066
|
+
return await listNames(this.root, this.io);
|
|
9067
|
+
}
|
|
8728
9068
|
async list(options = {}) {
|
|
8729
9069
|
const summaries = [];
|
|
8730
9070
|
for (const name of await listNames(this.root, this.io)) {
|
|
@@ -8774,7 +9114,8 @@ var SkillLibrary = class {
|
|
|
8774
9114
|
].some((value) => value === null),
|
|
8775
9115
|
managed: hermesManaged === true,
|
|
8776
9116
|
...typeof parsedWhenToUse === "string" && parsedWhenToUse.trim() !== "" ? { whenToUse: parsedWhenToUse } : {},
|
|
8777
|
-
...options.withContent === true ? { content: md } : {}
|
|
9117
|
+
...options.withContent === true ? { content: md } : {},
|
|
9118
|
+
...options.withFrontmatter === true && parsed !== null ? { frontmatter: parsed.frontmatter } : {}
|
|
8778
9119
|
});
|
|
8779
9120
|
}
|
|
8780
9121
|
return summaries;
|
|
@@ -8916,6 +9257,20 @@ var SkillLibrary = class {
|
|
|
8916
9257
|
* @returns path -> character count (possibly empty), or null when the listing or
|
|
8917
9258
|
* a size probe cannot answer — unknown is never an empty map.
|
|
8918
9259
|
*/
|
|
9260
|
+
/** A plan-time read whose bytes become a tree change's CAS baseline (`expected`). A failed read is
|
|
9261
|
+
* NAMED and answers `null`, which is FAIL-CLOSED at commit: the baseline is then "absent", so an
|
|
9262
|
+
* existing file counts as drift and the commit refuses instead of overwriting bytes nobody read
|
|
9263
|
+
* (N36: the failure is not silently "the file is not there").
|
|
9264
|
+
* @param target - the file a tree change is about to write.
|
|
9265
|
+
* @returns its bytes, or `null` for both "absent" and "could not read". */
|
|
9266
|
+
async readForCas(target) {
|
|
9267
|
+
const probe = await probeText(this.io, target);
|
|
9268
|
+
if (isUnknown(probe)) {
|
|
9269
|
+
console.warn(`skill-store: could not read ${target} for the tree change's baseline (${probe.reason}); treating it as absent — an existing file then counts as drift and the commit refuses`);
|
|
9270
|
+
return null;
|
|
9271
|
+
}
|
|
9272
|
+
return isPresent(probe) ? probe.value : null;
|
|
9273
|
+
}
|
|
8919
9274
|
async supportFileChars(rawName) {
|
|
8920
9275
|
const name = rawName.trim();
|
|
8921
9276
|
if (this.badName(name) !== null) return null;
|
|
@@ -8925,12 +9280,21 @@ var SkillLibrary = class {
|
|
|
8925
9280
|
const chars = {};
|
|
8926
9281
|
for (const path of listed.value) {
|
|
8927
9282
|
const full = join(dir, ...path.split("/").filter(Boolean));
|
|
8928
|
-
|
|
9283
|
+
let bytes = null;
|
|
9284
|
+
if (this.io.size !== void 0) try {
|
|
9285
|
+
bytes = await this.io.size(full);
|
|
9286
|
+
} catch (error) {
|
|
9287
|
+
console.warn(`skill-store: size of ${full} could not be read (${error instanceof Error ? error.message : String(error)}); reporting no support-file char counts`);
|
|
9288
|
+
return null;
|
|
9289
|
+
}
|
|
8929
9290
|
if (bytes === null) return null;
|
|
8930
9291
|
if (bytes <= this.limits.maxSkillContentChars) continue;
|
|
8931
|
-
const
|
|
8932
|
-
if (
|
|
8933
|
-
|
|
9292
|
+
const contentProbe = await probeText(this.io, full);
|
|
9293
|
+
if (!isPresent(contentProbe)) {
|
|
9294
|
+
if (isUnknown(contentProbe)) console.warn(`skill-store: ${full} could not be read (${contentProbe.reason}); reporting no support-file char counts`);
|
|
9295
|
+
return null;
|
|
9296
|
+
}
|
|
9297
|
+
chars[path] = contentProbe.value.length;
|
|
8934
9298
|
}
|
|
8935
9299
|
return chars;
|
|
8936
9300
|
}
|
|
@@ -9236,7 +9600,9 @@ var SkillLibrary = class {
|
|
|
9236
9600
|
ok: false,
|
|
9237
9601
|
message: `Skill "${name}" is protected (${protection}).`
|
|
9238
9602
|
};
|
|
9239
|
-
const
|
|
9603
|
+
const limitProbe = await probeText(this.io, path);
|
|
9604
|
+
if (isUnknown(limitProbe)) console.warn(`skill-store: could not re-read ${path} for the shrink exemption (${limitProbe.reason}); validating without it — an over-limit repair is refused, not accepted`);
|
|
9605
|
+
const currentForLimit = isPresent(limitProbe) ? limitProbe.value : null;
|
|
9240
9606
|
const validation = validateFrontmatter(content, name, this.limits, currentForLimit);
|
|
9241
9607
|
if (validation) return {
|
|
9242
9608
|
ok: false,
|
|
@@ -9598,8 +9964,13 @@ var SkillLibrary = class {
|
|
|
9598
9964
|
* R2 follow-up); anything else (a user support file or a live writer's
|
|
9599
9965
|
* lock) is left untouched. */
|
|
9600
9966
|
async sweepLockIfStranded(lockPath) {
|
|
9601
|
-
const
|
|
9602
|
-
if (
|
|
9967
|
+
const lockProbe = await probeText(this.io, lockPath);
|
|
9968
|
+
if (isUnknown(lockProbe)) {
|
|
9969
|
+
console.warn(`skill-store: could not read ${lockPath} to check whether its holder is alive (${lockProbe.reason}); leaving the lock in place`);
|
|
9970
|
+
return;
|
|
9971
|
+
}
|
|
9972
|
+
if (!isPresent(lockProbe)) return;
|
|
9973
|
+
const body = lockProbe.value;
|
|
9603
9974
|
const pid = parseLockBody(body);
|
|
9604
9975
|
if (pid === null) return;
|
|
9605
9976
|
if (Number.isInteger(pid) && pid > 0 && isProcessAlive(pid) && !await this.lockHolderAgedOut(lockPath)) return;
|
|
@@ -9908,7 +10279,7 @@ var SkillLibrary = class {
|
|
|
9908
10279
|
if (!freshTargetMd) throw new Error(`target "${targetName}" disappeared mid-consolidate`);
|
|
9909
10280
|
const writes = [];
|
|
9910
10281
|
for (const reference of referenceWrites) {
|
|
9911
|
-
const previous = await this.
|
|
10282
|
+
const previous = await this.readForCas(reference.target);
|
|
9912
10283
|
const base = previous?.trimEnd() ?? "";
|
|
9913
10284
|
writes.push({
|
|
9914
10285
|
target: reference.target,
|
|
@@ -9918,7 +10289,7 @@ var SkillLibrary = class {
|
|
|
9918
10289
|
}
|
|
9919
10290
|
for (const file of rehomed) {
|
|
9920
10291
|
const target = join(targetDir, ...file.to.split("/"));
|
|
9921
|
-
const previous = await this.
|
|
10292
|
+
const previous = await this.readForCas(target);
|
|
9922
10293
|
writes.push({
|
|
9923
10294
|
target,
|
|
9924
10295
|
content: file.content,
|
|
@@ -10083,7 +10454,7 @@ var SkillLibrary = class {
|
|
|
10083
10454
|
const writes = [];
|
|
10084
10455
|
for (const entry of byRel.values()) {
|
|
10085
10456
|
const target = join(dir, ...entry.rel.split("/"));
|
|
10086
|
-
const previous = await this.
|
|
10457
|
+
const previous = await this.readForCas(target);
|
|
10087
10458
|
const base = previous?.trimEnd() ?? "";
|
|
10088
10459
|
writes.push({
|
|
10089
10460
|
target,
|
|
@@ -10212,7 +10583,10 @@ var SkillLibrary = class {
|
|
|
10212
10583
|
message: `Tree change failed: ${reason}. The rollback could not restore ${stuck.join(", ")} — recover those targets from the .backups snapshot (or re-apply the change).`
|
|
10213
10584
|
};
|
|
10214
10585
|
}
|
|
10215
|
-
|
|
10586
|
+
const skillMdWrite = landing.find((entry) => entry.target.split(/[\\/]/).pop() === "SKILL.md");
|
|
10587
|
+
const auditedBefore = skillMdWrite === void 0 ? md : skillMdWrite.previous;
|
|
10588
|
+
const auditedAfter = skillMdWrite === void 0 ? md : skillMdWrite.content;
|
|
10589
|
+
await this.audit(name, plan.auditAction, auditedBefore, auditedAfter, plan.auditSummary);
|
|
10216
10590
|
this.notifyMutation({
|
|
10217
10591
|
action: plan.eventAction,
|
|
10218
10592
|
name,
|
|
@@ -10263,11 +10637,21 @@ var SkillLibrary = class {
|
|
|
10263
10637
|
let chosen;
|
|
10264
10638
|
const unreadable = [];
|
|
10265
10639
|
for (const candidate of candidates) {
|
|
10266
|
-
|
|
10640
|
+
const markerProbe = await probeText(this.io, join(archiveRoot, candidate, ".archive-name"));
|
|
10641
|
+
if (isUnknown(markerProbe)) {
|
|
10642
|
+
unreadable.push(candidate);
|
|
10643
|
+
continue;
|
|
10644
|
+
}
|
|
10645
|
+
if ((isPresent(markerProbe) ? markerProbe.value.trim() : "") === name) {
|
|
10267
10646
|
chosen = candidate;
|
|
10268
10647
|
break;
|
|
10269
10648
|
}
|
|
10270
|
-
const
|
|
10649
|
+
const mdProbe = await probeText(this.io, join(archiveRoot, candidate, "SKILL.md"));
|
|
10650
|
+
if (isUnknown(mdProbe)) {
|
|
10651
|
+
unreadable.push(candidate);
|
|
10652
|
+
continue;
|
|
10653
|
+
}
|
|
10654
|
+
const parsed = parseFrontmatter(isPresent(mdProbe) ? mdProbe.value : "");
|
|
10271
10655
|
if (parsed?.frontmatter.name === name) {
|
|
10272
10656
|
chosen = candidate;
|
|
10273
10657
|
break;
|
|
@@ -10306,7 +10690,9 @@ var SkillLibrary = class {
|
|
|
10306
10690
|
};
|
|
10307
10691
|
await this.deleteStrandedLocks(dest);
|
|
10308
10692
|
for (const marker of [".archive-reason", ".archive-name"]) if (await this.io.exists(join(dest, marker))) await this.io.remove(join(dest, marker));
|
|
10309
|
-
|
|
10693
|
+
const restoredProbe = await probeText(this.io, join(dest, "SKILL.md"));
|
|
10694
|
+
if (isUnknown(restoredProbe)) console.warn(`evolution-skill-store: the restored SKILL.md of "${name}" could not be read back (${restoredProbe.reason}); the restore is recorded without after-bytes`);
|
|
10695
|
+
await this.audit(name, "restore", null, isPresent(restoredProbe) ? restoredProbe.value : null, `restored from ${source}`);
|
|
10310
10696
|
this.notifyMutation({
|
|
10311
10697
|
action: "restore",
|
|
10312
10698
|
name,
|
|
@@ -10435,17 +10821,23 @@ var SkillLibrary = class {
|
|
|
10435
10821
|
message: `File "${filePath}" not found in skill "${name}".`
|
|
10436
10822
|
};
|
|
10437
10823
|
}
|
|
10438
|
-
const
|
|
10439
|
-
if (
|
|
10440
|
-
|
|
10441
|
-
|
|
10442
|
-
|
|
10824
|
+
const beforeProbe = await probeText(this.io, target);
|
|
10825
|
+
if (!isPresent(beforeProbe)) {
|
|
10826
|
+
if (isUnknown(beforeProbe)) console.warn(`skill-store: could not read ${target} before removing it (${beforeProbe.reason}); refusing the removal`);
|
|
10827
|
+
return {
|
|
10828
|
+
ok: false,
|
|
10829
|
+
message: `"${filePath}" is not a readable regular file — remove the files inside it one by one.`
|
|
10830
|
+
};
|
|
10831
|
+
}
|
|
10832
|
+
const before = beforeProbe.value;
|
|
10443
10833
|
let verdict = anchorVerdict(anchor, before);
|
|
10834
|
+
let lockedBefore = before;
|
|
10444
10835
|
if (verdict === "match" && this.transact) {
|
|
10445
10836
|
const ran = { done: false };
|
|
10446
10837
|
await this.transact(this.io, target, (current) => {
|
|
10447
10838
|
ran.done = true;
|
|
10448
10839
|
verdict = anchorVerdict(anchor, current);
|
|
10840
|
+
if (verdict === "match" && current !== null) lockedBefore = current;
|
|
10449
10841
|
return verdict === "match" ? null : current;
|
|
10450
10842
|
});
|
|
10451
10843
|
if (!ran.done) return {
|
|
@@ -10454,7 +10846,7 @@ var SkillLibrary = class {
|
|
|
10454
10846
|
};
|
|
10455
10847
|
} else if (verdict === "match") await this.io.remove(target);
|
|
10456
10848
|
if (verdict !== "match") return anchorRefusalFile(name, filePath, verdict);
|
|
10457
|
-
await this.audit(name, "remove_file",
|
|
10849
|
+
await this.audit(name, "remove_file", lockedBefore, null, `removed ${filePath}`, filePath);
|
|
10458
10850
|
this.notifyMutation({
|
|
10459
10851
|
action: "remove_file",
|
|
10460
10852
|
name,
|
|
@@ -10492,11 +10884,17 @@ var SkillLibrary = class {
|
|
|
10492
10884
|
if (!this.io.mtime) return null;
|
|
10493
10885
|
const cutoff = Date.now() - ARCHIVE_RETENTION_DAYS * 864e5;
|
|
10494
10886
|
const expired = [];
|
|
10887
|
+
let undated = 0;
|
|
10495
10888
|
for (const entry of listed.value) {
|
|
10496
|
-
const
|
|
10497
|
-
if (
|
|
10889
|
+
const probe = await probeMtime(this.io, join(archiveRoot, entry));
|
|
10890
|
+
if (isUnknown(probe)) {
|
|
10891
|
+
undated += 1;
|
|
10892
|
+
continue;
|
|
10893
|
+
}
|
|
10894
|
+
if (isAbsent(probe) || probe.value > cutoff) continue;
|
|
10498
10895
|
expired.push(entry);
|
|
10499
10896
|
}
|
|
10897
|
+
if (undated > 0) console.warn(`skill-store: ${undated} archived entr(ies) could not be dated (mtime probe failed); they are retained — a failed probe never expires an entry`);
|
|
10500
10898
|
return expired;
|
|
10501
10899
|
}
|
|
10502
10900
|
/** V4 (design §16.6): support-file char policy, resolved at the call site. */
|
|
@@ -10854,8 +11252,9 @@ function newSkillLibrary(options) {
|
|
|
10854
11252
|
* the SAME name in two carriers (reviewMode, staleAfterDays, archiveAfterDays —
|
|
10855
11253
|
* resolved by the existing policy-shadows-row rule) and six use DIFFERENT names.
|
|
10856
11254
|
* This module owns the six: the policy/snapshot name is the canonical id, the
|
|
10857
|
-
* plugin-row name is a deprecated alias kept readable
|
|
10858
|
-
*
|
|
11255
|
+
* plugin-row name is a deprecated alias kept readable since 0.6.x. Removal was
|
|
11256
|
+
* planned for 0.7.0 and is deferred: the aliases still ship, so no later version
|
|
11257
|
+
* is claimed here.
|
|
10859
11258
|
*
|
|
10860
11259
|
* Reading stays compatible (a carrier still spelling the legacy name resolves),
|
|
10861
11260
|
* writing is strict (the write path accepts canonical ids only, so no new
|
|
@@ -11992,6 +12391,21 @@ const PARAM_EXPOSURE = Object.freeze([
|
|
|
11992
12391
|
docAnchor: "PARAMETERS.md#deployment",
|
|
11993
12392
|
summary: "Stage foreground agent writes for approval too."
|
|
11994
12393
|
},
|
|
12394
|
+
{
|
|
12395
|
+
id: "evolution-approval.pendingTtlMs",
|
|
12396
|
+
group: "deployment",
|
|
12397
|
+
tier: "E3",
|
|
12398
|
+
authority: "cordis",
|
|
12399
|
+
owner: "evolution-approval",
|
|
12400
|
+
applies: "live",
|
|
12401
|
+
docAnchor: "PARAMETERS.md#deployment",
|
|
12402
|
+
summary: "How long a staged write may wait before it expires: expired writes are closed as rejected and are NEVER executed; 0 (the default) means the window never expires.",
|
|
12403
|
+
label: "待批过期时间",
|
|
12404
|
+
hint: "一条待批写入最多等多久:超时就按「拒绝」关掉(不会执行,写效果不会落地);0 = 永不过期(默认)。",
|
|
12405
|
+
control: "number",
|
|
12406
|
+
unit: "毫秒",
|
|
12407
|
+
values: ""
|
|
12408
|
+
},
|
|
11995
12409
|
{
|
|
11996
12410
|
id: "qualityWarnThreshold",
|
|
11997
12411
|
group: "deployment",
|
|
@@ -12042,6 +12456,16 @@ const PARAM_EXPOSURE = Object.freeze([
|
|
|
12042
12456
|
docAnchor: "PARAMETERS.md#deployment",
|
|
12043
12457
|
summary: "Timeout of one maintenance run started from the command surface."
|
|
12044
12458
|
},
|
|
12459
|
+
{
|
|
12460
|
+
id: "evolution-maintenance.probeTimeoutMs",
|
|
12461
|
+
group: "deployment",
|
|
12462
|
+
tier: "E2",
|
|
12463
|
+
authority: "cordis",
|
|
12464
|
+
owner: "evolution-maintenance",
|
|
12465
|
+
applies: "none",
|
|
12466
|
+
docAnchor: "PARAMETERS.md#deployment",
|
|
12467
|
+
summary: "Model-facing budget of one maintenance_probe call — a hang guard, not a latency budget."
|
|
12468
|
+
},
|
|
12045
12469
|
{
|
|
12046
12470
|
id: "skill-usage.supportReadToolNames",
|
|
12047
12471
|
group: "deployment",
|
|
@@ -12060,7 +12484,7 @@ const PARAM_EXPOSURE = Object.freeze([
|
|
|
12060
12484
|
owner: "scripts",
|
|
12061
12485
|
applies: "restart",
|
|
12062
12486
|
docAnchor: "PARAMETERS.md#deployment",
|
|
12063
|
-
summary: "Install target plane (layered preset
|
|
12487
|
+
summary: "Install target plane (values host / agent / layered / oneclick; the `layered` form uses the generated preset, the others mount the bundle at profile root)."
|
|
12064
12488
|
},
|
|
12065
12489
|
{
|
|
12066
12490
|
id: "install.basePreset",
|
|
@@ -12158,6 +12582,35 @@ function paramSettingsId(owner) {
|
|
|
12158
12582
|
* @returns the entry id the platform reports for that row.
|
|
12159
12583
|
* @throws when the owner publishes no user layer.
|
|
12160
12584
|
*/
|
|
12585
|
+
/** The keys the USER set in one row's settings namespace (G1 §8.4-A).
|
|
12586
|
+
*
|
|
12587
|
+
* The settings user layer is read in exactly ONE place, and it reads KEY NAMES, never values: the
|
|
12588
|
+
* platform resolves the user layer into the row's live fields, so a value always comes from the
|
|
12589
|
+
* row. The precedence rule (user > policy) needs to know WHICH keys the user set, or a deployment
|
|
12590
|
+
* policy would silently override the user. S2.9 (T3-10): review and curator each had their own
|
|
12591
|
+
* reader, so one key could be resolved two ways.
|
|
12592
|
+
* @param ctx - the plugin context (a `get(name)` view is enough).
|
|
12593
|
+
* @param rowId - the row's registry namespace (`paramRowId(<package>)`).
|
|
12594
|
+
* @returns the keys present in that row's user layer.
|
|
12595
|
+
*/
|
|
12596
|
+
function userSetKeys(ctx, rowId) {
|
|
12597
|
+
const entry = ctx.get("settings")?.describe?.({ redactSecrets: false }).find((item) => item.ns === rowId);
|
|
12598
|
+
return new Set(Object.keys(entry?.user ?? {}));
|
|
12599
|
+
}
|
|
12600
|
+
/** ONE key's effective value, in the design's precedence (G1 §8.4-A): the user layer (whose value
|
|
12601
|
+
* already arrived through the row's live field) → the deployment policy snapshot → the row itself.
|
|
12602
|
+
* S2.9 (T3-10): both readers of the policy row resolve through this, so a new key cannot get one
|
|
12603
|
+
* answer on the review side and another on the curator side.
|
|
12604
|
+
* @param userSet - the keys the user set (see {@link userSetKeys}).
|
|
12605
|
+
* @param row - the row's live fields.
|
|
12606
|
+
* @param snapshot - the deployment policy snapshot, when a policy row is mounted.
|
|
12607
|
+
* @param key - the key being resolved.
|
|
12608
|
+
* @returns the effective value.
|
|
12609
|
+
*/
|
|
12610
|
+
function pickWithPolicy(userSet, row, snapshot, key) {
|
|
12611
|
+
if (userSet.has(key)) return row[key];
|
|
12612
|
+
return snapshot?.[key] ?? row[key];
|
|
12613
|
+
}
|
|
12161
12614
|
function paramRowId(owner) {
|
|
12162
12615
|
const id = paramSettingsId(owner);
|
|
12163
12616
|
if (id === void 0) throw new Error("no settings namespace registered for owner `" + owner + "` (add it to PARAM_NAMESPACES)");
|
|
@@ -12462,4 +12915,48 @@ function sessionAudited(ctx, sessionId, sessionScoped) {
|
|
|
12462
12915
|
return false;
|
|
12463
12916
|
}
|
|
12464
12917
|
//#endregion
|
|
12465
|
-
|
|
12918
|
+
//#region lib/types/fold-untrusted.js
|
|
12919
|
+
/**
|
|
12920
|
+
* Untrusted text rendered as prompt lines (T3-03/A31) — ONE folding rule.
|
|
12921
|
+
*
|
|
12922
|
+
* The background-review digest embeds conversation text and tool output into a
|
|
12923
|
+
* plain-text prompt whose PRODUCT is a plan with write permission. Anything the
|
|
12924
|
+
* session ingested (a fetched page, a file body, command output) can therefore
|
|
12925
|
+
* carry a newline plus a line shaped like the prompt's own structure — a
|
|
12926
|
+
* `SYSTEM:`/`Review kind:` header, a `[result]` evidence line, a second copy
|
|
12927
|
+
* of the closing instruction — and forge structure or inject instructions.
|
|
12928
|
+
*
|
|
12929
|
+
* FOLDING, not deleting: the reader (the model) must still SEE what the data
|
|
12930
|
+
* said, it just must never become structure. Line separators become a visible
|
|
12931
|
+
* `⏎`, and a line that would read as a role/evidence header gets a visible
|
|
12932
|
+
* marker in front of it. Every original character stays in place.
|
|
12933
|
+
*
|
|
12934
|
+
* The boundary is the separator itself, not only a real newline: a payload can
|
|
12935
|
+
* paste the separator verbatim, so neutralization matches after `⏎` whether it
|
|
12936
|
+
* came from the fold or from the data. Both edits are idempotent (a second pass
|
|
12937
|
+
* cannot add a second marker).
|
|
12938
|
+
*
|
|
12939
|
+
* Scope: text rendered as its OWN line(s) of a prompt. `render-facts`'
|
|
12940
|
+
* sanitizer (evolution-maintenance) is the same posture for `key=value` fields
|
|
12941
|
+
* and additionally escapes its block delimiter; it is not superseded here.
|
|
12942
|
+
* @module @lmzhen/dsh-evolution-core
|
|
12943
|
+
*/
|
|
12944
|
+
/** Visible stand-in for a folded line separator: one character that cannot be read as line structure. */
|
|
12945
|
+
const UNTRUSTED_LINE_BREAK = " ⏎ ";
|
|
12946
|
+
/** Visible marker prefixed to a line that would otherwise read as a role/structure header. */
|
|
12947
|
+
const UNTRUSTED_LINE_MARK = "· ";
|
|
12948
|
+
const NEWLINES = /\r\n|\r|\n/g;
|
|
12949
|
+
/** A line that reads as structure: a `Label:` head (SYSTEM:, Review kind:, Signals:) or a digest evidence tag.
|
|
12950
|
+
* The head is case-TOLERANT on purpose: this family's own prompt labels are mixed case, so an
|
|
12951
|
+
* uppercase-only rule neutralized SYSTEM: while leaving a forged "Review kind:" untouched. */
|
|
12952
|
+
const STRUCTURAL_HEAD = /(^|⏎)([ \t]*)((?:[A-Za-z][A-Za-z0-9 _]{2,}:|\[result\]|\[call\]))/g;
|
|
12953
|
+
/**
|
|
12954
|
+
* Fold untrusted `text` so it cannot become structure, then bound it to `limit`
|
|
12955
|
+
* visible characters. A non-positive `limit` means "no truncation".
|
|
12956
|
+
*/
|
|
12957
|
+
function foldUntrustedLines(text, limit) {
|
|
12958
|
+
const marked = text.replace(NEWLINES, UNTRUSTED_LINE_BREAK).replace(STRUCTURAL_HEAD, (_match, boundary, spaces, head) => `${boundary}${spaces}· ${head}`);
|
|
12959
|
+
return limit > 0 ? marked.slice(0, limit) : marked;
|
|
12960
|
+
}
|
|
12961
|
+
//#endregion
|
|
12962
|
+
export { AGENT_PRESET_PACKAGE, AGENT_PRESET_ROOT_HINT, ALIVE_LOCK_TAKEOVER_MS, AUTHORING_DESCRIPTION_BAR, AUTHORING_SPLIT_LINE_CHARS, BASELINE_ACTION, CITATION_SUPPORT_DIRS, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CONTENT_SPLIT_HINT, COST_ASCII_TOKEN_HIGH, COST_ASCII_TOKEN_LOW, COST_CJK_TOKEN_HIGH, COST_CJK_TOKEN_LOW, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEDUP_MAX_PAIR_COMPARISONS, DEDUP_SIMILARITY_THRESHOLD, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_ARCHIVE_RETENTION_POLICY, DEFAULT_CITATION_POLICY, DEFAULT_CITATION_REF_BUDGET, 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_PROBE_TIMEOUT_MS, DEFAULT_REFERENCE_REWRITE_POLICY, 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_SKILL_VERSION_KEEP, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_SUPPORT_FILE_CHAR_POLICY, DEFAULT_SUPPORT_READ_TOOL_NAMES, DEFAULT_THREAT_MAX_SCAN_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, EMPTY_PATCH_SEED, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_ERRORS, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, FUZZY_MAX_PATTERN_CHARS, FUZZY_MAX_WORK, HEALTH_STAMP_RE, HISTORY_DIR, HISTORY_INDEX_VERSION, HOOK_LINE_RE, INSTANCE_KEYS, KEEP_LINE_RE, LOCK_BODY_RE, LOCK_SUFFIX, LOCK_TEAR_TAKEOVER_MS, LOW_QUALITY_THRESHOLD, LostWriteLock, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_REQUEST_BODY_BYTES, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_SUPPORT_READ_PATHS, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NAMESPACE_MIGRATIONS, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PARAM_ALIASES, PARAM_EXPOSURE, PARAM_GROUP_LABELS, PARAM_NAMESPACES, PATTERN_OVERLAP, PLATFORM_CATALOG_DESCRIPTION_DEFAULT, PLATFORM_ESTIMATE_CHARS_PER_TOKEN, PLATFORM_SERVICE_PROBES, POLICY_STAGE_DEFAULTS, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, READ_REQUIRED_ACTIONS, RESTRUCTURE_TARGET_RE, RETENTION_FEEDBACK_KEEP_RATIO, RETENTION_FEEDBACK_MIN_CHARS, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUMMARY_DUPLICATE_HINT_THRESHOLD, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, TEXT_DIFF_MAX_CHARS, TEXT_DIFF_MAX_LINES, THREAT_EXEMPTION_HINT, TOKEN_ASCII_WEIGHT, TOKEN_CJK_WEIGHT, ToolDispatchNormalizer, ToolDispatchPayloadError, UNTRUSTED_LINE_BREAK, UNTRUSTED_LINE_MARK, UPSTREAM_LIMIT_CHARS_PER_TOKEN, VERSION_SUMMARY_MAX_CHARS, absentHostServices, advanceReview, affinity, ageCopyKey, allowRowCollisions, anchorVerdict, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, applyNamespaceMigration, applyReferenceRewrite, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, basePresetPlugins, blobPath, bodyCost, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpSupportRead, bumpUse, bumpView, callingScope, canonicalWriteId, claimInstance, clampOnce, clampedNumber, classifyNamespaceMigration, clearReviewChannel, collectReadSkillNames, composePresetComposition, composePresetEntry, composePresetInsert, composePresetRow, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, contentRetentionFeedback, countDispatches, createGateSet, daysSinceIso, decideTakeover, declaredReportStamp, describeReferenceRewrite, dispatchKeyOf, dispatchStepOf, displayBodyOf, duplicateHeadings, elapsedSince, emptyRecord, entryTarget, errorText, evaluateThreat, eventsFile, evidenceKindIndex, evolutionActivityProjection, evolutionEventPayloadIssue, evolutionEvidenceProjection, evolutionHome, evolutionIoAdapter, evolutionReadsProjection, evolutionRoot, evolutionSignalsProjection, filterUnreadSkillOps, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurnEvent, foldTurnEvents, foldUntrustedLines, frontmatterBlock, frontmatterCatalogInvalid, fuzzyPatch, getRecord, historyIndexFile, historyRoot, identity, idleDays, initialTurnFold, installEvolutionProjections, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isBookkeepingFrame, isCommittedWarning, isDeprecatedParamId, isFileShapedPath, isLoopbackRequest, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, isUnreadWrite, latestActivityAt, latestVersionAt, lifecycleCandidate, listEventArchives, loadMutations, loadRowOverrides, loadSuppressedNames, loadUsage, loadVersionContent, loadVersions, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, mergePresetRow, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nearDuplicateSummaries, neutralizePromptVariables, newSkillLibrary, nextHistoryIndex, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, normalizeVersionSummary, observeEvent, orderVersions, overlongLines, paramNamespace, paramRowId, paramSettingsId, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseHistoryIndex, parseLockBody, partitionVersions, payloadText, pendingSelfCleanup, persistedWriteSite, persistedWriteSites, pickWithPolicy, planNamespaceMigration, planReferenceRewrite, planRehoming, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, probeAbsent, probeList, probeMtime, probePresent, probeReason, probeText, probeUnknown, projectionText, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, readHistoryIndex, readJsonBody, readJsonObject, readNumberParam, readParam, readingStats, recordMutation, recordVersions, redactSecrets, relatedSkillNames, releaseInstance, removePresetRow, renameWithRetry, renderCuratorReportMarkdown, reportTime, resolveBasePresetPatch, resolveCitations, resolveExecOrigins, resolveOrigins, resolveParamId, resolveRootConfig, resolveSkillsRoot, retainEventArchives, retirementReport, reviewPrompt, saveSuppressedNames, saveUsage, scanBodyHooks, scanContentThreats, scanMemoryThreats, scanThreats, scopedProbeReport, sessionAudited, sessionEvidenceIndex, sessionLastEventTime, sessionReadNames, sessionReadSkillNames, sessionSeesFamilyTools, sessionTurnSignals, skillReadNameOf, skillRowCells, skillStateKey, skillsRoot, supportFileReadOf, suppressedFile, sweepReviewChannelSessions, textDiffFacts, tokenLineFor, transactIo, transactTaskGuard, trimPatternBoundaries, unhookedSupportPointers, updateSuppressedNames, usageFile, usageObserved, userSetKeys, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, versionActionKind, versionRowCells, versionSummaryPrompt, versionTarget, vocabulary, writeDurableTmp, writeJson, yamlPlainScalarNeedsQuotes };
|