@lmzhen/dsh-evolution-core 0.17.1 → 0.18.0

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
@@ -1198,6 +1198,24 @@ async function probeList(io, dir) {
1198
1198
  }
1199
1199
  }
1200
1200
  /**
1201
+ * Three-state TEXT read of one file. A missing path (ENOENT/ENOTDIR, or a backend whose read
1202
+ * answers null) is `absent`; every other failure is `unknown` with its reason. The distinction is
1203
+ * the whole point (N14/O-7): "I could not read this file" and "this file is not there" lead to
1204
+ * opposite decisions once a write is about to replace the bytes — treating the first as the
1205
+ * second is how a broken store reads as empty and a rescue copy never happens.
1206
+ * @param io - the IO seam.
1207
+ * @param path - the file to read.
1208
+ * @returns the probe.
1209
+ */
1210
+ async function probeText(io, path) {
1211
+ try {
1212
+ const value = await io.readText(path);
1213
+ return value === null ? probeAbsent() : probePresent(value);
1214
+ } catch (error) {
1215
+ return isMissingPath(error) ? probeAbsent() : probeUnknown(probeReason(error));
1216
+ }
1217
+ }
1218
+ /**
1201
1219
  * Three-state mtime read. Absent covers both "the path is missing" and "this
1202
1220
  * backend has no mtime probe" — the seam cannot tell those apart, so consumers
1203
1221
  * that must know say so in their own log line. A stat that FAILS is unknown.
@@ -1210,6 +1228,37 @@ async function probeMtime(io, path) {
1210
1228
  return probeUnknown(probeReason(error));
1211
1229
  }
1212
1230
  }
1231
+ /** The timestamp a curator run report DECLARES: `startedAt` (the run's own start), then `at` (the
1232
+ * stamp the error writer sets on every report — F-327/P2-4).
1233
+ * @param parsed - the parsed report, or anything else.
1234
+ * @returns epoch milliseconds, or null when neither field carries a parseable time.
1235
+ */
1236
+ function declaredReportStamp(parsed) {
1237
+ if (typeof parsed !== "object" || parsed === null) return null;
1238
+ const record = parsed;
1239
+ for (const value of [record.startedAt, record.at]) {
1240
+ if (typeof value !== "string") continue;
1241
+ const ms = Date.parse(value);
1242
+ if (Number.isFinite(ms)) return ms;
1243
+ }
1244
+ return null;
1245
+ }
1246
+ /** The time one curator run report is ordered by — ONE口径 for every reader (S2.9/T3-12): the
1247
+ * declared stamp above, then the optional mtime probe as the fallback. `null` means "no usable
1248
+ * time", and that single answer serves both consumers: such a report is never the LATEST (the
1249
+ * panel) and is never DELETED (the retention sweep), which is exactly the pair of answers the two
1250
+ * readers disagreed on when one ordered by the declared stamp and the other by mtime alone.
1251
+ * @param io - the IO seam; a backend without `mtime` simply has no fallback.
1252
+ * @param path - the report file the probe reads.
1253
+ * @param parsed - the already-parsed report.
1254
+ * @returns epoch milliseconds, or null.
1255
+ */
1256
+ async function reportTime(io, path, parsed) {
1257
+ const declared = declaredReportStamp(parsed);
1258
+ if (declared !== null) return declared;
1259
+ const probe = await probeMtime(io, path);
1260
+ return isPresent(probe) ? probe.value : null;
1261
+ }
1213
1262
  //#endregion
1214
1263
  //#region lib/types/io.js
1215
1264
  /**
@@ -3218,7 +3267,6 @@ const EVOLUTION_ERRORS = Object.freeze({
3218
3267
  "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
3268
  "e-309-revision-conflict-message-Re": "E-309: revision conflict — %a1%. Re-read with /evolution params --json and retry.",
3220
3269
  "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
3270
  "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
3271
  "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
3272
  "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 +3390,41 @@ function makeSerialQueue() {
3342
3390
  };
3343
3391
  }
3344
3392
  //#endregion
3393
+ //#region lib/types/payload.js
3394
+ /**
3395
+ * The memory op's payload field, resolved in ONE place (T3-06/A34).
3396
+ *
3397
+ * A memory op carries its body under two names — `facts` (the original) and
3398
+ * `content` (the schema alias) — and every reader used to resolve the pair with
3399
+ * `op.facts ?? op.content`. `??` only skips null/undefined, so
3400
+ * `{ facts: '', content: '正文' }` counted as "has facts": the plan validator
3401
+ * refused the op as empty (a FALSE refusal of a payload that is right there),
3402
+ * and the writers that normalized the pair dropped the body to `''`.
3403
+ *
3404
+ * The payload is the first field whose TRIMMED value is non-empty — a blank
3405
+ * string carries no payload, so it yields to its alias. Both blank (or absent)
3406
+ * ⇒ `''`, the one shape every caller already reads as "no payload".
3407
+ *
3408
+ * Non-string values are SKIPPED, never trimmed: a truthy non-string `facts`
3409
+ * used to reach `.trim()` and throw a TypeError out of callers that promise to
3410
+ * be deterministic (the E-60 / P2-1 class). The field-type gates reject such an
3411
+ * op on their own; this helper must not be the one that throws.
3412
+ *
3413
+ * The union posture matches evolution-threat's E-28a scan (BOTH fields are
3414
+ * scanned, not only the first truthy one); the two agree on what counts as
3415
+ * provided.
3416
+ * @module @lmzhen/dsh-evolution-core
3417
+ */
3418
+ /** The op's payload text: the first of `facts`/`content` whose trim is non-empty, trimmed; `''` when neither carries one. */
3419
+ function payloadText(op) {
3420
+ for (const value of [op?.facts, op?.content]) {
3421
+ if (typeof value !== "string") continue;
3422
+ const text = value.trim();
3423
+ if (text !== "") return text;
3424
+ }
3425
+ return "";
3426
+ }
3427
+ //#endregion
3345
3428
  //#region lib/types/numeric.js
