@hmharness/evolution 0.14.9 → 0.14.11

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 (42) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +2 -0
  3. package/dist/judge.d.ts +65 -0
  4. package/dist/judge.js +194 -0
  5. package/dist/reward-model.d.ts +66 -0
  6. package/dist/reward-model.js +187 -0
  7. package/package.json +2 -2
  8. package/dist/bench.d.ts.v2bak.v2bak +0 -44
  9. package/dist/bench.js.v2bak.v2bak +0 -156
  10. package/dist/candidates.d.ts.v2bak.v2bak +0 -114
  11. package/dist/candidates.js.v2bak.v2bak +0 -232
  12. package/dist/dataset.d.ts.v2bak.v2bak +0 -64
  13. package/dist/dataset.js.v2bak.v2bak +0 -184
  14. package/dist/evolve.d.ts.v2bak.v2bak +0 -98
  15. package/dist/evolve.js.v2bak.v2bak +0 -573
  16. package/dist/impact.d.ts.v2bak.v2bak +0 -77
  17. package/dist/impact.js.v2bak.v2bak +0 -214
  18. package/dist/index.d.ts.v2bak.v2bak +0 -16
  19. package/dist/index.js.v2bak.v2bak +0 -16
  20. package/dist/insights.d.ts.v2bak.v2bak +0 -18
  21. package/dist/insights.js.v2bak.v2bak +0 -80
  22. package/dist/knowledge.d.ts.v2bak.v2bak +0 -15
  23. package/dist/knowledge.js.v2bak.v2bak +0 -145
  24. package/dist/labels.d.ts.v2bak.v2bak +0 -20
  25. package/dist/labels.js.v2bak.v2bak +0 -57
  26. package/dist/memory.d.ts.v2bak.v2bak +0 -32
  27. package/dist/memory.js.v2bak.v2bak +0 -233
  28. package/dist/patches.d.ts.v2bak.v2bak +0 -83
  29. package/dist/patches.js.v2bak.v2bak +0 -253
  30. package/dist/radar.d.ts.v2bak.v2bak +0 -3
  31. package/dist/radar.js.v2bak.v2bak +0 -40
  32. package/dist/ranker.d.ts.v2bak.v2bak +0 -44
  33. package/dist/ranker.js.v2bak.v2bak +0 -55
  34. package/dist/readiness.d.ts.v2bak.v2bak +0 -15
  35. package/dist/readiness.js.v2bak.v2bak +0 -167
  36. package/dist/skillpayload.d.ts.v2bak.v2bak +0 -1
  37. package/dist/skillpayload.js.v2bak +0 -28
  38. package/dist/skillpayload.js.v2bak.v2bak.v2bak +0 -28
  39. package/dist/skills.d.ts.v2bak.v2bak +0 -54
  40. package/dist/skills.js.v2bak.v2bak +0 -321
  41. package/dist/workflows.d.ts.v2bak.v2bak +0 -28
  42. package/dist/workflows.js.v2bak.v2bak +0 -84
