diffsplain 0.8.3 → 0.9.0

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.
Files changed (93) hide show
  1. package/README.md +10 -5
  2. package/dist/assets/{angular-html-e4D_THVy.js → angular-html-m_m91ZYB.js} +1 -1
  3. package/dist/assets/{angular-ts-Cx3870JF.js → angular-ts-CO5Z--Rb.js} +1 -1
  4. package/dist/assets/{apl-DZuu22Wh.js → apl-ogYYgLUu.js} +1 -1
  5. package/dist/assets/{astro-K-NemFAk.js → astro-DAfY3cNR.js} +1 -1
  6. package/dist/assets/{blade-ConbVu3U.js → blade-E6RzsLUV.js} +1 -1
  7. package/dist/assets/{c-DtmaBqln.js → c-kKdxhtHF.js} +1 -1
  8. package/dist/assets/{cobol-BMHoPOfB.js → cobol-3oDDyFNf.js} +1 -1
  9. package/dist/assets/{coffee-DfweJ19Y.js → coffee-dsiILuWe.js} +1 -1
  10. package/dist/assets/{cpp-CPh_zIOW.js → cpp-DYyNFq__.js} +1 -1
  11. package/dist/assets/{crystal-CAEX7C1J.js → crystal-B7dVp9TF.js} +1 -1
  12. package/dist/assets/{css-C8ihGqCA.js → css-DlxJ0aex.js} +1 -1
  13. package/dist/assets/{edge-DVZPwQ9j.js → edge-BjEVphkC.js} +1 -1
  14. package/dist/assets/{elixir-D03TKulP.js → elixir-0xTOtMcM.js} +1 -1
  15. package/dist/assets/{elm-JUX-2xVV.js → elm-iQk_Wrxq.js} +1 -1
  16. package/dist/assets/{erb-HbktAFZJ.js → erb-BO74ztiE.js} +1 -1
  17. package/dist/assets/{git-rebase-Ccd_ELAP.js → git-rebase-BN667QRY.js} +1 -1
  18. package/dist/assets/{glimmer-js-BcinUfi2.js → glimmer-js-DTGUIHeS.js} +1 -1
  19. package/dist/assets/{glimmer-ts-DuN2UkN9.js → glimmer-ts-BU3oQn2d.js} +1 -1
  20. package/dist/assets/{glsl-BWmCEPsR.js → glsl-KLiqLJi2.js} +1 -1
  21. package/dist/assets/{graphql-DJGYoBFW.js → graphql-D_idcsoJ.js} +1 -1
  22. package/dist/assets/{hack-ZPqBE9w3.js → hack-BXlsgPIr.js} +1 -1
  23. package/dist/assets/{haml-CZDZk6Gw.js → haml-fP0aUg4-.js} +1 -1
  24. package/dist/assets/{handlebars-d23PDS0n.js → handlebars-C1ZEaot-.js} +1 -1
  25. package/dist/assets/{html-BgzX2DyB.js → html-D04qIuup.js} +1 -1
  26. package/dist/assets/{html-derivative-Bq6M-ZFw.js → html-derivative-C6ai-9QL.js} +1 -1
  27. package/dist/assets/{http-DVhMl_cW.js → http-C7bz3ZvQ.js} +1 -1
  28. package/dist/assets/{hurl-CbspBAqF.js → hurl-jL6B6Ahr.js} +1 -1
  29. package/dist/assets/index-BPaJGi-m.js +1574 -0
  30. package/dist/assets/index-VAyBpb3u.css +2 -0
  31. package/dist/assets/{java--YZbq0z2.js → java-BVzvLCON.js} +1 -1
  32. package/dist/assets/{javascript-cvRuifRJ.js → javascript-UDbZHxCI.js} +1 -1
  33. package/dist/assets/{jinja-BeZmr3LP.js → jinja-DB-QKo2A.js} +1 -1
  34. package/dist/assets/{jison-qrpGZaxs.js → jison-B1ZbR2FF.js} +1 -1
  35. package/dist/assets/{json-4xUzsNJn.js → json-DV4WwJzT.js} +1 -1
  36. package/dist/assets/{jsx-BlXrTlSg.js → jsx-DS6_VTvW.js} +1 -1
  37. package/dist/assets/{julia-IN3jzmBc.js → julia-C4tevET0.js} +1 -1
  38. package/dist/assets/{just-C_50nLQH.js → just-CWMjefzB.js} +1 -1
  39. package/dist/assets/{latex-Cd-_2IS1.js → latex-tyEFf_Af.js} +1 -1
  40. package/dist/assets/{liquid-CEJf6Xg5.js → liquid-C3d1X1rL.js} +1 -1
  41. package/dist/assets/{lua-BlaV_n8i.js → lua-sXvrzLQY.js} +1 -1
  42. package/dist/assets/{marko-Cb-NPdc_.js → marko-CQweucif.js} +1 -1
  43. package/dist/assets/{mdc-Dt4ywVAS.js → mdc-TrxLWF7s.js} +1 -1
  44. package/dist/assets/{nginx-C_6SKViP.js → nginx-ChE2nSNJ.js} +1 -1
  45. package/dist/assets/{nim-BbkBjoeZ.js → nim-BY1CTRfx.js} +1 -1
  46. package/dist/assets/{perl-CYPU6TVa.js → perl-Cvj2PQQH.js} +1 -1
  47. package/dist/assets/{php-B6Oqilrc.js → php-CGmHp8HW.js} +1 -1
  48. package/dist/assets/{pug-C9Poo2KU.js → pug-DM4S0bRr.js} +1 -1
  49. package/dist/assets/{qml-DkMURMPv.js → qml-DI89Al9K.js} +1 -1
  50. package/dist/assets/{r-Bm-Gfi0p.js → r-Bs5_4yYh.js} +1 -1
  51. package/dist/assets/{razor-B0LqOok3.js → razor-DimzayhZ.js} +1 -1
  52. package/dist/assets/{regexp-jAbMHzwe.js → regexp-BLACz0cx.js} +1 -1
  53. package/dist/assets/{rst-BlWHfg8c.js → rst-DcWzxQh2.js} +1 -1
  54. package/dist/assets/{ruby-Bt5lKwPL.js → ruby-Dlat5E8d.js} +1 -1
  55. package/dist/assets/{sas-TuY2-8EF.js → sas-CCDmMaBY.js} +1 -1
  56. package/dist/assets/{scss-Dh1SsWlK.js → scss-BPiU2nu6.js} +1 -1
  57. package/dist/assets/{shellscript-CThiwmnp.js → shellscript-CKinVNrN.js} +1 -1
  58. package/dist/assets/{shellsession-DQ5iRUWd.js → shellsession-Cea8Pu5d.js} +1 -1
  59. package/dist/assets/{soy-BfM-BrZG.js → soy--BRAPY4-.js} +1 -1
  60. package/dist/assets/{sql-F617W1uN.js → sql-DjKiCyWK.js} +1 -1
  61. package/dist/assets/{stata-Ctov3ki2.js → stata-CDvo3J6y.js} +1 -1
  62. package/dist/assets/{surrealql-C-i2bJGB.js → surrealql-C7b7E5sh.js} +1 -1
  63. package/dist/assets/{svelte-oZT41pTl.js → svelte-DO0tKtiK.js} +1 -1
  64. package/dist/assets/{templ-CxX9Hf3e.js → templ-DhhmR_rv.js} +1 -1
  65. package/dist/assets/{tex-GyzJq8GJ.js → tex-Cn_V-Gg4.js} +1 -1
  66. package/dist/assets/{ts-tags-BugzKu0x.js → ts-tags-B6c-FTmA.js} +1 -1
  67. package/dist/assets/{tsx-DLqwDEUq.js → tsx-Cvtt2sJ-.js} +1 -1
  68. package/dist/assets/{twig-DADiR9Vq.js → twig-B7ZbCUmB.js} +1 -1
  69. package/dist/assets/{typescript-BlDIG_FO.js → typescript-i3C6um2B.js} +1 -1
  70. package/dist/assets/{vue-COXmmg2A.js → vue-DyPX0Vum.js} +1 -1
  71. package/dist/assets/{vue-html-Cy_3KYj1.js → vue-html-D362KpK3.js} +1 -1
  72. package/dist/assets/{vue-vine-CUqOY8dY.js → vue-vine-D34cBbwO.js} +1 -1
  73. package/dist/assets/{xml-C9JY7Wf6.js → xml-DJEk7u4n.js} +1 -1
  74. package/dist/assets/{xsl-DBCl8hSu.js → xsl-CoTtf90f.js} +1 -1
  75. package/dist/assets/{yaml-BbMebKkS.js → yaml-Cv5epQ4-.js} +1 -1
  76. package/dist/index.html +2 -2
  77. package/package.json +11 -3
  78. package/scripts/agent-exclusions.mjs +10 -0
  79. package/scripts/agent-review.mjs +94 -0
  80. package/scripts/build-diff-data.mjs +211 -49
  81. package/scripts/cli-args.mjs +36 -9
  82. package/scripts/coding-agents.mjs +206 -79
  83. package/scripts/generate-summaries.mjs +864 -425
  84. package/scripts/local-target.mjs +106 -0
  85. package/scripts/present.mjs +205 -72
  86. package/scripts/review-chat-context.mjs +510 -0
  87. package/scripts/review-chat-controller.mjs +568 -0
  88. package/scripts/review-chat-provider.mjs +352 -0
  89. package/scripts/review-chat.mjs +8 -0
  90. package/scripts/serve-built.mjs +206 -24
  91. package/scripts/summary-path.mjs +32 -11
  92. package/dist/assets/index-C2p_1iAk.css +0 -2
  93. package/dist/assets/index-divlsuKs.js +0 -1568
