Skip to content

Commit 43f5c15

Browse files
authored
Merge pull request #52 from alvinhui/main
Fix harness-creator bugs and sharpen skill triggering
2 parents 94bbd0b + f3c74e9 commit 43f5c15

6 files changed

Lines changed: 125 additions & 34 deletions

File tree

skills/harness-creator/SKILL.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
---
22
name: harness-creator
33
description: >-
4-
Build, audit, and improve lightweight harnesses for AI coding agents: AGENTS.md/CLAUDE.md,
5-
feature state, verification workflows, scope boundaries, lifecycle handoff,
6-
memory persistence, context control, tool safety, and multi-agent coordination.
4+
Build, audit, and improve harnesses that make AI coding agents reliable: AGENTS.md/CLAUDE.md
5+
instruction files, feature/state tracking, verification gates, scope boundaries, session
6+
handoff, memory persistence, context budgets, tool-permission safety, and multi-agent
7+
coordination. Use this whenever a coding agent is unreliable across sessions — forgets context,
8+
drifts out of scope, claims "done" before tests pass, or starts each session inconsistently —
9+
or when creating or assessing AGENTS.md, CLAUDE.md, feature_list.json, init.sh, progress.md, or
10+
session-handoff files. Reach for it even if the user never says the word "harness."
711
license: MIT
812
---
913

@@ -69,7 +73,7 @@ node skills/harness-creator/scripts/render-assessment-html.mjs --target /path/to
6973
node skills/harness-creator/scripts/run-benchmark.mjs --target /path/to/project --html /path/to/report.html
7074
```
7175

72-
Be clear that this is a structural benchmark. Real effectiveness still needs before/after agent sessions on representative tasks.
76+
Be clear that this is a structural benchmark. The benchmark first runs a self-check — it scaffolds a throwaway harness and validates it, proving the bundled scripts work end-to-end — then scores the target and eval coverage. Real effectiveness still needs before/after agent sessions on representative tasks.
7377

7478
## When to Read References
7579

skills/harness-creator/SKILL.md.en

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
---
22
name: harness-creator
33
description: >-
4-
Build, audit, and improve lightweight harnesses for AI coding agents: AGENTS.md/CLAUDE.md,
5-
feature state, verification workflows, scope boundaries, lifecycle handoff,
6-
memory persistence, context control, tool safety, and multi-agent coordination.
4+
Build, audit, and improve harnesses that make AI coding agents reliable: AGENTS.md/CLAUDE.md
5+
instruction files, feature/state tracking, verification gates, scope boundaries, session
6+
handoff, memory persistence, context budgets, tool-permission safety, and multi-agent
7+
coordination. Use this whenever a coding agent is unreliable across sessions — forgets context,
8+
drifts out of scope, claims "done" before tests pass, or starts each session inconsistently —
9+
or when creating or assessing AGENTS.md, CLAUDE.md, feature_list.json, init.sh, progress.md, or
10+
session-handoff files. Reach for it even if the user never says the word "harness."
711
license: MIT
812
---
913

@@ -69,7 +73,7 @@ node skills/harness-creator/scripts/render-assessment-html.mjs --target /path/to
6973
node skills/harness-creator/scripts/run-benchmark.mjs --target /path/to/project --html /path/to/report.html
7074
```
7175

72-
Be clear that this is a structural benchmark. Real effectiveness still needs before/after agent sessions on representative tasks.
76+
Be clear that this is a structural benchmark. The benchmark first runs a self-check — it scaffolds a throwaway harness and validates it, proving the bundled scripts work end-to-end — then scores the target and eval coverage. Real effectiveness still needs before/after agent sessions on representative tasks.
7377

7478
## When to Read References
7579

