adversarial-review-gate 2.0.3 → 2.1.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "adversarial-review",
3
3
  "owner": {
4
- "name": "Khoa Pham"
4
+ "name": "louisphamdev"
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Internal marketplace hosting the adversarial-review code-quality gate."
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "name": "adversarial-review",
12
12
  "source": "./",
13
- "description": "Soft Stop-hook gate that forces an adversarial review of significant code changes before an agent finishes."
13
+ "description": "Stop-hook gate (default: enforced) that forces an adversarial review of significant code changes before an agent finishes."
14
14
  }
15
15
  ]
16
16
  }
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "adversarial-review",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "NodeJS multi-tool adversarial review gate. Installs a SessionStart baseline hook and a Stop gate hook for Claude Code. Supports multiple host tools (Claude Code, Codex, opencode) with configurable reviewer mappings, policy modes (soft/enforced/strict-ci), and native or wrapper enforcement. Significant code changes must pass an adversarial review before the agent finishes.",
5
5
  "author": {
6
- "name": "adversarial-review contributors"
6
+ "name": "louisphamdev"
7
7
  },
8
8
  "keywords": ["code-review", "quality", "hook", "stop-hook", "adversarial"],
9
9
  "hooks": {
package/CHANGELOG.md ADDED
@@ -0,0 +1,78 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [2.1.0] - 2026-06-13
9
+
10
+ Public-release perfection pass: hardening, machine-wide install, and docs.
11
+
12
+ ### Added
13
+ - `install --global` (alias `--user`): machine-wide install that writes the
14
+ host/reviewer defaults to `~/.adversarial-review/config.json` and merges the
15
+ Claude Code `SessionStart` + `Stop` hooks into the user-level
16
+ `~/.claude/settings.json`.
17
+ - `uninstall` command (`uninstall [--user]`): removes the hooks this tool wrote
18
+ and the install-registry entry, for both project and machine-wide installs.
19
+ - `install` now merges into an existing Claude Code `settings.json` (preserving
20
+ other keys) instead of replacing it.
21
+ - Additional `doctor` checks for the merged settings, the opencode read-only
22
+ agent, and reviewer isolation.
23
+ - CI workflow (`.github/workflows/ci.yml`): tests on
24
+ `ubuntu-latest`/`windows-latest` x Node 20/22, plus `npm run pack:dry-run`.
25
+ - `SECURITY.md`, `CONTRIBUTING.md`, and this `CHANGELOG.md`.
26
+ - README badges (npm version, CI, license, node) and documentation for the new
27
+ install flags and `uninstall`.
28
+
29
+ ### Changed
30
+ - The installed Claude Code `Stop` hook now carries a **300-second timeout** so a
31
+ debate-tier review is not aborted mid-run.
32
+ - Hardened reviewer isolation: enforced/strict-ci modes reject any reviewer that
33
+ is not `readOnly && noEdit`.
34
+ - More robust coverage parsing in verdict handling.
35
+ - Deduplication of changed-file scope so a file is not reviewed twice.
36
+ - `package.json`: added `prepublishOnly` (test + pack dry-run) and
37
+ `publishConfig.access = public`; added `CHANGELOG.md` to the files allowlist.
38
+
39
+ ### Fixed
40
+ - `skills/adversarial-review-setup/SKILL.md` referenced the wrong package name
41
+ (`adversarial-review` instead of `adversarial-review-gate`).
42
+
43
+ ## [2.0.3] - 2026-06-13
44
+
45
+ ### Fixed
46
+ - A fresh install now produces a working `enforced` + opencode gate out of the
47
+ box: the installer creates the read-only opencode `adversarial-reviewer` agent
48
+ (idempotent), writes `reviewers.opencode.readOnlyConfig: true`, and skips the
49
+ install-time agent-existence check so a clean machine can bootstrap.
50
+
51
+ ## [2.0.2] - 2026-06-13
52
+
53
+ ### Added
54
+ - A package-name-matching `adversarial-review-gate` bin so `npx
55
+ adversarial-review-gate` resolves correctly.
56
+
57
+ ## [2.0.1] - 2026-06-13
58
+
59
+ ### Fixed
60
+ - Installer bin name corrected to `adversarial-review-gate`.
61
+
62
+ ## [2.0.0] - 2026-06-13
63
+
64
+ ### Added
65
+ - Initial NodeJS multi-tool adversarial-review gate, replacing the previous
66
+ Python/Claude-plugin implementation.
67
+ - Multi-host support (Claude Code native Stop hook; codex, opencode,
68
+ github-copilot-cli, antigravity via wrapper) with configurable reviewer
69
+ mappings and self-review (`none`) orchestration.
70
+ - Policy modes `soft` / `enforced` / `strict-ci`, layered config
71
+ (default < user < project) with a tighten-only user policy floor.
72
+ - `install`, `check`, `run`, `doctor`, and `hook` commands.
73
+
74
+ [2.1.0]: https://github.com/louisphamdev/adversarial-review/releases/tag/v2.1.0
75
+ [2.0.3]: https://github.com/louisphamdev/adversarial-review/releases/tag/v2.0.3
76
+ [2.0.2]: https://github.com/louisphamdev/adversarial-review/releases/tag/v2.0.2
77
+ [2.0.1]: https://github.com/louisphamdev/adversarial-review/releases/tag/v2.0.1
78
+ [2.0.0]: https://github.com/louisphamdev/adversarial-review/releases/tag/v2.0.0
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # adversarial-review
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/adversarial-review-gate.svg)](https://www.npmjs.com/package/adversarial-review-gate)
4
+ [![CI](https://github.com/louisphamdev/adversarial-review/actions/workflows/ci.yml/badge.svg)](https://github.com/louisphamdev/adversarial-review/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/adversarial-review-gate.svg)](./LICENSE)
6
+ [![node](https://img.shields.io/node/v/adversarial-review-gate.svg)](https://nodejs.org)
7
+
3
8
  A NodeJS multi-tool adversarial review gate for coding agents.
4
9
 
5
10
  The gate stops a coding agent from finishing a turn when a significant code
@@ -29,20 +34,62 @@ npx adversarial-review-gate install \
29
34
 
30
35
  The installer detects available host and reviewer tools, verifies each reviewer
31
36
  binary (it must resolve on `PATH` and pass a version/auth check), and writes the
32
- project config plus any native host integration files. Run with no flags for the
33
- interactive wizard, or pass `--hosts`/`--reviewer` for a scripted setup.
37
+ project config plus any native host integration files.
38
+
39
+ `--hosts` and `--reviewer` are **required** — there is no interactive wizard
40
+ (one may be added in a future release). Pass them explicitly for a scripted
41
+ setup.
42
+
43
+ What the installer writes for you (idempotent — it never clobbers a file you
44
+ have customized):
45
+
46
+ - The **project config** at `.adversarial-review/config.json` with the
47
+ host → reviewer mapping you chose.
48
+ - For an **opencode** reviewer: the read-only `adversarial-reviewer` opencode
49
+ agent at `~/.config/opencode/agent/adversarial-reviewer.md` (skipped if it
50
+ already exists) **and** `reviewers.opencode.readOnlyConfig: true` in the
51
+ project config, so enforced-mode isolation passes out of the box.
52
+ - For **Claude Code**: the native `SessionStart` + `Stop` hooks in
53
+ `.claude/settings.json`.
54
+ - The user-level install registry at `~/.adversarial-review/install.json`.
34
55
 
35
56
  Supported flags (see `src/cli/install.js`):
36
57
 
37
58
  | Flag | Meaning |
38
59
  |---|---|
39
- | `--hosts a,b` | Comma-separated list of hosts to install (repeatable). |
40
- | `--reviewer host=reviewer` | Reviewer mapping for a host (repeatable). Use `host=none` for self-review. |
60
+ | `--hosts a,b` | Comma-separated list of hosts to install (repeatable). **Required.** |
61
+ | `--reviewer host=reviewer` | Reviewer mapping for a host (repeatable). Use `host=none` for self-review. **Required per host.** |
62
+ | `--global` / `--user` | Machine-wide install: write the defaults to `~/.adversarial-review/config.json` and merge the Claude Code hooks into your user-level `~/.claude/settings.json` instead of the per-project files. |
41
63
  | `--dry-run` | Print every planned write and exit 0 without writing anything. |
42
64
  | `--project-config <path>` | Write the project config to an explicit path. |
43
65
 
44
- > There is no `--user-config` flag. The machine-wide defaults file below is
45
- > written/edited by hand — the installer does not generate it.
66
+ ### Machine-wide install
67
+
68
+ To install once for **every** project on the machine, add `--global` (alias
69
+ `--user`):
70
+
71
+ ```bash
72
+ npx adversarial-review-gate install --global \
73
+ --hosts claude-code,codex \
74
+ --reviewer claude-code=opencode \
75
+ --reviewer codex=opencode
76
+ ```
77
+
78
+ This writes the host/reviewer defaults to `~/.adversarial-review/config.json`
79
+ and merges the Claude Code `SessionStart` + `Stop` hooks into your user-level
80
+ `~/.claude/settings.json` (existing keys are preserved). New projects then
81
+ inherit the gate without re-running install per project.
82
+
83
+ ### Uninstall
84
+
85
+ ```bash
86
+ npx adversarial-review-gate uninstall # remove the project install
87
+ npx adversarial-review-gate uninstall --user # remove the machine-wide install
88
+ ```
89
+
90
+ `uninstall` removes the hooks this tool wrote (from the project or user
91
+ `settings.json`) and the install-registry entry. Re-run `doctor` afterward to
92
+ confirm the gate is no longer active.
46
93
 
47
94
  ### After install
48
95
 
@@ -55,13 +102,25 @@ project config validity, and the Claude Code session baseline.
55
102
 
56
103
  ### Machine-wide defaults
57
104
 
58
- A user-level `~/.adversarial-review/config.json` provides host/reviewer defaults
59
- that apply across **all** projects. Config is layered in this order, where each
60
- later layer overrides the earlier ones except the policy floor, which can only
61
- ever tighten, never loosen:
105
+ Two distinct user-level files shape every project on the machine:
106
+
107
+ - **`~/.adversarial-review/config.json`** — the user **override layer**. It
108
+ provides host/reviewer defaults and policy that apply across **all** projects.
109
+ As a normal config layer it can either loosen **or** tighten relative to the
110
+ built-in defaults; a project config can in turn override it.
111
+ - **`~/.adversarial-review/policy.json`** — the **policy floor**. It is
112
+ **tighten-only**: it can raise the minimum policy (e.g. force `enforced` or
113
+ `strict-ci`) but no later layer — user config or project config — can ever
114
+ loosen below it.
115
+
116
+ Config is layered in this order, where each later layer overrides the earlier
117
+ ones, and the policy floor is applied last and can only ever tighten:
62
118
 
63
119
  ```text
64
- DEFAULT_CONFIG < userConfig (~/.adversarial-review/config.json) < projectConfig (.adversarial-review/config.json) < policy floor
120
+ DEFAULT_CONFIG
121
+ < userConfig (~/.adversarial-review/config.json) # override layer (loosen or tighten)
122
+ < projectConfig (.adversarial-review/config.json) # override layer (loosen or tighten)
123
+ < policyFloor (~/.adversarial-review/policy.json) # tighten-only, applied last
65
124
  ```
66
125
 
67
126
  Example `~/.adversarial-review/config.json`:
@@ -87,7 +146,8 @@ Example `~/.adversarial-review/config.json`:
87
146
 
88
147
  With this in place, a new project inherits the host/reviewer mapping and the
89
148
  `enforced` mode without re-running install per project. A project may still ship
90
- its own `.adversarial-review/config.json` to make policy **stricter** (see
149
+ its own `.adversarial-review/config.json` to override these defaults, but it can
150
+ never go below the policy floor in `~/.adversarial-review/policy.json` (see
91
151
  [Policy Modes](#policy-modes)).
92
152
 
93
153
  ---
@@ -135,9 +195,23 @@ explicitly `none`.
135
195
  Claude Code -> codex (external reviewer)
136
196
  Codex -> opencode (external reviewer)
137
197
  opencode -> none (self-review orchestration)
138
- GitHub Copilot CLI -> claude-code (external reviewer, if available)
139
198
  ```
140
199
 
200
+ The five hosts in the registry, with their enforcement and whether they can
201
+ delegate to an **external** reviewer:
202
+
203
+ | Host | Enforcement | External reviewer? |
204
+ |---|---|---|
205
+ | `claude-code` | native-enforced | yes |
206
+ | `codex` | wrapper-enforced | yes |
207
+ | `opencode` | wrapper-enforced | yes |
208
+ | `github-copilot-cli` | wrapper-enforced | no — self-review (`none`) only |
209
+ | `antigravity` | wrapper-enforced | no — self-review (`none`) only |
210
+
211
+ `github-copilot-cli` and `antigravity` are marked `supportsExternalReview: false`
212
+ in the registry, so they cannot be mapped to an external reviewer — use
213
+ `--reviewer github-copilot-cli=none` (self-review orchestration) for those.
214
+
141
215
  Reviewer tools are verified during install: binary must exist, basic version
142
216
  check must succeed, and auth check must pass where available.
143
217
 
@@ -162,38 +236,50 @@ unresolved Critical or Important findings.
162
236
 
163
237
  ## Using opencode as the reviewer (read-only)
164
238
 
165
- **This setup is required before opencode can pass the gate.** It was a real
166
- gotcha during local validation, so follow every point below.
239
+ **The installer sets this up for you.** When you map any host to
240
+ `--reviewer <host>=opencode`, `install` writes a working read-only opencode
241
+ reviewer with no manual steps:
242
+
243
+ - It creates the bundled `adversarial-reviewer` agent at
244
+ `~/.config/opencode/agent/adversarial-reviewer.md` (idempotent — it is
245
+ **skipped if the file already exists**, so a customized agent is never
246
+ overwritten).
247
+ - It writes `reviewers.opencode.readOnlyConfig: true` into the project config so
248
+ the gate's enforced-mode isolation check passes.
167
249
 
168
250
  opencode is invoked as `opencode run --pure --agent adversarial-reviewer -f <diff>`
169
- with the review brief delivered on stdin. For that to work, opencode must have an
170
- `adversarial-reviewer` agent defined, for example at
171
- `~/.config/opencode/agent/adversarial-reviewer.md`.
251
+ with the review brief delivered on stdin.
252
+
253
+ ### What the bundled setup guarantees
172
254
 
173
- Three hard requirements:
255
+ The agent the installer ships satisfies three invariants that the gate enforces.
256
+ You do not configure these by hand — verify them with
257
+ `npx adversarial-review-gate doctor` and `opencode agent list`:
174
258
 
175
- 1. **The agent MUST be `mode: primary` — NOT `subagent`.** `opencode run --agent`
259
+ 1. **The agent is `mode: primary` — NOT `subagent`.** `opencode run --agent`
176
260
  rejects a subagent and **silently** falls back to the full-permission default
177
261
  agent, printing `Falling back to default agent` to stderr. The gate detects
178
262
  that marker and rejects the review as an operational failure
179
263
  (`reviewer_agent_fallback`), so a subagent-mode agent can never pass — even if
180
264
  it printed a perfect verdict block.
181
265
 
182
- 2. **It must be read-only.** Set `permission` to deny everything and turn tools
183
- off, so the gate's enforced isolation check passes. In `enforced`/`strict-ci`
184
- the gate refuses any reviewer whose `verify()` capabilities are not
185
- `readOnly === true && noEdit === true` (`reviewer_not_isolated`). The opencode
186
- adapter only asserts those capabilities when
187
- `reviewers.opencode.readOnlyConfig: true` is set in config — so you must both
188
- make the agent read-only **and** set that flag.
266
+ 2. **The agent is read-only.** `permission` denies everything and tools are
267
+ turned off, and `reviewers.opencode.readOnlyConfig: true` is set, so the
268
+ adapter reports `readOnly === true && noEdit === true`. In
269
+ `enforced`/`strict-ci` the gate refuses any reviewer whose `verify()`
270
+ capabilities are not isolated (`reviewer_not_isolated`).
189
271
 
190
- 3. **The agent body must contain the verdict-block format the gate parses.** The
272
+ 3. **The agent body contains the verdict-block format the gate parses.** The
191
273
  brief on stdin carries the per-job `job_id` / `diff_hash` / `payload_hash` /
192
- `reviewer` / `level`; the agent must echo those exact values back inside a
193
- single `<<<ADVERSARIAL-REVIEW-VERDICT>>> ... <<<END>>>` block (see
274
+ `reviewer` / `level`; the agent echoes those exact values back inside a single
275
+ `<<<ADVERSARIAL-REVIEW-VERDICT>>> ... <<<END>>>` block (see
194
276
  [Verdict Format](#verdict-format)) with nothing after `<<<END>>>`.
195
277
 
196
- Minimal `~/.config/opencode/agent/adversarial-reviewer.md`:
278
+ ### If you customize the agent, keep these invariants
279
+
280
+ Because the installer never overwrites an existing agent file, an edited
281
+ `~/.config/opencode/agent/adversarial-reviewer.md` must still satisfy all three
282
+ invariants above. A minimal shape:
197
283
 
198
284
  ```markdown
199
285
  ---
@@ -274,11 +360,20 @@ node C:\abs\path\to\adversarial-review\bin\adversarial-review.js run --host code
274
360
 
275
361
  ## Claude Code (native)
276
362
 
277
- Claude Code is the only **native-enforced** host. The installer adds two hooks to
278
- `.claude/settings.json`:
363
+ Claude Code is the only **native-enforced** host. **The installer writes the
364
+ hooks for you** — when `claude-code` is in `--hosts`, `install` merges two hooks
365
+ into `.claude/settings.json` (or, with `--global`, into your user-level
366
+ `~/.claude/settings.json`):
279
367
 
280
368
  - A **SessionStart** hook that records the workspace baseline.
281
- - A **Stop** hook that applies the gate before the turn finishes.
369
+ - A **Stop** hook (with a **300-second timeout**) that applies the gate before
370
+ the turn finishes.
371
+
372
+ ### What the bundled setup guarantees
373
+
374
+ The hook commands invoke `adversarial-review-gate` directly when it resolves on
375
+ `PATH` (a global npm install), otherwise via `npx adversarial-review-gate`. The
376
+ written block looks like this:
282
377
 
283
378
  ```json
284
379
  {
@@ -288,7 +383,7 @@ Claude Code is the only **native-enforced** host. The installer adds two hooks t
288
383
  "hooks": [
289
384
  {
290
385
  "type": "command",
291
- "command": "node /abs/path/to/adversarial-review/bin/adversarial-review.js hook --host claude-code --event session-start"
386
+ "command": "adversarial-review-gate hook --host claude-code --event session-start"
292
387
  }
293
388
  ]
294
389
  }
@@ -298,7 +393,8 @@ Claude Code is the only **native-enforced** host. The installer adds two hooks t
298
393
  "hooks": [
299
394
  {
300
395
  "type": "command",
301
- "command": "node /abs/path/to/adversarial-review/bin/adversarial-review.js hook --host claude-code --event stop"
396
+ "command": "adversarial-review-gate hook --host claude-code --event stop",
397
+ "timeout": 300
302
398
  }
303
399
  ]
304
400
  }
@@ -307,15 +403,19 @@ Claude Code is the only **native-enforced** host. The installer adds two hooks t
307
403
  }
308
404
  ```
309
405
 
310
- For a **local (non-marketplace) install**, the hook command needs an **absolute
311
- path** to `bin/adversarial-review.js`. `${CLAUDE_PLUGIN_ROOT}` only resolves
312
- inside a marketplace plugin, so it will not work for a plain local checkout — use
313
- the absolute node path as shown above.
406
+ > The Stop hook carries a **300s timeout** because an external review can take up
407
+ > to a few minutes — Claude Code will not abort the hook before the review
408
+ > finishes.
314
409
 
315
410
  > Both hooks are required. A Stop hook that sees edit evidence but finds **no
316
411
  > recorded SessionStart baseline** fails closed (blocks) in `enforced`/`strict-ci`,
317
- > because the full change scope is unknown. Restart Claude Code after editing
318
- > `settings.json`.
412
+ > because the full change scope is unknown. **Restart Claude Code after install**
413
+ > so it re-reads `settings.json`. Verify the hooks with
414
+ > `npx adversarial-review-gate doctor`.
415
+
416
+ > For a **local (non-marketplace) checkout** where the package is not on `PATH`,
417
+ > the hook command needs an **absolute path** to `bin/adversarial-review.js`;
418
+ > `${CLAUDE_PLUGIN_ROOT}` only resolves inside a marketplace plugin.
319
419
 
320
420
  ---
321
421
 
@@ -337,7 +437,9 @@ never committed.
337
437
 
338
438
  ## Cost
339
439
 
340
- An external opencode review takes roughly **30 seconds** and **BLOCKS in
440
+ An external opencode review takes roughly **30 seconds** and a debate-tier
441
+ review on a large or sensitive diff can take **up to a few minutes** (the Claude
442
+ Code Stop hook is given a 300-second timeout for this reason). It **BLOCKS in
341
443
  `enforced`** until it passes. With a machine-wide `enforced` config, that gate
342
444
  runs on every significant-edit Stop across **all** projects — which adds up
343
445
  quickly.
@@ -563,14 +665,19 @@ Common issues:
563
665
  ## Commands
564
666
 
565
667
  ```bash
566
- npx adversarial-review-gate install # Interactive install wizard
567
668
  npx adversarial-review-gate install --hosts claude-code,codex --reviewer claude-code=opencode --reviewer codex=opencode
568
- npx adversarial-review-gate install --dry-run # Preview without writing
669
+ npx adversarial-review-gate install --global --hosts claude-code --reviewer claude-code=opencode # Machine-wide
670
+ npx adversarial-review-gate install --dry-run ... # Preview without writing
671
+ npx adversarial-review-gate uninstall # Remove the project install
672
+ npx adversarial-review-gate uninstall --user # Remove the machine-wide install
569
673
  npx adversarial-review-gate check # Run the gate manually against current working tree
570
674
  npx adversarial-review-gate run --host codex -- codex exec "..." # Wrapper mode
571
675
  npx adversarial-review-gate doctor # Verify installation
572
676
  ```
573
677
 
678
+ `--hosts` and `--reviewer` are required for `install`; there is no interactive
679
+ wizard (one may be added later).
680
+
574
681
  For a local (unpublished) checkout, `npx adversarial-review-gate` becomes
575
682
  `node /abs/path/to/adversarial-review/bin/adversarial-review.js`.
576
683
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adversarial-review-gate",
3
- "version": "2.0.3",
3
+ "version": "2.1.0",
4
4
  "description": "NodeJS multi-tool adversarial review gate for coding agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,13 +10,18 @@
10
10
  "scripts": {
11
11
  "test": "node --test",
12
12
  "doctor": "node ./bin/adversarial-review.js doctor --dry-run",
13
- "pack:dry-run": "npm pack --dry-run"
13
+ "pack:dry-run": "npm pack --dry-run",
14
+ "prepublishOnly": "npm test && npm run pack:dry-run"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
14
18
  },
15
19
  "files": [
16
20
  "bin/",
17
21
  "src/",
18
22
  ".claude-plugin/",
19
23
  "README.md",
24
+ "CHANGELOG.md",
20
25
  "LICENSE"
21
26
  ],
22
27
  "engines": {
package/src/cli/doctor.js CHANGED
@@ -11,31 +11,44 @@
11
11
  import { readFile } from "node:fs/promises";
12
12
  import { existsSync } from "node:fs";
13
13
  import path from "node:path";
14
- import os from "node:os";
15
14
  import { fileURLToPath } from "node:url";
16
15
 
17
16
  import { HOSTS } from "../hosts/index.js";
18
17
  import { createReviewer } from "../reviewers/index.js";
19
- import { loadEffectiveConfig } from "../core/load-config.js";
18
+ import { loadEffectiveConfig, resolveHomeDir } from "../core/load-config.js";
19
+ import { detectClaudeCodeHooks, claudeCodeSettingsPath } from "../hosts/claude-code.js";
20
20
 
21
21
  // Paths relative to home / cwd.
22
22
  const PROJECT_CONFIG_REL = path.join(".adversarial-review", "config.json");
23
23
  const USER_CONFIG_REL = path.join(".adversarial-review", "config.json");
24
24
  const USER_POLICY_REL = path.join(".adversarial-review", "policy.json");
25
+ const OPENCODE_AGENT_REL = path.join(
26
+ ".config",
27
+ "opencode",
28
+ "agent",
29
+ "adversarial-reviewer.md"
30
+ );
25
31
 
26
32
  // ---------------------------------------------------------------------------
27
33
  // Helpers
28
34
  // ---------------------------------------------------------------------------
29
35
 
30
- /** Resolve home from env, falling back to os.homedir(). Honors
31
- * ADVERSARIAL_REVIEW_HOME so doctor reports the SAME user-level base that
32
- * loadEffectiveConfig (the gate's loader) uses. */
33
- function homeDir(env) {
34
- if (env) {
35
- const fromEnv = env.ADVERSARIAL_REVIEW_HOME || env.HOME || env.USERPROFILE;
36
- if (fromEnv) return fromEnv;
36
+ /**
37
+ * Read + tolerantly parse a Claude Code settings.json. Returns {} on any error
38
+ * (missing or corrupt) so the hook-presence detection never throws.
39
+ *
40
+ * @param {string} filePath
41
+ * @returns {Promise<object>}
42
+ */
43
+ async function readSettingsTolerant(filePath) {
44
+ try {
45
+ const raw = await readFile(filePath, "utf8");
46
+ const parsed = JSON.parse(raw);
47
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
48
+ return {};
49
+ } catch {
50
+ return {};
37
51
  }
38
- return os.homedir();
39
52
  }
40
53
 
41
54
  /** Read package.json to get the package version. */
@@ -77,7 +90,7 @@ export async function doctorCommand(argv, io) {
77
90
  const json = argv.includes("--json");
78
91
  const cwd = io.cwd || process.cwd();
79
92
  const env = io.env || process.env;
80
- const home = homeDir(env);
93
+ const home = resolveHomeDir(env);
81
94
 
82
95
  // Read package version.
83
96
  const version = await readPackageVersion();
@@ -110,6 +123,23 @@ export async function doctorCommand(argv, io) {
110
123
  // hosts.)
111
124
  const effectiveConfig = await loadEffectiveConfig(cwd, io);
112
125
 
126
+ // For native hosts (claude-code) detect whether OUR SessionStart + Stop hooks
127
+ // are actually registered in the relevant .claude/settings.json — at BOTH
128
+ // project (<cwd>/.claude) and user (<home>/.claude) scope. A configured host
129
+ // whose hooks are missing means the gate would never fire.
130
+ const projectSettingsPath = claudeCodeSettingsPath(cwd);
131
+ const userSettingsPath = claudeCodeSettingsPath(home);
132
+ const projectHooks = detectClaudeCodeHooks(
133
+ await readSettingsTolerant(projectSettingsPath)
134
+ );
135
+ const userHooks = detectClaudeCodeHooks(
136
+ await readSettingsTolerant(userSettingsPath)
137
+ );
138
+
139
+ // opencode read-only agent presence (machine-wide, under <home>).
140
+ const opencodeAgentPath = path.join(home, OPENCODE_AGENT_REL);
141
+ const opencodeAgentExists = existsSync(opencodeAgentPath);
142
+
113
143
  // Enumerate configured hosts.
114
144
  const configuredHostIds = Object.keys(effectiveConfig.hosts || {});
115
145
  const hostReports = [];
@@ -135,6 +165,54 @@ export async function doctorCommand(argv, io) {
135
165
  reviewerNote: reviewerResult.note || null,
136
166
  reviewerReason: reviewerResult.reason || null,
137
167
  };
168
+
169
+ // For claude-code, report whether our native hooks are registered. We treat
170
+ // the host as "registered" if BOTH SessionStart + Stop are present at EITHER
171
+ // scope (project or user), since a user-scope install also covers this cwd.
172
+ if (hostId === "claude-code") {
173
+ const projectRegistered = projectHooks.sessionStart && projectHooks.stop;
174
+ const userRegistered = userHooks.sessionStart && userHooks.stop;
175
+ hostReport.hooks = {
176
+ projectSettingsPath,
177
+ userSettingsPath,
178
+ project: projectHooks,
179
+ user: userHooks,
180
+ registered: projectRegistered || userRegistered,
181
+ };
182
+ if (!hostReport.hooks.registered) {
183
+ warnings.push(
184
+ `WARNING: Host "claude-code" is configured but our SessionStart + Stop ` +
185
+ `hooks are NOT registered in ${projectSettingsPath} (project) or ` +
186
+ `${userSettingsPath} (user). Run \`adversarial-review install\` to register them.`
187
+ );
188
+ }
189
+ }
190
+
191
+ // For an opencode reviewer, report whether the read-only agent exists and
192
+ // whether reviewers.opencode.readOnlyConfig is set — the two settings the
193
+ // README promises and that enforced-mode isolation depends on.
194
+ if (reviewerId === "opencode") {
195
+ const readOnlyConfig =
196
+ effectiveConfig.reviewers?.opencode?.readOnlyConfig === true;
197
+ hostReport.opencode = {
198
+ agentPath: opencodeAgentPath,
199
+ agentExists: opencodeAgentExists,
200
+ readOnlyConfig,
201
+ };
202
+ if (!opencodeAgentExists) {
203
+ warnings.push(
204
+ `WARNING: opencode reviewer for host "${hostId}" but the read-only agent ` +
205
+ `is MISSING at ${opencodeAgentPath}. Run \`adversarial-review install\` to create it.`
206
+ );
207
+ }
208
+ if (!readOnlyConfig) {
209
+ warnings.push(
210
+ `WARNING: reviewers.opencode.readOnlyConfig is not set for host "${hostId}". ` +
211
+ `Enforced-mode isolation (readOnly && noEdit) will fail without it.`
212
+ );
213
+ }
214
+ }
215
+
138
216
  hostReports.push(hostReport);
139
217
 
140
218
  // Warn about wrapper/advisory hosts.
@@ -247,6 +325,25 @@ function printHumanReport(report, io) {
247
325
  } else {
248
326
  w(` reviewer status: UNAVAILABLE (${h.reviewerReason || "unknown"})\n`);
249
327
  }
328
+ // Native hook registration (claude-code only).
329
+ if (h.hooks) {
330
+ w(
331
+ ` native hooks registered: ${h.hooks.registered ? "yes" : "NO"}\n`
332
+ );
333
+ w(
334
+ ` project (${h.hooks.projectSettingsPath}): ` +
335
+ `SessionStart=${h.hooks.project.sessionStart} Stop=${h.hooks.project.stop}\n`
336
+ );
337
+ w(
338
+ ` user (${h.hooks.userSettingsPath}): ` +
339
+ `SessionStart=${h.hooks.user.sessionStart} Stop=${h.hooks.user.stop}\n`
340
+ );
341
+ }
342
+ // opencode reviewer details.
343
+ if (h.opencode) {
344
+ w(` opencode agent exists: ${h.opencode.agentExists ? "yes" : "NO"} (${h.opencode.agentPath})\n`);
345
+ w(` opencode readOnlyConfig: ${h.opencode.readOnlyConfig}\n`);
346
+ }
250
347
  }
251
348
  }
252
349