reviewer-harness 1.1.0 → 1.2.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 CHANGED
@@ -176,7 +176,9 @@ Reusable playbooks, enabled per project:
176
176
  - `skills add <name>` — install into the project and enable it in the config
177
177
  - `skills remove <name>` — disable it
178
178
 
179
- Bundled: `safe-editing`, `no-hallucination`, `debug-first`, `explain-bisaya`. Enabled skills render below the rules in every instruction file.
179
+ Bundled: `safe-editing`, `no-hallucination`, `debug-first`, `explain-bisaya`. 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.
180
+
181
+ **Every project, instantly:** want the skills in the `/` menu of *every* project, with no per-project setup? Run `reviewer-harness global` once — it installs all bundled skills as slash commands into the global OpenCode (`~/.config/opencode/commands`) and Claude (`~/.claude/commands`) folders. From then on, they are in the `/` menu of any project you open. (Project rules still come from `sync` inside a project.)
180
182
 
181
183
  ## Sub-agents and Slash Commands
182
184
 
@@ -190,20 +192,37 @@ One master definition in the harness, generated into each runtime's format:
190
192
 
191
193
  ## Command reference
192
194
 
193
- Requires Node.js 18+ (no dependencies). Every command accepts `--target <dir>` (default: the current directory).
195
+ Requires Node.js 18+ (no dependencies). Run these inside your project — every command works on the current directory unless you add `--target <dir>`.
196
+
197
+ **Start & everyday**
198
+
199
+ ```bash
200
+ reviewer-harness init # one-time: create the master config
201
+ reviewer-harness validate # check the config (quality + secret scan)
202
+ reviewer-harness sync --dry-run # preview the changes, writes nothing
203
+ reviewer-harness sync # apply the rules to every runtime
204
+ reviewer-harness status # show the config + target files
205
+ ```
206
+
207
+ **Skills, agents, commands**
208
+
209
+ ```bash
210
+ reviewer-harness skills list
211
+ reviewer-harness skills add safe-editing
212
+ reviewer-harness skills remove safe-editing
213
+ reviewer-harness agents add reviewer
214
+ reviewer-harness commands add review
215
+ ```
216
+
217
+ **Local memory**
194
218
 
195
219
  ```bash
196
- node bin/cli.js init --target ./my-project
197
- node bin/cli.js validate --target ./my-project
198
- node bin/cli.js sync --dry-run --target ./my-project
199
- node bin/cli.js sync --target ./my-project
200
- node bin/cli.js memory init --target ./my-project
201
- node bin/cli.js skills add safe-editing --target ./my-project
202
- node bin/cli.js agents add reviewer --target ./my-project
203
- node bin/cli.js commands add review --target ./my-project
220
+ reviewer-harness memory init # start the memory/ vault
221
+ reviewer-harness memory list # list notes + their links
222
+ reviewer-harness memory index # rebuild INDEX.md + graph.json
204
223
  ```
205
224
 
206
- Installed globally or via npx (see Quickstart), the same commands run as `reviewer-harness <command>`.
225
+ Running from a source checkout instead? The same commands work as `node bin/cli.js <command>`.
207
226
 
208
227
  ## File map
209
228
 
package/bin/cli.js CHANGED
@@ -1,18 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  // reviewer-harness — CLI: sync engine, guardrail validation, local memory,
3
3
  // skills, sub-agents, and slash commands.
4
- // Commands: status | sync | init | validate | memory | skills | agents | commands
4
+ // Commands: status | sync | init | validate | memory | skills | agents | commands | global
5
5
 
6
6
  import { parseArgs } from "node:util";
7
7
  import { existsSync } from "node:fs";
8
8
  import { join } from "node:path";
9
+ import { homedir } from "node:os";
9
10
  import { loadConfig } from "../lib/config.js";
10
11
  import { adapters, supportedRuntimes } from "../lib/adapters/index.js";
11
- import { sync } from "../lib/sync.js";
12
+ import { sync, writeSafe } from "../lib/sync.js";
12
13
  import { initProject } from "../lib/init.js";
