diffsplain 0.8.2 → 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.
- package/README.md +10 -5
- package/dist/assets/{angular-html-CphzFC_v.js → angular-html-m_m91ZYB.js} +1 -1
- package/dist/assets/{angular-ts-Am0-mbFw.js → angular-ts-CO5Z--Rb.js} +1 -1
- package/dist/assets/{apl-Cs2rPKDN.js → apl-ogYYgLUu.js} +1 -1
- package/dist/assets/{astro-CxKUUn9h.js → astro-DAfY3cNR.js} +1 -1
- package/dist/assets/{blade-DqeCa6v5.js → blade-E6RzsLUV.js} +1 -1
- package/dist/assets/{c-DdI6lknk.js → c-kKdxhtHF.js} +1 -1
- package/dist/assets/{cobol-COuAGE2X.js → cobol-3oDDyFNf.js} +1 -1
- package/dist/assets/{coffee-BFlYT3Lp.js → coffee-dsiILuWe.js} +1 -1
- package/dist/assets/{cpp-nywLU0Np.js → cpp-DYyNFq__.js} +1 -1
- package/dist/assets/{crystal-BWp4dz1s.js → crystal-B7dVp9TF.js} +1 -1
- package/dist/assets/{css-CRb9tUpU.js → css-DlxJ0aex.js} +1 -1
- package/dist/assets/{edge-rUYSbJy4.js → edge-BjEVphkC.js} +1 -1
- package/dist/assets/{elixir-DOq9ybIH.js → elixir-0xTOtMcM.js} +1 -1
- package/dist/assets/{elm-C8e38E5-.js → elm-iQk_Wrxq.js} +1 -1
- package/dist/assets/{erb-BR9Q37Et.js → erb-BO74ztiE.js} +1 -1
- package/dist/assets/{git-rebase-Coo2LUtB.js → git-rebase-BN667QRY.js} +1 -1
- package/dist/assets/{glimmer-js-C7hX9jg6.js → glimmer-js-DTGUIHeS.js} +1 -1
- package/dist/assets/{glimmer-ts-CZLT0F0k.js → glimmer-ts-BU3oQn2d.js} +1 -1
- package/dist/assets/{glsl-BDZRv6e-.js → glsl-KLiqLJi2.js} +1 -1
- package/dist/assets/{graphql-DyzFc3y3.js → graphql-D_idcsoJ.js} +1 -1
- package/dist/assets/{hack-CWnO7WsF.js → hack-BXlsgPIr.js} +1 -1
- package/dist/assets/{haml-CvDj-ghO.js → haml-fP0aUg4-.js} +1 -1
- package/dist/assets/{handlebars-BnU_YcVA.js → handlebars-C1ZEaot-.js} +1 -1
- package/dist/assets/{html-CjB2TKKa.js → html-D04qIuup.js} +1 -1
- package/dist/assets/{html-derivative-CYQqEXS6.js → html-derivative-C6ai-9QL.js} +1 -1
- package/dist/assets/{http-NNQbnqIx.js → http-C7bz3ZvQ.js} +1 -1
- package/dist/assets/{hurl-DHaAA1BS.js → hurl-jL6B6Ahr.js} +1 -1
- package/dist/assets/index-BPaJGi-m.js +1574 -0
- package/dist/assets/index-VAyBpb3u.css +2 -0
- package/dist/assets/{java-D1J5V2CD.js → java-BVzvLCON.js} +1 -1
- package/dist/assets/{javascript-C3pX-HsE.js → javascript-UDbZHxCI.js} +1 -1
- package/dist/assets/{jinja-BY9CIY8g.js → jinja-DB-QKo2A.js} +1 -1
- package/dist/assets/{jison-Cr0H89gS.js → jison-B1ZbR2FF.js} +1 -1
- package/dist/assets/{json-CA3kKNfq.js → json-DV4WwJzT.js} +1 -1
- package/dist/assets/{jsx-BuMkR-Lj.js → jsx-DS6_VTvW.js} +1 -1
- package/dist/assets/{julia-C2sLwg3f.js → julia-C4tevET0.js} +1 -1
- package/dist/assets/{just-pXpkLBXv.js → just-CWMjefzB.js} +1 -1
- package/dist/assets/{latex-7v3EPOdF.js → latex-tyEFf_Af.js} +1 -1
- package/dist/assets/{liquid-CJZq0Z0t.js → liquid-C3d1X1rL.js} +1 -1
- package/dist/assets/{lua-BimBzvTH.js → lua-sXvrzLQY.js} +1 -1
- package/dist/assets/{marko-CCZHVnQN.js → marko-CQweucif.js} +1 -1
- package/dist/assets/{mdc-SLECNw2A.js → mdc-TrxLWF7s.js} +1 -1
- package/dist/assets/{nginx-DUwtUge2.js → nginx-ChE2nSNJ.js} +1 -1
- package/dist/assets/{nim-HD8o_Omp.js → nim-BY1CTRfx.js} +1 -1
- package/dist/assets/{perl-BGMeIkNk.js → perl-Cvj2PQQH.js} +1 -1
- package/dist/assets/{php-BL5xuY-v.js → php-CGmHp8HW.js} +1 -1
- package/dist/assets/{pug-C8G1U_u8.js → pug-DM4S0bRr.js} +1 -1
- package/dist/assets/{qml-BGVSyw1x.js → qml-DI89Al9K.js} +1 -1
- package/dist/assets/{r-BDnRcclC.js → r-Bs5_4yYh.js} +1 -1
- package/dist/assets/{razor-I8tbEIVJ.js → razor-DimzayhZ.js} +1 -1
- package/dist/assets/{regexp-D0yakB9Q.js → regexp-BLACz0cx.js} +1 -1
- package/dist/assets/{rst-DbTCn07T.js → rst-DcWzxQh2.js} +1 -1
- package/dist/assets/{ruby-BwgPFIcO.js → ruby-Dlat5E8d.js} +1 -1
- package/dist/assets/{sas-dN4ePfXU.js → sas-CCDmMaBY.js} +1 -1
- package/dist/assets/{scss-T7M1A4iM.js → scss-BPiU2nu6.js} +1 -1
- package/dist/assets/{shellscript-HFjBHS7_.js → shellscript-CKinVNrN.js} +1 -1
- package/dist/assets/{shellsession-DCAGskrx.js → shellsession-Cea8Pu5d.js} +1 -1
- package/dist/assets/{soy-BgT96TF9.js → soy--BRAPY4-.js} +1 -1
- package/dist/assets/{sql-DCIQeRzx.js → sql-DjKiCyWK.js} +1 -1
- package/dist/assets/{stata-Dn0tad0_.js → stata-CDvo3J6y.js} +1 -1
- package/dist/assets/{surrealql-CCgFEQfW.js → surrealql-C7b7E5sh.js} +1 -1
- package/dist/assets/{svelte-ITXimG8u.js → svelte-DO0tKtiK.js} +1 -1
- package/dist/assets/{templ-DJVZZqPw.js → templ-DhhmR_rv.js} +1 -1
- package/dist/assets/{tex-Bn4N-OtH.js → tex-Cn_V-Gg4.js} +1 -1
- package/dist/assets/{ts-tags-CxQ4PoC4.js → ts-tags-B6c-FTmA.js} +1 -1
- package/dist/assets/{tsx-CxwgcjkC.js → tsx-Cvtt2sJ-.js} +1 -1
- package/dist/assets/{twig-CtbyTyMH.js → twig-B7ZbCUmB.js} +1 -1
- package/dist/assets/{typescript-d6ru12L8.js → typescript-i3C6um2B.js} +1 -1
- package/dist/assets/{vue-RvqAsYDm.js → vue-DyPX0Vum.js} +1 -1
- package/dist/assets/{vue-html-BkHDVENe.js → vue-html-D362KpK3.js} +1 -1
- package/dist/assets/{vue-vine-DvS3Mlkn.js → vue-vine-D34cBbwO.js} +1 -1
- package/dist/assets/{xml-lzTq5A-V.js → xml-DJEk7u4n.js} +1 -1
- package/dist/assets/{xsl-BL0KXNWI.js → xsl-CoTtf90f.js} +1 -1
- package/dist/assets/{yaml-CF4XlBJS.js → yaml-Cv5epQ4-.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +11 -3
- package/scripts/agent-exclusions.mjs +10 -0
- package/scripts/agent-review.mjs +94 -0
- package/scripts/build-diff-data.mjs +235 -50
- package/scripts/cli-args.mjs +36 -9
- package/scripts/coding-agents.mjs +206 -79
- package/scripts/generate-summaries.mjs +864 -425
- package/scripts/local-target.mjs +106 -0
- package/scripts/present.mjs +205 -72
- package/scripts/review-chat-context.mjs +510 -0
- package/scripts/review-chat-controller.mjs +568 -0
- package/scripts/review-chat-provider.mjs +352 -0
- package/scripts/review-chat.mjs +8 -0
- package/scripts/serve-built.mjs +206 -24
- package/scripts/summary-path.mjs +32 -11
- package/dist/assets/index-C2p_1iAk.css +0 -2
- package/dist/assets/index-f9ekN94b.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 {
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|
|
@@ -609,8 +630,31 @@ function pullRequestInfo(pr, remote) {
|
|
|
609
630
|
return { value, repository };
|
|
610
631
|
} catch (error) {
|
|
611
632
|
const detail = error?.stderr?.toString().trim() || error?.message;
|
|
633
|
+
if (
|
|
634
|
+
repository &&
|
|
635
|
+
/Could not resolve to a PullRequest with the number of/i.test(detail)
|
|
636
|
+
) {
|
|
637
|
+
const prNumber = String(pr).replace(
|
|
638
|
+
/^.*\/pull\/(\d+)(?:\/.*)?$/,
|
|
639
|
+
"$1",
|
|
640
|
+
);
|
|
641
|
+
const repositoryShape = repository.selector.replace(
|
|
642
|
+
repository.name,
|
|
643
|
+
"owner/repo",
|
|
644
|
+
);
|
|
645
|
+
const pullRequestUrl = `${repository.webUrl.replace(repository.name, "owner/repo")}/pull/${prNumber}`;
|
|
646
|
+
throw new Error(
|
|
647
|
+
`Pull request ${prNumber} was not found in ${repository.selector}. Pass ${repositoryShape} before --pr ${prNumber}, or pass ${pullRequestUrl}.`,
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
const authHint =
|
|
651
|
+
/(?:HTTP 401|Bad credentials|authentication failed|not logged (?:in|into)|gh auth login)/i.test(
|
|
652
|
+
detail,
|
|
653
|
+
)
|
|
654
|
+
? ' Check gh auth status.'
|
|
655
|
+
: '';
|
|
612
656
|
throw new Error(
|
|
613
|
-
`Could not read pull request ${pr} with gh${detail ? `: ${detail}` : ''}
|
|
657
|
+
`Could not read pull request ${pr} with gh${detail ? `: ${detail}` : ''}.${authHint}`,
|
|
614
658
|
);
|
|
615
659
|
}
|
|
616
660
|
}
|
|
@@ -806,6 +850,36 @@ function resolveCheckoutTarget() {
|
|
|
806
850
|
};
|
|
807
851
|
}
|
|
808
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
|
+
|
|
809
883
|
function resolveLocalTarget() {
|
|
810
884
|
const currentHead = tryRepo(['rev-parse', '--verify', 'HEAD']);
|
|
811
885
|
const worktree = !baseOption && !headOption;
|
|
@@ -868,6 +942,7 @@ function resolveTarget() {
|
|
|
868
942
|
if (prOption) return resolvePullRequestTarget();
|
|
869
943
|
if (branchOption) return resolveBranchTarget();
|
|
870
944
|
if (checkoutOption) return resolveCheckoutTarget();
|
|
945
|
+
if (baseWorktreeOption) return resolveBaseWorktreeTarget();
|
|
871
946
|
return resolveLocalTarget();
|
|
872
947
|
}
|
|
873
948
|
|
|
@@ -936,6 +1011,60 @@ function untrackedStat(path) {
|
|
|
936
1011
|
};
|
|
937
1012
|
}
|
|
938
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
|
+
|
|
939
1068
|
function githubFileUrl(repositoryUrl, ref, path) {
|
|
940
1069
|
if (!repositoryUrl || !ref || ref === 'WORKTREE') return undefined;
|
|
941
1070
|
const filePath = path.split('/').map(encodeURIComponent).join('/');
|
|
@@ -949,6 +1078,11 @@ function githubComparisonUrl(repositoryUrl, base, head) {
|
|
|
949
1078
|
return `${repositoryUrl}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`;
|
|
950
1079
|
}
|
|
951
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.
|
|
952
1086
|
function build() {
|
|
953
1087
|
const localWorkspace =
|
|
954
1088
|
tryRepo(['rev-parse', '--is-inside-work-tree']) === 'true';
|
|
@@ -956,6 +1090,9 @@ function build() {
|
|
|
956
1090
|
throw new Error(`${repo} is not a Git checkout`);
|
|
957
1091
|
}
|
|
958
1092
|
const target = resolveTarget();
|
|
1093
|
+
const agentExcluded = createAgentExclusionMatcher(agentExcludeRules, {
|
|
1094
|
+
ignoreCase: tryRepo(['config', '--bool', 'core.ignoreCase']) === 'true',
|
|
1095
|
+
});
|
|
959
1096
|
const remoteRepository = githubRepository(target.remote?.url);
|
|
960
1097
|
const summaryDoc = noSummaries ? {} : readJson(summariesPath, {}) || {};
|
|
961
1098
|
const allTrackedFiles = parseNameStatus(
|
|
@@ -985,8 +1122,12 @@ function build() {
|
|
|
985
1122
|
);
|
|
986
1123
|
const patches = trackedPatches(allTrackedFiles, target);
|
|
987
1124
|
|
|
988
|
-
if (
|
|
989
|
-
|
|
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]));
|
|
990
1131
|
const untracked = tryRepo([
|
|
991
1132
|
'ls-files',
|
|
992
1133
|
'--others',
|
|
@@ -996,22 +1137,26 @@ function build() {
|
|
|
996
1137
|
.split('\0')
|
|
997
1138
|
.filter((path) => path && !excludedPaths.has(path));
|
|
998
1139
|
for (const path of untracked) {
|
|
999
|
-
|
|
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) {
|
|
1000
1150
|
nameStatus.push({ path, status: 'added', untracked: true });
|
|
1001
1151
|
}
|
|
1002
1152
|
}
|
|
1003
1153
|
nameStatus.sort((left, right) => left.path.localeCompare(right.path));
|
|
1004
1154
|
}
|
|
1005
1155
|
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
additions: 0,
|
|
1011
|
-
deletions: 0,
|
|
1012
|
-
isBinary: false,
|
|
1013
|
-
};
|
|
1014
|
-
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);
|
|
1015
1160
|
const binary =
|
|
1016
1161
|
stat.isBinary ||
|
|
1017
1162
|
patch.includes('Binary files ') ||
|
|
@@ -1042,11 +1187,16 @@ function build() {
|
|
|
1042
1187
|
totalDiffLines: textPatch ? textPatch.split('\n').length - 1 : 0,
|
|
1043
1188
|
patch: textPatch,
|
|
1044
1189
|
snippet: binary ? '' : compactSnippet(textPatch),
|
|
1190
|
+
...(agentExcluded(file.path) ? { agentExcluded: true } : {}),
|
|
1045
1191
|
...(sourceUrl ? { sourceUrl } : {}),
|
|
1046
1192
|
...(comparisonUrl ? { comparisonUrl } : {}),
|
|
1047
1193
|
};
|
|
1048
1194
|
});
|
|
1049
1195
|
|
|
1196
|
+
const agentFiles = filesWithoutSummaries.filter(
|
|
1197
|
+
(file) => !file.agentExcluded,
|
|
1198
|
+
);
|
|
1199
|
+
|
|
1050
1200
|
const reviewFingerprint = createHash('sha256')
|
|
1051
1201
|
.update(
|
|
1052
1202
|
JSON.stringify({
|
|
@@ -1058,38 +1208,55 @@ function build() {
|
|
|
1058
1208
|
remote: target.remote?.name,
|
|
1059
1209
|
targetKind: target.kind,
|
|
1060
1210
|
},
|
|
1061
|
-
files: filesWithoutSummaries.map(
|
|
1062
|
-
path: file.path,
|
|
1063
|
-
oldPath: file.oldPath,
|
|
1064
|
-
status: file.status,
|
|
1065
|
-
additions: file.additions,
|
|
1066
|
-
deletions: file.deletions,
|
|
1067
|
-
isBinary: file.isBinary,
|
|
1068
|
-
patch: file.patch,
|
|
1069
|
-
})),
|
|
1211
|
+
files: filesWithoutSummaries.map(fingerprintFile),
|
|
1070
1212
|
}),
|
|
1071
1213
|
)
|
|
1072
1214
|
.digest('hex');
|
|
1073
|
-
const
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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
|
|
1077
1233
|
: undefined;
|
|
1234
|
+
const generatedFor = !noSummaries && hasAgentReviewFingerprint
|
|
1235
|
+
? agentFiles.length === 0 && emptyAgentReviewFingerprint
|
|
1236
|
+
? emptyAgentReviewFingerprint
|
|
1237
|
+
: summaryDoc.meta.agentReviewFingerprint
|
|
1238
|
+
: undefined;
|
|
1078
1239
|
const summariesAreFresh =
|
|
1079
|
-
!noSummaries &&
|
|
1240
|
+
!noSummaries &&
|
|
1241
|
+
!hasLegacyReviewFingerprint &&
|
|
1242
|
+
(!generatedFor || generatedFor === agentReviewFingerprint);
|
|
1080
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));
|
|
1081
1246
|
const failedFiles = summariesAreFresh
|
|
1082
|
-
? failedFileRecords(summaryDoc.meta?.failedFiles)
|
|
1247
|
+
? failedFileRecords(summaryDoc.meta?.failedFiles).filter((failure) =>
|
|
1248
|
+
agentPaths.has(failure.path) || !reviewPaths.has(failure.path),
|
|
1249
|
+
)
|
|
1083
1250
|
: [];
|
|
1084
|
-
const summaryErrors = summariesAreFresh
|
|
1251
|
+
const summaryErrors = summariesAreFresh && agentFiles.length
|
|
1085
1252
|
? cleanList(summaryDoc.meta?.errors)
|
|
1086
1253
|
: [];
|
|
1087
1254
|
const failureByPath = new Map(
|
|
1088
1255
|
failedFiles.map((failure) => [failure.path, failure.reason]),
|
|
1089
1256
|
);
|
|
1090
1257
|
const emptyReviewComplete =
|
|
1091
|
-
|
|
1092
|
-
generatedFor ===
|
|
1258
|
+
agentFiles.length === 0 &&
|
|
1259
|
+
generatedFor === agentReviewFingerprint &&
|
|
1093
1260
|
sourceSummaries.meta?.status === 'complete';
|
|
1094
1261
|
const summariesAreComplete =
|
|
1095
1262
|
summariesAreFresh &&
|
|
@@ -1097,12 +1264,12 @@ function build() {
|
|
|
1097
1264
|
summaryErrors.length === 0 &&
|
|
1098
1265
|
(emptyReviewComplete ||
|
|
1099
1266
|
(completeChangeSummary(sourceSummaries.change) &&
|
|
1100
|
-
|
|
1267
|
+
agentFiles.every((file) =>
|
|
1101
1268
|
completeFileSummary(sourceSummaries.files?.[file.path]),
|
|
1102
1269
|
)));
|
|
1103
1270
|
const completedFiles = noSummaries
|
|
1104
1271
|
? 0
|
|
1105
|
-
:
|
|
1272
|
+
: agentFiles.filter((file) =>
|
|
1106
1273
|
completeFileSummary(sourceSummaries.files?.[file.path]),
|
|
1107
1274
|
).length;
|
|
1108
1275
|
const storedStatus = summaryDoc.meta?.status;
|
|
@@ -1117,18 +1284,28 @@ function build() {
|
|
|
1117
1284
|
: 'idle';
|
|
1118
1285
|
const files = filesWithoutSummaries.map((file) => ({
|
|
1119
1286
|
...file,
|
|
1120
|
-
summary: fileSummary(
|
|
1287
|
+
summary: fileSummary(
|
|
1288
|
+
file.path,
|
|
1289
|
+
file.agentExcluded ? undefined : sourceSummaries.files?.[file.path],
|
|
1290
|
+
),
|
|
1121
1291
|
noteReady: Boolean(
|
|
1122
|
-
|
|
1292
|
+
!file.agentExcluded &&
|
|
1293
|
+
completeFileSummary(sourceSummaries.files?.[file.path]),
|
|
1123
1294
|
),
|
|
1124
|
-
...(failureByPath.has(file.path)
|
|
1295
|
+
...(!file.agentExcluded && failureByPath.has(file.path)
|
|
1125
1296
|
? { noteFailure: failureByPath.get(file.path) }
|
|
1126
1297
|
: {}),
|
|
1127
1298
|
}));
|
|
1128
|
-
const change = changeSummary(
|
|
1299
|
+
const change = changeSummary(
|
|
1300
|
+
agentFiles.length ? sourceSummaries.change : undefined,
|
|
1301
|
+
target.changeDefaults,
|
|
1302
|
+
);
|
|
1129
1303
|
const content = {
|
|
1130
1304
|
repo: {
|
|
1131
1305
|
name: remoteRepository?.name || basename(repo),
|
|
1306
|
+
...(remoteRepository?.selector
|
|
1307
|
+
? { repository: remoteRepository.selector }
|
|
1308
|
+
: {}),
|
|
1132
1309
|
root: localWorkspace ? repo : target.remote?.url || repo,
|
|
1133
1310
|
base: target.base,
|
|
1134
1311
|
head: target.head,
|
|
@@ -1143,12 +1320,13 @@ function build() {
|
|
|
1143
1320
|
files,
|
|
1144
1321
|
notes: {
|
|
1145
1322
|
reviewFingerprint,
|
|
1323
|
+
agentReviewFingerprint,
|
|
1146
1324
|
...(generatedFor ? { generatedFor } : {}),
|
|
1147
1325
|
fresh: summariesAreFresh,
|
|
1148
1326
|
complete: summariesAreComplete,
|
|
1149
1327
|
status: noteStatus,
|
|
1150
1328
|
completedFiles,
|
|
1151
|
-
totalFiles:
|
|
1329
|
+
totalFiles: agentFiles.length,
|
|
1152
1330
|
...(failedFiles.length ? { failedFiles } : {}),
|
|
1153
1331
|
...(summaryErrors.length ? { errors: summaryErrors } : {}),
|
|
1154
1332
|
...(typeof summaryDoc.meta?.model === 'string'
|
|
@@ -1160,6 +1338,9 @@ function build() {
|
|
|
1160
1338
|
...(typeof summaryDoc.meta?.reasoning === 'string'
|
|
1161
1339
|
? { reasoning: summaryDoc.meta.reasoning }
|
|
1162
1340
|
: {}),
|
|
1341
|
+
...(typeof summaryDoc.meta?.accessMode === 'string'
|
|
1342
|
+
? { accessMode: summaryDoc.meta.accessMode }
|
|
1343
|
+
: {}),
|
|
1163
1344
|
},
|
|
1164
1345
|
};
|
|
1165
1346
|
const version = createHash('sha256')
|
|
@@ -1245,8 +1426,11 @@ function fingerprint() {
|
|
|
1245
1426
|
].join('|');
|
|
1246
1427
|
}
|
|
1247
1428
|
const content = createHash('sha256');
|
|
1429
|
+
const base = baseWorktreeOption
|
|
1430
|
+
? resolveBaseWorktreeCommit(repo, baseOption)
|
|
1431
|
+
: 'HEAD';
|
|
1248
1432
|
content.update(
|
|
1249
|
-
tryRepo(['diff', '--no-ext-diff', '--no-textconv', '--binary',
|
|
1433
|
+
tryRepo(['diff', '--no-ext-diff', '--no-textconv', '--binary', base, '--']),
|
|
1250
1434
|
);
|
|
1251
1435
|
const untracked = tryRepo([
|
|
1252
1436
|
'ls-files',
|
|
@@ -1261,6 +1445,7 @@ function fingerprint() {
|
|
|
1261
1445
|
fingerprintUntrackedPath(content, path);
|
|
1262
1446
|
}
|
|
1263
1447
|
return [
|
|
1448
|
+
baseWorktreeOption ? base : undefined,
|
|
1264
1449
|
tryRepo(['rev-parse', 'HEAD']),
|
|
1265
1450
|
tryRepo(['status', '--porcelain=v1', '--untracked-files=all']),
|
|
1266
1451
|
content.digest('hex'),
|
package/scripts/cli-args.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
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 &&
|
|
307
|
-
fail('--
|
|
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'),
|