@lmzhen/dsh-evolution-core 0.3.81 → 0.3.83

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.
@@ -34,16 +34,10 @@
34
34
  */
35
35
  import { transactIo, type EvolutionIoLike } from './io.ts';
36
36
  import { type Probe } from './probe.ts';
37
+ import type { SkillLimits } from './limits.ts';
37
38
  import { type MutationRecord } from './mutations.ts';
38
39
  import { type SkillHealthAssessment, type SkillHealthThresholds } from './skill-health.ts';
39
40
  import type { EvolutionSkillMutatedEvent } from './events.ts';
40
- export interface SkillLimits {
41
- maxNameLength: number;
42
- maxDescriptionLength: number;
43
- maxSkillContentChars: number;
44
- maxSkillFileBytes: number;
45
- }
46
- export declare const DEFAULT_SKILL_LIMITS: SkillLimits;
47
41
  export interface SkillSummary {
48
42
  name: string;
49
43
  description: string;
@@ -172,7 +166,16 @@ export declare function skillsRoot(env?: NodeJS.ProcessEnv): string;
172
166
  * (and the graph ignored config entirely). Empty/whitespace config falls
173
167
  * through to the default; callers pass their raw Config. The optional field is
174
168
  * declared `| undefined` so a config object whose root field is explicitly
175
- * `string | undefined` still assignable under exactOptionalPropertyTypes. */
169
+ * `string | undefined` still assignable under exactOptionalPropertyTypes.
170
+ * P2-31 core half (S2.2 batch, PLAN 2026-09-16): an explicit non-empty root
171
+ * is `resolve()`d — the same normalization `evolutionRoot` applies and the
172
+ * same one upstream skill-filesystem applies to `customSkillDirs`
173
+ * (`(config.customSkillDirs ?? []).map(root => resolve(root))`). The former
174
+ * verbatim return left a RELATIVE config root CWD-relative, so the family's
175
+ * skill tree moved with the host process's launch directory while every
176
+ * absolute consumer (watchers, the platform catalog) resolved it — a
177
+ * split-brain tree; a trailing slash or `..` segment likewise landed
178
+ * unnormalized. A clean absolute root is byte-identical (resolve is a no-op). */
176
179
  export declare function resolveSkillsRoot(config?: {
177
180
  root?: string | undefined;
178
181
  }): string;
@@ -246,155 +249,6 @@ export declare function resolveExecOrigins(exec: EvolutionExecOriginView | undef
246
249
  * consumers that must probe markers without re-deriving the name (curator's
247
250
  * archive-copy bundled probe, 0.3.26 V4-02). */
248
251
  export declare function markerEntryName(marker: 'bundled' | 'hub-installed' | 'pinned' | 'hermes-managed'): string;
249
- export interface Frontmatter {
250
- name?: string;
251
- description?: string;
252
- [key: string]: unknown;
253
- }
254
- export declare function frontmatterBlock(content: string): {
255
- block: string;
256
- lines: string[];
257
- end: number;
258
- nl: string;
259
- } | null;
260
- /**
261
- * One frontmatter read (V27 G2.1): the values, the body, and every signal the
262
- * strict-YAML platform catalog derives from the same block. Returned by
263
- * {@link parseFrontmatter} so a caller never has to parse the block twice to
264
- * reach a description and the catalog verdict.
265
- */
266
- export interface FrontmatterRead {
267
- frontmatter: Frontmatter;
268
- body: string;
269
- /** Raw entries whose UNQUOTED value the strict catalog cannot load as
270
- * written. Quotes are included, so a value already normalized by the write
271
- * path (`normalizeFrontmatter`) is never re-flagged. */
272
- unsafeValues: Array<{
273
- key: string;
274
- value: string;
275
- }>;
276
- /** Whether the frontmatter is not valid AS WRITTEN for the strict platform
277
- * catalog: the strict parser rejects the block, or an unquoted value would
278
- * read as something other than its text (a dropped ` # ` comment, a
279
- * number/bool shorthand the catalog refuses as a string field), or a
280
- * platform string field carries a non-string value. The write path quotes
281
- * such a value on its next edit. */
282
- catalogInvalid: boolean;
283
- /** Platform string fields (`name`/`description`/`whenToUse`) whose YAML value
284
- * is not a string: the strict catalog reads such a field as ABSENT and, for an
285
- * absent name/description, ignores the whole file (P2-9/v37). */
286
- platformStringSplit: PlatformStringSplit[];
287
- }
288
- /** One `name`/`description`/`whenToUse` entry the strict catalog cannot read as
289
- * a string, with the YAML kind the parser found. */
290
- export interface PlatformStringSplit {
291
- key: string;
292
- /** The YAML type read for this field. `scalar` covers number/boolean (the
293
- * E-47 auto-quote repair handles those); `sequence`/`mapping` are the shapes
294
- * no rewrite can repair without inventing text — see `validateFrontmatter`. */
295
- kind: 'sequence' | 'mapping' | 'scalar' | 'null';
296
- }
297
- /**
298
- * Parse a SKILL.md: its frontmatter values and body, or `null` when the file
299
- * has no frontmatter block or no body. Every consumer of frontmatter values
300
- * goes through here — the write path's validation, `list()`'s published
301
- * description, `relatedSkillNames` and the audit — so all of them read the same
302
- * bytes the same way.
303
- *
304
- * @param content - the SKILL.md text.
305
- * @returns the read, or `null` when there is no block or no body.
306
- */
307
- export declare function parseFrontmatter(content: string): FrontmatterRead | null;
308
- /**
309
- * Whether this file's frontmatter is valid as written for the strict platform
310
- * catalog (see `FrontmatterRead.catalogInvalid`). Body-independent (a body-less
311
- * file is still judged), and derived from the same read as `parseFrontmatter` —
312
- * so the audit's verdict and the values the family publishes for one file can
313
- * never disagree (V27 G2.1).
314
- *
315
- * @param content - the SKILL.md text.
316
- * @returns `true` when the strict parser rejects the block or an unquoted value would read as something else.
317
- */
318
- export declare function frontmatterCatalogInvalid(content: string): boolean;
319
- /** YAML plain-scalar hazards that make an UNQUOTED frontmatter value
320
- * unloadable to the platform catalog (strict YAML parser): `: ` (mapping
321
- * separator), ` #` (comment start), a trailing `:` (a mapping marker),
322
- * or a leading YAML indicator. The evolution `parseFrontmatter` is
323
- * deliberately lenient, so violations silently split family-visibility from
324
- * platform-visibility (0.3.11 inkos-harness case: the description carried
325
- * "…: " and the catalog dropped the whole skill). Already-quoted values and
326
- * well-formed flow collections (`[a, b]` / `{a: b}`) are considered safe.
327
- * 0.3.16 (E-47): null/bool/number-shaped plain scalars are flagged too — they
328
- * parse as booleans/numbers on the platform while the family keeps the string
329
- * (a `description: true` split-brain).
330
- * This rule is only the FAST PATH — the write path re-verifies every rewrite
331
- * with the real YAML parser (see normalizeFrontmatter), so an incomplete
332
- * approximation can never corrupt a multiline flow value (P3-4). */
333
- export declare function yamlPlainScalarNeedsQuotes(value: string): boolean;
334
- export interface FrontmatterNormalizeResult {
335
- content: string;
336
- changed: boolean;
337
- /** Frontmatter keys whose values were auto-quoted. */
338
- fields: string[];
339
- /** Values that cannot be auto-quoted safely (control characters, or a
340
- * rewrite that failed the real-parser verification — a multiline flow
341
- * collection line etc. is left untouched and reported here, so the write
342
- * path rejects instead of silently damaging a value; 0.3.14). */
343
- issues: string[];
344
- }
345
- /**
346
- * Normalize a SKILL.md frontmatter block into catalog-loadable YAML: values
347
- * that YAML forbids unquoted get quotes — double quotes normally, single
348
- * quotes (with `''` doubling) when the value contains `"` or `\` (both legal
349
- * unescaped inside single-quoted YAML). Idempotent; only single-line
350
- * `key: value` entries are touched; body text is never modified; line-ending
351
- * style is preserved. **Every rewrite is re-verified with the real YAML
352
- * parser** (js-yaml — the same parser the platform catalog uses): if the
353
- * rewritten block no longer parses, or a rewritten value's parsed content
354
- * differs from the original, the rewrite is rolled back and reported in
355
- * `issues` (fail-loud, never a silent value corruption — P3-4).
356
- *
357
- * V10-02 (P2-3): the rewrite decision is PER LINE — each entry parses its own
358
- * value, so a duplicated key can never route one entry's unsafe value into a
359
- * different line's rewrite (the old key→Map lookup rewrote the FIRST (safe)
360
- * line with the SECOND line's quoted value, and the last-wins YAML reader
361
- * masked the damage). A duplicated key is itself invalid input and is
362
- * reported in `issues` (the write path refuses) instead of being rewritten.
363
- */
364
- export declare function normalizeFrontmatter(content: string): FrontmatterNormalizeResult;
365
- /**
366
- * Skill names referenced by a SKILL.md's `related_skills` frontmatter
367
- * (B-line G3, rc.44): the single parsing source for the quality references
368
- * factor and the learning-graph edges. The DSH frontmatter parser keeps the
369
- * YAML value as a string (`"[a, b]"`), so names are scanned out of it; each
370
- * must satisfy the skill-name shape and the referencing skill itself is
371
- * excluded. Pure and deduplicated.
372
- */
373
- export declare function relatedSkillNames(content: string, exclude?: string): string[];
374
- export declare function validateFrontmatter(content: string, expectedName?: string, limits?: SkillLimits,
375
- /** On-disk bytes, so a NET SHRINK of an over-limit file is allowed (S1.2). */
376
- current?: string | null): string | null;
377
- /** Hermes authoring quality bar for descriptions — see constants.ts
378
- * (0.3.16 T-4 moved the single source there; the public re-export sits behind
379
- * the package root, which re-exports constants anyway). */
380
- export interface AuthoringFeedback {
381
- /** Frontmatter description length in characters (0 when absent). */
382
- descriptionChars: number;
383
- /** Whether the description exceeds the authoring bar (60) while still passing the platform limit. */
384
- over60: boolean;
385
- /** Whether the description contains a colon (the standard requires double-quote wrapping). */
386
- hasColon: boolean;
387
- /** Advice lines appended to mutation success messages. */
388
- lines: string[];
389
- }
390
- /**
391
- * Advisory authoring feedback (P0): evaluate frontmatter against the
392
- * authoring bar WITHOUT changing platform validation semantics. The bar is
393
- * the quality target, `validateFrontmatter`'s limits are the compatibility
394
- * floor, and this bridge layer tells the model when its text would be
395
- * truncated or route-poor instead of silently shipping it.
396
- */
397
- export declare function authoringFeedback(frontmatter: Frontmatter): AuthoringFeedback;
398
252
  /** A1-6 (v18): the regex alone admits `references/nul.md` (a Windows device
399
253
  * stem), which the support-file layer refuses. Restructure must use the same
400
254
  * rule, or it creates an orphan the later patch/write/remove paths refuse.
@@ -747,4 +601,8 @@ export interface NewSkillLibraryOptions {
747
601
  threatExemptLabels?: readonly string[] | undefined;
748
602
  }
749
603
  export declare function newSkillLibrary(options: NewSkillLibraryOptions): SkillLibrary;
604
+ export { DEFAULT_SKILL_LIMITS } from './limits.ts';
605
+ export type { SkillLimits } from './limits.ts';
606
+ export { authoringFeedback, frontmatterBlock, frontmatterCatalogInvalid, normalizeFrontmatter, parseFrontmatter, relatedSkillNames, validateFrontmatter, yamlPlainScalarNeedsQuotes } from './frontmatter.ts';
607
+ export type { AuthoringFeedback, Frontmatter, FrontmatterNormalizeResult, FrontmatterRead, PlatformStringSplit } from './frontmatter.ts';
750
608
  //# sourceMappingURL=skill-store.d.ts.map
@@ -14,12 +14,14 @@
14
14
  * DSH home root: `$DSH_HOME` or `~/.dsh`. Single source of the empty-string
15
15
  * fallback — an EMPTY or WHITESPACE-ONLY DSH_HOME resolves to the default
16
16
  * home, never to a CWD-relative path (0.3.19 W1.3, 0.3.22 F-207);
17
- * V8-06 (0.3.47) extends the guard to whitespace (upstream home-paths:
18
- * `trim().length > 0` is the adoption test — `DSH_HOME=" "` must not produce
19
- * a sidecar under a relative "." path).
20
- * C-11: the adoption test and the RETURNED value now come from the
21
- * SAME trimmed source the old form tested `trim()` but returned the raw
22
- * value, so `DSH_HOME=" /x "` was accepted AND persisted with literal spaces.
17
+ * V8-06 (0.3.47) extends the guard to whitespace.
18
+ * C-11 correction (S2.2, PLAN 2026-09-16): upstream `resolveDshHome`
19
+ * (util/home-paths) uses `trim()` ONLY as the ADOPTION test and then uses the
20
+ * RAW env value the earlier "same trimmed source" form returned the trimmed
21
+ * text, so `DSH_HOME=" /x "` produced `/x` where upstream produced the literal
22
+ * padded path. The value semantics now match upstream line for line:
23
+ * `const selected = configured ?? (fromEnv !== undefined &&
24
+ * fromEnv.trim().length > 0 ? fromEnv : defaultDshHome())`.
23
25
  * OPT-27 (2026-09, plan D5 — accepted): the v10-era "no `~` expansion, no
24
26
  * resolve" divergence from upstream `resolveDshHome` is RETIRED. It became
25
27
  * load-bearing when the skill-catalog shadow made "same tree as the upstream
@@ -27,9 +29,13 @@
27
29
  * absolute `<home>/skills` while this value fed a literal `~/x` (a directory
28
30
  * named `~` under the host CWD) or a CWD-relative path — split-brain skill
29
31
  * trees, preset installs the platform never reads, doctor probes of a
30
- * directory nothing serves. Behavior now matches upstream:
31
- * `resolve(expandHomePath(selected))`. Only `~`-prefixed and RELATIVE
32
- * DSH_HOME values change landing spot; absolute homes are byte-identical.
32
+ * directory nothing serves.
33
+ * S2.2 (PLAN 2026-09-16), final correction: the result is ALWAYS
34
+ * `resolve(expandHomePath(selected))` the earlier form returned an
35
+ * already-absolute value VERBATIM, so a trailing slash or `..` segment
36
+ * landed unnormalized while upstream normalizes every value. Relative
37
+ * `DSH_HOME` values change landing spot; a CLEAN absolute home stays
38
+ * byte-identical (resolve is a no-op on it).
33
39
  */
34
40
  export declare function evolutionRoot(env?: NodeJS.ProcessEnv): string;
35
41
  /** Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
@@ -13,8 +13,8 @@
13
13
  * REPORT findings: they stay visible to operators and tests but never reject a
14
14
  * write. Blocking them turned every emoji into a security event.
15
15
  */
16
- export type ThreatScope = 'all' | 'context' | 'strict';
17
- export interface ThreatFinding {
16
+ type ThreatScope = 'all' | 'context' | 'strict';
17
+ interface ThreatFinding {
18
18
  label: string;
19
19
  category: string;
20
20
  scope: ThreatScope;
@@ -74,4 +74,5 @@ export declare function scanContentThreats(text: string, maxScanChars?: number,
74
74
  * returns the scan message verbatim, so the hint rides along there too.
75
75
  */
76
76
  export declare const THREAT_EXEMPTION_HINT = " A deployment that needs a specific label can exempt it via threatExemptLabels (see the README env/dial reference).";
77
+ export {};
77
78
  //# sourceMappingURL=threats.d.ts.map
@@ -105,6 +105,16 @@ export declare class ToolDispatchNormalizer {
105
105
  constructor(options?: {
106
106
  maxTracked?: number;
107
107
  });
108
+ /**
109
+ * v43 audit (FLOW4-4): the ledger key. A normalizer shared by every session
110
+ * (skill-usage's live listener holds ONE process-wide instance) used the bare
111
+ * call id, so two sessions that produced the same id — PTC sub-call ids are
112
+ * short, and an id-less payload falls back to a type+payload key that is not
113
+ * unique by construction — collided: the second session's read was absorbed as
114
+ * "already seen" and never counted, and a settle in one session flipped the
115
+ * other's `ok`. Callers that span sessions pass the session id as `scope`.
116
+ */
117
+ private keyOf;
108
118
  /**
109
119
  * Absorb one session event.
110
120
  * @param event - the event to absorb; any non-dispatch event is ignored.
@@ -115,7 +125,7 @@ export declare class ToolDispatchNormalizer {
115
125
  advance(event: {
116
126
  type: string;
117
127
  data?: unknown;
118
- }): ToolDispatchSignal | null;
128
+ }, scope?: string): ToolDispatchSignal | null;
119
129
  /**
120
130
  * S2-P2-12: the SETTLE channel. \`advance\` answers \`null\` for the paired
121
131
  * event that settles an already-emitted dispatch, so a consumer that must act
@@ -129,7 +139,7 @@ export declare class ToolDispatchNormalizer {
129
139
  settledSignalOf(event: {
130
140
  type: string;
131
141
  data?: unknown;
132
- }): ToolDispatchSignal | null;
142
+ }, scope?: string): ToolDispatchSignal | null;
133
143
  /** Oldest-first eviction once the ledger (or the settle markers) overflows. */
134
144
  private evict;
135
145
  /** Every emitted dispatch, in first-seen order. */
@@ -3,7 +3,7 @@
3
3
  * Format-compatible with Hermes Agent / hermes-claw core fields.
4
4
  */
5
5
  import { type EvolutionIoLike } from './io.ts';
6
- export type SkillState = 'active' | 'stale' | 'archived';
6
+ type SkillState = 'active' | 'stale' | 'archived';
7
7
  export interface UsageRecord {
8
8
  created_by: string | null;
9
9
  use_count: number;
@@ -119,6 +119,23 @@ export declare function usageObserved(usage: ReadonlyMap<string, UsageRecord>):
119
119
  export declare const SUPPRESSED_FILE_VERSION = 1;
120
120
  export declare function suppressedFile(root: string): string;
121
121
  export declare function loadSuppressedNames(root: string, io?: EvolutionIoLike): Promise<ReadonlySet<string>>;
122
+ /**
123
+ * Plain wholesale write of the suppression sidecar (tests and fixture seeding).
124
+ * @internal S2.4 (PLAN 2026-09-16, audit P2-31): NO production consumer
125
+ * (verified by grep; production suppressions go through
126
+ * {@link updateSuppressedNames}) — exported for the family's tests only. Do
127
+ * not use it to write the sidecar in new code.
128
+ *
129
+ * The write rides the {@link transactIo} channel (the same lock the RMW
130
+ * writer uses), not the former naked read → `io.writeText`: that form was an
131
+ * unserialized read-modify-write — the last `saveSuppressedNames` in this file
132
+ * with no lock, while its sibling `saveUsage` already carried the `@internal`
133
+ * test-only note. Version discipline is unchanged (V24-09 / S2-14): a read
134
+ * failure surfaces (an unreadable sidecar is never written over), a newer
135
+ * on-disk schema is warned about and preserved byte-for-byte (the
136
+ * byte-identical result short-circuits the write), and a malformed sidecar is
137
+ * still overwritten with the v1 shape (the historical plain-writer posture).
138
+ */
122
139
  export declare function saveSuppressedNames(root: string, names: ReadonlySet<string>, io?: EvolutionIoLike): Promise<void>;
123
140
  /**
124
141
  * Atomic read-modify-write on the suppression sidecar (rc.50 P2-2): `task`
@@ -127,4 +144,5 @@ export declare function saveSuppressedNames(root: string, names: ReadonlySet<str
127
144
  * sharing DSH_HOME cannot interleave its RMW. Best-effort posture unchanged.
128
145
  */
129
146
  export declare function updateSuppressedNames(root: string, io: EvolutionIoLike, task: (names: Set<string>) => void | Promise<void>): Promise<void>;
147
+ export {};
130
148
  //# sourceMappingURL=usage.d.ts.map
@@ -16,7 +16,7 @@
16
16
  * @module @lmzhen/dsh-evolution-core/src/write-inventory
17
17
  */
18
18
  /** How concurrent writers of one site are kept apart. */
19
- export type WriteSerialization = 'transact' | 'write-lock' | 'instance-claim';
19
+ type WriteSerialization = 'transact' | 'write-lock' | 'instance-claim';
20
20
  /** One declared persisted write site. */
21
21
  export interface PersistedWriteSite {
22
22
  /** Stable id; referenced by the gate's failure text and by the specs. */
@@ -43,13 +43,31 @@ export declare const INSTANCE_KEYS: {
43
43
  /** The per-home curator: report writing + the retention sweep. */
44
44
  readonly curator: "evolution-curator";
45
45
  };
46
- /** The declared persisted write sites, in file order. */
47
- export declare const PERSISTED_WRITE_SITES: readonly PersistedWriteSite[];
48
- /** Sites serialized by the per-home instance claim, with their instance keys. */
46
+ /**
47
+ * The declared persisted write sites, in file order.
48
+ *
49
+ * v43 audit (S2-3 / P1-10): this used to be a module-scope readFileSync plus
50
+ * parse, so an unshipped asset threw AT IMPORT — one absent file took the whole
51
+ * family's load down (0.3.79 shipped a tarball without this asset and every
52
+ * package failed to load). The read is lazy now: importing the package never
53
+ * fails on this asset, while the first caller still gets a loud, descriptive
54
+ * failure instead of an empty table ("no declared write sites" would silently
55
+ * disable rule N20).
56
+ * @internal S2.4 (PLAN 2026-09-16): no production caller — the arch guard
57
+ * reads the JSON asset directly; the tests are the only in-tree consumers.
58
+ * @returns the sites, in file order.
59
+ */
60
+ export declare function persistedWriteSites(): readonly PersistedWriteSite[];
61
+ /** Sites serialized by the per-home instance claim, with their instance keys.
62
+ * @internal S2.4 (PLAN 2026-09-16): no production caller — the arch guard
63
+ * reads the JSON asset directly; the tests are the only in-tree consumers. */
49
64
  export declare function instanceClaimedWriteSites(): readonly (PersistedWriteSite & {
50
65
  readonly instance: string;
51
66
  })[];
52
67
  /** One declared site by id. An undeclared id throws — a stale caller must fail
53
- * loud rather than read "nothing is declared". */
68
+ * loud rather than read "nothing is declared".
69
+ * @internal S2.4 (PLAN 2026-09-16): no production caller — the arch guard
70
+ * reads the JSON asset directly; the tests are the only in-tree consumers. */
54
71
  export declare function persistedWriteSite(id: string): PersistedWriteSite;
72
+ export {};
55
73
  //# sourceMappingURL=write-inventory.d.ts.map
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.81",
4
+ "version": "0.3.83",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -30,7 +30,7 @@
30
30
  ],
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
- "js-yaml": "^4.2.0"
33
+ "yaml": "^2.4.2"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "@deepseek-ai/cordis": "^4.0.1",
@@ -38,7 +38,6 @@
38
38
  "@deepseek-ai/dsh-session": "^0.1.5-rc.2"
39
39
  },
40
40
  "devDependencies": {
41
- "@types/js-yaml": "^4.0.9",
42
41
  "@deepseek-ai/cordis": "^4.0.1",
43
42
  "@deepseek-ai/dsh-scope": "^0.1.5-rc.2",
44
43
  "@deepseek-ai/dsh-session": "^0.1.5-rc.2"
@@ -61,11 +61,10 @@
61
61
  "id": "curator-reports",
62
62
  "path": "<evolutionHome>/reports/curator-*.json",
63
63
  "writer": "evolution-curator/src/index.ts",
64
- "serializedBy": "instance-claim",
65
- "marker": "claimInstance(",
66
- "instance": "evolution-curator",
64
+ "serializedBy": "transact",
65
+ "marker": "transactIo(this.io, reportsSweepLockTarget()",
67
66
  "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."
67
+ "note": "Curator run reports + their retention sweep. The sweep is a multi-step list + delete-beyond-the-window over a DIRECTORY, so it has no per-file lock: v43 FLOW2-1 put it inside the IO backend's cross-process write lock (transactIo) on a per-home lock target, <evolutionHome>/reports/.retention (lock file <target>.lock, pid:token body). The former declaration here was instance-claim, which is in-process ONLY (instance-scope.ts is a module-scope Map). Two curator ROWS in one process still cannot sweep at once (the loser yields on that claim), and report writes are name-unique per runId. Trigger for the remaining gap: a second PROCESS sharing DSH_HOME now WAITS on the lock instead of sweeping concurrently, and on a backend without transact there is no lock at all, so the sweep degrades to best-effort idempotent deletes."
69
68
  },
70
69
  {
71
70
  "id": "skill-tree",