13
14
  import { initMemory, buildIndex, readNotes, memoryDir } from "../lib/memory.js";
14
15
  import { validateConfig } from "../lib/validate.js";
15
- import { listBundledSkills, addSkill, removeSkill } from "../lib/skills.js";
16
+ import { listBundledSkills, addSkill, removeSkill, loadBundledSkills, skillCommandContent } from "../lib/skills.js";
16
17
  import { listBundledAgents, addAgent } from "../lib/agents.js";
17
18
  import { listBundledCommands, addCommand } from "../lib/commands.js";
18
19
 
@@ -30,21 +31,27 @@ const dryRun = values["dry-run"];
30
31
  function help() {
31
32
  console.log(`reviewer-harness — your own AI harness
32
33
 
33
- Usage:
34
- node bin/cli.js status [--target <dir>] Show the config + target files
35
- node bin/cli.js sync [--dry-run] [--target <dir>] Sync the rules to every runtime
36
- node bin/cli.js init [--target <dir>] Create harness.config.json in a project
37
- node bin/cli.js validate [--target <dir>] Check the config: quality + secret scanning
38
- node bin/cli.js memory init [--target <dir>] Start the local memory (memory/ folder)
39
- node bin/cli.js memory index [--target <dir>] Rebuild the memory INDEX.md + graph.json
40
- node bin/cli.js memory list [--target <dir>] List memory notes + their links
41
- node bin/cli.js skills list [--target <dir>] List bundled skills (+ which are enabled)
42
- node bin/cli.js skills add <name> [--target <dir>] Install + enable a skill in the project
43
- node bin/cli.js skills remove <name> [--target <dir>] Disable + remove a skill
44
- node bin/cli.js agents list List bundled sub-agents
45
- node bin/cli.js agents add <name> [--target <dir>] Install a sub-agent (OpenCode + Claude)
46
- node bin/cli.js commands list List bundled slash commands
47
- node bin/cli.js commands add <name> [--target <dir>] Install a slash command (OpenCode + Claude)
34
+ Usage (run inside your project):
35
+ reviewer-harness init Create harness.config.json (one-time setup)
36
+ reviewer-harness validate Check the config: quality + secret scanning
37
+ reviewer-harness sync [--dry-run] Sync the rules to every runtime (--dry-run = preview only)
38
+ reviewer-harness status Show the config + target files
39
+
40
+ reviewer-harness skills list List bundled skills (+ which are enabled)
41
+ reviewer-harness skills add <name> Install + enable a skill in the project
42
+ reviewer-harness skills remove <name> Disable + remove a skill
43
+ reviewer-harness agents list List bundled sub-agents
44
+ reviewer-harness agents add <name> Install a sub-agent (OpenCode + Claude)
45
+ reviewer-harness commands list List bundled slash commands
46
+ reviewer-harness commands add <name> Install a slash command (OpenCode + Claude)
47
+
48
+ reviewer-harness global [--dry-run] Install EVERY bundled skill as a slash command for ALL projects (global OpenCode + Claude "/" menus)
49
+
50
+ reviewer-harness memory init Start the local memory (memory/ folder)
51
+ reviewer-harness memory index Rebuild the memory INDEX.md + graph.json
52
+ reviewer-harness memory list List memory notes + their links
53
+
54
+ Every command accepts --target <dir> (default: the current directory).
48
55
  `);
49
56
  }
50
57
 
