github-router 0.3.71 → 0.3.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -79,27 +79,28 @@ Then run `claude` as normal.
79
79
 
80
80
  `github-router claude` auto-wires four peer-model adversarial reviewers plus a coordinator into the spawned Claude Code session. No setup, no prior MCP config, no `.claude/agents/` files needed — they appear as Task `subagent_type` options the session can delegate to. Opt out with `--no-codex-mcp`.
81
81
 
82
- Each persona is exposed both as a Claude Code subagent (callable via the `Task` tool) AND as an MCP tool at `mcp__gh-router-peers__<name>`. Personas are stateless: each invocation runs a fresh request against its model with a baked persona prompt — they have no access to your scrollback or project memory, so the lead must paste the artifact into the brief.
82
+ Each persona is exposed both as a Claude Code subagent (callable via the `Task` tool) AND as an MCP tool at `mcp__peers__<name>`. Personas are stateless: each invocation runs a fresh request against its model with a baked persona prompt — they have no access to your scrollback or project memory, so the lead must paste the artifact into the brief.
83
83
 
84
84
  | Subagent | Model | Endpoint | Effort tiers (default) |
85
85
  |---|---|---|---|
86
86
  | `codex-critic` | gpt-5.5 | `/v1/responses` | low \| medium \| high \| xhigh (xhigh) |
87
87
  | `codex-reviewer` | gpt-5.3-codex | `/v1/responses` | low \| medium \| high \| xhigh (xhigh) |
88
+ | `gemini-reviewer` | gemini-3.1-pro-preview | `/v1/chat/completions` | low \| medium \| high (high) |
88
89
  | `opus-critic` | claude-opus-4-6 | `/v1/messages` | low \| medium \| high (high) |
89
90
  | `gemini-critic` | gemini-3.1-pro-preview | `/v1/chat/completions` | low \| medium \| high (high) |
90
91
  | `peer-review-coordinator` | (meta) | — | — |
91
92
 
92
- `peer-review-coordinator` is a subagent (not an MCP tool) that fans out to the right combination of the four critics in parallel based on artifact type — plan, diff, single file, or long-context — and aggregates findings.
93
+ `peer-review-coordinator` is a subagent (not an MCP tool) that fans out to the right combination of the critics in parallel based on artifact type — plan, diff, single file, or long-context — and aggregates findings.
93
94
 
94
- **Effort tiers** are exposed via the MCP tool's `effort` argument; subagents pass it through. All four tiers are accepted on every persona. `xhigh` routinely runs 60–90s; the proxy responds to `tools/call` requests with SSE-streamed responses (per MCP 2025-06-18 Streamable HTTP transport spec) so the connection stays open past the standard ~60s MCP per-tool-call ceiling and long calls complete transparently with no user setup.
95
+ **Effort tiers** are exposed via the MCP tool's `effort` argument; subagents pass it through. `xhigh` routinely runs 60–90s; the proxy responds to `tools/call` requests with SSE-streamed responses (per MCP 2025-06-18 Streamable HTTP transport spec) so the connection stays open past the standard ~60s MCP per-tool-call ceiling and long calls complete transparently with no user setup.
95
96
 
96
- `gemini-critic` only registers when `gemini-3.1-pro-preview` is present in your Copilot model catalog. If absent, the persona is silently dropped from both the MCP `tools/list` and the subagent set, and `peer-review-coordinator` skips it in routing decisions.
97
+ `gemini-critic` and `gemini-reviewer` both register only when `gemini-3.1-pro-preview` (or another `gemini-3.x-pro` model) is present in your Copilot model catalog — `gemini-critic` is the architecture-level critic, `gemini-reviewer` the line-level second-lab code reviewer on the same model at its highest reasoning tier. If absent, both personas are silently dropped from the MCP `tools/list` and the subagent set, and `peer-review-coordinator` skips them in routing decisions.
97
98
 
