@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/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
+ }