@@ -9,15 +9,28 @@ import {
9
9
  fstatSync,
10
10
  lstatSync,
11
11
  mkdirSync,
12
+ mkdtempSync,
12
13
  openSync,
13
14
  readFileSync,
14
15
  readlinkSync,
15
16
  renameSync,
17
+ rmSync,
16
18
  statSync,
17
19
  writeFileSync,
18
20
  } from 'node:fs';
19
- import { basename, dirname, relative, resolve } from 'node:path';
21
+ import { tmpdir } from 'node:os';
22
+ import { basename, dirname, join, relative, resolve } from 'node:path';
20
23
  import { fileURLToPath } from 'node:url';
24
+ import {
25
+ isBaseWorktreeTarget,
26
+ resolveBaseWorktreeCommit,
27
+ } from './local-target.mjs';
28
+ import { createAgentExclusionMatcher } from './agent-exclusions.mjs';
29
+ import {
30
+ agentReviewContext,
31
+ agentReviewFile,
32
+ createAgentReviewFingerprint,
33
+ } from './agent-review.mjs';
21
34
  import { summaryPath } from './summary-path.mjs';
22
35
 
23
36
  const args = process.argv.slice(2);
@@ -34,7 +47,11 @@ const option = (name) => {
34
47
  };
35
48
  const options = (name) =>
