diff --git a/CHANGELOG.md b/CHANGELOG.md index f8c9e7b..674aa7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- 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, reclaim them after completion, and add `jury cache clean` for stopped runs (#122). - Retain Windows agent caches when descendant termination cannot be confirmed, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5a92b25..4275ffd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,11 +65,13 @@ These tests use saved review data; they do not call live model providers. The Node/OS matrix runs the same `npm test` and `npm run test:package` commands through `node scripts/ci-check.mjs test` and `node scripts/ci-check.mjs test:package`. -Each phase has a three-minute watchdog, within the existing fifteen-minute job -limit. A timeout remains a failed check and terminates that phase's process group. +The source suite has a three-minute watchdog; package validation has five minutes +for packing, installation, and subprocess-heavy installed CLI regressions on slower +runners. Both remain within the fifteen-minute job limit. A timeout remains a +failed check and terminates that phase's process group. Tests and their parallelism are unchanged. -Package validation prints named subprocess start/end markers. The independent +Package validation streams test results and prints named subprocess start/end markers. The independent watchdog streams Node process lifecycle/resource records and snapshots of its own process group and descendants, without command arguments or environment values. CI uploads these records as `test-diagnostics-node-` artifacts for diff --git a/README.md b/README.md index a263b1e..f9b69c2 100644 --- a/README.md +++ b/README.md @@ -126,9 +126,9 @@ Start a new Codex chat, select **CodeJury**, and ask: > Review https://github.com/OWNER/REPO/pull/123 -Use your PR link. Reviews run locally and can make fixes; changes stay local unless -you ask to push. See [setup and usage](docs/codex-plugin.md) for prerequisites, -agent selection, and saved reviews. +Use your PR link. Reviews run locally and push fixes to the PR by default. Ask to +keep fixes local to disable pushing. See [setup and usage](docs/codex-plugin.md) +for prerequisites, agent selection, and saved reviews. ## Claude Code plugin diff --git a/bin/jury.js b/bin/jury.js index b75ef4f..aca4fff 100755 --- a/bin/jury.js +++ b/bin/jury.js @@ -538,8 +538,10 @@ async function cmdAgent(argv) { const language = normalizeLanguage(requestedLanguage ?? languageIn(priorEvents)); if (priorTargets?.targets && urls.length < 2) throw new Error("resuming a related-PR task requires all original PR URLs"); if (urls.length > 1 && values.resume && !priorTargets?.targets) throw new Error("no related-PR task found to resume"); - if (priorTargets?.targets && !argv.some(a => a === "--push" || a.startsWith("--push="))) { - values.push = priorTargets.pushEnabled ?? false; + // Older single-PR runs did not save this preference. Keep their fixes local + // unless the caller explicitly enables pushing on resume. + if (values.resume && !argv.some(a => a === "--push" || a.startsWith("--push="))) { + values.push = priorTargets?.pushEnabled ?? false; } const group = urls.length > 1 ? await prepareGroup(urls, { root: requestedWorktree, dir: resolveFrom, allowPush: values.push, trunk: values.trunk, previous: priorTargets, @@ -604,12 +606,13 @@ async function cmdAgent(argv) { title: values.title ?? pr.title ?? git.subject, branch: resolved?.branch ?? git.branch, trunk, + pushEnabled: values.push, state: "review", stateNote: "", ...(language ? { language } : {}), }; if (group) Object.assign(target, { - targets: group.targets, workspace: group.worktree, pushEnabled: values.push, repo: "related-prs", + targets: group.targets, workspace: group.worktree, repo: "related-prs", reviewedCommit: null, reviewedCommits: null, id: groupId(urls), url: "", branch: "", summary: values.summary ?? "", }); diff --git a/docs/codex-plugin.md b/docs/codex-plugin.md index 5281207..e0ee5ea 100644 --- a/docs/codex-plugin.md +++ b/docs/codex-plugin.md @@ -14,8 +14,9 @@ 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 installs its -CodeJury CLI automatically on first use; agent installation and login are separate. +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. ## Review a PR @@ -26,9 +27,10 @@ Provide a PR link when starting a review. For example: Replace the example URL with your PR and choose agents you have installed. The plugin shares a live browser console link so you can follow the review. -The judge can edit files and create commits. Fixes remain in a persistent local -checkout unless you ask to push them. Local files or folders alone are not -supported review targets for this plugin. +The judge can edit files, create commits, and push fixes to the PR's source branch. +Pushing is enabled by default (`--push=true`). Ask to keep fixes local, or specify +`--push=false`, to retain them in a persistent local checkout instead. Local files +or folders alone are not supported review targets for this plugin. To continue or inspect a saved review, ask: @@ -39,8 +41,8 @@ To continue or inspect a saved review, ask: ## Development and packaging The plugin source is in [`plugins/codejury`](../plugins/codejury). Its launcher -automatically downloads the pinned CLI release from npm, independently of any -globally installed `jury`. The plugin and CLI have separate versions. +checks for an installed `jury` or `codejury` on PATH before downloading its fallback +CLI release from npm. The plugin and CLI have separate versions. Build the submission archive from the repository root: diff --git a/docs/usage.md b/docs/usage.md index 3ba0644..fd0d43f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -53,8 +53,10 @@ jury review --resume --push=false ``` Repeat the same `--dir` root if you used one. Keep related-PR URLs in the same order. -Related-PR resume retains its previous push setting unless explicitly overridden; -`--push=true` publishes retained fixes with fast-forward pushes. If a remote branch changes +All reviews save their push setting and retain it on resume unless explicitly overridden. +Older runs without a saved setting resume with pushing disabled; pass `--push=true` +to enable it explicitly. For related PRs, `--push=true` publishes retained fixes +with fast-forward pushes. If a remote branch changes independently, start a new task rather than resuming obsolete heads. `--rounds 3` caps review rounds; it is not a monetary budget. Each round may involve multiple diff --git a/plugins/codejury/plugin.json b/plugins/codejury/plugin.json index 6da43ec..38474f0 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.2", + "version": "0.1.3", "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 CodeJury CLI 0.9.0, downloaded from npm on first use. Reviews can edit files and create commits; pushes are disabled unless requested. 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 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.", "developerName": "ZHOU ZHENGXI", "category": "Developer Tools", "capabilities": [ @@ -36,7 +36,7 @@ "termsOfServiceURL": "https://codejury.ai-infra.org/terms/", "defaultPrompt": [ "Set up CodeJury and check which coding agents I can use.", - "Review this PR link with CodeJury and keep fixes local: ", + "Review this PR link with CodeJury: ", "Summarize the findings from my latest saved CodeJury review." ], "brandColor": "#315BDC", @@ -47,7 +47,7 @@ ] }, "publication": { - "release_notes": "Use stable CodeJury CLI 0.9.0 with token usage reporting, separate sessions for same-agent reviewer and judge roles, and fixes for review readiness, saved runs, replies, and console startup. Reuse a complete CLI cache installed by a concurrent launcher. New reviews require PR links." + "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." } } } diff --git a/plugins/codejury/scripts/jury.mjs b/plugins/codejury/scripts/jury.mjs index ea26313..b353269 100644 --- a/plugins/codejury/scripts/jury.mjs +++ b/plugins/codejury/scripts/jury.mjs @@ -1,6 +1,6 @@ -// Invoke the exact released CLI, independent of local/global jury executables. +// Reuse an installed CodeJury CLI, or download the fallback release. import { spawnSync } from 'node:child_process'; -import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from 'node:fs'; +import { accessSync, constants, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync } from 'node:fs'; import { homedir } from 'node:os'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; @@ -8,40 +8,67 @@ 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+.'); -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.`); + +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. */ } + } + } + return null; } -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 }); } + +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; } -if (!installed()) throw new Error(`Expected CodeJury ${version} at ${install}.`); + +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 3e73d83..133c565 100644 --- a/plugins/codejury/skills/codejury-results/SKILL.md +++ b/plugins/codejury/skills/codejury-results/SKILL.md @@ -6,10 +6,10 @@ description: List, inspect, or summarize saved CodeJury reviews and findings, or # Inspect saved CodeJury reviews Resolve `` as two directories above this skill folder. Use -`node "/scripts/jury.mjs"` followed by command arguments. This invokes -the exact published stable release, downloading it into -`~/.cache/codejury/plugin-cli/` on first use. Respect an explicitly requested -CLI version. +`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. 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 35888c2..ba35ecf 100644 --- a/plugins/codejury/skills/codejury-review/SKILL.md +++ b/plugins/codejury/skills/codejury-review/SKILL.md @@ -23,10 +23,10 @@ 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 downloads the exact published stable release into -`~/.cache/codejury/plugin-cli/` on first use and invokes its absolute entrypoint, -so a different local/global `jury` cannot shadow it. If the user requests a different -version, use that version and inspect its help. +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. 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; @@ -40,7 +40,8 @@ launching agents. Read `jury help review` for flags rather than inventing them. `--push=false` promise; explain the mismatch and clarify the workflow. - Respect specified judges, juries, models, round limits, and repository config. Do not silently save global defaults or add reviewers the user did not request. -- Use `--push=false` unless pushing the resulting fixes is authorized. For an +- Use `--push=true` by default. If the user asks to keep fixes local or disables + pushes, pass `--push=false` and use the local-only workflow below. For an interactive desktop review, keep the default live console enabled, share its printed URL as a clickable link as soon as it starts, and retain the managed process. Use `--web=false` for a headless workflow that should exit when the @@ -50,8 +51,15 @@ launching agents. Read `jury help review` for flags rather than inventing them. ## Preserve fixes and state -**Single PR, local-only fixes:** create a persistent clone or worktree of the -PR's source branch, or use an existing matching clean checkout. Discover the +**PR URL (default):** pass the PR URL with `--push=true` to review, fix, commit, +and push to the PR's source branch. Never force-push to work around a rejected push. + +```sh +node "/scripts/jury.mjs" review --push=true +``` + +**Single PR, local-only fixes when requested:** create a persistent clone or +worktree of the PR's source branch, or use an existing matching clean checkout. Discover the head and base from the supplied PR's metadata and verify that the checkout matches that PR. This checkout only retains fixes for the URL-selected PR; it is not a separate local-file review mode. Leave unrelated working changes @@ -68,26 +76,26 @@ title or summary so the saved review remains traceable. A single-PR URL review r including unpushed commits; adding `--dir` to a URL only changes the state root and does not prevent this cleanup. -**PR URL with authorized pushes:** the URL workflow is suitable. Pass -`--push=true` explicitly when requested. Never force-push to work around a -rejected push. - **Related PRs:** pass the URLs together in the original order, omit `--trunk` so each PR uses its own base, and keep an explicit state root when useful. Related-PR workspaces retain local commits. Preserve the URLs, state root, and printed run slug for resume. **Resume:** use the same target arguments and state root plus `--resume `. -Carry the user's push preference explicitly; related-PR resumes can inherit -the prior push setting. If remote heads have changed, inspect the conflict -before starting a new review instead of publishing obsolete local fixes. +Pass the user's push preference explicitly. Preserve a previous local-only +request unless the user changes it; use the saved push setting when resuming +without a new preference, and default to `--push=false` if no setting is recorded. +Older single-PR runs may omit the setting, so only enable pushing when the user +explicitly requests it for that resume. +If remote heads have changed, inspect the conflict before starting a new review +instead of publishing obsolete local fixes. ## Execute and report Run through the host's managed long-running process facility and retain the -process handle and output. Give progress updates while agents run. A reviewer -has a fixed ten-minute execution deadline; the whole multi-round review can -take longer. Do not launch duplicate reviews merely because output is quiet. +process handle and output. Give progress updates while agents run. The whole +multi-round review can take several minutes. Do not launch duplicate reviews +merely because output is quiet. With the live console enabled, the process stays up after review completion; inspect the completion message and saved run status instead of waiting for the server process to exit. Keep the dashboard available while the user needs it. diff --git a/plugins/codejury/skills/codejury-setup/SKILL.md b/plugins/codejury/skills/codejury-setup/SKILL.md index d1cdf78..efc7414 100644 --- a/plugins/codejury/skills/codejury-setup/SKILL.md +++ b/plugins/codejury/skills/codejury-setup/SKILL.md @@ -17,16 +17,17 @@ selected, without starting a review. Identify missing prerequisites before attempting a review. Help install or authenticate them only within the user's requested setup scope. 2. Resolve `` as two directories above this skill folder. Use this - pinned runner for plugin commands: + runner for plugin commands: ```sh node "/scripts/jury.mjs" version node "/scripts/jury.mjs" agents ``` - First use downloads the exact published stable release into - `~/.cache/codejury/plugin-cli/` and invokes its absolute entrypoint. It - does not require a global installation or change the user's installed CLI. + 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. 3. `jury agents` checks executable availability, not authentication or quota. @@ -42,9 +43,11 @@ selected, without starting a review. subsequent review rather than defaulting to local files. Do not start agents just to validate setup. -A CodeJury review can edit files and create commits. Agent providers process -the review's code and prompts and may charge for usage. `--push=false` disables -Jury's pushes; it does not make a review read-only or sandbox an agent. +A CodeJury review can edit files, create commits, and push fixes. Pushing defaults +to `--push=true`. Honor requests to keep fixes local with `--push=false`. +Agent providers process the review's code and prompts and may charge for usage. +`--push=false` disables Jury's pushes; it does not make a review read-only or +sandbox an agent. Official project documentation: [setup](https://github.com/agentsdance/codejury#quick-start) and diff --git a/scripts/check-package.mjs b/scripts/check-package.mjs index 0479748..af59101 100644 --- a/scripts/check-package.mjs +++ b/scripts/check-package.mjs @@ -43,7 +43,9 @@ try { assert.match(exec('installed full help', process.execPath, [installed, 'help', '--all'], { cwd: temporary }), /jury review /); // Full related-PR path: subprocess agents, real local remotes, fixes, resume, failures. exec('installed CLI regression tests', process.execPath, ['--test', 'test/agent-temp.test.js', 'test/repository-url.test.js', 'test/route-marker-targets.test.js', 'test/prompt-context.test.js', 'test/language.test.js', 'test/language-replies.test.js', 'test/contested-resume.test.js', 'test/usage.test.js', 'test/models.test.js', 'test/run-shape.test.js', 'test/run-attempt.test.js', 'test/review-head.test.js', 'test/commit-fixes.test.js', 'test/reply-template.test.js', 'test/codex-output.test.js', 'test/cli-commands.test.js', 'test/run-publication.test.js', 'test/global-judge.test.js', 'test/help.test.js', 'test/web-launch.test.js', 'test/web-launch-cleanup.test.js', 'test/review-group.test.js', 'test/reviewer-selection.test.js', 'test/automatic-roles.test.js', 'test/same-agent-roles.test.js', 'test/agy.test.js', 'test/opencode.test.js', 'test/new-agents.test.js', 'test/kimi.test.js', 'test/timeout.test.js', 'test/trunk.test.js'], { - cwd: root, env: { ...isolated.env, JURY_TEST_CLI: installed }, maxBuffer: 8e6, + // Keep completed tests and failing assertions visible if the CI watchdog + // terminates the suite before execFileSync returns. + cwd: root, env: { ...isolated.env, JURY_TEST_CLI: installed }, stdio: 'inherit', }); console.log(`Verified installed @agentsdance/codejury@${version}: ${files.length} files, CLI commands/subcommands, executable links, saved console, and related-PR fixes/push/resume/failure tests.`); } catch (error) { diff --git a/scripts/ci-check.mjs b/scripts/ci-check.mjs index fdb3430..75955fb 100644 --- a/scripts/ci-check.mjs +++ b/scripts/ci-check.mjs @@ -107,5 +107,8 @@ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.me mkdirSync(base, { recursive: true }); const directory = mkdtempSync(path.join(base, task.replace(':', '-') + '-')); console.error(`[ci-check] ${task}; diagnostics: ${directory}`); - process.exitCode = await runCheck('npm', ['run', task], { directory }); + // The installed suite also packs/installs the CLI and runs subprocess-heavy + // regressions. macOS runners can still be progressing at the old 3m limit. + const timeoutMs = task === 'test:package' ? 300000 : 180000; + process.exitCode = await runCheck('npm', ['run', task], { directory, timeoutMs }); } diff --git a/test/codex-plugin.test.js b/test/codex-plugin.test.js index e7ba04e..0dddf7d 100644 --- a/test/codex-plugin.test.js +++ b/test/codex-plugin.test.js @@ -1,13 +1,52 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; -import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync, realpathSync } from 'node:fs'; +import { existsSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync, realpathSync, symlinkSync } 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)); +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); + } + 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 { @@ -32,7 +71,7 @@ test('plugin launcher installs into Windows cache paths containing spaces', () = fs.writeFileSync(path.join(pkg, 'bin', 'jury.js'), 'console.log(JSON.stringify({ cwd: process.cwd(), argv: process.argv.slice(2) }));'); `); const preload = path.join(root, 'windows.mjs'); - const env = { ...process.env, CODEJURY_PLUGIN_CACHE: cache, PATH: `${bin}${path.delimiter}${process.env.PATH}` }; + 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`); @@ -51,7 +90,7 @@ test('plugin launcher installs into Windows cache paths containing spaces', () = } finally { rmSync(root, { recursive: true, force: true }); } }); -test('plugin launcher preserves cwd, literal arguments, and exit status without using a shadowed jury', () => { +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'); @@ -79,7 +118,7 @@ test('plugin launcher rejects an incomplete cache without replacing user files', mkdirSync(install); writeFileSync(path.join(install, 'keep.txt'), 'preserve'); const result = spawnSync(process.execPath, [launcher, 'version'], { - env: { ...process.env, CODEJURY_PLUGIN_CACHE: root }, encoding: 'utf8', + env: { ...process.env, CODEJURY_PLUGIN_CACHE: root, PATH: root }, encoding: 'utf8', }); assert.notEqual(result.status, 0); assert.match(result.stderr, /Incomplete CLI cache/); @@ -137,7 +176,7 @@ test('plugin launcher reuses a cache published after its initial miss', () => { syncBuiltinESMExports(); `); const result = spawnSync(process.execPath, ['--import', pathToFileURL(preload).href, launcher, 'version'], { - env: { ...process.env, CODEJURY_PLUGIN_CACHE: cache, PATH: `${bin}${path.delimiter}${process.env.PATH}` }, + env: { ...process.env, CODEJURY_PLUGIN_CACHE: cache, PATH: bin }, encoding: 'utf8', timeout: 20000, }); assert.ifError(result.error); diff --git a/test/commit-fixes.test.js b/test/commit-fixes.test.js index a0d9335..1122610 100644 --- a/test/commit-fixes.test.js +++ b/test/commit-fixes.test.js @@ -6,10 +6,107 @@ import path from 'node:path'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import { testEnvironment } from '../scripts/test-environment.mjs'; +import { readEvents, publishEvents } from '../lib/store.js'; const exec = promisify(execFile); const cli = process.env.JURY_TEST_CLI ?? new URL('../bin/jury.js', import.meta.url).pathname; +async function singlePrFixture(t) { + const root = await mkdtemp(path.join(tmpdir(), 'jury-single-pr-push-')); + t.after(() => rm(root, { recursive: true, force: true })); + const isolated = testEnvironment(); + t.after(() => isolated.dispose()); + const repo = path.join(root, 'repo'); + const remote = path.join(root, 'remote.git'); + await mkdir(repo); + const git = async (cwd, ...args) => (await exec('git', args, { cwd, env: isolated.env })).stdout.trim(); + const agent = path.join(root, 'agent.cjs'); + await writeFile(agent, ` + const fs = require('node:fs'), assert = require('node:assert/strict'); + const file = 'fix-' + process.env.JURY_FIXTURE_ROUND + '.txt'; + if (process.argv[2] === 'judge') { + assert.equal(fs.existsSync(file), false); + fs.writeFileSync(file, 'fixed'); + assert.equal(fs.readFileSync(file, 'utf8'), 'fixed'); + console.log(JSON.stringify({ verdict: 'accepted', reproduced: file + ' was absent', + test: 'file assertion fails before creation and passes after', reason: 'created ' + file })); + } else if (fs.existsSync(file)) console.log('NO NEW FINDINGS'); + else console.log('FINDING: Missing ' + file + '\\nWHERE: ' + file + ':1\\nThe required file is absent.'); + `); + await git(repo, 'init', '-q', '-b', 'master'); + await writeFile(path.join(repo, '.gitignore'), 'runs/\n'); + await writeFile(path.join(repo, 'jury.config.json'), JSON.stringify({ agents: [ + { name: 'fixture-reviewer', role: 'reviewer', cwd: 'worktree', argv: [process.execPath, agent, 'reviewer', '{{promptText}}'], report: 'whole', resume: { supported: false } }, + { name: 'fixture-judge', role: 'main', cwd: 'worktree', argv: [process.execPath, agent, 'judge', '{{promptText}}'], report: 'whole', resume: { supported: false } }, + ] })); + await git(repo, 'add', '-A'); + await git(repo, 'commit', '-qm', 'base'); + await git(repo, 'checkout', '-qb', 'feature'); + await git(repo, 'commit', '--allow-empty', '-qm', 'feature'); + await git(root, 'clone', '--bare', '-q', repo, remote); + await git(repo, 'remote', 'add', 'origin', remote); + await git(repo, 'fetch', '-q', 'origin'); + const remoteHead = () => git(remote, 'rev-parse', 'feature'); + const head = () => git(repo, 'rev-parse', 'HEAD'); + const before = await remoteHead(); + let round = 0; + const review = async (args = []) => { + const result = await exec(process.execPath, [cli, 'review', '--dir', repo, '--trunk', 'master', + '--title', 'https://github.com/fixture/repo/pull/1', '--judge', 'fixture-judge', + '--reviewer', 'fixture-reviewer', '--rounds', '1', '--web=false', ...args], + { cwd: repo, env: { ...isolated.env, JURY_FIXTURE_ROUND: String(++round) }, timeout: 15000 }); + assert.match(result.stdout, /REVIEW COMPLETE/); + const [slug] = await readdir(path.join(repo, 'runs')); + const dir = path.join(repo, 'runs', slug); + return { slug, dir, run: JSON.parse(await readFile(path.join(dir, 'run.json'), 'utf8')) }; + }; + return { repo, remote, git, head, remoteHead, before, review }; +} + +for (const push of [false, true]) { + test(`single-PR fixes persist push=${push}, inherit it on resume, and honor explicit overrides`, { timeout: 60000 }, async t => { + const f = await singlePrFixture(t); + // New reviews still push by default; local-only reviews opt out explicitly. + let saved = await f.review(push ? [] : ['--push=false']); + assert.equal(saved.run.target.pushEnabled, push); + const firstHead = await f.head(); + assert.equal(await f.remoteHead(), push ? firstHead : f.before); + + saved = await f.review(['--resume', saved.slug]); + const secondHead = await f.head(); + assert.notEqual(secondHead, firstHead, 'the resumed judge must make another fix'); + assert.equal(await f.remoteHead(), push ? secondHead : f.before, 'resume must preserve the saved push preference'); + assert.equal(saved.run.target.pushEnabled, push); + + saved = await f.review(['--resume', saved.slug, `--push=${!push}`]); + const thirdHead = await f.head(); + assert.notEqual(thirdHead, secondHead); + assert.equal(await f.remoteHead(), push ? secondHead : thirdHead, 'an explicit push preference must override the saved value'); + assert.equal(saved.run.target.pushEnabled, !push); + if (!push) assert.equal(await f.git(f.remote, 'show', 'feature:fix-1.txt'), 'fixed', 'explicitly enabling pushes also publishes the earlier local fix'); + }); +} + +test('single-PR resume keeps earlier and new fixes local when an older run has no push setting', { timeout: 60000 }, async t => { + const f = await singlePrFixture(t); + let saved = await f.review(['--push=false']); + const firstHead = await f.head(); + assert.notEqual(firstHead, f.before); + assert.equal(await f.remoteHead(), f.before); + // Older single-PR runs omitted this setting from every persisted target. + const events = await readEvents(saved.dir); + for (const event of events) if (event.t === 'target') delete event.target.pushEnabled; + await writeFile(path.join(saved.dir, 'events.ndjson'), events.map(e => JSON.stringify(e)).join('\n') + '\n'); + await publishEvents(saved.dir); + + saved = await f.review(['--resume', saved.slug]); + assert.notEqual(await f.head(), firstHead, 'the resumed judge must make another fix'); + assert.equal(await f.remoteHead(), f.before, 'an unknown push preference must not publish either local fix'); + assert.equal(saved.run.target.pushEnabled, false); + assert.equal(await f.git(f.repo, 'show', 'HEAD:fix-1.txt'), 'fixed'); + assert.equal(await f.git(f.repo, 'show', 'HEAD:fix-2.txt'), 'fixed'); +}); + for (const visibility of ['normal', 'no', 'all']) { test(`judge-created files reach the reviewed commit with status.showUntrackedFiles=${visibility}`, { timeout: 20000 }, async () => { const root = await mkdtemp(path.join(tmpdir(), 'jury-commit-files-'));