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.
Files changed (126) hide show
  1. package/README.md +35 -122
  2. package/README.zh.md +26 -97
  3. package/cordis.patch.yml +2 -2
  4. package/lib/index.d.ts +44 -241
  5. package/lib/index.js +30 -253
  6. package/lib/memory-guide.d.ts +13 -14
  7. package/lib/memory-guide.js +32 -21
  8. package/lib/memory-section.d.ts +77 -0
  9. package/lib/memory-section.js +189 -0
  10. package/package.json +2 -17
  11. package/lib/apply.d.ts +0 -22
  12. package/lib/apply.js +0 -157
  13. package/lib/approval.d.ts +0 -72
  14. package/lib/approval.js +0 -127
  15. package/lib/auto.d.ts +0 -236
  16. package/lib/auto.js +0 -773
  17. package/lib/autocase.d.ts +0 -38
  18. package/lib/autocase.js +0 -76
  19. package/lib/benchmark-command.d.ts +0 -9
  20. package/lib/benchmark-command.js +0 -357
  21. package/lib/benchmark.d.ts +0 -151
  22. package/lib/benchmark.js +0 -250
  23. package/lib/command.d.ts +0 -49
  24. package/lib/command.js +0 -568
  25. package/lib/consolidate.d.ts +0 -90
  26. package/lib/consolidate.js +0 -180
  27. package/lib/copy.d.ts +0 -74
  28. package/lib/copy.js +0 -160
  29. package/lib/declines.d.ts +0 -101
  30. package/lib/declines.js +0 -175
  31. package/lib/evaluate.d.ts +0 -74
  32. package/lib/evaluate.js +0 -271
  33. package/lib/evolve-event.d.ts +0 -40
  34. package/lib/evolve-event.js +0 -59
  35. package/lib/failures.d.ts +0 -42
  36. package/lib/failures.js +0 -171
  37. package/lib/fate.d.ts +0 -130
  38. package/lib/fate.js +0 -350
  39. package/lib/goal-command.d.ts +0 -7
  40. package/lib/goal-command.js +0 -37
  41. package/lib/goal.d.ts +0 -72
  42. package/lib/goal.js +0 -72
  43. package/lib/inject.d.ts +0 -196
  44. package/lib/inject.js +0 -412
  45. package/lib/llm-text.d.ts +0 -104
  46. package/lib/llm-text.js +0 -140
  47. package/lib/logfile.d.ts +0 -71
  48. package/lib/logfile.js +0 -159
  49. package/lib/memory-agent.d.ts +0 -129
  50. package/lib/memory-agent.js +0 -568
  51. package/lib/memory-benchmark.d.ts +0 -76
  52. package/lib/memory-benchmark.js +0 -170
  53. package/lib/memory-index.d.ts +0 -114
  54. package/lib/memory-index.js +0 -197
  55. package/lib/message-source.d.ts +0 -23
  56. package/lib/message-source.js +0 -12
  57. package/lib/mount-command.d.ts +0 -10
  58. package/lib/mount-command.js +0 -48
  59. package/lib/mount.d.ts +0 -42
  60. package/lib/mount.js +0 -212
  61. package/lib/notify.d.ts +0 -59
  62. package/lib/notify.js +0 -97
  63. package/lib/plan.d.ts +0 -16
  64. package/lib/plan.js +0 -126
  65. package/lib/planner.d.ts +0 -61
  66. package/lib/planner.js +0 -144
  67. package/lib/pool.d.ts +0 -7
  68. package/lib/pool.js +0 -25
  69. package/lib/prefix-cache.d.ts +0 -78
  70. package/lib/prefix-cache.js +0 -168
  71. package/lib/project.d.ts +0 -28
  72. package/lib/project.js +0 -66
  73. package/lib/projection.d.ts +0 -22
  74. package/lib/projection.js +0 -112
  75. package/lib/promotion.d.ts +0 -94
  76. package/lib/promotion.js +0 -171
  77. package/lib/recall.d.ts +0 -85
  78. package/lib/recall.js +0 -142
  79. package/lib/record-language.d.ts +0 -85
  80. package/lib/record-language.js +0 -152
  81. package/lib/render.d.ts +0 -13
  82. package/lib/render.js +0 -80
  83. package/lib/review-scheduler.d.ts +0 -38
  84. package/lib/review-scheduler.js +0 -191
  85. package/lib/review.d.ts +0 -60
  86. package/lib/review.js +0 -145
  87. package/lib/rollback.d.ts +0 -9
  88. package/lib/rollback.js +0 -68
  89. package/lib/rubric.d.ts +0 -29
  90. package/lib/rubric.js +0 -119
  91. package/lib/runtime.d.ts +0 -24
  92. package/lib/runtime.js +0 -87
  93. package/lib/score.d.ts +0 -64
  94. package/lib/score.js +0 -191
  95. package/lib/search.d.ts +0 -83
  96. package/lib/search.js +0 -136
  97. package/lib/service.d.ts +0 -49
  98. package/lib/service.js +0 -180
  99. package/lib/skill-render.d.ts +0 -23
  100. package/lib/skill-render.js +0 -69
  101. package/lib/skill.d.ts +0 -15
  102. package/lib/skill.js +0 -80
  103. package/lib/skillquality.d.ts +0 -85
  104. package/lib/skillquality.js +0 -320
  105. package/lib/source.d.ts +0 -30
  106. package/lib/source.js +0 -43
  107. package/lib/state.d.ts +0 -26
  108. package/lib/state.js +0 -151
  109. package/lib/store.d.ts +0 -62
  110. package/lib/store.js +0 -164
  111. package/lib/token-usage.d.ts +0 -68
  112. package/lib/token-usage.js +0 -211
  113. package/lib/tool.d.ts +0 -34
  114. package/lib/tool.js +0 -320
  115. package/lib/turn-snapshot.d.ts +0 -72
  116. package/lib/turn-snapshot.js +0 -238
  117. package/lib/types.d.ts +0 -208
  118. package/lib/types.js +0 -127
  119. package/lib/usage.d.ts +0 -36
  120. package/lib/usage.js +0 -98
  121. package/lib/validate.d.ts +0 -28
  122. package/lib/validate.js +0 -181
  123. package/lib/wrapup-command.d.ts +0 -10
  124. package/lib/wrapup-command.js +0 -243
  125. package/lib/wrapup.d.ts +0 -260
  126. 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
@@ -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