3346
3429
  /**
3347
3430
  * Numeric config clamping for the dsh-evolution plugin family.
@@ -3383,6 +3466,35 @@ function clampedNumber(value, fallback, opts) {
3383
3466
  if (max !== void 0 && value > max) return fallback;
3384
3467
  return value;
3385
3468
  }
3469
+ /**
3470
+ * Clamp one numeric knob and warn ONCE per key.
3471
+ *
3472
+ * S2.9 (T3-10): `evolution-review` and `evolution-curator` each carried their own copy of this
3473
+ * helper — their own once-per-key set and their own message — so the two settings readers of ONE
3474
+ * policy row could drift apart. The logic lives here; each caller passes its own sink (and its own
3475
+ * package prefix). A live field is read at every use, so the warning cannot be per-read.
3476
+ *
3477
+ * @param warned - the keys this caller already warned about.
3478
+ * @param warn - the warning sink (each package prefixes its own name).
3479
+ * @param name - the config key, for the warning.
3480
+ * @param value - the value as supplied, or its volatile reference.
3481
+ * @param fallback - the package default a corrected value falls back to.
3482
+ * @param min - the smallest value the engine can act on.
3483
+ * @param max - the largest value the carrier can hold, when bounded.
3484
+ * @returns the value the engine may use.
3485
+ */
3486
+ function clampOnce(warned, warn, name, value, fallback, min, max) {
3487
+ const current = typeof value === "object" ? value.get() : value;
3488
+ const result = clampedNumber(current, fallback, max === void 0 ? { min } : {
3489
+ min,
3490
+ max
3491
+ });
3492
+ if (current !== void 0 && result !== current && !warned.has(name)) {
3493
+ warned.add(name);
3494
+ warn(`${name} provided an invalid value; falling back to the default`);
3495
+ }
3496
+ return result;
3497
+ }
3386
3498
  //#endregion
3387
3499
  //#region lib/types/threats.js
3388
3500
  /**
@@ -4204,7 +4316,7 @@ var MemoryStore = class {
4204
4316
  for (const [index, op] of operations.entries()) {
4205
4317
  const position = index + 1;
4206
4318
  if (op.action === "add") {
4207
- const body = (op.facts ?? op.content ?? "").trim();
4319
+ const body = payloadText(op);
4208
4320
  if (!body) return {
4209
4321
  result: {
4210
4322
  ok: false,
@@ -4283,7 +4395,7 @@ var MemoryStore = class {
4283
4395
  const matchIndex = matches[0]?.matchIndex ?? -1;
4284
4396
  if (op.action === "remove") working.splice(matchIndex, 1);
4285
4397
  else {
4286
- const body = (op.facts ?? op.content ?? "").trim();
4398
+ const body = payloadText(op);
4287
4399
  if (!body) return {
4288
4400
  result: {
4289
4401
  ok: false,
@@ -5025,8 +5137,8 @@ function presetPatchText(patchText) {
5025
5137
  * changes semantically: the bundle digest is the fail-closed signal for
5026
5138
  * review workers, so a stale id across deployments must be distinguishable.
5027
5139
  */
