dsh-continual-evolve 0.6.0 → 0.7.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,7 @@
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-573%20passing-brightgreen)]()
10
+ [![Tests](https://img.shields.io/badge/tests-600%20passing-brightgreen)]()
11
11
 
12
12
  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
13
 
@@ -17,7 +17,8 @@ Continual self-evolution for [DeepSeek Harness](https://github.com/deepseek-ai/d
17
17
 
18
18
  Agents accumulate reusable experience (repeated failures, durable facts, reusable procedures) and forget it next session. This plugin turns that experience into first-class state:
19
19
 
20
- - **Local scope** per session; **global scope** across sessions with merge semantics — plus mechanical promotion guards so only portable, substantial, non-duplicate knowledge reaches global
20
+ - **Three scopes** with merge semantics (global < project < local): **local** per-session staging, **project** per-workspace cross-session store, **global** cross-project — plus mechanical promotion guards so only portable, substantial, non-duplicate knowledge reaches global
21
+ - **Typed one-fact memories**: every memory entry carries a recall type (`user | feedback | project | reference`); pitfalls (`feedback`) must include Why + How to apply
21
22
  - **Deterministic rollback**: inverse edits generated from applied results — no LLM re-guessing
22
23
  - **Benchmark loop**: candidate refinements are evaluated against frozen cases by a separate scorer before acceptance (rubric encrypted at rest)
23
24
  - **Store hygiene**: `/evolve consolidate` turns write-time conflict hints and zero-use staleness into one approved, fully reversible batch of archives — with `merge`, near-duplicate content folds into the surviving original
@@ -26,8 +27,8 @@ Agents accumulate reusable experience (repeated failures, durable facts, reusabl
26
27
 
27
28
  1. **Sediment** — the model creates entries via `evolve_add`, or the automatic review gate proposes them from the session trajectory (turn-interval + compaction checkpoints).
28
29
  2. **Guard** — code-enforced validation: edit schema, blast-radius/scope coherence, and the promotion policy (project-scoped markers, thin content, near-duplicate detection, credential screening keep the global store clean — secrets are rejected at every write sink, including mount materialization). Global creates that near-duplicate an existing entry are rejected at write time (≥0.8 similarity); moderate overlaps carry a `conflictHint` for later consolidation.
29
- 3. **Approve** — global writes require explicit human approval; local-fate proposals are consulted before they land.
30
- 4. **Apply & inject** — atomic apply with snapshot + audit event. Prompt notes and delegation specs inject into the system prompt (capped, relevance-ranked, contradicted entries demoted, zero tokens when empty); memories/skills appear as a capped directory index.
30
+ 3. **Approve** — global and project writes require explicit human approval; local-fate proposals are consulted before they land.
31
+ 4. **Apply & inject** — atomic apply with snapshot + audit event. Prompt notes and delegation specs inject into the system prompt (capped, relevance-ranked, contradicted entries demoted, zero tokens when empty); memories/skills appear as a relevance-ordered capped directory index (`- [memory:type:id] title` hooks, full text one `evolve_list` away).
31
32
  5. **Validate & roll back** — benchmarks score candidates against frozen cases; rejected candidates roll back deterministically and are captured as draft regression cases (`auto_regression` benchmark).
32
33
 
33
34
  ## Install
@@ -49,7 +50,7 @@ Commands (in-session):
49
50
  | Command | Effect |
50
51
  |---|---|
51
52
  | `/evolve` | help + current local store |
52
- | `/evolve list · history · rollback <id>` | inspect and revert (add `global` for the cross-session store) |
53
+ | `/evolve list · history · rollback <id>` | inspect and revert (add `project` for this project's store, `global` for the cross-project store) |
53
54
  | `/evolve plan [msg]` | run the LLM planner against the store |
54
55
  | `/evolve wrapup` | assess this session's local entries: promote / archive / keep |
55
56
  | `/evolve archive · unarchive · demote <id>` | hide from injection (data kept, restorable) — `demote` targets global noise |
@@ -65,7 +66,7 @@ Model tools: `evolve_list / add / update / delete / rollback`.
65
66
 
66
67
  For third-party consumers: every applied evolution (gate or manual) appends a structured `evolve_complete` event to `reviews.jsonl` (`src/evolve-event.ts` defines the shape) alongside the human-readable audit records.
67
68
 
68
- Injection shape: prompt notes and delegation specs inject with content (≤6/kind × 180 chars, relevance-ranked). Memories and skills appear as a directory index (`[kind:id] title`, capped at 15 lines with a fold counter) — full text via `evolve_list`. Empty store = zero injected tokens.
69
+ Injection shape: prompt notes and delegation specs inject with content (≤6/kind × 180 chars, relevance-ranked). Memories and skills appear as a relevance-ordered directory index (`[memory:type:id] title` hooks, capped at 15 lines with a fold counter) — full text via `evolve_list`. Empty store = zero injected tokens.
69
70
 
70
71
  ## Configuration
71
72
 
@@ -77,7 +78,7 @@ Injection shape: prompt notes and delegation specs inject with content (≤6/kin
77
78
  | `maxReviewInputChars` | `40000` | trajectory slice handed to the gate |
78
79
  | `reviewBudgetTokens` | `4096` | output budget for the gate call |
79
80
  | `notifyOnAutoReview` | `true` | visible follow-up notice after an applied gate run |
80
- | `requireGlobalApproval` | `true` | global edits ask for explicit approval |
81
+ | `requireGlobalApproval` | `true` | global and project edits ask for explicit approval |
81
82
  | `localFate` | `true` | gate audits local entries and proposes promote/archive (consulted, never silent) |
82
83
  | `fateIntervalTurns` | follows `reviewIntervalTurns` | minimum turns between fate assessments |
83
84
  | `goalBlockedWrapupTurns` | `3` | consecutive blocked-goal gate runs trigger one fate assessment (`0` disables) |
@@ -105,7 +106,7 @@ Example profile patch:
105
106
 
106
107
  ```bash
107
108
  pnpm install && pnpm build # deps + tsc -> lib/
108
- pnpm test # vitest (573 tests)
109
+ pnpm test # vitest (600 tests)
109
110
  pnpm test:coverage # v8 coverage, thresholds enforced in CI
110
111
  pnpm lint # oxlint src test
111
112
  ```
@@ -114,7 +115,7 @@ Project layout:
114
115
 
115
116
  ```
116
117
  ├── src/ # engine, tools, commands, gate, fate, benchmark, usage…
117
- ├── test/ # vitest suites (36 files)
118
+ ├── test/ # vitest suites (37 files)
118
119
  ├── lib/ # build output (tsc)
119
120
  ├── docs/
120
121
  │ ├── design.md # full design doc (hardening matrix)
package/README.zh.md CHANGED
@@ -7,7 +7,7 @@
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-573%20passing-brightgreen)]()
10
+ [![Tests](https://img.shields.io/badge/tests-600%20passing-brightgreen)]()
11
11
 
12
12
  [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的持续自进化插件:一套**版本化、可审计、可回滚**的 harness 状态层——提示词补充、记忆、技能、子代理规格——从会话轨迹中沉淀而来。
13
13
 
@@ -17,7 +17,8 @@
17
17
 
18
18
  Agent 在每个会话里积累可复用经验(重复失败、持久事实、可复用流程),下个会话就忘掉。本插件把这些经验变成一等公民的持久状态:
19
19
 
20
- - **local 会话级 / global 跨会话** 双作用域与合并语义——配合机械化晋升守卫,只有可携带、有分量、非重复的知识才能进全局
20
+ - **三作用域**与合并语义(global < project < local):**local** 本会话暂存、**project** 本项目跨会话库、**global** 跨项目——配合机械化晋升守卫,只有可携带、有分量、非重复的知识才能进全局
21
+ - **类型化单条记忆**:每条 memory 带召回类型(`user | feedback | project | reference`);踩坑(`feedback`)必须含 Why + How to apply
21
22
  - **确定性回滚**:逆操作编辑由已应用结果生成——不靠 LLM 重新猜测
22
23
  - **benchmark 闭环**:候选沉淀先经冻结用例 + 独立评分者评估再接受(rubric 加密落盘)
23
24
  - **store 卫生**:`/evolve consolidate` 把写入时冲突提示与零使用陈旧条目变成一次批准、完全可逆的批量归档——加 `merge` 可将近重复内容并入幸存原条目
@@ -26,8 +27,8 @@ Agent 在每个会话里积累可复用经验(重复失败、持久事实、
26
27
 
27
28
  1. **沉淀**——模型经 `evolve_add` 创建条目,或自动 review 门禁从会话轨迹提议(回合间隔 + 压缩检查点)。
28
29
  2. **守卫**——代码强制校验:编辑 schema、blast-radius 与作用域一致性、晋升政策(项目专属标记 / 过薄内容 / 近似重复检测 / 凭据筛查保持全局库干净——密钥类内容在所有写入出口被拒,含 mount 物化)。全局 create 与既有条目高度相似(≥0.8)时写入即拒;中等重叠带 `conflictHint` 供后续合并。
29
- 3. **审批**——全局写入需明确人工批准;local 归宿提议先征询后落地。
30
- 4. **应用与注入**——原子应用带快照与审计事件。prompt 补充与委派规格注入系统提示词(封顶、按相关性排序、被证伪条目降权、空 store 零 token);memory/skill 以目录索引出现。
30
+ 3. **审批**——全局与项目写入需明确人工批准;local 归宿提议先征询后落地。
31
+ 4. **应用与注入**——原子应用带快照与审计事件。prompt 补充与委派规格注入系统提示词(封顶、按相关性排序、被证伪条目降权、空 store 零 token);memory/skill 以按相关性排序的目录索引出现(`[memory:type:id] 标题`钩子)。
31
32
  5. **验证与回滚**——benchmark 用冻结用例为候选打分;被拒候选确定性回滚,并自动沉淀为 draft 回归用例(`auto_regression` 基准)。
32
33
 
33
34
  ## 安装
@@ -49,7 +50,7 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
49
50
  | 命令 | 效果 |
50
51
  |---|---|
51
52
  | `/evolve` | 帮助 + 当前 local store |
52
- | `/evolve list · history · rollback <id>` | 查看与回滚(加 `global` 操作跨会话库) |
53
+ | `/evolve list · history · rollback <id>` | 查看与回滚(加 `project` 操作本项目库,`global` 操作跨项目库) |
53
54
  | `/evolve plan [msg]` | 对 store 运行 LLM 规划器 |
54
55
  | `/evolve wrapup` | 收尾本会话 local 条目:晋升 / 归档 / 保留 |
55
56
  | `/evolve archive · unarchive · demote <id>` | 从注入中隐藏(数据保留可恢复)——`demote` 针对全局噪声 |
@@ -65,7 +66,7 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
65
66
 
66
67
  第三方消费:每次进化落地(门禁或手动)都会向 `reviews.jsonl` 追加结构化 `evolve_complete` 事件(shape 见 `src/evolve-event.ts`),与人类可读的审计记录并存。
67
68
 
68
- 注入形态:prompt 补充与委派规格带内容注入(每 kind ≤6 条 × 180 字符,按相关性排序)。memory/skill 以目录索引出现(`[kind:id] 标题`,15 行封顶 + 折叠计数行)——全文经 `evolve_list` 获取。空 store = 零注入 token。
69
+ 注入形态:prompt 补充与委派规格带内容注入(每 kind ≤6 条 × 180 字符,按相关性排序)。memory/skill 以按相关性排序的目录索引出现(`[memory:type:id] 标题`钩子,15 行封顶 + 折叠计数行)——全文经 `evolve_list` 获取。空 store = 零注入 token。
69
70
 
70
71
  ## 配置
71
72
 
@@ -77,7 +78,7 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
77
78
  | `maxReviewInputChars` | `40000` | 交给门禁的轨迹切片 |
78
79
  | `reviewBudgetTokens` | `4096` | 门禁调用输出预算 |
79
80
  | `notifyOnAutoReview` | `true` | 门禁应用后发可见跟进通知 |
80
- | `requireGlobalApproval` | `true` | 全局写入需明确批准 |
81
+ | `requireGlobalApproval` | `true` | 全局与项目写入需明确批准 |
81
82
  | `localFate` | `true` | 门禁审计本地条目并提议晋升/归档(先征询,绝不静默) |
82
83
  | `fateIntervalTurns` | 跟随 `reviewIntervalTurns` | 归宿评估的最小回合间隔 |
83
84
  | `goalBlockedWrapupTurns` | `3` | 连续阻塞目标的门禁轮数触发一次归宿评估(`0` 关闭) |
@@ -105,7 +106,7 @@ profile patch 示例:
105
106
 
106
107
  ```bash
107
108
  pnpm install && pnpm build # 依赖 + tsc -> lib/
108
- pnpm test # vitest(573 例)
109
+ pnpm test # vitest(600 例)
109
110
  pnpm test:coverage # v8 覆盖率,CI 强制阈值
110
111
  pnpm lint # oxlint src test
111
112
  ```
@@ -114,7 +115,7 @@ pnpm lint # oxlint src test
114
115
 
115
116
  ```
116
117
  ├── src/ # 引擎、工具、命令、门禁、fate、benchmark、usage…
117
- ├── test/ # vitest 测试套件(36 个文件)
118
+ ├── test/ # vitest 测试套件(37 个文件)
118
119
  ├── lib/ # 构建产物(tsc)
119
120
  ├── docs/
120
121
  │ ├── design.md # 完整设计文档(硬化矩阵)
package/lib/auto.d.ts CHANGED
@@ -103,14 +103,16 @@ export declare function parseReviewModel(reviewModel: string | undefined, fallba
103
103
  } | undefined;
104
104
  /**
105
105
  * The state view the gate and planner judge: the session's local entries
106
- * merged with the global store, each entry carrying its real scope. Without
107
- * the global half the gate cannot see that a topic is already covered
108
- * cross-session and happily re-sediments a local duplicate of it.
106
+ * merged with the project and global stores, each entry carrying its real
107
+ * scope. Without the global half the gate cannot see that a topic is already
108
+ * covered cross-session and happily re-sediments a local duplicate of it.
109
109
  *
110
110
  * The merged view is read-only context — applying still targets the raw
111
111
  * local state (baseline checks compare local entries only).
112
112
  */
113
- export declare function loadGateHarnessView(engine: EvolutionEngine, sessionId: string): HarnessState;
113
+ export declare function loadGateHarnessView(engine: EvolutionEngine, sessionId: string, opts?: {
114
+ projectKey?: string;
115
+ }): HarnessState;
114
116
  /**
115
117
  * D3 (goal blocked → wrap-up coupling, reverse direction): count consecutive
116
118
  * gate runs whose goal is in phase "blocked"; when the streak reaches
package/lib/auto.js CHANGED
@@ -25,6 +25,7 @@ import { notifyAutoReview } from "./notify.js";
25
25
  import { runLocalFatePhase } from "./fate.js";
26
26
  import { entrySourceOf } from "./source.js";
27
27
  import { mergeHarnessStates } from "./state.js";
28
+ import { projectKeyOf } from "./project.js";
28
29
  import { questionServiceOf } from "./approval.js";
29
30
  import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
30
31
  import { captureAutoCase } from "./autocase.js";
@@ -153,15 +154,16 @@ function stateFor(map, sessionId) {
153
154
  }
154
155
  /**
155
156
  * The state view the gate and planner judge: the session's local entries
156
- * merged with the global store, each entry carrying its real scope. Without
157
- * the global half the gate cannot see that a topic is already covered
158
- * cross-session and happily re-sediments a local duplicate of it.
157
+ * merged with the project and global stores, each entry carrying its real
158
+ * scope. Without the global half the gate cannot see that a topic is already
159
+ * covered cross-session and happily re-sediments a local duplicate of it.
159
160
  *
160
161
  * The merged view is read-only context — applying still targets the raw
161
162
  * local state (baseline checks compare local entries only).
162
163
  */
163
- export function loadGateHarnessView(engine, sessionId) {
164
- return mergeHarnessStates(engine.load("global", undefined), engine.load("local", sessionId));
164
+ export function loadGateHarnessView(engine, sessionId, opts) {
165
+ const projectState = opts?.projectKey ? engine.load("project", opts.projectKey) : undefined;
166
+ return mergeHarnessStates(engine.load("global", undefined), engine.load("local", sessionId), projectState ? { projectState } : undefined);
165
167
  }
166
168
  /**
167
169
  * One gate run = review phase + local-fate phase (#11 P2). The review phase
@@ -239,7 +241,8 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
239
241
  // can recognize topics already covered globally and decline duplicates;
240
242
  // applying still targets the raw local store.
241
243
  const localState = engine.load("local", sessionId);
242
- const harnessState = loadGateHarnessView(engine, sessionId);
244
+ const projectKey = projectKeyOf(agent);
245
+ const harnessState = loadGateHarnessView(engine, sessionId, projectKey ? { projectKey } : undefined);
243
246
  const history = engine.history("local", sessionId);
244
247
  // Gap C1: resolve optional review model override.
245
248
  const reviewRoute = parseReviewModel(config.reviewModel, agent.options.provider);
package/lib/command.js CHANGED
@@ -13,11 +13,12 @@ import { executeGoalCommand } from "./goal-command.js";
13
13
  import { executeMountCommand, executeUnmountCommand } from "./mount-command.js";
14
14
  import { executeBenchmarkCommand } from "./benchmark-command.js";
15
15
  import { executeWrapupCommand } from "./wrapup-command.js";
16
+ import { projectKeyOf } from "./project.js";
16
17
  import { loadUsage } from "./usage.js";
17
18
  import { planConsolidation } from "./consolidate.js";
18
19
  const USAGE = `Usage:
19
20
  /evolve show this help and the current local store
20
- /evolve list [global] list entries (add "global" for the cross-session store)
21
+ /evolve list [project|global] list entries (default local staging; "project" = this project's cross-session store, "global" = cross-project store)
21
22
  /evolve history [global] show applied refinements (rollback ids)
22
23
  /evolve rollback <id> [global] deterministically revert a refinement
23
24
  /evolve plan [msg] run the LLM planner against the current store
@@ -52,8 +53,29 @@ function scopeArg(tokens) {
52
53
  if (tokens[0] === "global") {
53
54
  return { scope: "global", rest: tokens.slice(1) };
54
55
  }
56
+ if (tokens[0] === "project") {
57
+ return { scope: "project", rest: tokens.slice(1) };
58
+ }
55
59
  return { scope: "local", rest: tokens };
56
60
  }
61
+ /**
62
+ * The store id a scope reads/writes from the human command: live session id
63
+ * for local, derived project key for project, undefined for global. Throws
64
+ * for project when the session cwd is unavailable.
65
+ */
66
+ function storeIdForCommand(scope, invocation) {
67
+ if (scope === "local") {
68
+ return invocation.agent.id;
69
+ }
70
+ if (scope === "project") {
71
+ const key = projectKeyOf(invocation.agent);
72
+ if (!key) {
73
+ throw new Error("project scope needs the session cwd (unavailable here) — use local or global instead");
74
+ }
75
+ return key;
76
+ }
77
+ return undefined;
78
+ }
57
79
  /**
58
80
  * Tokenize a command's raw input with shell-like quoting:
59
81
  * - a `#` outside quotes starts a comment (rest of the line is dropped);
@@ -129,11 +151,11 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
129
151
  return success(`${USAGE}\n\n${formatHarnessStateForPrompt(engine.load("local", sessionId))}`);
130
152
  case "list": {
131
153
  const { scope } = scopeArg(rest);
132
- return success(formatHarnessStateForPrompt(engine.load(scope, sessionId)));
154
+ return success(formatHarnessStateForPrompt(engine.load(scope, storeIdForCommand(scope, invocation))));
133
155
  }
134
156
  case "history": {
135
157
  const { scope } = scopeArg(rest);
136
- const history = engine.history(scope, sessionId);
158
+ const history = engine.history(scope, storeIdForCommand(scope, invocation));
137
159
  return success(historyForPrompt(history) || "(no refinements yet)");
138
160
  }
139
161
  case "rollback": {
@@ -142,7 +164,7 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
142
164
  if (!id) {
143
165
  return error(`rollback requires a refinement id.\n${USAGE}`);
144
166
  }
145
- const result = engine.rollback(scope, sessionId, id);
167
+ const result = engine.rollback(scope, storeIdForCommand(scope, invocation), id);
146
168
  return success(renderResult(result));
147
169
  }
148
170
  case "archive":
@@ -154,9 +176,9 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
154
176
  return error(`${sub} requires an entry id.\n${USAGE}`);
155
177
  }
156
178
  if (sub === "demote") {
157
- return demoteEntry(engine, id, sessionId);
179
+ return demoteEntry(engine, id, sessionId, projectKeyOf(invocation.agent));
158
180
  }
159
- const state = engine.load(scope, sessionId);
181
+ const state = engine.load(scope, storeIdForCommand(scope, invocation));
160
182
  const found = findEntryById(state, id);
161
183
  if (!found) {
162
184
  return error(`entry ${id} not found in the ${scope} store`);
@@ -170,7 +192,7 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
170
192
  delete metadata[ARCHIVED_AT_KEY];
171
193
  }
172
194
  const archived = sub === "archive";
173
- const result = engine.apply(scope, sessionId, {
195
+ const result = engine.apply(scope, storeIdForCommand(scope, invocation), {
174
196
  summary: `${archived ? "Archive" : "Unarchive"} entry ${kind}:${id}`,
175
197
  rationale: "Human-invoked archive/unarchive via the /evolve command.",
176
198
  expectedOutcome: `Entry ${archived ? "is hidden from injection (data kept, restorable)" : "is injected again"}.`,
@@ -260,8 +282,8 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
260
282
  if (!path) {
261
283
  return error(`export requires an output path.\n${USAGE}`);
262
284
  }
263
- const state = engine.load(scope, sessionId);
264
- const history = engine.history(scope, sessionId);
285
+ const state = engine.load(scope, storeIdForCommand(scope, invocation));
286
+ const history = engine.history(scope, storeIdForCommand(scope, invocation));
265
287
  const payload = {
266
288
  version: 1,
267
289
  scope,
@@ -293,7 +315,7 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
293
315
  },
294
316
  refinements: Array.isArray(payload["refinements"]) ? payload["refinements"] : [],
295
317
  };
296
- const paths = storePaths(engine.baseDir, scope, sessionId);
318
+ const paths = storePaths(engine.baseDir, scope, storeIdForCommand(scope, invocation));
297
319
  saveHarnessState(paths.stateDir, state);
298
320
  if (Array.isArray(payload["history"])) {
299
321
  for (const result of payload["history"]) {
@@ -307,8 +329,8 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
307
329
  case "plan": {
308
330
  const { scope, rest: after } = scopeArg(rest);
309
331
  const instructions = after.length > 0 ? after.join(" ") : undefined;
310
- const state = engine.load(scope, sessionId);
311
- const history = engine.history(scope, sessionId);
332
+ const state = engine.load(scope, storeIdForCommand(scope, invocation));
333
+ const history = engine.history(scope, storeIdForCommand(scope, invocation));
312
334
  const proposal = await planWithLlm(ctx, {
313
335
  agent: invocation.agent,
314
336
  state,
@@ -319,10 +341,10 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
319
341
  // skill-creator template facts (fallback: builtin guide).
320
342
  skillsRoot: join(engine.baseDir, "skills"),
321
343
  });
322
- if (scope === "global" && opts.requireGlobalApproval && proposal.edits.length > 0) {
323
- await requireGlobalApproval(ctx, invocation.agent, invocation.signal, `/evolve plan global 将应用 ${proposal.edits.length} 条编辑到跨会话 store:${proposal.summary}`);
344
+ if ((scope === "global" || scope === "project") && opts.requireGlobalApproval && proposal.edits.length > 0) {
345
+ await requireGlobalApproval(ctx, invocation.agent, invocation.signal, `/evolve plan ${scope} 将应用 ${proposal.edits.length} 条编辑到${scope === "project" ? "本项目" : "跨会话"} store:${proposal.summary}`);
324
346
  }
325
- const result = engine.apply(scope, sessionId, proposal, {
347
+ const result = engine.apply(scope, storeIdForCommand(scope, invocation), proposal, {
326
348
  scope,
327
349
  baselineState: state,
328
350
  ...(entrySourceOf(invocation.agent, sessionId) ? { source: entrySourceOf(invocation.agent, sessionId) } : {}),
@@ -354,18 +376,23 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
354
376
  }
355
377
  /**
356
378
  * Demote (2026-08-22): hide an entry from injection WITHOUT deleting it —
357
- * the one-command remedy for global-store pollution. Searches the global
358
- * store first (the primary target: cross-project noise), then the session's
359
- * local store. The data stays; `/evolve unarchive` restores it.
379
+ * the one-command remedy for store pollution. Searches the global store
380
+ * first (the primary target: cross-project noise), then the project store,
381
+ * then the session's local store. The data stays; `/evolve unarchive` restores it.
360
382
  */
361
- function demoteEntry(engine, id, sessionId) {
362
- for (const scope of ["global", "local"]) {
363
- const state = engine.load(scope, sessionId);
383
+ function demoteEntry(engine, id, sessionId, projectKey) {
384
+ const targets = [
385
+ { scope: "global", storeId: undefined },
386
+ ...(projectKey ? [{ scope: "project", storeId: projectKey }] : []),
387
+ { scope: "local", storeId: sessionId },
388
+ ];
389
+ for (const { scope, storeId } of targets) {
390
+ const state = engine.load(scope, storeId);
364
391
  const found = findEntryById(state, id);
365
392
  if (!found)
366
393
  continue;
367
394
  const [kind, entry] = found;
368
- const result = engine.apply(scope, sessionId, {
395
+ const result = engine.apply(scope, storeId, {
369
396
  summary: `demote: archive ${kind}:${id} from the ${scope} store`,
370
397
  rationale: "Human-invoked demote via the /evolve command.",
371
398
  expectedOutcome: "The entry is hidden from injection in every scope it touched; data is kept and restorable.",
@@ -380,10 +407,10 @@ function demoteEntry(engine, id, sessionId) {
380
407
  },
381
408
  ],
382
409
  }, { scope });
383
- const restoreScope = scope === "global" ? " global" : "";
410
+ const restoreScope = scope === "global" ? " global" : scope === "project" ? " project" : "";
384
411
  return success(`demoted ${kind}:${id} from the ${scope} store (archived — restore with /evolve unarchive ${id}${restoreScope})\n${renderResult(result)}`);
385
412
  }
386
- return error(`entry ${id} not found in the global or local store`);
413
+ return error(`entry ${id} not found in the global, project, or local store`);
387
414
  }
388
415
  function renderResult(result) {
389
416
  const applied = result.appliedEdits.filter((e) => e.applied);
package/lib/fate.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
2
+ import { EVOLVE_MESSAGE_SOURCE } from "./message-source.js";
2
3
  import { DEFAULT_PROMOTION_POLICY } from "./promotion.js";
3
4
  import { questionServiceOf } from "./approval.js";
4
5
  import { assessLocalEntries, candidateKey, filterPromotable, listLocalCandidates, splitArchiveGuards, splitPromoteBlocked, splitPromoteProposals, wholePromoteProposals, } from "./wrapup.js";
@@ -25,7 +26,7 @@ export function planLocalFates(items, candidates, globalState, policy = DEFAULT_
25
26
  splitSkipped.push({ key: item.key, reason: "not in the audited candidate list" });
26
27
  continue;
27
28
  }
28
- const blocked = splitPromoteBlocked(item, globalState, candidate.kind, policy);
29
+ const blocked = splitPromoteBlocked(item, globalState, candidate.kind, policy, candidate.metadata);
29
30
  if (blocked) {
30
31
  splitSkipped.push({ key: item.key, reason: blocked });
31
32
  continue;
@@ -331,7 +332,7 @@ function notifyFateApplied(ctx, agent, applied) {
331
332
  try {
332
333
  agent.followup(createUserMessage({
333
334
  content: [{ type: "text", text: buildFateNotice(applied) }],
334
- source: { kind: "plugin", plugin: "dsh-continual-evolve" },
335
+ source: EVOLVE_MESSAGE_SOURCE,
335
336
  }));
336
337
  }
337
338
  catch (cause) {
package/lib/index.d.ts CHANGED
@@ -5,145 +5,145 @@ export declare const name = "continual-evolve";
5
5
  /** Service key under which the evolution engine is published. */
6
6
  export declare const EVOLUTION_SERVICE = "evolution";
7
7
  export declare const inject: string[];
8
- export declare const Config: z<Schemastery.ObjectS<{
8
+ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
9
9
  /** Root for evolution stores; defaults to the resolved DSH home. */
10
- baseDir: z<string, string>;
10
+ baseDir: z<string, string, "plain">;
11
11
  /** System-prompt section order for the evolution guidance. */
12
- sectionOrder: z<number, number>;
12
+ sectionOrder: z<number, number, "defined">;
13
13
  /** Enable the automatic review gate (off by default: it costs model calls). */
14
- autoReview: z<boolean, boolean>;
14
+ autoReview: z<boolean, boolean, "defined">;
15
15
  /** Gate runs when this many turns have passed since the last review. */
16
- reviewIntervalTurns: z<number, number>;
16
+ reviewIntervalTurns: z<number, number, "defined">;
17
17
  /** Trajectory slice handed to the gate, in characters. */
18
- maxReviewInputChars: z<number, number>;
18
+ maxReviewInputChars: z<number, number, "defined">;
19
19
  /** Output budget for the cheap gate call. */
20
- reviewBudgetTokens: z<number, number>;
20
+ reviewBudgetTokens: z<number, number, "defined">;
21
21
  /** After an approved gate run with applied edits, queue a visible follow-up notice. */
22
- notifyOnAutoReview: z<boolean, boolean>;
22
+ notifyOnAutoReview: z<boolean, boolean, "defined">;
23
23
  /** Cross-session (global) edits require an explicit human approval. */
24
- requireGlobalApproval: z<boolean, boolean>;
24
+ requireGlobalApproval: z<boolean, boolean, "defined">;
25
25
  /** Skills root for materialized skill entries; defaults to <dshHome>/skills. */
26
- skillsDir: z<string, string>;
26
+ skillsDir: z<string, string, "plain">;
27
27
  /** Passphrase for rubric encryption; falls back to DSH_EVOLVE_RUBRIC_KEY, then a local key file. */
28
- rubricKey: z<string, string>;
28
+ rubricKey: z<string, string, "plain">;
29
29
  /** Write all cordis log messages to <baseDir>/evolve/plugin.log (JSONL). */
30
- logToFile: z<boolean, boolean>;
30
+ logToFile: z<boolean, boolean, "defined">;
31
31
  /** File log level: 0=error, 1=info, 2=warn, 3=debug. */
32
- logLevel: z<number, number>;
32
+ logLevel: z<number, number, "defined">;
33
33
  /** Rotate the file log when it exceeds this many bytes. */
34
- logMaxBytes: z<number, number>;
34
+ logMaxBytes: z<number, number, "defined">;
35
35
  /** After a benchmark decision rejects a candidate, roll the refinement back automatically. */
36
- autoRollbackOnReject: z<boolean, boolean>;
36
+ autoRollbackOnReject: z<boolean, boolean, "defined">;
37
37
  /**
38
38
  * P1 auto-case capture: failed evolution attempts (benchmark-rejected
39
39
  * candidates, gate proposals without consent) land as draft cases in the
40
40
  * auto-regression container benchmark, seeding the regression loop.
41
41
  */
42
- autoCase: z<boolean, boolean>;
42
+ autoCase: z<boolean, boolean, "defined">;
43
43
  /**
44
44
  * Gap C1: optional model override for the review gate (cheaper model).
45
45
  * Format: "provider/model" or just "model" (same provider as the agent).
46
46
  * When absent, the review gate uses the agent's own provider/model.
47
47
  */
48
- reviewModel: z<string, string>;
48
+ reviewModel: z<string, string, "plain">;
49
49
  /**
50
50
  * Gate local-fate dimension (#11 P2): the gate audits the session's local
51
51
  * entries on its own cadence and proposes promote/archive — consulted
52
52
  * first, never written silently. Only meaningful with autoReview on.
53
53
  */
54
- localFate: z<boolean, boolean>;
54
+ localFate: z<boolean, boolean, "defined">;
55
55
  /**
56
56
  * Minimum turns between local-fate assessments on the turn-interval path
57
57
  * (compaction is unconditional). Absent → follows reviewIntervalTurns.
58
58
  */
59
- fateIntervalTurns: z<number, number>;
59
+ fateIntervalTurns: z<number, number, "plain">;
60
60
  /**
61
61
  * Goal-blocked trigger (D3): after this many CONSECUTIVE gate runs that
62
62
  * observe the session goal in phase "blocked", run one local-fate
63
63
  * assessment so the encounter is distilled. 0 disables.
64
64
  */
65
- goalBlockedWrapupTurns: z<number, number>;
65
+ goalBlockedWrapupTurns: z<number, number, "defined">;
66
66
  /**
67
67
  * Promotion policy (2026-08-22): regex sources whose match in a
68
68
  * candidate's title/content marks it project-scoped — such entries are
69
69
  * never promoted to the cross-session global store. Replaces the built-in
70
70
  * defaults when set.
71
71
  */
72
- promotionBlockPatterns: z<string[], string[]>;
72
+ promotionBlockPatterns: z<string[], string[], "plain">;
73
73
  /** Whole promotions below this content length stay local (chars). */
74
- promotionMinChars: z<number, number>;
74
+ promotionMinChars: z<number, number, "defined">;
75
75
  /** Entry-directory lines injected per build before folding into a counter. */
76
- injectionDirectoryLines: z<number, number>;
77
- }>, Schemastery.ObjectT<{
76
+ injectionDirectoryLines: z<number, number, "defined">;
77
+ }>>, Schemastery.ObjectT<NoInfer<{
78
78
  /** Root for evolution stores; defaults to the resolved DSH home. */
79
- baseDir: z<string, string>;
79
+ baseDir: z<string, string, "plain">;
80
80
  /** System-prompt section order for the evolution guidance. */
81
- sectionOrder: z<number, number>;
81
+ sectionOrder: z<number, number, "defined">;
82
82
  /** Enable the automatic review gate (off by default: it costs model calls). */
83
- autoReview: z<boolean, boolean>;
83
+ autoReview: z<boolean, boolean, "defined">;
84
84
  /** Gate runs when this many turns have passed since the last review. */
85
- reviewIntervalTurns: z<number, number>;
85
+ reviewIntervalTurns: z<number, number, "defined">;
86
86
  /** Trajectory slice handed to the gate, in characters. */
87
- maxReviewInputChars: z<number, number>;
87
+ maxReviewInputChars: z<number, number, "defined">;
88
88
  /** Output budget for the cheap gate call. */
89
- reviewBudgetTokens: z<number, number>;
89
+ reviewBudgetTokens: z<number, number, "defined">;
90
90
  /** After an approved gate run with applied edits, queue a visible follow-up notice. */
91
- notifyOnAutoReview: z<boolean, boolean>;
91
+ notifyOnAutoReview: z<boolean, boolean, "defined">;
92
92
  /** Cross-session (global) edits require an explicit human approval. */
93
- requireGlobalApproval: z<boolean, boolean>;
93
+ requireGlobalApproval: z<boolean, boolean, "defined">;
94
94
  /** Skills root for materialized skill entries; defaults to <dshHome>/skills. */
95
- skillsDir: z<string, string>;
95
+ skillsDir: z<string, string, "plain">;
96
96
  /** Passphrase for rubric encryption; falls back to DSH_EVOLVE_RUBRIC_KEY, then a local key file. */
97
- rubricKey: z<string, string>;
97
+ rubricKey: z<string, string, "plain">;
98
98
  /** Write all cordis log messages to <baseDir>/evolve/plugin.log (JSONL). */
99
- logToFile: z<boolean, boolean>;
99
+ logToFile: z<boolean, boolean, "defined">;
100
100
  /** File log level: 0=error, 1=info, 2=warn, 3=debug. */
101
- logLevel: z<number, number>;
101
+ logLevel: z<number, number, "defined">;
102
102
  /** Rotate the file log when it exceeds this many bytes. */
103
- logMaxBytes: z<number, number>;
103
+ logMaxBytes: z<number, number, "defined">;
104
104
  /** After a benchmark decision rejects a candidate, roll the refinement back automatically. */
105
- autoRollbackOnReject: z<boolean, boolean>;
105
+ autoRollbackOnReject: z<boolean, boolean, "defined">;
106
106
  /**
107
107
  * P1 auto-case capture: failed evolution attempts (benchmark-rejected
108
108
  * candidates, gate proposals without consent) land as draft cases in the
109
109
  * auto-regression container benchmark, seeding the regression loop.
110
110
  */
111
- autoCase: z<boolean, boolean>;
111
+ autoCase: z<boolean, boolean, "defined">;
112
112
  /**
113
113
  * Gap C1: optional model override for the review gate (cheaper model).
114
114
  * Format: "provider/model" or just "model" (same provider as the agent).
115
115
  * When absent, the review gate uses the agent's own provider/model.
116
116
  */
117
- reviewModel: z<string, string>;
117
+ reviewModel: z<string, string, "plain">;
118
118
  /**
119
119
  * Gate local-fate dimension (#11 P2): the gate audits the session's local
120
120
  * entries on its own cadence and proposes promote/archive — consulted
121
121
  * first, never written silently. Only meaningful with autoReview on.
122
122
  */
123
- localFate: z<boolean, boolean>;
123
+ localFate: z<boolean, boolean, "defined">;
124
124
  /**
125
125
  * Minimum turns between local-fate assessments on the turn-interval path
126
126
  * (compaction is unconditional). Absent → follows reviewIntervalTurns.
127
127
  */
128
- fateIntervalTurns: z<number, number>;
128
+ fateIntervalTurns: z<number, number, "plain">;
129
129
  /**
130
130
  * Goal-blocked trigger (D3): after this many CONSECUTIVE gate runs that
131
131
  * observe the session goal in phase "blocked", run one local-fate
132
132
  * assessment so the encounter is distilled. 0 disables.
133
133
  */
134
- goalBlockedWrapupTurns: z<number, number>;
134
+ goalBlockedWrapupTurns: z<number, number, "defined">;
135
135
  /**
136
136
  * Promotion policy (2026-08-22): regex sources whose match in a
137
137
  * candidate's title/content marks it project-scoped — such entries are
138
138
  * never promoted to the cross-session global store. Replaces the built-in
139
139
  * defaults when set.
140
140
  */
141
- promotionBlockPatterns: z<string[], string[]>;
141
+ promotionBlockPatterns: z<string[], string[], "plain">;
142
142
  /** Whole promotions below this content length stay local (chars). */
143
- promotionMinChars: z<number, number>;
143
+ promotionMinChars: z<number, number, "defined">;
144
144
  /** Entry-directory lines injected per build before folding into a counter. */
145
- injectionDirectoryLines: z<number, number>;
146
- }>>;
145
+ injectionDirectoryLines: z<number, number, "defined">;
146
+ }>>, "plain">;
147
147
  /**
148
148
  * Structurally typed resolved config (loader passes the validated object).
149
149
  * Derived from the schemastery schema — single source of truth, no manual sync.
package/lib/index.js CHANGED
@@ -116,7 +116,7 @@ export function apply(ctx, config) {
116
116
  ctx.systemPrompt.section({
117
117
  name: "tool:continual-evolve",
118
118
  order: config.sectionOrder ?? 118,
119
- text: "You have a continual harness: versioned, persistent prompt notes, memories, skills, and subagent specs. Prompt notes and delegation specs are injected below; use evolve_list for the full state. Create an entry (evolve_add) after a repeated failure, a reusable tactic, a durable fact or preference, a repeated procedure, or a repeated delegation role. Keep edits small and evidence-backed; prefer local scope, use global: true only for stable cross-session lessons. Update or delete (evolve_update / evolve_delete) when an entry is wrong or obsolete; roll back faulty refinements with evolve_rollback. Every edit is snapshotted, versioned, and recorded — no edit can be silently lost.",
119
+ text: "You have a continual harness: versioned, persistent prompt notes, memories, skills, and subagent specs. Memories are ONE FACT per entry, typed user/feedback/project/reference (feedback = pitfall + Why + How to apply). Stores: local (this session's staging), project (this project's cross-session store), global (cross-project). Prompt notes and delegation specs are injected below; use evolve_list for the full state. Create an entry (evolve_add) after a repeated failure, a reusable tactic, a durable fact or preference, a repeated procedure, or a repeated delegation role. Keep edits small and evidence-backed; prefer local scope, use project for repo-specific lessons, global: true only for stable cross-project lessons. Update or delete (evolve_update / evolve_delete) when an entry is wrong or obsolete; roll back faulty refinements with evolve_rollback. Every edit is snapshotted, versioned, and recorded — no edit can be silently lost.",
120
120
  });
121
121
  // Phase 2: make prompt entries real system-prompt content and subagent
122
122
  // entries real delegation specs. The text is a provider evaluated at every