pi-pr-review 1.15.13 → 1.15.15
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/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/extensions/pr-review-subagent.ts +54 -26
- package/lib/pr-review-artifacts.ts +5 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.15.15](https://github.com/10ego/pi-pr-review/compare/v1.15.14...v1.15.15) (2026-08-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **review:** eliminate large-review retry amplification ([#116](https://github.com/10ego/pi-pr-review/issues/116)) ([9aa0b82](https://github.com/10ego/pi-pr-review/commit/9aa0b82c3dbf80b3bb707634acd32c4a0c61b7df))
|
|
9
|
+
|
|
10
|
+
## [1.15.14](https://github.com/10ego/pi-pr-review/compare/v1.15.13...v1.15.14) (2026-08-28)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **review:** route oversized shards through read-only files ([#114](https://github.com/10ego/pi-pr-review/issues/114)) ([e300264](https://github.com/10ego/pi-pr-review/commit/e3002640e5d9060245a11a142a334652370598c0))
|
|
16
|
+
|
|
3
17
|
## [1.15.13](https://github.com/10ego/pi-pr-review/compare/v1.15.12...v1.15.13) (2026-08-28)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ A review uses five focused passes by default:
|
|
|
70
70
|
4. security (`heavy`);
|
|
71
71
|
5. performance and resource ownership (`heavy`).
|
|
72
72
|
|
|
73
|
-
`--full` adds a convention and maintainability pass (`medium`). Large multi-file diffs are split by whole-file boundaries and reviewed in parallel. The host enforces this before model dispatch: multi-file diffs from 200,000–399,999 bytes use two shards, and diffs of at least 400,000 bytes with at least three files use three shards, even if an orchestrator accidentally supplied the unified diff inline without requesting sharding. This avoids spending model attempts on payloads already known to exceed ordinary context budgets. Failed registered lanes remain visible and cannot be erased by model-named reruns. If the extension is unavailable, the prompt falls back to the current Pi session model.
|
|
73
|
+
`--full` adds a convention and maintainability pass (`medium`). Large multi-file diffs are split by whole-file boundaries and reviewed in parallel. The host enforces this before model dispatch: multi-file diffs from 200,000–399,999 bytes use two shards, and diffs of at least 400,000 bytes with at least three files use three shards, even if an orchestrator accidentally supplied the unified diff inline without requesting sharding. This avoids spending model attempts on payloads already known to exceed ordinary context budgets. If a whole-file shard still exceeds 1 MiB and has a captured `context_file`, the host sends only a bounded diff-header manifest and forces configured read-only tools so the reviewer inspects the file-backed shard instead of embedding megabytes in its prompt. Failed registered lanes remain visible and cannot be erased by model-named reruns. If the extension is unavailable, the prompt falls back to the current Pi session model.
|
|
74
74
|
|
|
75
75
|
When host lane evidence degrades or is incomplete, TUI and automation output starts with the deterministic host-owned `Comment` verdict and coverage disclosure. Any contradictory model verdict retained for evidence is relabeled as non-authoritative.
|
|
76
76
|
|
|
@@ -127,6 +127,7 @@ const TOOL_POLICIES: ToolPolicy[] = ["configured", "none"];
|
|
|
127
127
|
const TOOLS_PRESETS = ["read,bash,grep,find,ls", "read,grep,find,ls", "read"];
|
|
128
128
|
const DEFAULT_BATCH_PARALLEL = 4;
|
|
129
129
|
const MAX_BATCH_PARALLEL = 18;
|
|
130
|
+
const MAX_EMBEDDED_REVIEW_SHARD_BYTES = 1024 * 1024;
|
|
130
131
|
const TIER_PURPOSE: Record<Tier, string> = {
|
|
131
132
|
light: "overview / strengths / high-level risk scan",
|
|
132
133
|
medium: "convention compliance + readability / maintainability",
|
|
@@ -814,6 +815,8 @@ interface SubagentPassRequest {
|
|
|
814
815
|
generation?: number;
|
|
815
816
|
artifactKey?: string;
|
|
816
817
|
requestedPassOrdinal?: number;
|
|
818
|
+
toolNames?: readonly string[];
|
|
819
|
+
fileBackedContext?: boolean;
|
|
817
820
|
}
|
|
818
821
|
|
|
819
822
|
interface ModelAttemptReport {
|
|
@@ -928,7 +931,7 @@ async function runSubagentAttempt(
|
|
|
928
931
|
const args = buildReviewBaseArgs();
|
|
929
932
|
if (attempt.spec) args.push("--model", attempt.spec);
|
|
930
933
|
appendTierThinkingArgs(args, attempt.spec, config.thinkingLevels[pass.tier]);
|
|
931
|
-
appendToolPolicyArgs(args, toolPolicy, config.tools);
|
|
934
|
+
appendToolPolicyArgs(args, toolPolicy, pass.toolNames ?? config.tools);
|
|
932
935
|
|
|
933
936
|
tmp = await writeTempPrompt(
|
|
934
937
|
pass.tier,
|
|
@@ -1868,41 +1871,65 @@ export default function registerPrReviewSubagents(
|
|
|
1868
1871
|
? shardUnifiedDiff(diffForSharding!, requestedShardCount)
|
|
1869
1872
|
: [];
|
|
1870
1873
|
const sharded = requestedShards.length > 1;
|
|
1874
|
+
const topUnshardedFileBacked = !sharded && loadedContext.contextFileBytes > MAX_EMBEDDED_REVIEW_SHARD_BYTES && !!loadedContext.contextFile && !!loadedContext.contextFileText;
|
|
1871
1875
|
const selectedFileDiffIndex = loadedDiff ? inlineContext.indexOf(loadedDiff) : -1;
|
|
1872
1876
|
const compactInlineContext = selectedFileDiffIndex >= 0
|
|
1873
1877
|
? [inlineContext.slice(0, selectedFileDiffIndex).trim(), inlineContext.slice(selectedFileDiffIndex + loadedDiff!.length).trim()].filter(Boolean).join("\n\n")
|
|
1874
1878
|
: inlineDiffStart >= 0 && !loadedDiff
|
|
1875
1879
|
? inlineContext.slice(0, inlineDiffStart).trim()
|
|
1876
1880
|
: inlineContext;
|
|
1877
|
-
const sharedContext = sharded ? compactInlineContext || undefined : loadedContext.context;
|
|
1881
|
+
const sharedContext = sharded || topUnshardedFileBacked ? compactInlineContext || undefined : loadedContext.context;
|
|
1878
1882
|
const majorOnly = params.major_only === true;
|
|
1879
1883
|
const minorHygiene = params.minor_hygiene === true;
|
|
1880
|
-
const
|
|
1884
|
+
const exactHeaderManifest = (text: string) => {
|
|
1885
|
+
const headers = text.match(/^diff --git .+$/gm) ?? [], bytes = Buffer.byteLength(headers.join("\n"), "utf8");
|
|
1886
|
+
return headers.length > 0 && headers.length <= 2_000 && bytes <= 256 * 1024 && headers.every((line) => Buffer.byteLength(line, "utf8") <= 2_048 && !/[\u0000-\u001f\u007f]/.test(line)) ? headers : undefined;
|
|
1887
|
+
};
|
|
1888
|
+
const topShardHeaders = requestedShards.map((shard) => loadedContext.contextFile && Buffer.byteLength(shard, "utf8") > MAX_EMBEDDED_REVIEW_SHARD_BYTES ? exactHeaderManifest(shard) : []);
|
|
1889
|
+
if (topShardHeaders.some((headers) => headers === undefined)) {
|
|
1890
|
+
return { content: [{ type: "text", text: "Review batch context failed: oversized file-backed shard manifest is unsafe or exceeds deterministic bounds." }], isError: true, details: { passCount: rawPasses.length, contextFileBytes: loadedContext.contextFileBytes, shardCount: 0 } };
|
|
1891
|
+
}
|
|
1892
|
+
const fileBackedInstructions = (contextFile: string, label: string, headers?: readonly string[]) => [
|
|
1893
|
+
`--- File-backed ${label} ---`,
|
|
1894
|
+
`The complete captured unified diff is at ${JSON.stringify(contextFile)}. Use only the host-enforced read-only tools (read, grep, find, ls) to inspect it; the oversized diff is intentionally not embedded in this prompt.`,
|
|
1895
|
+
...(headers ? ["Review only these exact diff headers:", ...headers] : ["Review the complete captured diff in this file."]),
|
|
1896
|
+
].join("\n");
|
|
1897
|
+
const passesWithoutFocus = rawPasses.flatMap((pass, index) => {
|
|
1881
1898
|
const tier = pass.tier as Tier;
|
|
1882
1899
|
const baseId = typeof pass.id === "string" && pass.id.trim() ? pass.id.trim() : `${index + 1}-${tier}`;
|
|
1883
|
-
const
|
|
1884
|
-
const
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1900
|
+
const loadedPass = loadedPassContexts[index]!;
|
|
1901
|
+
const passFileBacked = loadedPass.contextFileBytes > MAX_EMBEDDED_REVIEW_SHARD_BYTES && !!loadedPass.contextFile && !!loadedPass.contextFileText;
|
|
1902
|
+
const passContext = passFileBacked ? (typeof pass.context === "string" ? pass.context.trim() : undefined) : loadedPass.context;
|
|
1903
|
+
const baseContext = combineContexts(sharedContext, passContext);
|
|
1904
|
+
const makePass = (shard: string | undefined, shardIndex: number) => {
|
|
1905
|
+
const topFileBacked = (!!shard && Buffer.byteLength(shard, "utf8") > MAX_EMBEDDED_REVIEW_SHARD_BYTES || topUnshardedFileBacked) && !!loadedContext.contextFile;
|
|
1906
|
+
const fileBacked = passFileBacked || topFileBacked;
|
|
1907
|
+
const label = sharded ? `diff shard ${shardIndex + 1}/${requestedShards.length}` : passFileBacked ? `pass context for ${baseId}` : "complete diff";
|
|
1908
|
+
const fileInstructions = passFileBacked
|
|
1909
|
+
? fileBackedInstructions(loadedPass.contextFile!, label)
|
|
1910
|
+
: topFileBacked
|
|
1911
|
+
? fileBackedInstructions(loadedContext.contextFile!, label, shard ? topShardHeaders[shardIndex] as readonly string[] : undefined)
|
|
1912
|
+
: undefined;
|
|
1913
|
+
return {
|
|
1914
|
+
// Every actual shard receives an explicit identity, including shard 1.
|
|
1915
|
+
// The unsharded compatibility path retains the caller's exact base ID.
|
|
1916
|
+
id: sharded ? `${baseId}-shard-${shardIndex + 1}` : baseId,
|
|
1917
|
+
expectedOutput: pass.expected_output,
|
|
1918
|
+
tier,
|
|
1919
|
+
objective: shard
|
|
1920
|
+
? `${pass.objective}\nReview every changed line in diff shard ${shardIndex + 1}/${requestedShards.length} under this objective. Other concurrent shard passes cover the remaining changed files.`
|
|
1921
|
+
: pass.objective,
|
|
1922
|
+
context: fileInstructions ? combineContexts(baseContext, fileInstructions) : shard
|
|
1923
|
+
? combineContexts(baseContext, `--- Complete diff shard ${shardIndex + 1}/${requestedShards.length} ---\n${shard}`)
|
|
1924
|
+
: baseContext,
|
|
1925
|
+
toolPolicy: fileBacked ? "configured" as const : normalizeToolPolicy(pass.tool_policy),
|
|
1926
|
+
...(fileBacked ? { toolNames: ["read", "grep", "find", "ls"] } : {}),
|
|
1927
|
+
majorOnly,
|
|
1928
|
+
minorHygiene: minorHygiene && tier === "light" && baseId === "overview",
|
|
1929
|
+
fileBackedContext: fileBacked,
|
|
1930
|
+
};
|
|
1931
|
+
};
|
|
1932
|
+
return sharded ? requestedShards.map((shard, shardIndex) => makePass(shard, shardIndex)) : [makePass(undefined, 0)];
|
|
1906
1933
|
});
|
|
1907
1934
|
if (!loopCoordinator.isLeaseActive(lease, ctx)) return reviewLoopDeniedResult("review_subagents");
|
|
1908
1935
|
const artifactPublisher = loopCoordinator.createArtifactPublisher(lease, ctx);
|
|
@@ -1986,6 +2013,7 @@ export default function registerPrReviewSubagents(
|
|
|
1986
2013
|
shardingSource: automaticShardCount > explicitShardCount ? "automatic-size-preflight" : explicitShardCount > 1 ? "requested" : "none",
|
|
1987
2014
|
diffBytes,
|
|
1988
2015
|
changedFileCount,
|
|
2016
|
+
fileBackedPassCount: passes.filter((pass) => pass.fileBackedContext === true).length,
|
|
1989
2017
|
sharedContextBytes: Buffer.byteLength(sharedContext ?? "", "utf8"),
|
|
1990
2018
|
contextFileBytes:
|
|
1991
2019
|
loadedContext.contextFileBytes +
|
|
@@ -462,7 +462,11 @@ function expectedLaneSections(input: ReviewLaneCompletionInput): boolean {
|
|
|
462
462
|
const fields = input.minorHygiene ? ["overview", "strengths", "minor candidates"] : ["overview", "strengths"];
|
|
463
463
|
return fields.every((field) => hasMeaningfulLightSection(text, field, fields));
|
|
464
464
|
}
|
|
465
|
-
|
|
465
|
+
// Ordinary lanes use the sentinel only as an unambiguous clean result; a
|
|
466
|
+
// missing final period carries no semantic uncertainty and must not amplify
|
|
467
|
+
// into expensive replacement passes. Integrated/deep `nonempty` output keeps
|
|
468
|
+
// its exact byte contract in parseIntegratedCompletion().
|
|
469
|
+
if (/^NO FINDINGS\.?$/.test(text)) return true;
|
|
466
470
|
return CANDIDATE_FIELDS.every((field) => hasMeaningfulField(text, field));
|
|
467
471
|
}
|
|
468
472
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-pr-review",
|
|
3
|
-
"version": "1.15.
|
|
3
|
+
"version": "1.15.15",
|
|
4
4
|
"description": "Parallel AI code review for GitHub pull requests in the Pi coding agent, with model-agnostic tiered subagents, structured findings, optional verification, and host-gated COMMENT or qualified APPROVE publishing.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|