5028
- const PROMPT_BUNDLE_VERSION = 20;
5029
- const PROMPT_BUNDLE_ID = `dsh-evolution@20`;
5140
+ const PROMPT_BUNDLE_VERSION = 21;
5141
+ const PROMPT_BUNDLE_ID = `dsh-evolution@21`;
5030
5142
  const MEMORY_REVIEW_PROMPT = `[Auto-review — Memory]
5031
5143
  Review the conversation above and consider saving to memory if appropriate.
5032
5144
 
@@ -5317,12 +5429,12 @@ function sha256(text) {
5317
5429
  function createPromptBundle(prompts) {
5318
5430
  const canonical = JSON.stringify({
5319
5431
  id: PROMPT_BUNDLE_ID,
5320
- version: 20,
5432
+ version: 21,
5321
5433
  prompts: Object.fromEntries(Object.entries(prompts).sort())
5322
5434
  });
5323
5435
  return Object.freeze({
5324
5436
  id: PROMPT_BUNDLE_ID,
5325
- version: 20,
5437
+ version: 21,
5326
5438
  prompts: Object.freeze({ ...prompts }),
5327
5439
  sha256: sha256(canonical)
5328
5440
  });
@@ -5334,16 +5446,17 @@ const PROMPT_BUNDLE = createPromptBundle({
5334
5446
  skillPlan: SKILL_REVIEW_PLAN_PROMPT,
5335
5447
  combinedPlan: COMBINED_REVIEW_PLAN_PROMPT,
5336
5448
  curator: CURATOR_PROMPT,
5449
+ curatorDryRun: CURATOR_DRY_RUN_BANNER,
5337
5450
  completion: COMPLETION_SKILL_REVIEW_PROMPT,
5338
5451
  maintain: MAINTAIN_PROMPT,
5339
5452
  maintainOutput: MAINTAIN_OUTPUT_INSTRUCTION,
5340
5453
  skillsGuidance: SKILLS_GUIDANCE
5341
5454
  });
5342
5455
  function verifyPromptBundle(bundle = PROMPT_BUNDLE) {
5343
- if (bundle.id !== PROMPT_BUNDLE_ID || bundle.version !== 20) return false;
5456
+ if (bundle.id !== PROMPT_BUNDLE_ID || bundle.version !== 21) return false;
5344
5457
  const canonical = JSON.stringify({
5345
5458
  id: PROMPT_BUNDLE_ID,
5346
- version: 20,
5459
+ version: 21,
5347
5460
  prompts: Object.fromEntries(Object.entries(bundle.prompts).sort())
5348
5461
  });
5349
5462
  return bundle.sha256 === sha256(canonical);
@@ -5793,6 +5906,164 @@ function sweepReviewChannelSessions(isAlive) {
5793
5906
  return removed;
5794
5907
  }
5795
5908
  //#endregion
5909
+ //#region lib/types/platform-services.js
5910
+ /**
5911
+ * Every platform service the family reaches for with `ctx.get`, and what degrades without it.
5912
+ *
5913
+ * O-2 (v46 audit): the family probes 26 platform services optionally — the platform has no
5914
+ * formalized "optional dependency", so an ABSENT service is a silent downgrade and the diagnostic
5915
+ * surface is the only place a reader can learn about it. `/evolution doctor` used to judge six of
5916
+ * them; this table is the single source it judges the rest from, and the arch guard cross-checks it
5917
+ * against the actual probe sites so a new `ctx.get` cannot stay undeclared.
5918
+ *
5919
+ * `side` decides who can judge it: `host` probes run in the plugin process (doctor judges those),
5920
+ * `client` probes run in the browser half, where the host has no view of the seats.
5921
+ * @module @lmzhen/dsh-evolution-core
5922
+ */
5923
+ /** The 26 probes, in service-name order (the guard checks this list against the tree). */
5924
+ const PLATFORM_SERVICE_PROBES = Object.freeze([
5925
+ {
5926
+ service: "agents",
5927
+ feature: "review and curator subagent wake-ups",
5928
+ side: "host"
5929
+ },
5930
+ {
5931
+ service: "approval",
5932
+ feature: "the platform approval policy the approval row shadows",
5933
+ side: "host"
5934
+ },
5935
+ {
5936
+ service: "configEditor",
5937
+ feature: "the params view inherited-vs-override column",
5938
+ side: "host"
5939
+ },
5940
+ {
5941
+ service: "configForms",
5942
+ feature: "the settings card seat (its section renders read-only without it)",
5943
+ side: "client"
5944
+ },
5945
+ {
5946
+ service: "evolutionApproval",
5947
+ feature: "pending writes: list, approve and reject from the command surface",
5948
+ side: "host"
5949
+ },
5950
+ {
5951
+ service: "evolutionCurator",
5952
+ feature: "/evolution curator subcommands, skill undo and history",
5953
+ side: "host"
5954
+ },
5955
+ {
5956
+ service: "evolutionIo",
5957
+ feature: "every durable read and write (the IO seam)",
5958
+ side: "host"
5959
+ },
5960
+ {
5961
+ service: "evolutionMemoryBudget",
5962
+ feature: "the memory-budget divergence section of this report",
5963
+ side: "host"
5964
+ },
5965
+ {
5966
+ service: "evolutionPolicy",
5967
+ feature: "the policy snapshot that shadows row knobs",
5968
+ side: "host"
5969
+ },
5970
+ {
5971
+ service: "evolutionReplay",
5972
+ feature: "/evolution replay",
5973
+ side: "host"
5974
+ },
5975
+ {
5976
+ service: "evolutionState",
5977
+ feature: "the state facade behind activity and replay",
5978
+ side: "host"
5979
+ },
5980
+ {
5981
+ service: "llm",
5982
+ feature: "the curator one-shot review model call",
5983
+ side: "host"
5984
+ },
5985
+ {
5986
+ service: "memory",
5987
+ feature: "the cross-session memory store (review and learning-graph reads)",
5988
+ side: "host"
5989
+ },
5990
+ {
5991
+ service: "pluginInventory",
5992
+ feature: "the plugin inventory view",
5993
+ side: "host"
5994
+ },
5995
+ {
5996
+ service: "pluginManager",
5997
+ feature: "the platform plugin and preset surfaces this report reads",
5998
+ side: "host"
5999
+ },
6000
+ {
6001
+ service: "profileContext",
6002
+ feature: "writing the agent-preset row into a profile patch",
6003
+ side: "host"
6004
+ },
6005
+ {
6006
+ service: "sessionProjections",
6007
+ feature: "the domain-owned session units",
6008
+ side: "host"
6009
+ },
6010
+ {
6011
+ service: "sessionQuery",
6012
+ feature: "the session corpus behind /evolution sessions",
6013
+ side: "host"
6014
+ },
6015
+ {
6016
+ service: "settings",
6017
+ feature: "the settings user layer and the params view",
6018
+ side: "host"
6019
+ },
6020
+ {
6021
+ service: "skillUsage",
6022
+ feature: "usage counters feeding the curator and the review planner",
6023
+ side: "host"
6024
+ },
6025
+ {
6026
+ service: "skills",
6027
+ feature: "the platform skill catalog the skill tool writes through",
6028
+ side: "host"
6029
+ },
6030
+ {
6031
+ service: "storageDomain",
6032
+ feature: "the storage-domain state provider",
6033
+ side: "host"
6034
+ },
6035
+ {
6036
+ service: "subagents",
6037
+ feature: "spawning review and maintenance subagents",
6038
+ side: "host"
6039
+ },
6040
+ {
6041
+ service: "systemPrompt",
6042
+ feature: "the protected-skills prompt variable",
6043
+ side: "host"
6044
+ },
6045
+ {
6046
+ service: "tools",
6047
+ feature: "tool registration and the guard seam",
6048
+ side: "host"
6049
+ },
6050
+ {
6051
+ service: "webServer",
6052
+ feature: "the skill-history panel routes",
6053
+ side: "host"
6054
+ }
6055
+ ]);
6056
+ /**
6057
+ * The HOST-side probes that do not resolve in this runtime — what `/evolution doctor` prints as
6058
+ * "capability absence". Empty is the normal, healthy answer: every capability the family can use
6059
+ * is mounted, and the section stays away.
6060
+ * @param has - a `ctx.get(name) !== undefined` probe of the running context.
6061
+ * @returns the absent host-side probes, in declaration order.
6062
+ */
6063
+ function absentHostServices(has) {
6064
+ return PLATFORM_SERVICE_PROBES.filter((entry) => entry.side === "host" && !has(entry.service));
6065
+ }
6066
+ //#endregion
5796
6067
  //#region lib/types/instance-scope.js
5797
6068
  /**
5798
6069
  * B3 / G4 (0.3.78): the family's single-instance contract, made explicit.
@@ -7061,7 +7332,7 @@ function sessionReadSkillNames(ctx, session) {
7061
7332
  if (session === void 0) return void 0;
7062
7333
  const projected = sessionReadNames(ctx, session);
7063
7334
  if (projected !== void 0) return projected;
7064
- const events = session.snapshotEvents?.();
7335
+ const events = session.readEvents?.();
7065
7336
  return events === void 0 ? void 0 : collectReadSkillNames(events);
7066
7337
  }
7067
7338
  /** Actions whose target must have been read first (create is authorship, so it is exempt). */
@@ -7921,7 +8192,13 @@ function contentRetentionFeedback(input) {
7921
8192
  * @returns the versions; an absent or unreadable index reads as empty (never a throw).
7922
8193
  */
7923
8194
  async function loadVersions(root, io, name) {
7924
- return parseHistoryIndex(await io.readText(historyIndexFile(root, name)).catch(() => null));
8195
+ const file = historyIndexFile(root, name);
8196
+ const probe = await probeText(io, file);
8197
+ if (isUnknown(probe)) {
8198
+ console.warn(`skill-store: ${file} could not be read (${probe.reason}); reporting no versions for "${name}" — the bytes are untouched`);
8199
+ return [];
8200
+ }
8201
+ return parseHistoryIndex(isPresent(probe) ? probe.value : null);
7925
8202
  }
7926
8203
  /**
7927
8204
  * One version's content.
@@ -7934,7 +8211,12 @@ async function loadVersions(root, io, name) {
7934
8211
  async function loadVersionContent(root, io, name, v) {
7935
8212
  const entry = (await loadVersions(root, io, name)).find((candidate) => candidate.v === v);
7936
8213
  if (entry === void 0) return null;
7937
- return await io.readText(blobPath(root, entry.hash)).catch(() => null);
8214
+ const probe = await probeText(io, blobPath(root, entry.hash));
8215
+ if (isUnknown(probe)) {
8216
+ console.warn(`skill-store: version ${v} of "${name}" could not be read (${probe.reason}); reporting no content`);
8217
+ return null;
8218
+ }
8219
+ return isPresent(probe) ? probe.value : null;
7938
8220
  }
7939
8221
  /**
7940
8222
  * Record the content one mutation produced, best-effort.
@@ -7952,17 +8234,39 @@ async function recordVersions(root, io, input, keep) {
7952
8234
  for (const side of [input.before, input.after]) {
7953
8235
  if (side === null) continue;
7954
8236
  const path = blobPath(root, contentHash(side));
7955
- if (!await io.exists(path).catch(() => false)) await io.writeText(path, side);
8237
+ let blobPresent;
8238
+ try {
8239
+ blobPresent = await io.exists(path);
8240
+ } catch (error) {
8241
+ blobPresent = false;
8242
+ 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`);
8243
+ }
8244
+ if (!blobPresent) await io.writeText(path, side);
7956
8245
  }
