@probelabs/probe 0.6.0-rc292 → 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.
@@ -90,9 +90,9 @@ If the solution is clear, you can jump to implementation right away. If not, ask
90
90
  - Do not add code comments unless the logic is genuinely complex and non-obvious.
91
91
 
92
92
  # Before Implementation
93
- - Focus on high-level design patterns and system organization
94
- - Identify architectural patterns and component relationships
95
- - Evaluate system structure and suggest architectural improvements
93
+ - Read tests first find existing test files for the module you're changing. They reveal expected behavior, edge cases, and the project's testing patterns.
94
+ - Read neighboring files — understand naming conventions, error handling patterns, import style, and existing utilities before creating new ones.
95
+ - Trace the call chain — follow how the code you're changing is called and what depends on it. Check interfaces, types, and consumers.
96
96
  - Focus on backward compatibility
97
97
  - Consider scalability, maintainability, and extensibility in your analysis
98
98
 
@@ -117,6 +117,20 @@ Before building or testing, determine the project's toolchain:
117
117
  - Read README for build/test instructions if the above are unclear
118
118
  - Common patterns: \`make build\`/\`make test\`, \`npm run build\`/\`npm test\`, \`cargo build\`/\`cargo test\`, \`go build ./...\`/\`go test ./...\`, \`python -m pytest\`
119
119
 
120
+ # File Editing Rules
121
+ You have access to the \`edit\`, \`create\`, and \`multi_edit\` tools for modifying files. You MUST use these tools for ALL code changes. They are purpose-built, atomic, and safe.
122
+
123
+ DO NOT use sed, awk, echo/cat redirection, or heredocs to modify source code. These commands cause real damage in practice: truncated lines, duplicate code blocks, broken syntax. Every bad edit wastes iterations on fix-up commits.
124
+
125
+ Use the right tool:
126
+ 1. To MODIFY existing code → \`edit\` tool (old_string → new_string, or start_line/end_line)
127
+ 2. To CREATE a new file → \`create\` tool
128
+ 3. To CHANGE multiple files at once → \`multi_edit\` tool
129
+ 4. To READ code → \`extract\` or \`search\` tools
130
+ 5. If \`edit\` fails with "file has not been read yet" → use \`extract\` with the EXACT same file path you will pass to \`edit\`. Relative vs absolute path mismatch causes this error. Use the same path format consistently. If it still fails, use bash \`cat\` to read the file, then use \`create\` to write the entire modified file. Do NOT fall back to sed.
131
+
132
+ Bash is fine for: formatters (gofmt, prettier, black), build/test/lint commands, git operations, and read-only file inspection (cat, head, tail). sed/awk should ONLY be used for trivial non-code tasks (e.g., config file tweaks) where the replacement is a simple literal string swap.
133
+
120
134
  # During Implementation
121
135
  - Always create a new branch before making changes to the codebase.
122
136
  - Fix problems at the root cause, not with surface-level patches. Prefer general solutions over special cases.
@@ -143,6 +157,22 @@ Before committing or creating a PR, run through this checklist:
143
157
 
144
158
  Do NOT skip verification. Do NOT proceed to PR creation with a broken build or failing tests.
145
159
 
160
+ # Output Integrity
161
+ Your final output MUST accurately reflect what ACTUALLY happened. Do NOT fabricate, hallucinate, or report aspirational results.
162
+
163
+ - Only report PR URLs you actually created or updated with \`gh pr create\` or \`git push\`. If you checked out an existing PR but did NOT push changes to it, do NOT claim you updated it.
164
+ - Describe what you ACTUALLY DID, not what you planned or intended to do. If you ran out of iterations, say so. If tests failed, say so.
165
+ - Only list files you actually modified AND committed.
166
+ - If you could not complete the task — ran out of iterations, tests failed, build broken, push rejected — report the real reason honestly.
167
+
168
+ NEVER claim success when:
169
+ - You did not run \`git push\` successfully
170
+ - Tests failed and you did not fix them
171
+ - You hit the iteration limit before completing the work
172
+ - You only analyzed/investigated but did not implement changes
173
+
174
+ A false success report is WORSE than an honest failure — it misleads the user into thinking work is done when it is not.
175
+
146
176
  # GitHub Integration
147
177
  - Use the \`gh\` CLI for all GitHub operations: issues, pull requests, checks, releases.
148
178
  - To view issues or PRs: \`gh issue view <number>\`, \`gh pr view <number>\`.
@@ -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
- this._seenFiles.add(resolvedPath);
110
- this._textEditCounts.set(resolvedPath, 0);
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: ${resolvedPath}`);
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
- this._seenFiles.add(resolvedPath);
273
- this.invalidateFileRecords(resolvedPath);
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 count = (this._textEditCounts.get(resolvedPath) || 0) + 1;
283
- this._textEditCounts.set(resolvedPath, count);
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 ${resolvedPath}`);
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
  /**
@@ -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, options.cwd);
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 = [options.cwd || '.'];
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: options.cwd, // Working directory for resolving relative paths
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, options.cwd || '.').catch(() => {});
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, options.cwd || '.').catch(() => {});
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, options.cwd || '.').catch(() => {});
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 the cwd from config for working directory
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) {
@@ -27566,7 +27566,7 @@ var init_vercel = __esm({
27566
27566
  name: "search",
27567
27567
  description: searchDelegate ? searchDelegateDescription : searchDescription,
27568
27568
  inputSchema: searchSchema,
27569
- execute: async ({ query: searchQuery, path: path9, allow_tests, exact, maxTokens: paramMaxTokens, language, session, nextPage }) => {
27569
+ execute: async ({ query: searchQuery, path: path9, allow_tests, exact, maxTokens: paramMaxTokens, language, session, nextPage, workingDirectory }) => {
27570
27570
  if (!exact && searchQuery) {
27571
27571
  const originalQuery = searchQuery;
27572
27572
  searchQuery = autoQuoteSearchTerms(searchQuery);
@@ -27575,18 +27575,19 @@ var init_vercel = __esm({
27575
27575
  }
27576
27576
  }
27577
27577
  const effectiveMaxTokens = paramMaxTokens || maxTokens;
27578
+ const effectiveSearchCwd = workingDirectory || options.cwd || ".";
27578
27579
  let searchPaths;
27579
27580
  if (path9) {
27580
- searchPaths = parseAndResolvePaths(path9, options.cwd);
27581
+ searchPaths = parseAndResolvePaths(path9, effectiveSearchCwd);
27581
27582
  }
27582
27583
  if (!searchPaths || searchPaths.length === 0) {
27583
- searchPaths = [options.cwd || "."];
27584
+ searchPaths = [effectiveSearchCwd];
27584
27585
  }
27585
27586
  const searchPath = searchPaths.join(" ");
27586
27587
  const searchOptions = {
27587
27588
  query: searchQuery,
27588
27589
  path: searchPath,
27589
- cwd: options.cwd,
27590
+ cwd: effectiveSearchCwd,
27590
27591
  // Working directory for resolving relative paths
27591
27592
  allowTests: allow_tests ?? true,
27592
27593
  exact,
@@ -27637,7 +27638,7 @@ var init_vercel = __esm({
27637
27638
  try {
27638
27639
  const result = maybeAnnotate(await runRawSearch());
27639
27640
  if (options.fileTracker && typeof result === "string") {
27640
- options.fileTracker.trackFilesFromOutput(result, options.cwd || ".").catch(() => {
27641
+ options.fileTracker.trackFilesFromOutput(result, effectiveSearchCwd).catch(() => {
27641
27642
  });
27642
27643
  }
27643
27644
  return result;
@@ -27690,7 +27691,7 @@ var init_vercel = __esm({
27690
27691
  }
27691
27692
  const fallbackResult = maybeAnnotate(await runRawSearch());
27692
27693
  if (options.fileTracker && typeof fallbackResult === "string") {
27693
- options.fileTracker.trackFilesFromOutput(fallbackResult, options.cwd || ".").catch(() => {
27694
+ options.fileTracker.trackFilesFromOutput(fallbackResult, effectiveSearchCwd).catch(() => {
27694
27695
  });
27695
27696
  }
27696
27697
  return fallbackResult;
@@ -27753,7 +27754,7 @@ var init_vercel = __esm({
27753
27754
  try {
27754
27755
  const fallbackResult2 = maybeAnnotate(await runRawSearch());
27755
27756
  if (options.fileTracker && typeof fallbackResult2 === "string") {
27756
- options.fileTracker.trackFilesFromOutput(fallbackResult2, options.cwd || ".").catch(() => {
27757
+ options.fileTracker.trackFilesFromOutput(fallbackResult2, effectiveSearchCwd).catch(() => {
27757
27758
  });
27758
27759
  }
27759
27760
  return fallbackResult2;
@@ -27807,9 +27808,9 @@ var init_vercel = __esm({
27807
27808
  name: "extract",
27808
27809
  description: extractDescription,
27809
27810
  inputSchema: extractSchema,
27810
- execute: async ({ targets, input_content, line, end_line, allow_tests, context_lines, format }) => {
27811
+ execute: async ({ targets, input_content, line, end_line, allow_tests, context_lines, format, workingDirectory }) => {
27811
27812
  try {
27812
- const effectiveCwd = options.cwd || ".";
27813
+ const effectiveCwd = workingDirectory || options.cwd || ".";
27813
27814
  if (debug) {
27814
27815
  if (targets) {
27815
27816
  console.error(`Executing extract with targets: "${targets}", cwd: "${effectiveCwd}", context lines: ${context_lines || 10}`);
@@ -48638,6 +48639,10 @@ var init_file_lister = __esm({
48638
48639
  });
48639
48640
 
48640
48641
  // src/tools/fileTracker.js
48642
+ function normalizePath(filePath) {
48643
+ if (!filePath) return filePath;
48644
+ return (0, import_path10.resolve)(filePath);
48645
+ }
48641
48646
  function computeContentHash(content) {
48642
48647
  const normalized = (content || "").split("\n").map((l) => l.trimEnd()).join("\n");
48643
48648
  return (0, import_crypto3.createHash)("sha256").update(normalized).digest("hex").slice(0, 16);
@@ -48700,10 +48705,11 @@ var init_fileTracker = __esm({
48700
48705
  * @param {string} resolvedPath - Absolute path to the file
48701
48706
  */
48702
48707
  markFileSeen(resolvedPath) {
48703
- this._seenFiles.add(resolvedPath);
48704
- this._textEditCounts.set(resolvedPath, 0);
48708
+ const normalized = normalizePath(resolvedPath);
48709
+ this._seenFiles.add(normalized);
48710
+ this._textEditCounts.set(normalized, 0);
48705
48711
  if (this.debug) {
48706
- console.error(`[FileTracker] Marked as seen: ${resolvedPath}`);
48712
+ console.error(`[FileTracker] Marked as seen: ${normalized}`);
48707
48713
  }
48708
48714
  }
48709
48715
  /**
@@ -48712,7 +48718,7 @@ var init_fileTracker = __esm({
48712
48718
  * @returns {boolean}
48713
48719
  */
48714
48720
  isFileSeen(resolvedPath) {
48715
- return this._seenFiles.has(resolvedPath);
48721
+ return this._seenFiles.has(normalizePath(resolvedPath));
48716
48722
  }
48717
48723
  /**
48718
48724
  * Store a content hash for a symbol in a file.
@@ -48724,7 +48730,7 @@ var init_fileTracker = __esm({
48724
48730
  * @param {string} [source='extract'] - How the content was obtained
48725
48731
  */
48726
48732
  trackSymbolContent(resolvedPath, symbolName, code, startLine, endLine, source = "extract") {
48727
- const key = `${resolvedPath}#${symbolName}`;
48733
+ const key = `${normalizePath(resolvedPath)}#${symbolName}`;
48728
48734
  const contentHash = computeContentHash(code);
