dsh-continual-evolve 0.7.0 → 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 +12 -5
- package/README.zh.md +12 -5
- package/lib/apply.js +17 -9
- package/lib/auto.d.ts +13 -0
- package/lib/auto.js +52 -2
- package/lib/command.d.ts +6 -0
- package/lib/command.js +139 -3
- package/lib/evolve-event.d.ts +4 -2
- package/lib/evolve-event.js +12 -2
- package/lib/fate.js +7 -1
- package/lib/index.d.ts +56 -0
- package/lib/index.js +28 -1
- package/lib/llm-text.d.ts +23 -0
- package/lib/llm-text.js +50 -33
- package/lib/planner.d.ts +12 -0
- package/lib/planner.js +23 -5
- package/lib/prefix-cache.d.ts +78 -0
- package/lib/prefix-cache.js +168 -0
- package/lib/review.d.ts +11 -0
- package/lib/review.js +23 -2
- package/lib/runtime.d.ts +24 -0
- package/lib/runtime.js +83 -0
- package/lib/service.d.ts +11 -1
- package/lib/service.js +19 -3
- package/lib/store.d.ts +38 -0
- package/lib/store.js +88 -1
- package/lib/token-usage.d.ts +68 -0
- package/lib/token-usage.js +211 -0
- package/lib/tool.d.ts +9 -0
- package/lib/tool.js +37 -6
- package/lib/wrapup-command.js +10 -1
- package/lib/wrapup.d.ts +5 -0
- package/lib/wrapup.js +6 -0
- 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
|
|
|
@@ -61,11 +61,15 @@ Commands (in-session):
|
|
|
61
61
|
| `/evolve mount · unmount <skillId>` | hot-mount an executable skill as a live plugin |
|
|
62
62
|
| `/evolve goal [objective · done · block]` | round-driven auto-review goal |
|
|
63
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) |
|
|
64
66
|
|
|
65
|
-
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).
|
|
66
68
|
|
|
67
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.
|
|
68
70
|
|
|
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
|
+
|
|
69
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.
|
|
70
74
|
|
|
71
75
|
## Configuration
|
|
@@ -92,6 +96,9 @@ Injection shape: prompt notes and delegation specs inject with content (≤6/kin
|
|
|
92
96
|
| `autoRollbackOnReject` | `true` | deterministic rollback after a benchmark rejection |
|
|
93
97
|
| `autoCase` | `true` | failed evolution attempts are captured as draft regression cases (`auto_regression` benchmark) |
|
|
94
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 |
|
|
95
102
|
|
|
96
103
|
Example profile patch:
|
|
97
104
|
|
|
@@ -106,7 +113,7 @@ Example profile patch:
|
|
|
106
113
|
|
|
107
114
|
```bash
|
|
108
115
|
pnpm install && pnpm build # deps + tsc -> lib/
|
|
109
|
-
pnpm test # vitest (
|
|
116
|
+
pnpm test # vitest (665 tests)
|
|
110
117
|
pnpm test:coverage # v8 coverage, thresholds enforced in CI
|
|
111
118
|
pnpm lint # oxlint src test
|
|
112
119
|
```
|
|
@@ -114,8 +121,8 @@ pnpm lint # oxlint src test
|
|
|
114
121
|
Project layout:
|
|
115
122
|
|
|
116
123
|
```
|
|
117
|
-
├── src/ # engine, tools, commands, gate, fate, benchmark, usage…
|
|
118
|
-
├── test/ # vitest suites (
|
|
124
|
+
├── src/ # engine, tools, commands, gate, fate, benchmark, injection + token usage…
|
|
125
|
+
├── test/ # vitest suites (41 files)
|
|
119
126
|
├── lib/ # build output (tsc)
|
|
120
127
|
├── docs/
|
|
121
128
|
│ ├── design.md # full design doc (hardening matrix)
|
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
|
|
|
@@ -61,11 +61,15 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
|
|
|
61
61
|
| `/evolve mount · unmount <skillId>` | 把可执行技能热挂载为 live 插件 |
|
|
62
62
|
| `/evolve goal [objective · done · block]` | 回合驱动的自进化目标 |
|
|
63
63
|
| `/evolve benchmark …` | 用例生命周期、运行、接受决策 |
|
|
64
|
+
| `/evolve pause · resume · status` | 暂停/恢复自动门禁(手动工具不受影响)、门禁状态 |
|
|
65
|
+
| `/evolve usage` | 每条目注入次数 + review/planner/wrapup/fate 直属调用的 provider 精确 token(不含 benchmark 宿主子代理) |
|
|
64
66
|
|
|
65
|
-
模型工具:`evolve_list / add / update / delete / rollback
|
|
67
|
+
模型工具:`evolve_list / add / update / delete / rollback`(`evolve_delete` 支持 `id` 或批量 `ids` 数组——一次 refinement、一次审批)。
|
|
66
68
|
|
|
67
69
|
第三方消费:每次进化落地(门禁或手动)都会向 `reviews.jsonl` 追加结构化 `evolve_complete` 事件(shape 见 `src/evolve-event.ts`),与人类可读的审计记录并存。
|
|
68
70
|
|
|
71
|
+
`/evolve usage` 还会读取 `evolve/token-usage.jsonl`:插件直属 review、planner、手动 wrapup 与自动 fate 调用的 provider 精确 input/cache/output/total token。报告只覆盖保留尾部而非终身累计,单独显示 provider 未返回 usage 的调用,并明确排除宿主 benchmark 子代理、其 agent-loop 调用与逐条 memory 注入归因。
|
|
72
|
+
|
|
69
73
|
注入形态:prompt 补充与委派规格带内容注入(每 kind ≤6 条 × 180 字符,按相关性排序)。memory/skill 以按相关性排序的目录索引出现(`[memory:type:id] 标题`钩子,15 行封顶 + 折叠计数行)——全文经 `evolve_list` 获取。空 store = 零注入 token。
|
|
70
74
|
|
|
71
75
|
## 配置
|
|
@@ -92,6 +96,9 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
|
|
|
92
96
|
| `autoRollbackOnReject` | `true` | benchmark 拒绝后自动确定性回滚 |
|
|
93
97
|
| `autoCase` | `true` | 失败的进化尝试自动沉淀为 draft 回归用例(`auto_regression` 基准) |
|
|
94
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` 尾行 |
|
|
95
102
|
|
|
96
103
|
profile patch 示例:
|
|
97
104
|
|
|
@@ -106,7 +113,7 @@ profile patch 示例:
|
|
|
106
113
|
|
|
107
114
|
```bash
|
|
108
115
|
pnpm install && pnpm build # 依赖 + tsc -> lib/
|
|
109
|
-
pnpm test # vitest(
|
|
116
|
+
pnpm test # vitest(665 例)
|
|
110
117
|
pnpm test:coverage # v8 覆盖率,CI 强制阈值
|
|
111
118
|
pnpm lint # oxlint src test
|
|
112
119
|
```
|
|
@@ -114,8 +121,8 @@ pnpm lint # oxlint src test
|
|
|
114
121
|
目录结构:
|
|
115
122
|
|
|
116
123
|
```
|
|
117
|
-
├── src/ # 引擎、工具、命令、门禁、fate、benchmark
|
|
118
|
-
├── test/ # vitest 测试套件(
|
|
124
|
+
├── src/ # 引擎、工具、命令、门禁、fate、benchmark、注入 + token 用量…
|
|
125
|
+
├── test/ # vitest 测试套件(41 个文件)
|
|
119
126
|
├── lib/ # 构建产物(tsc)
|
|
120
127
|
├── docs/
|
|
121
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 `
|
|
11
|
-
// A CREATE must never bake that view
|
|
12
|
-
// (observed: global entries literally
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
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;
|
package/lib/auto.js
CHANGED
|
@@ -28,6 +28,8 @@ import { mergeHarnessStates } from "./state.js";
|
|
|
28
28
|
import { projectKeyOf } from "./project.js";
|
|
29
29
|
import { questionServiceOf } from "./approval.js";
|
|
30
30
|
import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
|
|
31
|
+
import { DEFAULT_REVIEWS_RETAIN, pruneJsonlFile } from "./store.js";
|
|
32
|
+
import { isGatePaused } from "./runtime.js";
|
|
31
33
|
import { captureAutoCase } from "./autocase.js";
|
|
32
34
|
/** Turns a rejected skill candidate stays silent before being offered again. */
|
|
33
35
|
export const SKILL_CONSULT_COOLDOWN_TURNS = 10;
|
|
@@ -42,6 +44,14 @@ export function registerAutoReview(ctx, engine, config) {
|
|
|
42
44
|
}
|
|
43
45
|
catch (cause) {
|
|
44
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
|
|
45
55
|
}
|
|
46
56
|
};
|
|
47
57
|
// Turn counting uses `agent/turn-stopping` — empirically the only event
|
|
@@ -67,6 +77,12 @@ export function registerAutoReview(ctx, engine, config) {
|
|
|
67
77
|
const goalDriven = goalDrivesRounds(goalServiceOf(ctx)?.get(agent));
|
|
68
78
|
if (!goalDriven && state.turns - state.lastReviewAt < config.intervalTurns)
|
|
69
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;
|
|
70
86
|
// Run the gate outside the listener turn: agent is idle, work is auxiliary.
|
|
71
87
|
// Every failure is durably recorded — nothing fails silently.
|
|
72
88
|
void runGate(ctx, engine, agent, config, state, "turn_interval", record).catch((cause) => {
|
|
@@ -99,6 +115,12 @@ export function registerAutoReview(ctx, engine, config) {
|
|
|
99
115
|
catch (cause) {
|
|
100
116
|
logger.warn(`failed to write armed marker: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
101
117
|
}
|
|
118
|
+
try {
|
|
119
|
+
pruneJsonlFile(reviewsPath, config.reviewsRetain ?? DEFAULT_REVIEWS_RETAIN);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// ignored — the next record retries
|
|
123
|
+
}
|
|
102
124
|
ctx.on("session/event", (session, event) => {
|
|
103
125
|
if (event.type !== "compaction/start")
|
|
104
126
|
return;
|
|
@@ -107,7 +129,11 @@ export function registerAutoReview(ctx, engine, config) {
|
|
|
107
129
|
if (!agent)
|
|
108
130
|
return; // no live agent for that session (e.g. cold read)
|
|
109
131
|
const state = stateFor(perSession, agent.id);
|
|
110
|
-
// Compaction is unconditional
|
|
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;
|
|
111
137
|
void runGate(ctx, engine, agent, config, state, "compact", record).catch((cause) => {
|
|
112
138
|
logger.warn(`auto-review failed at compaction for ${agent.id}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
113
139
|
record({
|
|
@@ -228,6 +254,12 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
|
|
|
228
254
|
const sessionId = agent.id;
|
|
229
255
|
const turnsSinceLastReview = state.turns - state.lastReviewAt;
|
|
230
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
|
+
};
|
|
231
263
|
const trajectory = await readTrajectory(ctx, agent, config.maxInputChars).catch((cause) => {
|
|
232
264
|
logger.warn(`auto-review skipped for ${sessionId}: trajectory unavailable: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
233
265
|
record({ sessionId, reason, turnsSinceLastReview, outcome: "failed", rationale: `trajectory unavailable: ${cause instanceof Error ? cause.message : String(cause)}` });
|
|
@@ -253,7 +285,16 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
|
|
|
253
285
|
trajectory,
|
|
254
286
|
context: { reason, turnsSinceLastReview },
|
|
255
287
|
budgetTokens: config.budgetTokens,
|
|
288
|
+
tokenUsage,
|
|
256
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
|
+
: {})),
|
|
257
298
|
});
|
|
258
299
|
state.lastReviewAt = state.turns;
|
|
259
300
|
if (!review.shouldRefine) {
|
|
@@ -270,6 +311,15 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
|
|
|
270
311
|
// Read the skill-creator template facts (fallback: builtin distilled
|
|
271
312
|
// guide) so skill proposals follow the standard.
|
|
272
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
|
+
: {})),
|
|
273
323
|
});
|
|
274
324
|
// Skills are governed resources: an auto-created skill is OFFERED to the
|
|
275
325
|
// user for a decision (固化/不固化) before it lands — the gate never
|
|
@@ -317,7 +367,7 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
|
|
|
317
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}`);
|
|
318
368
|
record({ sessionId, reason, turnsSinceLastReview, outcome: "approved", rationale: review.rationale, refinementId: result.id });
|
|
319
369
|
// Gap C4: emit structured evolve_complete event for third-party consumers.
|
|
320
|
-
emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, `auto_review:${reason}`, sessionId));
|
|
370
|
+
emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, `auto_review:${reason}`, sessionId), config.reviewsRetain ?? DEFAULT_REVIEWS_RETAIN);
|
|
321
371
|
// Visibility: tell the user what the gate just persisted. Only the
|
|
322
372
|
// turn-interval path notifies — a compaction-triggered gate must not wake
|
|
323
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
|
/**
|
package/lib/command.js
CHANGED
|
@@ -5,7 +5,8 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { requireGlobalApproval } from "./approval.js";
|
|
7
7
|
import { saveHarnessState } from "./state.js";
|
|
8
|
-
import { appendResult, storePaths } from "./store.js";
|
|
8
|
+
import { appendResult, pruneJsonlFile, storePaths } from "./store.js";
|
|
9
|
+
import { DEFAULT_REFINEMENTS_RETAIN } from "./store.js";
|
|
9
10
|
import { entrySourceOf } from "./source.js";
|
|
10
11
|
import { filterLogBySession, formatLogLine, pluginLogFilePath } from "./logfile.js";
|
|
11
12
|
import { collectFailureSummary, formatFailureSummary } from "./failures.js";
|
|
@@ -14,8 +15,10 @@ import { executeMountCommand, executeUnmountCommand } from "./mount-command.js";
|
|
|
14
15
|
import { executeBenchmarkCommand } from "./benchmark-command.js";
|
|
15
16
|
import { executeWrapupCommand } from "./wrapup-command.js";
|
|
16
17
|
import { projectKeyOf } from "./project.js";
|
|
17
|
-
import { loadUsage } from "./usage.js";
|
|
18
|
+
import { loadUsage, getUsageCount } from "./usage.js";
|
|
19
|
+
import { loadGateRuntime, saveGateRuntime } from "./runtime.js";
|
|
18
20
|
import { planConsolidation } from "./consolidate.js";
|
|
21
|
+
import { loadTokenUsage, renderTokenUsageReport } from "./token-usage.js";
|
|
19
22
|
const USAGE = `Usage:
|
|
20
23
|
/evolve show this help and the current local store
|
|
21
24
|
/evolve list [project|global] list entries (default local staging; "project" = this project's cross-session store, "global" = cross-project store)
|
|
@@ -40,7 +43,10 @@ const USAGE = `Usage:
|
|
|
40
43
|
/evolve unmount <id> remove a hot-mounted plugin
|
|
41
44
|
/evolve goal show the evolution goal (round-driven auto-review)
|
|
42
45
|
/evolve goal <objective> create/update the evolution goal
|
|
43
|
-
/evolve goal done complete the evolution goal
|
|
46
|
+
/evolve goal done complete the evolution goal
|
|
47
|
+
/evolve pause | resume pause/resume the auto-review gate (manual tools and commands keep working)
|
|
48
|
+
/evolve status gate state (patch flag + runtime switch) plus store entry counts
|
|
49
|
+
/evolve usage injection counts + exact direct-call token usage (benchmark subagents excluded)`;
|
|
44
50
|
export function registerEvolveCommand(ctx, engine, opts, runtime) {
|
|
45
51
|
ctx.commands.register({
|
|
46
52
|
name: "evolve",
|
|
@@ -323,6 +329,14 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
|
|
|
323
329
|
appendResult(paths, result);
|
|
324
330
|
}
|
|
325
331
|
}
|
|
332
|
+
// Storage hygiene (#20): an imported history obeys the
|
|
333
|
+
// same tail budget as a live one.
|
|
334
|
+
try {
|
|
335
|
+
pruneJsonlFile(paths.resultsPath, engine.retention?.refinements ?? DEFAULT_REFINEMENTS_RETAIN);
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
// ignored — the next apply retries
|
|
339
|
+
}
|
|
326
340
|
}
|
|
327
341
|
return success(`imported ${scope} store from ${path}`);
|
|
328
342
|
}
|
|
@@ -338,6 +352,12 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
|
|
|
338
352
|
...(instructions ? { instructions } : {}),
|
|
339
353
|
global: scope === "global",
|
|
340
354
|
signal: invocation.signal,
|
|
355
|
+
tokenUsage: {
|
|
356
|
+
baseDir: engine.baseDir,
|
|
357
|
+
sessionId,
|
|
358
|
+
retain: engine.retention.tokenUsage,
|
|
359
|
+
onError: (cause) => ctx.logger("continual-evolve").warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
|
|
360
|
+
},
|
|
341
361
|
// skill-creator template facts (fallback: builtin guide).
|
|
342
362
|
skillsRoot: join(engine.baseDir, "skills"),
|
|
343
363
|
});
|
|
@@ -354,6 +374,27 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
|
|
|
354
374
|
case "wrapup": {
|
|
355
375
|
return await executeWrapupCommand(ctx, engine, invocation, runtime.promotionPolicy);
|
|
356
376
|
}
|
|
377
|
+
case "pause":
|
|
378
|
+
case "resume": {
|
|
379
|
+
// #21 P2 runtime switch: pause the AUTOMATIC gate only. Manual
|
|
380
|
+
// evolve_* tools and /evolve commands keep working — the human
|
|
381
|
+
// is acting explicitly there, so no gate is being bypassed.
|
|
382
|
+
const pausing = sub === "pause";
|
|
383
|
+
const current = loadGateRuntime(engine.baseDir);
|
|
384
|
+
if (current.paused === pausing) {
|
|
385
|
+
return success(`auto-review gate is already ${pausing ? "paused" : "running"} (no change).`);
|
|
386
|
+
}
|
|
387
|
+
saveGateRuntime(engine.baseDir, pausing);
|
|
388
|
+
return success(pausing
|
|
389
|
+
? "auto-review gate paused: no automatic reviews, fate assessments, or gate LLM calls until /evolve resume. Manual evolve_* tools and /evolve commands keep working."
|
|
390
|
+
: "auto-review gate resumed: automatic reviews run again on their configured cadence.");
|
|
391
|
+
}
|
|
392
|
+
case "status": {
|
|
393
|
+
return success(renderGateStatus(engine, sessionId, projectKeyOf(invocation.agent), runtime));
|
|
394
|
+
}
|
|
395
|
+
case "usage": {
|
|
396
|
+
return success(renderUsageReport(engine, sessionId, projectKeyOf(invocation.agent)));
|
|
397
|
+
}
|
|
357
398
|
case "goal": {
|
|
358
399
|
return executeGoalCommand(ctx, invocation, rest);
|
|
359
400
|
}
|
|
@@ -412,6 +453,101 @@ function demoteEntry(engine, id, sessionId, projectKey) {
|
|
|
412
453
|
}
|
|
413
454
|
return error(`entry ${id} not found in the global, project, or local store`);
|
|
414
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* #21 status: one screen answering "is the gate on, and what is in the
|
|
458
|
+
* stores". The patch flag says whether the gate was registered at boot;
|
|
459
|
+
* the runtime switch says whether the human paused it since.
|
|
460
|
+
*/
|
|
461
|
+
function renderGateStatus(engine, sessionId, projectKey, runtime) {
|
|
462
|
+
const paused = loadGateRuntime(engine.baseDir).paused;
|
|
463
|
+
const patch = runtime.autoReview === undefined ? "unknown" : runtime.autoReview ? "on" : "off";
|
|
464
|
+
const gateLine = patch === "off"
|
|
465
|
+
? "gate: disabled in patch config (autoReview off — /evolve pause has nothing to pause)"
|
|
466
|
+
: `gate: ${patch === "unknown" ? "patch flag unknown" : "enabled in patch config"} · ${paused ? "PAUSED by /evolve pause (resume with /evolve resume)" : "running"}`;
|
|
467
|
+
const countEntries = (state) => Object.values(state.entries).reduce((n, byKind) => n + Object.keys(byKind).length, 0);
|
|
468
|
+
const lines = [gateLine];
|
|
469
|
+
lines.push(`stores: global ${countEntries(engine.load("global", undefined))} entries · local(${sessionId}) ${countEntries(engine.load("local", sessionId))} entries`);
|
|
470
|
+
if (projectKey) {
|
|
471
|
+
try {
|
|
472
|
+
lines[lines.length - 1] += ` · project ${countEntries(engine.load("project", projectKey))} entries`;
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
lines[lines.length - 1] += " · project (unavailable)";
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const retention = engine.retention;
|
|
479
|
+
if (retention) {
|
|
480
|
+
lines.push(`retention: snapshots ${retention.snapshots} · refinements ${retention.refinements} · reviews ${retention.reviews} · token usage ${retention.tokenUsage} (historyRetain)`);
|
|
481
|
+
}
|
|
482
|
+
return lines.join("\n");
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* #21 P0 usage ledger: injection counts per entry across the stores the
|
|
486
|
+
* human can see (global + this session + this project). Counts are
|
|
487
|
+
* per-session since the v2 usage shape — "in how many sessions did this
|
|
488
|
+
* entry surface" — the exposure half of the #18a归零实验 verdict.
|
|
489
|
+
*/
|
|
490
|
+
function renderUsageReport(engine, sessionId, projectKey) {
|
|
491
|
+
const store = loadUsage(engine.baseDir);
|
|
492
|
+
const rows = [];
|
|
493
|
+
const seen = new Set();
|
|
494
|
+
const collect = (state) => {
|
|
495
|
+
for (const kind of Object.keys(state.entries)) {
|
|
496
|
+
for (const entry of Object.values(state.entries[kind])) {
|
|
497
|
+
const key = `${kind}:${entry.id}`;
|
|
498
|
+
if (seen.has(key))
|
|
499
|
+
continue;
|
|
500
|
+
seen.add(key);
|
|
501
|
+
const count = getUsageCount(store, kind, entry.id);
|
|
502
|
+
rows.push({
|
|
503
|
+
key,
|
|
504
|
+
title: entry.title,
|
|
505
|
+
count,
|
|
506
|
+
...(store.lastSession?.[`${kind}:${entry.id}`] ? { lastSession: store.lastSession[`${kind}:${entry.id}`] } : {}),
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
collect(engine.load("global", undefined));
|
|
512
|
+
collect(engine.load("local", sessionId));
|
|
513
|
+
if (projectKey) {
|
|
514
|
+
try {
|
|
515
|
+
collect(engine.load("project", projectKey));
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
// project store unavailable here — global+local still report
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const liveKeys = new Set(rows.map((r) => r.key));
|
|
522
|
+
const orphaned = Object.keys(store.counts).filter((k) => !liveKeys.has(k)).length;
|
|
523
|
+
const total = rows.reduce((n, r) => n + r.count, 0);
|
|
524
|
+
const injected = rows.filter((r) => r.count > 0).sort((a, b) => b.count - a.count || (a.key < b.key ? -1 : 1));
|
|
525
|
+
const stale = rows.filter((r) => r.count === 0).sort((a, b) => (a.key < b.key ? -1 : 1));
|
|
526
|
+
const lines = [`injected entries: ${total} injections across ${injected.length} of ${rows.length} stored entries${orphaned > 0 ? ` (+${orphaned} historical key(s) for deleted entries)` : ""}`];
|
|
527
|
+
if (injected.length > 0) {
|
|
528
|
+
lines.push("injected (top 15):");
|
|
529
|
+
for (const row of injected.slice(0, 15)) {
|
|
530
|
+
lines.push(` ${row.key} — ${row.count}× · ${row.title}${row.lastSession ? ` (last in ${row.lastSession})` : ""}`);
|
|
531
|
+
}
|
|
532
|
+
if (injected.length > 15) {
|
|
533
|
+
lines.push(` … and ${injected.length - 15} more`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
lines.push("injected: (none yet — nothing has surfaced into a session prompt)");
|
|
538
|
+
}
|
|
539
|
+
if (stale.length > 0) {
|
|
540
|
+
lines.push(`never injected (${stale.length}):`);
|
|
541
|
+
for (const row of stale.slice(0, 20)) {
|
|
542
|
+
lines.push(` ${row.key} · ${row.title}`);
|
|
543
|
+
}
|
|
544
|
+
if (stale.length > 20) {
|
|
545
|
+
lines.push(` … and ${stale.length - 20} more`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
lines.push("", ...renderTokenUsageReport(loadTokenUsage(engine.baseDir), engine.retention.tokenUsage));
|
|
549
|
+
return lines.join("\n");
|
|
550
|
+
}
|
|
415
551
|
function renderResult(result) {
|
|
416
552
|
const applied = result.appliedEdits.filter((e) => e.applied);
|
|
417
553
|
const failed = result.appliedEdits.filter((e) => !e.applied);
|
package/lib/evolve-event.d.ts
CHANGED
|
@@ -32,7 +32,9 @@ export declare function buildEvolveCompleteEvent(result: RefinementResult, trigg
|
|
|
32
32
|
/**
|
|
33
33
|
* Emit an evolve_complete event to the reviews.jsonl audit trail. The event
|
|
34
34
|
* is JSONL-formatted (one line) so consumers can tail and parse it. This is
|
|
35
|
-
* a best-effort write — failure never blocks the refinement path.
|
|
35
|
+
* a best-effort write — failure never blocks the refinement path. Storage
|
|
36
|
+
* hygiene (#20): the trail is truncated to its tail after the append so the
|
|
37
|
+
* audit file cannot grow without bound; readers only need a recent window.
|
|
36
38
|
*/
|
|
37
|
-
export declare function emitEvolveComplete(baseDir: string, event: EvolveCompleteEvent): void;
|
|
39
|
+
export declare function emitEvolveComplete(baseDir: string, event: EvolveCompleteEvent, reviewsRetain?: number): void;
|
|
38
40
|
//# sourceMappingURL=evolve-event.d.ts.map
|
package/lib/evolve-event.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
+
import { DEFAULT_REVIEWS_RETAIN, pruneJsonlFile, reviewsPath } from "./store.js";
|
|
14
15
|
/** Build a structured evolve-complete event from a refinement result. */
|
|
15
16
|
export function buildEvolveCompleteEvent(result, trigger, sessionId) {
|
|
16
17
|
return {
|
|
@@ -34,9 +35,11 @@ export function buildEvolveCompleteEvent(result, trigger, sessionId) {
|
|
|
34
35
|
/**
|
|
35
36
|
* Emit an evolve_complete event to the reviews.jsonl audit trail. The event
|
|
36
37
|
* is JSONL-formatted (one line) so consumers can tail and parse it. This is
|
|
37
|
-
* a best-effort write — failure never blocks the refinement path.
|
|
38
|
+
* a best-effort write — failure never blocks the refinement path. Storage
|
|
39
|
+
* hygiene (#20): the trail is truncated to its tail after the append so the
|
|
40
|
+
* audit file cannot grow without bound; readers only need a recent window.
|
|
38
41
|
*/
|
|
39
|
-
export function emitEvolveComplete(baseDir, event) {
|
|
42
|
+
export function emitEvolveComplete(baseDir, event, reviewsRetain = DEFAULT_REVIEWS_RETAIN) {
|
|
40
43
|
try {
|
|
41
44
|
const dir = join(baseDir, "evolve");
|
|
42
45
|
mkdirSync(dir, { recursive: true });
|
|
@@ -44,6 +47,13 @@ export function emitEvolveComplete(baseDir, event) {
|
|
|
44
47
|
}
|
|
45
48
|
catch {
|
|
46
49
|
// Event emission is diagnostic; never interrupt the refinement path.
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
pruneJsonlFile(reviewsPath(baseDir), reviewsRetain);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// ignored — the next emission retries
|
|
47
57
|
}
|
|
48
58
|
}
|
|
49
59
|
//# sourceMappingURL=evolve-event.js.map
|
package/lib/fate.js
CHANGED
|
@@ -201,6 +201,12 @@ export async function runLocalFatePhase(ctx, engine, agent, config, state, reaso
|
|
|
201
201
|
return;
|
|
202
202
|
const logger = ctx.logger("continual-evolve");
|
|
203
203
|
const sessionId = agent.id;
|
|
204
|
+
const tokenUsage = {
|
|
205
|
+
baseDir: engine.baseDir,
|
|
206
|
+
sessionId,
|
|
207
|
+
retain: engine.retention.tokenUsage,
|
|
208
|
+
onError: (cause) => logger.warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
|
|
209
|
+
};
|
|
204
210
|
const localState = engine.load("local", sessionId);
|
|
205
211
|
const globalState = engine.load("global", undefined);
|
|
206
212
|
const candidates = listLocalCandidates(localState, globalState, engine.baseDir);
|
|
@@ -218,7 +224,7 @@ export async function runLocalFatePhase(ctx, engine, agent, config, state, reaso
|
|
|
218
224
|
state.lastFateAt = state.turns;
|
|
219
225
|
let assessment;
|
|
220
226
|
try {
|
|
221
|
-
assessment = await assessLocalEntries(ctx, agent, candidates);
|
|
227
|
+
assessment = await assessLocalEntries(ctx, agent, candidates, { tokenUsage, tokenUsagePhase: "fate" });
|
|
222
228
|
}
|
|
223
229
|
catch (cause) {
|
|
224
230
|
const message = cause instanceof Error ? cause.message : String(cause);
|