dsh-continual-evolve 0.10.6 → 0.12.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 +35 -122
- package/README.zh.md +26 -97
- package/cordis.patch.yml +2 -2
- package/lib/index.d.ts +44 -241
- package/lib/index.js +30 -253
- package/lib/memory-guide.d.ts +13 -14
- package/lib/memory-guide.js +32 -21
- package/lib/memory-section.d.ts +77 -0
- package/lib/memory-section.js +189 -0
- package/package.json +2 -17
- package/lib/apply.d.ts +0 -22
- package/lib/apply.js +0 -157
- package/lib/approval.d.ts +0 -72
- package/lib/approval.js +0 -127
- package/lib/auto.d.ts +0 -236
- package/lib/auto.js +0 -773
- package/lib/autocase.d.ts +0 -38
- package/lib/autocase.js +0 -76
- package/lib/benchmark-command.d.ts +0 -9
- package/lib/benchmark-command.js +0 -357
- package/lib/benchmark.d.ts +0 -151
- package/lib/benchmark.js +0 -250
- package/lib/command.d.ts +0 -49
- package/lib/command.js +0 -568
- package/lib/consolidate.d.ts +0 -90
- package/lib/consolidate.js +0 -180
- package/lib/copy.d.ts +0 -74
- package/lib/copy.js +0 -160
- package/lib/declines.d.ts +0 -101
- package/lib/declines.js +0 -175
- package/lib/evaluate.d.ts +0 -74
- package/lib/evaluate.js +0 -271
- package/lib/evolve-event.d.ts +0 -40
- package/lib/evolve-event.js +0 -59
- package/lib/failures.d.ts +0 -42
- package/lib/failures.js +0 -171
- package/lib/fate.d.ts +0 -130
- package/lib/fate.js +0 -350
- package/lib/goal-command.d.ts +0 -7
- package/lib/goal-command.js +0 -37
- package/lib/goal.d.ts +0 -72
- package/lib/goal.js +0 -72
- package/lib/inject.d.ts +0 -196
- package/lib/inject.js +0 -412
- package/lib/llm-text.d.ts +0 -104
- package/lib/llm-text.js +0 -140
- package/lib/logfile.d.ts +0 -71
- package/lib/logfile.js +0 -159
- package/lib/memory-agent.d.ts +0 -129
- package/lib/memory-agent.js +0 -568
- package/lib/memory-benchmark.d.ts +0 -76
- package/lib/memory-benchmark.js +0 -170
- package/lib/memory-index.d.ts +0 -114
- package/lib/memory-index.js +0 -197
- package/lib/message-source.d.ts +0 -23
- package/lib/message-source.js +0 -12
- package/lib/mount-command.d.ts +0 -10
- package/lib/mount-command.js +0 -48
- package/lib/mount.d.ts +0 -42
- package/lib/mount.js +0 -212
- package/lib/notify.d.ts +0 -59
- package/lib/notify.js +0 -97
- package/lib/plan.d.ts +0 -16
- package/lib/plan.js +0 -126
- package/lib/planner.d.ts +0 -61
- package/lib/planner.js +0 -144
- package/lib/pool.d.ts +0 -7
- package/lib/pool.js +0 -25
- package/lib/prefix-cache.d.ts +0 -78
- package/lib/prefix-cache.js +0 -168
- package/lib/project.d.ts +0 -28
- package/lib/project.js +0 -66
- package/lib/projection.d.ts +0 -22
- package/lib/projection.js +0 -112
- package/lib/promotion.d.ts +0 -94
- package/lib/promotion.js +0 -171
- package/lib/recall.d.ts +0 -85
- package/lib/recall.js +0 -142
- package/lib/record-language.d.ts +0 -85
- package/lib/record-language.js +0 -152
- package/lib/render.d.ts +0 -13
- package/lib/render.js +0 -80
- package/lib/review-scheduler.d.ts +0 -38
- package/lib/review-scheduler.js +0 -191
- package/lib/review.d.ts +0 -60
- package/lib/review.js +0 -145
- package/lib/rollback.d.ts +0 -9
- package/lib/rollback.js +0 -68
- package/lib/rubric.d.ts +0 -29
- package/lib/rubric.js +0 -119
- package/lib/runtime.d.ts +0 -24
- package/lib/runtime.js +0 -87
- package/lib/score.d.ts +0 -64
- package/lib/score.js +0 -191
- package/lib/search.d.ts +0 -83
- package/lib/search.js +0 -136
- package/lib/service.d.ts +0 -49
- package/lib/service.js +0 -180
- package/lib/skill-render.d.ts +0 -23
- package/lib/skill-render.js +0 -69
- package/lib/skill.d.ts +0 -15
- package/lib/skill.js +0 -80
- package/lib/skillquality.d.ts +0 -85
- package/lib/skillquality.js +0 -320
- package/lib/source.d.ts +0 -30
- package/lib/source.js +0 -43
- package/lib/state.d.ts +0 -26
- package/lib/state.js +0 -151
- package/lib/store.d.ts +0 -62
- package/lib/store.js +0 -164
- package/lib/token-usage.d.ts +0 -68
- package/lib/token-usage.js +0 -211
- package/lib/tool.d.ts +0 -34
- package/lib/tool.js +0 -320
- package/lib/turn-snapshot.d.ts +0 -72
- package/lib/turn-snapshot.js +0 -238
- package/lib/types.d.ts +0 -208
- package/lib/types.js +0 -127
- package/lib/usage.d.ts +0 -36
- package/lib/usage.js +0 -98
- package/lib/validate.d.ts +0 -28
- package/lib/validate.js +0 -181
- package/lib/wrapup-command.d.ts +0 -10
- package/lib/wrapup-command.js +0 -243
- package/lib/wrapup.d.ts +0 -260
- package/lib/wrapup.js +0 -539
package/lib/command.js
DELETED
|
@@ -1,568 +0,0 @@
|
|
|
1
|
-
import { ARCHIVED_AT_KEY } from "./types.js";
|
|
2
|
-
import { formatHarnessStateForPrompt, historyForPrompt } from "./render.js";
|
|
3
|
-
import { planWithLlm } from "./planner.js";
|
|
4
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { requireGlobalApproval } from "./approval.js";
|
|
7
|
-
import { saveHarnessState } from "./state.js";
|
|
8
|
-
import { appendResult, pruneJsonlFile, storePaths } from "./store.js";
|
|
9
|
-
import { DEFAULT_REFINEMENTS_RETAIN } from "./store.js";
|
|
10
|
-
import { entrySourceOf } from "./source.js";
|
|
11
|
-
import { filterLogBySession, formatLogLine, pluginLogFilePath } from "./logfile.js";
|
|
12
|
-
import { collectFailureSummary, formatFailureSummary } from "./failures.js";
|
|
13
|
-
import { executeGoalCommand } from "./goal-command.js";
|
|
14
|
-
import { executeMountCommand, executeUnmountCommand } from "./mount-command.js";
|
|
15
|
-
import { executeBenchmarkCommand } from "./benchmark-command.js";
|
|
16
|
-
import { executeWrapupCommand } from "./wrapup-command.js";
|
|
17
|
-
import { projectKeyOf } from "./project.js";
|
|
18
|
-
import { loadUsage, getUsageCount } from "./usage.js";
|
|
19
|
-
import { loadGateRuntime, saveGateRuntime } from "./runtime.js";
|
|
20
|
-
import { planConsolidation } from "./consolidate.js";
|
|
21
|
-
import { loadTokenUsage, renderTokenUsageReport } from "./token-usage.js";
|
|
22
|
-
const USAGE_COMMON = `Usage:
|
|
23
|
-
/evolve show this help and the current local store
|
|
24
|
-
/evolve list [project|global] list entries (default local staging; "project" = this project's cross-session store, "global" = cross-project store)
|
|
25
|
-
/evolve history [global] show applied refinements (rollback ids)
|
|
26
|
-
/evolve rollback <id> [global] deterministically revert a refinement
|
|
27
|
-
/evolve plan [msg] run the LLM planner against the current store
|
|
28
|
-
/evolve wrapup assess this session's local entries: promote reusable ones
|
|
29
|
-
to the global store (approval required), archive one-offs
|
|
30
|
-
/evolve archive <id> [global] hide an entry from injection (data kept, restorable)
|
|
31
|
-
/evolve unarchive <id> [global] restore an archived entry
|
|
32
|
-
/evolve pause | resume pause/resume the automatic Memory Agent (manual tools and commands keep working)
|
|
33
|
-
/evolve status automatic Memory Agent state plus store entry counts
|
|
34
|
-
/evolve help all every subcommand, including the advanced ones below`;
|
|
35
|
-
const USAGE_ADVANCED = `Advanced:
|
|
36
|
-
/evolve consolidate [apply] [merge]
|
|
37
|
-
report (or apply) a batch archive of conflict-hinted
|
|
38
|
-
and stale zero-use global entries; "merge" folds
|
|
39
|
-
near-duplicate content into the surviving original
|
|
40
|
-
/evolve failures aggregated failure counts (gate + benchmark, by class)
|
|
41
|
-
/evolve log [tail N] show the recent plugin log (default 50 lines)
|
|
42
|
-
/evolve export [global] <path> backup a store to a JSON file
|
|
43
|
-
/evolve import [global] <path> restore a store from an export file
|
|
44
|
-
/evolve mount <skillId> hot-mount a skill entry as a live cordis plugin
|
|
45
|
-
/evolve mount list list hot-mounted plugins
|
|
46
|
-
/evolve unmount <id> remove a hot-mounted plugin
|
|
47
|
-
/evolve goal show the evolution goal (round-driven auto-review)
|
|
48
|
-
/evolve goal <objective> create/update the evolution goal
|
|
49
|
-
/evolve goal done complete the evolution goal
|
|
50
|
-
/evolve benchmark ... case lifecycle, runs, acceptance
|
|
51
|
-
/evolve usage injection counts + exact direct-call token usage (benchmark subagents excluded)`;
|
|
52
|
-
/** Default help (`/evolve`, `/evolve help`): the commands worth knowing. */
|
|
53
|
-
const USAGE = USAGE_COMMON;
|
|
54
|
-
export function registerEvolveCommand(ctx, engine, opts, runtime) {
|
|
55
|
-
ctx.commands.register({
|
|
56
|
-
name: "evolve",
|
|
57
|
-
description: "自进化:检查并演进 harness 状态(记忆/技能/提示词/子代理) | Inspect and evolve the continual harness state (memories, skills, prompt notes, subagent specs)",
|
|
58
|
-
input: { hint: "[list [global] | history [global] | rollback <id> [global] | plan [msg] | help all]" },
|
|
59
|
-
handler: (invocation) => executeEvolveCommand(ctx, engine, invocation, opts, runtime),
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
function scopeArg(tokens) {
|
|
63
|
-
if (tokens[0] === "global") {
|
|
64
|
-
return { scope: "global", rest: tokens.slice(1) };
|
|
65
|
-
}
|
|
66
|
-
if (tokens[0] === "project") {
|
|
67
|
-
return { scope: "project", rest: tokens.slice(1) };
|
|
68
|
-
}
|
|
69
|
-
return { scope: "local", rest: tokens };
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* The store id a scope reads/writes from the human command: live session id
|
|
73
|
-
* for local, derived project key for project, undefined for global. Throws
|
|
74
|
-
* for project when the session cwd is unavailable.
|
|
75
|
-
*/
|
|
76
|
-
function storeIdForCommand(scope, invocation) {
|
|
77
|
-
if (scope === "local") {
|
|
78
|
-
return invocation.agent.id;
|
|
79
|
-
}
|
|
80
|
-
if (scope === "project") {
|
|
81
|
-
const key = projectKeyOf(invocation.agent);
|
|
82
|
-
if (!key) {
|
|
83
|
-
throw new Error("project scope needs the session cwd (unavailable here) — use local or global instead");
|
|
84
|
-
}
|
|
85
|
-
return key;
|
|
86
|
-
}
|
|
87
|
-
return undefined;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Tokenize a command's raw input with shell-like quoting:
|
|
91
|
-
* - a `#` outside quotes starts a comment (rest of the line is dropped);
|
|
92
|
-
* - whitespace separates tokens;
|
|
93
|
-
* - double or single quotes group words into one token and are stripped.
|
|
94
|
-
*
|
|
95
|
-
* This lets users paste help-text examples verbatim, e.g.
|
|
96
|
-
* `/evolve benchmark add-case <bid> "<title>" "<statement>" "<rubric>"`.
|
|
97
|
-
*/
|
|
98
|
-
export function tokenizeEvolveInput(rawInput) {
|
|
99
|
-
const tokens = [];
|
|
100
|
-
let current = "";
|
|
101
|
-
let quote = null;
|
|
102
|
-
for (const char of rawInput) {
|
|
103
|
-
if (quote !== null) {
|
|
104
|
-
if (char === quote) {
|
|
105
|
-
quote = null;
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
current += char;
|
|
109
|
-
}
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
if (char === '"' || char === "'") {
|
|
113
|
-
quote = char;
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
if (char === "#") {
|
|
117
|
-
break; // rest of the line is a comment
|
|
118
|
-
}
|
|
119
|
-
if (/\s/.test(char)) {
|
|
120
|
-
if (current.length > 0) {
|
|
121
|
-
tokens.push(current);
|
|
122
|
-
current = "";
|
|
123
|
-
}
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
current += char;
|
|
127
|
-
}
|
|
128
|
-
if (current.length > 0) {
|
|
129
|
-
tokens.push(current);
|
|
130
|
-
}
|
|
131
|
-
return tokens;
|
|
132
|
-
}
|
|
133
|
-
/** Accept both `<id>` (help-text placeholder form) and bare `id`. */
|
|
134
|
-
export function stripAngleBrackets(value) {
|
|
135
|
-
return value.replace(/^<|>$/g, "");
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Locate an entry by id across every kind of a store. Ids are only unique
|
|
139
|
-
* within a kind, so the lookup scans all four and returns the first match
|
|
140
|
-
* (kind + entry) or undefined. Used by archive/unarchive, which take a bare
|
|
141
|
-
* id from the user.
|
|
142
|
-
*/
|
|
143
|
-
export function findEntryById(state, id) {
|
|
144
|
-
for (const kind of Object.keys(state.entries)) {
|
|
145
|
-
const entry = state.entries[kind][id];
|
|
146
|
-
if (entry) {
|
|
147
|
-
return [kind, entry];
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return undefined;
|
|
151
|
-
}
|
|
152
|
-
async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
|
|
153
|
-
const tokens = tokenizeEvolveInput(invocation.rawInput);
|
|
154
|
-
const sub = tokens[0] ?? "";
|
|
155
|
-
const rest = tokens.slice(1);
|
|
156
|
-
const sessionId = invocation.agent.id;
|
|
157
|
-
try {
|
|
158
|
-
switch (sub) {
|
|
159
|
-
case "":
|
|
160
|
-
case "help":
|
|
161
|
-
// Layered help (2026-10-01): the default listing stays short;
|
|
162
|
-
// `help all` expands the advanced half.
|
|
163
|
-
return success(`${rest[0] === "all" ? `${USAGE_COMMON}\n\n${USAGE_ADVANCED}` : USAGE_COMMON}\n\n${formatHarnessStateForPrompt(engine.load("local", sessionId))}`);
|
|
164
|
-
case "list": {
|
|
165
|
-
const { scope } = scopeArg(rest);
|
|
166
|
-
return success(formatHarnessStateForPrompt(engine.load(scope, storeIdForCommand(scope, invocation))));
|
|
167
|
-
}
|
|
168
|
-
case "history": {
|
|
169
|
-
const { scope } = scopeArg(rest);
|
|
170
|
-
const history = engine.history(scope, storeIdForCommand(scope, invocation));
|
|
171
|
-
return success(historyForPrompt(history) || "(no refinements yet)");
|
|
172
|
-
}
|
|
173
|
-
case "rollback": {
|
|
174
|
-
const { scope, rest: after } = scopeArg(rest);
|
|
175
|
-
const id = stripAngleBrackets(after[0] ?? "");
|
|
176
|
-
if (!id) {
|
|
177
|
-
return error(`rollback requires a refinement id.\n${USAGE}`);
|
|
178
|
-
}
|
|
179
|
-
const result = engine.rollback(scope, storeIdForCommand(scope, invocation), id);
|
|
180
|
-
return success(renderResult(result));
|
|
181
|
-
}
|
|
182
|
-
case "archive":
|
|
183
|
-
case "unarchive": {
|
|
184
|
-
const { scope, rest: after } = scopeArg(rest);
|
|
185
|
-
const id = stripAngleBrackets(after[0] ?? "");
|
|
186
|
-
if (!id) {
|
|
187
|
-
return error(`${sub} requires an entry id.\n${USAGE}`);
|
|
188
|
-
}
|
|
189
|
-
const state = engine.load(scope, storeIdForCommand(scope, invocation));
|
|
190
|
-
const found = findEntryById(state, id);
|
|
191
|
-
if (!found) {
|
|
192
|
-
return error(`entry ${id} not found in the ${scope} store`);
|
|
193
|
-
}
|
|
194
|
-
const [kind, entry] = found;
|
|
195
|
-
const metadata = { ...entry.metadata };
|
|
196
|
-
if (sub === "archive") {
|
|
197
|
-
metadata[ARCHIVED_AT_KEY] = new Date().toISOString();
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
delete metadata[ARCHIVED_AT_KEY];
|
|
201
|
-
}
|
|
202
|
-
const archived = sub === "archive";
|
|
203
|
-
const result = engine.apply(scope, storeIdForCommand(scope, invocation), {
|
|
204
|
-
summary: `${archived ? "Archive" : "Unarchive"} entry ${kind}:${id}`,
|
|
205
|
-
rationale: "Human-invoked archive/unarchive via the /evolve command.",
|
|
206
|
-
expectedOutcome: `Entry ${archived ? "is hidden from injection (data kept, restorable)" : "is injected again"}.`,
|
|
207
|
-
edits: [{ action: "update", kind, id, title: entry.title, content: entry.content, metadata }],
|
|
208
|
-
}, { scope });
|
|
209
|
-
return success(renderResult(result));
|
|
210
|
-
}
|
|
211
|
-
case "consolidate": {
|
|
212
|
-
// R3: deterministic global-store hygiene. Report by default;
|
|
213
|
-
// `apply` re-scans fresh state and lands the whole batch as ONE
|
|
214
|
-
// refinement (single snapshot + audit record, fully rollback-able).
|
|
215
|
-
// `merge` (P1 反膨胀) additionally merges conflict-pair content
|
|
216
|
-
// into the surviving original instead of only archiving.
|
|
217
|
-
const apply = rest[0] === "apply";
|
|
218
|
-
const merge = rest.includes("merge");
|
|
219
|
-
const state = engine.load("global", undefined);
|
|
220
|
-
const { candidates, edits } = planConsolidation(state, loadUsage(engine.baseDir), Date.now(), { mergeDuplicates: merge });
|
|
221
|
-
if (candidates.length === 0) {
|
|
222
|
-
return success("global store is already consolidated — no conflict-hinted or stale zero-use entries.");
|
|
223
|
-
}
|
|
224
|
-
const mergeCount = merge ? candidates.filter((candidate) => candidate.mergeInto).length : 0;
|
|
225
|
-
const report = candidates
|
|
226
|
-
.map((candidate, index) => {
|
|
227
|
-
const mergeNote = candidate.mergeInto && merge ? ` → 内容并入 ${candidate.mergeInto.id}` : "";
|
|
228
|
-
return `${index + 1}. [${candidate.kind}:${candidate.id}] ${candidate.title}${mergeNote}\n ${candidate.reason}`;
|
|
229
|
-
})
|
|
230
|
-
.join("\n");
|
|
231
|
-
if (!apply) {
|
|
232
|
-
return success(`consolidation plan — ${candidates.length} archive candidate(s):\n${report}\n(run "/evolve consolidate apply" to archive all of them in one refinement; add "merge" to fold near-duplicate content into the survivors)`);
|
|
233
|
-
}
|
|
234
|
-
const result = engine.apply("global", undefined, {
|
|
235
|
-
summary: `Consolidate global store: archive ${candidates.length} entries${mergeCount > 0 ? `, merge ${mergeCount} into survivors` : ""}`,
|
|
236
|
-
rationale: "Human-invoked batch consolidation via /evolve consolidate apply.",
|
|
237
|
-
expectedOutcome: "Candidate entries are hidden from injection (data kept; restorable via /evolve unarchive). Merged survivors carry the near-duplicate content with a mergedFrom provenance stamp.",
|
|
238
|
-
edits,
|
|
239
|
-
}, { scope: "global" });
|
|
240
|
-
return success(`${report}\n\napplied:\n${renderResult(result)}`);
|
|
241
|
-
}
|
|
242
|
-
case "failures": {
|
|
243
|
-
// /evolve failures — failure-signature aggregation (D1 observation):
|
|
244
|
-
// failed review-gate records + failed benchmark cells, counted by class.
|
|
245
|
-
const { summary, records } = collectFailureSummary(engine.baseDir);
|
|
246
|
-
const parts = formatFailureSummary(summary).split("\n");
|
|
247
|
-
const recent = records
|
|
248
|
-
.sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? ""))
|
|
249
|
-
.slice(0, 10)
|
|
250
|
-
.map((f) => ` [${f.timestamp ?? "(benchmark)"}] ${f.kind} · ${f.source}: ${f.message.slice(0, 140)}`);
|
|
251
|
-
if (recent.length > 0) {
|
|
252
|
-
parts.push("recent 10:");
|
|
253
|
-
parts.push(...recent);
|
|
254
|
-
}
|
|
255
|
-
return success(parts.join("\n"));
|
|
256
|
-
}
|
|
257
|
-
case "log": {
|
|
258
|
-
// /evolve log [tail N] [session <sessionId>]
|
|
259
|
-
let tail = 50;
|
|
260
|
-
let sessionFilter;
|
|
261
|
-
for (let i = 0; i < rest.length; i += 1) {
|
|
262
|
-
const token = rest[i] ?? "";
|
|
263
|
-
if (token === "session") {
|
|
264
|
-
sessionFilter = stripAngleBrackets(rest[i + 1] ?? "");
|
|
265
|
-
if (!sessionFilter) {
|
|
266
|
-
return error(`log session requires a session id (e.g. /evolve log session session-abc123).\n${USAGE}`);
|
|
267
|
-
}
|
|
268
|
-
i += 1;
|
|
269
|
-
}
|
|
270
|
-
else {
|
|
271
|
-
tail = Math.min(Math.max(parsePositiveInt(token, "tail"), 1), 1000);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
const path = pluginLogFilePath(engine.baseDir);
|
|
275
|
-
if (!existsSync(path)) {
|
|
276
|
-
return success(`(no plugin log yet — ${path} is created on the first log message)`);
|
|
277
|
-
}
|
|
278
|
-
const lines = readFileSync(path, "utf8").trimEnd().split("\n").filter((line) => line.length > 0);
|
|
279
|
-
if (lines.length === 0) {
|
|
280
|
-
return success(`(empty plugin log: ${path})`);
|
|
281
|
-
}
|
|
282
|
-
const filtered = sessionFilter ? filterLogBySession(lines, sessionFilter) : lines;
|
|
283
|
-
const shown = filtered.slice(-tail);
|
|
284
|
-
const scopeNote = sessionFilter ? `, ${filtered.length} for session ${sessionFilter}` : "";
|
|
285
|
-
return success(`plugin log ${path} (${lines.length} lines${scopeNote}, showing last ${shown.length}):\n${shown.map(formatLogLine).join("\n")}`);
|
|
286
|
-
}
|
|
287
|
-
case "export": {
|
|
288
|
-
const { scope, rest: after } = scopeArg(rest);
|
|
289
|
-
const path = after[0];
|
|
290
|
-
if (!path) {
|
|
291
|
-
return error(`export requires an output path.\n${USAGE}`);
|
|
292
|
-
}
|
|
293
|
-
const state = engine.load(scope, storeIdForCommand(scope, invocation));
|
|
294
|
-
const history = engine.history(scope, storeIdForCommand(scope, invocation));
|
|
295
|
-
const payload = {
|
|
296
|
-
version: 1,
|
|
297
|
-
scope,
|
|
298
|
-
schema: state.schema,
|
|
299
|
-
entries: state.entries,
|
|
300
|
-
refinements: state.refinements,
|
|
301
|
-
history,
|
|
302
|
-
};
|
|
303
|
-
writeFileSync(path, `${JSON.stringify(payload, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
304
|
-
return success(`exported ${scope} store (${Object.values(state.entries).reduce((n, e) => n + Object.keys(e).length, 0)} entries, ${history.length} refinements) to ${path}`);
|
|
305
|
-
}
|
|
306
|
-
case "import": {
|
|
307
|
-
const { scope, rest: after } = scopeArg(rest);
|
|
308
|
-
const path = after[0];
|
|
309
|
-
if (!path) {
|
|
310
|
-
return error(`import requires an input path.\n${USAGE}`);
|
|
311
|
-
}
|
|
312
|
-
const payload = JSON.parse(readFileSync(path, "utf8"));
|
|
313
|
-
if (!isValidExport(payload)) {
|
|
314
|
-
return error(`invalid export file shape: expected {version, entries: {prompt, memory, skill, subagent}, refinements, history}`);
|
|
315
|
-
}
|
|
316
|
-
const state = {
|
|
317
|
-
schema: typeof payload["schema"] === "number" ? payload["schema"] : 1,
|
|
318
|
-
entries: {
|
|
319
|
-
prompt: toEntryRecord(payload["entries"]["prompt"]),
|
|
320
|
-
memory: toEntryRecord(payload["entries"]["memory"]),
|
|
321
|
-
skill: toEntryRecord(payload["entries"]["skill"]),
|
|
322
|
-
subagent: toEntryRecord(payload["entries"]["subagent"]),
|
|
323
|
-
},
|
|
324
|
-
refinements: Array.isArray(payload["refinements"]) ? payload["refinements"] : [],
|
|
325
|
-
};
|
|
326
|
-
const paths = storePaths(engine.baseDir, scope, storeIdForCommand(scope, invocation));
|
|
327
|
-
saveHarnessState(paths.stateDir, state);
|
|
328
|
-
if (Array.isArray(payload["history"])) {
|
|
329
|
-
for (const result of payload["history"]) {
|
|
330
|
-
if (isResultRecord(result)) {
|
|
331
|
-
appendResult(paths, result);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
// Storage hygiene (#20): an imported history obeys the
|
|
335
|
-
// same tail budget as a live one.
|
|
336
|
-
try {
|
|
337
|
-
pruneJsonlFile(paths.resultsPath, engine.retention?.refinements ?? DEFAULT_REFINEMENTS_RETAIN);
|
|
338
|
-
}
|
|
339
|
-
catch {
|
|
340
|
-
// ignored — the next apply retries
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return success(`imported ${scope} store from ${path}`);
|
|
344
|
-
}
|
|
345
|
-
case "plan": {
|
|
346
|
-
const { scope, rest: after } = scopeArg(rest);
|
|
347
|
-
const instructions = after.length > 0 ? after.join(" ") : undefined;
|
|
348
|
-
const state = engine.load(scope, storeIdForCommand(scope, invocation));
|
|
349
|
-
const history = engine.history(scope, storeIdForCommand(scope, invocation));
|
|
350
|
-
const proposal = await planWithLlm(ctx, {
|
|
351
|
-
agent: invocation.agent,
|
|
352
|
-
state,
|
|
353
|
-
history,
|
|
354
|
-
...(instructions ? { instructions } : {}),
|
|
355
|
-
global: scope === "global",
|
|
356
|
-
signal: invocation.signal,
|
|
357
|
-
tokenUsage: {
|
|
358
|
-
baseDir: engine.baseDir,
|
|
359
|
-
sessionId,
|
|
360
|
-
retain: engine.retention.tokenUsage,
|
|
361
|
-
onError: (cause) => ctx.logger("continual-evolve").warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
|
|
362
|
-
},
|
|
363
|
-
// skill-creator template facts (fallback: builtin guide).
|
|
364
|
-
skillsRoot: join(engine.baseDir, "skills"),
|
|
365
|
-
});
|
|
366
|
-
if ((scope === "global" || scope === "project") && opts.requireGlobalApproval && proposal.edits.length > 0) {
|
|
367
|
-
await requireGlobalApproval(ctx, invocation.agent, invocation.signal, `/evolve plan ${scope} 将应用 ${proposal.edits.length} 条编辑到${scope === "project" ? "本项目" : "跨会话"} store:${proposal.summary}`, runtime.recordLanguage);
|
|
368
|
-
}
|
|
369
|
-
const result = engine.apply(scope, storeIdForCommand(scope, invocation), proposal, {
|
|
370
|
-
scope,
|
|
371
|
-
baselineState: state,
|
|
372
|
-
...(entrySourceOf(invocation.agent, sessionId) ? { source: entrySourceOf(invocation.agent, sessionId) } : {}),
|
|
373
|
-
});
|
|
374
|
-
return success(renderResult(result));
|
|
375
|
-
}
|
|
376
|
-
case "wrapup": {
|
|
377
|
-
return await executeWrapupCommand(ctx, engine, invocation, runtime.promotionPolicy, runtime.recordLanguage);
|
|
378
|
-
}
|
|
379
|
-
case "pause":
|
|
380
|
-
case "resume": {
|
|
381
|
-
// The switch is independent of the static registration flag. A
|
|
382
|
-
// resume explicitly enables the Memory Agent; a pause leaves the
|
|
383
|
-
// enable bit intact so status explains the paused state.
|
|
384
|
-
const pausing = sub === "pause";
|
|
385
|
-
const defaultEnabled = runtime.autoReview ?? false;
|
|
386
|
-
const current = loadGateRuntime(engine.baseDir, defaultEnabled);
|
|
387
|
-
if (pausing && current.paused) {
|
|
388
|
-
return success("automatic Memory Agent is already paused (no change).");
|
|
389
|
-
}
|
|
390
|
-
if (!pausing && current.enabled && !current.paused) {
|
|
391
|
-
return success("automatic Memory Agent is already running (no change).");
|
|
392
|
-
}
|
|
393
|
-
saveGateRuntime(engine.baseDir, pausing, pausing ? current.enabled : true);
|
|
394
|
-
return success(pausing
|
|
395
|
-
? "automatic Memory Agent paused: no automatic memory extraction calls until /evolve resume. Manual evolve_* tools and /evolve commands keep working."
|
|
396
|
-
: "automatic Memory Agent resumed: successful turns will produce incremental memory snapshots immediately.");
|
|
397
|
-
}
|
|
398
|
-
case "status": {
|
|
399
|
-
return success(renderGateStatus(engine, sessionId, projectKeyOf(invocation.agent), runtime));
|
|
400
|
-
}
|
|
401
|
-
case "usage": {
|
|
402
|
-
return success(renderUsageReport(engine, sessionId, projectKeyOf(invocation.agent)));
|
|
403
|
-
}
|
|
404
|
-
case "goal": {
|
|
405
|
-
return executeGoalCommand(ctx, invocation, rest);
|
|
406
|
-
}
|
|
407
|
-
case "mount": {
|
|
408
|
-
return await executeMountCommand(ctx, engine, invocation, rest);
|
|
409
|
-
}
|
|
410
|
-
case "unmount": {
|
|
411
|
-
return await executeUnmountCommand(ctx, engine, rest);
|
|
412
|
-
}
|
|
413
|
-
case "benchmark": {
|
|
414
|
-
return await executeBenchmarkCommand(ctx, engine, invocation, rest, runtime);
|
|
415
|
-
}
|
|
416
|
-
default:
|
|
417
|
-
return error(`unknown subcommand: ${sub}\n${USAGE}`);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
catch (cause) {
|
|
421
|
-
return error(cause instanceof Error ? cause.message : String(cause));
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
/**
|
|
425
|
-
* Status answers whether the automatic listener is wired and what
|
|
426
|
-
* stores/retention it currently sees.
|
|
427
|
-
*/
|
|
428
|
-
function renderGateStatus(engine, sessionId, projectKey, runtime) {
|
|
429
|
-
const configuredDefault = runtime.autoReview === undefined ? "unknown" : runtime.autoReview ? "on" : "off";
|
|
430
|
-
const current = loadGateRuntime(engine.baseDir, runtime.autoReview ?? false);
|
|
431
|
-
const effective = current.paused
|
|
432
|
-
? "PAUSED (resume with /evolve resume)"
|
|
433
|
-
: current.enabled
|
|
434
|
-
? "Memory Agent running"
|
|
435
|
-
: "Memory Agent off (enable with /evolve resume)";
|
|
436
|
-
const gateLine = `gate: Memory Agent-only listener registered · config default ${configuredDefault} · runtime ${effective}`;
|
|
437
|
-
const countEntries = (state) => Object.values(state.entries).reduce((n, byKind) => n + Object.keys(byKind).length, 0);
|
|
438
|
-
const lines = [gateLine];
|
|
439
|
-
lines.push(`stores: global ${countEntries(engine.load("global", undefined))} entries · local(${sessionId}) ${countEntries(engine.load("local", sessionId))} entries`);
|
|
440
|
-
if (projectKey) {
|
|
441
|
-
try {
|
|
442
|
-
lines[lines.length - 1] += ` · project ${countEntries(engine.load("project", projectKey))} entries`;
|
|
443
|
-
}
|
|
444
|
-
catch {
|
|
445
|
-
lines[lines.length - 1] += " · project (unavailable)";
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
const retention = engine.retention;
|
|
449
|
-
if (retention) {
|
|
450
|
-
lines.push(`retention: snapshots ${retention.snapshots} · refinements ${retention.refinements} · reviews ${retention.reviews} · token usage ${retention.tokenUsage} (historyRetain)`);
|
|
451
|
-
}
|
|
452
|
-
return lines.join("\n");
|
|
453
|
-
}
|
|
454
|
-
/**
|
|
455
|
-
* #21 P0 usage ledger: injection counts per entry across the stores the
|
|
456
|
-
* human can see (global + this session + this project). Counts are
|
|
457
|
-
* per-session since the v2 usage shape — "in how many sessions did this
|
|
458
|
-
* entry surface" — the exposure half of the #18a归零实验 verdict.
|
|
459
|
-
*/
|
|
460
|
-
function renderUsageReport(engine, sessionId, projectKey) {
|
|
461
|
-
const store = loadUsage(engine.baseDir);
|
|
462
|
-
const rows = [];
|
|
463
|
-
const seen = new Set();
|
|
464
|
-
const collect = (state) => {
|
|
465
|
-
for (const kind of Object.keys(state.entries)) {
|
|
466
|
-
for (const entry of Object.values(state.entries[kind])) {
|
|
467
|
-
const key = `${kind}:${entry.id}`;
|
|
468
|
-
if (seen.has(key))
|
|
469
|
-
continue;
|
|
470
|
-
seen.add(key);
|
|
471
|
-
const count = getUsageCount(store, kind, entry.id);
|
|
472
|
-
rows.push({
|
|
473
|
-
key,
|
|
474
|
-
title: entry.title,
|
|
475
|
-
count,
|
|
476
|
-
...(store.lastSession?.[`${kind}:${entry.id}`] ? { lastSession: store.lastSession[`${kind}:${entry.id}`] } : {}),
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
};
|
|
481
|
-
collect(engine.load("global", undefined));
|
|
482
|
-
collect(engine.load("local", sessionId));
|
|
483
|
-
if (projectKey) {
|
|
484
|
-
try {
|
|
485
|
-
collect(engine.load("project", projectKey));
|
|
486
|
-
}
|
|
487
|
-
catch {
|
|
488
|
-
// project store unavailable here — global+local still report
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
const liveKeys = new Set(rows.map((r) => r.key));
|
|
492
|
-
const orphaned = Object.keys(store.counts).filter((k) => !liveKeys.has(k)).length;
|
|
493
|
-
const total = rows.reduce((n, r) => n + r.count, 0);
|
|
494
|
-
const injected = rows.filter((r) => r.count > 0).sort((a, b) => b.count - a.count || (a.key < b.key ? -1 : 1));
|
|
495
|
-
const stale = rows.filter((r) => r.count === 0).sort((a, b) => (a.key < b.key ? -1 : 1));
|
|
496
|
-
const lines = [`injected entries: ${total} injections across ${injected.length} of ${rows.length} stored entries${orphaned > 0 ? ` (+${orphaned} historical key(s) for deleted entries)` : ""}`];
|
|
497
|
-
if (injected.length > 0) {
|
|
498
|
-
lines.push("injected (top 15):");
|
|
499
|
-
for (const row of injected.slice(0, 15)) {
|
|
500
|
-
lines.push(` ${row.key} — ${row.count}× · ${row.title}${row.lastSession ? ` (last in ${row.lastSession})` : ""}`);
|
|
501
|
-
}
|
|
502
|
-
if (injected.length > 15) {
|
|
503
|
-
lines.push(` … and ${injected.length - 15} more`);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
else {
|
|
507
|
-
lines.push("injected: (none yet — nothing has surfaced into a session prompt)");
|
|
508
|
-
}
|
|
509
|
-
if (stale.length > 0) {
|
|
510
|
-
lines.push(`never injected (${stale.length}):`);
|
|
511
|
-
for (const row of stale.slice(0, 20)) {
|
|
512
|
-
lines.push(` ${row.key} · ${row.title}`);
|
|
513
|
-
}
|
|
514
|
-
if (stale.length > 20) {
|
|
515
|
-
lines.push(` … and ${stale.length - 20} more`);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
lines.push("", ...renderTokenUsageReport(loadTokenUsage(engine.baseDir), engine.retention.tokenUsage));
|
|
519
|
-
return lines.join("\n");
|
|
520
|
-
}
|
|
521
|
-
function renderResult(result) {
|
|
522
|
-
const applied = result.appliedEdits.filter((e) => e.applied);
|
|
523
|
-
const failed = result.appliedEdits.filter((e) => !e.applied);
|
|
524
|
-
const lines = [
|
|
525
|
-
`refinement ${result.id}${result.rollbackOf ? ` (rollback of ${result.rollbackOf})` : ""}: ${applied.length} applied, ${failed.length} failed`,
|
|
526
|
-
`summary: ${result.summary}`,
|
|
527
|
-
];
|
|
528
|
-
for (const e of applied) {
|
|
529
|
-
lines.push(`- ${e.action} ${e.kind}:${e.id} (v${(e.after?.version ?? e.before?.version) ?? "?"})`);
|
|
530
|
-
}
|
|
531
|
-
for (const e of failed) {
|
|
532
|
-
lines.push(`- failed ${e.action} ${e.kind}:${e.id ?? "(computed)"} — ${e.error ?? "unknown error"}`);
|
|
533
|
-
}
|
|
534
|
-
lines.push(`expected outcome: ${result.expectedOutcome}`);
|
|
535
|
-
return lines.join("\n");
|
|
536
|
-
}
|
|
537
|
-
function toEntryRecord(value) {
|
|
538
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
539
|
-
return {};
|
|
540
|
-
}
|
|
541
|
-
return value;
|
|
542
|
-
}
|
|
543
|
-
function parsePositiveInt(value, what) {
|
|
544
|
-
const n = Number(value);
|
|
545
|
-
if (!Number.isInteger(n) || n < 1) {
|
|
546
|
-
throw new Error(`${what} must be a positive integer, got "${value}"`);
|
|
547
|
-
}
|
|
548
|
-
return n;
|
|
549
|
-
}
|
|
550
|
-
function isValidExport(payload) {
|
|
551
|
-
if (typeof payload !== "object" || payload === null)
|
|
552
|
-
return false;
|
|
553
|
-
const entries = payload["entries"];
|
|
554
|
-
if (typeof entries !== "object" || entries === null || Array.isArray(entries))
|
|
555
|
-
return false;
|
|
556
|
-
const kinds = ["prompt", "memory", "skill", "subagent"];
|
|
557
|
-
return kinds.every((kind) => Object.prototype.hasOwnProperty.call(entries, kind));
|
|
558
|
-
}
|
|
559
|
-
function isResultRecord(value) {
|
|
560
|
-
return typeof value === "object" && value !== null && "id" in value && "appliedEdits" in value;
|
|
561
|
-
}
|
|
562
|
-
function success(text) {
|
|
563
|
-
return { kind: "success", text };
|
|
564
|
-
}
|
|
565
|
-
function error(text) {
|
|
566
|
-
return { kind: "error", text };
|
|
567
|
-
}
|
|
568
|
-
//# sourceMappingURL=command.js.map
|
package/lib/consolidate.d.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Global-store consolidation (R3): turn the two hygiene signals — R2's
|
|
3
|
-
* `conflictHint` stamps and usage v2's zero-injection staleness — into one
|
|
4
|
-
* deterministic, human-approved batch of archive edits (`/evolve consolidate`).
|
|
5
|
-
*
|
|
6
|
-
* Design (ADR implemented/feature/2026-08-24-consolidation-command.md):
|
|
7
|
-
* - pure functions only; the command layer owns loading, reporting, and the
|
|
8
|
-
* apply phase, and ALWAYS re-scans fresh state before applying (the report
|
|
9
|
-
* and the apply are two separate invocations — prefer under-archiving over
|
|
10
|
-
* mis-archiving when state moved in between);
|
|
11
|
-
* - archiving keeps data (ARCHIVED_AT_KEY stamp, restorable via
|
|
12
|
-
* `/evolve unarchive`) and preserves every existing metadata key including
|
|
13
|
-
* the conflictHint itself, so unarchived entries keep their trail;
|
|
14
|
-
* - no LLM call anywhere: code proposes, the human disposes.
|
|
15
|
-
*/
|
|
16
|
-
import type { HarnessState, RefinementEdit, RefinementKind } from "./types.js";
|
|
17
|
-
import { type UsageStore } from "./usage.js";
|
|
18
|
-
/** Zero-use entries at least this old are stale candidates (30d, matching the injection recency half-life scale). */
|
|
19
|
-
export declare const STALE_MIN_AGE_MS: number;
|
|
20
|
-
/** Parsed `<kind>:<id>:<score>` value of a {@link CONFLICT_HINT_KEY} stamp. */
|
|
21
|
-
export interface ConflictHint {
|
|
22
|
-
kind: RefinementKind;
|
|
23
|
-
id: string;
|
|
24
|
-
score: number;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Parse one conflictHint metadata value. Returns undefined for anything that
|
|
28
|
-
* is not exactly `<kind>:<id>:<score>` with a known kind and a score in
|
|
29
|
-
* [0, 1] — foreign or legacy junk must never crash the scan.
|
|
30
|
-
*/
|
|
31
|
-
export declare function parseConflictHint(value: unknown): ConflictHint | undefined;
|
|
32
|
-
/** One planned archive with its human-readable justification. */
|
|
33
|
-
export interface ConsolidationCandidate {
|
|
34
|
-
kind: RefinementKind;
|
|
35
|
-
id: string;
|
|
36
|
-
title: string;
|
|
37
|
-
reason: string;
|
|
38
|
-
/**
|
|
39
|
-
* Merge tier (P1 反膨胀): set on conflict-pair candidates — the entry's
|
|
40
|
-
* content merges into this survivor (pointed-to original) before the
|
|
41
|
-
* entry itself archives, so nothing readable is lost by the archive. The
|
|
42
|
-
* plan only EMITS the merge when `mergeDuplicates` is on.
|
|
43
|
-
*/
|
|
44
|
-
mergeInto?: {
|
|
45
|
-
kind: RefinementKind;
|
|
46
|
-
id: string;
|
|
47
|
-
title: string;
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Entries stamped by the write-time conflict guard whose target still exists
|
|
52
|
-
* and is still active. The hinted (newer) entry is the archive candidate —
|
|
53
|
-
* the pointed-to original stays as the live copy.
|
|
54
|
-
*/
|
|
55
|
-
export declare function findConflictPairs(state: HarnessState): ConsolidationCandidate[];
|
|
56
|
-
/**
|
|
57
|
-
* Active global entries never injected in any session and untouched for at
|
|
58
|
-
* least `minAgeMs`: prime staleness candidates. Operates on whatever state
|
|
59
|
-
* it is handed (callers pass the global store).
|
|
60
|
-
*/
|
|
61
|
-
export declare function findStaleEntries(state: HarnessState, store: UsageStore, now: number, minAgeMs?: number): ConsolidationCandidate[];
|
|
62
|
-
/**
|
|
63
|
-
* Append a source entry's content to a survivor with an attributed divider,
|
|
64
|
-
* so the survivor's reader can see which part came from which entry.
|
|
65
|
-
*/
|
|
66
|
-
export declare function mergeContent(target: string, source: string, sourceRef: string, dateIso: string): string;
|
|
67
|
-
/**
|
|
68
|
-
* Merge both scans (conflict reason wins on overlap), deduped by kind:id,
|
|
69
|
-
* and build the batch archive edits. Each edit preserves the entry's full
|
|
70
|
-
* content and existing metadata — only ARCHIVED_AT_KEY is added — so
|
|
71
|
-
* `/evolve unarchive` restores everything intact.
|
|
72
|
-
*
|
|
73
|
-
* With `opts.mergeDuplicates`, conflict-pair candidates additionally merge
|
|
74
|
-
* their content INTO the pointed-to survivor (an attributed `mergeContent`
|
|
75
|
-
* section + a `mergedFrom` provenance stamp) before archiving — the #11
|
|
76
|
-
* candidate-c tier the 2026-08-28 ecosystem review promoted. Per-target
|
|
77
|
-
* accumulation composes multiple hints into one survivor (update edits
|
|
78
|
-
* replace content/metadata wholesale, so parallel merge edits would clobber
|
|
79
|
-
* each other).
|
|
80
|
-
*
|
|
81
|
-
* @param now Epoch ms used for the archivedAt stamp (injected for tests).
|
|
82
|
-
*/
|
|
83
|
-
export declare function planConsolidation(state: HarnessState, store: UsageStore, now?: number, opts?: {
|
|
84
|
-
minAgeMs?: number;
|
|
85
|
-
mergeDuplicates?: boolean;
|
|
86
|
-
}): {
|
|
87
|
-
candidates: ConsolidationCandidate[];
|
|
88
|
-
edits: RefinementEdit[];
|
|
89
|
-
};
|
|
90
|
-
//# sourceMappingURL=consolidate.d.ts.map
|