dsh-continual-evolve 0.6.1 → 0.7.1

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-665%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 |
@@ -60,12 +61,16 @@ Commands (in-session):
60
61
  | `/evolve mount · unmount <skillId>` | hot-mount an executable skill as a live plugin |
61
62
  | `/evolve goal [objective · done · block]` | round-driven auto-review goal |
62
63
  | `/evolve benchmark …` | case lifecycle, runs, acceptance |
64
+ | `/evolve pause · resume · status` | pause/resume the auto-review gate (manual tools keep working), gate state |
65
+ | `/evolve usage` | per-entry injection counts + exact provider-reported tokens for direct review/planner/wrapup/fate calls (benchmark host subagents excluded) |
63
66
 
64
- Model tools: `evolve_list / add / update / delete / rollback`.
67
+ Model tools: `evolve_list / add / update / delete / rollback` (`evolve_delete` takes `id` or a batch `ids` array — one refinement, one approval).
65
68
 
66
69
  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
70
 
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.
71
+ `/evolve usage` also reads `evolve/token-usage.jsonl`: exact provider-reported input/cache/output/total tokens for the plugin's direct review, planner, manual-wrapup, and automatic-fate calls. The report covers a retained tail rather than lifetime usage, distinguishes missing provider samples, and explicitly excludes host benchmark subagents, their agent-loop calls, and per-entry injection attribution.
72
+
73
+ 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
74
 
70
75
  ## Configuration
71
76
 
