@inceptionstack/pi-hard-no 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +287 -0
- package/architect.ts +128 -0
- package/changes.ts +404 -0
- package/commands.ts +635 -0
- package/context.ts +658 -0
- package/default-review-rules.md +150 -0
- package/git-roots.ts +94 -0
- package/helpers.ts +72 -0
- package/ignore.ts +105 -0
- package/index.ts +892 -0
- package/judge-skip-chain.ts +113 -0
- package/judge.ts +213 -0
- package/logger.ts +175 -0
- package/message-sender.ts +83 -0
- package/orchestrator.ts +521 -0
- package/package.json +55 -0
- package/prompt.ts +126 -0
- package/review-display.ts +571 -0
- package/reviewer.ts +433 -0
- package/scaffold.ts +120 -0
- package/session-kind.ts +139 -0
- package/settings.ts +332 -0
package/changes.ts
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* changes.ts ā Change detection and summary building
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const FILE_MODIFYING_TOOLS = ["write", "edit"];
|
|
6
|
+
|
|
7
|
+
const MAX_NON_GIT_FILE_SIZE = 100_000;
|
|
8
|
+
|
|
9
|
+
/** Common binary file extensions to skip */
|
|
10
|
+
const BINARY_EXTENSIONS = new Set([
|
|
11
|
+
".png",
|
|
12
|
+
".jpg",
|
|
13
|
+
".jpeg",
|
|
14
|
+
".gif",
|
|
15
|
+
".webp",
|
|
16
|
+
".ico",
|
|
17
|
+
".svg",
|
|
18
|
+
".woff",
|
|
19
|
+
".woff2",
|
|
20
|
+
".ttf",
|
|
21
|
+
".eot",
|
|
22
|
+
".otf",
|
|
23
|
+
".zip",
|
|
24
|
+
".gz",
|
|
25
|
+
".tar",
|
|
26
|
+
".bz2",
|
|
27
|
+
".7z",
|
|
28
|
+
".rar",
|
|
29
|
+
".exe",
|
|
30
|
+
".dll",
|
|
31
|
+
".so",
|
|
32
|
+
".dylib",
|
|
33
|
+
".bin",
|
|
34
|
+
".pdf",
|
|
35
|
+
".doc",
|
|
36
|
+
".docx",
|
|
37
|
+
".xls",
|
|
38
|
+
".xlsx",
|
|
39
|
+
".mp3",
|
|
40
|
+
".mp4",
|
|
41
|
+
".avi",
|
|
42
|
+
".mov",
|
|
43
|
+
".wav",
|
|
44
|
+
".pyc",
|
|
45
|
+
".class",
|
|
46
|
+
".o",
|
|
47
|
+
".obj",
|
|
48
|
+
".wasm",
|
|
49
|
+
".sqlite",
|
|
50
|
+
".db",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if a file path looks like a binary file.
|
|
55
|
+
*/
|
|
56
|
+
export function isBinaryPath(filePath: string): boolean {
|
|
57
|
+
const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
|
|
58
|
+
return BINARY_EXTENSIONS.has(ext);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface TrackedToolCall {
|
|
62
|
+
name: string;
|
|
63
|
+
input: any;
|
|
64
|
+
result?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Git subcommands that don't modify tracked files (VCS operations).
|
|
69
|
+
* Used to filter out bash calls like `git push`, `git commit`, etc.
|
|
70
|
+
* Note: merge/rebase/reset/checkout CAN modify files so they're NOT here.
|
|
71
|
+
*/
|
|
72
|
+
const GIT_READ_ONLY_SUBCOMMANDS = new Set([
|
|
73
|
+
"push",
|
|
74
|
+
"commit",
|
|
75
|
+
"add",
|
|
76
|
+
"log",
|
|
77
|
+
"status",
|
|
78
|
+
"diff",
|
|
79
|
+
"show",
|
|
80
|
+
"branch",
|
|
81
|
+
"tag",
|
|
82
|
+
"fetch",
|
|
83
|
+
"remote",
|
|
84
|
+
"stash",
|
|
85
|
+
"config",
|
|
86
|
+
"ls-files",
|
|
87
|
+
"ls-tree",
|
|
88
|
+
"rev-parse",
|
|
89
|
+
"rev-list",
|
|
90
|
+
"hash-object",
|
|
91
|
+
"blame",
|
|
92
|
+
"reflog",
|
|
93
|
+
"describe",
|
|
94
|
+
"shortlog",
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
/** Command roots that are treated as non-file-modifying regardless of args. */
|
|
98
|
+
const NON_MODIFYING_COMMAND_ROOTS = new Set([
|
|
99
|
+
"aws", // AWS CLI ā API calls
|
|
100
|
+
"curl", // HTTP requests
|
|
101
|
+
"wget", // though wget -O writes, treat as non-modifying per user request
|
|
102
|
+
"ping",
|
|
103
|
+
"dig",
|
|
104
|
+
"nslookup",
|
|
105
|
+
"whoami",
|
|
106
|
+
"hostname",
|
|
107
|
+
"date",
|
|
108
|
+
"uname",
|
|
109
|
+
"which",
|
|
110
|
+
"type",
|
|
111
|
+
"true",
|
|
112
|
+
"false",
|
|
113
|
+
"ps",
|
|
114
|
+
"df",
|
|
115
|
+
"du",
|
|
116
|
+
"free",
|
|
117
|
+
"uptime",
|
|
118
|
+
"env",
|
|
119
|
+
"printenv",
|
|
120
|
+
// Read-only / inspection commands
|
|
121
|
+
"cat",
|
|
122
|
+
"head",
|
|
123
|
+
"tail",
|
|
124
|
+
"less",
|
|
125
|
+
"more",
|
|
126
|
+
"wc",
|
|
127
|
+
"file",
|
|
128
|
+
"stat",
|
|
129
|
+
"readlink",
|
|
130
|
+
"realpath",
|
|
131
|
+
"basename",
|
|
132
|
+
"dirname",
|
|
133
|
+
"diff",
|
|
134
|
+
"md5sum",
|
|
135
|
+
"sha256sum",
|
|
136
|
+
"sha1sum",
|
|
137
|
+
"xxd",
|
|
138
|
+
"hexdump",
|
|
139
|
+
"strings",
|
|
140
|
+
"tree",
|
|
141
|
+
"jq",
|
|
142
|
+
"yq",
|
|
143
|
+
// Search / text processing (read-only)
|
|
144
|
+
"grep",
|
|
145
|
+
"egrep",
|
|
146
|
+
"fgrep",
|
|
147
|
+
"rg", // ripgrep
|
|
148
|
+
"ag", // the silver searcher
|
|
149
|
+
"ack",
|
|
150
|
+
"sort",
|
|
151
|
+
"uniq",
|
|
152
|
+
"cut",
|
|
153
|
+
"tr",
|
|
154
|
+
"awk",
|
|
155
|
+
"column",
|
|
156
|
+
"nl",
|
|
157
|
+
"test",
|
|
158
|
+
"[", // test alias
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
/** Single-part commands that are allowed in a chain without making it file-modifying. */
|
|
162
|
+
const ALLOWED_NAVIGATION = /^(cd|export|pwd|exit|return|true|false)\b/;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Patterns matching formatter/linter commands across languages.
|
|
166
|
+
* These modify files but only cosmetically ā not worth reviewing.
|
|
167
|
+
*/
|
|
168
|
+
const FORMATTER_PATTERNS: RegExp[] = [
|
|
169
|
+
// JavaScript / TypeScript
|
|
170
|
+
/\bprettier\b/,
|
|
171
|
+
/\beslint\b.*--fix/,
|
|
172
|
+
/\bbiome\b.*(?:format|check.*--fix|lint.*--fix)/,
|
|
173
|
+
/\bdprint\b.*fmt/,
|
|
174
|
+
// Python
|
|
175
|
+
/\bblack\b/,
|
|
176
|
+
/\bruff\b.*(?:format|check.*--fix)/,
|
|
177
|
+
/\bisort\b/,
|
|
178
|
+
/\bautopep8\b/,
|
|
179
|
+
/\byapf\b/,
|
|
180
|
+
// Go
|
|
181
|
+
/\bgofmt\b/,
|
|
182
|
+
/\bgoimports\b/,
|
|
183
|
+
/\bgolangci-lint\b.*--fix/,
|
|
184
|
+
// Rust
|
|
185
|
+
/\brustfmt\b/,
|
|
186
|
+
/\bcargo\s+fmt\b/,
|
|
187
|
+
/\bcargo\s+clippy\b.*--fix/,
|
|
188
|
+
// C / C++
|
|
189
|
+
/\bclang-format\b/,
|
|
190
|
+
// Java / Kotlin
|
|
191
|
+
/\bgoogle-java-format\b/,
|
|
192
|
+
/\bktlint\b.*--format/,
|
|
193
|
+
/\bktfmt\b/,
|
|
194
|
+
// Ruby
|
|
195
|
+
/\brubocop\b.*-[aA]/,
|
|
196
|
+
/\brubocop\b.*--auto-correct/,
|
|
197
|
+
// PHP
|
|
198
|
+
/\bphp-cs-fixer\b/,
|
|
199
|
+
/\bphpcbf\b/,
|
|
200
|
+
// Swift
|
|
201
|
+
/\bswiftformat\b/,
|
|
202
|
+
// Dart
|
|
203
|
+
/\bdart\s+format\b/,
|
|
204
|
+
// General
|
|
205
|
+
/\beditorconfig-checker\b/,
|
|
206
|
+
// npm/npx wrappers
|
|
207
|
+
/\bnpx\s+prettier\b/,
|
|
208
|
+
/\bnpx\s+eslint\b.*--fix/,
|
|
209
|
+
/\bnpm\s+run\s+(format|lint:fix|fix)\b/,
|
|
210
|
+
/\byarn\s+(format|lint:fix|fix)\b/,
|
|
211
|
+
/\bpnpm\s+(format|lint:fix|fix)\b/,
|
|
212
|
+
/\bbun\s+run\s+(format|lint:fix|fix)\b/,
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check if a bash command is a code formatter or linter auto-fix.
|
|
217
|
+
* These modify files but only cosmetically (whitespace, ordering, style).
|
|
218
|
+
*/
|
|
219
|
+
export function isFormatterCommand(command: string): boolean {
|
|
220
|
+
if (!command) return false;
|
|
221
|
+
return FORMATTER_PATTERNS.some((p) => p.test(command));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Check if an entire turn's tool calls are ONLY formatting/linting operations.
|
|
226
|
+
* Returns true if every file-modifying action is a known formatter command.
|
|
227
|
+
* Returns false if there are no tool calls, or any non-formatter modifications.
|
|
228
|
+
*/
|
|
229
|
+
export function isFormattingOnlyTurn(toolCalls: TrackedToolCall[]): boolean {
|
|
230
|
+
if (toolCalls.length === 0) return false;
|
|
231
|
+
|
|
232
|
+
let hasFormatterCall = false;
|
|
233
|
+
|
|
234
|
+
for (const tc of toolCalls) {
|
|
235
|
+
// write/edit tool calls are real modifications, not formatting
|
|
236
|
+
if (FILE_MODIFYING_TOOLS.includes(tc.name)) return false;
|
|
237
|
+
|
|
238
|
+
if (tc.name === "bash") {
|
|
239
|
+
const cmd = tc.input?.command ?? "";
|
|
240
|
+
if (isNonFileModifyingCommand(cmd)) continue; // skip non-modifying (git, curl, etc.)
|
|
241
|
+
if (isFormatterCommand(cmd)) {
|
|
242
|
+
hasFormatterCall = true;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
// Unknown bash command that could modify files ā not formatting-only
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return hasFormatterCall;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Check if a bash command part is a known non-file-modifying command.
|
|
255
|
+
*/
|
|
256
|
+
function isNonModifyingPart(part: string): boolean {
|
|
257
|
+
if (ALLOWED_NAVIGATION.test(part)) return true;
|
|
258
|
+
|
|
259
|
+
// Any output redirection to a file means the command modifies files
|
|
260
|
+
// Exclude only fd-to-fd redirects like 2>&1 (digit > & digit)
|
|
261
|
+
if (/>{1,2}/.test(part) && !/^[^>]*\d>&\d[^>]*$/.test(part)) {
|
|
262
|
+
// Has a > or >> that isn't purely a fd-to-fd redirect
|
|
263
|
+
const withoutFdRedirects = part.replace(/\d>&\d/g, "");
|
|
264
|
+
if (/>{1,2}/.test(withoutFdRedirects)) return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Git VCS read-only operations
|
|
268
|
+
const gitMatch = part.match(/^git(?:\s+-C\s+\S+)?\s+(\w[\w-]*)/);
|
|
269
|
+
if (gitMatch) return GIT_READ_ONLY_SUBCOMMANDS.has(gitMatch[1]);
|
|
270
|
+
|
|
271
|
+
// Generic non-modifying commands (aws, curl, etc.)
|
|
272
|
+
const rootMatch = part.match(/^(\w[\w-]*)/);
|
|
273
|
+
if (rootMatch && NON_MODIFYING_COMMAND_ROOTS.has(rootMatch[1])) return true;
|
|
274
|
+
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Check if a bash command has no file-modifying side effects.
|
|
280
|
+
* Examples: `git push`, `aws s3 ls`, `curl https://api`, `cd foo && git log`
|
|
281
|
+
*/
|
|
282
|
+
export function isNonFileModifyingCommand(command: string): boolean {
|
|
283
|
+
if (!command || typeof command !== "string") return false;
|
|
284
|
+
|
|
285
|
+
// Split on && || ; to handle command chains
|
|
286
|
+
const parts = command
|
|
287
|
+
.split(/&&|\|\||;/)
|
|
288
|
+
.map((p) => p.trim())
|
|
289
|
+
.filter(Boolean);
|
|
290
|
+
if (parts.length === 0) return false;
|
|
291
|
+
|
|
292
|
+
return parts.every(isNonModifyingPart);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Check if any tool calls include file modifications.
|
|
297
|
+
* Bash commands count UNLESS they are known non-modifying (git VCS ops, API calls, etc.)
|
|
298
|
+
*/
|
|
299
|
+
export function hasFileChanges(toolCalls: TrackedToolCall[]): boolean {
|
|
300
|
+
return toolCalls.some((tc) => {
|
|
301
|
+
if (FILE_MODIFYING_TOOLS.includes(tc.name)) return true;
|
|
302
|
+
if (tc.name === "bash") {
|
|
303
|
+
return !isNonFileModifyingCommand(tc.input?.command ?? "");
|
|
304
|
+
}
|
|
305
|
+
return false;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Check if a single tool call modifies files.
|
|
311
|
+
* Any bash command is conservatively treated as file-modifying.
|
|
312
|
+
* The reviewer checks git diff and skips if nothing actually changed.
|
|
313
|
+
*/
|
|
314
|
+
export function isFileModifyingTool(toolName: string): boolean {
|
|
315
|
+
return FILE_MODIFYING_TOOLS.includes(toolName) || toolName === "bash";
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Extract potential file paths from a bash command string.
|
|
320
|
+
* Best-effort: catches common patterns like redirections, common tools.
|
|
321
|
+
*/
|
|
322
|
+
export function extractPathsFromBashCommand(command: string): string[] {
|
|
323
|
+
const paths: string[] = [];
|
|
324
|
+
|
|
325
|
+
// Match quoted or unquoted file paths (absolute or relative)
|
|
326
|
+
// Patterns: > file, >> file, tool file, cp/mv src dst
|
|
327
|
+
const pathPattern = /(?:['"]([^'"]+\.\w+)['"]|\b(\/[\w./-]+\.\w+)\b|\b(\w[\w./-]*\.\w{1,10})\b)/g;
|
|
328
|
+
let match;
|
|
329
|
+
while ((match = pathPattern.exec(command)) !== null) {
|
|
330
|
+
const p = match[1] || match[2] || match[3];
|
|
331
|
+
if (p && !p.startsWith("-") && !isBinaryPath(p)) {
|
|
332
|
+
paths.push(p);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return [...new Set(paths)];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Collect all potential file paths from tracked tool calls.
|
|
341
|
+
* Includes explicit paths from write/edit and extracted paths from bash.
|
|
342
|
+
*/
|
|
343
|
+
export function collectModifiedPaths(toolCalls: TrackedToolCall[]): string[] {
|
|
344
|
+
const paths = new Set<string>();
|
|
345
|
+
|
|
346
|
+
for (const tc of toolCalls) {
|
|
347
|
+
if ((tc.name === "write" || tc.name === "edit") && tc.input?.path) {
|
|
348
|
+
paths.add(tc.input.path);
|
|
349
|
+
}
|
|
350
|
+
if (tc.name === "bash" && tc.input?.command) {
|
|
351
|
+
// Skip path extraction from non-file-modifying commands
|
|
352
|
+
// (commit messages, curl URLs, aws ARNs may look like file paths)
|
|
353
|
+
if (isNonFileModifyingCommand(tc.input.command)) continue;
|
|
354
|
+
for (const p of extractPathsFromBashCommand(tc.input.command)) {
|
|
355
|
+
paths.add(p);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return [...paths];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export { MAX_NON_GIT_FILE_SIZE };
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Check if the agent ran `git commit` during this turn.
|
|
367
|
+
* Used to gate last-commit fallback in auto-review content gathering.
|
|
368
|
+
*/
|
|
369
|
+
export function hasGitCommitCommand(toolCalls: TrackedToolCall[]): boolean {
|
|
370
|
+
return toolCalls.some((tc) => {
|
|
371
|
+
if (tc.name !== "bash") return false;
|
|
372
|
+
const cmd = String(tc.input?.command ?? "");
|
|
373
|
+
return /\bgit(?:\s+-C\s+\S+)?\s+commit\b/.test(cmd);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Build a human-readable summary of file changes from tool calls.
|
|
379
|
+
*/
|
|
380
|
+
export function buildChangeSummary(toolCalls: TrackedToolCall[]): string {
|
|
381
|
+
return toolCalls
|
|
382
|
+
.filter((tc) => FILE_MODIFYING_TOOLS.includes(tc.name) || tc.name === "bash")
|
|
383
|
+
.map((tc) => {
|
|
384
|
+
if (tc.name === "write") {
|
|
385
|
+
return `WROTE file: ${tc.input?.path}\n${String(tc.input?.content ?? "").slice(0, 3000)}`;
|
|
386
|
+
}
|
|
387
|
+
if (tc.name === "edit") {
|
|
388
|
+
const rawEdits = tc.input?.edits;
|
|
389
|
+
const edits = Array.isArray(rawEdits) ? rawEdits : [];
|
|
390
|
+
const editSummary = edits
|
|
391
|
+
.map(
|
|
392
|
+
(e: any, i: number) =>
|
|
393
|
+
` Edit ${i + 1}:\n OLD: ${String(e.oldText ?? "").slice(0, 500)}\n NEW: ${String(e.newText ?? "").slice(0, 500)}`,
|
|
394
|
+
)
|
|
395
|
+
.join("\n");
|
|
396
|
+
return `EDITED file: ${tc.input?.path}\n${editSummary}`;
|
|
397
|
+
}
|
|
398
|
+
if (tc.name === "bash") {
|
|
399
|
+
return `BASH: ${tc.input?.command}\nā ${String(tc.result ?? "").slice(0, 1000)}`;
|
|
400
|
+
}
|
|
401
|
+
return `${tc.name}: ${JSON.stringify(tc.input).slice(0, 500)}`;
|
|
402
|
+
})
|
|
403
|
+
.join("\n\n---\n\n");
|
|
404
|
+
}
|