@hmharness/evolution 0.14.7 → 0.14.10
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 +14 -0
- package/dist/evolve.js +24 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/patches.js +6 -2
- package/dist/readiness.js +14 -2
- package/dist/reward-model.d.ts +62 -0
- package/dist/reward-model.js +184 -0
- package/package.json +1 -1
package/dist/evolve.d.ts
CHANGED
|
@@ -95,4 +95,18 @@ export declare function runEvolution(opts: {
|
|
|
95
95
|
presetProposals?: SkillProposal[];
|
|
96
96
|
log?: (line: string) => void;
|
|
97
97
|
}): Promise<EvolveReport>;
|
|
98
|
+
/** Cost-regression veto (pass-by-rambling detector). Multiplicative cap
|
|
99
|
+
* alone is a HAIR TRIGGER on microscopic outputs: a case whose expected
|
|
100
|
+
* answer is ~4 tokens (cjk-ex-9: 等待中……) allows ~1 token of slack at
|
|
101
|
+
* 1.3x, so ANY skill that perturbs output by one character vetoes the
|
|
102
|
+
* candidate — ten consecutive candidates died exactly there. An absolute
|
|
103
|
+
* slack floor (+8 tokens) keeps the intent (real rambling is 2-10x) while
|
|
104
|
+
* letting tiny-output cases tolerate needle-scale perturbation. */
|
|
105
|
+
export declare function isCostRegression(base: number, cand: number, cap?: number): boolean;
|
|
106
|
+
/** Dual-metric veto helper: names of cases where the candidate passed but
|
|
107
|
+
* cost more than its baseline cap (rambling). Pure - unit-tested. */
|
|
108
|
+
export declare function costRegressionNames(train: Array<{
|
|
109
|
+
name: string;
|
|
110
|
+
costCap?: number;
|
|
111
|
+
}>, baseCost: Record<string, number>, candCost: Record<string, number>): string[];
|
|
98
112
|
export declare function screenForPoison(text: string): string | null;
|
package/dist/evolve.js
CHANGED
|
@@ -231,13 +231,9 @@ export async function runEvolution(opts) {
|
|
|
231
231
|
const regression = baseResults.some((b) => b.pass && !candResults.find((c) => c.name === b.name)?.pass);
|
|
232
232
|
// dual-metric veto: a passing case that costs > cost-cap x its
|
|
233
233
|
// baseline counts as a cost regression (the candidate passed by
|
|
234
|
-
// rambling)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const base = baseCost[c.name] ?? 0;
|
|
238
|
-
const cand = candCost[c.name] ?? 0;
|
|
239
|
-
return base > 0 && cand > base * cap;
|
|
240
|
-
}).map((c) => c.name);
|
|
234
|
+
// rambling); isCostRegression carries the absolute-slack floor that
|
|
235
|
+
// keeps microscopic-output cases from hair-triggering
|
|
236
|
+
const costRegressions = costRegressionNames(train, baseCost, candCost);
|
|
241
237
|
const summary = (rs) => rs.map((r) => `${r.name}:${r.pass ? 'pass' : 'FAIL'}`).join(' ');
|
|
242
238
|
if (regression || candRate < baseRate) {
|
|
243
239
|
await deleteDraft(home, p.name);
|
|
@@ -459,6 +455,7 @@ async function proposeSkills(provider, signals, say, ancestor, mergeWith) {
|
|
|
459
455
|
'Your job: read session signals and decide whether any repeatable procedure is worth crystallizing into a skill.',
|
|
460
456
|
'A skill is a markdown how-to document the agent reads on demand. Topics must be limited to: HarmonyOS toolchain usage (hdc/hvigorw/ohpm/DevEco), this framework\'s tools (list_dir/read_file/write_file/run_command/remember/harmony_*), and reusable task workflows observed in the signals.',
|
|
461
457
|
'Rules: name is kebab-case; description is one line; skill_md is at most 60 lines with concrete steps and example commands; do NOT propose skills about security config, approval policy, or anything outside the topics; if nothing is genuinely reusable, return an empty array.',
|
|
458
|
+
'COST GATE (learned from repeated rejections): every candidate runs against an output-cost cap - a skill whose text makes the agent MORE verbose on exact-output cases (it restates, adds preambles, or explains around the answer) is auto-rejected even when correctness improves. Write TERSE imperative steps (target under 25 lines): checklists and example commands only; never instruct the agent to add explanations, context, or restatements; the skill itself must teach doing LESS talking, not more.',
|
|
462
459
|
'Respond with ONLY a JSON array: [{"name":"...","description":"...","skill_md":"..."}] - no prose, no code fences.',
|
|
463
460
|
].join('\n');
|
|
464
461
|
// GEPA ancestor context: vary around a past rejection (its reason is the
|
|
@@ -511,6 +508,26 @@ async function distillMemory(provider, recentNotes, say) {
|
|
|
511
508
|
return null;
|
|
512
509
|
}
|
|
513
510
|
}
|
|
511
|
+
/** Cost-regression veto (pass-by-rambling detector). Multiplicative cap
|
|
512
|
+
* alone is a HAIR TRIGGER on microscopic outputs: a case whose expected
|
|
513
|
+
* answer is ~4 tokens (cjk-ex-9: 等待中……) allows ~1 token of slack at
|
|
514
|
+
* 1.3x, so ANY skill that perturbs output by one character vetoes the
|
|
515
|
+
* candidate — ten consecutive candidates died exactly there. An absolute
|
|
516
|
+
* slack floor (+8 tokens) keeps the intent (real rambling is 2-10x) while
|
|
517
|
+
* letting tiny-output cases tolerate needle-scale perturbation. */
|
|
518
|
+
export function isCostRegression(base, cand, cap = 1.3) {
|
|
519
|
+
if (base <= 0)
|
|
520
|
+
return false;
|
|
521
|
+
return cand > Math.max(base * cap, base + 8);
|
|
522
|
+
}
|
|
523
|
+
/** Dual-metric veto helper: names of cases where the candidate passed but
|
|
524
|
+
* cost more than its baseline cap (rambling). Pure - unit-tested. */
|
|
525
|
+
export function costRegressionNames(train, baseCost, candCost) {
|
|
526
|
+
return train.filter((c) => {
|
|
527
|
+
const cap = c.costCap ?? 1.3;
|
|
528
|
+
return isCostRegression(baseCost[c.name] ?? 0, candCost[c.name] ?? 0, cap);
|
|
529
|
+
}).map((c) => c.name);
|
|
530
|
+
}
|
|
514
531
|
/** First balanced JSON array in the text; tolerates fences and prose around it. */
|
|
515
532
|
function parseJsonArray(text) {
|
|
516
533
|
const cleaned = text.replace(/```(?:json)?/g, '');
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/patches.js
CHANGED
|
@@ -105,9 +105,13 @@ export async function mergeSandbox(repoRoot, branch) {
|
|
|
105
105
|
}
|
|
106
106
|
/** Run the bench gate on the current branch. Returns pass rate (0-1) or -1 on build failure. */
|
|
107
107
|
export async function sandboxBench(repoRoot, runCase, cases) {
|
|
108
|
-
// rebuild all packages (the patch may affect any layer)
|
|
108
|
+
// rebuild all packages (the patch may affect any layer). npm on Windows is
|
|
109
|
+
// a .cmd shim and modern Node refuses to execFile-spawn .cmd without a
|
|
110
|
+
// shell (CVE-2024-27980) - the sandbox build silently "failed" (and every
|
|
111
|
+
// patch reverted) on CI runners while looking fine where npm resolved to
|
|
112
|
+
// an executable. shell:true resolves the shim on win32, no-op elsewhere.
|
|
109
113
|
try {
|
|
110
|
-
await execCb('npm', ['run', 'build'], { cwd: repoRoot, timeout: 120_000, windowsHide: true });
|
|
114
|
+
await execCb('npm', ['run', 'build'], { cwd: repoRoot, timeout: 120_000, windowsHide: true, ...(process.platform === 'win32' ? { shell: true } : {}) });
|
|
111
115
|
}
|
|
112
116
|
catch {
|
|
113
117
|
return -1; // build failed = automatic reject
|
package/dist/readiness.js
CHANGED
|
@@ -107,8 +107,20 @@ export async function rlReadiness(home) {
|
|
|
107
107
|
workflows: false,
|
|
108
108
|
};
|
|
109
109
|
try {
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
// skills layout has BOTH shapes in the wild: legacy flat `skills/<name>.md`
|
|
111
|
+
// and current `skills/active/<name>/` directories (plus `skills/drafts/*.md`).
|
|
112
|
+
// A top-level-only .md check silently fails every modern install.
|
|
113
|
+
const entries = await readdir(join(home, 'skills'), { withFileTypes: true });
|
|
114
|
+
const activeDir = entries.find((e) => e.isDirectory() && e.name === 'active');
|
|
115
|
+
const draftsDir = entries.find((e) => e.isDirectory() && e.name === 'drafts');
|
|
116
|
+
const flatMd = entries.some((e) => e.isFile() && e.name.endsWith('.md'));
|
|
117
|
+
let active = 0;
|
|
118
|
+
if (activeDir)
|
|
119
|
+
active = (await readdir(join(home, 'skills', 'active'))).length;
|
|
120
|
+
let draftMd = 0;
|
|
121
|
+
if (draftsDir)
|
|
122
|
+
draftMd = (await readdir(join(home, 'skills', 'drafts'))).filter((f) => f.endsWith('.md')).length;
|
|
123
|
+
provenance.skills = flatMd || active > 0 || draftMd > 0;
|
|
112
124
|
}
|
|
113
125
|
catch { /* none */ }
|
|
114
126
|
try {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { HumanLabel } from './labels.ts';
|
|
2
|
+
export type { HumanLabel };
|
|
3
|
+
export interface RewardFeatures {
|
|
4
|
+
/** outcome bucket: 2=ok, 1=turn-budget, 0=error */
|
|
5
|
+
outcome: number;
|
|
6
|
+
/** failed tool calls / total tool calls, 0..1 */
|
|
7
|
+
toolFailRate: number;
|
|
8
|
+
/** agent turns used, raw count */
|
|
9
|
+
turns: number;
|
|
10
|
+
/** tool calls issued, raw count */
|
|
11
|
+
toolUses: number;
|
|
12
|
+
}
|
|
13
|
+
export interface RewardWeights {
|
|
14
|
+
/** logistic weights over [outcome, toolFailRate, log1p(turns), log1p(toolUses), bias] */
|
|
15
|
+
w: [number, number, number, number, number];
|
|
16
|
+
trainedAt: string;
|
|
17
|
+
nSamples: number;
|
|
18
|
+
trainRmse: number;
|
|
19
|
+
/** holdout RMSE when n >= 20 (80/20 split, deterministic) */
|
|
20
|
+
holdoutRmse?: number;
|
|
21
|
+
}
|
|
22
|
+
/** Human label record: re-used from labels.ts (single source). */
|
|
23
|
+
/** Insight record fields the model joins on. */
|
|
24
|
+
export interface InsightLike {
|
|
25
|
+
session: string;
|
|
26
|
+
outcome: string;
|
|
27
|
+
turns?: number;
|
|
28
|
+
toolUses?: number;
|
|
29
|
+
}
|
|
30
|
+
/** Join labels with insights and map to training rows. Pure - testable. */
|
|
31
|
+
export declare function trainingRows(labels: HumanLabel[], insights: InsightLike[]): Array<{
|
|
32
|
+
f: RewardFeatures;
|
|
33
|
+
y: number;
|
|
34
|
+
session: string;
|
|
35
|
+
}>;
|
|
36
|
+
/** Fit the reward model on joined human labels. Deterministic. */
|
|
37
|
+
export declare function fitRewardModel(labels: HumanLabel[], insights: InsightLike[]): RewardWeights;
|
|
38
|
+
/** Score a run's features with fitted weights -> reward in [0,1]. */
|
|
39
|
+
export declare function scoreFeatures(w: RewardWeights['w'], f: RewardFeatures): number;
|
|
40
|
+
/** Persistence: HMH_HOME/evolution/reward-model.json. */
|
|
41
|
+
export declare function saveRewardModel(home: string, m: RewardWeights): Promise<void>;
|
|
42
|
+
export declare function loadRewardModel(home: string): Promise<RewardWeights | null>;
|
|
43
|
+
/** DPO preference pair for external fine-tuning. */
|
|
44
|
+
export interface DpoPair {
|
|
45
|
+
prompt: string;
|
|
46
|
+
chosen: string;
|
|
47
|
+
rejected: string;
|
|
48
|
+
/** provenance for auditability */
|
|
49
|
+
chosenSession: string;
|
|
50
|
+
rejectedSession: string;
|
|
51
|
+
gap: number;
|
|
52
|
+
}
|
|
53
|
+
/** Build DPO pairs from joined labels + insights. Two classes:
|
|
54
|
+
* (a) IN-BUCKET (gold): same outcome, >=2 star gap - human preference the
|
|
55
|
+
* outcome reward cannot see (4-vs-5 star distinctions);
|
|
56
|
+
* (b) CROSS-BUCKET (classic): ok-vs-degraded completions of the SAME task
|
|
57
|
+
* template (task prefix match) - the textbook chosen/rejected pair.
|
|
58
|
+
* Pure - testable. */
|
|
59
|
+
export declare function exportDpoPairs(labels: HumanLabel[], insights: InsightLike[], taskOf: (session: string) => string, answerOf: (session: string) => string, opts?: {
|
|
60
|
+
minInBucketGap?: number;
|
|
61
|
+
crossBucketPrefix?: number;
|
|
62
|
+
}): DpoPair[];
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hmharness/evolution - reward-model (V3 RL phase, ADR-0010)
|
|
3
|
+
* A LEARNED reward model over run features, fit on the human star labels -
|
|
4
|
+
* the blueprint's "Reward -> RL" step after the M11 gate opened.
|
|
5
|
+
*
|
|
6
|
+
* Design (grounded in the Agent Lightning lesson: the app defines the task
|
|
7
|
+
* and the reward; the harness already produces rollouts):
|
|
8
|
+
* - features are what the harness already measures per session: outcome
|
|
9
|
+
* bucket, tool-failure rate, turns, tool uses (all z-free, reproducible)
|
|
10
|
+
* - the model is a tiny logistic regressor (zero deps, deterministic
|
|
11
|
+
* gradient descent) predicting the HUMAN score in [0,1]
|
|
12
|
+
* - the calibrated outcome-based reward (Spearman 0.981 vs human) is the
|
|
13
|
+
* prior; the learned model adds the fine, in-bucket distinctions the
|
|
14
|
+
* outcome signal cannot see (4-star vs 5-star runs)
|
|
15
|
+
* - weights persist to HMH_HOME/evolution/reward-model.json; refit as
|
|
16
|
+
* labels accumulate
|
|
17
|
+
*
|
|
18
|
+
* What this is NOT: a fine-tuned LLM. The blueprint explicitly defers
|
|
19
|
+
* model fine-tuning; DPO pairs for an external trainer are exported
|
|
20
|
+
* separately (exportDpoPairs).
|
|
21
|
+
*/
|
|
22
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
const FEATURES = [
|
|
25
|
+
(f) => f.outcome / 2, // 0..1
|
|
26
|
+
(f) => f.toolFailRate, // 0..1
|
|
27
|
+
(f) => Math.log1p(Math.min(f.turns, 200)) / Math.log(201), // 0..1
|
|
28
|
+
(f) => Math.log1p(Math.min(f.toolUses, 200)) / Math.log(201), // 0..1
|
|
29
|
+
];
|
|
30
|
+
function x(f) {
|
|
31
|
+
return [...FEATURES.map((fn) => fn(f)), 1];
|
|
32
|
+
}
|
|
33
|
+
function sigmoid(z) {
|
|
34
|
+
return 1 / (1 + Math.exp(-z));
|
|
35
|
+
}
|
|
36
|
+
/** Deterministic 80/20 split by index (no RNG - reproducible fits). */
|
|
37
|
+
function split(rows) {
|
|
38
|
+
const train = [];
|
|
39
|
+
const hold = [];
|
|
40
|
+
rows.forEach((r, i) => (i % 5 === 4 ? hold : train).push(r));
|
|
41
|
+
return { train, hold };
|
|
42
|
+
}
|
|
43
|
+
function fitLinear(rows, epochs = 4000, lr = 0.05, l2 = 0.01) {
|
|
44
|
+
const d = 5;
|
|
45
|
+
let w = new Array(d).fill(0);
|
|
46
|
+
const xs = rows.map((r) => x(r.f));
|
|
47
|
+
if (xs.length === 0)
|
|
48
|
+
return w;
|
|
49
|
+
for (let e = 0; e < epochs; e++) {
|
|
50
|
+
const grad = new Array(d).fill(0);
|
|
51
|
+
for (let i = 0; i < rows.length; i++) {
|
|
52
|
+
let z = 0;
|
|
53
|
+
for (let k = 0; k < d; k++)
|
|
54
|
+
z += w[k] * xs[i][k];
|
|
55
|
+
const err = sigmoid(z) - rows[i].y; // logistic loss gradient
|
|
56
|
+
for (let k = 0; k < d; k++)
|
|
57
|
+
grad[k] += err * xs[i][k];
|
|
58
|
+
}
|
|
59
|
+
for (let k = 0; k < d; k++)
|
|
60
|
+
w[k] -= lr * (grad[k] / rows.length + l2 * w[k]);
|
|
61
|
+
}
|
|
62
|
+
return w;
|
|
63
|
+
}
|
|
64
|
+
function rmse(w, rows) {
|
|
65
|
+
if (rows.length === 0)
|
|
66
|
+
return 0;
|
|
67
|
+
let s = 0;
|
|
68
|
+
for (const r of rows) {
|
|
69
|
+
let z = 0;
|
|
70
|
+
const xv = x(r.f);
|
|
71
|
+
for (let k = 0; k < w.length; k++)
|
|
72
|
+
z += w[k] * xv[k];
|
|
73
|
+
s += (sigmoid(z) - r.y) ** 2;
|
|
74
|
+
}
|
|
75
|
+
return Math.sqrt(s / rows.length);
|
|
76
|
+
}
|
|
77
|
+
/** Join labels with insights and map to training rows. Pure - testable. */
|
|
78
|
+
export function trainingRows(labels, insights) {
|
|
79
|
+
const bySession = new Map();
|
|
80
|
+
for (const i of insights)
|
|
81
|
+
if (!bySession.has(i.session))
|
|
82
|
+
bySession.set(i.session, i);
|
|
83
|
+
const rows = [];
|
|
84
|
+
for (const l of labels) {
|
|
85
|
+
const ins = bySession.get(l.session);
|
|
86
|
+
if (!ins)
|
|
87
|
+
continue;
|
|
88
|
+
const uses = Number(ins.toolUses ?? 0);
|
|
89
|
+
const outcome = ins.outcome === 'ok' ? 2 : ins.outcome === 'turn-budget' ? 1 : 0;
|
|
90
|
+
rows.push({
|
|
91
|
+
f: { outcome, toolFailRate: 0, turns: Number(ins.turns ?? 0), toolUses: uses },
|
|
92
|
+
y: l.score / 5,
|
|
93
|
+
session: l.session,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return rows;
|
|
97
|
+
}
|
|
98
|
+
/** Fit the reward model on joined human labels. Deterministic. */
|
|
99
|
+
export function fitRewardModel(labels, insights) {
|
|
100
|
+
const rows = trainingRows(labels, insights);
|
|
101
|
+
const { train, hold } = split(rows);
|
|
102
|
+
const w = fitLinear(train);
|
|
103
|
+
return {
|
|
104
|
+
w: w,
|
|
105
|
+
trainedAt: new Date().toISOString(),
|
|
106
|
+
nSamples: rows.length,
|
|
107
|
+
trainRmse: Number(rmse(w, train).toFixed(4)),
|
|
108
|
+
...(hold.length >= 4 ? { holdoutRmse: Number(rmse(w, hold).toFixed(4)) } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** Score a run's features with fitted weights -> reward in [0,1]. */
|
|
112
|
+
export function scoreFeatures(w, f) {
|
|
113
|
+
const xv = x(f);
|
|
114
|
+
let z = 0;
|
|
115
|
+
for (let k = 0; k < w.length; k++)
|
|
116
|
+
z += w[k] * xv[k];
|
|
117
|
+
return sigmoid(z);
|
|
118
|
+
}
|
|
119
|
+
/** Persistence: HMH_HOME/evolution/reward-model.json. */
|
|
120
|
+
export async function saveRewardModel(home, m) {
|
|
121
|
+
const dir = join(home, 'evolution');
|
|
122
|
+
await mkdir(dir, { recursive: true });
|
|
123
|
+
await writeFile(join(dir, 'reward-model.json'), JSON.stringify(m, null, 2) + '\n', 'utf8');
|
|
124
|
+
}
|
|
125
|
+
export async function loadRewardModel(home) {
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(await readFile(join(home, 'evolution', 'reward-model.json'), 'utf8'));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Build DPO pairs from joined labels + insights. Two classes:
|
|
134
|
+
* (a) IN-BUCKET (gold): same outcome, >=2 star gap - human preference the
|
|
135
|
+
* outcome reward cannot see (4-vs-5 star distinctions);
|
|
136
|
+
* (b) CROSS-BUCKET (classic): ok-vs-degraded completions of the SAME task
|
|
137
|
+
* template (task prefix match) - the textbook chosen/rejected pair.
|
|
138
|
+
* Pure - testable. */
|
|
139
|
+
export function exportDpoPairs(labels, insights, taskOf, answerOf, opts = {}) {
|
|
140
|
+
const minGap = opts.minInBucketGap ?? 0.4;
|
|
141
|
+
const prefixLen = opts.crossBucketPrefix ?? 60;
|
|
142
|
+
const rows = trainingRows(labels, insights);
|
|
143
|
+
const insOf = new Map(insights.map((i) => [i.session, i]));
|
|
144
|
+
const pairs = [];
|
|
145
|
+
for (const a of rows) {
|
|
146
|
+
for (const b of rows) {
|
|
147
|
+
if (a.session >= b.session)
|
|
148
|
+
continue;
|
|
149
|
+
const gap = a.y - b.y;
|
|
150
|
+
if (gap < minGap)
|
|
151
|
+
continue;
|
|
152
|
+
const hi = gap > 0 ? a : b;
|
|
153
|
+
const lo = gap > 0 ? b : a;
|
|
154
|
+
const hiIns = insOf.get(hi.session);
|
|
155
|
+
const loIns = insOf.get(lo.session);
|
|
156
|
+
if (!hiIns || !loIns)
|
|
157
|
+
continue;
|
|
158
|
+
const sameOutcome = hiIns.outcome === loIns.outcome;
|
|
159
|
+
let ok = false;
|
|
160
|
+
if (sameOutcome) {
|
|
161
|
+
ok = true; // in-bucket gold
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// cross-bucket: only pair completions of the SAME task template
|
|
165
|
+
// (bench templates share long prefixes) - otherwise they teach
|
|
166
|
+
// nothing transferable
|
|
167
|
+
const ta = taskOf(hi.session);
|
|
168
|
+
const tb = taskOf(lo.session);
|
|
169
|
+
ok = ta.length >= prefixLen && ta.slice(0, prefixLen) === tb.slice(0, prefixLen);
|
|
170
|
+
}
|
|
171
|
+
if (!ok)
|
|
172
|
+
continue;
|
|
173
|
+
pairs.push({
|
|
174
|
+
prompt: taskOf(hi.session),
|
|
175
|
+
chosen: answerOf(hi.session),
|
|
176
|
+
rejected: answerOf(lo.session),
|
|
177
|
+
chosenSession: hi.session,
|
|
178
|
+
rejectedSession: lo.session,
|
|
179
|
+
gap: Number(gap.toFixed(2)),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return pairs;
|
|
184
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hmharness/evolution",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.10",
|
|
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",
|