Skip to content
13 changes: 7 additions & 6 deletions docs/users/configuration/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,13 @@ Settings are organized into categories. Most settings should be placed within th

#### review

| Setting | Type | Description | Default |
| ---------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `review.attribution` | boolean | Append the attribution footer naming the model and CLI version (e.g. `_— qwen3-coder via Qwen Code /review (v0.21.2)_`) to review bodies and inline comments posted by `/review`. Disable to post reviews without AI attribution. With the footer off, presubmit duplicate detection still recognizes earlier posts by the same GitHub account, but footer-less posts from other accounts escape it. | `true` |
| `review.effort` | enum | Default effort for `/review` when `--effort` is not given: `"low"`, `"medium"`, `"high"`, or `"auto"` (the built-in rule: high for PRs, medium for local changes). An explicit `--effort` wins; an effective `--comment` still forces high and `--fix` still floors at medium. | `"auto"` |
| `review.comment` | boolean | Treat every PR `/review` as if `--comment` was passed: findings are posted to the pull request without the flag. The post still binds to the PR named in the invocation. Enable only if you always want reviews published. | `false` |
| `review.severityFloor` | enum | The lowest severity a PR `/review` posts when `--severity-floor` is not given: `"auto"` (the round-adaptive default — Suggestions post through round 5, only Criticals from round 6, with otherwise-postable high-confidence Suggestions recorded and deferred, and rounds 2–5 deferring new Suggestions on code unchanged since the previous round; low-confidence and Nice-to-have findings stay terminal-only), `"critical"` (that posture from round 1), or `"suggestion"` (Suggestions post at every round; turns the convergence posture off). Non-PR targets have no rounds and ignore this. | `"auto"` |
| Setting | Type | Description | Default |
| --------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `review.attribution` | boolean | Append the attribution footer naming the model and CLI version (e.g. `_— qwen3-coder via Qwen Code /review (v0.21.2)_`) to review bodies and inline comments posted by `/review`. Disable to post reviews without AI attribution. With the footer off, presubmit duplicate detection still recognizes earlier posts by the same GitHub account, but footer-less posts from other accounts escape it. | `true` |
| `review.effort` | enum | Default effort for `/review` when `--effort` is not given: `"low"`, `"medium"`, `"high"`, or `"auto"` (the built-in rule: high for PRs, medium for local changes). An explicit `--effort` wins; an effective `--comment` still forces high and `--fix` still floors at medium. | `"auto"` |
| `review.comment` | boolean | Treat every PR `/review` as if `--comment` was passed: findings are posted to the pull request without the flag. The post still binds to the PR named in the invocation. Enable only if you always want reviews published. | `false` |
| `review.severityFloor` | enum | The lowest severity a PR `/review` posts when `--severity-floor` is not given: `"auto"` (the round-adaptive default — Suggestions post through round 5, only Criticals from round 6, with otherwise-postable high-confidence Suggestions recorded and deferred, and rounds 2–5 deferring new Suggestions on code unchanged since the previous round; low-confidence and Nice-to-have findings stay terminal-only), `"critical"` (that posture from round 1), or `"suggestion"` (Suggestions post at every round; turns the convergence posture off). Non-PR targets have no rounds and ignore this. | `"auto"` |
| `review.reverseAuditRounds` | number | Lower the reverse-audit loop's round cap for every high-effort review. The cap otherwise follows the diff topology (10 small / 5 chunked; a huge diff is 3 with a review deadline and 5 without). This can only **lower** whichever tier applies: a value below 3, above the tier, or not a whole number above zero is ignored. Cutting the cap does not make reviews converge sooner — the loop ends on two consecutive dry rounds — it makes them stop before converging more often, and every such stop caps the verdict at Comment. | `0` (unset) |

These settings are read from operator scopes only (User, System, and SystemDefaults); values in a workspace `.qwen/settings.json` are ignored, so a repository cannot set review policy for its reviewers.