98
99
  For codex-side write capability (a `codex-implementer` persona that can mutate files via Codex's tool-use sandbox), pass `--codex-cli`. Requires `codex` CLI 0.129+ on `PATH`; falls back to HTTP-only with a warning if codex is missing or older. Pass `--codex-mcp-only` to also pass `--strict-mcp-config` to Claude Code so only the proxy's MCP servers are loaded (hides any MCP servers in your existing `~/.claude/mcp.json`).
99
100
 
100
- ### Code search (`mcp__gh-router-peers__code_search`)
101
+ ### Code search (`mcp__search__code`)
101
102
 
102
- Alongside the peer reviewers, the same MCP surface exposes a `code_search` tool — fast structured code search over the workspace, ranked by **BM25F** (Robertson, Zaragoza, Taylor 2004) over four code-aware fields: matched line, surrounding context, file path tokens, and a symbol-definition heuristic. On top of that, the top hits get a tree-sitter pass that promotes true identifier-definition sites over incidental string matches; depth is controlled by the optional `structural` argument (`"full"` parses the top 50 hits, `"topN"` parses the top 10 for tighter latency on big repos). A single `notice` field surfaces in the response on the rare occasions an actionable degradation fires — the structural pass overran its 200ms wall-clock budget, or the response hit the 256KB size cap and was truncated; the message text tells the model what to retry. Defaults to a "ranked" mode with shoulder pruning so models get the few right answers, not a flood of substring matches. `literal` and `regex` modes are also available for exact searches; single-identifier queries in `ranked`/`literal` mode auto-expand across camelCase / snake_case / kebab-case skeletons so `getUserName` also matches `get_user_name`.
103
+ Alongside the peer reviewers, the same MCP surface exposes a `code` tool (under the `search` server `mcp__search__code`) — fast structured code search over the workspace, ranked by **BM25F** (Robertson, Zaragoza, Taylor 2004) over four code-aware fields: matched line, surrounding context, file path tokens, and a symbol-definition heuristic. On top of that, the top hits get a tree-sitter pass that promotes true identifier-definition sites over incidental string matches; depth is controlled by the optional `structural` argument (`"full"` parses the top 50 hits, `"topN"` parses the top 10 for tighter latency on big repos). A single `notice` field surfaces in the response on the rare occasions an actionable degradation fires — the structural pass overran its 200ms wall-clock budget, or the response hit the 256KB size cap and was truncated; the message text tells the model what to retry. Defaults to a "ranked" mode with shoulder pruning so models get the few right answers, not a flood of substring matches. `literal` and `regex` modes are also available for exact searches; single-identifier queries in `ranked`/`literal` mode auto-expand across camelCase / snake_case / kebab-case skeletons so `getUserName` also matches `get_user_name`.
103
104
 
104
105
  `workspace` is any absolute path the proxy process can read — typically the project root or a sub-tree you're working in. The model picks it. There's no allow-set or secret-shape file denylist: the threat model is symmetric since Claude Code already has Read / Bash / Edit tools that reach the same paths, so gating one tool would have been inconsistency rather than defense. Paths in results are returned relative to the workspace, never absolute.
105
106
 
@@ -859,7 +859,7 @@ function clampNum(v, min, max) {
859
859
  // attachment, so two parallel browser_mouse / browser_drag / browser_type
860
860
  // calls on the same tab would interleave and corrupt each other (one
861
861
  // call's mouseMoved would land mid-drag of another). The global
862
- // MAX_INFLIGHT_TOOLS_CALL=8 cap doesn't help — it's global, not per-tab.
862
+ // MAX_INFLIGHT_TOOLS_CALL=32 cap doesn't help — it's global, not per-tab.
863
863
  // This mutex is per-tab, layered on top.
864
864
  const tabInputLockTails = new Map() // tabId → Promise (tail of the lock chain)
865
865
 
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "name": "github-router browser bridge",
4
4
  "short_name": "gh-router-browser",
5
- "version": "0.3.71",
5
+ "version": "0.3.73",
6
6
  "description": "Bridge between Claude (via github-router /mcp) and the browser. Implements tab control, navigation, clicks, form fill, downloads, screenshots, devtools eval. Blocks navigation to chrome://settings.",
7
7
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqJElxuBlonBS3TVW9FJN0mGTtShB3L1hoaYf6k39SOr1ogGYmF90EjRxy1i21k9wQQjPf26bcBu/9X67KrQjQV0uB38CaNukgiSeoLjfptN811u+PJHx6BP+jx3Qa6/3VenNPxHC8WEU0GXql8QSjIHEyCwKb6fMASXOK94JyB5Ywov2x8mt/+9ncqBBBMVzf6r5Sagy4PL1XnryLsuADD/vOEkPet8wXgH/Oj7v5tTsQQZ7U1JT51PoDs2BFnXc5v3TkVgZwd32k3ONh+nkDw1Hof+4zwUGOyJE6eMrlYzRlKM4Qxdf9JpavQvqfieAbTRWcyKeclnHeoIfE7cDBQIDAQAB",
8
8
  "background": {
@@ -0,0 +1,543 @@
1
+ import { createRequire } from "node:module";
2
+ import { readFileSync, statSync } from "node:fs";
3
+ import { parentPort } from "node:worker_threads";
4
+ import Parser from "web-tree-sitter";
5
+ import * as path from "node:path";
6
+ import consola from "consola";
7
+
8
+ //#region rolldown:runtime
9
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
10
+
11
+ //#endregion
12
+ //#region src/lib/tree-sitter-grammars.ts
13
+ /**
14
+ * Cap the per-file size we'll parse. 1MB of source covers all
15
+ * reasonable hand-written files; bigger files are almost always
16
+ * generated code or vendored bundles whose AST signal is worthless
17
+ * for ranking real definitions.
18
+ */
19
+ const STRUCTURAL_MAX_FILE_BYTES = 1024 * 1024;
20
+ /**
21
+ * Grammar key → wasm filename under `node_modules/tree-sitter-wasms/out/`.
22
+ * Resolved at runtime from `node_modules`; the file paths are stable
23
+ * because `tree-sitter-wasms` ships prebuilt binaries (no per-install
24
+ * codegen).
25
+ */
26
+ const GRAMMAR_FILES = {
27
+ typescript: "tree-sitter-typescript.wasm",
28
+ tsx: "tree-sitter-tsx.wasm",
29
+ javascript: "tree-sitter-javascript.wasm",
30
+ python: "tree-sitter-python.wasm",
31
+ go: "tree-sitter-go.wasm",
32
+ rust: "tree-sitter-rust.wasm",
33
+ java: "tree-sitter-java.wasm",
34
+ c: "tree-sitter-c.wasm",
35
+ cpp: "tree-sitter-cpp.wasm"
36
+ };
37
+ /**
38
+ * Per-language definition-shape node types. When a matched identifier
39
+ * sits inside one of these nodes AND is at the node's "name" position,
40
+ * we have AST-confirmed evidence the line is an identifier-definition
41
+ * site. The brief's enumeration plus a handful of language-idiomatic
42
+ * extras (e.g., `lexical_declaration` for TS/JS top-level `const`s,
43
+ * `mod_item` for Rust modules).
44
+ *
45
+ * The set lookup is per-language so a node type that means
46
+ * "definition" in one language but "reference" in another won't
47
+ * cross-pollute.
48
+ */
49
+ const DEFINITION_NODE_TYPES = {
50
+ typescript: new Set([
51
+ "function_declaration",
52
+ "function_signature",
53
+ "function_expression",
54
+ "method_definition",
55
+ "method_signature",
56
+ "class_declaration",
57
+ "interface_declaration",
58
+ "type_alias_declaration",
59
+ "enum_declaration",
60
+ "variable_declarator",
61
+ "generator_function_declaration",
62
+ "abstract_method_signature",
63
+ "public_field_definition",
64
+ "property_signature"
65
+ ]),
66
+ tsx: new Set([
67
+ "function_declaration",
68
+ "function_signature",
69
+ "function_expression",
70
+ "method_definition",
71
+ "method_signature",
72
+ "class_declaration",
73
+ "interface_declaration",
74
+ "type_alias_declaration",
75
+ "enum_declaration",
76
+ "variable_declarator",
77
+ "generator_function_declaration",
78
+ "abstract_method_signature",
79
+ "public_field_definition",
80
+ "property_signature"
81
+ ]),
82
+ javascript: new Set([
83
+ "function_declaration",
84
+ "function_expression",
85
+ "method_definition",
86
+ "class_declaration",
87
+ "variable_declarator",
88
+ "generator_function_declaration"
89
+ ]),
90
+ python: new Set([
91
+ "function_definition",
92
+ "class_definition",
93
+ "decorated_definition"
94
+ ]),
95
+ go: new Set([
96
+ "function_declaration",
97
+ "method_declaration",
98
+ "type_spec",
99
+ "type_alias",
100
+ "const_spec",
101
+ "var_spec"
102
+ ]),
103
+ rust: new Set([
104
+ "function_item",
105
+ "impl_item",
106
+ "trait_item",
107
+ "struct_item",
108
+ "enum_item",
109
+ "mod_item",
110
+ "type_item",
111
+ "const_item",
112
+ "static_item",
113
+ "macro_definition"
114
+ ]),
115
+ java: new Set([
116
+ "class_declaration",
117
+ "interface_declaration",
118
+ "method_declaration",
119
+ "constructor_declaration",
120
+ "enum_declaration",
121
+ "field_declaration",
122
+ "annotation_type_declaration"
123
+ ]),
124
+ c: new Set([
125
+ "function_definition",
126
+ "declaration",
127
+ "struct_specifier",
128
+ "enum_specifier",
129
+ "union_specifier",
130
+ "type_definition"
131
+ ]),
132
+ cpp: new Set([
133
+ "function_definition",
134
+ "declaration",
135
+ "struct_specifier",
136
+ "class_specifier",
137
+ "enum_specifier",
138
+ "union_specifier",
139
+ "type_definition",
140
+ "namespace_definition",
141
+ "template_declaration"
142
+ ])
143
+ };
144
+ /**
145
+ * Node types that the AST exposes as "this token is an identifier".
146
+ * The match-position lookup uses these to filter out parent-node hits
147
+ * before checking the definition-site predicate.
148
+ */
149
+ const IDENTIFIER_NODE_TYPES = new Set([
150
+ "identifier",
151
+ "type_identifier",
152
+ "field_identifier",
153
+ "property_identifier",
154
+ "shorthand_property_identifier_pattern",
155
+ "shorthand_property_identifier",
156
+ "scoped_identifier",
157
+ "name"
158
+ ]);
159
+ let _grammarBundle;
160
+ /**
161
+ * Resolve the `tree-sitter-wasms/out/` directory at the package root.
162
+ * `require.resolve` is used through a try/catch — the bundled-only
163
+ * fallback runs in environments where node_modules has been pruned to
164
+ * just runtime deps.
165
+ */
166
+ function resolveGrammarRoot() {
167
+ try {
168
+ const pkgPath = __require.resolve("tree-sitter-wasms/package.json");
169
+ return path.join(path.dirname(pkgPath), "out");
170
+ } catch {
171
+ return null;
172
+ }
173
+ }
174
+ /**
175
+ * Pre-load all grammars at module-init time so the first search
176
+ * doesn't pay a ~500ms cold-start cost. The Promise is captured at
177
+ * import time and awaited per-call; per-grammar failures are caught
178
+ * individually so one broken grammar can't take the whole tool down.
179
+ */
180
+ function getGrammarBundle() {
181
+ if (_grammarBundle) return _grammarBundle;
182
+ _grammarBundle = { ready: (async () => {
183
+ const out = /* @__PURE__ */ new Map();
184
+ try {
185
+ await Parser.init();
186
+ } catch (err) {
187
+ consola.warn(`[code_search] tree-sitter Parser.init failed; structural ranking disabled: ${err.message}`);
188
+ return out;
189
+ }
190
+ const root = resolveGrammarRoot();
191
+ if (!root) {
192
+ consola.warn("[code_search] tree-sitter-wasms package not resolvable; structural ranking disabled");
193
+ return out;
194
+ }
195
+ for (const [key, filename] of Object.entries(GRAMMAR_FILES)) {
196
+ const wasmPath = path.join(root, filename);
197
+ try {
198
+ const lang = await Parser.Language.load(wasmPath);
199
+ out.set(key, lang);
200
+ } catch (err) {
201
+ consola.warn(`[code_search] failed to load tree-sitter grammar '${key}' from ${filename}: ${err.message}`);
202
+ }
203
+ }
204
+ return out;
205
+ })() };
206
+ return _grammarBundle;
207
+ }
208
+ getGrammarBundle().ready.catch(() => {});
209
+ /**
210
+ * Robustness bound on outline entries per file. Normal source is far
211
+ * under it; generated/pathological files hit it and `outlineFile` then
212
+ * sets a `notice` so the model knows the map was truncated.
213
+ */
214
+ const MAX_OUTLINE_ENTRIES = 1e3;
215
+ /**
216
+ * First identifier-typed named child found in a pre-order walk. Mirrors
217
+ * the structural-pass helper in `code-search.ts` (kept local here so
218
+ * the grammar module has no dependency back on the ranking layer).
219
+ */
220
+ function firstIdentifierLeaf(node) {
221
+ if (IDENTIFIER_NODE_TYPES.has(node.type)) return node;
222
+ for (const child of node.namedChildren) {
223
+ const r = firstIdentifierLeaf(child);
224
+ if (r) return r;
225
+ }
226
+ return null;
227
+ }
228
+ /**
229
+ * Derive a human-readable name for a definition node. Tries the
230
+ * grammar's standard `name` field first, then the `declarator` /
231
+ * `type` fields (C/C++/Java declarators, Rust/Go type specs), then any
232
+ * identifier-typed named child as a last resort. Returns `null` when no
233
+ * name can be recovered — the caller skips such nodes.
234
+ */
235
+ function deriveDefinitionName(node) {
236
+ const nameField = node.childForFieldName("name");
237
+ if (nameField && nameField.text.length > 0) return nameField.text;
238
+ const declarator = node.childForFieldName("declarator");
239
+ if (declarator) {
240
+ const leaf = firstIdentifierLeaf(declarator);
241
+ if (leaf && leaf.text.length > 0) return leaf.text;
242
+ }
243
+ const typeField = node.childForFieldName("type");
244
+ if (typeField) {
245
+ const leaf = firstIdentifierLeaf(typeField);
246
+ if (leaf && leaf.text.length > 0) return leaf.text;
247
+ }
248
+ const fallback = firstIdentifierLeaf(node);
249
+ if (fallback && fallback.text.length > 0) return fallback.text;
250
+ return null;
251
+ }
252
+ /**
253
+ * Collect EVERY definition node from the parse tree — top-level AND
254
+ * nested (class methods, methods' inner functions, nested classes, …) —
255
+ * so the outline is a COMPLETE structural map the model can rely on to
256
+ * decide what to read. Recurses through non-definition wrappers (TS
257
+ * `export_statement`, Python `decorated_definition`, C++
258
+ * `template_declaration`, …) at the same depth, and INTO each definition
259
+ * at depth+1 to surface its members.
260
+ *
261
+ * `defTypes` is the language's definition-node-type set. Each node yields
262
+ * one entry; the `name` is derived per `deriveDefinitionName` (a node
263
+ * with no recoverable name is skipped, but the walk still descends into
264
+ * it so its named members aren't lost). Bounded at `MAX_OUTLINE_ENTRIES`.
265
+ */
266
+ function collectDefinitions(root, defTypes, signal) {
267
+ const out = [];
268
+ const visit = (node, depth) => {
269
+ if (signal?.aborted || out.length >= MAX_OUTLINE_ENTRIES) return;
270
+ for (const child of node.namedChildren) {
271
+ if (signal?.aborted || out.length >= MAX_OUTLINE_ENTRIES) return;
272
+ if (defTypes.has(child.type)) {
273
+ const name = deriveDefinitionName(child);
274
+ if (name !== null) out.push({
275
+ kind: child.type,
276
+ name,
277
+ line: child.startPosition.row + 1,
278
+ depth
279
+ });
280
+ visit(child, depth + 1);
281
+ continue;
282
+ }
283
+ visit(child, depth);
284
+ }
285
+ };
286
+ visit(root, 0);
287
+ return out;
288
+ }
289
+ /**
290
+ * Build a `FileOutlineResult` from an ALREADY-PARSED tree — walk-only,
291
+ * no read / parse / `delete`. The tree's ownership stays with the caller
292
+ * (e.g. the code-search structural pass's `_treeCache`), so this lets the
293
+ * outline step REUSE a tree the structural pass already parsed instead of
294
+ * re-reading + re-parsing the file. Never throws.
295
+ */
296
+ function outlineFromTree(tree, language, signal) {
297
+ if (signal?.aborted) return {
298
+ outline: [],
299
+ language
300
+ };
301
+ const defTypes = DEFINITION_NODE_TYPES[language];
302
+ if (!defTypes) return {
303
+ outline: [],
304
+ language,
305
+ notice: "outline unavailable (parse error)"
306
+ };
307
+ try {
308
+ const outline = collectDefinitions(tree.rootNode, defTypes, signal);
309
+ if (signal?.aborted) return {
310
+ outline: [],
311
+ language
312
+ };
313
+ outline.sort((a, b) => a.line - b.line);
314
+ if (outline.length >= MAX_OUTLINE_ENTRIES) return {
315
+ outline,
316
+ language,
317
+ notice: `outline truncated at ${MAX_OUTLINE_ENTRIES} symbols (very large file)`
318
+ };
319
+ return {
320
+ outline,
321
+ language
322
+ };
323
+ } catch {
324
+ return {
325
+ outline: [],
326
+ language,
327
+ notice: "outline unavailable (parse error)"
328
+ };
329
+ }
330
+ }
331
+ /**
332
+ * Compute the absolute byte offset where line `lineNumber1` starts in
333
+ * `source`. Lines are counted by LF; CRLF files have the same line starts as
334
+ * LF files (the \r is part of the previous line's content). 1-indexed to match
335
+ * ripgrep. Returns -1 if the line is past EOF.
336
+ */
337
+ function lineStartByte(source, lineNumber1) {
338
+ if (lineNumber1 <= 1) return 0;
339
+ let line = 1;
340
+ for (let i = 0; i < source.length; i++) if (source.charCodeAt(i) === 10) {
341
+ line += 1;
342
+ if (line === lineNumber1) return i + 1;
343
+ }
344
+ return -1;
345
+ }
346
+ function containsByteRange(outer, inner) {
347
+ return outer.startIndex <= inner.startIndex && outer.endIndex >= inner.endIndex;
348
+ }
349
+ /**
350
+ * Walk up from a matched identifier node looking for the closest
351
+ * definition-shape ancestor (per the language's allowed types). When found,
352
+ * verify the matched identifier is at the definition's "name" slot — NOT inside
353
+ * a parameter type, a body, or a parent's signature. Returns true iff this is a
354
+ * real definition site for the identifier the rg submatch landed on. Depth
355
+ * bound 6 — definition names sit close to their definition node in every
356
+ * supported grammar; deeper walks risk false positives.
357
+ */
358
+ function isDefiningSite(matchedNode, langKey) {
359
+ const defTypes = DEFINITION_NODE_TYPES[langKey];
360
+ if (!defTypes) return false;
361
+ let cur = matchedNode.parent;
362
+ let depth = 0;
363
+ while (cur && depth < 6) {
364
+ if (defTypes.has(cur.type)) {
365
+ const nameField = cur.childForFieldName("name");
366
+ if (nameField && containsByteRange(nameField, matchedNode)) return true;
367
+ const declarator = cur.childForFieldName("declarator");
368
+ if (declarator && containsByteRange(declarator, matchedNode)) {
369
+ const first = firstIdentifierLeaf(declarator);
370
+ if (first && first.startIndex === matchedNode.startIndex) return true;
371
+ }
372
+ const typeField = cur.childForFieldName("type");
373
+ if (typeField && containsByteRange(typeField, matchedNode)) {
374
+ const first = firstIdentifierLeaf(typeField);
375
+ if (first && first.startIndex === matchedNode.startIndex) return true;
376
+ }
377
+ }
378
+ cur = cur.parent;
379
+ depth += 1;
380
+ }
381
+ return false;
382
+ }
383
+ /**
384
+ * Run the AST-confirmation walk over `hits` against an ALREADY-PARSED tree.
385
+ * Returns the subset of input indices whose matched identifier is at a real
386
+ * definition site. Borrowed tree — never deleted. Pure + deterministic: the
387
+ * output set is a function of (tree, source, hits) only, independent of call
388
+ * order, so the in-process path and the worker path return identical sets.
389
+ *
390
+ * Never throws — a per-hit walk failure just omits that index (matches the
391
+ * in-process pass's per-hit try/catch). `signal` short-circuits between hits.
392
+ */
393
+ function confirmDefinitionSites(tree, source, language, hits, signal) {
394
+ const confirmed = [];
395
+ if (!DEFINITION_NODE_TYPES[language]) return confirmed;
396
+ for (let i = 0; i < hits.length; i++) {
397
+ if (signal?.aborted) break;
398
+ const hit = hits[i];
399
+ const lineStart = lineStartByte(source, hit.line);
400
+ if (lineStart < 0) continue;
401
+ const matchByteStart = lineStart + hit.matchStart;
402
+ const matchByteEnd = lineStart + hit.matchEnd;
403
+ let node;
404
+ try {
405
+ node = tree.rootNode.descendantForIndex(matchByteStart, matchByteEnd);
406
+ } catch {
407
+ node = null;
408
+ }
409
+ if (!node) continue;
410
+ if (!IDENTIFIER_NODE_TYPES.has(node.type)) {
411
+ let cur = node;
412
+ let depth = 0;
413
+ while (cur && !IDENTIFIER_NODE_TYPES.has(cur.type) && depth < 3) {
414
+ const leaf = firstIdentifierLeaf(cur);
415
+ if (leaf && leaf.startIndex === matchByteStart) {
416
+ cur = leaf;
417
+ break;
418
+ }
419
+ cur = cur.parent;
420
+ depth += 1;
421
+ }
422
+ node = cur;
423
+ }
424
+ if (!node || !IDENTIFIER_NODE_TYPES.has(node.type)) continue;
425
+ if (isDefiningSite(node, language)) confirmed.push(i);
426
+ }
427
+ return confirmed;
428
+ }
429
+
430
+ //#endregion
431
+ //#region src/lib/tree-sitter-pool/worker.ts
432
+ if (!parentPort) throw new Error("tree-sitter worker must be spawned via worker_threads");
433
+ const port = parentPort;
434
+ let grammars = /* @__PURE__ */ new Map();
435
+ const parsers = /* @__PURE__ */ new Map();
436
+ let cancelled = false;
437
+ function handleJob(job) {
438
+ if (process.env.GH_ROUTER_TS_WORKER_CRASH === "1") throw new Error("injected worker crash (test)");
439
+ if (cancelled) return {
440
+ id: job.id,
441
+ ok: true,
442
+ mtimeMs: job.mtimeMs
443
+ };
444
+ const lang = grammars.get(job.language);
445
+ if (!lang) return {
446
+ id: job.id,
447
+ ok: false,
448
+ error: "grammar not loaded"
449
+ };
450
+ let mtimeMs;
451
+ let size;
452
+ try {
453
+ const st = statSync(job.absPath);
454
+ mtimeMs = st.mtimeMs;
455
+ size = st.size;
456
+ } catch (err) {
457
+ return {
458
+ id: job.id,
459
+ ok: false,
460
+ error: `stat failed: ${err.message}`
461
+ };
462
+ }
463
+ if (size > STRUCTURAL_MAX_FILE_BYTES) return {
464
+ id: job.id,
465
+ ok: false,
466
+ error: "file too large"
467
+ };
468
+ let source;
469
+ try {
470
+ source = readFileSync(job.absPath, "utf8");
471
+ } catch (err) {
472
+ return {
473
+ id: job.id,
474
+ ok: false,
475
+ error: `read failed: ${err.message}`
476
+ };
477
+ }
478
+ let parser = parsers.get(job.language);
479
+ if (!parser) {
480
+ parser = new Parser();
481
+ parser.setLanguage(lang);
482
+ parsers.set(job.language, parser);
483
+ }
484
+ let tree = null;
485
+ try {
486
+ tree = parser.parse(source);
487
+ } catch (err) {
488
+ return {
489
+ id: job.id,
490
+ ok: false,
491
+ error: `parse failed: ${err.message}`
492
+ };
493
+ }
494
+ if (!tree) return {
495
+ id: job.id,
496
+ ok: false,
497
+ error: "parse returned null"
498
+ };
499
+ try {
500
+ const reply = {
501
+ id: job.id,
502
+ ok: true,
503
+ mtimeMs
504
+ };
505
+ if (job.want.confirmHits && job.want.confirmHits.length > 0) reply.confirmedHitIndexes = confirmDefinitionSites(tree, source, job.language, job.want.confirmHits);
506
+ if (job.want.outline) reply.outlineEntries = outlineFromTree(tree, job.language).outline;
507
+ return reply;
508
+ } catch (err) {
509
+ return {
510
+ id: job.id,
511
+ ok: false,
512
+ error: `walk failed: ${err.message}`
513
+ };
514
+ } finally {
515
+ try {
516
+ tree.delete();
517
+ } catch {}
518
+ }
519
+ }
520
+ port.on("message", (msg) => {
521
+ if ("type" in msg) {
522
+ if (msg.type === "cancel") cancelled = true;
523
+ return;
524
+ }
525
+ cancelled = false;
526
+ const reply = handleJob(msg);
527
+ port.postMessage(reply);
528
+ });
529
+ (async () => {
530
+ try {
531
+ grammars = await getGrammarBundle().ready;
532
+ } catch {
533
+ grammars = /* @__PURE__ */ new Map();
534
+ }
535
+ const ready = {
536
+ type: "ready",
537
+ loaded: [...grammars.keys()]
538
+ };
539
+ port.postMessage(ready);
540
+ })();
541
+
542
+ //#endregion
543
+ export { };
@@ -0,0 +1,4 @@
1
+ import "./paths-yJ97KlKp.js";
2
+ import { a as trackChild, i as sweepStaleColbertMetaAtBoot, n as registerColbertExitHandlers, r as sweepLiveChildren, t as getColbertInstanceUuid } from "./lifecycle-yaqqtsV1.js";
3
+
4
+ export { getColbertInstanceUuid, registerColbertExitHandlers, sweepLiveChildren, sweepStaleColbertMetaAtBoot, trackChild };
@@ -1,4 +1,4 @@
1
- import { l as writeRuntimeFileSecure, t as PATHS } from "./paths-CoFnpNZl.js";
1
+ import { l as writeRuntimeFileSecure, t as PATHS } from "./paths-yJ97KlKp.js";
2
2
  import { randomBytes, randomUUID } from "node:crypto";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
@@ -307,4 +307,4 @@ async function sweepStaleWorktreesAtBoot() {
307
307
 
308
308
  //#endregion
309
309
  export { sweepRegistry as a, registerExitHandlers as i, getInstanceUuid as n, sweepStaleWorktreesAtBoot as o, recordWorkerRepo as r, WorktreeRegistry as t };
310
- //# sourceMappingURL=lifecycle-NQRdfY1u.js.map
310
+ //# sourceMappingURL=lifecycle-CMPthagV.js.map