github-router 0.3.219 → 0.3.233

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.
@@ -1,6 +1,6 @@
1
- import { t as PATHS } from "./paths-BO22pMUb.js";
2
- import { d as runCommandCapture, l as parseBoolEnv, n as isPidAlive, o as trackChild, p as runManagedExeCapture, r as registerColbertExitHandlers, t as getColbertInstanceUuid, u as resolveExecutable } from "./lifecycle-D-1CYr1Y.js";
3
- import { i as registerExitHandlers, n as getInstanceUuid, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-bPdiXjYB.js";
1
+ import { t as PATHS } from "./paths-ogCi3URX.js";
2
+ import { d as runCommandCapture, l as parseBoolEnv, n as isPidAlive, o as trackChild, p as runManagedExeCapture, r as registerColbertExitHandlers, t as getColbertInstanceUuid, u as resolveExecutable } from "./lifecycle-C5ALWmZK.js";
3
+ import { i as registerExitHandlers, n as getInstanceUuid, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-DR4TGEIY.js";
4
4
  import { createRequire } from "node:module";
5
5
  import consola from "consola";
6
6
  import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
@@ -13335,40 +13335,47 @@ function deriveDefinitionName(node) {
13335
13335
  return null;
13336
13336
  }
13337
13337
  /**
13338
- * Collect EVERY definition node from the parse tree top-level AND
13339
- * nested (class methods, methods' inner functions, nested classes, …) —
13340
- * so the outline is a COMPLETE structural map the model can rely on to
13341
- * decide what to read. Recurses through non-definition wrappers (TS
13342
- * `export_statement`, Python `decorated_definition`, C++
13343
- * `template_declaration`, …) at the same depth, and INTO each definition
13344
- * at depth+1 to surface its members.
13345
- *
13346
- * `defTypes` is the language's definition-node-type set. Each node yields
13347
- * one entry; the `name` is derived per `deriveDefinitionName` (a node
13348
- * with no recoverable name is skipped, but the walk still descends into
13349
- * it so its named members aren't lost). Bounded at `MAX_OUTLINE_ENTRIES`.
13350
- */
13338
+ * Collect declarations that help a model navigate a file: top-level symbols
13339
+ * and members of class-like containers. Function-local variables and nested
13340
+ * helpers are deliberately omitted because they inflate the summary without
13341
+ * identifying useful read targets. Non-definition wrappers (exports,
13342
+ * decorators, templates) preserve the surrounding depth.
13343
+ */
13344
+ const OUTLINE_MEMBER_CONTAINERS = new Set([
13345
+ "class_declaration",
13346
+ "class_definition",
13347
+ "class_specifier",
13348
+ "interface_declaration",
13349
+ "annotation_type_declaration",
13350
+ "impl_item",
13351
+ "trait_item",
13352
+ "struct_item",
13353
+ "namespace_definition",
13354
+ "module"
13355
+ ]);
13351
13356
  function collectDefinitions(root, defTypes, signal) {
13352
13357
  const out = [];
13353
- const visit = (node, depth) => {
13358
+ const visit = (node, depth, includeDefinitions) => {
13354
13359
  if (signal?.aborted || out.length >= MAX_OUTLINE_ENTRIES) return;
13355
13360
  for (const child of node.namedChildren) {
13356
13361
  if (signal?.aborted || out.length >= MAX_OUTLINE_ENTRIES) return;
13357
13362
  if (defTypes.has(child.type)) {
13358
- const name = deriveDefinitionName(child);
13359
- if (name !== null) out.push({
13360
- kind: child.type,
13361
- name,
13362
- line: child.startPosition.row + 1,
13363
- depth
13364
- });
13365
- visit(child, depth + 1);
13363
+ if (includeDefinitions) {
13364
+ const name = deriveDefinitionName(child);
13365
+ if (name !== null) out.push({
13366
+ kind: child.type,
13367
+ name,
13368
+ line: child.startPosition.row + 1,
13369
+ depth
13370
+ });
13371
+ }
13372
+ if (includeDefinitions && OUTLINE_MEMBER_CONTAINERS.has(child.type)) visit(child, depth + 1, true);
13366
13373
  continue;
13367
13374
  }
13368
- visit(child, depth);
13375
+ visit(child, depth, includeDefinitions);
13369
13376
  }
13370
13377
  };
13371
- visit(root, 0);
13378
+ visit(root, 0, true);
13372
13379
  return out;
13373
13380
  }
13374
13381
  /**
@@ -13512,10 +13519,10 @@ function confirmDefinitionSites(tree, source, language, hits, signal) {
13512
13519
  return confirmed;
13513
13520
  }
13514
13521
  /**
13515
- * Full structural outline of a single file — EVERY definition, top-level
13516
- * AND nested (functions, classes, methods, nested functions, interfaces,
13517
- * type aliases, enums, including exported / decorated / templated
13518
- * wrappers). Each entry carries a `depth` (0 = top-level). Reuses the
13522
+ * Navigational structural outline of a single file: top-level declarations
13523
+ * plus class-like members (methods, fields, interface members), excluding
13524
+ * function-local variables and nested implementation helpers. Each entry
13525
+ * carries a `depth` (0 = top-level). Reuses the
13519
13526
  * shared grammar bundle and the same `Parser` the structural pass uses
13520
13527
  * — no second `Parser.init()`.
13521
13528
  *
@@ -13680,6 +13687,14 @@ var TreeSitterPool = class {
13680
13687
  spawned: this.workersSpawned
13681
13688
  };
13682
13689
  }
13690
+ /**
13691
+ * Launch-time latency optimization: ready ONE worker and its grammar bundle.
13692
+ * A single worker keeps startup CPU/memory bounded while removing the first
13693
+ * query's initialization wait; normal demand still grows the pool lazily.
13694
+ */
13695
+ async warm() {
13696
+ return await this.ensureWorkers(1) > 0;
13697
+ }
13683
13698
  queue = [];
13684
13699
  inflight = /* @__PURE__ */ new Map();
13685
13700
  constructor() {
@@ -13695,18 +13710,19 @@ var TreeSitterPool = class {
13695
13710
  * callers onto one in-flight ensure so 8 simultaneous searches don't each
13696
13711
  * spawn a fresh batch. Returns the live worker count (0 → caller must fall
13697
13712
  * back to the in-process path). */
13698
- ensureWorkers() {
13713
+ ensureWorkers(target = this.size) {
13699
13714
  if (this.unavailable || this.shuttingDown) return Promise.resolve(0);
13715
+ const desired = Math.max(1, Math.min(target, this.size));
13700
13716
  const liveNow = this.workers.filter((w) => w.ready && w.loaded.size > 0).length;
13701
- if (liveNow >= this.size) return Promise.resolve(liveNow);
13702
- if (this.ensuring) return this.ensuring;
13703
- this.ensuring = this.doEnsureWorkers().finally(() => {
13717
+ if (liveNow >= desired) return Promise.resolve(liveNow);
13718
+ if (this.ensuring) return liveNow > 0 ? Promise.resolve(liveNow) : this.ensuring;
13719
+ this.ensuring = this.doEnsureWorkers(desired).finally(() => {
13704
13720
  this.ensuring = null;
13705
13721
  });
13706
- return this.ensuring;
13722
+ return liveNow > 0 ? Promise.resolve(liveNow) : this.ensuring;
13707
13723
  }
13708
- async doEnsureWorkers() {
13709
- const need = this.size - this.workers.length;
13724
+ async doEnsureWorkers(target) {
13725
+ const need = target - this.workers.length;
13710
13726
  const spawns = [];
13711
13727
  for (let i = 0; i < need; i++) spawns.push(this.spawnWorker());
13712
13728
  const spawned = await Promise.all(spawns);
@@ -13927,13 +13943,23 @@ var TreeSitterPool = class {
13927
13943
  budgetHit: false
13928
13944
  };
13929
13945
  opts.signal.addEventListener("abort", onAbort, { once: true });
13930
- const budgetTimer = setTimeout(() => {
13931
- budgetHit = true;
13932
- stop();
13933
- }, opts.budgetMs);
13934
- budgetTimer.unref?.();
13946
+ let budgetTimer;
13935
13947
  try {
13936
13948
  if (await this.ensureWorkers() === 0) return null;
13949
+ if (opts.signal.aborted) return {
13950
+ byFile,
13951
+ budgetHit: false
13952
+ };
13953
+ if (opts.budgetMs <= 0) {
13954
+ budgetHit = true;
13955
+ stop();
13956
+ } else {
13957
+ budgetTimer = setTimeout(() => {
13958
+ budgetHit = true;
13959
+ stop();
13960
+ }, opts.budgetMs);
13961
+ budgetTimer.unref?.();
13962
+ }
13937
13963
  const dispatchFile = async (job, retried) => {
13938
13964
  if (stopped) return;
13939
13965
  const id = this.nextJobId++;
@@ -13964,7 +13990,7 @@ var TreeSitterPool = class {
13964
13990
  };
13965
13991
  await Promise.all(jobs.map((job) => dispatchFile(job, false)));
13966
13992
  } finally {
13967
- clearTimeout(budgetTimer);
13993
+ if (budgetTimer) clearTimeout(budgetTimer);
13968
13994
  opts.signal.removeEventListener("abort", onAbort);
13969
13995
  }
13970
13996
  const stillLive = this.workers.some((w) => w.ready && w.loaded.size > 0);
@@ -14010,6 +14036,7 @@ function resolveWorkerPath() {
14010
14036
  let _pool = null;
14011
14037
  let _shutdownRegistered = false;
14012
14038
  let _testCrashOnceArmed = false;
14039
+ let _warmOverride;
14013
14040
  /**
14014
14041
  * The pool is ON by default for real (non-CI) runs and OFF under CI.
14015
14042
  *
@@ -14055,6 +14082,24 @@ function getTreeSitterPool() {
14055
14082
  }
14056
14083
  return _pool;
14057
14084
  }
14085
+ /**
14086
+ * Best-effort launch warm-up. Returns immediately when disabled/unavailable and
14087
+ * never rejects; lazy initialization remains the fallback after any failure.
14088
+ */
14089
+ async function warmTreeSitterPool() {
14090
+ if (process.env.GH_ROUTER_DISABLE_TS_POOL_WARMUP === "1") return;
14091
+ const pool = getTreeSitterPool();
14092
+ if (!pool) return;
14093
+ try {
14094
+ if (await (_warmOverride?.(pool) ?? pool.warm())) return;
14095
+ } catch (err) {
14096
+ consola.debug(`[code_search] tree-sitter pool warm-up failed; using lazy initialization: ${err.message}`);
14097
+ }
14098
+ if (_pool === pool) {
14099
+ pool.shutdown();
14100
+ _pool = null;
14101
+ }
14102
+ }
14058
14103
 
14059
14104
  //#endregion
14060
14105
  //#region src/lib/worker-agent/paths.ts
@@ -14257,9 +14302,9 @@ const WALL_TIME_MS = 3e4;
14257
14302
  * Structural-pass settings. The wall-clock budget is checked between
14258
14303
  * files (NOT mid-parse — tree-sitter doesn't surface a usable cancel
14259
14304
  * hook in the web-tree-sitter binding we're on), so a single
14260
- * pathological file can overrun by one file's parse-time. In practice
14261
- * a single source file parses in well under 50ms; 200ms gives us
14262
- * comfortable headroom for ~5-10 files even on cold cache.
14305
+ * pathological file can overrun by one file's parse-time. Worker/grammar
14306
+ * initialization is completed before this timer starts; the 200ms budget
14307
+ * therefore measures parsing only, on cold and warm queries alike.
14263
14308
  */
14264
14309
  const STRUCTURAL_BUDGET_MS = 200;
14265
14310
  let _structuralBudgetTestOverride = null;
@@ -14854,7 +14899,7 @@ async function runStructuralPassPooled(opts) {
14854
14899
  }
14855
14900
  return {
14856
14901
  confirmedHitIndexes,
14857
- fallback: run.budgetHit ? `structural budget exceeded after parsing ${run.byFile.size}/${opts.cap} hits; retry with structural: "topN" or narrow your query` : null,
14902
+ fallback: run.budgetHit ? `structural budget exceeded after parsing ${run.byFile.size}/${jobs.length} files; retry with structural: "topN" or narrow your query` : null,
14858
14903
  outlinesByFile
14859
14904
  };
14860
14905
  }
@@ -15184,26 +15229,23 @@ function dropAstGrepSecrets(env) {
15184
15229
  return env;
15185
15230
  }
15186
15231
  /**
15187
- * Resolve the ast-grep binary. Checks the toolbelt bin dir (where the
15188
- * proxy materializes `sg` + `ast-grep`) AND the system PATH, trying `sg`
15189
- * first then `ast-grep`. Returns an ABSOLUTE path or `null` when neither
15190
- * is found. `resolveExecutable` honors PATHEXT on Windows and excludes
15191
- * the cwd (no planted-`sg.exe` vector). The toolbelt dir is searched by
15192
- * prepending it to a PATH copy so the same resolver handles both sources.
15232
+ * Resolve the ast-grep binary. Checks the known router-owned toolbelt paths
15233
+ * directly before consulting PATH for the unambiguous `ast-grep` name.
15234
+ * Returns an ABSOLUTE path or `null` when neither source has it.
15193
15235
  */
15194
15236
  function resolveAstGrep() {
15195
15237
  const toolbeltDir = PATHS.TOOLBELT_BIN_DIR;
15196
- const sgInToolbelt = resolveExecutable("sg", { env: {
15197
- ...process.env,
15198
- PATH: toolbeltDir
15199
- } });
15200
- if (sgInToolbelt) return sgInToolbelt;
15201
- const astGrep = resolveExecutable("ast-grep", { env: {
15202
- ...process.env,
15203
- PATH: `${toolbeltDir}${path.delimiter}${pathEnvValue()}`
15204
- } });
15205
- if (astGrep) return astGrep;
15206
- return null;
15238
+ const toolbeltNames = process.platform === "win32" ? [
15239
+ "sg.exe",
15240
+ "ast-grep.exe",
15241
+ "sg",
15242
+ "ast-grep"
15243
+ ] : ["sg", "ast-grep"];
15244
+ for (const name of toolbeltNames) {
15245
+ const candidate = path.resolve(toolbeltDir, name);
15246
+ if (existsSync(candidate)) return candidate;
15247
+ }
15248
+ return resolveExecutable("ast-grep", { env: process.env });
15207
15249
  }
15208
15250
  /**
15209
15251
  * Test-only override for the ast-grep resolver. `undefined` = use the real
@@ -15217,11 +15259,6 @@ let _astGrepResolverOverride;
15217
15259
  function resolveAstGrepForRun() {
15218
15260
  return (_astGrepResolverOverride ?? resolveAstGrep)();
15219
15261
  }
15220
- /** Read PATH case-insensitively from the live env. */
15221
- function pathEnvValue() {
15222
- for (const key of Object.keys(process.env)) if (key.toLowerCase() === "path") return process.env[key] ?? "";
15223
- return "";
15224
- }
15225
15262
  /**
15226
15263
  * Run ast-grep with `pattern` over `workspaceCanonical` and return its
15227
15264
  * matches in `RawHit` shape (relativized, 1-indexed). Read-only,
@@ -15243,7 +15280,7 @@ async function runAstGrep(opts) {
15243
15280
  const binary = resolveAstGrepForRun();
15244
15281
  if (!binary) return {
15245
15282
  hits: [],
15246
- notice: "ast_pattern requires ast-grep (sg), which isn't available here; the model can run ast-grep directly or omit ast_pattern"
15283
+ notice: `ast_pattern requires ast-grep, but no executable was found at ${PATHS.TOOLBELT_BIN_DIR} (sg/ast-grep) or on PATH (ast-grep); install ast-grep or omit ast_pattern`
15247
15284
  };
15248
15285
  if (!opts.lang || !/^[A-Za-z0-9_+-]{1,20}$/.test(opts.lang)) return {
15249
15286
  hits: [],
@@ -15824,318 +15861,6 @@ function modelDirName() {
15824
15861
  /** Short model id used in the sidecar metadata `model` field. */
15825
15862
  const MODEL_ID = "LateOn-Code-edge";
15826
15863
 
15827
- //#endregion
15828
- //#region src/lib/colbert/index-store.ts
15829
- const GIT_TIMEOUT_MS = 4e3;
15830
- /** Grace window after a `building` write before a workspace with no live
15831
- * build PID is declared `crashed` — covers the cross-process window where
15832
- * one proxy wrote `building` but hasn't yet recorded the colgrep child PID. */
15833
- const BUILD_SPAWN_GRACE_MS = 3e4;
15834
- /**
15835
- * Hash a workspace path the same way the metadata sidecar is keyed.
15836
- * NOTE: this is the ROUTER-OWNED meta key, independent of colgrep's
15837
- * internal xxh3 physical-dir key (we never need to predict colgrep's
15838
- * key because we pass the workspace as colgrep's PATH arg and let it
15839
- * route). A stable sha256-prefix of the canonical path is sufficient.
15840
- */
15841
- function metaHashForWorkspace(workspace) {
15842
- const canonical = process$1.platform === "win32" ? nodePath.resolve(workspace).toLowerCase().replace(/\\/g, "/") : nodePath.resolve(workspace);
15843
- let h = 2166136261;
15844
- for (let i = 0; i < canonical.length; i++) {
15845
- h ^= canonical.charCodeAt(i);
15846
- h = Math.imul(h, 16777619);
15847
- }
15848
- return (h >>> 0).toString(16).padStart(8, "0");
15849
- }
15850
- function metaPath(workspace) {
15851
- return nodePath.join(PATHS.COLBERT_META_DIR, `${metaHashForWorkspace(workspace)}.json`);
15852
- }
15853
- /** Read the sidecar metadata for a workspace (null if none yet). */
15854
- async function readColbertMeta(workspace) {
15855
- try {
15856
- const raw = await fs.readFile(metaPath(workspace), "utf8");
15857
- const parsed = JSON.parse(raw);
15858
- if (parsed && typeof parsed === "object" && typeof parsed.status === "string") return parsed;
15859
- return null;
15860
- } catch {
15861
- return null;
15862
- }
15863
- }
15864
- /**
15865
- * Per-workspace write serializer. `runInit` issues a pre-spawn
15866
- * `building` write, an `onSpawn` write that patches in the colgrep child
15867
- * PID, and a final `ready`/`failed` write. Chaining them per workspace
15868
- * guarantees the final write lands AFTER the (fire-and-forget) onSpawn
15869
- * write, so a `ready` result is never clobbered back to `building` by a
15870
- * late atomic-rename.
15871
- */
15872
- const _metaWriteChains = /* @__PURE__ */ new Map();
15873
- /** Atomically write the sidecar metadata for a workspace (serialized). */
15874
- async function writeColbertMeta(meta) {
15875
- const key = metaHashForWorkspace(meta.workspace);
15876
- const next = (_metaWriteChains.get(key) ?? Promise.resolve()).then(() => writeColbertMetaUnchained(meta));
15877
- _metaWriteChains.set(key, next.then(() => void 0, () => void 0));
15878
- return next;
15879
- }
15880
- async function writeColbertMetaUnchained(meta) {
15881
- await fs.mkdir(PATHS.COLBERT_META_DIR, { recursive: true });
15882
- const dest = metaPath(meta.workspace);
15883
- const tmp = `${dest}.${process$1.pid}.${Math.random().toString(16).slice(2, 10)}.tmp`;
15884
- try {
15885
- await fs.writeFile(tmp, JSON.stringify(meta, null, 2));
15886
- await fs.rename(tmp, dest);
15887
- } catch (err) {
15888
- await fs.rm(tmp, { force: true }).catch(() => {});
15889
- throw err;
15890
- }
15891
- }
15892
- /**
15893
- * Whether a COMPLETED colgrep index exists on disk for this workspace.
15894
- * The preflight uses this to distinguish `building`/`absent` (no
15895
- * completed index → don't spawn a foreground colgrep) from a real
15896
- * index. We scan COLGREP_DATA_DIR for any per-project dir containing a
15897
- * `project.json` whose canonical path matches this workspace AND an
15898
- * `index/metadata.json` marker.
15899
- */
15900
- async function completedIndexOnDisk(workspace) {
15901
- const indicesDir = PATHS.COLBERT_INDICES_DIR;
15902
- let names;
15903
- try {
15904
- names = await fs.readdir(indicesDir);
15905
- } catch {
15906
- return false;
15907
- }
15908
- const wantCanonical = await realpathForCompare(workspace);
15909
- for (const name of names) {
15910
- if (name === ".gh-router-meta") continue;
15911
- const projJson = nodePath.join(indicesDir, name, "project.json");
15912
- let proj;
15913
- try {
15914
- proj = JSON.parse(await fs.readFile(projJson, "utf8"));
15915
- } catch {
15916
- continue;
15917
- }
15918
- const projPath = proj.path ?? proj.project_path;
15919
- if (!projPath) continue;
15920
- if (await realpathForCompare(projPath) !== wantCanonical) continue;
15921
- if (existsSync(nodePath.join(indicesDir, name, "index", "metadata.json"))) return true;
15922
- if (existsSync(nodePath.join(indicesDir, name, "index"))) try {
15923
- if ((await fs.readdir(nodePath.join(indicesDir, name, "index"))).length > 0) return true;
15924
- } catch {}
15925
- }
15926
- return false;
15927
- }
15928
- function canonicalForCompare(p) {
15929
- return process$1.platform === "win32" ? nodePath.resolve(p).toLowerCase().replace(/\\/g, "/") : nodePath.resolve(p);
15930
- }
15931
- /** Sync realpath-aware canonicalization (sibling of `realpathForCompare`,
15932
- * for the on-a-timer inactivity probe which must be synchronous). */
15933
- function canonicalRealpathSync(p) {
15934
- try {
15935
- return canonicalForCompare(realpathSync(p));
15936
- } catch {
15937
- return canonicalForCompare(p);
15938
- }
15939
- }
15940
- /** Recursive (bytes, fileCount) of a directory; sync + best-effort. A
15941
- * colgrep index is a bounded set of shards so the walk stays small. */
15942
- function dirSizeSync(dir) {
15943
- let bytes = 0;
15944
- let count = 0;
15945
- let entries;
15946
- try {
15947
- entries = readdirSync(dir, { withFileTypes: true });
15948
- } catch {
15949
- return [0, 0];
15950
- }
15951
- for (const e of entries) {
15952
- const p = nodePath.join(dir, e.name);
15953
- if (e.isDirectory()) {
15954
- const [b, c] = dirSizeSync(p);
15955
- bytes += b;
15956
- count += c;
15957
- } else try {
15958
- bytes += statSync(p).size;
15959
- count += 1;
15960
- } catch {}
15961
- }
15962
- return [bytes, count];
15963
- }
15964
- /**
15965
- * (sync) Progress signature of a workspace's colgrep index dir for the init
15966
- * inactivity watchdog: `${totalBytes}:${fileCount}` of the project dir, or
15967
- * `null` if it isn't on disk yet. colgrep is SILENT on a non-TTY pipe
15968
- * during the (potentially multi-hour) encode phase, so output is useless as
15969
- * a progress signal — but it writes index shards incrementally, so a
15970
- * changing signature means "still progressing" and a frozen one means
15971
- * "hung". Successive signatures drive the watchdog: change ⇒ re-arm, frozen
15972
- * ⇒ kill. Sync because it's called from a `setTimeout` (not awaited).
15973
- */
15974
- function indexDirSignature(workspace) {
15975
- const indicesDir = PATHS.COLBERT_INDICES_DIR;
15976
- let names;
15977
- try {
15978
- names = readdirSync(indicesDir);
15979
- } catch {
15980
- return null;
15981
- }
15982
- const want = canonicalRealpathSync(workspace);
15983
- for (const name of names) {
15984
- if (name === ".gh-router-meta") continue;
15985
- const dir = nodePath.join(indicesDir, name);
15986
- let proj;
15987
- try {
15988
- proj = JSON.parse(readFileSync(nodePath.join(dir, "project.json"), "utf8"));
15989
- } catch {
15990
- continue;
15991
- }
15992
- const projPath = proj.path ?? proj.project_path;
15993
- if (!projPath || canonicalRealpathSync(projPath) !== want) continue;
15994
- const [bytes, count] = dirSizeSync(dir);
15995
- return `${bytes}:${count}`;
15996
- }
15997
- return null;
15998
- }
15999
- /**
16000
- * Realpath-aware canonicalization for matching a workspace against
16001
- * colgrep's stored `project_path`. colgrep stores the OS realpath (e.g.
16002
- * macOS `/tmp` → `/private/tmp`, Windows 8.3 short names), so a plain
16003
- * `path.resolve` comparison misses. Falls back to `canonicalForCompare`
16004
- * when realpath fails (path doesn't exist yet).
16005
- */
16006
- async function realpathForCompare(p) {
16007
- try {
16008
- return canonicalForCompare(await fs.realpath(p));
16009
- } catch {
16010
- return canonicalForCompare(p);
16011
- }
16012
- }
16013
- /**
16014
- * Compute the freshness verdict for a query against a workspace.
16015
- *
16016
- * Routing (per the dropped-fallback contract):
16017
- * - `failed` — sidecar says failed → caller returns isError.
16018
- * - `building` — a tracked init is live OR no completed index on disk
16019
- * → caller returns building notice (NO results).
16020
- * - `absent` — never indexed → caller kicks a debounced background
16021
- * init, returns absent (isError).
16022
- * - `stale` — ready but HEAD moved / tree dirty since index → caller
16023
- * returns stale notice (NO results, NO re-search).
16024
- * - `fresh` — ready + completed index + HEAD matches + not newly
16025
- * dirty → caller spawns colgrep search.
16026
- */
16027
- async function freshnessVerdict(workspace) {
16028
- const meta = await readColbertMeta(workspace);
16029
- if (!meta || meta.status === "absent") return {
16030
- verdict: "absent",
16031
- meta
16032
- };
16033
- if (meta.status === "failed") return {
16034
- verdict: "failed",
16035
- meta
16036
- };
16037
- if (meta.status === "building") {
16038
- const pid = typeof meta.buildPid === "number" ? meta.buildPid : 0;
16039
- if (isInitInFlight(workspace) || pid > 0 && isPidAlive(pid)) return {
16040
- verdict: "building",
16041
- meta
16042
- };
16043
- const startedMs = meta.lastIndexedAt ? Date.parse(meta.lastIndexedAt) : NaN;
16044
- if (Number.isFinite(startedMs) && Date.now() - startedMs < BUILD_SPAWN_GRACE_MS) return {
16045
- verdict: "building",
16046
- meta
16047
- };
16048
- if (!await completedIndexOnDisk(workspace)) return {
16049
- verdict: "crashed",
16050
- meta
16051
- };
16052
- }
16053
- if (!await completedIndexOnDisk(workspace)) return {
16054
- verdict: "building",
16055
- meta
16056
- };
16057
- const git = await gitState(workspace);
16058
- if (!git.isRepo) return {
16059
- verdict: "fresh",
16060
- meta
16061
- };
16062
- const headMoved = meta.lastIndexedHead !== void 0 && git.head !== meta.lastIndexedHead;
16063
- const newlyDirty = git.dirty && meta.lastIndexedDirty !== true;
16064
- if (headMoved || newlyDirty) return {
16065
- verdict: "stale",
16066
- meta,
16067
- head: git.head,
16068
- dirty: git.dirty
16069
- };
16070
- return {
16071
- verdict: "fresh",
16072
- meta,
16073
- head: git.head,
16074
- dirty: git.dirty
16075
- };
16076
- }
16077
- /** Cheap, bounded git probe via the native-exe runner. */
16078
- async function gitState(workspace) {
16079
- const git = resolveExecutable("git");
16080
- if (!git) return { isRepo: false };
16081
- try {
16082
- const inside = await runManagedExeCapture(git, [
16083
- "-C",
16084
- workspace,
16085
- "rev-parse",
16086
- "--is-inside-work-tree"
16087
- ], {
16088
- timeoutMs: GIT_TIMEOUT_MS,
16089
- maxStdoutBytes: 64 * 1024
16090
- });
16091
- if (inside.code !== 0 || inside.stdout.trim() !== "true") return { isRepo: false };
16092
- const head = await runManagedExeCapture(git, [
16093
- "-C",
16094
- workspace,
16095
- "rev-parse",
16096
- "HEAD"
16097
- ], {
16098
- timeoutMs: GIT_TIMEOUT_MS,
16099
- maxStdoutBytes: 64 * 1024
16100
- });
16101
- const status = await runManagedExeCapture(git, [
16102
- "-C",
16103
- workspace,
16104
- "status",
16105
- "--porcelain"
16106
- ], {
16107
- timeoutMs: GIT_TIMEOUT_MS,
16108
- maxStdoutBytes: 1024 * 1024
16109
- });
16110
- return {
16111
- isRepo: true,
16112
- head: head.code === 0 ? head.stdout.trim() || void 0 : void 0,
16113
- dirty: status.code === 0 ? status.stdout.trim().length > 0 : void 0
16114
- };
16115
- } catch {
16116
- return { isRepo: false };
16117
- }
16118
- }
16119
- const _initInFlight = /* @__PURE__ */ new Set();
16120
- /** True iff a background init for this workspace is already in flight. */
16121
- function isInitInFlight(workspace) {
16122
- return _initInFlight.has(initKey(workspace));
16123
- }
16124
- /** Mark a background init started (debounce). Returns false if already running. */
16125
- function tryClaimInit(workspace) {
16126
- const k = initKey(workspace);
16127
- if (_initInFlight.has(k)) return false;
16128
- _initInFlight.add(k);
16129
- return true;
16130
- }
16131
- /** Release the debounce claim (call in the init's finally). */
16132
- function releaseInit(workspace) {
16133
- _initInFlight.delete(initKey(workspace));
16134
- }
16135
- function initKey(workspace) {
16136
- return `${MODEL_ID}::${canonicalForCompare(workspace)}`;
16137
- }
16138
-
16139
15864
  //#endregion
16140
15865
  //#region src/lib/toolbelt/extract.ts
16141
15866
  function baseName(p) {
@@ -16340,6 +16065,14 @@ function colgrepBinaryPath() {
16340
16065
  function colbertModelDir() {
16341
16066
  return nodePath.join(PATHS.COLBERT_MODELS_DIR, "LateOn-Code-edge", modelDirName());
16342
16067
  }
16068
+ /**
16069
+ * Canonical model argument for every colgrep invocation and project lookup.
16070
+ * colgrep hashes the raw model string, so slash variants fork one physical
16071
+ * index into distinct project keys on Windows.
16072
+ */
16073
+ function canonicalColbertModelDir() {
16074
+ return nodePath.resolve(colbertModelDir());
16075
+ }
16343
16076
  /** Absolute path the provisioned ORT dylib lives at. */
16344
16077
  function colbertOrtDylibPath() {
16345
16078
  const lib = ortLibAsset()?.member ?? "libonnxruntime.so";
@@ -16462,7 +16195,7 @@ async function provisionColbert() {
16462
16195
  result.status = "incomplete";
16463
16196
  });
16464
16197
  if (result.binaryPath && result.ortDylibPath && result.modelDir) {
16465
- const smoke = await runSmokeTest(result.binaryPath, result.ortDylibPath, result.modelDir);
16198
+ const smoke = await runSmokeTest(result.binaryPath, result.ortDylibPath);
16466
16199
  if (smoke.ok) {
16467
16200
  await writeFile(smokeMarkerPath(), expectedSmokeMarker()).catch(() => {});
16468
16201
  result.status = "ready";
@@ -16591,7 +16324,7 @@ async function sidecarMatches(sidecar, sha256) {
16591
16324
  * scanning stderr for colgrep's exact "ignoring" warning — if present,
16592
16325
  * the dylib didn't load and we fail the smoke test even on exit 0.
16593
16326
  */
16594
- async function runSmokeTest(binaryPath, ortDylibPath, modelDir) {
16327
+ async function runSmokeTest(binaryPath, ortDylibPath) {
16595
16328
  const tmp = nodePath.join(PATHS.COLBERT_DIR, `smoke-${process$1.pid}-${randomBytes(4).toString("hex")}`);
16596
16329
  const fixtureDir = nodePath.join(tmp, "fixture");
16597
16330
  const dataDir = nodePath.join(tmp, "data");
@@ -16620,7 +16353,7 @@ async function runSmokeTest(binaryPath, ortDylibPath, modelDir) {
16620
16353
  "never",
16621
16354
  "--force-cpu",
16622
16355
  "--model",
16623
- modelDir,
16356
+ canonicalColbertModelDir(),
16624
16357
  "-y",
16625
16358
  "-k",
16626
16359
  "1",
@@ -16658,6 +16391,395 @@ async function runSmokeTest(binaryPath, ortDylibPath, modelDir) {
16658
16391
  }
16659
16392
  }
16660
16393
 
16394
+ //#endregion
16395
+ //#region src/lib/colbert/index-store.ts
16396
+ const GIT_TIMEOUT_MS = 4e3;
16397
+ /** Grace window after a `building` write before a workspace with no live
16398
+ * build PID is declared `crashed` — covers the cross-process window where
16399
+ * one proxy wrote `building` but hasn't yet recorded the colgrep child PID. */
16400
+ const BUILD_SPAWN_GRACE_MS = 3e4;
16401
+ /**
16402
+ * Hash a workspace path the same way the metadata sidecar is keyed.
16403
+ * NOTE: this is the ROUTER-OWNED meta key, independent of colgrep's
16404
+ * internal xxh3 physical-dir key (we never need to predict colgrep's
16405
+ * key because we pass the workspace as colgrep's PATH arg and let it
16406
+ * route). A stable sha256-prefix of the canonical path is sufficient.
16407
+ */
16408
+ function metaHashForWorkspace(workspace) {
16409
+ const canonical = process$1.platform === "win32" ? nodePath.resolve(workspace).toLowerCase().replace(/\\/g, "/") : nodePath.resolve(workspace);
16410
+ let h = 2166136261;
16411
+ for (let i = 0; i < canonical.length; i++) {
16412
+ h ^= canonical.charCodeAt(i);
16413
+ h = Math.imul(h, 16777619);
16414
+ }
16415
+ return (h >>> 0).toString(16).padStart(8, "0");
16416
+ }
16417
+ function metaPath(workspace) {
16418
+ return nodePath.join(PATHS.COLBERT_META_DIR, `${metaHashForWorkspace(workspace)}.json`);
16419
+ }
16420
+ /** Read the sidecar metadata for a workspace (null if none yet). */
16421
+ async function readColbertMeta(workspace) {
16422
+ try {
16423
+ const raw = await fs.readFile(metaPath(workspace), "utf8");
16424
+ const parsed = JSON.parse(raw);
16425
+ if (parsed && typeof parsed === "object" && typeof parsed.status === "string") return parsed;
16426
+ return null;
16427
+ } catch {
16428
+ return null;
16429
+ }
16430
+ }
16431
+ /**
16432
+ * Per-workspace write serializer. `runInit` issues a pre-spawn
16433
+ * `building` write, an `onSpawn` write that patches in the colgrep child
16434
+ * PID, and a final `ready`/`failed` write. Chaining them per workspace
16435
+ * guarantees the final write lands AFTER the (fire-and-forget) onSpawn
16436
+ * write, so a `ready` result is never clobbered back to `building` by a
16437
+ * late atomic-rename.
16438
+ */
16439
+ const _metaWriteChains = /* @__PURE__ */ new Map();
16440
+ /** Atomically write the sidecar metadata for a workspace (serialized). */
16441
+ async function writeColbertMeta(meta) {
16442
+ const key = metaHashForWorkspace(meta.workspace);
16443
+ const next = (_metaWriteChains.get(key) ?? Promise.resolve()).then(() => writeColbertMetaUnchained(meta));
16444
+ _metaWriteChains.set(key, next.then(() => void 0, () => void 0));
16445
+ return next;
16446
+ }
16447
+ async function writeColbertMetaUnchained(meta) {
16448
+ await fs.mkdir(PATHS.COLBERT_META_DIR, { recursive: true });
16449
+ const dest = metaPath(meta.workspace);
16450
+ const tmp = `${dest}.${process$1.pid}.${Math.random().toString(16).slice(2, 10)}.tmp`;
16451
+ try {
16452
+ await fs.writeFile(tmp, JSON.stringify(meta, null, 2));
16453
+ await fs.rename(tmp, dest);
16454
+ } catch (err) {
16455
+ await fs.rm(tmp, { force: true }).catch(() => {});
16456
+ throw err;
16457
+ }
16458
+ }
16459
+ /**
16460
+ * Whether a COMPLETED colgrep index exists on disk for this workspace.
16461
+ * The preflight uses this to distinguish `building`/`absent` (no
16462
+ * completed index → don't spawn a foreground colgrep) from a real
16463
+ * index. We scan COLGREP_DATA_DIR for any per-project dir containing a
16464
+ * `project.json` whose canonical path matches this workspace AND an
16465
+ * `index/metadata.json` marker.
16466
+ */
16467
+ async function colbertProjectDir(workspace) {
16468
+ const indicesDir = PATHS.COLBERT_INDICES_DIR;
16469
+ let names;
16470
+ try {
16471
+ names = await fs.readdir(indicesDir);
16472
+ } catch {
16473
+ return null;
16474
+ }
16475
+ const wantCanonical = await realpathForCompare(workspace);
16476
+ for (const name of names) {
16477
+ if (name === ".gh-router-meta" || name.includes(".corrupt-")) continue;
16478
+ const dir = nodePath.join(indicesDir, name);
16479
+ let proj;
16480
+ try {
16481
+ proj = JSON.parse(await fs.readFile(nodePath.join(dir, "project.json"), "utf8"));
16482
+ } catch {
16483
+ continue;
16484
+ }
16485
+ const projPath = proj.path ?? proj.project_path;
16486
+ if (!projPath || await realpathForCompare(projPath) !== wantCanonical) continue;
16487
+ if (proj.model !== canonicalColbertModelDir()) continue;
16488
+ return dir;
16489
+ }
16490
+ return null;
16491
+ }
16492
+ /** Validate the contiguous embedding interval encoded by PLAID shard metadata. */
16493
+ function validateIndexIntegrity(projectIndexDir) {
16494
+ let names;
16495
+ try {
16496
+ names = readdirSync(projectIndexDir).filter((name) => /^\d+\.metadata\.json$/.test(name));
16497
+ } catch (err) {
16498
+ return err.code === "ENOENT" ? { verdict: "not-built" } : {
16499
+ verdict: "corrupt",
16500
+ reason: "index directory unreadable"
16501
+ };
16502
+ }
16503
+ if (names.length === 0) return { verdict: "not-built" };
16504
+ const intervals = [];
16505
+ for (const name of names) try {
16506
+ const parsed = JSON.parse(readFileSync(nodePath.join(projectIndexDir, name), "utf8"));
16507
+ const start = parsed.embedding_offset;
16508
+ const count = parsed.num_embeddings;
16509
+ if (typeof start !== "number" || typeof count !== "number" || !Number.isSafeInteger(start) || !Number.isSafeInteger(count) || start < 0 || count < 0) return {
16510
+ verdict: "corrupt",
16511
+ reason: `invalid shard metadata: ${name}`
16512
+ };
16513
+ intervals.push({
16514
+ start,
16515
+ count
16516
+ });
16517
+ } catch {
16518
+ return {
16519
+ verdict: "corrupt",
16520
+ reason: `unreadable shard metadata: ${name}`
16521
+ };
16522
+ }
16523
+ intervals.sort((a, b) => a.start - b.start || a.count - b.count);
16524
+ let cursorEnd = 0;
16525
+ let sum = 0;
16526
+ let gapped = false;
16527
+ for (const interval of intervals) {
16528
+ if (interval.start < cursorEnd) return {
16529
+ verdict: "corrupt",
16530
+ reason: "overlapping shard intervals"
16531
+ };
16532
+ if (interval.start > cursorEnd) gapped = true;
16533
+ cursorEnd = interval.start + interval.count;
16534
+ sum += interval.count;
16535
+ }
16536
+ if (gapped) return {
16537
+ verdict: "suspect",
16538
+ reason: "gap between shard intervals"
16539
+ };
16540
+ return {
16541
+ verdict: "coherent",
16542
+ shardCount: intervals.length,
16543
+ embeddingCount: sum
16544
+ };
16545
+ }
16546
+ async function completedIndexOnDisk(workspace) {
16547
+ const projectDir = await colbertProjectDir(workspace);
16548
+ if (!projectDir) return false;
16549
+ return validateIndexIntegrity(nodePath.join(projectDir, "index")).verdict !== "not-built";
16550
+ }
16551
+ function canonicalForCompare(p) {
16552
+ return process$1.platform === "win32" ? nodePath.resolve(p).toLowerCase().replace(/\\/g, "/") : nodePath.resolve(p);
16553
+ }
16554
+ /** Sync realpath-aware canonicalization (sibling of `realpathForCompare`,
16555
+ * for the on-a-timer inactivity probe which must be synchronous). */
16556
+ function canonicalRealpathSync(p) {
16557
+ try {
16558
+ return canonicalForCompare(realpathSync(p));
16559
+ } catch {
16560
+ return canonicalForCompare(p);
16561
+ }
16562
+ }
16563
+ /** Recursive (bytes, fileCount) of a directory; sync + best-effort. A
16564
+ * colgrep index is a bounded set of shards so the walk stays small. */
16565
+ function dirSizeSync(dir) {
16566
+ let bytes = 0;
16567
+ let count = 0;
16568
+ let entries;
16569
+ try {
16570
+ entries = readdirSync(dir, { withFileTypes: true });
16571
+ } catch {
16572
+ return [0, 0];
16573
+ }
16574
+ for (const e of entries) {
16575
+ const p = nodePath.join(dir, e.name);
16576
+ if (e.isDirectory()) {
16577
+ const [b, c] = dirSizeSync(p);
16578
+ bytes += b;
16579
+ count += c;
16580
+ } else try {
16581
+ bytes += statSync(p).size;
16582
+ count += 1;
16583
+ } catch {}
16584
+ }
16585
+ return [bytes, count];
16586
+ }
16587
+ /**
16588
+ * (sync) Progress signature of a workspace's colgrep index dir for the init
16589
+ * inactivity watchdog: `${totalBytes}:${fileCount}` of the project dir, or
16590
+ * `null` if it isn't on disk yet. colgrep is SILENT on a non-TTY pipe
16591
+ * during the (potentially multi-hour) encode phase, so output is useless as
16592
+ * a progress signal — but it writes index shards incrementally, so a
16593
+ * changing signature means "still progressing" and a frozen one means
16594
+ * "hung". Successive signatures drive the watchdog: change ⇒ re-arm, frozen
16595
+ * ⇒ kill. Sync because it's called from a `setTimeout` (not awaited).
16596
+ */
16597
+ function indexDirSignature(workspace) {
16598
+ const indicesDir = PATHS.COLBERT_INDICES_DIR;
16599
+ let names;
16600
+ try {
16601
+ names = readdirSync(indicesDir);
16602
+ } catch {
16603
+ return null;
16604
+ }
16605
+ const want = canonicalRealpathSync(workspace);
16606
+ for (const name of names) {
16607
+ if (name === ".gh-router-meta") continue;
16608
+ if (name.includes(".corrupt-")) continue;
16609
+ const dir = nodePath.join(indicesDir, name);
16610
+ let proj;
16611
+ try {
16612
+ proj = JSON.parse(readFileSync(nodePath.join(dir, "project.json"), "utf8"));
16613
+ } catch {
16614
+ continue;
16615
+ }
16616
+ const projPath = proj.path ?? proj.project_path;
16617
+ if (!projPath || canonicalRealpathSync(projPath) !== want) continue;
16618
+ const [bytes, count] = dirSizeSync(dir);
16619
+ return `${bytes}:${count}`;
16620
+ }
16621
+ return null;
16622
+ }
16623
+ /**
16624
+ * Realpath-aware canonicalization for matching a workspace against
16625
+ * colgrep's stored `project_path`. colgrep stores the OS realpath (e.g.
16626
+ * macOS `/tmp` → `/private/tmp`, Windows 8.3 short names), so a plain
16627
+ * `path.resolve` comparison misses. Falls back to `canonicalForCompare`
16628
+ * when realpath fails (path doesn't exist yet).
16629
+ */
16630
+ async function realpathForCompare(p) {
16631
+ try {
16632
+ return canonicalForCompare(await fs.realpath(p));
16633
+ } catch {
16634
+ return canonicalForCompare(p);
16635
+ }
16636
+ }
16637
+ /**
16638
+ * Compute the freshness verdict for a query against a workspace.
16639
+ *
16640
+ * Routing (per the dropped-fallback contract):
16641
+ * - `failed` — sidecar says failed → caller returns isError.
16642
+ * - `building` — a tracked init is live OR no completed index on disk
16643
+ * → caller returns building notice (NO results).
16644
+ * - `absent` — never indexed → caller kicks a debounced background
16645
+ * init, returns absent (isError).
16646
+ * - `stale` — ready but HEAD moved / tree dirty since index → caller
16647
+ * returns stale notice (NO results, NO re-search).
16648
+ * - `fresh` — ready + completed index + HEAD matches + not newly
16649
+ * dirty → caller spawns colgrep search.
16650
+ */
16651
+ async function freshnessVerdict(workspace) {
16652
+ const meta = await readColbertMeta(workspace);
16653
+ if (!meta || meta.status === "absent") return {
16654
+ verdict: "absent",
16655
+ meta
16656
+ };
16657
+ if (meta.status === "failed") return {
16658
+ verdict: "failed",
16659
+ meta
16660
+ };
16661
+ if (meta.status === "building") {
16662
+ const pid = typeof meta.buildPid === "number" ? meta.buildPid : 0;
16663
+ if (isInitInFlight(workspace) || pid > 0 && isPidAlive(pid)) return {
16664
+ verdict: "building",
16665
+ meta
16666
+ };
16667
+ const startedMs = meta.lastIndexedAt ? Date.parse(meta.lastIndexedAt) : NaN;
16668
+ if (Number.isFinite(startedMs) && Date.now() - startedMs < BUILD_SPAWN_GRACE_MS) return {
16669
+ verdict: "building",
16670
+ meta
16671
+ };
16672
+ if (!await completedIndexOnDisk(workspace)) return {
16673
+ verdict: "crashed",
16674
+ meta
16675
+ };
16676
+ }
16677
+ const projectDir = await colbertProjectDir(workspace);
16678
+ if (!projectDir) return {
16679
+ verdict: "building",
16680
+ meta
16681
+ };
16682
+ const integrity = validateIndexIntegrity(nodePath.join(projectDir, "index"));
16683
+ if (integrity.verdict === "not-built") return {
16684
+ verdict: "building",
16685
+ meta
16686
+ };
16687
+ if (integrity.verdict === "corrupt") return {
16688
+ verdict: "corrupt",
16689
+ meta
16690
+ };
16691
+ if (integrity.verdict === "suspect") return {
16692
+ verdict: "stale",
16693
+ meta
16694
+ };
16695
+ const binarySha = colgrepBinAsset()?.sha256;
16696
+ const ortSha = ortLibAsset()?.sha256;
16697
+ if (binarySha && meta.binarySha !== binarySha || ortSha && meta.ortSha !== ortSha) return {
16698
+ verdict: "stale",
16699
+ meta
16700
+ };
16701
+ const git = await gitState(workspace);
16702
+ if (!git.isRepo) return {
16703
+ verdict: "fresh",
16704
+ meta
16705
+ };
16706
+ const headMoved = meta.lastIndexedHead !== void 0 && git.head !== meta.lastIndexedHead;
16707
+ const newlyDirty = git.dirty && meta.lastIndexedDirty !== true;
16708
+ if (headMoved || newlyDirty) return {
16709
+ verdict: "stale",
16710
+ meta,
16711
+ head: git.head,
16712
+ dirty: git.dirty
16713
+ };
16714
+ return {
16715
+ verdict: "fresh",
16716
+ meta,
16717
+ head: git.head,
16718
+ dirty: git.dirty
16719
+ };
16720
+ }
16721
+ /** Cheap, bounded git probe via the native-exe runner. */
16722
+ async function gitState(workspace) {
16723
+ const git = resolveExecutable("git");
16724
+ if (!git) return { isRepo: false };
16725
+ try {
16726
+ const inside = await runManagedExeCapture(git, [
16727
+ "-C",
16728
+ workspace,
16729
+ "rev-parse",
16730
+ "--is-inside-work-tree"
16731
+ ], {
16732
+ timeoutMs: GIT_TIMEOUT_MS,
16733
+ maxStdoutBytes: 64 * 1024
16734
+ });
16735
+ if (inside.code !== 0 || inside.stdout.trim() !== "true") return { isRepo: false };
16736
+ const head = await runManagedExeCapture(git, [
16737
+ "-C",
16738
+ workspace,
16739
+ "rev-parse",
16740
+ "HEAD"
16741
+ ], {
16742
+ timeoutMs: GIT_TIMEOUT_MS,
16743
+ maxStdoutBytes: 64 * 1024
16744
+ });
16745
+ const status = await runManagedExeCapture(git, [
16746
+ "-C",
16747
+ workspace,
16748
+ "status",
16749
+ "--porcelain"
16750
+ ], {
16751
+ timeoutMs: GIT_TIMEOUT_MS,
16752
+ maxStdoutBytes: 1024 * 1024
16753
+ });
16754
+ return {
16755
+ isRepo: true,
16756
+ head: head.code === 0 ? head.stdout.trim() || void 0 : void 0,
16757
+ dirty: status.code === 0 ? status.stdout.trim().length > 0 : void 0
16758
+ };
16759
+ } catch {
16760
+ return { isRepo: false };
16761
+ }
16762
+ }
16763
+ const _initInFlight = /* @__PURE__ */ new Set();
16764
+ /** True iff a background init for this workspace is already in flight. */
16765
+ function isInitInFlight(workspace) {
16766
+ return _initInFlight.has(initKey(workspace));
16767
+ }
16768
+ /** Mark a background init started (debounce). Returns false if already running. */
16769
+ function tryClaimInit(workspace) {
16770
+ const k = initKey(workspace);
16771
+ if (_initInFlight.has(k)) return false;
16772
+ _initInFlight.add(k);
16773
+ return true;
16774
+ }
16775
+ /** Release the debounce claim (call in the init's finally). */
16776
+ function releaseInit(workspace) {
16777
+ _initInFlight.delete(initKey(workspace));
16778
+ }
16779
+ function initKey(workspace) {
16780
+ return `${MODEL_ID}::${canonicalForCompare(workspace)}`;
16781
+ }
16782
+
16661
16783
  //#endregion
16662
16784
  //#region src/lib/colbert/runner.ts
16663
16785
  /** Caller responsiveness budget for a search. A warm search is sub-second;
@@ -16727,6 +16849,10 @@ function makeIndexProgressProbe(workspace) {
16727
16849
  * colgrep writer per workspace" goal as the init debounce. Cleared when the
16728
16850
  * detached search completes. */
16729
16851
  const _searchIndexInFlight = /* @__PURE__ */ new Set();
16852
+ const _initPromises = /* @__PURE__ */ new Map();
16853
+ /** In-flight quarantine deletions, drained by teardown (Windows EBUSY). */
16854
+ const _quarantineRemovals = /* @__PURE__ */ new Set();
16855
+ let _runManagedExeCapture = runManagedExeCapture;
16730
16856
  /** Build the isolating env for any colgrep child (search or init). */
16731
16857
  function colgrepEnv() {
16732
16858
  const ortDir = nodePath.dirname(colbertOrtDylibPath());
@@ -16764,6 +16890,7 @@ async function runSemanticSearch(opts) {
16764
16890
  };
16765
16891
  case "failed": return handleFailure(workspace, fresh.meta, false);
16766
16892
  case "crashed": return handleFailure(workspace, fresh.meta, true);
16893
+ case "corrupt": return repairCorruptIndex(workspace, fresh.meta);
16767
16894
  case "building": return {
16768
16895
  status: "building",
16769
16896
  notice: "semantic index is being built for this workspace; retry shortly (or use code_search now)"
@@ -16796,6 +16923,64 @@ async function runSemanticSearch(opts) {
16796
16923
  * by the inactivity watchdog) retries at most once — re-running a hung build
16797
16924
  * usually hangs again; transient classes retry up to `MAX_FAILED_ATTEMPTS`.
16798
16925
  */
16926
+ async function quarantineProjectDir(projectDir) {
16927
+ const quarantine = `${projectDir}.corrupt-${process$1.pid}-${randomBytes(4).toString("hex")}`;
16928
+ try {
16929
+ await fs.rename(projectDir, quarantine);
16930
+ } catch (err) {
16931
+ consola.debug("colbert: corrupt index quarantine rename failed:", err);
16932
+ return false;
16933
+ }
16934
+ const removal = fs.rm(quarantine, {
16935
+ recursive: true,
16936
+ force: true
16937
+ }).catch((err) => {
16938
+ consola.debug("colbert: corrupt index quarantine cleanup failed:", err);
16939
+ }).finally(() => {
16940
+ _quarantineRemovals.delete(removal);
16941
+ });
16942
+ _quarantineRemovals.add(removal);
16943
+ return true;
16944
+ }
16945
+ async function repairCorruptIndex(workspace, meta) {
16946
+ const wsKey = nodePath.resolve(workspace);
16947
+ const attempts = (meta?.failureClass === "corrupt" ? meta.failedAttempts ?? 0 : 0) + 1;
16948
+ const failedMeta = {
16949
+ workspace,
16950
+ model: meta?.model ?? MODEL_ID,
16951
+ modelRev: meta?.modelRev ?? MODEL_REVISION,
16952
+ binarySha: colgrepBinAsset()?.sha256,
16953
+ ortSha: ortLibAsset()?.sha256,
16954
+ status: "failed",
16955
+ failureClass: "corrupt",
16956
+ failedAttempts: attempts,
16957
+ lastIndexedAt: (/* @__PURE__ */ new Date()).toISOString(),
16958
+ lastIndexedHead: meta?.lastIndexedHead,
16959
+ lastIndexedDirty: meta?.lastIndexedDirty,
16960
+ ownerInstanceId: getColbertInstanceUuid()
16961
+ };
16962
+ if (_searchIndexInFlight.has(wsKey) || isInitInFlight(workspace)) return {
16963
+ status: "building",
16964
+ notice: "semantic index was found corrupt but a writer is still active; returned results are disabled until it exits"
16965
+ };
16966
+ const projectDir = await colbertProjectDir(workspace);
16967
+ if (!projectDir) return handleFailure(workspace, failedMeta, false);
16968
+ if (!await quarantineProjectDir(projectDir)) {
16969
+ await writeColbertMeta(failedMeta).catch(() => {});
16970
+ return {
16971
+ status: "failed",
16972
+ isError: true,
16973
+ notice: "semantic index is corrupt but could not be quarantined; returned lexical results — close active colgrep processes and retry"
16974
+ };
16975
+ }
16976
+ await writeColbertMeta(failedMeta).catch(() => {});
16977
+ if (attempts < 2) kickBackgroundInit(workspace);
16978
+ return {
16979
+ status: "failed",
16980
+ isError: true,
16981
+ notice: attempts < 2 ? "semantic index was found corrupt and quarantined; a clean rebuild was started — retry mode:\"semantic\" shortly" : "semantic index repeatedly failed integrity checks; automatic rebuild is capped — do NOT retry mode:\"semantic\", use lexical search with specific symbol/keyword terms and see proxy logs"
16982
+ };
16983
+ }
16799
16984
  async function handleFailure(workspace, meta, crashedVerdict) {
16800
16985
  const cls = crashedVerdict ? "crashed" : meta?.failureClass ?? "error";
16801
16986
  const attempts = crashedVerdict ? (meta?.failedAttempts ?? 0) + 1 : meta?.failedAttempts ?? 1;
@@ -16812,7 +16997,7 @@ async function handleFailure(workspace, meta, crashedVerdict) {
16812
16997
  lastIndexedDirty: meta?.lastIndexedDirty,
16813
16998
  ownerInstanceId: getColbertInstanceUuid()
16814
16999
  }).catch(() => {});
16815
- const cap = cls === "stuck" ? 2 : MAX_FAILED_ATTEMPTS;
17000
+ const cap = cls === "stuck" || cls === "corrupt" ? 2 : MAX_FAILED_ATTEMPTS;
16816
17001
  const lastMs = lastAt ? Date.parse(lastAt) : NaN;
16817
17002
  const backoffElapsed = !Number.isFinite(lastMs) || Date.now() - lastMs >= FAILED_RETRY_BACKOFF_MS;
16818
17003
  if (attempts < cap && backoffElapsed) {
@@ -16855,7 +17040,7 @@ async function spawnSearch(opts) {
16855
17040
  "never",
16856
17041
  "--force-cpu",
16857
17042
  "--model",
16858
- colbertModelDir(),
17043
+ canonicalColbertModelDir(),
16859
17044
  "-y",
16860
17045
  "-k",
16861
17046
  String(opts.limit)
@@ -16870,7 +17055,7 @@ async function spawnSearch(opts) {
16870
17055
  _searchIndexInFlight.add(wsKey);
16871
17056
  let searchPromise;
16872
17057
  try {
16873
- searchPromise = runManagedExeCapture(binary, args, {
17058
+ searchPromise = _runManagedExeCapture(binary, args, {
16874
17059
  env: colgrepEnv(),
16875
17060
  inactivityTimeoutMs: INIT_STALL_MS,
16876
17061
  onInactivityCheck: makeIndexProgressProbe(opts.workspace),
@@ -16986,14 +17171,24 @@ function buildSnippet(unit) {
16986
17171
  const sig = typeof unit.signature === "string" ? unit.signature.trim() : "";
16987
17172
  const code = typeof unit.code === "string" ? unit.code : "";
16988
17173
  if (!code) return sig;
16989
- const body = code.split("\n").slice(0, 6).join("\n");
16990
- const snippet = sig && !body.startsWith(sig) ? `${sig}\n${body}` : body;
17174
+ const lines = code.split("\n");
17175
+ const body = lines.slice(0, 6).join("\n");
17176
+ const firstBodyLine = lines[0]?.trim() ?? "";
17177
+ const snippet = sig && firstBodyLine !== sig ? `${sig}\n${body}` : body;
16991
17178
  return snippet.length > 600 ? snippet.slice(0, 600) + "…" : snippet;
16992
17179
  }
17180
+ function stripExtendedPathPrefix(value) {
17181
+ if (/^\\\\\?\\UNC\\/i.test(value)) return `\\\\${value.slice(8)}`;
17182
+ if (/^\\\\\?\\[a-z]:\\/i.test(value)) return value.slice(4);
17183
+ return value;
17184
+ }
16993
17185
  function relativize(file, workspace, workspaceReal) {
16994
- for (const base of [workspace, workspaceReal]) try {
16995
- const rel = nodePath.relative(base, file);
16996
- if (rel && !rel.startsWith("..") && !nodePath.isAbsolute(rel)) return rel;
17186
+ const normalizedFile = stripExtendedPathPrefix(file);
17187
+ for (const rawBase of [workspace, workspaceReal]) try {
17188
+ const base = stripExtendedPathPrefix(rawBase);
17189
+ const flavor = /^[a-z]:[\\/]/i.test(base) || base.startsWith("\\\\") ? nodePath.win32 : nodePath;
17190
+ const rel = flavor.relative(base, normalizedFile);
17191
+ if (rel && !rel.startsWith("..") && !flavor.isAbsolute(rel)) return rel;
16997
17192
  } catch {}
16998
17193
  return file;
16999
17194
  }
@@ -17020,9 +17215,31 @@ function clampLimit(limit) {
17020
17215
  function kickBackgroundInit(workspace) {
17021
17216
  if (isInitInFlight(workspace)) return;
17022
17217
  if (!tryClaimInit(workspace)) return;
17023
- runInit(workspace).catch((err) => {
17024
- consola.debug("colbert: background init failed:", err);
17218
+ const key = nodePath.resolve(workspace);
17219
+ const promise = runInit(workspace).catch(async (err) => {
17220
+ releaseInit(workspace);
17221
+ consola.error("colbert: background init failed:", err);
17222
+ const prior = await readColbertMeta(workspace);
17223
+ await writeColbertMeta({
17224
+ workspace,
17225
+ model: prior?.model ?? MODEL_ID,
17226
+ modelRev: prior?.modelRev ?? MODEL_REVISION,
17227
+ binarySha: colgrepBinAsset()?.sha256,
17228
+ ortSha: ortLibAsset()?.sha256,
17229
+ status: "failed",
17230
+ failureClass: "launch",
17231
+ failedAttempts: (prior?.failedAttempts ?? 0) + 1,
17232
+ lastIndexedAt: (/* @__PURE__ */ new Date()).toISOString(),
17233
+ lastIndexedHead: prior?.lastIndexedHead,
17234
+ lastIndexedDirty: prior?.lastIndexedDirty,
17235
+ ownerInstanceId: getColbertInstanceUuid()
17236
+ }).catch((writeErr) => {
17237
+ consola.error("colbert: failed to record background init failure:", writeErr);
17238
+ });
17239
+ }).finally(() => {
17240
+ _initPromises.delete(key);
17025
17241
  });
17242
+ _initPromises.set(key, promise);
17026
17243
  }
17027
17244
  /**
17028
17245
  * Whether the STARTUP auto-kick should fire for a workspace. Skips a build
@@ -17035,25 +17252,72 @@ function kickBackgroundInit(workspace) {
17035
17252
  async function startupKickAllowed(workspace) {
17036
17253
  const meta = await readColbertMeta(workspace);
17037
17254
  if (!meta || meta.status !== "failed") return true;
17038
- if ((meta.failedAttempts ?? 0) >= MAX_FAILED_ATTEMPTS) return false;
17255
+ const cap = meta.failureClass === "stuck" || meta.failureClass === "corrupt" ? 2 : MAX_FAILED_ATTEMPTS;
17256
+ if ((meta.failedAttempts ?? 0) >= cap) return false;
17039
17257
  if (meta.failureClass === "stuck") return false;
17040
17258
  return true;
17041
17259
  }
17260
+ /**
17261
+ * Encoding sessions colgrep may run in parallel: 25% of the machine's
17262
+ * threads, never fewer than 2.
17263
+ *
17264
+ * colgrep defaults this to the FULL CPU count, so a background index build
17265
+ * saturates the machine — the exact opposite of what a background build
17266
+ * should do during an interactive agent session (the proxy even holds a
17267
+ * keep-awake assertion so those sessions run long and unattended). The floor
17268
+ * of 2 keeps a 1- to 7-thread box from dropping to a single session and
17269
+ * taking proportionally forever.
17270
+ *
17271
+ * Override with `GH_ROUTER_COLBERT_PARALLEL` (a positive integer).
17272
+ */
17273
+ function colbertParallelSessions() {
17274
+ const raw = Number(process$1.env.GH_ROUTER_COLBERT_PARALLEL);
17275
+ if (Number.isSafeInteger(raw) && raw > 0) return raw;
17276
+ const threads = os.availableParallelism?.() ?? os.cpus?.().length ?? 4;
17277
+ return Math.max(2, Math.floor(threads * .25));
17278
+ }
17279
+ /**
17280
+ * Persist the parallelism cap into the router-owned colgrep config.
17281
+ *
17282
+ * `--parallel` exists only on the `settings` subcommand — there is no
17283
+ * per-run flag and no env var. It writes `parallel_sessions` to
17284
+ * `<COLGREP_DATA_DIR>/../config.json`, which for us is
17285
+ * `<APP_DIR>/colbert/config.json`: router-owned, and NOT the user's own
17286
+ * colgrep config (verified — after we write ours, a plain `colgrep
17287
+ * settings` with no COLGREP_DATA_DIR still reports `auto`).
17288
+ *
17289
+ * Best-effort: failing here means colgrep encodes at its default (all
17290
+ * threads), which is greedy but not incorrect, so it must never block a
17291
+ * build.
17292
+ */
17293
+ async function applyParallelismCap(binary) {
17294
+ const sessions$1 = colbertParallelSessions();
17295
+ try {
17296
+ await _runManagedExeCapture(binary, [
17297
+ "settings",
17298
+ "--parallel",
17299
+ String(sessions$1)
17300
+ ], {
17301
+ env: colgrepEnv(),
17302
+ timeoutMs: 3e4,
17303
+ maxStdoutBytes: MAX_STDOUT_BYTES
17304
+ });
17305
+ } catch (err) {
17306
+ consola.debug("colbert: could not cap colgrep parallelism:", err);
17307
+ }
17308
+ }
17042
17309
  async function runInit(workspace) {
17043
17310
  const binary = colgrepBinaryPath();
17044
- if (!existsSync(binary)) {
17045
- releaseInit(workspace);
17046
- return;
17047
- }
17048
- if (!existsSync(colbertOrtDylibPath())) {
17049
- releaseInit(workspace);
17050
- return;
17051
- }
17311
+ if (!existsSync(binary)) throw new Error("colgrep binary is missing");
17312
+ if (!existsSync(colbertOrtDylibPath())) throw new Error("ColBERT ONNX runtime is missing");
17313
+ await applyParallelismCap(binary);
17052
17314
  const prior = await readColbertMeta(workspace);
17053
17315
  const baseMeta = {
17054
17316
  workspace,
17055
17317
  model: MODEL_ID,
17056
17318
  modelRev: MODEL_REVISION,
17319
+ binarySha: colgrepBinAsset()?.sha256,
17320
+ ortSha: ortLibAsset()?.sha256,
17057
17321
  status: "building",
17058
17322
  buildPid: void 0,
17059
17323
  ownerInstanceId: getColbertInstanceUuid(),
@@ -17075,7 +17339,7 @@ async function runInit(workspace) {
17075
17339
  "never",
17076
17340
  "--force-cpu",
17077
17341
  "--model",
17078
- colbertModelDir(),
17342
+ canonicalColbertModelDir(),
17079
17343
  workspace
17080
17344
  ];
17081
17345
  const onInactivityCheck = makeIndexProgressProbe(workspace);
@@ -17083,7 +17347,7 @@ async function runInit(workspace) {
17083
17347
  let ok$3 = false;
17084
17348
  let failureClass;
17085
17349
  try {
17086
- const res = await runManagedExeCapture(binary, args, {
17350
+ const res = await _runManagedExeCapture(binary, args, {
17087
17351
  env: colgrepEnv(),
17088
17352
  timeoutMs: INIT_TIMEOUT_MS,
17089
17353
  inactivityTimeoutMs: INIT_STALL_MS,
@@ -17098,10 +17362,18 @@ async function runInit(workspace) {
17098
17362
  }
17099
17363
  });
17100
17364
  ok$3 = !res.stalled && !res.timedOut && res.code === 0;
17101
- if (!ok$3) failureClass = res.stalled || res.timedOut ? "stuck" : "error";
17102
- } catch {
17365
+ if (ok$3) {
17366
+ const projectDir = await colbertProjectDir(workspace);
17367
+ ok$3 = projectDir !== null && validateIndexIntegrity(nodePath.join(projectDir, "index")).verdict === "coherent";
17368
+ if (!ok$3) {
17369
+ failureClass = "corrupt";
17370
+ if (projectDir) await quarantineProjectDir(projectDir);
17371
+ }
17372
+ } else failureClass = res.stalled || res.timedOut ? "stuck" : "error";
17373
+ } catch (err) {
17103
17374
  ok$3 = false;
17104
17375
  failureClass = "launch";
17376
+ consola.error("colbert: init failed to launch:", err);
17105
17377
  } finally {
17106
17378
  releaseInit(workspace);
17107
17379
  }
@@ -17204,8 +17476,10 @@ function lexicalSearchCodeMode(mode) {
17204
17476
  * it both levers: retry `mode:"semantic"` shortly (the index is self-healing
17205
17477
  * in the background) OR re-query now with specific symbol/keyword terms.
17206
17478
  */
17479
+ const FALLBACK_GUIDANCE_MARKER = "retry mode:\"semantic\"";
17480
+ const FALLBACK_GUIDANCE = `${FALLBACK_GUIDANCE_MARKER} shortly, or re-query now with specific symbol/keyword terms`;
17207
17481
  function fallbackNoticeFor(status) {
17208
- const tail = "retry mode:\"semantic\" shortly, or re-query now with specific symbol/keyword terms";
17482
+ const tail = FALLBACK_GUIDANCE;
17209
17483
  switch (status) {
17210
17484
  case "building": return `semantic index is building; returned lexical keyword matches — ${tail}`;
17211
17485
  case "stale": return `semantic index predates the current HEAD/tree (a background re-index was started); returned lexical keyword matches — ${tail}`;
@@ -17224,6 +17498,38 @@ function joinNotice(primary, secondary) {
17224
17498
  if (primary && secondary) return `${primary} (${secondary})`;
17225
17499
  return primary || secondary || void 0;
17226
17500
  }
17501
+ /** Preserve runner-specific context while guaranteeing actionable guidance once. */
17502
+ function semanticFallbackNotice(sem) {
17503
+ if (!sem.notice) return fallbackNoticeFor(sem.status);
17504
+ if (sem.notice.includes(FALLBACK_GUIDANCE_MARKER)) return sem.notice;
17505
+ return `${sem.notice} — ${FALLBACK_GUIDANCE}`;
17506
+ }
17507
+ async function outlinesForSemanticResults(input, results, signal) {
17508
+ if (input.summary === false) return void 0;
17509
+ const seen = /* @__PURE__ */ new Set();
17510
+ const files = [];
17511
+ for (const result of results) {
17512
+ if (seen.has(result.file)) continue;
17513
+ seen.add(result.file);
17514
+ files.push(result.file);
17515
+ if (files.length >= 10) break;
17516
+ }
17517
+ const outlines = [];
17518
+ const deadline = Date.now() + 2e3;
17519
+ const workspace = nodePath.resolve(input.workspace);
17520
+ for (const file of files) {
17521
+ if (signal?.aborted || Date.now() > deadline) break;
17522
+ const abs = nodePath.resolve(workspace, file);
17523
+ const rel = nodePath.relative(workspace, abs);
17524
+ if (!rel || rel.startsWith("..") || nodePath.isAbsolute(rel)) continue;
17525
+ const outlined = await outlineFile(abs, signal);
17526
+ outlines.push({
17527
+ file,
17528
+ outline: outlined.outline
17529
+ });
17530
+ }
17531
+ return outlines;
17532
+ }
17227
17533
  async function runLexical(input, mode, source, signal) {
17228
17534
  const isAst = mode === "ast";
17229
17535
  const resp = await searchCode({
@@ -17285,22 +17591,26 @@ async function runUnifiedCodeSearch(input, signal) {
17285
17591
  notice: joinNotice(r$1.notice, "semantic search errored; returned lexical results")
17286
17592
  };
17287
17593
  }
17288
- if (sem.status === "ready") return {
17289
- source: "semantic",
17290
- results: (sem.results ?? []).map((r$1) => ({
17594
+ if (sem.status === "ready") {
17595
+ const results = (sem.results ?? []).map((r$1) => ({
17291
17596
  file: r$1.file,
17292
17597
  line: r$1.line,
17293
17598
  snippet: r$1.snippet,
17294
17599
  ...r$1.endLine !== void 0 ? { endLine: r$1.endLine } : {},
17295
17600
  ...r$1.name !== void 0 ? { name: r$1.name } : {},
17296
17601
  ...r$1.score !== void 0 ? { score: r$1.score } : {}
17297
- })),
17298
- ...sem.notice ? { notice: sem.notice } : {}
17299
- };
17602
+ }));
17603
+ return {
17604
+ source: "semantic",
17605
+ results,
17606
+ outlines: await outlinesForSemanticResults(input, results, signal),
17607
+ ...sem.notice ? { notice: sem.notice } : {}
17608
+ };
17609
+ }
17300
17610
  const r = await runLexical(input, "lexical", "lexical-fallback", signal);
17301
17611
  return {
17302
17612
  ...r,
17303
- notice: joinNotice(r.notice, fallbackNoticeFor(sem.status))
17613
+ notice: joinNotice(r.notice, semanticFallbackNotice(sem))
17304
17614
  };
17305
17615
  }
17306
17616
 
@@ -18498,7 +18808,7 @@ function logAudit$1(record) {
18498
18808
  try {
18499
18809
  const fs$2 = await import("node:fs/promises");
18500
18810
  const path$1 = await import("node:path");
18501
- const { PATHS: PATHS$1 } = await import("./paths-B5k78n0d.js");
18811
+ const { PATHS: PATHS$1 } = await import("./paths-D20MaHeo.js");
18502
18812
  const dir = path$1.join(PATHS$1.APP_DIR, "browser-mcp");
18503
18813
  await fs$2.mkdir(dir, { recursive: true });
18504
18814
  const line = JSON.stringify({
@@ -25311,7 +25621,14 @@ async function countTokens(body, extraHeaders, callerSignal, retryTransient = fa
25311
25621
  */
25312
25622
  /** Preference-ordered OpenAI frontier reasoning models (SELECTION list). */
25313
25623
  const OPENAI_FRONTIER_MODELS = ["gpt-5.6-sol", "gpt-5.5"];
25314
- /** Models whose shim DEFAULT reasoning effort is xhigh (effort POLICY set). */
25624
+ /** Models whose shim reasoning effort becomes xhigh when the operator opts in
25625
+ * with `GH_ROUTER_FRONTIER_XHIGH_DEFAULT=1` (effort POLICY set).
25626
+ *
25627
+ * This is opt-IN, not the default. The shim maps a client's level to the
25628
+ * identical provider level and injects only `high` when the client sends no
25629
+ * `thinking` block at all; forcing xhigh here would silently override the level
25630
+ * the user chose. The set is retained so the opt-in restores the previous
25631
+ * behavior exactly, targeting the same models it used to. */
25315
25632
  const XHIGH_DEFAULT_SHIM_MODELS = ["gpt-5.6-sol", "gpt-5.5"];
25316
25633
  /** Normalize a model id for policy comparison: strip a leading `vendor/`
25317
25634
  * prefix and any trailing `[...]` decoration(s) (e.g. `[1m]`, `[1m][beta]`)
@@ -25319,7 +25636,8 @@ const XHIGH_DEFAULT_SHIM_MODELS = ["gpt-5.6-sol", "gpt-5.5"];
25319
25636
  function normalizeModelId(id) {
25320
25637
  return (id.includes("/") ? id.slice(id.lastIndexOf("/") + 1) : id).replace(/(?:\[[^\]]*\])+\s*$/, "");
25321
25638
  }
25322
- /** True iff `id` (after normalization) is in the xhigh effort-policy set. */
25639
+ /** True iff `id` (after normalization) is in the xhigh effort-policy set. Only
25640
+ * consulted when `GH_ROUTER_FRONTIER_XHIGH_DEFAULT=1` opts in. */
25323
25641
  function shimDefaultsToXhigh(id) {
25324
25642
  return XHIGH_DEFAULT_SHIM_MODELS.includes(normalizeModelId(id));
25325
25643
  }
@@ -25351,21 +25669,35 @@ function geminiAvailable(source = state) {
25351
25669
  return models.some((m) => /^gemini-3\..*pro/i.test(m.id));
25352
25670
  }
25353
25671
  /**
25354
- * First available OpenAI frontier model in the live catalog (prefer
25355
- * `gpt-5.6-sol`, fall back to `gpt-5.5`). Returns undefined when neither is
25356
- * present. With `requireToolCalls`, only returns a model whose catalog entry
25357
- * advertises `tool_calls`.
25672
+ * First id in `chain` that is present in the live catalog. With
25673
+ * `requireToolCalls`, skips an entry whose catalog record does not advertise
25674
+ * `tool_calls` (strict `!== true`, so absent metadata fails closed). Returns
25675
+ * undefined when the catalog is unavailable or nothing in the chain matches, so
25676
+ * every caller degrades gracefully rather than throwing on a thin catalog.
25677
+ *
25678
+ * Extracted from `resolveOpenAiFrontier` so the per-agent resolvers below share
25679
+ * one walk instead of hand-copying it. Ids are matched EXACTLY against
25680
+ * `catalog.id` — no slug translation, matching the pre-existing behavior.
25358
25681
  */
25359
- function resolveOpenAiFrontier(opts) {
25682
+ function firstPresentInCatalog(chain, opts) {
25360
25683
  const models = state.models?.data;
25361
25684
  if (!models) return void 0;
25362
- for (const id of OPENAI_FRONTIER_MODELS) {
25685
+ for (const id of chain) {
25363
25686
  const found = models.find((m) => m.id === id);
25364
25687
  if (!found) continue;
25365
25688
  if (opts?.requireToolCalls && found.capabilities?.supports?.tool_calls !== true) continue;
25366
25689
  return id;
25367
25690
  }
25368
25691
  }
25692
+ /**
25693
+ * First available OpenAI frontier model in the live catalog (prefer
25694
+ * `gpt-5.6-sol`, fall back to `gpt-5.5`). Returns undefined when neither is
25695
+ * present. With `requireToolCalls`, only returns a model whose catalog entry
25696
+ * advertises `tool_calls`.
25697
+ */
25698
+ function resolveOpenAiFrontier(opts) {
25699
+ return firstPresentInCatalog(OPENAI_FRONTIER_MODELS, opts);
25700
+ }
25369
25701
  function standInToolEnabled() {
25370
25702
  const models = state.models?.data;
25371
25703
  if (!models) return false;
@@ -25374,13 +25706,44 @@ function standInToolEnabled() {
25374
25706
  const hasGeminiPro = geminiAvailable();
25375
25707
  return hasOpenAi && hasOpus && hasGeminiPro;
25376
25708
  }
25377
- /** Return the model for the native OpenAI subagents (implementer, debugger,
25378
- * qa-engineer) iff it is live with tool calls. Prefers `gpt-5.6-sol`, falls
25379
- * back to `gpt-5.5`. One gate governs all three — they need the same frontier
25380
- * model. */
25709
+ /** Model for the native subagents that want the OpenAI frontier coder
25710
+ * (`implementer`, `reviewer`) iff it is live with tool calls. Prefers
25711
+ * `gpt-5.6-sol`, falls back to `gpt-5.5`. Absent those agents omit their
25712
+ * `model:` line and inherit the lead's model. */
25381
25713
  function nativeSubagentModel() {
25382
25714
  return resolveOpenAiFrontier({ requireToolCalls: true });
25383
25715
  }
25716
+ /** Model for `brainstorm`. Absent → inherits the lead's model.
25717
+ *
25718
+ * Leads with Google so the options it generates come from a third lab: the
25719
+ * Anthropic lead is the producer and the OpenAI frontier already backs
25720
+ * `implementer`/`reviewer`, so a same-lab brainstormer would mostly restate
25721
+ * what the lead already thought of. */
25722
+ function brainstormModel() {
25723
+ return firstPresentInCatalog([REVIEW_DEFAULT_MODEL, ...OPENAI_FRONTIER_MODELS], { requireToolCalls: true });
25724
+ }
25725
+ /** Model for `scribe`. Absent → inherits the lead's model.
25726
+ *
25727
+ * Leads with the mid tier: documentation is verifiable prose, not frontier
25728
+ * reasoning. */
25729
+ function scribeModel() {
25730
+ return firstPresentInCatalog(["gpt-5.6-terra", ...OPENAI_FRONTIER_MODELS], { requireToolCalls: true });
25731
+ }
25732
+ /**
25733
+ * Model for `scout` — CHEAP TIER ONLY, with no frontier fallback on purpose.
25734
+ *
25735
+ * `scout` exists so a foreground repository lookup does not run at the lead's
25736
+ * model rates. The usual "absent → omit `model:` and inherit the lead" fallback
25737
+ * would therefore defeat the agent: on a thin or briefly-unavailable catalog it
25738
+ * would silently start answering grep-and-summarize questions on Opus, which is
25739
+ * the exact cost it was added to avoid. Returning undefined here makes the
25740
+ * caller drop the agent instead, so the lead falls back to the CLI's `Explore`
25741
+ * (same behavior as before `scout` existed) rather than to an expensive
25742
+ * impostor wearing the cheap agent's name.
25743
+ */
25744
+ function scoutModel() {
25745
+ return firstPresentInCatalog([EXPLORE_DEFAULT_MODEL, DEFAULT_MODEL], { requireToolCalls: true });
25746
+ }
25384
25747
  /**
25385
25748
  * Gate for the worker tools (`explore`, `review`, `implement`).
25386
25749
  *
@@ -32646,9 +33009,12 @@ function buildAgentPrompt(persona, opts) {
32646
33009
  * of the live catalog). The raw `mcp__<workers>__*` tools are named only
32647
33010
  * as the guarded plumbing the dispatchers call, never as a main-agent
32648
33011
  * interface.
32649
- * - Always names the implementer/debugger/qa-engineer native subagents
32650
- * (they are injected unconditionally); the implementer-vs-`worker-implement`
32651
- * contrast is added only when worker tools are available.
33012
+ * - Always names the implementer/reviewer/brainstorm/scribe native subagents
33013
+ * (they are injected unconditionally, degrading to the lead's model rather
33014
+ * than disappearing); `scout` is named only when `scoutAvailable` is not
33015
+ * false, because it is dropped outright when no cheap-tier model resolves.
33016
+ * The implementer-vs-`worker-implement` contrast is added only when worker
33017
+ * tools are available.
32652
33018
  * - Conditionally lists stand_in only when `standInAvailable`
32653
33019
  * (mirrors `standInToolEnabled()`).
32654
33020
  * - Conditionally lists gh-first-mate only when `agentToolsAvailable`
@@ -32680,7 +33046,7 @@ function buildPeerAwarenessSnippet(opts) {
32680
33046
  const codexCliClause = opts.codexCli ? " `mcp__codex-cli__codex` dispatches to `codex-implementer` (gpt-5.3-codex with workspace-write) for end-to-end coding tasks." : "";
32681
33047
  const para2Parts = [`\`mcp__${searchKey}__code\` is the one-stop code search (no extra model call). Its DEFAULT mode (or \`mode:"semantic"\`) ranks by MEANING via ColBERT over a per-workspace index, the first thing to reach for on intent/concept questions ("where is retry/backoff handled", "how does auth work"); when that index isn't ready it transparently falls back to lexical (the response \`source\` says which engine ran). Forced modes cover the rest: \`lexical\` (BM25F-ranked + tree-sitter, best for exact symbols), \`exact\`, \`regex\`, \`complete\` (exhaustive set), \`ast_pattern\`+\`ast_lang\` for multi-line AST shapes, \`scan\` for a whole-workspace symbol outline, \`multiline\` for cross-line regex. Multiple queries can run in a single turn. The index covers code-shaped files; for unstructured files (logs, \`.csv\`, \`.env*\`, config-only wiring), \`grep\`/\`glob\` still apply.`];
32682
33048
  if (opts.workerToolsAvailable) para2Parts.push(`\`worker-*\` are background Agent subagents (subagent_type) that run the matching worker in its own context and deliver the result as a completion notification, so a long run never blocks the turn: \`worker-explore\` (read-only research), \`worker-review\` (reads the code to verify a change or claim), \`worker-plan\` (ordered implementation plan), \`worker-implement\` (edit/write/bash; ALWAYS runs in an isolated git worktree and returns the diff via a saved patch file; for in-place edits use the \`implementer\` subagent), \`worker-test\` (independent test author; also always worktree-isolated). The raw \`mcp__${workersKey}__*\` tools they call are guarded (a direct main-thread call is redirected to the matching agent); Workers themselves have \`code_search\`.`);
32683
- para2Parts.push(`Three native subagents are always available (Task): \`implementer\` (bounded implementation), \`debugger\` (reproduce + isolate a failure's root cause), and \`qa-engineer\` (review + author/run tests), each in its own context so the lead's context stays free; on gpt-5.6-sol when in the catalog, else the lead's model.`);
33049
+ para2Parts.push(`Native subagents (Task), each in its own context so heavy work never fills yours: \`implementer\` (you know what to build), \`reviewer\` (something exists and you want it assessed, including reproducing and root-causing a failure), \`brainstorm\` (you do not yet know which approach to take)${opts.scoutAvailable === false ? "" : ", `scout` (find or understand something in the repo, cheap)"}, \`scribe\` (docs and ADRs that trail the code).`);
32684
33050
  if (opts.workerToolsAvailable) para2Parts.push(`For a bounded, well-scoped implementation, prefer the \`implementer\` subagent over \`worker-implement\`; reach for \`worker-implement\` only when you specifically need git-worktree isolation, parallel variants, or a throwaway experiment.`);
32685
33051
  if (opts.workerToolsAvailable) para2Parts.push(`\`mcp__${orchestrateKey}__decompose\` composes an open-ended ask into a typed, VERIFIED workflow IR (a strong driver decorrelated by a cross-lab critic, so the decompose step isn't a single point of failure), and \`mcp__${orchestrateKey}__run_workflow\` executes that IR through a frozen kernel delivering max(orchestrated, baseline) over a sealed executable gate, so it never ships worse than a plain single-model run. \`mcp__${orchestrateKey}__verify_workflow\` checks an IR's floor invariants before you run it, and \`mcp__${orchestrateKey}__attest_step\` audits that a finished run's producers were each checked by a different lab. They suit non-trivial, role-separated asks; a trivial ask does not need them.`);
32686
33052
  else para2Parts.push(`\`mcp__${orchestrateKey}__verify_workflow\` statically checks a workflow IR's floor invariants and \`mcp__${orchestrateKey}__attest_step\` audits a run's cross-lab lineage (the \`decompose\`/\`run_workflow\` composer + kernel need the worker backend, unavailable here).`);
@@ -33901,5 +34267,5 @@ function enumerateInjectedMcpToolNames(groupKeys, opts = {}) {
33901
34267
  }
33902
34268
 
33903
34269
  //#endregion
33904
- export { searchWeb as $, UPSTREAM_INACTIVITY_TIMEOUT_MS as $t, trustRepo as A, createResponses as At, TEST_DEFAULT_MODEL as B, CONDENSED_OPERATING_SEQUENCE as Bt, fileLastPromptStore as C, copilotHeaders as Cn, shimDefaultsToXhigh as Ct, repoRoot as D, assembleResponsesPayload as Dt, repoFingerprint as E, getTokenCount as Et, EXPLORE_DEFAULT_MODEL as F, provisionBrowserAssets as Ft, buildEnv as G, buildWorkspaceHeaderJson as Gt, resolveModeDefaults as H, shouldUseInsecureTls as Ht, EXPLORE_DEFAULT_THINKING as I, hasSupportedBrowserInstalled as It, toolbeltEnabled as J, DEFAULT_CLAUDE_MODEL_FALLBACKS as Jt, availableToolCommands as K, collapsePathKeys as Kt, IMPLEMENT_DEFAULT_MODEL as L, provisionAndIndexColbert as Lt, resolveSealedGate as M, MAX_RESPONSE_BODY_BYTES as Mt, BROWSE_DEFAULT_MODEL as N, readResponseBodyCapped as Nt, stopGateEnabledForRepo as O, resolveMcpToolTimeoutMs as Ot, DEFAULT_MODEL as P, parseJsonOrDiagnose as Pt, assetFor as Q, UPSTREAM_FETCH_TIMEOUT_MS as Qt, PLAN_DEFAULT_MODEL as R, extractTarGzMember as Rt, fileFindingsStore as S, copilotBaseUrl as Sn, workerToolsEnabled as St, isSubagentContext as T, state as Tn, createMessages as Tt, resolveWorkerRunOpts as U, ArtifactClient as Ut, appendPlanReminder as V, DEFINITION_OF_GREATNESS as Vt, runWorkerAgent as W, buildWorkspaceHeaderHelperCommand as Wt, vscodeRipgrepPath as X, DEFAULT_CODEX_MODEL_FALLBACKS as Xt, toolbeltSkipSet as Y, DEFAULT_CODEX_MODEL as Yt, TOOLBELT_TOOLS$1 as Z, DEFAULT_PORT as Zt, stopGateDisabled as _, getGitHubUser as _n, browserToolsEnabled as _t, buildPeerAwarenessSnippet as a, setupGitHubAgentToken as an, buildAnthropicErrorEvent as at, stopReviewEnabled as b, forwardError as bn, nativeSubagentModel as bt, personasFor as c, cacheCopilotVersion as cn, logStreamError as ct, buildStopHookCommand as d, filterBetaHeader as dn, handleMcpDelete as dt, generateRandomPort as en, ADVISOR_INTERNAL_TOOL_NAME as et, captureLaunchBaseline as f, isNullish as fn, handleMcpPost as ft, launchBaselineKey as g, getModels as gn, browserCompoundToolsEnabled as gt, injectStopHookIntoSettingsFile as h, sleep as hn, browseAgentEnabled as ht, buildAgentPrompt as i, setupCopilotToken as in, isAdvisorRequested as it, liveExec as j, createChatCompletions as jt, stopReviewStateDir as k, pickEndpoint as kt, buildArtifactOpenHookCommand as l, cacheModels as ln, readIteratorWithTimeout as lt, fileBlockBudget as m, resolveModel as mn, artifactToolsEnabled as mt, MCP_GROUPS as n, getPackageVersion as nn, buildAdvisorStream as nt, buildPeerAwarenessSummary as o, setupGitHubToken as on, buildOpenAIErrorEvent as ot, decideStopHook as p, resolveCodexModel as pn, agentToolsEnabled as pt, buildToolbeltAwareness as q, toolbeltPathOverride as qt, assertMcpToolSurfaceConsistent as r, withInstallLock as rn, injectAdvisorTool as rt, enumerateInjectedMcpToolNames as s, tryRefreshAndRetry as sn, isControllerClosedError as st, GROUP_META as t, pickClaudeDefault as tn, ADVISOR_TOOL_INSTRUCTIONS as tt, buildSessionBindHookCommand as u, cacheVSCodeVersion as un, relayAnthropicStream as ut, stopGateId as v, fetchWithTransientRetry as vn, fleetToolsEnabled as vt, fileReviewDebounce as w, githubHeaders as wn, countTokens as wt, fileBaselineStore as x, GITHUB_API_BASE_URL as xn, standInToolEnabled as xt, stopGatePlanMode as y, HTTPError as yn, geminiAvailable as yt, REVIEW_DEFAULT_MODEL as z, extractZipMember as zt };
33905
- //# sourceMappingURL=peer-mcp-personas-A6PytLD6.js.map
34270
+ export { searchWeb as $, DEFAULT_CODEX_MODEL as $t, trustRepo as A, assembleResponsesPayload as At, TEST_DEFAULT_MODEL as B, provisionAndIndexColbert as Bt, fileLastPromptStore as C, HTTPError as Cn, scribeModel as Ct, repoRoot as D, copilotHeaders as Dn, countTokens as Dt, repoFingerprint as E, copilotBaseUrl as En, shimDefaultsToXhigh as Et, EXPLORE_DEFAULT_MODEL as F, MAX_RESPONSE_BODY_BYTES as Ft, buildEnv as G, DEFINITION_OF_GREATNESS as Gt, resolveModeDefaults as H, extractZipMember as Ht, EXPLORE_DEFAULT_THINKING as I, readResponseBodyCapped as It, toolbeltEnabled as J, buildWorkspaceHeaderHelperCommand as Jt, availableToolCommands as K, shouldUseInsecureTls as Kt, IMPLEMENT_DEFAULT_MODEL as L, parseJsonOrDiagnose as Lt, resolveSealedGate as M, pickEndpoint as Mt, BROWSE_DEFAULT_MODEL as N, createResponses as Nt, stopGateEnabledForRepo as O, githubHeaders as On, createMessages as Ot, DEFAULT_MODEL as P, createChatCompletions as Pt, assetFor as Q, DEFAULT_CLAUDE_MODEL_FALLBACKS as Qt, PLAN_DEFAULT_MODEL as R, provisionBrowserAssets as Rt, fileFindingsStore as S, fetchWithTransientRetry as Sn, scoutModel as St, isSubagentContext as T, GITHUB_API_BASE_URL as Tn, workerToolsEnabled as Tt, resolveWorkerRunOpts as U, warmTreeSitterPool as Ut, appendPlanReminder as V, extractTarGzMember as Vt, runWorkerAgent as W, CONDENSED_OPERATING_SEQUENCE as Wt, vscodeRipgrepPath as X, collapsePathKeys as Xt, toolbeltSkipSet as Y, buildWorkspaceHeaderJson as Yt, TOOLBELT_TOOLS$1 as Z, toolbeltPathOverride as Zt, stopGateDisabled as _, resolveCodexModel as _n, browserCompoundToolsEnabled as _t, buildPeerAwarenessSnippet as a, pickClaudeDefault as an, buildAnthropicErrorEvent as at, stopReviewEnabled as b, getModels as bn, geminiAvailable as bt, personasFor as c, setupCopilotToken as cn, logStreamError as ct, buildStopHookCommand as d, tryRefreshAndRetry as dn, handleMcpDelete as dt, DEFAULT_CODEX_MODEL_FALLBACKS as en, ADVISOR_INTERNAL_TOOL_NAME as et, captureLaunchBaseline as f, cacheCopilotVersion as fn, handleMcpPost as ft, launchBaselineKey as g, isNullish as gn, browseAgentEnabled as gt, injectStopHookIntoSettingsFile as h, filterBetaHeader as hn, brainstormModel as ht, buildAgentPrompt as i, generateRandomPort as in, isAdvisorRequested as it, liveExec as j, resolveMcpToolTimeoutMs as jt, stopReviewStateDir as k, state as kn, getTokenCount as kt, buildArtifactOpenHookCommand as l, setupGitHubAgentToken as ln, readIteratorWithTimeout as lt, fileBlockBudget as m, cacheVSCodeVersion as mn, artifactToolsEnabled as mt, MCP_GROUPS as n, UPSTREAM_FETCH_TIMEOUT_MS as nn, buildAdvisorStream as nt, buildPeerAwarenessSummary as o, getPackageVersion as on, buildOpenAIErrorEvent as ot, decideStopHook as p, cacheModels as pn, agentToolsEnabled as pt, buildToolbeltAwareness as q, ArtifactClient as qt, assertMcpToolSurfaceConsistent as r, UPSTREAM_INACTIVITY_TIMEOUT_MS as rn, injectAdvisorTool as rt, enumerateInjectedMcpToolNames as s, withInstallLock as sn, isControllerClosedError as st, GROUP_META as t, DEFAULT_PORT as tn, ADVISOR_TOOL_INSTRUCTIONS as tt, buildSessionBindHookCommand as u, setupGitHubToken as un, relayAnthropicStream as ut, stopGateId as v, resolveModel as vn, browserToolsEnabled as vt, fileReviewDebounce as w, forwardError as wn, standInToolEnabled as wt, fileBaselineStore as x, getGitHubUser as xn, nativeSubagentModel as xt, stopGatePlanMode as y, sleep as yn, fleetToolsEnabled as yt, REVIEW_DEFAULT_MODEL as z, hasSupportedBrowserInstalled as zt };
34271
+ //# sourceMappingURL=peer-mcp-personas-B-EPdjSE.js.map