7957
8246
  const file = historyIndexFile(root, input.skillName);
7958
- if (readHistoryIndex(await io.readText(file).catch(() => null)).kind === "unreadable") {
8247
+ const fileProbe = await probeText(io, file);
8248
+ if (isUnknown(fileProbe)) {
8249
+ const quarantine = `${file}.corrupt`;
8250
+ try {
8251
+ await io.copy(file, quarantine);
8252
+ } catch {}
8253
+ 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`);
8254
+ return null;
8255
+ }
8256
+ const probe = readHistoryIndex(isPresent(fileProbe) ? fileProbe.value : null);
8257
+ if (probe.kind === "unreadable") {
7959
8258
  const quarantine = `${file}.corrupt`;
7960
8259
  await io.copy(file, quarantine);
7961
8260
  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
8261
  }
7963
8262
  const recorded = {};
8263
+ let raceRefused = false;
7964
8264
  await transactIo(io, file, (current) => {
7965
8265
  const state = readHistoryIndex(current);
8266
+ if (state.kind === "unreadable" && probe.kind !== "unreadable") {
8267
+ raceRefused = true;
8268
+ return current;
8269
+ }
7966
8270
  const { versions, recorded: numbers } = nextHistoryIndex(state.kind === "ok" ? state.versions : [], input, keep);
7967
8271
  recorded.beforeVersion = numbers.beforeVersion;
7968
8272
  recorded.afterVersion = numbers.afterVersion;
@@ -7971,6 +8275,14 @@ async function recordVersions(root, io, input, keep) {
7971
8275
  versions
7972
8276
  }, null, 2);
7973
8277
  });
8278
+ if (raceRefused) {
8279
+ const quarantine = `${file}.corrupt`;
8280
+ try {
8281
+ await io.copy(file, quarantine);
8282
+ } catch {}
8283
+ 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`);
8284
+ return null;
8285
+ }
7974
8286
  return recorded;
