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 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 passesWithoutFocus: SubagentPassRequest[] = rawPasses.flatMap((pass, index) => {
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 baseContext = combineContexts(sharedContext, loadedPassContexts[index]!.context);
1884
- const makePass = (shard: string | undefined, shardIndex: number): SubagentPassRequest => ({
1885
- // Every actual shard receives an explicit identity, including shard 1.
1886
- // The unsharded compatibility path retains the caller's exact base ID.
1887
- id: sharded ? `${baseId}-shard-${shardIndex + 1}` : baseId,
1888
- expectedOutput: pass.expected_output,
1889
- tier,
1890
- objective: shard
1891
- ? `${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.`
1892
- : pass.objective,
1893
- context: shard
1894
- ? combineContexts(
1895
- baseContext,
1896
- `--- Complete diff shard ${shardIndex + 1}/${requestedShards.length} ---\n${shard}`,
1897
- )
1898
- : baseContext,
1899
- toolPolicy: normalizeToolPolicy(pass.tool_policy),
1900
- majorOnly,
1901
- minorHygiene: minorHygiene && tier === "light" && baseId === "overview",
1902
- });
1903
- return sharded
1904
- ? requestedShards.map((shard, shardIndex) => makePass(shard, shardIndex))
1905
- : [makePass(undefined, 0)];
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
- if (text === NO_FINDINGS_SENTINEL) return true;
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.13",
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",