Expand Down
16 changes: 8 additions & 8 deletions docs/users/features/code-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Step 3B: high, >500 src OR >3200 total: territory x dim. [N+5..7+3H calls]
Step 4: Deduplicate --> Sharded verify (<=8 findings each)
--> Aggregate [ceil(F/8) calls, F=findings]
Step 5: Iterative reverse audit, fanned out per chunk;
stop after 2 consecutive dry rounds (cap 5)
stop after 2 consecutive dry rounds (cap 10/5/3 by topology)
Step 6: Present findings + verdict (high; low pass: findings only)
Canonicalize findings -> .qwen/tmp/...-findings.json
Step 6B: Apply findings + record per-finding outcomes (--fix only)
Expand Down Expand Up @@ -124,7 +124,7 @@ A **source** file that is largely rewritten (an existing file of 300+ lines that

The checklist is split three ways on purpose. Handing one agent all eight checks over a 2 400-line file gets one of them done properly; three agents with two or three checks each get all of them done. Chunk agents do not substitute for this — on PR #6457 they held every one of these defects inside their assigned territory and reported none. What they lacked was not the lines but the question.

Findings are verified in **sharded batches** (at most 8 findings per verification agent, all launched together). A verifier may reject a Critical only by quoting the code that contradicts it (or when the diff's own comments document the flagged behavior as deliberate); anything less certain is downgraded to low confidence rather than deleted — a silently rejected Critical is invisible to every later stage, while a downgraded one still reaches a human. After verification, **iterative reverse audit** hunts for gaps, fanned out one auditor per chunk per round, each with the cumulative finding list. The loop stops after **two consecutive dry rounds** (or 5 rounds, hard cap — reported as such rather than as convergence). One dry round is not evidence of convergence, and reverse-audit findings are verified like any other.
Findings are verified in **sharded batches** (at most 8 findings per verification agent, all launched together). A verifier may reject a Critical only by quoting the code that contradicts it (or when the diff's own comments document the flagged behavior as deliberate); anything less certain is downgraded to low confidence rather than deleted — a silently rejected Critical is invisible to every later stage, while a downgraded one still reaches a human. After verification, **iterative reverse audit** hunts for gaps, fanned out one auditor per chunk per round, each with the cumulative finding list. The loop stops after **two consecutive dry rounds** (or at the plan's round cap — reported as such rather than as convergence). That cap follows the diff's topology: **10** on a small diff, where a round is a single auditor; **5** on a chunked one, where it is one auditor per chunk; and **3** on a huge diff (≥ 3000 effective lines) _when the run has a deadline_, because five ~90-minute rounds do not fit a six-hour CI ceiling and a review killed mid-flight posts nothing — with no deadline a huge diff keeps the chunked cap of 5. An operator can lower whichever cap applies for every review with the `review.reverseAuditRounds` setting; it can never raise one. One dry round is not evidence of convergence, and reverse-audit findings are verified like any other.
Comment thread
wenshao marked this conversation as resolved.

## Severity Levels

Expand Down Expand Up @@ -421,12 +421,12 @@ Why the floors are where they are: on a nine-line typo fix, six inline walks are

The high-effort pipeline bounds each stage (shard size, audit rounds), but total calls scale with findings — `ceil(F/8)` verification shards — and, under 3B, with chunk count (reverse audit runs per chunk per round). Typical 3A profile:

| Stage | LLM calls | Notes |
| -------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| Review agents (Step 3) | 14 (+0-2) | Run in parallel; cross-repo skips Agents 1c and 7 (12), local/file skips Agent 0 (13) |
| Sharded verification (Step 4) | ceil(F/8) | F = findings; at most 8 per verification agent, launched together |
| Iterative reverse audit (Step 5) | 2-5 (3A); rounds × chunks (3B) | Two consecutive dry rounds to stop (cap 5); 3B fans out one auditor per chunk per round |
| **Total** | **~17-23 (~15-22)** | 3A same-repo: ~17-23 (typical ~17-19); cross-repo or local/file: ~15-22; 3B scales with chunks (see DESIGN.md) |
| Stage | LLM calls | Notes |
| -------------------------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Review agents (Step 3) | 14 (+0-2) | Run in parallel; cross-repo skips Agents 1c and 7 (12), local/file skips Agent 0 (13) |
| Sharded verification (Step 4) | ceil(F/8) | F = findings; at most 8 per verification agent, launched together |
| Iterative reverse audit (Step 5) | 2-10 (3A); rounds × chunks (3B) | Two consecutive dry rounds to stop; the cap follows the topology — 10 on a small diff, 5 on a chunked one, 3 on a huge one when the run has a deadline. 3B fans out one auditor per chunk per round |
| **Total** | **~17-28 (~15-27)** | 3A same-repo: ~17-28 (typical ~17-19); cross-repo or local/file: ~15-27; 3B scales with chunks (see DESIGN.md) |

Most PRs converge to the lower end of the range; the caps prevent runaway cost on pathological cases. At `--effort low` the review runs entirely inline — **0 subagent calls** — walking the diff once per angle instead of once in total.

Expand Down
56 changes: 56 additions & 0 deletions packages/cli/src/commands/review/agent-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,62 @@ describe('--round — the CLI bakes the round into the identity line and the key
}
});

