harness-everything 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +33 -0
- package/behavioral-evals/README.md +39 -0
- package/behavioral-evals/case-validator.js +34 -0
- package/behavioral-evals/cases/baseline-worktree-submodule-reachability.yaml +30 -0
- package/behavioral-evals/fixtures/setup/fail.js +3 -0
- package/behavioral-evals/fixtures/setup/noop.js +4 -0
- package/behavioral-evals/fixtures/setup/verify-worktree-submodule.js +46 -0
- package/behavioral-evals/fixtures/setup/worktree-submodule.js +86 -0
- package/behavioral-evals/paired-benchmark.js +39 -5
- package/behavioral-evals/run-with-plugin.js +53 -39
- package/behavioral-evals/run.js +88 -9
- package/benchmarks/fixtures/system-one-holdout-reviews.json +18 -18
- package/benchmarks/fixtures/system-one-holdout.json +9 -9
- package/benchmarks/results/system-one/harness-routing-v1-cpu-2026-09-24/README.md +75 -0
- package/benchmarks/results/system-one/harness-routing-v1-cpu-2026-09-24/report.json +10832 -0
- package/benchmarks/results/system-one/harness-routing-v1-ngram-2026-09-24/README.md +72 -0
- package/benchmarks/results/system-one/harness-routing-v1-ngram-2026-09-24/report.json +10817 -0
- package/benchmarks/results/system-one/harness-routing-v3-ngram-2026-09-24/README.md +63 -0
- package/benchmarks/results/system-one/harness-routing-v3-ngram-2026-09-24/report.json +10817 -0
- package/benchmarks/results/system-one/labeler-holdout-check-2026-09-24/report.json +1104 -0
- package/benchmarks/results/system-one/labeler-holdout-check-codex-2026-09-24/report.json +1104 -0
- package/benchmarks/results/system-one/labeler-holdout-check-codex-tier3-2026-09-24/report.json +1104 -0
- package/ci/mechanism-2j-behavioral-runner.test.js +121 -1
- package/ci/mechanism-2j-fixture-entrypoints.test.js +153 -0
- package/ci/mechanism-system-one-evaluation.test.js +17 -0
- package/ci/mechanism-system-one-label.test.js +27 -0
- package/ci/mechanism-system-one-ngram.test.js +161 -0
- package/ci/mechanism-system-one-train.test.js +12 -0
- package/ci/system-one-ngram-parity.py +19 -0
- package/ci/system-one-train-test.py +6 -0
- package/ci/test-behavioral-case-validation.js +26 -0
- package/docs/system-one-corpus.md +31 -3
- package/docs/system-one-routing.md +84 -2
- package/docs/system-one-training.md +14 -2
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/system-one/evaluate.js +3 -2
- package/harness-everything/scripts/system-one/ngram.js +84 -0
- package/harness-everything/scripts/system-one/provider.js +15 -5
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/evaluate.js +3 -2
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/ngram.js +84 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/provider.js +15 -5
- package/sbom.cdx.json +7 -7
- package/scripts/evaluate-system-one.js +16 -9
- package/scripts/system-one-calibrate.js +1 -1
- package/scripts/system-one-label.js +32 -3
- package/scripts/system-one-train-ngram.py +129 -0
- package/scripts/system-one-train.py +14 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Labels System One training prompts with an LLM against the owner's rules (docs/system-one-corpus.md).
|
|
4
4
|
// See docs/system-one-training.md. Labels stay local; only the holdout agreement report may be committed.
|
|
5
5
|
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
6
7
|
const path = require('node:path');
|
|
7
8
|
const { spawn } = require('node:child_process');
|
|
8
9
|
const { createHash } = require('node:crypto');
|
|
@@ -105,6 +106,32 @@ function commandRunner(model) {
|
|
|
105
106
|
child.stdin.end(prompt);
|
|
106
107
|
});
|
|
107
108
|
}
|
|
109
|
+
// One stateless `codex exec` per batch: prompt on stdin, schema-checked last message in a temp file,
|
|
110
|
+
// read-only sandbox, no session files and no user config (plugins/hooks), so nothing accumulates across batches.
|
|
111
|
+
function codexRunner(model) {
|
|
112
|
+
const base = process.env.HARNESS_S1_LABEL_COMMAND ? JSON.parse(process.env.HARNESS_S1_LABEL_COMMAND) : ['codex'];
|
|
113
|
+
const work = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-s1-codex-'));
|
|
114
|
+
const schemaFile = path.join(work, 'schema.json');
|
|
115
|
+
fs.writeFileSync(schemaFile, SCHEMA);
|
|
116
|
+
let n = 0;
|
|
117
|
+
return prompt => new Promise((resolve, reject) => {
|
|
118
|
+
const outFile = path.join(work, `reply-${process.pid}-${n++}.json`);
|
|
119
|
+
const args = ['exec', '--model', model, '-c', 'model_reasoning_effort=medium', '--sandbox', 'read-only', '--skip-git-repo-check',
|
|
120
|
+
'--ephemeral', '--ignore-user-config', '--output-schema', schemaFile, '-o', outFile, '-C', work, '-'];
|
|
121
|
+
const child = spawn(base[0], [...base.slice(1), ...args], { stdio: ['pipe', 'ignore', 'ignore'], windowsHide: true,
|
|
122
|
+
shell: process.platform === 'win32' && !process.env.HARNESS_S1_LABEL_COMMAND });
|
|
123
|
+
const timer = setTimeout(() => child.kill(), 600000);
|
|
124
|
+
child.on('error', err => { clearTimeout(timer); reject(err); });
|
|
125
|
+
child.on('close', code => {
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
let reply = null;
|
|
128
|
+
try { reply = JSON.parse(fs.readFileSync(outFile, 'utf8')); } catch (_) { reject(new Error(`codex-exit-${code}: no reply`)); return; }
|
|
129
|
+
try { fs.unlinkSync(outFile); } catch (_) { /* temp file */ }
|
|
130
|
+
resolve(reply);
|
|
131
|
+
});
|
|
132
|
+
child.stdin.end(prompt);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
108
135
|
function outsideRepo(file) {
|
|
109
136
|
const rel = path.relative(REPO, path.resolve(file));
|
|
110
137
|
if (!rel.startsWith('..') && !path.isAbsolute(rel)) throw new Error('refusing to write labels inside the repository');
|
|
@@ -113,10 +140,12 @@ function outsideRepo(file) {
|
|
|
113
140
|
async function main(argv) {
|
|
114
141
|
const [op, ...rest] = argv;
|
|
115
142
|
const opt = (name, fallback) => { const i = rest.indexOf(name); return i >= 0 ? rest[i + 1] : fallback; };
|
|
116
|
-
const
|
|
143
|
+
const engine = opt('--engine', 'claude');
|
|
144
|
+
if (!['claude', 'codex'].includes(engine)) throw new Error('--engine must be claude or codex');
|
|
145
|
+
const model = opt('--model', engine === 'codex' ? 'gpt-5.6-luna' : 'sonnet');
|
|
117
146
|
const rules = rulesFromDoc(fs.readFileSync(path.join(REPO, 'docs', 'system-one-corpus.md'), 'utf8'));
|
|
118
|
-
const labeler = { model, rulesSha256: sha(rules).slice(0, 16) };
|
|
119
|
-
const run = commandRunner(model);
|
|
147
|
+
const labeler = { model, rulesSha256: sha(rules).slice(0, 16), ...(engine === 'codex' ? { engine } : {}) };
|
|
148
|
+
const run = engine === 'codex' ? codexRunner(model) : commandRunner(model);
|
|
120
149
|
const concurrency = Number(opt('--concurrency', '4')); const batchSize = Number(opt('--batch', '100'));
|
|
121
150
|
if (op === 'label') {
|
|
122
151
|
const input = opt('--in'); const out = opt('--out');
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Train the System One ngram tier scorer (docs/system-one-training.md, docs/system-one-routing.md "N-gram provider").
|
|
3
|
+
|
|
4
|
+
Featurization is stdlib-only and must match harness-everything/scripts/system-one/ngram.js exactly
|
|
5
|
+
(ci/mechanism-system-one-ngram.test.js compares them). torch is imported only inside train() for the optimizer.
|
|
6
|
+
"""
|
|
7
|
+
import argparse
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import math
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import struct
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
sys.dont_write_bytecode = True
|
|
16
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
17
|
+
DEFAULT_HOLDOUT = REPO / 'benchmarks' / 'fixtures' / 'system-one-holdout.json'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def fnv1a32(text):
|
|
21
|
+
h = 0x811c9dc5
|
|
22
|
+
for byte in text.encode('utf-8'):
|
|
23
|
+
h ^= byte
|
|
24
|
+
h = (h * 0x01000193) & 0xFFFFFFFF
|
|
25
|
+
return h
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def featurize(text, nmax, dim):
|
|
29
|
+
"""bucket -> weight: log(1 + count) per code-point n-gram, one length bucket, L2-normalized."""
|
|
30
|
+
t = ' '.join(text.lower().split())
|
|
31
|
+
counts = {}
|
|
32
|
+
for n in range(1, nmax + 1):
|
|
33
|
+
for i in range(len(t) - n + 1):
|
|
34
|
+
gram = t[i:i + n]
|
|
35
|
+
if not gram.strip():
|
|
36
|
+
continue
|
|
37
|
+
b = fnv1a32(f'{n}:{gram}') % dim
|
|
38
|
+
counts[b] = counts.get(b, 0) + 1
|
|
39
|
+
k = min(8, int(math.floor(math.log2(len(t.encode('utf-8')) + 1))))
|
|
40
|
+
counts[fnv1a32(f'len:{k}') % dim] = 1
|
|
41
|
+
feats = {b: math.log1p(c) for b, c in counts.items()}
|
|
42
|
+
norm = math.sqrt(sum(v * v for v in feats.values()))
|
|
43
|
+
return {b: v / norm for b, v in feats.items()}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load_jsonl(path):
|
|
47
|
+
return [json.loads(line) for line in Path(path).read_text(encoding='utf-8').splitlines() if line.strip()]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def sha256_file(path):
|
|
51
|
+
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def train(args):
|
|
55
|
+
import torch
|
|
56
|
+
|
|
57
|
+
torch.manual_seed(args.seed)
|
|
58
|
+
data = Path(args.data_dir)
|
|
59
|
+
corpus = json.loads(Path(args.holdout).read_text(encoding='utf-8'))
|
|
60
|
+
catalog = [o['id'] for o in corpus['cases'][0]['request']['options']]
|
|
61
|
+
gold = {r['id']: r['gold'] for r in load_jsonl(data / 'labels.jsonl')}
|
|
62
|
+
overrides = data / 'owner-overrides.jsonl'
|
|
63
|
+
if overrides.exists():
|
|
64
|
+
gold.update({r['id']: r['gold'] for r in load_jsonl(overrides)})
|
|
65
|
+
excluded = data / 'owner-excluded.jsonl'
|
|
66
|
+
skip = {r['id'] for r in load_jsonl(excluded)} if excluded.exists() else set()
|
|
67
|
+
rows = [p for p in load_jsonl(data / 'prompts.jsonl') if p['id'] in gold and p['id'] not in skip]
|
|
68
|
+
index = lambda g: catalog.index('unclassified' if g is None else g)
|
|
69
|
+
split = {name: [r for r in rows if r['split'] == name] for name in ('train', 'validation')}
|
|
70
|
+
|
|
71
|
+
def matrix(items):
|
|
72
|
+
ii, jj, vv = [], [], []
|
|
73
|
+
for r, item in enumerate(items):
|
|
74
|
+
for b, v in featurize(item['text'], args.nmax, args.dim).items():
|
|
75
|
+
ii.append(r); jj.append(b); vv.append(v)
|
|
76
|
+
return torch.sparse_coo_tensor([ii, jj], vv, (len(items), args.dim), dtype=torch.float32).coalesce()
|
|
77
|
+
|
|
78
|
+
x_train, x_val = matrix(split['train']), matrix(split['validation'])
|
|
79
|
+
y_train = torch.tensor([index(gold[r['id']]) for r in split['train']])
|
|
80
|
+
counts = torch.bincount(y_train, minlength=len(catalog)).float()
|
|
81
|
+
inverse = torch.where(counts > 0, counts.sum() / (len(catalog) * counts.clamp_min(1)), torch.zeros_like(counts))
|
|
82
|
+
weight = {'none': None, 'sqrt': inverse.sqrt(), 'full': inverse}[args.balance]
|
|
83
|
+
W = torch.zeros(args.dim, len(catalog), requires_grad=True)
|
|
84
|
+
bias = torch.zeros(len(catalog), requires_grad=True)
|
|
85
|
+
optimizer = torch.optim.Adam([W, bias], lr=args.lr)
|
|
86
|
+
for _ in range(args.epochs):
|
|
87
|
+
optimizer.zero_grad()
|
|
88
|
+
loss = torch.nn.functional.cross_entropy(torch.sparse.mm(x_train, W) + bias, y_train, weight=weight) + args.weight_decay * (W ** 2).sum()
|
|
89
|
+
loss.backward()
|
|
90
|
+
optimizer.step()
|
|
91
|
+
with torch.no_grad():
|
|
92
|
+
probs = torch.softmax(torch.sparse.mm(x_val, W) + bias, 1)
|
|
93
|
+
out = Path(args.out)
|
|
94
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
weights = W.detach().contiguous().view(-1).tolist()
|
|
96
|
+
(out / 'harness-routing-v1.bin').write_bytes(struct.pack(f'<{len(weights)}f', *weights))
|
|
97
|
+
datasets = {name: sha256_file(data / name) for name in ('prompts.jsonl', 'labels.jsonl')}
|
|
98
|
+
if overrides.exists():
|
|
99
|
+
datasets['owner-overrides.jsonl'] = sha256_file(overrides)
|
|
100
|
+
sidecar = {'format': 'harness-ngram', 'formatVersion': 1, 'dim': args.dim, 'nmax': args.nmax, 'hash': 'fnv1a32', 'catalog': catalog,
|
|
101
|
+
'bias': [float(v) for v in bias.detach().tolist()],
|
|
102
|
+
'metadata': {'domain': 'harness-routing-v1', 'balance': args.balance, 'weightDecay': args.weight_decay, 'epochs': args.epochs,
|
|
103
|
+
'seed': args.seed, 'datasets': datasets, 'counts': {k: len(v) for k, v in split.items()}}}
|
|
104
|
+
(out / 'harness-routing-v1.json').write_text(json.dumps(sidecar), encoding='utf-8')
|
|
105
|
+
with (out / 'val-scores.jsonl').open('w', encoding='utf-8') as fh:
|
|
106
|
+
for r, p in zip(split['validation'], probs.tolist()):
|
|
107
|
+
fh.write(json.dumps({'id': r['id'], 'gold': gold[r['id']], 'probs': p}) + '\n')
|
|
108
|
+
y_val = torch.tensor([index(gold[r['id']]) for r in split['validation']])
|
|
109
|
+
accuracy = (probs.argmax(1) == y_val).float().mean().item() if len(y_val) else 0.0
|
|
110
|
+
print(json.dumps({'counts': sidecar['metadata']['counts'], 'validationAccuracy': accuracy}))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main(argv=None):
|
|
114
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
115
|
+
parser.add_argument('--data-dir', required=True)
|
|
116
|
+
parser.add_argument('--out', required=True)
|
|
117
|
+
parser.add_argument('--holdout', default=str(DEFAULT_HOLDOUT), help='source of the fixed catalog only; holdout rows are never trained on')
|
|
118
|
+
parser.add_argument('--dim', type=int, default=1 << 16)
|
|
119
|
+
parser.add_argument('--nmax', type=int, default=3)
|
|
120
|
+
parser.add_argument('--balance', choices=['none', 'sqrt', 'full'], default='sqrt')
|
|
121
|
+
parser.add_argument('--weight-decay', type=float, default=1e-4)
|
|
122
|
+
parser.add_argument('--epochs', type=int, default=60)
|
|
123
|
+
parser.add_argument('--lr', type=float, default=0.01)
|
|
124
|
+
parser.add_argument('--seed', type=int, default=7)
|
|
125
|
+
train(parser.parse_args(argv))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == '__main__':
|
|
129
|
+
main()
|
|
@@ -50,6 +50,16 @@ def join(prompts, labels, overrides):
|
|
|
50
50
|
return [{'id': p['id'], 'split': p['split'], 'text': p['text'], 'gold': gold[p['id']]} for p in prompts if p['id'] in gold]
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
def class_weights(label_indices, num_classes):
|
|
54
|
+
"""Inverse-frequency weights that average to 1 over the examples; absent classes get 0."""
|
|
55
|
+
counts = [0] * num_classes
|
|
56
|
+
for i in label_indices:
|
|
57
|
+
counts[i] += 1
|
|
58
|
+
present = sum(1 for c in counts if c)
|
|
59
|
+
total = len(label_indices)
|
|
60
|
+
return [total / (present * c) if c else 0.0 for c in counts]
|
|
61
|
+
|
|
62
|
+
|
|
53
63
|
def make_batches(lengths, token_budget):
|
|
54
64
|
"""Length-bucketed batches: each batch's size times its longest item stays within the budget."""
|
|
55
65
|
order = sorted(range(len(lengths)), key=lambda i: (lengths[i], i))
|
|
@@ -94,6 +104,8 @@ def train(args):
|
|
|
94
104
|
train_batches = make_batches(lengths(split['train']), args.token_budget)
|
|
95
105
|
val_batches = make_batches(lengths(split['validation']), args.token_budget)
|
|
96
106
|
optimizer = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=0.01)
|
|
107
|
+
# Class balance counters the tier2-heavy labels; validation NLL stays unweighted so epochs remain comparable.
|
|
108
|
+
weight = torch.tensor(class_weights([e.label for e in train_x], len(catalog)), dtype=torch.float) if args.balance else None
|
|
97
109
|
|
|
98
110
|
def evaluate():
|
|
99
111
|
model.eval()
|
|
@@ -118,7 +130,7 @@ def train(args):
|
|
|
118
130
|
total = 0.0
|
|
119
131
|
for b in order:
|
|
120
132
|
tensors = collator([train_x[i] for i in train_batches[b]])
|
|
121
|
-
loss = torch.nn.functional.cross_entropy(model(tensors), tensors['labels'])
|
|
133
|
+
loss = torch.nn.functional.cross_entropy(model(tensors), tensors['labels'], weight=weight)
|
|
122
134
|
optimizer.zero_grad()
|
|
123
135
|
loss.backward()
|
|
124
136
|
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
|
@@ -161,6 +173,7 @@ def main(argv=None):
|
|
|
161
173
|
parser.add_argument('--threads', type=int, default=4)
|
|
162
174
|
parser.add_argument('--context-tokens', type=int, default=1024)
|
|
163
175
|
parser.add_argument('--token-budget', type=int, default=16384)
|
|
176
|
+
parser.add_argument('--balance', action='store_true', help='inverse-frequency class weights in the training loss')
|
|
164
177
|
train(parser.parse_args(argv))
|
|
165
178
|
|
|
166
179
|
|