dsh-continual-evolve 0.8.0 → 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.
package/README.md CHANGED
@@ -7,7 +7,10 @@
7
7
  [![CI](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml/badge.svg)](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
8
8
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
9
  [![Node](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933)](package.json)
10
- [![Tests](https://img.shields.io/badge/tests-783%20passing-brightgreen)]()
10
+ [![Tests](https://img.shields.io/badge/tests-1031%20passing-brightgreen)]()
11
+ [![Coverage · statements](https://img.shields.io/badge/coverage_statements-98%25-brightgreen)]()
12
+ [![Coverage · branches](https://img.shields.io/badge/coverage_branches-92%25-green)]()
13
+ [![Coverage · functions](https://img.shields.io/badge/coverage_functions-99%25-brightgreen)]()
11
14
 
12
15
  Continual self-evolution for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): a versioned, auditable, rollback-safe harness state layer — prompt notes, memories, skills, subagent specs — refined from session trajectories.
13
16
 
@@ -139,8 +142,9 @@ before aborting.
139
142
 
140
143
  ```bash
141
144
  pnpm install && pnpm build # deps + tsc -> lib/
142
- pnpm test # vitest (783 tests)
145
+ pnpm test # vitest (1031 tests)
143
146
  pnpm test:coverage # v8 coverage, thresholds enforced in CI
147
+ pnpm coverage:gaps # locate uncovered lines per file (read-only)
144
148
  pnpm lint # oxlint src test
145
149
  ```
146
150
 
@@ -148,7 +152,7 @@ Project layout:
148
152
 
149
153
  ```
150
154
  ├── src/ # engine, tools, commands, memory agent, recall, projection, gate, fate, benchmark, injection + token usage…
151
- ├── test/ # vitest suites (47 files)
155
+ ├── test/ # vitest suites (50 files)
152
156
  ├── lib/ # build output (tsc)
153
157
  ├── docs/
154
158
  │ ├── design.md # full design doc (hardening matrix)
package/README.zh.md CHANGED
@@ -7,7 +7,10 @@
7
7
  [![CI](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml/badge.svg)](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
8
8
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
9
  [![Node](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933)](package.json)
10
- [![Tests](https://img.shields.io/badge/tests-783%20passing-brightgreen)]()
10
+ [![Tests](https://img.shields.io/badge/tests-1031%20passing-brightgreen)]()
11
+ [![Coverage · statements](https://img.shields.io/badge/coverage_statements-98%25-brightgreen)]()
12
+ [![Coverage · branches](https://img.shields.io/badge/coverage_branches-92%25-green)]()
13
+ [![Coverage · functions](https://img.shields.io/badge/coverage_functions-99%25-brightgreen)]()
11
14
 
12
15
  [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的持续自进化插件:一套**版本化、可审计、可回滚**的 harness 状态层——提示词补充、记忆、技能、子代理规格——从会话轨迹中沉淀而来。
13
16
 
@@ -123,8 +126,9 @@ profile patch 示例:
123
126
 
124
127
  ```bash
125
128
  pnpm install && pnpm build # 依赖 + tsc -> lib/
126
- pnpm test # vitest(783 例)
129
+ pnpm test # vitest(1031 例)
127
130
  pnpm test:coverage # v8 覆盖率,CI 强制阈值
131
+ pnpm coverage:gaps # 定位各文件未覆盖行(只读)
128
132
  pnpm lint # oxlint src test
129
133
  ```
130
134
 
@@ -132,7 +136,7 @@ pnpm lint # oxlint src test
132
136
 
133
137
  ```
134
138
  ├── src/ # 引擎、工具、命令、memory Agent、召回、投影、门禁、fate、benchmark、注入 + token 用量…
135
- ├── test/ # vitest 测试套件(47 个文件)
139
+ ├── test/ # vitest 测试套件(50 个文件)
136
140
  ├── lib/ # 构建产物(tsc)
137
141
  ├── docs/
138
142
  │ ├── design.md # 完整设计文档(硬化矩阵)
@@ -106,7 +106,7 @@ export async function executeBenchmarkCommand(ctx, engine, invocation, rest, run
106
106
  for (const d of board.decisions) {
107
107
  lines.push(`decision: ${d.accepted ? "ACCEPTED" : "rejected"} ${d.candidateLabel} — ${d.reasons.join("; ") || "ok"}`);
108
108
  }
109
- return success(lines.join("\n") || "(empty scoreboard)");
109
+ return success(lines.join("\n"));
110
110
  }
111
111
  case "run": {
112
112
  const bid = stripAngleBrackets(args[0] ?? "");
package/lib/fate.js CHANGED
@@ -241,7 +241,10 @@ export async function runLocalFatePhase(ctx, engine, agent, config, state, reaso
241
241
  const plan = planLocalFates(assessment.items, candidates, globalState, config.promotionPolicy);
242
242
  const needsDialog = plan.promotable.length + plan.splits.length + plan.reviewArchives.length > 0;
243
243
  let consent = { approved: false, asked: false, reason: "nothing-to-ask" };
244
- if (reason !== "compact" && needsDialog) {
244
+ // Turn-path tightening (2026-09-25): turn_snapshot/turn_interval never open
245
+ // a dialog — governed actions are deferred to `/evolve wrapup`. Only an
246
+ // explicit goal_blocked signal (own streak counter) may consult.
247
+ if (reason === "goal_blocked" && needsDialog) {
245
248
  consent = await consultLocalFates(ctx, agent, plan, state);
246
249
  }
247
250
  if (consent.approved) {
@@ -255,9 +258,6 @@ export async function runLocalFatePhase(ctx, engine, agent, config, state, reaso
255
258
  rationale: `fate: ${assessment.rationale} (${applied.join("; ")})`,
256
259
  refinementId: results.map((result) => result.id).join(","),
257
260
  });
258
- if (config.notifyOnAutoReview && (reason === "turn_snapshot" || reason === "turn_interval") && applied.length > 0) {
259
- notifyFateApplied(ctx, agent, applied);
260
- }
261
261
  return;
262
262
  }
263
263
  if (consent.reason === "declined" || consent.reason === "unavailable" || consent.reason === "error") {
@@ -273,18 +273,18 @@ export async function runLocalFatePhase(ctx, engine, agent, config, state, reaso
273
273
  });
274
274
  return;
275
275
  }
276
- if (reason === "compact" && needsDialog) {
277
- // Compaction: no dialog. Only deterministic silent archives apply;
278
- // governed actions are deferred with an audit record.
276
+ if (needsDialog && reason !== "goal_blocked") {
277
+ // Turn/compact path: no dialog. Only deterministic silent archives apply;
278
+ // governed actions are deferred with an audit record pointing at wrapup.
279
279
  const { applied, results } = applyLocalFates(engine, sessionId, plan, localState, "silent-only");
280
280
  if (applied.length > 0) {
281
- logger.info(`auto-review local-fate (compact) [${sessionId}]: silent-archived ${applied.length} — ${assessment.rationale}`);
281
+ logger.info(`auto-review local-fate (${reason}) [${sessionId}]: silent-archived ${applied.length} — ${assessment.rationale}`);
282
282
  record({
283
283
  sessionId,
284
284
  reason,
285
285
  turnsSinceLastReview: turnsSinceFate,
286
286
  outcome: "approved",
287
- rationale: `fate (compact): ${assessment.rationale} (${applied.join("; ")})`,
287
+ rationale: `fate (${reason}): ${assessment.rationale} (${applied.join("; ")})`,
288
288
  refinementId: results.map((result) => result.id).join(","),
289
289
  });
290
290
  }
@@ -293,7 +293,7 @@ export async function runLocalFatePhase(ctx, engine, agent, config, state, reaso
293
293
  reason,
294
294
  turnsSinceLastReview: turnsSinceFate,
295
295
  outcome: "deferred",
296
- rationale: `fate (compact): ${plan.promotable.length} promotes, ${plan.splits.length} splits, ${plan.reviewArchives.length} review-archives deferred — run /evolve wrapup for a full session exit`,
296
+ rationale: `fate (${reason}): ${plan.promotable.length} promotes, ${plan.splits.length} splits, ${plan.reviewArchives.length} review-archives deferred — run /evolve wrapup for a full session exit`,
297
297
  });
298
298
  return;
299
299
  }
@@ -20,6 +20,13 @@ import type { EntrySource, HarnessEntry, HarnessScope, HarnessState, RefinementE
20
20
  export declare const MEMORY_AGENT_MAX_TURNS = 5;
21
21
  /** Bound one proposal so approval text and engine compensation stay reviewable. */
22
22
  export declare const MEMORY_AGENT_MAX_EDITS = 20;
23
+ /**
24
+ * System-prompt size budget (chars): the prompt ships on EVERY extractor
25
+ * turn (up to five per snapshot), so each added line is recurring token
26
+ * spend, not one-off text. Raise this cap only in the same commit that
27
+ * justifies the added lines (regression pressure beats silent bloat).
28
+ */
29
+ export declare const MEMORY_AGENT_SYSTEM_PROMPT_BUDGET_CHARS = 3000;
23
30
  /** The complete tool-name allowlist exposed by the memory agent. */
24
31
  export declare const MEMORY_AGENT_TOOL_NAMES: readonly ["memory_search", "memory_propose"];
25
32
  export type MemoryAgentToolName = (typeof MEMORY_AGENT_TOOL_NAMES)[number];
@@ -54,7 +61,7 @@ export interface MemoryApplicationResult {
54
61
  /** Provider-facing schemas for the closed memory-only tool set. */
55
62
  export declare const MEMORY_AGENT_TOOL_SCHEMAS: readonly ToolSchema[];
56
63
  /** Instructions for the dedicated loop; model text is always untrusted. */
57
- export declare const MEMORY_AGENT_SYSTEM_PROMPT = "You are the dedicated background memory extraction agent for a continual-evolution harness.\n\nYou receive only an incremental conversation checkpoint and a frozen manifest of existing memory entries. Your entire job is to decide whether this checkpoint contains a durable fact worth remembering. Never propose prompt notes, skills, subagent specs, code changes, or general conversation summaries.\n\nRules:\n- Use memory_search before creating when the manifest may already cover the topic. Update an existing entry instead of creating a near-duplicate.\n- Every memory entry contains exactly one fact. Classify it with metadata.memoryType = user | feedback | project | reference.\n- feedback and project memories must state the fact, Why it matters, and How to apply it next time.\n- Never invent a user preference. Ground every proposal in the supplied conversation evidence and existing memory.\n- Every edit must have kind=memory, an explicit targetScope, and a coherent blastRadius.\n- targetScope=local is session staging. project persists within the current repository. global persists across projects. project/global require human approval before application.\n- update/delete/archive must name an existing id from the same targetScope. Prefer archive over delete when the fact is obsolete but should remain restorable.\n- Do not remember: code structure or file paths re-readable from the repository; git history, diffs, commit hashes, or CI logs; content already written in project instruction files; temporary session state, one-off debugging trails, or current task progress; model-only speculation without conversation evidence.\n- You cannot read or write source files, call agents, use MCP, use the network, or mutate the harness directly. memory_propose is the only finish tool and never writes state itself.\n- If no durable memory is justified, call memory_propose with edits=[].\n- Always finish by calling memory_propose. Do not return unproposed prose.";
64
+ export declare const MEMORY_AGENT_SYSTEM_PROMPT = "You are the dedicated background memory extraction agent for a continual-evolution harness.\n\nYou receive only an incremental conversation checkpoint and a frozen manifest of existing memory entries. Your entire job is to decide whether this checkpoint contains a durable fact worth remembering. Never propose prompt notes, skills, subagent specs, code changes, or general conversation summaries.\n\nRules:\n- Use memory_search before creating when the manifest may already cover the topic. Update an existing entry instead of creating a near-duplicate.\n- Every memory entry contains exactly one fact. Classify it with metadata.memoryType = user | feedback | project | reference.\n- feedback and project memories must state the fact, Why it matters, and How to apply it next time.\n- Never invent a user preference. Ground every proposal in the supplied conversation evidence and existing memory.\n- Every edit must have kind=memory, an explicit targetScope, and a coherent blastRadius.\n- targetScope=local is session staging. project persists within the current repository. global persists across projects. project/global require human approval before application.\n- update/delete/archive must name an existing id from the same targetScope. Prefer archive over delete when the fact is obsolete but should remain restorable.\n- Do not remember: code structure or file paths re-readable from the repository; git history, diffs, commit hashes, or CI logs; content already written in project instruction files; temporary session state, one-off debugging trails, or current task progress; model-only speculation without conversation evidence.\n- Be specific enough to act on: prefer a fact that tells the next session what to DO over a summary of what happened. If the fact is obvious from the conversation itself with no future action attached, skip it.\n- Save only what is surprising or non-obvious: routine chatter, restated context, and facts with no bearing on future work are no-ops, not memories.\n- Granularity examples (vague \u2192 reject; sharp \u2192 save):\n - \"user communicates in Chinese\" \u2192 \"user reads English with effort \u2014 write memory and handoff content in Chinese prose\".\n - \"user works on the desktop project\" \u2192 \"desktop profile loads npm-installed plugin copies \u2014 source edits need a release before they take effect\".\n- You cannot read or write source files, call agents, use MCP, use the network, or mutate the harness directly. memory_propose is the only finish tool and never writes state itself.\n- If no durable memory is justified, call memory_propose with edits=[].\n- Always finish by calling memory_propose. Do not return unproposed prose.";
58
65
  /** Build the frozen memory manifest from a merged harness view. */
59
66
  export declare function buildMemoryManifest(state: HarnessState): MemoryManifestEntry[];
60
67
  /** Render a bounded manifest index; full bodies remain available through memory_search. */
@@ -15,6 +15,13 @@ import { validateBlastRadiusScope, validateEdit } from "./validate.js";
15
15
  export const MEMORY_AGENT_MAX_TURNS = 5;
16
16
  /** Bound one proposal so approval text and engine compensation stay reviewable. */
17
17
  export const MEMORY_AGENT_MAX_EDITS = 20;
18
+ /**
19
+ * System-prompt size budget (chars): the prompt ships on EVERY extractor
20
+ * turn (up to five per snapshot), so each added line is recurring token
21
+ * spend, not one-off text. Raise this cap only in the same commit that
22
+ * justifies the added lines (regression pressure beats silent bloat).
23
+ */
24
+ export const MEMORY_AGENT_SYSTEM_PROMPT_BUDGET_CHARS = 3000;
18
25
  /** The complete tool-name allowlist exposed by the memory agent. */
19
26
  export const MEMORY_AGENT_TOOL_NAMES = ["memory_search", "memory_propose"];
20
27
  /** Provider-facing schemas for the closed memory-only tool set. */
@@ -85,6 +92,11 @@ Rules:
85
92
  - targetScope=local is session staging. project persists within the current repository. global persists across projects. project/global require human approval before application.
86
93
  - update/delete/archive must name an existing id from the same targetScope. Prefer archive over delete when the fact is obsolete but should remain restorable.
87
94
  - Do not remember: code structure or file paths re-readable from the repository; git history, diffs, commit hashes, or CI logs; content already written in project instruction files; temporary session state, one-off debugging trails, or current task progress; model-only speculation without conversation evidence.
95
+ - Be specific enough to act on: prefer a fact that tells the next session what to DO over a summary of what happened. If the fact is obvious from the conversation itself with no future action attached, skip it.
96
+ - Save only what is surprising or non-obvious: routine chatter, restated context, and facts with no bearing on future work are no-ops, not memories.
97
+ - Granularity examples (vague → reject; sharp → save):
98
+ - "user communicates in Chinese" → "user reads English with effort — write memory and handoff content in Chinese prose".
99
+ - "user works on the desktop project" → "desktop profile loads npm-installed plugin copies — source edits need a release before they take effect".
88
100
  - You cannot read or write source files, call agents, use MCP, use the network, or mutate the harness directly. memory_propose is the only finish tool and never writes state itself.
89
101
  - If no durable memory is justified, call memory_propose with edits=[].
90
102
  - Always finish by calling memory_propose. Do not return unproposed prose.`;
package/lib/notify.js CHANGED
@@ -53,16 +53,17 @@ export function buildMemoryReceipt(receipt) {
53
53
  if (receipt.outcome === "noop") {
54
54
  return `🧠 记忆提取:本轮无可沉淀的持久事实(no-op,${stats}),未写入任何条目。`;
55
55
  }
56
- if (receipt.outcome === "declined" || (receipt.results ?? []).length === 0) {
57
- const scopes = (receipt.declinedScopes ?? []).join("、") || "持久化作用域";
56
+ const results = receipt.results ?? [];
57
+ const declinedScopes = receipt.declinedScopes ?? [];
58
+ if (receipt.outcome === "declined" || results.length === 0) {
59
+ const scopes = declinedScopes.join("、") || "持久化作用域";
58
60
  return `🧠 记忆提取:${scopes}的写入未获批准,已跳过(${stats}),未写入任何条目。`;
59
61
  }
60
- const results = receipt.results ?? [];
61
62
  const applied = results.flatMap((result) => result.appliedEdits.filter((edit) => edit.applied));
62
63
  const lines = applied.slice(0, 12).map((edit) => `- 记忆「${edit.title ?? edit.id}」(${edit.id})`);
63
64
  if (applied.length > 12)
64
65
  lines.push(`- …另有 ${applied.length - 12} 条(见 /evolve history)`);
65
- const declined = (receipt.declinedScopes ?? []).length > 0 ? `\n另有作用域${(receipt.declinedScopes ?? []).join("、")}未经批准(已跳过)。` : "";
66
+ const declined = declinedScopes.length > 0 ? `\n另有作用域${declinedScopes.join("、")}未经批准(已跳过)。` : "";
66
67
  const rollbacks = [...new Set(results.map((result) => result.id))].map((id) => `/evolve rollback ${id}`).join(";");
67
68
  return [
68
69
  `🧠 记忆提取:本轮沉淀 ${applied.length} 条记忆(${results.length} 个作用域批次,${stats}):`,
package/lib/planner.d.ts CHANGED
@@ -11,7 +11,7 @@ import type { Agent } from "@deepseek-ai/dsh-agent";
11
11
  import type { HarnessState, RefinementProposal, RefinementResult } from "./types.js";
12
12
  import { type PrefixCacheOptions } from "./prefix-cache.js";
13
13
  import { type TokenUsageTarget } from "./token-usage.js";
14
- export declare const PLANNER_SYSTEM_PROMPT = "You are the /evolve continual harness subsystem.\n\nYour job is to improve the editable continual harness state. Instead of\nsummarizing the conversation you emit precise Create, Update, or Delete edits\nto reusable state: prompt notes, memories, skills, and subagent specs.\n\nRules:\n- The base system prompt is immutable and MUST NOT be rewritten (never edit id \"base_system_prompt\").\n- Prefer small evidence-backed edits. If no useful edit is justified, return an empty edits array.\n- prompt = narrow behavioral policy addendums; memory = ONE FACT per entry, typed via metadata.memoryType\n- (user = who the user is; feedback = a pitfall/correction or confirmed approach, MUST carry Why + How to apply;\n- project = work goal/constraint not derivable from the repo; reference = URL/dashboard/ticket pointer).\n- Memory rules: one fact per entry (title = one-line retrieval hook, content = the fact alone);\n- never save what the repo already records (code structure, past fixes, git history, AGENTS.md) or what only\n- matters to this conversation \u2014 save the non-obvious point instead; before creating, update the existing\n- entry when one already covers the fact (creates carry metadata {memoryType} or they are rejected);\n- skill = repeatable procedures (must carry a python reference {type:\"python\", import, callable}\n and an arguments object); subagent = reusable delegation roles.\n- Skill entries are authored to the DSH skill quality standard\n (skill-creator, distilled from the official deepseek-harness 11 skills;\n the full facts are provided in the <skill_quality_standard> block below):\n only for a REAL trigger scenario grounded in the trajectory\n (who, in what real task, what signal) \u2014 never invent one to pad the store;\n never duplicate the official 11 skills or existing entries; content is a\n SKILL.md document (frontmatter routing with \"use when / do not use when\"\n description, boundary declaration, prerequisites and exclusions, layered\n information, verifiable completion criteria). Self-check every proposed\n skill against the 7 structural features and state the result in its\n reason field.\n- Repeated multi-step workflows (session start/end routines, recurring\n wrap-up or handoff procedures) may be proposed as guidance skills:\n kind=skill, skill_kind=\"guidance\", content = a SKILL.md document (no\n python reference \u2014 executable skills keep requiring reference +\n arguments). Only propose with repeated evidence in the trajectory, never\n for one-off flows. Guidance skills materialize as discoverable SKILL.md\n files under <skillsRoot>/<kebab-name>/SKILL.md and are always offered to\n the user for a decision before they land.\n- Local edits are session-scoped; global edits persist across sessions.\n- Gap C2: EVERY edit MUST include a \"blastRadius\" field indicating how broadly\n the edit applies: \"general\" (cross-project tactical rule, applies everywhere),\n \"project\" (valid for the current project/repo), or \"session\" (one-off,\n specific to this session's context). The review gate validates that\n local-scope edits use \"session\" or \"project\", and global-scope edits use\n \"general\" or \"project\". When in doubt, prefer narrower blast radius.\n- Ground every edit in evidence: the session trajectory (recent direct user\n messages) is provided when available; prefer edits backed by it over\n speculation, and never invent preferences the user did not express.\n- Stale entries (superseded by newer ones, never referenced in recent\n trajectories, obsolete facts): propose action \"archive\" instead of\n \"delete\" \u2014 archive hides the entry from injection while keeping its data\n restorable; it requires only kind + id.\n- Output JSON only, exactly this shape:\n{\n \"summary\": \"one sentence\",\n \"rationale\": \"why these edits are justified by the evidence\",\n \"expectedOutcome\": \"what should improve and how to validate it\",\n \"edits\": [\n {\n \"action\": \"create|update|delete\",\n \"kind\": \"prompt|memory|skill|subagent\",\n \"id\": \"stable id for update/delete, optional for create\",\n \"title\": \"required for create/update except delete\",\n \"content\": \"required for create/update except delete\",\n \"path\": \"optional grouping path\",\n \"reference\": {\"type\":\"python\",\"import\":\"pkg.mod\",\"callable\":\"fn\"} ,\n \"arguments\": {\"name\": {\"type\":\"string\",\"required\":true,\"description\":\"...\"}},\n \"skill_kind\": \"executable|guidance (optional; skill kind only \u2014 guidance = SKILL.md document without reference)\",\n \"metadata\": {},\n \"reason\": \"why this edit is useful\",\n \"blastRadius\": \"general|project|session\"\n }\n ]\n}";
14
+ export declare const PLANNER_SYSTEM_PROMPT = "You are the /evolve continual harness subsystem.\n\nYour job is to improve the editable continual harness state. Instead of\nsummarizing the conversation you emit precise Create, Update, or Delete edits\nto reusable state: prompt notes, memories, skills, and subagent specs.\n\nRules:\n- The base system prompt is immutable and MUST NOT be rewritten (never edit id \"base_system_prompt\").\n- Prefer small evidence-backed edits. If no useful edit is justified, return an empty edits array.\n- Repeated evidence is mandatory for prompt/skill/subagent proposals: the same\n pattern must recur at least twice in the trajectory. A single one-off\n interaction never justifies a prompt, skill, or subagent edit \u2014 return an\n empty edits array with a rationale instead. Session-scoped local prompt\n landings converge on /evolve wrapup human review; the turn pipeline must\n not silently persist one-off abstractions.\n- prompt = narrow behavioral policy addendums; memory = ONE FACT per entry, typed via metadata.memoryType\n- (user = who the user is; feedback = a pitfall/correction or confirmed approach, MUST carry Why + How to apply;\n- project = work goal/constraint not derivable from the repo; reference = URL/dashboard/ticket pointer).\n- Memory rules: one fact per entry (title = one-line retrieval hook, content = the fact alone);\n- never save what the repo already records (code structure, past fixes, git history, AGENTS.md) or what only\n- matters to this conversation \u2014 save the non-obvious point instead; before creating, update the existing\n- entry when one already covers the fact (creates carry metadata {memoryType} or they are rejected);\n- skill = repeatable procedures (must carry a python reference {type:\"python\", import, callable}\n and an arguments object); subagent = reusable delegation roles.\n- Skill entries are authored to the DSH skill quality standard\n (skill-creator, distilled from the official deepseek-harness 11 skills;\n the full facts are provided in the <skill_quality_standard> block below):\n only for a REAL trigger scenario grounded in the trajectory\n (who, in what real task, what signal) \u2014 never invent one to pad the store;\n never duplicate the official 11 skills or existing entries; content is a\n SKILL.md document (frontmatter routing with \"use when / do not use when\"\n description, boundary declaration, prerequisites and exclusions, layered\n information, verifiable completion criteria). Self-check every proposed\n skill against the 7 structural features and state the result in its\n reason field.\n- Repeated multi-step workflows (session start/end routines, recurring\n wrap-up or handoff procedures) may be proposed as guidance skills:\n kind=skill, skill_kind=\"guidance\", content = a SKILL.md document (no\n python reference \u2014 executable skills keep requiring reference +\n arguments). Only propose with repeated evidence in the trajectory, never\n for one-off flows. Guidance skills materialize as discoverable SKILL.md\n files under <skillsRoot>/<kebab-name>/SKILL.md and are always offered to\n the user for a decision before they land.\n- Local edits are session-scoped; global edits persist across sessions.\n- Gap C2: EVERY edit MUST include a \"blastRadius\" field indicating how broadly\n the edit applies: \"general\" (cross-project tactical rule, applies everywhere),\n \"project\" (valid for the current project/repo), or \"session\" (one-off,\n specific to this session's context). The review gate validates that\n local-scope edits use \"session\" or \"project\", and global-scope edits use\n \"general\" or \"project\". When in doubt, prefer narrower blast radius.\n- Ground every edit in evidence: the session trajectory (recent direct user\n messages) is provided when available; prefer edits backed by it over\n speculation, and never invent preferences the user did not express.\n- Stale entries (superseded by newer ones, never referenced in recent\n trajectories, obsolete facts): propose action \"archive\" instead of\n \"delete\" \u2014 archive hides the entry from injection while keeping its data\n restorable; it requires only kind + id.\n- Output JSON only, exactly this shape:\n{\n \"summary\": \"one sentence\",\n \"rationale\": \"why these edits are justified by the evidence\",\n \"expectedOutcome\": \"what should improve and how to validate it\",\n \"edits\": [\n {\n \"action\": \"create|update|delete\",\n \"kind\": \"prompt|memory|skill|subagent\",\n \"id\": \"stable id for update/delete, optional for create\",\n \"title\": \"required for create/update except delete\",\n \"content\": \"required for create/update except delete\",\n \"path\": \"optional grouping path\",\n \"reference\": {\"type\":\"python\",\"import\":\"pkg.mod\",\"callable\":\"fn\"} ,\n \"arguments\": {\"name\": {\"type\":\"string\",\"required\":true,\"description\":\"...\"}},\n \"skill_kind\": \"executable|guidance (optional; skill kind only \u2014 guidance = SKILL.md document without reference)\",\n \"metadata\": {},\n \"reason\": \"why this edit is useful\",\n \"blastRadius\": \"general|project|session\"\n }\n ]\n}";
15
15
  export interface PlanOptions {
16
16
  agent: Agent;
17
17
  state: HarnessState;
package/lib/planner.js CHANGED
@@ -14,6 +14,12 @@ to reusable state: prompt notes, memories, skills, and subagent specs.
14
14
  Rules:
15
15
  - The base system prompt is immutable and MUST NOT be rewritten (never edit id "base_system_prompt").
16
16
  - Prefer small evidence-backed edits. If no useful edit is justified, return an empty edits array.
17
+ - Repeated evidence is mandatory for prompt/skill/subagent proposals: the same
18
+ pattern must recur at least twice in the trajectory. A single one-off
19
+ interaction never justifies a prompt, skill, or subagent edit — return an
20
+ empty edits array with a rationale instead. Session-scoped local prompt
21
+ landings converge on /evolve wrapup human review; the turn pipeline must
22
+ not silently persist one-off abstractions.
17
23
  - prompt = narrow behavioral policy addendums; memory = ONE FACT per entry, typed via metadata.memoryType
18
24
  - (user = who the user is; feedback = a pitfall/correction or confirmed approach, MUST carry Why + How to apply;
19
25
  - project = work goal/constraint not derivable from the repo; reference = URL/dashboard/ticket pointer).
@@ -85,7 +91,7 @@ export async function planWithLlm(ctx, options) {
85
91
  }
86
92
  const scopeInstruction = options.global
87
93
  ? "Requested scope: global. Only propose stable cross-session lessons, durable preferences, reusable skills/subagents, or explicitly project-qualified facts."
88
- : "Requested scope: local. Prefer session-scoped edits for current task progress; global entries are read-only context — do not propose update/delete for them.";
94
+ : "Requested scope: local. Propose session-scoped edits only when the trajectory shows repeated evidence (>=2 occurrences of the same pattern); a single one-off interaction returns an empty edits array. Global entries are read-only context — do not propose update/delete for them.";
89
95
  // Ground the plan in the caller's session: Route A carries the context
90
96
  // as a session-derived message prefix (cache-eligible on providers with
91
97
  // prompt caching) and drops the auto-extracted trajectory block; Route B
@@ -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");
@@ -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/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.8.0",
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
  }