@lmzhen/dsh-evolution-core 0.3.16 → 0.3.18

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
@@ -45,6 +45,10 @@ function evolutionIoAdapter(provider) {
45
45
  isSymlink: (path) => {
46
46
  const io = provider();
47
47
  return io.isSymlink ? io.isSymlink(path) : Promise.resolve(null);
48
+ },
49
+ mtime: (path) => {
50
+ const io = provider();
51
+ return io.mtime ? io.mtime(path) : Promise.resolve(null);
48
52
  }
49
53
  };
50
54
  }
@@ -64,45 +68,81 @@ function nodeEvolutionIo() {
64
68
  };
65
69
  /**
66
70
  * Cross-process write lock (claw `withFileLock` parity): an O_EXCL lock file
67
- * guards the atomic write. A >5s-old lock is taken over ONLY after probing
71
+ * guards the atomic write. A >1s-old lock is taken over ONLY after probing
68
72
  * the holder pid it carries (rc.66): a LIVE holder is never stolen, so a
69
- * slow writer no longer loses its lock to a peer at the 5s mark (the
73
+ * slow writer no longer loses its lock to a peer at the threshold (the
70
74
  * takeover is the only best-effort surface; the retry budget fails loud —
71
75
  * rc.65 — instead of ever proceeding unlocked). Budget = 40 * 50ms (~2s,
72
76
  * rc.69): 8-writer contention bursts on a loaded CI runner exceed 10
73
77
  * attempts (500ms), and a fail-loud throw was observed instead of a clean
74
78
  * serialization.
79
+ * 0.3.17 (E-8): acquisition and the task are now SEPARATE try blocks — a
80
+ * task error (win32 rename/EBUSY surfaces as EPERM) used to be mistaken for
81
+ * lock contention, retried up to 40x and finally reported as
82
+ * "could not acquire write lock" while the real cause was hidden.
83
+ * 0.3.17 (E-8a): the takeover threshold (1000ms) now fits INSIDE the ~2s
84
+ * retry budget (budget >= 2 x threshold), so a dead holder's lock is
85
+ * actually recoverable within one budget instead of being arithmetically
86
+ * unreachable.
75
87
  */
