argus-reviewer-e2e 0.4.0 → 0.4.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/README.md +32 -5
- package/action/action.yml +3 -3
- package/action/runtime.mjs +2 -2
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +259 -237
- package/dist/config.d.ts +60 -2
- package/dist/config.js +85 -2
- package/dist/detect.js +2 -2
- package/dist/engine/loop.d.ts +8 -1
- package/dist/onboarding/pr-content.d.ts +12 -0
- package/dist/onboarding/pr-content.js +56 -0
- package/dist/onboarding/pr.d.ts +23 -0
- package/dist/onboarding/pr.js +196 -0
- package/dist/onboarding/scaffold.d.ts +37 -0
- package/dist/onboarding/scaffold.js +174 -0
- package/dist/pipeline/budget.d.ts +13 -0
- package/dist/pipeline/budget.js +34 -0
- package/dist/review/chunks.d.ts +21 -0
- package/dist/review/chunks.js +96 -0
- package/dist/vision/openrouter.d.ts +37 -1
- package/dist/vision/openrouter.js +111 -5
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { basename, extname, isAbsolute, join, relative, resolve } from 'node:pat
|
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
7
|
import { parseArgs } from 'node:util';
|
|
8
8
|
import { bindSession, renderTestFile, takeTests, td, test as registerTest, TdSession, } from './api.js';
|
|
9
|
-
import { DEFAULT_RECORD_STEP_CAP, loadConfig, resolveBlockSeverities, resolveConfig, resolveMaxComments, sanitizeExpectation, unknownProviderSlugs, } from './config.js';
|
|
9
|
+
import { checkRequestTimeoutMs, applyBudgetSetting, DEFAULT_BUDGET_USD, DEFAULT_RECORD_STEP_CAP, loadConfig, parseBudgetSetting, resolveBatchModel, resolveBlockSeverities, resolveConfig, resolveMaxComments, sanitizeExpectation, UNCAPPED_WARNING, unknownProviderSlugs, } from './config.js';
|
|
10
10
|
import { debug, setLiveDir } from './debug.js';
|
|
11
11
|
import { defaultExec, detectEnvironment, resolveA0Host } from './detect.js';
|
|
12
12
|
import { BrowserDriver } from './driver/browser.js';
|
|
@@ -23,6 +23,7 @@ import { resolveTrust } from './trust.js';
|
|
|
23
23
|
import { linkFindings } from './evidence/link.js';
|
|
24
24
|
import { DecisionClient } from './vision/decisions.js';
|
|
25
25
|
import { isReviewProfile, packRubric } from './review/packs.js';
|
|
26
|
+
import { planChunks } from './review/chunks.js';
|
|
26
27
|
import { partitionByExclude } from './review/scope.js';
|
|
27
28
|
import { capTestFindings } from './review/testfiles.js';
|
|
28
29
|
import { auditOf, validateFindings } from './review/validate.js';
|
|
@@ -47,12 +48,15 @@ import { OpenRouterClient } from './vision/openrouter.js';
|
|
|
47
48
|
import { Ledger } from './vision/ledger.js';
|
|
48
49
|
import { selectionFromFlags } from './pipeline/contracts.js';
|
|
49
50
|
import { MENTION_HELP, mayRunMention, parseMention, postIssueComment, } from './mention.js';
|
|
51
|
+
import { affordableBatchPrefix } from './pipeline/budget.js';
|
|
50
52
|
import { runVerify, writeEvidenceReport } from './pipeline/verify.js';
|
|
51
53
|
import { APP_LANE_DEFAULT_TIMEOUT_MS, APP_LANE_REPORT, runAppLane, } from './pipeline/app.js';
|
|
52
54
|
import { CliError, errorJson, renderError, toCliError } from './ui/errors.js';
|
|
53
55
|
import { colorEnabled, createStyler } from './ui/style.js';
|
|
54
56
|
import { renderSummary, verifySummary } from './ui/summary.js';
|
|
55
57
|
import { PROOF_LEVELS, proofMeter, SEVERITY_GLYPH, SEVERITY_LABEL, shortSha } from './report/viewmodel.js';
|
|
58
|
+
import { DEFAULT_BRANCH as DEFAULT_PR_BRANCH, initPr, validateBranch, validateRepo } from './onboarding/pr.js';
|
|
59
|
+
import { renderScaffold, scaffoldChecklist } from './onboarding/scaffold.js';
|
|
56
60
|
import { INLINE_SENTINEL, inlineDedupKey, normalizeFindingMessage } from './review/inline.js';
|
|
57
61
|
/** Flags accepted before or after any command; stripped before dispatch. */
|
|
58
62
|
const GLOBAL_FLAGS = new Set(['--json', '--no-color', '--debug']);
|
|
@@ -131,7 +135,7 @@ const HELP_GROUPS = [
|
|
|
131
135
|
{
|
|
132
136
|
title: 'Setup',
|
|
133
137
|
commands: [
|
|
134
|
-
[['init [--force]'], 'Scaffold config, a smoke test and the PR workflow.'],
|
|
138
|
+
[['init [--force | --pr]'], 'Scaffold config, a smoke test and the PR workflow.'],
|
|
135
139
|
[['--help'], 'Show this help.'],
|
|
136
140
|
],
|
|
137
141
|
},
|
|
@@ -182,6 +186,14 @@ Options:
|
|
|
182
186
|
--report-dir <dir> Report output dir (default: config reportDir or ./argus-reviewer-report)
|
|
183
187
|
--fixture <dir> Review a local fixture repo (ref argus-fixture-base vs HEAD)
|
|
184
188
|
instead of a live PR, with no GitHub API calls. Used by npm run demo.
|
|
189
|
+
--mode <mode> realtime (default) | batch. batch submits the chunks through
|
|
190
|
+
OpenRouter's async Batch API and falls back to realtime on
|
|
191
|
+
failure or timeout. Overrides ARGUS_REVIEW_MODE and review.mode.
|
|
192
|
+
--batch-model <slug> Model for batch mode (a :batch slug; default
|
|
193
|
+
deepseek/deepseek-v4.1-flash:batch). Overrides
|
|
194
|
+
ARGUS_BATCH_MODEL and review.batchModel.
|
|
195
|
+
Env: ARGUS_REQUEST_TIMEOUT_MS sets the per-request timeout (default 120000,
|
|
196
|
+
max 900000; also review.requestTimeoutMs).
|
|
185
197
|
-h, --help Show this help`;
|
|
186
198
|
const CACHE_USAGE = `Usage: argus-reviewer cache <list|prune> [options]
|
|
187
199
|
|
|
@@ -305,29 +317,32 @@ function createClient(deps, config, ctx) {
|
|
|
305
317
|
// Lazy: a cache-hit replay makes zero vision calls and needs no key. The
|
|
306
318
|
// error fires clearly on the first actual model call.
|
|
307
319
|
let inner;
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
throw new CliError('OPENROUTER_KEY_MISSING', 'OPENROUTER_API_KEY is not set; model calls bill through this key (BYOK)');
|
|
314
|
-
}
|
|
315
|
-
const envTrace = parseOpenRouterTrace(ctx.env);
|
|
316
|
-
const trace = { ...(envTrace ?? {}), ...(config.openrouter?.trace ?? {}) };
|
|
317
|
-
const headers = { ...(config.openrouter?.headers ?? {}) };
|
|
318
|
-
const traceOpt = Object.keys(trace).length > 0 ? trace : undefined;
|
|
319
|
-
const headersOpt = Object.keys(headers).length > 0 ? headers : undefined;
|
|
320
|
-
inner = new OpenRouterClient({
|
|
321
|
-
apiKey,
|
|
322
|
-
...(traceOpt ? { trace: traceOpt } : {}),
|
|
323
|
-
...(headersOpt ? { headers: headersOpt } : {}),
|
|
324
|
-
onCall: (call) => {
|
|
325
|
-
ctx.out(`openrouter ${call.kind} ${call.model} ${call.tokens}tok $${call.costUsd.toFixed(6)}`);
|
|
326
|
-
},
|
|
327
|
-
});
|
|
320
|
+
const getInner = () => {
|
|
321
|
+
if (inner === undefined) {
|
|
322
|
+
const apiKey = ctx.env.OPENROUTER_API_KEY;
|
|
323
|
+
if (apiKey === undefined || apiKey === '') {
|
|
324
|
+
throw new CliError('OPENROUTER_KEY_MISSING', 'OPENROUTER_API_KEY is not set; model calls bill through this key (BYOK)');
|
|
328
325
|
}
|
|
329
|
-
|
|
330
|
-
|
|
326
|
+
const envTrace = parseOpenRouterTrace(ctx.env);
|
|
327
|
+
const trace = { ...(envTrace ?? {}), ...(config.openrouter?.trace ?? {}) };
|
|
328
|
+
const headers = { ...(config.openrouter?.headers ?? {}) };
|
|
329
|
+
const traceOpt = Object.keys(trace).length > 0 ? trace : undefined;
|
|
330
|
+
const headersOpt = Object.keys(headers).length > 0 ? headers : undefined;
|
|
331
|
+
inner = new OpenRouterClient({
|
|
332
|
+
apiKey,
|
|
333
|
+
timeoutMs: config.review.requestTimeoutMs,
|
|
334
|
+
...(traceOpt ? { trace: traceOpt } : {}),
|
|
335
|
+
...(headersOpt ? { headers: headersOpt } : {}),
|
|
336
|
+
onCall: (call) => {
|
|
337
|
+
ctx.out(`openrouter ${call.kind} ${call.model} ${call.tokens}tok $${call.costUsd.toFixed(6)}`);
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return inner;
|
|
342
|
+
};
|
|
343
|
+
return {
|
|
344
|
+
complete: async (opts) => getInner().complete(opts),
|
|
345
|
+
completeBatch: async (opts) => getInner().completeBatch(opts),
|
|
331
346
|
};
|
|
332
347
|
}
|
|
333
348
|
async function launchDriver(config, deps) {
|
|
@@ -554,13 +569,15 @@ async function cmdRun(args, ctx, deps) {
|
|
|
554
569
|
config.cacheDir = resolve(ctx.cwd, values['cache-dir']);
|
|
555
570
|
}
|
|
556
571
|
const envBudget = ctx.env.ARGUS_BUDGET_USD;
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
572
|
+
const envSetting = parseBudgetSetting(envBudget);
|
|
573
|
+
if (envSetting.kind === 'invalid') {
|
|
574
|
+
ctx.err(`warning: ignoring invalid ARGUS_BUDGET_USD="${envBudget}"`);
|
|
575
|
+
}
|
|
576
|
+
const applied = applyBudgetSetting(envSetting);
|
|
577
|
+
if (applied !== 'keep')
|
|
578
|
+
config.budgetUsd = applied;
|
|
579
|
+
if (config.budgetUsd === undefined)
|
|
580
|
+
ctx.err(UNCAPPED_WARNING);
|
|
564
581
|
const liveDir = resolve(ctx.cwd, config.cacheDir ?? '.argus-reviewer-cache');
|
|
565
582
|
// liveLog's mkdir is non-recursive by design — create a custom nested
|
|
566
583
|
// cache dir (and ancestors) here once so the first live write lands.
|
|
@@ -1072,8 +1089,6 @@ const CODE_REVIEW_SCHEMA = {
|
|
|
1072
1089
|
required: ['summary', 'verdict', 'findings'],
|
|
1073
1090
|
},
|
|
1074
1091
|
};
|
|
1075
|
-
const CHUNK_TOKEN_TARGET = 6000;
|
|
1076
|
-
const CHUNK_FILE_OVERHEAD = 100;
|
|
1077
1092
|
const MAX_PR_FILE_PAGES = 10;
|
|
1078
1093
|
async function fetchPrFiles(repo, pr, token, ctx) {
|
|
1079
1094
|
const files = [];
|
|
@@ -1142,32 +1157,7 @@ export async function loadFixture(dir, exec = defaultExec) {
|
|
|
1142
1157
|
return { files: filesFromUnifiedDiff(diff.stdout), meta, diff: diff.stdout };
|
|
1143
1158
|
}
|
|
1144
1159
|
export function buildPatchChunks(files, contexts = {}) {
|
|
1145
|
-
|
|
1146
|
-
const ctxBlock = contexts[c.filename];
|
|
1147
|
-
const head = ctxBlock === undefined ? `### ${c.filename}` : `### ${c.filename}\n${ctxBlock}`;
|
|
1148
|
-
return `${head}\n\`\`\`diff\n${c.patch}\n\`\`\``;
|
|
1149
|
-
};
|
|
1150
|
-
const chunks = [];
|
|
1151
|
-
let current = [];
|
|
1152
|
-
let currentTokens = 0;
|
|
1153
|
-
for (const f of files) {
|
|
1154
|
-
const fileTokens = Math.ceil((f.patch?.length ?? 0) / 4) +
|
|
1155
|
-
Math.ceil((contexts[f.filename]?.length ?? 0) / 4) +
|
|
1156
|
-
CHUNK_FILE_OVERHEAD;
|
|
1157
|
-
if (current.length > 0 && currentTokens + fileTokens > CHUNK_TOKEN_TARGET) {
|
|
1158
|
-
chunks.push(current.map(section).join('\n\n'));
|
|
1159
|
-
current = [f];
|
|
1160
|
-
currentTokens = fileTokens;
|
|
1161
|
-
}
|
|
1162
|
-
else {
|
|
1163
|
-
current.push(f);
|
|
1164
|
-
currentTokens += fileTokens;
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
if (current.length > 0) {
|
|
1168
|
-
chunks.push(current.map(section).join('\n\n'));
|
|
1169
|
-
}
|
|
1170
|
-
return chunks;
|
|
1160
|
+
return planChunks(files, contexts).map((c) => c.text);
|
|
1171
1161
|
}
|
|
1172
1162
|
export function buildCodeReviewMessages(repo, pr, patchText, chunkIndex = 0, totalChunks = 1, profiles = []) {
|
|
1173
1163
|
const rubric = packRubric(profiles);
|
|
@@ -1560,6 +1550,8 @@ async function cmdCodeReview(args, ctx, deps) {
|
|
|
1560
1550
|
help: { type: 'boolean', short: 'h', default: false },
|
|
1561
1551
|
'report-dir': { type: 'string' },
|
|
1562
1552
|
fixture: { type: 'string' },
|
|
1553
|
+
mode: { type: 'string' },
|
|
1554
|
+
'batch-model': { type: 'string' },
|
|
1563
1555
|
},
|
|
1564
1556
|
});
|
|
1565
1557
|
if (values.help) {
|
|
@@ -1612,9 +1604,35 @@ async function cmdCodeReview(args, ctx, deps) {
|
|
|
1612
1604
|
.map((s) => s.trim())
|
|
1613
1605
|
.filter(isReviewProfile);
|
|
1614
1606
|
}
|
|
1607
|
+
// Review mode: --mode beats the operator env, which beats config (same
|
|
1608
|
+
// operator-env pattern as ARGUS_CODE_MODEL — the only lever in the
|
|
1609
|
+
// untrusted lane, where PR-controlled config never executes).
|
|
1610
|
+
const modeRaw = (values.mode ?? ctx.env.ARGUS_REVIEW_MODE ?? '').trim();
|
|
1611
|
+
if (modeRaw === 'realtime' || modeRaw === 'batch')
|
|
1612
|
+
config.review.mode = modeRaw;
|
|
1613
|
+
else if (modeRaw !== '') {
|
|
1614
|
+
if (values.mode !== undefined) {
|
|
1615
|
+
usageError(ctx, 'code-review', `--mode must be realtime or batch, got "${modeRaw}"`, 'argus-reviewer code-review --mode batch');
|
|
1616
|
+
return 2;
|
|
1617
|
+
}
|
|
1618
|
+
ctx.err(`warning: ignoring invalid ARGUS_REVIEW_MODE="${modeRaw}"`);
|
|
1619
|
+
}
|
|
1620
|
+
const batchModelRaw = (values['batch-model'] ?? ctx.env.ARGUS_BATCH_MODEL ?? '').trim();
|
|
1621
|
+
if (batchModelRaw !== '')
|
|
1622
|
+
config.review.batchModel = batchModelRaw;
|
|
1623
|
+
const timeoutRaw = ctx.env.ARGUS_REQUEST_TIMEOUT_MS?.trim();
|
|
1624
|
+
if (timeoutRaw !== undefined && timeoutRaw !== '') {
|
|
1625
|
+
const ms = /^\d+$/.test(timeoutRaw) ? Number(timeoutRaw) : Number.NaN;
|
|
1626
|
+
const bad = checkRequestTimeoutMs(ms);
|
|
1627
|
+
if (bad !== undefined) {
|
|
1628
|
+
usageError(ctx, 'code-review', `ARGUS_REQUEST_TIMEOUT_MS: ${bad.replace('requestTimeoutMs', 'value')}, got "${timeoutRaw}"`, 'ARGUS_REQUEST_TIMEOUT_MS=300000 argus-reviewer code-review');
|
|
1629
|
+
return 2;
|
|
1630
|
+
}
|
|
1631
|
+
config.review.requestTimeoutMs = ms;
|
|
1632
|
+
}
|
|
1615
1633
|
const model = config.code_model ?? config.model;
|
|
1616
1634
|
const runNonce = runNonceFrom(ctx.env);
|
|
1617
|
-
debug('code-review', `repo=${repo ?? 'none'} pr=${pr ?? 'none'} model=${model} budget=${config.codeReviewBudgetUsd ?? '
|
|
1635
|
+
debug('code-review', `repo=${repo ?? 'none'} pr=${pr ?? 'none'} model=${model} budget=${config.codeReviewBudgetUsd ?? 'UNCAPPED'}`);
|
|
1618
1636
|
const skip = async (reason) => {
|
|
1619
1637
|
ctx.out(`code-review: skipping: ${reason}`);
|
|
1620
1638
|
stage(`skipped — ${reason}`);
|
|
@@ -1657,14 +1675,16 @@ async function cmdCodeReview(args, ctx, deps) {
|
|
|
1657
1675
|
const prNum = pr;
|
|
1658
1676
|
const ghToken = token;
|
|
1659
1677
|
const envBudget = ctx.env.ARGUS_BUDGET_USD;
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
config.codeReviewBudgetUsd = parsed;
|
|
1664
|
-
else
|
|
1665
|
-
ctx.err(`warning: ignoring invalid ARGUS_BUDGET_USD="${envBudget}"`);
|
|
1678
|
+
const envSetting = parseBudgetSetting(envBudget);
|
|
1679
|
+
if (envSetting.kind === 'invalid') {
|
|
1680
|
+
ctx.err(`warning: ignoring invalid ARGUS_BUDGET_USD="${envBudget}"`);
|
|
1666
1681
|
}
|
|
1682
|
+
const appliedBudget = applyBudgetSetting(envSetting);
|
|
1683
|
+
if (appliedBudget !== 'keep')
|
|
1684
|
+
config.codeReviewBudgetUsd = appliedBudget;
|
|
1667
1685
|
const budget = config.codeReviewBudgetUsd;
|
|
1686
|
+
if (budget === undefined)
|
|
1687
|
+
ctx.err(UNCAPPED_WARNING);
|
|
1668
1688
|
const [allFiles, index] = await Promise.all([
|
|
1669
1689
|
fixture !== undefined
|
|
1670
1690
|
? Promise.resolve(fixture.files)
|
|
@@ -1690,7 +1710,8 @@ async function cmdCodeReview(args, ctx, deps) {
|
|
|
1690
1710
|
if (attached > 0) {
|
|
1691
1711
|
debug('code-review', `contexts=${attached}/${files.length}`);
|
|
1692
1712
|
}
|
|
1693
|
-
const
|
|
1713
|
+
const plan = planChunks(files, contexts);
|
|
1714
|
+
const chunks = plan.map((c) => c.text);
|
|
1694
1715
|
debug('code-review', `chunks=${chunks.length} files=${files.length}`);
|
|
1695
1716
|
try {
|
|
1696
1717
|
const client = createClient(deps, config, ctx);
|
|
@@ -1806,21 +1827,101 @@ async function cmdCodeReview(args, ctx, deps) {
|
|
|
1806
1827
|
});
|
|
1807
1828
|
}
|
|
1808
1829
|
};
|
|
1830
|
+
// Batch mode: all chunks go out as one async batch up front. A whole-
|
|
1831
|
+
// batch failure (or timeout) leaves `batched` empty so every chunk runs
|
|
1832
|
+
// realtime below; a single errored request falls back for that chunk
|
|
1833
|
+
// only. Synthesis stays realtime — it needs the merged chunk findings.
|
|
1834
|
+
const batched = new Map();
|
|
1835
|
+
let batchRecord;
|
|
1836
|
+
if (config.review.mode === 'batch') {
|
|
1837
|
+
if (client.completeBatch === undefined || chunks.length === 0) {
|
|
1838
|
+
batchRecord = { used: false, chunks: chunks.length, fellBack: 'client has no batch support' };
|
|
1839
|
+
}
|
|
1840
|
+
else {
|
|
1841
|
+
const allRequests = chunks.map((chunk, i) => ({
|
|
1842
|
+
customId: `chunk-${i}`,
|
|
1843
|
+
messages: buildCodeReviewMessages(repoName, prNum, chunk, i, chunks.length, config.review.profiles),
|
|
1844
|
+
schema: CODE_REVIEW_SCHEMA,
|
|
1845
|
+
provider: config.provider,
|
|
1846
|
+
}));
|
|
1847
|
+
// A submitted batch cannot be stopped, so size it against the
|
|
1848
|
+
// remaining budget first (conservative token-based estimate). Chunks
|
|
1849
|
+
// past the prefix run realtime below, where the per-chunk budget
|
|
1850
|
+
// gate applies.
|
|
1851
|
+
const fit = affordableBatchPrefix(allRequests, budget, ledger.visionCostUsd);
|
|
1852
|
+
const requests = allRequests.slice(0, fit);
|
|
1853
|
+
if (fit < allRequests.length) {
|
|
1854
|
+
ctx.err(fit === 0
|
|
1855
|
+
? `code-review: batch skipped: projected cost of ${allRequests.length} chunk(s) exceeds the $${budget} budget; using realtime with budget checks`
|
|
1856
|
+
: `code-review: batch limited to ${fit} of ${allRequests.length} chunk(s): projected cost exceeds the $${budget} budget; the rest run realtime with budget checks`);
|
|
1857
|
+
}
|
|
1858
|
+
if (fit === 0) {
|
|
1859
|
+
batchRecord = { used: false, chunks: chunks.length, fellBack: 'projected batch cost exceeds budget' };
|
|
1860
|
+
}
|
|
1861
|
+
else
|
|
1862
|
+
try {
|
|
1863
|
+
stage(`submitting ${requests.length} chunk(s) as a batch, poll deadline ${Math.round(config.review.batchTimeoutMs / 1000)}s`);
|
|
1864
|
+
const batchModel = resolveBatchModel(reviewModel, config.review.batchModel);
|
|
1865
|
+
stage(`batch model ${batchModel}`);
|
|
1866
|
+
const items = await client.completeBatch({
|
|
1867
|
+
model: batchModel,
|
|
1868
|
+
requests,
|
|
1869
|
+
kind: 'code',
|
|
1870
|
+
deadlineMs: config.review.batchTimeoutMs,
|
|
1871
|
+
});
|
|
1872
|
+
items.forEach((item, i) => {
|
|
1873
|
+
if (item.result !== undefined)
|
|
1874
|
+
batched.set(i, item.result);
|
|
1875
|
+
});
|
|
1876
|
+
batchRecord = {
|
|
1877
|
+
used: true,
|
|
1878
|
+
chunks: chunks.length,
|
|
1879
|
+
retriedRealtime: chunks.length - batched.size,
|
|
1880
|
+
};
|
|
1881
|
+
if (batched.size < chunks.length) {
|
|
1882
|
+
ctx.err(`code-review: ${chunks.length - batched.size} batch request(s) failed; running those chunks realtime`);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
catch (e) {
|
|
1886
|
+
const reason = e.message;
|
|
1887
|
+
debug('code-review', `batch failed: ${reason}`);
|
|
1888
|
+
ctx.err(`code-review: batch failed (${reason}); falling back to realtime`);
|
|
1889
|
+
batchRecord = { used: false, chunks: chunks.length, fellBack: reason };
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
const reviewedChunks = new Set();
|
|
1894
|
+
let chunkSpend = 0;
|
|
1809
1895
|
for (let i = 0; i < chunks.length; i++) {
|
|
1810
|
-
|
|
1811
|
-
|
|
1896
|
+
const fromBatch = batched.get(i);
|
|
1897
|
+
// Batch results are already paid for: always take them. Only
|
|
1898
|
+
// realtime chunks are gated by the budget.
|
|
1899
|
+
if (fromBatch === undefined) {
|
|
1900
|
+
if (ledger.budgetExceeded)
|
|
1901
|
+
continue;
|
|
1902
|
+
// Stop BEFORE a chunk the remaining budget cannot be expected to
|
|
1903
|
+
// cover (projected at the mean cost so far) — checking only after
|
|
1904
|
+
// the spend lets every run overshoot its cap by a whole chunk.
|
|
1905
|
+
if (reviewedChunks.size > 0 && !ledger.canSpend(chunkSpend / reviewedChunks.size)) {
|
|
1906
|
+
ctx.err(`code-review: budget would be exceeded by chunk ${i + 1}; stopping early`);
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1812
1910
|
debug('code-review', `chunk=${i + 1}/${chunks.length}`);
|
|
1813
1911
|
const chunk = chunks[i];
|
|
1814
1912
|
if (chunk === undefined)
|
|
1815
1913
|
continue;
|
|
1816
|
-
const response =
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1914
|
+
const response = fromBatch ??
|
|
1915
|
+
(await client.complete({
|
|
1916
|
+
model: reviewModel,
|
|
1917
|
+
messages: buildCodeReviewMessages(repoName, prNum, chunk, i, chunks.length, config.review.profiles),
|
|
1918
|
+
schema: CODE_REVIEW_SCHEMA,
|
|
1919
|
+
kind: 'code',
|
|
1920
|
+
provider: config.provider,
|
|
1921
|
+
}));
|
|
1823
1922
|
recordSpend(response.cost);
|
|
1923
|
+
chunkSpend += response.cost.costUsd;
|
|
1924
|
+
reviewedChunks.add(i);
|
|
1824
1925
|
lastModel = response.model;
|
|
1825
1926
|
const parsed = parseCodeReview(response.content);
|
|
1826
1927
|
const anchored = filterToDiffLines(parsed.findings, diffRanges, blockSeverities);
|
|
@@ -1834,15 +1935,18 @@ async function cmdCodeReview(args, ctx, deps) {
|
|
|
1834
1935
|
}
|
|
1835
1936
|
allFindings.push(...vetted.kept);
|
|
1836
1937
|
stage(`chunk ${i + 1}/${chunks.length} — ${parsed.findings.length} finding(s)`);
|
|
1837
|
-
if (ledger.budgetExceeded) {
|
|
1938
|
+
if (ledger.budgetExceeded && fromBatch === undefined) {
|
|
1838
1939
|
ctx.err(`code-review: budget exceeded after chunk ${i + 1}; stopping early`);
|
|
1839
|
-
break;
|
|
1840
1940
|
}
|
|
1841
1941
|
}
|
|
1942
|
+
const chunksReviewed = reviewedChunks.size;
|
|
1842
1943
|
let modelVerdict;
|
|
1843
1944
|
let modelSummary;
|
|
1844
1945
|
let finalFindings = allFindings;
|
|
1845
|
-
|
|
1946
|
+
// Synthesis is paid too: project it at the mean chunk cost.
|
|
1947
|
+
const synthAllowed = !ledger.budgetExceeded &&
|
|
1948
|
+
(reviewedChunks.size === 0 || ledger.canSpend(chunkSpend / reviewedChunks.size));
|
|
1949
|
+
if (chunks.length > 1 && synthAllowed) {
|
|
1846
1950
|
try {
|
|
1847
1951
|
debug('code-review', 'synthesis');
|
|
1848
1952
|
stage('synthesizing chunk findings');
|
|
@@ -1911,12 +2015,25 @@ async function cmdCodeReview(args, ctx, deps) {
|
|
|
1911
2015
|
}
|
|
1912
2016
|
if (modelVerdict === verdict && modelSummary !== undefined)
|
|
1913
2017
|
summary = modelSummary;
|
|
2018
|
+
// Files with at least one reviewed chunk. A budget stop leaves the
|
|
2019
|
+
// tail of the plan unreviewed; the scope and summary must say so.
|
|
2020
|
+
const reviewedSet = new Set(plan.flatMap((c, i) => (reviewedChunks.has(i) ? c.files : [])));
|
|
2021
|
+
const unreviewed = files.length - reviewedSet.size;
|
|
1914
2022
|
const scope = {
|
|
1915
2023
|
totalFiles: allFiles.length,
|
|
1916
|
-
reviewedFiles:
|
|
2024
|
+
reviewedFiles: reviewedSet.size,
|
|
1917
2025
|
excludedFiles: excluded.length,
|
|
1918
2026
|
excludedSample: excluded.slice(0, 5).map((f) => f.filename),
|
|
2027
|
+
chunksTotal: chunks.length,
|
|
2028
|
+
chunksReviewed,
|
|
2029
|
+
unreviewedFiles: unreviewed,
|
|
1919
2030
|
};
|
|
2031
|
+
if (chunks.length > 1) {
|
|
2032
|
+
const coverage = chunksReviewed === chunks.length
|
|
2033
|
+
? `Reviewed all ${chunks.length} chunks (${reviewedSet.size} of ${files.length} files).`
|
|
2034
|
+
: `Reviewed ${chunksReviewed} of ${chunks.length} chunks (${reviewedSet.size} of ${files.length} files); ${unreviewed} file(s) were not reviewed.`;
|
|
2035
|
+
summary = `${coverage} ${summary}`;
|
|
2036
|
+
}
|
|
1920
2037
|
if (excluded.length > 0) {
|
|
1921
2038
|
summary = `Reviewed ${files.length} of ${allFiles.length} changed files (${excluded.length} excluded by review.exclude). ${summary}`;
|
|
1922
2039
|
}
|
|
@@ -2169,6 +2286,7 @@ async function cmdCodeReview(args, ctx, deps) {
|
|
|
2169
2286
|
...(droppedFindings.length > 0 ? { droppedFindings } : {}),
|
|
2170
2287
|
...(modelVerdict !== undefined && modelVerdict !== verdict ? { modelVerdict } : {}),
|
|
2171
2288
|
scope,
|
|
2289
|
+
...(batchRecord !== undefined ? { batch: batchRecord } : {}),
|
|
2172
2290
|
...(validation !== undefined ? { validation } : {}),
|
|
2173
2291
|
...(testFileCapped > 0 ? { testFileCapped } : {}),
|
|
2174
2292
|
maxComments,
|
|
@@ -2275,22 +2393,24 @@ async function cmdVerify(args, ctx, deps) {
|
|
|
2275
2393
|
const trace = parseOpenRouterTrace(ctx.env);
|
|
2276
2394
|
const git = await gitInfo(ctx.cwd);
|
|
2277
2395
|
const envBudget = envOr(ctx.env.ARGUS_BUDGET_USD);
|
|
2278
|
-
|
|
2279
|
-
if (
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2396
|
+
const envSetting = parseBudgetSetting(envBudget);
|
|
2397
|
+
if (envSetting.kind === 'invalid') {
|
|
2398
|
+
ctx.err(`warning: ignoring invalid ARGUS_BUDGET_USD="${envBudget}"`);
|
|
2399
|
+
}
|
|
2400
|
+
// 'keep' → fall back to config; otherwise the env value (undefined = unlimited)
|
|
2401
|
+
const appliedBudget = applyBudgetSetting(envSetting);
|
|
2402
|
+
const hasEnvBudget = appliedBudget !== 'keep';
|
|
2403
|
+
const envCap = appliedBudget === 'keep' ? undefined : appliedBudget;
|
|
2286
2404
|
const budgets = {};
|
|
2287
|
-
const reviewBudget =
|
|
2288
|
-
const flowBudget =
|
|
2405
|
+
const reviewBudget = hasEnvBudget ? envCap : config.codeReviewBudgetUsd;
|
|
2406
|
+
const flowBudget = hasEnvBudget ? envCap : config.budgetUsd;
|
|
2289
2407
|
if (reviewBudget !== undefined)
|
|
2290
2408
|
budgets.review = { limitUsd: reviewBudget };
|
|
2291
2409
|
if (flowBudget !== undefined)
|
|
2292
2410
|
budgets.flow = { limitUsd: flowBudget };
|
|
2293
|
-
const appBudget = config.app.budgetUsd ??
|
|
2411
|
+
const appBudget = config.app.budgetUsd ?? (hasEnvBudget ? envCap : config.budgetUsd);
|
|
2412
|
+
if (selection.app && appBudget === undefined)
|
|
2413
|
+
ctx.err(UNCAPPED_WARNING);
|
|
2294
2414
|
budgets.app = {
|
|
2295
2415
|
...(appBudget !== undefined ? { limitUsd: appBudget } : {}),
|
|
2296
2416
|
maxDurationMs: config.app.timeoutMs ?? APP_LANE_DEFAULT_TIMEOUT_MS,
|
|
@@ -2730,145 +2850,52 @@ Then reports which optional features your environment already supports
|
|
|
2730
2850
|
|
|
2731
2851
|
Options:
|
|
2732
2852
|
--force Overwrite files that already exist
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
const a0Block = a0Host !== undefined
|
|
2739
|
-
? `
|
|
2740
|
-
// Optional: Agent Zero detected at ${a0Host}. Nothing below runs unless
|
|
2741
|
-
// you ask for it — both stays commented until you opt in deliberately.
|
|
2742
|
-
// a0: { url: ${JSON.stringify(a0Host)} }, // enables \`verify --a0\` (self-reported, unmetered)
|
|
2743
|
-
// heal: 'a0', // escalates a failed heal to the A0 host
|
|
2744
|
-
`
|
|
2745
|
-
: '';
|
|
2746
|
-
return `import { defineConfig } from 'argus-reviewer-e2e'
|
|
2853
|
+
--pr Open an onboarding pull request instead of writing files here
|
|
2854
|
+
(uses your git and gh; never reads or sends your OpenRouter key)
|
|
2855
|
+
--repo <owner/name> With --pr: confirm the target (must match origin)
|
|
2856
|
+
--branch <name> With --pr: branch to use (default argus/onboarding)
|
|
2857
|
+
-h, --help
|
|
2747
2858
|
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2859
|
+
--pr refuses to overwrite existing files and, if the branch or an open PR
|
|
2860
|
+
already exists, reports it instead of creating another.`;
|
|
2861
|
+
/** `argus-reviewer init --pr` — open an onboarding PR through local git + gh. */
|
|
2862
|
+
async function cmdInitPr(values, ctx, deps) {
|
|
2863
|
+
const branch = values.branch ?? DEFAULT_PR_BRANCH;
|
|
2864
|
+
const invalid = (values.force ? '--force cannot be combined with --pr (a PR never overwrites files)' : undefined) ??
|
|
2865
|
+
(values.repo !== undefined ? validateRepo(values.repo) : undefined) ??
|
|
2866
|
+
validateBranch(branch);
|
|
2867
|
+
if (invalid !== undefined) {
|
|
2868
|
+
usageError(ctx, 'init', invalid, 'argus-reviewer init --help');
|
|
2869
|
+
return 2;
|
|
2870
|
+
}
|
|
2871
|
+
try {
|
|
2872
|
+
const result = await initPr({
|
|
2873
|
+
cwd: ctx.cwd,
|
|
2874
|
+
exec: deps.exec ?? defaultExec,
|
|
2875
|
+
repo: values.repo,
|
|
2876
|
+
branch,
|
|
2877
|
+
budgetUsd: DEFAULT_BUDGET_USD,
|
|
2878
|
+
});
|
|
2879
|
+
ctx.out(result.kind === 'existing'
|
|
2880
|
+
? `onboarding PR already open for ${result.repo} (${result.branch}): ${result.url}`
|
|
2881
|
+
: `opened onboarding PR for ${result.repo} (${result.branch}): ${result.url}`);
|
|
2882
|
+
ctx.out('Add OPENROUTER_API_KEY as a repository secret before merging; this command never reads it.');
|
|
2883
|
+
return 0;
|
|
2884
|
+
}
|
|
2885
|
+
catch (e) {
|
|
2886
|
+
reportError(ctx, e, 'init --pr', 'COMMAND_FAILED');
|
|
2887
|
+
return 1;
|
|
2888
|
+
}
|
|
2770
2889
|
}
|
|
2771
|
-
const INIT_TEST = `test('home renders', async (td) => {
|
|
2772
|
-
const ok = await td.assert('the page rendered without obvious errors')
|
|
2773
|
-
if (!ok) throw new Error('home did not render')
|
|
2774
|
-
})
|
|
2775
|
-
`;
|
|
2776
|
-
const INIT_WORKFLOW = `name: argus-reviewer
|
|
2777
|
-
|
|
2778
|
-
on:
|
|
2779
|
-
pull_request:
|
|
2780
|
-
# 'labeled' lets a maintainer re-trigger with the argus-probe label when
|
|
2781
|
-
# sandbox probes are enabled for fork PRs.
|
|
2782
|
-
types: [opened, synchronize, reopened, labeled]
|
|
2783
|
-
|
|
2784
|
-
jobs:
|
|
2785
|
-
argus:
|
|
2786
|
-
runs-on: ubuntu-latest
|
|
2787
|
-
# 'labeled' fires on EVERY label — only argus-probe is the fork-gate
|
|
2788
|
-
# signal worth a full review run.
|
|
2789
|
-
if: github.event.action != 'labeled' || github.event.label.name == 'argus-probe'
|
|
2790
|
-
permissions:
|
|
2791
|
-
contents: read
|
|
2792
|
-
issues: write
|
|
2793
|
-
pull-requests: write
|
|
2794
|
-
checks: write
|
|
2795
|
-
statuses: write
|
|
2796
|
-
steps:
|
|
2797
|
-
# persist-credentials: false keeps the GITHUB_TOKEN out of .git/config —
|
|
2798
|
-
# the probe sandbox masks .git regardless, but don't store it at all.
|
|
2799
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
2800
|
-
with:
|
|
2801
|
-
persist-credentials: false
|
|
2802
|
-
ref: \${{ github.event.pull_request.head.sha || github.sha }}
|
|
2803
|
-
# Optional verdict-as-review: let Argus submit APPROVE / REQUEST_CHANGES
|
|
2804
|
-
# so require_approving_reviews counts it. GITHUB_TOKEN cannot approve, so
|
|
2805
|
-
# create + install your own GitHub App (docs/github-app.md), set the
|
|
2806
|
-
# ARGUS_APP_ID variable and ARGUS_APP_PRIVATE_KEY secret, then uncomment:
|
|
2807
|
-
# - uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2
|
|
2808
|
-
# id: argus-app
|
|
2809
|
-
# with:
|
|
2810
|
-
# app-id: \${{ vars.ARGUS_APP_ID }}
|
|
2811
|
-
# private-key: \${{ secrets.ARGUS_APP_PRIVATE_KEY }}
|
|
2812
|
-
# and pass approval-token plus its evidence inputs to the action below:
|
|
2813
|
-
# approval-token: \${{ steps.argus-app.outputs.token }}
|
|
2814
|
-
# approval-evidence: 'npm test' # command the approval stands on
|
|
2815
|
-
# approval-check: 'test' # check-run name, green on head SHA
|
|
2816
|
-
- uses: duketopceo/Argus/action@1f6bdc322f06e6dab39b3e3765b34676a0058882 # v0.4.0
|
|
2817
|
-
with:
|
|
2818
|
-
openrouter-api-key: \${{ secrets.OPENROUTER_API_KEY }}
|
|
2819
|
-
`;
|
|
2820
|
-
const INIT_MENTION_WORKFLOW = `name: argus-mention
|
|
2821
|
-
|
|
2822
|
-
# @argus mention commands on PR comments — '@argus review', '@argus
|
|
2823
|
-
# record "<flow>"', '@argus persist', '@argus help'. issue_comment is
|
|
2824
|
-
# strictly more privileged than pull_request (secrets + write token are
|
|
2825
|
-
# present), so the checkout below deliberately resolves the BASE ref —
|
|
2826
|
-
# never the PR head. Argus reviews the head diff over the API.
|
|
2827
|
-
on:
|
|
2828
|
-
issue_comment:
|
|
2829
|
-
types: [created]
|
|
2830
|
-
|
|
2831
|
-
jobs:
|
|
2832
|
-
argus-mention:
|
|
2833
|
-
runs-on: ubuntu-latest
|
|
2834
|
-
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '@argus')
|
|
2835
|
-
permissions:
|
|
2836
|
-
# contents: write — '@argus persist' commits reproduced probes to an
|
|
2837
|
-
# argus/ branch via the git/refs + contents APIs and opens a PR.
|
|
2838
|
-
contents: write
|
|
2839
|
-
issues: write
|
|
2840
|
-
pull-requests: write
|
|
2841
|
-
checks: write
|
|
2842
|
-
statuses: write
|
|
2843
|
-
steps:
|
|
2844
|
-
# No 'ref' — the default checkout resolves the base branch. persist
|
|
2845
|
-
# writes via the API, so checkout credentials stay disabled.
|
|
2846
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
2847
|
-
with:
|
|
2848
|
-
persist-credentials: false
|
|
2849
|
-
# Record commands need the app's dependencies to boot its target.
|
|
2850
|
-
# Uncomment if you use '@argus record':
|
|
2851
|
-
# - run: npm ci
|
|
2852
|
-
- uses: duketopceo/Argus/action@1f6bdc322f06e6dab39b3e3765b34676a0058882 # v0.4.0
|
|
2853
|
-
with:
|
|
2854
|
-
openrouter-api-key: \${{ secrets.OPENROUTER_API_KEY }}
|
|
2855
|
-
# '@argus record' uploads the generated test + flow cache as an
|
|
2856
|
-
# artifact — committing to a PR branch is intentionally not done.
|
|
2857
|
-
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
2858
|
-
if: contains(github.event.comment.body, 'record')
|
|
2859
|
-
with:
|
|
2860
|
-
name: argus-recorded-flow
|
|
2861
|
-
path: |
|
|
2862
|
-
tests/argus/
|
|
2863
|
-
.argus-reviewer-cache/
|
|
2864
|
-
if-no-files-found: ignore
|
|
2865
|
-
`;
|
|
2866
2890
|
/** `argus-reviewer init` — scaffold config, a smoke test, and the workflow. */
|
|
2867
2891
|
async function cmdInit(args, ctx, deps) {
|
|
2868
2892
|
const { values } = parseArgs({
|
|
2869
2893
|
args,
|
|
2870
2894
|
options: {
|
|
2871
2895
|
force: { type: 'boolean', default: false },
|
|
2896
|
+
pr: { type: 'boolean', default: false },
|
|
2897
|
+
repo: { type: 'string' },
|
|
2898
|
+
branch: { type: 'string' },
|
|
2872
2899
|
help: { type: 'boolean', short: 'h', default: false },
|
|
2873
2900
|
},
|
|
2874
2901
|
});
|
|
@@ -2876,6 +2903,12 @@ async function cmdInit(args, ctx, deps) {
|
|
|
2876
2903
|
ctx.out(INIT_USAGE);
|
|
2877
2904
|
return 0;
|
|
2878
2905
|
}
|
|
2906
|
+
if (values.pr)
|
|
2907
|
+
return cmdInitPr(values, ctx, deps);
|
|
2908
|
+
if (values.repo !== undefined || values.branch !== undefined) {
|
|
2909
|
+
usageError(ctx, 'init', '--repo and --branch only apply with --pr', 'argus-reviewer init --pr');
|
|
2910
|
+
return 2;
|
|
2911
|
+
}
|
|
2879
2912
|
// Detect first so the generated config can auto-enable what is present
|
|
2880
2913
|
// (e.g. an Agent Zero instance → heal: 'a0').
|
|
2881
2914
|
const env = await detectEnvironment(ctx.env, {
|
|
@@ -2887,22 +2920,15 @@ async function cmdInit(args, ctx, deps) {
|
|
|
2887
2920
|
'vision-e2e.config.ts',
|
|
2888
2921
|
'vision-e2e.config.json',
|
|
2889
2922
|
];
|
|
2890
|
-
const files = [
|
|
2891
|
-
['tests/argus/smoke.test.ts', INIT_TEST],
|
|
2892
|
-
['.github/workflows/argus-reviewer.yml', INIT_WORKFLOW],
|
|
2893
|
-
['.github/workflows/argus-mention.yml', INIT_MENTION_WORKFLOW],
|
|
2894
|
-
];
|
|
2895
2923
|
const hasConfig = configNames.some((n) => existsSync(join(ctx.cwd, n)));
|
|
2896
|
-
|
|
2897
|
-
files.unshift(['argus-reviewer.config.ts', initConfig(env.a0.host)]);
|
|
2898
|
-
}
|
|
2924
|
+
const files = renderScaffold({ a0Host: env.a0.host, includeConfig: !hasConfig || values.force });
|
|
2899
2925
|
// DESIGN.md 7.8: a three-step checklist (files, environment, next
|
|
2900
2926
|
// command) around the unchanged "What runs and what it costs" block.
|
|
2901
2927
|
const { style } = ctx;
|
|
2902
2928
|
const row = (status, text) => ` ${style.glyph(status)} ${text}`;
|
|
2903
2929
|
const fixLine = (cmd) => ` ${style.role('accent', cmd)}`;
|
|
2904
2930
|
ctx.out(style.bold('1. Write the setup files'));
|
|
2905
|
-
for (const
|
|
2931
|
+
for (const { path: rel, content } of files) {
|
|
2906
2932
|
const path = join(ctx.cwd, rel);
|
|
2907
2933
|
if (existsSync(path) && !values.force) {
|
|
2908
2934
|
ctx.out(row('skipped', `exists, skipping: ${rel}`));
|
|
@@ -2950,12 +2976,8 @@ async function cmdInit(args, ctx, deps) {
|
|
|
2950
2976
|
// R19: name what leaves the machine, the default spend posture, and
|
|
2951
2977
|
// the stop path before the user runs anything. Kept verbatim (DESIGN 7.8).
|
|
2952
2978
|
ctx.out('');
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
ctx.out(' review prompts — via your OpenRouter key (BYOK)');
|
|
2956
|
-
ctx.out(` default budget $${dm.budgetUsd ?? 1}/run cap (budgetUsd); cached replay costs $0`);
|
|
2957
|
-
ctx.out(' how to stop Ctrl+C locally; in CI remove the workflow file');
|
|
2958
|
-
ctx.out(' or delete the OPENROUTER_API_KEY secret');
|
|
2979
|
+
for (const line of scaffoldChecklist(DEFAULT_BUDGET_USD))
|
|
2980
|
+
ctx.out(line);
|
|
2959
2981
|
ctx.out('');
|
|
2960
2982
|
ctx.out(style.bold('3. Run the default lane (code review)'));
|
|
2961
2983
|
ctx.out(fixLine('argus-reviewer verify'));
|