@shanepadgett/tau-agent 0.33.0 → 0.34.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.
Files changed (190) hide show
  1. package/docs/context.md +29 -20
  2. package/docs/extending-tau-agent.md +63 -24
  3. package/extensions/appshot/index.ts +112 -111
  4. package/extensions/aside/README.md +7 -0
  5. package/extensions/aside/index.ts +168 -0
  6. package/extensions/aside/panel.ts +83 -0
  7. package/extensions/branch/index.ts +38 -21
  8. package/extensions/cache-diagnostics/index.ts +70 -58
  9. package/extensions/checkpoint/README.md +9 -0
  10. package/extensions/checkpoint/checkpoint-budget.ts +79 -0
  11. package/extensions/checkpoint/checkpoint.ts +277 -0
  12. package/extensions/checkpoint/index.ts +102 -0
  13. package/extensions/checkpoint/messages.ts +169 -0
  14. package/extensions/checkpoint/prompt.ts +24 -0
  15. package/extensions/checkpoint/settings.ts +27 -0
  16. package/extensions/commit/commit-effort.ts +3 -3
  17. package/extensions/context/README.md +12 -8
  18. package/extensions/context/definitions.ts +178 -55
  19. package/extensions/context/index.ts +192 -160
  20. package/extensions/context/panel.ts +57 -37
  21. package/extensions/context/sync.ts +139 -89
  22. package/extensions/context/validation.ts +37 -25
  23. package/extensions/effort/README.md +1 -1
  24. package/extensions/effort/index.ts +3 -3
  25. package/extensions/effort/state.ts +3 -18
  26. package/extensions/explore/guidance.ts +6 -47
  27. package/extensions/explore/index.ts +38 -53
  28. package/extensions/explore/{ast/read → read}/hook.ts +12 -7
  29. package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
  30. package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
  31. package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
  32. package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
  33. package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
  34. package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
  35. package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
  36. package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
  37. package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
  38. package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
  39. package/extensions/explore/tools/target-params.ts +9 -0
  40. package/extensions/footer/index.ts +75 -56
  41. package/extensions/handoff/index.ts +8 -30
  42. package/extensions/image-gen/index.ts +46 -40
  43. package/extensions/patch/executor.ts +190 -135
  44. package/extensions/patch/matcher.ts +82 -48
  45. package/extensions/patch/parser.ts +149 -105
  46. package/extensions/patch/render.ts +32 -39
  47. package/extensions/patch/summary.ts +30 -26
  48. package/extensions/ready/README.md +7 -0
  49. package/extensions/ready/index.ts +58 -0
  50. package/extensions/ready/model.ts +128 -0
  51. package/extensions/ready/packs.ts +272 -0
  52. package/extensions/ready/render.ts +445 -0
  53. package/extensions/ready/scan.ts +760 -0
  54. package/extensions/reference/panel.ts +28 -16
  55. package/extensions/review/index.ts +63 -51
  56. package/extensions/review/panel.ts +5 -34
  57. package/extensions/runtime-context/context.ts +30 -26
  58. package/extensions/script-runner/README.md +2 -2
  59. package/extensions/script-runner/index.ts +130 -93
  60. package/extensions/silent-command-runner/index.ts +38 -23
  61. package/extensions/subagent/agents/context-sync.md +185 -33
  62. package/extensions/subagent/agents/scout.md +2 -2
  63. package/extensions/subagent/agents.ts +137 -84
  64. package/extensions/subagent/cmux-dashboard.ts +39 -30
  65. package/extensions/subagent/index.ts +79 -59
  66. package/extensions/subagent/resume.ts +1 -1
  67. package/extensions/subagent/run.ts +304 -193
  68. package/extensions/subagent/runtime.ts +781 -343
  69. package/extensions/tau/index.ts +47 -38
  70. package/extensions/tau-help/help.md +14 -6
  71. package/extensions/tool-loader/README.md +4 -2
  72. package/extensions/tool-loader/index.ts +77 -52
  73. package/extensions/web/index.ts +6 -3
  74. package/extensions/web/webfetch.ts +114 -86
  75. package/package.json +3 -3
  76. package/schemas/tau.schema.json +20 -33
  77. package/shared/bounded-text-result.ts +1 -0
  78. package/shared/checkpoint-visibility.ts +9 -0
  79. package/shared/events.ts +28 -10
  80. package/shared/isolated-session.ts +71 -29
  81. package/shared/model-effort.ts +9 -19
  82. package/shared/model-fallback/index.ts +66 -41
  83. package/shared/settings/load.ts +45 -38
  84. package/shared/temporary-output-store.ts +16 -10
  85. package/{extensions/explore → src}/ast/adapter.ts +11 -2
  86. package/{extensions/explore → src}/ast/engine.ts +54 -38
  87. package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
  88. package/{extensions/explore → src}/ast/format/context.ts +3 -6
  89. package/src/ast/format/deps.ts +94 -0
  90. package/{extensions/explore → src}/ast/format/discover.ts +2 -2
  91. package/{extensions/explore → src}/ast/format/impact.ts +9 -11
  92. package/{extensions/explore → src}/ast/format/outline.ts +2 -2
  93. package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
  94. package/{extensions/explore → src}/ast/format/show.ts +1 -1
  95. package/src/ast/format/targeted-result.ts +20 -0
  96. package/src/ast/grammars/c_sharp.wasm +0 -0
  97. package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
  98. package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
  99. package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
  100. package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
  101. package/{extensions/explore → src}/ast/ir.ts +6 -0
  102. package/src/ast/languages/csharp-file-deps.ts +90 -0
  103. package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
  104. package/src/ast/languages/dotted-index.ts +131 -0
  105. package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
  106. package/src/ast/languages/fixtures/sample.cs +53 -0
  107. package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
  108. package/src/ast/languages/fixtures/sample.go +37 -0
  109. package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
  110. package/src/ast/languages/fixtures/sample.java +40 -0
  111. package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
  112. package/src/ast/languages/fixtures/sample.kt +31 -0
  113. package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
  114. package/src/ast/languages/fixtures/sample.md +23 -0
  115. package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
  116. package/src/ast/languages/fixtures/sample.rs +54 -0
  117. package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
  118. package/src/ast/languages/fixtures/sample.swift +45 -0
  119. package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
  120. package/src/ast/languages/fixtures/sample.ts +45 -0
  121. package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
  122. package/src/ast/languages/fixtures/sample.tsx +36 -0
  123. package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
  124. package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
  125. package/{extensions/explore → src}/ast/languages/go.ts +21 -3
  126. package/{extensions/explore → src}/ast/languages/java.ts +5 -38
  127. package/src/ast/languages/jvm-file-deps.ts +135 -0
  128. package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
  129. package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
  130. package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
  131. package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
  132. package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
  133. package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
  134. package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
  135. package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
  136. package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
  137. package/{extensions/explore → src}/ast/markdown.ts +72 -47
  138. package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
  139. package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
  140. package/{extensions/explore → src}/ast/queries/context.ts +116 -80
  141. package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
  142. package/src/ast/queries/impact.ts +291 -0
  143. package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
  144. package/{extensions/explore → src}/ast/queries/show.ts +46 -42
  145. package/{extensions/explore → src}/ast/query.ts +1 -1
  146. package/{extensions/explore → src}/ast/registry.ts +1 -2
  147. package/{extensions/explore → src}/ast/scan.ts +1 -1
  148. package/src/ast/session.ts +50 -0
  149. package/{extensions/explore → src}/ast/slice.ts +8 -0
  150. package/{extensions/explore → src/ast}/traverse.ts +32 -20
  151. package/src/file-injection/index.ts +547 -0
  152. package/src/image-generation/client.ts +50 -31
  153. package/src/image-generation/index.ts +177 -134
  154. package/src/index.ts +2 -0
  155. package/src/tool-loading/index.ts +58 -0
  156. package/extensions/context/evidence.ts +0 -517
  157. package/extensions/context/projection.ts +0 -141
  158. package/extensions/context/state.ts +0 -30
  159. package/extensions/context/write-scope.ts +0 -109
  160. package/extensions/explore/ast/format/deps.ts +0 -86
  161. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  162. package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
  163. package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
  164. package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
  165. package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
  166. package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
  167. package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
  168. package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
  169. package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
  170. package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
  171. package/extensions/explore/ast/languages/odin.ts +0 -407
  172. package/extensions/explore/ast/queries/impact.ts +0 -250
  173. package/extensions/explore/outline-injection.ts +0 -151
  174. package/extensions/explore/read/autoread.ts +0 -118
  175. package/extensions/working-memory/README.md +0 -17
  176. package/extensions/working-memory/checkpoint.ts +0 -242
  177. package/extensions/working-memory/index.ts +0 -341
  178. package/extensions/working-memory/memory.ts +0 -235
  179. package/extensions/working-memory/render.ts +0 -185
  180. package/extensions/working-memory/settings.ts +0 -38
  181. package/extensions/working-memory/state.ts +0 -158
  182. package/shared/autoread.ts +0 -234
  183. package/shared/context-messages.ts +0 -19
  184. package/shared/outline-injection.ts +0 -56
  185. /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
  186. /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
  187. /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
  188. /package/{extensions/explore → src}/ast/identity.ts +0 -0
  189. /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
  190. /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
