@staff0rd/assist 0.219.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 +501 -273
- 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 [
|
|
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.
|
|
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) {
|
|
@@ -11639,60 +11647,6 @@ function registerRefactor(program2) {
|
|
|
11639
11647
|
registerRestructure(refactorCommand);
|
|
11640
11648
|
}
|
|
11641
11649
|
|
|
11642
|
-
// src/commands/review/buildRequest.ts
|
|
11643
|
-
import { execSync as execSync38 } from "child_process";
|
|
11644
|
-
|
|
11645
|
-
// src/commands/review/fetchPrDiffInfo.ts
|
|
11646
|
-
import { execSync as execSync37 } from "child_process";
|
|
11647
|
-
function getCurrentBranch2() {
|
|
11648
|
-
return execSync37("git rev-parse --abbrev-ref HEAD", {
|
|
11649
|
-
encoding: "utf-8"
|
|
11650
|
-
}).trim();
|
|
11651
|
-
}
|
|
11652
|
-
function fetchPrDiffInfo() {
|
|
11653
|
-
const { org, repo } = getRepoInfo();
|
|
11654
|
-
const branch = getCurrentBranch2();
|
|
11655
|
-
const fields = "number,baseRefName,baseRefOid,headRefName,headRefOid";
|
|
11656
|
-
let raw;
|
|
11657
|
-
try {
|
|
11658
|
-
raw = execSync37(`gh pr view ${branch} --json ${fields} -R ${org}/${repo}`, {
|
|
11659
|
-
encoding: "utf-8",
|
|
11660
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
11661
|
-
});
|
|
11662
|
-
} catch (error) {
|
|
11663
|
-
if (error instanceof Error && error.message.includes("no pull requests")) {
|
|
11664
|
-
console.error(
|
|
11665
|
-
`Error: No open pull request found for branch \`${branch}\`. Open a PR for this branch before running \`assist review\`.`
|
|
11666
|
-
);
|
|
11667
|
-
process.exit(1);
|
|
11668
|
-
}
|
|
11669
|
-
throw error;
|
|
11670
|
-
}
|
|
11671
|
-
const parsed = JSON.parse(raw);
|
|
11672
|
-
return {
|
|
11673
|
-
prNumber: parsed.number,
|
|
11674
|
-
baseRef: parsed.baseRefName,
|
|
11675
|
-
baseSha: parsed.baseRefOid,
|
|
11676
|
-
headRef: parsed.headRefName,
|
|
11677
|
-
headSha: parsed.headRefOid
|
|
11678
|
-
};
|
|
11679
|
-
}
|
|
11680
|
-
function fetchPrChangedFiles(prNumber) {
|
|
11681
|
-
const { org, repo } = getRepoInfo();
|
|
11682
|
-
const out = execSync37(`gh pr diff ${prNumber} --name-only -R ${org}/${repo}`, {
|
|
11683
|
-
encoding: "utf-8",
|
|
11684
|
-
maxBuffer: 64 * 1024 * 1024
|
|
11685
|
-
});
|
|
11686
|
-
return out.trim().split("\n").filter(Boolean);
|
|
11687
|
-
}
|
|
11688
|
-
function fetchPrDiff(prNumber) {
|
|
11689
|
-
const { org, repo } = getRepoInfo();
|
|
11690
|
-
return execSync37(`gh pr diff ${prNumber} -R ${org}/${repo}`, {
|
|
11691
|
-
encoding: "utf-8",
|
|
11692
|
-
maxBuffer: 256 * 1024 * 1024
|
|
11693
|
-
});
|
|
11694
|
-
}
|
|
11695
|
-
|
|
11696
11650
|
// src/commands/review/formatPriorComments.ts
|
|
11697
11651
|
function threadKey(c, byId) {
|
|
11698
11652
|
if (c.threadId) return c.threadId;
|
|
@@ -11743,30 +11697,6 @@ ${blocks.join("\n\n")}`;
|
|
|
11743
11697
|
}
|
|
11744
11698
|
|
|
11745
11699
|
// src/commands/review/buildRequest.ts
|
|
11746
|
-
function gatherContext() {
|
|
11747
|
-
const branch = execSync38("git rev-parse --abbrev-ref HEAD", {
|
|
11748
|
-
encoding: "utf-8"
|
|
11749
|
-
}).trim();
|
|
11750
|
-
const sha = execSync38("git rev-parse HEAD", { encoding: "utf-8" }).trim();
|
|
11751
|
-
const shortSha = execSync38("git rev-parse --short=7 HEAD", {
|
|
11752
|
-
encoding: "utf-8"
|
|
11753
|
-
}).trim();
|
|
11754
|
-
const prInfo = fetchPrDiffInfo();
|
|
11755
|
-
const changedFiles = fetchPrChangedFiles(prInfo.prNumber);
|
|
11756
|
-
const diff2 = fetchPrDiff(prInfo.prNumber);
|
|
11757
|
-
return {
|
|
11758
|
-
branch,
|
|
11759
|
-
sha,
|
|
11760
|
-
shortSha,
|
|
11761
|
-
prNumber: prInfo.prNumber,
|
|
11762
|
-
baseRef: prInfo.baseRef,
|
|
11763
|
-
baseSha: prInfo.baseSha,
|
|
11764
|
-
headRef: prInfo.headRef,
|
|
11765
|
-
headSha: prInfo.headSha,
|
|
11766
|
-
changedFiles,
|
|
11767
|
-
diff: diff2
|
|
11768
|
-
};
|
|
11769
|
-
}
|
|
11770
11700
|
function formatFiles(files) {
|
|
11771
11701
|
if (files.length === 0) return "(none)";
|
|
11772
11702
|
return files.map((file) => `- ${file}`).join("\n");
|
|
@@ -11790,6 +11720,23 @@ ${formatFiles(context.changedFiles)}
|
|
|
11790
11720
|
${priorBlock}
|
|
11791
11721
|
## Diff (PR #${context.prNumber}: ${context.baseSha}..${context.headSha})
|
|
11792
11722
|
|
|
11723
|
+
\`\`\`diff
|
|
11724
|
+
${context.diff.trimEnd()}
|
|
11725
|
+
\`\`\`
|
|
11726
|
+
`;
|
|
11727
|
+
}
|
|
11728
|
+
function buildShaRequest(context) {
|
|
11729
|
+
return `# Code review request
|
|
11730
|
+
|
|
11731
|
+
- Commit: \`${context.sha}\`
|
|
11732
|
+
- Parent: \`${context.parentSha}\`
|
|
11733
|
+
|
|
11734
|
+
## Changed files
|
|
11735
|
+
|
|
11736
|
+
${formatFiles(context.changedFiles)}
|
|
11737
|
+
|
|
11738
|
+
## Diff (commit ${context.sha}: ${context.parentSha}..${context.sha})
|
|
11739
|
+
|
|
11793
11740
|
\`\`\`diff
|
|
11794
11741
|
${context.diff.trimEnd()}
|
|
11795
11742
|
\`\`\`
|
|
@@ -11798,13 +11745,8 @@ ${context.diff.trimEnd()}
|
|
|
11798
11745
|
|
|
11799
11746
|
// src/commands/review/buildReviewPaths.ts
|
|
11800
11747
|
import { join as join35 } from "path";
|
|
11801
|
-
function buildReviewPaths(repoRoot,
|
|
11802
|
-
const reviewDir = join35(
|
|
11803
|
-
repoRoot,
|
|
11804
|
-
".assist",
|
|
11805
|
-
"reviews",
|
|
11806
|
-
`${branch}-${shortSha}`
|
|
11807
|
-
);
|
|
11748
|
+
function buildReviewPaths(repoRoot, key) {
|
|
11749
|
+
const reviewDir = join35(repoRoot, ".assist", "reviews", key);
|
|
11808
11750
|
return {
|
|
11809
11751
|
reviewDir,
|
|
11810
11752
|
requestPath: join35(reviewDir, "request.md"),
|
|
@@ -11815,9 +11757,9 @@ function buildReviewPaths(repoRoot, branch, shortSha) {
|
|
|
11815
11757
|
}
|
|
11816
11758
|
|
|
11817
11759
|
// src/commands/review/fetchExistingComments.ts
|
|
11818
|
-
import { execSync as
|
|
11760
|
+
import { execSync as execSync37 } from "child_process";
|
|
11819
11761
|
function fetchRawComments(org, repo, prNumber) {
|
|
11820
|
-
const out =
|
|
11762
|
+
const out = execSync37(
|
|
11821
11763
|
`gh api --paginate repos/${org}/${repo}/pulls/${prNumber}/comments`,
|
|
11822
11764
|
{ encoding: "utf-8", maxBuffer: 64 * 1024 * 1024 }
|
|
11823
11765
|
);
|
|
@@ -11847,6 +11789,86 @@ function fetchExistingComments() {
|
|
|
11847
11789
|
});
|
|
11848
11790
|
}
|
|
11849
11791
|
|
|
11792
|
+
// src/commands/review/gatherContext.ts
|
|
11793
|
+
import { execSync as execSync39 } from "child_process";
|
|
11794
|
+
|
|
11795
|
+
// src/commands/review/fetchPrDiffInfo.ts
|
|
11796
|
+
import { execSync as execSync38 } from "child_process";
|
|
11797
|
+
function getCurrentBranch2() {
|
|
11798
|
+
return execSync38("git rev-parse --abbrev-ref HEAD", {
|
|
11799
|
+
encoding: "utf-8"
|
|
11800
|
+
}).trim();
|
|
11801
|
+
}
|
|
11802
|
+
function fetchPrDiffInfo() {
|
|
11803
|
+
const { org, repo } = getRepoInfo();
|
|
11804
|
+
const branch = getCurrentBranch2();
|
|
11805
|
+
const fields = "number,baseRefName,baseRefOid,headRefName,headRefOid";
|
|
11806
|
+
let raw;
|
|
11807
|
+
try {
|
|
11808
|
+
raw = execSync38(`gh pr view ${branch} --json ${fields} -R ${org}/${repo}`, {
|
|
11809
|
+
encoding: "utf-8",
|
|
11810
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
11811
|
+
});
|
|
11812
|
+
} catch (error) {
|
|
11813
|
+
if (error instanceof Error && error.message.includes("no pull requests")) {
|
|
11814
|
+
console.error(
|
|
11815
|
+
`Error: No open pull request found for branch \`${branch}\`. Open a PR for this branch before running \`assist review\`.`
|
|
11816
|
+
);
|
|
11817
|
+
process.exit(1);
|
|
11818
|
+
}
|
|
11819
|
+
throw error;
|
|
11820
|
+
}
|
|
11821
|
+
const parsed = JSON.parse(raw);
|
|
11822
|
+
return {
|
|
11823
|
+
prNumber: parsed.number,
|
|
11824
|
+
baseRef: parsed.baseRefName,
|
|
11825
|
+
baseSha: parsed.baseRefOid,
|
|
11826
|
+
headRef: parsed.headRefName,
|
|
11827
|
+
headSha: parsed.headRefOid
|
|
11828
|
+
};
|
|
11829
|
+
}
|
|
11830
|
+
function fetchPrChangedFiles(prNumber) {
|
|
11831
|
+
const { org, repo } = getRepoInfo();
|
|
11832
|
+
const out = execSync38(`gh pr diff ${prNumber} --name-only -R ${org}/${repo}`, {
|
|
11833
|
+
encoding: "utf-8",
|
|
11834
|
+
maxBuffer: 64 * 1024 * 1024
|
|
11835
|
+
});
|
|
11836
|
+
return out.trim().split("\n").filter(Boolean);
|
|
11837
|
+
}
|
|
11838
|
+
function fetchPrDiff(prNumber) {
|
|
11839
|
+
const { org, repo } = getRepoInfo();
|
|
11840
|
+
return execSync38(`gh pr diff ${prNumber} -R ${org}/${repo}`, {
|
|
11841
|
+
encoding: "utf-8",
|
|
11842
|
+
maxBuffer: 256 * 1024 * 1024
|
|
11843
|
+
});
|
|
11844
|
+
}
|
|
11845
|
+
|
|
11846
|
+
// src/commands/review/gatherContext.ts
|
|
11847
|
+
function gatherContext() {
|
|
11848
|
+
const branch = execSync39("git rev-parse --abbrev-ref HEAD", {
|
|
11849
|
+
encoding: "utf-8"
|
|
11850
|
+
}).trim();
|
|
11851
|
+
const sha = execSync39("git rev-parse HEAD", { encoding: "utf-8" }).trim();
|
|
11852
|
+
const shortSha = execSync39("git rev-parse --short=7 HEAD", {
|
|
11853
|
+
encoding: "utf-8"
|
|
11854
|
+
}).trim();
|
|
11855
|
+
const prInfo = fetchPrDiffInfo();
|
|
11856
|
+
const changedFiles = fetchPrChangedFiles(prInfo.prNumber);
|
|
11857
|
+
const diff2 = fetchPrDiff(prInfo.prNumber);
|
|
11858
|
+
return {
|
|
11859
|
+
branch,
|
|
11860
|
+
sha,
|
|
11861
|
+
shortSha,
|
|
11862
|
+
prNumber: prInfo.prNumber,
|
|
11863
|
+
baseRef: prInfo.baseRef,
|
|
11864
|
+
baseSha: prInfo.baseSha,
|
|
11865
|
+
headRef: prInfo.headRef,
|
|
11866
|
+
headSha: prInfo.headSha,
|
|
11867
|
+
changedFiles,
|
|
11868
|
+
diff: diff2
|
|
11869
|
+
};
|
|
11870
|
+
}
|
|
11871
|
+
|
|
11850
11872
|
// src/commands/review/postReviewToPr.ts
|
|
11851
11873
|
import { readFileSync as readFileSync29 } from "fs";
|
|
11852
11874
|
|
|
@@ -12202,68 +12224,18 @@ async function runApplySession(synthesisPath) {
|
|
|
12202
12224
|
await done2;
|
|
12203
12225
|
}
|
|
12204
12226
|
|
|
12205
|
-
// src/commands/review/
|
|
12206
|
-
import {
|
|
12207
|
-
|
|
12208
|
-
|
|
12209
|
-
|
|
12210
|
-
|
|
12211
|
-
|
|
12212
|
-
|
|
12213
|
-
|
|
12214
|
-
|
|
12215
|
-
|
|
12216
|
-
|
|
12217
|
-
1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
|
|
12218
|
-
2. The issue is discrete and actionable \u2014 not a vague observation about the codebase or a tangle of several things.
|
|
12219
|
-
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).
|
|
12220
|
-
4. The issue was introduced by this change. Do not flag pre-existing bugs.
|
|
12221
|
-
5. The original author would likely fix it if made aware.
|
|
12222
|
-
6. It does not rely on unstated assumptions about the codebase or author's intent.
|
|
12223
|
-
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.
|
|
12224
|
-
8. It is clearly not an intentional change by the author.
|
|
12225
|
-
|
|
12226
|
-
## How to write the comment (Impact + Recommendation)
|
|
12227
|
-
|
|
12228
|
-
1. Make clear *why* the issue is a bug.
|
|
12229
|
-
2. Communicate severity accurately \u2014 do not inflate.
|
|
12230
|
-
3. Keep it brief: at most one paragraph of prose. Avoid line breaks inside the natural-language flow unless needed for a code fragment.
|
|
12231
|
-
4. Do not paste code chunks longer than 3 lines. Wrap short snippets in inline code or a fenced block.
|
|
12232
|
-
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.
|
|
12233
|
-
6. Tone is matter-of-fact: not accusatory, not gushing. Read as a helpful assistant, not a performative human reviewer.
|
|
12234
|
-
7. Write so the author grasps the point on first read.
|
|
12235
|
-
8. Avoid flattery and filler ("Great job\u2026", "Thanks for\u2026"). They are not useful to the author.
|
|
12236
|
-
|
|
12237
|
-
Ignore trivial style unless it obscures meaning or violates a documented standard. One finding per distinct issue.
|
|
12238
|
-
|
|
12239
|
-
## How many findings to return
|
|
12240
|
-
|
|
12241
|
-
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.
|
|
12242
|
-
|
|
12243
|
-
## Output format
|
|
12244
|
-
|
|
12245
|
-
For each finding include:
|
|
12246
|
-
- Severity (blocker, major, minor, nit) \u2014 see rubric below
|
|
12247
|
-
- File and line (e.g. \`src/foo.ts:42\`) when the finding is tied to a specific location
|
|
12248
|
-
- Impact: what could go wrong, including the conditions under which it manifests
|
|
12249
|
-
- Recommendation: a concrete change
|
|
12250
|
-
|
|
12251
|
-
Severity rubric:
|
|
12252
|
-
- **blocker** \u2014 ships broken behaviour: crash, data loss, security hole, breaks the build or existing tests, or violates a stated requirement.
|
|
12253
|
-
- **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."
|
|
12254
|
-
- **minor** \u2014 narrow correctness or clarity issue with limited blast radius; worth fixing but not urgent.
|
|
12255
|
-
- **nit** \u2014 style, naming, micro-refactors, comment wording; reviewer would not block on it.
|
|
12256
|
-
|
|
12257
|
-
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.
|
|
12258
|
-
|
|
12259
|
-
Group findings by severity. If you have no findings in a category, omit it. End with a short overall summary.
|
|
12260
|
-
|
|
12261
|
-
Output only the review Markdown. Do not include any preamble or commentary about the process.`;
|
|
12262
|
-
function buildReviewerStdin(requestPath) {
|
|
12263
|
-
return `${REVIEW_PROMPT}
|
|
12264
|
-
|
|
12265
|
-
The review request is at: ${requestPath}
|
|
12266
|
-
`;
|
|
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: "" };
|
|
12267
12239
|
}
|
|
12268
12240
|
|
|
12269
12241
|
// src/commands/review/MultiSpinner.ts
|
|
@@ -12368,18 +12340,114 @@ var MultiSpinner = class {
|
|
|
12368
12340
|
}
|
|
12369
12341
|
};
|
|
12370
12342
|
|
|
12371
|
-
// src/commands/review/
|
|
12372
|
-
import {
|
|
12373
|
-
function
|
|
12374
|
-
|
|
12375
|
-
|
|
12376
|
-
|
|
12377
|
-
}
|
|
12378
|
-
|
|
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";
|
|
12379
12362
|
}
|
|
12380
|
-
|
|
12381
|
-
|
|
12382
|
-
|
|
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
|
+
`;
|
|
12383
12451
|
}
|
|
12384
12452
|
|
|
12385
12453
|
// src/commands/review/printReviewerFailures.ts
|
|
@@ -12563,42 +12631,90 @@ function handleChildClose(args) {
|
|
|
12563
12631
|
return { exitCode, elapsedMs };
|
|
12564
12632
|
}
|
|
12565
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
|
+
|
|
12566
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
|
+
}
|
|
12567
12699
|
function startChild(spec) {
|
|
12568
12700
|
const child = spawn6(spec.command, spec.args, {
|
|
12569
12701
|
stdio: ["pipe", "pipe", "pipe"]
|
|
12570
12702
|
});
|
|
12571
12703
|
const flushPending = attachLineParser(child, spec.onLine);
|
|
12572
12704
|
const stderr = attachStderrCollector(child);
|
|
12573
|
-
child
|
|
12574
|
-
child.stdin?.end();
|
|
12705
|
+
writeStdinSafely(child, spec.stdin);
|
|
12575
12706
|
return { child, flushPending, stderr };
|
|
12576
12707
|
}
|
|
12577
|
-
function waitForExit(ctx) {
|
|
12578
|
-
return new Promise((resolve15, reject) => {
|
|
12579
|
-
ctx.child.on("error", reject);
|
|
12580
|
-
ctx.child.on("close", (code) => {
|
|
12581
|
-
ctx.flushPending();
|
|
12582
|
-
const closed = handleChildClose({
|
|
12583
|
-
code,
|
|
12584
|
-
startedAt: ctx.startedAt,
|
|
12585
|
-
name: ctx.name,
|
|
12586
|
-
stderr: ctx.stderr.value,
|
|
12587
|
-
quiet: ctx.quiet
|
|
12588
|
-
});
|
|
12589
|
-
resolve15({ ...closed, stderr: ctx.stderr.value });
|
|
12590
|
-
});
|
|
12591
|
-
});
|
|
12592
|
-
}
|
|
12593
12708
|
function runStreamingChild(spec) {
|
|
12594
12709
|
const startedAt = Date.now();
|
|
12595
12710
|
if (!spec.quiet) console.log(`[${spec.name}] starting`);
|
|
12596
12711
|
const { child, flushPending, stderr } = startChild(spec);
|
|
12597
|
-
return
|
|
12712
|
+
return waitForChildExit({
|
|
12598
12713
|
child,
|
|
12599
12714
|
flushPending,
|
|
12600
12715
|
stderr,
|
|
12601
12716
|
name: spec.name,
|
|
12717
|
+
command: spec.command,
|
|
12602
12718
|
startedAt,
|
|
12603
12719
|
quiet: spec.quiet ?? false
|
|
12604
12720
|
});
|
|
@@ -12636,6 +12752,19 @@ async function runClaudeReviewer(spec) {
|
|
|
12636
12752
|
return finaliseReviewerRun(spec, spinner, result);
|
|
12637
12753
|
}
|
|
12638
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
|
+
|
|
12639
12768
|
// src/commands/review/runCodexReviewer.ts
|
|
12640
12769
|
import { existsSync as existsSync34, unlinkSync as unlinkSync11 } from "fs";
|
|
12641
12770
|
|
|
@@ -12696,38 +12825,42 @@ async function runCodexReviewer(spec) {
|
|
|
12696
12825
|
return finaliseReviewerRun(spec, spinner, result);
|
|
12697
12826
|
}
|
|
12698
12827
|
|
|
12699
|
-
// src/commands/review/
|
|
12700
|
-
function
|
|
12701
|
-
if (
|
|
12702
|
-
|
|
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
|
+
});
|
|
12703
12842
|
}
|
|
12843
|
+
|
|
12844
|
+
// src/commands/review/runReviewers.ts
|
|
12704
12845
|
async function runReviewers(reviewDir, claudePath, codexPath, stdinPrompt, options2) {
|
|
12705
|
-
const
|
|
12706
|
-
|
|
12707
|
-
|
|
12708
|
-
|
|
12709
|
-
|
|
12710
|
-
|
|
12711
|
-
|
|
12712
|
-
const
|
|
12713
|
-
|
|
12714
|
-
|
|
12715
|
-
|
|
12716
|
-
|
|
12717
|
-
|
|
12718
|
-
|
|
12719
|
-
|
|
12720
|
-
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
reviewDir,
|
|
12724
|
-
stdin: stdinPrompt,
|
|
12725
|
-
outputPath: codexPath,
|
|
12726
|
-
spinner: codexSpinner
|
|
12727
|
-
})
|
|
12728
|
-
]);
|
|
12729
|
-
if (multi) printReviewerFailures(results);
|
|
12730
|
-
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 };
|
|
12731
12864
|
}
|
|
12732
12865
|
|
|
12733
12866
|
// src/commands/review/synthesise.ts
|
|
@@ -12736,7 +12869,9 @@ import { readFileSync as readFileSync30 } from "fs";
|
|
|
12736
12869
|
// src/commands/review/buildSynthesisStdin.ts
|
|
12737
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.
|
|
12738
12871
|
|
|
12739
|
-
|
|
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:
|
|
12740
12875
|
|
|
12741
12876
|
# Code review synthesis
|
|
12742
12877
|
|
|
@@ -12816,6 +12951,29 @@ async function synthesise(paths, options2) {
|
|
|
12816
12951
|
return result;
|
|
12817
12952
|
}
|
|
12818
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
|
+
|
|
12819
12977
|
// src/commands/review/useSpinnerUi.ts
|
|
12820
12978
|
function useSpinnerUi(verbose) {
|
|
12821
12979
|
if (verbose) return false;
|
|
@@ -12837,27 +12995,16 @@ function finishUi(ui, ok) {
|
|
|
12837
12995
|
else ui.elapsed.fail(label2);
|
|
12838
12996
|
}
|
|
12839
12997
|
async function runReviewPipeline(paths, options2) {
|
|
12998
|
+
const cachedClaude = cachedReviewerResult("claude", paths.claudePath);
|
|
12999
|
+
const codexPlan = await planCodexReviewer(paths.codexPath);
|
|
12840
13000
|
const ui = createUi(useSpinnerUi(options2.verbose));
|
|
12841
13001
|
try {
|
|
12842
|
-
const
|
|
12843
|
-
paths
|
|
12844
|
-
|
|
12845
|
-
|
|
12846
|
-
|
|
12847
|
-
|
|
12848
|
-
);
|
|
12849
|
-
if (results.every((r) => r.exitCode !== 0)) {
|
|
12850
|
-
console.error(
|
|
12851
|
-
"Both reviewers failed; skipping synthesis. See review folder for stderr details."
|
|
12852
|
-
);
|
|
12853
|
-
finishUi(ui, false);
|
|
12854
|
-
return false;
|
|
12855
|
-
}
|
|
12856
|
-
if (anyFresh && existsSync35(paths.synthesisPath)) {
|
|
12857
|
-
unlinkSync12(paths.synthesisPath);
|
|
12858
|
-
}
|
|
12859
|
-
const synthesisResult = await synthesise(paths, { multi: ui.multi });
|
|
12860
|
-
const ok = synthesisResult.exitCode === 0;
|
|
13002
|
+
const ok = await runAndSynthesise({
|
|
13003
|
+
paths,
|
|
13004
|
+
cachedClaude,
|
|
13005
|
+
codexPlan,
|
|
13006
|
+
multi: ui.multi
|
|
13007
|
+
});
|
|
12861
13008
|
finishUi(ui, ok);
|
|
12862
13009
|
return ok;
|
|
12863
13010
|
} catch (err) {
|
|
@@ -12866,19 +13013,7 @@ async function runReviewPipeline(paths, options2) {
|
|
|
12866
13013
|
}
|
|
12867
13014
|
}
|
|
12868
13015
|
|
|
12869
|
-
// src/commands/review/
|
|
12870
|
-
function resolveRepoRoot() {
|
|
12871
|
-
const repoRoot = findRepoRoot(process.cwd());
|
|
12872
|
-
if (repoRoot) return repoRoot;
|
|
12873
|
-
console.error("Error: not inside a git repository.");
|
|
12874
|
-
process.exit(1);
|
|
12875
|
-
}
|
|
12876
|
-
function validateOptions(options2) {
|
|
12877
|
-
if (options2.apply && options2.refine) {
|
|
12878
|
-
console.error("Error: --apply cannot be combined with --refine.");
|
|
12879
|
-
process.exit(1);
|
|
12880
|
-
}
|
|
12881
|
-
}
|
|
13016
|
+
// src/commands/review/reviewPr.ts
|
|
12882
13017
|
function logPriorComments(count) {
|
|
12883
13018
|
if (count === 0) return;
|
|
12884
13019
|
console.log(`Including ${count} prior review comment(s) in request.md.`);
|
|
@@ -12892,7 +13027,10 @@ function gatherChangedContext() {
|
|
|
12892
13027
|
process.exit(1);
|
|
12893
13028
|
}
|
|
12894
13029
|
function setupReviewDir(repoRoot, context, force) {
|
|
12895
|
-
const paths = buildReviewPaths(
|
|
13030
|
+
const paths = buildReviewPaths(
|
|
13031
|
+
repoRoot,
|
|
13032
|
+
`${context.branch}-${context.shortSha}`
|
|
13033
|
+
);
|
|
12896
13034
|
const priorComments = fetchExistingComments();
|
|
12897
13035
|
logPriorComments(priorComments?.length ?? 0);
|
|
12898
13036
|
prepareReviewDir(paths, buildRequest(context, priorComments), force);
|
|
@@ -12910,9 +13048,7 @@ async function runPostSynthesis(synthesisPath, options2) {
|
|
|
12910
13048
|
submit: options2.submit ?? false
|
|
12911
13049
|
});
|
|
12912
13050
|
}
|
|
12913
|
-
async function
|
|
12914
|
-
validateOptions(options2);
|
|
12915
|
-
const repoRoot = resolveRepoRoot();
|
|
13051
|
+
async function reviewPr(repoRoot, options2) {
|
|
12916
13052
|
const context = gatherChangedContext();
|
|
12917
13053
|
const paths = setupReviewDir(repoRoot, context, options2.force ?? false);
|
|
12918
13054
|
const synthesisOk = await runReviewPipeline(paths, {
|
|
@@ -12922,10 +13058,100 @@ async function review(options2 = {}) {
|
|
|
12922
13058
|
console.log(`Done. Review folder: ${paths.reviewDir}`);
|
|
12923
13059
|
}
|
|
12924
13060
|
|
|
13061
|
+
// src/commands/review/gatherShaContext.ts
|
|
13062
|
+
import { execSync as execSync40 } from "child_process";
|
|
13063
|
+
function resolveSha(ref, format2) {
|
|
13064
|
+
const flag = format2 === "short" ? "--short=7 " : "";
|
|
13065
|
+
try {
|
|
13066
|
+
return execSync40(`git rev-parse --verify ${flag}${ref}^{commit}`, {
|
|
13067
|
+
encoding: "utf-8",
|
|
13068
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
13069
|
+
}).trim();
|
|
13070
|
+
} catch {
|
|
13071
|
+
console.error(`Error: could not resolve commit \`${ref}\`.`);
|
|
13072
|
+
process.exit(1);
|
|
13073
|
+
}
|
|
13074
|
+
}
|
|
13075
|
+
function gatherShaContext(ref) {
|
|
13076
|
+
const sha = resolveSha(ref, "long");
|
|
13077
|
+
const shortSha = resolveSha(sha, "short");
|
|
13078
|
+
const parentSha = resolveSha(`${sha}^`, "long");
|
|
13079
|
+
const range = `${parentSha}..${sha}`;
|
|
13080
|
+
const changedFiles = execSync40(`git diff --name-only ${range}`, {
|
|
13081
|
+
encoding: "utf-8",
|
|
13082
|
+
maxBuffer: 64 * 1024 * 1024
|
|
13083
|
+
}).trim().split("\n").filter(Boolean);
|
|
13084
|
+
const diff2 = execSync40(`git diff ${range}`, {
|
|
13085
|
+
encoding: "utf-8",
|
|
13086
|
+
maxBuffer: 256 * 1024 * 1024
|
|
13087
|
+
});
|
|
13088
|
+
return { sha, shortSha, parentSha, changedFiles, diff: diff2 };
|
|
13089
|
+
}
|
|
13090
|
+
|
|
13091
|
+
// src/commands/review/reviewSha.ts
|
|
13092
|
+
function gatherShaChangedContext(ref) {
|
|
13093
|
+
const context = gatherShaContext(ref);
|
|
13094
|
+
if (context.changedFiles.length > 0) return context;
|
|
13095
|
+
console.error(
|
|
13096
|
+
`Error: commit ${context.sha} has no changed files \u2014 nothing to review.`
|
|
13097
|
+
);
|
|
13098
|
+
process.exit(1);
|
|
13099
|
+
}
|
|
13100
|
+
function setupShaReviewDir(repoRoot, context, force) {
|
|
13101
|
+
const paths = buildReviewPaths(repoRoot, context.shortSha);
|
|
13102
|
+
prepareReviewDir(paths, buildShaRequest(context), force);
|
|
13103
|
+
console.log(`Review folder: ${paths.reviewDir}`);
|
|
13104
|
+
return paths;
|
|
13105
|
+
}
|
|
13106
|
+
async function reviewSha(repoRoot, options2) {
|
|
13107
|
+
const context = gatherShaChangedContext(options2.sha);
|
|
13108
|
+
const paths = setupShaReviewDir(repoRoot, context, options2.force ?? false);
|
|
13109
|
+
await runReviewPipeline(paths, { verbose: options2.verbose ?? false });
|
|
13110
|
+
console.log(`Done. Review folder: ${paths.reviewDir}`);
|
|
13111
|
+
}
|
|
13112
|
+
|
|
13113
|
+
// src/commands/review/review.ts
|
|
13114
|
+
function resolveRepoRoot() {
|
|
13115
|
+
const repoRoot = findRepoRoot(process.cwd());
|
|
13116
|
+
if (repoRoot) return repoRoot;
|
|
13117
|
+
console.error("Error: not inside a git repository.");
|
|
13118
|
+
process.exit(1);
|
|
13119
|
+
}
|
|
13120
|
+
function rejectShaFlag(flag) {
|
|
13121
|
+
console.error(`Error: ${flag} cannot be combined with a SHA argument.`);
|
|
13122
|
+
process.exit(1);
|
|
13123
|
+
}
|
|
13124
|
+
function validateOptions(options2) {
|
|
13125
|
+
if (options2.apply && options2.refine) {
|
|
13126
|
+
console.error("Error: --apply cannot be combined with --refine.");
|
|
13127
|
+
process.exit(1);
|
|
13128
|
+
}
|
|
13129
|
+
if (!options2.sha) return;
|
|
13130
|
+
if (options2.refine) rejectShaFlag("--refine");
|
|
13131
|
+
if (options2.apply) rejectShaFlag("--apply");
|
|
13132
|
+
if (options2.submit) rejectShaFlag("--submit");
|
|
13133
|
+
}
|
|
13134
|
+
async function review(options2 = {}) {
|
|
13135
|
+
validateOptions(options2);
|
|
13136
|
+
const repoRoot = resolveRepoRoot();
|
|
13137
|
+
if (options2.sha) {
|
|
13138
|
+
await reviewSha(repoRoot, {
|
|
13139
|
+
sha: options2.sha,
|
|
13140
|
+
force: options2.force,
|
|
13141
|
+
verbose: options2.verbose
|
|
13142
|
+
});
|
|
13143
|
+
return;
|
|
13144
|
+
}
|
|
13145
|
+
await reviewPr(repoRoot, options2);
|
|
13146
|
+
}
|
|
13147
|
+
|
|
12925
13148
|
// src/commands/registerReview.ts
|
|
12926
13149
|
function registerReview(program2) {
|
|
12927
13150
|
program2.command("review").description(
|
|
12928
|
-
"Run Claude and Codex in parallel to review the current branch"
|
|
13151
|
+
"Run Claude and Codex in parallel to review the current branch, or a single commit when a SHA is given"
|
|
13152
|
+
).argument(
|
|
13153
|
+
"[sha]",
|
|
13154
|
+
"Optional commit SHA to review (sha^..sha); when provided, no PR lookup or GitHub posting happens"
|
|
12929
13155
|
).option(
|
|
12930
13156
|
"--no-prompt",
|
|
12931
13157
|
"Skip confirmation prompts; use flag defaults non-interactively"
|
|
@@ -12944,7 +13170,9 @@ function registerReview(program2) {
|
|
|
12944
13170
|
).option(
|
|
12945
13171
|
"--verbose",
|
|
12946
13172
|
"Disable spinner UI and use per-line log output (per-tool lines, starting/done lines)"
|
|
12947
|
-
).action(
|
|
13173
|
+
).action(
|
|
13174
|
+
(sha, options2) => review({ ...options2, sha })
|
|
13175
|
+
);
|
|
12948
13176
|
}
|
|
12949
13177
|
|
|
12950
13178
|
// src/commands/seq/seqAuth.ts
|
|
@@ -14218,7 +14446,7 @@ function registerVerify(program2) {
|
|
|
14218
14446
|
}
|
|
14219
14447
|
|
|
14220
14448
|
// src/commands/voice/devices.ts
|
|
14221
|
-
import { spawnSync as
|
|
14449
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
14222
14450
|
import { join as join43 } from "path";
|
|
14223
14451
|
|
|
14224
14452
|
// src/commands/voice/shared.ts
|
|
@@ -14251,7 +14479,7 @@ function getLockFile() {
|
|
|
14251
14479
|
// src/commands/voice/devices.ts
|
|
14252
14480
|
function devices() {
|
|
14253
14481
|
const script = join43(getPythonDir(), "list_devices.py");
|
|
14254
|
-
|
|
14482
|
+
spawnSync4(getVenvPython(), [script], { stdio: "inherit" });
|
|
14255
14483
|
}
|
|
14256
14484
|
|
|
14257
14485
|
// src/commands/voice/logs.ts
|
|
@@ -14283,12 +14511,12 @@ function logs(options2) {
|
|
|
14283
14511
|
}
|
|
14284
14512
|
|
|
14285
14513
|
// src/commands/voice/setup.ts
|
|
14286
|
-
import { spawnSync as
|
|
14514
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
14287
14515
|
import { mkdirSync as mkdirSync14 } from "fs";
|
|
14288
14516
|
import { join as join45 } from "path";
|
|
14289
14517
|
|
|
14290
14518
|
// src/commands/voice/checkLockFile.ts
|
|
14291
|
-
import { execSync as
|
|
14519
|
+
import { execSync as execSync41 } from "child_process";
|
|
14292
14520
|
import { existsSync as existsSync42, mkdirSync as mkdirSync13, readFileSync as readFileSync34, writeFileSync as writeFileSync29 } from "fs";
|
|
14293
14521
|
import { join as join44 } from "path";
|
|
14294
14522
|
function isProcessAlive2(pid) {
|
|
@@ -14317,7 +14545,7 @@ function bootstrapVenv() {
|
|
|
14317
14545
|
if (existsSync42(getVenvPython())) return;
|
|
14318
14546
|
console.log("Setting up Python environment...");
|
|
14319
14547
|
const pythonDir = getPythonDir();
|
|
14320
|
-
|
|
14548
|
+
execSync41(
|
|
14321
14549
|
`uv sync --project "${pythonDir}" --extra runtime --no-install-project`,
|
|
14322
14550
|
{
|
|
14323
14551
|
stdio: "inherit",
|
|
@@ -14344,7 +14572,7 @@ function setup() {
|
|
|
14344
14572
|
bootstrapVenv();
|
|
14345
14573
|
console.log("\nDownloading models...\n");
|
|
14346
14574
|
const script = join45(getPythonDir(), "setup_models.py");
|
|
14347
|
-
const result =
|
|
14575
|
+
const result = spawnSync5(getVenvPython(), [script], {
|
|
14348
14576
|
stdio: "inherit",
|
|
14349
14577
|
env: { ...process.env, VOICE_LOG_FILE: voicePaths.log }
|
|
14350
14578
|
});
|
|
@@ -14484,11 +14712,11 @@ import { randomBytes } from "crypto";
|
|
|
14484
14712
|
import chalk143 from "chalk";
|
|
14485
14713
|
|
|
14486
14714
|
// src/lib/openBrowser.ts
|
|
14487
|
-
import { execSync as
|
|
14715
|
+
import { execSync as execSync42 } from "child_process";
|
|
14488
14716
|
function tryExec(commands) {
|
|
14489
14717
|
for (const cmd of commands) {
|
|
14490
14718
|
try {
|
|
14491
|
-
|
|
14719
|
+
execSync42(cmd);
|
|
14492
14720
|
return true;
|
|
14493
14721
|
} catch {
|
|
14494
14722
|
}
|
|
@@ -14830,11 +15058,11 @@ function resolveParams(params, cliArgs) {
|
|
|
14830
15058
|
}
|
|
14831
15059
|
|
|
14832
15060
|
// src/commands/run/runPreCommands.ts
|
|
14833
|
-
import { execSync as
|
|
15061
|
+
import { execSync as execSync43 } from "child_process";
|
|
14834
15062
|
function runPreCommands(pre, cwd) {
|
|
14835
15063
|
for (const cmd of pre) {
|
|
14836
15064
|
try {
|
|
14837
|
-
|
|
15065
|
+
execSync43(cmd, { stdio: "inherit", cwd });
|
|
14838
15066
|
} catch (err) {
|
|
14839
15067
|
const code = err && typeof err === "object" && "status" in err ? err.status : 1;
|
|
14840
15068
|
process.exit(code);
|
|
@@ -15097,7 +15325,7 @@ function registerRun(program2) {
|
|
|
15097
15325
|
}
|
|
15098
15326
|
|
|
15099
15327
|
// src/commands/screenshot/index.ts
|
|
15100
|
-
import { execSync as
|
|
15328
|
+
import { execSync as execSync44 } from "child_process";
|
|
15101
15329
|
import { existsSync as existsSync47, mkdirSync as mkdirSync17, unlinkSync as unlinkSync15, writeFileSync as writeFileSync32 } from "fs";
|
|
15102
15330
|
import { tmpdir as tmpdir7 } from "os";
|
|
15103
15331
|
import { join as join51, resolve as resolve13 } from "path";
|
|
@@ -15240,7 +15468,7 @@ function runPowerShellScript(processName, outputPath) {
|
|
|
15240
15468
|
const scriptPath = join51(tmpdir7(), `assist-screenshot-${Date.now()}.ps1`);
|
|
15241
15469
|
writeFileSync32(scriptPath, captureWindowPs1, "utf-8");
|
|
15242
15470
|
try {
|
|
15243
|
-
|
|
15471
|
+
execSync44(
|
|
15244
15472
|
`powershell -NoProfile -ExecutionPolicy Bypass -File "${scriptPath}" -ProcessName "${processName}" -OutputPath "${outputPath}"`,
|
|
15245
15473
|
{ stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }
|
|
15246
15474
|
);
|
|
@@ -15641,7 +15869,7 @@ function syncCommands(claudeDir, targetBase) {
|
|
|
15641
15869
|
}
|
|
15642
15870
|
|
|
15643
15871
|
// src/commands/update.ts
|
|
15644
|
-
import { execSync as
|
|
15872
|
+
import { execSync as execSync45 } from "child_process";
|
|
15645
15873
|
import * as path51 from "path";
|
|
15646
15874
|
function isGlobalNpmInstall(dir) {
|
|
15647
15875
|
try {
|
|
@@ -15649,7 +15877,7 @@ function isGlobalNpmInstall(dir) {
|
|
|
15649
15877
|
if (resolved.split(path51.sep).includes("node_modules")) {
|
|
15650
15878
|
return true;
|
|
15651
15879
|
}
|
|
15652
|
-
const globalPrefix =
|
|
15880
|
+
const globalPrefix = execSync45("npm prefix -g", { stdio: "pipe" }).toString().trim();
|
|
15653
15881
|
return resolved.toLowerCase().startsWith(path51.resolve(globalPrefix).toLowerCase());
|
|
15654
15882
|
} catch {
|
|
15655
15883
|
return false;
|
|
@@ -15660,18 +15888,18 @@ async function update2() {
|
|
|
15660
15888
|
console.log(`Assist is installed at: ${installDir}`);
|
|
15661
15889
|
if (isGitRepo(installDir)) {
|
|
15662
15890
|
console.log("Detected git repo installation, pulling latest...");
|
|
15663
|
-
|
|
15891
|
+
execSync45("git pull", { cwd: installDir, stdio: "inherit" });
|
|
15664
15892
|
console.log("Installing dependencies...");
|
|
15665
|
-
|
|
15893
|
+
execSync45("npm i", { cwd: installDir, stdio: "inherit" });
|
|
15666
15894
|
console.log("Building...");
|
|
15667
|
-
|
|
15895
|
+
execSync45("npm run build", { cwd: installDir, stdio: "inherit" });
|
|
15668
15896
|
console.log("Syncing commands...");
|
|
15669
|
-
|
|
15897
|
+
execSync45("assist sync", { stdio: "inherit" });
|
|
15670
15898
|
} else if (isGlobalNpmInstall(installDir)) {
|
|
15671
15899
|
console.log("Detected global npm installation, updating...");
|
|
15672
|
-
|
|
15900
|
+
execSync45("npm i -g @staff0rd/assist@latest", { stdio: "inherit" });
|
|
15673
15901
|
console.log("Syncing commands...");
|
|
15674
|
-
|
|
15902
|
+
execSync45("assist sync", { stdio: "inherit" });
|
|
15675
15903
|
} else {
|
|
15676
15904
|
console.error(
|
|
15677
15905
|
"Could not determine installation method. Expected a git repo or global npm install."
|