reviewer-harness 1.14.0 → 1.15.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/README.md +18 -5
- package/bin/cli.js +93 -25
- package/lib/adapters/antigravity.js +4 -0
- package/lib/adapters/claude.js +3 -17
- package/lib/adapters/freebuff.js +4 -0
- package/lib/adapters/gemini.js +3 -17
- package/lib/adapters/opencode.js +3 -27
- package/lib/adapters/render.js +29 -0
- package/lib/agents.js +15 -9
- package/lib/commands.js +9 -3
- package/lib/config.js +15 -2
- package/lib/frontmatter.js +22 -4
- package/lib/init.js +4 -2
- package/lib/memory.js +6 -2
- package/lib/skills.js +83 -22
- package/lib/sync.js +334 -19
- package/lib/validate.js +46 -7
- package/package.json +5 -1
- package/skills/fix-team.md +114 -0
- package/skills/instruction_registers.md +38 -0
- package/skills/verify_before_done.md +58 -0
- package/skills/verify_provider_claims.md +44 -0
- package/skills/verify_publish_install_surface.md +48 -0
package/README.md
CHANGED
|
@@ -187,7 +187,7 @@ Expected output when everything is fine:
|
|
|
187
187
|
|
|
188
188
|
`validate` checks for empty or duplicate rules and scans the config for leaked API keys and tokens — secrets must never be synced into instruction files (they would leak into Git).
|
|
189
189
|
|
|
190
|
-
**Step 3 — Add the skills you want.** Pick from the
|
|
190
|
+
**Step 3 — Add the skills you want.** Pick from the 27 bundled skills (full list below):
|
|
191
191
|
|
|
192
192
|
```bash
|
|
193
193
|
reviewer-harness skills add safe-editing
|
|
@@ -237,12 +237,12 @@ npx reviewer-harness skills add no-hallucination
|
|
|
237
237
|
A skill is a playbook: a short, checkable set of rules for one job (safe editing, API security, UI taste, debugging, and so on), distilled from real, reputable sources — not generic advice. Every rule cites its source.
|
|
238
238
|
|
|
239
239
|
```bash
|
|
240
|
-
reviewer-harness skills list # see all
|
|
240
|
+
reviewer-harness skills list # see all 27 bundled skills (+ which are enabled)
|
|
241
241
|
reviewer-harness skills add <name> # install into the project and enable it in the config
|
|
242
242
|
reviewer-harness skills remove <name> # disable it in the config and remove it
|
|
243
243
|
```
|
|
244
244
|
|
|
245
|
-
All
|
|
245
|
+
All 27 bundled skills (v1.14.0):
|
|
246
246
|
|
|
247
247
|
| Skill | What it does |
|
|
248
248
|
|---|---|
|
|
@@ -268,6 +268,11 @@ All 22 bundled skills (v1.14.0):
|
|
|
268
268
|
| `vengeance-ui` | Modern animated React components from VengeanceUI — 79 copy-paste components across 9 families. |
|
|
269
269
|
| `brainstorming` | Find out what you actually want — and why — before anything gets built. Design doc first, approval first. |
|
|
270
270
|
| `writing-plans` | Turn an approved spec into a bite-sized TDD implementation plan. |
|
|
271
|
+
| `fix-team` | Run an 11-agent fix over an audit report's findings — 8 domain fixers plus ub-mode, superpowers, and a verification agent that runs last. Fix plan first, user approves, verification re-checks every fix. |
|
|
272
|
+
| `verify_before_done` | Check defined-and-called plus remote evidence before reporting done, pushed, fixed, or working — never present an untested guess as done. |
|
|
273
|
+
| `instruction_registers` | Name the user's target and action registers before building, pushing, or planning, and ask one clarifying question when they are ambiguous. |
|
|
274
|
+
| `verify_provider_claims` | Check a provider's live documentation or the user's verified setup before asserting its current availability, free tier, pricing, endpoints, or retired status. |
|
|
275
|
+
| `verify_publish_install_surface` | Verify a published version on each surface the user can see (commit/tag, registry, installed file) before claiming it is installed or live. |
|
|
271
276
|
|
|
272
277
|
Enabled skills render below the rules in every instruction file — and sync also generates a **slash command** for each one (`.opencode/commands/<name>.md` and `.claude/commands/<name>.md`), so every enabled skill appears in the `/` menu of OpenCode and Claude, ready to invoke on a specific task: `/safe-editing`, `/no-hallucination`, and so on.
|
|
273
278
|
|
|
@@ -304,6 +309,14 @@ Four guardrails run before any file is written:
|
|
|
304
309
|
3. **BACKUP** — before overwriting an existing file, sync writes a `.bak` copy first.
|
|
305
310
|
4. **IDEMPOTENT** — if nothing changed, sync touches nothing. Safe to run repeatedly.
|
|
306
311
|
|
|
312
|
+
### Scale limit — instruction files grow linearly with skills
|
|
313
|
+
|
|
314
|
+
Every enabled skill is inlined in full into every instruction file, bundle, and slash command. Measured at audit (22 skills): **266,198 bytes** of skill source → **2,660,559 bytes** of generated output across 124 files (**10×** amplification); `CLAUDE.md` alone is **262,243 bytes** today. Growth is linear: at 10× skills (220), each instruction file would be ≈ 2.6 MB and total output ≈ 26.6 MB. The binding constraint is the AI runtime's context window, not disk.
|
|
315
|
+
|
|
316
|
+
**Target ceiling the project accepts: total generated output ≤ 10 MB, and no single instruction file larger than 1 MB** — roughly 4× today's skills (≈ 80 skills). Two cheap wins already reduce the amplification: shared bundle directories are written once (Freebuff + Antigravity share `.agents/skills`), and each skill's content is built once per `sync` run instead of once per runtime.
|
|
317
|
+
|
|
318
|
+
Beyond that ceiling, the fix is a re-architecture (summaries + lazy-loading skills on demand) — that is **out of scope** for this fix; nothing here shrinks the files, it only documents the limit and the ceiling.
|
|
319
|
+
|
|
307
320
|
### Validate + secret scanning
|
|
308
321
|
|
|
309
322
|
`validate` inspects the config before it becomes instructions:
|
|
@@ -387,7 +400,7 @@ Requires Node.js 18+ (no dependencies). Run these inside your project — every
|
|
|
387
400
|
|
|
388
401
|
| Command | What it does |
|
|
389
402
|
|---|---|
|
|
390
|
-
| `reviewer-harness skills list` | List all
|
|
403
|
+
| `reviewer-harness skills list` | List all 27 bundled skills (+ which are enabled) |
|
|
391
404
|
| `reviewer-harness skills add <name>` | Install a skill into the project and enable it in the config |
|
|
392
405
|
| `reviewer-harness skills remove <name>` | Disable a skill and remove it |
|
|
393
406
|
| `reviewer-harness agents list` | List the bundled sub-agents |
|
|
@@ -452,7 +465,7 @@ Check that you edited the config in the same folder you are syncing (`--target`
|
|
|
452
465
|
5. ✅ Sub-agents and slash commands — `agents add`, `commands add`
|
|
453
466
|
6. ✅ Polish and publish — v1.0.0, LICENSE, publish-ready for GitHub/npx
|
|
454
467
|
7. ✅ v1.1.0 — all-English repo, validate-and-report rule, self-governing repo, modular skills, one-liner installers, Docker
|
|
455
|
-
8. ✅ v1.14.0 — the working peak:
|
|
468
|
+
8. ✅ v1.14.0 — the working peak: 27 skills (audit-team + fix-team, ub-mode, design-taste-frontend, ui-ux-pro-max, vengeance-ui, brainstorming, writing-plans, verification skills), Qoder as 8th runtime, system-design, nodejs-quality, all fix rounds verified WORKS, CRLF frontmatter fix (Windows sync without manual LF conversion), --help/--version flags fixed
|
|
456
469
|
|
|
457
470
|
## Contributing & Security
|
|
458
471
|
|
package/bin/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Commands: status | sync | init | validate | memory | skills | agents | commands | global
|
|
5
5
|
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
|
-
import { existsSync } from "node:fs";
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { loadConfig } from "../lib/config.js";
|
|
@@ -17,19 +17,49 @@ import { listBundledSkills, addSkill, removeSkill, loadBundledSkills, skillComma
|
|
|
17
17
|
import { listBundledAgents, addAgent } from "../lib/agents.js";
|
|
18
18
|
import { listBundledCommands, addCommand } from "../lib/commands.js";
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
// F-B1 (full fix): the top-level parse sits outside main().catch, so
|
|
21
|
+
// an unknown flag used to crash with a raw Node stack trace. Catch it
|
|
22
|
+
// here: clean one-line error + help on stderr, exit 1. helpText() is a
|
|
23
|
+
// hoisted function declaration, so it is callable this early.
|
|
24
|
+
let values;
|
|
25
|
+
let positionals;
|
|
26
|
+
try {
|
|
27
|
+
({ values, positionals } = parseArgs({
|
|
28
|
+
allowPositionals: true,
|
|
29
|
+
options: {
|
|
30
|
+
"dry-run": { type: "boolean", default: false },
|
|
31
|
+
target: { type: "string", default: process.cwd() },
|
|
32
|
+
help: { type: "boolean", short: "h", default: false },
|
|
33
|
+
version: { type: "boolean", default: false },
|
|
34
|
+
verbose: { type: "boolean", default: false },
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error(`Error: ${err.message}`);
|
|
39
|
+
console.error(helpText());
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
27
42
|
const [command, sub, arg] = positionals;
|
|
28
43
|
const targetDir = values.target;
|
|
29
44
|
const dryRun = values["dry-run"];
|
|
30
45
|
|
|
31
|
-
|
|
32
|
-
|
|
46
|
+
// devops-basics rule 4: handle SIGTERM/SIGINT gracefully — finish the
|
|
47
|
+
// current write, then exit cleanly. Node runs a synchronous write to
|
|
48
|
+
// completion before a signal handler executes, and writeSafe is atomic
|
|
49
|
+
// (temp file + rename), so exiting here can never leave a half-written
|
|
50
|
+
// file behind.
|
|
51
|
+
let shuttingDown = false;
|
|
52
|
+
function handleShutdownSignal(signal) {
|
|
53
|
+
if (shuttingDown) return;
|
|
54
|
+
shuttingDown = true;
|
|
55
|
+
console.error(`\nReceived ${signal} — finishing the current write, then exiting.`);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
process.on("SIGTERM", () => handleShutdownSignal("SIGTERM"));
|
|
59
|
+
process.on("SIGINT", () => handleShutdownSignal("SIGINT"));
|
|
60
|
+
|
|
61
|
+
function helpText() {
|
|
62
|
+
return `reviewer-harness — your own AI harness
|
|
33
63
|
|
|
34
64
|
Usage (run inside your project):
|
|
35
65
|
reviewer-harness init Create harness.config.json (one-time setup)
|
|
@@ -52,7 +82,15 @@ Usage (run inside your project):
|
|
|
52
82
|
reviewer-harness memory list List memory notes + their links
|
|
53
83
|
|
|
54
84
|
Every command accepts --target <dir> (default: the current directory).
|
|
55
|
-
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function help() {
|
|
89
|
+
console.log(helpText());
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function packageVersion() {
|
|
93
|
+
return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
56
94
|
}
|
|
57
95
|
|
|
58
96
|
function cmdStatus() {
|
|
@@ -60,6 +98,7 @@ function cmdStatus() {
|
|
|
60
98
|
console.log(`Harness : ${config.name} v${config.version}`);
|
|
61
99
|
console.log(`Target : ${targetDir}`);
|
|
62
100
|
console.log(`Runtimes:`);
|
|
101
|
+
if (!config.runtimes?.length) console.log(` (none)`);
|
|
63
102
|
for (const name of config.runtimes) {
|
|
64
103
|
const adapter = adapters[name];
|
|
65
104
|
const target = !adapter ? "(no adapter!)"
|
|
@@ -72,8 +111,13 @@ function cmdStatus() {
|
|
|
72
111
|
}
|
|
73
112
|
|
|
74
113
|
function cmdSync() {
|
|
114
|
+
const config = loadConfig(targetDir);
|
|
75
115
|
const results = sync({ targetDir, dryRun });
|
|
76
116
|
if (dryRun) console.log("(dry-run: nothing was written)\n");
|
|
117
|
+
if (!config.runtimes?.length && !config.skills?.length) {
|
|
118
|
+
console.log(`Nothing to sync — no runtimes and no skills are enabled.`);
|
|
119
|
+
console.log(`Run "skills add <name>" or add a runtime to the config to enable something.`);
|
|
120
|
+
}
|
|
77
121
|
for (const r of results) {
|
|
78
122
|
if (r.skillCommands) {
|
|
79
123
|
const written = r.skillCommands.filter((f) => f.written).length;
|
|
@@ -106,10 +150,18 @@ function cmdSync() {
|
|
|
106
150
|
}
|
|
107
151
|
}
|
|
108
152
|
// If the project has a local memory, refresh its index as part of
|
|
109
|
-
// sync — so the graph the AI reads is always current.
|
|
153
|
+
// sync — so the graph the AI reads is always current. The index is
|
|
154
|
+
// a secondary artifact, so its failure is a warning, not a failed
|
|
155
|
+
// sync (system-design failure isolation): the sync already succeeded.
|
|
110
156
|
if (!dryRun && existsSync(memoryDir(targetDir))) {
|
|
111
|
-
|
|
112
|
-
|
|
157
|
+
try {
|
|
158
|
+
const stats = buildIndex(targetDir);
|
|
159
|
+
console.log(`- memory: index refreshed (${stats.notes} notes, ${stats.edges} links) ✓`);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.error(
|
|
162
|
+
`Warning: memory index could not be updated — ${err.message} (sync itself succeeded; run "memory index" to retry.)`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
113
165
|
}
|
|
114
166
|
}
|
|
115
167
|
|
|
@@ -137,7 +189,8 @@ function cmdMemory() {
|
|
|
137
189
|
console.log(`- [[${n.id}]] ${n.title}${n.links.length ? ` → ${n.links.join(", ")}` : ""}`);
|
|
138
190
|
}
|
|
139
191
|
} else {
|
|
140
|
-
console.
|
|
192
|
+
console.error(`Usage: memory <init|index|list> [--target <dir>]`);
|
|
193
|
+
process.exitCode = 1;
|
|
141
194
|
}
|
|
142
195
|
}
|
|
143
196
|
|
|
@@ -195,7 +248,8 @@ function cmdSkills() {
|
|
|
195
248
|
: `Skill "${arg}" was not enabled.`
|
|
196
249
|
);
|
|
197
250
|
} else {
|
|
198
|
-
console.
|
|
251
|
+
console.error(`Usage: skills <list|add <name>|remove <name>> [--target <dir>]`);
|
|
252
|
+
process.exitCode = 1;
|
|
199
253
|
}
|
|
200
254
|
}
|
|
201
255
|
|
|
@@ -207,7 +261,8 @@ function cmdAgents() {
|
|
|
207
261
|
console.log(`Agent "${arg}" installed:`);
|
|
208
262
|
for (const f of r.written) console.log(` → ${f}`);
|
|
209
263
|
} else {
|
|
210
|
-
console.
|
|
264
|
+
console.error(`Usage: agents <list|add <name>> [--target <dir>]`);
|
|
265
|
+
process.exitCode = 1;
|
|
211
266
|
}
|
|
212
267
|
}
|
|
213
268
|
|
|
@@ -219,7 +274,8 @@ function cmdCommands() {
|
|
|
219
274
|
console.log(`Command "/${arg}" installed:`);
|
|
220
275
|
for (const f of r.written) console.log(` → ${f}`);
|
|
221
276
|
} else {
|
|
222
|
-
console.
|
|
277
|
+
console.error(`Usage: commands <list|add <name>> [--target <dir>]`);
|
|
278
|
+
process.exitCode = 1;
|
|
223
279
|
}
|
|
224
280
|
}
|
|
225
281
|
|
|
@@ -251,15 +307,21 @@ function cmdGlobal() {
|
|
|
251
307
|
if (dryRun) console.log(`- ${runtime} (global): ${pending} would be written/updated, ${unchanged} unchanged → ${where}`);
|
|
252
308
|
else console.log(`- ${runtime} (global): ${written} written ✓, ${unchanged} unchanged → ${where}`);
|
|
253
309
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
310
|
+
if (dryRun) {
|
|
311
|
+
console.log(`Dry run complete — nothing was written. Run without --dry-run to install.`);
|
|
312
|
+
} else {
|
|
313
|
+
console.log(
|
|
314
|
+
`\nDone: ${skills.map((s) => "/" + s.name).join(", ")} are now available in EVERY project.` +
|
|
315
|
+
`\n(OpenCode + Claude: "/" menu commands. Freebuff, Copilot CLI, Antigravity, Qoder: Agent Skills bundles.)` +
|
|
316
|
+
`\n(Per-project rules still come from "sync" inside a project.)`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
259
319
|
}
|
|
260
320
|
|
|
261
321
|
async function main() {
|
|
262
|
-
if (
|
|
322
|
+
if (values.help) help();
|
|
323
|
+
else if (values.version) console.log(`reviewer-harness ${packageVersion()}`);
|
|
324
|
+
else if (!command || command === "status") cmdStatus();
|
|
263
325
|
else if (command === "sync") cmdSync();
|
|
264
326
|
else if (command === "init") cmdInit();
|
|
265
327
|
else if (command === "validate") cmdValidate();
|
|
@@ -268,10 +330,16 @@ async function main() {
|
|
|
268
330
|
else if (command === "agents") cmdAgents();
|
|
269
331
|
else if (command === "commands") cmdCommands();
|
|
270
332
|
else if (command === "global") cmdGlobal();
|
|
271
|
-
else help();
|
|
333
|
+
else if (command === "help") help();
|
|
334
|
+
else {
|
|
335
|
+
console.error(`Unknown command "${command}".`);
|
|
336
|
+
console.error(helpText());
|
|
337
|
+
process.exitCode = 1;
|
|
338
|
+
}
|
|
272
339
|
}
|
|
273
340
|
|
|
274
341
|
main().catch((err) => {
|
|
275
342
|
console.error("Error:", err.message);
|
|
343
|
+
if (values.verbose) console.error(err.stack);
|
|
276
344
|
process.exit(1);
|
|
277
345
|
});
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
//
|
|
10
10
|
// Like Freebuff and Copilot CLI, Antigravity is skills-only here:
|
|
11
11
|
// it exports skillBundleDir() instead of targetFile()/renderInstructions().
|
|
12
|
+
//
|
|
13
|
+
// NOTE: this directory is shared with Freebuff (which returns the
|
|
14
|
+
// same ".agents/skills"). sync() writes a shared bundle directory
|
|
15
|
+
// once per run — the later runtime reports it as deduped (F-S44).
|
|
12
16
|
|
|
13
17
|
export function skillBundleDir() {
|
|
14
18
|
return ".agents/skills";
|
package/lib/adapters/claude.js
CHANGED
|
@@ -3,26 +3,12 @@
|
|
|
3
3
|
// Claude Code reads CLAUDE.md in the project root at every session.
|
|
4
4
|
// It is the best-known instruction file among AI coding tools.
|
|
5
5
|
|
|
6
|
+
import { renderInstructions as renderShared } from "./render.js";
|
|
7
|
+
|
|
6
8
|
export function targetFile() {
|
|
7
9
|
return "CLAUDE.md";
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export function renderInstructions(config) {
|
|
11
|
-
return
|
|
12
|
-
`# ${config.name} — harness instructions`,
|
|
13
|
-
``,
|
|
14
|
-
`> This file is generated by your harness. Do not edit it directly —`,
|
|
15
|
-
`> change harness.config.json and sync again.`,
|
|
16
|
-
``,
|
|
17
|
-
// The "why" — the problem the rules solve, synced along with them.
|
|
18
|
-
...(config.problem
|
|
19
|
-
? [`## The problem this solves`, ``, config.problem, ``]
|
|
20
|
-
: []),
|
|
21
|
-
`## Rules — the AI must never break these`,
|
|
22
|
-
``,
|
|
23
|
-
...config.rules.map((rule, i) => `${i + 1}. ${rule}`),
|
|
24
|
-
``,
|
|
25
|
-
// Skills (Lesson 4): rendered after the rules.
|
|
26
|
-
...(config.skillSections ?? []).flatMap((s) => [s, ``]),
|
|
27
|
-
].join("\n");
|
|
13
|
+
return renderShared(config, { numbered: true });
|
|
28
14
|
}
|
package/lib/adapters/freebuff.js
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
// Freebuff has no single instruction file like CLAUDE.md, so this
|
|
11
11
|
// adapter is skills-only: it exports skillBundleDir() instead of
|
|
12
12
|
// targetFile()/renderInstructions().
|
|
13
|
+
//
|
|
14
|
+
// NOTE: this directory is shared with Antigravity (which returns the
|
|
15
|
+
// same ".agents/skills"). sync() writes a shared bundle directory
|
|
16
|
+
// once per run — the later runtime reports it as deduped (F-S44).
|
|
13
17
|
|
|
14
18
|
export function skillBundleDir() {
|
|
15
19
|
return ".agents/skills";
|
package/lib/adapters/gemini.js
CHANGED
|
@@ -5,26 +5,12 @@
|
|
|
5
5
|
// renderInstructions) — only the target file and a little flavor
|
|
6
6
|
// differ. That is the power of the adapter pattern.
|
|
7
7
|
|
|
8
|
+
import { renderInstructions as renderShared } from "./render.js";
|
|
9
|
+
|
|
8
10
|
export function targetFile() {
|
|
9
11
|
return "GEMINI.md";
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export function renderInstructions(config) {
|
|
13
|
-
return
|
|
14
|
-
`# ${config.name} — harness instructions`,
|
|
15
|
-
``,
|
|
16
|
-
`> This file is generated by your harness. Do not edit it directly —`,
|
|
17
|
-
`> change harness.config.json and sync again.`,
|
|
18
|
-
``,
|
|
19
|
-
// The "why" — the problem the rules solve, synced along with them.
|
|
20
|
-
...(config.problem
|
|
21
|
-
? [`## The problem this solves`, ``, config.problem, ``]
|
|
22
|
-
: []),
|
|
23
|
-
`## Rules — the AI must never break these`,
|
|
24
|
-
``,
|
|
25
|
-
...config.rules.map((rule) => `- ${rule}`),
|
|
26
|
-
``,
|
|
27
|
-
// Skills (Lesson 4): rendered after the rules.
|
|
28
|
-
...(config.skillSections ?? []).flatMap((s) => [s, ``]),
|
|
29
|
-
].join("\n");
|
|
15
|
+
return renderShared(config, { numbered: false });
|
|
30
16
|
}
|
package/lib/adapters/opencode.js
CHANGED
|
@@ -7,36 +7,12 @@
|
|
|
7
7
|
//
|
|
8
8
|
// OpenCode reads `.opencode/instructions.md` in your project.
|
|
9
9
|
|
|
10
|
+
import { renderInstructions as renderShared } from "./render.js";
|
|
11
|
+
|
|
10
12
|
export function targetFile() {
|
|
11
13
|
return ".opencode/instructions.md";
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export function renderInstructions(config) {
|
|
15
|
-
|
|
16
|
-
lines.push(`# ${config.name} — harness instructions`);
|
|
17
|
-
lines.push(``);
|
|
18
|
-
lines.push(
|
|
19
|
-
`> This file is generated by your harness. Do not edit it directly —`,
|
|
20
|
-
`> change harness.config.json and sync again.`
|
|
21
|
-
);
|
|
22
|
-
lines.push(``);
|
|
23
|
-
// The "why" — the problem the rules solve, synced along with them.
|
|
24
|
-
if (config.problem) {
|
|
25
|
-
lines.push(`## The problem this solves`);
|
|
26
|
-
lines.push(``);
|
|
27
|
-
lines.push(config.problem);
|
|
28
|
-
lines.push(``);
|
|
29
|
-
}
|
|
30
|
-
lines.push(`## Rules — the AI must never break these`);
|
|
31
|
-
lines.push(``);
|
|
32
|
-
config.rules.forEach((rule, i) => {
|
|
33
|
-
lines.push(`${i + 1}. ${rule}`);
|
|
34
|
-
});
|
|
35
|
-
lines.push(``);
|
|
36
|
-
// Skills (Lesson 4): rendered after the rules.
|
|
37
|
-
for (const section of config.skillSections ?? []) {
|
|
38
|
-
lines.push(section);
|
|
39
|
-
lines.push(``);
|
|
40
|
-
}
|
|
41
|
-
return lines.join("\n");
|
|
17
|
+
return renderShared(config, { numbered: true });
|
|
42
18
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// lib/adapters/render.js — the ONE shared instruction-file renderer.
|
|
2
|
+
//
|
|
3
|
+
// Every adapter renders the same shape: header, generated-file notice,
|
|
4
|
+
// optional problem section, numbered-or-bulleted rules, skill sections.
|
|
5
|
+
// The only per-adapter difference is the list marker, chosen here by
|
|
6
|
+
// `numbered` (true → `1. …`, false → `- …`). Adapters are thin wrappers
|
|
7
|
+
// over this function; see lib/adapters/claude.js, gemini.js, opencode.js.
|
|
8
|
+
|
|
9
|
+
export function renderInstructions(config, { numbered }) {
|
|
10
|
+
return [
|
|
11
|
+
`# ${config.name} — harness instructions`,
|
|
12
|
+
``,
|
|
13
|
+
`> This file is generated by your harness. Do not edit it directly —`,
|
|
14
|
+
`> change harness.config.json and sync again.`,
|
|
15
|
+
``,
|
|
16
|
+
// The "why" — the problem the rules solve, synced along with them.
|
|
17
|
+
...(config.problem
|
|
18
|
+
? [`## The problem this solves`, ``, config.problem, ``]
|
|
19
|
+
: []),
|
|
20
|
+
`## Rules — the AI must never break these`,
|
|
21
|
+
``,
|
|
22
|
+
...config.rules.map((rule, i) =>
|
|
23
|
+
numbered ? `${i + 1}. ${rule}` : `- ${rule}`
|
|
24
|
+
),
|
|
25
|
+
``,
|
|
26
|
+
// Skills (Lesson 4): rendered after the rules.
|
|
27
|
+
...(config.skillSections ?? []).flatMap((s) => [s, ``]),
|
|
28
|
+
].join("\n");
|
|
29
|
+
}
|
package/lib/agents.js
CHANGED
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
// harness), and `agents add` generates both formats. The same pattern
|
|
11
11
|
// as the rules: one source, every runtime.
|
|
12
12
|
|
|
13
|
-
import { readdirSync, readFileSync,
|
|
13
|
+
import { readdirSync, readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
14
14
|
import { join, dirname } from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { parseFrontmatter } from "./frontmatter.js";
|
|
17
|
+
import { assertValidName, assertInside, warnIfModified, yamlQuote } from "./skills.js";
|
|
18
|
+
import { writeSafe } from "./sync.js";
|
|
17
19
|
|
|
18
20
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
21
|
const BUNDLED_DIR = join(ROOT, "agents");
|
|
@@ -31,7 +33,9 @@ export function listBundledAgents() {
|
|
|
31
33
|
|
|
32
34
|
// Install an agent into the project: one master, two runtime files.
|
|
33
35
|
export function addAgent(name, targetDir) {
|
|
36
|
+
assertValidName(name, "agent");
|
|
34
37
|
const src = join(BUNDLED_DIR, `${name}.md`);
|
|
38
|
+
assertInside(BUNDLED_DIR, src);
|
|
35
39
|
if (!existsSync(src)) {
|
|
36
40
|
const available = listBundledAgents().map((a) => a.name).join(", ");
|
|
37
41
|
throw new Error(`No bundled agent named "${name}". Available: ${available}`);
|
|
@@ -43,19 +47,21 @@ export function addAgent(name, targetDir) {
|
|
|
43
47
|
const opencodeDir = join(targetDir, ".opencode", "agents");
|
|
44
48
|
mkdirSync(opencodeDir, { recursive: true });
|
|
45
49
|
const opencodeFile = join(opencodeDir, `${name}.md`);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`---\ndescription: ${description}\nmode: subagent\n---\n\n${body.trim()}\n
|
|
49
|
-
);
|
|
50
|
+
assertInside(opencodeDir, opencodeFile);
|
|
51
|
+
const opencodeContent =
|
|
52
|
+
`---\ndescription: ${yamlQuote(description)}\nmode: subagent\n---\n\n${body.trim()}\n`;
|
|
53
|
+
warnIfModified(opencodeFile, opencodeContent);
|
|
54
|
+
writeSafe(opencodeFile, opencodeContent);
|
|
50
55
|
written.push(opencodeFile);
|
|
51
56
|
|
|
52
57
|
const claudeDir = join(targetDir, ".claude", "agents");
|
|
53
58
|
mkdirSync(claudeDir, { recursive: true });
|
|
54
59
|
const claudeFile = join(claudeDir, `${name}.md`);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
`---\nname: ${name}\ndescription: ${description}\n---\n\n${body.trim()}\n
|
|
58
|
-
);
|
|
60
|
+
assertInside(claudeDir, claudeFile);
|
|
61
|
+
const claudeContent =
|
|
62
|
+
`---\nname: ${name}\ndescription: ${yamlQuote(description)}\n---\n\n${body.trim()}\n`;
|
|
63
|
+
warnIfModified(claudeFile, claudeContent);
|
|
64
|
+
writeSafe(claudeFile, claudeContent);
|
|
59
65
|
written.push(claudeFile);
|
|
60
66
|
|
|
61
67
|
return { written };
|
package/lib/commands.js
CHANGED
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
// $ARGUMENTS is replaced by the runtime with whatever you type
|
|
11
11
|
// after the command.
|
|
12
12
|
|
|
13
|
-
import { readdirSync, readFileSync,
|
|
13
|
+
import { readdirSync, readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
14
14
|
import { join, dirname } from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { parseFrontmatter } from "./frontmatter.js";
|
|
17
|
+
import { assertValidName, assertInside, warnIfModified, yamlQuote } from "./skills.js";
|
|
18
|
+
import { writeSafe } from "./sync.js";
|
|
17
19
|
|
|
18
20
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
21
|
const BUNDLED_DIR = join(ROOT, "commands");
|
|
@@ -30,14 +32,16 @@ export function listBundledCommands() {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export function addCommand(name, targetDir) {
|
|
35
|
+
assertValidName(name, "command");
|
|
33
36
|
const src = join(BUNDLED_DIR, `${name}.md`);
|
|
37
|
+
assertInside(BUNDLED_DIR, src);
|
|
34
38
|
if (!existsSync(src)) {
|
|
35
39
|
const available = listBundledCommands().map((c) => c.name).join(", ");
|
|
36
40
|
throw new Error(`No bundled command named "${name}". Available: ${available}`);
|
|
37
41
|
}
|
|
38
42
|
const { data, body } = parseFrontmatter(readFileSync(src, "utf8"));
|
|
39
43
|
const description = data.description ?? name;
|
|
40
|
-
const content = `---\ndescription: ${description}\n---\n\n${body.trim()}\n`;
|
|
44
|
+
const content = `---\ndescription: ${yamlQuote(description)}\n---\n\n${body.trim()}\n`;
|
|
41
45
|
const written = [];
|
|
42
46
|
|
|
43
47
|
for (const dir of [
|
|
@@ -46,7 +50,9 @@ export function addCommand(name, targetDir) {
|
|
|
46
50
|
]) {
|
|
47
51
|
mkdirSync(dir, { recursive: true });
|
|
48
52
|
const file = join(dir, `${name}.md`);
|
|
49
|
-
|
|
53
|
+
assertInside(dir, file);
|
|
54
|
+
warnIfModified(file, content);
|
|
55
|
+
writeSafe(file, content);
|
|
50
56
|
written.push(file);
|
|
51
57
|
}
|
|
52
58
|
return { written };
|
package/lib/config.js
CHANGED
|
@@ -21,8 +21,21 @@ export function loadConfig(dir = ROOT) {
|
|
|
21
21
|
let config;
|
|
22
22
|
try {
|
|
23
23
|
config = JSON.parse(readFileSync(path, "utf8"));
|
|
24
|
-
} catch {
|
|
25
|
-
throw new Error(
|
|
24
|
+
} catch (err) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Config is not valid JSON: ${path}: ${err.message}\nCheck for a trailing comma or other syntax error.`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// A valid-JSON file is not necessarily a config: null, an array, or a
|
|
31
|
+
// bare string must fail here with a clear error — never a raw
|
|
32
|
+
// TypeError from the `.name` check below (F-W20, Review Focus #5).
|
|
33
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Config must be an object, not ${
|
|
36
|
+
config === null ? "null" : Array.isArray(config) ? "an array" : `a ${typeof config}`
|
|
37
|
+
}.`
|
|
38
|
+
);
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
// Light structural validation — the harness rejects a broken spec.
|
package/lib/frontmatter.js
CHANGED
|
@@ -7,13 +7,31 @@ export function parseFrontmatter(text) {
|
|
|
7
7
|
// use CR (\r). Without this, the "---\n" checks below fail on CRLF input and
|
|
8
8
|
// the whole frontmatter is silently dropped (name/description lost).
|
|
9
9
|
text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
10
|
+
// A byte-order mark in front of the opening delimiter is not part of
|
|
11
|
+
// the frontmatter — strip it so BOM-prefixed files parse.
|
|
12
|
+
if (text.charCodeAt(0) === 0xfeff) text = text.slice(1);
|
|
10
13
|
if (!text.startsWith("---\n")) return { data: {}, body: text };
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
// Find the closing delimiter as a whole line, so a file ending exactly
|
|
15
|
+
// at "---" (EOF, no trailing newline) still parses, and so does empty
|
|
16
|
+
// frontmatter ("---\n---\nbody").
|
|
17
|
+
const lines = text.split("\n");
|
|
18
|
+
let close = -1;
|
|
19
|
+
for (let i = 1; i < lines.length; i++) {
|
|
20
|
+
if (lines[i] === "---") {
|
|
21
|
+
close = i;
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (close === -1) {
|
|
26
|
+
console.warn(
|
|
27
|
+
"Warning: frontmatter block has no closing '---' — treating the whole file as body."
|
|
28
|
+
);
|
|
29
|
+
return { data: {}, body: text };
|
|
30
|
+
}
|
|
13
31
|
const data = {};
|
|
14
|
-
for (const line of
|
|
32
|
+
for (const line of lines.slice(1, close)) {
|
|
15
33
|
const idx = line.indexOf(":");
|
|
16
34
|
if (idx > 0) data[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
17
35
|
}
|
|
18
|
-
return { data, body:
|
|
36
|
+
return { data, body: lines.slice(close + 1).join("\n") };
|
|
19
37
|
}
|
package/lib/init.js
CHANGED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
// Creates harness.config.json from the template if none exists yet.
|
|
4
4
|
// Never overwrites an existing config — safe by default.
|
|
5
5
|
|
|
6
|
-
import { writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { writeFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
|
|
9
|
+
const TOOL_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
10
|
+
|
|
9
11
|
const TEMPLATE = {
|
|
10
12
|
name: "reviewer-harness",
|
|
11
|
-
version:
|
|
13
|
+
version: TOOL_VERSION,
|
|
12
14
|
problem:
|
|
13
15
|
"The problem — an AI with no guardrails: it sometimes breaks existing code it was never asked to touch, and hallucinates — inventing functions, libraries, or output that do not actually exist. These rules are the guardrails against that.",
|
|
14
16
|
runtimes: ["opencode", "claude", "gemini", "cline", "freebuff", "copilot", "antigravity", "qoder"],
|
package/lib/memory.js
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
mkdirSync,
|
|
26
26
|
} from "node:fs";
|
|
27
27
|
import { join } from "node:path";
|
|
28
|
+
import { writeSafe } from "./sync.js";
|
|
28
29
|
|
|
29
30
|
export function memoryDir(targetDir) {
|
|
30
31
|
return join(targetDir, "memory");
|
|
@@ -111,7 +112,10 @@ export function buildIndex(targetDir) {
|
|
|
111
112
|
const notes = readNotes(targetDir);
|
|
112
113
|
const graph = buildGraph(notes);
|
|
113
114
|
|
|
114
|
-
|
|
115
|
+
// Atomic writes (F-W16 memory half): graph.json and INDEX.md go
|
|
116
|
+
// through writeSafe — temp file + fsync + rename, rotating backups —
|
|
117
|
+
// so a kill mid-index cannot leave the pair half-updated.
|
|
118
|
+
writeSafe(
|
|
115
119
|
join(dir, "graph.json"),
|
|
116
120
|
JSON.stringify({ generated_by: "reviewer-harness", ...graph }, null, 2) + "\n"
|
|
117
121
|
);
|
|
@@ -147,7 +151,7 @@ export function buildIndex(targetDir) {
|
|
|
147
151
|
lines.push(``);
|
|
148
152
|
lines.push(`Graph: ${graph.nodes.length} nodes, ${graph.edges.length} edges.`);
|
|
149
153
|
lines.push(``);
|
|
150
|
-
|
|
154
|
+
writeSafe(join(dir, "INDEX.md"), lines.join("\n"));
|
|
151
155
|
|
|
152
156
|
return {
|
|
153
157
|
notes: notes.length,
|