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 +12 -6
- package/README.zh.md +12 -6
- package/lib/apply.d.ts +0 -2
- package/lib/apply.js +18 -8
- package/lib/auto.d.ts +8 -6
- package/lib/auto.js +42 -26
- package/lib/autocase.d.ts +38 -0
- package/lib/autocase.js +76 -0
- package/lib/benchmark-command.js +25 -1
- package/lib/benchmark.d.ts +0 -5
- package/lib/benchmark.js +1 -24
- package/lib/command.d.ts +2 -0
- package/lib/command.js +41 -5
- package/lib/consolidate.d.ts +90 -0
- package/lib/consolidate.js +180 -0
- package/lib/failures.d.ts +4 -1
- package/lib/failures.js +2 -1
- package/lib/index.d.ts +12 -0
- package/lib/index.js +11 -1
- package/lib/inject.d.ts +13 -17
- package/lib/inject.js +49 -37
- package/lib/mount.js +9 -0
- package/lib/promotion.d.ts +38 -6
- package/lib/promotion.js +93 -24
- package/lib/rollback.js +7 -0
- package/lib/search.d.ts +83 -0
- package/lib/search.js +136 -0
- package/lib/service.js +74 -1
- package/lib/state.d.ts +0 -12
- package/lib/state.js +4 -19
- package/lib/store.js +3 -1
- package/lib/tool.js +8 -1
- package/lib/types.d.ts +22 -0
- package/lib/types.js +22 -0
- package/lib/usage.d.ts +1 -10
- package/lib/usage.js +0 -17
- package/lib/validate.d.ts +10 -3
- package/lib/validate.js +52 -5
- package/lib/wrapup-command.js +17 -1
- package/lib/wrapup.d.ts +25 -1
- package/lib/wrapup.js +57 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
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 (
|
|
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 (
|
|
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
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
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
|
|
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(
|
|
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 测试套件(
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
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}]
|
|
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
|
package/lib/autocase.js
ADDED
|
@@ -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
|
package/lib/benchmark-command.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/lib/benchmark.d.ts
CHANGED
|
@@ -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,
|
|
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
|
}
|