@@ -64,7 +71,16 @@ function cmdSync() {
64
71
  const results = sync({ targetDir, dryRun });
65
72
  if (dryRun) console.log("(dry-run: nothing was written)\n");
66
73
  for (const r of results) {
67
- if (r.skipped) {
74
+ if (r.skillCommands) {
75
+ const written = r.skillCommands.filter((f) => f.written).length;
76
+ const pending = r.skillCommands.filter((f) => f.dryRun && f.changed).length;
77
+ const unchanged = r.skillCommands.filter((f) => f.unchanged || (f.dryRun && !f.changed)).length;
78
+ if (dryRun) {
79
+ console.log(`- skill commands: ${pending} would be written/updated, ${unchanged} unchanged (they appear in the "/" menu of OpenCode and Claude)`);
80
+ } else {
81
+ console.log(`- skill commands: ${written} written ✓, ${unchanged} unchanged (one per skill, in the "/" menu of OpenCode and Claude)`);
82
+ }
83
+ } else if (r.skipped) {
68
84
  console.log(`- ${r.runtime}: SKIP — ${r.skipped}`);
69
85
  } else if (r.dryRun) {
70
86
  console.log(`- ${r.runtime} → ${r.file}: ${r.changed ? "WOULD WRITE (changes pending)" : "no changes"}`);
@@ -192,6 +208,30 @@ function cmdCommands() {
192
208
  }
193
209
  }
194
210
 
211
+ function cmdGlobal() {
212
+ const skills = loadBundledSkills();
213
+ const targets = [
214
+ { runtime: "OpenCode", dir: join(homedir(), ".config", "opencode", "commands") },
215
+ { runtime: "Claude", dir: join(homedir(), ".claude", "commands") },
216
+ ];
217
+ if (dryRun) console.log("(dry-run: nothing was written)\n");
218
+ for (const { runtime, dir } of targets) {
219
+ let written = 0, unchanged = 0, pending = 0;
220
+ for (const skill of skills) {
221
+ const outcome = writeSafe(join(dir, `${skill.name}.md`), skillCommandContent(skill), dryRun);
222
+ if (outcome.written) written++;
223
+ else if (outcome.dryRun && outcome.changed) pending++;
224
+ else unchanged++;
225
+ }
226
+ if (dryRun) console.log(`- ${runtime} (global): ${pending} would be written/updated, ${unchanged} unchanged → ${dir}`);
227
+ else console.log(`- ${runtime} (global): ${written} written ✓, ${unchanged} unchanged → ${dir}`);
228
+ }
229
+ console.log(
230
+ `\nDone: ${skills.map((s) => "/" + s.name).join(", ")} are now in the "/" menu of EVERY project.` +
231
+ `\n(Per-project rules still come from "sync" inside a project.)`
232
+ );
233
+ }
234
+
195
235
  async function main() {
196
236
  if (!command || command === "status") cmdStatus();
197
237
  else if (command === "sync") cmdSync();
@@ -201,6 +241,7 @@ async function main() {
201
241
  else if (command === "skills") cmdSkills();
202
242
  else if (command === "agents") cmdAgents();
203
243
  else if (command === "commands") cmdCommands();
244
+ else if (command === "global") cmdGlobal();
204
245
  else help();
205
246
  }
206
247
 
package/lib/skills.js CHANGED
@@ -1,8 +1,7 @@
1
1
  // lib/skills.js — LESSON 4: SKILLS SYSTEM.
2
2
  //
3
- // Rules are short laws. SKILLS are deeper playbooks for specific work
4
- // (like sauron's `add`): reusable instruction bundles you enable
5
- // per project.
3
+ // Rules are short laws. SKILLS are deeper playbooks for specific work:
4
+ // reusable instruction bundles you enable per project.
6
5
  //
7
6
  // skills/<name>.md — bundled in the harness (the "store")
8
7
  // <project>/skills/ — the installed copy in your project
@@ -27,6 +26,26 @@ import { parseFrontmatter } from "./frontmatter.js";
27
26
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
28
27
  const BUNDLED_DIR = join(ROOT, "skills");
29
28
 
29
+ // The bundled skills in full (name + description + body) — used by
30
+ // the "global" command, which installs them as slash commands for
31
+ // every project on the machine.
32
+ export function loadBundledSkills() {
33
+ if (!existsSync(BUNDLED_DIR)) return [];
34
+ return readdirSync(BUNDLED_DIR)
35
+ .filter((f) => f.endsWith(".md"))
36
+ .sort()
37
+ .map((file) => {
38
+ const { data, body } = parseFrontmatter(
39
+ readFileSync(join(BUNDLED_DIR, file), "utf8")
40
+ );
41
+ return {
42
+ name: file.replace(/\.md$/, ""),
43
+ description: data.description ?? "",
44
+ body: body.trim(),
45
+ };
46
+ });
47
+ }
48
+
30
49
  export function listBundledSkills() {
31
50
  if (!existsSync(BUNDLED_DIR)) return [];
32
51
  return readdirSync(BUNDLED_DIR)
@@ -101,19 +120,36 @@ export function removeSkill(name, targetDir) {
101
120
  return { wasEnabled, removedFile };
102
121
  }
103
122
 
104
- // Read the enabled skills' contents so sync can render them.
105
- // Enabled but no file? That's a clear error, not a silent skip:
106
- // a strict harness never guesses.
107
- export function loadSkillSections(targetDir, config) {
108
- const sections = [];
123
+ // Read the enabled skills in full (name + description + body) so sync
124
+ // can render them into the instruction files AND generate a slash
125
+ // command for each one.
126
+ export function loadSkills(targetDir, config) {
127
+ const skills = [];
109
128
  for (const name of config.skills ?? []) {
110
129
  const file = join(targetDir, "skills", `${name}.md`);
111
130
  if (!existsSync(file))
112
131
  throw new Error(
113
132
  `Skill "${name}" is enabled in the config but has no file in skills/ — run "skills add ${name}".`
114
133
  );
115
- const { body } = parseFrontmatter(readFileSync(file, "utf8"));
116
- sections.push(body.trim());
134
+ const { data, body } = parseFrontmatter(readFileSync(file, "utf8"));
135
+ skills.push({ name, description: data.description ?? "", body: body.trim() });
117
136
  }
118
- return sections;
137
+ return skills;
138
+ }
139
+
140
+ // Enabled but no file? loadSkills throws a clear error, never a
141
+ // silent skip: a strict harness never guesses.
142
+ export function loadSkillSections(targetDir, config) {
143
+ return loadSkills(targetDir, config).map((s) => s.body);
144
+ }
145
+
146
+ // The slash-command file generated for a skill, so the skill shows up
147
+ // in the runtime's "/" menu (OpenCode and Claude list the files in
148
+ // their commands folders). Invoking /<skill> hands the playbook to
149
+ // the AI together with whatever the user types after it.
150
+ export function skillCommandContent(skill) {
151
+ return (
152
+ `---\ndescription: ${skill.description}\n---\n\n` +
153
+ `${skill.body}\n\nApply this playbook to the following: $ARGUMENTS\n`
154
+ );
119
155
  }
package/lib/sync.js CHANGED
@@ -8,6 +8,10 @@
8
8
  // 3. BACKUP — before overwriting an existing file, make a .bak copy
9
9
  // 4. SKIP — if nothing changed, touch nothing (idempotent)
10
10
  //
11
+ // Sync also generates one SLASH COMMAND per enabled skill (into the
12
+ // OpenCode and Claude commands folders), so every skill appears in
13
+ // the runtime's "/" menu and can be invoked on demand.
14
+ //
11
15
  // This is the harness's first guardrail: the harness itself cannot
12
16
  // destroy your files — the same protection it gives you against an
13
17
  // AI breaking your code.
@@ -23,7 +27,33 @@ import { join, dirname } from "node:path";
23
27
  import { loadConfig } from "./config.js";
24
28
  import { adapters, supportedRuntimes } from "./adapters/index.js";
25
29
  import { validateConfig } from "./validate.js";
26
- import { loadSkillSections } from "./skills.js";
30
+ import { loadSkills, skillCommandContent } from "./skills.js";
31
+
32
+ // Write one file the safe way. Returns what happened:
33
+ // dry-run → { dryRun: true, changed } · no change → { unchanged: true }
34
+ // changed → { written: true, backup } (with a .bak copy when the
35
+ // file already existed). Every real write is verified by reading
36
+ // the file back.
37
+ export function writeSafe(abs, content, dryRun) {
38
+ if (dryRun) {
39
+ const existing = existsSync(abs) ? readFileSync(abs, "utf8") : null;
40
+ return { dryRun: true, changed: existing !== content };
41
+ }
42
+ mkdirSync(dirname(abs), { recursive: true });
43
+ if (existsSync(abs)) {
44
+ const old = readFileSync(abs, "utf8");
45
+ if (old === content) return { unchanged: true };
46
+ copyFileSync(abs, abs + ".bak");
47
+ writeFileSync(abs, content);
48
+ if (readFileSync(abs, "utf8") !== content)
49
+ throw new Error(`Failed to write ${abs}`);
50
+ return { written: true, backup: true };
51
+ }
52
+ writeFileSync(abs, content);
53
+ if (readFileSync(abs, "utf8") !== content)
54
+ throw new Error(`Failed to write ${abs}`);
55
+ return { written: true, backup: false };
56
+ }
27
57
 
28
58
  export function sync({ targetDir, dryRun = false }) {
29
59
  const config = loadConfig(targetDir);
@@ -39,8 +69,11 @@ export function sync({ targetDir, dryRun = false }) {
39
69
  );
40
70
  }
41
71
 
42
- // LESSON 4: attach the enabled skills — the adapters render them.
43
- config.skillSections = loadSkillSections(targetDir, config);
72
+ // LESSON 4: attach the enabled skills — the adapters render them
73
+ // into the instruction files, and each one also becomes a slash
74
+ // command below.
75
+ const skills = loadSkills(targetDir, config);
76
+ config.skillSections = skills.map((s) => s.body);
44
77
 
45
78
  const results = [];
46
79
 
@@ -54,38 +87,22 @@ export function sync({ targetDir, dryRun = false }) {
54
87
  }
55
88
 
56
89
  const rel = adapter.targetFile();
57
- const abs = join(targetDir, rel);
58
- const content = adapter.renderInstructions(config);
59
-
60
- // DRY-RUN: read only, never write. "Look before you agree."
61
- if (dryRun) {
62
- const existing = existsSync(abs) ? readFileSync(abs, "utf8") : null;
63
- results.push({ runtime: name, file: rel, dryRun: true, changed: existing !== content });
64
- continue;
65
- }
66
-
67
- mkdirSync(dirname(abs), { recursive: true });
90
+ const outcome = writeSafe(join(targetDir, rel), adapter.renderInstructions(config), dryRun);
91
+ results.push({ runtime: name, file: rel, ...outcome });
92
+ }
68
93
 
69
- if (existsSync(abs)) {
70
- const old = readFileSync(abs, "utf8");
71
- // Nothing changed? Touch nothing — sync is idempotent.
72
- if (old === content) {
73
- results.push({ runtime: name, file: rel, unchanged: true });
74
- continue;
94
+ // SLASH COMMANDS: one per enabled skill, for the two runtimes whose
95
+ // "/" menu lists command files (OpenCode and Claude).
96
+ if (skills.length) {
97
+ const files = [];
98
+ for (const skill of skills) {
99
+ const content = skillCommandContent(skill);
100
+ for (const dir of [".opencode/commands", ".claude/commands"]) {
101
+ const rel = `${dir}/${skill.name}.md`;
102
+ files.push({ file: rel, ...writeSafe(join(targetDir, rel), content, dryRun) });
75
103
  }
76
- // About to overwrite? BACK UP first — protection against loss.
77
- copyFileSync(abs, abs + ".bak");
78
- writeFileSync(abs, content);
79
- results.push({ runtime: name, file: rel, written: true, backup: true });
80
- } else {
81
- writeFileSync(abs, content);
82
- results.push({ runtime: name, file: rel, written: true, backup: false });
83
- }
84
-
85
- // Verify the write actually landed (read it back).
86
- if (!dryRun && readFileSync(abs, "utf8") !== content) {
87
- throw new Error(`Failed to write ${abs}`);
88
104
  }
105
+ results.push({ skillCommands: files });
89
106
  }
90
107
 
91
108
  return results;
package/package.json CHANGED
@@ -23,7 +23,7 @@
23
23
  "url": "https://github.com/xcell321/reviewer-harness.git"
24
24
  },
25
25
  "type": "module",
26
- "version": "1.1.0",
26
+ "version": "1.2.0",
27
27
  "files": [
28
28
  "bin",
29
29
  "lib",