76
88
  const withWriteLock = async (path, task) => {
77
89
  const lock = `${path}.lock`;
78
- for (let attempt = 0; attempt < 40; attempt += 1) try {
79
- await writeFile(lock, String(process.pid), { flag: "wx" });
90
+ for (let attempt = 0; attempt < 40; attempt += 1) {
80
91
  try {
81
- return await task();
82
- } finally {
83
- await rm(lock, { force: true }).catch(() => {});
84
- }
85
- } catch (error) {
86
- const code = error?.code;
87
- if (code !== "EEXIST" && code !== "EPERM") throw error;
88
- try {
89
- const st = await stat(lock);
90
- if (Date.now() - st.mtimeMs > 5e3) {
91
- const holder = Number(await readFile(lock, "utf8").catch(() => ""));
92
- if (!(Number.isInteger(holder) && holder > 0 && isAlive(holder))) {
93
- try {
94
- await rm(lock, { force: true });
95
- } catch {}
96
- continue;
92
+ await writeFile(lock, String(process.pid), { flag: "wx" });
93
+ } catch (error) {
94
+ const code = error?.code;
95
+ if (code !== "EEXIST" && code !== "EPERM") throw error;
96
+ try {
97
+ const st = await stat(lock);
98
+ if (Date.now() - st.mtimeMs > 1e3) {
99
+ const holder = Number(await readFile(lock, "utf8").catch(() => ""));
100
+ if (!(Number.isInteger(holder) && holder > 0 && isAlive(holder))) {
101
+ try {
102
+ await rm(lock, { force: true });
103
+ } catch {}
104
+ continue;
105
+ }
97
106
  }
107
+ } catch {
108
+ continue;
98
109
  }
99
- } catch {
110
+ await new Promise((resolve) => setTimeout(resolve, 50));
100
111
  continue;
101
112
  }
102
- await new Promise((resolve) => setTimeout(resolve, 50));
113
+ try {
114
+ return await task();
115
+ } finally {
116
+ await rm(lock, { force: true }).catch(() => {});
117
+ }
103
118
  }
104
119
  throw new Error(`could not acquire write lock for ${path} after 40 attempts`);
105
120
  };
121
+ /** 0.3.17 (E-8b): sweep tmp files a crashed writer left behind — same
122
+ * `<target>.<pid>.<rand>.tmp` shape, older than 1h AND held by a dead pid.
123
+ * Lazy: only the directory a write is about to touch gets swept, once per
124
+ * write, inside the write lock. */
125
+ const sweepStaleTmps = async (path) => {
126
+ const dir = dirname(path);
127
+ const base = basename(path);
128
+ let entries;
129
+ try {
130
+ entries = await readdir(dir);
131
+ } catch {
132
+ return;
133
+ }
134
+ const prefix = `${base}.`;
135
+ for (const name of entries) {
136
+ if (!name.startsWith(prefix) || !name.endsWith(".tmp")) continue;
137
+ const tmpPath = join(dir, name);
138
+ const holder = Number(name.slice(prefix.length, name.length - 4).split(".")[0] ?? "");
139
+ try {
140
+ const st = await stat(tmpPath);
141
+ const deadHolder = !Number.isInteger(holder) || holder <= 0 || !isAlive(holder);
142
+ if (Date.now() - st.mtimeMs > 36e5 && deadHolder) await rm(tmpPath, { force: true });
143
+ } catch {}
144
+ }
145
+ };
106
146
  return {
107
147
  async readText(path) {
108
148
  try {
@@ -115,6 +155,7 @@ function nodeEvolutionIo() {
115
155
  async writeText(path, content) {
116
156
  await mkdir(dirname(path), { recursive: true });
117
157
  await withWriteLock(path, async () => {
158
+ await sweepStaleTmps(path);
118
159
  const tmp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
119
160
  await writeFile(tmp, content, "utf8");
120
161
  await rename(tmp, path);
@@ -123,6 +164,7 @@ function nodeEvolutionIo() {
123
164
  async transact(path, task) {
124
165
  await mkdir(dirname(path), { recursive: true });
125
166
  await withWriteLock(path, async () => {
167
+ await sweepStaleTmps(path);
126
168
  let current;
127
169
  try {
128
170
  current = await readFile(path, "utf8");
@@ -188,6 +230,14 @@ function nodeEvolutionIo() {
188
230
  } catch {
189
231
  return null;
190
232
  }
233
+ },
234
+ async mtime(path) {
235
+ try {
236
+ return (await stat(path)).mtimeMs;
237
+ } catch (error) {
238
+ if (isMissing(error)) return null;
239
+ throw error;
240
+ }
191
241
  }
192
242
  };
193
243
  }
@@ -494,6 +544,19 @@ const DEFAULT_USER_CHAR_LIMIT = 1375;
494
544
  /** Consolidation-failure backoff cap, shared by MemoryStore and memory-files' Config default. */
495
545
  const DEFAULT_CONSOLIDATION_FAILURES = 3;
496
546
  const DEFAULT_SKILL_CONTENT_CHARS = 1e5;
547
+ /** 0.3.17 (S3.10, T-1): control-plane fields a model-facing write call may
548
+ * never carry — single source for plan-validator, evolution-policy and the
549
+ * threat scanner (they used to each hardcode the list). */
550
+ const FORBIDDEN_CONTROL_KEYS = [
551
+ "policy",
552
+ "threshold",
553
+ "prompt_hash",
554
+ "model_route",
555
+ "evolution_config"
556
+ ];
557
+ /** 0.3.17 (S3.10): the model-facing write tools the policy guard and threat
558
+ * scanner cover. */
559
+ const EVOLUTION_WRITE_TOOLS = ["memory", "skill_manage"];
497
560
  /** Hermes authoring quality bar for descriptions (the 60-char Rule). The
498
561
  * platform's own index limit stays in validateFrontmatter; this bar is the
499
562
  * target the authoring standard names, enforced as ADVISORY feedback.
@@ -2362,6 +2425,24 @@ function redactSecrets(text) {
2362
2425
  return out;
2363
2426
  }
2364
2427
  //#endregion
2428
+ //#region lib/types/serial.js
2429
+ /**
2430
+ * A process-local serial task queue: each task starts only after the previous
2431
+ * one settles (success or failure), so read-modify-write sequences that share
2432
+ * one file never interleave inside this process. The durable cross-process
2433
+ * serialization layer is the IO backend's transact lock; this chain is the
2434
+ * second layer (0.3.17 S2.8, T-1: the shape was duplicated in state-json and
2435
+ * memory-files — one factory now).
2436
+ */
2437
+ function makeSerialQueue() {
2438
+ let chain = Promise.resolve();
2439
+ return (task) => {
2440
+ const run = chain.then(task, task);
2441
+ chain = run.then(() => void 0, () => void 0);
2442
+ return run;
2443
+ };
2444
+ }
2445
+ //#endregion
2365
2446
  //#region lib/types/skill-health.js
2366
2447
  /**
2367
2448
  * Skill structure-health domain (rc.73 A1, 008 design): a SECOND assessment
@@ -2400,7 +2481,7 @@ function assessStructureHealth(snapshot, thresholds = DEFAULT_HEALTH_THRESHOLDS)
2400
2481
  const needs = snapshot.bodyChars >= thresholds.softBodyChars * 2;
2401
2482
  if (needs) reasons.push(`body ${snapshot.bodyChars} chars is >= 2x the soft limit (${thresholds.softBodyChars}) — consider splitting or offloading`);
2402
2483
  else if (snapshot.bodyChars >= thresholds.softBodyChars) reasons.push(`body ${snapshot.bodyChars} chars above the soft limit (${thresholds.softBodyChars})`);
2403
- if (snapshot.bodyText && snapshot.bodyChars >= MIN_STAMP_BODY_CHARS) {
2484
+ if (snapshot.bodyText && snapshot.bodyChars >= 2e3) {
2404
2485
  const kb = Math.max(1, snapshot.bodyChars / 1024);
2405
2486
  dims.stampDensityPerKb = (snapshot.bodyText.match(HEALTH_STAMP_RE) ?? []).length / kb;
2406
2487
  if (dims.stampDensityPerKb >= thresholds.stampDensityPerKb) reasons.push(`stamp density ${dims.stampDensityPerKb.toFixed(1)}/KB (rc/sha/date lines — log-like content in the body)`);
@@ -2683,6 +2764,14 @@ const SNAPSHOT_EXTRA_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
2683
2764
  function skillsRoot(env = process.env) {
2684
2765
  return join(env.DSH_HOME || join(homedir(), ".dsh"), "skills");
2685
2766
  }
2767
+ /** 0.3.18 (S4.1, E-30): the ONE root resolution for every member that reads
2768
+ * the skills tree — tool-skill-manage / evolution-skill-catalog / skill-usage
2769
+ * / evolution-learning-graph used to each resolve `config.root || skillsRoot()`
2770
+ * (and the graph ignored config entirely). Empty/whitespace config falls
2771
+ * through to the default; callers pass their raw Config. */
2772
+ function resolveSkillsRoot(config = {}) {
2773
+ return (config.root ?? "").trim() || skillsRoot();
2774
+ }
2686
2775
  /**
2687
2776
  * Map a requesting session onto the two origin surfaces (rc.44 plan M2-2.3):
2688
2777
  * the APPROVAL surface treats every delegated subagent as the autonomous
@@ -3511,6 +3600,12 @@ var SkillLibrary = class {
3511
3600
  ok: false,
3512
3601
  message: threat
3513
3602
  };
3603
+ if (writeContent.trimEnd() + "\n" === md) return {
3604
+ ok: true,
3605
+ message: `Skill "${name}" unchanged: old_string already equals the replacement (${patchLabel}); nothing written.`,
3606
+ noop: true,
3607
+ path: dir
3608
+ };
3514
3609
  await this.io.writeText(target, writeContent.trimEnd() + "\n");
3515
3610
  await this.audit(name, "patch", md, writeContent, `patched ${patchLabel}`);
3516
3611
  this.notifyMutation({
@@ -3544,6 +3639,10 @@ var SkillLibrary = class {
3544
3639
  message: `Skill "${name}" is protected (${protection}).`
3545
3640
  };
3546
3641
  if (options.absorbedInto) {
3642
+ if (options.absorbedInto.trim() === name) return {
3643
+ ok: false,
3644
+ message: "absorbed_into cannot be the skill being archived (cannot absorb into itself)."
3645
+ };
3547
3646
  if (!await this.io.readText(join(this.dirOf(options.absorbedInto), "SKILL.md"))) return {
3548
3647
  ok: false,
3549
3648
  message: `absorbed_into="${options.absorbedInto}" does not exist.`
@@ -4301,4 +4400,4 @@ function evolutionHome(env = process.env) {
4301
4400
  return join(env.DSH_HOME || join(homedir(), ".dsh"), "evolution");
4302
4401
  }
4303
4402
  //#endregion
4304
- export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EvolutionGateSet, HEALTH_STAMP_RE, LOW_QUALITY_THRESHOLD, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MUTATIONS_FILE_VERSION, MemoryStore, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, advanceReview, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterYamlUnsafeValues, getRecord, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, markAgentCreated, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renderCuratorReportMarkdown, resolveOrigins, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, verifyPromptBundle, yamlPlainScalarNeedsQuotes };
4403
+ export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, LOW_QUALITY_THRESHOLD, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, advanceReview, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterYamlUnsafeValues, getRecord, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, makeSerialQueue, markAgentCreated, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renderCuratorReportMarkdown, resolveOrigins, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, verifyPromptBundle, yamlPlainScalarNeedsQuotes };
@@ -50,6 +50,13 @@ export declare const DEFAULT_USER_CHAR_LIMIT = 1375;
50
50
  /** Consolidation-failure backoff cap, shared by MemoryStore and memory-files' Config default. */
51
51
  export declare const DEFAULT_CONSOLIDATION_FAILURES = 3;
52
52
  export declare const DEFAULT_SKILL_CONTENT_CHARS = 100000;
53
+ /** 0.3.17 (S3.10, T-1): control-plane fields a model-facing write call may
54
+ * never carry — single source for plan-validator, evolution-policy and the
55
+ * threat scanner (they used to each hardcode the list). */
56
+ export declare const FORBIDDEN_CONTROL_KEYS: readonly ["policy", "threshold", "prompt_hash", "model_route", "evolution_config"];
57
+ /** 0.3.17 (S3.10): the model-facing write tools the policy guard and threat
58
+ * scanner cover. */
59
+ export declare const EVOLUTION_WRITE_TOOLS: readonly ["memory", "skill_manage"];
53
60
  /** Hermes authoring quality bar for descriptions (the 60-char Rule). The
54
61
  * platform's own index limit stays in validateFrontmatter; this bar is the
55
62
  * target the authoring standard names, enforced as ADVISORY feedback.
@@ -42,11 +42,23 @@ export interface EvolutionSkillMutatedEvent {
42
42
  file?: string;
43
43
  archivedPath?: string;
44
44
  }
45
+ /** 0.3.18 (E-6): a turn-end review pipeline failure was caught (never an
46
+ * unhandled rejection); this event lets operators/observability see it. The
47
+ * reason is already logged by the emitter — the event is a timestamped signal. */
48
+ export interface EvolutionReviewErrorEvent {
49
+ sessionId: string;
50
+ }
45
51
  declare module '@deepseek-ai/cordis' {
46
52
  interface Events {
47
53
  'evolution/review-scheduled'(event: EvolutionReviewScheduledEvent): void;
48
54
  'evolution/plan-applied'(event: EvolutionPlanAppliedEvent): void;
49
55
  'evolution/skill-mutated'(event: EvolutionSkillMutatedEvent): void;
56
+ /** 0.3.18 (E-71): explicit catalog refresh request (`/evolution skills
57
+ * refresh`). Out-of-band tree edits (manual, git) may bypass the mutation
58
+ * event; listeners drop caches and invalidate downstream catalogs. No
59
+ * payload — it is a bare "re-read" signal, never a mutation record. */
60
+ 'evolution/skills-refresh'(): void;
61
+ 'evolution/review-error'(event: EvolutionReviewErrorEvent): void;
50
62
  }
51
63
  }
52
64
  //# sourceMappingURL=events.d.ts.map
@@ -19,6 +19,7 @@ export * from './preset-composition.ts';
19
19
  export * from './prompts.ts';
20
20
  export * from './quality.ts';
21
21
  export * from './redact.ts';
22
+ export * from './serial.ts';
22
23
  export * from './skill-health.ts';
23
24
  export * from './signals.ts';
24
25
  export * from './drift-signals.ts';
package/lib/types/io.d.ts CHANGED
@@ -36,6 +36,14 @@ export interface EvolutionIoLike {
36
36
  * the path does not exist). Consumers treat `null` as "let it through".
37
37
  */
38
38
  isSymlink?(this: void, path: string): Promise<boolean | null>;
39
+ /**
40
+ * Optional mtime-generation probe (0.3.18, E-71): the path's mtime in
41
+ * milliseconds since epoch, or `null` when unknown (unsupported backend,
42
+ * missing path, stat failure). Consumers use it as a cheap invalidation
43
+ * stamp for a cached directory listing; a backend without it keeps
44
+ * event-driven invalidation only.
45
+ */
46
+ mtime?(this: void, path: string): Promise<number | null>;
39
47
  }
40
48
  /**
41
49
  * Run `task` inside `io.transact` when the backend provides it; otherwise fall
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A process-local serial task queue: each task starts only after the previous
3
+ * one settles (success or failure), so read-modify-write sequences that share
4
+ * one file never interleave inside this process. The durable cross-process
5
+ * serialization layer is the IO backend's transact lock; this chain is the
6
+ * second layer (0.3.17 S2.8, T-1: the shape was duplicated in state-json and
7
+ * memory-files — one factory now).
8
+ */
9
+ export declare function makeSerialQueue(): <T>(task: () => Promise<T>) => Promise<T>;
10
+ //# sourceMappingURL=serial.d.ts.map
@@ -24,6 +24,13 @@ export interface SkillHealthThresholds {
24
24
  export declare const DEFAULT_HEALTH_THRESHOLDS: SkillHealthThresholds;
25
25
  /** Stamp regex shared by health assessment and the maintenance probe (single source, 011). */
26
26
  export declare const HEALTH_STAMP_RE: RegExp;
27
+ /**
28
+ * Bodies below this size skip stamp-density assessment: a few dates or shas
29
+ * in a short body are ordinary documentation, not log-like content. With the
30
+ * 1KB density floor a 3-date sentence in a small skill measured 3.0/KB and
31
+ * warned on a perfectly healthy body (audit 2026-08-31 X1).
32
+ */
33
+ export declare const MIN_STAMP_BODY_CHARS = 2000;
27
34
  export type SkillHealthVerdict = 'healthy' | 'warn' | 'needs-restructure';
28
35
  /** Facts a caller already has; assessors never do IO. */
29
36
  export interface SkillHealthSnapshot {
@@ -32,6 +32,9 @@ export interface SkillActionResult {
32
32
  /** Frontmatter keys auto-quoted for catalog-loadable YAML (0.3.11) — set
33
33
  * only when the write path modified the block. */
34
34
  normalizedFrontmatterFields?: string[];
35
+ /** 0.3.18 (E-68): patch produced byte-identical content (old===new) — no
36
+ * write, no audit, no mutation event; callers must not count a patch. */
37
+ noop?: boolean;
35
38
  }
36
39
  /**
37
40
  * One section move of a restructure proposal (008 batch B): a body section
@@ -81,6 +84,14 @@ export interface ArchiveOptions {
81
84
  allowBundled?: boolean;
82
85
  }
83
86
  export declare function skillsRoot(env?: NodeJS.ProcessEnv): string;
87
+ /** 0.3.18 (S4.1, E-30): the ONE root resolution for every member that reads
88
+ * the skills tree — tool-skill-manage / evolution-skill-catalog / skill-usage
89
+ * / evolution-learning-graph used to each resolve `config.root || skillsRoot()`
90
+ * (and the graph ignored config entirely). Empty/whitespace config falls
91
+ * through to the default; callers pass their raw Config. */
92
+ export declare function resolveSkillsRoot(config?: {
93
+ root?: string;
94
+ }): string;
84
95
  /**
85
96
  * Map a requesting session onto the two origin surfaces (rc.44 plan M2-2.3):
86
97
  * the APPROVAL surface treats every delegated subagent as the autonomous
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.16",
4
+ "version": "0.3.18",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },