@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/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.facts ?? op.content ?? "").trim();
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.facts ?? op.content ?? "").trim();
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 = 20;
5029
- const PROMPT_BUNDLE_ID = `dsh-evolution@20`;
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: 20,
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: 20,
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 !== 20) return false;
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: 20,
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.snapshotEvents?.();
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
- return parseHistoryIndex(await io.readText(historyIndexFile(root, name)).catch(() => null));
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
- return await io.readText(blobPath(root, entry.hash)).catch(() => null);
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
- if (!await io.exists(path).catch(() => false)) await io.writeText(path, side);
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
- if (readHistoryIndex(await io.readText(file).catch(() => null)).kind === "unreadable") {
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): the read this listing already performs is the one the
8725
- * body would cost again, so a content-consuming caller pays no second pass.
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
- const bytes = this.io.size === void 0 ? null : await this.io.size(full).catch(() => null);
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 content = await this.io.readText(full).catch(() => null);
8932
- if (content === null) return null;
8933
- chars[path] = content.length;
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 currentForLimit = await this.io.readText(path).catch(() => null);
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 body = await this.io.readText(lockPath).catch(() => null);
9602
- if (body === null) return;
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.io.readText(reference.target).catch(() => null);
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.io.readText(target).catch(() => null);
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.io.readText(target).catch(() => null);
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
- await this.audit(name, plan.auditAction, md, landing.find((entry) => entry.target.split(/[\\/]/).pop() === "SKILL.md")?.content ?? md, plan.auditSummary);
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
- if ((await this.io.readText(join(archiveRoot, candidate, ".archive-name")).catch(() => null))?.trim() === name) {
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 parsed = parseFrontmatter(await this.io.readText(join(archiveRoot, candidate, "SKILL.md")).catch(() => null) ?? "");
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
- await this.audit(name, "restore", null, await this.io.readText(join(dest, "SKILL.md")).catch(() => null), `restored from ${source}`);
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 before = await this.io.readText(target).catch(() => null);
10439
- if (before === null) return {
10440
- ok: false,
10441
- message: `"${filePath}" is not a readable regular file — remove the files inside it one by one.`
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", before, null, `removed ${filePath}`, filePath);
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 mtime = await this.io.mtime(join(archiveRoot, entry)).catch(() => null);
10497
- if (mtime === null || mtime > cutoff) continue;
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 for one minor version
10858
- * (0.6.x) and removable in 0.7.0.
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 vs profile-root bundle)."
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
- 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_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, 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, UPSTREAM_LIMIT_CHARS_PER_TOKEN, VERSION_SUMMARY_MAX_CHARS, 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, clampedNumber, classifyNamespaceMigration, clearReviewChannel, collectReadSkillNames, composePresetComposition, composePresetEntry, composePresetInsert, composePresetRow, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, contentRetentionFeedback, countDispatches, createGateSet, daysSinceIso, decideTakeover, 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, 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, pendingSelfCleanup, persistedWriteSite, persistedWriteSites, planNamespaceMigration, planReferenceRewrite, planRehoming, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, probeAbsent, probeList, probeMtime, probePresent, probeReason, probeUnknown, projectionText, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, readHistoryIndex, readJsonBody, readJsonObject, readNumberParam, readParam, readingStats, recordMutation, recordVersions, redactSecrets, relatedSkillNames, releaseInstance, removePresetRow, renameWithRetry, renderCuratorReportMarkdown, 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, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, versionActionKind, versionRowCells, versionSummaryPrompt, versionTarget, vocabulary, writeDurableTmp, writeJson, yamlPlainScalarNeedsQuotes };
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 };