@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.
@@ -33,7 +33,6 @@ export declare const EVOLUTION_ERRORS: Readonly<{
33
33
  readonly '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.";
34
34
  readonly 'e-309-revision-conflict-message-Re': "E-309: revision conflict — %a1%. Re-read with /evolution params --json and retry.";
35
35
  readonly '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.";
36
- readonly '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).";
37
36
  readonly '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.";
38
37
  readonly '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.";
39
38
  readonly '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.";
@@ -85,14 +85,39 @@ interface EvolutionReviewErrorEvent {
85
85
  }
86
86
  declare module '@deepseek-ai/cordis' {
87
87
  interface Events {
88
+ /**
89
+ * A review was scheduled and delivered on some channel. An ANNOUNCEMENT: consumers observe
90
+ * that the cadence fired, they do not decide anything (A98 / O-1).
91
+ * @param event - the scheduled review: owning session, kind, the turn's counts, delivery channel.
92
+ * @mode emit
93
+ */
88
94
  'evolution/review-scheduled'(event: EvolutionReviewScheduledEvent): void;
95
+ /**
96
+ * A reviewed plan finished its write leg, with the counts that describe what landed. An
97
+ * ANNOUNCEMENT: the family's own ledger of plan outcomes (A98 / O-1).
98
+ * @param event - the outcome counts, the first failure message, and the policy fingerprint.
99
+ * @mode emit
100
+ */
89
101
  'evolution/plan-applied'(event: EvolutionPlanAppliedEvent): void;
102
+ /**
103
+ * One skill mutation landed (the cache-invalidation signal every catalog consumer reads). An
104
+ * ANNOUNCEMENT: it names what changed, it decides nothing (A98 / O-1).
105
+ * @param event - the action, the skill name, and the file for support-file operations.
106
+ * @mode emit
107
+ */
90
108
  'evolution/skill-mutated'(event: EvolutionSkillMutatedEvent): void;
91
109
  /** 0.3.18 (E-71): explicit catalog refresh request (`/evolution skills
92
110
  * refresh`). Out-of-band tree edits (manual, git) may bypass the mutation
93
111
  * event; listeners drop caches and invalidate downstream catalogs. No
94
- * payload — it is a bare "re-read" signal, never a mutation record. */
112
+ * payload — it is a bare "re-read" signal, never a mutation record.
113
+ * @mode emit */
95
114
  'evolution/skills-refresh'(): void;
115
+ /**
116
+ * A review pipeline failure was caught (never an unhandled rejection). An ANNOUNCEMENT for
117
+ * operators and observability — the reason is already logged by the emitter (A98 / O-1).
118
+ * @param event - the session whose review pipeline failed.
119
+ * @mode emit
120
+ */
96
121
  'evolution/review-error'(event: EvolutionReviewErrorEvent): void;
97
122
  }
98
123
  }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Untrusted text rendered as prompt lines (T3-03/A31) — ONE folding rule.
3
+ *
4
+ * The background-review digest embeds conversation text and tool output into a
5
+ * plain-text prompt whose PRODUCT is a plan with write permission. Anything the
6
+ * session ingested (a fetched page, a file body, command output) can therefore
7
+ * carry a newline plus a line shaped like the prompt's own structure — a
8
+ * `SYSTEM:`/`Review kind:` header, a `[result]` evidence line, a second copy
9
+ * of the closing instruction — and forge structure or inject instructions.
10
+ *
11
+ * FOLDING, not deleting: the reader (the model) must still SEE what the data
12
+ * said, it just must never become structure. Line separators become a visible
13
+ * `⏎`, and a line that would read as a role/evidence header gets a visible
14
+ * marker in front of it. Every original character stays in place.
15
+ *
16
+ * The boundary is the separator itself, not only a real newline: a payload can
17
+ * paste the separator verbatim, so neutralization matches after `⏎` whether it
18
+ * came from the fold or from the data. Both edits are idempotent (a second pass
19
+ * cannot add a second marker).
20
+ *
21
+ * Scope: text rendered as its OWN line(s) of a prompt. `render-facts`'
22
+ * sanitizer (evolution-maintenance) is the same posture for `key=value` fields
23
+ * and additionally escapes its block delimiter; it is not superseded here.
24
+ * @module @lmzhen/dsh-evolution-core
25
+ */
26
+ /** Visible stand-in for a folded line separator: one character that cannot be read as line structure. */
27
+ export declare const UNTRUSTED_LINE_BREAK = " \u23CE ";
28
+ /** Visible marker prefixed to a line that would otherwise read as a role/structure header. */
29
+ export declare const UNTRUSTED_LINE_MARK = "\u00B7 ";
30
+ /**
31
+ * Fold untrusted `text` so it cannot become structure, then bound it to `limit`
32
+ * visible characters. A non-positive `limit` means "no truncation".
33
+ */
34
+ export declare function foldUntrustedLines(text: string, limit: number): string;
35
+ //# sourceMappingURL=fold-untrusted.d.ts.map
@@ -14,9 +14,11 @@
14
14
  *
15
15
  * - **Cross-cutting basics** — `state-store.ts` (env roots — the single
16
16
  * source of DSH-home semantics), `serial.ts`, `numeric.ts`, `constants.ts`,
17
- * `mutations.ts`, `events.ts`, `gates.ts`, `errors.ts` (the error-code table).
17
+ * `mutations.ts`, `events.ts`, `gates.ts`, `errors.ts` (the error-code table),
18
+ * `payload.ts` (which of `facts`/`content` is the payload — one rule for the whole family).
18
19
  * - **Security primitives** — `threats.ts` (content threat scanner),
19
- * `redact.ts` (credential masking at model boundaries). Consumers:
20
+ * `redact.ts` (credential masking at model boundaries),
21
+ * `fold-untrusted.ts` (untrusted text must not become prompt structure). Consumers:
20
22
  * evolution-policy/threat, both stores, review, maintenance.
21
23
  * - **Core domain stores/logic** — `skill-store.ts` (skill tree engine +
22
24
  * IO-seam consumer), `memory-store.ts`, `usage.ts`, `curator.ts`,
@@ -51,6 +53,7 @@ export * from './redact.ts';
51
53
  export * from './review-channel.ts';
52
54
  export * from './serial.ts';
53
55
  export * from './probe.ts';
56
+ export * from './platform-services.ts';
54
57
  export * from './instance-scope.ts';
55
58
  export * from './write-inventory.ts';
56
59
  export * from './scope.ts';
@@ -73,4 +76,6 @@ export * from './params.ts';
73
76
  export * from './namespace-migration.ts';
74
77
  export * from './numeric.ts';
75
78
  export * from './opt-in.ts';
79
+ export * from './payload.ts';
80
+ export * from './fold-untrusted.ts';
76
81
  //# sourceMappingURL=index.d.ts.map
@@ -9,8 +9,9 @@ export interface MemoryOperation {
9
9
  action: 'add' | 'replace' | 'remove';
10
10
  facts?: string | undefined;
11
11
  /** PLAN-R2 P2-6 (2026-09-16): the dsh-memory contract's alias. applyBatchCore
12
- * reads `facts ?? content`, so a provider reusing this store directly can
13
- * write the tool-memory schema shape without a normalization shim. */
12
+ * resolves the pair through core's `payloadText` (T3-06/A34), so a provider
13
+ * reusing this store directly can write the tool-memory schema shape without
14
+ * a normalization shim. */
14
15
  content?: string | undefined;
15
16
  old_text?: string | undefined;
16
17
  }
@@ -98,7 +98,6 @@ export interface NamespaceMigrationDeps {
98
98
  */
99
99
  warn(message: string): void;
100
100
  }
101
- /**
102
101
  /** One row's pending write: the keys that row does not carry yet. */
103
102
  export interface NamespaceMigrationWrite {
104
103
  /** The row id. */
@@ -35,4 +35,24 @@ export declare function clampedNumber(value: number | undefined, fallback: numbe
35
35
  min?: number;
36
36
  max?: number;
37
37
  }): number;
38
+ /**
39
+ * Clamp one numeric knob and warn ONCE per key.
40
+ *
41
+ * S2.9 (T3-10): `evolution-review` and `evolution-curator` each carried their own copy of this
42
+ * helper — their own once-per-key set and their own message — so the two settings readers of ONE
43
+ * policy row could drift apart. The logic lives here; each caller passes its own sink (and its own
44
+ * package prefix). A live field is read at every use, so the warning cannot be per-read.
45
+ *
46
+ * @param warned - the keys this caller already warned about.
47
+ * @param warn - the warning sink (each package prefixes its own name).
48
+ * @param name - the config key, for the warning.
49
+ * @param value - the value as supplied, or its volatile reference.
50
+ * @param fallback - the package default a corrected value falls back to.
51
+ * @param min - the smallest value the engine can act on.
52
+ * @param max - the largest value the carrier can hold, when bounded.
53
+ * @returns the value the engine may use.
54
+ */
55
+ export declare function clampOnce(warned: Set<string>, warn: (message: string) => void, name: string, value: number | {
56
+ get(): number | undefined;
57
+ } | undefined, fallback: number, min: number, max?: number): number;
38
58
  //# sourceMappingURL=numeric.d.ts.map
@@ -5,8 +5,9 @@
5
5
  * the SAME name in two carriers (reviewMode, staleAfterDays, archiveAfterDays —
6
6
  * resolved by the existing policy-shadows-row rule) and six use DIFFERENT names.
7
7
  * This module owns the six: the policy/snapshot name is the canonical id, the
8
- * plugin-row name is a deprecated alias kept readable for one minor version
9
- * (0.6.x) and removable in 0.7.0.
8
+ * plugin-row name is a deprecated alias kept readable since 0.6.x. Removal was
9
+ * planned for 0.7.0 and is deferred: the aliases still ship, so no later version
10
+ * is claimed here.
10
11
  *
11
12
  * Reading stays compatible (a carrier still spelling the legacy name resolves),
12
13
  * writing is strict (the write path accepts canonical ids only, so no new
@@ -163,6 +164,31 @@ export declare function paramSettingsId(owner: string): string | undefined;
163
164
  * @returns the entry id the platform reports for that row.
164
165
  * @throws when the owner publishes no user layer.
165
166
  */
167
+ /** The keys the USER set in one row's settings namespace (G1 §8.4-A).
168
+ *
169
+ * The settings user layer is read in exactly ONE place, and it reads KEY NAMES, never values: the
170
+ * platform resolves the user layer into the row's live fields, so a value always comes from the
171
+ * row. The precedence rule (user > policy) needs to know WHICH keys the user set, or a deployment
172
+ * policy would silently override the user. S2.9 (T3-10): review and curator each had their own
173
+ * reader, so one key could be resolved two ways.
174
+ * @param ctx - the plugin context (a `get(name)` view is enough).
175
+ * @param rowId - the row's registry namespace (`paramRowId(<package>)`).
176
+ * @returns the keys present in that row's user layer.
177
+ */
178
+ export declare function userSetKeys(ctx: {
179
+ get(name: string): unknown;
180
+ }, rowId: string): ReadonlySet<string>;
181
+ /** ONE key's effective value, in the design's precedence (G1 §8.4-A): the user layer (whose value
182
+ * already arrived through the row's live field) → the deployment policy snapshot → the row itself.
183
+ * S2.9 (T3-10): both readers of the policy row resolve through this, so a new key cannot get one
184
+ * answer on the review side and another on the curator side.
185
+ * @param userSet - the keys the user set (see {@link userSetKeys}).
186
+ * @param row - the row's live fields.
187
+ * @param snapshot - the deployment policy snapshot, when a policy row is mounted.
188
+ * @param key - the key being resolved.
189
+ * @returns the effective value.
190
+ */
191
+ export declare function pickWithPolicy<R extends object, K extends keyof R & string>(userSet: ReadonlySet<string>, row: R, snapshot: Partial<Record<K, R[K]>> | undefined, key: K): R[K];
166
192
  export declare function paramRowId(owner: string): string;
167
193
  /**
168
194
  * Structural view of the platform settings provider: only the members the family
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The memory op's payload field, resolved in ONE place (T3-06/A34).
3
+ *
4
+ * A memory op carries its body under two names — `facts` (the original) and
5
+ * `content` (the schema alias) — and every reader used to resolve the pair with
6
+ * `op.facts ?? op.content`. `??` only skips null/undefined, so
7
+ * `{ facts: '', content: '正文' }` counted as "has facts": the plan validator
8
+ * refused the op as empty (a FALSE refusal of a payload that is right there),
9
+ * and the writers that normalized the pair dropped the body to `''`.
10
+ *
11
+ * The payload is the first field whose TRIMMED value is non-empty — a blank
12
+ * string carries no payload, so it yields to its alias. Both blank (or absent)
13
+ * ⇒ `''`, the one shape every caller already reads as "no payload".
14
+ *
15
+ * Non-string values are SKIPPED, never trimmed: a truthy non-string `facts`
16
+ * used to reach `.trim()` and throw a TypeError out of callers that promise to
17
+ * be deterministic (the E-60 / P2-1 class). The field-type gates reject such an
18
+ * op on their own; this helper must not be the one that throws.
19
+ *
20
+ * The union posture matches evolution-threat's E-28a scan (BOTH fields are
21
+ * scanned, not only the first truthy one); the two agree on what counts as
22
+ * provided.
23
+ * @module @lmzhen/dsh-evolution-core
24
+ */
25
+ /** Anything carrying the two payload aliases; `null` (a persisted or garbage op) resolves to `''`. */
26
+ export interface PayloadFields {
27
+ facts?: unknown;
28
+ content?: unknown;
29
+ }
30
+ /** The op's payload text: the first of `facts`/`content` whose trim is non-empty, trimmed; `''` when neither carries one. */
31
+ export declare function payloadText(op: PayloadFields | null | undefined): string;
32
+ //# sourceMappingURL=payload.d.ts.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Every platform service the family reaches for with `ctx.get`, and what degrades without it.
3
+ *
4
+ * O-2 (v46 audit): the family probes 26 platform services optionally — the platform has no
5
+ * formalized "optional dependency", so an ABSENT service is a silent downgrade and the diagnostic
6
+ * surface is the only place a reader can learn about it. `/evolution doctor` used to judge six of
7
+ * them; this table is the single source it judges the rest from, and the arch guard cross-checks it
8
+ * against the actual probe sites so a new `ctx.get` cannot stay undeclared.
9
+ *
10
+ * `side` decides who can judge it: `host` probes run in the plugin process (doctor judges those),
11
+ * `client` probes run in the browser half, where the host has no view of the seats.
12
+ * @module @lmzhen/dsh-evolution-core
13
+ */
14
+ /** One platform service the family probes, with the feature that goes quiet without it. */
15
+ export interface PlatformServiceProbe {
16
+ /** The service key as `ctx.get` spells it. */
17
+ readonly service: string;
18
+ /** The family feature that silently degrades when the service is absent. */
19
+ readonly feature: string;
20
+ /** Where the probe runs. */
21
+ readonly side: 'host' | 'client';
22
+ }
23
+ /** The 26 probes, in service-name order (the guard checks this list against the tree). */
24
+ export declare const PLATFORM_SERVICE_PROBES: readonly PlatformServiceProbe[];
25
+ /**
26
+ * The HOST-side probes that do not resolve in this runtime — what `/evolution doctor` prints as
27
+ * "capability absence". Empty is the normal, healthy answer: every capability the family can use
28
+ * is mounted, and the section stays away.
29
+ * @param has - a `ctx.get(name) !== undefined` probe of the running context.
30
+ * @returns the absent host-side probes, in declaration order.
31
+ */
32
+ export declare function absentHostServices(has: (name: string) => boolean): PlatformServiceProbe[];
33
+ //# sourceMappingURL=platform-services.d.ts.map
@@ -52,10 +52,38 @@ export declare function isMissingPath(error: unknown): boolean;
52
52
  * `catch { return [] }` served a broken store as an empty one (the N14 class).
53
53
  */
54
54
  export declare function probeList(io: EvolutionIoLike, dir: string): Promise<Probe<string[]>>;
55
+ /**
56
+ * Three-state TEXT read of one file. A missing path (ENOENT/ENOTDIR, or a backend whose read
57
+ * answers null) is `absent`; every other failure is `unknown` with its reason. The distinction is
58
+ * the whole point (N14/O-7): "I could not read this file" and "this file is not there" lead to
59
+ * opposite decisions once a write is about to replace the bytes — treating the first as the
60
+ * second is how a broken store reads as empty and a rescue copy never happens.
61
+ * @param io - the IO seam.
62
+ * @param path - the file to read.
63
+ * @returns the probe.
64
+ */
65
+ export declare function probeText(io: EvolutionIoLike, path: string): Promise<Probe<string>>;
55
66
  /**
56
67
  * Three-state mtime read. Absent covers both "the path is missing" and "this
57
68
  * backend has no mtime probe" — the seam cannot tell those apart, so consumers
58
69
  * that must know say so in their own log line. A stat that FAILS is unknown.
59
70
  */
60
71
  export declare function probeMtime(io: EvolutionIoLike, path: string): Promise<Probe<number>>;
72
+ /** The timestamp a curator run report DECLARES: `startedAt` (the run's own start), then `at` (the
73
+ * stamp the error writer sets on every report — F-327/P2-4).
74
+ * @param parsed - the parsed report, or anything else.
75
+ * @returns epoch milliseconds, or null when neither field carries a parseable time.
76
+ */
77
+ export declare function declaredReportStamp(parsed: unknown): number | null;
78
+ /** The time one curator run report is ordered by — ONE口径 for every reader (S2.9/T3-12): the
79
+ * declared stamp above, then the optional mtime probe as the fallback. `null` means "no usable
80
+ * time", and that single answer serves both consumers: such a report is never the LATEST (the
81
+ * panel) and is never DELETED (the retention sweep), which is exactly the pair of answers the two
82
+ * readers disagreed on when one ordered by the declared stamp and the other by mtime alone.
83
+ * @param io - the IO seam; a backend without `mtime` simply has no fallback.
84
+ * @param path - the report file the probe reads.
85
+ * @param parsed - the already-parsed report.
86
+ * @returns epoch milliseconds, or null.
87
+ */
88
+ export declare function reportTime(io: EvolutionIoLike, path: string, parsed: unknown): Promise<number | null>;
61
89
  //# sourceMappingURL=probe.d.ts.map
@@ -3,8 +3,8 @@
3
3
  * changes semantically: the bundle digest is the fail-closed signal for
4
4
  * review workers, so a stale id across deployments must be distinguishable.
5
5
  */
6
- export declare const PROMPT_BUNDLE_VERSION = 20;
7
- export declare const PROMPT_BUNDLE_ID = "dsh-evolution@20";
6
+ export declare const PROMPT_BUNDLE_VERSION = 21;
7
+ export declare const PROMPT_BUNDLE_ID = "dsh-evolution@21";
8
8
  export declare const MEMORY_REVIEW_PROMPT = "[Auto-review \u2014 Memory]\nReview the conversation above and consider saving to memory if appropriate.\n\nFocus on:\n1. Has the user revealed things about themselves \u2014 persona, desires, preferences, or personal details worth remembering?\n2. Has the user expressed expectations about how you should behave, their work style, or ways they want you to operate?\n\nIf something stands out, save it using the memory tool.\nIf nothing is worth saving, just say \"Nothing to save.\" and stop.";
9
9
  export declare const SKILL_REVIEW_PROMPT = "[Auto-review \u2014 Skills]\nReview the conversation above and update the skill library. Be ACTIVE \u2014 most sessions produce at least one skill update, even if small. A pass that does nothing is a missed learning opportunity, not a neutral outcome.\n\nTarget shape of the library: CLASS-LEVEL skills, each with a rich SKILL.md and a references/ directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries. This shapes HOW you update, not WHETHER you update.\n\nSignals to look for (any one of these warrants action):\n \u2022 User corrected your style, tone, format, legibility, or verbosity. Frustration signals like 'stop doing X', 'this is too verbose', 'don't format like this', 'why are you explaining', 'just give me the answer', 'you always do Y and I hate it', or an explicit 'remember this' are FIRST-CLASS skill signals, not just memory signals. Update the relevant skill(s) to embed the preference so the next session starts already knowing.\n \u2022 User corrected your workflow, approach, or sequence of steps. Encode the correction as a pitfall or explicit step in the skill that governs that class of task.\n \u2022 Non-trivial technique, fix, workaround, debugging path, or tool-usage pattern emerged that a future session would benefit from. Capture it.\n \u2022 A skill that got loaded or consulted this session turned out to be wrong, missing a step, or outdated. Patch it NOW.\n\nRead-before-write: update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session. On the plan channel ops on unread skills are rejected; direct writes have no such guard, so treat the rule as binding. CREATE of a brand-new umbrella is the only exception.\n\nPreference order \u2014 prefer the earliest action that fits, but do pick one when a signal above fired:\n 1. UPDATE A CURRENTLY-LOADED SKILL. Look back through the conversation for skills the user loaded or you read. If any of them covers the territory of the new learning, PATCH that one first. It is the skill that was in play, so it's the right one to extend.\n 2. UPDATE AN EXISTING UMBRELLA. If no loaded skill fits but an existing class-level skill does, patch it. Add a subsection, a pitfall, or broaden a trigger.\n 3. ADD A SUPPORT FILE under an existing umbrella. Skills can be packaged with three kinds of support files \u2014 use the right directory per kind:\n \u2022 references/<topic>.md \u2014 session-specific detail (error transcripts, reproduction recipes, provider quirks) AND condensed knowledge banks: quoted research, API docs, external authoritative excerpts, or domain notes you found while working on the problem. Write it concise and for the value of the task, not as a full mirror of upstream docs.\n \u2022 templates/<name>.<ext> \u2014 starter files meant to be copied and modified (boilerplate configs, scaffolding, a known-good example the agent can reproduce with modifications).\n \u2022 scripts/<name>.<ext> \u2014 statically re-runnable actions the skill can invoke directly (verification scripts, fixture generators, deterministic probes, anything the agent should run rather than hand-type each time).\n Add support files via skill_manage action=write_file with file_path starting 'references/', 'templates/', or 'scripts/'. The umbrella's SKILL.md should gain a one-line pointer to any new support file so future agents know it exists.\n 4. RESTRUCTURE a loaded skill whose body grew log-like \u2014 rc/sha/date-dense sections, session-detail spirals, or a fat body with no support files. Use skill_manage action=restructure with restructure: [{\"heading\": \"<the exact ## heading text>\", \"to_file\": \"references/<topic>.md\"}] \u2014 the ENTIRE ## section (from that heading to the next heading) moves into the support file and its position becomes a pointer line. The skill's name and directory never change. Only propose headings that exist verbatim in the body; never invent one, and never restructure a healthy small skill.\n 5. CREATE A NEW CLASS-LEVEL UMBRELLA SKILL when no existing skill covers the class. The name MUST be at the class level. The name MUST NOT be a specific PR number, error string, feature codename, library-alone name, or 'fix-X / debug-Y / audit-Z-today' session artifact. If the proposed name only makes sense for today's task, it's wrong \u2014 fall back to (1), (2), or (3).\n\nUser-preference embedding (important): when the user expressed a style/format/workflow preference, the update belongs in the SKILL.md body, not just in memory. Memory captures 'who the user is and what the current situation and state of your operations are'; skills capture 'how to do this class of task for this user'. When they complain about how you handled a task, the skill that governs that task needs to carry the lesson.\n\nIf you notice two existing skills that overlap, note it in your reply \u2014 the background curator handles consolidation at scale.\n\nTwo-tier deposition discipline (DSH addition, same spirit as the umbrella rule): before writing, classify the knowledge:\n \u2022 PATTERN (reusable \u2014 symptom \u2192 mechanism \u2192 fix \u2192 verification, still valuable next session) belongs in the SKILL.md body.\n \u2022 LOG (one-off \u2014 commit SHAs, npm/profile states, what this release changed, this session's process narrative) belongs in a references/ file, never the body. Body density IS reuse rate. Keep new entries tight: a pattern fits in 2-8 physical lines; prefer changing the current-state pointer over appending history.\n\nProtected skills (DO NOT edit these):\n \u2022 Bundled skills (shipped with the platform).\n \u2022 Hub-installed skills (installed from a hub).\nPinned skills are read-only to THIS background review pass \u2014 the pinned write guard refuses background changes, so this pass may not update them. They also cannot be archived by any writer (the foreground included): remove the .pinned marker first. Foreground and delegated-subagent update/patch writes to pinned skills remain allowed.\nIf the only skills that need updating are protected, say 'Nothing to save.' and stop.\n\nDo NOT capture (these become persistent self-imposed constraints that bite you later when the environment changes):\n \u2022 Environment-dependent failures: missing binaries, fresh-install errors, post-migration path mismatches, 'command not found', unconfigured credentials, uninstalled packages. The user can fix these \u2014 they are not durable rules.\n \u2022 Negative claims about tools or features ('browser tools do not work', 'X tool is broken', 'cannot use Y'). These harden into refusals the agent cites against itself for months after the actual problem was fixed.\n \u2022 Session-specific transient errors that resolved before the conversation ended. If retrying worked, the lesson is the retry pattern, not the original failure.\n \u2022 One-off task narratives. A user asking 'summarize today's market' or 'analyze this PR' is not a class of work that warrants a skill.\n\nIf a tool failed because of setup state, capture the FIX (install command, config step, env var to set) under an existing setup or troubleshooting skill \u2014 never 'this tool does not work' as a standalone constraint.\n\n'Nothing to save.' is a real option but should NOT be the default. If the session ran smoothly with no corrections and produced no new technique, just say 'Nothing to save.' and stop. Otherwise, act.";
10
10
  export declare const COMBINED_REVIEW_PROMPT = "[Auto-review]\nReview the conversation above and update two things:\n\n**Memory**: who the user is. Did the user reveal persona, desires, preferences, personal details, or expectations about how you should behave? Save facts about the user and durable preferences with the memory tool.\n\n**Skills**: how to do this class of task. Be ACTIVE \u2014 most sessions produce at least one skill update. A pass that does nothing is a missed learning opportunity, not a neutral outcome.\n\nTarget shape of the skill library: CLASS-LEVEL skills with a rich SKILL.md and a references/ directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries.\n\nSignals that warrant a skill update (any one is enough):\n \u2022 User corrected your style, tone, format, legibility, verbosity, or approach. Frustration is a FIRST-CLASS skill signal, not just a memory signal. 'stop doing X', 'don't format like this', 'I hate when you Y' \u2014 embed the lesson in the skill that governs that task so the next session starts fixed.\n \u2022 Non-trivial technique, fix, workaround, or debugging path emerged.\n \u2022 A skill that was loaded or consulted turned out wrong, missing, or outdated \u2014 patch it now.\n\nRead-before-write: update, patch, delete, or write support files ONLY into skills you loaded or read in THIS session. On the plan channel ops on unread skills are rejected; direct writes have no such guard, so treat the rule as binding. CREATE of a brand-new umbrella is the only exception.\n\nPreference order for skills \u2014 pick the earliest that fits:\n 1. UPDATE A CURRENTLY-LOADED SKILL. Check what skills were loaded or read in the conversation. If one of them covers the learning, PATCH it first. It was in play; it's the right place.\n 2. UPDATE AN EXISTING UMBRELLA. Patch it.\n 3. ADD A SUPPORT FILE under an existing umbrella via skill_manage action=write_file. Three kinds: references/<topic>.md for session-specific detail OR condensed knowledge banks (quoted research, API docs excerpts, domain notes) written concise and task-focused; templates/<name>.<ext> for starter files meant to be copied and modified; scripts/<name>.<ext> for statically re-runnable actions (verification, fixture generators, probes). Add a one-line pointer in SKILL.md so future agents find them.\n 4. RESTRUCTURE a loaded skill whose body grew log-like (rc/sha/date-dense sections, session-detail spirals, fat body with no support files) via skill_manage action=restructure with restructure: [{\"heading\": \"<the exact ## heading text>\", \"to_file\": \"references/<topic>.md\"}] \u2014 the ENTIRE ## section moves into the support file and its position becomes a pointer line; the skill's name and directory never change. Only propose headings that exist verbatim in the body.\n 5. CREATE A NEW CLASS-LEVEL UMBRELLA when nothing exists. Name at the class level \u2014 NOT a PR number, error string, codename, library-alone name, or 'fix-X / debug-Y' session artifact. If the name only fits today's task, fall back to (1), (2), or (3).\n\nTwo-tier deposition discipline (DSH addition): classify before writing \u2014 PATTERN (symptom \u2192 mechanism \u2192 fix \u2192 verification) goes in the SKILL.md body; LOG (commit SHAs, npm/profile states, this release's change list, this session's narrative) goes in a references/ file. Body density IS reuse rate; a pattern fits in 2-8 physical lines.\n\nUser-preference embedding: when the user complains about how you handled a task, update the skill that governs that task \u2014 memory alone isn't enough. Memory says 'who the user is and what the current situation and state of your operations are'; skills say 'how to do this class of task for this user'. Both should carry user-preference lessons when relevant.\n\nIf you notice overlapping existing skills, mention it \u2014 the background curator handles consolidation.\n\nProtected skills (DO NOT edit these):\n \u2022 Bundled skills (shipped with the platform).\n \u2022 Hub-installed skills (installed from a hub).\nPinned skills are read-only to THIS background review pass \u2014 the pinned write guard refuses background changes, so this pass may not update them. They also cannot be archived by any writer (the foreground included): remove the .pinned marker first. Foreground and delegated-subagent update/patch writes to pinned skills remain allowed.\nIf the only skills that need updating are protected, say 'Nothing to save.' and stop.\n\nDo NOT capture as skills (these become persistent self-imposed constraints that bite you later when the environment changes):\n \u2022 Environment-dependent failures: missing binaries, fresh-install errors, post-migration path mismatches, 'command not found', unconfigured credentials, uninstalled packages. The user can fix these \u2014 they are not durable rules.\n \u2022 Negative claims about tools or features ('browser tools do not work', 'X tool is broken', 'cannot use Y'). These harden into refusals the agent cites against itself for months after the actual problem was fixed.\n \u2022 Session-specific transient errors that resolved before the conversation ended. If retrying worked, the lesson is the retry pattern, not the original failure.\n \u2022 One-off task narratives. A user asking 'summarize today's market' or 'analyze this PR' is not a class of work that warrants a skill.\n\nIf a tool failed because of setup state, capture the FIX (install command, config step, env var to set) under an existing setup or troubleshooting skill \u2014 never 'this tool does not work' as a standalone constraint.\n\nAct on whichever of the two dimensions has real signal. If genuinely nothing stands out on either, say 'Nothing to save.' and stop \u2014 but don't reach for that conclusion as a default.";
@@ -13,7 +13,8 @@
13
13
  * path's gate, whose exec view may be a bare stub): it answers "which accessor, and what does an
14
14
  * unreadable session mean" — `undefined`, so the gate keeps its previous behavior instead of
15
15
  * refusing every write. The review path reads a real platform `Session` and calls the reader
16
- * directly; a fallback there would be dead code for an impossible case.
16
+ * directly; a fallback there would be dead code for an impossible case. The structural accessor is
17
+ * the family's OWN `readEvents` (A69): production reads no deprecated platform log accessor.
17
18
  *
18
19
  * v32 REV-06(a) is preserved by the dispatch normalizer: a skill counts as READ only when its read
19
20
  * did not fail, so a failed/timeout read cannot pass the gate and let a writer blind-overwrite
@@ -23,10 +24,18 @@
23
24
  * @module @lmzhen/dsh-evolution-core
24
25
  */
25
26
  import type { Context } from '@deepseek-ai/cordis';
26
- /** The session-log accessor the platform exposes (0.1.5 on; `session.events` before it). */
27
+ /**
28
+ * The family's OWN structural view of a session log, for a caller that holds no platform `Session`
29
+ * (the tool path's exec view may be a bare stub). The CALLER supplies it — the family reads no
30
+ * deprecated platform accessor (`snapshotEvents`/`eventAt`/`ownEvents`): those are the platform's own
31
+ * migration debt, and a family that reads them keeps the deprecated surface alive in every
32
+ * composition (A69 / 6-2). A platform `Session` does not carry this member and therefore takes the
33
+ * projection path below; this view exists so the fallback still has a basis instead of being deleted
34
+ * (deleting it would leave a stub session's read-before-write gate with nothing to fold).
35
+ */
27
36
  export interface EvolutionSessionLogView {
28
- /** @returns the session's events, oldest first. */
29
- snapshotEvents?: () => Iterable<{
37
+ /** @returns the events this view can see, oldest first; absent when the caller has no log. */
38
+ readEvents?: () => Iterable<{
30
39
  type: string;
31
40
  data?: unknown;
32
41
  }>;
@@ -64,6 +64,13 @@ export interface SkillSummary {
64
64
  * drift scans) saves the second per-skill read; the default stays body-free so
65
65
  * the common listing does not hold a whole tree in memory. */
66
66
  content?: string;
67
+ /** T2-07/A22: the parsed frontmatter record, present only for
68
+ * {@link SkillLibrary.list} calls that pass `{ withFrontmatter: true }`. The
69
+ * listing parses the file it already read, so a consumer that derives a policy
70
+ * from frontmatter (the catalog’s invocation map) shares that ONE read instead
71
+ * of paying a second per-skill read. Absent when the file carries no parseable
72
+ * frontmatter — the same posture as its description. */
73
+ frontmatter?: Record<string, unknown>;
67
74
  }
68
75
  /**
69
76
  * Stage-time anchor for a full-content write (v35 C9): the sha256 the caller read
@@ -326,12 +333,20 @@ export declare class SkillLibrary {
326
333
  /**
327
334
  * Summarize the skill tree.
328
335
  * @param options - `withContent` attaches each skill's whole SKILL.md body to
329
- * its summary (v35 C11): the read this listing already performs is the one the
330
- * body would cost again, so a content-consuming caller pays no second pass.
336
+ * its summary (v35 C11), `withFrontmatter` its parsed frontmatter record
337
+ * (T2-07/A22): the read this listing already performs is the one either would
338
+ * cost again, so a caller pays no second pass over the tree.
331
339
  * @returns one summary per readable skill directory.
332
340
  */
341
+ /** T2-V2/A27: the skill names the TREE holds — the same listing rule {@link list} applies (dot
342
+ * entries skipped, a SKILL.md must exist), WITHOUT reading any SKILL.md. A consumer that needs the
343
+ * member set rather than the summaries (the learning graph's node set) pays one directory listing
344
+ * instead of a whole-tree read pass.
345
+ * @returns the sorted skill directory names. */
346
+ treeNames(): Promise<string[]>;
333
347
  list(options?: {
334
348
  withContent?: boolean;
349
+ withFrontmatter?: boolean;
335
350
  }): Promise<SkillSummary[]>;
336
351
  read(rawName: string): Promise<string | null>;
337
352
  /**
@@ -390,6 +405,13 @@ export declare class SkillLibrary {
390
405
  * @returns path -> character count (possibly empty), or null when the listing or
391
406
  * a size probe cannot answer — unknown is never an empty map.
392
407
  */
408
+ /** A plan-time read whose bytes become a tree change's CAS baseline (`expected`). A failed read is
409
+ * NAMED and answers `null`, which is FAIL-CLOSED at commit: the baseline is then "absent", so an
410
+ * existing file counts as drift and the commit refuses instead of overwriting bytes nobody read
411
+ * (N36: the failure is not silently "the file is not there").
412
+ * @param target - the file a tree change is about to write.
413
+ * @returns its bytes, or `null` for both "absent" and "could not read". */
414
+ private readForCas;
393
415
  supportFileChars(rawName: string): Promise<Record<string, number> | null>;
394
416
  /**
395
417
  * Structure-health facts for one skill (rc.73 A1, 008 design): body
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.17.1",
4
+ "version": "0.18.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -50,12 +50,8 @@
50
50
  "writer": "evolution-state-json/src/index.ts",
51
51
  "serializedBy": "transact",
52
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."
53
+ "state": [],
54
+ "note": "The durable state stack (review/curator/pending). S4.4/O-6 moved its three warn/rewrite sets into the provider instance (QuarantineMemory), so no module-scope N12 store remains here."
59
55
  },
60
56
  {
61
57
  "id": "curator-reports",
@@ -74,5 +70,50 @@
74
70
  "marker": "LOCK_SUFFIX",
75
71
  "state": [],
76
72
  "note": "The skill tree itself. Per-target `<path>.lock` claims (pid:token body) guard every byte writer; destructive movers refuse a live lock."
73
+ },
74
+ {
75
+ "id": "activity-log",
76
+ "path": "<evolutionRoot>/activity.json",
77
+ "writer": "evolution-activity/src/index.ts",
78
+ "serializedBy": "transact",
79
+ "marker": "transactIo(io, activityFile(root)",
80
+ "state": [],
81
+ "note": "The plan-applied activity sidecar. Declared by v46 S1.12: the file carried a transactional write that no inventory row vouched for, and the N20 reverse scan is what named it."
82
+ },
83
+ {
84
+ "id": "skill-history-index",
85
+ "path": "<historyRoot>/skills/<name>/index.json",
86
+ "writer": "evolution-core/src/skill-history.ts",
87
+ "serializedBy": "transact",
88
+ "marker": "transactIo(io, file,",
89
+ "state": [],
90
+ "note": "One skill's recorded-version index (content chain + blob references). Declared by v46 S1.12: the replace path was already transactional but undeclared, so a regression to a plain write would have been invisible."
91
+ },
92
+ {
93
+ "id": "skill-store-transact-primitive",
94
+ "path": "<caller-supplied>",
95
+ "writer": "evolution-core/src/skill-store.ts",
96
+ "serializedBy": "transact",
97
+ "marker": "transactIo(ioLike, path, task)",
98
+ "state": [],
99
+ "note": "The store's transaction helper: it prefers the backend's own transact and falls back to the seam's transactIo. The target is the caller's path — each durable target has its own row above; this row vouches for the primitive itself."
100
+ },
101
+ {
102
+ "id": "feedback-events",
103
+ "path": "<dshHome>/evolution/events.json",
104
+ "writer": "evolution-feedback/src/index.ts",
105
+ "serializedBy": "transact",
106
+ "marker": "transactIo(io, eventsPath,",
107
+ "state": [],
108
+ "note": "Feedback appends to the family event log under the cross-process lock. Declared by v46 S1.12 (N20 reverse)."
109
+ },
110
+ {
111
+ "id": "pending-archive",
112
+ "path": "<stateHome>/pending-state-archive.json",
113
+ "writer": "evolution-state-json/src/index.ts",
114
+ "serializedBy": "transact",
115
+ "marker": "transactIo(io(), pathOf(PENDING_ARCHIVE_FILE)",
116
+ "state": [],
117
+ "note": "The resolved-pending archive (the legacy merge target). The store already declared its record map; v46 S1.12 declared the archive append as well — the second site in one file is exactly what the reverse scan exists to find."
77
118
  }
78
119
  ]