it('takes the round cap from the CLOCK as well, on a sized huge plan', () => {
// Every other cap test here uses the unsized `PLAN` fixture, whose tier is
// the LARGE fallback whatever the clock says, or forces a cap by storing
// one — so the `hasReviewDeadline(process.env)` argument at all four call
// sites was mutation-invisible: hardcoding it to either constant left the
// whole suite green. A SIZED huge plan is the only shape where the flag
// decides anything.
const dir = mkdtempSync(join(tmpdir(), 'ap-clock-tier-'));
try {
const findings = join(dir, 'f.md');
writeFileSync(findings, '- x');
const handler = agentPromptCommand.handler as (a: unknown) => void;
const before = process.env[DEADLINE_ENV];
const stderr = () =>
(writeStderrLine as unknown as Mock).mock.calls
.map((c) => c[0])
.join('\n');
const huge = join(dir, 'huge.json');
writeFileSync(
huge,
JSON.stringify({ ...PLAN, srcDiffLines: 5000, diffLines: 5000 }),
);
try {
// No clock: the huge reduction does not apply, so the 3B tier stands
// and round 4 builds.
delete process.env[DEADLINE_ENV];
process.exitCode = undefined;
(writeStderrLine as unknown as Mock).mockClear();
handler({ plan: huge, role: 'reverse-audit', findings, round: 4 });
expect(process.exitCode).toBeUndefined();
expect(readRecordedPrompts(huge).size).toBe(1);

(writeStderrLine as unknown as Mock).mockClear();
handler({ plan: huge, role: 'reverse-audit', findings, round: 6 });
expect(process.exitCode).toBe(4);
expect(stderr()).toContain('round cap is 5');

// A clock: the same plan, the same round, refused at the reduced tier.
process.env[DEADLINE_ENV] = String(
Math.floor(Date.now() / 1000) + 7200,
);
process.exitCode = undefined;
(writeStderrLine as unknown as Mock).mockClear();
handler({ plan: huge, role: 'reverse-audit', findings, round: 4 });
expect(process.exitCode).toBe(4);
expect(stderr()).toContain('round cap is 3');
} finally {
if (before === undefined) delete process.env[DEADLINE_ENV];
else process.env[DEADLINE_ENV] = before;
}
} finally {
process.exitCode = undefined;
rmSync(dir, { recursive: true, force: true });
}
});

it('takes the round cap from the plan’s topology on the chunkless path', () => {
// 3A is the topology that actually runs this path — one auditor a round,
// the whole diff — and it is the one the tier raises. Both arms use the
Expand Down
15 changes: 10 additions & 5 deletions packages/cli/src/commands/review/agent-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
verifyBudgetMessage,
writeBudgetStop,
writeRoundCapStop,
hasReviewDeadline,
} from './lib/deadline.js';
import {
READ_FILE_CHAR_CAP,
Expand Down Expand Up @@ -2004,7 +2005,8 @@ function admitReverseAuditRound(
// The plan's round cap first: deterministic, and cheaper than the
// deadline arithmetic. One value per topology (`reverseAuditRoundTier`) —
// ten on a 3A diff, where a round is one auditor; five on a 3B one, where
// it is one per non-retired chunk; a reduced three for a huge
// it is one per non-retired chunk; and — only in a run that has a deadline,
// since the reduction answers a ceiling — a reduced three for a huge
// diff, where a single reverse-audit round is ~90 minutes and the full
// loop cannot finish (measured: the 6-hour CI reviews that posted nothing
// were 4,000-5,300-line PRs). A round past the cap writes a marker so
Expand Down Expand Up @@ -2177,7 +2179,7 @@ function runAllChunks(
!admitReverseAuditRound(
planPath,
round,
reverseAuditRoundCap(report),
reverseAuditRoundCap(report, hasReviewDeadline(process.env)),
chunks.length,
Comment thread
wenshao marked this conversation as resolved.
Comment thread
wenshao marked this conversation as resolved.
)
) {
Expand Down Expand Up @@ -2233,7 +2235,10 @@ function runAllChunks(
: `one per chunk still under audit (${skipped.length} retired ` +
`chunk(s) skipped; the retirement note after the end-of-round line ` +
`says which — relay it to the terminal)`;
const planRoundCap = reverseAuditRoundCap(report);
const planRoundCap = reverseAuditRoundCap(
report,
hasReviewDeadline(process.env),
);
Comment thread
wenshao marked this conversation as resolved.
Comment thread
wenshao marked this conversation as resolved.
Comment thread
wenshao marked this conversation as resolved.
const retirementNote =
skipped.length === 0
? []
Expand Down Expand Up @@ -2596,7 +2601,7 @@ function runAgentPrompt(args: AgentPromptArgs): void {
!admitReverseAuditRound(
args.plan,
args.round,
reverseAuditRoundCap(report),
reverseAuditRoundCap(report, hasReviewDeadline(process.env)),
1,
)
) {
Expand Down Expand Up @@ -2679,7 +2684,7 @@ function runAgentPrompt(args: AgentPromptArgs): void {
!admitReverseAuditRound(
args.plan,
args.round,
reverseAuditRoundCap(report),
reverseAuditRoundCap(report, hasReviewDeadline(process.env)),
planChunkIds.length,
Comment thread
wenshao marked this conversation as resolved.
Comment thread
wenshao marked this conversation as resolved.
)
)
Expand Down
Loading
Loading