@staff0rd/assist 0.220.0 → 0.220.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/claude/CLAUDE.md +1 -3
- package/dist/index.js +284 -151
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -88,7 +88,14 @@ After installation, the `assist` command will be available globally. You can als
|
|
|
88
88
|
- `assist prs fixed <comment-id> <sha>` - Reply with commit link and resolve thread
|
|
89
89
|
- `assist prs wontfix <comment-id> <reason>` - Reply with reason and resolve thread
|
|
90
90
|
- `assist prs comment <path> <line> <body>` - Add a line comment to the pending review
|
|
91
|
-
- `assist review [sha] [
|
|
91
|
+
- `assist review [sha] [options]` - Run Claude and Codex in parallel to review the open PR for the current branch. The diff is fetched from GitHub (base SHA → head SHA via `gh pr diff`), so stale local base branches don't pollute the review; fails fast if no PR is open. By default, prompts before posting line-bound comments and then prompts again to submit the pending review (defaulting to no). Cached `claude.md` / `codex.md` / `synthesis.md` are reused when present; if any reviewer is re-run, the synthesis is invalidated.
|
|
92
|
+
- `[sha]` - Review that commit's diff (`sha^..sha`) instead of the open PR. Files land under `.assist/reviews/<shortSha>/`, no GitHub lookup or posting happens, and `--refine` / `--apply` / `--submit` are rejected
|
|
93
|
+
- `--no-prompt` - Skip all confirmations
|
|
94
|
+
- `--submit` - Default the submit prompt to yes (or auto-submit when combined with `--no-prompt`)
|
|
95
|
+
- `--force` - Clear all cached files and re-run every phase
|
|
96
|
+
- `--refine` - Skip posting; launch an interactive Claude session that walks through `synthesis.md` and edits it in place. A subsequent `assist review` reuses the refined file and posts only the surviving findings
|
|
97
|
+
- `--apply` - Skip posting; launch an interactive Claude session that walks through each finding asking apply/skip. Applied findings are fixed in the working tree (unstaged) and removed from `synthesis.md`; skipped findings stay so a subsequent `assist review` posts them. Cannot be combined with `--refine`
|
|
98
|
+
- `--verbose` - Disable the stacked-spinner UI and fall back to per-line log output. Non-TTY environments (CI) automatically use this mode
|
|
92
99
|
- `assist news` - Start the news web UI showing latest RSS feed items (same as `news web`)
|
|
93
100
|
- `assist news add [url]` - Add an RSS feed URL to the config
|
|
94
101
|
- `assist news web [-p, --port <number>]` - Start a web view of the news feeds (default port 3001)
|
package/claude/CLAUDE.md
CHANGED
|
@@ -11,6 +11,4 @@ When using extract, name the destination file after the exported function (e.g.
|
|
|
11
11
|
|
|
12
12
|
Do not modify `claude/settings.json` without asking the user first. Only read-only commands should be added to the allow list — write operations (add, remove, set, delete) must require confirmation.
|
|
13
13
|
|
|
14
|
-
When the user mentions a Jira issue key (e.g. `BAD-671`, `PROJ-123`), use
|
|
15
|
-
- `assist jira view <issue-key>` — print the title and description
|
|
16
|
-
- `assist jira ac <issue-key>` — print acceptance criteria
|
|
14
|
+
When the user mentions a Jira issue key (e.g. `BAD-671`, `PROJ-123`), use the Atlassian MCP to fetch context.
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Command } from "commander";
|
|
|
6
6
|
// package.json
|
|
7
7
|
var package_default = {
|
|
8
8
|
name: "@staff0rd/assist",
|
|
9
|
-
version: "0.220.
|
|
9
|
+
version: "0.220.1",
|
|
10
10
|
type: "module",
|
|
11
11
|
main: "dist/index.js",
|
|
12
12
|
bin: {
|
|
@@ -6135,9 +6135,9 @@ import { existsSync as existsSync23, mkdirSync as mkdirSync6, readFileSync as re
|
|
|
6135
6135
|
import { homedir as homedir6 } from "os";
|
|
6136
6136
|
import { join as join22 } from "path";
|
|
6137
6137
|
|
|
6138
|
-
// src/
|
|
6138
|
+
// src/shared/checkCliAvailable.ts
|
|
6139
6139
|
import { execSync as execSync17 } from "child_process";
|
|
6140
|
-
function
|
|
6140
|
+
function checkCliAvailable(cli) {
|
|
6141
6141
|
const binary = cli.split(/\s+/)[0];
|
|
6142
6142
|
const opts = {
|
|
6143
6143
|
encoding: "utf-8",
|
|
@@ -6145,16 +6145,24 @@ function assertCliExists(cli) {
|
|
|
6145
6145
|
};
|
|
6146
6146
|
try {
|
|
6147
6147
|
execSync17(`command -v ${binary}`, opts);
|
|
6148
|
+
return true;
|
|
6148
6149
|
} catch {
|
|
6149
6150
|
try {
|
|
6150
6151
|
execSync17(`where ${binary}`, opts);
|
|
6152
|
+
return true;
|
|
6151
6153
|
} catch {
|
|
6152
|
-
|
|
6153
|
-
process.exit(1);
|
|
6154
|
+
return false;
|
|
6154
6155
|
}
|
|
6155
6156
|
}
|
|
6156
6157
|
}
|
|
6157
6158
|
|
|
6159
|
+
// src/commands/permitCliReads/assertCliExists.ts
|
|
6160
|
+
function assertCliExists(cli) {
|
|
6161
|
+
if (checkCliAvailable(cli)) return;
|
|
6162
|
+
console.error(`CLI "${cli}" not found in PATH`);
|
|
6163
|
+
process.exit(1);
|
|
6164
|
+
}
|
|
6165
|
+
|
|
6158
6166
|
// src/commands/permitCliReads/colorize.ts
|
|
6159
6167
|
import chalk66 from "chalk";
|
|
6160
6168
|
function colorize(plainOutput) {
|
|
@@ -12216,68 +12224,18 @@ async function runApplySession(synthesisPath) {
|
|
|
12216
12224
|
await done2;
|
|
12217
12225
|
}
|
|
12218
12226
|
|
|
12219
|
-
// src/commands/review/
|
|
12220
|
-
import {
|
|
12221
|
-
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
|
|
12225
|
-
|
|
12226
|
-
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
|
|
12230
|
-
|
|
12231
|
-
1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
|
|
12232
|
-
2. The issue is discrete and actionable \u2014 not a vague observation about the codebase or a tangle of several things.
|
|
12233
|
-
3. Fixing it does not demand more rigour than the rest of the codebase already shows (e.g. don't ask for exhaustive input validation in a repo of one-off scripts).
|
|
12234
|
-
4. The issue was introduced by this change. Do not flag pre-existing bugs.
|
|
12235
|
-
5. The original author would likely fix it if made aware.
|
|
12236
|
-
6. It does not rely on unstated assumptions about the codebase or author's intent.
|
|
12237
|
-
7. You can name the concretely affected code path. Speculation that a change *might* disrupt something elsewhere is not enough \u2014 identify the other code that is provably affected.
|
|
12238
|
-
8. It is clearly not an intentional change by the author.
|
|
12239
|
-
|
|
12240
|
-
## How to write the comment (Impact + Recommendation)
|
|
12241
|
-
|
|
12242
|
-
1. Make clear *why* the issue is a bug.
|
|
12243
|
-
2. Communicate severity accurately \u2014 do not inflate.
|
|
12244
|
-
3. Keep it brief: at most one paragraph of prose. Avoid line breaks inside the natural-language flow unless needed for a code fragment.
|
|
12245
|
-
4. Do not paste code chunks longer than 3 lines. Wrap short snippets in inline code or a fenced block.
|
|
12246
|
-
5. State explicitly the scenarios, environments, or inputs needed for the bug to manifest \u2014 and signal up front that severity depends on those factors.
|
|
12247
|
-
6. Tone is matter-of-fact: not accusatory, not gushing. Read as a helpful assistant, not a performative human reviewer.
|
|
12248
|
-
7. Write so the author grasps the point on first read.
|
|
12249
|
-
8. Avoid flattery and filler ("Great job\u2026", "Thanks for\u2026"). They are not useful to the author.
|
|
12250
|
-
|
|
12251
|
-
Ignore trivial style unless it obscures meaning or violates a documented standard. One finding per distinct issue.
|
|
12252
|
-
|
|
12253
|
-
## How many findings to return
|
|
12254
|
-
|
|
12255
|
-
List every finding that the original author would want to know about and fix. Do not stop at the first qualifying one. If nothing clears the bar above, return no findings \u2014 empty is better than padded.
|
|
12256
|
-
|
|
12257
|
-
## Output format
|
|
12258
|
-
|
|
12259
|
-
For each finding include:
|
|
12260
|
-
- Severity (blocker, major, minor, nit) \u2014 see rubric below
|
|
12261
|
-
- File and line (e.g. \`src/foo.ts:42\`) when the finding is tied to a specific location
|
|
12262
|
-
- Impact: what could go wrong, including the conditions under which it manifests
|
|
12263
|
-
- Recommendation: a concrete change
|
|
12264
|
-
|
|
12265
|
-
Severity rubric:
|
|
12266
|
-
- **blocker** \u2014 ships broken behaviour: crash, data loss, security hole, breaks the build or existing tests, or violates a stated requirement.
|
|
12267
|
-
- **major** \u2014 likely bug, missing error handling on a real failure mode, or a regression in existing behaviour. Not "this could be cleaner" or "this might be slow."
|
|
12268
|
-
- **minor** \u2014 narrow correctness or clarity issue with limited blast radius; worth fixing but not urgent.
|
|
12269
|
-
- **nit** \u2014 style, naming, micro-refactors, comment wording; reviewer would not block on it.
|
|
12270
|
-
|
|
12271
|
-
Default to the lower tier when uncertain. Code-style preferences, refactor suggestions, and "I would have written it differently" belong in nit \u2014 not major. A finding is only major if you can name a concrete failure mode or regression.
|
|
12272
|
-
|
|
12273
|
-
Group findings by severity. If you have no findings in a category, omit it. End with a short overall summary.
|
|
12274
|
-
|
|
12275
|
-
Output only the review Markdown. Do not include any preamble or commentary about the process.`;
|
|
12276
|
-
function buildReviewerStdin(requestPath) {
|
|
12277
|
-
return `${REVIEW_PROMPT}
|
|
12278
|
-
|
|
12279
|
-
The review request is at: ${requestPath}
|
|
12280
|
-
`;
|
|
12227
|
+
// src/commands/review/cachedReviewerResult.ts
|
|
12228
|
+
import { statSync as statSync4 } from "fs";
|
|
12229
|
+
function cachedReviewerResult(name, outputPath) {
|
|
12230
|
+
let size;
|
|
12231
|
+
try {
|
|
12232
|
+
size = statSync4(outputPath).size;
|
|
12233
|
+
} catch {
|
|
12234
|
+
return null;
|
|
12235
|
+
}
|
|
12236
|
+
if (size === 0) return null;
|
|
12237
|
+
console.log(`[${name}] cached \u2192 ${outputPath} (${size} bytes)`);
|
|
12238
|
+
return { name, outputPath, exitCode: 0, stderr: "" };
|
|
12281
12239
|
}
|
|
12282
12240
|
|
|
12283
12241
|
// src/commands/review/MultiSpinner.ts
|
|
@@ -12382,18 +12340,114 @@ var MultiSpinner = class {
|
|
|
12382
12340
|
}
|
|
12383
12341
|
};
|
|
12384
12342
|
|
|
12385
|
-
// src/commands/review/
|
|
12386
|
-
import {
|
|
12387
|
-
function
|
|
12388
|
-
|
|
12389
|
-
|
|
12390
|
-
|
|
12391
|
-
}
|
|
12392
|
-
|
|
12343
|
+
// src/commands/review/ensureCodexAvailable.ts
|
|
12344
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
12345
|
+
function runNpmInstall() {
|
|
12346
|
+
const result = spawnSync3("npm", ["install", "-g", "@openai/codex"], {
|
|
12347
|
+
stdio: "inherit",
|
|
12348
|
+
shell: true
|
|
12349
|
+
});
|
|
12350
|
+
return result.status === 0 && result.error === void 0;
|
|
12351
|
+
}
|
|
12352
|
+
async function ensureCodexAvailable() {
|
|
12353
|
+
if (checkCliAvailable("codex")) return "available";
|
|
12354
|
+
console.log("codex CLI was not found on PATH.");
|
|
12355
|
+
const install = await promptConfirm(
|
|
12356
|
+
"Install @openai/codex globally via npm install -g @openai/codex?",
|
|
12357
|
+
true
|
|
12358
|
+
);
|
|
12359
|
+
if (!install) {
|
|
12360
|
+
console.log("Skipping codex reviewer.");
|
|
12361
|
+
return "skipped";
|
|
12393
12362
|
}
|
|
12394
|
-
|
|
12395
|
-
|
|
12396
|
-
|
|
12363
|
+
console.log("Installing @openai/codex...");
|
|
12364
|
+
if (!runNpmInstall()) {
|
|
12365
|
+
console.error(
|
|
12366
|
+
"npm install -g @openai/codex failed. Skipping codex reviewer."
|
|
12367
|
+
);
|
|
12368
|
+
return "skipped";
|
|
12369
|
+
}
|
|
12370
|
+
if (checkCliAvailable("codex")) return "available";
|
|
12371
|
+
console.error(
|
|
12372
|
+
"codex still not found on PATH after install. Skipping codex reviewer."
|
|
12373
|
+
);
|
|
12374
|
+
return "skipped";
|
|
12375
|
+
}
|
|
12376
|
+
|
|
12377
|
+
// src/commands/review/planCodexReviewer.ts
|
|
12378
|
+
async function planCodexReviewer(codexPath) {
|
|
12379
|
+
const cached = cachedReviewerResult("codex", codexPath);
|
|
12380
|
+
if (cached) return { kind: "cached", cached };
|
|
12381
|
+
const status2 = await ensureCodexAvailable();
|
|
12382
|
+
if (status2 === "available") return { kind: "run" };
|
|
12383
|
+
return { kind: "skipped" };
|
|
12384
|
+
}
|
|
12385
|
+
function skippedCodexResult(outputPath) {
|
|
12386
|
+
return { name: "codex", outputPath, exitCode: 0, stderr: "" };
|
|
12387
|
+
}
|
|
12388
|
+
|
|
12389
|
+
// src/commands/review/runAndSynthesise.ts
|
|
12390
|
+
import { existsSync as existsSync35, unlinkSync as unlinkSync12 } from "fs";
|
|
12391
|
+
|
|
12392
|
+
// src/commands/review/buildReviewerStdin.ts
|
|
12393
|
+
var REVIEW_PROMPT = `You are acting as a reviewer for a proposed code change made by another engineer. The full review request \u2014 branch, base, changed files, and unified diff \u2014 is in request.md in the current working directory.
|
|
12394
|
+
|
|
12395
|
+
Read request.md, then produce a thorough code review in Markdown.
|
|
12396
|
+
|
|
12397
|
+
## When to flag a finding
|
|
12398
|
+
|
|
12399
|
+
A finding is worth raising only if all of the following hold:
|
|
12400
|
+
|
|
12401
|
+
1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
|
|
12402
|
+
2. The issue is discrete and actionable \u2014 not a vague observation about the codebase or a tangle of several things.
|
|
12403
|
+
3. Fixing it does not demand more rigour than the rest of the codebase already shows (e.g. don't ask for exhaustive input validation in a repo of one-off scripts).
|
|
12404
|
+
4. The issue was introduced by this change. Do not flag pre-existing bugs.
|
|
12405
|
+
5. The original author would likely fix it if made aware.
|
|
12406
|
+
6. It does not rely on unstated assumptions about the codebase or author's intent.
|
|
12407
|
+
7. You can name the concretely affected code path. Speculation that a change *might* disrupt something elsewhere is not enough \u2014 identify the other code that is provably affected.
|
|
12408
|
+
8. It is clearly not an intentional change by the author.
|
|
12409
|
+
|
|
12410
|
+
## How to write the comment (Impact + Recommendation)
|
|
12411
|
+
|
|
12412
|
+
1. Make clear *why* the issue is a bug.
|
|
12413
|
+
2. Communicate severity accurately \u2014 do not inflate.
|
|
12414
|
+
3. Keep it brief: at most one paragraph of prose. Avoid line breaks inside the natural-language flow unless needed for a code fragment.
|
|
12415
|
+
4. Do not paste code chunks longer than 3 lines. Wrap short snippets in inline code or a fenced block.
|
|
12416
|
+
5. State explicitly the scenarios, environments, or inputs needed for the bug to manifest \u2014 and signal up front that severity depends on those factors.
|
|
12417
|
+
6. Tone is matter-of-fact: not accusatory, not gushing. Read as a helpful assistant, not a performative human reviewer.
|
|
12418
|
+
7. Write so the author grasps the point on first read.
|
|
12419
|
+
8. Avoid flattery and filler ("Great job\u2026", "Thanks for\u2026"). They are not useful to the author.
|
|
12420
|
+
|
|
12421
|
+
Ignore trivial style unless it obscures meaning or violates a documented standard. One finding per distinct issue.
|
|
12422
|
+
|
|
12423
|
+
## How many findings to return
|
|
12424
|
+
|
|
12425
|
+
List every finding that the original author would want to know about and fix. Do not stop at the first qualifying one. If nothing clears the bar above, return no findings \u2014 empty is better than padded.
|
|
12426
|
+
|
|
12427
|
+
## Output format
|
|
12428
|
+
|
|
12429
|
+
For each finding include:
|
|
12430
|
+
- Severity (blocker, major, minor, nit) \u2014 see rubric below
|
|
12431
|
+
- File and line (e.g. \`src/foo.ts:42\`) when the finding is tied to a specific location
|
|
12432
|
+
- Impact: what could go wrong, including the conditions under which it manifests
|
|
12433
|
+
- Recommendation: a concrete change
|
|
12434
|
+
|
|
12435
|
+
Severity rubric:
|
|
12436
|
+
- **blocker** \u2014 ships broken behaviour: crash, data loss, security hole, breaks the build or existing tests, or violates a stated requirement.
|
|
12437
|
+
- **major** \u2014 likely bug, missing error handling on a real failure mode, or a regression in existing behaviour. Not "this could be cleaner" or "this might be slow."
|
|
12438
|
+
- **minor** \u2014 narrow correctness or clarity issue with limited blast radius; worth fixing but not urgent.
|
|
12439
|
+
- **nit** \u2014 style, naming, micro-refactors, comment wording; reviewer would not block on it.
|
|
12440
|
+
|
|
12441
|
+
Default to the lower tier when uncertain. Code-style preferences, refactor suggestions, and "I would have written it differently" belong in nit \u2014 not major. A finding is only major if you can name a concrete failure mode or regression.
|
|
12442
|
+
|
|
12443
|
+
Group findings by severity. If you have no findings in a category, omit it. End with a short overall summary.
|
|
12444
|
+
|
|
12445
|
+
Output only the review Markdown. Do not include any preamble or commentary about the process.`;
|
|
12446
|
+
function buildReviewerStdin(requestPath) {
|
|
12447
|
+
return `${REVIEW_PROMPT}
|
|
12448
|
+
|
|
12449
|
+
The review request is at: ${requestPath}
|
|
12450
|
+
`;
|
|
12397
12451
|
}
|
|
12398
12452
|
|
|
12399
12453
|
// src/commands/review/printReviewerFailures.ts
|
|
@@ -12577,42 +12631,90 @@ function handleChildClose(args) {
|
|
|
12577
12631
|
return { exitCode, elapsedMs };
|
|
12578
12632
|
}
|
|
12579
12633
|
|
|
12634
|
+
// src/commands/review/handleSpawnError.ts
|
|
12635
|
+
function messageFor(err, command) {
|
|
12636
|
+
if (err.code === "ENOENT") return `command not found: ${command}`;
|
|
12637
|
+
return err.message || String(err);
|
|
12638
|
+
}
|
|
12639
|
+
function handleSpawnError(ctx, err) {
|
|
12640
|
+
const message = messageFor(err, ctx.command);
|
|
12641
|
+
const stderr = ctx.stderr ? `${ctx.stderr}
|
|
12642
|
+
${message}` : message;
|
|
12643
|
+
if (!ctx.quiet) console.error(`[${ctx.name}] failed: ${message}`);
|
|
12644
|
+
return {
|
|
12645
|
+
exitCode: 127,
|
|
12646
|
+
stderr,
|
|
12647
|
+
elapsedMs: Date.now() - ctx.startedAt
|
|
12648
|
+
};
|
|
12649
|
+
}
|
|
12650
|
+
|
|
12651
|
+
// src/commands/review/waitForChildExit.ts
|
|
12652
|
+
function onErrorResult(ctx, err) {
|
|
12653
|
+
ctx.flushPending();
|
|
12654
|
+
return handleSpawnError(
|
|
12655
|
+
{
|
|
12656
|
+
command: ctx.command,
|
|
12657
|
+
name: ctx.name,
|
|
12658
|
+
stderr: ctx.stderr.value,
|
|
12659
|
+
startedAt: ctx.startedAt,
|
|
12660
|
+
quiet: ctx.quiet
|
|
12661
|
+
},
|
|
12662
|
+
err
|
|
12663
|
+
);
|
|
12664
|
+
}
|
|
12665
|
+
function onCloseResult(ctx, code) {
|
|
12666
|
+
ctx.flushPending();
|
|
12667
|
+
const closed = handleChildClose({
|
|
12668
|
+
code,
|
|
12669
|
+
startedAt: ctx.startedAt,
|
|
12670
|
+
name: ctx.name,
|
|
12671
|
+
stderr: ctx.stderr.value,
|
|
12672
|
+
quiet: ctx.quiet
|
|
12673
|
+
});
|
|
12674
|
+
return { ...closed, stderr: ctx.stderr.value };
|
|
12675
|
+
}
|
|
12676
|
+
function waitForChildExit(ctx) {
|
|
12677
|
+
return new Promise((resolve15) => {
|
|
12678
|
+
let settled = false;
|
|
12679
|
+
const settle = (result) => {
|
|
12680
|
+
if (settled) return;
|
|
12681
|
+
settled = true;
|
|
12682
|
+
resolve15(result);
|
|
12683
|
+
};
|
|
12684
|
+
ctx.child.on("error", (err) => settle(onErrorResult(ctx, err)));
|
|
12685
|
+
ctx.child.on("close", (code) => settle(onCloseResult(ctx, code)));
|
|
12686
|
+
});
|
|
12687
|
+
}
|
|
12688
|
+
|
|
12580
12689
|
// src/commands/review/runStreamingChild.ts
|
|
12690
|
+
function writeStdinSafely(child, payload) {
|
|
12691
|
+
child.stdin?.on("error", () => {
|
|
12692
|
+
});
|
|
12693
|
+
try {
|
|
12694
|
+
child.stdin?.write(payload);
|
|
12695
|
+
child.stdin?.end();
|
|
12696
|
+
} catch {
|
|
12697
|
+
}
|
|
12698
|
+
}
|
|
12581
12699
|
function startChild(spec) {
|
|
12582
12700
|
const child = spawn6(spec.command, spec.args, {
|
|
12583
12701
|
stdio: ["pipe", "pipe", "pipe"]
|
|
12584
12702
|
});
|
|
12585
12703
|
const flushPending = attachLineParser(child, spec.onLine);
|
|
12586
12704
|
const stderr = attachStderrCollector(child);
|
|
12587
|
-
child
|
|
12588
|
-
child.stdin?.end();
|
|
12705
|
+
writeStdinSafely(child, spec.stdin);
|
|
12589
12706
|
return { child, flushPending, stderr };
|
|
12590
12707
|
}
|
|
12591
|
-
function waitForExit(ctx) {
|
|
12592
|
-
return new Promise((resolve15, reject) => {
|
|
12593
|
-
ctx.child.on("error", reject);
|
|
12594
|
-
ctx.child.on("close", (code) => {
|
|
12595
|
-
ctx.flushPending();
|
|
12596
|
-
const closed = handleChildClose({
|
|
12597
|
-
code,
|
|
12598
|
-
startedAt: ctx.startedAt,
|
|
12599
|
-
name: ctx.name,
|
|
12600
|
-
stderr: ctx.stderr.value,
|
|
12601
|
-
quiet: ctx.quiet
|
|
12602
|
-
});
|
|
12603
|
-
resolve15({ ...closed, stderr: ctx.stderr.value });
|
|
12604
|
-
});
|
|
12605
|
-
});
|
|
12606
|
-
}
|
|
12607
12708
|
function runStreamingChild(spec) {
|
|
12608
12709
|
const startedAt = Date.now();
|
|
12609
12710
|
if (!spec.quiet) console.log(`[${spec.name}] starting`);
|
|
12610
12711
|
const { child, flushPending, stderr } = startChild(spec);
|
|
12611
|
-
return
|
|
12712
|
+
return waitForChildExit({
|
|
12612
12713
|
child,
|
|
12613
12714
|
flushPending,
|
|
12614
12715
|
stderr,
|
|
12615
12716
|
name: spec.name,
|
|
12717
|
+
command: spec.command,
|
|
12616
12718
|
startedAt,
|
|
12617
12719
|
quiet: spec.quiet ?? false
|
|
12618
12720
|
});
|
|
@@ -12650,6 +12752,19 @@ async function runClaudeReviewer(spec) {
|
|
|
12650
12752
|
return finaliseReviewerRun(spec, spinner, result);
|
|
12651
12753
|
}
|
|
12652
12754
|
|
|
12755
|
+
// src/commands/review/resolveClaude.ts
|
|
12756
|
+
function resolveClaude(args) {
|
|
12757
|
+
if (args.cached) return Promise.resolve(args.cached);
|
|
12758
|
+
const spinner = args.multi?.create("claude \u2014 starting");
|
|
12759
|
+
return runClaudeReviewer({
|
|
12760
|
+
name: "claude",
|
|
12761
|
+
reviewDir: args.reviewDir,
|
|
12762
|
+
stdin: args.stdin,
|
|
12763
|
+
outputPath: args.claudePath,
|
|
12764
|
+
spinner
|
|
12765
|
+
});
|
|
12766
|
+
}
|
|
12767
|
+
|
|
12653
12768
|
// src/commands/review/runCodexReviewer.ts
|
|
12654
12769
|
import { existsSync as existsSync34, unlinkSync as unlinkSync11 } from "fs";
|
|
12655
12770
|
|
|
@@ -12710,38 +12825,42 @@ async function runCodexReviewer(spec) {
|
|
|
12710
12825
|
return finaliseReviewerRun(spec, spinner, result);
|
|
12711
12826
|
}
|
|
12712
12827
|
|
|
12713
|
-
// src/commands/review/
|
|
12714
|
-
function
|
|
12715
|
-
if (
|
|
12716
|
-
|
|
12828
|
+
// src/commands/review/resolveCodex.ts
|
|
12829
|
+
function resolveCodex(args) {
|
|
12830
|
+
if (args.plan.kind === "cached") return Promise.resolve(args.plan.cached);
|
|
12831
|
+
if (args.plan.kind === "skipped") {
|
|
12832
|
+
return Promise.resolve(skippedCodexResult(args.codexPath));
|
|
12833
|
+
}
|
|
12834
|
+
const spinner = args.multi?.create("codex \u2014 starting");
|
|
12835
|
+
return runCodexReviewer({
|
|
12836
|
+
name: "codex",
|
|
12837
|
+
reviewDir: args.reviewDir,
|
|
12838
|
+
stdin: args.stdin,
|
|
12839
|
+
outputPath: args.codexPath,
|
|
12840
|
+
spinner
|
|
12841
|
+
});
|
|
12717
12842
|
}
|
|
12843
|
+
|
|
12844
|
+
// src/commands/review/runReviewers.ts
|
|
12718
12845
|
async function runReviewers(reviewDir, claudePath, codexPath, stdinPrompt, options2) {
|
|
12719
|
-
const
|
|
12720
|
-
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
|
|
12724
|
-
|
|
12725
|
-
|
|
12726
|
-
const
|
|
12727
|
-
|
|
12728
|
-
|
|
12729
|
-
|
|
12730
|
-
|
|
12731
|
-
|
|
12732
|
-
|
|
12733
|
-
|
|
12734
|
-
|
|
12735
|
-
|
|
12736
|
-
|
|
12737
|
-
reviewDir,
|
|
12738
|
-
stdin: stdinPrompt,
|
|
12739
|
-
outputPath: codexPath,
|
|
12740
|
-
spinner: codexSpinner
|
|
12741
|
-
})
|
|
12742
|
-
]);
|
|
12743
|
-
if (multi) printReviewerFailures(results);
|
|
12744
|
-
return { results, anyFresh: true };
|
|
12846
|
+
const claudePromise = resolveClaude({
|
|
12847
|
+
reviewDir,
|
|
12848
|
+
claudePath,
|
|
12849
|
+
stdin: stdinPrompt,
|
|
12850
|
+
cached: options2.cachedClaude,
|
|
12851
|
+
multi: options2.multi
|
|
12852
|
+
});
|
|
12853
|
+
const codexPromise = resolveCodex({
|
|
12854
|
+
reviewDir,
|
|
12855
|
+
codexPath,
|
|
12856
|
+
stdin: stdinPrompt,
|
|
12857
|
+
plan: options2.codexPlan,
|
|
12858
|
+
multi: options2.multi
|
|
12859
|
+
});
|
|
12860
|
+
const results = await Promise.all([claudePromise, codexPromise]);
|
|
12861
|
+
if (options2.multi) printReviewerFailures(results);
|
|
12862
|
+
const anyFresh = options2.cachedClaude === null || options2.codexPlan.kind !== "cached";
|
|
12863
|
+
return { results, anyFresh };
|
|
12745
12864
|
}
|
|
12746
12865
|
|
|
12747
12866
|
// src/commands/review/synthesise.ts
|
|
@@ -12750,7 +12869,9 @@ import { readFileSync as readFileSync30 } from "fs";
|
|
|
12750
12869
|
// src/commands/review/buildSynthesisStdin.ts
|
|
12751
12870
|
var SYNTHESIS_PROMPT = `You are consolidating two independent code reviews of the same change. The original review request is in request.md. The two reviews are in claude.md and codex.md in the current working directory.
|
|
12752
12871
|
|
|
12753
|
-
|
|
12872
|
+
If codex.md does not exist on disk, the codex reviewer was skipped (CLI unavailable). In that case, work from claude.md alone; treat every finding as 'claude-only', do not mark anything 'confirmed' or 'codex-only', and add a note in the Summary that the codex reviewer was skipped.
|
|
12873
|
+
|
|
12874
|
+
Read all available review files, deduplicate findings, and produce a single consolidated review in Markdown with this exact structure:
|
|
12754
12875
|
|
|
12755
12876
|
# Code review synthesis
|
|
12756
12877
|
|
|
@@ -12830,6 +12951,29 @@ async function synthesise(paths, options2) {
|
|
|
12830
12951
|
return result;
|
|
12831
12952
|
}
|
|
12832
12953
|
|
|
12954
|
+
// src/commands/review/runAndSynthesise.ts
|
|
12955
|
+
async function runAndSynthesise(args) {
|
|
12956
|
+
const { paths, multi } = args;
|
|
12957
|
+
const { results, anyFresh } = await runReviewers(
|
|
12958
|
+
paths.reviewDir,
|
|
12959
|
+
paths.claudePath,
|
|
12960
|
+
paths.codexPath,
|
|
12961
|
+
buildReviewerStdin(paths.requestPath),
|
|
12962
|
+
{ multi, codexPlan: args.codexPlan, cachedClaude: args.cachedClaude }
|
|
12963
|
+
);
|
|
12964
|
+
if (results.every((r) => r.exitCode !== 0)) {
|
|
12965
|
+
console.error(
|
|
12966
|
+
"Both reviewers failed; skipping synthesis. See review folder for stderr details."
|
|
12967
|
+
);
|
|
12968
|
+
return false;
|
|
12969
|
+
}
|
|
12970
|
+
if (anyFresh && existsSync35(paths.synthesisPath)) {
|
|
12971
|
+
unlinkSync12(paths.synthesisPath);
|
|
12972
|
+
}
|
|
12973
|
+
const synthesisResult = await synthesise(paths, { multi });
|
|
12974
|
+
return synthesisResult.exitCode === 0;
|
|
12975
|
+
}
|
|
12976
|
+
|
|
12833
12977
|
// src/commands/review/useSpinnerUi.ts
|
|
12834
12978
|
function useSpinnerUi(verbose) {
|
|
12835
12979
|
if (verbose) return false;
|
|
@@ -12851,27 +12995,16 @@ function finishUi(ui, ok) {
|
|
|
12851
12995
|
else ui.elapsed.fail(label2);
|
|
12852
12996
|
}
|
|
12853
12997
|
async function runReviewPipeline(paths, options2) {
|
|
12998
|
+
const cachedClaude = cachedReviewerResult("claude", paths.claudePath);
|
|
12999
|
+
const codexPlan = await planCodexReviewer(paths.codexPath);
|
|
12854
13000
|
const ui = createUi(useSpinnerUi(options2.verbose));
|
|
12855
13001
|
try {
|
|
12856
|
-
const
|
|
12857
|
-
paths
|
|
12858
|
-
|
|
12859
|
-
|
|
12860
|
-
|
|
12861
|
-
|
|
12862
|
-
);
|
|
12863
|
-
if (results.every((r) => r.exitCode !== 0)) {
|
|
12864
|
-
console.error(
|
|
12865
|
-
"Both reviewers failed; skipping synthesis. See review folder for stderr details."
|
|
12866
|
-
);
|
|
12867
|
-
finishUi(ui, false);
|
|
12868
|
-
return false;
|
|
12869
|
-
}
|
|
12870
|
-
if (anyFresh && existsSync35(paths.synthesisPath)) {
|
|
12871
|
-
unlinkSync12(paths.synthesisPath);
|
|
12872
|
-
}
|
|
12873
|
-
const synthesisResult = await synthesise(paths, { multi: ui.multi });
|
|
12874
|
-
const ok = synthesisResult.exitCode === 0;
|
|
13002
|
+
const ok = await runAndSynthesise({
|
|
13003
|
+
paths,
|
|
13004
|
+
cachedClaude,
|
|
13005
|
+
codexPlan,
|
|
13006
|
+
multi: ui.multi
|
|
13007
|
+
});
|
|
12875
13008
|
finishUi(ui, ok);
|
|
12876
13009
|
return ok;
|
|
12877
13010
|
} catch (err) {
|
|
@@ -14313,7 +14446,7 @@ function registerVerify(program2) {
|
|
|
14313
14446
|
}
|
|
14314
14447
|
|
|
14315
14448
|
// src/commands/voice/devices.ts
|
|
14316
|
-
import { spawnSync as
|
|
14449
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
14317
14450
|
import { join as join43 } from "path";
|
|
14318
14451
|
|
|
14319
14452
|
// src/commands/voice/shared.ts
|
|
@@ -14346,7 +14479,7 @@ function getLockFile() {
|
|
|
14346
14479
|
// src/commands/voice/devices.ts
|
|
14347
14480
|
function devices() {
|
|
14348
14481
|
const script = join43(getPythonDir(), "list_devices.py");
|
|
14349
|
-
|
|
14482
|
+
spawnSync4(getVenvPython(), [script], { stdio: "inherit" });
|
|
14350
14483
|
}
|
|
14351
14484
|
|
|
14352
14485
|
// src/commands/voice/logs.ts
|
|
@@ -14378,7 +14511,7 @@ function logs(options2) {
|
|
|
14378
14511
|
}
|
|
14379
14512
|
|
|
14380
14513
|
// src/commands/voice/setup.ts
|
|
14381
|
-
import { spawnSync as
|
|
14514
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
14382
14515
|
import { mkdirSync as mkdirSync14 } from "fs";
|
|
14383
14516
|
import { join as join45 } from "path";
|
|
14384
14517
|
|
|
@@ -14439,7 +14572,7 @@ function setup() {
|
|
|
14439
14572
|
bootstrapVenv();
|
|
14440
14573
|
console.log("\nDownloading models...\n");
|
|
14441
14574
|
const script = join45(getPythonDir(), "setup_models.py");
|
|
14442
|
-
const result =
|
|
14575
|
+
const result = spawnSync5(getVenvPython(), [script], {
|
|
14443
14576
|
stdio: "inherit",
|
|
14444
14577
|
env: { ...process.env, VOICE_LOG_FILE: voicePaths.log }
|
|
14445
14578
|
});
|