7975
8287
  }
7976
8288
  /**
@@ -8721,10 +9033,19 @@ var SkillLibrary = class {
8721
9033
  /**
8722
9034
  * Summarize the skill tree.
8723
9035
  * @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.
9036
+ * its summary (v35 C11), `withFrontmatter` its parsed frontmatter record
9037
+ * (T2-07/A22): the read this listing already performs is the one either would
9038
+ * cost again, so a caller pays no second pass over the tree.
8726
9039
  * @returns one summary per readable skill directory.
8727
9040
  */
9041
+ /** T2-V2/A27: the skill names the TREE holds — the same listing rule {@link list} applies (dot
9042
+ * entries skipped, a SKILL.md must exist), WITHOUT reading any SKILL.md. A consumer that needs the
9043
+ * member set rather than the summaries (the learning graph's node set) pays one directory listing
9044
+ * instead of a whole-tree read pass.
9045
+ * @returns the sorted skill directory names. */
9046
+ async treeNames() {
9047
+ return await listNames(this.root, this.io);
9048
+ }
8728
9049
  async list(options = {}) {
8729
9050
  const summaries = [];
8730
9051
  for (const name of await listNames(this.root, this.io)) {
@@ -8774,7 +9095,8 @@ var SkillLibrary = class {
8774
9095
  ].some((value) => value === null),
8775
9096
  managed: hermesManaged === true,
8776
9097
  ...typeof parsedWhenToUse === "string" && parsedWhenToUse.trim() !== "" ? { whenToUse: parsedWhenToUse } : {},
8777
- ...options.withContent === true ? { content: md } : {}
9098
+ ...options.withContent === true ? { content: md } : {},
9099
+ ...options.withFrontmatter === true && parsed !== null ? { frontmatter: parsed.frontmatter } : {}
8778
9100
  });
8779
9101
  }
8780
9102
  return summaries;