skills/harness-creator/references/tool-registry-pattern.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,7 @@ protected_commands:
161161
162162
## Related Patterns
163163
164-
- [Lifecycle & Bootstrap](lifecycle-bootstrap-pattern.md) — How tools are registered at init
165-
- Hook Lifecycle (`hook-lifecycle-pattern.md`) — Pre/post tool execution hooks
164+
- [Lifecycle & Bootstrap](lifecycle-bootstrap-pattern.md) — How tools are registered at init, and pre/post-tool hook dispatch
166165
167166
## Template: Tool Safety Checklist
168167

skills/harness-creator/scripts/lib/harness-utils.mjs

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,14 @@ export function verificationCommands(project, explicitPackageManager) {
150150
};
151151

152152
if (project.stack === 'python') {
153+
// python3 is the portable name (many systems no longer ship a bare `python`).
154+
// pytest exits 5 when it collects zero tests — harmless here, so don't let `set -e`
155+
// treat "no tests yet" as a failure. compileall's -x skips virtualenvs and build
156+
// artifacts so a syntax check doesn't choke on dependencies it shouldn't compile.
157+
const py = 'python3';
153158
return [
154-
'python -m pytest',
155-
'python -m compileall .'
159+
`${py} -m pytest || [ $? -eq 5 ]`,
160+
`${py} -m compileall -q -x '(^|/)(\\.?venv|env|node_modules|build|dist|__pycache__)(/|$)' .`
156161
];
157162
}
158163

