feat: scaffold redsen-lean-harness v0.1.0

Recovered from crashed session (Node OOM). Repo contains full P0-P6
scaffold: plugin.json/marketplace.json, AGENTS.md, ADRs 0001-0006,
lh CLI (init/index/graph/lane/run/memory/host/report/doctor), 10
.github/agents, 12 CLI skills, instructions, context7 mcp.json, and
unit/e2e test suite.

Fixed: run.mjs read --in-tokens/--out-tokens but tests and CLI docs
use --input-tokens/--output-tokens, so telemetry totals were always 0.
Now accepts both forms.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-09-09 22:44:15 +02:00
co-authored by Copilot
commit 383129f571
79 changed files with 7855 additions and 0 deletions
+287
View File
@@ -0,0 +1,287 @@
import { test, describe, after } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, existsSync, appendFileSync } from 'node:fs';
import { join } from 'node:path';
import { tempRepo, write, commitAll, lh, cleanup } from './helpers.mjs';
after(cleanup);
const PKG = JSON.stringify({
name: 'fixture',
version: '1.0.0',
scripts: { test: "node -e \"console.log('ok')\"" },
}, null, 2);
/** A brownfield repo carrying two planted structural defects. */
function brownfield() {
return tempRepo({
'package.json': PKG,
'src/a.js': 'export function helper(x) { return x + 1; }\nexport function shared() { return 1; }\n',
'src/b.js': "import { helper } from './a.js';\nexport function shared() { return 2; }\nexport function main() { return helper(missingFn()); }\n",
'README.md': '# fixture\n',
});
}
describe('cli contract', () => {
test('--help exits 0 and lists the command surface', () => {
const r = lh(process.cwd(), ['--help']);
assert.equal(r.code, 0);
for (const cmd of ['init', 'index', 'graph', 'lane', 'run', 'memory', 'host', 'report', 'doctor']) {
assert.ok(r.stdout.includes(cmd), `help should mention ${cmd}`);
}
});
test('an unknown command fails rather than silently succeeding', () => {
assert.notEqual(lh(process.cwd(), ['no-such-command']).code, 0);
});
test('host detection always emits a usable strategy', () => {
const r = lh(brownfield(), ['host', '--json']);
assert.equal(r.code, 0);
const host = JSON.parse(r.stdout);
assert.ok(host.host, 'a host must always be named');
assert.ok(host.maxWriteLanes >= 1, 'must allow at least one write lane');
assert.ok(host.readOnlyFanOut >= 1, 'must allow at least one scout');
});
});
describe('init (greenfield + brownfield)', () => {
test('greenfield init creates config and memory shards', () => {
const dir = tempRepo({ 'README.md': '# empty\n' });
assert.equal(lh(dir, ['init', '--yes']).code, 0);
assert.ok(existsSync(join(dir, '.agents/harness.config.json')));
assert.ok(existsSync(join(dir, '.agents/memory/INDEX.md')));
for (const shard of ['failures', 'corrections', 'insights', 'conventions', 'quirks']) {
assert.ok(existsSync(join(dir, `.agents/memory/${shard}.md`)), `missing shard ${shard}`);
}
});
test('brownfield init autodetects the existing verify command', () => {
const dir = brownfield();
const r = lh(dir, ['init', '--yes', '--json']);
assert.equal(r.code, 0);
const out = JSON.parse(r.stdout);
assert.ok(out.verify.some((v) => v.command.includes('test')), 'should detect npm test');
});
test('the Context7 key is never written to disk', () => {
const dir = brownfield();
lh(dir, ['init', '--yes']);
const raw = readFileSync(join(dir, '.agents/harness.config.json'), 'utf8');
assert.ok(raw.includes('CONTEXT7_API_KEY'), 'config should reference the env var by name');
if (process.env.CONTEXT7_API_KEY) {
assert.ok(!raw.includes(process.env.CONTEXT7_API_KEY), 'the key value must never be persisted');
}
});
test('re-running init refuses to clobber an existing config', () => {
const dir = brownfield();
assert.equal(lh(dir, ['init', '--yes']).code, 0);
assert.notEqual(lh(dir, ['init', '--yes']).code, 0, 'second init must refuse');
});
test('doctor passes once the repo is initialised', () => {
const dir = brownfield();
lh(dir, ['init', '--yes']);
lh(dir, ['index']);
assert.equal(lh(dir, ['doctor']).code, 0);
});
test('doctor fails on an uninitialised repo', () => {
assert.notEqual(lh(tempRepo({ 'README.md': '#\n' }), ['doctor']).code, 0);
});
});
describe('index and structural gate', () => {
test('index works offline and reports symbols', () => {
const dir = brownfield();
lh(dir, ['init', '--yes']);
const r = lh(dir, ['index', '--stats']);
assert.equal(r.code, 0);
assert.match(r.stdout, /symbols\s+[1-9]/, 'should discover symbols without network access');
assert.ok(existsSync(join(dir, '.agents/.cache/symbols.json')));
});
test('a second index run hits the cache', () => {
const dir = brownfield();
lh(dir, ['init', '--yes']);
lh(dir, ['index']);
assert.match(lh(dir, ['index', '--stats']).stdout, /cacheHits\s+[1-9]/);
});
test('the gate exits 1 and names both planted defects', () => {
const dir = brownfield();
lh(dir, ['init', '--yes']);
lh(dir, ['index']);
const r = lh(dir, ['graph', '--brief']);
assert.equal(r.code, 1, 'structural violations must exit 1 — the Ralph loop gates on this');
assert.match(r.stdout, /duplicate-definition/);
assert.match(r.stdout, /shared/);
assert.match(r.stdout, /unresolved-call/);
assert.match(r.stdout, /missingFn/);
});
test('the gate exits 0 on a clean repo', () => {
const dir = tempRepo({
'package.json': PKG,
'src/a.js': 'export function helper(x) { return x + 1; }\n',
'src/b.js': "import { helper } from './a.js';\nexport function main() { return helper(1); }\n",
});
lh(dir, ['init', '--yes']);
lh(dir, ['index']);
const r = lh(dir, ['graph', '--brief']);
assert.equal(r.code, 0, `clean repo must pass, got: ${r.stdout}${r.stderr}`);
});
});
describe('parallel write lanes', () => {
function laneRepo() {
const dir = tempRepo({
'package.json': PKG,
'src/a/deep/f.txt': 'one\n',
'src/b/g.txt': 'two\n',
'root.txt': 'base\n',
});
lh(dir, ['init', '--yes']);
return dir;
}
test('disjoint lanes both get a worktree', () => {
const dir = laneRepo();
assert.equal(lh(dir, ['lane', 'create', '--id', 'a', '--title', 'A', '--kind', 'write', '--scope', 'src/a/**']).code, 0);
assert.equal(lh(dir, ['lane', 'create', '--id', 'b', '--title', 'B', '--kind', 'write', '--scope', 'src/b/**']).code, 0);
assert.match(lh(dir, ['lane', 'list']).stdout, /\ba\b[\s\S]*\bb\b/);
});
test('an overlapping write lane is rejected', () => {
const dir = laneRepo();
lh(dir, ['lane', 'create', '--id', 'a', '--title', 'A', '--kind', 'write', '--scope', 'src/a/**']);
// src/a/deep/** is a strict subset of src/a/** — must still be refused.
const r = lh(dir, ['lane', 'create', '--id', 'c', '--title', 'C', '--kind', 'write', '--scope', 'src/a/deep/**']);
assert.notEqual(r.code, 0, 'overlapping write scopes must never both be leased');
assert.match(r.stderr + r.stdout, /overlap|intersect/i);
});
test('out-of-scope writes are detected and exit 1', () => {
const dir = laneRepo();
lh(dir, ['lane', 'create', '--id', 'a', '--title', 'A', '--kind', 'write', '--scope', 'src/a/**']);
const wt = join(dir, '.agents/.cache/worktrees/a');
write(wt, 'src/a/deep/f.txt', 'legit edit\n');
write(wt, 'root.txt', 'OUT OF SCOPE\n');
commitAll(wt, 'one legit, one violation');
const r = lh(dir, ['lane', 'status', 'a']);
assert.equal(r.code, 1, 'scope violation is Ralph exit criterion 5 — must be machine-detectable');
assert.match(r.stdout, /root\.txt/);
assert.match(r.stdout, /OUT/);
});
test('a clean lane merges, then a conflicting lane is blocked not auto-resolved', () => {
const dir = laneRepo();
lh(dir, ['lane', 'create', '--id', 'a', '--title', 'A', '--kind', 'write', '--scope', 'src/a/**']);
lh(dir, ['lane', 'create', '--id', 'b', '--title', 'B', '--kind', 'write', '--scope', 'src/b/**']);
const wtA = join(dir, '.agents/.cache/worktrees/a');
write(wtA, 'root.txt', 'LANE A\n');
commitAll(wtA, 'lane a');
const wtB = join(dir, '.agents/.cache/worktrees/b');
write(wtB, 'root.txt', 'LANE B\n');
commitAll(wtB, 'lane b');
assert.equal(lh(dir, ['lane', 'merge', 'a']).code, 0, 'first merge should be clean');
const conflict = lh(dir, ['lane', 'merge', 'b']);
assert.notEqual(conflict.code, 0, 'a conflict must fail loudly');
assert.match(conflict.stdout + conflict.stderr, /root\.txt/);
assert.match(lh(dir, ['lane', 'list']).stdout, /blocked/);
});
test('merge --abort restores a clean tree', () => {
const dir = laneRepo();
lh(dir, ['lane', 'create', '--id', 'a', '--title', 'A', '--kind', 'write', '--scope', 'src/a/**']);
lh(dir, ['lane', 'create', '--id', 'b', '--title', 'B', '--kind', 'write', '--scope', 'src/b/**']);
for (const id of ['a', 'b']) {
const wt = join(dir, `.agents/.cache/worktrees/${id}`);
write(wt, 'root.txt', `LANE ${id}\n`);
commitAll(wt, `lane ${id}`);
}
lh(dir, ['lane', 'merge', 'a']);
lh(dir, ['lane', 'merge', 'b']);
assert.equal(lh(dir, ['lane', 'merge', 'b', '--abort']).code, 0);
assert.equal(lh(dir, ['lane', 'drop', 'b', '--force']).code, 0);
});
});
describe('memory', () => {
function memRepo() {
const dir = brownfield();
lh(dir, ['init', '--yes']);
return dir;
}
test('put then retrieve by query', () => {
const dir = memRepo();
assert.equal(lh(dir, ['memory', 'put', '--shard', 'failures', '--title', 'flaky suite', '--body', 'Retry once before reporting.']).code, 0);
const r = lh(dir, ['memory', 'get', '--query', 'flaky']);
assert.equal(r.code, 0);
assert.match(r.stdout, /Retry once/);
});
test('writing a secret is refused', () => {
const dir = memRepo();
const r = lh(dir, ['memory', 'put', '--shard', 'insights', '--title', 'leak',
'--body', 'key ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789']);
assert.notEqual(r.code, 0, 'secrets must never reach the memory shards');
const shard = readFileSync(join(dir, '.agents/memory/insights.md'), 'utf8');
assert.ok(!shard.includes('ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 'shard must stay clean');
});
test('INDEX.md stays small — it is the only always-loaded file', () => {
const dir = memRepo();
for (let i = 0; i < 20; i++) {
lh(dir, ['memory', 'put', '--shard', 'insights', '--title', `note ${i}`, '--body', 'x'.repeat(200)]);
}
const index = readFileSync(join(dir, '.agents/memory/INDEX.md'), 'utf8');
assert.ok(index.length < 2000, `INDEX.md must stay lean, was ${index.length} bytes`);
});
});
describe('telemetry', () => {
test('a full run produces events, a board and a report', () => {
const dir = brownfield();
lh(dir, ['init', '--yes']);
const start = lh(dir, ['run', 'start', '--objective', 'test run', '--json']);
assert.equal(start.code, 0);
const runId = JSON.parse(start.stdout).runId;
assert.ok(runId, 'run start must return a runId');
assert.equal(lh(dir, ['run', 'event', '--run', runId, '--type', 'agent.end',
'--agent', 'scout', '--input-tokens', '100', '--output-tokens', '50']).code, 0);
assert.equal(lh(dir, ['run', 'end', '--run', runId]).code, 0);
assert.ok(existsSync(join(dir, `.agents/runs/${runId}/events.ndjson`)));
assert.ok(existsSync(join(dir, `.agents/runs/${runId}/board.md`)));
const report = lh(dir, ['report', runId, '--json']);
assert.equal(report.code, 0);
assert.equal(JSON.parse(report.stdout).totals.totalTokens, 150);
});
test('a malformed NDJSON line does not break reporting', () => {
const dir = brownfield();
lh(dir, ['init', '--yes']);
const runId = JSON.parse(lh(dir, ['run', 'start', '--objective', 'test run', '--json']).stdout).runId;
lh(dir, ['run', 'event', '--run', runId, '--type', 'agent.end', '--input-tokens', '10']);
const events = join(dir, `.agents/runs/${runId}/events.ndjson`);
appendFileSync(events, '{ this is not json\n');
lh(dir, ['run', 'event', '--run', runId, '--type', 'agent.end', '--input-tokens', '5']);
const r = lh(dir, ['report', runId, '--json']);
assert.equal(r.code, 0, 'the parser must tolerate corrupt lines');
assert.equal(JSON.parse(r.stdout).totals.inputTokens, 15, 'valid events must still be counted');
});
});
+56
View File
@@ -0,0 +1,56 @@
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, dirname } from 'node:path';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
export const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
export const CLI = join(ROOT, 'src', 'cli.mjs');
const created = [];
export function tempRepo(files = {}) {
const dir = mkdtempSync(join(tmpdir(), 'lh-test-'));
created.push(dir);
git(dir, ['init', '-q', '-b', 'main']);
git(dir, ['config', 'user.email', 'test@example.com']);
git(dir, ['config', 'user.name', 'Test']);
git(dir, ['config', 'commit.gpgsign', 'false']);
for (const [rel, body] of Object.entries(files)) write(dir, rel, body);
if (Object.keys(files).length) commitAll(dir, 'init');
return dir;
}
export function write(dir, rel, body) {
const full = join(dir, rel);
mkdirSync(dirname(full), { recursive: true });
writeFileSync(full, body);
return full;
}
export function git(dir, args) {
return execFileSync('git', args, { cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
}
export function commitAll(dir, msg) {
git(dir, ['add', '-A']);
git(dir, ['commit', '-qm', msg]);
}
/** Run the lh CLI. Never throws — returns {code, stdout, stderr}. */
export function lh(dir, args) {
try {
const stdout = execFileSync(process.execPath, [CLI, ...args], {
cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
});
return { code: 0, stdout, stderr: '' };
} catch (e) {
return { code: e.status ?? 1, stdout: e.stdout ?? '', stderr: e.stderr ?? '' };
}
}
export function cleanup() {
while (created.length) {
try { rmSync(created.pop(), { recursive: true, force: true }); } catch {}
}
}
+158
View File
@@ -0,0 +1,158 @@
import { test, describe, after } from 'node:test';
import assert from 'node:assert/strict';
import { defaultConfig, validateConfig, modelForRole, MEMORY_SHARDS, RALPH_EXIT_CRITERIA } from '../src/lib/config.mjs';
import { globsIntersect } from '../src/lib/lane.mjs';
import { scanSecrets } from '../src/lib/memory.mjs';
import { summarize } from '../src/lib/telemetry.mjs';
import { globMatch } from '../src/lib/repomap.mjs';
import { cleanup } from './helpers.mjs';
after(cleanup);
describe('config', () => {
test('default config is valid', () => {
assert.deepEqual(validateConfig(defaultConfig()), []);
});
test('rejects a bad isolation backend', () => {
const c = defaultConfig();
c.isolation.backend = 'kubernetes';
assert.ok(validateConfig(c).some((p) => p.includes('isolation.backend')));
});
test('rejects an unknown ralph exit criterion', () => {
const c = defaultConfig();
c.ralph.exitCriteria = ['vibes'];
assert.ok(validateConfig(c).some((p) => p.includes('vibes')));
});
test('rejects a non-positive memory budget', () => {
const c = defaultConfig();
c.memory.tokenBudget = 0;
assert.ok(validateConfig(c).some((p) => p.includes('memory.tokenBudget')));
});
test('cheap roles resolve to the cheap tier model', () => {
const c = defaultConfig();
const cheap = modelForRole(c, 'scout');
assert.equal(cheap, modelForRole(c, 'verifier'));
assert.notEqual(cheap, modelForRole(c, 'conductor'));
});
test('a role override is honoured', () => {
const c = defaultConfig();
c.models.roles.scout = 'strong';
assert.equal(modelForRole(c, 'scout'), modelForRole(c, 'conductor'));
});
test('exposes the five memory shards plus seed', () => {
for (const s of ['failures', 'corrections', 'insights', 'conventions', 'quirks']) {
assert.ok(MEMORY_SHARDS.includes(s), `missing shard ${s}`);
}
assert.ok(RALPH_EXIT_CRITERIA.length >= 5);
});
});
describe('lane scope safety', () => {
// The load-bearing invariant: two write lanes must never share a file.
const overlapping = [
['src/**', 'src/a/deep/**'],
['src/a/**', 'src/a/**'],
['**', 'anything/at/all.js'],
['src/a.js', 'src/a.js'],
['src/*.js', 'src/a.js'],
['lib/**/*.ts', 'lib/deep/nested/x.ts'],
];
for (const [a, b] of overlapping) {
test(`rejects overlap: ${a} vs ${b}`, () => {
assert.equal(globsIntersect(a, b), true, `${a} should intersect ${b}`);
assert.equal(globsIntersect(b, a), true, 'intersection must be symmetric');
});
}
const disjoint = [
['src/a/**', 'src/b/**'],
['src/a.js', 'src/b.js'],
['docs/**', 'src/**'],
['lib/**/*.ts', 'lib/x.js'],
];
for (const [a, b] of disjoint) {
test(`allows disjoint: ${a} vs ${b}`, () => {
assert.equal(globsIntersect(a, b), false, `${a} should not intersect ${b}`);
assert.equal(globsIntersect(b, a), false, 'disjointness must be symmetric');
});
}
});
describe('glob matching', () => {
test('** crosses directory boundaries', () => {
assert.equal(globMatch('src/**', 'src/a/b/c.js'), true);
});
test('* does not cross directory boundaries', () => {
assert.equal(globMatch('src/*.js', 'src/a/b.js'), false);
});
test('non-matching prefix fails', () => {
assert.equal(globMatch('src/**', 'docs/a.js'), false);
});
});
describe('secret scanning', () => {
const secrets = [
['github token', 'token ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'],
['openai key', 'sk-ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrst'],
['private key', '-----BEGIN RSA PRIVATE KEY-----'],
['aws key', 'AKIAIOSFODNN7EXAMPLE'],
];
for (const [name, text] of secrets) {
test(`detects a ${name}`, () => {
const hits = scanSecrets(text);
assert.ok(hits.length > 0, `expected a hit for ${name}`);
});
}
test('does not fire on ordinary prose', () => {
assert.deepEqual(scanSecrets('The build failed because the test timed out after 30s.'), []);
});
test('never echoes the raw secret back', () => {
const raw = 'ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
for (const hit of scanSecrets(`token ${raw}`)) {
assert.ok(!JSON.stringify(hit).includes(raw), 'secret must be redacted in the finding');
}
});
});
describe('telemetry summarize', () => {
test('sums token usage across events', () => {
const s = summarize([
{ ts: '2026-01-01T00:00:00.000Z', type: 'run.start' },
{ ts: '2026-01-01T00:00:10.000Z', type: 'agent.end', 'gen_ai.usage.input_tokens': 100, 'gen_ai.usage.output_tokens': 50 },
{ ts: '2026-01-01T00:01:00.000Z', type: 'agent.end', 'gen_ai.usage.input_tokens': 200, 'gen_ai.usage.output_tokens': 25 },
]);
assert.equal(s.totals.inputTokens, 300);
assert.equal(s.totals.outputTokens, 75);
assert.equal(s.totals.totalTokens, 375);
assert.equal(s.totals.wallMs, 60_000);
});
test('tolerates empty input', () => {
const s = summarize([]);
assert.equal(s.totals.totalTokens, 0);
assert.equal(s.totals.wallMs, 0);
});
test('ignores malformed timestamps rather than producing NaN', () => {
const s = summarize([
{ ts: 'not-a-date', type: 'run.start' },
{ ts: '2026-01-01T00:00:00.000Z', type: 'agent.end', 'gen_ai.usage.input_tokens': 10 },
]);
assert.ok(Number.isFinite(s.totals.wallMs));
assert.equal(s.totals.inputTokens, 10);
});
test('ignores non-numeric token fields', () => {
const s = summarize([{ ts: '2026-01-01T00:00:00.000Z', 'gen_ai.usage.input_tokens': 'lots' }]);
assert.equal(s.totals.inputTokens, 0);
});
});