@@ -71,69 +71,94 @@ function closeOpen(stack: OpenHeading[], depth: number, endLine: number, endOffs
71
71
  }
72
72
  }
73
73
 
74
+ type FenceState = { char: "`" | "~"; length: number };
75
+
76
+ function closesFence(text: string, fence: FenceState): boolean {
77
+ const close = text.match(/^ {0,3}(`{3,}|~{3,})[ \t]*$/);
78
+ const marker = close?.[1];
79
+ return marker !== undefined && marker[0] === fence.char && marker.length >= fence.length;
80
+ }
81
+
82
+ function openFence(text: string): FenceState | undefined {
83
+ const fence = text.match(OPEN_FENCE);
84
+ const fenceMarker = fence?.[2];
85
+ if (fenceMarker === undefined) return undefined;
86
+ const char = fenceMarker[0] === "~" ? "~" : "`";
87
+ return { char, length: fenceMarker.length };
88
+ }
89
+
90
+ function parseAtxHeading(text: string): { depth: number; name: string } | undefined {
91
+ const heading = text.match(ATX_HEADING);
92
+ if (heading === null) return undefined;
93
+ const hashes = heading[1];
94
+ if (hashes === undefined) return undefined;
95
+ const name = (heading[2] ?? "").trim();
96
+ if (name.length === 0) return undefined;
97
+ return { depth: hashes.length, name };
98
+ }
99
+
100
+ function makeHeadingDecl(info: LineInfo, name: string, parent: OpenHeading | undefined): Decl {
101
+ const qualifiedName = parent === undefined ? name : `${parent.decl.qualifiedName}.${name}`;
102
+ return {
103
+ kind: "heading",
104
+ name,
105
+ qualifiedName,
106
+ startLine: info.line,
107
+ endLine: info.line,
108
+ startOffset: info.startOffset,
109
+ endOffset: info.endOffset,
110
+ signatureEndOffset: info.endOffset,
111
+ bodyStartOffset: info.nextOffset,
112
+ bodyEndOffset: info.nextOffset,
113
+ visibility: "public",
114
+ exported: true,
115
+ children: [],
116
+ calls: [],
117
+ bases: [],
118
+ };
119
+ }
120
+
74
121
  /**
75
122
  * ATX heading scanner. Fenced code (``` / ~~~, length ≥ 3, matching closer) is not heading material.
76
123
  * Section range runs to the line before the next same-or-shallower heading, else EOF.
77
124
  */
125
+ function pushHeading(
126
+ lines: readonly LineInfo[],
127
+ info: LineInfo,
128
+ heading: { depth: number; name: string },
129
+ stack: OpenHeading[],
130
+ roots: Decl[],
131
+ ): void {
132
+ const prev = info.line > 1 ? lines[info.line - 2] : undefined;
133
+ const closeEndLine = prev === undefined ? info.line : prev.line;
134
+ closeOpen(stack, heading.depth, closeEndLine, info.startOffset);
135
+ const parent = stack[stack.length - 1];
136
+ const decl = makeHeadingDecl(info, heading.name, parent);
137
+ if (parent === undefined) roots.push(decl);
138
+ else parent.decl.children.push(decl);
139
+ stack.push({ decl, depth: heading.depth });
140
+ }
141
+
78
142
  export function extractMarkdown(source: string): ExtractResult {
79
143
  const lines = splitLines(source);
80
144
  const roots: Decl[] = [];
81
145
  const stack: OpenHeading[] = [];
82
- let inFence: { char: "`" | "~"; length: number } | undefined;
146
+ let inFence: FenceState | undefined;
83
147
 
84
148
  for (const info of lines) {
85
149
  if (inFence !== undefined) {
86
- const close = info.text.match(/^ {0,3}(`{3,}|~{3,})[ \t]*$/);
87
- const marker = close?.[1];
88
- if (marker !== undefined && marker[0] === inFence.char && marker.length >= inFence.length) {
89
- inFence = undefined;
90
- }
150
+ if (closesFence(info.text, inFence)) inFence = undefined;
91
151
  continue;
92
152
  }
93
153
 
94
- const fence = info.text.match(OPEN_FENCE);
95
- const fenceMarker = fence?.[2];
96
- if (fenceMarker !== undefined) {
97
- const char = fenceMarker[0] === "~" ? "~" : "`";
98
- inFence = { char, length: fenceMarker.length };
154
+ const opened = openFence(info.text);
155
+ if (opened !== undefined) {
156
+ inFence = opened;
99
157
  continue;
100
158
  }
101
159
 
102
- const heading = info.text.match(ATX_HEADING);
103
- if (heading === null) continue;
104
- const hashes = heading[1];
105
- if (hashes === undefined) continue;
106
- const depth = hashes.length;
107
- const name = (heading[2] ?? "").trim();
108
- if (name.length === 0) continue;
109
-
110
- const prev = info.line > 1 ? lines[info.line - 2] : undefined;
111
- const closeEndLine = prev === undefined ? info.line : prev.line;
112
- closeOpen(stack, depth, closeEndLine, info.startOffset);
113
-
114
- const parent = stack[stack.length - 1];
115
- const qualifiedName = parent === undefined ? name : `${parent.decl.qualifiedName}.${name}`;
116
- const decl: Decl = {
117
- kind: "heading",
118
- name,
119
- qualifiedName,
120
- startLine: info.line,
121
- endLine: info.line,
122
- startOffset: info.startOffset,
123
- endOffset: info.endOffset,
124
- signatureEndOffset: info.endOffset,
125
- bodyStartOffset: info.nextOffset,
126
- bodyEndOffset: info.nextOffset,
127
- visibility: "public",
128
- exported: true,
129
- children: [],
130
- calls: [],
131
- bases: [],
132
- };
133
-
134
- if (parent === undefined) roots.push(decl);
135
- else parent.decl.children.push(decl);
136
- stack.push({ decl, depth });
160
+ const heading = parseAtxHeading(info.text);
161
+ if (heading !== undefined) pushHeading(lines, info, heading, stack, roots);
137
162
  }
138
163
 
139
164
  const last = lines[lines.length - 1];
@@ -141,5 +166,5 @@ export function extractMarkdown(source: string): ExtractResult {
141
166
  const eofOffset = last === undefined ? 0 : last.nextOffset;
142
167
  closeOpen(stack, 1, eofLine, eofOffset);
143
168
 
144
- return { decls: roots, imports: [] };
169
+ return { decls: roots, imports: [], fileCalls: [] };
145
170
  }
@@ -1,9 +1,9 @@
1
1
  import { lstat, readFile } from "node:fs/promises";
2
- import type { AstSearchHit, ExploreEngine } from "../engine.ts";
2
+ import type { AstSearchHit, ExploreEngine, FileSource } from "../engine.ts";
3
3
  import type { Decl, DeclKind, FileIr } from "../ir.ts";
4
4
  import { walkDecls } from "../query.ts";
5
5
  import { scanSources, type ScanOutcome } from "../scan.ts";
6
- import { formatPathForDisplay, pathResolutionError, resolveExplorePath } from "../../traverse.ts";
6
+ import { formatPathForDisplay, pathResolutionError, resolveExplorePath } from "../traverse.ts";
7
7
 
8
8
  const PATTERN_MAX_BYTES = 16 * 1024;
9
9
 
@@ -32,32 +32,39 @@ function assertPattern(pattern: string): void {
32
32
  }
33
33
  }
34
34
 
35
+ function requireSearchSupport(supportsSearch: boolean, label: string): void {
36
+ if (!supportsSearch) {
37
+ throw new Error(`Language does not support structural search: ${label}`);
38
+ }
39
+ }
40
+
41
+ function pathLanguageId(engine: ExploreEngine, absolutePath: string): string {
42
+ const byPath = engine.registry.adapterForPath(absolutePath);
43
+ if (byPath === undefined) {
44
+ throw new Error(`Unsupported language for path: ${formatPathForDisplay(absolutePath, engine.cwd)}`);
45
+ }
46
+ return byPath.id;
47
+ }
48
+
35
49
  function resolveLanguageId(
36
50
  engine: ExploreEngine,
37
51
  language: string | undefined,
38
52
  absolutePath: string | undefined,
39
53
  isDirectory: boolean,
40
54
  ): string {
41
- if (isDirectory) {
42
- if (language === undefined || language.length === 0) {
43
- throw new Error("language is required for directory targets");
44
- }
55
+ if (isDirectory && (language === undefined || language.length === 0)) {
56
+ throw new Error("language is required for directory targets");
45
57
  }
46
58
  if (language !== undefined && language.length > 0) {
47
59
  const adapter = engine.registry.adapterForId(language);
48
60
  if (adapter === undefined) {
49
61
  throw new Error(`Unregistered language: ${language}`);
50
62
  }
51
- if (!adapter.capabilities.search) {
52
- throw new Error(`Language does not support structural search: ${language}`);
53
- }
63
+ requireSearchSupport(adapter.capabilities.search, language);
54
64
  if (absolutePath !== undefined && !isDirectory) {
55
- const byPath = engine.registry.adapterForPath(absolutePath);
56
- if (byPath === undefined) {
57
- throw new Error(`Unsupported language for path: ${formatPathForDisplay(absolutePath, engine.cwd)}`);
58
- }
59
- if (byPath.id !== language) {
60
- throw new Error(`language mismatch: path is ${byPath.id}, requested ${language}`);
65
+ const pathId = pathLanguageId(engine, absolutePath);
66
+ if (pathId !== language) {
67
+ throw new Error(`language mismatch: path is ${pathId}, requested ${language}`);
61
68
  }
62
69
  }
63
70
  return language;
@@ -65,14 +72,10 @@ function resolveLanguageId(
65
72
  if (absolutePath === undefined) {
66
73
  throw new Error("language is required");
67
74
  }
68
- const byPath = engine.registry.adapterForPath(absolutePath);
69
- if (byPath === undefined) {
70
- throw new Error(`Unsupported language for path: ${formatPathForDisplay(absolutePath, engine.cwd)}`);
71
- }
72
- if (!byPath.capabilities.search) {
73
- throw new Error(`Language does not support structural search: ${byPath.id}`);
74
- }
75
- return byPath.id;
75
+ const pathId = pathLanguageId(engine, absolutePath);
76
+ const adapter = engine.registry.adapterForId(pathId);
77
+ requireSearchSupport(adapter?.capabilities.search === true, pathId);
78
+ return pathId;
76
79
  }
77
80
 
78
81
  function innermostEnclosing(
@@ -116,6 +119,40 @@ function attachEnclosing(
116
119
  }));
117
120
  }
118
121
 
122
+ async function loadHitMeta(
123
+ engine: ExploreEngine,
124
+ absolutePath: string,
125
+ hitCount: number,
126
+ ): Promise<{ ir: FileIr | undefined; parseDegraded: boolean }> {
127
+ if (hitCount === 0) return { ir: undefined, parseDegraded: false };
128
+ try {
129
+ const file = await engine.sourceForFile(absolutePath);
130
+ // Multi-hit files get enclosing scopes; single hits only need parseDegraded.
131
+ return {
132
+ ir: hitCount > 1 ? file.ir : undefined,
133
+ parseDegraded: file.ir.parseDegraded,
134
+ };
135
+ } catch {
136
+ return { ir: undefined, parseDegraded: false };
137
+ }
138
+ }
139
+
140
+ async function searchHitsInSource(
141
+ engine: ExploreEngine,
142
+ languageId: string,
143
+ source: string,
144
+ pattern: string,
145
+ ): Promise<{ hits: AstSearchHit[]; error: string | undefined }> {
146
+ try {
147
+ return { hits: await engine.searchInSource(languageId, source, pattern), error: undefined };
148
+ } catch (error) {
149
+ const message = error instanceof Error ? error.message : String(error);
150
+ // Invalid pattern is global — rethrow.
151
+ if (message.startsWith("Invalid pattern:")) throw error;
152
+ return { hits: [], error: message };
153
+ }
154
+ }
155
+
119
156
  async function searchOneFile(
120
157
  engine: ExploreEngine,
121
158
  absolutePath: string,
@@ -133,43 +170,106 @@ async function searchOneFile(
133
170
  }
134
171
  signal.throwIfAborted();
135
172
 
136
- let hits: AstSearchHit[];
137
- try {
138
- hits = await engine.searchInSource(languageId, source, pattern);
139
- } catch (error) {
140
- const message = error instanceof Error ? error.message : String(error);
141
- // Invalid pattern is global — rethrow.
142
- if (message.startsWith("Invalid pattern:")) throw error;
143
- return { matches: [], error: message };
173
+ const searched = await searchHitsInSource(engine, languageId, source, pattern);
174
+ if (searched.error !== undefined) return { matches: [], error: searched.error };
175
+
176
+ const meta = await loadHitMeta(engine, absolutePath, searched.hits.length);
177
+ return {
178
+ matches: attachEnclosing(absolutePath, searched.hits, meta.ir, meta.parseDegraded, searched.hits.length > 1),
179
+ error: undefined,
180
+ };
181
+ }
182
+
183
+ type DirectorySearchState = {
184
+ matches: AstSearchMatch[];
185
+ fileErrors: { path: string; message: string }[];
186
+ resultLimitReached: boolean;
187
+ scan: ScanOutcome | undefined;
188
+ };
189
+
190
+ /** `stop` means the walk should end after this file. */
191
+ function absorbDirectoryHits(
192
+ state: DirectorySearchState,
193
+ path: string,
194
+ hits: readonly AstSearchHit[],
195
+ ir: FileIr,
196
+ resultLimit: number,
197
+ ): "continue" | "stop" {
198
+ const room = resultLimit - state.matches.length;
199
+ if (room <= 0) {
200
+ if (hits.length > 0) state.resultLimitReached = true;
201
+ return "stop";
144
202
  }
203
+ const limited = hits.length > room ? hits.slice(0, room) : hits;
204
+ if (hits.length > room) state.resultLimitReached = true;
205
+ state.matches.push(
206
+ ...attachEnclosing(path, limited, hits.length > 1 ? ir : undefined, ir.parseDegraded, hits.length > 1),
207
+ );
208
+ if (state.matches.length >= resultLimit) {
209
+ state.resultLimitReached = true;
210
+ return "stop";
211
+ }
212
+ return "continue";
213
+ }
214
+
215
+ function scanOutcomeFromStep(step: IteratorResult<FileSource, ScanOutcome>, matchCount: number): ScanOutcome {
216
+ if (step.done) return step.value;
217
+ return {
218
+ limit: undefined,
219
+ filesVisited: 0,
220
+ sourceBytes: 0,
221
+ elapsedMs: 0,
222
+ filesEmitted: matchCount > 0 ? 1 : 0,
223
+ };
224
+ }
225
+
226
+ async function searchDirectory(
227
+ engine: ExploreEngine,
228
+ absolutePath: string,
229
+ languageId: string,
230
+ pattern: string,
231
+ resultLimit: number,
232
+ signal: AbortSignal,
233
+ ): Promise<DirectorySearchState> {
234
+ const state: DirectorySearchState = {
235
+ matches: [],
236
+ fileErrors: [],
237
+ resultLimitReached: false,
238
+ scan: undefined,
239
+ };
145
240
 
146
- let ir: FileIr | undefined;
147
- let parseDegraded = false;
148
- if (hits.length > 1) {
149
- try {
150
- const file = await engine.sourceForFile(absolutePath);
151
- ir = file.ir;
152
- parseDegraded = file.ir.parseDegraded;
153
- } catch {
154
- // Enclosing scope is best-effort.
241
+ const generator = scanSources({
242
+ engine,
243
+ cwd: engine.cwd,
244
+ root: absolutePath,
245
+ signal,
246
+ });
247
+
248
+ let step = await generator.next();
249
+ while (!step.done) {
250
+ signal.throwIfAborted();
251
+ const file = step.value;
252
+ if (file.ir.languageId !== languageId) {
253
+ step = await generator.next();
254
+ continue;
155
255
  }
156
- } else if (hits.length === 1) {
157
- // Still surface parse uncertainty when IR is cheap via cache after sourceForFile.
158
- try {
159
- const file = await engine.sourceForFile(absolutePath);
160
- parseDegraded = file.ir.parseDegraded;
161
- // Single hit: only attach enclosing when degraded parse needs trust signal? Spec:
162
- // enclosing when disambiguating — single hit does not need it.
163
- ir = undefined;
164
- } catch {
165
- // ignore
256
+
257
+ const searched = await searchHitsInSource(engine, languageId, file.source, pattern);
258
+ if (searched.error !== undefined) {
259
+ state.fileErrors.push({ path: file.ir.path, message: searched.error });
260
+ step = await generator.next();
261
+ continue;
166
262
  }
263
+
264
+ if (absorbDirectoryHits(state, file.ir.path, searched.hits, file.ir, resultLimit) === "stop") break;
265
+ step = await generator.next();
167
266
  }
168
267
 
169
- return {
170
- matches: attachEnclosing(absolutePath, hits, ir, parseDegraded, hits.length > 1),
171
- error: undefined,
172
- };
268
+ if (signal.aborted) throw new Error("ast_search cancelled");
269
+ const scan = scanOutcomeFromStep(step, state.matches.length);
270
+ if (scan.limit === "cancelled") throw new Error("ast_search cancelled");
271
+ state.scan = scan;
272
+ return state;
173
273
  }
174
274
 
175
275
  /**
@@ -198,109 +298,27 @@ export async function astSearch(
198
298
  const isDirectory = stats.isDirectory();
199
299
  const languageId = resolveLanguageId(engine, language, isDirectory ? undefined : absolutePath, isDirectory);
200
300
 
201
- const matches: AstSearchMatch[] = [];
202
- const fileErrors: { path: string; message: string }[] = [];
203
- let resultLimitReached = false;
204
- let scan: ScanOutcome | undefined;
205
-
206
301
  if (!isDirectory) {
207
302
  const one = await searchOneFile(engine, absolutePath, languageId, pattern, signal);
208
- if (one.error !== undefined) {
209
- throw new Error(one.error);
210
- }
211
- if (one.matches.length > resultLimit) {
212
- resultLimitReached = true;
213
- matches.push(...one.matches.slice(0, resultLimit));
214
- } else {
215
- matches.push(...one.matches);
216
- }
303
+ if (one.error !== undefined) throw new Error(one.error);
304
+ const truncated = one.matches.length > resultLimit;
217
305
  return {
218
- matches,
306
+ matches: truncated ? one.matches.slice(0, resultLimit) : one.matches,
219
307
  resultLimit,
220
- resultLimitReached,
308
+ resultLimitReached: truncated,
221
309
  languageId,
222
310
  scan: undefined,
223
- fileErrors,
311
+ fileErrors: [],
224
312
  };
225
313
  }
226
314
 
227
- // Directory: walk supported files; keep only the requested language.
228
- const generator = scanSources({
229
- engine,
230
- cwd: engine.cwd,
231
- root: absolutePath,
232
- signal,
233
- // scanSources already filters to registered languages; further filter below.
234
- });
235
-
236
- let step = await generator.next();
237
- while (!step.done) {
238
- signal.throwIfAborted();
239
- const file = step.value;
240
- if (file.ir.languageId !== languageId) {
241
- step = await generator.next();
242
- continue;
243
- }
244
-
245
- // Prefer source already loaded by scan (avoids second read for IR).
246
- let hits: AstSearchHit[];
247
- try {
248
- hits = await engine.searchInSource(languageId, file.source, pattern);
249
- } catch (error) {
250
- const message = error instanceof Error ? error.message : String(error);
251
- if (message.startsWith("Invalid pattern:")) throw error;
252
- fileErrors.push({ path: file.ir.path, message });
253
- step = await generator.next();
254
- continue;
255
- }
256
-
257
- const room = resultLimit - matches.length;
258
- if (room <= 0) {
259
- if (hits.length > 0) resultLimitReached = true;
260
- // Still drain walk for budget outcome? Stop early for latency.
261
- break;
262
- }
263
-
264
- const limited = hits.length > room ? hits.slice(0, room) : hits;
265
- if (hits.length > room) resultLimitReached = true;
266
-
267
- const withScope = attachEnclosing(
268
- file.ir.path,
269
- limited,
270
- hits.length > 1 ? file.ir : undefined,
271
- file.ir.parseDegraded,
272
- hits.length > 1,
273
- );
274
- matches.push(...withScope);
275
-
276
- if (matches.length >= resultLimit) {
277
- resultLimitReached = true;
278
- break;
279
- }
280
- step = await generator.next();
281
- }
282
-
283
- if (signal.aborted) throw new Error("ast_search cancelled");
284
-
285
- if (step.done) {
286
- scan = step.value;
287
- } else {
288
- scan = {
289
- limit: undefined,
290
- filesVisited: 0,
291
- sourceBytes: 0,
292
- elapsedMs: 0,
293
- filesEmitted: matches.length > 0 ? 1 : 0,
294
- };
295
- }
296
- if (scan.limit === "cancelled") throw new Error("ast_search cancelled");
297
-
315
+ const directory = await searchDirectory(engine, absolutePath, languageId, pattern, resultLimit, signal);
298
316
  return {
299
- matches,
317
+ matches: directory.matches,
300
318
  resultLimit,
301
- resultLimitReached,
319
+ resultLimitReached: directory.resultLimitReached,
302
320
  languageId,
303
- scan,
304
- fileErrors,
321
+ scan: directory.scan,
322
+ fileErrors: directory.fileErrors,
305
323
  };
306
324
  }
@@ -1,4 +1,4 @@
1
- import { pathResolutionError, resolveExplorePath } from "../../traverse.ts";
1
+ import { pathResolutionError, resolveExplorePath } from "../traverse.ts";
2
2
  import type { ExploreEngine } from "../engine.ts";
3
3
  import type { Candidate } from "../identity.ts";
4
4
  import { resolveTarget } from "../identity.ts";