@diegopetrucci/pi-extensions 0.1.74 → 0.1.76
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/LICENSE +2 -0
- package/README.md +9 -4
- package/extensions/agent-workflow-audit/index.ts +1398 -1009
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/README.md +3 -1
- package/extensions/annotate-git-diff/clipboard.ts +109 -100
- package/extensions/annotate-git-diff/git.ts +853 -767
- package/extensions/annotate-git-diff/glimpseui.d.ts +87 -78
- package/extensions/annotate-git-diff/index.ts +712 -671
- package/extensions/annotate-git-diff/package.json +1 -1
- package/extensions/annotate-git-diff/prompt.ts +45 -45
- package/extensions/annotate-git-diff/quiet-glimpse.ts +161 -155
- package/extensions/annotate-git-diff/review-server.ts +337 -311
- package/extensions/annotate-git-diff/types.ts +132 -132
- package/extensions/annotate-git-diff/ui.ts +27 -27
- package/extensions/annotate-git-diff/watch.ts +74 -74
- package/extensions/annotate-git-diff/web/app.js +1947 -1884
- package/extensions/annotate-git-diff/web/index.html +5 -6
- package/extensions/annotate-last-message/README.md +1 -1
- package/extensions/annotate-last-message/glimpseui.d.ts +87 -78
- package/extensions/annotate-last-message/index.ts +210 -206
- package/extensions/annotate-last-message/package.json +1 -1
- package/extensions/annotate-last-message/prompt.ts +87 -66
- package/extensions/annotate-last-message/quiet-glimpse.ts +161 -155
- package/extensions/annotate-last-message/session.ts +94 -89
- package/extensions/annotate-last-message/theme.ts +199 -193
- package/extensions/annotate-last-message/types.ts +23 -23
- package/extensions/annotate-last-message/ui.ts +27 -25
- package/extensions/annotate-last-message/web/app.js +4 -7
- package/extensions/annotate-last-message/web/md-renderer.js +1 -1
- package/extensions/brrr/README.md +1 -1
- package/extensions/brrr/index.ts +213 -191
- package/extensions/brrr/package.json +1 -1
- package/extensions/claude-fast/README.md +5 -79
- package/extensions/claude-fast/index.ts +31 -289
- package/extensions/claude-fast/package.json +2 -3
- package/extensions/code-reviewer/README.md +1 -1
- package/extensions/code-reviewer/index.ts +1598 -1350
- package/extensions/code-reviewer/package.json +1 -1
- package/extensions/confirm-destructive/README.md +1 -1
- package/extensions/confirm-destructive/index.ts +55 -51
- package/extensions/confirm-destructive/package.json +1 -1
- package/extensions/context-cap/index.ts +312 -304
- package/extensions/context-cap/package.json +1 -1
- package/extensions/context-inspector/index.ts +1211 -1064
- package/extensions/context-inspector/package.json +1 -1
- package/extensions/contrarian/index.ts +1854 -1735
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dirty-repo-guard/index.ts +41 -41
- package/extensions/dynamic-context-pruning/README.md +6 -38
- package/extensions/dynamic-context-pruning/index.ts +2124 -1980
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/README.md +50 -12
- package/extensions/fast/index.ts +513 -317
- package/extensions/fast/package.json +1 -1
- package/extensions/git-footer/README.md +1 -1
- package/extensions/git-footer/index.ts +458 -411
- package/extensions/git-footer/package.json +2 -3
- package/extensions/gnosis/index.ts +180 -130
- package/extensions/gnosis/package.json +1 -1
- package/extensions/inline-bash/index.ts +88 -88
- package/extensions/inline-bash/package.json +1 -1
- package/extensions/librarian/index.ts +1669 -1419
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/index.ts +994 -1012
- package/extensions/minimal-footer/openai-usage.ts +195 -190
- package/extensions/notify/index.ts +222 -230
- package/extensions/notify/package.json +1 -1
- package/extensions/openai-fast/README.md +5 -73
- package/extensions/openai-fast/index.ts +32 -272
- package/extensions/openai-fast/package.json +2 -3
- package/extensions/oracle/index.ts +1731 -1625
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/index.ts +658 -608
- package/extensions/permission-gate/package.json +1 -1
- package/extensions/permission-gate/powershell-safety.ts +840 -793
- package/extensions/project-mcp-json/.pi-fleet-tested-version +1 -0
- package/extensions/project-mcp-json/README.md +199 -0
- package/extensions/project-mcp-json/config.ts +564 -0
- package/extensions/project-mcp-json/index.ts +119 -0
- package/extensions/project-mcp-json/package.json +38 -0
- package/extensions/quiet-tools/index.ts +282 -264
- package/extensions/review/README.md +2 -2
- package/extensions/review/index.ts +1745 -1872
- package/extensions/review/package.json +1 -1
- package/extensions/todo/index.ts +284 -265
- package/extensions/triage-comments/README.md +2 -2
- package/extensions/triage-comments/index.ts +2294 -1882
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +6 -3
- package/assets/social-preview.png +0 -0
- package/extensions/claude-fast/claude-fast.example.json +0 -4
- package/extensions/openai-fast/openai-fast.example.json +0 -4
|
@@ -3,11 +3,11 @@ import * as path from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import type { AgentSession, ExtensionAPI, ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
DefaultResourceLoader,
|
|
7
|
+
SessionManager,
|
|
8
|
+
SettingsManager,
|
|
9
|
+
createAgentSession,
|
|
10
|
+
getAgentDir,
|
|
11
11
|
} from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { Type } from "typebox";
|
|
13
13
|
|
|
@@ -23,1574 +23,1822 @@ const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "ma
|
|
|
23
23
|
type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
24
24
|
type ThinkingLevelMap = Partial<Record<ThinkingLevel, unknown | null>>;
|
|
25
25
|
const CODE_REVIEWER_MODEL_PREFERENCES = [
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
26
|
+
"openai/gpt-6-astra ",
|
|
27
|
+
"global.openai.gpt-6-astra ",
|
|
28
|
+
"us.openai.gpt-6-astra ",
|
|
29
|
+
"openai.gpt-6-astra ",
|
|
30
|
+
"gpt-6-astra ",
|
|
31
|
+
"gpt-6-astra",
|
|
32
|
+
"openai/gpt-6.1-sol ",
|
|
33
|
+
"global.openai.gpt-6.1-sol ",
|
|
34
|
+
"us.openai.gpt-6.1-sol ",
|
|
35
|
+
"openai.gpt-6.1-sol ",
|
|
36
|
+
"gpt-6.1-sol ",
|
|
37
|
+
"openai/gpt-6-sol ",
|
|
38
|
+
"gpt-6-sol ",
|
|
39
|
+
"gpt-6-sol",
|
|
40
|
+
"gpt-5.6-sol",
|
|
41
|
+
"gpt-5.6-terra",
|
|
42
|
+
"openai/gpt-6-luna ",
|
|
43
|
+
"gpt-6-luna ",
|
|
44
|
+
"gpt-6-luna",
|
|
45
|
+
"gpt-5.6-luna",
|
|
46
|
+
"gpt-5.5",
|
|
47
|
+
"anthropic/claude-opus-5.5 ",
|
|
48
|
+
"global.anthropic.claude-opus-5-5 ",
|
|
49
|
+
"au.anthropic.claude-opus-5-5 ",
|
|
50
|
+
"eu.anthropic.claude-opus-5-5 ",
|
|
51
|
+
"jp.anthropic.claude-opus-5-5 ",
|
|
52
|
+
"us.anthropic.claude-opus-5-5 ",
|
|
53
|
+
"anthropic.claude-opus-5-5 ",
|
|
54
|
+
"claude-opus-5.5 ",
|
|
55
|
+
"claude-opus-5-5 ",
|
|
56
|
+
"claude-opus-5-5",
|
|
57
|
+
"claude-opus-5.5",
|
|
58
|
+
"claude-opus-5",
|
|
59
|
+
"claude-opus-4-8",
|
|
60
|
+
"claude-opus-4.8",
|
|
61
|
+
"claude-sonnet-5",
|
|
62
|
+
"claude-sonnet-4-6",
|
|
63
|
+
"claude-sonnet-4.6",
|
|
64
|
+
"claude-sonnet-4-5",
|
|
65
|
+
"claude-sonnet-4.5",
|
|
66
|
+
"claude-sonnet-4",
|
|
67
|
+
"x-ai/grok-4.7 ",
|
|
68
|
+
"spacexai/grok-4.7 ",
|
|
69
|
+
"grok-4.7 ",
|
|
70
|
+
"grok-4.7",
|
|
71
|
+
"grok-4.6",
|
|
72
|
+
"grok-4.5",
|
|
73
|
+
"gemini-3.5-flash",
|
|
74
74
|
];
|
|
75
75
|
const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
76
|
+
"amazon-bedrock": [
|
|
77
|
+
"global.anthropic.claude-opus-5-5 ",
|
|
78
|
+
"au.anthropic.claude-opus-5-5 ",
|
|
79
|
+
"eu.anthropic.claude-opus-5-5 ",
|
|
80
|
+
"jp.anthropic.claude-opus-5-5 ",
|
|
81
|
+
"us.anthropic.claude-opus-5-5 ",
|
|
82
|
+
"anthropic.claude-opus-5-5 ",
|
|
83
|
+
"claude-opus-5-5",
|
|
84
|
+
"claude-opus-5",
|
|
85
|
+
"claude-fable-5",
|
|
86
|
+
"claude-opus-4-8",
|
|
87
|
+
"global.openai.gpt-6-astra ",
|
|
88
|
+
"us.openai.gpt-6-astra ",
|
|
89
|
+
"openai.gpt-6-astra ",
|
|
90
|
+
"gpt-6-astra",
|
|
91
|
+
"claude-sonnet-5",
|
|
92
|
+
"claude-sonnet-4-6",
|
|
93
|
+
"claude-sonnet-4-5",
|
|
94
|
+
],
|
|
95
|
+
anthropic: [
|
|
96
|
+
"claude-opus-5-5 ",
|
|
97
|
+
"claude-opus-5-5",
|
|
98
|
+
"claude-opus-5",
|
|
99
|
+
"claude-fable-5",
|
|
100
|
+
"claude-opus-4-8",
|
|
101
|
+
"claude-sonnet-5",
|
|
102
|
+
"claude-sonnet-4-6",
|
|
103
|
+
"claude-sonnet-4-5",
|
|
104
|
+
],
|
|
105
|
+
"azure-openai-responses": [
|
|
106
|
+
"gpt-6-astra ",
|
|
107
|
+
"gpt-6-astra",
|
|
108
|
+
"gpt-6.1-sol ",
|
|
109
|
+
"gpt-6-sol ",
|
|
110
|
+
"gpt-6-sol",
|
|
111
|
+
"gpt-5.6-sol",
|
|
112
|
+
"gpt-5.6-terra",
|
|
113
|
+
"gpt-6-luna ",
|
|
114
|
+
"gpt-6-luna",
|
|
115
|
+
"gpt-5.6-luna",
|
|
116
|
+
"gpt-5.5-pro",
|
|
117
|
+
"gpt-5.5",
|
|
118
|
+
"gpt-5.4-pro",
|
|
119
|
+
"gpt-5.4",
|
|
120
|
+
"gpt-5.3-codex",
|
|
121
|
+
"gpt-5-pro",
|
|
122
|
+
"gpt-5-chat-latest",
|
|
123
|
+
"gpt-5.2-pro",
|
|
124
|
+
"gpt-5.2",
|
|
125
|
+
"gpt-5.1",
|
|
126
|
+
"o3-pro",
|
|
127
|
+
"o1-pro",
|
|
128
|
+
"gpt-5.4-mini",
|
|
129
|
+
"gpt-5-mini",
|
|
130
|
+
],
|
|
131
|
+
"cloudflare-ai-gateway": [
|
|
132
|
+
"claude-fable-5",
|
|
133
|
+
"claude-opus-5",
|
|
134
|
+
"claude-opus-4.8",
|
|
135
|
+
"claude-opus-4.7",
|
|
136
|
+
"claude-opus-4.6",
|
|
137
|
+
"claude-opus-4.5",
|
|
138
|
+
"gpt-6-astra ",
|
|
139
|
+
"gpt-6-astra",
|
|
140
|
+
"gpt-5.5",
|
|
141
|
+
"gpt-5.4",
|
|
142
|
+
],
|
|
143
|
+
"github-copilot": [
|
|
144
|
+
"gpt-6-astra ",
|
|
145
|
+
"gpt-6-astra",
|
|
146
|
+
"gpt-6.1-sol ",
|
|
147
|
+
"gpt-6-sol ",
|
|
148
|
+
"gpt-6-sol",
|
|
149
|
+
"gpt-5.6-sol",
|
|
150
|
+
"gpt-5.6-terra",
|
|
151
|
+
"gpt-6-luna ",
|
|
152
|
+
"gpt-6-luna",
|
|
153
|
+
"gpt-5.6-luna",
|
|
154
|
+
"gpt-5.5",
|
|
155
|
+
"claude-opus-5.5 ",
|
|
156
|
+
"claude-opus-5.5",
|
|
157
|
+
"claude-opus-5",
|
|
158
|
+
"gpt-5.4",
|
|
159
|
+
"gpt-5.3-codex",
|
|
160
|
+
"gpt-5-mini",
|
|
161
|
+
"grok-4.7 ",
|
|
162
|
+
"grok-4.7",
|
|
163
|
+
"grok-4.6",
|
|
164
|
+
"grok-4.5",
|
|
165
|
+
],
|
|
166
|
+
opencode: [
|
|
167
|
+
"gpt-6-astra ",
|
|
168
|
+
"gpt-6-astra",
|
|
169
|
+
"gpt-6.1-sol ",
|
|
170
|
+
"gpt-6-sol ",
|
|
171
|
+
"gpt-6-sol",
|
|
172
|
+
"gpt-5.6-sol",
|
|
173
|
+
"gpt-5.6-terra",
|
|
174
|
+
"gpt-6-luna ",
|
|
175
|
+
"gpt-6-luna",
|
|
176
|
+
"gpt-5.6-luna",
|
|
177
|
+
"gpt-5.5-pro",
|
|
178
|
+
"gpt-5.5",
|
|
179
|
+
"claude-opus-5-5 ",
|
|
180
|
+
"claude-opus-5-5",
|
|
181
|
+
"claude-opus-5",
|
|
182
|
+
"gpt-5.4-pro",
|
|
183
|
+
"gpt-5.4",
|
|
184
|
+
"gpt-5.3-codex",
|
|
185
|
+
],
|
|
186
|
+
"opencode-go": ["grok-4.7 ", "grok-4.7", "grok-4.6"],
|
|
187
|
+
openrouter: [
|
|
188
|
+
"openai/gpt-6-astra ",
|
|
189
|
+
"openai/gpt-6-astra",
|
|
190
|
+
"openai/gpt-6.1-sol ",
|
|
191
|
+
"openai/gpt-6-sol ",
|
|
192
|
+
"openai/gpt-6-sol",
|
|
193
|
+
"openai/gpt-5.6-sol-pro",
|
|
194
|
+
"openai/gpt-5.6-sol",
|
|
195
|
+
"openai/gpt-5.6-terra-pro",
|
|
196
|
+
"openai/gpt-5.6-terra",
|
|
197
|
+
"openai/gpt-6-luna ",
|
|
198
|
+
"openai/gpt-6-luna",
|
|
199
|
+
"openai/gpt-5.6-luna-pro",
|
|
200
|
+
"openai/gpt-5.6-luna",
|
|
201
|
+
"openai/gpt-5.5-pro",
|
|
202
|
+
"openai/gpt-5.5",
|
|
203
|
+
"anthropic/claude-opus-5.5 ",
|
|
204
|
+
"anthropic/claude-opus-5.5",
|
|
205
|
+
"anthropic/claude-opus-5",
|
|
206
|
+
"x-ai/grok-4.7 ",
|
|
207
|
+
"x-ai/grok-4.7",
|
|
208
|
+
"x-ai/grok-4.6",
|
|
209
|
+
"x-ai/grok-4.5",
|
|
210
|
+
],
|
|
211
|
+
radius: [
|
|
212
|
+
"gpt-6-astra ",
|
|
213
|
+
"gpt-6-astra",
|
|
214
|
+
"gpt-6-sol ",
|
|
215
|
+
"gpt-6-sol",
|
|
216
|
+
"gpt-5.6-sol",
|
|
217
|
+
"gpt-5.6-terra",
|
|
218
|
+
"gpt-6-luna ",
|
|
219
|
+
"gpt-6-luna",
|
|
220
|
+
"gpt-5.6-luna",
|
|
221
|
+
"gpt-5.5",
|
|
222
|
+
"claude-opus-5-5 ",
|
|
223
|
+
"claude-opus-5-5",
|
|
224
|
+
"claude-opus-5",
|
|
225
|
+
"gpt-5.4",
|
|
226
|
+
"gpt-5.3-codex",
|
|
227
|
+
"gpt-5.4-mini",
|
|
228
|
+
],
|
|
229
|
+
openai: [
|
|
230
|
+
"gpt-6-astra ",
|
|
231
|
+
"gpt-6-astra",
|
|
232
|
+
"gpt-6.1-sol ",
|
|
233
|
+
"gpt-6-sol ",
|
|
234
|
+
"gpt-6-sol",
|
|
235
|
+
"gpt-5.6-sol",
|
|
236
|
+
"gpt-5.6-terra",
|
|
237
|
+
"gpt-6-luna ",
|
|
238
|
+
"gpt-6-luna",
|
|
239
|
+
"gpt-5.6-luna",
|
|
240
|
+
"gpt-5.5",
|
|
241
|
+
"gpt-5",
|
|
242
|
+
"gpt-4.1",
|
|
243
|
+
"o3",
|
|
244
|
+
"o4-mini",
|
|
245
|
+
],
|
|
246
|
+
"openai-codex": [
|
|
247
|
+
"gpt-6-astra ",
|
|
248
|
+
"gpt-6-astra",
|
|
249
|
+
"gpt-6.1-sol ",
|
|
250
|
+
"gpt-6-sol ",
|
|
251
|
+
"gpt-6-sol",
|
|
252
|
+
"gpt-5.6-sol",
|
|
253
|
+
"gpt-5.6-terra",
|
|
254
|
+
"gpt-6-luna ",
|
|
255
|
+
"gpt-6-luna",
|
|
256
|
+
"gpt-5.6-luna",
|
|
257
|
+
"gpt-5.5",
|
|
258
|
+
"gpt-5.3-codex-spark",
|
|
259
|
+
],
|
|
260
|
+
xai: ["grok-4.7 ", "grok-4.7", "grok-4.6", "grok-4.5"],
|
|
261
|
+
"vercel-ai-gateway": [
|
|
262
|
+
"anthropic/claude-opus-5.5 ",
|
|
263
|
+
"anthropic/claude-opus-5.5",
|
|
264
|
+
"anthropic/claude-opus-5",
|
|
265
|
+
"anthropic/claude-fable-5",
|
|
266
|
+
"anthropic/claude-opus-4.8",
|
|
267
|
+
"anthropic/claude-opus-4.7",
|
|
268
|
+
"anthropic/claude-opus-4.6",
|
|
269
|
+
"anthropic/claude-sonnet-5",
|
|
270
|
+
"openai/gpt-6-astra ",
|
|
271
|
+
"openai/gpt-6-astra",
|
|
272
|
+
"openai/gpt-6.1-sol ",
|
|
273
|
+
"openai/gpt-6-sol ",
|
|
274
|
+
"openai/gpt-6-sol",
|
|
275
|
+
"openai/gpt-5.6-sol",
|
|
276
|
+
"openai/gpt-5.6-terra",
|
|
277
|
+
"openai/gpt-6-luna ",
|
|
278
|
+
"openai/gpt-6-luna",
|
|
279
|
+
"openai/gpt-5.6-luna",
|
|
280
|
+
"openai/gpt-5.5",
|
|
281
|
+
"spacexai/grok-4.7 ",
|
|
282
|
+
"spacexai/grok-4.7",
|
|
283
|
+
"spacexai/grok-4.6",
|
|
284
|
+
"xai/grok-4.5",
|
|
285
|
+
"google/gemini-3.5-flash",
|
|
286
|
+
],
|
|
275
287
|
};
|
|
276
288
|
|
|
277
289
|
const READ_ONLY_TOOL_NAMES = new Set(["read", "grep", "find", "ls", "bash"]);
|
|
278
290
|
const READ_ONLY_BASH_COMMANDS = new Set(["gh", "git", "pwd"]);
|
|
279
291
|
const READ_ONLY_GIT_SUBCOMMANDS = new Set([
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
292
|
+
"blame",
|
|
293
|
+
"cat-file",
|
|
294
|
+
"describe",
|
|
295
|
+
"diff",
|
|
296
|
+
"for-each-ref",
|
|
297
|
+
"log",
|
|
298
|
+
"ls-files",
|
|
299
|
+
"ls-tree",
|
|
300
|
+
"merge-base",
|
|
301
|
+
"name-rev",
|
|
302
|
+
"remote",
|
|
303
|
+
"rev-parse",
|
|
304
|
+
"shortlog",
|
|
305
|
+
"show",
|
|
306
|
+
"show-ref",
|
|
307
|
+
"status",
|
|
308
|
+
"whatchanged",
|
|
297
309
|
]);
|
|
298
310
|
const SAFE_GIT_BRANCH_FLAGS = new Set([
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
311
|
+
"-a",
|
|
312
|
+
"--all",
|
|
313
|
+
"-r",
|
|
314
|
+
"--remotes",
|
|
315
|
+
"-v",
|
|
316
|
+
"-vv",
|
|
317
|
+
"--show-current",
|
|
318
|
+
"--list",
|
|
319
|
+
"--contains",
|
|
320
|
+
"--merged",
|
|
321
|
+
"--no-merged",
|
|
310
322
|
]);
|
|
311
323
|
const SAFE_GIT_GLOBAL_FLAGS = new Set(["--no-pager", "--no-optional-locks"]);
|
|
312
324
|
const SAFE_GIT_CONFIG_OVERRIDES = ["core.pager=cat", "core.fsmonitor=false", "diff.external="];
|
|
313
325
|
const GIT_SUBCOMMAND_SAFE_FLAGS: Partial<Record<string, string[]>> = {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
326
|
+
blame: ["--no-textconv"],
|
|
327
|
+
diff: ["--no-ext-diff", "--no-textconv"],
|
|
328
|
+
log: ["--no-ext-diff", "--no-textconv"],
|
|
329
|
+
show: ["--no-ext-diff", "--no-textconv"],
|
|
330
|
+
whatchanged: ["--no-ext-diff", "--no-textconv"],
|
|
319
331
|
};
|
|
320
332
|
const GIT_OPTIONS_REQUIRING_LOCAL_FILE = [
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
333
|
+
{
|
|
334
|
+
flag: "--contents",
|
|
335
|
+
blockedPrefixes: ["--con"],
|
|
336
|
+
reason:
|
|
337
|
+
"Code Reviewer bash blocks git --contents because it can read local files outside built-in path guards.",
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
flag: "--pathspec-from-file",
|
|
341
|
+
blockedPrefixes: ["--pathspec"],
|
|
342
|
+
reason:
|
|
343
|
+
"Code Reviewer bash blocks git --pathspec-from-file because it can read local files outside built-in path guards.",
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
flag: "--ignore-revs-file",
|
|
347
|
+
blockedPrefixes: ["--ignore-rev"],
|
|
348
|
+
reason:
|
|
349
|
+
"Code Reviewer bash blocks git --ignore-revs-file because it can read local files outside built-in path guards.",
|
|
350
|
+
},
|
|
336
351
|
] as const;
|
|
337
|
-
const GIT_SUBCOMMAND_OPTIONS_REQUIRING_LOCAL_FILE: Partial<
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
352
|
+
const GIT_SUBCOMMAND_OPTIONS_REQUIRING_LOCAL_FILE: Partial<
|
|
353
|
+
Record<string, Array<{ reason: string; matches: (token: string) => boolean }>>
|
|
354
|
+
> = {
|
|
355
|
+
blame: [
|
|
356
|
+
{
|
|
357
|
+
reason:
|
|
358
|
+
"Code Reviewer bash blocks git blame -S because it can read local revs files outside built-in path guards.",
|
|
359
|
+
matches: (token) => token === "-S" || (token.startsWith("-S") && token.length > 2),
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
"ls-files": [
|
|
363
|
+
{
|
|
364
|
+
reason:
|
|
365
|
+
"Code Reviewer bash blocks git ls-files -X/--exclude-from because it can read local files outside built-in path guards.",
|
|
366
|
+
matches: (token) => token === "-X" || (token.startsWith("-X") && token.length > 2),
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
reason:
|
|
370
|
+
"Code Reviewer bash blocks git ls-files -X/--exclude-from because it can read local files outside built-in path guards.",
|
|
371
|
+
matches: (token) => {
|
|
372
|
+
if (!token.startsWith("--")) return false;
|
|
373
|
+
const option = token.split("=", 1)[0]?.toLowerCase() ?? "";
|
|
374
|
+
return option === "--exclude-from" || option.startsWith("--exclude-f");
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
],
|
|
358
378
|
};
|
|
359
|
-
const GH_GLOBAL_OPTIONS_WITH_VALUE = new Set([
|
|
379
|
+
const GH_GLOBAL_OPTIONS_WITH_VALUE = new Set([
|
|
380
|
+
"--repo",
|
|
381
|
+
"-R",
|
|
382
|
+
"--hostname",
|
|
383
|
+
"--jq",
|
|
384
|
+
"-q",
|
|
385
|
+
"--template",
|
|
386
|
+
]);
|
|
360
387
|
const MUTATING_GH_API_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
361
388
|
|
|
362
389
|
const CodeReviewerParams = Type.Object({
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
390
|
+
task: Type.String({
|
|
391
|
+
description:
|
|
392
|
+
"What change to review, what ticket/story it should satisfy, and any specific review focus.",
|
|
393
|
+
}),
|
|
394
|
+
diff: Type.Optional(
|
|
395
|
+
Type.String({
|
|
396
|
+
description: "Optional diff, patch, or change summary to treat as the primary review target.",
|
|
397
|
+
}),
|
|
398
|
+
),
|
|
399
|
+
context: Type.Optional(
|
|
400
|
+
Type.String({
|
|
401
|
+
description:
|
|
402
|
+
"Optional caller context such as constraints, known risks, or expected behavior.",
|
|
403
|
+
}),
|
|
404
|
+
),
|
|
376
405
|
});
|
|
377
406
|
|
|
378
407
|
type ToolCall = {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
408
|
+
id: string;
|
|
409
|
+
name: string;
|
|
410
|
+
args: unknown;
|
|
411
|
+
startedAt: number;
|
|
412
|
+
endedAt?: number;
|
|
413
|
+
isError?: boolean;
|
|
385
414
|
};
|
|
386
415
|
|
|
387
416
|
type ReviewStatus = "running" | "done" | "error" | "aborted";
|
|
388
417
|
|
|
389
418
|
type ReviewDetails = {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
419
|
+
status: ReviewStatus;
|
|
420
|
+
cwd: string;
|
|
421
|
+
task: string;
|
|
422
|
+
turns: number;
|
|
423
|
+
toolCalls: ToolCall[];
|
|
424
|
+
startedAt: number;
|
|
425
|
+
endedAt?: number;
|
|
426
|
+
error?: string;
|
|
427
|
+
modelRef?: string;
|
|
428
|
+
modelName?: string;
|
|
429
|
+
requestedThinkingLevel?: ThinkingLevel;
|
|
430
|
+
effectiveThinkingLevel?: ThinkingLevel;
|
|
431
|
+
thinkingLevelClamped?: boolean;
|
|
432
|
+
thinkingLevelNote?: string;
|
|
404
433
|
};
|
|
405
434
|
|
|
406
435
|
type PiModel = {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
436
|
+
provider: string;
|
|
437
|
+
id: string;
|
|
438
|
+
name?: string;
|
|
439
|
+
reasoning?: boolean;
|
|
440
|
+
contextWindow?: number;
|
|
441
|
+
maxTokens?: number;
|
|
442
|
+
thinkingLevelMap?: ThinkingLevelMap;
|
|
414
443
|
};
|
|
415
444
|
|
|
416
445
|
type ModelRegistryContext = {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
446
|
+
model?: PiModel;
|
|
447
|
+
modelRegistry: { getAvailable(): PiModel[] | Promise<PiModel[]> };
|
|
448
|
+
scopedModels?: ReadonlyArray<{ model: PiModel; thinkingLevel?: ThinkingLevel }>;
|
|
420
449
|
};
|
|
421
450
|
|
|
422
451
|
function modelKey(model: PiModel): string {
|
|
423
|
-
|
|
452
|
+
return `${model.provider}/${model.id}`.toLowerCase();
|
|
424
453
|
}
|
|
425
454
|
|
|
426
455
|
function hasModelScope(ctx: ModelRegistryContext): boolean {
|
|
427
|
-
|
|
456
|
+
return Array.isArray(ctx.scopedModels) && ctx.scopedModels.length > 0;
|
|
428
457
|
}
|
|
429
458
|
|
|
430
459
|
function isModelInScope(ctx: ModelRegistryContext, model: PiModel): boolean {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
460
|
+
if (!hasModelScope(ctx)) return true;
|
|
461
|
+
const key = modelKey(model);
|
|
462
|
+
return ctx.scopedModels!.some((entry) => modelKey(entry.model) === key);
|
|
434
463
|
}
|
|
435
464
|
|
|
436
465
|
async function getAutoSelectableModels(ctx: ModelRegistryContext): Promise<PiModel[]> {
|
|
437
|
-
|
|
438
|
-
|
|
466
|
+
const available = await ctx.modelRegistry.getAvailable();
|
|
467
|
+
return hasModelScope(ctx) ? available.filter((model) => isModelInScope(ctx, model)) : available;
|
|
439
468
|
}
|
|
440
469
|
|
|
441
470
|
type CreateAgentSessionOptions = NonNullable<Parameters<typeof createAgentSession>[0]>;
|
|
442
471
|
type CreateAgentSessionModel = CreateAgentSessionOptions["model"];
|
|
443
472
|
|
|
444
|
-
function getModelRuntimeOption(ctx: {
|
|
445
|
-
|
|
446
|
-
|
|
473
|
+
function getModelRuntimeOption(ctx: {
|
|
474
|
+
modelRegistry?: unknown;
|
|
475
|
+
}): Pick<CreateAgentSessionOptions, "modelRuntime"> {
|
|
476
|
+
const modelRuntime = (
|
|
477
|
+
ctx.modelRegistry as { runtime?: CreateAgentSessionOptions["modelRuntime"] } | undefined
|
|
478
|
+
)?.runtime;
|
|
479
|
+
return modelRuntime ? { modelRuntime } : {};
|
|
447
480
|
}
|
|
448
481
|
|
|
449
482
|
const MODEL_AVAILABILITY_ERROR_PATTERN =
|
|
450
|
-
|
|
483
|
+
/\b(?:404|403|not[_ ]?found(?:[_ ]?error)?|model[_ ]?not[_ ]?found(?:[_ ]?error)?|no such model|unknown model|does not exist|is not available|not available|model[_ ]?not[_ ]?available|unsupported model|invalid model|forbidden|access[ _-]?denied|permission[ _-]?denied|not[ _-]?entitled|do(?:es)? not have access)\b/i;
|
|
451
484
|
|
|
452
485
|
function isModelAvailabilityError(message: string | undefined): boolean {
|
|
453
|
-
|
|
454
|
-
|
|
486
|
+
if (!message) return false;
|
|
487
|
+
return MODEL_AVAILABILITY_ERROR_PATTERN.test(message);
|
|
455
488
|
}
|
|
456
489
|
|
|
457
490
|
function parseVersionScore(text: string): number {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
491
|
+
const matches = text.match(/\d+(?:[._-]\d+)*/g) ?? [];
|
|
492
|
+
let best = 0;
|
|
493
|
+
for (const rawMatch of matches) {
|
|
494
|
+
const match = rawMatch.replace(/[_-]/g, ".");
|
|
495
|
+
const [major = "0", minor = "0", patch = "0"] = match.split(".");
|
|
496
|
+
const score = Number(major) * 1_000_000 + Number(minor) * 1_000 + Number(patch);
|
|
497
|
+
if (score > best) best = score;
|
|
498
|
+
}
|
|
499
|
+
return best;
|
|
467
500
|
}
|
|
468
501
|
|
|
469
502
|
function rankModel(model: PiModel): number {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
503
|
+
const text = `${model.id} ${model.name ?? ""}`.toLowerCase();
|
|
504
|
+
const has = (regex: RegExp): boolean => regex.test(text);
|
|
505
|
+
let score = 0;
|
|
506
|
+
|
|
507
|
+
if (model.reasoning) score += 10_000_000;
|
|
508
|
+
score += parseVersionScore(text) * 1_000;
|
|
509
|
+
score += Math.min(model.maxTokens ?? 0, 200_000);
|
|
510
|
+
score += Math.floor(Math.min(model.contextWindow ?? 0, 1_000_000) / 100);
|
|
511
|
+
|
|
512
|
+
if (has(/\bopus\b/)) score += 350_000;
|
|
513
|
+
if (has(/\bpro\b/)) score += 180_000;
|
|
514
|
+
if (has(/\bmax\b/)) score += 60_000;
|
|
515
|
+
if (has(/\bultra\b/)) score += 150_000;
|
|
516
|
+
if (has(/\bsonnet\b/)) score += 120_000;
|
|
517
|
+
if (has(/\bcodex\b|\bcoder\b|\bcode\b/)) score += 40_000;
|
|
518
|
+
if (has(/\breasoning\b|\bthink(?:ing)?\b/)) score += 60_000;
|
|
519
|
+
|
|
520
|
+
if (has(/\bhaiku\b/)) score -= 420_000;
|
|
521
|
+
if (has(/\bmini\b/)) score -= 520_000;
|
|
522
|
+
if (has(/\bnano\b/)) score -= 700_000;
|
|
523
|
+
if (has(/\bflash\b/)) score -= 520_000;
|
|
524
|
+
if (has(/\bspark\b/)) score -= 650_000;
|
|
525
|
+
if (has(/\blite\b|\bsmall\b|\bfast\b|\binstant\b/)) score -= 300_000;
|
|
526
|
+
|
|
527
|
+
return score;
|
|
495
528
|
}
|
|
496
529
|
|
|
497
530
|
function modelText(model: PiModel): string {
|
|
498
|
-
|
|
531
|
+
return `${model.provider} ${model.id} ${model.name ?? ""}`.toLowerCase();
|
|
499
532
|
}
|
|
500
533
|
|
|
501
534
|
function isAnthropicFamily(model: PiModel): boolean {
|
|
502
|
-
|
|
535
|
+
return /\b(anthropic|claude|opus|sonnet|haiku|fable)\b/.test(modelText(model));
|
|
503
536
|
}
|
|
504
537
|
|
|
505
538
|
function isOpenAiFamily(model: PiModel): boolean {
|
|
506
|
-
|
|
539
|
+
return /\b(openai|codex|chatgpt|gpt[-_. ]?\d|o[1345](?:\b|-|_))\b/.test(modelText(model));
|
|
507
540
|
}
|
|
508
541
|
|
|
509
542
|
function getProviderPreferenceList(provider: string | undefined): string[] | undefined {
|
|
510
|
-
|
|
511
|
-
|
|
543
|
+
if (!provider) return undefined;
|
|
544
|
+
return PROVIDER_MODEL_PREFERENCES[provider.toLowerCase()];
|
|
512
545
|
}
|
|
513
546
|
|
|
514
|
-
function selectPreferredModel(
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
547
|
+
function selectPreferredModel(
|
|
548
|
+
models: PiModel[],
|
|
549
|
+
provider: string | undefined,
|
|
550
|
+
): PiModel | undefined {
|
|
551
|
+
const preferences = getProviderPreferenceList(provider);
|
|
552
|
+
if (!preferences || preferences.length === 0) return undefined;
|
|
553
|
+
const lowered = models.map((model) => ({
|
|
554
|
+
model,
|
|
555
|
+
haystack: `${model.id} ${model.name ?? ""}`.toLowerCase(),
|
|
556
|
+
}));
|
|
557
|
+
for (const pattern of preferences) {
|
|
558
|
+
const match = lowered.find((entry) => entry.haystack.includes(pattern.toLowerCase()));
|
|
559
|
+
if (match) return match.model;
|
|
560
|
+
}
|
|
561
|
+
return undefined;
|
|
523
562
|
}
|
|
524
563
|
|
|
525
564
|
function selectPreferredAcrossProviders(models: PiModel[]): PiModel | undefined {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
565
|
+
const lowered = models.map((model) => ({
|
|
566
|
+
model,
|
|
567
|
+
haystack: `${model.id} ${model.name ?? ""}`.toLowerCase(),
|
|
568
|
+
}));
|
|
569
|
+
for (const pattern of CODE_REVIEWER_MODEL_PREFERENCES) {
|
|
570
|
+
const match = lowered.find((entry) => entry.haystack.includes(pattern.toLowerCase()));
|
|
571
|
+
if (match) return match.model;
|
|
572
|
+
}
|
|
573
|
+
for (const preferences of Object.values(PROVIDER_MODEL_PREFERENCES)) {
|
|
574
|
+
for (const pattern of preferences) {
|
|
575
|
+
const match = lowered.find((entry) => entry.haystack.includes(pattern.toLowerCase()));
|
|
576
|
+
if (match) return match.model;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return undefined;
|
|
538
580
|
}
|
|
539
581
|
|
|
540
582
|
function orderPreferredModels(candidates: PiModel[], providerForPreferences?: string): PiModel[] {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
583
|
+
const sorted = [...candidates].sort((a, b) => rankModel(b) - rankModel(a));
|
|
584
|
+
const preferred = providerForPreferences
|
|
585
|
+
? selectPreferredModel(candidates, providerForPreferences)
|
|
586
|
+
: selectPreferredAcrossProviders(candidates);
|
|
587
|
+
return preferred ? [preferred, ...sorted.filter((model) => model !== preferred)] : sorted;
|
|
546
588
|
}
|
|
547
589
|
|
|
548
590
|
function getModelKey(model: PiModel): string {
|
|
549
|
-
|
|
591
|
+
return `${model.provider}::${model.id}`;
|
|
550
592
|
}
|
|
551
593
|
|
|
552
594
|
function appendOrderedCandidates(
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
595
|
+
ordered: PiModel[],
|
|
596
|
+
seen: Set<string>,
|
|
597
|
+
candidates: PiModel[],
|
|
598
|
+
providerForPreferences?: string,
|
|
557
599
|
): void {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
600
|
+
for (const model of orderPreferredModels(candidates, providerForPreferences)) {
|
|
601
|
+
const key = getModelKey(model);
|
|
602
|
+
if (seen.has(key)) continue;
|
|
603
|
+
seen.add(key);
|
|
604
|
+
ordered.push(model);
|
|
605
|
+
}
|
|
564
606
|
}
|
|
565
607
|
|
|
566
608
|
async function selectCodeReviewerModel(
|
|
567
|
-
|
|
609
|
+
ctx: ModelRegistryContext,
|
|
568
610
|
): Promise<{ ok: true; selection: PiModel; ordered: PiModel[] } | { ok: false; error: string }> {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
611
|
+
const available = await getAutoSelectableModels(ctx);
|
|
612
|
+
if (available.length === 0) {
|
|
613
|
+
return {
|
|
614
|
+
ok: false,
|
|
615
|
+
error: hasModelScope(ctx)
|
|
616
|
+
? "No authenticated models are available in the current session model scope. Adjust the scope, log in, or configure an API key."
|
|
617
|
+
: "No authenticated models are available. Log in or configure an API key first.",
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const currentModel = ctx.model;
|
|
622
|
+
const currentProvider = currentModel?.provider;
|
|
623
|
+
const sameProvider = currentProvider
|
|
624
|
+
? available.filter((model) => model.provider === currentProvider)
|
|
625
|
+
: [];
|
|
626
|
+
const sameProviderReasoning = sameProvider.filter((model) => model.reasoning);
|
|
627
|
+
const allReasoning = available.filter((model) => model.reasoning);
|
|
628
|
+
const oppositeFamily = currentModel
|
|
629
|
+
? isAnthropicFamily(currentModel)
|
|
630
|
+
? available.filter(isOpenAiFamily)
|
|
631
|
+
: isOpenAiFamily(currentModel)
|
|
632
|
+
? available.filter(isAnthropicFamily)
|
|
633
|
+
: []
|
|
634
|
+
: [];
|
|
635
|
+
const oppositeProvider = currentProvider
|
|
636
|
+
? available.filter((model) => model.provider !== currentProvider)
|
|
637
|
+
: [];
|
|
638
|
+
const oppositeProviderFamily = currentProvider
|
|
639
|
+
? oppositeFamily.filter((model) => model.provider !== currentProvider)
|
|
640
|
+
: oppositeFamily;
|
|
641
|
+
const sameProviderOppositeFamily = currentProvider
|
|
642
|
+
? oppositeFamily.filter((model) => model.provider === currentProvider)
|
|
643
|
+
: [];
|
|
644
|
+
const oppositeProviderFamilyReasoning = oppositeProviderFamily.filter((model) => model.reasoning);
|
|
645
|
+
const sameProviderOppositeFamilyReasoning = sameProviderOppositeFamily.filter(
|
|
646
|
+
(model) => model.reasoning,
|
|
647
|
+
);
|
|
648
|
+
const oppositeProviderReasoning = oppositeProvider.filter((model) => model.reasoning);
|
|
649
|
+
|
|
650
|
+
const ordered: PiModel[] = [];
|
|
651
|
+
const seen = new Set<string>();
|
|
652
|
+
appendOrderedCandidates(ordered, seen, oppositeProviderFamilyReasoning);
|
|
653
|
+
appendOrderedCandidates(ordered, seen, oppositeProviderFamily);
|
|
654
|
+
appendOrderedCandidates(ordered, seen, oppositeProviderReasoning);
|
|
655
|
+
appendOrderedCandidates(ordered, seen, oppositeProvider);
|
|
656
|
+
appendOrderedCandidates(ordered, seen, sameProviderOppositeFamilyReasoning);
|
|
657
|
+
appendOrderedCandidates(ordered, seen, sameProviderOppositeFamily);
|
|
658
|
+
appendOrderedCandidates(ordered, seen, sameProviderReasoning, currentProvider);
|
|
659
|
+
appendOrderedCandidates(ordered, seen, sameProvider, currentProvider);
|
|
660
|
+
appendOrderedCandidates(ordered, seen, allReasoning);
|
|
661
|
+
appendOrderedCandidates(ordered, seen, available);
|
|
662
|
+
|
|
663
|
+
return { ok: true, selection: ordered[0], ordered };
|
|
618
664
|
}
|
|
619
665
|
|
|
620
666
|
function normalizeThinkingLevel(value: unknown): ThinkingLevel | undefined {
|
|
621
|
-
|
|
667
|
+
return typeof value === "string" && THINKING_LEVELS.includes(value as ThinkingLevel)
|
|
668
|
+
? (value as ThinkingLevel)
|
|
669
|
+
: undefined;
|
|
622
670
|
}
|
|
623
671
|
|
|
624
672
|
// Keep these local so the extension stays compatible with older pi peer installs that do not export clamp helpers.
|
|
625
673
|
function isThinkingLevelSupported(model: PiModel, level: ThinkingLevel): boolean {
|
|
626
|
-
|
|
674
|
+
if (!model.reasoning) return level === "off";
|
|
627
675
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
676
|
+
const map = model.thinkingLevelMap;
|
|
677
|
+
if (level === "xhigh" || level === "max") {
|
|
678
|
+
return !!map && Object.prototype.hasOwnProperty.call(map, level) && map[level] != null;
|
|
679
|
+
}
|
|
680
|
+
return map?.[level] !== null;
|
|
633
681
|
}
|
|
634
682
|
|
|
635
683
|
function clampThinkingLevel(model: PiModel, requested: ThinkingLevel): ThinkingLevel {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
684
|
+
if (isThinkingLevelSupported(model, requested)) return requested;
|
|
685
|
+
|
|
686
|
+
const requestedIndex = THINKING_LEVELS.indexOf(requested);
|
|
687
|
+
for (let index = requestedIndex + 1; index < THINKING_LEVELS.length; index += 1) {
|
|
688
|
+
const level = THINKING_LEVELS[index];
|
|
689
|
+
if (isThinkingLevelSupported(model, level)) return level;
|
|
690
|
+
}
|
|
691
|
+
for (let index = requestedIndex - 1; index >= 0; index -= 1) {
|
|
692
|
+
const level = THINKING_LEVELS[index];
|
|
693
|
+
if (isThinkingLevelSupported(model, level)) return level;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return "off";
|
|
649
697
|
}
|
|
650
698
|
|
|
651
699
|
function resolveThinkingLevel(
|
|
652
|
-
|
|
653
|
-
|
|
700
|
+
model: PiModel | undefined,
|
|
701
|
+
requestedThinkingLevel: ThinkingLevel | undefined,
|
|
654
702
|
): { requested: ThinkingLevel; effective: ThinkingLevel; clamped: boolean; note: string } {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
703
|
+
const requested = requestedThinkingLevel ?? (model?.reasoning ? DEFAULT_THINKING_LEVEL : "off");
|
|
704
|
+
const effective = model ? clampThinkingLevel(model, requested) : requested;
|
|
705
|
+
const clamped = effective !== requested;
|
|
706
|
+
if (clamped) {
|
|
707
|
+
return {
|
|
708
|
+
requested,
|
|
709
|
+
effective,
|
|
710
|
+
clamped,
|
|
711
|
+
note: `requested ${requested}; clamped to ${effective}`,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
if (requestedThinkingLevel) {
|
|
715
|
+
return { requested, effective, clamped, note: `requested ${requested}` };
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
requested,
|
|
719
|
+
effective,
|
|
720
|
+
clamped,
|
|
721
|
+
note: model?.reasoning
|
|
722
|
+
? `defaulted to ${effective}`
|
|
723
|
+
: `defaulted to off for non-reasoning model`,
|
|
724
|
+
};
|
|
675
725
|
}
|
|
676
726
|
|
|
677
|
-
function applyModelAttemptDetails(
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
727
|
+
function applyModelAttemptDetails(
|
|
728
|
+
details: ReviewDetails,
|
|
729
|
+
model: PiModel,
|
|
730
|
+
thinking: ReturnType<typeof resolveThinkingLevel>,
|
|
731
|
+
): void {
|
|
732
|
+
details.modelRef = `${model.provider}/${model.id}`;
|
|
733
|
+
details.modelName = model.name;
|
|
734
|
+
details.requestedThinkingLevel = thinking.requested;
|
|
735
|
+
details.effectiveThinkingLevel = thinking.effective;
|
|
736
|
+
details.thinkingLevelClamped = thinking.clamped || undefined;
|
|
737
|
+
details.thinkingLevelNote = thinking.note;
|
|
684
738
|
}
|
|
685
739
|
|
|
686
740
|
function createEmptyUsage(): Usage {
|
|
687
|
-
|
|
741
|
+
return {
|
|
742
|
+
input: 0,
|
|
743
|
+
output: 0,
|
|
744
|
+
cacheRead: 0,
|
|
745
|
+
cacheWrite: 0,
|
|
746
|
+
totalTokens: 0,
|
|
747
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
748
|
+
};
|
|
688
749
|
}
|
|
689
750
|
|
|
690
751
|
function addUsage(total: Usage, usage: Usage): void {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
752
|
+
total.input += usage.input || 0;
|
|
753
|
+
total.output += usage.output || 0;
|
|
754
|
+
total.cacheRead += usage.cacheRead || 0;
|
|
755
|
+
total.cacheWrite += usage.cacheWrite || 0;
|
|
756
|
+
if (usage.cacheWrite1h !== undefined)
|
|
757
|
+
total.cacheWrite1h = (total.cacheWrite1h ?? 0) + usage.cacheWrite1h;
|
|
758
|
+
if (usage.reasoning !== undefined) total.reasoning = (total.reasoning ?? 0) + usage.reasoning;
|
|
759
|
+
total.totalTokens += usage.totalTokens || 0;
|
|
760
|
+
total.cost.input += usage.cost?.input || 0;
|
|
761
|
+
total.cost.output += usage.cost?.output || 0;
|
|
762
|
+
total.cost.cacheRead += usage.cost?.cacheRead || 0;
|
|
763
|
+
total.cost.cacheWrite += usage.cost?.cacheWrite || 0;
|
|
764
|
+
total.cost.total += usage.cost?.total || 0;
|
|
703
765
|
}
|
|
704
766
|
|
|
705
|
-
type AssistantLikeMessage = {
|
|
767
|
+
type AssistantLikeMessage = {
|
|
768
|
+
role?: string;
|
|
769
|
+
content?: unknown;
|
|
770
|
+
stopReason?: unknown;
|
|
771
|
+
errorMessage?: unknown;
|
|
772
|
+
usage?: unknown;
|
|
773
|
+
};
|
|
706
774
|
|
|
707
775
|
function addAssistantMessageUsage(total: Usage, message: unknown): void {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
776
|
+
const assistant = message as AssistantLikeMessage;
|
|
777
|
+
if (assistant?.role !== "assistant" || !assistant.usage || typeof assistant.usage !== "object")
|
|
778
|
+
return;
|
|
779
|
+
addUsage(total, assistant.usage as Usage);
|
|
711
780
|
}
|
|
712
781
|
|
|
713
782
|
function aggregateAssistantUsage(messages: unknown[]): Usage {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
783
|
+
const total = createEmptyUsage();
|
|
784
|
+
for (const message of messages) addAssistantMessageUsage(total, message);
|
|
785
|
+
return total;
|
|
717
786
|
}
|
|
718
787
|
|
|
719
788
|
function addSessionEventUsage(total: Usage, event: unknown): void {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
789
|
+
const sessionEvent = event as { type?: string; message?: unknown; result?: { usage?: Usage } };
|
|
790
|
+
if (sessionEvent?.type === "message_end")
|
|
791
|
+
return void addAssistantMessageUsage(total, sessionEvent.message);
|
|
792
|
+
if (sessionEvent?.type === "compaction_end" && sessionEvent.result?.usage)
|
|
793
|
+
addUsage(total, sessionEvent.result.usage);
|
|
723
794
|
}
|
|
724
795
|
|
|
725
|
-
function inspectFinalAssistant(
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
796
|
+
function inspectFinalAssistant(
|
|
797
|
+
messages: unknown[],
|
|
798
|
+
):
|
|
799
|
+
| { ok: true; answer: string; stopReason?: string }
|
|
800
|
+
| { ok: false; reason: string; stopReason?: string; errorMessage?: string } {
|
|
801
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
802
|
+
const message = messages[i] as AssistantLikeMessage;
|
|
803
|
+
if (message?.role !== "assistant") continue;
|
|
804
|
+
const stopReason = typeof message.stopReason === "string" ? message.stopReason.trim() : "";
|
|
805
|
+
const errorMessage =
|
|
806
|
+
typeof message.errorMessage === "string" ? message.errorMessage.trim() : "";
|
|
807
|
+
const answer = Array.isArray(message.content)
|
|
808
|
+
? message.content
|
|
809
|
+
.map((part) =>
|
|
810
|
+
part &&
|
|
811
|
+
typeof part === "object" &&
|
|
812
|
+
(part as { type?: string }).type === "text" &&
|
|
813
|
+
typeof (part as { text?: unknown }).text === "string"
|
|
814
|
+
? (part as { text: string }).text
|
|
815
|
+
: "",
|
|
816
|
+
)
|
|
817
|
+
.join("")
|
|
818
|
+
.trim()
|
|
819
|
+
: "";
|
|
820
|
+
if (stopReason === "error")
|
|
821
|
+
return {
|
|
822
|
+
ok: false,
|
|
823
|
+
reason: `code_reviewer subagent error: ${errorMessage || "provider/model error"}`,
|
|
824
|
+
stopReason,
|
|
825
|
+
errorMessage,
|
|
826
|
+
};
|
|
827
|
+
if (stopReason === "aborted")
|
|
828
|
+
return {
|
|
829
|
+
ok: false,
|
|
830
|
+
reason: "code_reviewer subagent aborted before producing a usable answer",
|
|
831
|
+
stopReason,
|
|
832
|
+
};
|
|
833
|
+
if (answer) return { ok: true, answer, stopReason: stopReason || undefined };
|
|
834
|
+
return {
|
|
835
|
+
ok: false,
|
|
836
|
+
reason: stopReason
|
|
837
|
+
? `code_reviewer subagent produced no final assistant text (stopReason: ${stopReason})`
|
|
838
|
+
: "code_reviewer subagent produced no final assistant text",
|
|
839
|
+
stopReason: stopReason || undefined,
|
|
840
|
+
errorMessage: errorMessage || undefined,
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
return { ok: false, reason: "code_reviewer subagent produced no assistant message" };
|
|
747
844
|
}
|
|
748
845
|
|
|
749
|
-
function classifyRunFailure(
|
|
750
|
-
|
|
751
|
-
|
|
846
|
+
function classifyRunFailure(
|
|
847
|
+
error: unknown,
|
|
848
|
+
aborted: boolean,
|
|
849
|
+
): { status: "aborted" | "error"; message: string; error?: string } {
|
|
850
|
+
const message = aborted ? "Aborted" : error instanceof Error ? error.message : String(error);
|
|
851
|
+
return { status: aborted ? "aborted" : "error", message, error: aborted ? undefined : message };
|
|
752
852
|
}
|
|
753
853
|
|
|
754
854
|
function isInside(parent: string, child: string): boolean {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
855
|
+
const parentResolved = path.resolve(parent);
|
|
856
|
+
const childResolved = path.resolve(child);
|
|
857
|
+
return (
|
|
858
|
+
childResolved === parentResolved || childResolved.startsWith(`${parentResolved}${path.sep}`)
|
|
859
|
+
);
|
|
758
860
|
}
|
|
759
861
|
|
|
760
862
|
function resolveToolPath(cwd: string, rawPath: string | undefined): string {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
863
|
+
const input = rawPath?.trim() || ".";
|
|
864
|
+
const normalized = input.startsWith("@") ? input.slice(1) : input;
|
|
865
|
+
return path.isAbsolute(normalized) ? path.resolve(normalized) : path.resolve(cwd, normalized);
|
|
764
866
|
}
|
|
765
867
|
|
|
766
|
-
async function assertToolPathInsideCwd(
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
868
|
+
async function assertToolPathInsideCwd(
|
|
869
|
+
cwd: string,
|
|
870
|
+
rawPath: unknown,
|
|
871
|
+
toolName: string,
|
|
872
|
+
): Promise<string | undefined> {
|
|
873
|
+
if (rawPath !== undefined && typeof rawPath !== "string")
|
|
874
|
+
return `${toolName} path must be a string.`;
|
|
875
|
+
if (typeof rawPath === "string") {
|
|
876
|
+
const normalizedInput = rawPath.trim().startsWith("@")
|
|
877
|
+
? rawPath.trim().slice(1)
|
|
878
|
+
: rawPath.trim();
|
|
879
|
+
if (/(^|[/\\])\.\.(?:[/\\]|$)/.test(normalizedInput)) {
|
|
880
|
+
return `${toolName} path must not traverse outside the local checkout.`;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
const root = await fs.realpath(cwd).catch(() => path.resolve(cwd));
|
|
884
|
+
const resolved = resolveToolPath(cwd, rawPath);
|
|
885
|
+
const realPath = await fs.realpath(resolved).catch(() => resolved);
|
|
886
|
+
if (!isInside(root, realPath)) return `${toolName} is limited to the local checkout: ${realPath}`;
|
|
887
|
+
return undefined;
|
|
779
888
|
}
|
|
780
889
|
|
|
781
890
|
function getUnsafePathPatternReason(value: unknown, label: string): string | undefined {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
891
|
+
if (value === undefined) return undefined;
|
|
892
|
+
if (typeof value !== "string") return `${label} must be a string.`;
|
|
893
|
+
if (path.isAbsolute(value)) return `${label} must be relative to the local checkout.`;
|
|
894
|
+
if (/(^|[/\\])\.\.(?:[/\\]|$)/.test(value))
|
|
895
|
+
return `${label} must not traverse outside the local checkout.`;
|
|
896
|
+
return undefined;
|
|
787
897
|
}
|
|
788
898
|
|
|
789
899
|
function stripTokenQuotes(token: string): string {
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
900
|
+
if (
|
|
901
|
+
(token.startsWith("'") && token.endsWith("'")) ||
|
|
902
|
+
(token.startsWith('"') && token.endsWith('"'))
|
|
903
|
+
) {
|
|
904
|
+
return token.slice(1, -1);
|
|
905
|
+
}
|
|
906
|
+
return token;
|
|
794
907
|
}
|
|
795
908
|
|
|
796
909
|
type ShellTokenizeResult = {
|
|
797
|
-
|
|
798
|
-
|
|
910
|
+
tokens: string[];
|
|
911
|
+
reason?: string;
|
|
799
912
|
};
|
|
800
913
|
|
|
801
914
|
function tokenizeShellCommand(command: string): ShellTokenizeResult {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
915
|
+
const tokens: string[] = [];
|
|
916
|
+
let token = "";
|
|
917
|
+
let tokenStarted = false;
|
|
918
|
+
let inSingleQuote = false;
|
|
919
|
+
let inDoubleQuote = false;
|
|
920
|
+
|
|
921
|
+
const pushToken = () => {
|
|
922
|
+
if (!tokenStarted) return;
|
|
923
|
+
tokens.push(token);
|
|
924
|
+
token = "";
|
|
925
|
+
tokenStarted = false;
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
929
|
+
const char = command[index];
|
|
930
|
+
const next = command[index + 1] ?? "";
|
|
931
|
+
|
|
932
|
+
if (inSingleQuote) {
|
|
933
|
+
if (char === "'") {
|
|
934
|
+
inSingleQuote = false;
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
token += char;
|
|
938
|
+
tokenStarted = true;
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (char === "\\") {
|
|
943
|
+
return {
|
|
944
|
+
tokens,
|
|
945
|
+
reason:
|
|
946
|
+
"Code Reviewer bash blocks shell escape sequences; pass plain git, gh, or pwd arguments without backslashes.",
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (inDoubleQuote) {
|
|
951
|
+
if (char === '"') {
|
|
952
|
+
inDoubleQuote = false;
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
if (char === "`" || char === "$") {
|
|
956
|
+
return {
|
|
957
|
+
tokens,
|
|
958
|
+
reason:
|
|
959
|
+
char === "`" || next === "("
|
|
960
|
+
? "Code Reviewer bash blocks command substitution."
|
|
961
|
+
: "Code Reviewer bash blocks shell expansion in double quotes.",
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
token += char;
|
|
965
|
+
tokenStarted = true;
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (char === "\n" || char === "\r") {
|
|
970
|
+
return {
|
|
971
|
+
tokens,
|
|
972
|
+
reason:
|
|
973
|
+
"Code Reviewer bash allows one git, gh, or pwd invocation only; pipelines, shell control operators, and multiple commands are blocked.",
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
if (/\s/.test(char)) {
|
|
977
|
+
pushToken();
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
if (char === "'") {
|
|
981
|
+
inSingleQuote = true;
|
|
982
|
+
tokenStarted = true;
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
if (char === '"') {
|
|
986
|
+
inDoubleQuote = true;
|
|
987
|
+
tokenStarted = true;
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
if (char === "`") return { tokens, reason: "Code Reviewer bash blocks command substitution." };
|
|
991
|
+
if (char === "$") {
|
|
992
|
+
return {
|
|
993
|
+
tokens,
|
|
994
|
+
reason:
|
|
995
|
+
next === "'" || next === '"'
|
|
996
|
+
? "Code Reviewer bash blocks ANSI-C and localized shell quotes."
|
|
997
|
+
: next === "("
|
|
998
|
+
? "Code Reviewer bash blocks command substitution."
|
|
999
|
+
: "Code Reviewer bash blocks shell expansion.",
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
if (/[;&|()]/.test(char)) {
|
|
1003
|
+
return {
|
|
1004
|
+
tokens,
|
|
1005
|
+
reason:
|
|
1006
|
+
"Code Reviewer bash allows one git, gh, or pwd invocation only; pipelines, shell control operators, and multiple commands are blocked.",
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
if (char === ">" || char === "<") {
|
|
1010
|
+
return {
|
|
1011
|
+
tokens,
|
|
1012
|
+
reason: "Code Reviewer bash blocks shell redirection to keep inspection read-only.",
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
if (char === "{" || char === "}")
|
|
1016
|
+
return { tokens, reason: "Code Reviewer bash blocks shell brace expansion." };
|
|
1017
|
+
if (char === "*" || char === "?" || char === "[" || char === "]") {
|
|
1018
|
+
return { tokens, reason: "Code Reviewer bash blocks shell glob expansion." };
|
|
1019
|
+
}
|
|
1020
|
+
if (char === "~" && !tokenStarted)
|
|
1021
|
+
return { tokens, reason: "Code Reviewer bash blocks shell home-directory expansion." };
|
|
1022
|
+
|
|
1023
|
+
token += char;
|
|
1024
|
+
tokenStarted = true;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (inSingleQuote || inDoubleQuote)
|
|
1028
|
+
return { tokens, reason: "Code Reviewer bash blocks unterminated or malformed shell quoting." };
|
|
1029
|
+
pushToken();
|
|
1030
|
+
return { tokens };
|
|
911
1031
|
}
|
|
912
1032
|
|
|
913
1033
|
function tokenizeSegment(segment: string): string[] {
|
|
914
|
-
|
|
1034
|
+
return tokenizeShellCommand(segment).tokens;
|
|
915
1035
|
}
|
|
916
1036
|
|
|
917
1037
|
function shellQuoteToken(token: string): string {
|
|
918
|
-
|
|
1038
|
+
return `'${token.replace(/'/g, `'"'"'`)}'`;
|
|
919
1039
|
}
|
|
920
1040
|
|
|
921
1041
|
function getExecutableName(token: string): string {
|
|
922
|
-
|
|
1042
|
+
return path.basename(token).toLowerCase();
|
|
923
1043
|
}
|
|
924
1044
|
|
|
925
1045
|
function getShellSyntaxReason(command: string): string | undefined {
|
|
926
|
-
|
|
1046
|
+
return tokenizeShellCommand(command).reason;
|
|
927
1047
|
}
|
|
928
1048
|
|
|
929
1049
|
function getUnsafeGitTokenPathReason(token: string): string | undefined {
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1050
|
+
const valueParts = [token];
|
|
1051
|
+
const equalsIndex = token.indexOf("=");
|
|
1052
|
+
if (equalsIndex >= 0 && equalsIndex < token.length - 1)
|
|
1053
|
+
valueParts.push(token.slice(equalsIndex + 1));
|
|
1054
|
+
for (const value of valueParts) {
|
|
1055
|
+
const normalized = value.startsWith("@") ? value.slice(1) : value;
|
|
1056
|
+
if (normalized === "~" || normalized.startsWith("~/") || /^~[^/\\]*/.test(normalized)) {
|
|
1057
|
+
return "Code Reviewer bash git arguments must not use home-directory paths; use built-in read/grep/find/ls for local files.";
|
|
1058
|
+
}
|
|
1059
|
+
if (path.isAbsolute(normalized)) {
|
|
1060
|
+
return "Code Reviewer bash git arguments must not use absolute filesystem paths; use built-in read/grep/find/ls for local files.";
|
|
1061
|
+
}
|
|
1062
|
+
if (/(^|[/\\])\.\.(?:[/\\]|$)/.test(normalized)) {
|
|
1063
|
+
return "Code Reviewer bash git arguments must not traverse outside the local checkout.";
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
return undefined;
|
|
946
1067
|
}
|
|
947
1068
|
|
|
948
1069
|
function getBlockedPwdReason(tokens: string[]): string | undefined {
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1070
|
+
const args = tokens.slice(1);
|
|
1071
|
+
if (args.length === 0) return undefined;
|
|
1072
|
+
if (args.length === 1 && (args[0] === "-P" || args[0] === "-L")) return undefined;
|
|
1073
|
+
return "Code Reviewer bash allows pwd only with no arguments or -P/-L.";
|
|
953
1074
|
}
|
|
954
1075
|
|
|
955
|
-
function getGitSubcommand(tokens: string[]): {
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1076
|
+
function getGitSubcommand(tokens: string[]): {
|
|
1077
|
+
subcommand?: string;
|
|
1078
|
+
index: number;
|
|
1079
|
+
reason?: string;
|
|
1080
|
+
} {
|
|
1081
|
+
let index = 1;
|
|
1082
|
+
while (index < tokens.length) {
|
|
1083
|
+
const token = tokens[index];
|
|
1084
|
+
if (!token.startsWith("-")) break;
|
|
1085
|
+
if (token === "--git-dir" || token.startsWith("--git-dir=")) {
|
|
1086
|
+
return {
|
|
1087
|
+
index,
|
|
1088
|
+
reason:
|
|
1089
|
+
"Code Reviewer bash blocks git --git-dir because it can inspect or mutate outside the checkout.",
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
if (token === "--work-tree" || token.startsWith("--work-tree=")) {
|
|
1093
|
+
return {
|
|
1094
|
+
index,
|
|
1095
|
+
reason:
|
|
1096
|
+
"Code Reviewer bash blocks git --work-tree because it can inspect or mutate outside the checkout.",
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
if (token === "-C") {
|
|
1100
|
+
const gitCwd = tokens[index + 1];
|
|
1101
|
+
if (gitCwd !== ".")
|
|
1102
|
+
return {
|
|
1103
|
+
index,
|
|
1104
|
+
reason: "Code Reviewer bash only allows git -C .; run git from the local checkout.",
|
|
1105
|
+
};
|
|
1106
|
+
index += 2;
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
if (token.startsWith("-C")) {
|
|
1110
|
+
if (token !== "-C.")
|
|
1111
|
+
return {
|
|
1112
|
+
index,
|
|
1113
|
+
reason: "Code Reviewer bash only allows git -C .; run git from the local checkout.",
|
|
1114
|
+
};
|
|
1115
|
+
index += 1;
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
if (token === "--paginate" || token === "-p") {
|
|
1119
|
+
return {
|
|
1120
|
+
index,
|
|
1121
|
+
reason:
|
|
1122
|
+
"Code Reviewer bash blocks git pagination because pagers can execute local utilities.",
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
if (SAFE_GIT_GLOBAL_FLAGS.has(token)) {
|
|
1126
|
+
index += 1;
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
return {
|
|
1130
|
+
index,
|
|
1131
|
+
reason: `Code Reviewer bash blocks git global option ${token}; use direct read-only git inspection commands from the checkout.`,
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
return { subcommand: tokens[index]?.toLowerCase(), index };
|
|
987
1135
|
}
|
|
988
1136
|
|
|
989
1137
|
function isSafeGitBranchCommand(tokens: string[], subcommandIndex: number): boolean {
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1138
|
+
const args = tokens.slice(subcommandIndex + 1);
|
|
1139
|
+
if (args.length === 0) return true;
|
|
1140
|
+
if (
|
|
1141
|
+
args.some((arg) =>
|
|
1142
|
+
/^(?:-(?:d|D|m|M|c|C|f)|--(?:delete|move|copy|force|set-upstream-to|unset-upstream))$/.test(
|
|
1143
|
+
arg,
|
|
1144
|
+
),
|
|
1145
|
+
)
|
|
1146
|
+
) {
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
return args.some(
|
|
1150
|
+
(arg) =>
|
|
1151
|
+
SAFE_GIT_BRANCH_FLAGS.has(arg) ||
|
|
1152
|
+
arg.startsWith("--contains=") ||
|
|
1153
|
+
arg.startsWith("--merged=") ||
|
|
1154
|
+
arg.startsWith("--no-merged="),
|
|
1155
|
+
);
|
|
996
1156
|
}
|
|
997
1157
|
|
|
998
1158
|
function isSafeGitRemoteCommand(tokens: string[], subcommandIndex: number): boolean {
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1159
|
+
const args = tokens.slice(subcommandIndex + 1);
|
|
1160
|
+
if (args.length === 0) return true;
|
|
1161
|
+
if (args.length === 1 && (args[0] === "-v" || args[0] === "--verbose")) return true;
|
|
1162
|
+
const remoteAction = args.find((arg) => !arg.startsWith("-"));
|
|
1163
|
+
if (remoteAction !== "get-url") return false;
|
|
1164
|
+
return true;
|
|
1005
1165
|
}
|
|
1006
1166
|
|
|
1007
|
-
function isBlockedGitLocalFileOption(
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1167
|
+
function isBlockedGitLocalFileOption(
|
|
1168
|
+
token: string,
|
|
1169
|
+
flag: string,
|
|
1170
|
+
blockedPrefixes: readonly string[],
|
|
1171
|
+
): boolean {
|
|
1172
|
+
if (!token.startsWith("--")) return false;
|
|
1173
|
+
const option = token.split("=", 1)[0]?.toLowerCase() ?? "";
|
|
1174
|
+
if (option === flag) return true;
|
|
1175
|
+
return blockedPrefixes.some((prefix) => option.startsWith(prefix));
|
|
1012
1176
|
}
|
|
1013
1177
|
|
|
1014
|
-
function getUnsafeGitArgumentReason(
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1178
|
+
function getUnsafeGitArgumentReason(
|
|
1179
|
+
tokens: string[],
|
|
1180
|
+
parsed: { subcommand?: string; index: number },
|
|
1181
|
+
): string | undefined {
|
|
1182
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
1183
|
+
const token = tokens[index];
|
|
1184
|
+
const lowerToken = token.toLowerCase();
|
|
1185
|
+
if (token === "--no-index" || token.startsWith("--no-index=")) {
|
|
1186
|
+
return "Code Reviewer bash blocks git --no-index because it can inspect arbitrary filesystem paths outside the checkout.";
|
|
1187
|
+
}
|
|
1188
|
+
if (token === "--paginate" || token.startsWith("--paginate=")) {
|
|
1189
|
+
return "Code Reviewer bash blocks git --paginate because pagers can execute local utilities.";
|
|
1190
|
+
}
|
|
1191
|
+
if (token === "--open-files-in-pager" || token.startsWith("--open-files-in-pager=")) {
|
|
1192
|
+
return "Code Reviewer bash blocks git --open-files-in-pager because it can execute local utilities.";
|
|
1193
|
+
}
|
|
1194
|
+
if (token === "-O" || token.startsWith("-O")) {
|
|
1195
|
+
return "Code Reviewer bash blocks git -O because it can execute local utilities for some subcommands.";
|
|
1196
|
+
}
|
|
1197
|
+
if (token === "--ext-diff" || token.startsWith("--ext-diff=")) {
|
|
1198
|
+
return "Code Reviewer bash blocks git --ext-diff because it can execute external diff helpers.";
|
|
1199
|
+
}
|
|
1200
|
+
if (token === "--textconv" || token.startsWith("--textconv=")) {
|
|
1201
|
+
return "Code Reviewer bash blocks git --textconv because it can execute external text conversion filters.";
|
|
1202
|
+
}
|
|
1203
|
+
if (token === "--filters" || token.startsWith("--filters=")) {
|
|
1204
|
+
return "Code Reviewer bash blocks git --filters because it can execute clean/smudge filters from local config.";
|
|
1205
|
+
}
|
|
1206
|
+
if (token === "--show-signature" || lowerToken.startsWith("--show-signat")) {
|
|
1207
|
+
return "Code Reviewer bash blocks git signature verification flags because they can execute configured GPG helpers.";
|
|
1208
|
+
}
|
|
1209
|
+
if (token === "--help" || token.startsWith("--help=")) {
|
|
1210
|
+
return "Code Reviewer bash blocks git help output because it can execute configured help, man, or browser helpers.";
|
|
1211
|
+
}
|
|
1212
|
+
if (token.includes("%G") || /%\((?:[^)]*signature[^)]*)\)/i.test(token)) {
|
|
1213
|
+
return "Code Reviewer bash blocks git signature format atoms because they can execute configured GPG helpers.";
|
|
1214
|
+
}
|
|
1215
|
+
if (token === "--output" || token.startsWith("--output=")) {
|
|
1216
|
+
return "Code Reviewer bash blocks git output-writing flags.";
|
|
1217
|
+
}
|
|
1218
|
+
for (const { flag, blockedPrefixes, reason } of GIT_OPTIONS_REQUIRING_LOCAL_FILE) {
|
|
1219
|
+
if (isBlockedGitLocalFileOption(token, flag, blockedPrefixes)) return reason;
|
|
1220
|
+
}
|
|
1221
|
+
if (index > parsed.index) {
|
|
1222
|
+
for (const option of GIT_SUBCOMMAND_OPTIONS_REQUIRING_LOCAL_FILE[parsed.subcommand ?? ""] ??
|
|
1223
|
+
[]) {
|
|
1224
|
+
if (option.matches(token)) return option.reason;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
const pathReason = getUnsafeGitTokenPathReason(token);
|
|
1228
|
+
if (pathReason) return pathReason;
|
|
1229
|
+
}
|
|
1230
|
+
return undefined;
|
|
1063
1231
|
}
|
|
1064
1232
|
|
|
1065
1233
|
function buildSafeGitCommand(tokens: string[]): string {
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1234
|
+
const parsed = getGitSubcommand(tokens);
|
|
1235
|
+
const prefix = [
|
|
1236
|
+
"git",
|
|
1237
|
+
"--no-pager",
|
|
1238
|
+
"--no-optional-locks",
|
|
1239
|
+
...SAFE_GIT_CONFIG_OVERRIDES.flatMap((value) => ["-c", value]),
|
|
1240
|
+
];
|
|
1241
|
+
const subcommand = parsed.subcommand;
|
|
1242
|
+
if (!subcommand) return prefix.map(shellQuoteToken).join(" ");
|
|
1243
|
+
const args = tokens.slice(parsed.index + 1);
|
|
1244
|
+
const injectedFlags = (GIT_SUBCOMMAND_SAFE_FLAGS[subcommand] ?? []).filter(
|
|
1245
|
+
(flag) => !args.includes(flag),
|
|
1246
|
+
);
|
|
1247
|
+
return [...prefix, subcommand, ...injectedFlags, ...args].map(shellQuoteToken).join(" ");
|
|
1073
1248
|
}
|
|
1074
1249
|
|
|
1075
1250
|
function getBlockedGitReason(tokens: string[]): string | undefined {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1251
|
+
if (getExecutableName(tokens[0] ?? "") !== "git") return undefined;
|
|
1252
|
+
const parsed = getGitSubcommand(tokens);
|
|
1253
|
+
if (parsed.reason) return parsed.reason;
|
|
1254
|
+
if (parsed.subcommand === "help") {
|
|
1255
|
+
return "Code Reviewer bash blocks git help because it can execute configured help, man, or browser helpers.";
|
|
1256
|
+
}
|
|
1257
|
+
const argumentReason = getUnsafeGitArgumentReason(tokens, parsed);
|
|
1258
|
+
if (argumentReason) return argumentReason;
|
|
1259
|
+
if (!parsed.subcommand) return undefined;
|
|
1260
|
+
if (parsed.subcommand === "branch") {
|
|
1261
|
+
if (!isSafeGitBranchCommand(tokens, parsed.index)) {
|
|
1262
|
+
return "Code Reviewer bash allows git branch only for read-only listing/show-current/contains/merged queries.";
|
|
1263
|
+
}
|
|
1264
|
+
return undefined;
|
|
1265
|
+
}
|
|
1266
|
+
if (parsed.subcommand === "remote") {
|
|
1267
|
+
if (!isSafeGitRemoteCommand(tokens, parsed.index)) {
|
|
1268
|
+
return "Code Reviewer bash allows git remote only for read-only list or get-url queries.";
|
|
1269
|
+
}
|
|
1270
|
+
return undefined;
|
|
1271
|
+
}
|
|
1272
|
+
if (!READ_ONLY_GIT_SUBCOMMANDS.has(parsed.subcommand)) {
|
|
1273
|
+
return `Code Reviewer bash blocks git ${parsed.subcommand}; only known read-only git subcommands are allowed.`;
|
|
1274
|
+
}
|
|
1275
|
+
return undefined;
|
|
1101
1276
|
}
|
|
1102
1277
|
|
|
1103
1278
|
function getGhCommand(tokens: string[]): { command?: string; subcommand?: string } {
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1279
|
+
let index = 1;
|
|
1280
|
+
while (index < tokens.length) {
|
|
1281
|
+
const token = tokens[index];
|
|
1282
|
+
if (!token.startsWith("-")) break;
|
|
1283
|
+
if (GH_GLOBAL_OPTIONS_WITH_VALUE.has(token)) {
|
|
1284
|
+
index += 2;
|
|
1285
|
+
continue;
|
|
1286
|
+
}
|
|
1287
|
+
index += 1;
|
|
1288
|
+
}
|
|
1289
|
+
return { command: tokens[index]?.toLowerCase(), subcommand: tokens[index + 1]?.toLowerCase() };
|
|
1115
1290
|
}
|
|
1116
1291
|
|
|
1117
1292
|
function normalizeFlagValue(value: string | undefined): string | undefined {
|
|
1118
|
-
|
|
1293
|
+
return value ? stripTokenQuotes(value).trim() : undefined;
|
|
1119
1294
|
}
|
|
1120
1295
|
|
|
1121
1296
|
function getBlockedGhApiReason(tokens: string[]): string | undefined {
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1297
|
+
const parsed = getGhCommand(tokens);
|
|
1298
|
+
if (parsed.command !== "api") return undefined;
|
|
1299
|
+
if (tokens.find((token) => token.toLowerCase() === "graphql")) {
|
|
1300
|
+
return "Code Reviewer bash blocks gh api graphql because it uses POST/body fields.";
|
|
1301
|
+
}
|
|
1302
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
1303
|
+
const token = tokens[index];
|
|
1304
|
+
const lowerToken = token.toLowerCase();
|
|
1305
|
+
if (
|
|
1306
|
+
token === "-f" ||
|
|
1307
|
+
token === "-F" ||
|
|
1308
|
+
token === "--field" ||
|
|
1309
|
+
token === "--raw-field" ||
|
|
1310
|
+
token === "--input" ||
|
|
1311
|
+
lowerToken.startsWith("-f") ||
|
|
1312
|
+
lowerToken.startsWith("--field=") ||
|
|
1313
|
+
lowerToken.startsWith("--raw-field=") ||
|
|
1314
|
+
lowerToken.startsWith("--input=")
|
|
1315
|
+
) {
|
|
1316
|
+
return "Code Reviewer bash allows read-only gh api calls only; request fields and input files are blocked.";
|
|
1317
|
+
}
|
|
1318
|
+
if (token === "--cache" || lowerToken.startsWith("--cache=")) {
|
|
1319
|
+
return "Code Reviewer bash blocks gh api --cache because it writes local cache files.";
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
let method: string | undefined;
|
|
1323
|
+
if (lowerToken === "-x" || lowerToken === "--method")
|
|
1324
|
+
method = normalizeFlagValue(tokens[index + 1]);
|
|
1325
|
+
else if (lowerToken.startsWith("-x") && token.length > 2)
|
|
1326
|
+
method = normalizeFlagValue(token.slice(2).replace(/^=/, ""));
|
|
1327
|
+
else if (lowerToken.startsWith("--method="))
|
|
1328
|
+
method = normalizeFlagValue(token.slice("--method=".length));
|
|
1329
|
+
if (method && MUTATING_GH_API_METHODS.has(method.toUpperCase())) {
|
|
1330
|
+
return "Code Reviewer bash allows read-only gh api calls only; mutating methods are blocked.";
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return undefined;
|
|
1156
1334
|
}
|
|
1157
1335
|
|
|
1158
1336
|
function isReadOnlyGhCommand(command: string | undefined, subcommand: string | undefined): boolean {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1337
|
+
if (!command) return true;
|
|
1338
|
+
if (command === "api") return true;
|
|
1339
|
+
if (command === "search") return true;
|
|
1340
|
+
if (command === "repo") return subcommand === "view" || subcommand === "list";
|
|
1341
|
+
if (command === "pr")
|
|
1342
|
+
return (
|
|
1343
|
+
subcommand === "view" ||
|
|
1344
|
+
subcommand === "list" ||
|
|
1345
|
+
subcommand === "diff" ||
|
|
1346
|
+
subcommand === "status" ||
|
|
1347
|
+
subcommand === "checks"
|
|
1348
|
+
);
|
|
1349
|
+
if (command === "issue")
|
|
1350
|
+
return subcommand === "view" || subcommand === "list" || subcommand === "status";
|
|
1351
|
+
if (command === "release") return subcommand === "view" || subcommand === "list";
|
|
1352
|
+
if (command === "run") return subcommand === "view" || subcommand === "list";
|
|
1353
|
+
if (command === "workflow") return subcommand === "view" || subcommand === "list";
|
|
1354
|
+
if (command === "label") return subcommand === "list";
|
|
1355
|
+
if (command === "milestone") return subcommand === "list";
|
|
1356
|
+
return false;
|
|
1171
1357
|
}
|
|
1172
1358
|
|
|
1173
1359
|
function getBlockedGhReason(command: string): string | undefined {
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1360
|
+
const tokens = tokenizeSegment(command);
|
|
1361
|
+
if (getExecutableName(tokens[0] ?? "") !== "gh") return undefined;
|
|
1362
|
+
if (/\bgh\s+auth\s+(?:login|logout|refresh|setup-git|token)\b/i.test(command)) {
|
|
1363
|
+
return "Code Reviewer bash blocks gh auth commands and token inspection.";
|
|
1364
|
+
}
|
|
1365
|
+
if (
|
|
1366
|
+
tokens.some((token) => {
|
|
1367
|
+
const lowerToken = token.toLowerCase();
|
|
1368
|
+
return (
|
|
1369
|
+
lowerToken === "--web" ||
|
|
1370
|
+
lowerToken === "-w" ||
|
|
1371
|
+
lowerToken.startsWith("--web=") ||
|
|
1372
|
+
lowerToken.startsWith("-w=")
|
|
1373
|
+
);
|
|
1374
|
+
})
|
|
1375
|
+
) {
|
|
1376
|
+
return "Code Reviewer bash blocks gh --web because it can launch external browser helpers.";
|
|
1377
|
+
}
|
|
1378
|
+
const apiReason = getBlockedGhApiReason(tokens);
|
|
1379
|
+
if (apiReason) return apiReason;
|
|
1380
|
+
if (
|
|
1381
|
+
/\bgh\s+(?:repo\s+(?:archive|clone|create|delete|edit|fork|rename|sync)|pr\s+(?:checkout|close|comment|create|edit|lock|merge|ready|reopen|review|unlock)|issue\s+(?:close|comment|create|delete|edit|lock|reopen|transfer|unlock)|release\s+(?:create|delete|edit|upload)|workflow\s+run|run\s+(?:cancel|delete|rerun)|gist\s+(?:create|delete|edit))\b/i.test(
|
|
1382
|
+
command,
|
|
1383
|
+
)
|
|
1384
|
+
) {
|
|
1385
|
+
return "Code Reviewer bash blocks mutating gh commands.";
|
|
1386
|
+
}
|
|
1387
|
+
const parsed = getGhCommand(tokens);
|
|
1388
|
+
if (!isReadOnlyGhCommand(parsed.command, parsed.subcommand)) {
|
|
1389
|
+
return `Code Reviewer bash blocks gh ${[parsed.command, parsed.subcommand].filter(Boolean).join(" ")}; only known read-only gh commands are allowed.`;
|
|
1390
|
+
}
|
|
1391
|
+
return undefined;
|
|
1197
1392
|
}
|
|
1198
1393
|
|
|
1199
1394
|
function getBlockedBashReason(command: string): string | undefined {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1395
|
+
const trimmed = command.trim();
|
|
1396
|
+
if (!trimmed) return "Code Reviewer bash requires a non-empty command.";
|
|
1397
|
+
if (/`|\$\(/.test(trimmed)) return "Code Reviewer bash blocks command substitution.";
|
|
1398
|
+
const syntaxReason = getShellSyntaxReason(trimmed);
|
|
1399
|
+
if (syntaxReason) return syntaxReason;
|
|
1400
|
+
|
|
1401
|
+
const tokens = tokenizeSegment(trimmed);
|
|
1402
|
+
if (tokens.length === 0) return "Code Reviewer bash requires a non-empty command.";
|
|
1403
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[0]))
|
|
1404
|
+
return "Code Reviewer bash blocks inline environment assignment.";
|
|
1405
|
+
if (/[\\/]/.test(tokens[0])) {
|
|
1406
|
+
return "Code Reviewer bash requires direct git, gh, or pwd invocation without path-qualified executables.";
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
const executable = getExecutableName(tokens[0]);
|
|
1410
|
+
if (!READ_ONLY_BASH_COMMANDS.has(executable)) {
|
|
1411
|
+
return `Code Reviewer bash blocks ${executable || "this command"}; use built-in read/grep/find/ls for local files or read-only git/gh inspection commands.`;
|
|
1412
|
+
}
|
|
1413
|
+
if (executable === "pwd") return getBlockedPwdReason(tokens);
|
|
1414
|
+
if (executable === "git") return getBlockedGitReason(tokens);
|
|
1415
|
+
if (executable === "gh") return getBlockedGhReason(trimmed);
|
|
1416
|
+
return undefined;
|
|
1221
1417
|
}
|
|
1222
1418
|
|
|
1223
|
-
function createCodeReviewerRuntimeGuardExtension(options: {
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1419
|
+
function createCodeReviewerRuntimeGuardExtension(options: {
|
|
1420
|
+
cwd: string;
|
|
1421
|
+
maxTurns: number;
|
|
1422
|
+
}): ExtensionFactory {
|
|
1423
|
+
return (pi) => {
|
|
1424
|
+
let currentTurn = 0;
|
|
1425
|
+
|
|
1426
|
+
pi.on("turn_start", async (event) => {
|
|
1427
|
+
currentTurn = event.turnIndex;
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
pi.on("tool_call", async (event) => {
|
|
1431
|
+
if (!READ_ONLY_TOOL_NAMES.has(event.toolName)) {
|
|
1432
|
+
return {
|
|
1433
|
+
block: true,
|
|
1434
|
+
reason: `code_reviewer exposes read-only tools only; ${event.toolName} is not allowed.`,
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
if (currentTurn >= options.maxTurns - 1) {
|
|
1439
|
+
return {
|
|
1440
|
+
block: true,
|
|
1441
|
+
reason: `Tool use is disabled on final code_reviewer turn ${options.maxTurns}/${options.maxTurns}. Answer now with the evidence already gathered.`,
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
if (event.toolName === "read") {
|
|
1446
|
+
const reason = await assertToolPathInsideCwd(
|
|
1447
|
+
options.cwd,
|
|
1448
|
+
(event.input as { path?: unknown }).path,
|
|
1449
|
+
"read",
|
|
1450
|
+
);
|
|
1451
|
+
if (reason) return { block: true, reason };
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
if (event.toolName === "grep" || event.toolName === "find" || event.toolName === "ls") {
|
|
1455
|
+
const reason = await assertToolPathInsideCwd(
|
|
1456
|
+
options.cwd,
|
|
1457
|
+
(event.input as { path?: unknown }).path,
|
|
1458
|
+
event.toolName,
|
|
1459
|
+
);
|
|
1460
|
+
if (reason) return { block: true, reason };
|
|
1461
|
+
if (event.toolName === "grep") {
|
|
1462
|
+
const globReason = getUnsafePathPatternReason(
|
|
1463
|
+
(event.input as { glob?: unknown }).glob,
|
|
1464
|
+
"grep glob",
|
|
1465
|
+
);
|
|
1466
|
+
if (globReason) return { block: true, reason: globReason };
|
|
1467
|
+
}
|
|
1468
|
+
if (event.toolName === "find") {
|
|
1469
|
+
const patternReason = getUnsafePathPatternReason(
|
|
1470
|
+
(event.input as { pattern?: unknown }).pattern,
|
|
1471
|
+
"find pattern",
|
|
1472
|
+
);
|
|
1473
|
+
if (patternReason) return { block: true, reason: patternReason };
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
if (event.toolName === "bash") {
|
|
1478
|
+
const input = event.input as { command?: unknown; timeout?: unknown };
|
|
1479
|
+
if (typeof input.timeout !== "number") input.timeout = DEFAULT_BASH_TIMEOUT_SECONDS;
|
|
1480
|
+
const command = typeof input.command === "string" ? input.command : "";
|
|
1481
|
+
const reason = getBlockedBashReason(command);
|
|
1482
|
+
if (reason) return { block: true, reason };
|
|
1483
|
+
const tokens = tokenizeSegment(command.trim());
|
|
1484
|
+
if (getExecutableName(tokens[0] ?? "") === "git")
|
|
1485
|
+
input.command = buildSafeGitCommand(tokens);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
return undefined;
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
pi.on("tool_result", async (event) => ({
|
|
1492
|
+
content: [
|
|
1493
|
+
...(event.content ?? []),
|
|
1494
|
+
{
|
|
1495
|
+
type: "text",
|
|
1496
|
+
text: `\n\n[code_reviewer turn budget] turn ${Math.min(currentTurn + 1, options.maxTurns)}/${options.maxTurns}`,
|
|
1497
|
+
},
|
|
1498
|
+
],
|
|
1499
|
+
}));
|
|
1500
|
+
};
|
|
1284
1501
|
}
|
|
1285
1502
|
|
|
1286
|
-
function buildSystemPrompt(options: {
|
|
1287
|
-
|
|
1503
|
+
function buildSystemPrompt(options: {
|
|
1504
|
+
cwd: string;
|
|
1505
|
+
maxTurns: number;
|
|
1506
|
+
maxRunSeconds: number;
|
|
1507
|
+
}): string {
|
|
1508
|
+
return `You are Code Reviewer, an isolated read-only review agent running inside the code-reviewer package. Review the proposed change against the stated task and the local checkout, then return a concise evidence-based review report.\n\nWorking directory: ${options.cwd}\nTurn budget: ${options.maxTurns} turns total, including your final answer.\nWall-clock budget: ${options.maxRunSeconds} seconds.\n\nAvailable tools are intended to be read-only: read, grep, find, ls, and bash. Use the built-in read, grep, find, and ls tools for local file inspection. Use bash only for a single read-only git, gh, or pwd invocation, such as git status/diff/show/log/blame, gh pr view/diff/list/status/checks, gh api GET calls, or pwd. A runtime guard blocks write/edit tools, shell pipelines/control operators, redirection, mutating git/gh commands, npm/publish commands, path traversal outside the checkout, and other filesystem mutation.\n\nReview priorities, in order:\n1. Ticket fit: does the change actually satisfy the requested task and stay in scope?\n2. Diff accuracy: do the files and edits match what the task claims changed?\n3. Correctness: could the change fail, regress behavior, mishandle errors, or break edge cases?\n4. Security and safety: look for unsafe command use, path handling issues, secret exposure, injection risks, destructive behavior, and policy violations.\n5. Simplicity and maintainability: call out unnecessary complexity, unclear behavior, or avoidable duplication.\n6. Tests and validation: check whether meaningful verification exists and whether gaps matter for this task.\n\nNon-negotiable constraints:\n- Never implement changes. Never edit, write, move, delete, format, install, commit, checkout, reset, clean, push, publish, or mutate GitHub.\n- Treat the supplied task and diff/context as claims to verify, not as facts. Cite file paths, line ranges, git diff/status output, or supplied diff excerpts as evidence.\n- Prefer the built-in file tools over bash when local file inspection is enough.\n- If evidence is missing, stale, or contradictory, say so plainly.\n- Keep the final review concise and actionable. Report the most important findings first; do not pad with non-findings.\n\nOutput format, exact order:\n## Verdict\nOne short paragraph with the overall review outcome.\n\n## Findings\nUse bullets ordered by severity. For each finding include: severity ('blocker', 'major', or 'minor'), a short title, evidence, and why it matters. If there are no findings, write '- none'.\n\n## Validation\nList the read-only checks you performed, including key files inspected, notable git/gh commands, and any uncertainty that limited the review, or '- none'.\n\n## Scope check\nState whether the change appears to match the requested task and note any missing or extra scope.`;
|
|
1288
1509
|
}
|
|
1289
1510
|
|
|
1290
|
-
function buildUserPrompt(
|
|
1291
|
-
|
|
1511
|
+
function buildUserPrompt(
|
|
1512
|
+
input: { task: string; diff?: string; context?: string },
|
|
1513
|
+
cwd: string,
|
|
1514
|
+
): string {
|
|
1515
|
+
return `Task to review:\n${input.task}\n\nLocal checkout: ${cwd}\n\nOptional diff or patch:\n${input.diff?.trim() ? input.diff : "(not provided)"}\n\nOptional caller context:\n${input.context?.trim() ? input.context : "(not provided)"}\n\nInspect only as much as needed to review the change against the task. Do not implement anything. Respond using the required review format.`;
|
|
1292
1516
|
}
|
|
1293
1517
|
|
|
1294
1518
|
function formatToolCall(call: ToolCall): string {
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1519
|
+
const args =
|
|
1520
|
+
call.args && typeof call.args === "object" ? (call.args as Record<string, unknown>) : {};
|
|
1521
|
+
if (call.name === "read") {
|
|
1522
|
+
const readPath = typeof args.path === "string" ? args.path : "";
|
|
1523
|
+
const offset = typeof args.offset === "number" ? args.offset : undefined;
|
|
1524
|
+
const limit = typeof args.limit === "number" ? args.limit : undefined;
|
|
1525
|
+
const range =
|
|
1526
|
+
offset || limit ? `:${offset ?? 1}${limit ? `-${(offset ?? 1) + limit - 1}` : ""}` : "";
|
|
1527
|
+
return `read ${readPath}${range}`.trim();
|
|
1528
|
+
}
|
|
1529
|
+
if (call.name === "grep") {
|
|
1530
|
+
const pattern = typeof args.pattern === "string" ? args.pattern : "";
|
|
1531
|
+
const grepPath = typeof args.path === "string" ? args.path : ".";
|
|
1532
|
+
return `grep ${pattern.slice(0, 50)} ${grepPath}`.trim();
|
|
1533
|
+
}
|
|
1534
|
+
if (call.name === "find") {
|
|
1535
|
+
const pattern = typeof args.pattern === "string" ? args.pattern : "";
|
|
1536
|
+
const findPath = typeof args.path === "string" ? args.path : ".";
|
|
1537
|
+
return `find ${pattern.slice(0, 50)} ${findPath}`.trim();
|
|
1538
|
+
}
|
|
1539
|
+
if (call.name === "ls") return `ls ${typeof args.path === "string" ? args.path : "."}`.trim();
|
|
1540
|
+
if (call.name === "bash") {
|
|
1541
|
+
const command = typeof args.command === "string" ? args.command : "";
|
|
1542
|
+
return `bash ${command.slice(0, 120)}`.trim();
|
|
1543
|
+
}
|
|
1544
|
+
return call.name;
|
|
1319
1545
|
}
|
|
1320
1546
|
|
|
1321
1547
|
function formatDuration(ms: number): string {
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1548
|
+
if (ms < 1000) return `${ms}ms`;
|
|
1549
|
+
const seconds = Math.round(ms / 100) / 10;
|
|
1550
|
+
if (seconds < 60) return `${seconds}s`;
|
|
1551
|
+
const minutes = Math.floor(seconds / 60);
|
|
1552
|
+
const remainingSeconds = Math.round((seconds % 60) * 10) / 10;
|
|
1553
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
1328
1554
|
}
|
|
1329
1555
|
|
|
1330
1556
|
function appendRunDetails(report: string, details: ReviewDetails): string {
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1557
|
+
const trimmed = report.trim();
|
|
1558
|
+
const toolSummary =
|
|
1559
|
+
details.toolCalls.length > 0 ? details.toolCalls.map(formatToolCall).join("; ") : "none";
|
|
1560
|
+
const duration = formatDuration((details.endedAt ?? Date.now()) - details.startedAt);
|
|
1561
|
+
const modelSummary = details.modelRef ? `model ${details.modelRef}` : "model unknown";
|
|
1562
|
+
const thinkingSummary = details.effectiveThinkingLevel
|
|
1563
|
+
? `thinking ${details.effectiveThinkingLevel}${details.thinkingLevelNote ? ` (${details.thinkingLevelNote})` : ""}`
|
|
1564
|
+
: "thinking unknown";
|
|
1565
|
+
const suffix = `\n\n---\nRun details: ${modelSummary}; ${thinkingSummary}; ${details.turns} turn(s); ${details.toolCalls.length} tool call(s); duration ${duration}; cwd ${details.cwd}; tools ${toolSummary}.`;
|
|
1566
|
+
if (!trimmed)
|
|
1567
|
+
return `## Verdict\nNo review output was produced.\n\n## Findings\n- major — Missing review output. Evidence: the isolated code_reviewer session returned no assistant text. Why it matters: the review could not be completed.\n\n## Validation\n- none\n\n## Scope check\nReview could not be completed.\n\n## Run details\n- ${suffix.trim()}`;
|
|
1568
|
+
return `${trimmed}${suffix}`;
|
|
1341
1569
|
}
|
|
1342
1570
|
|
|
1343
1571
|
export const __test__ = {
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1572
|
+
assertToolPathInsideCwd,
|
|
1573
|
+
buildSystemPrompt,
|
|
1574
|
+
buildUserPrompt,
|
|
1575
|
+
createCodeReviewerRuntimeGuardExtension,
|
|
1576
|
+
buildSafeGitCommand,
|
|
1577
|
+
formatToolCall,
|
|
1578
|
+
getBlockedBashReason,
|
|
1579
|
+
isModelAvailabilityError,
|
|
1580
|
+
normalizeThinkingLevel,
|
|
1581
|
+
resolveThinkingLevel,
|
|
1582
|
+
selectCodeReviewerModel,
|
|
1583
|
+
aggregateAssistantUsage,
|
|
1584
|
+
addAssistantMessageUsage,
|
|
1585
|
+
addSessionEventUsage,
|
|
1586
|
+
inspectFinalAssistant,
|
|
1587
|
+
classifyRunFailure,
|
|
1360
1588
|
};
|
|
1361
1589
|
|
|
1362
1590
|
export default function codeReviewerExtension(pi: ExtensionAPI) {
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1591
|
+
pi.on("tool_result", async (event) => {
|
|
1592
|
+
if (event.toolName !== "code_reviewer") return undefined;
|
|
1593
|
+
const details = event.details as ReviewDetails | undefined;
|
|
1594
|
+
if (details?.status !== "error") return undefined;
|
|
1595
|
+
return { isError: true };
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
pi.registerTool({
|
|
1599
|
+
name: "code_reviewer",
|
|
1600
|
+
label: "Code reviewer",
|
|
1601
|
+
description:
|
|
1602
|
+
"Read-only isolated reviewer that checks a proposed change for ticket fit, diff accuracy, correctness, security, simplicity, and validation gaps without implementing anything.",
|
|
1603
|
+
promptSnippet:
|
|
1604
|
+
"Run an isolated read-only code review against the local checkout and optional diff; returns concise findings with evidence and run details.",
|
|
1605
|
+
promptGuidelines: [
|
|
1606
|
+
"Use code_reviewer when you want an independent read-only review of a proposed change or task implementation.",
|
|
1607
|
+
"Provide the requested task and any relevant diff/context. Do not use code_reviewer to implement or edit files.",
|
|
1608
|
+
],
|
|
1609
|
+
parameters: CodeReviewerParams,
|
|
1610
|
+
|
|
1611
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
1612
|
+
const task = typeof params.task === "string" ? params.task.trim() : "";
|
|
1613
|
+
if (!task) throw new Error("Invalid parameters: expected task to be a non-empty string.");
|
|
1614
|
+
|
|
1615
|
+
const input = {
|
|
1616
|
+
task,
|
|
1617
|
+
diff: typeof params.diff === "string" ? params.diff : undefined,
|
|
1618
|
+
context: typeof params.context === "string" ? params.context : undefined,
|
|
1619
|
+
};
|
|
1620
|
+
const cwd = path.resolve(ctx.cwd);
|
|
1621
|
+
const details: ReviewDetails = {
|
|
1622
|
+
status: "running",
|
|
1623
|
+
cwd,
|
|
1624
|
+
task,
|
|
1625
|
+
turns: 0,
|
|
1626
|
+
toolCalls: [],
|
|
1627
|
+
startedAt: Date.now(),
|
|
1628
|
+
};
|
|
1629
|
+
|
|
1630
|
+
let lastContent = "(reviewing change...)";
|
|
1631
|
+
let session: AgentSession | undefined;
|
|
1632
|
+
let unsubscribe: (() => void) | undefined;
|
|
1633
|
+
let runTimeout: NodeJS.Timeout | undefined;
|
|
1634
|
+
let abortListenerAdded = false;
|
|
1635
|
+
let callerAborted = Boolean(signal?.aborted);
|
|
1636
|
+
const usage = createEmptyUsage();
|
|
1637
|
+
|
|
1638
|
+
const emit = () => {
|
|
1639
|
+
onUpdate?.({ content: [{ type: "text", text: lastContent }], details });
|
|
1640
|
+
};
|
|
1641
|
+
|
|
1642
|
+
const abortForCleanup = () => {
|
|
1643
|
+
void session?.abort();
|
|
1644
|
+
};
|
|
1645
|
+
|
|
1646
|
+
const abortFromCaller = () => {
|
|
1647
|
+
callerAborted = true;
|
|
1648
|
+
details.status = "aborted";
|
|
1649
|
+
details.endedAt = Date.now();
|
|
1650
|
+
lastContent = "Aborted";
|
|
1651
|
+
emit();
|
|
1652
|
+
abortForCleanup();
|
|
1653
|
+
};
|
|
1654
|
+
|
|
1655
|
+
if (signal?.aborted) abortFromCaller();
|
|
1656
|
+
if (signal && !signal.aborted) {
|
|
1657
|
+
signal.addEventListener("abort", abortFromCaller);
|
|
1658
|
+
abortListenerAdded = true;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
try {
|
|
1662
|
+
emit();
|
|
1663
|
+
|
|
1664
|
+
const isolatedSettingsManager = SettingsManager.inMemory({});
|
|
1665
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
1666
|
+
cwd,
|
|
1667
|
+
agentDir: getAgentDir(),
|
|
1668
|
+
settingsManager: isolatedSettingsManager,
|
|
1669
|
+
noExtensions: true,
|
|
1670
|
+
noSkills: true,
|
|
1671
|
+
noPromptTemplates: true,
|
|
1672
|
+
noThemes: true,
|
|
1673
|
+
noContextFiles: true,
|
|
1674
|
+
extensionFactories: [
|
|
1675
|
+
createCodeReviewerRuntimeGuardExtension({ cwd, maxTurns: MAX_TURNS }),
|
|
1676
|
+
],
|
|
1677
|
+
systemPromptOverride: () =>
|
|
1678
|
+
buildSystemPrompt({
|
|
1679
|
+
cwd,
|
|
1680
|
+
maxTurns: MAX_TURNS,
|
|
1681
|
+
maxRunSeconds: Math.round(MAX_RUN_MS / 1000),
|
|
1682
|
+
}),
|
|
1683
|
+
appendSystemPromptOverride: () => [],
|
|
1684
|
+
skillsOverride: () => ({ skills: [], diagnostics: [] }),
|
|
1685
|
+
promptsOverride: () => ({ prompts: [], diagnostics: [] }),
|
|
1686
|
+
themesOverride: () => ({ themes: [], diagnostics: [] }),
|
|
1687
|
+
agentsFilesOverride: () => ({ agentsFiles: [] }),
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
await resourceLoader.reload();
|
|
1691
|
+
|
|
1692
|
+
const modelSelection = await selectCodeReviewerModel(ctx);
|
|
1693
|
+
if (!modelSelection.ok) throw new Error(modelSelection.error);
|
|
1694
|
+
|
|
1695
|
+
let answer = "";
|
|
1696
|
+
let lastAttemptError: string | undefined;
|
|
1697
|
+
const activeThinkingLevel = normalizeThinkingLevel(pi.getThinkingLevel());
|
|
1698
|
+
for (let index = 0; index < modelSelection.ordered.length; index += 1) {
|
|
1699
|
+
const candidate = modelSelection.ordered[index];
|
|
1700
|
+
const thinking = resolveThinkingLevel(candidate, activeThinkingLevel);
|
|
1701
|
+
applyModelAttemptDetails(details, candidate, thinking);
|
|
1702
|
+
let attemptSession: AgentSession | undefined;
|
|
1703
|
+
let attemptUnsubscribe: (() => void) | undefined;
|
|
1704
|
+
let succeeded = false;
|
|
1705
|
+
lastContent = "(reviewing change...)";
|
|
1706
|
+
try {
|
|
1707
|
+
const created = await createAgentSession({
|
|
1708
|
+
cwd,
|
|
1709
|
+
...getModelRuntimeOption(ctx),
|
|
1710
|
+
resourceLoader,
|
|
1711
|
+
settingsManager: isolatedSettingsManager,
|
|
1712
|
+
sessionManager: SessionManager.inMemory(cwd),
|
|
1713
|
+
model: candidate as CreateAgentSessionModel,
|
|
1714
|
+
thinkingLevel: thinking.effective,
|
|
1715
|
+
tools: ["read", "grep", "find", "ls", "bash"],
|
|
1716
|
+
});
|
|
1717
|
+
attemptSession = created.session;
|
|
1718
|
+
session = attemptSession;
|
|
1719
|
+
attemptUnsubscribe = attemptSession.subscribe((event) => {
|
|
1720
|
+
addSessionEventUsage(usage, event);
|
|
1721
|
+
switch (event.type) {
|
|
1722
|
+
case "turn_end":
|
|
1723
|
+
details.turns += 1;
|
|
1724
|
+
emit();
|
|
1725
|
+
break;
|
|
1726
|
+
case "tool_execution_start":
|
|
1727
|
+
details.toolCalls.push({
|
|
1728
|
+
id: event.toolCallId,
|
|
1729
|
+
name: event.toolName,
|
|
1730
|
+
args: event.args,
|
|
1731
|
+
startedAt: Date.now(),
|
|
1732
|
+
});
|
|
1733
|
+
if (details.toolCalls.length > MAX_TOOL_CALLS_TO_KEEP) {
|
|
1734
|
+
details.toolCalls.splice(0, details.toolCalls.length - MAX_TOOL_CALLS_TO_KEEP);
|
|
1735
|
+
}
|
|
1736
|
+
emit();
|
|
1737
|
+
break;
|
|
1738
|
+
case "tool_execution_end": {
|
|
1739
|
+
const call = details.toolCalls.find((item) => item.id === event.toolCallId);
|
|
1740
|
+
if (call) {
|
|
1741
|
+
call.endedAt = Date.now();
|
|
1742
|
+
call.isError = event.isError;
|
|
1743
|
+
}
|
|
1744
|
+
emit();
|
|
1745
|
+
break;
|
|
1746
|
+
}
|
|
1747
|
+
case "message_update":
|
|
1748
|
+
if (event.assistantMessageEvent?.type === "text_delta") {
|
|
1749
|
+
lastContent += event.assistantMessageEvent.delta ?? "";
|
|
1750
|
+
emit();
|
|
1751
|
+
}
|
|
1752
|
+
break;
|
|
1753
|
+
}
|
|
1754
|
+
});
|
|
1755
|
+
unsubscribe = attemptUnsubscribe;
|
|
1756
|
+
|
|
1757
|
+
if (!callerAborted) {
|
|
1758
|
+
const promptPromise = attemptSession.prompt(buildUserPrompt(input, cwd), {
|
|
1759
|
+
expandPromptTemplates: false,
|
|
1760
|
+
});
|
|
1761
|
+
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
|
1762
|
+
runTimeout = setTimeout(() => {
|
|
1763
|
+
abortForCleanup();
|
|
1764
|
+
reject(
|
|
1765
|
+
new Error(
|
|
1766
|
+
`code_reviewer timed out after ${Math.round(MAX_RUN_MS / 1000)} seconds.`,
|
|
1767
|
+
),
|
|
1768
|
+
);
|
|
1769
|
+
}, MAX_RUN_MS);
|
|
1770
|
+
});
|
|
1771
|
+
await Promise.race([promptPromise, timeoutPromise]);
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
const outcome = inspectFinalAssistant(attemptSession.state.messages);
|
|
1775
|
+
if (outcome.ok) {
|
|
1776
|
+
answer = outcome.answer;
|
|
1777
|
+
succeeded = true;
|
|
1778
|
+
break;
|
|
1779
|
+
}
|
|
1780
|
+
lastAttemptError = outcome.reason;
|
|
1781
|
+
const canFallBack =
|
|
1782
|
+
index < modelSelection.ordered.length - 1 &&
|
|
1783
|
+
outcome.stopReason === "error" &&
|
|
1784
|
+
isModelAvailabilityError(outcome.errorMessage || outcome.reason);
|
|
1785
|
+
if (!canFallBack) break;
|
|
1786
|
+
} catch (error) {
|
|
1787
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1788
|
+
lastAttemptError = message;
|
|
1789
|
+
const canFallBack =
|
|
1790
|
+
index < modelSelection.ordered.length - 1 && isModelAvailabilityError(message);
|
|
1791
|
+
if (!canFallBack) throw error;
|
|
1792
|
+
} finally {
|
|
1793
|
+
if (runTimeout) {
|
|
1794
|
+
clearTimeout(runTimeout);
|
|
1795
|
+
runTimeout = undefined;
|
|
1796
|
+
}
|
|
1797
|
+
if (attemptSession && !succeeded) {
|
|
1798
|
+
attemptUnsubscribe?.();
|
|
1799
|
+
if (unsubscribe === attemptUnsubscribe) unsubscribe = undefined;
|
|
1800
|
+
attemptSession.dispose();
|
|
1801
|
+
if (session === attemptSession) session = undefined;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
if (!answer) {
|
|
1806
|
+
if (callerAborted) {
|
|
1807
|
+
lastContent = "Aborted";
|
|
1808
|
+
details.status = "aborted";
|
|
1809
|
+
} else {
|
|
1810
|
+
throw new Error(
|
|
1811
|
+
lastAttemptError ||
|
|
1812
|
+
"the isolated code_reviewer session finished without a usable answer",
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
} else {
|
|
1816
|
+
lastContent = answer;
|
|
1817
|
+
details.status = "done";
|
|
1818
|
+
}
|
|
1819
|
+
details.endedAt = Date.now();
|
|
1820
|
+
const report = appendRunDetails(lastContent, details);
|
|
1821
|
+
lastContent = report;
|
|
1822
|
+
emit();
|
|
1823
|
+
return { content: [{ type: "text", text: report }], details, usage };
|
|
1824
|
+
} catch (error) {
|
|
1825
|
+
const failure = classifyRunFailure(error, callerAborted);
|
|
1826
|
+
const { message } = failure;
|
|
1827
|
+
details.status = failure.status;
|
|
1828
|
+
details.error = failure.error;
|
|
1829
|
+
details.endedAt = Date.now();
|
|
1830
|
+
lastContent = appendRunDetails(
|
|
1831
|
+
`## Verdict\nReview failed.\n\n## Findings\n- major — Review execution failed. Evidence: ${message}. Why it matters: the requested read-only review did not complete.\n\n## Validation\n- none\n\n## Scope check\nReview could not be completed.\n\n## Run details\n- failure: ${message}`,
|
|
1832
|
+
details,
|
|
1833
|
+
);
|
|
1834
|
+
emit();
|
|
1835
|
+
return { content: [{ type: "text", text: lastContent }], details, usage };
|
|
1836
|
+
} finally {
|
|
1837
|
+
if (runTimeout) clearTimeout(runTimeout);
|
|
1838
|
+
if (signal && abortListenerAdded) signal.removeEventListener("abort", abortFromCaller);
|
|
1839
|
+
unsubscribe?.();
|
|
1840
|
+
session?.dispose();
|
|
1841
|
+
}
|
|
1842
|
+
},
|
|
1843
|
+
});
|
|
1596
1844
|
}
|