dsh-continual-evolve 0.4.0 → 0.6.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-527%20passing-brightgreen)]()
10
+ [![Tests](https://img.shields.io/badge/tests-573%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
 
@@ -20,14 +20,15 @@ Agents accumulate reusable experience (repeated failures, durable facts, reusabl
20
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
21
21
  - **Deterministic rollback**: inverse edits generated from applied results — no LLM re-guessing
22
22
  - **Benchmark loop**: candidate refinements are evaluated against frozen cases by a separate scorer before acceptance (rubric encrypted at rest)
23
+ - **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
23
24
 
24
25
  ## How it works
25
26
 
26
27
  1. **Sediment** — the model creates entries via `evolve_add`, or the automatic review gate proposes them from the session trajectory (turn-interval + compaction checkpoints).
27
- 2. **Guard** — code-enforced validation: edit schema, blast-radius/scope coherence, and the promotion policy (project-scoped markers, thin content, near-duplicate detection keep the global store clean).
28
+ 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.
28
29
  3. **Approve** — global writes require explicit human approval; local-fate proposals are consulted before they land.
29
- 4. **Apply & inject** — atomic apply with snapshot + audit event. Prompt notes and delegation specs inject into the system prompt (capped, relevance-ranked, zero tokens when empty); memories/skills appear as a capped directory index.
30
- 5. **Validate & roll back** — benchmarks score candidates against frozen cases; rejected candidates roll back deterministically.
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.
31
+ 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).
31
32
 
32
33
  ## Install
33
34
 
@@ -52,6 +53,7 @@ Commands (in-session):
52
53
  | `/evolve plan [msg]` | run the LLM planner against the store |
53
54
  | `/evolve wrapup` | assess this session's local entries: promote / archive / keep |
54
55
  | `/evolve archive · unarchive · demote <id>` | hide from injection (data kept, restorable) — `demote` targets global noise |
56
+ | `/evolve consolidate [apply] [merge]` | report (or apply) one batch archive of conflict-hinted + stale zero-use global entries; `merge` folds near-duplicate content into the survivors |
55
57
  | `/evolve failures` | aggregated failure classes (gate + benchmark) |
56
58
  | `/evolve log [tail N] [session <id>]` | plugin log |
57
59
  | `/evolve export · import <path>` | backup / restore a store |
@@ -61,6 +63,8 @@ Commands (in-session):
61
63
 
62
64
  Model tools: `evolve_list / add / update / delete / rollback`.
63
65
 
66
+ 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
+
64
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.
65
69
 
66
70
  ## Configuration
@@ -85,6 +89,7 @@ Injection shape: prompt notes and delegation specs inject with content (≤6/kin
85
89
  | `rubricKey` | auto-generated key file | AES-256-GCM passphrase for benchmark rubrics (`DSH_EVOLVE_RUBRIC_KEY` overrides) |
86
90
  | `logToFile` / `logLevel` / `logMaxBytes` | `true` / `1` / 5 MiB | plugin-owned JSONL file log with rotation |
87
91
  | `autoRollbackOnReject` | `true` | deterministic rollback after a benchmark rejection |
92
+ | `autoCase` | `true` | failed evolution attempts are captured as draft regression cases (`auto_regression` benchmark) |
88
93
  | `reviewModel` | agent's own | optional cheaper model for the gate (`"provider/model"`) |
89
94
 
90
95
  Example profile patch:
@@ -100,7 +105,7 @@ Example profile patch:
100
105
 
101
106
  ```bash
102
107
  pnpm install && pnpm build # deps + tsc -> lib/
103
- pnpm test # vitest (527 tests)
108
+ pnpm test # vitest (573 tests)
104
109
  pnpm test:coverage # v8 coverage, thresholds enforced in CI
105
110
  pnpm lint # oxlint src test
106
111
  ```
@@ -109,12 +114,13 @@ Project layout:
109
114
 
110
115
  ```
111
116
  ├── src/ # engine, tools, commands, gate, fate, benchmark, usage…
112
- ├── test/ # vitest suites (33 files)
117
+ ├── test/ # vitest suites (36 files)
113
118
  ├── lib/ # build output (tsc)
114
119
  ├── docs/
115
120
  │ ├── design.md # full design doc (hardening matrix)
116
121
  │ ├── FAQ.md # real failure/fix records
117
122
  │ ├── gap-analysis.md # vs prime-agent /refine + penguin-harness
123
+ │ ├── research/pi-dsh-competitor-gap-analysis.md # pi/dsh ecosystem competitors
118
124
  │ ├── experiment-bootstrap.md
119
125
  │ ├── archive/ # closed point-in-time reports
120
126
  │ └── research/ # penguin report + prime-agent annotated source
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-527%20passing-brightgreen)]()
10
+ [![Tests](https://img.shields.io/badge/tests-573%20passing-brightgreen)]()
11
11
 
12
12
  [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的持续自进化插件:一套**版本化、可审计、可回滚**的 harness 状态层——提示词补充、记忆、技能、子代理规格——从会话轨迹中沉淀而来。
13
13
 
@@ -20,14 +20,15 @@ Agent 在每个会话里积累可复用经验(重复失败、持久事实、
20
20
  - **local 会话级 / global 跨会话** 双作用域与合并语义——配合机械化晋升守卫,只有可携带、有分量、非重复的知识才能进全局
21
21
  - **确定性回滚**:逆操作编辑由已应用结果生成——不靠 LLM 重新猜测
22
22
  - **benchmark 闭环**:候选沉淀先经冻结用例 + 独立评分者评估再接受(rubric 加密落盘)
23
+ - **store 卫生**:`/evolve consolidate` 把写入时冲突提示与零使用陈旧条目变成一次批准、完全可逆的批量归档——加 `merge` 可将近重复内容并入幸存原条目
23
24
 
24
25
  ## 工作原理
25
26
 
26
27
  1. **沉淀**——模型经 `evolve_add` 创建条目,或自动 review 门禁从会话轨迹提议(回合间隔 + 压缩检查点)。
27
- 2. **守卫**——代码强制校验:编辑 schema、blast-radius 与作用域一致性、晋升政策(项目专属标记 / 过薄内容 / 近似重复检测保持全局库干净)。
28
+ 2. **守卫**——代码强制校验:编辑 schema、blast-radius 与作用域一致性、晋升政策(项目专属标记 / 过薄内容 / 近似重复检测 / 凭据筛查保持全局库干净——密钥类内容在所有写入出口被拒,含 mount 物化)。全局 create 与既有条目高度相似(≥0.8)时写入即拒;中等重叠带 `conflictHint` 供后续合并。
28
29
  3. **审批**——全局写入需明确人工批准;local 归宿提议先征询后落地。
29
- 4. **应用与注入**——原子应用带快照与审计事件。prompt 补充与委派规格注入系统提示词(封顶、按相关性排序、空 store 零 token);memory/skill 以目录索引出现。
30
- 5. **验证与回滚**——benchmark 用冻结用例为候选打分;被拒候选确定性回滚。
30
+ 4. **应用与注入**——原子应用带快照与审计事件。prompt 补充与委派规格注入系统提示词(封顶、按相关性排序、被证伪条目降权、空 store 零 token);memory/skill 以目录索引出现。
31
+ 5. **验证与回滚**——benchmark 用冻结用例为候选打分;被拒候选确定性回滚,并自动沉淀为 draft 回归用例(`auto_regression` 基准)。
31
32
 
32
33
  ## 安装
33
34
 
@@ -52,6 +53,7 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
52
53
  | `/evolve plan [msg]` | 对 store 运行 LLM 规划器 |
53
54
  | `/evolve wrapup` | 收尾本会话 local 条目:晋升 / 归档 / 保留 |
54
55
  | `/evolve archive · unarchive · demote <id>` | 从注入中隐藏(数据保留可恢复)——`demote` 针对全局噪声 |
56
+ | `/evolve consolidate [apply] [merge]` | 报告(或应用)冲突提示 + 零使用陈旧全局条目的批量归档;`merge` 将近重复内容并入幸存原条目 |
55
57
  | `/evolve failures` | 失败类聚合(门禁 + benchmark) |
56
58
  | `/evolve log [tail N] [session <id>]` | 插件日志 |
57
59
  | `/evolve export · import <path>` | 备份 / 恢复 store |
@@ -61,6 +63,8 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
61
63
 
62
64
  模型工具:`evolve_list / add / update / delete / rollback`。
63
65
 
66
+ 第三方消费:每次进化落地(门禁或手动)都会向 `reviews.jsonl` 追加结构化 `evolve_complete` 事件(shape 见 `src/evolve-event.ts`),与人类可读的审计记录并存。
67
+
64
68
  注入形态:prompt 补充与委派规格带内容注入(每 kind ≤6 条 × 180 字符,按相关性排序)。memory/skill 以目录索引出现(`[kind:id] 标题`,15 行封顶 + 折叠计数行)——全文经 `evolve_list` 获取。空 store = 零注入 token。
65
69
 
66
70
  ## 配置
@@ -85,6 +89,7 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
85
89
  | `rubricKey` | 自动生成本地密钥文件 | benchmark rubric 的 AES-256-GCM 口令(`DSH_EVOLVE_RUBRIC_KEY` 可覆盖) |
86
90
  | `logToFile` / `logLevel` / `logMaxBytes` | `true` / `1` / 5 MiB | 插件自带 JSONL 文件日志带轮转 |
87
91
  | `autoRollbackOnReject` | `true` | benchmark 拒绝后自动确定性回滚 |
92
+ | `autoCase` | `true` | 失败的进化尝试自动沉淀为 draft 回归用例(`auto_regression` 基准) |
88
93
  | `reviewModel` | agent 自身 | 门禁可选更便宜的模型(`"provider/model"`) |
89
94
 
90
95
  profile patch 示例:
@@ -100,7 +105,7 @@ profile patch 示例:
100
105
 
101
106
  ```bash
102
107
  pnpm install && pnpm build # 依赖 + tsc -> lib/
103
- pnpm test # vitest(527 例)
108
+ pnpm test # vitest(573 例)
104
109
  pnpm test:coverage # v8 覆盖率,CI 强制阈值
105
110
  pnpm lint # oxlint src test
106
111
  ```
@@ -109,12 +114,13 @@ pnpm lint # oxlint src test
109
114
 
110
115
  ```
111
116
  ├── src/ # 引擎、工具、命令、门禁、fate、benchmark、usage…
112
- ├── test/ # vitest 测试套件(33 个文件)
117
+ ├── test/ # vitest 测试套件(36 个文件)
113
118
  ├── lib/ # 构建产物(tsc)
114
119
  ├── docs/
115
120
  │ ├── design.md # 完整设计文档(硬化矩阵)
116
121
  │ ├── FAQ.md # 真实踩坑记录
117
122
  │ ├── gap-analysis.md # 对照 prime-agent /refine + penguin-harness
123
+ │ ├── research/pi-dsh-competitor-gap-analysis.md # pi/dsh 生态竞品差距分析
118
124
  │ ├── experiment-bootstrap.md
119
125
  │ ├── archive/ # 已完结的一次性报告
120
126
  │ └── research/ # penguin 报告 + prime-agent 注释源码
package/lib/apply.d.ts CHANGED
@@ -5,7 +5,6 @@
5
5
  * edits whose target entry changed while planning was in flight.
6
6
  */
7
7
  import type { EntrySource, HarnessScope, HarnessState, RefinementProposal, RefinementResult } from "./types.js";
8
- import { entryChangedSince } from "./state.js";
9
8
  export interface ApplyOptions {
10
9
  id: string;
11
10
  scope?: HarnessScope;
@@ -20,5 +19,4 @@ export interface ApplyOptions {
20
19
  source?: EntrySource;
21
20
  }
22
21
  export declare function applyRefinementProposal(state: HarnessState, proposal: RefinementProposal, options: ApplyOptions): RefinementResult;
23
- export { entryChangedSince };
24
22
  //# sourceMappingURL=apply.d.ts.map
package/lib/apply.js CHANGED
@@ -11,19 +11,23 @@ export function applyRefinementProposal(state, proposal, options) {
11
11
  // A CREATE must never bake that view prefix into a permanent id
12
12
  // (observed: global entries literally named "local:handoff_todo_…").
13
13
  // Updates/deletes keep the raw id — they address existing entries.
14
- const requestedId = edit.action === "create" ? edit.id?.replace(/^(?:local|global):/, "") : edit.id;
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;
15
19
  const computedId = requestedId ?? (edit.action === "create" ? slug(edit.title ?? edit.kind, edit.kind) : undefined);
16
20
  const id = computedId ?? "";
17
- const validationError = validateEdit(edit, computedId, options.scope);
18
- if (validationError) {
19
- appliedEdits.push({ ...edit, id, applied: false, error: validationError });
21
+ // Unknown kinds fail per-edit a malformed proposal must never crash
22
+ // the whole pass (review audit 2026-08-28 S1).
23
+ const records = state.entries[edit.kind];
24
+ if (!records) {
25
+ appliedEdits.push({ ...edit, id, applied: false, error: `unsupported kind ${String(edit.kind)}` });
20
26
  continue;
21
27
  }
22
- const records = state.entries[edit.kind];
23
28
  const before = cloneEntry(records[id]);
24
29
  const entryKey = `${edit.kind}:${id}`;
25
- const baseline = cloneEntry(options.baselineState?.entries[edit.kind][id]);
26
- if (options.baselineState && !touched.has(entryKey) && JSON.stringify(before ?? null) !== JSON.stringify(baseline ?? null)) {
30
+ if (options.baselineState && !touched.has(entryKey) && entryChangedSince(options.baselineState, state, edit.kind, id)) {
27
31
  appliedEdits.push({
28
32
  ...edit,
29
33
  id,
@@ -33,6 +37,13 @@ export function applyRefinementProposal(state, proposal, options) {
33
37
  });
34
38
  continue;
35
39
  }
40
+ // Validation sees the current entry so update rules can distinguish
41
+ // "carrying the persisted value" from "changing it" (skill_kind).
42
+ const validationError = validateEdit(edit, computedId, options.scope, before);
43
+ if (validationError) {
44
+ appliedEdits.push({ ...edit, id, applied: false, error: validationError });
45
+ continue;
46
+ }
36
47
  if (edit.action === "delete") {
37
48
  if (!before) {
38
49
  appliedEdits.push({ ...edit, id, applied: false, error: "entry not found" });
@@ -135,5 +146,4 @@ export function applyRefinementProposal(state, proposal, options) {
135
146
  ...(options.scope ? { scope: options.scope } : {}),
136
147
  };
137
148
  }
138
- export { entryChangedSince };
139
149
  //# sourceMappingURL=apply.js.map
package/lib/auto.d.ts CHANGED
@@ -44,6 +44,14 @@ export interface AutoReviewConfig {
44
44
  * local-fate dimension before anything reaches the global store.
45
45
  */
46
46
  promotionPolicy: PromotionPolicy;
47
+ /**
48
+ * P1 auto-case capture: a gate run whose planned edits all failed to get
49
+ * consent captures the attempt as a draft regression scaffold in the
50
+ * auto-regression container benchmark (never in a user benchmark).
51
+ */
52
+ autoCase: boolean;
53
+ /** Resolved rubric key for the capture's encrypted scaffold rubric. */
54
+ rubricKey?: Buffer;
47
55
  }
48
56
  export interface GateState {
49
57
  turns: number;
@@ -81,12 +89,6 @@ export interface ReviewRecord {
81
89
  rationale?: string;
82
90
  refinementId?: string;
83
91
  }
84
- /**
85
- * Count completed turns from agent/status transitions (running → idle).
86
- * Exported for unit testing; production counting uses agent/turn-stopping
87
- * (see registerAutoReview) which empirically carries the agent subject.
88
- */
89
- export declare function advanceGateState(state: GateState, status: string): boolean;
90
92
  export declare function registerAutoReview(ctx: Context, engine: EvolutionEngine, config: AutoReviewConfig): void;
91
93
  /**
92
94
  * Gap C1: parse a "provider/model" or "model" string into its components.
package/lib/auto.js CHANGED
@@ -20,32 +20,16 @@ import { join } from "node:path";
20
20
  import { slug } from "./types.js";
21
21
  import { planWithLlm } from "./planner.js";
22
22
  import { reviewAutoRefine, serializeSurface } from "./review.js";
23
- import { goalServiceOf } from "./goal.js";
23
+ import { goalDrivesRounds, goalServiceOf } from "./goal.js";
24
24
  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
28
  import { questionServiceOf } from "./approval.js";
29
29
  import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
30
+ import { captureAutoCase } from "./autocase.js";
30
31
  /** Turns a rejected skill candidate stays silent before being offered again. */
31
32
  export const SKILL_CONSULT_COOLDOWN_TURNS = 10;
32
- /**
33
- * Count completed turns from agent/status transitions (running → idle).
34
- * Exported for unit testing; production counting uses agent/turn-stopping
35
- * (see registerAutoReview) which empirically carries the agent subject.
36
- */
37
- export function advanceGateState(state, status) {
38
- if (status === "running") {
39
- state.running = true;
40
- return false;
41
- }
42
- if (status === "idle" && state.running) {
43
- state.running = false;
44
- state.turns += 1;
45
- return true;
46
- }
47
- return false;
48
- }
49
33
  export function registerAutoReview(ctx, engine, config) {
50
34
  const perSession = new Map();
51
35
  const logger = ctx.logger("continual-evolve");
@@ -79,7 +63,7 @@ export function registerAutoReview(ctx, engine, config) {
79
63
  // v3 optional: an active evolution goal drives the gate EVERY round
80
64
  // (the goal's round machine keeps the session continuing); without a
81
65
  // goal the plain turn interval applies.
82
- const goalDriven = goalServiceOf(ctx)?.get(agent)?.phase === "active";
66
+ const goalDriven = goalDrivesRounds(goalServiceOf(ctx)?.get(agent));
83
67
  if (!goalDriven && state.turns - state.lastReviewAt < config.intervalTurns)
84
68
  return;
85
69
  // Run the gate outside the listener turn: agent is idle, work is auxiliary.
@@ -188,12 +172,26 @@ export function loadGateHarnessView(engine, sessionId) {
188
172
  * review's optimistic-concurrency checks.
189
173
  */
190
174
  async function runGate(ctx, engine, agent, config, state, reason, record) {
191
- await runReviewPhase(ctx, engine, agent, config, state, reason, record);
192
- // D3: a goal stuck in "blocked" for consecutive gate runs gets one
193
- // local-fate assessment (the pipeline below), so whatever led the goal
194
- // astray is distilled before the session moves on.
195
- await runGoalBlockedFate(ctx, engine, agent, config, state, reason, record);
196
- await runLocalFatePhase(ctx, engine, agent, config, state, reason, record);
175
+ // Reentry guard (review audit 2026-08-28 S3): a gate run holds LLM calls
176
+ // and possibly a user question for a long time; an idle/compaction
177
+ // trigger overlapping the run would start a second concurrent pipeline
178
+ // whose stale whole-file saves clobber the first run's writes.
179
+ if (state.running) {
180
+ ctx.logger("continual-evolve").info(`auto-review skipped [${agent.id}]: previous gate run still in flight`);
181
+ return;
182
+ }
183
+ state.running = true;
184
+ try {
185
+ await runReviewPhase(ctx, engine, agent, config, state, reason, record);
186
+ // D3: a goal stuck in "blocked" for consecutive gate runs gets one
187
+ // local-fate assessment (the pipeline below), so whatever led the
188
+ // goal astray is distilled before the session moves on.
189
+ await runGoalBlockedFate(ctx, engine, agent, config, state, reason, record);
190
+ await runLocalFatePhase(ctx, engine, agent, config, state, reason, record);
191
+ }
192
+ finally {
193
+ state.running = false;
194
+ }
197
195
  }
198
196
  /**
199
197
  * D3 (goal blocked → wrap-up coupling, reverse direction): count consecutive
@@ -285,7 +283,25 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
285
283
  };
286
284
  if (finalProposal.edits.length === 0) {
287
285
  const withheld = skillEdits.length > 0 ? " (skill proposal withheld — user not consulted or declined)" : "";
288
- logger.info(`auto-review declined (${reason}) [${sessionId}] after ${turnsSinceLastReview} turns: no consented edits${withheld} — ${review.rationale}`);
286
+ logger.info(`auto-review declined (${reason}) [${sessionId}]: no consented edits${withheld} — ${review.rationale}`);
287
+ // P1 auto-case capture: an attempted evolution that never landed is a
288
+ // regression asset. Contained — capture failure must not disturb the gate.
289
+ if (config.autoCase) {
290
+ try {
291
+ const captured = captureAutoCase({
292
+ baseDir: engine.baseDir,
293
+ rubricKey: config.rubricKey,
294
+ source: "gate_no_consent",
295
+ sessionId,
296
+ summary: finalProposal.summary,
297
+ reasons: [review.rationale],
298
+ });
299
+ logger.info(`auto-review auto-case captured (${reason}) [${sessionId}]: ${captured.caseId}`);
300
+ }
301
+ catch (cause) {
302
+ logger.warn(`auto-case capture failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`);
303
+ }
304
+ }
289
305
  record({ sessionId, reason, turnsSinceLastReview, outcome: "declined", rationale: `${review.rationale}${withheld}` });
290
306
  return;
291
307
  }
@@ -0,0 +1,38 @@
1
+ /** The dedicated container benchmark every auto-case lands in (sanitizeId-safe). */
2
+ export declare const AUTO_CASE_BENCHMARK_ID = "auto_regression";
3
+ /** Which failed-evolution trigger produced the capture. */
4
+ export type AutoCaseSource = "benchmark_rejection" | "gate_no_consent";
5
+ export interface AutoCaseInput {
6
+ baseDir: string;
7
+ /**
8
+ * Resolved rubric key — captures must decrypt under the installation's
9
+ * real key. Omitting it silently encrypts with the dev fallback, making
10
+ * the scaffold unreadable to the real key; callers resolve once and pass.
11
+ */
12
+ rubricKey?: Buffer | undefined;
13
+ source: AutoCaseSource;
14
+ sessionId?: string | undefined;
15
+ /** One line for what was being attempted (candidate label / proposal summary). */
16
+ summary: string;
17
+ /** Machine-captured failure reasons (decision reasons / gate rationale). */
18
+ reasons: readonly string[];
19
+ /** Refinement id when the attempt produced one (e.g. a rolled-back candidate). */
20
+ refinementId?: string | undefined;
21
+ now?: number | undefined;
22
+ }
23
+ export interface AutoCaseCapture {
24
+ bid: string;
25
+ caseId: string;
26
+ }
27
+ /** The draft statement: a structured scaffold, not an evaluable task yet. */
28
+ export declare function renderAutoCaseStatement(input: AutoCaseInput, stamp: string): string;
29
+ /** The draft rubric: explicitly not scoreable until a human rewrites it. */
30
+ export declare function renderAutoCaseRubric(input: AutoCaseInput): string;
31
+ /**
32
+ * Capture one failed evolution attempt as a draft case in the container
33
+ * benchmark, creating the container on first use. Id uniqueness comes from
34
+ * the millisecond stamp; two captures in the same millisecond throw (the
35
+ * caller's containment turns that into a warning, never a lost trigger).
36
+ */
37
+ export declare function captureAutoCase(input: AutoCaseInput): AutoCaseCapture;
38
+ //# sourceMappingURL=autocase.d.ts.map
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Auto-case capture (P1, 2026-08-28): failed evolution attempts become DRAFT
3
+ * benchmark cases — regression assets for the benchmark loop instead of
4
+ * discarded failure notes.
5
+ *
6
+ * Captures land in a dedicated container benchmark (auto-regression), NEVER
7
+ * in a user benchmark: a benchmark run evaluates every case in its container
8
+ * without a status filter, and an auto-case's rubric is a mechanical scaffold
9
+ * that must never be scored. Humans promote a capture into a real benchmark
10
+ * by re-authoring statement and rubric there, then casecheck → pilot →
11
+ * freeze. The container benchmark keeps the staging area visible and
12
+ * rollback-free (no engine involvement anywhere).
13
+ *
14
+ * No LLM call: the capture is deterministic. Callers own containment — a
15
+ * failed capture must never break the trigger path (benchmark run, gate).
16
+ */
17
+ import { addCase, createBenchmark, loadBenchmark } from "./benchmark.js";
18
+ /** The dedicated container benchmark every auto-case lands in (sanitizeId-safe). */
19
+ export const AUTO_CASE_BENCHMARK_ID = "auto_regression";
20
+ /** Id-safe compact timestamp (20260828T131934000Z style), unique per capture. */
21
+ function idStamp(now) {
22
+ return new Date(now)
23
+ .toISOString()
24
+ .replace(/[-:]/g, "")
25
+ .replace(".", "")
26
+ .replace("Z", "Z");
27
+ }
28
+ /** The draft statement: a structured scaffold, not an evaluable task yet. */
29
+ export function renderAutoCaseStatement(input, stamp) {
30
+ const lines = [
31
+ "# Auto-generated regression scaffold",
32
+ "",
33
+ `- source: ${input.source}`,
34
+ `- captured: ${stamp}`,
35
+ `- session: ${input.sessionId ?? "(unknown)"}`,
36
+ ...(input.refinementId ? [`- refinement: ${input.refinementId}`] : []),
37
+ `- attempted: ${input.summary}`,
38
+ "- failure reasons:",
39
+ ...(input.reasons.length > 0 ? input.reasons.map((reason) => ` - ${reason}`) : [" - (none captured)"]),
40
+ "",
41
+ "This case was captured mechanically from a failed evolution attempt.",
42
+ "It lives in the auto-regression container and never enters a user",
43
+ "benchmark automatically. To promote it: re-author the statement and",
44
+ "rubric in a real benchmark, then casecheck → pilot → freeze.",
45
+ ];
46
+ return `${lines.join("\n")}\n`;
47
+ }
48
+ /** The draft rubric: explicitly not scoreable until a human rewrites it. */
49
+ export function renderAutoCaseRubric(input) {
50
+ return [
51
+ "Draft scaffold — the real rubric is authored at calibration time.",
52
+ `Scoring signal captured from the failure: ${input.reasons.join("; ") || "(none)"}.`,
53
+ "Scoring against this scaffold is meaningless until a human rewrites it;",
54
+ "the case stays in the auto-regression container and out of real runs.",
55
+ ].join("\n");
56
+ }
57
+ /**
58
+ * Capture one failed evolution attempt as a draft case in the container
59
+ * benchmark, creating the container on first use. Id uniqueness comes from
60
+ * the millisecond stamp; two captures in the same millisecond throw (the
61
+ * caller's containment turns that into a warning, never a lost trigger).
62
+ */
63
+ export function captureAutoCase(input) {
64
+ if (!loadBenchmark(input.baseDir, AUTO_CASE_BENCHMARK_ID)) {
65
+ createBenchmark(input.baseDir, {
66
+ title: AUTO_CASE_BENCHMARK_ID,
67
+ description: "Draft regression scaffolds captured mechanically from failed evolution attempts. Not evaluated; promote captures into a real benchmark by hand (re-author statement/rubric, then casecheck → pilot → freeze).",
68
+ });
69
+ }
70
+ const stamp = idStamp(input.now ?? Date.now());
71
+ // Stamp first: sanitizeId truncates to 40 chars, and the stamp must
72
+ // survive the truncation for per-millisecond capture uniqueness.
73
+ const added = addCase(input.baseDir, AUTO_CASE_BENCHMARK_ID, `auto ${stamp} ${input.source}`, renderAutoCaseStatement(input, stamp), renderAutoCaseRubric(input), input.rubricKey);
74
+ return { bid: AUTO_CASE_BENCHMARK_ID, caseId: added.id };
75
+ }
76
+ //# sourceMappingURL=autocase.js.map
@@ -1,6 +1,7 @@
1
1
  import { formatHarnessStateForPrompt } from "./render.js";
2
2
  import { stripAngleBrackets } from "./command.js";
3
3
  import { addCase, caseCheckProblems, createBenchmark, listBenchmarks, listCases, loadBenchmark, loadCaseMeta, loadScoreboard, rollbackRejectedCandidate, saveCaseMeta, saveScoreboard, transitionCaseStatus } from "./benchmark.js";
4
+ import { AUTO_CASE_BENCHMARK_ID, captureAutoCase } from "./autocase.js";
4
5
  import { decide, decisionReport, entryFromCells, flagMaterialDrift } from "./score.js";
5
6
  import { evaluateState } from "./evaluate.js";
6
7
  function success(text) {
@@ -143,7 +144,10 @@ export async function executeBenchmarkCommand(ctx, engine, invocation, rest, run
143
144
  const lines = [
144
145
  `evaluation "${label}": ${outcome.cells.length} cells${failedCells > 0 ? `, ${failedCells} failed` : ""}, overall=${entry.overall ?? "?"}`,
145
146
  ...Object.entries(entry.aggregate)
146
- .filter(([key]) => key !== "overall" && key !== "failed" && key !== "total")
147
+ // totalDurationMs is metadata, not a case (FAQ #11: the same
148
+ // leak class decide/decisionReport already fixed — this
149
+ // display path was the remaining one; review audit S4).
150
+ .filter(([key]) => key !== "overall" && key !== "failed" && key !== "total" && key !== "totalDurationMs")
147
151
  .map(([key, value]) => ` ${key}: ${value ?? "?"}`),
148
152
  ];
149
153
  if (failedCells > 0) {
@@ -177,6 +181,26 @@ export async function executeBenchmarkCommand(ctx, engine, invocation, rest, run
177
181
  const outcome = rollbackRejectedCandidate(engine, sessionId, candidateId);
178
182
  lines.push(outcome.message);
179
183
  }
184
+ // P1 auto-case capture: the rejected candidate becomes a
185
+ // draft regression scaffold. Contained — a failed capture
186
+ // must never break the benchmark run's report.
187
+ if (runtime.autoCase) {
188
+ try {
189
+ const captured = captureAutoCase({
190
+ baseDir,
191
+ rubricKey: runtime.rubricKey,
192
+ source: "benchmark_rejection",
193
+ sessionId,
194
+ summary: `${bid} candidate ${candidateId}`,
195
+ reasons: decision.reasons,
196
+ refinementId: candidateId,
197
+ });
198
+ lines.push(`auto-case captured: ${AUTO_CASE_BENCHMARK_ID}/${captured.caseId} (draft — promote manually if it earns a place)`);
199
+ }
200
+ catch (cause) {
201
+ ctx.logger("continual-evolve").warn(`auto-case capture failed: ${cause instanceof Error ? cause.message : String(cause)}`);
202
+ }
203
+ }
180
204
  }
181
205
  }
182
206
  }
@@ -136,10 +136,6 @@ export declare function loadScoreboard(baseDir: string, bid: string): Scoreboard
136
136
  export declare function saveScoreboard(baseDir: string, bid: string, board: Scoreboard): void;
137
137
  export declare function loadCaseMeta(baseDir: string, bid: string, cid: string): CaseMeta | undefined;
138
138
  export declare function saveCaseMeta(baseDir: string, bid: string, cid: string, meta: CaseMeta): void;
139
- /** List case metas for all cases in a benchmark (missing meta → defaults). */
140
- export declare function listCaseMetas(baseDir: string, bid: string): Map<string, CaseMeta>;
141
- /** Check whether a case is frozen (immutable). */
142
- export declare function isCaseFrozen(baseDir: string, bid: string, cid: string): boolean;
143
139
  /**
144
140
  * Transition a case's lifecycle state. Throws on illegal transitions.
145
141
  * draft → calibrating (start pilot)
@@ -152,5 +148,4 @@ export declare function transitionCaseStatus(baseDir: string, bid: string, cid:
152
148
  * Returns human-readable problems; empty array means the case passes.
153
149
  */
154
150
  export declare function caseCheckProblems(baseDir: string, bid: string, cid: string): string[];
155
- export declare function removeBenchmark(baseDir: string, bid: string): void;
156
151
  //# sourceMappingURL=benchmark.d.ts.map
package/lib/benchmark.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * optimizer can read the file and sees ciphertext only. Legacy files that
14
14
  * predate encryption carry plaintext and are still readable.
15
15
  */
16
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
16
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
17
17
  import { join } from "node:path";
18
18
  import { encryptRubric, DEV_RUBRIC_KEY, deriveKey } from "./rubric.js";
19
19
  /**
@@ -169,23 +169,6 @@ export function saveCaseMeta(baseDir, bid, cid, meta) {
169
169
  mkdirSync(metaDir, { recursive: true });
170
170
  writeFileSync(join(metaDir, "meta.json"), `${JSON.stringify(meta, null, 2)}\n`, "utf8");
171
171
  }
172
- /** List case metas for all cases in a benchmark (missing meta → defaults). */
173
- export function listCaseMetas(baseDir, bid) {
174
- const result = new Map();
175
- const cases = listCases(baseDir, bid);
176
- for (const c of cases) {
177
- const meta = loadCaseMeta(baseDir, bid, c.id);
178
- if (meta) {
179
- result.set(c.id, meta);
180
- }
181
- }
182
- return result;
183
- }
184
- /** Check whether a case is frozen (immutable). */
185
- export function isCaseFrozen(baseDir, bid, cid) {
186
- const meta = loadCaseMeta(baseDir, bid, cid);
187
- return meta?.status === "frozen";
188
- }
189
172
  /**
190
173
  * Transition a case's lifecycle state. Throws on illegal transitions.
191
174
  * draft → calibrating (start pilot)
@@ -255,12 +238,6 @@ export function caseCheckProblems(baseDir, bid, cid) {
255
238
  }
256
239
  return problems;
257
240
  }
258
- export function removeBenchmark(baseDir, bid) {
259
- const dir = benchmarkDir(baseDir, bid);
260
- if (existsSync(dir)) {
261
- rmSync(dir, { recursive: true, force: true });
262
- }
263
- }
264
241
  import { readdirSync } from "node:fs";
265
242
  function readdirSafe(dir) {
266
243
  try {
package/lib/command.d.ts CHANGED
@@ -13,6 +13,8 @@ export interface CommandRuntimeOptions {
13
13
  rubricKey: Buffer;
14
14
  /** When a benchmark decision rejects a candidate, roll the refinement back automatically. */
15
15
  autoRollbackOnReject: boolean;
16
+ /** P1: capture failed evolution attempts as draft cases in the auto-regression benchmark. */
17
+ autoCase: boolean;
16
18
  /** Mechanical promotion guards for wrapup/fate (2026-08-22 policy). */
17
19
  promotionPolicy: PromotionPolicy;
18
20
  }