@lmzhen/dsh-evolution-core 0.3.78 → 0.3.80

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
@@ -1249,6 +1249,7 @@ function parseCuratorNominations(text) {
1249
1249
  let section = null;
1250
1250
  let currentFrom = "";
1251
1251
  let currentMode;
1252
+ let currentIntoSpoken = false;
1252
1253
  for (const line of text.split("\n")) {
1253
1254
  const header = /^\s*(consolidations|prunings)\s*:\s*$/.exec(line);
1254
1255
  if (header) {
@@ -1257,9 +1258,11 @@ function parseCuratorNominations(text) {
1257
1258
  }
1258
1259
  const consolidated = /^\s*-\s*from:\s*([a-z0-9][a-z0-9-]*)(?:\s*#.*)?\s*$/.exec(line);
1259
1260
  if (consolidated) {
1261
+ if (currentFrom !== "" && !currentIntoSpoken) warnings.push(`"- from: ${currentFrom}" dropped — no "into:" arrived before the next entry`);
1260
1262
  section = "consolidations";
1261
1263
  currentFrom = consolidated[1] ?? "";
1262
1264
  currentMode = void 0;
1265
+ currentIntoSpoken = false;
1263
1266
  continue;
1264
1267
  }
1265
1268
  const mode = /^\s*mode:\s*(append|reference)(?:\s*#.*)?\s*$/.exec(line);
@@ -1278,6 +1281,7 @@ function parseCuratorNominations(text) {
1278
1281
  });
1279
1282
  currentFrom = "";
1280
1283
  currentMode = void 0;
1284
+ currentIntoSpoken = false;
1281
1285
  continue;
1282
1286
  }
1283
1287
  const pruned = /^\s*-\s*name:\s*([a-z0-9][a-z0-9-]*)(?:\s*#.*)?\s*$/.exec(line);
@@ -1288,8 +1292,12 @@ function parseCuratorNominations(text) {
1288
1292
  if (name) prunings.push(name);
1289
1293
  continue;
1290
1294
  }
1291
- if (/^\s*(?:-\s*(?:from|name)\s*:|(?:into|mode)\s*:)/.test(line)) warnings.push(`"${line.trim()}" ignored - not a usable nomination line (names are lowercase letters, digits and hyphens; one optional trailing "# comment" is allowed)`);
1295
+ if (/^\s*(?:-\s*(?:from|name)\s*:|(?:into|mode)\s*:)/.test(line)) {
1296
+ if (currentFrom !== "" && /^\s*into\s*:/.test(line)) currentIntoSpoken = true;
1297
+ warnings.push(`"${line.trim()}" ignored - not a usable nomination line (names are lowercase letters, digits and hyphens; one optional trailing "# comment" is allowed)`);
1298
+ }
1292
1299
  }
1300
+ if (currentFrom !== "" && !currentIntoSpoken) warnings.push(`"- from: ${currentFrom}" dropped — the nomination output ended before its "into:"`);
1293
1301
  const valid = (name) => NOMINATION_NAME_RE.test(name);
1294
1302
  return {
1295
1303
  prunings: prunings.filter(valid),
@@ -2167,12 +2175,16 @@ const TYPOGRAPHY_CHARS = /[\u00ad\u061c\u180e\u200c\ufe00-\ufe0f]/;
2167
2175
  const BIDI_CHARS = /[\u202a-\u202e\u2066-\u2069]/;
2168
2176
  const FORMAT_CONTROL_CLASS = "\\p{Cf}\\p{Zl}\\p{Zp}\\u0000\\u2065\\ufff0-\\ufff8\\u{e0080}-\\u{e00ff}";
2169
2177
  const FORMAT_CONTROL_TEST = new RegExp(`[${FORMAT_CONTROL_CLASS}]`, "u");
2170
- /** S0.4 (v37 P0-3): a format control the three finding sets above do NOT report. */
2178
+ /** S0.4 (v37 P0-3): a format control the three finding sets above do NOT report.
2179
+ * S1-E3: the ZWJ exemption compares the real character. It used to read
2180
+ * `'\\u200d'` — a 6-character literal that never equals a code point — so the
2181
+ * branch was dead and every emoji ZWJ sequence (👨‍👩‍👧) raised the report-level
2182
+ * `unicode_format_control` finding the class comment above promises it will not. */
2171
2183
  function hasUnreportedFormatControl(text) {
2172
2184
  for (const character of text) {
2173
2185
  if (!FORMAT_CONTROL_TEST.test(character)) continue;
2174
2186
  if (ZERO_WIDTH_CHARS.test(character) || TYPOGRAPHY_CHARS.test(character) || BIDI_CHARS.test(character)) continue;
2175
- if (character === "\\u200d") continue;
2187
+ if (character === "") continue;
2176
2188
  return true;
2177
2189
  }
2178
2190
  return false;
@@ -4212,8 +4224,19 @@ function readDispatchRecord(event) {
4212
4224
  */
4213
4225
  var ToolDispatchNormalizer = class {
4214
4226
  records = /* @__PURE__ */ new Map();
4215
- /** Call ids of \`run_code\`-class calls, i.e. of dispatches whose sub-dispatches are \`program\`. */
4216
- programRoots = /* @__PURE__ */ new Set();
4227
+ /** S2-P2-14: cap for the LEDGER (and the settle markers) — opt-in, meant for
4228
+ * the long-lived live listener (skill-usage) whose ledger is pure dedup
4229
+ * state and would otherwise grow with the process lifetime. `Infinity` (the
4230
+ * default, used by whole-log folding) keeps every dispatch. Eviction is
4231
+ * oldest-first: a call/result pair spans one tool call, so an evicted
4232
+ * dispatch can never be re-paired within any realistic window. */
4233
+ maxTracked;
4234
+ /** S2-P2-12: call ids whose outcome has already been reported to the settle
4235
+ * channel — a replayed result event must not settle the same dispatch twice. */
4236
+ settledIds = /* @__PURE__ */ new Set();
4237
+ constructor(options = {}) {
4238
+ this.maxTracked = options.maxTracked ?? Number.POSITIVE_INFINITY;
4239
+ }
4217
4240
  /**
4218
4241
  * Absorb one session event.
4219
4242
  * @param event - the event to absorb; any non-dispatch event is ignored.
@@ -4235,7 +4258,7 @@ var ToolDispatchNormalizer = class {
4235
4258
  return null;
4236
4259
  }
4237
4260
  const signal = {
4238
- kind: record.kind === "native" && this.programRoots.has(record.callId) ? "program-root" : record.kind,
4261
+ kind: record.kind,
4239
4262
  callId: record.callId,
4240
4263
  rootCallId: record.rootCallId,
4241
4264
  name: record.name,
@@ -4243,9 +4266,43 @@ var ToolDispatchNormalizer = class {
4243
4266
  ok: record.outcome?.ok
4244
4267
  };
4245
4268
  this.records.set(record.callId, signal);
4246
- if (record.kind === "program" && record.rootCallId !== record.callId) this.programRoots.add(record.rootCallId);
4269
+ this.evict();
4270
+ return signal;
4271
+ }
4272
+ /**
4273
+ * S2-P2-12: the SETTLE channel. \`advance\` answers \`null\` for the paired
4274
+ * event that settles an already-emitted dispatch, so a consumer that must act
4275
+ * on the OUTCOME (e.g. count successful skill reads only) cannot tell which
4276
+ * dispatch a \`null\` belonged to. Call this with the same event AFTER
4277
+ * \`advance\`: it answers the settled dispatch exactly once (replayed settle
4278
+ * events answer \`null\`).
4279
+ * @param event - the event already absorbed by \`advance\`.
4280
+ * @returns the dispatch this event settled, or \`null\`.
4281
+ */
4282
+ settledSignalOf(event) {
4283
+ const record = readDispatchRecord(event);
4284
+ if (record === null || record.outcome === void 0) return null;
4285
+ if (this.settledIds.has(record.callId)) return null;
4286
+ const signal = this.records.get(record.callId);
4287
+ if (signal === void 0) return null;
4288
+ this.settledIds.add(record.callId);
4289
+ this.evict();
4247
4290
  return signal;
4248
4291
  }
4292
+ /** Oldest-first eviction once the ledger (or the settle markers) overflows. */
4293
+ evict() {
4294
+ while (this.records.size > this.maxTracked) {
4295
+ const oldest = this.records.keys().next().value;
4296
+ if (oldest === void 0) break;
4297
+ this.records.delete(oldest);
4298
+ this.settledIds.delete(oldest);
4299
+ }
4300
+ while (this.settledIds.size > this.maxTracked) {
4301
+ const oldest = this.settledIds.values().next().value;
4302
+ if (oldest === void 0) break;
4303
+ this.settledIds.delete(oldest);
4304
+ }
4305
+ }
4249
4306
  /** Every emitted dispatch, in first-seen order. */
4250
4307
  get signals() {
4251
4308
  return [...this.records.values()];
@@ -4366,10 +4423,14 @@ function isProgramToolName(name) {
4366
4423
  * Assert that a payload really is the dispatch event it claims to be.
4367
4424
  *
4368
4425
  * The normalizer is deliberately lenient (it also folds persisted logs, where a
4369
- * payload may predate the current declaration); this is the loud gate for the
4370
- * live path, where a malformed payload means the producer is broken. It never
4371
- * silently downgrades: an unrecognized event type or a payload missing a
4372
- * declared field throws a named \`ToolDispatchPayloadError\`.
4426
+ * payload may predate the current declaration). S1-E7 (honesty fix): this
4427
+ * function is an OPT-IN gate NO production consumer calls it; the shipped
4428
+ * listeners fold tolerant (`'malformed'` synthesis) precisely because a
4429
+ * throwing gate wired into every session event would brick the family on any
4430
+ * upstream payload drift. Deployments that want fail-loud behavior call this
4431
+ * from their own `session/event` listener. It never silently downgrades: an
4432
+ * unrecognized event type or a payload missing a declared field throws a named
4433
+ * \`ToolDispatchPayloadError\`.
4373
4434
  * @param event - the session event to check.
4374
4435
  * @returns nothing; throws when the payload violates the platform declaration.
4375
4436
  */
@@ -4856,6 +4917,18 @@ function resolveOrigins(headerOrigin, isReview = false) {
4856
4917
  library: "foreground"
4857
4918
  };
4858
4919
  }
4920
+ /**
4921
+ * S1-E8 (0.3.80): ONE exec→origins resolution for the two write tools — reads
4922
+ * the session header origin AND the v37 S2.2 review-channel session mark, so a
4923
+ * tool cannot forget the mark half (tool-memory shipped without it, which
4924
+ * mislabeled every inject-mode review memory write as `foreground` and let it
4925
+ * bypass staging under `stageForeground: false`).
4926
+ * Single source: both tools call this instead of re-deriving the pair.
4927
+ */
4928
+ function resolveExecOrigins(exec) {
4929
+ const session = exec?.agent?.session;
4930
+ return resolveOrigins(session?.header?.origin, isReviewChannelSession(typeof session?.id === "string" ? session.id : void 0));
4931
+ }
4859
4932
  function skillDir(root, name) {
4860
4933
  return join(root, name);
4861
4934
  }
@@ -6194,7 +6267,7 @@ var SkillLibrary = class {
6194
6267
  skillDir: dir
6195
6268
  }
6196
6269
  };
6197
- }, anchor !== void 0 ? anchorRefusal(name, "missing") : void 0);
6270
+ }, anchor !== void 0 ? anchorUnverifiable(name, null) : void 0);
6198
6271
  }
6199
6272
  async patch(rawName, oldString, newString, filePath = "", replaceAll = false, origin = "foreground") {
6200
6273
  const name = rawName.trim();
@@ -6619,6 +6692,7 @@ var SkillLibrary = class {
6619
6692
  return await this.serial(async () => {
6620
6693
  const referenceWrites = [];
6621
6694
  const parts = [];
6695
+ const plannedSourceBytes = /* @__PURE__ */ new Map();
6622
6696
  if (mode === "append") for (const source of normalizedSources) {
6623
6697
  const protection = await this.deleteProtection(source);
6624
6698
  if (protection) return {
@@ -6645,6 +6719,7 @@ var SkillLibrary = class {
6645
6719
  message: `Consolidation rejected: source "${source}" body references support files (${refs.join(", ")}) that would be left behind — use mode:'reference' or archive the whole package instead.`
6646
6720
  };
6647
6721
  parts.push(`\n<!-- consolidated from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}`);
6722
+ plannedSourceBytes.set(source, sourceMd);
6648
6723
  }
6649
6724
  else for (const source of normalizedSources) {
6650
6725
  const protection = await this.deleteProtection(source);
@@ -6672,6 +6747,7 @@ var SkillLibrary = class {
6672
6747
  target,
6673
6748
  content: `<!-- demoted from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}\n`
6674
6749
  });
6750
+ plannedSourceBytes.set(source, sourceMd);
6675
6751
  }
6676
6752
  const archived = [];
6677
6753
  try {
@@ -6680,15 +6756,15 @@ var SkillLibrary = class {
6680
6756
  message: `Skill "${targetName}" not found.`
6681
6757
  };
6682
6758
  for (const source of normalizedSources) {
6759
+ if (plannedSourceBytes.has(source)) {
6760
+ if (await this.io.readText(join(this.dirOf(source), "SKILL.md")) !== plannedSourceBytes.get(source)) throw new Error(`Consolidation aborted: source "${source}" changed while the consolidation ran (its planned bytes are no longer current).`);
6761
+ }
6683
6762
  const result = await this.archive(source, { absorbedInto: targetName });
6684
6763
  if (!result.ok) throw new Error(result.message);
6685
6764
  archived.push(source);
6686
6765
  }
6687
6766
  const freshTargetMd = await this.io.readText(join(targetDir, "SKILL.md"));
6688
- if (!freshTargetMd) return {
6689
- ok: false,
6690
- message: `Skill "${targetName}" not found.`
6691
- };
6767
+ if (!freshTargetMd) throw new Error(`target "${targetName}" disappeared mid-consolidate`);
6692
6768
  const writes = [];
6693
6769
  for (const reference of referenceWrites) {
6694
6770
  const previous = await this.io.readText(reference.target).catch(() => null);
@@ -6702,10 +6778,7 @@ var SkillLibrary = class {
6702
6778
  if (mode === "append") {
6703
6779
  const merged = freshTargetMd.trimEnd() + parts.join("\n") + "\n";
6704
6780
  const validation = validateFrontmatter(merged, targetName, this.limits);
6705
- if (validation) return {
6706
- ok: false,
6707
- message: `Consolidation rejected: ${validation}`
6708
- };
6781
+ if (validation) throw new Error(`merge validation failed: ${validation}`);
6709
6782
  writes.push({
6710
6783
  target: join(targetDir, "SKILL.md"),
6711
6784
  content: merged,
@@ -6715,10 +6788,7 @@ var SkillLibrary = class {
6715
6788
  const pointerLines = normalizedSources.map((source) => `\n${POINTER_LINE_PREFIX}${source}.md`).join("");
6716
6789
  const extended = freshTargetMd.trimEnd() + pointerLines + "\n";
6717
6790
  const validation = validateFrontmatter(extended, targetName, this.limits);
6718
- if (validation) return {
6719
- ok: false,
6720
- message: `Consolidation rejected: ${validation}`
6721
- };
6791
+ if (validation) throw new Error(`merge validation failed: ${validation}`);
6722
6792
  writes.push({
6723
6793
  target: join(targetDir, "SKILL.md"),
6724
6794
  content: extended,
@@ -7459,7 +7529,15 @@ var SkillLibrary = class {
7459
7529
  message: `Snapshot restore was refused before anything was cleared: taking the pre-rollback snapshot failed (${error instanceof Error ? error.message : String(error)}) — the active tree is UNCHANGED. Resolve the snapshot write failure and retry.`
7460
7530
  };
7461
7531
  }
7462
- const snapshotExtras = await this.readSnapshotExtras(latest.path);
7532
+ let snapshotExtras;
7533
+ try {
7534
+ snapshotExtras = await this.readSnapshotExtras(latest.path);
7535
+ } catch (error) {
7536
+ return {
7537
+ ok: false,
7538
+ message: `Snapshot restore was refused before anything was cleared: reading the snapshot manifest/extras failed (${error instanceof Error ? error.message : String(error)}) — the active tree is UNCHANGED. Resolve the read failure and retry.`
7539
+ };
7540
+ }
7463
7541
  try {
7464
7542
  await this.restoreSnapshotIntoRoot(latest.path);
7465
7543
  } catch (error) {
@@ -7607,4 +7685,4 @@ function sessionAudited(ctx, sessionId, sessionScoped) {
7607
7685
  return sessionSeesFamilyTools(ctx, sessionId);
7608
7686
  }
7609
7687
  //#endregion
7610
- export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, EMPTY_LOCK_TAKEOVER_MS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, INSTANCE_KEYS, LOCK_BODY_RE, LOCK_SUFFIX, LOCK_TEAR_TAKEOVER_MS, LOW_QUALITY_THRESHOLD, LostWriteLock, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PATTERN_OVERLAP, PERSISTED_WRITE_SITES, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, THREAT_EXEMPTION_HINT, ToolDispatchNormalizer, ToolDispatchPayloadError, advanceReview, allowRowCollisions, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, callingScope, claimInstance, clampedNumber, clearReviewChannel, collectReadSkillNames, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, countDispatches, createGateSet, decideTakeover, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionEventPayloadIssue, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurn, frontmatterBlock, frontmatterCatalogInvalid, getRecord, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isCommittedWarning, isGlobalRead, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadRowOverrides, loadSuppressedNames, loadUsage, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, neutralizePromptVariables, newSkillLibrary, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseLockBody, pendingSelfCleanup, persistedWriteSite, probeAbsent, probeList, probeMtime, probePresent, probeReason, probeUnknown, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, releaseInstance, renameWithRetry, renderCuratorReportMarkdown, resolveOrigins, resolveRootConfig, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, sessionAudited, sessionSeesFamilyTools, skillReadNameOf, skillsRoot, suppressedFile, sweepReviewChannelSessions, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, writeDurableTmp, yamlPlainScalarNeedsQuotes };
7688
+ export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, EMPTY_LOCK_TAKEOVER_MS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, INSTANCE_KEYS, LOCK_BODY_RE, LOCK_SUFFIX, LOCK_TEAR_TAKEOVER_MS, LOW_QUALITY_THRESHOLD, LostWriteLock, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PATTERN_OVERLAP, PERSISTED_WRITE_SITES, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, THREAT_EXEMPTION_HINT, ToolDispatchNormalizer, ToolDispatchPayloadError, advanceReview, allowRowCollisions, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, callingScope, claimInstance, clampedNumber, clearReviewChannel, collectReadSkillNames, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, countDispatches, createGateSet, decideTakeover, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionEventPayloadIssue, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurn, frontmatterBlock, frontmatterCatalogInvalid, getRecord, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isCommittedWarning, isGlobalRead, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadRowOverrides, loadSuppressedNames, loadUsage, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, neutralizePromptVariables, newSkillLibrary, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseLockBody, pendingSelfCleanup, persistedWriteSite, probeAbsent, probeList, probeMtime, probePresent, probeReason, probeUnknown, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, releaseInstance, renameWithRetry, renderCuratorReportMarkdown, resolveExecOrigins, resolveOrigins, resolveRootConfig, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, sessionAudited, sessionSeesFamilyTools, skillReadNameOf, skillsRoot, suppressedFile, sweepReviewChannelSessions, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, writeDurableTmp, yamlPlainScalarNeedsQuotes };
package/lib/types/io.d.ts CHANGED
@@ -46,16 +46,17 @@ export interface EvolutionIoLike {
46
46
  * missing path, stat failure). Intended as a cheap invalidation stamp for a
47
47
  * cached directory listing; a backend without it keeps event-driven
48
48
  * invalidation only. v20 correction: the former "NO in-tree consumer" note
49
- * (V9-07, 0.3.51) went stale — there are now FOUR in-tree consumers, and a
49
+ * (V9-07, 0.3.51) went stale — in-tree consumers exist, and a
50
50
  * custom backend that omits `mtime` degrades them silently (every call
51
51
  * site is optional-call + null-fallback, so omission stays legal):
52
- * - evolution-skill-catalog: root-mtime stamp on the summaries cache —
53
- * the second, out-of-band invalidation signal next to the
54
- * `evolution/skill-mutated` / `evolution/skills-refresh` events;
55
52
  * - evolution-curator: run-report recency ordering (2 call sites);
56
53
  * - evolution-commands: `.bak` freshness probe in the preset installer.
57
- * With `mtime` absent, catalog invalidation degrades to purely
58
- * event-driven. Register new consumers here (the seam contract).
54
+ * S3-P2-10: evolution-skill-catalog's summaries stamp the third consumer —
55
+ * moved to a NAMES-based stamp over `list()` (a required seam method): the
56
+ * root-mtime stamp was invalidated by the family's own sidecar flushes in
57
+ * the watched root, so every usage-counter write paid a full rescan. The
58
+ * names stamp is sidecar-immune and works on mtime-less backends. Register
59
+ * new consumers here (the seam contract).
59
60
  */
60
61
  mtime?(this: void, path: string): Promise<number | null>;
61
62
  }
@@ -215,6 +215,30 @@ export declare function resolveOrigins(headerOrigin: string | undefined, isRevie
215
215
  approval: 'foreground' | 'background_review';
216
216
  library: WriteOrigin;
217
217
  };
218
+ /** Minimal structural view of a tool execution context, so the helper below
219
+ * stays decoupled from the platform's exec type (extra fields are ignored). */
220
+ export interface EvolutionExecOriginView {
221
+ agent?: {
222
+ session?: {
223
+ id?: unknown;
224
+ header?: {
225
+ origin?: string;
226
+ };
227
+ };
228
+ };
229
+ }
230
+ /**
231
+ * S1-E8 (0.3.80): ONE exec→origins resolution for the two write tools — reads
232
+ * the session header origin AND the v37 S2.2 review-channel session mark, so a
233
+ * tool cannot forget the mark half (tool-memory shipped without it, which
234
+ * mislabeled every inject-mode review memory write as `foreground` and let it
235
+ * bypass staging under `stageForeground: false`).
236
+ * Single source: both tools call this instead of re-deriving the pair.
237
+ */
238
+ export declare function resolveExecOrigins(exec: EvolutionExecOriginView | undefined): {
239
+ approval: 'foreground' | 'background_review';
240
+ library: WriteOrigin;
241
+ };
218
242
  /** Dot-prefixed on-disk marker name. SINGLE source: `list()` matches directory
219
243
  * entries against this name, and path builders must never hardcode a marker
220
244
  * literal (N-1: the rc.49 exists()-probe convergence dropped the dot,
@@ -39,9 +39,7 @@ export type ToolDispatchKind =
39
39
  /** A model-authored call, logged as \`tool/call\` and settled by \`tool/result\`. */
40
40
  'native'
41
41
  /** A sub-dispatch of a code program, logged as the PTC dispatch pair. */
42
- | 'program'
43
- /** A \`run_code\` call itself: a native call whose program dispatches sub-calls. */
44
- | 'program-root';
42
+ | 'program';
45
43
  /** The platform event type carrying a PTC sub-dispatch start. */
46
44
  export declare const PTC_DISPATCH_START_EVENT = "tool/ptc-dispatch-start";
47
45
  /** The platform event type carrying a PTC sub-dispatch settle. */
@@ -63,7 +61,7 @@ export declare const DISPATCH_EVENT_TYPES: readonly string[];
63
61
  * so a consumer that reacted at start already holds the outcome.
64
62
  */
65
63
  export interface ToolDispatchSignal {
66
- /** \`native\` | \`program\` (a code-program sub-dispatch) | \`program-root\` (the \`run_code\` call itself). */
64
+ /** \`native\` | \`program\` (a code-program sub-dispatch); the \`run_code\` root itself reads as \`native\`. */
67
65
  readonly kind: ToolDispatchKind;
68
66
  /** \`callId\` for a native call, \`subCallId\` for a PTC sub-dispatch — the platform's own identity. */
69
67
  readonly callId: string;
@@ -94,8 +92,19 @@ export declare class ToolDispatchPayloadError extends TypeError {
94
92
  */
95
93
  export declare class ToolDispatchNormalizer {
96
94
  private readonly records;
97
- /** Call ids of \`run_code\`-class calls, i.e. of dispatches whose sub-dispatches are \`program\`. */
98
- private readonly programRoots;
95
+ /** S2-P2-14: cap for the LEDGER (and the settle markers) — opt-in, meant for
96
+ * the long-lived live listener (skill-usage) whose ledger is pure dedup
97
+ * state and would otherwise grow with the process lifetime. `Infinity` (the
98
+ * default, used by whole-log folding) keeps every dispatch. Eviction is
99
+ * oldest-first: a call/result pair spans one tool call, so an evicted
100
+ * dispatch can never be re-paired within any realistic window. */
101
+ private readonly maxTracked;
102
+ /** S2-P2-12: call ids whose outcome has already been reported to the settle
103
+ * channel — a replayed result event must not settle the same dispatch twice. */
104
+ private readonly settledIds;
105
+ constructor(options?: {
106
+ maxTracked?: number;
107
+ });
99
108
  /**
100
109
  * Absorb one session event.
101
110
  * @param event - the event to absorb; any non-dispatch event is ignored.
@@ -107,6 +116,22 @@ export declare class ToolDispatchNormalizer {
107
116
  type: string;
108
117
  data?: unknown;
109
118
  }): ToolDispatchSignal | null;
119
+ /**
120
+ * S2-P2-12: the SETTLE channel. \`advance\` answers \`null\` for the paired
121
+ * event that settles an already-emitted dispatch, so a consumer that must act
122
+ * on the OUTCOME (e.g. count successful skill reads only) cannot tell which
123
+ * dispatch a \`null\` belonged to. Call this with the same event AFTER
124
+ * \`advance\`: it answers the settled dispatch exactly once (replayed settle
125
+ * events answer \`null\`).
126
+ * @param event - the event already absorbed by \`advance\`.
127
+ * @returns the dispatch this event settled, or \`null\`.
128
+ */
129
+ settledSignalOf(event: {
130
+ type: string;
131
+ data?: unknown;
132
+ }): ToolDispatchSignal | null;
133
+ /** Oldest-first eviction once the ledger (or the settle markers) overflows. */
134
+ private evict;
110
135
  /** Every emitted dispatch, in first-seen order. */
111
136
  get signals(): readonly ToolDispatchSignal[];
112
137
  /**
@@ -188,10 +213,14 @@ export declare function isProgramToolName(name: string): boolean;
188
213
  * Assert that a payload really is the dispatch event it claims to be.
189
214
  *
190
215
  * The normalizer is deliberately lenient (it also folds persisted logs, where a
191
- * payload may predate the current declaration); this is the loud gate for the
192
- * live path, where a malformed payload means the producer is broken. It never
193
- * silently downgrades: an unrecognized event type or a payload missing a
194
- * declared field throws a named \`ToolDispatchPayloadError\`.
216
+ * payload may predate the current declaration). S1-E7 (honesty fix): this
217
+ * function is an OPT-IN gate NO production consumer calls it; the shipped
218
+ * listeners fold tolerant (`'malformed'` synthesis) precisely because a
219
+ * throwing gate wired into every session event would brick the family on any
220
+ * upstream payload drift. Deployments that want fail-loud behavior call this
221
+ * from their own `session/event` listener. It never silently downgrades: an
222
+ * unrecognized event type or a payload missing a declared field throws a named
223
+ * \`ToolDispatchPayloadError\`.
195
224
  * @param event - the session event to check.
196
225
  * @returns nothing; throws when the payload violates the platform declaration.
197
226
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-core",
3
3
  "description": "Shared stores, prompts, signals and lifecycle logic for the dsh-evolution plugin family (community build)",
4
- "version": "0.3.78",
4
+ "version": "0.3.80",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -19,12 +19,14 @@
19
19
  "default": "./lib/index.js"
20
20
  },
21
21
  "./package.json": "./package.json",
22
- "./row-overrides.json": "./row-overrides.json"
22
+ "./row-overrides.json": "./row-overrides.json",
23
+ "./persisted-write-inventory.json": "./persisted-write-inventory.json"
23
24
  },
24
25
  "files": [
25
26
  "lib/*.js",
26
27
  "lib/types/**/*.d.ts",
27
- "row-overrides.json"
28
+ "row-overrides.json",
29
+ "persisted-write-inventory.json"
28
30
  ],
29
31
  "license": "MIT",
30
32
  "dependencies": {
@@ -0,0 +1,79 @@
1
+ [
2
+ {
3
+ "id": "usage",
4
+ "path": "<skillsRoot>/.usage.json",
5
+ "writer": "evolution-core/src/usage.ts",
6
+ "serializedBy": "transact",
7
+ "marker": "transactIo(io, usageFile(root)",
8
+ "state": [],
9
+ "note": "Skill usage counters. RMW under the IO backend's cross-process lock; the reader side rebuilds from the events log."
10
+ },
11
+ {
12
+ "id": "suppressed",
13
+ "path": "<skillsRoot>/.curator-suppressed.json",
14
+ "writer": "evolution-core/src/usage.ts",
15
+ "serializedBy": "transact",
16
+ "marker": "transactIo(io, suppressedFile(root)",
17
+ "state": [],
18
+ "note": "Curator suppression list (archived bundled skills that must not re-seed)."
19
+ },
20
+ {
21
+ "id": "mutations",
22
+ "path": "<skillsRoot>/.mutations.json",
23
+ "writer": "evolution-core/src/mutations.ts",
24
+ "serializedBy": "transact",
25
+ "marker": "transactIo(io, mutationsFile(root)",
26
+ "state": [],
27
+ "note": "Skill mutation audit records (bounded ring)."
28
+ },
29
+ {
30
+ "id": "events",
31
+ "path": "<evolutionHome>/evolution/events.json",
32
+ "writer": "evolution-core/src/evolution-events.ts",
33
+ "serializedBy": "transact",
34
+ "marker": "transactIo(io, path",
35
+ "state": [],
36
+ "note": "The rc.68 evolution event log; feedback/skill-usage stats fold it."
37
+ },
38
+ {
39
+ "id": "memory",
40
+ "path": "<memoryRoot>/<target>.md",
41
+ "writer": "evolution-core/src/memory-store.ts",
42
+ "serializedBy": "transact",
43
+ "marker": "transactIo(this.io, fileFor(this.root, target)",
44
+ "state": [],
45
+ "note": "Durable memory documents written by the memory tool."
46
+ },
47
+ {
48
+ "id": "state",
49
+ "path": "<evolutionHome>/<review|curator|pending>-state.json",
50
+ "writer": "evolution-state-json/src/index.ts",
51
+ "serializedBy": "transact",
52
+ "marker": "transactIo(io(), join(root, file)",
53
+ "state": [
54
+ "evolution-state-json/src/index.ts :: recordGateWarned",
55
+ "evolution-state-json/src/index.ts :: corruptWritten",
56
+ "evolution-state-json/src/index.ts :: corruptWriteWarned"
57
+ ],
58
+ "note": "The durable state stack (review/curator/pending). Its three warn sets are per-process de-duplication and are registered under N12."
59
+ },
60
+ {
61
+ "id": "curator-reports",
62
+ "path": "<evolutionHome>/reports/curator-*.json",
63
+ "writer": "evolution-curator/src/index.ts",
64
+ "serializedBy": "instance-claim",
65
+ "marker": "claimInstance(",
66
+ "instance": "evolution-curator",
67
+ "state": [],
68
+ "note": "Curator run reports + their retention sweep. No per-file lock: the writer holds the per-home instance claim, so a second curator yields instead of racing the sweep."
69
+ },
70
+ {
71
+ "id": "skill-tree",
72
+ "path": "<skillsRoot>/**",
73
+ "writer": "evolution-core/src/skill-store.ts",
74
+ "serializedBy": "write-lock",
75
+ "marker": "LOCK_SUFFIX",
76
+ "state": [],
77
+ "note": "The skill tree itself. Per-target `<path>.lock` claims (pid:token body) guard every byte writer; destructive movers refuse a live lock."
78
+ }
79
+ ]