From 95051f62fecdf0cc48760ab604a72085d56b6550 Mon Sep 17 00:00:00 2001 From: zzxwill Date: Sat, 24 Oct 2026 09:43:39 +0800 Subject: [PATCH 1/3] docs: clarify Claude Code plugin setup and usage --- README.md | 36 ++++++--- plugin/README.md | 200 +++++++++++++++++++++++++++++++++++------------ 2 files changed, 172 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 304591e..17f3a50 100644 --- a/README.md +++ b/README.md @@ -134,25 +134,37 @@ for prerequisites, agent selection, and saved reviews. ## Claude Code plugin -Code Jury is also a [Claude Code plugin](https://code.claude.com/docs/en/plugins). Add this -repository as a plugin marketplace and install it: +Use `/codejury:review ` inside Claude Code to review and fix a pull request. +You need Node.js 20.10+, npm, Git, and a signed-in Claude Code CLI. GitHub PRs also +require a signed-in `gh` CLI. A separate `jury` installation is optional: the plugin +uses it if available, or runs `npx -y @agentsdance/codejury` otherwise. + +**1. Install from your terminal:** ```sh claude plugin marketplace add agentsdance/codejury claude plugin install codejury@codejury ``` -In a session, `/codejury:review ` runs `jury review` (or `npx @agentsdance/codejury` -when `jury` is not installed). In Claude Code it always sets `--judge claude --reviewer claude`, -overriding supplied and saved role choices for new and resumed runs. The two roles use separate -sessions. Other flags take precedence over the `--web=true` and `--push=false` defaults. -Claude shares the web console link, summarizes the findings and verdicts when the review ends, -and leaves the console running. Pass `--web=false` to disable it. The plugin requires a PR URL; -if omitted, Claude asks for it before starting. The plugin also ships the -`codejury` skill for running the review loop by hand. Agent CLIs are still installed -and authenticated separately. `--push=false` disables pushing; it does not make the review read-only. +**2. Start a new Claude Code session** by running `claude` in your terminal. + +**3. Enter this at the Claude Code prompt**, replacing the URL with your PR: + +```text +/codejury:review https://github.com/owner/repo/pull/123 +``` + +Claude starts a separate Claude reviewer and Claude judge, shares the browser console +link, and summarizes the result. The reviewer reports findings; the judge can edit +files and create commits. Both roles always use Claude, overriding supplied and saved +role choices for new and resumed runs. + +**Pushing is off by default.** Add `--push` to allow pushes to the PR branch. Single-PR checkouts are temporary and their unpushed fixes are removed during cleanup. -See the [plugin guide](plugin/README.md) for prerequisites, updates, and local installation checks. +The console stays open after review; add `--web=false` to skip it. A PR URL is required. + +See the [step-by-step plugin guide](plugin/README.md) for setup checks, examples, +saved results, troubleshooting, and the separate manual-triage skill. ## Screenshots diff --git a/plugin/README.md b/plugin/README.md index 8c04597..9a8610f 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -1,73 +1,168 @@ # Code Jury for Claude Code -Review PRs with independent AI code reviewers and a judge from a Claude Code session. The judge -triages findings and fixes accepted issues; Jury repeats the loop until the reviewers agree or the run -reaches its limit. +Run `/codejury:review ` inside Claude Code to review and fix a pull request. +Code Jury starts a Claude reviewer to find issues and a separate Claude judge to +evaluate them and make fixes. It repeats the review until there are no new findings +or the round limit is reached. -## Install +## Quick start + +### 1. Check prerequisites in your terminal + +You need Node.js **20.10 or newer**, npm, Git, and a signed-in Claude Code CLI. +For GitHub PRs, you also need the GitHub CLI (`gh`) signed in to an account that can +access the repository. Claude is the only coding-agent CLI this plugin needs. + +Run these in your **terminal** (Terminal, iTerm, or your IDE's terminal): + +```sh +node --version +npm --version +git --version +claude --version +claude auth status +gh auth status +``` + +Install any missing tools before continuing. If authentication is missing, run +`claude auth login` or `gh auth login` for the corresponding tool. + +You do **not** need to install the `jury` CLI separately. The plugin uses `jury` +if it is on your PATH; otherwise it runs `npx -y @agentsdance/codejury`, which +downloads the CLI on first use. The plugin does not install or sign in to Claude +or GitHub for you. + +### 2. Install the plugin in your terminal ```sh claude plugin marketplace add agentsdance/codejury claude plugin install codejury@codejury +claude plugin list ``` -Start a new Claude Code session after installing, then run: +The first command registers this repository as a plugin source. The second installs +the plugin for your user account, so it is available across projects. In +`codejury@codejury`, the first name is the plugin and the second is the marketplace. +The list should show `codejury@codejury` as enabled. + +Start a new Claude Code session to load it: + +```sh +claude +``` + +### 3. Start a review inside Claude Code + +Enter this at the **Claude Code prompt**, replacing the example URL with your PR: ```text /codejury:review https://github.com/owner/repo/pull/123 -/codejury:review https://github.com/owner/repo/pull/123 --rounds 3 ``` -Both examples use Claude as judge and jury, in separate sessions. The second sets the round limit. -Both open the web console and display its link in Claude Code. - -A PR URL is required. If you run `/codejury:review` without one, Claude asks for the URL before -starting. Local-branch and local-directory reviews are outside this plugin's scope. -You can pass multiple related PR URLs or other `jury review` flags. With a PR URL, `--dir` selects -where checkouts and run records are stored. Resuming a run also requires its original PR URL(s). - -You need Node.js 20.10 or newer, npm, Git, and installed, authenticated agent CLIs. GitHub PR URLs -also need an authenticated `gh` CLI. The plugin uses an installed `jury`, or -`npx -y @agentsdance/codejury` if `jury` is absent. It does not bundle or authenticate agent CLIs. -Use `jury agents` (or `npx -y @agentsdance/codejury agents`) to inspect your setup. - -If an older CLI rejects Claude in both roles, the plugin retries once with the repository CLI -using `npx -y --package=git+https://github.com/agentsdance/codejury.git jury`. This leaves your -installed CLI and saved configuration unchanged. - -## Behavior - -- `/codejury:review` runs only when invoked by the user. It always sets `--judge claude` and - `--reviewer claude`, including on resume. Supplied role flags and saved CLI role choices are - overridden for this run; model and other settings still apply. Saved settings are not changed. -- Jury launches separate Claude sessions for the two roles. The reviewer is read-only; the judge - can make fixes. Your Claude Code session monitors the run. Both roles use the same agent, so - this does not provide model diversity. -- It defaults to `--web=true`: the browser console opens and Claude shares its URL. Claude reports - the review result when it is ready and leaves the console running for you to inspect. - Pass `--web=false` for results only in Claude Code and a command that exits after review. -- It defaults to `--push=false`, so Jury does not push. The judge can still edit files and Jury - can create commits. Pass `--push` to publish them. Single-PR URL checkouts are temporary and - their unpushed fixes are removed during cleanup. -- Explicit `--web` and `--push` values take precedence. -- `codejury` provides guidance for manually coordinating reviewers and triaging - findings. Its source moved from `skill/SKILL.md` to `plugin/skills/codejury/SKILL.md`; - update any copies or symlinks that used the old path. +This is a Claude slash command. All `/codejury:...` examples in this guide go in +Claude Code; commands beginning with `claude`, `jury`, or `npx` go in your terminal. + +You should see Claude report the selected roles and share a clickable browser +console URL. The console shows findings, the judge's verdicts, and review progress. +When the review ends, Claude summarizes the outcome and leaves the console running +for you to inspect. A run that hits its round limit or fails is reported as incomplete. + +A PR URL is required, even if you start Claude in the PR's repository. If you omit +it, Claude asks for it. Jury creates an isolated checkout from the URL; you do not +need to clone the PR yourself. Local files, folders, and branches alone are not +supported review targets for this plugin. + +## What the review does + +| Role | What it does | +| --- | --- | +| Your Claude Code session | Starts and monitors the automated run, shares its console link, and reports the result. | +| Claude reviewer | Reads the code and reports findings in a separate, read-only session. | +| Claude judge | Evaluates findings and can edit files and create commits in another session. | + +The plugin always selects `--judge claude --reviewer claude`, including when +resuming. It overrides supplied role flags and saved role choices for this run. +Other settings, such as models, still apply; saved configuration is unchanged. +Separate Claude sessions do not provide model diversity. + +**Reviews can edit files and create commits. Pushing is off by default.** +The plugin adds `--push=false`; add `--push` only when you want Jury to push fixes +to the PR branch. A single-PR checkout is temporary, and its unpushed fixes are +removed during cleanup. Saved review records remain available, but they are not +a retained checkout of those fixes. See [state and retention](https://github.com/agentsdance/codejury/blob/master/docs/usage.md#working-directories). + +The browser console is on by default (`--web=true`). Add `--web=false` to skip it +and let the command exit after review. Explicit `--web` and `--push` values take +precedence over these defaults. + +## Common commands + +Enter these **inside Claude Code**, replacing the URLs and run slug: + +| To… | Enter… | +| --- | --- | +| Limit the review to three rounds | `/codejury:review https://github.com/owner/repo/pull/123 --rounds 3` | +| Skip the browser console | `/codejury:review https://github.com/owner/repo/pull/123 --web=false` | +| Allow pushes to the PR branch | `/codejury:review https://github.com/owner/repo/pull/123 --push` | +| Review related PRs together | `/codejury:review https://github.com/owner/repo/pull/123 https://github.com/owner/repo/pull/456` | +| Continue a saved run | `/codejury:review https://github.com/owner/repo/pull/123 --resume ` | + +For resume, supply the original PR URL(s), keeping related URLs in the same order. +With a PR URL, `--dir /path/to/state` chooses where Jury stores checkouts and run +records; it does not select a local folder to review. Repeat the same `--dir` when +resuming. For all flags, run `npx -y @agentsdance/codejury help review` in your +terminal (or `jury help review` if `jury` is installed). + +### Find saved results + +URL-based reviews store records in `~/.jury` by default. Run these in your **terminal**: + +```sh +npx -y @agentsdance/codejury runs --dir "" +npx -y @agentsdance/codejury --web-only --dir "" --run +``` + +Replace `` with a slug from the first command. `--dir ""` explicitly selects +`~/.jury`, even when your terminal is in a Git repository. If the review used a custom +`--dir`, use that path instead. Use `jury` in place of `npx -y @agentsdance/codejury` +if that was the CLI used for the review. Opening saved results does not start reviewers. + +### Which skill should I use? + +| Claude Code command | Use it for | +| --- | --- | +| `/codejury:review ` | The automated review-and-fix loop described above. Start here. It runs only when you invoke it. | +| `/codejury:codejury` | Guidance for explicitly requested manual review and triage, where your current Claude session acts as judge and coordinates separate Claude reviewers. It still requires a PR URL before reviewers start. | + +## Troubleshooting + +| Symptom | What to do | +| --- | --- | +| `/codejury:review` is missing | Run `claude plugin list` in your terminal. If absent, repeat installation; if disabled, run `claude plugin enable codejury@codejury`. Start a new Claude session. `claude plugin details codejury` should list the `review` and `codejury` skills. | +| `jury: command not found` in your terminal | Use `npx -y @agentsdance/codejury` in place of `jury`. The plugin already makes this choice automatically. | +| Claude or GitHub authentication fails | Check `claude auth status` or `gh auth status` and sign in with the corresponding `auth login` command. `jury agents` checks installed executables, not authentication or quota. | +| An older CLI rejects Claude as both judge and reviewer | The plugin retries once with the repository CLI: `npx -y --package=git+https://github.com/agentsdance/codejury.git jury`. It keeps your arguments and saved configuration. If that also fails, it reports the error. | +| The review is done but the process is still running | The web console stays up for inspection. Ask Claude to stop the console when finished, or use `--web=false` on the next review. | +| A saved run is missing | List runs with the same state root used for review. For the default URL-based review, use `runs --dir ""`. | ## Update +Run these in your **terminal**, then start a new Claude Code session: + ```sh claude plugin marketplace update codejury claude plugin update codejury@codejury ``` -The plugin deliberately omits a version field so updates follow the repository without maintaining -a second release number. The installed Code Jury CLI is separate; update a global CLI installation -with `npm install -g @agentsdance/codejury` when needed. +The plugin and Code Jury CLI are separate. Updating the plugin refreshes its skills; +if you installed `jury` globally, update that CLI with `npm install -g @agentsdance/codejury`. +The plugin deliberately omits a version field so it follows the repository without +maintaining a second release number. See Claude Code's [plugin installation and management guide](https://code.claude.com/docs/en/discover-plugins) +for installation scopes and other plugin controls. -## Develop and validate +## For plugin contributors -From the repository root: +From the Code Jury repository root, run these in your terminal: ```sh claude plugin validate ./plugin @@ -76,10 +171,11 @@ node --test test/plugin.test.js claude --plugin-dir ./plugin ``` -To test marketplace installation, add the local repository path as a marketplace and install -`codejury@codejury`. Use an isolated `CLAUDE_CONFIG_DIR` for development so this does not change your -normal marketplace settings. `claude plugin details codejury` should list `codejury` and `review`. +`--plugin-dir` loads the local plugin for that session. To test marketplace installation, +add the local repository path as a marketplace and install `codejury@codejury` using an +isolated `CLAUDE_CONFIG_DIR`. `claude plugin details codejury` should list `codejury` and `review`. -The plugin lives under `plugin/` so installation includes only its manifest, skills, and this guide. -Skill references to repository documentation use GitHub URLs because the rest of the source tree -is not part of the installed plugin. +The Claude Code plugin lives under `plugin/`. Installation includes its manifest, +skills, and this guide. Skill references to repository documentation use GitHub URLs +because the rest of the source tree is not installed. The manual skill moved from +`skill/SKILL.md` to `plugin/skills/codejury/SKILL.md`; update old copies or symlinks. From 731db2e9aed15accc6a3e5495b640c8bac71275d Mon Sep 17 00:00:00 2001 From: zzxwill Date: Sat, 24 Oct 2026 12:16:48 +0800 Subject: [PATCH 2/3] docs: simplify Claude Code plugin quick start --- README.md | 27 ++------ plugin/README.md | 170 ++++++----------------------------------------- 2 files changed, 26 insertions(+), 171 deletions(-) diff --git a/README.md b/README.md index 17f3a50..72e8a37 100644 --- a/README.md +++ b/README.md @@ -134,37 +134,24 @@ for prerequisites, agent selection, and saved reviews. ## Claude Code plugin -Use `/codejury:review ` inside Claude Code to review and fix a pull request. -You need Node.js 20.10+, npm, Git, and a signed-in Claude Code CLI. GitHub PRs also -require a signed-in `gh` CLI. A separate `jury` installation is optional: the plugin -uses it if available, or runs `npx -y @agentsdance/codejury` otherwise. - -**1. Install from your terminal:** +Install from your **terminal** and start a new Claude Code session: ```sh claude plugin marketplace add agentsdance/codejury claude plugin install codejury@codejury +claude ``` -**2. Start a new Claude Code session** by running `claude` in your terminal. - -**3. Enter this at the Claude Code prompt**, replacing the URL with your PR: +At the **Claude Code prompt**, enter your PR URL: ```text /codejury:review https://github.com/owner/repo/pull/123 ``` -Claude starts a separate Claude reviewer and Claude judge, shares the browser console -link, and summarizes the result. The reviewer reports findings; the judge can edit -files and create commits. Both roles always use Claude, overriding supplied and saved -role choices for new and resumed runs. - -**Pushing is off by default.** Add `--push` to allow pushes to the PR branch. -Single-PR checkouts are temporary and their unpushed fixes are removed during cleanup. -The console stays open after review; add `--web=false` to skip it. A PR URL is required. - -See the [step-by-step plugin guide](plugin/README.md) for setup checks, examples, -saved results, troubleshooting, and the separate manual-triage skill. +Claude reviews and fixes the PR in separate reviewer and judge sessions, and opens +a browser console. Pushing is off by default; add `--push` to push fixes. +Unpushed fixes in temporary single-PR checkouts are removed during cleanup. +See the [plugin guide](plugin/README.md) for prerequisites, options, and updates. ## Screenshots diff --git a/plugin/README.md b/plugin/README.md index 9a8610f..d0fbd95 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -1,181 +1,49 @@ # Code Jury for Claude Code -Run `/codejury:review ` inside Claude Code to review and fix a pull request. -Code Jury starts a Claude reviewer to find issues and a separate Claude judge to -evaluate them and make fixes. It repeats the review until there are no new findings -or the round limit is reached. +Review and fix pull requests with a Claude reviewer and a separate Claude judge. -## Quick start +You need Node.js 20.10+, npm, Git, and a signed-in Claude Code CLI. GitHub PRs +also need a signed-in `gh` CLI. Installing `jury` separately is optional. -### 1. Check prerequisites in your terminal +## Install -You need Node.js **20.10 or newer**, npm, Git, and a signed-in Claude Code CLI. -For GitHub PRs, you also need the GitHub CLI (`gh`) signed in to an account that can -access the repository. Claude is the only coding-agent CLI this plugin needs. - -Run these in your **terminal** (Terminal, iTerm, or your IDE's terminal): - -```sh -node --version -npm --version -git --version -claude --version -claude auth status -gh auth status -``` - -Install any missing tools before continuing. If authentication is missing, run -`claude auth login` or `gh auth login` for the corresponding tool. - -You do **not** need to install the `jury` CLI separately. The plugin uses `jury` -if it is on your PATH; otherwise it runs `npx -y @agentsdance/codejury`, which -downloads the CLI on first use. The plugin does not install or sign in to Claude -or GitHub for you. - -### 2. Install the plugin in your terminal +In your **terminal**, install the plugin and start a new Claude Code session: ```sh claude plugin marketplace add agentsdance/codejury claude plugin install codejury@codejury -claude plugin list -``` - -The first command registers this repository as a plugin source. The second installs -the plugin for your user account, so it is available across projects. In -`codejury@codejury`, the first name is the plugin and the second is the marketplace. -The list should show `codejury@codejury` as enabled. - -Start a new Claude Code session to load it: - -```sh claude ``` -### 3. Start a review inside Claude Code +## Review -Enter this at the **Claude Code prompt**, replacing the example URL with your PR: +At the **Claude Code prompt**, enter this with your PR URL: ```text /codejury:review https://github.com/owner/repo/pull/123 ``` -This is a Claude slash command. All `/codejury:...` examples in this guide go in -Claude Code; commands beginning with `claude`, `jury`, or `npx` go in your terminal. - -You should see Claude report the selected roles and share a clickable browser -console URL. The console shows findings, the judge's verdicts, and review progress. -When the review ends, Claude summarizes the outcome and leaves the console running -for you to inspect. A run that hits its round limit or fails is reported as incomplete. - -A PR URL is required, even if you start Claude in the PR's repository. If you omit -it, Claude asks for it. Jury creates an isolated checkout from the URL; you do not -need to clone the PR yourself. Local files, folders, and branches alone are not -supported review targets for this plugin. - -## What the review does - -| Role | What it does | -| --- | --- | -| Your Claude Code session | Starts and monitors the automated run, shares its console link, and reports the result. | -| Claude reviewer | Reads the code and reports findings in a separate, read-only session. | -| Claude judge | Evaluates findings and can edit files and create commits in another session. | - -The plugin always selects `--judge claude --reviewer claude`, including when -resuming. It overrides supplied role flags and saved role choices for this run. -Other settings, such as models, still apply; saved configuration is unchanged. -Separate Claude sessions do not provide model diversity. - -**Reviews can edit files and create commits. Pushing is off by default.** -The plugin adds `--push=false`; add `--push` only when you want Jury to push fixes -to the PR branch. A single-PR checkout is temporary, and its unpushed fixes are -removed during cleanup. Saved review records remain available, but they are not -a retained checkout of those fixes. See [state and retention](https://github.com/agentsdance/codejury/blob/master/docs/usage.md#working-directories). - -The browser console is on by default (`--web=true`). Add `--web=false` to skip it -and let the command exit after review. Explicit `--web` and `--push` values take -precedence over these defaults. +Claude starts the review, opens a browser console, and summarizes the findings +and fixes when it finishes. Both reviewer and judge use separate Claude sessions. +A PR URL is required. -## Common commands +**Pushing is off by default.** The judge can still edit and commit. Single-PR +checkouts are temporary; unpushed fixes are removed during cleanup. -Enter these **inside Claude Code**, replacing the URLs and run slug: +Optional flags to add to the review command: -| To… | Enter… | -| --- | --- | -| Limit the review to three rounds | `/codejury:review https://github.com/owner/repo/pull/123 --rounds 3` | -| Skip the browser console | `/codejury:review https://github.com/owner/repo/pull/123 --web=false` | -| Allow pushes to the PR branch | `/codejury:review https://github.com/owner/repo/pull/123 --push` | -| Review related PRs together | `/codejury:review https://github.com/owner/repo/pull/123 https://github.com/owner/repo/pull/456` | -| Continue a saved run | `/codejury:review https://github.com/owner/repo/pull/123 --resume ` | - -For resume, supply the original PR URL(s), keeping related URLs in the same order. -With a PR URL, `--dir /path/to/state` chooses where Jury stores checkouts and run -records; it does not select a local folder to review. Repeat the same `--dir` when -resuming. For all flags, run `npx -y @agentsdance/codejury help review` in your -terminal (or `jury help review` if `jury` is installed). - -### Find saved results - -URL-based reviews store records in `~/.jury` by default. Run these in your **terminal**: - -```sh -npx -y @agentsdance/codejury runs --dir "" -npx -y @agentsdance/codejury --web-only --dir "" --run -``` - -Replace `` with a slug from the first command. `--dir ""` explicitly selects -`~/.jury`, even when your terminal is in a Git repository. If the review used a custom -`--dir`, use that path instead. Use `jury` in place of `npx -y @agentsdance/codejury` -if that was the CLI used for the review. Opening saved results does not start reviewers. - -### Which skill should I use? - -| Claude Code command | Use it for | -| --- | --- | -| `/codejury:review ` | The automated review-and-fix loop described above. Start here. It runs only when you invoke it. | -| `/codejury:codejury` | Guidance for explicitly requested manual review and triage, where your current Claude session acts as judge and coordinates separate Claude reviewers. It still requires a PR URL before reviewers start. | - -## Troubleshooting - -| Symptom | What to do | -| --- | --- | -| `/codejury:review` is missing | Run `claude plugin list` in your terminal. If absent, repeat installation; if disabled, run `claude plugin enable codejury@codejury`. Start a new Claude session. `claude plugin details codejury` should list the `review` and `codejury` skills. | -| `jury: command not found` in your terminal | Use `npx -y @agentsdance/codejury` in place of `jury`. The plugin already makes this choice automatically. | -| Claude or GitHub authentication fails | Check `claude auth status` or `gh auth status` and sign in with the corresponding `auth login` command. `jury agents` checks installed executables, not authentication or quota. | -| An older CLI rejects Claude as both judge and reviewer | The plugin retries once with the repository CLI: `npx -y --package=git+https://github.com/agentsdance/codejury.git jury`. It keeps your arguments and saved configuration. If that also fails, it reports the error. | -| The review is done but the process is still running | The web console stays up for inspection. Ask Claude to stop the console when finished, or use `--web=false` on the next review. | -| A saved run is missing | List runs with the same state root used for review. For the default URL-based review, use `runs --dir ""`. | +- `--push` — push fixes to the PR branch. +- `--rounds 3` — stop after at most three rounds. +- `--web=false` — skip the browser console, which otherwise stays open after review. ## Update -Run these in your **terminal**, then start a new Claude Code session: +Run in your terminal, then start a new Claude Code session: ```sh claude plugin marketplace update codejury claude plugin update codejury@codejury ``` -The plugin and Code Jury CLI are separate. Updating the plugin refreshes its skills; -if you installed `jury` globally, update that CLI with `npm install -g @agentsdance/codejury`. -The plugin deliberately omits a version field so it follows the repository without -maintaining a second release number. See Claude Code's [plugin installation and management guide](https://code.claude.com/docs/en/discover-plugins) -for installation scopes and other plugin controls. - -## For plugin contributors - -From the Code Jury repository root, run these in your terminal: - -```sh -claude plugin validate ./plugin -claude plugin validate . -node --test test/plugin.test.js -claude --plugin-dir ./plugin -``` - -`--plugin-dir` loads the local plugin for that session. To test marketplace installation, -add the local repository path as a marketplace and install `codejury@codejury` using an -isolated `CLAUDE_CONFIG_DIR`. `claude plugin details codejury` should list `codejury` and `review`. - -The Claude Code plugin lives under `plugin/`. Installation includes its manifest, -skills, and this guide. Skill references to repository documentation use GitHub URLs -because the rest of the source tree is not installed. The manual skill moved from -`skill/SKILL.md` to `plugin/skills/codejury/SKILL.md`; update old copies or symlinks. +For saved results, resuming, and troubleshooting, see the +[CLI usage guide](https://github.com/agentsdance/codejury/blob/master/docs/usage.md). From e1d46d68f93f0c1eb955a3c9fa61a95c48ef7d66 Mon Sep 17 00:00:00 2001 From: zzxwill Date: Sun, 25 Oct 2026 17:24:38 +0800 Subject: [PATCH 3/3] fix: use the global CLI in Codex and Claude plugins --- CHANGELOG.md | 3 + README.zh-CN.md | 2 +- docs/codex-plugin.md | 9 +- plugin/README.md | 3 +- plugin/scripts/jury.mjs | 43 +++ plugin/skills/codejury/SKILL.md | 7 +- plugin/skills/review/SKILL.md | 25 +- plugins/codejury/plugin.json | 6 +- plugins/codejury/scripts/jury.mjs | 87 ++---- .../codejury/skills/codejury-results/SKILL.md | 7 +- .../codejury/skills/codejury-review/SKILL.md | 9 +- .../codejury/skills/codejury-setup/SKILL.md | 12 +- test/codex-plugin.test.js | 272 +++++++----------- test/plugin.test.js | 5 +- 14 files changed, 219 insertions(+), 271 deletions(-) create mode 100644 plugin/scripts/jury.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 674aa7d..b255d4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Make Codex and Claude plugins reuse the global CodeJury CLI, install globally + when missing, and stop on setup errors instead of using private or npx copies. + - Preserve single-PR push preferences on resume, and keep fixes local when an older run has no saved preference unless pushing is explicitly enabled. - Scope agent temporary files and default Go caches to owned invocation directories, diff --git a/README.zh-CN.md b/README.zh-CN.md index 114eb1e..5abfab5 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -137,7 +137,7 @@ claude plugin install codejury@codejury ``` 在会话中运行 `/codejury:review `,插件会调用 `jury review`; -如果没有安装 `jury`,则使用 `npx @agentsdance/codejury`。 +插件先检查全局 `jury`;未安装时通过 npm 全局安装,不使用独立目录或临时副本。 在 Claude Code 中,插件始终设置 `--judge claude --reviewer claude`, 对新任务和恢复的任务都会覆盖传入或已保存的角色选择。两种角色使用独立会话。 其他显式传入的参数优先于默认的 `--web=true` 和 `--push=false`。 diff --git a/docs/codex-plugin.md b/docs/codex-plugin.md index e0ee5ea..45be651 100644 --- a/docs/codex-plugin.md +++ b/docs/codex-plugin.md @@ -15,8 +15,8 @@ In a new Codex chat, select **CodeJury** and ask: > Set up CodeJury and check which agents I can use. Setup checks prerequisites without starting a review. The plugin uses your global -CodeJury CLI if available on PATH, or installs its fallback release automatically. -Agent installation and login are separate. +CodeJury CLI, or installs it globally with `npm install --global @agentsdance/codejury@latest` +if missing. Agent installation and login are separate. ## Review a PR @@ -41,8 +41,9 @@ To continue or inspect a saved review, ask: ## Development and packaging The plugin source is in [`plugins/codejury`](../plugins/codejury). Its launcher -checks for an installed `jury` or `codejury` on PATH before downloading its fallback -CLI release from npm. The plugin and CLI have separate versions. +checks npm's global package directory and reuses CodeJury there. If missing, it +installs globally; it never creates a separate plugin CLI installation. The plugin +and CLI have separate versions. Build the submission archive from the repository root: diff --git a/plugin/README.md b/plugin/README.md index d0fbd95..210482c 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -3,7 +3,8 @@ Review and fix pull requests with a Claude reviewer and a separate Claude judge. You need Node.js 20.10+, npm, Git, and a signed-in Claude Code CLI. GitHub PRs -also need a signed-in `gh` CLI. Installing `jury` separately is optional. +also need a signed-in `gh` CLI. The plugin checks your global `jury` installation +and installs it globally if missing. ## Install diff --git a/plugin/scripts/jury.mjs b/plugin/scripts/jury.mjs new file mode 100644 index 0000000..f4bdaff --- /dev/null +++ b/plugin/scripts/jury.mjs @@ -0,0 +1,43 @@ +// Both plugins use npm's global CodeJury installation. Keep the two shipped runners in sync. +import { spawnSync } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +const [major, minor] = process.versions.node.split('.').map(Number); +if (major < 20 || (major === 20 && minor < 10)) throw new Error('CodeJury requires Node.js 20.10+.'); + +function npm(args, capture = false) { + const result = spawnSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', args, { + encoding: 'utf8', stdio: capture ? ['ignore', 'pipe', 'inherit'] : 'inherit', + shell: process.platform === 'win32', + }); + if (result.error || result.status !== 0) { + throw new Error(`npm ${args.join(' ')} failed (${result.error?.message ?? result.status ?? result.signal}). Fix npm's global installation and retry.`, { cause: result.error }); + } + return capture ? result.stdout.trim() : undefined; +} + +const globalRoot = npm(['root', '--global'], true); +if (!path.isAbsolute(globalRoot)) throw new Error('npm did not return an absolute global package directory.'); +const packageRoot = path.join(globalRoot, '@agentsdance', 'codejury'); +const entry = path.join(packageRoot, 'bin', 'jury.js'); +function installed() { + try { + const metadata = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf8')); + return metadata.name === '@agentsdance/codejury' && existsSync(entry) ? metadata : null; + } catch { return null; } +} + +let metadata = installed(); +if (!metadata) { + console.error('Installing CodeJury globally with npm.'); + npm(['install', '--global', '--ignore-scripts', '--no-audit', '--no-fund', '@agentsdance/codejury@latest']); + metadata = installed(); +} +if (!metadata) throw new Error(`Global CodeJury installation is incomplete at ${packageRoot}. Run npm install --global @agentsdance/codejury@latest and retry.`); +console.error(`Using global CodeJury ${metadata.version} at ${entry}`); + +// Import in this process so cancellation reaches the CLI and its agent cleanup. +process.argv = [process.execPath, entry, ...process.argv.slice(2)]; +await import(pathToFileURL(entry).href); diff --git a/plugin/skills/codejury/SKILL.md b/plugin/skills/codejury/SKILL.md index a6a218e..02a0fab 100644 --- a/plugin/skills/codejury/SKILL.md +++ b/plugin/skills/codejury/SKILL.md @@ -17,8 +17,11 @@ The CLI owns the mechanics; you own the judgement. See [DESIGN.md](https://githu For the automated loop, use `/codejury:review`; it always selects Claude as both judge and jury in separate sessions, and only the judge owns fixes. The guidance below is for explicitly requested manual review and triage. Do not edit a checkout while an -automated Jury run is using it. In CLI examples, substitute `npx -y @agentsdance/codejury` for -`jury` if it is not installed, and keep the same `--dir` and `--run` when working with saved findings. +automated Jury run is using it. Resolve `` as two directories above this +skill folder and run `node "/scripts/jury.mjs" version` first. The runner +checks npm's global CodeJury installation and installs globally if missing; it stops +on errors instead of using npx or a private copy. Use this runner in place of `jury` +in the examples below. Keep the same `--dir` and `--run` for saved findings. ## The loop diff --git a/plugin/skills/review/SKILL.md b/plugin/skills/review/SKILL.md index 8ef4b17..e63be27 100644 --- a/plugin/skills/review/SKILL.md +++ b/plugin/skills/review/SKILL.md @@ -24,14 +24,17 @@ or start a local review. Flags alone, including `--dir` or `--resume`, do not su ## 2. Find the CLI -Use `jury` when `command -v jury` finds it. Otherwise run it without installing, as -`npx -y @agentsdance/codejury`. It needs Node.js 20.10 or newer and npm; if neither works, explain -which prerequisite is missing and how to install it. Use the same CLI prefix for every command -below, including `agents`, `runs`, and reopening the console. +Resolve `` as two directories above this skill folder. Run +`node "/scripts/jury.mjs" version` to check the global CLI. The runner +uses `npm root --global` to find the installed CodeJury package; if missing, it +installs the latest stable release globally. It reports the version and absolute +CLI path, and stops on installation errors. Node.js 20.10+ and npm are required. +Use this runner in place of `jury` in every command below, including `agents`, +`runs`, and reopening the console. Never use npx or install a private CLI copy. ## 3. Build the command -Start from `jury review` (or `npx -y @agentsdance/codejury review`). Normalize the role flags as +Start from `node "/scripts/jury.mjs" review`. Normalize the role flags as described below, preserve the remaining arguments, and quote each value safely for the shell. Do not evaluate argument text as shell code. @@ -66,12 +69,12 @@ missing or unauthenticated agent CLI, a missing `gh` login for a GitHub URL), sh the fix it names; `jury agents` lists which agent CLIs are installed. Older CLIs, including 0.8.1, reject a reviewer that is also the judge. If that specific error -appears, explain that the installed CLI is too old and retry once using -`npx -y --package=git+https://github.com/agentsdance/codejury.git jury` with the same normalized -arguments. The repository CLI supports separate sessions for both roles. Use that CLI prefix -for subsequent commands too. If the retry fails, show the error; do not substitute another agent -or keep retrying. Explicitly disabled Claude or missing authentication still requires fixing -that prerequisite. +appears, explain that the global CLI is too old. Upgrade it with +`npm install --global @agentsdance/codejury@latest`, then retry once through the +same runner with the same normalized arguments. Honor an explicitly requested +CLI version instead of upgrading it silently. If the upgrade or retry fails, +report the error and stop. Do not use npx, a private install, or another agent +as a fallback. Disabled Claude or missing authentication still needs to be fixed. Monitor the background output while the review runs. With the console enabled, do not rely on a background-task completion notification: the server keeps the process alive after review ends. diff --git a/plugins/codejury/plugin.json b/plugins/codejury/plugin.json index 38474f0..478ab64 100644 --- a/plugins/codejury/plugin.json +++ b/plugins/codejury/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "codejury", - "version": "0.1.3", + "version": "0.1.4", "description": "Review pull requests supplied by URL with CodeJury's independent AI reviewers and judge, resume PR reviews, and inspect saved findings.", "author": { "name": "ZHOU ZHENGXI", @@ -22,7 +22,7 @@ "interface": { "displayName": "CodeJury", "shortDescription": "AI pull request review", - "longDescription": "Review pull requests supplied by URL with independent coding agents and a judge that investigates findings, applies fixes, and asks reviewers to check again. Set up the CLI, run or resume a PR review, and inspect saved findings and conversations. A new review requires a PR link; arbitrary local files and folders are not supported plugin review targets. PR reviews use a local checkout. Desktop only: requires Node.js 20.10+, npm, Git, and separately installed and authenticated coding-agent CLIs. GitHub PRs also require GitHub CLI. This plugin uses your installed CodeJury CLI when available on PATH, or downloads CodeJury CLI 0.9.0 from npm as a fallback. Reviews can edit files, create commits, and push fixes to the PR by default; ask to keep fixes local or use --push=false to disable pushing. Code and prompts are processed by your selected agents and their providers, subject to their terms and usage charges. CodeJury keeps review records locally and operates no hosted review service. The included console screenshot shows a real prior review; results vary by repository and agent.", + "longDescription": "Review pull requests supplied by URL with independent coding agents and a judge that investigates findings, applies fixes, and asks reviewers to check again. Set up the CLI, run or resume a PR review, and inspect saved findings and conversations. A new review requires a PR link; arbitrary local files and folders are not supported plugin review targets. PR reviews use a local checkout. Desktop only: requires Node.js 20.10+, npm, Git, and separately installed and authenticated coding-agent CLIs. GitHub PRs also require GitHub CLI. This plugin reuses your npm-global CodeJury CLI, or installs the latest stable CLI globally if missing. It does not create a private CLI installation. Reviews can edit files, create commits, and push fixes to the PR by default; ask to keep fixes local or use --push=false to disable pushing. Code and prompts are processed by your selected agents and their providers, subject to their terms and usage charges. CodeJury keeps review records locally and operates no hosted review service. The included console screenshot shows a real prior review; results vary by repository and agent.", "developerName": "ZHOU ZHENGXI", "category": "Developer Tools", "capabilities": [ @@ -47,7 +47,7 @@ ] }, "publication": { - "release_notes": "Push PR fixes by default, while preserving explicit local-only requests. Reuse an installed CodeJury CLI on PATH before downloading the fallback release." + "release_notes": "Reuse the global CodeJury CLI and install globally when missing. Remove private CLI downloads and stop on installation errors." } } } diff --git a/plugins/codejury/scripts/jury.mjs b/plugins/codejury/scripts/jury.mjs index b353269..f4bdaff 100644 --- a/plugins/codejury/scripts/jury.mjs +++ b/plugins/codejury/scripts/jury.mjs @@ -1,74 +1,43 @@ -// Reuse an installed CodeJury CLI, or download the fallback release. +// Both plugins use npm's global CodeJury installation. Keep the two shipped runners in sync. import { spawnSync } from 'node:child_process'; -import { accessSync, constants, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync } from 'node:fs'; -import { homedir } from 'node:os'; +import { existsSync, readFileSync } from 'node:fs'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; -const version = '0.9.0'; const [major, minor] = process.versions.node.split('.').map(Number); if (major < 20 || (major === 20 && minor < 10)) throw new Error('CodeJury requires Node.js 20.10+.'); -function installedCli() { - for (const directory of (process.env.PATH || '').split(path.delimiter).filter(Boolean)) { - for (const command of ['jury', 'codejury']) { - const executable = path.resolve(directory, command + (process.platform === 'win32' ? '.cmd' : '')); - try { - accessSync(executable, process.platform === 'win32' ? constants.F_OK : constants.X_OK); - const target = realpathSync(executable); - // npm uses symlinks on POSIX and .cmd shims beside node_modules on Windows. - const packageRoot = process.platform === 'win32' - ? path.join(directory, 'node_modules', '@agentsdance', 'codejury') - : path.dirname(path.dirname(target)); - const metadata = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf8')); - const entry = realpathSync(path.join(packageRoot, 'bin', 'jury.js')); - if (metadata.name === '@agentsdance/codejury' && - (process.platform === 'win32' || entry === target)) return entry; - } catch { /* No usable CodeJury package behind this executable. */ } - } +function npm(args, capture = false) { + const result = spawnSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', args, { + encoding: 'utf8', stdio: capture ? ['ignore', 'pipe', 'inherit'] : 'inherit', + shell: process.platform === 'win32', + }); + if (result.error || result.status !== 0) { + throw new Error(`npm ${args.join(' ')} failed (${result.error?.message ?? result.status ?? result.signal}). Fix npm's global installation and retry.`, { cause: result.error }); } - return null; + return capture ? result.stdout.trim() : undefined; } -function cachedCli() { - const cache = path.resolve(process.env.CODEJURY_PLUGIN_CACHE || path.join(homedir(), '.cache', 'codejury', 'plugin-cli')); - const install = path.join(cache, version); - const packageRoot = path.join(install, 'node_modules', '@agentsdance', 'codejury'); - const entry = path.join(packageRoot, 'bin', 'jury.js'); - function installed() { - try { - const metadata = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf8')); - return metadata.name === '@agentsdance/codejury' && metadata.version === version && existsSync(entry); - } catch { return false; } - } - let ready = installed(); - if (!ready && existsSync(install)) { - // A concurrent launcher may have published a complete cache after our miss. - ready = installed(); - if (!ready) throw new Error(`Incomplete CLI cache at ${install}; move it aside and retry.`); - } - if (!ready) { - mkdirSync(cache, { recursive: true }); - const staging = mkdtempSync(path.join(cache, '.install-')); - try { - console.error(`Downloading CodeJury ${version} from npm into ${cache}`); - // cwd is the staging directory; keep cache paths out of the Windows shell command. - const result = spawnSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', [ - 'install', '--prefix', '.', '--no-save', '--package-lock=false', - '--ignore-scripts', '--no-audit', '--no-fund', '--registry=https://registry.npmjs.org', - `@agentsdance/codejury@${version}`, - ], { cwd: staging, stdio: 'inherit', shell: process.platform === 'win32' }); - if (result.error) throw result.error; - if (result.status !== 0) throw new Error(`npm installation failed (${result.status ?? result.signal}).`); - try { renameSync(staging, install); } - catch (error) { if (!installed()) throw error; } // Another concurrent invocation may have installed it. - } finally { rmSync(staging, { recursive: true, force: true }); } - } - if (!installed()) throw new Error(`Expected CodeJury ${version} at ${install}.`); - return entry; +const globalRoot = npm(['root', '--global'], true); +if (!path.isAbsolute(globalRoot)) throw new Error('npm did not return an absolute global package directory.'); +const packageRoot = path.join(globalRoot, '@agentsdance', 'codejury'); +const entry = path.join(packageRoot, 'bin', 'jury.js'); +function installed() { + try { + const metadata = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf8')); + return metadata.name === '@agentsdance/codejury' && existsSync(entry) ? metadata : null; + } catch { return null; } +} + +let metadata = installed(); +if (!metadata) { + console.error('Installing CodeJury globally with npm.'); + npm(['install', '--global', '--ignore-scripts', '--no-audit', '--no-fund', '@agentsdance/codejury@latest']); + metadata = installed(); } +if (!metadata) throw new Error(`Global CodeJury installation is incomplete at ${packageRoot}. Run npm install --global @agentsdance/codejury@latest and retry.`); +console.error(`Using global CodeJury ${metadata.version} at ${entry}`); -const entry = installedCli() || cachedCli(); // Import in this process so cancellation reaches the CLI and its agent cleanup. process.argv = [process.execPath, entry, ...process.argv.slice(2)]; await import(pathToFileURL(entry).href); diff --git a/plugins/codejury/skills/codejury-results/SKILL.md b/plugins/codejury/skills/codejury-results/SKILL.md index 133c565..20c096d 100644 --- a/plugins/codejury/skills/codejury-results/SKILL.md +++ b/plugins/codejury/skills/codejury-results/SKILL.md @@ -7,9 +7,10 @@ description: List, inspect, or summarize saved CodeJury reviews and findings, or Resolve `` as two directories above this skill folder. Use `node "/scripts/jury.mjs"` followed by command arguments. This uses an -installed CodeJury CLI on PATH first, or downloads the fallback release into -`~/.cache/codejury/plugin-cli/` if none is found. Respect an explicitly requested -CLI version and check its help for supported flags. +existing npm-global CodeJury installation, or installs globally if missing. +Installation errors stop the operation; do not use npx or a private CLI copy. +Respect an explicitly requested CLI version, install it globally if needed, +and check its help for supported flags. 1. Identify the original state root. URL reviews default to `~/.jury`; use `--dir ""` to select it explicitly. A local-checkout review stores `runs/` diff --git a/plugins/codejury/skills/codejury-review/SKILL.md b/plugins/codejury/skills/codejury-review/SKILL.md index ba35ecf..7ff0932 100644 --- a/plugins/codejury/skills/codejury-review/SKILL.md +++ b/plugins/codejury/skills/codejury-review/SKILL.md @@ -23,10 +23,11 @@ original PR link from the saved run or conversation. Resolve `` as two directories above this skill folder. Use `node "/scripts/jury.mjs"` followed by the command and its arguments. -The launcher first uses an installed CodeJury CLI available as `jury` or `codejury` -on PATH. If none is found, it downloads the fallback release into -`~/.cache/codejury/plugin-cli/`. Run `version` to report the selected CLI version. -If the user requests a different version, use that version and inspect its help. +The launcher checks npm's global CodeJury installation and reuses it. If missing, +it installs the latest stable release globally. Run `version` to report the CLI +version and absolute path. Installation errors stop the run; do not use npx or a +private CLI copy. If the user requests a different version, install that version +globally and inspect its help. Check Node.js 20.10+, npm, Git, and the requested agent CLIs. For GitHub PRs, check GitHub CLI authentication. Run `jury agents` to discover executables; diff --git a/plugins/codejury/skills/codejury-setup/SKILL.md b/plugins/codejury/skills/codejury-setup/SKILL.md index efc7414..44f127d 100644 --- a/plugins/codejury/skills/codejury-setup/SKILL.md +++ b/plugins/codejury/skills/codejury-setup/SKILL.md @@ -24,12 +24,12 @@ selected, without starting a review. node "/scripts/jury.mjs" agents ``` - The launcher first checks for an installed CodeJury CLI available as `jury` - or `codejury` on PATH and uses it. Otherwise, it downloads the fallback - release into `~/.cache/codejury/plugin-cli/`. Report the selected CLI version. - It does not replace or upgrade the user's global installation. - An explicitly requested CLI version takes precedence; check its help before - using version-dependent options. + The launcher checks `npm root --global` for CodeJury and reuses that global + installation. If missing, it runs `npm install --global @agentsdance/codejury@latest`. + It reports the version and absolute CLI path. Installation errors stop setup; + never use npx, a custom prefix, or a plugin-private CLI copy as a fallback. + Existing versions are not upgraded automatically. For a requested version, + install that version globally and check its help before using its options. 3. `jury agents` checks executable availability, not authentication or quota. Report these separately. Consult the CLI's built-in install hints for missing agents. Let the user complete provider sign-in; do not collect credentials diff --git a/test/codex-plugin.test.js b/test/codex-plugin.test.js index 0dddf7d..d15d1b3 100644 --- a/test/codex-plugin.test.js +++ b/test/codex-plugin.test.js @@ -1,191 +1,113 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; -import { existsSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync, realpathSync, symlinkSync } from 'node:fs'; +import { existsSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync, realpathSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; -const launcher = fileURLToPath(new URL('../plugins/codejury/scripts/jury.mjs', import.meta.url)); +const launchers = ['plugins/codejury/scripts/jury.mjs', 'plugin/scripts/jury.mjs']; -for (const windows of process.platform === 'win32' ? [true] : [false, true]) { - test(`plugin launcher prefers an installed CodeJury ${windows ? 'Windows shim' : 'POSIX symlink'} over downloading`, () => { - const root = realpathSync(mkdtempSync(path.join(tmpdir(), 'codejury-global-'))); - try { - const bin = path.join(root, 'global install with spaces'); - const pkg = path.join(bin, 'node_modules', '@agentsdance', 'codejury'); - const entry = path.join(pkg, 'bin', 'jury.js'); - const cache = path.join(root, 'unused-cache'); - const cwd = path.join(root, 'repository with spaces'); - mkdirSync(path.dirname(entry), { recursive: true }); - mkdirSync(cwd); - writeFileSync(path.join(pkg, 'package.json'), JSON.stringify({ name: '@agentsdance/codejury', version: '0.8.2', type: 'module' })); - writeFileSync(entry, `#!${process.execPath}\nconsole.log(JSON.stringify({ cwd: process.cwd(), argv: process.argv.slice(2), version: '0.8.2' })); process.exitCode = 7;`, { mode: 0o755 }); - const env = { ...process.env, PATH: bin, CODEJURY_PLUGIN_CACHE: cache }; - const nodeArgs = []; - if (windows && process.platform !== 'win32') { - const preload = path.join(root, 'windows.mjs'); - writeFileSync(preload, "Object.defineProperty(process, 'platform', { value: 'win32' });"); - nodeArgs.push('--import', pathToFileURL(preload).href); +function fixture(t, { installed = false, windows = process.platform === 'win32', mode = 'ok' } = {}) { + const root = realpathSync(mkdtempSync(path.join(tmpdir(), 'codejury-global-'))); + t.after(() => rmSync(root, { recursive: true, force: true })); + const bin = path.join(root, 'bin'); + const globalRoot = path.join(root, 'global install with spaces', 'node_modules'); + const pkg = path.join(globalRoot, '@agentsdance', 'codejury'); + const entry = path.join(pkg, 'bin', 'jury.js'); + const cwd = path.join(root, 'repository with spaces'); + const cache = path.join(root, 'old-plugin-cache'); + const calls = path.join(root, 'npm-calls.jsonl'); + mkdirSync(bin); + mkdirSync(cwd); + // A project-local CLI must never shadow the global installation. + writeFileSync(path.join(bin, windows ? 'jury.cmd' : 'jury'), windows ? '@exit /b 99\r\n' : '#!/bin/sh\nexit 99\n', { mode: 0o755 }); + const cli = `console.log(JSON.stringify({ cwd: process.cwd(), argv: process.argv.slice(2), entry: process.argv[1] })); process.exitCode = 7;`; + const metadata = JSON.stringify({ name: '@agentsdance/codejury', version: '0.9.0', type: 'module' }); + if (installed) { + mkdirSync(path.dirname(entry), { recursive: true }); + writeFileSync(path.join(pkg, 'package.json'), metadata); + writeFileSync(entry, cli); + } + const npm = path.join(bin, 'npm.cjs'); + writeFileSync(npm, `#!${process.execPath} + const fs = require('node:fs'), path = require('node:path'); + const args = process.argv.slice(2); + fs.appendFileSync(${JSON.stringify(calls)}, JSON.stringify(args) + '\\n'); + if (args[0] === 'root' && args[1] === '--global') { + if (${JSON.stringify(mode)} === 'root-fails') process.exit(13); + console.log(${JSON.stringify(globalRoot)}); + } else if (args[0] === 'install') { + if (!args.includes('--global') || args.includes('--prefix')) throw new Error('installation must use the configured global prefix'); + if (!args.includes('@agentsdance/codejury@latest')) throw new Error('expected the latest stable global CLI'); + if (${JSON.stringify(mode)} === 'install-fails') { console.error('EACCES: global prefix is not writable'); process.exit(13); } + if (${JSON.stringify(mode)} !== 'empty-install') { + fs.mkdirSync(${JSON.stringify(path.dirname(entry))}, { recursive: true }); + fs.writeFileSync(${JSON.stringify(path.join(pkg, 'package.json'))}, ${JSON.stringify(metadata)}); + fs.writeFileSync(${JSON.stringify(entry)}, ${JSON.stringify(cli)}); } - for (const command of ['jury', 'codejury']) { - const executable = path.join(bin, command + (windows ? '.cmd' : '')); - // Resolve npm's JS entrypoint without executing a Windows shell shim. - if (windows) writeFileSync(executable, '@exit /b 99\r\n'); - else symlinkSync(entry, executable); - for (const push of ['--push=true', '--push=false']) { - const args = ['review', 'https://github.com/owner/repo/pull/1', push, '--summary', 'literal $(touch unwanted) `echo bad` ; & "text"']; - const result = spawnSync(process.execPath, [...nodeArgs, launcher, ...args], { cwd, env, encoding: 'utf8' }); - assert.equal(result.status, 7, result.stderr); - assert.deepEqual(JSON.parse(result.stdout), { cwd, argv: args, version: '0.8.2' }); - assert.equal(existsSync(cache), false, 'global CLI must run before creating or installing a fallback cache'); - assert.equal(existsSync(path.join(cwd, 'unwanted')), false); - } - rmSync(executable); - } - } finally { rmSync(root, { recursive: true, force: true }); } - }); -} - -test('plugin launcher installs into Windows cache paths containing spaces', () => { - const root = realpathSync(mkdtempSync(path.join(tmpdir(), 'codejury-plugin-'))); - try { - const cache = path.join(root, 'Jane Doe', '.cache', 'codejury', 'plugin-cli'); - const cwd = path.join(root, 'repository with spaces'); - const bin = path.join(root, 'bin'); - mkdirSync(cwd); - mkdirSync(bin); - const fixture = path.join(bin, 'npm.cjs'); - writeFileSync(fixture, `#!${process.execPath} - const assert = require('node:assert/strict'); - const fs = require('node:fs'); - const path = require('node:path'); - const args = process.argv.slice(2); - assert.equal(args[0], 'install'); - assert.equal(args[1], '--prefix'); - assert.equal(path.resolve(args[2]), process.cwd(), 'npm received a split cache path: ' + JSON.stringify(args)); - assert.deepEqual(args.slice(3), ['--no-save', '--package-lock=false', '--ignore-scripts', '--no-audit', '--no-fund', '--registry=https://registry.npmjs.org', '@agentsdance/codejury@0.9.0']); - const pkg = path.join(path.resolve(args[2]), 'node_modules', '@agentsdance', 'codejury'); - fs.mkdirSync(path.join(pkg, 'bin'), { recursive: true }); - fs.writeFileSync(path.join(pkg, 'package.json'), JSON.stringify({ name: '@agentsdance/codejury', version: '0.9.0', type: 'module' })); - fs.writeFileSync(path.join(pkg, 'bin', 'jury.js'), 'console.log(JSON.stringify({ cwd: process.cwd(), argv: process.argv.slice(2) }));'); - `); + } else throw new Error('unexpected npm command: ' + JSON.stringify(args)); + `, { mode: 0o755 }); + if (process.platform === 'win32') { + writeFileSync(path.join(bin, 'npm.cmd'), `@"${process.execPath}" "${npm}" %*\r\n`); + } else { + writeFileSync(path.join(bin, windows ? 'npm.cmd' : 'npm'), readFileSync(npm), { mode: 0o755 }); + } + const nodeArgs = []; + if (windows && process.platform !== 'win32') { const preload = path.join(root, 'windows.mjs'); - const env = { ...process.env, CODEJURY_PLUGIN_CACHE: cache, PATH: bin }; - const nodeArgs = []; - if (process.platform === 'win32') { - writeFileSync(path.join(bin, 'npm.cmd'), `@"${process.execPath}" "${fixture}" %*\r\n`); - } else { - writeFileSync(path.join(bin, 'npm.cmd'), readFileSync(fixture), { mode: 0o755 }); - // Exercise the Windows launcher branch with a real shell on other hosts. - // Both shells split an unquoted prefix at spaces; Windows uses cmd.exe above. - writeFileSync(preload, "Object.defineProperty(process, 'platform', { value: 'win32' });\nprocess.env.comspec = '/bin/sh';\n"); - nodeArgs.push('--import', pathToFileURL(preload).href); - } - const result = spawnSync(process.execPath, [...nodeArgs, launcher, 'version'], { cwd, env, encoding: 'utf8' }); - assert.equal(result.status, 0, result.stderr); - assert.deepEqual(JSON.parse(result.stdout), { cwd: realpathSync(cwd), argv: ['version'] }); - const metadata = JSON.parse(readFileSync(path.join(cache, '0.9.0', 'node_modules', '@agentsdance', 'codejury', 'package.json'), 'utf8')); - assert.equal(metadata.version, '0.9.0'); - } finally { rmSync(root, { recursive: true, force: true }); } -}); + writeFileSync(preload, "Object.defineProperty(process, 'platform', { value: 'win32' }); process.env.comspec = '/bin/sh';\n"); + nodeArgs.push('--import', pathToFileURL(preload).href); + } + return { + entry, cwd, cache, bin, + calls: () => existsSync(calls) ? readFileSync(calls, 'utf8').trim().split('\n').map(line => JSON.parse(line)) : [], + run: (launcher, args = ['version']) => spawnSync(process.execPath, [...nodeArgs, fileURLToPath(new URL('../' + launcher, import.meta.url)), ...args], { + cwd, env: { ...process.env, PATH: bin, CODEJURY_PLUGIN_CACHE: cache }, encoding: 'utf8', timeout: 10000, + }), + }; +} -test('plugin launcher falls back when jury on PATH is not a CodeJury installation', () => { - const root = mkdtempSync(path.join(tmpdir(), 'codejury-plugin-')); - try { - const pkg = path.join(root, 'cache', '0.9.0', 'node_modules', '@agentsdance', 'codejury'); - const cwd = path.join(root, 'repository with spaces'); - const shadow = path.join(root, 'bin'); - mkdirSync(path.join(pkg, 'bin'), { recursive: true }); - mkdirSync(cwd); - mkdirSync(shadow); - writeFileSync(path.join(pkg, 'package.json'), JSON.stringify({ name: '@agentsdance/codejury', version: '0.9.0', type: 'module' })); - writeFileSync(path.join(pkg, 'bin', 'jury.js'), 'console.log(JSON.stringify({ cwd: process.cwd(), argv: process.argv.slice(2) })); process.exitCode = 7;'); - writeFileSync(path.join(shadow, 'jury'), '#!/bin/sh\nexit 99\n', { mode: 0o755 }); - const args = ['review', '--summary', 'literal $(touch unwanted) `echo bad` ; & "text"']; - const result = spawnSync(process.execPath, [launcher, ...args], { - cwd, env: { ...process.env, CODEJURY_PLUGIN_CACHE: path.join(root, 'cache'), PATH: shadow }, encoding: 'utf8', +for (const launcher of launchers) { + for (const windows of process.platform === 'win32' ? [true] : [false, true]) { + const label = `${launcher} (${windows ? 'Windows' : 'POSIX'})`; + test(`${label} reuses the global CLI even when PATH contains another jury`, t => { + const f = fixture(t, { installed: true, windows }); + const args = ['review', 'https://github.com/owner/repo/pull/1', '--push=false', '--summary', 'literal $(touch unwanted) `echo bad` ; & "text"']; + const result = f.run(launcher, args); + assert.equal(result.status, 7, result.stderr); + assert.deepEqual(JSON.parse(result.stdout), { cwd: f.cwd, argv: args, entry: f.entry }); + assert.deepEqual(f.calls(), [['root', '--global']], 'an existing global CLI must not be reinstalled or upgraded'); + assert.equal(existsSync(f.cache), false); + assert.equal(existsSync(path.join(f.cwd, 'unwanted')), false); }); - assert.equal(result.status, 7, result.stderr); - assert.deepEqual(JSON.parse(result.stdout), { cwd: realpathSync(cwd), argv: args }); - } finally { rmSync(root, { recursive: true, force: true }); } -}); -test('plugin launcher rejects an incomplete cache without replacing user files', () => { - const root = mkdtempSync(path.join(tmpdir(), 'codejury-plugin-')); - try { - const install = path.join(root, '0.9.0'); - mkdirSync(install); - writeFileSync(path.join(install, 'keep.txt'), 'preserve'); - const result = spawnSync(process.execPath, [launcher, 'version'], { - env: { ...process.env, CODEJURY_PLUGIN_CACHE: root, PATH: root }, encoding: 'utf8', + test(`${label} installs globally once, then reuses that installation`, t => { + const f = fixture(t, { windows }); + for (let attempt = 0; attempt < 2; attempt++) { + const result = f.run(launcher); + assert.equal(result.status, 7, result.stderr); + assert.deepEqual(JSON.parse(result.stdout), { cwd: f.cwd, argv: ['version'], entry: f.entry }); + } + const installs = f.calls().filter(args => args[0] === 'install'); + assert.equal(installs.length, 1); + assert.ok(installs[0].includes('--global')); + assert.equal(installs[0].includes('--prefix'), false); + assert.equal(existsSync(f.cache), false); + assert.equal(existsSync(path.join(f.cwd, 'node_modules')), false); }); - assert.notEqual(result.status, 0); - assert.match(result.stderr, /Incomplete CLI cache/); - assert.equal(readFileSync(path.join(install, 'keep.txt'), 'utf8'), 'preserve'); - } finally { rmSync(root, { recursive: true, force: true }); } -}); + } -test('plugin launcher reuses a cache published after its initial miss', () => { - const root = mkdtempSync(path.join(tmpdir(), 'codejury-cache-race-')); - try { - const cache = path.join(root, 'cache'); - const bin = path.join(root, 'bin'); - const calls = path.join(root, 'npm-calls'); - const peerResult = path.join(root, 'peer.json'); - const metadata = path.join(cache, '0.9.0', 'node_modules', '@agentsdance', 'codejury', 'package.json'); - mkdirSync(bin); - const fixture = path.join(bin, 'npm.cjs'); - writeFileSync(fixture, `#!${process.execPath} - const fs = require('node:fs'), path = require('node:path'); - fs.appendFileSync(${JSON.stringify(calls)}, 'install\\n'); - const pkg = path.join(process.cwd(), 'node_modules', '@agentsdance', 'codejury'); - fs.mkdirSync(path.join(pkg, 'bin'), { recursive: true }); - fs.writeFileSync(path.join(pkg, 'package.json'), JSON.stringify({ name: '@agentsdance/codejury', version: '0.9.0', type: 'module' })); - fs.writeFileSync(path.join(pkg, 'bin', 'jury.js'), 'console.log("cached fixture CLI");'); - `); - if (process.platform === 'win32') { - writeFileSync(path.join(bin, 'npm.cmd'), `@"${process.execPath}" "${fixture}" %*\r\n`); - } else { - writeFileSync(path.join(bin, 'npm'), readFileSync(fixture), { mode: 0o755 }); - } - const preload = path.join(root, 'interleave.mjs'); - // Finish a second real launcher between the first launcher's failed read - // and its directory check. This reproduces the interleaving without sleeps. - writeFileSync(preload, ` - import fs from 'node:fs'; - import { spawnSync } from 'node:child_process'; - import { syncBuiltinESMExports } from 'node:module'; - const original = fs.readFileSync; - let fired = false; - fs.readFileSync = function(file, ...args) { - try { return original.call(this, file, ...args); } - catch (error) { - if (!fired && file === ${JSON.stringify(metadata)} && error.code === 'ENOENT') { - fired = true; - const result = spawnSync(process.execPath, [${JSON.stringify(launcher)}, 'version'], { - env: process.env, encoding: 'utf8', timeout: 10000, - }); - fs.writeFileSync(${JSON.stringify(peerResult)}, JSON.stringify({ status: result.status, stdout: result.stdout, stderr: result.stderr })); - if (result.error) throw result.error; - if (result.status !== 0) throw new Error('peer launcher failed'); - } - throw error; - } - }; - syncBuiltinESMExports(); - `); - const result = spawnSync(process.execPath, ['--import', pathToFileURL(preload).href, launcher, 'version'], { - env: { ...process.env, CODEJURY_PLUGIN_CACHE: cache, PATH: bin }, - encoding: 'utf8', timeout: 20000, + for (const mode of ['root-fails', 'install-fails', 'empty-install']) { + test(`${launcher} stops on ${mode} without falling back to a private cache`, t => { + const f = fixture(t, { mode }); + const result = f.run(launcher); + assert.notEqual(result.status, 0); + assert.notEqual(result.status, 7, 'the CLI must not be invoked'); + assert.match(result.stderr, /global/i); + assert.equal(f.calls().filter(args => args[0] === 'install').length, mode === 'root-fails' ? 0 : 1); + assert.equal(existsSync(f.cache), false); + assert.equal(existsSync(path.join(f.cwd, 'node_modules')), false); }); - assert.ifError(result.error); - const peer = JSON.parse(readFileSync(peerResult, 'utf8')); - assert.equal(peer.status, 0, peer.stderr); - assert.equal(peer.stdout.trim(), 'cached fixture CLI'); - assert.equal(result.status, 0, result.stderr); - assert.equal(result.stdout.trim(), 'cached fixture CLI'); - assert.equal(readFileSync(calls, 'utf8'), 'install\n', 'the completed cache must be reused without another installation'); - assert.equal(JSON.parse(readFileSync(metadata, 'utf8')).version, '0.9.0'); - } finally { rmSync(root, { recursive: true, force: true }); } -}); + } +} diff --git a/test/plugin.test.js b/test/plugin.test.js index e002d0f..d6c826c 100644 --- a/test/plugin.test.js +++ b/test/plugin.test.js @@ -42,10 +42,11 @@ test("every plugin skill has a name and description, and the review skill uses r assert.match(review, /^disable-model-invocation: true$/m); const help = execFileSync(process.execPath, [path.join(root, "bin/jury.js"), "help", "review"], { encoding: "utf8" }); for (const flag of review.match(/--[a-z][\w-]*/g)) { - if (flag === "--package") continue; // npm's repository-CLI fallback selector + if (flag === "--global") continue; // npm's global CLI installation // Reviewer aliases include a supported legacy option omitted from public help. assert.ok(help.includes(`${flag} `) || Object.hasOwn(reviewerOptions, flag.slice(2)), `jury review has no ${flag}`); } assert.equal(json("package.json").name, "@agentsdance/codejury"); - assert.match(review, /npx -y @agentsdance\/codejury/); + assert.match(review, /npm install --global @agentsdance\/codejury@latest/); + assert.ok(existsSync(path.join(root, "plugin/scripts/jury.mjs"))); });