@@ -1,233 +0,0 @@
1
- /**
2
- * @hmharness/evolution - memory
3
- * Cross-session persistent memory. Notes are append-only lines in
4
- * memory/memory.md (ACE lesson: append beats rewrite - rewriting is where
5
- * hard-won context gets lost). Injection is retrieval-based and layered:
6
- *
7
- * 1. lexical scoring (ASCII words + CJK bigrams, no deps) - the baseline
8
- * 2. workspace scoping - notes tagged [ws:<name>] are boosted 2.5x when
9
- * the current task runs inside that workspace and dampened to 0.3x in
10
- * others. Isolation WITHOUT walls: project-local facts rank first at
11
- * home, but global lessons stay reachable everywhere. Untagged notes
12
- * (the entire pre-existing memory) behave exactly as before.
13
- * 3. optional embedding hybrid - when an embedding provider is passed in,
14
- * notes are vectorised once (cache: memory/embeddings.json, keyed by
15
- * content hash) and ranked by cosine similarity blended with the
16
- * lexical score. Any failure falls back to pure lexical - embeddings
17
- * are an upgrade, never a dependency.
18
- */
19
- import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
20
- import { createHash } from 'node:crypto';
21
- import { join } from 'node:path';
22
- import { rankContext } from "./ranker.js";
23
- export async function readNotes(home) {
24
- let raw;
25
- try {
26
- raw = await readFile(join(home, 'memory', 'memory.md'), 'utf8');
27
- }
28
- catch {
29
- return [];
30
- }
31
- const notes = [];
32
- for (const line of raw.split('\n')) {
33
- const m = line.match(/^\s*-\s*\[([^\]]*)\]\s*(.+)$/);
34
- if (m)
35
- notes.push({ time: m[1].trim(), text: m[2].trim() });
36
- }
37
- return notes;
38
- }
39
- /** Resolve the workspace a cwd belongs to (longest path prefix wins).
40
- * Returns null outside every workspace - those notes stay global. */
41
- export async function workspaceForCwd(home, cwd) {
42
- let entries;
43
- try {
44
- const j = JSON.parse(await readFile(join(home, 'workspaces.json'), 'utf8'));
45
- entries = Array.isArray(j) ? j : (j.workspaces ?? []);
46
- }
47
- catch {
48
- return null;
49
- }
50
- const norm = (p) => p.replace(/[\\/]+/g, '\\').toLowerCase().replace(/\\$/, '');
51
- const target = norm(cwd);
52
- let best = null;
53
- for (const w of entries) {
54
- if (!w?.path || !w?.name)
55
- continue;
56
- const p = norm(w.path);
57
- if ((target === p || target.startsWith(p + '\\')) && (!best || p.length > best.len)) {
58
- best = { name: w.name, len: p.length };
59
- }
60
- }
61
- return best?.name ?? null;
62
- }
63
- const WS_TAG = /\s*\[ws:([^\]]+)\]\s*$/;
64
- function noteWorkspace(text) {
65
- const m = text.match(WS_TAG);
66
- return m ? m[1] : null;
67
- }
68
- /** Tokenize for scoring: ASCII words as-is, CJK runs as bigrams. */
69
- function tokens(text) {
70
- const out = new Set();
71
- for (const w of text.match(/[a-zA-Z0-9_.\\/:+-]{2,}/g) ?? [])
72
- out.add(w.toLowerCase());
73
- for (const run of text.match(/[\u4e00-\u9fff]+/g) ?? []) {
74
- for (let i = 0; i + 1 < run.length; i++)
75
- out.add(run.slice(i, i + 2));
76
- }
77
- return out;
78
- }
79
- export function scoreNotes(notes, task, workspace) {
80
- const taskTokens = tokens(task);
81
- const ranked = notes.map((note, idx) => {
82
- const n = tokens(note.text);
83
- let overlap = 0;
84
- for (const t of n)
85
- if (taskTokens.has(t))
86
- overlap++;
87
- let score = overlap + idx / Math.max(notes.length, 1) * 0.01;
88
- // workspace scoping: boost at home, dampen abroad, globals untouched
89
- const ws = noteWorkspace(note.text);
90
- if (workspace && ws === workspace)
91
- score *= 2.5;
92
- else if (workspace && ws && ws !== workspace)
93
- score *= 0.3;
94
- return { note, score };
95
- });
96
- return ranked.sort((a, b) => b.score - a.score);
97
- }
98
- function cosine(a, b) {
99
- let dot = 0, na = 0, nb = 0;
100
- for (let i = 0; i < a.length; i++) {
101
- dot += a[i] * b[i];
102
- na += a[i] * a[i];
103
- nb += b[i] * b[i];
104
- }
105
- return na && nb ? dot / (Math.sqrt(na) * Math.sqrt(nb)) : 0;
106
- }
107
- async function embed(inputs, p, fetchImpl) {
108
- const doFetch = fetchImpl ?? fetch;
109
- try {
110
- const res = await doFetch(p.baseUrl.replace(/\/+$/, '') + '/embeddings', {
111
- method: 'POST',
112
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${p.apiKey}` },
113
- body: JSON.stringify({ model: p.model, input: inputs }),
114
- signal: AbortSignal.timeout(20_000),
115
- });
116
- if (!res.ok)
117
- return null;
118
- const j = await res.json();
119
- const out = (j.data ?? []).map((d) => d.embedding ?? []);
120
- return out.length === inputs.length ? out : null;
121
- }
122
- catch {
123
- return null;
124
- }
125
- }
126
- async function hybridRank(notes, task, workspace, home, embedding, fetchImpl) {
127
- const lexical = scoreNotes(notes, task, workspace);
128
- const maxLex = Math.max(...lexical.map((r) => r.score), 1e-9);
129
- const cacheFile = join(home, 'memory', 'embeddings.json');
130
- let cache = {};
131
- try {
132
- cache = JSON.parse(await readFile(cacheFile, 'utf8'));
133
- }
134
- catch { /* empty */ }
135
- const hash = (s) => createHash('sha256').update(s).digest('hex').slice(0, 24);
136
- const missing = [...new Set(notes.filter((n) => !cache[hash(n.text)]).map((n) => n.text))];
137
- if (missing.length > 0) {
138
- const vecs = await embed(missing, embedding, fetchImpl);
139
- if (!vecs)
140
- return null; // embedding endpoint down -> pure lexical
141
- missing.forEach((text, i) => { cache[hash(text)] = vecs[i]; });
142
- try {
143
- await mkdir(join(home, 'memory'), { recursive: true });
144
- await writeFile(cacheFile, JSON.stringify(cache), 'utf8');
145
- }
146
- catch { /* best effort */ }
147
- }
148
- const queryVec = await embed([task], embedding, fetchImpl);
149
- if (!queryVec)
150
- return null;
151
- const q = queryVec[0];
152
- const blended = lexical.map((r) => {
153
- const v = cache[hash(r.note.text)];
154
- const cos = v ? cosine(v, q) : 0;
155
- return { note: r.note, score: 0.5 * (r.score / maxLex) + 0.5 * Math.max(cos, 0) };
156
- });
157
- return blended.sort((a, b) => b.score - a.score);
158
- }
159
- /**
160
- * Build the prompt block: top-k task-relevant notes plus the newest few
161
- * (deduplicated), bounded in chars. Empty string when memory is empty.
162
- */
163
- export async function retrieveMemory(home, task, opts = {}) {
164
- const { topK = 12, newest = 3, maxChars = 4000 } = opts;
165
- const notes = await readNotes(home);
166
- if (notes.length === 0)
167
- return '';
168
- let ranked;
169
- if (opts.embedding) {
170
- ranked = (await hybridRank(notes, task, opts.workspace, home, opts.embedding, opts.fetchImpl)) ?? scoreNotes(notes, task, opts.workspace);
171
- }
172
- else {
173
- ranked = scoreNotes(notes, task, opts.workspace);
174
- }
175
- // V2 M5: the blueprint-weighted ContextRanker governs the final pick.
176
- // Existing scores become `relevance`; recency derives from the note time
177
- // (distillations are curated -> more important); dependency/similarity stay
178
- // neutral until richer signals exist. Cost = chars/4.
179
- const maxScore = Math.max(1, ...ranked.map((r) => r.score));
180
- const newestTime = Date.parse(notes[notes.length - 1]?.time ?? '') || Date.now();
181
- const oldestTime = Date.parse(notes[0]?.time ?? '') || newestTime;
182
- const span = Math.max(1, newestTime - oldestTime);
183
- const reRanked = rankContext(ranked.map(({ note, score }) => ({
184
- source: 'memory',
185
- contentRef: note.text,
186
- relevance: score / maxScore,
187
- recency: span > 1 ? (Date.parse(note.time) - oldestTime) / span : 1,
188
- importance: /^\(distilled\)/.test(note.text) ? 0.8 : 0.5,
189
- dependency: 0.5,
190
- similarity: 0.5,
191
- tokenCost: Math.ceil(note.text.length / 4),
192
- })));
193
- const picked = [];
194
- const seen = new Set();
195
- for (const c of reRanked.slice(0, topK)) {
196
- const hit = ranked.find((r) => r.note.text === c.contentRef);
197
- if (!hit || seen.has(hit.note.text))
198
- continue;
199
- picked.push(hit.note);
200
- seen.add(hit.note.text);
201
- }
202
- for (const note of (newest > 0 ? notes.slice(-newest) : [])) {
203
- if (!seen.has(note.text))
204
- picked.push(note);
205
- seen.add(note.text);
206
- }
207
- // stable output: keep chronological order
208
- picked.sort((a, b) => (a.time < b.time ? -1 : 1));
209
- const lines = picked.map((n) => `- [${n.time}] ${n.text}`);
210
- let text = lines.join('\n');
211
- if (text.length > maxChars)
212
- text = text.slice(0, maxChars) + '\n...[memory truncated]';
213
- return text;
214
- }
215
- /** Legacy full load (tail-bounded) - kept for callers that want everything. */
216
- export async function loadMemory(home) {
217
- try {
218
- const text = (await readFile(join(home, 'memory', 'memory.md'), 'utf8')).trim();
219
- if (!text)
220
- return '';
221
- return text.length > 8000 ? text.slice(-8000) : text;
222
- }
223
- catch {
224
- return '';
225
- }
226
- }
227
- export async function appendMemory(home, note, workspace) {
228
- const dir = join(home, 'memory');
229
- await mkdir(dir, { recursive: true });
230
- const stamp = new Date().toISOString().slice(0, 16).replace('T', ' ');
231
- const tag = workspace ? ` [ws:${workspace}]` : '';
232
- await appendFile(join(dir, 'memory.md'), `\n- [${stamp}] ${note.replace(/\n+/g, ' ').trim()}${tag}\n`, 'utf8');
233
- }
@@ -1,83 +0,0 @@
1
- /**
2
- * @hmharness/evolution - patches
3
- * CODE-LEVEL self-evolution: the evolution loop can now propose source-code
4
- * patches (not just prompt-level skills), test them on an isolated git
5
- * branch, and merge or revert based on the benchmark gate.
6
- *
7
- * The Darwin Gödel Machine insight: an agent that can only take notes
8
- * (skills/memory) learns WHAT to do; an agent that can patch its own code
9
- * learns WHAT IT CAN DO. This module is the bridge.
10
- *
11
- * Safety model (same shape as the skill gate, extended to code):
12
- * 1. patches may only modify files under packages/.../src/ (never config,
13
- * never security code, never the kernel loop itself)
14
- * 2. every patch runs on a git branch (sandbox), never on main
15
- * 3. the bench gate must show no regression (same double-sample rule)
16
- * 4. revert = git checkout main + branch delete (zero residue)
17
- */
18
- import { readFile } from 'node:fs/promises';
19
- /** A proposed code change: find-and-replace in one source file. */
20
- export interface CodePatch {
21
- name: string;
22
- description: string;
23
- /** relative path from repo root, must match packages/.../src/.../*.ts */
24
- file: string;
25
- /** exact string to find in the file (must be unique) */
26
- find: string;
27
- /** replacement string */
28
- replace: string;
29
- /** why this change helps (for the audit log) */
30
- reason: string;
31
- }
32
- export interface PatchOutcome {
33
- name: string;
34
- action: 'merged' | 'reverted' | 'error';
35
- reason: string;
36
- branch?: string;
37
- }
38
- /** Guard: only source files inside packages, never kernel internals. */
39
- export declare function isPatchableFile(file: string): boolean;
40
- /** Validate + apply a patch to a file on disk. Returns error if find-string
41
- * is not found or not unique. */
42
- export declare function applyPatch(repoRoot: string, patch: CodePatch): Promise<string>;
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. */
48
- export declare function createSandbox(repoRoot: string, name: string): Promise<string>;
49
- /** Commit the patch on the sandbox branch so it is fully isolated from main. */
50
- export declare function commitOnSandbox(repoRoot: string, patchName: string): Promise<void>;
51
- /** Merge the sandbox branch back to main (patch promoted). */
52
- export declare function mergeSandbox(repoRoot: string, branch: string): Promise<void>;
53
- /** Run the bench gate on the current branch. Returns pass rate (0-1) or -1 on build failure. */
54
- export declare function sandboxBench(repoRoot: string, runCase: (c: import('./bench.ts').BenchCase, skillsPrompt: string) => Promise<string>, cases: import('./bench.ts').BenchCase[]): Promise<number>;
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. */
60
- export declare function revertSandbox(repoRoot: string, branch: string): Promise<void>;
61
- /**
62
- * Full sandbox cycle: apply patch on a branch, rebuild, bench, merge or revert.
63
- * This is the CODE-LEVEL equivalent of the skill A/B gate.
64
- */
65
- export declare function runPatchSandbox(opts: {
66
- repoRoot: string;
67
- patch: CodePatch;
68
- baselineRate: number;
69
- runCase: (c: import('./bench.ts').BenchCase, skillsPrompt: string) => Promise<string>;
70
- benchCases: import('./bench.ts').BenchCase[];
71
- log?: (line: string) => void;
72
- }): Promise<PatchOutcome>;
73
- /**
74
- * Meta-model call: propose code patches from session signals.
75
- * The model sees the signals + the target file's current source and
76
- * outputs find/replace pairs. Constrained to optimization prompts:
77
- * fix a bug, speed up a hot path, improve error messages.
78
- */
79
- export declare function proposePatches(provider: {
80
- baseUrl: string;
81
- apiKey: string;
82
- model: string;
83
- }, signals: Record<string, unknown>, readFileFn: typeof readFile, repoRoot: string, say: (l: string) => void): Promise<CodePatch[]>;
@@ -1,253 +0,0 @@
1
- /**
2
- * @hmharness/evolution - patches
3
- * CODE-LEVEL self-evolution: the evolution loop can now propose source-code
4
- * patches (not just prompt-level skills), test them on an isolated git
5
- * branch, and merge or revert based on the benchmark gate.
6
- *
7
- * The Darwin Gödel Machine insight: an agent that can only take notes
8
- * (skills/memory) learns WHAT to do; an agent that can patch its own code
9
- * learns WHAT IT CAN DO. This module is the bridge.
10
- *
11
- * Safety model (same shape as the skill gate, extended to code):
12
- * 1. patches may only modify files under packages/.../src/ (never config,
13
- * never security code, never the kernel loop itself)
14
- * 2. every patch runs on a git branch (sandbox), never on main
15
- * 3. the bench gate must show no regression (same double-sample rule)
16
- * 4. revert = git checkout main + branch delete (zero residue)
17
- */
18
- import { readFile, writeFile } from 'node:fs/promises';
19
- import { join } from 'node:path';
20
- import { execFile } from 'node:child_process';
21
- import { promisify } from 'node:util';
22
- const execCb = promisify(execFile);
23
- /** git executable: PATH first, then known Windows locations (this repo's
24
- * dev machine has git only outside the test-process PATH). */
25
- async function git(args, opts) {
26
- const { homedir } = await import('node:os');
27
- // .cmd shims can't execFile-spawn on modern Node (EINVAL) - real .exe only
28
- const candidates = [
29
- 'git',
30
- 'C:\\Program Files\\Git\\cmd\\git.exe',
31
- 'C:\\Program Files (x86)\\Git\\cmd\\git.exe',
32
- 'C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\Common7\\IDE\\CommonExtensions\\Microsoft\\TeamFoundation\\Team Explorer\\Git\\cmd\\git.exe',
33
- ];
34
- let lastErr;
35
- for (const exe of candidates) {
36
- try {
37
- return await execCb(exe, args, { ...opts, windowsHide: true });
38
- }
39
- catch (err) {
40
- lastErr = err;
41
- const msg = String(err);
42
- if (!/ENOENT|not found|EINVAL/i.test(msg))
43
- throw err; // real failure, not missing-exe
44
- }
45
- }
46
- throw lastErr;
47
- }
48
- /** Guard: only source files inside packages, never kernel internals. */
49
- export function isPatchableFile(file) {
50
- if (!/^packages\/[a-z-]+\/src\/[a-zA-Z0-9_/.-]+\.ts$/.test(file))
51
- return false;
52
- // never allow patching the kernel loop itself (bootstrapping paradox)
53
- if (file.startsWith('packages/kernel/src/loop'))
54
- return false;
55
- if (file.startsWith('packages/kernel/src/provider'))
56
- return false;
57
- // never config or security
58
- if (/config|security|mcp/.test(file))
59
- return false;
60
- return true;
61
- }
62
- /** Validate + apply a patch to a file on disk. Returns error if find-string
63
- * is not found or not unique. */
64
- export async function applyPatch(repoRoot, patch) {
65
- if (!isPatchableFile(patch.file)) {
66
- return `refused: ${patch.file} is not a patchable source file (must be packages/*/src/*.ts, not kernel loop/provider/config)`;
67
- }
68
- const filePath = join(repoRoot, patch.file);
69
- let content;
70
- try {
71
- content = await readFile(filePath, 'utf8');
72
- }
73
- catch {
74
- return `file not found: ${patch.file}`;
75
- }
76
- const count = content.split(patch.find).length - 1;
77
- if (count === 0)
78
- return `find-string not found in ${patch.file}`;
79
- if (count > 1)
80
- return `find-string appears ${count}x in ${patch.file} (must be unique)`;
81
- const patched = content.replace(patch.find, patch.replace);
82
- await writeFile(filePath, patched, 'utf8');
83
- return 'applied';
84
- }
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. */
90
- export async function createSandbox(repoRoot, name) {
91
- const branch = `evolve/${name}-${Date.now().toString(36)}`;
92
- await git(['checkout', '-b', branch], { cwd: repoRoot, timeout: 10_000 });
93
- return branch;
94
- }
95
- /** Commit the patch on the sandbox branch so it is fully isolated from main. */
96
- export async function commitOnSandbox(repoRoot, patchName) {
97
- await git(['add', '-A'], { cwd: repoRoot, timeout: 10_000 });
98
- await git(['commit', '-m', `evolve(code-patch): ${patchName}`, '--no-verify'], { cwd: repoRoot, timeout: 15_000 });
99
- }
100
- /** Merge the sandbox branch back to main (patch promoted). */
101
- export async function mergeSandbox(repoRoot, branch) {
102
- await git(['checkout', 'main'], { cwd: repoRoot, timeout: 10_000 });
103
- await git(['merge', '--no-edit', branch], { cwd: repoRoot, timeout: 15_000 });
104
- await git(['branch', '-D', branch], { cwd: repoRoot, timeout: 5000 });
105
- }
106
- /** Run the bench gate on the current branch. Returns pass rate (0-1) or -1 on build failure. */
107
- export async function sandboxBench(repoRoot, runCase, cases) {
108
- // rebuild all packages (the patch may affect any layer)
109
- try {
110
- await execCb('npm', ['run', 'build'], { cwd: repoRoot, timeout: 120_000, windowsHide: true });
111
- }
112
- catch {
113
- return -1; // build failed = automatic reject
114
- }
115
- // double-sample rule: each case must pass BOTH runs
116
- let pass = 0;
117
- for (const c of cases) {
118
- let ok = true;
119
- for (let i = 0; i < 2 && ok; i++) {
120
- try {
121
- const out = await runCase(c, '');
122
- ok = c.expect.every((e) => out.toLowerCase().includes(e.toLowerCase()));
123
- }
124
- catch {
125
- ok = false;
126
- }
127
- }
128
- if (ok)
129
- pass++;
130
- }
131
- return cases.length > 0 ? pass / cases.length : -1;
132
- }
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. */
138
- export async function revertSandbox(repoRoot, branch) {
139
- await git(['checkout', 'main'], { cwd: repoRoot, timeout: 10_000 });
140
- await git(['branch', '-D', branch], { cwd: repoRoot, timeout: 5000 });
141
- }
142
- /**
143
- * Full sandbox cycle: apply patch on a branch, rebuild, bench, merge or revert.
144
- * This is the CODE-LEVEL equivalent of the skill A/B gate.
145
- */
146
- export async function runPatchSandbox(opts) {
147
- const say = opts.log ?? (() => undefined);
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
- }
160
- say(` code-patch "${patch.name}": creating sandbox branch`);
161
- let branch = '';
162
- try {
163
- branch = await createSandbox(opts.repoRoot, patch.name);
164
- const applyResult = await applyPatch(opts.repoRoot, patch);
165
- if (applyResult !== 'applied') {
166
- await revertSandbox(opts.repoRoot, branch);
167
- return { name: patch.name, action: 'error', reason: applyResult, branch };
168
- }
169
- // commit on the branch BEFORE benching: the patch must be fully isolated
170
- // so a later checkout of main can never carry uncommitted patch changes
171
- await commitOnSandbox(opts.repoRoot, patch.name);
172
- say(` code-patch "${patch.name}": applied + committed on branch, rebuilding + benching`);
173
- const rate = await sandboxBench(opts.repoRoot, opts.runCase, opts.benchCases);
174
- if (rate < 0) {
175
- await revertSandbox(opts.repoRoot, branch);
176
- return { name: patch.name, action: 'reverted', reason: 'build failed in sandbox', branch };
177
- }
178
- if (rate < opts.baselineRate) {
179
- await revertSandbox(opts.repoRoot, branch);
180
- return { name: patch.name, action: 'reverted', reason: `bench ${rate} < baseline ${opts.baselineRate}`, branch };
181
- }
182
- say(` code-patch "${patch.name}": bench ${rate} >= baseline ${opts.baselineRate}, merging`);
183
- await mergeSandbox(opts.repoRoot, branch);
184
- return { name: patch.name, action: 'merged', reason: `bench ${rate} >= baseline (merged to main)`, branch };
185
- }
186
- catch (err) {
187
- if (branch) {
188
- try {
189
- await revertSandbox(opts.repoRoot, branch);
190
- }
191
- catch { /* best effort */ }
192
- }
193
- return { name: patch.name, action: 'error', reason: String(err).slice(0, 200), branch };
194
- }
195
- }
196
- /**
197
- * Meta-model call: propose code patches from session signals.
198
- * The model sees the signals + the target file's current source and
199
- * outputs find/replace pairs. Constrained to optimization prompts:
200
- * fix a bug, speed up a hot path, improve error messages.
201
- */
202
- export async function proposePatches(provider, signals, readFileFn, repoRoot, say) {
203
- // pick candidate files: the most-used tools from insights
204
- const toolFiles = {
205
- run_command: 'packages/agent/src/tools.ts',
206
- web_search: 'packages/agent/src/tools.ts',
207
- web_fetch: 'packages/agent/src/tools.ts',
208
- harmony_build: 'packages/domain-harmony/src/index.ts',
209
- harmony_devices: 'packages/domain-harmony/src/index.ts',
210
- // add more as insights reveal hot paths
211
- };
212
- const toolsUsed = (signals.toolUsage ?? {});
213
- const hotTools = Object.entries(toolsUsed)
214
- .filter(([t]) => toolFiles[t])
215
- .sort((a, b) => b[1] - a[1])
216
- .slice(0, 2)
217
- .map(([t]) => t);
218
- if (hotTools.length === 0)
219
- return [];
220
- const fileContents = {};
221
- for (const t of hotTools) {
222
- const f = toolFiles[t];
223
- try {
224
- fileContents[f] = (await readFileFn(join(repoRoot, f), 'utf8')).slice(0, 6000);
225
- }
226
- catch { /* skip */ }
227
- }
228
- const system = [
229
- 'You are the code-evolution module of hmharness. Given session signals and the CURRENT source of the most-used tool files, propose at most 1 code patch as a JSON object.',
230
- 'A patch is: {"name":"kebab-case","description":"one line","file":"packages/.../src/...ts","find":"exact string from the source (must be unique)","replace":"the improved code","reason":"why"}',
231
- 'Rules: ONLY optimize what the signals show is slow/broken/verbose. Keep patches SMALL (under 20 lines of change). Do NOT restructure. Do NOT touch security, config, or the agent loop. If nothing genuinely needs a code fix, return [].',
232
- 'Respond with ONLY a JSON array.',
233
- ].join('\n');
234
- const user = `Session signals:\n${JSON.stringify(signals, null, 2)}\n\nHot tool source files:\n${JSON.stringify(fileContents, null, 2)}\n\nPropose at most 1 patch (or []).`;
235
- say(' code-evolution: asking meta-model for patch proposals');
236
- try {
237
- const { chat } = await import('@hmharness/kernel');
238
- const r = await chat(provider, [
239
- { role: 'system', content: system },
240
- { role: 'user', content: user },
241
- ]);
242
- const text = r.message.content ?? '[]';
243
- const start = text.indexOf('[');
244
- const end = text.lastIndexOf(']');
245
- if (start < 0 || end < 0)
246
- return [];
247
- const parsed = JSON.parse(text.slice(start, end + 1));
248
- return parsed.filter((p) => p.file && p.find && p.replace && isPatchableFile(p.file));
249
- }
250
- catch {
251
- return [];
252
- }
253
- }
@@ -1,3 +0,0 @@
1
- /** The newest radar brief, capped (the meta-model needs a headline, not
2
- * the whole document; stale-brief protection via the filename date). */
3
- export declare function latestRadarBrief(home: string, maxChars?: number, maxAgeDays?: number): Promise<string | null>;
@@ -1,40 +0,0 @@
1
- /**
2
- * @hmharness/evolution - radar (ops-signal feed)
3
- * The ops keeper's ecosystem radar scans OpenHarmony release sources and
4
- * writes dated briefs (home/ops/briefs/YYYY-MM-DD.md). This module hands
5
- * the NEWEST brief text to the evolution loop as context: toolchain flags
6
- * and API surfaces move with releases, and a skill proposal that doesn't
7
- * know "ArkUI 5.1.2 shipped last week" proposes stale advice.
8
- *
9
- * Read-only and best-effort: evolution never triggers a scan (that's
10
- * `hmh ops scan`'s own budgeted job) - it reads what the keeper already
11
- * published, and an absent brief simply means no ecosystem signal.
12
- */
13
- import { readdir, readFile } from 'node:fs/promises';
14
- import { join } from 'node:path';
15
- /** The newest radar brief, capped (the meta-model needs a headline, not
16
- * the whole document; stale-brief protection via the filename date). */
17
- export async function latestRadarBrief(home, maxChars = 1200, maxAgeDays = 14) {
18
- const dir = join(home, 'ops', 'briefs');
19
- let files;
20
- try {
21
- files = (await readdir(dir)).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
22
- }
23
- catch {
24
- return null;
25
- }
26
- const latest = files[files.length - 1];
27
- if (!latest)
28
- return null;
29
- const date = latest.replace(/\.md$/, '');
30
- if (Date.now() - new Date(date + 'T00:00:00Z').getTime() > maxAgeDays * 86400_000) {
31
- return null; // too old to be ecosystem "news"
32
- }
33
- try {
34
- const text = (await readFile(join(dir, latest), 'utf8')).trim();
35
- return text.length > 20 ? text.slice(0, maxChars) : null;
36
- }
37
- catch {
38
- return null;
39
- }
40
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * @hmharness/evolution - context ranker (V2 blueprint M5).
3
- *
4
- * Scores retrieval candidates for the context pack. First version uses the
5
- * blueprint's fixed weights; Evolution is meant to optimize the weights
6
- * later (that is why they live in one exported constant, not inline math).
7
- * All inputs are normalized 0..1 by the caller; tokenCost is normalized
8
- * against the candidate budget by normalize().
9
- */
10
- export interface ContextCandidate {
11
- /** where it came from: memory | skill | insight | agents-md | history */
12
- source: string;
13
- /** reference (text or id) - ranked, not necessarily injected verbatim */
14
- contentRef: string;
15
- relevance: number;
16
- recency: number;
17
- importance: number;
18
- dependency: number;
19
- similarity: number;
20
- /** raw token estimate; normalized by the ranker */
21
- tokenCost: number;
22
- }
23
- export declare const RANK_WEIGHTS: {
24
- readonly relevance: 0.3;
25
- readonly dependency: 0.2;
26
- readonly recency: 0.15;
27
- readonly importance: 0.15;
28
- readonly similarity: 0.1;
29
- readonly tokenCost: -0.1;
30
- };
31
- /** Rank candidates: fixed weights, token cost normalized to the batch max,
32
- * ties broken by cheaper-first. Returns a NEW sorted array. */
33
- export declare function rankContext(candidates: ContextCandidate[]): Array<ContextCandidate & {
34
- score: number;
35
- }>;
36
- /** Pick the top-K candidates under a token budget (greedy, ranked order). */
37
- export declare function packContext(candidates: ContextCandidate[], budgetTokens: number): Array<ContextCandidate & {
38
- score: number;
39
- }>;
40
- /** The four memory classes (V2 M5). Entries classify by prefix today:
41
- * [self-note]/tool lessons -> procedural; (distilled) -> semantic;
42
- * session recaps -> episodic; design/decision notes -> project. */
43
- export type MemoryClass = 'episodic' | 'semantic' | 'procedural' | 'project';
44
- export declare function classifyMemory(entry: string): MemoryClass;