co-maintainer 0.5.0 → 0.5.1-beta.2
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/LICENSE +21 -21
- package/README.md +5 -3
- package/dist/package.json +1 -1
- package/dist/src/ai/anthropic.d.ts +55 -0
- package/dist/src/ai/anthropic.js +262 -0
- package/dist/src/ai/batch.d.ts +2 -2
- package/dist/src/ai/batch.js +1 -1
- package/dist/src/ai/estimate.d.ts +4 -0
- package/dist/src/ai/estimate.js +17 -2
- package/dist/src/ai/mermaid_loop.js +7 -4
- package/dist/src/ai/openai.d.ts +45 -0
- package/dist/src/ai/openai.js +206 -0
- package/dist/src/ai/opencode.d.ts +10 -0
- package/dist/src/ai/opencode.js +76 -0
- package/dist/src/ai/openrouter.d.ts +23 -7
- package/dist/src/ai/openrouter.js +39 -23
- package/dist/src/ai/pricing.js +1 -1
- package/dist/src/ai/provider.d.ts +9 -0
- package/dist/src/ai/provider.js +42 -3
- package/dist/src/ai/verify.d.ts +9 -1
- package/dist/src/ai/verify.js +81 -3
- package/dist/src/cli/args.d.ts +1 -2
- package/dist/src/cli/args.js +38 -26
- package/dist/src/cli/commands/config.d.ts +1 -1
- package/dist/src/cli/commands/config.js +10 -4
- package/dist/src/cli/commands/probe.js +3 -11
- package/dist/src/cli/commands/registry.d.ts +1 -9
- package/dist/src/cli/commands/registry.js +75 -10
- package/dist/src/cli/commands/review.js +18 -13
- package/dist/src/cli/commands/rollback.d.ts +1 -0
- package/dist/src/cli/commands/rollback.js +42 -0
- package/dist/src/cli/commands/serve.js +2 -3
- package/dist/src/cli/commands/set.js +71 -7
- package/dist/src/cli/commands/view.js +10 -9
- package/dist/src/cli/error.d.ts +4 -4
- package/dist/src/cli/error.js +4 -4
- package/dist/src/cli/main.d.ts +4 -4
- package/dist/src/cli/main.js +13 -8
- package/dist/src/cli/prompt.js +1 -1
- package/dist/src/cli/review_args.d.ts +9 -3
- package/dist/src/cli/review_args.js +28 -5
- package/dist/src/cli/review_github.d.ts +31 -0
- package/dist/src/cli/review_github.js +128 -0
- package/dist/src/cli/review_result.d.ts +27 -16
- package/dist/src/cli/review_result.js +7 -7
- package/dist/src/config.d.ts +28 -6
- package/dist/src/config.js +15 -1
- package/dist/src/git/default_branch.d.ts +1 -1
- package/dist/src/git/default_branch.js +1 -1
- package/dist/src/github/app.js +2 -2
- package/dist/src/github/app_manifest.d.ts +1 -7
- package/dist/src/github/app_manifest.js +2 -7
- package/dist/src/github/client.d.ts +4 -1
- package/dist/src/github/client.js +14 -2
- package/dist/src/github/gh.js +10 -7
- package/dist/src/github/pat.js +4 -4
- package/dist/src/github/permissions.d.ts +36 -0
- package/dist/src/github/permissions.js +334 -0
- package/dist/src/knowledge/facts.js +1 -1
- package/dist/src/knowledge/guide.d.ts +3 -3
- package/dist/src/knowledge/guide.js +5 -5
- package/dist/src/knowledge/probe.js +1 -2
- package/dist/src/knowledge/sections.d.ts +3 -3
- package/dist/src/knowledge/sections.js +3 -3
- package/dist/src/knowledge/skill.d.ts +1 -1
- package/dist/src/knowledge/skill.js +4 -4
- package/dist/src/knowledge/synthesis.d.ts +1 -1
- package/dist/src/knowledge/synthesis.js +5 -5
- package/dist/src/knowledge/types.d.ts +1 -1
- package/dist/src/local/codegraph_exclude.d.ts +1 -1
- package/dist/src/local/codegraph_exclude.js +1 -1
- package/dist/src/local/codegraph_prepare.js +1 -1
- package/dist/src/local/git_ops.d.ts +1 -1
- package/dist/src/local/git_ops.js +1 -1
- package/dist/src/local/git_parse.d.ts +2 -2
- package/dist/src/local/git_parse.js +2 -2
- package/dist/src/local/git_revision.d.ts +1 -1
- package/dist/src/local/git_revision.js +1 -1
- package/dist/src/local/git_untracked.d.ts +1 -1
- package/dist/src/local/git_untracked.js +1 -1
- package/dist/src/local/review_local.js +17 -14
- package/dist/src/local/review_lock.d.ts +1 -1
- package/dist/src/local/review_lock.js +1 -1
- package/dist/src/pr/checkout.js +2 -2
- package/dist/src/pr/findings.js +1 -1
- package/dist/src/pr/findings_json.d.ts +2 -2
- package/dist/src/pr/findings_json.js +1 -1
- package/dist/src/pr/github_revision.js +1 -1
- package/dist/src/pr/review_copy.d.ts +2 -2
- package/dist/src/pr/review_copy.js +2 -2
- package/dist/src/pr/reviewer.d.ts +6 -0
- package/dist/src/pr/reviewer.js +49 -39
- package/dist/src/remote/client.d.ts +4 -0
- package/dist/src/remote/client.js +84 -23
- package/dist/src/remote/fixtures.js +1 -1
- package/dist/src/remote/http.d.ts +10 -1
- package/dist/src/remote/http.js +15 -3
- package/dist/src/remote/schema.d.ts +3 -3
- package/dist/src/remote/schema.js +3 -3
- package/dist/src/remote/server/body.d.ts +1 -1
- package/dist/src/remote/server/body.js +1 -1
- package/dist/src/remote/server/byok_keys.d.ts +15 -0
- package/dist/src/remote/server/byok_keys.js +40 -0
- package/dist/src/remote/server/guides.js +2 -2
- package/dist/src/remote/server/routes.js +8 -1
- package/dist/src/remote/validate.js +15 -1
- package/dist/src/review/blocking.d.ts +2 -2
- package/dist/src/review/blocking.js +2 -2
- package/dist/src/review/carry_over.d.ts +2 -2
- package/dist/src/review/carry_over.js +2 -2
- package/dist/src/review/guides.js +1 -1
- package/dist/src/server/api/remote_tokens.js +2 -0
- package/dist/src/server/api/repos.js +51 -0
- package/dist/src/server/api/settings.js +10 -0
- package/dist/src/server/errors.d.ts +1 -1
- package/dist/src/server/http.js +1 -1
- package/dist/src/server/pages/activity.d.ts +2 -0
- package/dist/src/server/pages/activity.js +18 -4
- package/dist/src/server/pages/add_repo.js +61 -1
- package/dist/src/server/pages/analytics.js +18 -8
- package/dist/src/server/pages/cost.d.ts +21 -0
- package/dist/src/server/pages/cost.js +72 -0
- package/dist/src/server/pages/pr.js +5 -4
- package/dist/src/server/pages/repo.js +3 -2
- package/dist/src/server/pages/repo_prs.js +3 -2
- package/dist/src/server/pages/repo_remote.js +3 -2
- package/dist/src/server/pages/repo_settings.js +326 -12
- package/dist/src/server/pages/router.js +3 -2
- package/dist/src/server/pages/settings.js +50 -6
- package/dist/src/server/pages/styles.d.ts +1 -1
- package/dist/src/server/pages/styles.js +1 -1
- package/dist/src/services/credentials.js +8 -11
- package/dist/src/services/dashboard.d.ts +22 -9
- package/dist/src/services/dashboard.js +16 -2
- package/dist/src/services/probe.js +1 -1
- package/dist/src/services/remote_review.js +142 -35
- package/dist/src/services/remote_review_abort.d.ts +5 -1
- package/dist/src/services/remote_review_abort.js +9 -1
- package/dist/src/services/review.d.ts +1 -1
- package/dist/src/services/review.js +19 -5
- package/dist/src/services/review_policy.d.ts +116 -0
- package/dist/src/services/review_policy.js +489 -0
- package/dist/src/services/setup.d.ts +4 -3
- package/dist/src/services/setup.js +18 -16
- package/dist/src/services/setup_checklist.js +2 -2
- package/dist/src/services/webhook.d.ts +23 -0
- package/dist/src/services/webhook.js +141 -34
- package/dist/src/store/app_db.d.ts +5 -0
- package/dist/src/store/app_db.js +69 -13
- package/dist/src/store/backup.d.ts +31 -0
- package/dist/src/store/backup.js +158 -0
- package/dist/src/store/migrations.d.ts +1 -1
- package/dist/src/store/migrations.js +21 -1
- package/dist/src/store/remote_review_inputs.d.ts +3 -0
- package/dist/src/store/remote_review_inputs.js +3 -3
- package/dist/src/store/remote_tokens.d.ts +1 -1
- package/dist/src/store/remote_tokens.js +1 -1
- package/dist/src/store/repos.d.ts +2 -2
- package/dist/src/store/repos.js +1 -1
- package/dist/src/store/reviews.d.ts +21 -13
- package/dist/src/store/reviews.js +60 -15
- package/dist/src/store/rows.d.ts +10 -0
- package/dist/src/tools/codegraph.d.ts +4 -4
- package/dist/src/tools/codegraph.js +5 -5
- package/dist/src/tools/codegraph_exec.d.ts +1 -1
- package/dist/src/tools/codegraph_exec.js +1 -1
- package/dist/src/types.d.ts +12 -6
- package/dist/src/util/cost.d.ts +52 -0
- package/dist/src/util/cost.js +70 -0
- package/dist/src/util/log.d.ts +1 -1
- package/dist/src/util/log.js +2 -2
- package/dist/src/util/redact.js +2 -0
- package/dist/src/util/run_summary.d.ts +7 -2
- package/dist/src/util/run_summary.js +28 -2
- package/dist/src/util/time.d.ts +1 -1
- package/dist/src/util/time.js +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { join } from "node:path";
|
|
|
2
2
|
import { normalizePath } from "../review/revision.js";
|
|
3
3
|
import { lstat, readFile, readLink } from "../util/runtime.js";
|
|
4
4
|
const NUL_SCAN_BYTES = 8000;
|
|
5
|
-
/** Build patch for a new untracked file
|
|
5
|
+
/** Build patch for a new untracked file. */
|
|
6
6
|
export function patchFromNewFileContent(content) {
|
|
7
7
|
const scan = content.subarray(0, Math.min(content.length, NUL_SCAN_BYTES));
|
|
8
8
|
if (scan.includes(0)) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseArgs } from "../cli/args.js";
|
|
2
2
|
import { filterReviewConfigArgs, } from "../cli/review_args.js";
|
|
3
|
-
import { buildLocalReviewJson, formatHumanLocalReview, resolvedFromFirstReview, reviewExitCodeFromResolved, } from "../cli/review_result.js";
|
|
4
|
-
import { emptyAiMetrics, recordAiCost, runInitOrRemake, } from "../services/setup.js";
|
|
3
|
+
import { buildLocalReviewJson, formatHumanLocalReview, humanFindingsFromResolved, resolvedFromFirstReview, reviewExitCodeFromResolved, } from "../cli/review_result.js";
|
|
4
|
+
import { addAiMetrics, emptyAiMetrics, recordAiCost, runInitOrRemake, } from "../services/setup.js";
|
|
5
5
|
import { aiFor } from "../services/review.js";
|
|
6
6
|
import { loadGuides } from "../review/guides.js";
|
|
7
7
|
import { buildCarryPromptSection, classifyCarryItems, guideRebuiltSince, incrementalDiffPaths, parsePreviousVerdicts, resolveCarryOutcomes, } from "../review/carry_over.js";
|
|
@@ -11,6 +11,7 @@ import { reviewWorkspaceRevision } from "../pr/reviewer.js";
|
|
|
11
11
|
import { runCommand } from "../pr/checkout.js";
|
|
12
12
|
import { log, startHeartbeat, timed, withCliLogsToStderr, } from "../util/log.js";
|
|
13
13
|
import { printRunSummary, summaryFromMetrics } from "../util/run_summary.js";
|
|
14
|
+
import { emitGithubOutput } from "../cli/review_github.js";
|
|
14
15
|
import { setCliInteractive } from "../cli/args.js";
|
|
15
16
|
import { prepareLocalCodegraph } from "./codegraph_prepare.js";
|
|
16
17
|
import { canPrompt } from "../tools/codegraph.js";
|
|
@@ -19,6 +20,7 @@ import { assertGitQuiet, currentBranch, detectRemoteRepo, gitRoot, headSha, Revi
|
|
|
19
20
|
import { EXIT_RUNTIME, exitWith } from "../cli/error.js";
|
|
20
21
|
import { buildLocalRevision, mergeBase, resolveBaseRef, } from "./git_revision.js";
|
|
21
22
|
import { clearLocalCarry, localSubjectId, saveLocalCarry, tryLoadLocalCarry, } from "./carry_over_store.js";
|
|
23
|
+
import { costUsage } from "../util/cost.js";
|
|
22
24
|
function storedFindings(resolved) {
|
|
23
25
|
return resolved
|
|
24
26
|
.filter((row) => row.state === "new" || row.state === "open")
|
|
@@ -49,7 +51,7 @@ function fail(error, json) {
|
|
|
49
51
|
console.error(`Hint: ${error.hint}`);
|
|
50
52
|
}
|
|
51
53
|
// Set the exit code and return; `process.exit` here would assert in libuv on
|
|
52
|
-
// Windows whenever a fetch pool is open
|
|
54
|
+
// Windows whenever a fetch pool is open.
|
|
53
55
|
exitWith(error.exitCode);
|
|
54
56
|
}
|
|
55
57
|
function installInterruptCleanup(releaseSync, json) {
|
|
@@ -159,7 +161,7 @@ export async function runLocalReview(cli) {
|
|
|
159
161
|
let previous = carryLoad.data;
|
|
160
162
|
// A guide rebuilt after the last review judged its findings under rules
|
|
161
163
|
// that no longer exist. Rather than let those stale findings mask new
|
|
162
|
-
// ones, start fresh automatically
|
|
164
|
+
// ones, start fresh automatically — the same effect as
|
|
163
165
|
// `--fresh`, without making the user remember the flag.
|
|
164
166
|
if (previous &&
|
|
165
167
|
guideRebuiltSince(previous.guideBuiltAt, guides.guideBuiltAt)) {
|
|
@@ -189,7 +191,7 @@ export async function runLocalReview(cli) {
|
|
|
189
191
|
enabled: options.useCodegraph === true,
|
|
190
192
|
allowInstall: cli.allowToolInstall,
|
|
191
193
|
// `--json` output must stay machine-readable, so never prompt then;
|
|
192
|
-
// `canPrompt` adds the TTY and CI checks
|
|
194
|
+
// `canPrompt` adds the TTY and CI checks.
|
|
193
195
|
interactive: !json && canPrompt(),
|
|
194
196
|
});
|
|
195
197
|
extras.prepareCodegraphTools = () => Promise.resolve(codegraphPrep.tools);
|
|
@@ -199,19 +201,13 @@ export async function runLocalReview(cli) {
|
|
|
199
201
|
let response;
|
|
200
202
|
try {
|
|
201
203
|
response = await timed("local review AI", options.logTime, () => reviewWorkspaceRevision(revision, options, sha, async (usage) => {
|
|
202
|
-
aiMetrics
|
|
203
|
-
aiMetrics.tokensIn += usage.tokensIn;
|
|
204
|
-
aiMetrics.tokensOut += usage.tokensOut;
|
|
205
|
-
if (usage.cost === undefined)
|
|
206
|
-
aiMetrics.costKnown = false;
|
|
207
|
-
else
|
|
208
|
-
aiMetrics.cost += usage.cost;
|
|
204
|
+
addAiMetrics(aiMetrics, usage);
|
|
209
205
|
await recordAiCost(repo, "review_local", usage);
|
|
210
206
|
}, aiFor(options), (message) => log("review", message), extras));
|
|
211
207
|
}
|
|
212
208
|
finally {
|
|
213
209
|
// Stopping the heartbeat on the error path too, otherwise the interval
|
|
214
|
-
// keeps the event loop alive and a post-fetch exit never lands
|
|
210
|
+
// keeps the event loop alive and a post-fetch exit never lands.
|
|
215
211
|
stopHeartbeat();
|
|
216
212
|
await lock?.release();
|
|
217
213
|
lock = null;
|
|
@@ -255,7 +251,7 @@ export async function runLocalReview(cli) {
|
|
|
255
251
|
const usage = {
|
|
256
252
|
tokensIn: aiMetrics.tokensIn,
|
|
257
253
|
tokensOut: aiMetrics.tokensOut,
|
|
258
|
-
|
|
254
|
+
...costUsage(aiMetrics),
|
|
259
255
|
};
|
|
260
256
|
if (json) {
|
|
261
257
|
console.log(buildLocalReviewJson({
|
|
@@ -273,6 +269,13 @@ export async function runLocalReview(cli) {
|
|
|
273
269
|
reviewBlocking: options.reviewBlocking,
|
|
274
270
|
}));
|
|
275
271
|
}
|
|
272
|
+
else if (cli.output === "github") {
|
|
273
|
+
emitGithubOutput({
|
|
274
|
+
title: header,
|
|
275
|
+
findings: humanFindingsFromResolved(allResolved, options.reviewBlocking),
|
|
276
|
+
run: summaryFromMetrics(aiMetrics, performance.now() - started),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
276
279
|
else {
|
|
277
280
|
console.log("\n" +
|
|
278
281
|
formatHumanLocalReview(header, revision, response.guideBuiltAt, codegraphState, allResolved, warnings, options.reviewBlocking) +
|
|
@@ -3,5 +3,5 @@ export type LocalReviewLock = {
|
|
|
3
3
|
/** For signal handlers — must not await before exit. */
|
|
4
4
|
releaseSync: () => void;
|
|
5
5
|
};
|
|
6
|
-
/** One local review per repo root
|
|
6
|
+
/** One local review per repo root. */
|
|
7
7
|
export declare function acquireLocalReviewLock(gitRoot: string): Promise<LocalReviewLock>;
|
|
@@ -11,7 +11,7 @@ async function lockPath(gitRoot) {
|
|
|
11
11
|
await mkdir(dir, { recursive: true });
|
|
12
12
|
return `${dir}/${hex}.lock`;
|
|
13
13
|
}
|
|
14
|
-
/** One local review per repo root
|
|
14
|
+
/** One local review per repo root. */
|
|
15
15
|
export async function acquireLocalReviewLock(gitRoot) {
|
|
16
16
|
const path = await lockPath(gitRoot);
|
|
17
17
|
let existingPid;
|
package/dist/src/pr/checkout.js
CHANGED
|
@@ -34,7 +34,7 @@ async function cloneInto(repo, dir, run) {
|
|
|
34
34
|
const started = performance.now();
|
|
35
35
|
// `-c core.longpaths=true` has to be in effect for the clone itself: a deep
|
|
36
36
|
// source tree can overrun the Windows path limit while the objects are being
|
|
37
|
-
// written, and setting the config afterwards is too late to help.
|
|
37
|
+
// written, and setting the config afterwards is too late to help.
|
|
38
38
|
const result = await run("git", [
|
|
39
39
|
"-c",
|
|
40
40
|
"core.longpaths=true",
|
|
@@ -45,7 +45,7 @@ async function cloneInto(repo, dir, run) {
|
|
|
45
45
|
]);
|
|
46
46
|
if (result.code !== 0) {
|
|
47
47
|
// One actionable line, not the raw multi-line git transcript: the caller
|
|
48
|
-
// only needs to know the clone failed and what it costs them.
|
|
48
|
+
// only needs to know the clone failed and what it costs them.
|
|
49
49
|
const firstLine = result.stderr
|
|
50
50
|
.split(/\r?\n/)
|
|
51
51
|
.map((line) => line.trim())
|
package/dist/src/pr/findings.js
CHANGED
|
@@ -3,7 +3,7 @@ const FILE_LINE = /(?<path>(?:[\w.-]+\/)*[\w.-]+\.[A-Za-z0-9]+):(?<from>\d+)(?:-
|
|
|
3
3
|
* sometimes writes `:` where the prompt asked for the em dash, and sometimes
|
|
4
4
|
* leaves the symbol off. Both are accepted: an unrecognised heading used to
|
|
5
5
|
* send every finding down the fallback path, which is what produced the
|
|
6
|
-
* truncated and shifted output
|
|
6
|
+
* truncated and shifted output. The separator may also sit directly
|
|
7
7
|
* against the path (`\`src/a.ts\`: \`helper()\``), which is how the model
|
|
8
8
|
* usually writes the colon form. */
|
|
9
9
|
const NEW_HEADING = /^\[(?<severity>P[0-3])\s*·\s*(?<impact>blocking|non-blocking)\]\s+(?<path>.+?)(?:\s*(?:—|:)\s*(?<symbol>.+))?$/;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/** Structured review output
|
|
1
|
+
/** Structured review output.
|
|
2
2
|
*
|
|
3
|
-
*
|
|
3
|
+
* Findings were truncated mid-sentence (`... while \`u`) and split at the
|
|
4
4
|
* wrong offsets, because the model's free-form Markdown was sliced by the
|
|
5
5
|
* fallback parser. The fix is to have the model return JSON and generate the
|
|
6
6
|
* Markdown ourselves, so a finding's boundaries can no longer be guessed from
|
|
@@ -78,7 +78,7 @@ function asText(value) {
|
|
|
78
78
|
}
|
|
79
79
|
/** Like {@link asText} but keeps the leading indentation. A suggestion is
|
|
80
80
|
* source lines with their original indentation, so trimming the left edge
|
|
81
|
-
* changes the code it proposes
|
|
81
|
+
* changes the code it proposes. Only the outer blank lines go. */
|
|
82
82
|
function asSource(value) {
|
|
83
83
|
return typeof value === "string"
|
|
84
84
|
? value.replace(/^\s*\n/, "").replace(/\s+$/, "")
|
|
@@ -14,7 +14,7 @@ export function githubFileToRevisionFile(file) {
|
|
|
14
14
|
const additions = Number(file.additions ?? 0);
|
|
15
15
|
const deletions = Number(file.deletions ?? 0);
|
|
16
16
|
const patch = String(file.patch ?? "");
|
|
17
|
-
// GitHub omits large text patches; do not treat that as binary
|
|
17
|
+
// GitHub omits large text patches; do not treat that as binary.
|
|
18
18
|
const binary = false;
|
|
19
19
|
return {
|
|
20
20
|
path,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** The severity legend and the review-copy rule
|
|
1
|
+
/** The severity legend and the review-copy rule.
|
|
2
2
|
*
|
|
3
3
|
* Product text avoids the em dash and the semicolon: the docs linter rejects
|
|
4
4
|
* them, and the finding parser had to accept both a `—` and a `:` heading
|
|
@@ -15,6 +15,6 @@ export declare const SEVERITY_LEGEND: ReadonlyArray<{
|
|
|
15
15
|
/** The `## Severity` block the review text starts with. The colon keeps it
|
|
16
16
|
* free of the em dash the old prompt used. */
|
|
17
17
|
export declare function severitySection(): string;
|
|
18
|
-
/** What the model is told about prose
|
|
18
|
+
/** What the model is told about prose. The review body is rendered
|
|
19
19
|
* by our code, not the model, so this only has to keep the fields clean. */
|
|
20
20
|
export declare const REVIEW_COPY_RULE = "Write prose without the em dash or the semicolon. Use a colon or a comma instead.";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** The severity legend and the review-copy rule
|
|
1
|
+
/** The severity legend and the review-copy rule.
|
|
2
2
|
*
|
|
3
3
|
* Product text avoids the em dash and the semicolon: the docs linter rejects
|
|
4
4
|
* them, and the finding parser had to accept both a `—` and a `:` heading
|
|
@@ -35,6 +35,6 @@ export function severitySection() {
|
|
|
35
35
|
const items = SEVERITY_LEGEND.map((row) => `- ${row.level}: ${row.name}: ${row.description}.`);
|
|
36
36
|
return `## Severity\n\n${items.join("\n")}\n`;
|
|
37
37
|
}
|
|
38
|
-
/** What the model is told about prose
|
|
38
|
+
/** What the model is told about prose. The review body is rendered
|
|
39
39
|
* by our code, not the model, so this only has to keep the fields clean. */
|
|
40
40
|
export const REVIEW_COPY_RULE = "Write prose without the em dash or the semicolon. Use a colon or a comma instead.";
|
|
@@ -14,6 +14,12 @@ export declare const MERMAID_GUIDANCE = "Mermaid selection and minimal syntax:\n
|
|
|
14
14
|
* asking for a diagram only invites invented syntax. */
|
|
15
15
|
export declare const NO_DIAGRAM_RULES = "Do not use Mermaid or any other diagram. Explain with prose only.";
|
|
16
16
|
export declare function reviewSystemPrompt(diagrams: boolean): string;
|
|
17
|
+
/** The guide block every review sends, kept as the stable head of the system
|
|
18
|
+
* prompt rather than the user prompt so a provider with prompt caching
|
|
19
|
+
* (Anthropic's `cache_control`) reads it instead of re-billing it. Nothing
|
|
20
|
+
* time-varying may enter this string — a single differing byte makes the cache
|
|
21
|
+
* miss. */
|
|
22
|
+
export declare function reviewGuidesBlock(guide: string, detailed: string, codebase: string): string;
|
|
17
23
|
/** Shared PR and local/remote workspace review instructions: confirm claims
|
|
18
24
|
* against the indexed graph, not only the diff slice. */
|
|
19
25
|
export declare const CODEGRAPH_DIFF_VERIFICATION = "Examine the changes line by line, not just file by file. A single file can\ncontain more than one independent defect, and a change that looks fine in\nisolation can be wrong once you trace what calls it or what else it affects.\nWhen a finding depends on behavior outside the changed lines, confirm it with\ncodegraph-node, codegraph-callers, codegraph-callees, codegraph-impact, and\ncodegraph-affected before reporting it. Drop or correct findings that only seem\nplausible from the diff but contradict unchanged callers, callees, or the same\npattern elsewhere in the repo. Use those tools to check blast radius and whether\na test reaches the path. Do not guess coverage or impact from the diff alone\nwhen a tool can answer. A missing regression test is not a substitute for\nidentifying the concrete input or code path that misbehaves when you can.";
|
package/dist/src/pr/reviewer.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createAiProvider } from "../ai/provider.js";
|
|
2
|
+
import { CliError, EXIT_USAGE } from "../cli/error.js";
|
|
2
3
|
import { completeWithMermaidTools, } from "../ai/mermaid_loop.js";
|
|
3
4
|
import { loadGuides } from "../review/guides.js";
|
|
4
5
|
import { computeScope } from "./scope.js";
|
|
@@ -7,6 +8,23 @@ import { REVIEW_COPY_RULE, severitySection } from "./review_copy.js";
|
|
|
7
8
|
import { needsSummary, READ_FULL_DIFF_TOOL, readFullDiff, summarizeDiff, } from "./diff_summary.js";
|
|
8
9
|
import { numberPatch } from "./hunks.js";
|
|
9
10
|
import { FINDINGS_JSON_INSTRUCTIONS, FINDINGS_JSON_SCHEMA, findingsMarkdownFromJson, } from "./findings_json.js";
|
|
11
|
+
/** A model id is never guessed here. Models go stale, and a build already
|
|
12
|
+
* installed cannot fetch a fresher one, so the id to run comes from config
|
|
13
|
+
* or a flag, set at least once by whoever runs it (Murat, 2026-09-28). */
|
|
14
|
+
/** The configured provider for one model. Built only when a call needs it:
|
|
15
|
+
* a provider's constructor requires a real key, which a fake-AI run never has. */
|
|
16
|
+
function reviewProvider(options, model, label) {
|
|
17
|
+
const provider = createAiProvider(options, requiredModel(model, label));
|
|
18
|
+
if (!provider) {
|
|
19
|
+
throw new CliError("missing_provider", "Review needs an AI provider.", "Run co-maintainer set --ai=... --token=...", EXIT_USAGE);
|
|
20
|
+
}
|
|
21
|
+
return provider;
|
|
22
|
+
}
|
|
23
|
+
function requiredModel(model, label) {
|
|
24
|
+
if (model)
|
|
25
|
+
return model;
|
|
26
|
+
throw new CliError("missing_model", `No ${label} model is configured.`, `Run co-maintainer set --${label}-model=..., or pass --${label}-model=... on the command that needs it.`, EXIT_USAGE);
|
|
27
|
+
}
|
|
10
28
|
const MAX_REVIEW_DIFF_CHARS = 240_000;
|
|
11
29
|
export const MERMAID_GUIDANCE = `Mermaid selection and minimal syntax:
|
|
12
30
|
flowchart = decisions, branches, pipelines, and fallback paths;
|
|
@@ -59,6 +77,21 @@ export function reviewSystemPrompt(diagrams) {
|
|
|
59
77
|
return `${REVIEW_ROLE}
|
|
60
78
|
${diagrams ? REVIEW_DIAGRAM_RULES : NO_DIAGRAM_RULES}`;
|
|
61
79
|
}
|
|
80
|
+
/** The guide block every review sends, kept as the stable head of the system
|
|
81
|
+
* prompt rather than the user prompt so a provider with prompt caching
|
|
82
|
+
* (Anthropic's `cache_control`) reads it instead of re-billing it. Nothing
|
|
83
|
+
* time-varying may enter this string — a single differing byte makes the cache
|
|
84
|
+
* miss. */
|
|
85
|
+
export function reviewGuidesBlock(guide, detailed, codebase) {
|
|
86
|
+
return `REVIEW GUIDE:
|
|
87
|
+
${guide}
|
|
88
|
+
|
|
89
|
+
DETAILED GUIDE:
|
|
90
|
+
${detailed}
|
|
91
|
+
|
|
92
|
+
CODEBASE CONVENTIONS:
|
|
93
|
+
${codebase || "None recorded."}`;
|
|
94
|
+
}
|
|
62
95
|
/** Shared PR and local/remote workspace review instructions: confirm claims
|
|
63
96
|
* against the indexed graph, not only the diff slice. */
|
|
64
97
|
export const CODEGRAPH_DIFF_VERIFICATION = `Examine the changes line by line, not just file by file. A single file can
|
|
@@ -78,8 +111,8 @@ function text(value, limit = 20_000) {
|
|
|
78
111
|
? `${result.slice(0, limit)}\n[truncated]`
|
|
79
112
|
: result;
|
|
80
113
|
}
|
|
81
|
-
/** Turns a model reply into the Markdown every consumer already parses
|
|
82
|
-
*
|
|
114
|
+
/** Turns a model reply into the Markdown every consumer already parses.
|
|
115
|
+
* A reply that is not usable JSON is retried once with an
|
|
83
116
|
* explicit reminder; if that also fails, the raw text is returned so the
|
|
84
117
|
* legacy Markdown parser can still read it. This keeps a provider that ignores
|
|
85
118
|
* `response_format` working, and never drops a review on the floor. */
|
|
@@ -219,12 +252,10 @@ export async function reviewPullRequest(client, options, usage, snapshot, ai, pr
|
|
|
219
252
|
if (scope) {
|
|
220
253
|
report(`scope resolved · own=${ownFiles.length} files · upstream=${upstreamFiles.length} files`);
|
|
221
254
|
}
|
|
222
|
-
// Built only if needed
|
|
223
|
-
// API key, which a fake-AI test run never has.
|
|
255
|
+
// Built only if needed, see reviewProvider.
|
|
224
256
|
const filesNeedSummary = ownFiles.some(({ file }) => needsSummary(Number(file.changes ?? 0), String(file.patch ?? "")));
|
|
225
257
|
const lowProvider = filesNeedSummary
|
|
226
|
-
? (ai ??
|
|
227
|
-
new OpenRouterProvider(options.aiToken ?? "", options.lowModel ?? "openai/gpt-oss-120b"))
|
|
258
|
+
? (ai ?? reviewProvider(options, options.lowModel, "low"))
|
|
228
259
|
: undefined;
|
|
229
260
|
const patchByPath = new Map();
|
|
230
261
|
const [ownSections, codegraphTools] = await Promise.all([
|
|
@@ -288,8 +319,7 @@ interaction if the pull request's own change above relies on or conflicts with
|
|
|
288
319
|
one of these files, and never mark that finding blocking:
|
|
289
320
|
${upstreamListing}${unchangedListing}`;
|
|
290
321
|
report(`diff prepared · ${ownPatch.length} chars${diffWasTruncated ? " · truncated for model context" : ""}`);
|
|
291
|
-
const provider = ai ??
|
|
292
|
-
new OpenRouterProvider(options.aiToken ?? "", options.highModel ?? "openai/gpt-5.6-luna");
|
|
322
|
+
const provider = ai ?? reviewProvider(options, options.highModel, "high");
|
|
293
323
|
const diagrams = provider.supportsTools !== false;
|
|
294
324
|
const carryBlock = extras?.carryPrompt ? `${extras.carryPrompt}\n` : "";
|
|
295
325
|
const prompt = `Review this pull request against the repository's review guide and
|
|
@@ -324,15 +354,6 @@ code to the nearest numbered line.
|
|
|
324
354
|
${diagrams ? DIAGRAM_PROMPT_RULES : NO_DIAGRAM_RULES}
|
|
325
355
|
${FINDINGS_JSON_INSTRUCTIONS}
|
|
326
356
|
|
|
327
|
-
REVIEW GUIDE:
|
|
328
|
-
${guide}
|
|
329
|
-
|
|
330
|
-
DETAILED GUIDE:
|
|
331
|
-
${detailed}
|
|
332
|
-
|
|
333
|
-
CODEBASE CONVENTIONS:
|
|
334
|
-
${codebase || "None recorded."}
|
|
335
|
-
|
|
336
357
|
PULL REQUEST:
|
|
337
358
|
${JSON.stringify({
|
|
338
359
|
number,
|
|
@@ -349,17 +370,17 @@ ${diff}`;
|
|
|
349
370
|
const matrix = clampImproveMatrix(options.improveMatrix);
|
|
350
371
|
const request = {
|
|
351
372
|
job: "review_pull_request",
|
|
352
|
-
system: reviewSystemPrompt(diagrams),
|
|
373
|
+
system: `${reviewSystemPrompt(diagrams)}\n\n${reviewGuidesBlock(guide, detailed, codebase)}`,
|
|
353
374
|
prompt,
|
|
354
375
|
maxTokens: 24_000 * matrix,
|
|
355
376
|
reasoningEffort: "high",
|
|
356
377
|
responseFormat: FINDINGS_JSON_SCHEMA,
|
|
357
378
|
};
|
|
358
|
-
report(`AI request · model=${options.highModel ?? "
|
|
379
|
+
report(`AI request · ${options.ai} · model=${options.highModel ?? "unset"} · ` +
|
|
359
380
|
`prompt=${prompt.length} chars · maxTokens=${request.maxTokens}`);
|
|
360
381
|
if (options.debug) {
|
|
361
382
|
console.log(`[debug] review prompt · ${prompt.length} chars · diff=${diff.length} chars`);
|
|
362
|
-
console.log(`[debug]
|
|
383
|
+
console.log(`[debug] ${options.ai} request · model=${options.highModel ?? "unset"} · maxTokens=${request.maxTokens}`);
|
|
363
384
|
}
|
|
364
385
|
let response = await completeWithMermaidTools(provider, request, 1, extraTools, maxToolRounds);
|
|
365
386
|
if (usage)
|
|
@@ -370,9 +391,9 @@ ${diff}`;
|
|
|
370
391
|
console.log(`\n----- INITIAL REVIEW -----\n${response.text}\n`);
|
|
371
392
|
}
|
|
372
393
|
if (!response.text.trim()) {
|
|
373
|
-
throw new Error(
|
|
394
|
+
throw new Error(`${options.ai} returned an empty review. The reasoning budget may have been exhausted`);
|
|
374
395
|
}
|
|
375
|
-
// The model returns JSON; we render the Markdown
|
|
396
|
+
// The model returns JSON; we render the Markdown. A reply
|
|
376
397
|
// that is not usable JSON is retried once and then falls back to the legacy
|
|
377
398
|
// Markdown parser, so a provider that ignores the schema still works.
|
|
378
399
|
let reviewText = await normalizeReviewResponse(provider, request, response, extraTools, maxToolRounds, usage, report, options);
|
|
@@ -402,7 +423,7 @@ ${reviewText}`,
|
|
|
402
423
|
report(`AI improvement response · input=${response.tokensIn} tokens · ` +
|
|
403
424
|
`output=${response.tokensOut} tokens`);
|
|
404
425
|
if (!response.text.trim()) {
|
|
405
|
-
throw new Error(
|
|
426
|
+
throw new Error(`${options.ai} returned an empty review improvement at pass ${pass - 1}`);
|
|
406
427
|
}
|
|
407
428
|
reviewText = await normalizeReviewResponse(provider, improvementRequest, response, extraTools, maxToolRounds, usage, report, options);
|
|
408
429
|
if (options.debug) {
|
|
@@ -456,8 +477,7 @@ export async function reviewWorkspaceRevision(revision, options, headSha, usage,
|
|
|
456
477
|
const changes = Number(file.changes ?? 0);
|
|
457
478
|
const patch = String(file.patch ?? "");
|
|
458
479
|
const lowProvider = needsSummary(changes, patch)
|
|
459
|
-
? (ai ??
|
|
460
|
-
new OpenRouterProvider(options.aiToken ?? "", options.lowModel ?? "openai/gpt-oss-120b"))
|
|
480
|
+
? (ai ?? reviewProvider(options, options.lowModel, "low"))
|
|
461
481
|
: undefined;
|
|
462
482
|
if (!lowProvider)
|
|
463
483
|
return `FILE: ${path}\n${filePatch(file)}`;
|
|
@@ -491,8 +511,7 @@ export async function reviewWorkspaceRevision(revision, options, headSha, usage,
|
|
|
491
511
|
.join("\n")}`
|
|
492
512
|
: "";
|
|
493
513
|
const carryBlock = extras?.carryPrompt ? `${extras.carryPrompt}\n` : "";
|
|
494
|
-
const provider = ai ??
|
|
495
|
-
new OpenRouterProvider(options.aiToken ?? "", options.highModel ?? "openai/gpt-5.6-luna");
|
|
514
|
+
const provider = ai ?? reviewProvider(options, options.highModel, "high");
|
|
496
515
|
const diagrams = provider.supportsTools !== false;
|
|
497
516
|
const prompt = `Review these local changes against the repository's review guide and
|
|
498
517
|
codebase conventions. Find only actionable code-level violations supported by
|
|
@@ -504,15 +523,6 @@ ${CODEGRAPH_DIFF_VERIFICATION}
|
|
|
504
523
|
${diagrams ? DIAGRAM_PROMPT_RULES : NO_DIAGRAM_RULES}
|
|
505
524
|
${FINDINGS_JSON_INSTRUCTIONS}
|
|
506
525
|
|
|
507
|
-
REVIEW GUIDE:
|
|
508
|
-
${guide}
|
|
509
|
-
|
|
510
|
-
DETAILED GUIDE:
|
|
511
|
-
${detailed}
|
|
512
|
-
|
|
513
|
-
CODEBASE CONVENTIONS:
|
|
514
|
-
${codebase || "None recorded."}
|
|
515
|
-
|
|
516
526
|
WORKSPACE:
|
|
517
527
|
${JSON.stringify({
|
|
518
528
|
branch: revision.baseLabel,
|
|
@@ -524,7 +534,7 @@ ${ownDiff}${unchangedListing}`;
|
|
|
524
534
|
const matrix = clampImproveMatrix(options.improveMatrix);
|
|
525
535
|
const request = {
|
|
526
536
|
job: "review_local",
|
|
527
|
-
system: reviewSystemPrompt(diagrams),
|
|
537
|
+
system: `${reviewSystemPrompt(diagrams)}\n\n${reviewGuidesBlock(guide, detailed, codebase)}`,
|
|
528
538
|
prompt,
|
|
529
539
|
maxTokens: 24_000 * matrix,
|
|
530
540
|
reasoningEffort: "high",
|
|
@@ -535,7 +545,7 @@ ${ownDiff}${unchangedListing}`;
|
|
|
535
545
|
if (usage)
|
|
536
546
|
await usage(response);
|
|
537
547
|
if (!response.text.trim()) {
|
|
538
|
-
throw new Error(
|
|
548
|
+
throw new Error(`${options.ai} returned an empty review`);
|
|
539
549
|
}
|
|
540
550
|
let reviewText = await normalizeReviewResponse(provider, request, response, extraTools, maxToolRounds, usage, report, options);
|
|
541
551
|
for (let pass = 2; pass <= matrix; pass++) {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { ReviewCliArgs } from "../cli/review_args.ts";
|
|
2
|
+
/** A plain-http, non-local host means the BYOK key travels in the clear.
|
|
3
|
+
* Pulled out as a pure function so the rule is unit-testable
|
|
4
|
+
* without standing up a plain-http fake server. */
|
|
5
|
+
export declare function byokHttpWarning(host: string): string | null;
|
|
2
6
|
export declare function runRemoteReview(cli: ReviewCliArgs & {
|
|
3
7
|
mode: "remote";
|
|
4
8
|
}): Promise<void>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { VERSION } from "../version.js";
|
|
2
2
|
import { CliError, EXIT_USAGE, exitWith } from "../cli/error.js";
|
|
3
3
|
import { formatHumanReview, humanFindingsFromJson, reviewExitCodeFromJsonFindings, } from "../cli/review_result.js";
|
|
4
|
-
import { baseUrl, die, readApiError, remoteFetch } from "./http.js";
|
|
4
|
+
import { baseUrl, die, readApiError, remoteFetch, remoteFromEnv, } from "./http.js";
|
|
5
5
|
import { readConfig, writeUserConfig } from "../config.js";
|
|
6
|
+
import { getEnv } from "../util/runtime.js";
|
|
6
7
|
import { prepareLocalCodegraph } from "../local/codegraph_prepare.js";
|
|
7
8
|
import { canPrompt } from "../tools/codegraph.js";
|
|
8
9
|
import { runRemoteToolCalls, toolHandlerMap, } from "./client_tools.js";
|
|
@@ -11,19 +12,42 @@ import { runCommand } from "../pr/checkout.js";
|
|
|
11
12
|
import { assertGitQuiet, currentBranch, detectRemoteRepo, gitRoot, } from "../local/git_ops.js";
|
|
12
13
|
import { buildLocalRevision, mergeBase, resolveBaseRef, } from "../local/git_revision.js";
|
|
13
14
|
import { withCliLogsToStderr } from "../util/log.js";
|
|
14
|
-
import { printRunSummary
|
|
15
|
+
import { printRunSummary } from "../util/run_summary.js";
|
|
16
|
+
import { emitGithubOutput, neutralizeCommands } from "../cli/review_github.js";
|
|
17
|
+
import { costReasonText } from "../util/cost.js";
|
|
15
18
|
import { setCliInteractive } from "../cli/args.js";
|
|
16
19
|
import { MIN_SERVER_SCHEMA, REMOTE_CLI_UPGRADE_COMMAND, REMOTE_SCHEMA_VERSION, } from "./schema.js";
|
|
20
|
+
const LOCAL_BYOK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1"]);
|
|
21
|
+
/** A plain-http, non-local host means the BYOK key travels in the clear.
|
|
22
|
+
* Pulled out as a pure function so the rule is unit-testable
|
|
23
|
+
* without standing up a plain-http fake server. */
|
|
24
|
+
export function byokHttpWarning(host) {
|
|
25
|
+
const url = new URL(baseUrl(host));
|
|
26
|
+
if (url.protocol !== "http:" || LOCAL_BYOK_HOSTNAMES.has(url.hostname)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return (`Warning: sending your AI key over plain http to ${url.hostname}. ` +
|
|
30
|
+
"Anyone on the network path can read it.");
|
|
31
|
+
}
|
|
17
32
|
export async function runRemoteReview(cli) {
|
|
18
33
|
const config = readConfig();
|
|
19
|
-
// Inline flags win over the saved config for this run only
|
|
34
|
+
// Inline flags win over the saved config for this run only, so a
|
|
20
35
|
// one-off review does not have to be written to disk first.
|
|
21
|
-
const
|
|
22
|
-
const
|
|
36
|
+
const fromEnv = remoteFromEnv();
|
|
37
|
+
const host = cli.remoteHost ?? fromEnv.host ?? config.remoteHost;
|
|
38
|
+
const token = cli.remoteToken ?? fromEnv.token ?? config.remoteToken;
|
|
23
39
|
if (!host || !token) {
|
|
24
40
|
die("remote_not_configured", "Remote review is not configured.", "co-maintainer config set remote-host https://your-server " +
|
|
25
41
|
"&& co-maintainer config set remote-token <token> " +
|
|
26
|
-
"(or pass --remote-host=... --remote-token=... for one run)");
|
|
42
|
+
"(or pass --remote-host=... --remote-token=... for one run, or set CM_REMOTE_HOST and CM_REMOTE_TOKEN)");
|
|
43
|
+
}
|
|
44
|
+
// The client's own AI key, used only when this run turns BYOK
|
|
45
|
+
// on. The env var wins so a hosted CI runner can supply it without writing
|
|
46
|
+
// config, mirroring how CM_REMOTE_BYOK_POLICY overrides the server side.
|
|
47
|
+
const byokKey = getEnv("CM_REMOTE_BYOK") ?? config.remoteByok;
|
|
48
|
+
const byokEnabled = cli.remoteByok ?? config.remoteByokDefault ?? false;
|
|
49
|
+
if (byokEnabled && !byokKey) {
|
|
50
|
+
die("remote_byok_not_configured", "No BYOK key is set.", "co-maintainer config set remote-byok <key>");
|
|
27
51
|
}
|
|
28
52
|
setCliInteractive(!cli.json);
|
|
29
53
|
await withCliLogsToStderr(async () => {
|
|
@@ -76,6 +100,25 @@ export async function runRemoteReview(cli) {
|
|
|
76
100
|
if (hs.schemaVersion < MIN_SERVER_SCHEMA) {
|
|
77
101
|
die("server_too_old", "The dashboard server is too old for this CLI.", undefined, 4);
|
|
78
102
|
}
|
|
103
|
+
// Never silently fall back to the server's own key. Each of
|
|
104
|
+
// these is a mismatch between what this run wants and what the server
|
|
105
|
+
// will do, so all four are a hard stop.
|
|
106
|
+
const serverSupportsByok = hs.features?.includes("byok") ?? false;
|
|
107
|
+
if (byokEnabled && !serverSupportsByok) {
|
|
108
|
+
die("remote_byok_unsupported", `This server (version ${hs.serverVersion}) does not support your own key.`, "Upgrade the server or run without --remote-byok.");
|
|
109
|
+
}
|
|
110
|
+
if (byokEnabled && hs.byok?.policy === "off") {
|
|
111
|
+
die("remote_byok_rejected", "This server does not accept your own key.");
|
|
112
|
+
}
|
|
113
|
+
if (serverSupportsByok && hs.byok?.policy === "require" && !byokEnabled) {
|
|
114
|
+
die("remote_byok_required", "This server requires your own key.", "co-maintainer review --remote --remote-byok");
|
|
115
|
+
}
|
|
116
|
+
if (byokEnabled && !cli.json) {
|
|
117
|
+
const warning = byokHttpWarning(host);
|
|
118
|
+
if (warning)
|
|
119
|
+
console.error(warning);
|
|
120
|
+
console.error(`Using your own key with the server's provider: ${hs.ai?.provider ?? "unknown"}.`);
|
|
121
|
+
}
|
|
79
122
|
const hostKey = baseUrl(host);
|
|
80
123
|
const shown = config.remoteNoticeShownFor ?? [];
|
|
81
124
|
if (!shown.includes(hostKey) && !cli.json) {
|
|
@@ -89,7 +132,7 @@ export async function runRemoteReview(cli) {
|
|
|
89
132
|
gitRoot: root,
|
|
90
133
|
enabled: !cli.disableCodegraph,
|
|
91
134
|
allowInstall: cli.allowToolInstall,
|
|
92
|
-
// `--json` must stay machine-readable; `canPrompt` adds TTY and CI
|
|
135
|
+
// `--json` must stay machine-readable; `canPrompt` adds TTY and CI.
|
|
93
136
|
interactive: !cli.json && canPrompt(),
|
|
94
137
|
});
|
|
95
138
|
const localTools = toolHandlerMap(codegraphPrep.tools);
|
|
@@ -111,6 +154,9 @@ export async function runRemoteReview(cli) {
|
|
|
111
154
|
files: built.revision.files,
|
|
112
155
|
},
|
|
113
156
|
capabilities: { tools: capabilityTools },
|
|
157
|
+
// Only present when this run turned BYOK on; `byokKey` is
|
|
158
|
+
// known to be set here because of the earlier `byokEnabled` guard.
|
|
159
|
+
...(byokEnabled ? { byok: { key: byokKey } } : {}),
|
|
114
160
|
};
|
|
115
161
|
const serialized = JSON.stringify(submitBody);
|
|
116
162
|
if (serialized.length > hs.limits.maxBodyBytes) {
|
|
@@ -143,14 +189,33 @@ export async function runRemoteReview(cli) {
|
|
|
143
189
|
const payload = (await sync.json());
|
|
144
190
|
for (const line of payload.logs ?? []) {
|
|
145
191
|
afterLogSeq = Math.max(afterLogSeq, line.seq);
|
|
146
|
-
if (!cli.json)
|
|
147
|
-
console.error(
|
|
192
|
+
if (!cli.json) {
|
|
193
|
+
console.error(cli.output === "github"
|
|
194
|
+
? neutralizeCommands(line.message)
|
|
195
|
+
: line.message);
|
|
196
|
+
}
|
|
148
197
|
}
|
|
149
198
|
if (payload.toolCalls?.length) {
|
|
150
199
|
toolResults = await runRemoteToolCalls(payload.toolCalls, localTools);
|
|
151
200
|
continue;
|
|
152
201
|
}
|
|
153
202
|
if (payload.status === "done" && payload.result) {
|
|
203
|
+
const findings = (payload.result.findings ?? []);
|
|
204
|
+
const usage = payload.result.usage;
|
|
205
|
+
const known = usage !== undefined &&
|
|
206
|
+
usage.costUsd !== null &&
|
|
207
|
+
usage.costUsd !== undefined;
|
|
208
|
+
const run = usage
|
|
209
|
+
? {
|
|
210
|
+
durationMs: performance.now() - reviewStarted,
|
|
211
|
+
tokensIn: usage.tokensIn ?? 0,
|
|
212
|
+
tokensOut: usage.tokensOut ?? 0,
|
|
213
|
+
costUsd: known ? usage.costUsd : null,
|
|
214
|
+
...(!known && usage.costNote
|
|
215
|
+
? { costNote: costReasonText(usage.costNote) }
|
|
216
|
+
: {}),
|
|
217
|
+
}
|
|
218
|
+
: undefined;
|
|
154
219
|
if (cli.json) {
|
|
155
220
|
console.log(JSON.stringify({
|
|
156
221
|
schemaVersion: 1,
|
|
@@ -159,9 +224,14 @@ export async function runRemoteReview(cli) {
|
|
|
159
224
|
...payload.result,
|
|
160
225
|
}));
|
|
161
226
|
}
|
|
227
|
+
else if (cli.output === "github") {
|
|
228
|
+
emitGithubOutput({
|
|
229
|
+
title: `${repo} · ${branch} (remote)`,
|
|
230
|
+
findings: humanFindingsFromJson(findings),
|
|
231
|
+
run,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
162
234
|
else {
|
|
163
|
-
const findings = (payload.result.findings ??
|
|
164
|
-
[]);
|
|
165
235
|
const resultGuide = payload.result.guide;
|
|
166
236
|
const resultCodegraph = payload.result.codegraph;
|
|
167
237
|
console.log("\n" +
|
|
@@ -172,22 +242,13 @@ export async function runRemoteReview(cli) {
|
|
|
172
242
|
findings: humanFindingsFromJson(findings),
|
|
173
243
|
}) +
|
|
174
244
|
"\n");
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
printRunSummary(summaryFromMetrics({
|
|
178
|
-
calls: 0,
|
|
179
|
-
tokensIn: usage.tokensIn ?? 0,
|
|
180
|
-
tokensOut: usage.tokensOut ?? 0,
|
|
181
|
-
cost: usage.costUsd ?? 0,
|
|
182
|
-
costKnown: usage.costUsd !== null && usage.costUsd !== undefined,
|
|
183
|
-
}, performance.now() - reviewStarted));
|
|
184
|
-
}
|
|
245
|
+
if (run)
|
|
246
|
+
printRunSummary(run);
|
|
185
247
|
}
|
|
186
|
-
const findings = (payload.result.findings ?? []);
|
|
187
248
|
exitWith(reviewExitCodeFromJsonFindings(findings));
|
|
188
249
|
// `done` is terminal. Without this return the loop polls `sync` again
|
|
189
250
|
// and, since the server keeps reporting the same finished job, the
|
|
190
|
-
// client reprints the result and re-requests forever
|
|
251
|
+
// client reprints the result and re-requests forever.
|
|
191
252
|
return;
|
|
192
253
|
}
|
|
193
254
|
if (payload.status === "failed" || payload.status === "canceled") {
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Where the remote server is, as an environment variable can say it.
|
|
2
|
+
* A CI job has no config file, and a token on the command line
|
|
3
|
+
* ends up in the process list, so `CM_REMOTE_HOST` and `CM_REMOTE_TOKEN` are
|
|
4
|
+
* read here. An empty value counts as unset, so a workflow secret that did not
|
|
5
|
+
* resolve falls through to the "not configured" error instead of a blank host. */
|
|
6
|
+
export declare function remoteFromEnv(): {
|
|
7
|
+
host?: string;
|
|
8
|
+
token?: string;
|
|
9
|
+
};
|
|
10
|
+
/** A usage error the CLI prints with its hint and exit code. */
|
|
2
11
|
export declare function die(code: string, message: string, hint?: string, exitCode?: number): never;
|
|
3
12
|
export declare function baseUrl(host: string): string;
|
|
4
13
|
export declare function remoteFetch(host: string, token: string, path: string, init: RequestInit): Promise<Response>;
|