dsh-continual-evolve 0.7.6 → 0.9.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.
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Targeted memory recall (P1): the precise-reading counterpart of the cheap
3
+ * directory index.
4
+ *
5
+ * The injected directory view (`inject.ts`) shows one line per entry and the
6
+ * full `evolve_list` dumps whole stores; neither answers "give me everything
7
+ * about X". Recall fills that gap: filter by query/kind/scope/memory-type,
8
+ * rank with the same CJK BM25 machinery as injection, and return full
9
+ * content plus source, version, and staleness signals. Read-only — it never
10
+ * mutates a store.
11
+ */
12
+ import type { EvolutionEngine } from "./service.js";
13
+ import type { HarnessScope, RefinementKind } from "./types.js";
14
+ /** Memory recall types (the `memoryType` taxonomy, not entry kinds). */
15
+ export declare const RECALL_MEMORY_TYPES: readonly ["user", "feedback", "project", "reference"];
16
+ export type RecallMemoryType = (typeof RECALL_MEMORY_TYPES)[number];
17
+ /** Default and maximum hits returned per recall call. */
18
+ export declare const DEFAULT_RECALL_LIMIT = 10;
19
+ export declare const MAX_RECALL_LIMIT = 50;
20
+ export interface RecallFilters {
21
+ /** Free-text relevance query; empty means "most recently updated first". */
22
+ query?: string;
23
+ /** Entry kinds to search; default is memory only. */
24
+ kinds?: RefinementKind[];
25
+ /** Stores to search; default is local + project + global. */
26
+ scopes?: HarnessScope[];
27
+ /** Memory-type filter (applies to memory entries only). */
28
+ memoryTypes?: RecallMemoryType[];
29
+ /** Maximum hits; clamped to 1..50, defaults to 10. */
30
+ limit?: number;
31
+ /** Include archived entries; default false. */
32
+ includeArchived?: boolean;
33
+ }
34
+ /** One recalled entry with full content and provenance. */
35
+ export interface RecallHit {
36
+ scope: HarnessScope;
37
+ kind: RefinementKind;
38
+ id: string;
39
+ title: string;
40
+ content: string;
41
+ path: string;
42
+ version: number;
43
+ createdAt: string;
44
+ updatedAt: string;
45
+ archived: boolean;
46
+ /** Memory-type hook for memory entries; undefined for other kinds. */
47
+ memoryType?: string;
48
+ /** Conflict-hint stamp left by the promotion guard, when present. */
49
+ staleHint?: string;
50
+ /** Session the entry was distilled from, when recorded. */
51
+ sourceSession?: string;
52
+ /** Source user-message seqs, when recorded. */
53
+ sourceSeqs?: number[];
54
+ }
55
+ export interface RecallResult {
56
+ hits: RecallHit[];
57
+ /** Skipped scopes with reasons (read-path honesty: never silently drop a store). */
58
+ notes: string[];
59
+ /** Candidates seen before ranking/truncation. */
60
+ totalCandidates: number;
61
+ }
62
+ export interface RecallContext {
63
+ /** Live session id for the local store; absent → local is skipped with a note. */
64
+ sessionId?: string | undefined;
65
+ /** Resolved project key; absent → project is skipped with a note. */
66
+ projectKey?: string | undefined;
67
+ }
68
+ /**
69
+ * Search the harness stores with mechanical filters and BM25 ranking.
70
+ *
71
+ * @param engine The evolution engine (read-only use).
72
+ * @param context Store identities for the session-scoped stores.
73
+ * @param filters Query/kind/scope/type/limit/archive filters.
74
+ * @returns Ranked hits plus a note for every scope that could not be read.
75
+ * @throws On an unknown kind, scope, or memory type — fail loud instead of
76
+ * silently narrowing the search.
77
+ */
78
+ export declare function recallMemories(engine: EvolutionEngine, context: RecallContext, filters?: RecallFilters): RecallResult;
79
+ /**
80
+ * Render recall hits for a model or human reader: full content, version,
81
+ * source, and staleness on every hit; skipped scopes are always shown so
82
+ * "no results" can never hide an unread store.
83
+ */
84
+ export declare function formatRecallResult(result: RecallResult, query?: string): string;
85
+ //# sourceMappingURL=recall.d.ts.map
package/lib/recall.js ADDED
@@ -0,0 +1,142 @@
1
+ import { buildRelevanceIndex, relevanceScore } from "./search.js";
2
+ import { CONFLICT_HINT_KEY } from "./types.js";
3
+ import { isArchived, isMemoryType, ARCHIVED_AT_KEY, MEMORY_TYPE_KEY, SOURCE_SEQS_KEY, SOURCE_SESSION_KEY } from "./types.js";
4
+ /** Memory recall types (the `memoryType` taxonomy, not entry kinds). */
5
+ export const RECALL_MEMORY_TYPES = ["user", "feedback", "project", "reference"];
6
+ /** Entry kinds recall can filter on; memory is the default. */
7
+ const RECALL_KINDS = ["prompt", "memory", "skill", "subagent"];
8
+ /** Default and maximum hits returned per recall call. */
9
+ export const DEFAULT_RECALL_LIMIT = 10;
10
+ export const MAX_RECALL_LIMIT = 50;
11
+ /**
12
+ * Search the harness stores with mechanical filters and BM25 ranking.
13
+ *
14
+ * @param engine The evolution engine (read-only use).
15
+ * @param context Store identities for the session-scoped stores.
16
+ * @param filters Query/kind/scope/type/limit/archive filters.
17
+ * @returns Ranked hits plus a note for every scope that could not be read.
18
+ * @throws On an unknown kind, scope, or memory type — fail loud instead of
19
+ * silently narrowing the search.
20
+ */
21
+ export function recallMemories(engine, context, filters = {}) {
22
+ const kinds = filters.kinds ?? ["memory"];
23
+ for (const kind of kinds) {
24
+ if (!RECALL_KINDS.includes(kind)) {
25
+ throw new Error(`evolve_recall: unknown kind "${kind}" (expected one of ${RECALL_KINDS.join(", ")})`);
26
+ }
27
+ }
28
+ const scopes = filters.scopes ?? ["local", "project", "global"];
29
+ for (const scope of scopes) {
30
+ if (scope !== "local" && scope !== "project" && scope !== "global") {
31
+ throw new Error(`evolve_recall: unknown scope "${scope}" (expected local, project, or global)`);
32
+ }
33
+ }
34
+ const memoryTypes = filters.memoryTypes ?? [];
35
+ for (const memoryType of memoryTypes) {
36
+ if (!RECALL_MEMORY_TYPES.includes(memoryType)) {
37
+ throw new Error(`evolve_recall: unknown memoryType "${memoryType}" (expected one of ${RECALL_MEMORY_TYPES.join(", ")})`);
38
+ }
39
+ }
40
+ const limit = typeof filters.limit === "number" && Number.isFinite(filters.limit) && filters.limit > 0
41
+ ? Math.min(Math.floor(filters.limit), MAX_RECALL_LIMIT)
42
+ : DEFAULT_RECALL_LIMIT;
43
+ const query = filters.query?.trim() ?? "";
44
+ const candidates = [];
45
+ const notes = [];
46
+ for (const scope of scopes) {
47
+ const storeId = scope === "local" ? context.sessionId : scope === "project" ? context.projectKey : undefined;
48
+ if (scope !== "global" && storeId === undefined) {
49
+ notes.push(`${scope} store skipped: ${scope === "local" ? "no live session id" : "no project key for this session"}`);
50
+ continue;
51
+ }
52
+ let state;
53
+ try {
54
+ state = engine.load(scope, storeId);
55
+ }
56
+ catch (cause) {
57
+ notes.push(`${scope} store unreadable: ${cause instanceof Error ? cause.message : String(cause)}`);
58
+ continue;
59
+ }
60
+ for (const kind of kinds) {
61
+ for (const entry of Object.values(state.entries[kind])) {
62
+ if (!filters.includeArchived && isArchived(entry))
63
+ continue;
64
+ if (kind === "memory" && memoryTypes.length > 0) {
65
+ const type = entry.metadata[MEMORY_TYPE_KEY];
66
+ if (typeof type !== "string" || !memoryTypes.includes(type))
67
+ continue;
68
+ }
69
+ candidates.push(entry);
70
+ }
71
+ }
72
+ }
73
+ const ranked = query.length > 0 ? rankByRelevance(candidates, query) : [...candidates].sort((a, b) => (a.updated_at < b.updated_at ? 1 : a.updated_at > b.updated_at ? -1 : 0));
74
+ return { hits: ranked.slice(0, limit).map(toRecallHit), notes, totalCandidates: candidates.length };
75
+ }
76
+ function rankByRelevance(candidates, query) {
77
+ const index = buildRelevanceIndex(candidates);
78
+ const normalized = query.trim().toLowerCase();
79
+ return candidates
80
+ .map((entry) => {
81
+ let score = relevanceScore(index, entry, query);
82
+ // Exact scope:id addressing wins over fuzzy relevance (the
83
+ // memory-agent manifest search precedent).
84
+ if (`${entry.scope}:${entry.id}`.toLowerCase() === normalized)
85
+ score += 1000;
86
+ return { entry, score };
87
+ })
88
+ .filter((hit) => hit.score > 0)
89
+ .sort((a, b) => b.score - a.score || (a.entry.updated_at < b.entry.updated_at ? 1 : -1))
90
+ .map((hit) => hit.entry);
91
+ }
92
+ function toRecallHit(entry) {
93
+ const memoryType = entry.kind === "memory" && isMemoryType(entry.metadata[MEMORY_TYPE_KEY])
94
+ ? String(entry.metadata[MEMORY_TYPE_KEY])
95
+ : undefined;
96
+ const staleHint = typeof entry.metadata[CONFLICT_HINT_KEY] === "string" && entry.metadata[CONFLICT_HINT_KEY].length > 0
97
+ ? String(entry.metadata[CONFLICT_HINT_KEY])
98
+ : undefined;
99
+ const sourceSession = typeof entry.metadata[SOURCE_SESSION_KEY] === "string" ? String(entry.metadata[SOURCE_SESSION_KEY]) : undefined;
100
+ const rawSeqs = entry.metadata[SOURCE_SEQS_KEY];
101
+ const sourceSeqs = Array.isArray(rawSeqs) && rawSeqs.every((seq) => typeof seq === "number") ? rawSeqs : undefined;
102
+ return {
103
+ scope: entry.scope,
104
+ kind: entry.kind,
105
+ id: entry.id,
106
+ title: entry.title,
107
+ content: entry.content,
108
+ path: entry.path,
109
+ version: entry.version,
110
+ createdAt: entry.created_at,
111
+ updatedAt: entry.updated_at,
112
+ archived: isArchived(entry),
113
+ ...(memoryType !== undefined ? { memoryType } : {}),
114
+ ...(staleHint !== undefined ? { staleHint } : {}),
115
+ ...(sourceSession !== undefined ? { sourceSession } : {}),
116
+ ...(sourceSeqs !== undefined ? { sourceSeqs } : {}),
117
+ };
118
+ }
119
+ /**
120
+ * Render recall hits for a model or human reader: full content, version,
121
+ * source, and staleness on every hit; skipped scopes are always shown so
122
+ * "no results" can never hide an unread store.
123
+ */
124
+ export function formatRecallResult(result, query) {
125
+ const lines = [`recall: ${result.hits.length} hit(s)${query?.trim() ? ` for "${query.trim()}"` : ""} (${result.totalCandidates} candidate(s))`];
126
+ for (const hit of result.hits) {
127
+ const type = hit.memoryType !== undefined ? ` · type=${hit.memoryType}` : "";
128
+ const archived = hit.archived ? " · archived" : "";
129
+ const stale = hit.staleHint !== undefined ? ` · conflict-hint=${hit.staleHint}` : "";
130
+ const source = hit.sourceSession !== undefined ? ` · src=${hit.sourceSession}${hit.sourceSeqs ? `:${hit.sourceSeqs.join(",")}` : ""}` : "";
131
+ lines.push(`- [${hit.scope}:${hit.kind}:${hit.id}] ${hit.title} (v${hit.version}${type}${archived}${stale}${source})`);
132
+ lines.push(` ${hit.content}`);
133
+ }
134
+ for (const note of result.notes) {
135
+ lines.push(`note: ${note}`);
136
+ }
137
+ if (result.hits.length === 0) {
138
+ lines.push(`(${ARCHIVED_AT_KEY} entries are hidden unless includeArchived is set)`);
139
+ }
140
+ return lines.join("\n");
141
+ }
142
+ //# sourceMappingURL=recall.js.map
@@ -68,10 +68,9 @@ export function createReviewScheduler(execute, cursorOf) {
68
68
  bestCursor = candidate;
69
69
  }
