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.md b/README.md index 304591e..72e8a37 100644 --- a/README.md +++ b/README.md @@ -134,25 +134,24 @@ 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: +Install from your **terminal** and start a new Claude Code session: ```sh claude plugin marketplace add agentsdance/codejury claude plugin install codejury@codejury +claude ``` -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. -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. +At the **Claude Code prompt**, enter your PR URL: + +```text +/codejury:review https://github.com/owner/repo/pull/123 +``` + +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/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 8c04597..210482c 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -1,85 +1,50 @@ # 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. +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. The plugin checks your global `jury` installation +and installs it globally if missing. ## Install +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 ``` -Start a new Claude Code session after installing, then run: +## Review + +At the **Claude Code prompt**, enter this with your PR URL: ```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. +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. -## Update +**Pushing is off by default.** The judge can still edit and commit. Single-PR +checkouts are temporary; unpushed fixes are removed during cleanup. -```sh -claude plugin marketplace update codejury -claude plugin update codejury@codejury -``` +Optional flags to add to the review command: -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. +- `--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. -## Develop and validate +## Update -From the repository root: +Run in your terminal, then start a new Claude Code session: ```sh -claude plugin validate ./plugin -claude plugin validate . -node --test test/plugin.test.js -claude --plugin-dir ./plugin +claude plugin marketplace update codejury +claude plugin update codejury@codejury ``` -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`. - -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. +For saved results, resuming, and troubleshooting, see the +[CLI usage guide](https://github.com/agentsdance/codejury/blob/master/docs/usage.md). 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"))); });