36
49
  args.flatMap((argument, index) =>
37
- argument === name && args[index + 1] ? [args[index + 1]] : [],
50
+ argument === name && args[index + 1]
51
+ ? [args[index + 1]]
52
+ : argument.startsWith(`${name}=`)
53
+ ? [argument.slice(name.length + 1)]
54
+ : [],
38
55
  );
39
56
  const has = (name) => args.includes(name);
40
57
 
@@ -45,8 +62,8 @@ Targets:
45
62
  --pr NUMBER|URL Fetch and show a GitHub pull request
46
63
  --branch NAME Fetch and show a remote branch
47
64
  --checkout Show the current checkout against its default branch
48
- --base REF --head REF
49
- Show an exact local Git range
65
+ --base REF [--head REF]
66
+ Compare a base with the working tree, or show an exact range
50
67
  (no target) Show worktree changes against HEAD
51
68
 
52
69
  Options:
@@ -63,6 +80,7 @@ Options:
63
80
  const repo = resolve(option('--repo') || process.cwd());
64
81
  const output = resolve(option('--output') || '.cache/diff-data.json');
65
82
  const excludedOutputs = options('--exclude-output');
83
+ const agentExcludeRules = options('--exclude');
66
84
  const baseOption = option('--base');
67
85
  const headOption = option('--head');
68
86
  const prOption = option('--pr');
@@ -88,6 +106,14 @@ const cacheRoot = cacheOption
88
106
  ? resolve(cacheOption)
89
107
  : resolve(projectRoot, '.cache/git');
90
108
  const remoteMode = Boolean(prOption || branchOption);
109
+ const baseWorktreeOption = isBaseWorktreeTarget({
110
+ base: baseOption,
111
+ branch: branchOption,
112
+ checkout: checkoutOption,
113
+ head: headOption,
114
+ pullRequest: prOption,
115
+ worktree: worktreeOption,
116
+ });
91
117
  const watching = has('--watch');
92
118
  const watchContent = has('--watch-content');
93
119
  const ignoreSummaryWatch = has('--ignore-summary-watch');
@@ -117,13 +143,8 @@ if (
117
143
  ) {
118
144
  fail('--worktree cannot be combined with another target');
119
145
  }
