dsh-continual-evolve 0.6.1 → 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-576%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 (576 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-576%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(576 例)
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
@@ -26,7 +26,7 @@ export function planLocalFates(items, candidates, globalState, policy = DEFAULT_
26
26
  splitSkipped.push({ key: item.key, reason: "not in the audited candidate list" });
27
27
  continue;
28
28
  }
29
- const blocked = splitPromoteBlocked(item, globalState, candidate.kind, policy);
29
+ const blocked = splitPromoteBlocked(item, globalState, candidate.kind, policy, candidate.metadata);
30
30
  if (blocked) {
31
31
  splitSkipped.push({ key: item.key, reason: blocked });
32
32
  continue;
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
package/lib/inject.d.ts CHANGED
@@ -114,20 +114,42 @@ export declare function formatPromptEntriesSection(entries: readonly HarnessEntr
114
114
  export declare function formatSubagentSpecsSection(entries: readonly HarnessEntry[], query?: string): string;
115
115
  /**
116
116
  * Gap B3: a lightweight directory of ALL non-archived entries across all
117
- * kinds — one line per entry (`- [kind:id] title`), no content. This gives
118
- * the model a zero-cost overview of what exists so it can ask for full text
119
- * via `evolve_list` or `/evolve list`. The directory is appended after the
120
- * curated top-N injection sections and adds minimal tokens.
117
+ * kinds — one line per entry, no content. Memory lines carry their recall
118
+ * type (`- [memory:feedback:id] title`) so the model can judge relevance
119
+ * from the index alone. This gives the model a zero-cost overview of what
120
+ * exists so it can ask for full text via `evolve_list` or `/evolve list`.
121
+ * The directory is appended after the curated top-N injection sections and
122
+ * adds minimal tokens.
121
123
  *
122
124
  * 2026-08-22 throttle: the directory is CAPPED at {@link DEFAULT_DIRECTORY_LINES}
123
- * lines (oldest-sorted stable order) with the remainder folded into a single
124
- * counter line — an uncapped directory across a polluted global store was
125
- * measured at ~2K chars of every build in every project.
125
+ * lines with the remainder folded into a single counter line — an uncapped
126
+ * directory across a polluted global store was measured at ~2K chars of
127
+ * every build in every project.
128
+ *
129
+ * 2026-09-23 relevance order: the directory is sorted by
130
+ * {@link rankEntries} against the assembly's relevance query, so the lines
131
+ * folded below the cap are the least relevant — not the dictionary tail.
126
132
  */
127
133
  export declare const DEFAULT_DIRECTORY_LINES = 15;
134
+ /** One directory index line: memory lines carry their recall-type hook. */
135
+ export declare function directoryLine(entry: HarnessEntry): string;
128
136
  export declare function formatEntriesDirectory(...kindEntries: readonly HarnessEntry[][]): string;
129
137
  /** {@link formatEntriesDirectory} with an explicit cap (configurable). */
130
138
  export declare function formatEntriesDirectoryCapped(maxLines: number, ...kindEntries: readonly HarnessEntry[][]): string;
139
+ /**
140
+ * {@link formatEntriesDirectoryCapped} with a relevance query: entries are
141
+ * ordered by {@link rankEntries} (relevance first, then recency) so the cap
142
+ * folds the least relevant entries — never the dictionary tail. An empty
143
+ * query degrades to the valence/recency/stable order, still deterministic.
144
+ */
145
+ export declare function formatEntriesDirectoryRanked(maxLines: number, query: string | undefined, ...kindEntries: readonly HarnessEntry[][]): string;
146
+ /**
147
+ * The directory entries actually shown under the cap, in display order —
148
+ * the single source for both the rendered text and usage accounting (so a
149
+ * memory's injection IS its directory line even with the `:type` hook).
150
+ * Returns [] when the directory is suppressed or empty.
151
+ */
152
+ export declare function rankedDirectoryEntries(maxLines: number, query: string | undefined, ...kindEntries: readonly HarnessEntry[][]): HarnessEntry[];
131
153
  /**
132
154
  * Walk the parent-session chain from `agent` upward and return the nearest
133
155
  * session whose local store is non-empty, if any. Children inherit their
@@ -138,19 +160,23 @@ export declare function formatEntriesDirectoryCapped(maxLines: number, ...kindEn
138
160
  export declare function nearestLocalStateWithEntries(engine: EvolutionEngine, agent: AgentLike): HarnessState | undefined;
139
161
  /**
140
162
  * Compose the full injected block for one assembling agent: global entries
141
- * merged with the nearest carrying local store (local wins on id collision).
163
+ * merged with this project's store (when the session cwd resolves one) and
164
+ * the nearest carrying local store (precedence global < project < local).
142
165
  * The optional `query` — when absent, derived from the agent's most recent
143
166
  * direct user messages — ranks which entries fill the per-kind cap
144
167
  * (relevance first, then recency; see {@link rankEntries}). Returns "" when
145
168
  * nothing is injectable — the prompt renderer then drops the section, so an
146
169
  * empty store adds zero tokens to every assembly.
147
170
  *
148
- * `opts.directoryLines` caps the entry-directory index (2026-08-22 throttle).
171
+ * `opts.directoryLines` caps the entry-directory index (2026-08-22 throttle);
172
+ * `opts.projectKey` pins the project layer explicitly (tests, tools) —
173
+ * otherwise it is derived from the agent's session cwd, best-effort.
149
174
  * Usage recording covers ALL kinds — memories and skills appear as directory
150
175
  * lines, prompts/subagents as content — and is deduped per session so the
151
176
  * counts read "how many sessions saw this", not "how many prompt builds".
152
177
  */
153
178
  export declare function entriesSectionText(engine: EvolutionEngine, agent: AgentLike | undefined, query?: string, opts?: {
154
179
  directoryLines?: number;
180
+ projectKey?: string;
155
181
  }): string;
156
182
  //# sourceMappingURL=inject.d.ts.map