@hmharness/evolution 0.6.6 → 0.6.7
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/dist/evolve.d.ts +3 -0
- package/dist/evolve.js +55 -25
- package/dist/impact.d.ts +7 -1
- package/dist/impact.js +20 -6
- package/dist/patches.d.ts +10 -2
- package/dist/patches.js +22 -7
- package/package.json +2 -2
package/dist/evolve.d.ts
CHANGED
|
@@ -74,6 +74,9 @@ export interface EvolveReport {
|
|
|
74
74
|
cyclesToday: number;
|
|
75
75
|
maxCyclesPerDay?: number;
|
|
76
76
|
};
|
|
77
|
+
/** chars/4 estimate of this cycle's meta-call traffic; feeds the daily
|
|
78
|
+
* token budget gate (readBudget sums today's entries) */
|
|
79
|
+
estTokens?: number;
|
|
77
80
|
}
|
|
78
81
|
/** Runs one bench case with the given skills block injected. */
|
|
79
82
|
export type CaseRunner = (c: BenchCase, skillsPrompt: string) => Promise<string>;
|
package/dist/evolve.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { appendFile, mkdir, readFile } from 'node:fs/promises';
|
|
17
17
|
import { join } from 'node:path';
|
|
18
|
-
import { chat } from '@hmharness/kernel';
|
|
18
|
+
import { chat, loadConfig } from '@hmharness/kernel';
|
|
19
19
|
import { listCases, matchCase, seedCases } from "./bench.js";
|
|
20
20
|
import { deleteDraft, listCanary, listDrafts, listSkills, promoteSkill, rollbackSkill, skillsToPrompt, unpromoteSkill, writeDraft } from "./skills.js";
|
|
21
21
|
import { appendMemory, readNotes } from "./memory.js";
|
|
@@ -52,14 +52,24 @@ export async function runEvolution(opts) {
|
|
|
52
52
|
memoryDistilled: null,
|
|
53
53
|
};
|
|
54
54
|
// P0 evolution budget gate (AZR "safety alarms" + cost control): a day's
|
|
55
|
-
// cycle count and a
|
|
56
|
-
//
|
|
55
|
+
// cycle count and a token ceiling live in config; overspending skips the
|
|
56
|
+
// cycle instead of burning money unattended. The token gate compares the
|
|
57
|
+
// summed estTokens of today's logged cycles against maxCyclesPerDay *
|
|
58
|
+
// maxTokensPerCycle (the old code read maxTokensPerCycle but never checked
|
|
59
|
+
// anything but cycles - the documented budget was decorative).
|
|
57
60
|
const budget = await readBudget(home);
|
|
58
61
|
const today = new Date().toISOString().slice(0, 10);
|
|
59
62
|
if (budget.maxCyclesPerDay && budget.cyclesToday >= budget.maxCyclesPerDay) {
|
|
60
63
|
report.outcomes.push({ name: '(budget)', action: 'error', reason: `daily cycle limit reached (${budget.cyclesToday}/${budget.maxCyclesPerDay} today) - skipped` });
|
|
61
64
|
return report;
|
|
62
65
|
}
|
|
66
|
+
if (budget.maxCyclesPerDay && budget.maxTokensPerCycle) {
|
|
67
|
+
const dailyCap = budget.maxCyclesPerDay * budget.maxTokensPerCycle;
|
|
68
|
+
if ((budget.tokensToday ?? 0) >= dailyCap) {
|
|
69
|
+
report.outcomes.push({ name: '(budget)', action: 'error', reason: `daily token limit reached (~${budget.tokensToday}/${dailyCap} est-tokens today) - skipped` });
|
|
70
|
+
return report;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
63
73
|
// 1. Seed bench cases on a fresh home so the gate always has a signal.
|
|
64
74
|
report.seededCases = await seedCases(home);
|
|
65
75
|
const cases = await listCases(home);
|
|
@@ -297,29 +307,42 @@ export async function runEvolution(opts) {
|
|
|
297
307
|
// 6. CODE-LEVEL evolution: propose + sandbox-bench + merge/revert patches.
|
|
298
308
|
// This is the DGM bridge - the agent can now modify its own tool code,
|
|
299
309
|
// gated by the same bench discipline as skill promotion.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
patch,
|
|
310
|
-
baselineRate: baseRate,
|
|
311
|
-
runCase: async (c) => runCase(c, skillsToPrompt(active)),
|
|
312
|
-
benchCases: train,
|
|
313
|
-
log: say,
|
|
314
|
-
});
|
|
315
|
-
report.patchOutcomes = report.patchOutcomes ?? [];
|
|
316
|
-
report.patchOutcomes.push(outcome);
|
|
317
|
-
say(` code-patch ${outcome.action}: ${outcome.reason}`);
|
|
318
|
-
}
|
|
310
|
+
// OFF BY DEFAULT (DGM's own paper calls self-modifying systems "unsafe by
|
|
311
|
+
// default"; AlphaEvolve evolves external programs in isolation, never the
|
|
312
|
+
// live repo). Opt in with evolution.autoPatch=true in config.json - and
|
|
313
|
+
// even then it only ever touches the repo the evolution runs in, with
|
|
314
|
+
// human-reviewed sandbox benches before any merge.
|
|
315
|
+
const cfg = await loadConfig();
|
|
316
|
+
if (cfg.evolution?.autoPatch !== true) {
|
|
317
|
+
if (insights.length > 0 || notes.length > 0) {
|
|
318
|
+
say(' code-evolution: off (set evolution.autoPatch=true in config.json to enable)');
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
|
-
|
|
322
|
-
|
|
321
|
+
else {
|
|
322
|
+
try {
|
|
323
|
+
const { proposePatches, runPatchSandbox } = await import("./patches.js");
|
|
324
|
+
const repoRoot = process.cwd();
|
|
325
|
+
const patches = await proposePatches(provider, signals, readFile, repoRoot, say);
|
|
326
|
+
if (patches.length > 0) {
|
|
327
|
+
report.codePatches = patches.map((p) => ({ name: p.name, file: p.file, reason: p.reason }));
|
|
328
|
+
for (const patch of patches.slice(0, 1)) { // at most 1 patch per cycle
|
|
329
|
+
const outcome = await runPatchSandbox({
|
|
330
|
+
repoRoot,
|
|
331
|
+
patch,
|
|
332
|
+
baselineRate: baseRate,
|
|
333
|
+
runCase: async (c) => runCase(c, skillsToPrompt(active)),
|
|
334
|
+
benchCases: train,
|
|
335
|
+
log: say,
|
|
336
|
+
});
|
|
337
|
+
report.patchOutcomes = report.patchOutcomes ?? [];
|
|
338
|
+
report.patchOutcomes.push(outcome);
|
|
339
|
+
say(` code-patch ${outcome.action}: ${outcome.reason}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
say(` code-evolution skipped: ${String(err).slice(0, 120)}`);
|
|
345
|
+
}
|
|
323
346
|
}
|
|
324
347
|
// 7. Append-only memory distillation.
|
|
325
348
|
if (notes.length >= 4) {
|
|
@@ -356,7 +379,14 @@ export async function runEvolution(opts) {
|
|
|
356
379
|
catch (err) {
|
|
357
380
|
say(` impact/decay skipped: ${String(err).slice(0, 100)}`);
|
|
358
381
|
}
|
|
359
|
-
// 7. Durable evolution log.
|
|
382
|
+
// 7. Durable evolution log. estTokens feeds the daily token budget gate:
|
|
383
|
+
// a chars/4 estimate of this cycle's meta-call traffic (proposals + bench
|
|
384
|
+
// outputs + distilled notes) - coarse on purpose, the gate only needs an
|
|
385
|
+
// order of magnitude to stop unattended spend.
|
|
386
|
+
const estTokensSpent = (proposals ?? []).reduce((n, p) => n + estTokens(p.skill_md ?? '') + estTokens(p.description ?? ''), 0)
|
|
387
|
+
+ (report.memoryDistilled ? estTokens(report.memoryDistilled) : 0)
|
|
388
|
+
+ (report.patchOutcomes ?? []).length * 4_000;
|
|
389
|
+
report.estTokens = estTokensSpent;
|
|
360
390
|
const logDir = join(home, 'evolution');
|
|
361
391
|
await mkdir(logDir, { recursive: true });
|
|
362
392
|
await appendFile(join(logDir, 'log.jsonl'), JSON.stringify(report) + '\n', 'utf8');
|
package/dist/impact.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
export interface EvolutionBudget {
|
|
2
2
|
maxCyclesPerDay?: number;
|
|
3
3
|
maxTokensPerCycle?: number;
|
|
4
|
+
/** summed estTokens of today's logged cycles (filled by readBudget) */
|
|
5
|
+
tokensToday?: number;
|
|
4
6
|
}
|
|
5
|
-
/** How many evolve cycles already ran today (counts the log.jsonl).
|
|
7
|
+
/** How many evolve cycles already ran today (counts the log.jsonl).
|
|
8
|
+
* Accepts BOTH key spellings: the code's maxCyclesPerDay/maxTokensPerCycle
|
|
9
|
+
* and the documented-in-SELFFEED.md cyclesPerDay/tokensPerCycle - the docs
|
|
10
|
+
* shipped with the short names, so users who configured by the book were
|
|
11
|
+
* silently unlimited. */
|
|
6
12
|
export declare function readBudget(home: string): Promise<EvolutionBudget & {
|
|
7
13
|
cyclesToday: number;
|
|
8
14
|
}>;
|
package/dist/impact.js
CHANGED
|
@@ -20,26 +20,40 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { appendFile, mkdir, readFile, rename } from 'node:fs/promises';
|
|
22
22
|
import { join } from 'node:path';
|
|
23
|
-
/** How many evolve cycles already ran today (counts the log.jsonl).
|
|
23
|
+
/** How many evolve cycles already ran today (counts the log.jsonl).
|
|
24
|
+
* Accepts BOTH key spellings: the code's maxCyclesPerDay/maxTokensPerCycle
|
|
25
|
+
* and the documented-in-SELFFEED.md cyclesPerDay/tokensPerCycle - the docs
|
|
26
|
+
* shipped with the short names, so users who configured by the book were
|
|
27
|
+
* silently unlimited. */
|
|
24
28
|
export async function readBudget(home) {
|
|
25
29
|
const budget = { cyclesToday: 0 };
|
|
26
30
|
try {
|
|
27
31
|
const cfg = JSON.parse(await readFile(join(home, 'config.json'), 'utf8'));
|
|
28
|
-
budget.maxCyclesPerDay = cfg.evolutionBudget?.maxCyclesPerDay;
|
|
29
|
-
budget.maxTokensPerCycle = cfg.evolutionBudget?.maxTokensPerCycle;
|
|
32
|
+
budget.maxCyclesPerDay = cfg.evolutionBudget?.maxCyclesPerDay ?? cfg.evolutionBudget?.cyclesPerDay;
|
|
33
|
+
budget.maxTokensPerCycle = cfg.evolutionBudget?.maxTokensPerCycle ?? cfg.evolutionBudget?.tokensPerCycle;
|
|
30
34
|
}
|
|
31
35
|
catch { /* no config / no budget - unlimited */ }
|
|
32
36
|
const today = new Date().toISOString().slice(0, 10);
|
|
33
37
|
try {
|
|
34
38
|
const text = await readFile(join(home, 'evolution', 'log.jsonl'), 'utf8');
|
|
35
|
-
|
|
39
|
+
const todays = text.trim().split('\n')
|
|
36
40
|
.filter((l) => { try {
|
|
37
41
|
return JSON.parse(l).time.startsWith(today);
|
|
38
42
|
}
|
|
39
43
|
catch {
|
|
40
44
|
return false;
|
|
41
|
-
} })
|
|
42
|
-
|
|
45
|
+
} });
|
|
46
|
+
budget.cyclesToday = todays.length;
|
|
47
|
+
// token budget uses each cycle's logged estTokens (chars/4 estimate; the
|
|
48
|
+
// precise per-call usage is not threaded through the meta-call helpers)
|
|
49
|
+
budget.tokensToday = todays.reduce((n, l) => {
|
|
50
|
+
try {
|
|
51
|
+
return n + (JSON.parse(l).estTokens ?? 0);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return n;
|
|
55
|
+
}
|
|
56
|
+
}, 0);
|
|
43
57
|
}
|
|
44
58
|
catch { /* no log yet */ }
|
|
45
59
|
return budget;
|
package/dist/patches.d.ts
CHANGED
|
@@ -40,7 +40,11 @@ export declare function isPatchableFile(file: string): boolean;
|
|
|
40
40
|
/** Validate + apply a patch to a file on disk. Returns error if find-string
|
|
41
41
|
* is not found or not unique. */
|
|
42
42
|
export declare function applyPatch(repoRoot: string, patch: CodePatch): Promise<string>;
|
|
43
|
-
/** Create a sandbox git branch for testing a patch.
|
|
43
|
+
/** Create a sandbox git branch for testing a patch.
|
|
44
|
+
* Precondition: the working tree is CLEAN (runPatchSandbox enforces this).
|
|
45
|
+
* The old version stashed uncommitted user work here and never popped it -
|
|
46
|
+
* a failed cycle silently swallowed the user's changes into a stash. Now a
|
|
47
|
+
* dirty tree refuses the cycle outright instead of hiding the work. */
|
|
44
48
|
export declare function createSandbox(repoRoot: string, name: string): Promise<string>;
|
|
45
49
|
/** Commit the patch on the sandbox branch so it is fully isolated from main. */
|
|
46
50
|
export declare function commitOnSandbox(repoRoot: string, patchName: string): Promise<void>;
|
|
@@ -48,7 +52,11 @@ export declare function commitOnSandbox(repoRoot: string, patchName: string): Pr
|
|
|
48
52
|
export declare function mergeSandbox(repoRoot: string, branch: string): Promise<void>;
|
|
49
53
|
/** Run the bench gate on the current branch. Returns pass rate (0-1) or -1 on build failure. */
|
|
50
54
|
export declare function sandboxBench(repoRoot: string, runCase: (c: import('./bench.ts').BenchCase, skillsPrompt: string) => Promise<string>, cases: import('./bench.ts').BenchCase[]): Promise<number>;
|
|
51
|
-
/** Revert: go back to main, delete the sandbox branch (zero residue).
|
|
55
|
+
/** Revert: go back to main, delete the sandbox branch (zero residue).
|
|
56
|
+
* No `reset --hard` anymore: the tree is guaranteed clean at entry (clean-
|
|
57
|
+
* tree precondition) and the patch is committed on the sandbox branch, so
|
|
58
|
+
* there is nothing to hard-reset - and a hard reset on main is exactly the
|
|
59
|
+
* operation that can destroy a user's uncommitted work. */
|
|
52
60
|
export declare function revertSandbox(repoRoot: string, branch: string): Promise<void>;
|
|
53
61
|
/**
|
|
54
62
|
* Full sandbox cycle: apply patch on a branch, rebuild, bench, merge or revert.
|
package/dist/patches.js
CHANGED
|
@@ -82,11 +82,13 @@ export async function applyPatch(repoRoot, patch) {
|
|
|
82
82
|
await writeFile(filePath, patched, 'utf8');
|
|
83
83
|
return 'applied';
|
|
84
84
|
}
|
|
85
|
-
/** Create a sandbox git branch for testing a patch.
|
|
85
|
+
/** Create a sandbox git branch for testing a patch.
|
|
86
|
+
* Precondition: the working tree is CLEAN (runPatchSandbox enforces this).
|
|
87
|
+
* The old version stashed uncommitted user work here and never popped it -
|
|
88
|
+
* a failed cycle silently swallowed the user's changes into a stash. Now a
|
|
89
|
+
* dirty tree refuses the cycle outright instead of hiding the work. */
|
|
86
90
|
export async function createSandbox(repoRoot, name) {
|
|
87
91
|
const branch = `evolve/${name}-${Date.now().toString(36)}`;
|
|
88
|
-
// stash any stray working-tree changes first so the branch starts clean
|
|
89
|
-
await git(['stash', '--include-untracked'], { cwd: repoRoot, timeout: 10_000 }).catch(() => undefined);
|
|
90
92
|
await git(['checkout', '-b', branch], { cwd: repoRoot, timeout: 10_000 });
|
|
91
93
|
return branch;
|
|
92
94
|
}
|
|
@@ -128,11 +130,13 @@ export async function sandboxBench(repoRoot, runCase, cases) {
|
|
|
128
130
|
}
|
|
129
131
|
return cases.length > 0 ? pass / cases.length : -1;
|
|
130
132
|
}
|
|
131
|
-
/** Revert: go back to main, delete the sandbox branch (zero residue).
|
|
133
|
+
/** Revert: go back to main, delete the sandbox branch (zero residue).
|
|
134
|
+
* No `reset --hard` anymore: the tree is guaranteed clean at entry (clean-
|
|
135
|
+
* tree precondition) and the patch is committed on the sandbox branch, so
|
|
136
|
+
* there is nothing to hard-reset - and a hard reset on main is exactly the
|
|
137
|
+
* operation that can destroy a user's uncommitted work. */
|
|
132
138
|
export async function revertSandbox(repoRoot, branch) {
|
|
133
139
|
await git(['checkout', 'main'], { cwd: repoRoot, timeout: 10_000 });
|
|
134
|
-
// discard any uncommitted changes on the sandbox branch
|
|
135
|
-
await git(['reset', '--hard', 'HEAD'], { cwd: repoRoot, timeout: 5000 });
|
|
136
140
|
await git(['branch', '-D', branch], { cwd: repoRoot, timeout: 5000 });
|
|
137
141
|
}
|
|
138
142
|
/**
|
|
@@ -142,6 +146,17 @@ export async function revertSandbox(repoRoot, branch) {
|
|
|
142
146
|
export async function runPatchSandbox(opts) {
|
|
143
147
|
const say = opts.log ?? (() => undefined);
|
|
144
148
|
const { patch } = opts;
|
|
149
|
+
// clean-tree precondition: a dirty working tree means a human is mid-work
|
|
150
|
+
// in this repo - refuse rather than stash/reset around their changes
|
|
151
|
+
try {
|
|
152
|
+
const { stdout } = await git(['status', '--porcelain'], { cwd: opts.repoRoot, timeout: 10_000 });
|
|
153
|
+
if (stdout.trim()) {
|
|
154
|
+
return { name: patch.name, action: 'error', reason: 'working tree not clean - refusing to run a patch cycle in a repo with uncommitted changes (commit or stash them first)', branch: '' };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
return { name: patch.name, action: 'error', reason: `git status failed: ${String(err).slice(0, 120)}`, branch: '' };
|
|
159
|
+
}
|
|
145
160
|
say(` code-patch "${patch.name}": creating sandbox branch`);
|
|
146
161
|
let branch = '';
|
|
147
162
|
try {
|
|
@@ -191,7 +206,7 @@ export async function proposePatches(provider, signals, readFileFn, repoRoot, sa
|
|
|
191
206
|
web_search: 'packages/agent/src/tools.ts',
|
|
192
207
|
web_fetch: 'packages/agent/src/tools.ts',
|
|
193
208
|
harmony_build: 'packages/domain-harmony/src/index.ts',
|
|
194
|
-
harmony_devices: 'packages/domain-harmony/src/
|
|
209
|
+
harmony_devices: 'packages/domain-harmony/src/index.ts',
|
|
195
210
|
// add more as insights reveal hot paths
|
|
196
211
|
};
|
|
197
212
|
const toolsUsed = (signals.toolUsage ?? {});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hmharness/evolution",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"description": "hmharness evolution subsystem: persistent memory, insight capture, skill library, and the bench that gives evolution its fitness signal. First-class kernel citizen, not a plugin.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"build": "tsc -p tsconfig.build.json"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@hmharness/kernel": "0.6.
|
|
18
|
+
"@hmharness/kernel": "0.6.7"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"dist"
|