@@ -8916,6 +9238,20 @@ var SkillLibrary = class {
8916
9238
  * @returns path -> character count (possibly empty), or null when the listing or
8917
9239
  * a size probe cannot answer — unknown is never an empty map.
8918
9240
  */
9241
+ /** A plan-time read whose bytes become a tree change's CAS baseline (`expected`). A failed read is
9242
+ * NAMED and answers `null`, which is FAIL-CLOSED at commit: the baseline is then "absent", so an
9243
+ * existing file counts as drift and the commit refuses instead of overwriting bytes nobody read
9244
+ * (N36: the failure is not silently "the file is not there").
9245
+ * @param target - the file a tree change is about to write.
9246
+ * @returns its bytes, or `null` for both "absent" and "could not read". */
9247
+ async readForCas(target) {
9248
+ const probe = await probeText(this.io, target);
9249
+ if (isUnknown(probe)) {
9250
+ 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`);
9251
+ return null;
9252
+ }
9253
+ return isPresent(probe) ? probe.value : null;
9254
+ }
8919
9255
  async supportFileChars(rawName) {
8920
9256
  const name = rawName.trim();
8921
9257
  if (this.badName(name) !== null) return null;
@@ -8925,12 +9261,21 @@ var SkillLibrary = class {
8925
9261
  const chars = {};
8926
9262
  for (const path of listed.value) {
8927
9263
  const full = join(dir, ...path.split("/").filter(Boolean));
8928
- const bytes = this.io.size === void 0 ? null : await this.io.size(full).catch(() => null);
9264
+ let bytes = null;
9265
+ if (this.io.size !== void 0) try {
9266
+ bytes = await this.io.size(full);
9267
+ } catch (error) {
9268
+ console.warn(`skill-store: size of ${full} could not be read (${error instanceof Error ? error.message : String(error)}); reporting no support-file char counts`);
9269
+ return null;
9270
+ }
8929
9271
  if (bytes === null) return null;
8930
9272
  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;
9273
+ const contentProbe = await probeText(this.io, full);
9274
+ if (!isPresent(contentProbe)) {
9275
+ if (isUnknown(contentProbe)) console.warn(`skill-store: ${full} could not be read (${contentProbe.reason}); reporting no support-file char counts`);
9276
+ return null;
9277
+ }
9278
+ chars[path] = contentProbe.value.length;
8934
9279
  }
8935
9280
  return chars;
8936
9281
  }
@@ -9236,7 +9581,9 @@ var SkillLibrary = class {
9236
9581
  ok: false,
9237
9582
  message: `Skill "${name}" is protected (${protection}).`
9238
9583
  };
9239
- const currentForLimit = await this.io.readText(path).catch(() => null);
9584
+ const limitProbe = await probeText(this.io, path);
9585
+ 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`);
9586
+ const currentForLimit = isPresent(limitProbe) ? limitProbe.value : null;
9240
9587
  const validation = validateFrontmatter(content, name, this.limits, currentForLimit);
9241
9588
  if (validation) return {
9242
9589
  ok: false,
@@ -9598,8 +9945,13 @@ var SkillLibrary = class {
9598
9945
  * R2 follow-up); anything else (a user support file or a live writer's
9599
9946
  * lock) is left untouched. */
9600
9947
  async sweepLockIfStranded(lockPath) {
9601
- const body = await this.io.readText(lockPath).catch(() => null);
9602
- if (body === null) return;
9948
+ const lockProbe = await probeText(this.io, lockPath);
9949
+ if (isUnknown(lockProbe)) {
9950
+ console.warn(`skill-store: could not read ${lockPath} to check whether its holder is alive (${lockProbe.reason}); leaving the lock in place`);
9951
+ return;
9952
+ }
9953
+ if (!isPresent(lockProbe)) return;
9954
+ const body = lockProbe.value;
9603
9955
  const pid = parseLockBody(body);
9604
9956
  if (pid === null) return;
9605
9957
  if (Number.isInteger(pid) && pid > 0 && isProcessAlive(pid) && !await this.lockHolderAgedOut(lockPath)) return;
@@ -9908,7 +10260,7 @@ var SkillLibrary = class {
9908
10260
  if (!freshTargetMd) throw new Error(`target "${targetName}" disappeared mid-consolidate`);
9909
10261
  const writes = [];
9910
10262
  for (const reference of referenceWrites) {
9911
- const previous = await this.io.readText(reference.target).catch(() => null);
10263
+ const previous = await this.readForCas(reference.target);
9912
10264
  const base = previous?.trimEnd() ?? "";
9913
10265
  writes.push({
9914
10266
  target: reference.target,
@@ -9918,7 +10270,7 @@ var SkillLibrary = class {
9918
10270
  }
9919
10271
  for (const file of rehomed) {
9920
10272
  const target = join(targetDir, ...file.to.split("/"));
9921
- const previous = await this.io.readText(target).catch(() => null);
10273
+ const previous = await this.readForCas(target);
9922
10274
  writes.push({
9923
10275
  target,
9924
10276
  content: file.content,
@@ -10083,7 +10435,7 @@ var SkillLibrary = class {
10083
10435
  const writes = [];
10084
10436
  for (const entry of byRel.values()) {
10085
10437
  const target = join(dir, ...entry.rel.split("/"));
10086
- const previous = await this.io.readText(target).catch(() => null);
10438
+ const previous = await this.readForCas(target);
10087
10439
  const base = previous?.trimEnd() ?? "";
10088
10440
  writes.push({
10089
10441
  target,
@@ -10212,7 +10564,10 @@ var SkillLibrary = class {
10212
10564
  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
10565
  };
10214
10566
  }
10215
- await this.audit(name, plan.auditAction, md, landing.find((entry) => entry.target.split(/[\\/]/).pop() === "SKILL.md")?.content ?? md, plan.auditSummary);
10567
+ const skillMdWrite = landing.find((entry) => entry.target.split(/[\\/]/).pop() === "SKILL.md");
10568
+ const auditedBefore = skillMdWrite === void 0 ? md : skillMdWrite.previous;
10569
+ const auditedAfter = skillMdWrite === void 0 ? md : skillMdWrite.content;
10570
+ await this.audit(name, plan.auditAction, auditedBefore, auditedAfter, plan.auditSummary);
10216
10571
  this.notifyMutation({
10217
10572
  action: plan.eventAction,
10218
10573
  name,
@@ -10263,11 +10618,21 @@ var SkillLibrary = class {
10263
10618
  let chosen;
10264
10619
  const unreadable = [];
10265
10620
  for (const candidate of candidates) {
10266
- if ((await this.io.readText(join(archiveRoot, candidate, ".archive-name")).catch(() => null))?.trim() === name) {
10621
+ const markerProbe = await probeText(this.io, join(archiveRoot, candidate, ".archive-name"));
10622
+ if (isUnknown(markerProbe)) {
10623
+ unreadable.push(candidate);
10624
+ continue;
10625
+ }
10626
+ if ((isPresent(markerProbe) ? markerProbe.value.trim() : "") === name) {
10267
10627
  chosen = candidate;
10268
10628
  break;
10269
10629
  }
10270
- const parsed = parseFrontmatter(await this.io.readText(join(archiveRoot, candidate, "SKILL.md")).catch(() => null) ?? "");
10630
+ const mdProbe = await probeText(this.io, join(archiveRoot, candidate, "SKILL.md"));
10631
+ if (isUnknown(mdProbe)) {
10632
+ unreadable.push(candidate);
10633
+ continue;
10634
+ }
10635
+ const parsed = parseFrontmatter(isPresent(mdProbe) ? mdProbe.value : "");
10271
10636
  if (parsed?.frontmatter.name === name) {
10272
10637
  chosen = candidate;
10273
10638
  break;
@@ -10306,7 +10671,9 @@ var SkillLibrary = class {
10306
10671
  };
10307
10672
  await this.deleteStrandedLocks(dest);
10308
10673
  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}`);
10674
+ const restoredProbe = await probeText(this.io, join(dest, "SKILL.md"));
10675
+ 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`);
10676
+ await this.audit(name, "restore", null, isPresent(restoredProbe) ? restoredProbe.value : null, `restored from ${source}`);
10310
10677
  this.notifyMutation({
10311
10678
  action: "restore",
10312
10679
  name,
@@ -10435,17 +10802,23 @@ var SkillLibrary = class {
10435
10802
  message: `File "${filePath}" not found in skill "${name}".`
10436
10803
  };
10437
10804
  }
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
- };
10805
+ const beforeProbe = await probeText(this.io, target);
10806
+ if (!isPresent(beforeProbe)) {
10807
+ if (isUnknown(beforeProbe)) console.warn(`skill-store: could not read ${target} before removing it (${beforeProbe.reason}); refusing the removal`);
10808
+ return {
10809
+ ok: false,
10810
+ message: `"${filePath}" is not a readable regular file — remove the files inside it one by one.`
10811
+ };
10812
+ }
10813
+ const before = beforeProbe.value;
10443
10814
  let verdict = anchorVerdict(anchor, before);
10815
+ let lockedBefore = before;
10444
10816
  if (verdict === "match" && this.transact) {
10445
10817
  const ran = { done: false };
10446
10818
  await this.transact(this.io, target, (current) => {
10447
10819
  ran.done = true;
10448
10820
  verdict = anchorVerdict(anchor, current);
10821
+ if (verdict === "match" && current !== null) lockedBefore = current;
10449
10822
  return verdict === "match" ? null : current;
10450
10823
  });
10451
10824
  if (!ran.done) return {
@@ -10454,7 +10827,7 @@ var SkillLibrary = class {
10454
10827
  };
10455
10828
  } else if (verdict === "match") await this.io.remove(target);
10456
10829
  if (verdict !== "match") return anchorRefusalFile(name, filePath, verdict);
10457
- await this.audit(name, "remove_file", before, null, `removed ${filePath}`, filePath);
10830
+ await this.audit(name, "remove_file", lockedBefore, null, `removed ${filePath}`, filePath);
10458
10831
  this.notifyMutation({
10459
10832
  action: "remove_file",
10460
10833
  name,
@@ -10492,11 +10865,17 @@ var SkillLibrary = class {
10492
10865
  if (!this.io.mtime) return null;
10493
10866
  const cutoff = Date.now() - ARCHIVE_RETENTION_DAYS * 864e5;
10494
10867
  const expired = [];
10868
+ let undated = 0;
10495
10869
  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;
10870
+ const probe = await probeMtime(this.io, join(archiveRoot, entry));
10871
+ if (isUnknown(probe)) {
10872
+ undated += 1;
10873
+ continue;
10874
+ }
10875
+ if (isAbsent(probe) || probe.value > cutoff) continue;
10498
10876
  expired.push(entry);
10499
10877
  }
10878
+ 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
10879
  return expired;
10501
10880
  }
10502
10881
  /** V4 (design §16.6): support-file char policy, resolved at the call site. */
@@ -10854,8 +11233,9 @@ function newSkillLibrary(options) {
10854
11233
  * the SAME name in two carriers (reviewMode, staleAfterDays, archiveAfterDays —
10855
11234
  * resolved by the existing policy-shadows-row rule) and six use DIFFERENT names.
10856
11235
  * 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.
11236
+ * plugin-row name is a deprecated alias kept readable since 0.6.x. Removal was
11237
+ * planned for 0.7.0 and is deferred: the aliases still ship, so no later version
11238
+ * is claimed here.
10859
11239
  *
10860
11240
  * Reading stays compatible (a carrier still spelling the legacy name resolves),
10861
11241
  * writing is strict (the write path accepts canonical ids only, so no new
@@ -11992,6 +12372,21 @@ const PARAM_EXPOSURE = Object.freeze([
11992
12372
  docAnchor: "PARAMETERS.md#deployment",
11993
12373
  summary: "Stage foreground agent writes for approval too."
11994
12374
  },
12375
+ {
12376
+ id: "evolution-approval.pendingTtlMs",
12377
+ group: "deployment",
12378
+ tier: "E3",
12379
+ authority: "cordis",
12380
+ owner: "evolution-approval",
12381
+ applies: "live",
12382
+ docAnchor: "PARAMETERS.md#deployment",
12383
+ 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.",
12384
+ label: "待批过期时间",
12385
+ hint: "一条待批写入最多等多久:超时就按「拒绝」关掉(不会执行,写效果不会落地);0 = 永不过期(默认)。",
12386
+ control: "number",
12387
+ unit: "毫秒",
12388
+ values: ""
12389
+ },
11995
12390
  {
11996
12391
  id: "qualityWarnThreshold",
11997
12392
  group: "deployment",
@@ -12060,7 +12455,7 @@ const PARAM_EXPOSURE = Object.freeze([
12060
12455
  owner: "scripts",
12061
12456
  applies: "restart",
12062
12457
  docAnchor: "PARAMETERS.md#deployment",
12063
- summary: "Install target plane (layered preset vs profile-root bundle)."
12458
+ 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
12459
  },
12065
12460
  {
12066
12461
  id: "install.basePreset",
@@ -12158,6 +12553,35 @@ function paramSettingsId(owner) {
12158
12553
  * @returns the entry id the platform reports for that row.
12159
12554
  * @throws when the owner publishes no user layer.
12160
12555
  */
12556
+ /** The keys the USER set in one row's settings namespace (G1 §8.4-A).
12557
+ *
12558
+ * The settings user layer is read in exactly ONE place, and it reads KEY NAMES, never values: the
12559
+ * platform resolves the user layer into the row's live fields, so a value always comes from the
12560
+ * row. The precedence rule (user > policy) needs to know WHICH keys the user set, or a deployment
12561
+ * policy would silently override the user. S2.9 (T3-10): review and curator each had their own
12562
+ * reader, so one key could be resolved two ways.
12563
+ * @param ctx - the plugin context (a `get(name)` view is enough).
12564
+ * @param rowId - the row's registry namespace (`paramRowId(<package>)`).
12565
+ * @returns the keys present in that row's user layer.
12566
+ */
12567
+ function userSetKeys(ctx, rowId) {
12568
+ const entry = ctx.get("settings")?.describe?.({ redactSecrets: false }).find((item) => item.ns === rowId);
12569
+ return new Set(Object.keys(entry?.user ?? {}));
12570
+ }
12571
+ /** ONE key's effective value, in the design's precedence (G1 §8.4-A): the user layer (whose value
12572
+ * already arrived through the row's live field) → the deployment policy snapshot → the row itself.
12573
+ * S2.9 (T3-10): both readers of the policy row resolve through this, so a new key cannot get one
12574
+ * answer on the review side and another on the curator side.
12575
+ * @param userSet - the keys the user set (see {@link userSetKeys}).
12576
+ * @param row - the row's live fields.
12577
+ * @param snapshot - the deployment policy snapshot, when a policy row is mounted.
12578
+ * @param key - the key being resolved.
12579
+ * @returns the effective value.
12580
+ */
12581
+ function pickWithPolicy(userSet, row, snapshot, key) {
12582
+ if (userSet.has(key)) return row[key];
12583
+ return snapshot?.[key] ?? row[key];
12584
+ }
12161
12585
  function paramRowId(owner) {
12162
12586
  const id = paramSettingsId(owner);
12163
12587
  if (id === void 0) throw new Error("no settings namespace registered for owner `" + owner + "` (add it to PARAM_NAMESPACES)");
@@ -12462,4 +12886,48 @@ function sessionAudited(ctx, sessionId, sessionScoped) {
12462
12886
  return false;
12463
12887
  }
12464
12888
  //#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 };
12889
+ //#region lib/types/fold-untrusted.js
12890
+ /**
12891
+ * Untrusted text rendered as prompt lines (T3-03/A31) — ONE folding rule.
12892
+ *
12893
+ * The background-review digest embeds conversation text and tool output into a
12894
+ * plain-text prompt whose PRODUCT is a plan with write permission. Anything the
12895
+ * session ingested (a fetched page, a file body, command output) can therefore
12896
+ * carry a newline plus a line shaped like the prompt's own structure — a
12897
+ * `SYSTEM:`/`Review kind:` header, a `[result]` evidence line, a second copy
12898
+ * of the closing instruction — and forge structure or inject instructions.
12899
+ *
12900
+ * FOLDING, not deleting: the reader (the model) must still SEE what the data
12901
+ * said, it just must never become structure. Line separators become a visible
12902
+ * `⏎`, and a line that would read as a role/evidence header gets a visible
12903
+ * marker in front of it. Every original character stays in place.
12904
+ *
12905
+ * The boundary is the separator itself, not only a real newline: a payload can
12906
+ * paste the separator verbatim, so neutralization matches after `⏎` whether it
12907
+ * came from the fold or from the data. Both edits are idempotent (a second pass
12908
+ * cannot add a second marker).
12909
+ *
12910
+ * Scope: text rendered as its OWN line(s) of a prompt. `render-facts`'
12911
+ * sanitizer (evolution-maintenance) is the same posture for `key=value` fields
12912
+ * and additionally escapes its block delimiter; it is not superseded here.
12913
+ * @module @lmzhen/dsh-evolution-core
12914
+ */
12915
+ /** Visible stand-in for a folded line separator: one character that cannot be read as line structure. */
12916
+ const UNTRUSTED_LINE_BREAK = " ⏎ ";
12917
+ /** Visible marker prefixed to a line that would otherwise read as a role/structure header. */
12918
+ const UNTRUSTED_LINE_MARK = "· ";
12919
+ const NEWLINES = /\r\n|\r|\n/g;
12920
+ /** A line that reads as structure: a `Label:` head (SYSTEM:, Review kind:, Signals:) or a digest evidence tag.
12921
+ * The head is case-TOLERANT on purpose: this family's own prompt labels are mixed case, so an
12922
+ * uppercase-only rule neutralized SYSTEM: while leaving a forged "Review kind:" untouched. */
12923
+ const STRUCTURAL_HEAD = /(^|⏎)([ \t]*)((?:[A-Za-z][A-Za-z0-9 _]{2,}:|\[result\]|\[call\]))/g;
12924
+ /**
12925
+ * Fold untrusted `text` so it cannot become structure, then bound it to `limit`
12926
+ * visible characters. A non-positive `limit` means "no truncation".
12927
+ */
12928
+ function foldUntrustedLines(text, limit) {
12929
+ const marked = text.replace(NEWLINES, UNTRUSTED_LINE_BREAK).replace(STRUCTURAL_HEAD, (_match, boundary, spaces, head) => `${boundary}${spaces}· ${head}`);
12930
+ return limit > 0 ? marked.slice(0, limit) : marked;
12931
+ }
12932
+ //#endregion
12933
+ 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, 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 };