@@ -77,7 +82,7 @@ Injection shape: prompt notes and delegation specs inject with content (≤6/kin
77
82
  | `maxReviewInputChars` | `40000` | trajectory slice handed to the gate |
78
83
  | `reviewBudgetTokens` | `4096` | output budget for the gate call |
79
84
  | `notifyOnAutoReview` | `true` | visible follow-up notice after an applied gate run |
80
- | `requireGlobalApproval` | `true` | global edits ask for explicit approval |
85
+ | `requireGlobalApproval` | `true` | global and project edits ask for explicit approval |
81
86
  | `localFate` | `true` | gate audits local entries and proposes promote/archive (consulted, never silent) |
82
87
  | `fateIntervalTurns` | follows `reviewIntervalTurns` | minimum turns between fate assessments |
83
88
  | `goalBlockedWrapupTurns` | `3` | consecutive blocked-goal gate runs trigger one fate assessment (`0` disables) |
@@ -91,6 +96,9 @@ Injection shape: prompt notes and delegation specs inject with content (≤6/kin
91
96
  | `autoRollbackOnReject` | `true` | deterministic rollback after a benchmark rejection |
92
97
  | `autoCase` | `true` | failed evolution attempts are captured as draft regression cases (`auto_regression` benchmark) |
93
98
  | `reviewModel` | agent's own | optional cheaper model for the gate (`"provider/model"`) |
99
+ | `plannerPrefixCache` | `auto` | Route A session-prefix input when cache evidence exists (`session` always, `off` legacy flat text) |
100
+ | `plannerPrefixMaxChars` | `12000` | session-prefix budget for Route A planning inputs (chars) |
101
+ | `historyRetain` | `{snapshots: 20, refinements: 500, reviews: 500, tokenUsage: 500}` | storage hygiene: snapshots per store, tail lines per store history, shared `reviews.jsonl` tail, and direct-call `token-usage.jsonl` tail |
94
102
 
95
103
  Example profile patch:
96
104
 
@@ -105,7 +113,7 @@ Example profile patch:
105
113
 
106
114
  ```bash
107
115
  pnpm install && pnpm build # deps + tsc -> lib/
108
- pnpm test # vitest (576 tests)
116
+ pnpm test # vitest (665 tests)
109
117
  pnpm test:coverage # v8 coverage, thresholds enforced in CI
110
118
  pnpm lint # oxlint src test
111
119
  ```
@@ -113,8 +121,8 @@ pnpm lint # oxlint src test
113
121
  Project layout:
114
122
 
115
123
  ```
116
- ├── src/ # engine, tools, commands, gate, fate, benchmark, usage…
117
- ├── test/ # vitest suites (36 files)
124
+ ├── src/ # engine, tools, commands, gate, fate, benchmark, injection + token usage…
125
+ ├── test/ # vitest suites (41 files)
118
126
  ├── lib/ # build output (tsc)
119
127
  ├── docs/
120
128
  │ ├── 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-665%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` 针对全局噪声 |
@@ -60,12 +61,16 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
60
61
  | `/evolve mount · unmount <skillId>` | 把可执行技能热挂载为 live 插件 |
61
62
  | `/evolve goal [objective · done · block]` | 回合驱动的自进化目标 |
62
63
  | `/evolve benchmark …` | 用例生命周期、运行、接受决策 |
64
+ | `/evolve pause · resume · status` | 暂停/恢复自动门禁(手动工具不受影响)、门禁状态 |
65
+ | `/evolve usage` | 每条目注入次数 + review/planner/wrapup/fate 直属调用的 provider 精确 token(不含 benchmark 宿主子代理) |
63
66
 
64
- 模型工具:`evolve_list / add / update / delete / rollback`。
67
+ 模型工具:`evolve_list / add / update / delete / rollback`(`evolve_delete` 支持 `id` 或批量 `ids` 数组——一次 refinement、一次审批)。
65
68
 
66
69
  第三方消费:每次进化落地(门禁或手动)都会向 `reviews.jsonl` 追加结构化 `evolve_complete` 事件(shape 见 `src/evolve-event.ts`),与人类可读的审计记录并存。
67
70
 
68
- 注入形态:prompt 补充与委派规格带内容注入(每 kind ≤6 条 × 180 字符,按相关性排序)。memory/skill 以目录索引出现(`[kind:id] 标题`,15 行封顶 + 折叠计数行)——全文经 `evolve_list` 获取。空 store = 零注入 token。
71
+ `/evolve usage` 还会读取 `evolve/token-usage.jsonl`:插件直属 review、planner、手动 wrapup 与自动 fate 调用的 provider 精确 input/cache/output/total token。报告只覆盖保留尾部而非终身累计,单独显示 provider 未返回 usage 的调用,并明确排除宿主 benchmark 子代理、其 agent-loop 调用与逐条 memory 注入归因。
72
+
73
+ 注入形态:prompt 补充与委派规格带内容注入(每 kind ≤6 条 × 180 字符,按相关性排序)。memory/skill 以按相关性排序的目录索引出现(`[memory:type:id] 标题`钩子,15 行封顶 + 折叠计数行)——全文经 `evolve_list` 获取。空 store = 零注入 token。
69
74
 
70
75
  ## 配置
71
76
 
@@ -77,7 +82,7 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
77
82
  | `maxReviewInputChars` | `40000` | 交给门禁的轨迹切片 |
78
83
  | `reviewBudgetTokens` | `4096` | 门禁调用输出预算 |
79
84
  | `notifyOnAutoReview` | `true` | 门禁应用后发可见跟进通知 |
80
- | `requireGlobalApproval` | `true` | 全局写入需明确批准 |
85
+ | `requireGlobalApproval` | `true` | 全局与项目写入需明确批准 |
81
86
  | `localFate` | `true` | 门禁审计本地条目并提议晋升/归档(先征询,绝不静默) |
82
87
  | `fateIntervalTurns` | 跟随 `reviewIntervalTurns` | 归宿评估的最小回合间隔 |
83
88
  | `goalBlockedWrapupTurns` | `3` | 连续阻塞目标的门禁轮数触发一次归宿评估(`0` 关闭) |
@@ -91,6 +96,9 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
91
96
  | `autoRollbackOnReject` | `true` | benchmark 拒绝后自动确定性回滚 |
92
97
  | `autoCase` | `true` | 失败的进化尝试自动沉淀为 draft 回归用例(`auto_regression` 基准) |
93
98
  | `reviewModel` | agent 自身 | 门禁可选更便宜的模型(`"provider/model"`) |
99
+ | `plannerPrefixCache` | `auto` | 有缓存证据时用会话前缀输入(`session` 总是前缀,`off` 保持旧扁平文本) |
100
+ | `plannerPrefixMaxChars` | `12000` | Route A 会话前缀预算(字符) |
101
+ | `historyRetain` | `{snapshots: 20, refinements: 500, reviews: 500, tokenUsage: 500}` | 存储卫生:每 store 快照数、每 store 历史尾行、共享 `reviews.jsonl` 尾行、直属调用 `token-usage.jsonl` 尾行 |
94
102
 
95
103
  profile patch 示例:
96
104
 
@@ -105,7 +113,7 @@ profile patch 示例:
105
113
 
106
114
  ```bash
107
115
  pnpm install && pnpm build # 依赖 + tsc -> lib/
108
- pnpm test # vitest(576 例)
116
+ pnpm test # vitest(665 例)
109
117
  pnpm test:coverage # v8 覆盖率,CI 强制阈值
110
118
  pnpm lint # oxlint src test
111
119
  ```
@@ -113,8 +121,8 @@ pnpm lint # oxlint src test
113
121
  目录结构:
114
122
 
115
123
  ```
116
- ├── src/ # 引擎、工具、命令、门禁、fate、benchmark、usage…
117
- ├── test/ # vitest 测试套件(36 个文件)
124
+ ├── src/ # 引擎、工具、命令、门禁、fate、benchmark、注入 + token 用量…
125
+ ├── test/ # vitest 测试套件(41 个文件)
118
126
  ├── lib/ # 构建产物(tsc)
119
127
  ├── docs/
120
128
  │ ├── design.md # 完整设计文档(硬化矩阵)
package/lib/apply.js CHANGED
@@ -7,15 +7,23 @@ export function applyRefinementProposal(state, proposal, options) {
7
7
  const now = new Date().toISOString();
8
8
  for (const edit of proposal.edits) {
9
9
  // Store-prefix hygiene: planner edits are written against the MERGED
10
- // view, where colliding local ids carry a `local:` / `global:` prefix.
11
- // A CREATE must never bake that view prefix into a permanent id
12
- // (observed: global entries literally named "local:handoff_todo_…").
13
- // Updates/deletes keep the raw id — they address existing entries.
14
- // (Review audit 2026-08-28 S7: planner update/delete edits against
15
- // the merged view carry the same prefix — strip it there too, they
16
- // address the same underlying entry.)
17
- const prefixlessId = edit.id?.replace(/^(?:local|global):/, "");
18
- const requestedId = edit.action === "create" ? prefixlessId : edit.id && prefixlessId;
10
+ // view, where colliding project/local ids carry a `project:` /
11
+ // `local:` / `global:` prefix. A CREATE must never bake that view
12
+ // prefix into a permanent id (observed: global entries literally
13
+ // named "local:handoff_todo_…").
14
+ // Non-create edits address existing entries: the literal id wins —
15
+ // legacy stores contain entries whose literal id carries a colon
16
+ // prefix, and stripping unconditionally would resolve them to a
17
+ // sibling that does not exist ("entry not found", forcing file-level
18
+ // surgery). Only when the literal id is absent does the stripped
19
+ // form serve as the merged-view fallback.
20
+ // (Review audit 2026-08-28 S7 origin; literal-first fix 2026-09-24.)
21
+ const strippedId = edit.id?.replace(/^(?:local|project|global):/, "");
22
+ const requestedId = edit.action === "create"
23
+ ? strippedId
24
+ : edit.id !== undefined && state.entries[edit.kind]?.[edit.id] !== undefined
25
+ ? edit.id
26
+ : edit.id && strippedId;
19
27
  const computedId = requestedId ?? (edit.action === "create" ? slug(edit.title ?? edit.kind, edit.kind) : undefined);
20
28
  const id = computedId ?? "";
21
29
  // Unknown kinds fail per-edit — a malformed proposal must never crash
package/lib/auto.d.ts CHANGED
@@ -4,6 +4,7 @@ import type { HarnessState, RefinementEdit, RefinementProposal } from "./types.j
4
4
  import type { EvolutionEngine } from "./service.js";
5
5
  import { type AutoRefineReason } from "./review.js";
6
6
  import type { PromotionPolicy } from "./promotion.js";
7
+ import type { PlannerPrefixCacheMode } from "./prefix-cache.js";
7
8
  export interface AutoReviewConfig {
8
9
  intervalTurns: number;
9
10
  maxInputChars: number;
@@ -29,6 +30,12 @@ export interface AutoReviewConfig {
29
30
  * When absent, the review gate uses the agent's own provider/model.
30
31
  */
31
32
  reviewModel?: string;
33
+ /**
34
+ * Prefix-cache routing for the gate input (see reviewAutoRefine).
35
+ * Absent mode → auto-detect; absent budget → the default prefix budget.
36
+ */
37
+ prefixCacheMode?: PlannerPrefixCacheMode;
38
+ prefixMaxChars?: number;
32
39
  /**
33
40
  * Goal-blocked trigger (D3): after this many CONSECUTIVE gate runs that
34
41
  * observe the session goal in phase "blocked", run one local-fate
@@ -52,6 +59,12 @@ export interface AutoReviewConfig {
52
59
  autoCase: boolean;
53
60
  /** Resolved rubric key for the capture's encrypted scaffold rubric. */
54
61
  rubricKey?: Buffer;
62
+ /**
63
+ * Storage hygiene (#20): tail lines kept in the shared reviews.jsonl
64
+ * audit trail. Absent → the store default (readers need a recent
65
+ * window, not the full past).
66
+ */
67
+ reviewsRetain?: number;
55
68
  }
56
69
  export interface GateState {
57
70
  turns: number;
@@ -103,14 +116,16 @@ export declare function parseReviewModel(reviewModel: string | undefined, fallba
103
116
  } | undefined;
104
117
  /**
105
118
  * 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.
119
+ * merged with the project and global stores, each entry carrying its real
120
+ * scope. Without the global half the gate cannot see that a topic is already
121
+ * covered cross-session and happily re-sediments a local duplicate of it.
109
122
  *
110
123
  * The merged view is read-only context — applying still targets the raw
111
124
  * local state (baseline checks compare local entries only).
112
125
  */
113
- export declare function loadGateHarnessView(engine: EvolutionEngine, sessionId: string): HarnessState;
126
+ export declare function loadGateHarnessView(engine: EvolutionEngine, sessionId: string, opts?: {
127
+ projectKey?: string;
128
+ }): HarnessState;
114
129
  /**
115
130
  * D3 (goal blocked → wrap-up coupling, reverse direction): count consecutive
116
131
  * gate runs whose goal is in phase "blocked"; when the streak reaches
package/lib/auto.js CHANGED
@@ -25,8 +25,11 @@ 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";
31
+ import { DEFAULT_REVIEWS_RETAIN, pruneJsonlFile } from "./store.js";
32
+ import { isGatePaused } from "./runtime.js";
30
33
  import { captureAutoCase } from "./autocase.js";
31
34
  /** Turns a rejected skill candidate stays silent before being offered again. */
32
35
  export const SKILL_CONSULT_COOLDOWN_TURNS = 10;
@@ -41,6 +44,14 @@ export function registerAutoReview(ctx, engine, config) {
41
44
  }
42
45
  catch (cause) {
43
46
  logger.warn(`failed to record auto-review: ${cause instanceof Error ? cause.message : String(cause)}`);
47
+ return;
48
+ }
49
+ // Storage hygiene (#20): bound the audit trail at write time.
50
+ try {
51
+ pruneJsonlFile(reviewsPath, config.reviewsRetain ?? DEFAULT_REVIEWS_RETAIN);
52
+ }
53
+ catch {
54
+ // ignored — the next record retries
44
55
  }
45
56
  };
46
57
  // Turn counting uses `agent/turn-stopping` — empirically the only event
@@ -66,6 +77,12 @@ export function registerAutoReview(ctx, engine, config) {
66
77
  const goalDriven = goalDrivesRounds(goalServiceOf(ctx)?.get(agent));
67
78
  if (!goalDriven && state.turns - state.lastReviewAt < config.intervalTurns)
68
79
  return;
80
+ // #21 P2 runtime switch: a paused gate stays fully dormant — no LLM
81
+ // calls, no fate assessments, no audit records. Checked after the
82
+ // cheap in-memory interval check so the paused path costs one file
83
+ // read at most. Manual tools/commands are unaffected (separate path).
84
+ if (isGatePaused(engine.baseDir))
85
+ return;
69
86
  // Run the gate outside the listener turn: agent is idle, work is auxiliary.
70
87
  // Every failure is durably recorded — nothing fails silently.
71
88
  void runGate(ctx, engine, agent, config, state, "turn_interval", record).catch((cause) => {
@@ -98,6 +115,12 @@ export function registerAutoReview(ctx, engine, config) {
98
115
  catch (cause) {
99
116
  logger.warn(`failed to write armed marker: ${cause instanceof Error ? cause.message : String(cause)}`);
100
117
  }
118
+ try {
119
+ pruneJsonlFile(reviewsPath, config.reviewsRetain ?? DEFAULT_REVIEWS_RETAIN);
120
+ }
121
+ catch {
122
+ // ignored — the next record retries
123
+ }
101
124
  ctx.on("session/event", (session, event) => {
102
125
  if (event.type !== "compaction/start")
103
126
  return;
@@ -106,7 +129,11 @@ export function registerAutoReview(ctx, engine, config) {
106
129
  if (!agent)
107
130
  return; // no live agent for that session (e.g. cold read)
108
131
  const state = stateFor(perSession, agent.id);
109
- // Compaction is unconditional: persist what is about to be summarized away.
132
+ // Compaction is unconditional — unless the human paused the gate
133
+ // (#21): a paused gate skips even the compaction run, so "pause"
134
+ // is a true zero-LLM-call state.
135
+ if (isGatePaused(engine.baseDir))
136
+ return;
110
137
  void runGate(ctx, engine, agent, config, state, "compact", record).catch((cause) => {
111
138
  logger.warn(`auto-review failed at compaction for ${agent.id}: ${cause instanceof Error ? cause.message : String(cause)}`);
112
139
  record({
@@ -153,15 +180,16 @@ function stateFor(map, sessionId) {
153
180
  }
154
181
  /**
155
182
  * 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.
183
+ * merged with the project and global stores, each entry carrying its real
184
+ * scope. Without the global half the gate cannot see that a topic is already
185
+ * covered cross-session and happily re-sediments a local duplicate of it.
159
186
  *
160
187
  * The merged view is read-only context — applying still targets the raw
161
188
  * local state (baseline checks compare local entries only).
162
189
  */
163
- export function loadGateHarnessView(engine, sessionId) {
164
- return mergeHarnessStates(engine.load("global", undefined), engine.load("local", sessionId));
190
+ export function loadGateHarnessView(engine, sessionId, opts) {
191
+ const projectState = opts?.projectKey ? engine.load("project", opts.projectKey) : undefined;
192
+ return mergeHarnessStates(engine.load("global", undefined), engine.load("local", sessionId), projectState ? { projectState } : undefined);
165
193
  }
166
194
  /**
167
195
  * One gate run = review phase + local-fate phase (#11 P2). The review phase
@@ -226,6 +254,12 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
226
254
  const sessionId = agent.id;
227
255
  const turnsSinceLastReview = state.turns - state.lastReviewAt;
228
256
  const logger = ctx.logger("continual-evolve");
257
+ const tokenUsage = {
258
+ baseDir: engine.baseDir,
259
+ sessionId,
260
+ retain: engine.retention.tokenUsage,
261
+ onError: (cause) => logger.warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
262
+ };
229
263
  const trajectory = await readTrajectory(ctx, agent, config.maxInputChars).catch((cause) => {
230
264
  logger.warn(`auto-review skipped for ${sessionId}: trajectory unavailable: ${cause instanceof Error ? cause.message : String(cause)}`);
231
265
  record({ sessionId, reason, turnsSinceLastReview, outcome: "failed", rationale: `trajectory unavailable: ${cause instanceof Error ? cause.message : String(cause)}` });
@@ -239,7 +273,8 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
239
273
  // can recognize topics already covered globally and decline duplicates;
240
274
  // applying still targets the raw local store.
241
275
  const localState = engine.load("local", sessionId);
242
- const harnessState = loadGateHarnessView(engine, sessionId);
276
+ const projectKey = projectKeyOf(agent);
277
+ const harnessState = loadGateHarnessView(engine, sessionId, projectKey ? { projectKey } : undefined);
243
278
  const history = engine.history("local", sessionId);
244
279
  // Gap C1: resolve optional review model override.
245
280
  const reviewRoute = parseReviewModel(config.reviewModel, agent.options.provider);
@@ -250,7 +285,16 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
250
285
  trajectory,
251
286
  context: { reason, turnsSinceLastReview },
252
287
  budgetTokens: config.budgetTokens,
288
+ tokenUsage,
253
289
  ...(reviewRoute ? { overrideProvider: reviewRoute.provider, overrideModel: reviewRoute.model } : {}),
290
+ ...((config.prefixCacheMode !== undefined || config.prefixMaxChars !== undefined
291
+ ? {
292
+ prefixCache: {
293
+ ...(config.prefixCacheMode !== undefined ? { mode: config.prefixCacheMode } : {}),
294
+ ...(config.prefixMaxChars !== undefined ? { maxChars: config.prefixMaxChars } : {}),
295
+ },
296
+ }
297
+ : {})),
254
298
  });
255
299
  state.lastReviewAt = state.turns;
256
300
  if (!review.shouldRefine) {
@@ -267,6 +311,15 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
267
311
  // Read the skill-creator template facts (fallback: builtin distilled
268
312
  // guide) so skill proposals follow the standard.
269
313
  skillsRoot: join(engine.baseDir, "skills"),
314
+ tokenUsage,
315
+ ...((config.prefixCacheMode !== undefined || config.prefixMaxChars !== undefined
316
+ ? {
317
+ prefixCache: {
318
+ ...(config.prefixCacheMode !== undefined ? { mode: config.prefixCacheMode } : {}),
319
+ ...(config.prefixMaxChars !== undefined ? { maxChars: config.prefixMaxChars } : {}),
320
+ },
321
+ }
322
+ : {})),
270
323
  });
271
324
  // Skills are governed resources: an auto-created skill is OFFERED to the
272
325
  // user for a decision (固化/不固化) before it lands — the gate never
@@ -314,7 +367,7 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
314
367
  logger.info(`auto-review approved (${reason}) [${sessionId}] after ${turnsSinceLastReview} turns; auto-refine ${result.id}: ${result.appliedEdits.filter((e) => e.applied).length} applied, ${result.appliedEdits.filter((e) => !e.applied).length} failed — ${review.rationale}`);
315
368
  record({ sessionId, reason, turnsSinceLastReview, outcome: "approved", rationale: review.rationale, refinementId: result.id });
316
369
  // Gap C4: emit structured evolve_complete event for third-party consumers.
317
- emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, `auto_review:${reason}`, sessionId));
370
+ emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, `auto_review:${reason}`, sessionId), config.reviewsRetain ?? DEFAULT_REVIEWS_RETAIN);
318
371
  // Visibility: tell the user what the gate just persisted. Only the
319
372
  // turn-interval path notifies — a compaction-triggered gate must not wake
320
373
  // the agent mid-compaction — and only when something was actually applied
package/lib/command.d.ts CHANGED
@@ -17,6 +17,12 @@ export interface CommandRuntimeOptions {
17
17
  autoCase: boolean;
18
18
  /** Mechanical promotion guards for wrapup/fate (2026-08-22 policy). */
19
19
  promotionPolicy: PromotionPolicy;
20
+ /**
21
+ * Static patch flag for the auto-review gate (#21 status display).
22
+ * Absent (older wiring/tests) renders as unknown — the runtime pause
23
+ * switch still works.
24
+ */
25
+ autoReview?: boolean;
20
26
  }
21
27
  export declare function registerEvolveCommand(ctx: Context, engine: EvolutionEngine, opts: CommandGateOptions, runtime: CommandRuntimeOptions): void;
22
28
  /**