@probelabs/probe 0.6.0-rc291 → 0.6.0-rc293
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/bin/binaries/{probe-v0.6.0-rc291-aarch64-apple-darwin.tar.gz → probe-v0.6.0-rc293-aarch64-apple-darwin.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc291-aarch64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc293-aarch64-unknown-linux-musl.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc291-x86_64-apple-darwin.tar.gz → probe-v0.6.0-rc293-x86_64-apple-darwin.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc291-x86_64-pc-windows-msvc.zip → probe-v0.6.0-rc293-x86_64-pc-windows-msvc.zip} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc291-x86_64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc293-x86_64-unknown-linux-musl.tar.gz} +0 -0
- package/build/agent/dsl/environment.js +8 -1
- package/build/agent/dsl/runtime.js +9 -1
- package/build/agent/shared/prompts.js +33 -3
- package/build/tools/executePlan.js +1 -0
- package/build/tools/fileTracker.js +33 -17
- package/build/tools/fuzzyMatch.js +52 -1
- package/build/tools/lineEditHeuristics.js +11 -0
- package/build/tools/vercel.js +13 -10
- package/cjs/agent/ProbeAgent.cjs +125 -40
- package/cjs/index.cjs +132 -47
- package/package.json +1 -1
- package/src/agent/dsl/environment.js +8 -1
- package/src/agent/dsl/runtime.js +9 -1
- package/src/agent/shared/prompts.js +33 -3
- package/src/tools/executePlan.js +1 -0
- package/src/tools/fileTracker.js +33 -17
- package/src/tools/fuzzyMatch.js +52 -1
- package/src/tools/lineEditHeuristics.js +11 -0
- package/src/tools/vercel.js +13 -10
package/src/tools/fileTracker.js
CHANGED
|
@@ -12,9 +12,22 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { createHash } from 'crypto';
|
|
15
|
-
import { resolve, isAbsolute } from 'path';
|
|
15
|
+
import { resolve, isAbsolute, normalize } from 'path';
|
|
16
16
|
import { findSymbol } from './symbolEdit.js';
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Normalize a file path for consistent storage and lookup.
|
|
20
|
+
* Resolves '.', '..', double slashes, and ensures absolute paths are canonical.
|
|
21
|
+
* Does NOT resolve symlinks (that would be expensive and might fail for non-existent files).
|
|
22
|
+
* @param {string} filePath - Path to normalize
|
|
23
|
+
* @returns {string} Normalized path
|
|
24
|
+
*/
|
|
25
|
+
function normalizePath(filePath) {
|
|
26
|
+
if (!filePath) return filePath;
|
|
27
|
+
// resolve() handles '.', '..', double slashes, and makes the path absolute
|
|
28
|
+
return resolve(filePath);
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
/**
|
|
19
32
|
* Compute a SHA-256 content hash for a code block.
|
|
20
33
|
* Normalizes trailing whitespace per line for robustness against editor formatting.
|
|
@@ -106,10 +119,11 @@ export class FileTracker {
|
|
|
106
119
|
* @param {string} resolvedPath - Absolute path to the file
|
|
107
120
|
*/
|
|
108
121
|
markFileSeen(resolvedPath) {
|
|
109
|
-
|
|
110
|
-
this.
|
|
122
|
+
const normalized = normalizePath(resolvedPath);
|
|
123
|
+
this._seenFiles.add(normalized);
|
|
124
|
+
this._textEditCounts.set(normalized, 0);
|
|
111
125
|
if (this.debug) {
|
|
112
|
-
console.error(`[FileTracker] Marked as seen: ${
|
|
126
|
+
console.error(`[FileTracker] Marked as seen: ${normalized}`);
|
|
113
127
|
}
|
|
114
128
|
}
|
|
115
129
|
|
|
@@ -119,7 +133,7 @@ export class FileTracker {
|
|
|
119
133
|
* @returns {boolean}
|
|
120
134
|
*/
|
|
121
135
|
isFileSeen(resolvedPath) {
|
|
122
|
-
return this._seenFiles.has(resolvedPath);
|
|
136
|
+
return this._seenFiles.has(normalizePath(resolvedPath));
|
|
123
137
|
}
|
|
124
138
|
|
|
125
139
|
/**
|
|
@@ -132,7 +146,7 @@ export class FileTracker {
|
|
|
132
146
|
* @param {string} [source='extract'] - How the content was obtained
|
|
133
147
|
*/
|
|
134
148
|
trackSymbolContent(resolvedPath, symbolName, code, startLine, endLine, source = 'extract') {
|
|
135
|
-
const key = `${resolvedPath}#${symbolName}`;
|
|
149
|
+
const key = `${normalizePath(resolvedPath)}#${symbolName}`;
|
|
136
150
|
const contentHash = computeContentHash(code);
|
|
137
151
|
this._contentRecords.set(key, {
|
|
138
152
|
contentHash,
|
|
@@ -154,7 +168,7 @@ export class FileTracker {
|
|
|
154
168
|
* @returns {Object|null} The stored record or null
|
|
155
169
|
*/
|
|
156
170
|
getSymbolRecord(resolvedPath, symbolName) {
|
|
157
|
-
return this._contentRecords.get(`${resolvedPath}#${symbolName}`) || null;
|
|
171
|
+
return this._contentRecords.get(`${normalizePath(resolvedPath)}#${symbolName}`) || null;
|
|
158
172
|
}
|
|
159
173
|
|
|
160
174
|
/**
|
|
@@ -165,7 +179,7 @@ export class FileTracker {
|
|
|
165
179
|
* @returns {{ok: boolean, reason?: string, message?: string}}
|
|
166
180
|
*/
|
|
167
181
|
checkSymbolContent(resolvedPath, symbolName, currentCode) {
|
|
168
|
-
const key = `${resolvedPath}#${symbolName}`;
|
|
182
|
+
const key = `${normalizePath(resolvedPath)}#${symbolName}`;
|
|
169
183
|
const record = this._contentRecords.get(key);
|
|
170
184
|
|
|
171
185
|
if (!record) {
|
|
@@ -253,7 +267,7 @@ export class FileTracker {
|
|
|
253
267
|
* @returns {{ok: boolean, reason?: string, message?: string}}
|
|
254
268
|
*/
|
|
255
269
|
checkBeforeEdit(resolvedPath) {
|
|
256
|
-
if (!this._seenFiles.has(resolvedPath)) {
|
|
270
|
+
if (!this._seenFiles.has(normalizePath(resolvedPath))) {
|
|
257
271
|
return {
|
|
258
272
|
ok: false,
|
|
259
273
|
reason: 'untracked',
|
|
@@ -269,8 +283,9 @@ export class FileTracker {
|
|
|
269
283
|
* @param {string} resolvedPath - Absolute path to the file
|
|
270
284
|
*/
|
|
271
285
|
async trackFileAfterWrite(resolvedPath) {
|
|
272
|
-
|
|
273
|
-
this.
|
|
286
|
+
const normalized = normalizePath(resolvedPath);
|
|
287
|
+
this._seenFiles.add(normalized);
|
|
288
|
+
this.invalidateFileRecords(normalized);
|
|
274
289
|
}
|
|
275
290
|
|
|
276
291
|
/**
|
|
@@ -279,10 +294,11 @@ export class FileTracker {
|
|
|
279
294
|
* @param {string} resolvedPath - Absolute path to the file
|
|
280
295
|
*/
|
|
281
296
|
recordTextEdit(resolvedPath) {
|
|
282
|
-
const
|
|
283
|
-
this._textEditCounts.
|
|
297
|
+
const normalized = normalizePath(resolvedPath);
|
|
298
|
+
const count = (this._textEditCounts.get(normalized) || 0) + 1;
|
|
299
|
+
this._textEditCounts.set(normalized, count);
|
|
284
300
|
if (this.debug) {
|
|
285
|
-
console.error(`[FileTracker] Text edit #${count} for ${
|
|
301
|
+
console.error(`[FileTracker] Text edit #${count} for ${normalized}`);
|
|
286
302
|
}
|
|
287
303
|
}
|
|
288
304
|
|
|
@@ -292,7 +308,7 @@ export class FileTracker {
|
|
|
292
308
|
* @returns {{ok: boolean, editCount?: number, message?: string}}
|
|
293
309
|
*/
|
|
294
310
|
checkTextEditStaleness(resolvedPath) {
|
|
295
|
-
const count = this._textEditCounts.get(resolvedPath) || 0;
|
|
311
|
+
const count = this._textEditCounts.get(normalizePath(resolvedPath)) || 0;
|
|
296
312
|
if (count >= this.maxConsecutiveTextEdits) {
|
|
297
313
|
return {
|
|
298
314
|
ok: false,
|
|
@@ -323,7 +339,7 @@ export class FileTracker {
|
|
|
323
339
|
* @param {string} resolvedPath - Absolute path to the file
|
|
324
340
|
*/
|
|
325
341
|
invalidateFileRecords(resolvedPath) {
|
|
326
|
-
const prefix = resolvedPath + '#';
|
|
342
|
+
const prefix = normalizePath(resolvedPath) + '#';
|
|
327
343
|
for (const key of this._contentRecords.keys()) {
|
|
328
344
|
if (key.startsWith(prefix)) {
|
|
329
345
|
this._contentRecords.delete(key);
|
|
@@ -340,7 +356,7 @@ export class FileTracker {
|
|
|
340
356
|
* @returns {boolean}
|
|
341
357
|
*/
|
|
342
358
|
isTracked(resolvedPath) {
|
|
343
|
-
return this.isFileSeen(resolvedPath);
|
|
359
|
+
return this.isFileSeen(normalizePath(resolvedPath));
|
|
344
360
|
}
|
|
345
361
|
|
|
346
362
|
/**
|
package/src/tools/fuzzyMatch.js
CHANGED
|
@@ -73,7 +73,17 @@ export function lineTrimmedMatch(contentLines, searchLines) {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
if (allMatch) {
|
|
76
|
-
|
|
76
|
+
// Limit indent tolerance: even though trimmed content matches, reject when
|
|
77
|
+
// the indentation level difference is too large — it likely means the match
|
|
78
|
+
// is in a completely different scope (issue #507).
|
|
79
|
+
const windowLines = contentLines.slice(i, i + windowSize);
|
|
80
|
+
const windowMinIndent = getMinIndent(windowLines);
|
|
81
|
+
const searchMinIndent = getMinIndent(searchLines);
|
|
82
|
+
const indentDiff = Math.abs(windowMinIndent - searchMinIndent);
|
|
83
|
+
if (isIndentDiffTooLarge(windowLines, searchLines, indentDiff)) {
|
|
84
|
+
continue; // Skip — too far off in nesting
|
|
85
|
+
}
|
|
86
|
+
const matchedText = windowLines.join('\n');
|
|
77
87
|
matches.push(matchedText);
|
|
78
88
|
}
|
|
79
89
|
}
|
|
@@ -134,6 +144,19 @@ export function whitespaceNormalizedMatch(content, search) {
|
|
|
134
144
|
}
|
|
135
145
|
|
|
136
146
|
const matchedText = content.substring(originalStart, actualEnd);
|
|
147
|
+
|
|
148
|
+
// Limit indent tolerance: reject matches where the indentation level
|
|
149
|
+
// difference is too large — likely a wrong-scope match (issue #507).
|
|
150
|
+
const matchedLines = matchedText.split('\n');
|
|
151
|
+
const searchLines = search.split('\n');
|
|
152
|
+
const matchMinIndent = getMinIndent(matchedLines);
|
|
153
|
+
const searchMinIndent = getMinIndent(searchLines);
|
|
154
|
+
const indentDiff = Math.abs(matchMinIndent - searchMinIndent);
|
|
155
|
+
if (isIndentDiffTooLarge(matchedLines, searchLines, indentDiff)) {
|
|
156
|
+
searchStart = idx + 1;
|
|
157
|
+
continue; // Skip — too far off in nesting
|
|
158
|
+
}
|
|
159
|
+
|
|
137
160
|
matches.push(matchedText);
|
|
138
161
|
|
|
139
162
|
searchStart = idx + 1;
|
|
@@ -219,6 +242,15 @@ export function indentFlexibleMatch(contentLines, searchLines) {
|
|
|
219
242
|
}
|
|
220
243
|
|
|
221
244
|
if (allMatch) {
|
|
245
|
+
// Limit indent tolerance: reject matches where indentation differs by more than
|
|
246
|
+
// 1 level. Larger differences likely mean the match is in a completely different
|
|
247
|
+
// scope/nesting level — silent file corruption risk (issue #507).
|
|
248
|
+
// For tabs: 1 tab = 1 level, so max diff = 1.
|
|
249
|
+
// For spaces: detect indent unit (2 or 4), allow 1 unit of difference.
|
|
250
|
+
const indentDiff = Math.abs(windowMinIndent - searchMinIndent);
|
|
251
|
+
if (isIndentDiffTooLarge(windowLines, searchLines, indentDiff)) {
|
|
252
|
+
continue; // Skip — too far off in nesting
|
|
253
|
+
}
|
|
222
254
|
const matchedText = windowLines.join('\n');
|
|
223
255
|
matches.push(matchedText);
|
|
224
256
|
}
|
|
@@ -232,6 +264,25 @@ export function indentFlexibleMatch(contentLines, searchLines) {
|
|
|
232
264
|
};
|
|
233
265
|
}
|
|
234
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Check if an indentation difference exceeds the allowed limit.
|
|
269
|
+
* Uses tab-aware threshold: 1 for tabs, 4 for spaces.
|
|
270
|
+
* Checks BOTH sides for tab usage to avoid asymmetric detection.
|
|
271
|
+
*
|
|
272
|
+
* @param {string[]} linesA - First set of lines
|
|
273
|
+
* @param {string[]} linesB - Second set of lines
|
|
274
|
+
* @param {number} indentDiff - Absolute difference in min indent
|
|
275
|
+
* @returns {boolean} true if the diff exceeds the limit
|
|
276
|
+
*/
|
|
277
|
+
function isIndentDiffTooLarge(linesA, linesB, indentDiff) {
|
|
278
|
+
if (indentDiff <= 0) return false;
|
|
279
|
+
const sampleA = linesA.find(l => l.trim().length > 0) || '';
|
|
280
|
+
const sampleB = linesB.find(l => l.trim().length > 0) || '';
|
|
281
|
+
const useTabs = sampleA.startsWith('\t') || sampleB.startsWith('\t');
|
|
282
|
+
const maxAllowedDiff = useTabs ? 1 : 4;
|
|
283
|
+
return indentDiff > maxAllowedDiff;
|
|
284
|
+
}
|
|
285
|
+
|
|
235
286
|
/**
|
|
236
287
|
* Get the minimum indentation level (number of leading whitespace characters)
|
|
237
288
|
* across all non-empty lines.
|
|
@@ -92,6 +92,17 @@ export function restoreIndentation(newStr, originalLines) {
|
|
|
92
92
|
const newIndent = detectBaseIndent(newStr);
|
|
93
93
|
|
|
94
94
|
if (targetIndent !== newIndent) {
|
|
95
|
+
// Limit auto-reindent tolerance: reject when indentation differs by more than
|
|
96
|
+
// 1 level. Larger differences likely mean the match landed in a completely
|
|
97
|
+
// different scope — allowing it risks silent file corruption (issue #507).
|
|
98
|
+
// For tabs: 1 tab = 1 level, so max diff = 1 char.
|
|
99
|
+
// For spaces: 1 level = up to 4 spaces, so max diff = 4 chars.
|
|
100
|
+
const indentDiff = Math.abs(targetIndent.length - newIndent.length);
|
|
101
|
+
const useTabs = targetIndent.includes('\t') || newIndent.includes('\t');
|
|
102
|
+
const maxAllowedDiff = useTabs ? 1 : 4;
|
|
103
|
+
if (indentDiff > maxAllowedDiff) {
|
|
104
|
+
return { result: newStr, modifications };
|
|
105
|
+
}
|
|
95
106
|
const reindented = reindent(newStr, targetIndent);
|
|
96
107
|
if (reindented !== newStr) {
|
|
97
108
|
modifications.push(`reindented from "${newIndent}" to "${targetIndent}"`);
|
package/src/tools/vercel.js
CHANGED
|
@@ -385,7 +385,7 @@ export const searchTool = (options = {}) => {
|
|
|
385
385
|
? searchDelegateDescription
|
|
386
386
|
: searchDescription,
|
|
387
387
|
inputSchema: searchSchema,
|
|
388
|
-
execute: async ({ query: searchQuery, path, allow_tests, exact, maxTokens: paramMaxTokens, language, session, nextPage }) => {
|
|
388
|
+
execute: async ({ query: searchQuery, path, allow_tests, exact, maxTokens: paramMaxTokens, language, session, nextPage, workingDirectory }) => {
|
|
389
389
|
// Auto-quote mixed-case and underscore terms to prevent unwanted stemming/splitting
|
|
390
390
|
// Skip when exact=true since that already preserves the literal string
|
|
391
391
|
if (!exact && searchQuery) {
|
|
@@ -399,15 +399,18 @@ export const searchTool = (options = {}) => {
|
|
|
399
399
|
// Use parameter maxTokens if provided, otherwise use the default
|
|
400
400
|
const effectiveMaxTokens = paramMaxTokens || maxTokens;
|
|
401
401
|
|
|
402
|
+
// Use workingDirectory (injected by _buildNativeTools at runtime) > cwd from config > fallback
|
|
403
|
+
const effectiveSearchCwd = workingDirectory || options.cwd || '.';
|
|
404
|
+
|
|
402
405
|
// Parse and resolve paths (supports comma-separated and relative paths)
|
|
403
406
|
let searchPaths;
|
|
404
407
|
if (path) {
|
|
405
|
-
searchPaths = parseAndResolvePaths(path,
|
|
408
|
+
searchPaths = parseAndResolvePaths(path, effectiveSearchCwd);
|
|
406
409
|
}
|
|
407
410
|
|
|
408
411
|
// Default to cwd or '.' if no paths provided
|
|
409
412
|
if (!searchPaths || searchPaths.length === 0) {
|
|
410
|
-
searchPaths = [
|
|
413
|
+
searchPaths = [effectiveSearchCwd];
|
|
411
414
|
}
|
|
412
415
|
|
|
413
416
|
// Join paths with space for CLI (probe search supports multiple paths)
|
|
@@ -416,7 +419,7 @@ export const searchTool = (options = {}) => {
|
|
|
416
419
|
const searchOptions = {
|
|
417
420
|
query: searchQuery,
|
|
418
421
|
path: searchPath,
|
|
419
|
-
cwd:
|
|
422
|
+
cwd: effectiveSearchCwd, // Working directory for resolving relative paths
|
|
420
423
|
allowTests: allow_tests ?? true,
|
|
421
424
|
exact,
|
|
422
425
|
json: false,
|
|
@@ -473,7 +476,7 @@ export const searchTool = (options = {}) => {
|
|
|
473
476
|
const result = maybeAnnotate(await runRawSearch());
|
|
474
477
|
// Track files found in search results for staleness detection
|
|
475
478
|
if (options.fileTracker && typeof result === 'string') {
|
|
476
|
-
options.fileTracker.trackFilesFromOutput(result,
|
|
479
|
+
options.fileTracker.trackFilesFromOutput(result, effectiveSearchCwd).catch(() => {});
|
|
477
480
|
}
|
|
478
481
|
return result;
|
|
479
482
|
} catch (error) {
|
|
@@ -532,7 +535,7 @@ export const searchTool = (options = {}) => {
|
|
|
532
535
|
}
|
|
533
536
|
const fallbackResult = maybeAnnotate(await runRawSearch());
|
|
534
537
|
if (options.fileTracker && typeof fallbackResult === 'string') {
|
|
535
|
-
options.fileTracker.trackFilesFromOutput(fallbackResult,
|
|
538
|
+
options.fileTracker.trackFilesFromOutput(fallbackResult, effectiveSearchCwd).catch(() => {});
|
|
536
539
|
}
|
|
537
540
|
return fallbackResult;
|
|
538
541
|
}
|
|
@@ -614,7 +617,7 @@ export const searchTool = (options = {}) => {
|
|
|
614
617
|
try {
|
|
615
618
|
const fallbackResult2 = maybeAnnotate(await runRawSearch());
|
|
616
619
|
if (options.fileTracker && typeof fallbackResult2 === 'string') {
|
|
617
|
-
options.fileTracker.trackFilesFromOutput(fallbackResult2,
|
|
620
|
+
options.fileTracker.trackFilesFromOutput(fallbackResult2, effectiveSearchCwd).catch(() => {});
|
|
618
621
|
}
|
|
619
622
|
return fallbackResult2;
|
|
620
623
|
} catch (fallbackError) {
|
|
@@ -693,10 +696,10 @@ export const extractTool = (options = {}) => {
|
|
|
693
696
|
name: 'extract',
|
|
694
697
|
description: extractDescription,
|
|
695
698
|
inputSchema: extractSchema,
|
|
696
|
-
execute: async ({ targets, input_content, line, end_line, allow_tests, context_lines, format }) => {
|
|
699
|
+
execute: async ({ targets, input_content, line, end_line, allow_tests, context_lines, format, workingDirectory }) => {
|
|
697
700
|
try {
|
|
698
|
-
// Use
|
|
699
|
-
const effectiveCwd = options.cwd || '.';
|
|
701
|
+
// Use workingDirectory (injected by _buildNativeTools at runtime) > cwd from config > fallback
|
|
702
|
+
const effectiveCwd = workingDirectory || options.cwd || '.';
|
|
700
703
|
|
|
701
704
|
if (debug) {
|
|
702
705
|
if (targets) {
|