@@ -224,17 +229,17 @@ export function scoreHarness(files) {
224229
const checks = {
225230
instructions: [
226231
hasFile(byPath, ['AGENTS.md', 'CLAUDE.md'], 'Agent instruction file exists'),
227-
textHas(agents, ['Startup Workflow', 'Before writing code'], 'Startup workflow documented'),
228-
textHas(agents, ['Definition of Done', 'done only when'], 'Definition of done documented'),
229-
textHas(agents, ['Verification Commands', './init.sh', 'test', 'verify'], 'Verification commands discoverable'),
230-
textHas(agents, ['feature_list.json', 'progress.md'], 'State artifacts routed from instructions')
232+
structuredHas(agents, ['Startup Workflow', 'Before writing code'], 'Startup workflow documented'),
233+
structuredHas(agents, ['Definition of Done', 'done only when'], 'Definition of done documented'),
234+
structuredHas(agents, ['Verification Commands', './init.sh', 'test', 'verify'], 'Verification commands discoverable'),
235+
structuredHas(agents, ['feature_list.json', 'progress.md'], 'State artifacts routed from instructions')
231236
],
232237
state: [
233238
hasFile(byPath, ['feature_list.json', 'feature-list.json'], 'Feature tracker exists'),
234239
jsonFeatureList(featureList, 'Feature tracker is valid and has feature fields'),
235240
hasFile(byPath, ['progress.md'], 'Progress log exists'),
236-
textHas(progress, ['Current State', 'What', 'Next'], 'Progress log supports restart'),
237-
textHas(handoff || progress, ['Blockers', 'Files', 'Next Session'], 'Handoff captures blockers/files/next step')
241+
structuredHas(progress, ['Current State', 'What', 'Next'], 'Progress log supports restart'),
242+
structuredHas(handoff || progress, ['Blockers', 'Files', 'Next Session'], 'Handoff captures blockers/files/next step')
238243
],
239244
verification: [
240245
hasFile(byPath, ['init.sh'], 'Verification entrypoint exists'),
@@ -244,17 +249,17 @@ export function scoreHarness(files) {
244249
textHas(allText, ['Evidence', 'Verification Evidence', 'command and output'], 'Verification evidence is recorded')
245250
],
246251
scope: [
247-
textHas(agents, ['One feature at a time', 'one-feature-at-a-time'], 'One-feature-at-a-time rule exists'),
252+
structuredHas(agents, ['One feature at a time', 'one-feature-at-a-time'], 'One-feature-at-a-time rule exists'),
248253
textHas(featureList, ['dependencies'], 'Feature dependencies are tracked'),
249254
textHas(agents + featureList, ['status'], 'Feature status is explicit'),
250-
textHas(agents, ['Stay in scope', 'scope'], 'Scope boundary documented'),
251-
textHas(agents, ['Definition of Done'], 'Completion gate limits scope closure')
255+
structuredHas(agents, ['Stay in scope', 'scope'], 'Scope boundary documented'),
256+
structuredHas(agents, ['Definition of Done'], 'Completion gate limits scope closure')
252257
],
253258
lifecycle: [
254259
hasFile(byPath, ['init.sh'], 'Startup script exists'),
255-
textHas(agents, ['End of Session', 'Before ending'], 'End-of-session procedure exists'),
260+
structuredHas(agents, ['End of Session', 'Before ending'], 'End-of-session procedure exists'),
256261
hasFile(byPath, ['session-handoff.md'], 'Session handoff template exists'),
257-
textHas(progress + handoff, ['Last Updated', 'Current Objective', 'Recommended Next Step'], 'Session restart markers exist'),
262+
structuredHas(progress + '\n' + handoff, ['Last Updated', 'Current Objective', 'Recommended Next Step'], 'Session restart markers exist'),
258263
textHas(agents + init, ['restartable', 'clean', 'Next steps'], 'Clean restart path documented')
259264
]
260265
};
@@ -272,7 +277,10 @@ export function scoreHarness(files) {
272277

273278
const total = Object.values(subsystems).reduce((sum, item) => sum + item.score, 0);
274279
const overall = Math.round((total / (SUBSYSTEMS.length * 5)) * 100);
275-
const bottleneck = Object.entries(subsystems).sort((a, b) => a[1].score - b[1].score)[0][0];
280+
const ranked = Object.entries(subsystems).sort((a, b) => a[1].score - b[1].score);
281+
// A bottleneck only means something when a subsystem is weaker than the rest.
282+
// When every subsystem already maxes out, reporting one is misleading.
283+
const bottleneck = ranked[0][1].score === 5 ? null : ranked[0][0];
276284
return { overall, bottleneck, subsystems };
277285
}
278286

@@ -285,6 +293,31 @@ function textHas(text, needles, message) {
285293
return { pass: needles.some((needle) => lower.includes(needle.toLowerCase())), message };
286294
}
287295

296+
// A real instruction doc carries its load-bearing phrases in structure — headings,
297+
// list items, tables, fenced code, or bold lead-ins — not in free prose. Scoring only
298+
// the structured lines means a genuine harness still passes, while a file that just
299+
// sprinkles the right keywords across a paragraph to game the score does not.
300+
function structuredText(markdown) {
301+
const kept = [];
302+
let inFence = false;
303+
for (const raw of markdown.split(/\r?\n/)) {
304+
const line = raw.trim();
305+
if (/^(```|~~~)/.test(line)) { inFence = !inFence; continue; }
306+
if (inFence) { kept.push(line); continue; }
307+
if (!line) continue;
308+
const isHeading = /^#{1,6}\s/.test(line);
309+
const isList = /^([-*+]|\d+\.)\s/.test(line);
310+
const isTable = line.startsWith('|');
311+
const isBoldLead = /^\*\*[^*]+\*\*/.test(line);
312+
if (isHeading || isList || isTable || isBoldLead) kept.push(line);
313+
}
314+
return kept.join('\n');
315+
}
316+
317+
function structuredHas(markdown, needles, message) {
318+
return textHas(structuredText(markdown), needles, message);
319+
}
320+
288321
function jsonFeatureList(text, message) {
289322
try {
290323
const parsed = JSON.parse(text);
@@ -324,7 +357,7 @@ export function formatScoreReport(result, root = '.') {
324357
const lines = [
325358
`Harness validation for ${root}`,
326359
`Overall: ${result.overall}/100`,
327-
`Bottleneck: ${result.bottleneck}`,
360+
`Bottleneck: ${result.bottleneck ?? 'none — all subsystems at full score'}`,
328361
''
329362
];
330363

@@ -378,7 +411,7 @@ export function htmlReport(result, title = 'Harness Assessment') {
378411
<p>Five-subsystem harness validation report.</p>
379412
<div class="summary">
380413
<div class="metric">Overall<strong>${result.overall}/100</strong></div>
381-
<div class="metric">Bottleneck<strong>${escapeHtml(result.bottleneck)}</strong></div>
414+
<div class="metric">Bottleneck<strong>${escapeHtml(result.bottleneck ?? 'none')}</strong></div>
382415
</div>
383416
</header>
384417
${rows}

skills/harness-creator/scripts/run-benchmark.mjs

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
#!/usr/bin/env node
2+
import { execFile } from 'node:child_process';
3+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
4+
import os from 'node:os';
25
import path from 'node:path';
6+
import { promisify } from 'node:util';
37
import { fileURLToPath } from 'node:url';
48
import {
59
formatScoreReport,
@@ -11,17 +15,20 @@ import {
1115
writeText
1216
} from './lib/harness-utils.mjs';
1317

18+
const execFileAsync = promisify(execFile);
19+
1420
const args = parseArgs(process.argv.slice(2));
1521
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
1622
const skillRoot = path.resolve(scriptDir, '..');
1723

1824
if (args.help) {
19-
console.log(`Usage: node scripts/run-benchmark.mjs [--target DIR] [--output FILE] [--html FILE]
25+
console.log(`Usage: node scripts/run-benchmark.mjs [--target DIR] [--output FILE] [--html FILE] [--no-self-check]
2026
2127
Runs a lightweight harness benchmark:
22-
1. Scores the current target harness.
23-
2. Checks eval coverage in evals/evals.json.
24-
3. Produces a JSON report and optional HTML report.
28+
1. Self-check: scaffold a throwaway harness and confirm it validates (proves the scripts work).
29+
2. Scores the current target harness.
30+
3. Checks eval coverage in evals/evals.json.
31+
4. Produces a JSON report and optional HTML report.
2532
2633
This is a structural benchmark, not an LLM judge. Use it before/after real agent sessions.`);
2734
process.exit(0);
@@ -34,9 +41,11 @@ const evalPath = path.resolve(args.evals || path.join(skillRoot, 'evals', 'evals
3441
const harnessResult = scoreHarness(await loadHarnessFiles(target));
3542
const evals = await readJson(evalPath);
3643
const evalResult = scoreEvals(evals);
44+
const selfCheck = args.noSelfCheck ? { skipped: true } : await runSelfCheck();
3745
const report = {
3846
generatedAt: new Date().toISOString(),
3947
target,
48+
selfCheck,
4049
harness: harnessResult,
4150
evals: evalResult,
4251
recommendation: recommend(harnessResult, evalResult)
@@ -45,6 +54,10 @@ const report = {
4554
await writeText(output, `${JSON.stringify(report, null, 2)}\n`);
4655
console.log(`Benchmark report written to ${output}`);
4756
console.log('');
57+
if (!selfCheck.skipped) {
58+
console.log(`Self-check: ${selfCheck.pass ? 'PASS' : 'FAIL'} — scaffolded harness scored ${selfCheck.score}/100`);
59+
if (!selfCheck.pass && selfCheck.error) console.log(` ${selfCheck.error}`);
60+
}
4861
console.log(formatScoreReport(harnessResult, target));
4962
console.log(`Eval coverage: ${evalResult.score}/100 (${evalResult.passed}/${evalResult.total})`);
5063
console.log(`Recommendation: ${report.recommendation}`);
@@ -55,10 +68,39 @@ if (args.html) {
5568
console.log(`HTML benchmark report written to ${htmlPath}`);
5669
}
5770

58-
if (harnessResult.overall < Number(args.minScore || 70) || evalResult.score < Number(args.minEvalScore || 80)) {
71+
if (
72+
harnessResult.overall < Number(args.minScore || 70) ||
73+
evalResult.score < Number(args.minEvalScore || 80) ||
74+
selfCheck.pass === false
75+
) {
5976
process.exitCode = 1;
6077
}
6178

79+
// Prove the bundled scripts actually work end-to-end: scaffold a harness into a throwaway
80+
// directory, then score it. A structural eval-coverage check can't catch a broken
81+
// create-harness.mjs — this can. Failure here means the skill ships broken, not just thin.
82+
async function runSelfCheck() {
83+
let dir;
84+
try {
85+
dir = await mkdtemp(path.join(os.tmpdir(), 'harness-selfcheck-'));
86+
await writeFile(
87+
path.join(dir, 'package.json'),
88+
JSON.stringify({ name: 'selfcheck', scripts: { check: 'tsc', test: 'vitest run', build: 'vite build' } })
89+
);
90+
await execFileAsync('node', [path.join(scriptDir, 'create-harness.mjs'), '--target', dir]);
91+
const scored = scoreHarness(await loadHarnessFiles(dir));
92+
return {
93+
pass: scored.overall >= Number(args.minSelfCheckScore || 90),
94+
score: scored.overall,
95+
bottleneck: scored.bottleneck
96+
};
97+
} catch (error) {
98+
return { pass: false, score: 0, error: error.message };
99+
} finally {
100+
if (dir) await rm(dir, { recursive: true, force: true });
101+
}
102+
}
103+
62104
function scoreEvals(evalsJson) {
63105
const cases = Array.isArray(evalsJson.evals) ? evalsJson.evals : [];
64106
const checks = [];
@@ -97,8 +139,14 @@ function recommend(harnessResult, evalResult) {
97139
}
98140

99141
function renderBenchmarkHtml(report) {
142+
const selfCheckSection = report.selfCheck?.skipped
143+
? ''
144+
: `<section>
145+
<h2>Script Self-Check <span>${report.selfCheck.pass ? 'PASS' : 'FAIL'}</span></h2>
146+
<p>Scaffolded a throwaway harness and scored it ${report.selfCheck.score}/100 — confirms the bundled scripts run end-to-end.${report.selfCheck.error ? ` Error: ${escapeHtml(report.selfCheck.error)}` : ''}</p>
147+
</section>`;
100148
const evalHtml = htmlReport(report.harness, `Harness Benchmark: ${path.basename(report.target)}`)
101-
.replace('</main>', `<section>
149+
.replace('</main>', `${selfCheckSection}<section>
102150
<h2>Eval Coverage <span>${report.evals.score}/100</span></h2>
103151
<p>${report.evals.passed}/${report.evals.total} benchmark checks passed across ${report.evals.cases} eval cases.</p>
104152
<ul>${report.evals.checks.map((check) => `<li class="${check.pass ? 'pass' : 'fail'}">${check.pass ? 'PASS' : 'FAIL'} ${escapeHtml(check.message)}</li>`).join('')}</ul>

skills/harness-creator/templates/init.sh

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,11 @@ if [ -f package.json ]; then
4444
}
4545
elif [ -f pyproject.toml ] || [ -f requirements.txt ]; then
4646
echo "=== Running Python verification ==="
47-
python -m pytest
48-
python -m compileall .
47+
PY="$(command -v python3 || command -v python)"
48+
# pytest exits 5 when no tests are collected — not a failure for a fresh project.
49+
"$PY" -m pytest || [ $? -eq 5 ]
50+
# -x skips virtualenvs/build dirs so the syntax check doesn't compile dependencies.
51+
"$PY" -m compileall -q -x '(^|/)(\.?venv|env|node_modules|build|dist|__pycache__)(/|$)' .
4952
elif [ -f go.mod ]; then
5053
echo "=== Running Go verification ==="
5154
go test ./...

0 commit comments

Comments
 (0)