48729
48735
  this._contentRecords.set(key, {
48730
48736
  contentHash,
@@ -48745,7 +48751,7 @@ var init_fileTracker = __esm({
48745
48751
  * @returns {Object|null} The stored record or null
48746
48752
  */
48747
48753
  getSymbolRecord(resolvedPath, symbolName) {
48748
- return this._contentRecords.get(`${resolvedPath}#${symbolName}`) || null;
48754
+ return this._contentRecords.get(`${normalizePath(resolvedPath)}#${symbolName}`) || null;
48749
48755
  }
48750
48756
  /**
48751
48757
  * Check if a symbol's current content matches what was stored.
@@ -48755,7 +48761,7 @@ var init_fileTracker = __esm({
48755
48761
  * @returns {{ok: boolean, reason?: string, message?: string}}
48756
48762
  */
48757
48763
  checkSymbolContent(resolvedPath, symbolName, currentCode) {
48758
- const key = `${resolvedPath}#${symbolName}`;
48764
+ const key = `${normalizePath(resolvedPath)}#${symbolName}`;
48759
48765
  const record2 = this._contentRecords.get(key);
48760
48766
  if (!record2) {
48761
48767
  return { ok: true };
@@ -48832,7 +48838,7 @@ var init_fileTracker = __esm({
48832
48838
  * @returns {{ok: boolean, reason?: string, message?: string}}
48833
48839
  */
48834
48840
  checkBeforeEdit(resolvedPath) {
48835
- if (!this._seenFiles.has(resolvedPath)) {
48841
+ if (!this._seenFiles.has(normalizePath(resolvedPath))) {
48836
48842
  return {
48837
48843
  ok: false,
48838
48844
  reason: "untracked",
@@ -48847,8 +48853,9 @@ var init_fileTracker = __esm({
48847
48853
  * @param {string} resolvedPath - Absolute path to the file
48848
48854
  */
48849
48855
  async trackFileAfterWrite(resolvedPath) {
48850
- this._seenFiles.add(resolvedPath);
48851
- this.invalidateFileRecords(resolvedPath);
48856
+ const normalized = normalizePath(resolvedPath);
48857
+ this._seenFiles.add(normalized);
48858
+ this.invalidateFileRecords(normalized);
48852
48859
  }
48853
48860
  /**
48854
48861
  * Record a text-mode edit (old_string/new_string) to a file.
@@ -48856,10 +48863,11 @@ var init_fileTracker = __esm({
48856
48863
  * @param {string} resolvedPath - Absolute path to the file
48857
48864
  */
48858
48865
  recordTextEdit(resolvedPath) {
48859
- const count = (this._textEditCounts.get(resolvedPath) || 0) + 1;
48860
- this._textEditCounts.set(resolvedPath, count);
48866
+ const normalized = normalizePath(resolvedPath);
48867
+ const count = (this._textEditCounts.get(normalized) || 0) + 1;
48868
+ this._textEditCounts.set(normalized, count);
48861
48869
  if (this.debug) {
48862
- console.error(`[FileTracker] Text edit #${count} for ${resolvedPath}`);
48870
+ console.error(`[FileTracker] Text edit #${count} for ${normalized}`);
48863
48871
  }
48864
48872
  }
48865
48873
  /**
@@ -48868,7 +48876,7 @@ var init_fileTracker = __esm({
48868
48876
  * @returns {{ok: boolean, editCount?: number, message?: string}}
48869
48877
  */
48870
48878
  checkTextEditStaleness(resolvedPath) {
48871
- const count = this._textEditCounts.get(resolvedPath) || 0;
48879
+ const count = this._textEditCounts.get(normalizePath(resolvedPath)) || 0;
48872
48880
  if (count >= this.maxConsecutiveTextEdits) {
48873
48881
  return {
48874
48882
  ok: false,
@@ -48897,7 +48905,7 @@ var init_fileTracker = __esm({
48897
48905
  * @param {string} resolvedPath - Absolute path to the file
48898
48906
  */
48899
48907
  invalidateFileRecords(resolvedPath) {
48900
- const prefix = resolvedPath + "#";
48908
+ const prefix = normalizePath(resolvedPath) + "#";
48901
48909
  for (const key of this._contentRecords.keys()) {
48902
48910
  if (key.startsWith(prefix)) {
48903
48911
  this._contentRecords.delete(key);
@@ -48913,7 +48921,7 @@ var init_fileTracker = __esm({
48913
48921
  * @returns {boolean}
48914
48922
  */
48915
48923
  isTracked(resolvedPath) {
48916
- return this.isFileSeen(resolvedPath);
48924
+ return this.isFileSeen(normalizePath(resolvedPath));
48917
48925
  }
48918
48926
  /**
48919
48927
  * Clear all tracking state.
@@ -69026,7 +69034,7 @@ var init_graph_builder = __esm({
69026
69034
  applyLinkStyles() {
69027
69035
  if (!this.pendingLinkStyles.length || !this.edges.length)
69028
69036
  return;
69029
- const normalize3 = (s) => {
69037
+ const normalize4 = (s) => {
69030
69038
  const out = {};
69031
69039
  for (const [kRaw, vRaw] of Object.entries(s)) {
69032
69040
  const k = kRaw.trim().toLowerCase();
@@ -69047,7 +69055,7 @@ var init_graph_builder = __esm({
69047
69055
  return out;
69048
69056
  };
69049
69057
  for (const cmd of this.pendingLinkStyles) {
69050
- const style = normalize3(cmd.props);
69058
+ const style = normalize4(cmd.props);
69051
69059
  for (const idx of cmd.indices) {
69052
69060
  if (idx >= 0 && idx < this.edges.length) {
69053
69061
  const e = this.edges[idx];
@@ -76314,7 +76322,7 @@ var require_layout = __commonJS({
76314
76322
  "use strict";
76315
76323
  var _ = require_lodash2();
76316
76324
  var acyclic = require_acyclic();
76317
- var normalize3 = require_normalize();
76325
+ var normalize4 = require_normalize();
76318
76326
  var rank = require_rank();
76319
76327
  var normalizeRanks = require_util().normalizeRanks;
76320
76328
  var parentDummyChains = require_parent_dummy_chains();
@@ -76376,7 +76384,7 @@ var require_layout = __commonJS({
76376
76384
  removeEdgeLabelProxies(g);
76377
76385
  });
76378
76386
  time3(" normalize.run", function() {
76379
- normalize3.run(g);
76387
+ normalize4.run(g);
76380
76388
  });
76381
76389
  time3(" parentDummyChains", function() {
76382
76390
  parentDummyChains(g);
@@ -76403,7 +76411,7 @@ var require_layout = __commonJS({
76403
76411
  removeBorderNodes(g);
76404
76412
  });
76405
76413
  time3(" normalize.undo", function() {
76406
- normalize3.undo(g);
76414
+ normalize4.undo(g);
76407
76415
  });
76408
76416
  time3(" fixupEdgeLabelCoords", function() {
76409
76417
  fixupEdgeLabelCoords(g);
@@ -82774,8 +82782,8 @@ var require_resolve = __commonJS({
82774
82782
  }
82775
82783
  return count;
82776
82784
  }
82777
- function getFullPath(resolver, id = "", normalize3) {
82778
- if (normalize3 !== false)
82785
+ function getFullPath(resolver, id = "", normalize4) {
82786
+ if (normalize4 !== false)
82779
82787
  id = normalizeId(id);
82780
82788
  const p = resolver.parse(id);
82781
82789
  return _getFullPath(resolver, p);
@@ -84115,7 +84123,7 @@ var require_fast_uri = __commonJS({
84115
84123
  "use strict";
84116
84124
  var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils2();
84117
84125
  var { SCHEMES, getSchemeHandler } = require_schemes();
84118
- function normalize3(uri, options) {
84126
+ function normalize4(uri, options) {
84119
84127
  if (typeof uri === "string") {
84120
84128
  uri = /** @type {T} */
84121
84129
  serialize(parse11(uri, options), options);
@@ -84351,7 +84359,7 @@ var require_fast_uri = __commonJS({
84351
84359
  }
84352
84360
  var fastUri = {
84353
84361
  SCHEMES,
84354
- normalize: normalize3,
84362
+ normalize: normalize4,
84355
84363
  resolve: resolve9,
84356
84364
  resolveComponent,
84357
84365
  equal,
@@ -88558,9 +88566,9 @@ If the solution is clear, you can jump to implementation right away. If not, ask
88558
88566
  - Do not add code comments unless the logic is genuinely complex and non-obvious.
88559
88567
 
88560
88568
  # Before Implementation
88561
- - Focus on high-level design patterns and system organization
88562
- - Identify architectural patterns and component relationships
88563
- - Evaluate system structure and suggest architectural improvements
88569
+ - Read tests first \u2014 find existing test files for the module you're changing. They reveal expected behavior, edge cases, and the project's testing patterns.
88570
+ - Read neighboring files \u2014 understand naming conventions, error handling patterns, import style, and existing utilities before creating new ones.
88571
+ - Trace the call chain \u2014 follow how the code you're changing is called and what depends on it. Check interfaces, types, and consumers.
88564
88572
  - Focus on backward compatibility
88565
88573
  - Consider scalability, maintainability, and extensibility in your analysis
88566
88574
 
@@ -88585,6 +88593,20 @@ Before building or testing, determine the project's toolchain:
88585
88593
  - Read README for build/test instructions if the above are unclear
88586
88594
  - Common patterns: \`make build\`/\`make test\`, \`npm run build\`/\`npm test\`, \`cargo build\`/\`cargo test\`, \`go build ./...\`/\`go test ./...\`, \`python -m pytest\`
88587
88595
 
88596
+ # File Editing Rules
88597
+ You have access to the \`edit\`, \`create\`, and \`multi_edit\` tools for modifying files. You MUST use these tools for ALL code changes. They are purpose-built, atomic, and safe.
88598
+
88599
+ DO NOT use sed, awk, echo/cat redirection, or heredocs to modify source code. These commands cause real damage in practice: truncated lines, duplicate code blocks, broken syntax. Every bad edit wastes iterations on fix-up commits.
88600
+
88601
+ Use the right tool:
88602
+ 1. To MODIFY existing code \u2192 \`edit\` tool (old_string \u2192 new_string, or start_line/end_line)
88603
+ 2. To CREATE a new file \u2192 \`create\` tool
88604
+ 3. To CHANGE multiple files at once \u2192 \`multi_edit\` tool
88605
+ 4. To READ code \u2192 \`extract\` or \`search\` tools
88606
+ 5. If \`edit\` fails with "file has not been read yet" \u2192 use \`extract\` with the EXACT same file path you will pass to \`edit\`. Relative vs absolute path mismatch causes this error. Use the same path format consistently. If it still fails, use bash \`cat\` to read the file, then use \`create\` to write the entire modified file. Do NOT fall back to sed.
88607
+
88608
+ Bash is fine for: formatters (gofmt, prettier, black), build/test/lint commands, git operations, and read-only file inspection (cat, head, tail). sed/awk should ONLY be used for trivial non-code tasks (e.g., config file tweaks) where the replacement is a simple literal string swap.
88609
+
88588
88610
  # During Implementation
88589
88611
  - Always create a new branch before making changes to the codebase.
88590
88612
  - Fix problems at the root cause, not with surface-level patches. Prefer general solutions over special cases.
@@ -88611,6 +88633,22 @@ Before committing or creating a PR, run through this checklist:
88611
88633
 
88612
88634
  Do NOT skip verification. Do NOT proceed to PR creation with a broken build or failing tests.
88613
88635
 
88636
+ # Output Integrity
88637
+ Your final output MUST accurately reflect what ACTUALLY happened. Do NOT fabricate, hallucinate, or report aspirational results.
88638
+
88639
+ - Only report PR URLs you actually created or updated with \`gh pr create\` or \`git push\`. If you checked out an existing PR but did NOT push changes to it, do NOT claim you updated it.
88640
+ - Describe what you ACTUALLY DID, not what you planned or intended to do. If you ran out of iterations, say so. If tests failed, say so.
88641
+ - Only list files you actually modified AND committed.
88642
+ - If you could not complete the task \u2014 ran out of iterations, tests failed, build broken, push rejected \u2014 report the real reason honestly.
88643
+
88644
+ NEVER claim success when:
88645
+ - You did not run \`git push\` successfully
88646
+ - Tests failed and you did not fix them
88647
+ - You hit the iteration limit before completing the work
88648
+ - You only analyzed/investigated but did not implement changes
88649
+
88650
+ A false success report is WORSE than an honest failure \u2014 it misleads the user into thinking work is done when it is not.
88651
+
88614
88652
  # GitHub Integration
88615
88653
  - Use the \`gh\` CLI for all GitHub operations: issues, pull requests, checks, releases.
88616
88654
  - To view issues or PRs: \`gh issue view <number>\`, \`gh pr view <number>\`.
package/cjs/index.cjs CHANGED
@@ -27260,6 +27260,10 @@ var init_symbolEdit = __esm({
27260
27260
  });
27261
27261
 
27262
27262
  // src/tools/fileTracker.js
27263
+ function normalizePath(filePath) {
27264
+ if (!filePath) return filePath;
27265
+ return (0, import_path6.resolve)(filePath);
27266
+ }
27263
27267
  function computeContentHash(content) {
27264
27268
  const normalized = (content || "").split("\n").map((l) => l.trimEnd()).join("\n");
27265
27269
  return (0, import_crypto2.createHash)("sha256").update(normalized).digest("hex").slice(0, 16);
@@ -27322,10 +27326,11 @@ var init_fileTracker = __esm({
27322
27326
  * @param {string} resolvedPath - Absolute path to the file
27323
27327
  */
27324
27328
  markFileSeen(resolvedPath) {
27325
- this._seenFiles.add(resolvedPath);
27326
- this._textEditCounts.set(resolvedPath, 0);
27329
+ const normalized = normalizePath(resolvedPath);
27330
+ this._seenFiles.add(normalized);
27331
+ this._textEditCounts.set(normalized, 0);
27327
27332
  if (this.debug) {
27328
- console.error(`[FileTracker] Marked as seen: ${resolvedPath}`);
27333
+ console.error(`[FileTracker] Marked as seen: ${normalized}`);
27329
27334
  }
27330
27335
  }
27331
27336
  /**
@@ -27334,7 +27339,7 @@ var init_fileTracker = __esm({
27334
27339
  * @returns {boolean}
27335
27340
  */
27336
27341
  isFileSeen(resolvedPath) {
27337
- return this._seenFiles.has(resolvedPath);
27342
+ return this._seenFiles.has(normalizePath(resolvedPath));
27338
27343
  }
27339
27344
  /**
27340
27345
  * Store a content hash for a symbol in a file.
@@ -27346,7 +27351,7 @@ var init_fileTracker = __esm({
27346
27351
  * @param {string} [source='extract'] - How the content was obtained
27347
27352
  */
27348
27353
  trackSymbolContent(resolvedPath, symbolName, code, startLine, endLine, source = "extract") {
27349
- const key = `${resolvedPath}#${symbolName}`;
27354
+ const key = `${normalizePath(resolvedPath)}#${symbolName}`;
27350
27355
  const contentHash = computeContentHash(code);
27351
27356
  this._contentRecords.set(key, {
27352
27357
  contentHash,
@@ -27367,7 +27372,7 @@ var init_fileTracker = __esm({
27367
27372
  * @returns {Object|null} The stored record or null
27368
27373
  */
27369
27374
  getSymbolRecord(resolvedPath, symbolName) {
27370
- return this._contentRecords.get(`${resolvedPath}#${symbolName}`) || null;
27375
+ return this._contentRecords.get(`${normalizePath(resolvedPath)}#${symbolName}`) || null;
27371
27376
  }
27372
27377
  /**
27373
27378
  * Check if a symbol's current content matches what was stored.
@@ -27377,7 +27382,7 @@ var init_fileTracker = __esm({
27377
27382
  * @returns {{ok: boolean, reason?: string, message?: string}}
27378
27383
  */
27379
27384
  checkSymbolContent(resolvedPath, symbolName, currentCode) {
27380
- const key = `${resolvedPath}#${symbolName}`;
27385
+ const key = `${normalizePath(resolvedPath)}#${symbolName}`;
27381
27386
  const record2 = this._contentRecords.get(key);
27382
27387
  if (!record2) {
27383
27388
  return { ok: true };
@@ -27454,7 +27459,7 @@ var init_fileTracker = __esm({
27454
27459
  * @returns {{ok: boolean, reason?: string, message?: string}}
27455
27460
  */
27456
27461
  checkBeforeEdit(resolvedPath) {
27457
- if (!this._seenFiles.has(resolvedPath)) {
27462
+ if (!this._seenFiles.has(normalizePath(resolvedPath))) {
27458
27463
  return {
27459
27464
  ok: false,
27460
27465
  reason: "untracked",
@@ -27469,8 +27474,9 @@ var init_fileTracker = __esm({
27469
27474
  * @param {string} resolvedPath - Absolute path to the file
27470
27475
  */
27471
27476
  async trackFileAfterWrite(resolvedPath) {
27472
- this._seenFiles.add(resolvedPath);
27473
- this.invalidateFileRecords(resolvedPath);
27477
+ const normalized = normalizePath(resolvedPath);
27478
+ this._seenFiles.add(normalized);
27479
+ this.invalidateFileRecords(normalized);
27474
27480
  }
27475
27481
  /**
27476
27482
  * Record a text-mode edit (old_string/new_string) to a file.
@@ -27478,10 +27484,11 @@ var init_fileTracker = __esm({
27478
27484
  * @param {string} resolvedPath - Absolute path to the file
27479
27485
  */
27480
27486
  recordTextEdit(resolvedPath) {
27481
- const count = (this._textEditCounts.get(resolvedPath) || 0) + 1;
27482
- this._textEditCounts.set(resolvedPath, count);
27487
+ const normalized = normalizePath(resolvedPath);
27488
+ const count = (this._textEditCounts.get(normalized) || 0) + 1;
27489
+ this._textEditCounts.set(normalized, count);
27483
27490
  if (this.debug) {
27484
- console.error(`[FileTracker] Text edit #${count} for ${resolvedPath}`);
27491
+ console.error(`[FileTracker] Text edit #${count} for ${normalized}`);
27485
27492
  }
27486
27493
  }
27487
27494
  /**
@@ -27490,7 +27497,7 @@ var init_fileTracker = __esm({
27490
27497
  * @returns {{ok: boolean, editCount?: number, message?: string}}
27491
27498
  */
27492
27499
  checkTextEditStaleness(resolvedPath) {
27493
- const count = this._textEditCounts.get(resolvedPath) || 0;
27500
+ const count = this._textEditCounts.get(normalizePath(resolvedPath)) || 0;
27494
27501
  if (count >= this.maxConsecutiveTextEdits) {
27495
27502
  return {
27496
27503
  ok: false,
@@ -27519,7 +27526,7 @@ var init_fileTracker = __esm({
27519
27526
  * @param {string} resolvedPath - Absolute path to the file
27520
27527
  */
27521
27528
  invalidateFileRecords(resolvedPath) {
27522
- const prefix = resolvedPath + "#";
27529
+ const prefix = normalizePath(resolvedPath) + "#";
27523
27530
  for (const key of this._contentRecords.keys()) {
27524
27531
  if (key.startsWith(prefix)) {
27525
27532
  this._contentRecords.delete(key);
@@ -27535,7 +27542,7 @@ var init_fileTracker = __esm({
27535
27542
  * @returns {boolean}
27536
27543
  */
27537
27544
  isTracked(resolvedPath) {
27538
- return this.isFileSeen(resolvedPath);
27545
+ return this.isFileSeen(normalizePath(resolvedPath));
27539
27546
  }
27540
27547
  /**
27541
27548
  * Clear all tracking state.
@@ -31588,7 +31595,7 @@ var init_esm3 = __esm({
31588
31595
  });
31589
31596
 
31590
31597
  // node_modules/path-scurry/dist/esm/index.js
31591
- var import_node_path, import_node_url, import_fs4, actualFS, import_promises, realpathSync2, defaultFS, fsFromOption, uncDriveRegexp, uncToDrive, eitherSep, UNKNOWN, IFIFO, IFCHR, IFDIR, IFBLK, IFREG, IFLNK, IFSOCK, IFMT, IFMT_UNKNOWN, READDIR_CALLED, LSTAT_CALLED, ENOTDIR, ENOENT, ENOREADLINK, ENOREALPATH, ENOCHILD, TYPEMASK, entToType, normalizeCache, normalize, normalizeNocaseCache, normalizeNocase, ResolveCache, ChildrenCache, setAsCwd, PathBase, PathWin32, PathPosix, PathScurryBase, PathScurryWin32, PathScurryPosix, PathScurryDarwin, Path, PathScurry;
31598
+ var import_node_path, import_node_url, import_fs4, actualFS, import_promises, realpathSync2, defaultFS, fsFromOption, uncDriveRegexp, uncToDrive, eitherSep, UNKNOWN, IFIFO, IFCHR, IFDIR, IFBLK, IFREG, IFLNK, IFSOCK, IFMT, IFMT_UNKNOWN, READDIR_CALLED, LSTAT_CALLED, ENOTDIR, ENOENT, ENOREADLINK, ENOREALPATH, ENOCHILD, TYPEMASK, entToType, normalizeCache, normalize2, normalizeNocaseCache, normalizeNocase, ResolveCache, ChildrenCache, setAsCwd, PathBase, PathWin32, PathPosix, PathScurryBase, PathScurryWin32, PathScurryPosix, PathScurryDarwin, Path, PathScurry;
31592
31599
  var init_esm4 = __esm({
31593
31600
  "node_modules/path-scurry/dist/esm/index.js"() {
31594
31601
  init_esm2();
@@ -31643,7 +31650,7 @@ var init_esm4 = __esm({
31643
31650
  TYPEMASK = 1023;
31644
31651
  entToType = (s) => s.isFile() ? IFREG : s.isDirectory() ? IFDIR : s.isSymbolicLink() ? IFLNK : s.isCharacterDevice() ? IFCHR : s.isBlockDevice() ? IFBLK : s.isSocket() ? IFSOCK : s.isFIFO() ? IFIFO : UNKNOWN;
31645
31652
  normalizeCache = /* @__PURE__ */ new Map();
31646
- normalize = (s) => {
31653
+ normalize2 = (s) => {
31647
31654
  const c = normalizeCache.get(s);
31648
31655
  if (c)
31649
31656
  return c;
@@ -31656,7 +31663,7 @@ var init_esm4 = __esm({
31656
31663
  const c = normalizeNocaseCache.get(s);
31657
31664
  if (c)
31658
31665
  return c;
31659
- const n = normalize(s.toLowerCase());
31666
+ const n = normalize2(s.toLowerCase());
31660
31667
  normalizeNocaseCache.set(s, n);
31661
31668
  return n;
31662
31669
  };
@@ -31823,7 +31830,7 @@ var init_esm4 = __esm({
31823
31830
  */
31824
31831
  constructor(name15, type = UNKNOWN, root2, roots, nocase, children, opts) {
31825
31832
  this.name = name15;
31826
- this.#matchName = nocase ? normalizeNocase(name15) : normalize(name15);
31833
+ this.#matchName = nocase ? normalizeNocase(name15) : normalize2(name15);
31827
31834
  this.#type = type & TYPEMASK;
31828
31835
  this.nocase = nocase;
31829
31836
  this.roots = roots;
@@ -31916,7 +31923,7 @@ var init_esm4 = __esm({
31916
31923
  return this.parent || this;
31917
31924
  }
31918
31925
  const children = this.children();
31919
- const name15 = this.nocase ? normalizeNocase(pathPart) : normalize(pathPart);
31926
+ const name15 = this.nocase ? normalizeNocase(pathPart) : normalize2(pathPart);
31920
31927
  for (const p of children) {
31921
31928
  if (p.#matchName === name15) {
31922
31929
  return p;
@@ -32161,7 +32168,7 @@ var init_esm4 = __esm({
32161
32168
  * directly.
32162
32169
  */
32163
32170
  isNamed(n) {
32164
- return !this.nocase ? this.#matchName === normalize(n) : this.#matchName === normalizeNocase(n);
32171
+ return !this.nocase ? this.#matchName === normalize2(n) : this.#matchName === normalizeNocase(n);
32165
32172
  }
32166
32173
  /**
32167
32174
  * Return the Path object corresponding to the target of a symbolic link.
@@ -32300,7 +32307,7 @@ var init_esm4 = __esm({
32300
32307
  #readdirMaybePromoteChild(e, c) {
32301
32308
  for (let p = c.provisional; p < c.length; p++) {
32302
32309
  const pchild = c[p];
32303
- const name15 = this.nocase ? normalizeNocase(e.name) : normalize(e.name);
32310
+ const name15 = this.nocase ? normalizeNocase(e.name) : normalize2(e.name);
32304
32311
  if (name15 !== pchild.#matchName) {
32305
32312
  continue;
32306
32313
  }
@@ -53617,7 +53624,7 @@ var init_graph_builder = __esm({
53617
53624
  applyLinkStyles() {
53618
53625
  if (!this.pendingLinkStyles.length || !this.edges.length)
53619
53626
  return;
53620
- const normalize3 = (s) => {
53627
+ const normalize4 = (s) => {
53621
53628
  const out = {};
53622
53629
  for (const [kRaw, vRaw] of Object.entries(s)) {
53623
53630
  const k = kRaw.trim().toLowerCase();
@@ -53638,7 +53645,7 @@ var init_graph_builder = __esm({
53638
53645
  return out;
53639
53646
  };
53640
53647
  for (const cmd of this.pendingLinkStyles) {
53641
- const style = normalize3(cmd.props);
53648
+ const style = normalize4(cmd.props);
53642
53649
  for (const idx of cmd.indices) {
53643
53650
  if (idx >= 0 && idx < this.edges.length) {
53644
53651
  const e = this.edges[idx];
@@ -60905,7 +60912,7 @@ var require_layout = __commonJS({
60905
60912
  "use strict";
60906
60913
  var _ = require_lodash2();
60907
60914
  var acyclic = require_acyclic();
60908
- var normalize3 = require_normalize();
60915
+ var normalize4 = require_normalize();
60909
60916
  var rank = require_rank();
60910
60917
  var normalizeRanks = require_util().normalizeRanks;
60911
60918
  var parentDummyChains = require_parent_dummy_chains();
@@ -60967,7 +60974,7 @@ var require_layout = __commonJS({
60967
60974
  removeEdgeLabelProxies(g);
60968
60975
  });
60969
60976
  time3(" normalize.run", function() {
60970
- normalize3.run(g);
60977
+ normalize4.run(g);
60971
60978
  });
60972
60979
  time3(" parentDummyChains", function() {
60973
60980
  parentDummyChains(g);
@@ -60994,7 +61001,7 @@ var require_layout = __commonJS({
60994
61001
  removeBorderNodes(g);
60995
61002
  });
60996
61003
  time3(" normalize.undo", function() {
60997
- normalize3.undo(g);
61004
+ normalize4.undo(g);
60998
61005
  });
60999
61006
  time3(" fixupEdgeLabelCoords", function() {
61000
61007
  fixupEdgeLabelCoords(g);
@@ -67365,8 +67372,8 @@ var require_resolve = __commonJS({
67365
67372
  }
67366
67373
  return count;
67367
67374
  }
67368
- function getFullPath(resolver, id = "", normalize3) {
67369
- if (normalize3 !== false)
67375
+ function getFullPath(resolver, id = "", normalize4) {
67376
+ if (normalize4 !== false)
67370
67377
  id = normalizeId(id);
67371
67378
  const p = resolver.parse(id);
67372
67379
  return _getFullPath(resolver, p);
@@ -68706,7 +68713,7 @@ var require_fast_uri = __commonJS({
68706
68713
  "use strict";
68707
68714
  var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils();
68708
68715
  var { SCHEMES, getSchemeHandler } = require_schemes();
68709
- function normalize3(uri, options) {
68716
+ function normalize4(uri, options) {
68710
68717
  if (typeof uri === "string") {
68711
68718
  uri = /** @type {T} */
68712
68719
  serialize(parse11(uri, options), options);
@@ -68942,7 +68949,7 @@ var require_fast_uri = __commonJS({
68942
68949
  }
68943
68950
  var fastUri = {
68944
68951
  SCHEMES,
68945
- normalize: normalize3,
68952
+ normalize: normalize4,
68946
68953
  resolve: resolve9,
68947
68954
  resolveComponent,
68948
68955
  equal,
@@ -73149,9 +73156,9 @@ If the solution is clear, you can jump to implementation right away. If not, ask
73149
73156
  - Do not add code comments unless the logic is genuinely complex and non-obvious.
73150
73157
 
73151
73158
  # Before Implementation
73152
- - Focus on high-level design patterns and system organization
73153
- - Identify architectural patterns and component relationships
73154
- - Evaluate system structure and suggest architectural improvements
73159
+ - Read tests first \u2014 find existing test files for the module you're changing. They reveal expected behavior, edge cases, and the project's testing patterns.
73160
+ - Read neighboring files \u2014 understand naming conventions, error handling patterns, import style, and existing utilities before creating new ones.
73161
+ - Trace the call chain \u2014 follow how the code you're changing is called and what depends on it. Check interfaces, types, and consumers.
73155
73162
  - Focus on backward compatibility
73156
73163
  - Consider scalability, maintainability, and extensibility in your analysis
73157
73164
 
@@ -73176,6 +73183,20 @@ Before building or testing, determine the project's toolchain:
73176
73183
  - Read README for build/test instructions if the above are unclear
73177
73184
  - Common patterns: \`make build\`/\`make test\`, \`npm run build\`/\`npm test\`, \`cargo build\`/\`cargo test\`, \`go build ./...\`/\`go test ./...\`, \`python -m pytest\`
73178
73185
 
73186
+ # File Editing Rules
73187
+ You have access to the \`edit\`, \`create\`, and \`multi_edit\` tools for modifying files. You MUST use these tools for ALL code changes. They are purpose-built, atomic, and safe.
73188
+
73189
+ DO NOT use sed, awk, echo/cat redirection, or heredocs to modify source code. These commands cause real damage in practice: truncated lines, duplicate code blocks, broken syntax. Every bad edit wastes iterations on fix-up commits.
73190
+
73191
+ Use the right tool:
73192
+ 1. To MODIFY existing code \u2192 \`edit\` tool (old_string \u2192 new_string, or start_line/end_line)
73193
+ 2. To CREATE a new file \u2192 \`create\` tool
73194
+ 3. To CHANGE multiple files at once \u2192 \`multi_edit\` tool
73195
+ 4. To READ code \u2192 \`extract\` or \`search\` tools
73196
+ 5. If \`edit\` fails with "file has not been read yet" \u2192 use \`extract\` with the EXACT same file path you will pass to \`edit\`. Relative vs absolute path mismatch causes this error. Use the same path format consistently. If it still fails, use bash \`cat\` to read the file, then use \`create\` to write the entire modified file. Do NOT fall back to sed.
73197
+
73198
+ Bash is fine for: formatters (gofmt, prettier, black), build/test/lint commands, git operations, and read-only file inspection (cat, head, tail). sed/awk should ONLY be used for trivial non-code tasks (e.g., config file tweaks) where the replacement is a simple literal string swap.
73199
+
73179
73200
  # During Implementation
73180
73201
  - Always create a new branch before making changes to the codebase.
73181
73202
  - Fix problems at the root cause, not with surface-level patches. Prefer general solutions over special cases.
@@ -73202,6 +73223,22 @@ Before committing or creating a PR, run through this checklist:
73202
73223
 
73203
73224
  Do NOT skip verification. Do NOT proceed to PR creation with a broken build or failing tests.
73204
73225
 
73226
+ # Output Integrity
73227
+ Your final output MUST accurately reflect what ACTUALLY happened. Do NOT fabricate, hallucinate, or report aspirational results.
73228
+
73229
+ - Only report PR URLs you actually created or updated with \`gh pr create\` or \`git push\`. If you checked out an existing PR but did NOT push changes to it, do NOT claim you updated it.
73230
+ - Describe what you ACTUALLY DID, not what you planned or intended to do. If you ran out of iterations, say so. If tests failed, say so.
73231
+ - Only list files you actually modified AND committed.
73232
+ - If you could not complete the task \u2014 ran out of iterations, tests failed, build broken, push rejected \u2014 report the real reason honestly.
73233
+
73234
+ NEVER claim success when:
73235
+ - You did not run \`git push\` successfully
73236
+ - Tests failed and you did not fix them
73237
+ - You hit the iteration limit before completing the work
73238
+ - You only analyzed/investigated but did not implement changes
73239
+
73240
+ A false success report is WORSE than an honest failure \u2014 it misleads the user into thinking work is done when it is not.
73241
+
73205
73242
  # GitHub Integration
73206
73243
  - Use the \`gh\` CLI for all GitHub operations: issues, pull requests, checks, releases.
73207
73244
  - To view issues or PRs: \`gh issue view <number>\`, \`gh pr view <number>\`.
@@ -101404,7 +101441,7 @@ var init_vercel = __esm({
101404
101441
  name: "search",
101405
101442
  description: searchDelegate ? searchDelegateDescription : searchDescription,
101406
101443
  inputSchema: searchSchema,
101407
- execute: async ({ query: searchQuery, path: path9, allow_tests, exact, maxTokens: paramMaxTokens, language, session, nextPage }) => {
101444
+ execute: async ({ query: searchQuery, path: path9, allow_tests, exact, maxTokens: paramMaxTokens, language, session, nextPage, workingDirectory }) => {
101408
101445
  if (!exact && searchQuery) {
101409
101446
  const originalQuery = searchQuery;
101410
101447
  searchQuery = autoQuoteSearchTerms(searchQuery);
@@ -101413,18 +101450,19 @@ var init_vercel = __esm({
101413
101450
  }
101414
101451
  }
101415
101452
  const effectiveMaxTokens = paramMaxTokens || maxTokens;
101453
+ const effectiveSearchCwd = workingDirectory || options.cwd || ".";
101416
101454
  let searchPaths;
101417
101455
  if (path9) {
101418
- searchPaths = parseAndResolvePaths(path9, options.cwd);
101456
+ searchPaths = parseAndResolvePaths(path9, effectiveSearchCwd);
101419
101457
  }
101420
101458
  if (!searchPaths || searchPaths.length === 0) {
101421
- searchPaths = [options.cwd || "."];
101459
+ searchPaths = [effectiveSearchCwd];
101422
101460
  }
101423
101461
  const searchPath = searchPaths.join(" ");
101424
101462
  const searchOptions = {
101425
101463
  query: searchQuery,
101426
101464
  path: searchPath,
101427
- cwd: options.cwd,
101465
+ cwd: effectiveSearchCwd,
101428
101466
  // Working directory for resolving relative paths
101429
101467
  allowTests: allow_tests ?? true,
101430
101468
  exact,
@@ -101475,7 +101513,7 @@ var init_vercel = __esm({
101475
101513
  try {
101476
101514
  const result = maybeAnnotate(await runRawSearch());
101477
101515
  if (options.fileTracker && typeof result === "string") {
101478
- options.fileTracker.trackFilesFromOutput(result, options.cwd || ".").catch(() => {
101516
+ options.fileTracker.trackFilesFromOutput(result, effectiveSearchCwd).catch(() => {
101479
101517
  });
101480
101518
  }
101481
101519
  return result;
@@ -101528,7 +101566,7 @@ var init_vercel = __esm({
101528
101566
  }
101529
101567
  const fallbackResult = maybeAnnotate(await runRawSearch());
101530
101568
  if (options.fileTracker && typeof fallbackResult === "string") {
101531
- options.fileTracker.trackFilesFromOutput(fallbackResult, options.cwd || ".").catch(() => {
101569
+ options.fileTracker.trackFilesFromOutput(fallbackResult, effectiveSearchCwd).catch(() => {
101532
101570
  });
101533
101571
  }
101534
101572
  return fallbackResult;
@@ -101591,7 +101629,7 @@ var init_vercel = __esm({
101591
101629
  try {
101592
101630
  const fallbackResult2 = maybeAnnotate(await runRawSearch());
101593
101631
  if (options.fileTracker && typeof fallbackResult2 === "string") {
101594
- options.fileTracker.trackFilesFromOutput(fallbackResult2, options.cwd || ".").catch(() => {
101632
+ options.fileTracker.trackFilesFromOutput(fallbackResult2, effectiveSearchCwd).catch(() => {
101595
101633
  });
101596
101634
  }
101597
101635
  return fallbackResult2;
@@ -101645,9 +101683,9 @@ var init_vercel = __esm({
101645
101683
  name: "extract",
101646
101684
  description: extractDescription,
101647
101685
  inputSchema: extractSchema,
101648
- execute: async ({ targets, input_content, line, end_line, allow_tests, context_lines, format }) => {
101686
+ execute: async ({ targets, input_content, line, end_line, allow_tests, context_lines, format, workingDirectory }) => {
101649
101687
  try {
101650
- const effectiveCwd = options.cwd || ".";
101688
+ const effectiveCwd = workingDirectory || options.cwd || ".";
101651
101689
  if (debug) {
101652
101690
  if (targets) {
101653
101691
  console.error(`Executing extract with targets: "${targets}", cwd: "${effectiveCwd}", context lines: ${context_lines || 10}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc292",
3
+ "version": "0.6.0-rc293",
4
4
  "description": "Node.js wrapper for the probe code search tool",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -90,9 +90,9 @@ If the solution is clear, you can jump to implementation right away. If not, ask
90
90
  - Do not add code comments unless the logic is genuinely complex and non-obvious.
91
91
 
92
92
  # Before Implementation
93
- - Focus on high-level design patterns and system organization
94
- - Identify architectural patterns and component relationships
95
- - Evaluate system structure and suggest architectural improvements
93
+ - Read tests first find existing test files for the module you're changing. They reveal expected behavior, edge cases, and the project's testing patterns.
94
+ - Read neighboring files — understand naming conventions, error handling patterns, import style, and existing utilities before creating new ones.
95
+ - Trace the call chain — follow how the code you're changing is called and what depends on it. Check interfaces, types, and consumers.
96
96
  - Focus on backward compatibility
97
97
  - Consider scalability, maintainability, and extensibility in your analysis
98
98
 
@@ -117,6 +117,20 @@ Before building or testing, determine the project's toolchain:
117
117
  - Read README for build/test instructions if the above are unclear
118
118
  - Common patterns: \`make build\`/\`make test\`, \`npm run build\`/\`npm test\`, \`cargo build\`/\`cargo test\`, \`go build ./...\`/\`go test ./...\`, \`python -m pytest\`
119
119
 
120
+ # File Editing Rules
121
+ You have access to the \`edit\`, \`create\`, and \`multi_edit\` tools for modifying files. You MUST use these tools for ALL code changes. They are purpose-built, atomic, and safe.
122
+
123
+ DO NOT use sed, awk, echo/cat redirection, or heredocs to modify source code. These commands cause real damage in practice: truncated lines, duplicate code blocks, broken syntax. Every bad edit wastes iterations on fix-up commits.
124
+
125
+ Use the right tool:
126
+ 1. To MODIFY existing code → \`edit\` tool (old_string → new_string, or start_line/end_line)
127
+ 2. To CREATE a new file → \`create\` tool
128
+ 3. To CHANGE multiple files at once → \`multi_edit\` tool
129
+ 4. To READ code → \`extract\` or \`search\` tools
130
+ 5. If \`edit\` fails with "file has not been read yet" → use \`extract\` with the EXACT same file path you will pass to \`edit\`. Relative vs absolute path mismatch causes this error. Use the same path format consistently. If it still fails, use bash \`cat\` to read the file, then use \`create\` to write the entire modified file. Do NOT fall back to sed.
131
+
132
+ Bash is fine for: formatters (gofmt, prettier, black), build/test/lint commands, git operations, and read-only file inspection (cat, head, tail). sed/awk should ONLY be used for trivial non-code tasks (e.g., config file tweaks) where the replacement is a simple literal string swap.
133
+
120
134
  # During Implementation
121
135
  - Always create a new branch before making changes to the codebase.
122
136
  - Fix problems at the root cause, not with surface-level patches. Prefer general solutions over special cases.
@@ -143,6 +157,22 @@ Before committing or creating a PR, run through this checklist:
143
157
 
144
158
  Do NOT skip verification. Do NOT proceed to PR creation with a broken build or failing tests.
145
159
 
160
+ # Output Integrity
161
+ Your final output MUST accurately reflect what ACTUALLY happened. Do NOT fabricate, hallucinate, or report aspirational results.
162
+
163
+ - Only report PR URLs you actually created or updated with \`gh pr create\` or \`git push\`. If you checked out an existing PR but did NOT push changes to it, do NOT claim you updated it.
164
+ - Describe what you ACTUALLY DID, not what you planned or intended to do. If you ran out of iterations, say so. If tests failed, say so.
165
+ - Only list files you actually modified AND committed.
166
+ - If you could not complete the task — ran out of iterations, tests failed, build broken, push rejected — report the real reason honestly.
167
+
168
+ NEVER claim success when:
169
+ - You did not run \`git push\` successfully
170
+ - Tests failed and you did not fix them
171
+ - You hit the iteration limit before completing the work
172
+ - You only analyzed/investigated but did not implement changes
173
+
174
+ A false success report is WORSE than an honest failure — it misleads the user into thinking work is done when it is not.
175
+
146
176
  # GitHub Integration
147
177
  - Use the \`gh\` CLI for all GitHub operations: issues, pull requests, checks, releases.
148
178
  - To view issues or PRs: \`gh issue view <number>\`, \`gh pr view <number>\`.
@@ -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
- this._seenFiles.add(resolvedPath);
110
- this._textEditCounts.set(resolvedPath, 0);
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: ${resolvedPath}`);
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
- this._seenFiles.add(resolvedPath);
273
- this.invalidateFileRecords(resolvedPath);
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 count = (this._textEditCounts.get(resolvedPath) || 0) + 1;
283
- this._textEditCounts.set(resolvedPath, count);
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 ${resolvedPath}`);
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
  /**
@@ -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, options.cwd);
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 = [options.cwd || '.'];
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: options.cwd, // Working directory for resolving relative paths
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, options.cwd || '.').catch(() => {});
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, options.cwd || '.').catch(() => {});
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, options.cwd || '.').catch(() => {});
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 the cwd from config for working directory
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) {