120
- if (
121
- !prOption &&
122
- !branchOption &&
123
- !checkoutOption &&
124
- Boolean(baseOption) !== Boolean(headOption)
125
- ) {
126
- fail('--base and --head must be used together');
146
+ if (!prOption && !branchOption && !checkoutOption && headOption && !baseOption) {
147
+ fail('--head must be used with --base');
127
148
  }
128
149
 
129
150
  const repoPath = (file) => {
@@ -829,6 +850,36 @@ function resolveCheckoutTarget() {
829
850
  };
830
851
  }
831
852
 
853
+ function resolveBaseWorktreeTarget() {
854
+ const currentHead = tryRepo(['rev-parse', '--verify', 'HEAD']);
855
+ const resolvedBase = resolveBaseWorktreeCommit(repo, baseOption);
856
+ const remoteUrl = tryRepo(['remote', 'get-url', 'origin']) || undefined;
857
+ return {
858
+ kind: 'base-worktree',
859
+ runGit: runRepo,
860
+ range: [resolvedBase],
861
+ base: resolvedBase,
862
+ head: currentHead || 'WORKTREE',
863
+ branch: tryRepo(['branch', '--show-current']) || undefined,
864
+ remote: remoteUrl ? { name: 'origin', url: remoteUrl } : undefined,
865
+ sourceRepositoryUrl: undefined,
866
+ baseRepositoryUrl: undefined,
867
+ comparisonCommitsOnRemote: false,
868
+ target: {
869
+ kind: 'base-worktree',
870
+ base: { ref: baseOption, oid: resolvedBase },
871
+ head: { ref: 'WORKTREE', oid: currentHead || null },
872
+ },
873
+ changeDefaults: {
874
+ title: `Changes since ${baseOption}`,
875
+ summary: `Shows commits and working-tree changes since ${baseOption}.`,
876
+ why: 'Reviews the working tree against the chosen base without changing the repo.',
877
+ highlights: [],
878
+ risks: [],
879
+ },
880
+ };
881
+ }
882
+
832
883
  function resolveLocalTarget() {
833
884
  const currentHead = tryRepo(['rev-parse', '--verify', 'HEAD']);
834
885
  const worktree = !baseOption && !headOption;
@@ -891,6 +942,7 @@ function resolveTarget() {
891
942
  if (prOption) return resolvePullRequestTarget();
892
943
  if (branchOption) return resolveBranchTarget();
893
944
  if (checkoutOption) return resolveCheckoutTarget();
945
+ if (baseWorktreeOption) return resolveBaseWorktreeTarget();
894
946
  return resolveLocalTarget();
895
947
  }
896
948
 
@@ -959,6 +1011,60 @@ function untrackedStat(path) {
959
1011
  };
960
1012
  }
961
1013
 
1014
+ function baseWorktreeReplacement(path, target) {
1015
+ const directory = mkdtempSync(join(tmpdir(), 'diffsplain-base-index-'));
1016
+ const env = { ...process.env, GIT_INDEX_FILE: join(directory, 'index') };
1017
+ const diff = (args) => command('git', ['-C', repo, 'diff', ...args], { env });
1018
+
1019
+ try {
1020
+ command('git', ['-C', repo, 'read-tree', target.base], { env });
1021
+ const patch = diff([
1022
+ '--no-ext-diff',
1023
+ '--no-textconv',
1024
+ '--binary',
1025
+ '--find-renames',
1026
+ target.base,
1027
+ '--',
1028
+ path,
1029
+ ]);
1030
+ const stat = parseNumstat(
1031
+ diff([
1032
+ '--no-ext-diff',
1033
+ '--no-textconv',
1034
+ '--numstat',
1035
+ '-z',
1036
+ '--find-renames',
1037
+ target.base,
1038
+ '--',
1039
+ path,
1040
+ ]),
1041
+ ).get(path) || {
1042
+ additions: 0,
1043
+ deletions: 0,
1044
+ isBinary: false,
1045
+ };
1046
+ return { patch, stat };
1047
+ } finally {
1048
+ rmSync(directory, { recursive: true, force: true });
1049
+ }
1050
+ }
1051
+
1052
+ function reviewFileStat(file, numstat) {
1053
+ if (file.replacement) return file.replacement.stat;
1054
+ if (file.untracked) return untrackedStat(file.path);
1055
+ return (
1056
+ numstat.get(file.path) || {
1057
+ additions: 0,
1058
+ deletions: 0,
1059
+ isBinary: false,
1060
+ }
1061
+ );
1062
+ }
1063
+
1064
+ function reviewFilePatch(file, target, patches) {
1065
+ return file.replacement?.patch ?? filePatch(file, target, patches);
1066
+ }
1067
+
962
1068
  function githubFileUrl(repositoryUrl, ref, path) {
963
1069
  if (!repositoryUrl || !ref || ref === 'WORKTREE') return undefined;
964
1070
  const filePath = path.split('/').map(encodeURIComponent).join('/');
@@ -972,6 +1078,11 @@ function githubComparisonUrl(repositoryUrl, base, head) {
972
1078
  return `${repositoryUrl}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`;
973
1079
  }
974
1080
 
1081
+ function fingerprintFile(file) {
1082
+ return agentReviewFile(file);
1083
+ }
1084
+
1085
+ // fallow-ignore-next-line complexity -- Existing aggregation boundary; target handling stays in resolvers.
975
1086
  function build() {
976
1087
  const localWorkspace =
977
1088
  tryRepo(['rev-parse', '--is-inside-work-tree']) === 'true';
@@ -979,6 +1090,9 @@ function build() {
979
1090
  throw new Error(`${repo} is not a Git checkout`);
980
1091
  }
981
1092
  const target = resolveTarget();
1093
+ const agentExcluded = createAgentExclusionMatcher(agentExcludeRules, {
1094
+ ignoreCase: tryRepo(['config', '--bool', 'core.ignoreCase']) === 'true',
1095
+ });
982
1096
  const remoteRepository = githubRepository(target.remote?.url);
983
1097
  const summaryDoc = noSummaries ? {} : readJson(summariesPath, {}) || {};
984
1098
  const allTrackedFiles = parseNameStatus(
@@ -1008,8 +1122,12 @@ function build() {
1008
1122
  );
1009
1123
  const patches = trackedPatches(allTrackedFiles, target);
1010
1124
 
1011
- if (target.kind === 'worktree' || target.kind === 'checkout') {
1012
- const trackedPaths = new Set(nameStatus.map((file) => file.path));
1125
+ if (
1126
+ target.kind === 'worktree' ||
1127
+ target.kind === 'base-worktree' ||
1128
+ target.kind === 'checkout'
1129
+ ) {
1130
+ const trackedByPath = new Map(nameStatus.map((file) => [file.path, file]));
1013
1131
  const untracked = tryRepo([
1014
1132
  'ls-files',
1015
1133
  '--others',
@@ -1019,22 +1137,26 @@ function build() {
1019
1137
  .split('\0')
1020
1138
  .filter((path) => path && !excludedPaths.has(path));
1021
1139
  for (const path of untracked) {
1022
- if (!trackedPaths.has(path)) {
1140
+ const tracked = trackedByPath.get(path);
1141
+ if (target.kind === 'base-worktree' && tracked?.status === 'deleted') {
1142
+ const replacement = baseWorktreeReplacement(path, target);
1143
+ if (replacement.patch) {
1144
+ tracked.status = 'modified';
1145
+ tracked.replacement = replacement;
1146
+ } else {
1147
+ tracked.omit = true;
1148
+ }
1149
+ } else if (!tracked) {
1023
1150
  nameStatus.push({ path, status: 'added', untracked: true });
1024
1151
  }
1025
1152
  }
1026
1153
  nameStatus.sort((left, right) => left.path.localeCompare(right.path));
1027
1154
  }
1028
1155
 
1029
- const filesWithoutSummaries = nameStatus.map((file) => {
1030
- const stat = file.untracked
1031
- ? untrackedStat(file.path)
1032
- : numstat.get(file.path) || {
1033
- additions: 0,
1034
- deletions: 0,
1035
- isBinary: false,
1036
- };
1037
- const patch = filePatch(file, target, patches);
1156
+ const reviewFiles = nameStatus.filter((file) => !file.omit);
1157
+ const filesWithoutSummaries = reviewFiles.map((file) => {
1158
+ const stat = reviewFileStat(file, numstat);
1159
+ const patch = reviewFilePatch(file, target, patches);
1038
1160
  const binary =
1039
1161
  stat.isBinary ||
1040
1162
  patch.includes('Binary files ') ||
@@ -1065,11 +1187,16 @@ function build() {
1065
1187
  totalDiffLines: textPatch ? textPatch.split('\n').length - 1 : 0,
1066
1188
  patch: textPatch,
1067
1189
  snippet: binary ? '' : compactSnippet(textPatch),
1190
+ ...(agentExcluded(file.path) ? { agentExcluded: true } : {}),
1068
1191
  ...(sourceUrl ? { sourceUrl } : {}),
1069
1192
  ...(comparisonUrl ? { comparisonUrl } : {}),
1070
1193
  };
1071
1194
  });
1072
1195
 
1196
+ const agentFiles = filesWithoutSummaries.filter(
1197
+ (file) => !file.agentExcluded,
1198
+ );
1199
+
1073
1200
  const reviewFingerprint = createHash('sha256')
1074
1201
  .update(
1075
1202
  JSON.stringify({
@@ -1081,38 +1208,55 @@ function build() {
1081
1208
  remote: target.remote?.name,
1082
1209
  targetKind: target.kind,
1083
1210
  },
1084
- files: filesWithoutSummaries.map((file) => ({
1085
- path: file.path,
1086
- oldPath: file.oldPath,
1087
- status: file.status,
1088
- additions: file.additions,
1089
- deletions: file.deletions,
1090
- isBinary: file.isBinary,
1091
- patch: file.patch,
1092
- })),
1211
+ files: filesWithoutSummaries.map(fingerprintFile),
1093
1212
  }),
1094
1213
  )
1095
1214
  .digest('hex');
1096
- const generatedFor =
1097
- !noSummaries &&
1098
- typeof summaryDoc.meta?.reviewFingerprint === 'string'
1099
- ? summaryDoc.meta.reviewFingerprint
1215
+ const agentReviewFingerprint = createAgentReviewFingerprint({
1216
+ context: agentReviewContext({
1217
+ name: remoteRepository?.name || basename(repo),
1218
+ selector: remoteRepository?.selector,
1219
+ target: target.target,
1220
+ branch: target.branch,
1221
+ baseBranch: target.baseBranch,
1222
+ }),
1223
+ files: agentFiles,
1224
+ });
1225
+ const hasAgentReviewFingerprint =
1226
+ typeof summaryDoc.meta?.agentReviewFingerprint === 'string';
1227
+ const hasLegacyReviewFingerprint =
1228
+ !hasAgentReviewFingerprint &&
1229
+ typeof summaryDoc.meta?.reviewFingerprint === 'string';
1230
+ const emptyAgentReviewFingerprint =
1231
+ typeof summaryDoc.meta?.emptyAgentReviewFingerprint === 'string'
1232
+ ? summaryDoc.meta.emptyAgentReviewFingerprint
1100
1233
  : undefined;
1234
+ const generatedFor = !noSummaries && hasAgentReviewFingerprint
1235
+ ? agentFiles.length === 0 && emptyAgentReviewFingerprint
1236
+ ? emptyAgentReviewFingerprint
1237
+ : summaryDoc.meta.agentReviewFingerprint
1238
+ : undefined;
1101
1239
  const summariesAreFresh =
1102
- !noSummaries && (!generatedFor || generatedFor === reviewFingerprint);
1240
+ !noSummaries &&
1241
+ !hasLegacyReviewFingerprint &&
1242
+ (!generatedFor || generatedFor === agentReviewFingerprint);
1103
1243
  const sourceSummaries = summariesAreFresh ? summaryDoc : {};
1244
+ const agentPaths = new Set(agentFiles.map((file) => file.path));
1245
+ const reviewPaths = new Set(filesWithoutSummaries.map((file) => file.path));
1104
1246
  const failedFiles = summariesAreFresh
1105
- ? failedFileRecords(summaryDoc.meta?.failedFiles)
1247
+ ? failedFileRecords(summaryDoc.meta?.failedFiles).filter((failure) =>
1248
+ agentPaths.has(failure.path) || !reviewPaths.has(failure.path),
1249
+ )
1106
1250
  : [];
1107
- const summaryErrors = summariesAreFresh
1251
+ const summaryErrors = summariesAreFresh && agentFiles.length
1108
1252
  ? cleanList(summaryDoc.meta?.errors)
1109
1253
  : [];
1110
1254
  const failureByPath = new Map(
1111
1255
  failedFiles.map((failure) => [failure.path, failure.reason]),
1112
1256
  );
1113
1257
  const emptyReviewComplete =
1114
- filesWithoutSummaries.length === 0 &&
1115
- generatedFor === reviewFingerprint &&
1258
+ agentFiles.length === 0 &&
1259
+ generatedFor === agentReviewFingerprint &&
1116
1260
  sourceSummaries.meta?.status === 'complete';
1117
1261
  const summariesAreComplete =
1118
1262
  summariesAreFresh &&
@@ -1120,12 +1264,12 @@ function build() {
1120
1264
  summaryErrors.length === 0 &&
1121
1265
  (emptyReviewComplete ||
1122
1266
  (completeChangeSummary(sourceSummaries.change) &&
1123
- filesWithoutSummaries.every((file) =>
1267
+ agentFiles.every((file) =>
1124
1268
  completeFileSummary(sourceSummaries.files?.[file.path]),
1125
1269
  )));
1126
1270
  const completedFiles = noSummaries
1127
1271
  ? 0
1128
- : filesWithoutSummaries.filter((file) =>
1272
+ : agentFiles.filter((file) =>
1129
1273
  completeFileSummary(sourceSummaries.files?.[file.path]),
1130
1274
  ).length;
1131
1275
  const storedStatus = summaryDoc.meta?.status;
@@ -1140,18 +1284,28 @@ function build() {
1140
1284
  : 'idle';
1141
1285
  const files = filesWithoutSummaries.map((file) => ({
1142
1286
  ...file,
1143
- summary: fileSummary(file.path, sourceSummaries.files?.[file.path]),
1287
+ summary: fileSummary(
1288
+ file.path,
1289
+ file.agentExcluded ? undefined : sourceSummaries.files?.[file.path],
1290
+ ),
1144
1291
  noteReady: Boolean(
1145
- completeFileSummary(sourceSummaries.files?.[file.path]),
1292
+ !file.agentExcluded &&
1293
+ completeFileSummary(sourceSummaries.files?.[file.path]),
1146
1294
  ),
1147
- ...(failureByPath.has(file.path)
1295
+ ...(!file.agentExcluded && failureByPath.has(file.path)
1148
1296
  ? { noteFailure: failureByPath.get(file.path) }
1149
1297
  : {}),
1150
1298
  }));
1151
- const change = changeSummary(sourceSummaries.change, target.changeDefaults);
1299
+ const change = changeSummary(
1300
+ agentFiles.length ? sourceSummaries.change : undefined,
1301
+ target.changeDefaults,
1302
+ );
1152
1303
  const content = {
1153
1304
  repo: {
1154
1305
  name: remoteRepository?.name || basename(repo),
1306
+ ...(remoteRepository?.selector
1307
+ ? { repository: remoteRepository.selector }
1308
+ : {}),
1155
1309
  root: localWorkspace ? repo : target.remote?.url || repo,
1156
1310
  base: target.base,
1157
1311
  head: target.head,
@@ -1166,12 +1320,13 @@ function build() {
1166
1320
  files,
1167
1321
  notes: {
1168
1322
  reviewFingerprint,
1323
+ agentReviewFingerprint,
1169
1324
  ...(generatedFor ? { generatedFor } : {}),
1170
1325
  fresh: summariesAreFresh,
1171
1326
  complete: summariesAreComplete,
1172
1327
  status: noteStatus,
1173
1328
  completedFiles,
1174
- totalFiles: filesWithoutSummaries.length,
1329
+ totalFiles: agentFiles.length,
1175
1330
  ...(failedFiles.length ? { failedFiles } : {}),
1176
1331
  ...(summaryErrors.length ? { errors: summaryErrors } : {}),
1177
1332
  ...(typeof summaryDoc.meta?.model === 'string'
@@ -1183,6 +1338,9 @@ function build() {
1183
1338
  ...(typeof summaryDoc.meta?.reasoning === 'string'
1184
1339
  ? { reasoning: summaryDoc.meta.reasoning }
1185
1340
  : {}),
1341
+ ...(typeof summaryDoc.meta?.accessMode === 'string'
1342
+ ? { accessMode: summaryDoc.meta.accessMode }
1343
+ : {}),
1186
1344
  },
1187
1345
  };
1188
1346
  const version = createHash('sha256')
@@ -1268,8 +1426,11 @@ function fingerprint() {
1268
1426
  ].join('|');
1269
1427
  }
1270
1428
  const content = createHash('sha256');
1429
+ const base = baseWorktreeOption
1430
+ ? resolveBaseWorktreeCommit(repo, baseOption)
1431
+ : 'HEAD';
1271
1432
  content.update(
1272
- tryRepo(['diff', '--no-ext-diff', '--no-textconv', '--binary', 'HEAD', '--']),
1433
+ tryRepo(['diff', '--no-ext-diff', '--no-textconv', '--binary', base, '--']),
1273
1434
  );
1274
1435
  const untracked = tryRepo([
1275
1436
  'ls-files',
@@ -1284,6 +1445,7 @@ function fingerprint() {
1284
1445
  fingerprintUntrackedPath(content, path);
1285
1446
  }
1286
1447
  return [
1448
+ baseWorktreeOption ? base : undefined,
1287
1449
  tryRepo(['rev-parse', 'HEAD']),
1288
1450
  tryRepo(['status', '--porcelain=v1', '--untracked-files=all']),
1289
1451
  content.digest('hex'),
@@ -29,6 +29,7 @@ export const cliOptions = defineCliOptions({
29
29
  '--cache-dir': { kind: 'value', path: true },
30
30
  '--codex-bin': { kind: 'value', path: true },
31
31
  '--support-record-file': { kind: 'value', path: true },
32
+ '--exclude': { kind: 'repeatable-value' },
32
33
  '--model': { kind: 'value' },
33
34
  '--reasoning': { kind: 'value' },
34
35
  '--batch-size': { kind: 'value', default: 12, min: 1, max: 50 },
@@ -39,6 +40,7 @@ export const cliOptions = defineCliOptions({
39
40
  '--version': { kind: 'flag' },
40
41
  '--agent': { kind: 'agent' },
41
42
  '--no-agent': { kind: 'no-agent' },
43
+ '--no-checkout-access': { kind: 'flag' },
42
44
  '--force': { kind: 'flag' },
43
45
  '--worktree': { kind: 'flag' },
44
46
  '--no-browser': { kind: 'flag' },
@@ -55,6 +57,11 @@ const flagOptions = new Set(
55
57
  .filter(([, record]) => record.kind === 'flag')
56
58
  .map(([name]) => name),
57
59
  );
60
+ const repeatableValueOptions = new Set(
61
+ Object.entries(cliOptions)
62
+ .filter(([, record]) => record.kind === 'repeatable-value')
63
+ .map(([name]) => name),
64
+ );
58
65
  const pathOptions = new Set(
59
66
  Object.entries(cliOptions)
60
67
  .filter(([, record]) => record.path)
@@ -80,14 +87,17 @@ Targets:
80
87
  --branch NAME Show a remote branch against its default branch
81
88
  --pr NUMBER|URL Show a GitHub pull request
82
89
  --worktree Show only worktree changes against HEAD
83
- --base REF --head REF
84
- Show an exact local Git range
90
+ --base REF [--head REF]
91
+ Compare a base with the working tree, or show an exact range
85
92
 
86
93
  Options:
87
94
  --repo PATH|URL|OWNER/NAME
88
95
  Repo to review (default: current repo)
89
96
  --agent NAME Use codex, claude, copilot, cursor, or opencode
90
97
  --no-agent Do not write agent notes
98
+ --no-checkout-access
99
+ Limit agent notes to the supplied snapshot
100
+ --exclude PATTERN Omit matching files from automatic agent context
91
101
  --model NAME Model for agent notes
92
102
  --reasoning LEVEL Agent reasoning effort when supported
93
103
  --batch-size COUNT Maximum files per agent pass (default: ${batchSizeOption.default})
@@ -107,8 +117,9 @@ Options:
107
117
  -h, --help Show this help
108
118
  -v, --version Show the installed version
109
119
 
110
- Automatic agent selection:
111
- codex, claude, copilot, cursor, opencode
120
+ Agent choice:
121
+ Without --agent, an interactive terminal lists usable agents. In scripts,
122
+ pass --agent NAME or --no-agent.
112
123
 
113
124
  Cursor:
114
125
  Requires Cursor Agent 2026.08.11 or newer. Uses the signed-in CLI in the
@@ -199,6 +210,7 @@ export function parseCliArgs(
199
210
 
200
211
  const options = new Map();
201
212
  const positionals = [];
213
+ const excludePatterns = [];
202
214
  let agent;
203
215
  let agentSet = false;
204
216
  let noAgent = false;
@@ -246,10 +258,13 @@ export function parseCliArgs(
246
258
  continue;
247
259
  }
248
260
 
249
- if (!valueOptions.has(parsed.name)) {
261
+ const repeatable = repeatableValueOptions.has(parsed.name);
262
+ if (!repeatable && !valueOptions.has(parsed.name)) {
250
263
  fail(`Unknown option: ${parsed.name}`);
251
264
  }
252
- if (options.has(parsed.name)) fail(`${parsed.name} was passed more than once`);
265
+ if (!repeatable && options.has(parsed.name)) {
266
+ fail(`${parsed.name} was passed more than once`);
267
+ }
253
268
 
254
269
  let value = parsed.value;
255
270
  if (value === undefined) {
@@ -258,7 +273,8 @@ export function parseCliArgs(
258
273
  index += 1;
259
274
  }
260
275
  if (!value) fail(`${parsed.name} needs a value`);
261
- options.set(parsed.name, value);
276
+ if (repeatable) excludePatterns.push(value);
277
+ else options.set(parsed.name, value);
262
278
  }
263
279
 
264
280
  if (options.has('--help')) return { help: true };
@@ -303,8 +319,8 @@ export function parseCliArgs(
303
319
  if (worktree && (branch || pullRequest || base || head)) {
304
320
  fail('--worktree cannot be combined with another target');
305
321
  }
306
- if (!branch && !pullRequest && !worktree && Boolean(base) !== Boolean(head)) {
307
- fail('--base and --head must be used together');
322
+ if (!branch && !pullRequest && !worktree && head && !base) {
323
+ fail('--head must be used with --base');
308
324
  }
309
325
  if (pullRequest && !validPullRequest(pullRequest)) {
310
326
  fail('--pr must be a positive number or a pull request URL');
@@ -359,6 +375,9 @@ export function parseCliArgs(
359
375
  }
360
376
 
361
377
  const feedArgs = [...commonArgs];
378
+ for (const pattern of excludePatterns) {
379
+ feedArgs.push(`--exclude=${pattern}`);
380
+ }
362
381
  const supportRecordFile = options.get('--support-record-file');
363
382
  if (supportRecordFile) {
364
383
  feedArgs.push(
@@ -367,6 +386,9 @@ export function parseCliArgs(
367
386
  );
368
387
  }
369
388
  const agentArgs = [...commonArgs];
389
+ for (const pattern of excludePatterns) {
390
+ agentArgs.push(`--exclude=${pattern}`);
391
+ }
370
392
  if (options.has('--force')) agentArgs.push('--force');
371
393
  for (const name of [
372
394
  '--codex-bin',
@@ -382,6 +404,9 @@ export function parseCliArgs(
382
404
  }
383
405
  }
384
406
  if (!noAgent) {
407
+ if (options.has('--no-checkout-access')) {
408
+ agentArgs.push('--no-checkout-access');
409
+ }
385
410
  for (const name of ['--batch-size', '--jobs']) {
386
411
  agentArgs.push(
387
412
  name,
@@ -458,6 +483,8 @@ export function parseCliArgs(
458
483
  version: false,
459
484
  agentEnabled: !noAgent,
460
485
  agent,
486
+ noCheckoutAccess: options.has('--no-checkout-access'),
487
+ excludePatterns,
461
488
  model: options.get('--model'),
462
489
  reasoning,
463
490
  codexBin: options.get('--codex-bin'),