70
70
  }
71
- if (bestIndex < 0) {
72
- pending.length = 0;
73
- return undefined;
74
- }
71
+ // bestIndex is always set here: the error purge above leaves only
72
+ // acquired snapshots in `settled` (SnapshotAcquisition has exactly
73
+ // two variants), so the first settled entry takes the slot.
75
74
  const chosen = pending[bestIndex];
76
75
  pending.splice(bestIndex, 1);
77
76
  for (let index = pending.length - 1; index >= 0; index -= 1) {
package/lib/review.d.ts CHANGED
@@ -44,7 +44,7 @@ export interface ReviewOptions {
44
44
  /** Direct-call token ledger destination; the review phase is fixed by this module. */
45
45
  tokenUsage?: TokenUsageTarget;
46
46
  }
47
- export declare const AUTO_REVIEW_SYSTEM_PROMPT = "You are the automatic /evolve review gate.\n\nDecide whether this checkpoint should continue the non-memory /evolve path.\nA dedicated memory extraction agent has already processed the same snapshot\nwith a frozen memory manifest, so do NOT approve solely to create, update,\narchive, or delete a memory entry. Decline with a rationale that the memory\nphase owns that evidence. For the remaining kinds, approve repeated failures,\nreusable tactics, repeated delegation roles, or narrow behavioral policies.\n\nThe current harness state below includes GLOBAL entries (scope=global) plus\nthis session's local entries (scope=local). When a topic is already covered\nby a global entry, do NOT approve a local duplicate of it \u2014 decline and say\nin the rationale that the topic is already covered globally.\n\nReject one-off noise, unsupported hypotheses, transient tool outputs, and\nrequests that carry no reusable content.\n\nStale local entries (superseded, long-unused, obsolete facts) are a valid\nrefine target: approve with instructions naming the entry ids, and tell the\nplanner to archive them (archive hides from injection, data stays restorable)\nrather than delete.\n\nSkill-related trajectories (the evidence concerns creating or improving a\nskill entry) are judged against the DSH skill quality standard (skill-audit\ndimensions: frontmatter routing, the 7 structural features, paragraph\nskeleton, no duplication of the official 11 skills or covered skills).\nApprove only when the trajectory shows a REAL trigger scenario and the\nresulting skill would meet the standard; otherwise decline and say in the\nrationale what must improve \u2014 drafting follows skill-creator, and the\nplanner receives the standard as its <skill_quality_standard> block.\n\nRepeated multi-step workflows (session start/end routines, recurring\nwrap-up or handoff procedures) are a valid refine target: approve with\ninstructions telling the planner to propose a guidance skill (kind=skill,\nskill_kind=guidance \u2014 a SKILL.md document, no python reference). Only\npropose when the same workflow recurs in the trajectory \u2014 never for\none-off flows. Auto-created skills are always offered to the user for a\ndecision before they land; the gate never writes a skill silently.\n\nReturn JSON only:\n{\n \"shouldRefine\": true|false,\n \"rationale\": \"short reason\",\n \"instructions\": \"optional concise instructions for /evolve if shouldRefine is true\"\n}";
47
+ export declare const AUTO_REVIEW_SYSTEM_PROMPT = "You are the automatic /evolve review gate.\n\nDecide whether this checkpoint should continue the non-memory /evolve path.\nA dedicated memory extraction agent has already processed the same snapshot\nwith a frozen memory manifest, so do NOT approve solely to create, update,\narchive, or delete a memory entry. Decline with a rationale that the memory\nphase owns that evidence. For the remaining kinds, approve ONLY when the\ntrajectory shows repeated evidence: the same failure, tactic, delegation\nrole, or behavioral policy recurs at least twice across turns. A single\none-off interaction defaults to decline no matter how useful it looks for\nfuture turns \u2014 one data point is never enough to generalize. When approving,\ncite the repeated spans in the instructions so the planner can ground its\nedits.\n\nThe current harness state below includes GLOBAL entries (scope=global) plus\nthis session's local entries (scope=local). When a topic is already covered\nby a global entry, do NOT approve a local duplicate of it \u2014 decline and say\nin the rationale that the topic is already covered globally.\n\nReject one-off noise, unsupported hypotheses, transient tool outputs, and\nrequests that carry no reusable content.\n\nStale local entries (superseded, long-unused, obsolete facts) are a valid\nrefine target: approve with instructions naming the entry ids, and tell the\nplanner to archive them (archive hides from injection, data stays restorable)\nrather than delete.\n\nSkill-related trajectories (the evidence concerns creating or improving a\nskill entry) are judged against the DSH skill quality standard (skill-audit\ndimensions: frontmatter routing, the 7 structural features, paragraph\nskeleton, no duplication of the official 11 skills or covered skills).\nApprove only when the trajectory shows a REAL trigger scenario and the\nresulting skill would meet the standard; otherwise decline and say in the\nrationale what must improve \u2014 drafting follows skill-creator, and the\nplanner receives the standard as its <skill_quality_standard> block.\n\nRepeated multi-step workflows (session start/end routines, recurring\nwrap-up or handoff procedures) are a valid refine target: approve with\ninstructions telling the planner to propose a guidance skill (kind=skill,\nskill_kind=guidance \u2014 a SKILL.md document, no python reference). Only\npropose when the same workflow recurs in the trajectory \u2014 never for\none-off flows. Auto-created skills are always offered to the user for a\ndecision before they land; the gate never writes a skill silently.\n\nReturn JSON only:\n{\n \"shouldRefine\": true|false,\n \"rationale\": \"short reason\",\n \"instructions\": \"optional concise instructions for /evolve if shouldRefine is true\"\n}";
48
48
  /** Parse the gate's JSON reply. */
49
49
  export declare function parseAutoRefineReview(text: string): AutoRefineReview;
50
50
  /** Serialize surface events to bounded role-prefixed text. */
package/lib/review.js CHANGED
@@ -10,8 +10,13 @@ Decide whether this checkpoint should continue the non-memory /evolve path.
10
10
  A dedicated memory extraction agent has already processed the same snapshot
11
11
  with a frozen memory manifest, so do NOT approve solely to create, update,
12
12
  archive, or delete a memory entry. Decline with a rationale that the memory
13
- phase owns that evidence. For the remaining kinds, approve repeated failures,
14
- reusable tactics, repeated delegation roles, or narrow behavioral policies.
13
+ phase owns that evidence. For the remaining kinds, approve ONLY when the
14
+ trajectory shows repeated evidence: the same failure, tactic, delegation
15
+ role, or behavioral policy recurs at least twice across turns. A single
16
+ one-off interaction defaults to decline no matter how useful it looks for
17
+ future turns — one data point is never enough to generalize. When approving,
18
+ cite the repeated spans in the instructions so the planner can ground its
19
+ edits.
15
20
 
16
21
  The current harness state below includes GLOBAL entries (scope=global) plus
17
22
  this session's local entries (scope=local). When a topic is already covered
@@ -119,7 +124,7 @@ export async function reviewAutoRefine(ctx, options) {
119
124
  `<current_harness_state>\n${formatHarnessStateForPrompt(state)}\n</current_harness_state>`,
120
125
  `<refinement_history>\n${historyForPrompt(history)}\n</refinement_history>`,
121
126
  conversationBlock,
122
- "Return shouldRefine=true when the trajectory contains evidence useful to this session's future turns. Prefer local edits; do not ask for global refinement here.",
127
+ "Return shouldRefine=true only when the trajectory shows repeated evidence (>=2 occurrences) useful to this session's future turns. A single one-off interaction defaults to shouldRefine=false. Prefer local edits; do not ask for global refinement here.",
123
128
  ]
124
129
  .filter(Boolean)
125
130
  .join("\n\n");
package/lib/service.js CHANGED
@@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto";
4
4
  import { rollbackProposal } from "./rollback.js";
5
5
  import { loadHarnessState, saveHarnessState } from "./state.js";
6
6
  import { appendResult, loadResults, pruneJsonlFile, pruneSnapshots, resolveHistoryRetention, snapshotBefore, storePaths } from "./store.js";
7
+ import { materializeMemoryProjection } from "./projection.js";
7
8
  import { CONFLICT_BLOCK_SCORE, CONFLICT_WARN_SCORE, buildConflictNotice, mostSimilarEntry, secretLeakReason } from "./promotion.js";
8
9
  /** A post-commit apply failure that still carries the durable applied result. */
9
10
  export class EvolutionApplyPostCommitError extends Error {
@@ -137,6 +138,19 @@ export function createEvolutionEngine(baseDir, hooks = {}, opts = {}) {
137
138
  catch {
138
139
  // ignored — the next apply retries
139
140
  }
141
+ // Readable Markdown projection (P1): rewritten from the post-apply
142
+ // state whenever a memory edit landed, so rollback and archive stay
143
+ // in sync through this same apply path. The model has no writer for
144
+ // these files — only this engine entry point materializes them.
145
+ if (result.appliedEdits.some((edit) => edit.applied && edit.kind === "memory")) {
146
+ try {
147
+ materializeMemoryProjection(paths.stateDir, state);
148
+ }
149
+ catch {
150
+ // ignored — the projection is derived (JSON stays the source
151
+ // of truth) and the next memory apply retries
152
+ }
153
+ }
140
154
  try {
141
155
  hooks.onApplied?.(result);
142
156
  }
@@ -57,7 +57,8 @@ export function renderSkillMarkdown(entry) {
57
57
  lines.push(`- ${key}: ${JSON.stringify(value)}`);
58
58
  }
59
59
  }
60
- if (Object.keys(entry.arguments).length > 0) {
60
+ const args = entry.arguments;
61
+ if (args && typeof args === "object" && Object.keys(args).length > 0) {
61
62
  lines.push("", "## Arguments", "```json", JSON.stringify(entry.arguments, null, 2), "```");
62
63
  }
63
64
  return `${lines.join("\n").trimEnd()}\n`;
@@ -54,8 +54,11 @@ export declare function validateRenderedSkillMarkdown(markdown: string): string[
54
54
  * parsed instead of the generated one (the platform reads the FIRST
55
55
  * closing `---`), so the file could be ignored or routed wrongly;
56
56
  * - resource references (`references/…`, `scripts/…`) must be skill-local
57
- * relative paths — parent-relative (`../`) or absolute targets escape the
58
- * skill directory and are rejected.
57
+ * relative paths — an embedded `/../` after the category escapes the skill
58
+ * directory and is rejected. A leading `../` instead addresses a sibling
59
+ * skill (cross-skill interlink, e.g. `../skill-creator/…`) and is allowed:
60
+ * it is not dangling-checked since cross-skill targets live outside this
61
+ * entry's shipped files (see skillResourceRefs).
59
62
  * Returns human-readable problems; an empty array means the content is
60
63
  * mechanically acceptable.
61
64
  */
@@ -73,6 +76,8 @@ export declare function validateRenderedSkill(entry: HarnessEntry): string[];
73
76
  * targets starting with the category, plus backticked/prose paths carrying
74
77
  * a filename extension. Used after materialization to warn about dangling
75
78
  * references (a body referencing a resource the entry never ships).
79
+ * Leading-`../` cross-skill interlinks are skipped, not warned: they resolve
80
+ * against sibling skill directories, never against shipped files.
76
81
  */
77
82
  export declare function skillResourceRefs(content: string): string[];
78
83
  /** Kebab-case name under which the entry materializes (exported for diagnostics). */
@@ -244,8 +244,11 @@ export function validateRenderedSkillMarkdown(markdown) {
244
244
  * parsed instead of the generated one (the platform reads the FIRST
245
245
  * closing `---`), so the file could be ignored or routed wrongly;
246
246
  * - resource references (`references/…`, `scripts/…`) must be skill-local
247
- * relative paths — parent-relative (`../`) or absolute targets escape the
248
- * skill directory and are rejected.
247
+ * relative paths — an embedded `/../` after the category escapes the skill
248
+ * directory and is rejected. A leading `../` instead addresses a sibling
249
+ * skill (cross-skill interlink, e.g. `../skill-creator/…`) and is allowed:
250
+ * it is not dangling-checked since cross-skill targets live outside this
251
+ * entry's shipped files (see skillResourceRefs).
249
252
  * Returns human-readable problems; an empty array means the content is
250
253
  * mechanically acceptable.
251
254
  */
@@ -260,8 +263,12 @@ export function validateSkillEntryContent(content) {
260
263
  problems.push("skill content must not start with a `---` frontmatter block (the materializer generates frontmatter from id/title; a body-level `---` would shadow it and the platform could IGNORE the file)");
261
264
  }
262
265
  for (const match of trimmed.matchAll(/(?<![\w])(references|scripts)\/[^\s)]+/g)) {
266
+ // match[0] always starts with `references/` or `scripts/` (the regex
267
+ // anchors it there), so only an embedded `/../` can escape the skill
268
+ // directory — the ../-prefix, absolute-path, and drive-letter
269
+ // spellings can never match and are intentionally not checked.
263
270
  const ref = match[0] ?? "";
264
- if (ref.startsWith("../") || ref.includes("/../") || ref.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(ref)) {
271
+ if (ref.includes("/../")) {
265
272
  problems.push(`skill content resource reference escapes the skill directory: ${ref}`);
266
273
  }
267
274
  }
@@ -282,6 +289,8 @@ export function validateRenderedSkill(entry) {
282
289
  * targets starting with the category, plus backticked/prose paths carrying
283
290
  * a filename extension. Used after materialization to warn about dangling
284
291
  * references (a body referencing a resource the entry never ships).
292
+ * Leading-`../` cross-skill interlinks are skipped, not warned: they resolve
293
+ * against sibling skill directories, never against shipped files.
285
294
  */
286
295
  export function skillResourceRefs(content) {
287
296
  const refs = new Set();
package/lib/tool.js CHANGED
@@ -5,6 +5,7 @@ import { formatHarnessStateForPrompt } from "./render.js";
5
5
  import { requireGlobalApproval } from "./approval.js";
6
6
  import { CONFLICT_WARN_SCORE, buildConflictNotice, mostSimilarEntry } from "./promotion.js";
7
7
  import { entrySourceOf } from "./source.js";
8
+ import { formatRecallResult, recallMemories } from "./recall.js";
8
9
  import { getUsageCount, loadUsage } from "./usage.js";
9
10
  import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
10
11
  import { DEFAULT_REVIEWS_RETAIN } from "./store.js";
@@ -115,6 +116,45 @@ export function registerEvolveTools(ctx, engine, opts) {
115
116
  return textResult(text);
116
117
  },
117
118
  }));
119
+ ctx.tools.register(defineTool({
120
+ name: "evolve_recall",
121
+ description: "Targeted recall over the harness stores: filter memories (or other kinds) by query, kind, scope, and memory type, and read back full content with version, source, and staleness signals. Read-only — it never mutates a store. Prefer this over evolve_list when looking for something specific.",
122
+ parameters: {
123
+ query: { type: "string", description: "Free-text relevance query. Empty means most-recently-updated first." },
124
+ kinds: {
125
+ type: "array",
126
+ items: { type: "string" },
127
+ description: "Entry kinds to search (default memory only).",
128
+ },
129
+ scopes: {
130
+ type: "array",
131
+ items: { type: "string" },
132
+ description: "Stores to search (default local, project, and global).",
133
+ },
134
+ memoryTypes: {
135
+ type: "array",
136
+ items: { type: "string" },
137
+ description: "Memory-type filter: user, feedback, project, reference (memory entries only).",
138
+ },
139
+ limit: { type: "number", description: "Maximum hits (default 10, at most 50)." },
140
+ includeArchived: { type: "boolean", description: "Include archived entries (default false)." },
141
+ },
142
+ output: {
143
+ schema: { type: "object", additionalProperties: false, properties: { text: { type: "string", required: true } } },
144
+ render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
145
+ },
146
+ execute: async (args, exec) => {
147
+ const result = recallMemories(engine, { sessionId: sessionIdOf(exec), projectKey: projectKeyOf(exec.agent) ?? undefined }, {
148
+ ...(typeof args.query === "string" ? { query: args.query } : {}),
149
+ ...(Array.isArray(args.kinds) ? { kinds: args.kinds } : {}),
150
+ ...(Array.isArray(args.scopes) ? { scopes: args.scopes } : {}),
151
+ ...(Array.isArray(args.memoryTypes) ? { memoryTypes: args.memoryTypes } : {}),
152
+ ...(typeof args.limit === "number" ? { limit: args.limit } : {}),
153
+ ...(typeof args.includeArchived === "boolean" ? { includeArchived: args.includeArchived } : {}),
154
+ });
155
+ return textResult(formatRecallResult(result, typeof args.query === "string" ? args.query : undefined));
156
+ },
157
+ }));
118
158
  ctx.tools.register(defineTool({
119
159
  name: "evolve_add",
120
160
  description: "Create one harness entry (prompt/memory/skill/subagent). Executable skills require reference {type:python, import, callable} and an arguments contract; guidance skills (skill_kind=guidance) are SKILL.md documents — recurring multi-step workflows — and must NOT carry a reference. Snapshot, version, and history are handled automatically.",
package/lib/types.d.ts CHANGED
@@ -1,13 +1,3 @@
1
- /**
2
- * Type model for the continual-evolution harness state.
3
- *
4
- * Design provenance: the state model follows the continual-harness design
5
- * validated in prime-agent's `/refine` subsystem (MIT): versioned entries
6
- * keyed by kind, an append-only refinement history carrying an evidence
7
- * trail, atomic persistence, and deterministic inverse-op rollback. The
8
- * code here is an original implementation, written for the DeepSeek Harness
9
- * plugin surface.
10
- */
11
1
  /** What a harness entry can be. */
12
2
  export type RefinementKind = "prompt" | "memory" | "skill" | "subagent";
13
3
  /**
@@ -201,7 +191,15 @@ export interface PythonReference {
201
191
  callable?: string;
202
192
  call_pattern?: string;
203
193
  }
204
- /** A stable id derived from a title. */
194
+ /**
195
+ * A stable id derived from a title: ASCII runs become snake_case (unchanged
196
+ * legacy behavior). Titles with no ASCII content at all (e.g. pure Chinese)
197
+ * used to collapse onto the bare fallback, so distinct memories shared one
198
+ * id and every later create failed as "already exists". They now take
199
+ * `<fallback>_<sha256-8>` instead: distinct titles get distinct ids while
200
+ * identical titles still dedupe onto the same id. The fallback itself is
201
+ * assumed id-safe (callers pass entry kinds).
202
+ */
205
203
  export declare function slug(raw: string, fallback: string): string;
206
204
  /** Fresh empty state with the current schema version. */
207
205
  export declare function emptyHarnessState(): HarnessState;
package/lib/types.js CHANGED
@@ -8,6 +8,7 @@
8
8
  * code here is an original implementation, written for the DeepSeek Harness
9
9
  * plugin surface.
10
10
  */
11
+ import { createHash } from "node:crypto";
11
12
  /**
12
13
  * Memory-entry shape discipline (one fact per entry, typed like ZCode's
13
14
  * memory frontmatter): every `memory` create must stamp one of these in
@@ -85,7 +86,15 @@ export function isArchived(entry) {
85
86
  const value = entry.metadata[ARCHIVED_AT_KEY];
86
87
  return typeof value === "string" && value.length > 0;
87
88
  }
88
- /** A stable id derived from a title. */
89
+ /**
90
+ * A stable id derived from a title: ASCII runs become snake_case (unchanged
91
+ * legacy behavior). Titles with no ASCII content at all (e.g. pure Chinese)
92
+ * used to collapse onto the bare fallback, so distinct memories shared one
93
+ * id and every later create failed as "already exists". They now take
94
+ * `<fallback>_<sha256-8>` instead: distinct titles get distinct ids while
95
+ * identical titles still dedupe onto the same id. The fallback itself is
96
+ * assumed id-safe (callers pass entry kinds).
97
+ */
89
98
  export function slug(raw, fallback) {
90
99
  const normalized = raw
91
100
  .trim()
@@ -93,7 +102,10 @@ export function slug(raw, fallback) {
93
102
  .replace(/[^a-z0-9]+/g, "_")
94
103
  .replace(/^_+|_+$/g, "")
95
104
  .slice(0, 80);
96
- return normalized || fallback;
105
+ if (normalized)
106
+ return normalized;
107
+ const digest = createHash("sha256").update(raw, "utf8").digest("hex").slice(0, 8);
108
+ return `${fallback}_${digest}`;
97
109
  }
98
110
  /** Fresh empty state with the current schema version. */
99
111
  export function emptyHarnessState() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-continual-evolve",
3
- "version": "0.7.6",
3
+ "version": "0.9.0",
4
4
  "description": "Continual self-evolution plugin for DeepSeek Harness: versioned, auditable, rollback-safe harness state (prompt notes, memories, skills, subagent specs) refined from session trajectories.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -48,12 +48,15 @@
48
48
  "build": "tsc -p tsconfig.json",
49
49
  "prepare": "tsc -p tsconfig.json",
50
50
  "dev": "tsc -p tsconfig.json --watch",
51
- "typecheck": "tsc -p tsconfig.json --noEmit",
51
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.scripts.json --noEmit",
52
52
  "test": "vitest run",
53
53
  "test:coverage": "vitest run --coverage",
54
+ "coverage:gaps": "tsx scripts/coverage-gaps.ts",
54
55
  "test:watch": "vitest",
55
56
  "lint": "oxlint src test",
56
- "clean": "rm -rf lib"
57
+ "clean": "rm -rf lib",
58
+ "check:docs": "tsx scripts/run-gates.ts docs",
59
+ "check:governance": "tsx scripts/verify-governance.ts"
57
60
  },
58
61
  "peerDependencies": {
59
62
  "@deepseek-ai/cordis": "^4.0.1",
@@ -73,6 +76,7 @@
73
76
  "@types/node": "^22.10.0",
74
77
  "@vitest/coverage-v8": "^3.2.0",
75
78
  "oxlint": "^0.16.0",
79
+ "tsx": "^4.23.15",
76
80
  "typescript": "^5.9.0",
77
81
  "vitest": "^3.2.0"
78
82
  }