github-router 0.3.82 → 0.3.87
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 +1 -1
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/{lifecycle-CQlm3YlF.js → lifecycle-C5fB3ODy.js} +2 -2
- package/dist/{lifecycle-CMPthagV.js → lifecycle-CHjAPu8u.js} +2 -2
- package/dist/{lifecycle-CMPthagV.js.map → lifecycle-CHjAPu8u.js.map} +1 -1
- package/dist/{lifecycle-yaqqtsV1.js → lifecycle-CTLlFU45.js} +54 -10
- package/dist/lifecycle-CTLlFU45.js.map +1 -0
- package/dist/lifecycle-uNpNYzQ_.js +4 -0
- package/dist/main.js +1132 -267
- package/dist/main.js.map +1 -1
- package/dist/{paths-BGx0RpNs.js → paths-Czi0-nEE.js} +1 -1
- package/dist/{paths-yJ97KlKp.js → paths-DWVKYv16.js} +3 -3
- package/dist/paths-DWVKYv16.js.map +1 -0
- package/package.json +1 -1
- package/dist/lifecycle-BL4rWSrT.js +0 -4
- package/dist/lifecycle-yaqqtsV1.js.map +0 -1
- package/dist/paths-yJ97KlKp.js.map +0 -1
package/dist/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as removeOwnClaudeConfigMirror, i as isUnderClaudeConfigMirror, l as writeRuntimeFileSecure, n as ensureClaudeConfigMirror, r as ensurePaths, t as PATHS } from "./paths-
|
|
3
|
-
import {
|
|
4
|
-
import { a as sweepRegistry, i as registerExitHandlers, n as getInstanceUuid, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-
|
|
2
|
+
import { a as removeOwnClaudeConfigMirror, i as isUnderClaudeConfigMirror, l as writeRuntimeFileSecure, n as ensureClaudeConfigMirror, r as ensurePaths, t as PATHS } from "./paths-DWVKYv16.js";
|
|
3
|
+
import { c as resolveExecutable, d as runManagedExeCapture, l as runCommandCapture, n as isPidAlive, o as trackChild, r as registerColbertExitHandlers, s as parseBoolEnv, t as getColbertInstanceUuid, u as runCommandVoid } from "./lifecycle-CTLlFU45.js";
|
|
4
|
+
import { a as sweepRegistry, i as registerExitHandlers, n as getInstanceUuid, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-CHjAPu8u.js";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { defineCommand, runMain } from "citty";
|
|
7
7
|
import consola from "consola";
|
|
@@ -4432,6 +4432,10 @@ const MODEL_ID = "LateOn-Code-edge";
|
|
|
4432
4432
|
//#endregion
|
|
4433
4433
|
//#region src/lib/colbert/index-store.ts
|
|
4434
4434
|
const GIT_TIMEOUT_MS = 4e3;
|
|
4435
|
+
/** Grace window after a `building` write before a workspace with no live
|
|
4436
|
+
* build PID is declared `crashed` — covers the cross-process window where
|
|
4437
|
+
* one proxy wrote `building` but hasn't yet recorded the colgrep child PID. */
|
|
4438
|
+
const BUILD_SPAWN_GRACE_MS = 3e4;
|
|
4435
4439
|
/**
|
|
4436
4440
|
* Hash a workspace path the same way the metadata sidecar is keyed.
|
|
4437
4441
|
* NOTE: this is the ROUTER-OWNED meta key, independent of colgrep's
|
|
@@ -4529,6 +4533,74 @@ async function completedIndexOnDisk(workspace) {
|
|
|
4529
4533
|
function canonicalForCompare(p) {
|
|
4530
4534
|
return process$1.platform === "win32" ? path.resolve(p).toLowerCase().replace(/\\/g, "/") : path.resolve(p);
|
|
4531
4535
|
}
|
|
4536
|
+
/** Sync realpath-aware canonicalization (sibling of `realpathForCompare`,
|
|
4537
|
+
* for the on-a-timer inactivity probe which must be synchronous). */
|
|
4538
|
+
function canonicalRealpathSync(p) {
|
|
4539
|
+
try {
|
|
4540
|
+
return canonicalForCompare(realpathSync(p));
|
|
4541
|
+
} catch {
|
|
4542
|
+
return canonicalForCompare(p);
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
/** Recursive (bytes, fileCount) of a directory; sync + best-effort. A
|
|
4546
|
+
* colgrep index is a bounded set of shards so the walk stays small. */
|
|
4547
|
+
function dirSizeSync(dir) {
|
|
4548
|
+
let bytes = 0;
|
|
4549
|
+
let count = 0;
|
|
4550
|
+
let entries;
|
|
4551
|
+
try {
|
|
4552
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
4553
|
+
} catch {
|
|
4554
|
+
return [0, 0];
|
|
4555
|
+
}
|
|
4556
|
+
for (const e of entries) {
|
|
4557
|
+
const p = path.join(dir, e.name);
|
|
4558
|
+
if (e.isDirectory()) {
|
|
4559
|
+
const [b, c] = dirSizeSync(p);
|
|
4560
|
+
bytes += b;
|
|
4561
|
+
count += c;
|
|
4562
|
+
} else try {
|
|
4563
|
+
bytes += statSync(p).size;
|
|
4564
|
+
count += 1;
|
|
4565
|
+
} catch {}
|
|
4566
|
+
}
|
|
4567
|
+
return [bytes, count];
|
|
4568
|
+
}
|
|
4569
|
+
/**
|
|
4570
|
+
* (sync) Progress signature of a workspace's colgrep index dir for the init
|
|
4571
|
+
* inactivity watchdog: `${totalBytes}:${fileCount}` of the project dir, or
|
|
4572
|
+
* `null` if it isn't on disk yet. colgrep is SILENT on a non-TTY pipe
|
|
4573
|
+
* during the (potentially multi-hour) encode phase, so output is useless as
|
|
4574
|
+
* a progress signal — but it writes index shards incrementally, so a
|
|
4575
|
+
* changing signature means "still progressing" and a frozen one means
|
|
4576
|
+
* "hung". Successive signatures drive the watchdog: change ⇒ re-arm, frozen
|
|
4577
|
+
* ⇒ kill. Sync because it's called from a `setTimeout` (not awaited).
|
|
4578
|
+
*/
|
|
4579
|
+
function indexDirSignature(workspace) {
|
|
4580
|
+
const indicesDir = PATHS.COLBERT_INDICES_DIR;
|
|
4581
|
+
let names;
|
|
4582
|
+
try {
|
|
4583
|
+
names = readdirSync(indicesDir);
|
|
4584
|
+
} catch {
|
|
4585
|
+
return null;
|
|
4586
|
+
}
|
|
4587
|
+
const want = canonicalRealpathSync(workspace);
|
|
4588
|
+
for (const name$1 of names) {
|
|
4589
|
+
if (name$1 === ".gh-router-meta") continue;
|
|
4590
|
+
const dir = path.join(indicesDir, name$1);
|
|
4591
|
+
let proj;
|
|
4592
|
+
try {
|
|
4593
|
+
proj = JSON.parse(readFileSync(path.join(dir, "project.json"), "utf8"));
|
|
4594
|
+
} catch {
|
|
4595
|
+
continue;
|
|
4596
|
+
}
|
|
4597
|
+
const projPath = proj.path ?? proj.project_path;
|
|
4598
|
+
if (!projPath || canonicalRealpathSync(projPath) !== want) continue;
|
|
4599
|
+
const [bytes, count] = dirSizeSync(dir);
|
|
4600
|
+
return `${bytes}:${count}`;
|
|
4601
|
+
}
|
|
4602
|
+
return null;
|
|
4603
|
+
}
|
|
4532
4604
|
/**
|
|
4533
4605
|
* Realpath-aware canonicalization for matching a workspace against
|
|
4534
4606
|
* colgrep's stored `project_path`. colgrep stores the OS realpath (e.g.
|
|
@@ -4567,10 +4639,22 @@ async function freshnessVerdict(workspace) {
|
|
|
4567
4639
|
verdict: "failed",
|
|
4568
4640
|
meta
|
|
4569
4641
|
};
|
|
4570
|
-
if (meta.status === "building")
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4642
|
+
if (meta.status === "building") {
|
|
4643
|
+
const pid = typeof meta.buildPid === "number" ? meta.buildPid : 0;
|
|
4644
|
+
if (isInitInFlight(workspace) || pid > 0 && isPidAlive(pid)) return {
|
|
4645
|
+
verdict: "building",
|
|
4646
|
+
meta
|
|
4647
|
+
};
|
|
4648
|
+
const startedMs = meta.lastIndexedAt ? Date.parse(meta.lastIndexedAt) : NaN;
|
|
4649
|
+
if (Number.isFinite(startedMs) && Date.now() - startedMs < BUILD_SPAWN_GRACE_MS) return {
|
|
4650
|
+
verdict: "building",
|
|
4651
|
+
meta
|
|
4652
|
+
};
|
|
4653
|
+
if (!await completedIndexOnDisk(workspace)) return {
|
|
4654
|
+
verdict: "crashed",
|
|
4655
|
+
meta
|
|
4656
|
+
};
|
|
4657
|
+
}
|
|
4574
4658
|
if (!await completedIndexOnDisk(workspace)) return {
|
|
4575
4659
|
verdict: "building",
|
|
4576
4660
|
meta
|
|
@@ -5181,14 +5265,73 @@ async function runSmokeTest(binaryPath, ortDylibPath, modelDir) {
|
|
|
5181
5265
|
|
|
5182
5266
|
//#endregion
|
|
5183
5267
|
//#region src/lib/colbert/runner.ts
|
|
5184
|
-
/**
|
|
5185
|
-
*
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5268
|
+
/** Caller responsiveness budget for a search. A warm search is sub-second;
|
|
5269
|
+
* if colgrep instead starts a foreground auto-index / reconcile (its index is
|
|
5270
|
+
* behind) and hasn't returned results by this point, the search DETACHES —
|
|
5271
|
+
* the caller gets a `building` fallback now and the colgrep child finishes
|
|
5272
|
+
* the index in the background (never killed mid-write — that would orphan
|
|
5273
|
+
* docs and desync the index). The next query is then fast. */
|
|
5274
|
+
const SEARCH_RESPOND_MS = envIntMs("GH_ROUTER_COLBERT_SEARCH_RESPOND_MS", 2e4);
|
|
5275
|
+
/** Inactivity (stall) watchdog for the background init: if the colgrep
|
|
5276
|
+
* index dir stops growing for this long, the build is hung → kill it. This
|
|
5277
|
+
* is the PRIMARY "stuck vs slow" signal — a build that keeps writing shards
|
|
5278
|
+
* runs as long as it needs (a 50GB repo can take hours), only a genuinely
|
|
5279
|
+
* hung build is killed. colgrep is silent on a non-TTY pipe during the
|
|
5280
|
+
* encode, so disk growth (not output) is the progress signal. */
|
|
5281
|
+
const INIT_STALL_MS = envIntMs("GH_ROUTER_COLBERT_INIT_STALL_MS", 300 * 1e3);
|
|
5282
|
+
/** Absolute backstop on the background init — a generous ceiling so a truly
|
|
5283
|
+
* runaway process can't live forever, NOT the primary mechanism (the stall
|
|
5284
|
+
* watchdog is). Raised well above the old 30-min cap so a legitimately huge
|
|
5285
|
+
* repo isn't cut off mid-progress. */
|
|
5286
|
+
const INIT_TIMEOUT_MS = envIntMs("GH_ROUTER_COLBERT_INIT_TIMEOUT_MS", 360 * 60 * 1e3);
|
|
5287
|
+
/** After a failed build, don't re-kick a fresh one until this long has
|
|
5288
|
+
* elapsed (throttles a fast-failing init; the per-workspace debounce +
|
|
5289
|
+
* attempt cap are the other two guards). */
|
|
5290
|
+
const FAILED_RETRY_BACKOFF_MS = 300 * 1e3;
|
|
5291
|
+
/** Consecutive failed-build attempts before the self-heal gives up and the
|
|
5292
|
+
* notice goes operator-actionable. Reset to 0 on a successful build. */
|
|
5293
|
+
const MAX_FAILED_ATTEMPTS = 3;
|
|
5189
5294
|
/** Reuse code-search's stdout cap (10 MiB) for the full-CodeUnit payload. */
|
|
5190
5295
|
const MAX_STDOUT_BYTES = 10 * 1024 * 1024;
|
|
5191
5296
|
const DEFAULT_LIMIT = 15;
|
|
5297
|
+
/** Parse a positive-integer-milliseconds env override, else the default. */
|
|
5298
|
+
function envIntMs(name$1, fallback) {
|
|
5299
|
+
const raw = process$1.env[name$1];
|
|
5300
|
+
if (raw === void 0) return fallback;
|
|
5301
|
+
const n = Number(raw);
|
|
5302
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
5303
|
+
}
|
|
5304
|
+
/**
|
|
5305
|
+
* A progress probe for the inactivity watchdog: returns `false` (→ kill)
|
|
5306
|
+
* only when colgrep's index dir for `workspace` has stopped growing. colgrep
|
|
5307
|
+
* is SILENT on a non-TTY pipe during the encode, so disk growth — not output
|
|
5308
|
+
* — is the progress signal. `null` (dir not found yet) gets one window of
|
|
5309
|
+
* grace, then counts as no-progress (a build/search hung before it ever
|
|
5310
|
+
* wrote anything). Shared by BOTH the background init and the foreground
|
|
5311
|
+
* search so neither colgrep child is killed mid-write (which orphans docs).
|
|
5312
|
+
*/
|
|
5313
|
+
function makeIndexProgressProbe(workspace) {
|
|
5314
|
+
let lastSig;
|
|
5315
|
+
let nullStreak = 0;
|
|
5316
|
+
return () => {
|
|
5317
|
+
const sig = indexDirSignature(workspace);
|
|
5318
|
+
if (sig === null) {
|
|
5319
|
+
nullStreak += 1;
|
|
5320
|
+
return nullStreak <= 1;
|
|
5321
|
+
}
|
|
5322
|
+
nullStreak = 0;
|
|
5323
|
+
const prev = lastSig;
|
|
5324
|
+
lastSig = sig;
|
|
5325
|
+
if (prev === void 0) return true;
|
|
5326
|
+
return sig !== prev;
|
|
5327
|
+
};
|
|
5328
|
+
}
|
|
5329
|
+
/** Workspaces with a DETACHED indexing search in flight. A new search for
|
|
5330
|
+
* such a workspace returns `building` instead of spawning a concurrent
|
|
5331
|
+
* colgrep that could collide on the index write — serving the same "one
|
|
5332
|
+
* colgrep writer per workspace" goal as the init debounce. Cleared when the
|
|
5333
|
+
* detached search completes. */
|
|
5334
|
+
const _searchIndexInFlight = /* @__PURE__ */ new Set();
|
|
5192
5335
|
/** Build the isolating env for any colgrep child (search or init). */
|
|
5193
5336
|
function colgrepEnv() {
|
|
5194
5337
|
const ortDir = path.dirname(colbertOrtDylibPath());
|
|
@@ -5215,7 +5358,8 @@ function colgrepEnv() {
|
|
|
5215
5358
|
async function runSemanticSearch(opts) {
|
|
5216
5359
|
const { query, workspace } = opts;
|
|
5217
5360
|
const limit = clampLimit(opts.limit);
|
|
5218
|
-
|
|
5361
|
+
const fresh = await freshnessVerdict(workspace);
|
|
5362
|
+
switch (fresh.verdict) {
|
|
5219
5363
|
case "absent":
|
|
5220
5364
|
kickBackgroundInit(workspace);
|
|
5221
5365
|
return {
|
|
@@ -5223,11 +5367,8 @@ async function runSemanticSearch(opts) {
|
|
|
5223
5367
|
isError: true,
|
|
5224
5368
|
notice: "no semantic index for this workspace yet — a background index was started; retry shortly or use code_search"
|
|
5225
5369
|
};
|
|
5226
|
-
case "failed": return
|
|
5227
|
-
|
|
5228
|
-
isError: true,
|
|
5229
|
-
notice: "semantic index build failed for this workspace; use code_search"
|
|
5230
|
-
};
|
|
5370
|
+
case "failed": return handleFailure(workspace, fresh.meta, false);
|
|
5371
|
+
case "crashed": return handleFailure(workspace, fresh.meta, true);
|
|
5231
5372
|
case "building": return {
|
|
5232
5373
|
status: "building",
|
|
5233
5374
|
notice: "semantic index is being built for this workspace; retry shortly (or use code_search now)"
|
|
@@ -5247,6 +5388,59 @@ async function runSemanticSearch(opts) {
|
|
|
5247
5388
|
pattern: opts.pattern
|
|
5248
5389
|
});
|
|
5249
5390
|
}
|
|
5391
|
+
/**
|
|
5392
|
+
* Decide how to respond to a failed/crashed index and SELF-HEAL when the
|
|
5393
|
+
* failure looks transient: re-kick a debounced background re-index when the
|
|
5394
|
+
* attempt count is under the per-class cap AND the backoff has elapsed,
|
|
5395
|
+
* else return an actionable notice (transient-throttled vs operator-action).
|
|
5396
|
+
*
|
|
5397
|
+
* A `crashed` verdict is a per-query detection of a build whose PID died
|
|
5398
|
+
* without recording a result (proxy kill / OOM); persist it as
|
|
5399
|
+
* `failed`+`crashed` (incrementing the attempt counter) before deciding so a
|
|
5400
|
+
* later query sees a consistent `failed` state. `stuck` (hung build killed
|
|
5401
|
+
* by the inactivity watchdog) retries at most once — re-running a hung build
|
|
5402
|
+
* usually hangs again; transient classes retry up to `MAX_FAILED_ATTEMPTS`.
|
|
5403
|
+
*/
|
|
5404
|
+
async function handleFailure(workspace, meta, crashedVerdict) {
|
|
5405
|
+
const cls = crashedVerdict ? "crashed" : meta?.failureClass ?? "error";
|
|
5406
|
+
const attempts = crashedVerdict ? (meta?.failedAttempts ?? 0) + 1 : meta?.failedAttempts ?? 1;
|
|
5407
|
+
const lastAt = meta?.lastIndexedAt;
|
|
5408
|
+
if (crashedVerdict) await writeColbertMeta({
|
|
5409
|
+
workspace,
|
|
5410
|
+
model: meta?.model ?? MODEL_ID,
|
|
5411
|
+
modelRev: meta?.modelRev ?? MODEL_REVISION,
|
|
5412
|
+
status: "failed",
|
|
5413
|
+
failureClass: "crashed",
|
|
5414
|
+
failedAttempts: attempts,
|
|
5415
|
+
lastIndexedAt: lastAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
5416
|
+
lastIndexedHead: meta?.lastIndexedHead,
|
|
5417
|
+
lastIndexedDirty: meta?.lastIndexedDirty,
|
|
5418
|
+
ownerInstanceId: getColbertInstanceUuid()
|
|
5419
|
+
}).catch(() => {});
|
|
5420
|
+
const cap = cls === "stuck" ? 2 : MAX_FAILED_ATTEMPTS;
|
|
5421
|
+
const lastMs = lastAt ? Date.parse(lastAt) : NaN;
|
|
5422
|
+
const backoffElapsed = !Number.isFinite(lastMs) || Date.now() - lastMs >= FAILED_RETRY_BACKOFF_MS;
|
|
5423
|
+
if (attempts < cap && backoffElapsed) {
|
|
5424
|
+
kickBackgroundInit(workspace);
|
|
5425
|
+
consola.debug(`colbert: re-kicking index (class=${cls}, attempt=${attempts}/${cap})`);
|
|
5426
|
+
return {
|
|
5427
|
+
status: "failed",
|
|
5428
|
+
isError: true,
|
|
5429
|
+
notice: "semantic index unavailable; a background re-index was started — retry mode:\"semantic\" shortly, or use code_search with specific symbol/keyword terms now"
|
|
5430
|
+
};
|
|
5431
|
+
}
|
|
5432
|
+
if (attempts < cap) return {
|
|
5433
|
+
status: "failed",
|
|
5434
|
+
isError: true,
|
|
5435
|
+
notice: "semantic index unavailable (recent build failure); retry mode:\"semantic\" shortly, or use code_search with specific symbol/keyword terms now"
|
|
5436
|
+
};
|
|
5437
|
+
consola.debug(`colbert: index ${cls}, giving up (attempts=${attempts})`);
|
|
5438
|
+
return {
|
|
5439
|
+
status: "failed",
|
|
5440
|
+
isError: true,
|
|
5441
|
+
notice: `semantic index keeps failing (${cls}); use code_search. See logs; for a very large repo raise GH_ROUTER_COLBERT_INIT_STALL_MS / GH_ROUTER_COLBERT_INIT_TIMEOUT_MS`
|
|
5442
|
+
};
|
|
5443
|
+
}
|
|
5250
5444
|
async function spawnSearch(opts) {
|
|
5251
5445
|
const binary = colgrepBinaryPath();
|
|
5252
5446
|
if (!existsSync(binary)) return {
|
|
@@ -5273,36 +5467,83 @@ async function spawnSearch(opts) {
|
|
|
5273
5467
|
];
|
|
5274
5468
|
if (opts.pattern) args.push("-e", opts.pattern);
|
|
5275
5469
|
args.push(opts.query, opts.workspace);
|
|
5276
|
-
|
|
5470
|
+
const wsKey = path.resolve(opts.workspace);
|
|
5471
|
+
if (_searchIndexInFlight.has(wsKey)) return {
|
|
5472
|
+
status: "building",
|
|
5473
|
+
notice: "semantic index is busy (another search is running); retry shortly"
|
|
5474
|
+
};
|
|
5475
|
+
_searchIndexInFlight.add(wsKey);
|
|
5476
|
+
let searchPromise;
|
|
5277
5477
|
try {
|
|
5278
|
-
|
|
5478
|
+
searchPromise = runManagedExeCapture(binary, args, {
|
|
5279
5479
|
env: colgrepEnv(),
|
|
5280
|
-
|
|
5480
|
+
inactivityTimeoutMs: INIT_STALL_MS,
|
|
5481
|
+
onInactivityCheck: makeIndexProgressProbe(opts.workspace),
|
|
5482
|
+
timeoutMs: INIT_TIMEOUT_MS,
|
|
5281
5483
|
maxStdoutBytes: MAX_STDOUT_BYTES,
|
|
5484
|
+
truncateInsteadOfKill: true,
|
|
5282
5485
|
onSpawn: trackChild
|
|
5283
5486
|
});
|
|
5284
5487
|
} catch {
|
|
5488
|
+
_searchIndexInFlight.delete(wsKey);
|
|
5489
|
+
consola.debug("colbert: search failed to launch");
|
|
5285
5490
|
return {
|
|
5286
5491
|
status: "failed",
|
|
5287
5492
|
isError: true,
|
|
5288
5493
|
notice: "semantic search failed to launch; use code_search"
|
|
5289
5494
|
};
|
|
5290
5495
|
}
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5496
|
+
searchPromise.catch(() => void 0).finally(() => _searchIndexInFlight.delete(wsKey));
|
|
5497
|
+
let respondTimer;
|
|
5498
|
+
const slow = new Promise((resolve) => {
|
|
5499
|
+
respondTimer = setTimeout(() => resolve({ kind: "slow" }), SEARCH_RESPOND_MS);
|
|
5500
|
+
respondTimer.unref?.();
|
|
5501
|
+
});
|
|
5502
|
+
const raced = await Promise.race([searchPromise.then((res$1) => ({
|
|
5503
|
+
kind: "done",
|
|
5504
|
+
res: res$1
|
|
5505
|
+
}), (err) => ({
|
|
5506
|
+
kind: "error",
|
|
5507
|
+
err
|
|
5508
|
+
})), slow]);
|
|
5509
|
+
if (respondTimer) clearTimeout(respondTimer);
|
|
5510
|
+
if (raced.kind === "slow") {
|
|
5511
|
+
consola.debug(`colbert: search detached (indexing) for ${opts.workspace}`);
|
|
5512
|
+
return {
|
|
5513
|
+
status: "building",
|
|
5514
|
+
notice: "semantic index is updating in the background; retry mode:\"semantic\" shortly"
|
|
5515
|
+
};
|
|
5516
|
+
}
|
|
5517
|
+
if (raced.kind === "error") {
|
|
5518
|
+
consola.debug("colbert: search failed to launch");
|
|
5519
|
+
return {
|
|
5520
|
+
status: "failed",
|
|
5521
|
+
isError: true,
|
|
5522
|
+
notice: "semantic search failed to launch; use code_search"
|
|
5523
|
+
};
|
|
5524
|
+
}
|
|
5525
|
+
const res = raced.res;
|
|
5526
|
+
if (res.timedOut || res.stalled) {
|
|
5527
|
+
consola.debug(`colbert: search ${res.stalled ? "stalled (hung, no progress)" : "hit the runaway backstop"}`);
|
|
5528
|
+
return {
|
|
5529
|
+
status: "failed",
|
|
5530
|
+
isError: true,
|
|
5531
|
+
notice: "semantic search timed out; use code_search"
|
|
5532
|
+
};
|
|
5533
|
+
}
|
|
5296
5534
|
if (res.stdoutTruncated) return {
|
|
5297
5535
|
status: "failed",
|
|
5298
5536
|
isError: true,
|
|
5299
5537
|
notice: "semantic search produced an oversized result; narrow the query or use code_search"
|
|
5300
5538
|
};
|
|
5301
|
-
if (res.code !== 0)
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5539
|
+
if (res.code !== 0) {
|
|
5540
|
+
consola.debug(`colbert: search exited ${res.code}`);
|
|
5541
|
+
return {
|
|
5542
|
+
status: "failed",
|
|
5543
|
+
isError: true,
|
|
5544
|
+
notice: "semantic search returned an error; use code_search"
|
|
5545
|
+
};
|
|
5546
|
+
}
|
|
5306
5547
|
const rows = parseAndTrim(res.stdout, opts.workspace);
|
|
5307
5548
|
if (rows === null) return {
|
|
5308
5549
|
status: "failed",
|
|
@@ -5388,6 +5629,21 @@ function kickBackgroundInit(workspace) {
|
|
|
5388
5629
|
consola.debug("colbert: background init failed:", err);
|
|
5389
5630
|
});
|
|
5390
5631
|
}
|
|
5632
|
+
/**
|
|
5633
|
+
* Whether the STARTUP auto-kick should fire for a workspace. Skips a build
|
|
5634
|
+
* that's already in a capped/persistent failure state (`failedAttempts >=
|
|
5635
|
+
* MAX`) or was killed as `stuck` (hung) — so a restart loop doesn't re-burn
|
|
5636
|
+
* a known-bad build on every launch. The per-query self-heal still gives a
|
|
5637
|
+
* `stuck` build its one retry and a capped one its post-backoff probe;
|
|
5638
|
+
* absent/stale/under-cap/ready all kick normally.
|
|
5639
|
+
*/
|
|
5640
|
+
async function startupKickAllowed(workspace) {
|
|
5641
|
+
const meta = await readColbertMeta(workspace);
|
|
5642
|
+
if (!meta || meta.status !== "failed") return true;
|
|
5643
|
+
if ((meta.failedAttempts ?? 0) >= MAX_FAILED_ATTEMPTS) return false;
|
|
5644
|
+
if (meta.failureClass === "stuck") return false;
|
|
5645
|
+
return true;
|
|
5646
|
+
}
|
|
5391
5647
|
async function runInit(workspace) {
|
|
5392
5648
|
const binary = colgrepBinaryPath();
|
|
5393
5649
|
if (!existsSync(binary)) {
|
|
@@ -5398,6 +5654,7 @@ async function runInit(workspace) {
|
|
|
5398
5654
|
releaseInit(workspace);
|
|
5399
5655
|
return;
|
|
5400
5656
|
}
|
|
5657
|
+
const prior = await readColbertMeta(workspace);
|
|
5401
5658
|
const baseMeta = {
|
|
5402
5659
|
workspace,
|
|
5403
5660
|
model: MODEL_ID,
|
|
@@ -5405,7 +5662,8 @@ async function runInit(workspace) {
|
|
|
5405
5662
|
status: "building",
|
|
5406
5663
|
buildPid: void 0,
|
|
5407
5664
|
ownerInstanceId: getColbertInstanceUuid(),
|
|
5408
|
-
lastIndexedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5665
|
+
lastIndexedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5666
|
+
failedAttempts: prior?.failedAttempts ?? 0
|
|
5409
5667
|
};
|
|
5410
5668
|
try {
|
|
5411
5669
|
const g = await gitState(workspace);
|
|
@@ -5425,11 +5683,16 @@ async function runInit(workspace) {
|
|
|
5425
5683
|
colbertModelDir(),
|
|
5426
5684
|
workspace
|
|
5427
5685
|
];
|
|
5686
|
+
const onInactivityCheck = makeIndexProgressProbe(workspace);
|
|
5687
|
+
const startMs = Date.now();
|
|
5428
5688
|
let ok = false;
|
|
5689
|
+
let failureClass;
|
|
5429
5690
|
try {
|
|
5430
5691
|
const res = await runManagedExeCapture(binary, args, {
|
|
5431
5692
|
env: colgrepEnv(),
|
|
5432
5693
|
timeoutMs: INIT_TIMEOUT_MS,
|
|
5694
|
+
inactivityTimeoutMs: INIT_STALL_MS,
|
|
5695
|
+
onInactivityCheck,
|
|
5433
5696
|
maxStdoutBytes: MAX_STDOUT_BYTES,
|
|
5434
5697
|
onSpawn: (child) => {
|
|
5435
5698
|
trackChild(child);
|
|
@@ -5439,12 +5702,15 @@ async function runInit(workspace) {
|
|
|
5439
5702
|
}).catch(() => {});
|
|
5440
5703
|
}
|
|
5441
5704
|
});
|
|
5442
|
-
ok = !res.timedOut && res.code === 0;
|
|
5705
|
+
ok = !res.stalled && !res.timedOut && res.code === 0;
|
|
5706
|
+
if (!ok) failureClass = res.stalled || res.timedOut ? "stuck" : "error";
|
|
5443
5707
|
} catch {
|
|
5444
5708
|
ok = false;
|
|
5709
|
+
failureClass = "launch";
|
|
5445
5710
|
} finally {
|
|
5446
5711
|
releaseInit(workspace);
|
|
5447
5712
|
}
|
|
5713
|
+
const elapsedMs = Date.now() - startMs;
|
|
5448
5714
|
const finalMeta = {
|
|
5449
5715
|
...baseMeta,
|
|
5450
5716
|
buildPid: void 0
|
|
@@ -5458,9 +5724,190 @@ async function runInit(workspace) {
|
|
|
5458
5724
|
} catch {}
|
|
5459
5725
|
finalMeta.status = ok ? "ready" : "failed";
|
|
5460
5726
|
finalMeta.lastIndexedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5727
|
+
if (ok) {
|
|
5728
|
+
finalMeta.failedAttempts = 0;
|
|
5729
|
+
finalMeta.failureClass = void 0;
|
|
5730
|
+
} else {
|
|
5731
|
+
finalMeta.failureClass = failureClass;
|
|
5732
|
+
finalMeta.failedAttempts = (prior?.failedAttempts ?? 0) + 1;
|
|
5733
|
+
consola.debug(`colbert: init ${failureClass} after ${Math.round(elapsedMs / 1e3)}s (attempt ${finalMeta.failedAttempts}) for ${workspace}`);
|
|
5734
|
+
}
|
|
5461
5735
|
await writeColbertMeta(finalMeta).catch(() => {});
|
|
5462
5736
|
}
|
|
5463
5737
|
|
|
5738
|
+
//#endregion
|
|
5739
|
+
//#region src/lib/colbert/index.ts
|
|
5740
|
+
/**
|
|
5741
|
+
* True unless the operator opted out via
|
|
5742
|
+
* `GH_ROUTER_DISABLE_SEMANTIC_SEARCH=1`. Semantic search is ON BY
|
|
5743
|
+
* DEFAULT (the proxy auto-provisions + background-indexes); the
|
|
5744
|
+
* capability gate additionally requires the artifacts to be present on
|
|
5745
|
+
* disk + smoke-passed, so in any environment where provisioning hasn't
|
|
5746
|
+
* completed the tool simply doesn't appear (no regression).
|
|
5747
|
+
*/
|
|
5748
|
+
function semanticSearchOptedIn() {
|
|
5749
|
+
return parseBoolEnv(process$1.env.GH_ROUTER_DISABLE_SEMANTIC_SEARCH) !== true;
|
|
5750
|
+
}
|
|
5751
|
+
/**
|
|
5752
|
+
* Availability predicate for ColBERT semantic search — the single
|
|
5753
|
+
* source of truth, living in this leaf module so callers that must not
|
|
5754
|
+
* import `mcp-capabilities` (notably the unified code-search helper)
|
|
5755
|
+
* can read it without closing an import cycle through `worker-agent`.
|
|
5756
|
+
*
|
|
5757
|
+
* True iff the operator hasn't opted out AND the colgrep binary + model
|
|
5758
|
+
* + ORT are provisioned on disk AND the post-provision smoke test
|
|
5759
|
+
* passed. `mcp-capabilities.semanticSearchEnabled()` delegates here.
|
|
5760
|
+
*/
|
|
5761
|
+
function colbertSearchEnabled() {
|
|
5762
|
+
return semanticSearchOptedIn() && colbertArtifactsPresent() && colbertSmokeOk();
|
|
5763
|
+
}
|
|
5764
|
+
let _started = false;
|
|
5765
|
+
/**
|
|
5766
|
+
* Fire-and-forget provision + background-index. Never throws; safe to
|
|
5767
|
+
* `void`-call from a launcher right after the server is listening.
|
|
5768
|
+
* Idempotent within a proxy run (subsequent calls no-op).
|
|
5769
|
+
*/
|
|
5770
|
+
async function provisionAndIndexColbert(opts = {}) {
|
|
5771
|
+
if (!semanticSearchOptedIn()) return;
|
|
5772
|
+
if (_started) return;
|
|
5773
|
+
_started = true;
|
|
5774
|
+
registerColbertExitHandlers();
|
|
5775
|
+
let provisioned = false;
|
|
5776
|
+
try {
|
|
5777
|
+
const result = await provisionColbert();
|
|
5778
|
+
provisioned = result.status === "ready";
|
|
5779
|
+
if (result.status === "unsupported") consola.debug("colbert: semantic search unsupported on this platform");
|
|
5780
|
+
else if (result.status !== "ready") consola.debug(`colbert: provision not ready (${result.status}: ${result.reason ?? ""})`);
|
|
5781
|
+
} catch (err) {
|
|
5782
|
+
consola.debug("colbert: provision threw (swallowed):", err);
|
|
5783
|
+
return;
|
|
5784
|
+
}
|
|
5785
|
+
if (!provisioned) return;
|
|
5786
|
+
const cwd = opts.cwd ?? process$1.cwd();
|
|
5787
|
+
try {
|
|
5788
|
+
if ((await gitState(cwd)).isRepo && await startupKickAllowed(cwd)) kickBackgroundInit(cwd);
|
|
5789
|
+
} catch (err) {
|
|
5790
|
+
consola.debug("colbert: cwd git-detect skipped:", err);
|
|
5791
|
+
}
|
|
5792
|
+
}
|
|
5793
|
+
|
|
5794
|
+
//#endregion
|
|
5795
|
+
//#region src/lib/unified-code-search.ts
|
|
5796
|
+
/** Map the unified mode onto `searchCode`'s internal `mode` enum. */
|
|
5797
|
+
function lexicalSearchCodeMode(mode) {
|
|
5798
|
+
switch (mode) {
|
|
5799
|
+
case "exact": return "literal";
|
|
5800
|
+
case "regex": return "regex";
|
|
5801
|
+
default: return "ranked";
|
|
5802
|
+
}
|
|
5803
|
+
}
|
|
5804
|
+
/**
|
|
5805
|
+
* Status-specific, actionable fallback hint. The semantic index isn't ready,
|
|
5806
|
+
* so the model got LEXICAL results (great for exact symbols, sparse for a
|
|
5807
|
+
* natural-language phrase since the lexical backend matches literally). Tell
|
|
5808
|
+
* it both levers: retry `mode:"semantic"` shortly (the index is self-healing
|
|
5809
|
+
* in the background) OR re-query now with specific symbol/keyword terms.
|
|
5810
|
+
*/
|
|
5811
|
+
function fallbackNoticeFor(status) {
|
|
5812
|
+
const tail = "retry mode:\"semantic\" shortly, or re-query now with specific symbol/keyword terms";
|
|
5813
|
+
switch (status) {
|
|
5814
|
+
case "building": return `semantic index is building; returned lexical keyword matches — ${tail}`;
|
|
5815
|
+
case "stale": return `semantic index predates the current HEAD/tree (a background re-index was started); returned lexical keyword matches — ${tail}`;
|
|
5816
|
+
case "unavailable": return `no semantic index for this workspace yet (a background build was started); returned lexical keyword matches — ${tail}`;
|
|
5817
|
+
case "failed": return `semantic index unavailable (build failing — see proxy logs); returned lexical keyword matches — ${tail}`;
|
|
5818
|
+
default: return "returned lexical results";
|
|
5819
|
+
}
|
|
5820
|
+
}
|
|
5821
|
+
/**
|
|
5822
|
+
* Combine the lexical backend's own notice (size-cap / structural, the
|
|
5823
|
+
* urgent "you're missing results" signal) with a fallback hint, keeping a
|
|
5824
|
+
* single string. The lexical notice stays primary; the hint is appended so
|
|
5825
|
+
* neither is lost.
|
|
5826
|
+
*/
|
|
5827
|
+
function joinNotice(primary, secondary) {
|
|
5828
|
+
if (primary && secondary) return `${primary} (${secondary})`;
|
|
5829
|
+
return primary || secondary || void 0;
|
|
5830
|
+
}
|
|
5831
|
+
async function runLexical(input, mode, source, signal) {
|
|
5832
|
+
const isAst = mode === "ast";
|
|
5833
|
+
const resp = await searchCode({
|
|
5834
|
+
query: input.query,
|
|
5835
|
+
workspace: input.workspace,
|
|
5836
|
+
mode: lexicalSearchCodeMode(mode),
|
|
5837
|
+
file_glob: input.file_glob,
|
|
5838
|
+
limit: input.limit,
|
|
5839
|
+
context_lines: input.context_lines,
|
|
5840
|
+
structural: input.structural,
|
|
5841
|
+
summary: input.summary,
|
|
5842
|
+
complete: input.complete,
|
|
5843
|
+
multiline: input.multiline,
|
|
5844
|
+
scan: input.scan,
|
|
5845
|
+
ast_pattern: isAst ? input.ast_pattern : void 0,
|
|
5846
|
+
ast_lang: isAst ? input.ast_lang : void 0
|
|
5847
|
+
}, signal);
|
|
5848
|
+
return {
|
|
5849
|
+
source,
|
|
5850
|
+
results: resp.results.map((h) => ({
|
|
5851
|
+
file: h.file,
|
|
5852
|
+
line: h.line,
|
|
5853
|
+
snippet: h.snippet,
|
|
5854
|
+
...h.role ? { role: h.role } : {}
|
|
5855
|
+
})),
|
|
5856
|
+
notice: resp.notice ?? void 0,
|
|
5857
|
+
outlines: resp.outlines,
|
|
5858
|
+
truncated: resp.truncated
|
|
5859
|
+
};
|
|
5860
|
+
}
|
|
5861
|
+
/**
|
|
5862
|
+
* Route a unified code-search request. Throws only on input/workspace
|
|
5863
|
+
* validation failure (propagated from `searchCode`); callers wrap in
|
|
5864
|
+
* try/catch exactly as they do today for `searchCode`.
|
|
5865
|
+
*/
|
|
5866
|
+
async function runUnifiedCodeSearch(input, signal) {
|
|
5867
|
+
const mode = input.mode ?? "semantic";
|
|
5868
|
+
if (mode !== "semantic") return runLexical(input, mode, "lexical", signal);
|
|
5869
|
+
if (!colbertSearchEnabled()) {
|
|
5870
|
+
const r$1 = await runLexical(input, "lexical", "lexical-fallback", signal);
|
|
5871
|
+
return {
|
|
5872
|
+
...r$1,
|
|
5873
|
+
notice: joinNotice(r$1.notice, "semantic search unavailable on this host; returned lexical results")
|
|
5874
|
+
};
|
|
5875
|
+
}
|
|
5876
|
+
let sem;
|
|
5877
|
+
try {
|
|
5878
|
+
sem = await runSemanticSearch({
|
|
5879
|
+
query: input.query,
|
|
5880
|
+
workspace: input.workspace,
|
|
5881
|
+
limit: input.limit,
|
|
5882
|
+
pattern: input.pattern,
|
|
5883
|
+
signal
|
|
5884
|
+
});
|
|
5885
|
+
} catch {
|
|
5886
|
+
const r$1 = await runLexical(input, "lexical", "lexical-fallback", signal);
|
|
5887
|
+
return {
|
|
5888
|
+
...r$1,
|
|
5889
|
+
notice: joinNotice(r$1.notice, "semantic search errored; returned lexical results")
|
|
5890
|
+
};
|
|
5891
|
+
}
|
|
5892
|
+
if (sem.status === "ready") return {
|
|
5893
|
+
source: "semantic",
|
|
5894
|
+
results: (sem.results ?? []).map((r$1) => ({
|
|
5895
|
+
file: r$1.file,
|
|
5896
|
+
line: r$1.line,
|
|
5897
|
+
snippet: r$1.snippet,
|
|
5898
|
+
...r$1.endLine !== void 0 ? { endLine: r$1.endLine } : {},
|
|
5899
|
+
...r$1.name !== void 0 ? { name: r$1.name } : {},
|
|
5900
|
+
...r$1.score !== void 0 ? { score: r$1.score } : {}
|
|
5901
|
+
})),
|
|
5902
|
+
...sem.notice ? { notice: sem.notice } : {}
|
|
5903
|
+
};
|
|
5904
|
+
const r = await runLexical(input, "lexical", "lexical-fallback", signal);
|
|
5905
|
+
return {
|
|
5906
|
+
...r,
|
|
5907
|
+
notice: joinNotice(r.notice, fallbackNoticeFor(sem.status))
|
|
5908
|
+
};
|
|
5909
|
+
}
|
|
5910
|
+
|
|
5464
5911
|
//#endregion
|
|
5465
5912
|
//#region src/lib/browser-mcp/browser-detect.ts
|
|
5466
5913
|
let cached;
|
|
@@ -6655,7 +7102,7 @@ function logAudit$1(record) {
|
|
|
6655
7102
|
try {
|
|
6656
7103
|
const fs$2 = await import("node:fs/promises");
|
|
6657
7104
|
const path$2 = await import("node:path");
|
|
6658
|
-
const { PATHS: PATHS$1 } = await import("./paths-
|
|
7105
|
+
const { PATHS: PATHS$1 } = await import("./paths-Czi0-nEE.js");
|
|
6659
7106
|
const dir = path$2.join(PATHS$1.APP_DIR, "browser-mcp");
|
|
6660
7107
|
await fs$2.mkdir(dir, { recursive: true });
|
|
6661
7108
|
const line = JSON.stringify({
|
|
@@ -10895,7 +11342,7 @@ function resolveModelAndThinking(opts) {
|
|
|
10895
11342
|
* doesn't redirect Pi.
|
|
10896
11343
|
* 3. State what each tool does in one short sentence — Pi runs on
|
|
10897
11344
|
* `gemini-3.1-pro-preview` and has no built-in knowledge of the
|
|
10898
|
-
* proxy-specific tools (`code_search`, `
|
|
11345
|
+
* proxy-specific tools (`code_search`, `advisor`, `update_plan`,
|
|
10899
11346
|
* `fetch_url`). Listing names alone wastes the first turn on
|
|
10900
11347
|
* discovery probing.
|
|
10901
11348
|
*
|
|
@@ -10912,9 +11359,12 @@ const READ_TOOL_NOTES = [
|
|
|
10912
11359
|
"`read` — return a file's content.",
|
|
10913
11360
|
"`glob` — list files matching a glob pattern.",
|
|
10914
11361
|
"`grep` — regex search across files.",
|
|
10915
|
-
"`code_search` —
|
|
11362
|
+
"`code_search` — semantic-first code search: the default `semantic` mode ranks by MEANING (ColBERT), falling back to lexical BM25F-ranked hits when the index isn't ready (the `source` field says which ran); use `lexical`/`exact`/`regex`/`ast` for exact symbols. Multiple independent queries can run in a single turn. The index covers code-shaped files; for unstructured files (logs, `.csv`, `.env*`, config-only wiring) and when a search returns no hits, `grep`/`glob` apply.",
|
|
10916
11363
|
"`web_search` — Copilot-backed web search; returns titles, URLs, and snippets.",
|
|
10917
|
-
"`fetch_url` — fetch a single URL and return body text."
|
|
11364
|
+
"`fetch_url` — fetch a single URL and return body text.",
|
|
11365
|
+
"`toolbelt` — run a read-only analysis CLI (no shell): rg, fd, sg, jq, yq, gron, scc, tokei, difft, git (read-only subcommands).",
|
|
11366
|
+
"`advisor` — consult a stronger cross-lab reviewer model on a focused concern (your approach, a blocker, a decision); it sees the recent transcript automatically.",
|
|
11367
|
+
"`update_plan` — maintain a short ordered checklist of your steps (send the full list each call); it's re-surfaced to you each turn so it survives context compaction."
|
|
10918
11368
|
];
|
|
10919
11369
|
const WRITE_TOOL_NOTES = [
|
|
10920
11370
|
"`edit` — exact-string replacement in a file.",
|
|
@@ -13056,15 +13506,18 @@ function standInToolEnabled() {
|
|
|
13056
13506
|
return hasGpt55 && hasOpus && hasGeminiPro;
|
|
13057
13507
|
}
|
|
13058
13508
|
/**
|
|
13059
|
-
* Gate for the worker tools (`
|
|
13509
|
+
* Gate for the worker tools (`explore`, `review`, `implement`).
|
|
13060
13510
|
*
|
|
13061
13511
|
* Returns true iff BOTH:
|
|
13062
13512
|
* 1. Copilot's live catalog (`state.models?.data`) contains the
|
|
13063
|
-
* worker
|
|
13064
|
-
* advertises `capabilities.supports.tool_calls ===
|
|
13065
|
-
* worker loop is function-calling; a model that can't
|
|
13066
|
-
* tool_calls is unusable, so dormant-register (omit from
|
|
13067
|
-
* `tools/list`) keeps the surface honest.
|
|
13513
|
+
* worker default model (`gemini-3.5-flash`, used by explore/review)
|
|
13514
|
+
* AND that entry advertises `capabilities.supports.tool_calls ===
|
|
13515
|
+
* true`. The worker loop is function-calling; a model that can't
|
|
13516
|
+
* emit tool_calls is unusable, so dormant-register (omit from
|
|
13517
|
+
* `tools/list`) keeps the surface honest. (The implement default
|
|
13518
|
+
* `gpt-5.5` is NOT gated here — if it's absent, implement calls
|
|
13519
|
+
* surface a clean resolve error rather than disabling all worker
|
|
13520
|
+
* tools, since explore/review still work.)
|
|
13068
13521
|
* 2. The operator hasn't set `GH_ROUTER_DISABLE_WORKER_TOOLS=1`
|
|
13069
13522
|
* (opt-out — workers ship enabled by default per plan).
|
|
13070
13523
|
*
|
|
@@ -13182,37 +13635,6 @@ function browseAgentEnabled() {
|
|
|
13182
13635
|
if (!found) return false;
|
|
13183
13636
|
return pickEndpoint(found) !== void 0;
|
|
13184
13637
|
}
|
|
13185
|
-
/**
|
|
13186
|
-
* Gate for the `semantic_search` tool (the ColBERT sidecar).
|
|
13187
|
-
*
|
|
13188
|
-
* Semantic search is ON BY DEFAULT (the proxy auto-provisions the
|
|
13189
|
-
* colgrep binary + ONNX Runtime + ColBERT model and background-indexes
|
|
13190
|
-
* the cwd at launch), so unlike `--browse` there is no opt-IN flag —
|
|
13191
|
-
* only an opt-OUT env var, mirroring the toolbelt convention.
|
|
13192
|
-
*
|
|
13193
|
-
* Returns true iff BOTH:
|
|
13194
|
-
* 1. **Not opted out:** `GH_ROUTER_DISABLE_SEMANTIC_SEARCH` is unset /
|
|
13195
|
-
* falsy.
|
|
13196
|
-
* 2. **Actually available on disk:** the colgrep binary + model + ORT
|
|
13197
|
-
* are provisioned AND the post-provision smoke test passed
|
|
13198
|
-
* (`colbertArtifactsPresent()` && `colbertSmokeOk()`).
|
|
13199
|
-
*
|
|
13200
|
-
* This is **availability-based**, exactly like `browserToolsEnabled()`'s
|
|
13201
|
-
* `hasSupportedBrowserInstalled()` check — and it's the load-bearing
|
|
13202
|
-
* regression guard: in any environment where provisioning hasn't
|
|
13203
|
-
* completed or can't run (CI, sandboxes, no network), the artifacts are
|
|
13204
|
-
* absent ⇒ the gate is false ⇒ `semantic_search` is NOT listed and NOT
|
|
13205
|
-
* callable ⇒ the existing `{code, web}` `tools/list` surface is
|
|
13206
|
-
* unchanged. The tool appears only on a machine where provisioning
|
|
13207
|
-
* succeeded.
|
|
13208
|
-
*
|
|
13209
|
-
* Gate fires symmetrically at `tools/list` and `tools/call` (drop +
|
|
13210
|
-
* -32601), exactly like the other capability tags.
|
|
13211
|
-
*/
|
|
13212
|
-
function semanticSearchEnabled() {
|
|
13213
|
-
if (parseBoolEnv(process.env.GH_ROUTER_DISABLE_SEMANTIC_SEARCH) === true) return false;
|
|
13214
|
-
return colbertArtifactsPresent() && colbertSmokeOk();
|
|
13215
|
-
}
|
|
13216
13638
|
|
|
13217
13639
|
//#endregion
|
|
13218
13640
|
//#region src/routes/mcp/handler.ts
|
|
@@ -13373,7 +13795,6 @@ function toolEntries(scope) {
|
|
|
13373
13795
|
if (t.capability === "browse_agent") return browseAgentEnabled();
|
|
13374
13796
|
if (t.capability === "stand_in") return standInToolEnabled();
|
|
13375
13797
|
if (t.capability === "browser") return browserToolsEnabled();
|
|
13376
|
-
if (t.capability === "semantic_search") return semanticSearchEnabled();
|
|
13377
13798
|
if (t.capability === "browser_compound") return browserToolsEnabled() && browserCompoundToolsEnabled();
|
|
13378
13799
|
if (t.capability === "browser_power") return browserToolsEnabled() && browserPowerToolsEnabled();
|
|
13379
13800
|
return true;
|
|
@@ -13699,7 +14120,6 @@ async function handleToolsCall(body, scope) {
|
|
|
13699
14120
|
if (nonPersonaTool && nonPersonaTool.capability === "worker" && !workerToolsEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name$1}"`);
|
|
13700
14121
|
if (nonPersonaTool && nonPersonaTool.capability === "browse_agent" && !browseAgentEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name$1}"`);
|
|
13701
14122
|
if (nonPersonaTool && nonPersonaTool.capability === "stand_in" && !standInToolEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name$1}"`);
|
|
13702
|
-
if (nonPersonaTool && nonPersonaTool.capability === "semantic_search" && !semanticSearchEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name$1}"`);
|
|
13703
14123
|
if (nonPersonaTool && nonPersonaTool.capability === "browser" && !browserToolsEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name$1}"`);
|
|
13704
14124
|
if (nonPersonaTool && nonPersonaTool.capability === "browser_compound" && !(browserToolsEnabled() && browserCompoundToolsEnabled())) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name$1}"`);
|
|
13705
14125
|
if (nonPersonaTool && nonPersonaTool.capability === "browser_power" && !(browserToolsEnabled() && browserPowerToolsEnabled())) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name$1}"`);
|
|
@@ -15250,6 +15670,114 @@ const TOOLBELT_TOOLS = [
|
|
|
15250
15670
|
archive: "zip"
|
|
15251
15671
|
}
|
|
15252
15672
|
}
|
|
15673
|
+
},
|
|
15674
|
+
{
|
|
15675
|
+
command: "scc",
|
|
15676
|
+
binBasename: "scc",
|
|
15677
|
+
assets: {
|
|
15678
|
+
"win32-x64": {
|
|
15679
|
+
url: "https://github.com/boyter/scc/releases/download/v3.7.0/scc_Windows_x86_64.zip",
|
|
15680
|
+
sha256: "97abf9d55d4b79d3310536d576ccbdf5017aeb425780e850336120b6e67622e1",
|
|
15681
|
+
archive: "zip"
|
|
15682
|
+
},
|
|
15683
|
+
"win32-arm64": {
|
|
15684
|
+
url: "https://github.com/boyter/scc/releases/download/v3.7.0/scc_Windows_arm64.zip",
|
|
15685
|
+
sha256: "fd114614c10382c9ed2e32d5455cc4b51960a9f71691c5c1ca42b31adea5b84d",
|
|
15686
|
+
archive: "zip"
|
|
15687
|
+
},
|
|
15688
|
+
"darwin-x64": {
|
|
15689
|
+
url: "https://github.com/boyter/scc/releases/download/v3.7.0/scc_Darwin_x86_64.tar.gz",
|
|
15690
|
+
sha256: "c3f7457856b9169ccb3c1dd14198e67f730bee065f24d9051bf52cdc2a719ecc",
|
|
15691
|
+
archive: "tar.gz"
|
|
15692
|
+
},
|
|
15693
|
+
"darwin-arm64": {
|
|
15694
|
+
url: "https://github.com/boyter/scc/releases/download/v3.7.0/scc_Darwin_arm64.tar.gz",
|
|
15695
|
+
sha256: "376cbae670be59ee64f398de20e0694ec434bf8a9b842642952b0ab0be5f3961",
|
|
15696
|
+
archive: "tar.gz"
|
|
15697
|
+
},
|
|
15698
|
+
"linux-x64": {
|
|
15699
|
+
url: "https://github.com/boyter/scc/releases/download/v3.7.0/scc_Linux_x86_64.tar.gz",
|
|
15700
|
+
sha256: "3d9d65b00ca874c2b29151abe7e1480736f5229edc3ce8e4b2791460cdfabf5a",
|
|
15701
|
+
archive: "tar.gz"
|
|
15702
|
+
},
|
|
15703
|
+
"linux-arm64": {
|
|
15704
|
+
url: "https://github.com/boyter/scc/releases/download/v3.7.0/scc_Linux_arm64.tar.gz",
|
|
15705
|
+
sha256: "dcb05c6e993bb2d8d2da4765ff018f2e752325dd205a41698929c55e4123575d",
|
|
15706
|
+
archive: "tar.gz"
|
|
15707
|
+
}
|
|
15708
|
+
}
|
|
15709
|
+
},
|
|
15710
|
+
{
|
|
15711
|
+
command: "difftastic",
|
|
15712
|
+
binBasename: "difft",
|
|
15713
|
+
assets: {
|
|
15714
|
+
"win32-x64": {
|
|
15715
|
+
url: "https://github.com/Wilfred/difftastic/releases/download/0.69.0/difft-x86_64-pc-windows-msvc.zip",
|
|
15716
|
+
sha256: "a5adbf57eb1b923b62d1c3596c4f827df143f5b52cfba48bb9e83f41dea90c02",
|
|
15717
|
+
archive: "zip"
|
|
15718
|
+
},
|
|
15719
|
+
"win32-arm64": {
|
|
15720
|
+
url: "https://github.com/Wilfred/difftastic/releases/download/0.69.0/difft-aarch64-pc-windows-msvc.zip",
|
|
15721
|
+
sha256: "fa709e803088b54774adf0111409483ee5edfbbc1f9dcc5610e81e4ed3841e53",
|
|
15722
|
+
archive: "zip"
|
|
15723
|
+
},
|
|
15724
|
+
"darwin-x64": {
|
|
15725
|
+
url: "https://github.com/Wilfred/difftastic/releases/download/0.69.0/difft-x86_64-apple-darwin.tar.gz",
|
|
15726
|
+
sha256: "5f5487e7a6e817194a1cef297d2ffb300454371635a4cde865087dbc064730a2",
|
|
15727
|
+
archive: "tar.gz"
|
|
15728
|
+
},
|
|
15729
|
+
"darwin-arm64": {
|
|
15730
|
+
url: "https://github.com/Wilfred/difftastic/releases/download/0.69.0/difft-aarch64-apple-darwin.tar.gz",
|
|
15731
|
+
sha256: "c958b87885a5825a356c5899ac7ecdd752a7942084199f2be4bc0bf8c9de8e33",
|
|
15732
|
+
archive: "tar.gz"
|
|
15733
|
+
},
|
|
15734
|
+
"linux-x64": {
|
|
15735
|
+
url: "https://github.com/Wilfred/difftastic/releases/download/0.69.0/difft-x86_64-unknown-linux-gnu.tar.gz",
|
|
15736
|
+
sha256: "038db96a0e8fce69f2554e33e04ff75fbf6f96ea45cb4edb9ed6203a2c4750ff",
|
|
15737
|
+
archive: "tar.gz"
|
|
15738
|
+
},
|
|
15739
|
+
"linux-arm64": {
|
|
15740
|
+
url: "https://github.com/Wilfred/difftastic/releases/download/0.69.0/difft-aarch64-unknown-linux-gnu.tar.gz",
|
|
15741
|
+
sha256: "abd2f42d2afd424312b4862aa7c7bb0320447670ae22fabcc5159db03e2dccbd",
|
|
15742
|
+
archive: "tar.gz"
|
|
15743
|
+
}
|
|
15744
|
+
}
|
|
15745
|
+
},
|
|
15746
|
+
{
|
|
15747
|
+
command: "gron",
|
|
15748
|
+
binBasename: "gron",
|
|
15749
|
+
assets: {
|
|
15750
|
+
"win32-x64": {
|
|
15751
|
+
url: "https://github.com/tomnomnom/gron/releases/download/v0.7.1/gron-windows-amd64-0.7.1.zip",
|
|
15752
|
+
sha256: "5ed427a4a504d8e03a1770b71d4ad16a3764179e085b5ae84e51a57b299f300d",
|
|
15753
|
+
archive: "zip"
|
|
15754
|
+
},
|
|
15755
|
+
"win32-arm64": {
|
|
15756
|
+
url: "https://github.com/tomnomnom/gron/releases/download/v0.7.1/gron-windows-arm64-0.7.1.zip",
|
|
15757
|
+
sha256: "9bd38a241f1afdbd3c8f952b92b7090e7a446cac5251bfed3fdf28f219c9dda8",
|
|
15758
|
+
archive: "zip"
|
|
15759
|
+
},
|
|
15760
|
+
"darwin-x64": {
|
|
15761
|
+
url: "https://github.com/tomnomnom/gron/releases/download/v0.7.1/gron-darwin-amd64-0.7.1.tgz",
|
|
15762
|
+
sha256: "59034d4aa883c5815784b290567d104669a51f20eaf97f1d8baa4f74e22047d6",
|
|
15763
|
+
archive: "tar.gz"
|
|
15764
|
+
},
|
|
15765
|
+
"darwin-arm64": {
|
|
15766
|
+
url: "https://github.com/tomnomnom/gron/releases/download/v0.7.1/gron-darwin-arm64-0.7.1.tgz",
|
|
15767
|
+
sha256: "1b9b987c6ead684a992db91b7a32fd15ef946013dfabfe84d00b2fa6f55d7182",
|
|
15768
|
+
archive: "tar.gz"
|
|
15769
|
+
},
|
|
15770
|
+
"linux-x64": {
|
|
15771
|
+
url: "https://github.com/tomnomnom/gron/releases/download/v0.7.1/gron-linux-amd64-0.7.1.tgz",
|
|
15772
|
+
sha256: "ca0335826b02b044fa05d7e951521e45c6ced1c381a73ed5803450088e18bf22",
|
|
15773
|
+
archive: "tar.gz"
|
|
15774
|
+
},
|
|
15775
|
+
"linux-arm64": {
|
|
15776
|
+
url: "https://github.com/tomnomnom/gron/releases/download/v0.7.1/gron-linux-arm64-0.7.1.tgz",
|
|
15777
|
+
sha256: "5d1d4764723a0f768d9ddef0685a052f564c8bbf5e475382342faf4224a07d80",
|
|
15778
|
+
archive: "tar.gz"
|
|
15779
|
+
}
|
|
15780
|
+
}
|
|
15253
15781
|
}
|
|
15254
15782
|
];
|
|
15255
15783
|
|
|
@@ -16023,34 +16551,38 @@ function fetchUrlTool() {
|
|
|
16023
16551
|
};
|
|
16024
16552
|
}
|
|
16025
16553
|
const CODE_SEARCH_PARAMS = Type.Object({
|
|
16026
|
-
query: Type.String({ description: "Search text
|
|
16554
|
+
query: Type.String({ description: "Search text. Natural-language intent in the default `semantic` mode; a literal string in `lexical`/`exact`; a PCRE2 regex in `regex`." }),
|
|
16027
16555
|
mode: Type.Optional(Type.Union([
|
|
16028
|
-
Type.Literal("
|
|
16029
|
-
Type.Literal("
|
|
16030
|
-
Type.Literal("
|
|
16031
|
-
|
|
16556
|
+
Type.Literal("semantic"),
|
|
16557
|
+
Type.Literal("lexical"),
|
|
16558
|
+
Type.Literal("exact"),
|
|
16559
|
+
Type.Literal("regex"),
|
|
16560
|
+
Type.Literal("ast")
|
|
16561
|
+
], { description: "Search mode. `semantic` (DEFAULT): ColBERT meaning-based ranking, falls back to lexical when the index isn't ready (response `source` says which engine ran). `lexical`: BM25F + tree-sitter (best for exact symbols). `exact`: fixed-string. `regex`: PCRE2. `ast`: ast-grep structural (needs `ast_pattern` + `ast_lang`)." })),
|
|
16562
|
+
pattern: Type.Optional(Type.String({ description: "Semantic mode only: regex pre-filter (colgrep -e) — grep first, then rank semantically. Ignored in lexical modes." })),
|
|
16032
16563
|
file_glob: Type.Optional(Type.String({ description: "ripgrep glob filter." })),
|
|
16033
16564
|
limit: Type.Optional(Type.Integer({
|
|
16034
16565
|
minimum: 1,
|
|
16035
16566
|
description: "Max hits to return."
|
|
16036
16567
|
})),
|
|
16037
|
-
structural: Type.Optional(Type.Union([Type.Literal("full"), Type.Literal("topN")], { description: "Structural-ranking depth (
|
|
16038
|
-
complete: Type.Optional(Type.Boolean({ description: "
|
|
16039
|
-
multiline: Type.Optional(Type.Boolean({ description: "Set true with mode:'regex' to let a pattern span newlines (ripgrep -U), e.g. 'foo[\\s\\S]*?bar' across lines. (literal/
|
|
16040
|
-
ast_pattern: Type.Optional(Type.String({ description: "ast
|
|
16568
|
+
structural: Type.Optional(Type.Union([Type.Literal("full"), Type.Literal("topN")], { description: "Structural-ranking depth (lexical mode only)." })),
|
|
16569
|
+
complete: Type.Optional(Type.Boolean({ description: "Lexical mode: when true, return the COMPLETE match set (every line ripgrep would find, capped only by `limit`) — disables the default precision shoulder cut + per-file cap. Use it when you must not miss any occurrence (every caller of X, a rename, an audit). The default response `notice` says when matches were hidden." })),
|
|
16570
|
+
multiline: Type.Optional(Type.Boolean({ description: "Set true with mode:'regex' to let a pattern span newlines (ripgrep -U), e.g. 'foo[\\s\\S]*?bar' across lines. (literal/lexical queries can't contain a newline.)" })),
|
|
16571
|
+
ast_pattern: Type.Optional(Type.String({ description: "mode:'ast' structural pattern (e.g. 'function $F($$$) { $$$ }'). Matches come from ast-grep instead of ripgrep — for multi-line AST shapes the regex modes can't express. Takes precedence over `query`. REQUIRES `ast_lang`. If ast-grep isn't installed you get a `notice`; it never falls back to regex." })),
|
|
16041
16572
|
ast_lang: Type.Optional(Type.String({ description: "Language grammar for `ast_pattern` (REQUIRED with it): 'ts' | 'tsx' | 'js' | 'py' | 'rust' | 'go' | … Without it ast-grep cross-matches every language and returns garbage." }))
|
|
16042
16573
|
});
|
|
16043
16574
|
function codeSearchTool(workspace) {
|
|
16044
16575
|
return {
|
|
16045
16576
|
name: "code_search",
|
|
16046
|
-
label: "
|
|
16047
|
-
description: "
|
|
16577
|
+
label: "Code search (semantic-first)",
|
|
16578
|
+
description: "Semantic-first code search over the worker's workspace. Default (`mode:\"semantic\"`) ranks by MEANING via ColBERT and transparently falls back to lexical BM25F when the index isn't ready (the response `source` is \"semantic\" | \"lexical\" | \"lexical-fallback\"). Force lexical with mode `lexical` (exact symbols) / `exact` / `regex` / `ast`. Prefer over `grep` for \"where is X / which files reference Y\" discovery. Returns `{source, results:[{file,line,snippet}], ...}` in JSON.",
|
|
16048
16579
|
parameters: CODE_SEARCH_PARAMS,
|
|
16049
16580
|
async execute(_toolCallId, params, signal) {
|
|
16050
|
-
const r = await
|
|
16581
|
+
const r = await runUnifiedCodeSearch({
|
|
16051
16582
|
query: params.query,
|
|
16052
16583
|
workspace,
|
|
16053
16584
|
mode: params.mode,
|
|
16585
|
+
pattern: params.pattern,
|
|
16054
16586
|
file_glob: params.file_glob,
|
|
16055
16587
|
limit: params.limit,
|
|
16056
16588
|
structural: params.structural,
|
|
@@ -16061,18 +16593,251 @@ function codeSearchTool(workspace) {
|
|
|
16061
16593
|
summary: false
|
|
16062
16594
|
}, signal);
|
|
16063
16595
|
const minimal = {
|
|
16596
|
+
source: r.source,
|
|
16064
16597
|
results: r.results.map((h) => ({
|
|
16065
16598
|
file: h.file,
|
|
16066
16599
|
line: h.line,
|
|
16067
16600
|
snippet: h.snippet
|
|
16068
16601
|
})),
|
|
16069
|
-
truncated: r.truncated,
|
|
16602
|
+
truncated: r.truncated ?? false,
|
|
16070
16603
|
notice: r.notice ?? void 0
|
|
16071
16604
|
};
|
|
16072
16605
|
return textResult(JSON.stringify(minimal));
|
|
16073
16606
|
}
|
|
16074
16607
|
};
|
|
16075
16608
|
}
|
|
16609
|
+
/**
|
|
16610
|
+
* Allowlisted read-only analysis CLIs the worker may invoke through the
|
|
16611
|
+
* `toolbelt` tool. Each runs via `runManagedExeCapture` with `shell:false`,
|
|
16612
|
+
* so args are passed LITERALLY — no pipes / redirects / chaining / glob
|
|
16613
|
+
* expansion / `rm`. `sd` is deliberately ABSENT (it rewrites files in
|
|
16614
|
+
* place); it stays available to `implement` via `bash`.
|
|
16615
|
+
*/
|
|
16616
|
+
const TOOLBELT_TOOLS$1 = [
|
|
16617
|
+
"rg",
|
|
16618
|
+
"fd",
|
|
16619
|
+
"sg",
|
|
16620
|
+
"jq",
|
|
16621
|
+
"yq",
|
|
16622
|
+
"gron",
|
|
16623
|
+
"scc",
|
|
16624
|
+
"tokei",
|
|
16625
|
+
"difft",
|
|
16626
|
+
"git"
|
|
16627
|
+
];
|
|
16628
|
+
/**
|
|
16629
|
+
* Per-tool denied flags, split into `short` (single chars, matched
|
|
16630
|
+
* per-character across a cluster so attached / combined forms like
|
|
16631
|
+
* `fd -Hx`, `fd -xCMD`, `sg -iU` can't slip past an exact-token check) and
|
|
16632
|
+
* `long` (`--flag`, matched on the name even with an `=value` suffix). The
|
|
16633
|
+
* no-shell spawn already blocks the big vectors (redirects, chaining,
|
|
16634
|
+
* arbitrary programs); these block the specific exec / file-write flags the
|
|
16635
|
+
* individual CLIs expose. PER-TOOL, not global, because the same flag means
|
|
16636
|
+
* different things across tools (`rg -i` = ignore-case [read]; `yq -i` =
|
|
16637
|
+
* in-place [write]).
|
|
16638
|
+
*/
|
|
16639
|
+
const TOOLBELT_DENIED_FLAGS = {
|
|
16640
|
+
fd: {
|
|
16641
|
+
short: ["x", "X"],
|
|
16642
|
+
long: ["--exec", "--exec-batch"]
|
|
16643
|
+
},
|
|
16644
|
+
rg: {
|
|
16645
|
+
short: [],
|
|
16646
|
+
long: ["--pre", "--hostname-bin"]
|
|
16647
|
+
},
|
|
16648
|
+
sg: {
|
|
16649
|
+
short: ["U", "i"],
|
|
16650
|
+
long: [
|
|
16651
|
+
"--rewrite",
|
|
16652
|
+
"--update-all",
|
|
16653
|
+
"--update",
|
|
16654
|
+
"--interactive"
|
|
16655
|
+
]
|
|
16656
|
+
},
|
|
16657
|
+
yq: {
|
|
16658
|
+
short: ["i", "s"],
|
|
16659
|
+
long: [
|
|
16660
|
+
"--inplace",
|
|
16661
|
+
"--in-place",
|
|
16662
|
+
"--split-exp"
|
|
16663
|
+
]
|
|
16664
|
+
},
|
|
16665
|
+
scc: {
|
|
16666
|
+
short: ["o"],
|
|
16667
|
+
long: ["--output", "--format-multi"]
|
|
16668
|
+
}
|
|
16669
|
+
};
|
|
16670
|
+
/**
|
|
16671
|
+
* ast-grep (`sg`) subcommands that write files (`new` scaffolds a project /
|
|
16672
|
+
* rules / tests) or start a long-running server (`lsp`). The default
|
|
16673
|
+
* subcommand is `run` (search), and `scan`/`test` are read-only unless a
|
|
16674
|
+
* denied write flag (`-U`/`-i`/`--rewrite`) is also passed — so only these
|
|
16675
|
+
* two need an explicit positional block.
|
|
16676
|
+
*/
|
|
16677
|
+
const SG_DENIED_SUBCOMMANDS = new Set(["new", "lsp"]);
|
|
16678
|
+
/** Runtime allowlist guard (defense-in-depth on top of the schema enum). */
|
|
16679
|
+
const TOOLBELT_TOOL_SET = new Set(TOOLBELT_TOOLS$1);
|
|
16680
|
+
/**
|
|
16681
|
+
* Read-only git subcommands. The worker must pass the subcommand as
|
|
16682
|
+
* `args[0]` (no leading global flags like `-C`/`-c`, which can redirect
|
|
16683
|
+
* git or inject config); everything not in this set — every mutating
|
|
16684
|
+
* subcommand (commit/checkout/reset/rebase/push/clean/rm/…) — is rejected.
|
|
16685
|
+
* `cwd` is already the workspace, so `-C` is unnecessary.
|
|
16686
|
+
*/
|
|
16687
|
+
const GIT_READONLY_SUBCOMMANDS = new Set([
|
|
16688
|
+
"log",
|
|
16689
|
+
"show",
|
|
16690
|
+
"diff",
|
|
16691
|
+
"blame",
|
|
16692
|
+
"status",
|
|
16693
|
+
"ls-files",
|
|
16694
|
+
"ls-tree",
|
|
16695
|
+
"rev-parse",
|
|
16696
|
+
"shortlog",
|
|
16697
|
+
"describe",
|
|
16698
|
+
"cat-file",
|
|
16699
|
+
"for-each-ref",
|
|
16700
|
+
"name-rev",
|
|
16701
|
+
"rev-list"
|
|
16702
|
+
]);
|
|
16703
|
+
/**
|
|
16704
|
+
* git flags that write files or execute helper programs, rejected in ANY
|
|
16705
|
+
* position (args[0] is the validated subcommand; these can follow it).
|
|
16706
|
+
* Matched on the `--flag` name, tolerating an `=value` suffix. Short
|
|
16707
|
+
* aliases (`-o`, `-O`) are intentionally NOT denied — they are overloaded
|
|
16708
|
+
* with read-only meanings across the allowed subcommands (`ls-files -o`
|
|
16709
|
+
* = --others; `diff -O<orderfile>` reads an order file).
|
|
16710
|
+
*/
|
|
16711
|
+
const GIT_DENIED_FLAGS = new Set([
|
|
16712
|
+
"--output",
|
|
16713
|
+
"--open-files-in-pager",
|
|
16714
|
+
"--ext-diff",
|
|
16715
|
+
"--textconv",
|
|
16716
|
+
"--filters"
|
|
16717
|
+
]);
|
|
16718
|
+
/**
|
|
16719
|
+
* Diff-producing subcommands where git would otherwise honor a configured
|
|
16720
|
+
* external-diff / textconv helper (exec) on matching files. We force
|
|
16721
|
+
* `--no-ext-diff --no-textconv` after the subcommand so a repo with a
|
|
16722
|
+
* malicious local config can't turn a plain `git log -p` / `git show` into
|
|
16723
|
+
* code execution. (User-supplied `--ext-diff`/`--textconv` are separately
|
|
16724
|
+
* denied, so they can't re-enable it after our defaults.)
|
|
16725
|
+
*/
|
|
16726
|
+
const GIT_DIFF_PRODUCING = new Set([
|
|
16727
|
+
"log",
|
|
16728
|
+
"show",
|
|
16729
|
+
"diff"
|
|
16730
|
+
]);
|
|
16731
|
+
const TOOLBELT_PARAMS = Type.Object({
|
|
16732
|
+
tool: Type.Union(TOOLBELT_TOOLS$1.map((t) => Type.Literal(t)), { description: "Which read-only analysis CLI to run: rg (ripgrep search), fd (file find), sg (ast-grep structural search), jq (JSON), yq (YAML/TOML/XML), gron (flatten JSON to greppable lines), scc (code stats: LOC + complexity), tokei (code stats), difft (difftastic structural diff), git (read-only subcommands only)." }),
|
|
16733
|
+
args: Type.Optional(Type.Array(Type.String(), { description: "Arguments passed LITERALLY to the tool (no shell: no pipes, redirects, chaining, or glob expansion). For git, args[0] must be a read-only subcommand (log/show/diff/blame/ls-files/…)." }))
|
|
16734
|
+
});
|
|
16735
|
+
/**
|
|
16736
|
+
* True iff `arg` triggers a denied flag. Long flags (`--foo`) match on the
|
|
16737
|
+
* name, tolerating a `=value` suffix. Short flags are matched per-character
|
|
16738
|
+
* across a cluster (`-Hx`, `-xVALUE`) so attached / combined forms can't
|
|
16739
|
+
* bypass an exact-token check. Conservative: a denied short char appearing
|
|
16740
|
+
* as the value of a preceding value-taking short flag is also rejected (the
|
|
16741
|
+
* worker can re-issue with a space-separated form).
|
|
16742
|
+
*/
|
|
16743
|
+
function argViolatesDenylist(denied, arg) {
|
|
16744
|
+
if (arg.startsWith("--")) {
|
|
16745
|
+
const eq = arg.indexOf("=");
|
|
16746
|
+
const name$1 = eq === -1 ? arg : arg.slice(0, eq);
|
|
16747
|
+
return denied.long.includes(name$1);
|
|
16748
|
+
}
|
|
16749
|
+
if (arg.length >= 2 && arg[0] === "-" && arg[1] !== "-") {
|
|
16750
|
+
for (const ch of arg.slice(1)) if (denied.short.includes(ch)) return true;
|
|
16751
|
+
}
|
|
16752
|
+
return false;
|
|
16753
|
+
}
|
|
16754
|
+
/** True iff `arg` is a git denied flag (`--name`, `--name=value`, or a git
|
|
16755
|
+
* long-option abbreviation of one — git's parseopt accepts unambiguous
|
|
16756
|
+
* prefixes, so `--ext-d` resolves to `--ext-diff`). */
|
|
16757
|
+
function gitArgDenied(arg) {
|
|
16758
|
+
if (!arg.startsWith("--")) return false;
|
|
16759
|
+
const eq = arg.indexOf("=");
|
|
16760
|
+
const name$1 = eq === -1 ? arg : arg.slice(0, eq);
|
|
16761
|
+
if (GIT_DENIED_FLAGS.has(name$1)) return true;
|
|
16762
|
+
if (name$1.length >= 3) {
|
|
16763
|
+
for (const flag of GIT_DENIED_FLAGS) if (flag.startsWith(name$1)) return true;
|
|
16764
|
+
}
|
|
16765
|
+
return false;
|
|
16766
|
+
}
|
|
16767
|
+
/**
|
|
16768
|
+
* Build the actual git argv: prepend safe global options + force read-only
|
|
16769
|
+
* diff defaults so a repo with a malicious local config can't turn a git
|
|
16770
|
+
* call into code execution or a file write. `--no-pager` (also
|
|
16771
|
+
* GIT_PAGER=cat) kills the pager; `--no-optional-locks` (also
|
|
16772
|
+
* GIT_OPTIONAL_LOCKS=0) stops `status` from refreshing/writing `.git/index`;
|
|
16773
|
+
* `--no-ext-diff`/`--no-textconv` on diff-producing subcommands disable
|
|
16774
|
+
* configured external-diff / textconv helpers. `args[0]` is the validated
|
|
16775
|
+
* subcommand.
|
|
16776
|
+
*/
|
|
16777
|
+
function buildGitExecArgs(args) {
|
|
16778
|
+
const sub = args[0] ?? "";
|
|
16779
|
+
const out = [
|
|
16780
|
+
"--no-pager",
|
|
16781
|
+
"--no-optional-locks",
|
|
16782
|
+
sub
|
|
16783
|
+
];
|
|
16784
|
+
if (GIT_DIFF_PRODUCING.has(sub)) out.push("--no-ext-diff", "--no-textconv");
|
|
16785
|
+
out.push(...args.slice(1));
|
|
16786
|
+
return out;
|
|
16787
|
+
}
|
|
16788
|
+
function toolbeltTool(workspace) {
|
|
16789
|
+
return {
|
|
16790
|
+
name: "toolbelt",
|
|
16791
|
+
label: "Toolbelt CLI (read-only)",
|
|
16792
|
+
description: "Run a read-only code-analysis CLI in the workspace with NO shell (args are literal — no pipes / redirects / chaining / globbing). Tools: rg, fd, sg (ast-grep), jq, yq, gron, scc, tokei, difft (difftastic), and git (read-only subcommands). Write/exec flags (fd -x, rg --pre, ast-grep --rewrite, yq -i) and mutating git subcommands are rejected. Returns combined stdout (stderr appended on non-zero exit).",
|
|
16793
|
+
parameters: TOOLBELT_PARAMS,
|
|
16794
|
+
async execute(_toolCallId, params, signal) {
|
|
16795
|
+
const tool = params.tool;
|
|
16796
|
+
const args = Array.isArray(params.args) ? params.args.map(String) : [];
|
|
16797
|
+
if (!TOOLBELT_TOOL_SET.has(tool)) throw new Error(`toolbelt: unknown tool '${tool}'`);
|
|
16798
|
+
if (tool === "git") {
|
|
16799
|
+
const sub = args[0];
|
|
16800
|
+
if (!sub || !GIT_READONLY_SUBCOMMANDS.has(sub)) throw new Error(`git: only read-only subcommands are allowed and the subcommand must be args[0] (no leading -C/-c). Allowed: ${[...GIT_READONLY_SUBCOMMANDS].join(", ")}. Got: ${sub ? `'${sub}'` : "<none>"}`);
|
|
16801
|
+
for (const arg of args) if (gitArgDenied(arg)) throw new Error(`git: flag '${arg}' is not allowed (toolbelt is read-only)`);
|
|
16802
|
+
} else {
|
|
16803
|
+
if (tool === "sg" && args[0] && SG_DENIED_SUBCOMMANDS.has(args[0])) throw new Error(`sg: subcommand '${args[0]}' is not allowed (toolbelt is read-only)`);
|
|
16804
|
+
const denied = TOOLBELT_DENIED_FLAGS[tool];
|
|
16805
|
+
if (denied) {
|
|
16806
|
+
for (const arg of args) if (argViolatesDenylist(denied, arg)) throw new Error(`${tool}: arg '${arg}' carries a write/exec flag (toolbelt is read-only)`);
|
|
16807
|
+
}
|
|
16808
|
+
}
|
|
16809
|
+
const env = buildEnv();
|
|
16810
|
+
if (tool === "git") {
|
|
16811
|
+
env.GIT_PAGER = "cat";
|
|
16812
|
+
env.PAGER = "cat";
|
|
16813
|
+
env.GIT_TERMINAL_PROMPT = "0";
|
|
16814
|
+
env.GIT_OPTIONAL_LOCKS = "0";
|
|
16815
|
+
}
|
|
16816
|
+
const binPath = resolveExecutable(tool, { env });
|
|
16817
|
+
if (!binPath) return textResult(`${tool}: not available on this host (not on PATH / toolbelt). rg/fd/jq/yq/sg/gron/scc/difft ship with the toolbelt; git and tokei may require a system install.`);
|
|
16818
|
+
const TOOLBELT_TIMEOUT_MS = 6e4;
|
|
16819
|
+
const TOOLBELT_STDOUT_CAP = 1024 * 1024;
|
|
16820
|
+
const res = await runManagedExeCapture(binPath, tool === "git" ? buildGitExecArgs(args) : args, {
|
|
16821
|
+
cwd: workspace,
|
|
16822
|
+
env,
|
|
16823
|
+
timeoutMs: TOOLBELT_TIMEOUT_MS,
|
|
16824
|
+
maxStdoutBytes: TOOLBELT_STDOUT_CAP,
|
|
16825
|
+
onSpawn: (child) => {
|
|
16826
|
+
if (signal?.aborted) killChildTree(child);
|
|
16827
|
+
else signal?.addEventListener("abort", () => killChildTree(child), { once: true });
|
|
16828
|
+
}
|
|
16829
|
+
});
|
|
16830
|
+
if (signal?.aborted) throw new Error(`${tool} aborted`);
|
|
16831
|
+
if (res.timedOut) throw new Error(`${tool} timed out after ${TOOLBELT_TIMEOUT_MS}ms`);
|
|
16832
|
+
const parts = [];
|
|
16833
|
+
if (res.stdout) parts.push(res.stdout);
|
|
16834
|
+
if ((res.code !== 0 || !res.stdout) && res.stderr.trim()) parts.push(`[stderr] ${res.stderr.trim()}`);
|
|
16835
|
+
if (res.stdoutTruncated) parts.push(`[truncated at ${TOOLBELT_STDOUT_CAP} bytes — narrow the query]`);
|
|
16836
|
+
if (parts.length === 0) parts.push(`(${tool} exited ${res.code} with no output)`);
|
|
16837
|
+
return textResult(parts.join("\n"));
|
|
16838
|
+
}
|
|
16839
|
+
};
|
|
16840
|
+
}
|
|
16076
16841
|
const PEER_CRITIC_TUPLE = [
|
|
16077
16842
|
Type.Literal("codex_critic"),
|
|
16078
16843
|
Type.Literal("gemini_critic"),
|
|
@@ -16127,6 +16892,7 @@ function codexReviewTool() {
|
|
|
16127
16892
|
label: "Codex code review",
|
|
16128
16893
|
description: "Code review by `codex-reviewer` (gpt-5.3-codex, code-specialist critic). Returns line-level findings on a diff or single file. Use to overcome blind spots on a coding change before committing.",
|
|
16129
16894
|
parameters: CODEX_REVIEW_PARAMS,
|
|
16895
|
+
executionMode: "sequential",
|
|
16130
16896
|
async execute(_toolCallId, params, signal) {
|
|
16131
16897
|
if (networkDisabled()) throw new Error("rejected: network disabled");
|
|
16132
16898
|
const persona = lookupPersona("codex-reviewer");
|
|
@@ -16165,30 +16931,192 @@ const ADVISOR_PARAMS = Type.Object({ concern: Type.String({
|
|
|
16165
16931
|
* cases consistent. Override via env if needed. */
|
|
16166
16932
|
const ADVISOR_TRANSCRIPT_MAX_CHARS = Number(process$1.env.GH_ROUTER_WORKER_ADVISOR_MAX_CHARS ?? 72e4);
|
|
16167
16933
|
/**
|
|
16934
|
+
* Render Pi's `Agent.state.messages` as a flat text transcript for
|
|
16935
|
+
* the advisor's user prompt. Mirrors the intent of advisor.ts's
|
|
16936
|
+
* `renderConversationAsText` but consumes Pi's shape directly
|
|
16937
|
+
* (`UserMessage | AssistantMessage | ToolResultMessage` plus harness-
|
|
16938
|
+
* custom messages — we walk only the LLM-meaningful three and skip
|
|
16939
|
+
* custom variants since the advisor never needs UI status events).
|
|
16940
|
+
*
|
|
16941
|
+
* Truncation policy: keep the TAIL. If the joined transcript exceeds
|
|
16942
|
+
* `maxChars`, drop entries from the front until it fits and prepend a
|
|
16943
|
+
* `[…earlier turns omitted…]` marker. This matches advisor.ts's
|
|
16944
|
+
* front-truncate strategy — the freshest turn is where the worker is
|
|
16945
|
+
* stuck.
|
|
16946
|
+
*/
|
|
16947
|
+
function renderPiMessagesAsText(messages, maxChars) {
|
|
16948
|
+
const lines = [];
|
|
16949
|
+
for (const msg of messages) {
|
|
16950
|
+
if (typeof msg !== "object" || msg === null) continue;
|
|
16951
|
+
const role = msg.role;
|
|
16952
|
+
if (role === "user") {
|
|
16953
|
+
const content = msg.content;
|
|
16954
|
+
lines.push(`USER: ${stringifyMessageContent(content)}`);
|
|
16955
|
+
} else if (role === "assistant") {
|
|
16956
|
+
const content = msg.content;
|
|
16957
|
+
lines.push(`ASSISTANT: ${stringifyMessageContent(content)}`);
|
|
16958
|
+
} else if (role === "toolResult") {
|
|
16959
|
+
const m = msg;
|
|
16960
|
+
const flag = m.isError ? " [error]" : "";
|
|
16961
|
+
lines.push(`TOOL_RESULT ${m.toolName ?? "?"}${flag}: ${stringifyMessageContent(m.content)}`);
|
|
16962
|
+
}
|
|
16963
|
+
}
|
|
16964
|
+
let joined = lines.join("\n\n");
|
|
16965
|
+
if (joined.length <= maxChars) return joined;
|
|
16966
|
+
const marker = "[…earlier turns omitted…]\n\n";
|
|
16967
|
+
const budget = maxChars - 27;
|
|
16968
|
+
while (joined.length > budget && lines.length > 0) {
|
|
16969
|
+
lines.shift();
|
|
16970
|
+
joined = lines.join("\n\n");
|
|
16971
|
+
}
|
|
16972
|
+
return marker + joined;
|
|
16973
|
+
}
|
|
16974
|
+
/**
|
|
16975
|
+
* Flatten a message's content (union of string / TextContent[] /
|
|
16976
|
+
* ToolCall[] / ImageContent[]) to a single text line. Images become
|
|
16977
|
+
* `[image]` placeholders — the advisor only needs to know they
|
|
16978
|
+
* existed, not see their bytes. ToolCalls render as
|
|
16979
|
+
* `→ <toolName>(<args-as-json>)` so the advisor can reason about
|
|
16980
|
+
* what the worker tried.
|
|
16981
|
+
*/
|
|
16982
|
+
function stringifyMessageContent(content) {
|
|
16983
|
+
if (typeof content === "string") return content;
|
|
16984
|
+
if (!Array.isArray(content)) return "";
|
|
16985
|
+
const parts = [];
|
|
16986
|
+
for (const part of content) {
|
|
16987
|
+
if (typeof part !== "object" || part === null) continue;
|
|
16988
|
+
const p = part;
|
|
16989
|
+
if (p.type === "text" && typeof p.text === "string") parts.push(p.text);
|
|
16990
|
+
else if (p.type === "image") parts.push("[image]");
|
|
16991
|
+
else if (p.type === "thinking") continue;
|
|
16992
|
+
else if (p.type === "toolCall") {
|
|
16993
|
+
const name$1 = typeof p.toolName === "string" ? p.toolName : "?";
|
|
16994
|
+
const args = typeof p.input === "object" && p.input !== null ? JSON.stringify(p.input) : "";
|
|
16995
|
+
parts.push(`→ ${name$1}(${args.slice(0, 200)})`);
|
|
16996
|
+
}
|
|
16997
|
+
}
|
|
16998
|
+
return parts.join(" ");
|
|
16999
|
+
}
|
|
17000
|
+
function advisorTool(getMessages) {
|
|
17001
|
+
return {
|
|
17002
|
+
name: "advisor",
|
|
17003
|
+
label: "Advisor",
|
|
17004
|
+
description: "Consult a stronger reviewer model (cross-lab: gpt-5.5 xhigh by default) on a specific concern. Use BEFORE substantive work, WHEN stuck, or WHEN considering a change of approach. The advisor automatically receives the recent conversation transcript as context — give it a focused `concern`, not background.",
|
|
17005
|
+
parameters: ADVISOR_PARAMS,
|
|
17006
|
+
async execute(_toolCallId, params, signal) {
|
|
17007
|
+
if (networkDisabled()) throw new Error("rejected: network disabled");
|
|
17008
|
+
const advisorSystem = "You are an expert advisor reviewing an in-progress coding worker's concern. The worker shares its recent conversation transcript (USER / ASSISTANT / TOOL_RESULT lines) followed by the specific concern under `### Concern`. Provide concrete, actionable advice grounded in the transcript — name the specific assumption or step to revisit. If the worker is on the right track, say so. Aim for 2–5 paragraphs of substantive guidance.";
|
|
17009
|
+
const transcript = getMessages ? renderPiMessagesAsText(getMessages(), ADVISOR_TRANSCRIPT_MAX_CHARS) : "";
|
|
17010
|
+
const userText = transcript.length > 0 ? `### Recent transcript\n${transcript}\n\n### Concern\n${params.concern}` : `### Concern\n${params.concern}`;
|
|
17011
|
+
const resolvedModel = resolveModel(ADVISOR_DEFAULT_MODEL);
|
|
17012
|
+
const release = acquireInFlightSlot();
|
|
17013
|
+
if (!release) throw new Error(`advisor: MCP in-flight cap (${MAX_INFLIGHT_TOOLS_CALL}) saturated; retry shortly`);
|
|
17014
|
+
try {
|
|
17015
|
+
const text = extractResponsesText(await createResponses({
|
|
17016
|
+
model: resolvedModel,
|
|
17017
|
+
instructions: advisorSystem,
|
|
17018
|
+
input: [{
|
|
17019
|
+
role: "user",
|
|
17020
|
+
content: [{
|
|
17021
|
+
type: "input_text",
|
|
17022
|
+
text: userText
|
|
17023
|
+
}]
|
|
17024
|
+
}],
|
|
17025
|
+
stream: false,
|
|
17026
|
+
reasoning: { effort: ADVISOR_DEFAULT_EFFORT }
|
|
17027
|
+
}, void 0, signal));
|
|
17028
|
+
if (!text) throw new Error("advisor returned empty output");
|
|
17029
|
+
return textResult(text);
|
|
17030
|
+
} finally {
|
|
17031
|
+
release();
|
|
17032
|
+
}
|
|
17033
|
+
}
|
|
17034
|
+
};
|
|
17035
|
+
}
|
|
17036
|
+
const UPDATE_PLAN_PARAMS = Type.Object({
|
|
17037
|
+
steps: Type.Array(Type.Object({
|
|
17038
|
+
title: Type.String({
|
|
17039
|
+
minLength: 1,
|
|
17040
|
+
description: "Short imperative description of the step."
|
|
17041
|
+
}),
|
|
17042
|
+
status: Type.Union([
|
|
17043
|
+
Type.Literal("pending"),
|
|
17044
|
+
Type.Literal("in_progress"),
|
|
17045
|
+
Type.Literal("completed")
|
|
17046
|
+
], { description: "Current status of this step." })
|
|
17047
|
+
}), {
|
|
17048
|
+
minItems: 1,
|
|
17049
|
+
description: "The FULL ordered plan. Each call replaces the previous plan, so always send every step (not just the changed one)."
|
|
17050
|
+
}),
|
|
17051
|
+
explanation: Type.Optional(Type.String({ description: "Optional one-line note on what changed this update." }))
|
|
17052
|
+
});
|
|
17053
|
+
function createPlanState() {
|
|
17054
|
+
return { current: [] };
|
|
17055
|
+
}
|
|
17056
|
+
/** Deterministic checklist render: `N. [ |~|x] title`, optional leading
|
|
17057
|
+
* explanation line. Used both as the tool's return value and as the
|
|
17058
|
+
* per-turn reminder injected at the request boundary. */
|
|
17059
|
+
function renderPlan(state$1) {
|
|
17060
|
+
if (state$1.current.length === 0) return "(no plan yet)";
|
|
17061
|
+
const mark = (s) => s === "completed" ? "x" : s === "in_progress" ? "~" : " ";
|
|
17062
|
+
const lines = state$1.current.map((step, i) => `${i + 1}. [${mark(step.status)}] ${step.title}`);
|
|
17063
|
+
return `${state$1.explanation ? `${state$1.explanation}\n` : ""}${lines.join("\n")}`;
|
|
17064
|
+
}
|
|
17065
|
+
function updatePlanTool(planState) {
|
|
17066
|
+
return {
|
|
17067
|
+
name: "update_plan",
|
|
17068
|
+
label: "Update plan",
|
|
17069
|
+
description: "Maintain a short, ordered checklist for the delegated task. Call it at the start (lay out the steps) and again whenever a step's status changes (mark one in_progress / completed). Each call REPLACES the whole plan — always send the full ordered list. The current plan is re-surfaced to you every turn so it survives context compaction; use it to stay oriented on long, multi-step work.",
|
|
17070
|
+
parameters: UPDATE_PLAN_PARAMS,
|
|
17071
|
+
executionMode: "sequential",
|
|
17072
|
+
async execute(_toolCallId, params) {
|
|
17073
|
+
const steps = params.steps.map((s) => ({
|
|
17074
|
+
title: s.title,
|
|
17075
|
+
status: s.status
|
|
17076
|
+
}));
|
|
17077
|
+
if (planState) {
|
|
17078
|
+
planState.current = steps;
|
|
17079
|
+
planState.explanation = params.explanation;
|
|
17080
|
+
}
|
|
17081
|
+
return textResult(renderPlan(planState ?? {
|
|
17082
|
+
current: steps,
|
|
17083
|
+
explanation: params.explanation
|
|
17084
|
+
}));
|
|
17085
|
+
}
|
|
17086
|
+
};
|
|
17087
|
+
}
|
|
17088
|
+
/**
|
|
16168
17089
|
* Build the AgentTool array for the requested mode.
|
|
16169
17090
|
*
|
|
16170
|
-
* - explore →
|
|
16171
|
-
*
|
|
17091
|
+
* - explore → 9 read-only tools (read, glob, grep, code_search,
|
|
17092
|
+
* web_search, fetch_url, toolbelt, advisor, update_plan)
|
|
17093
|
+
* - review → same 9 read-only tools as explore (reviewer framing lives
|
|
16172
17094
|
* in the system prompt, not the toolset)
|
|
16173
|
-
* - implement → explore + edit/write/bash/codex_review
|
|
17095
|
+
* - implement → explore + edit/write/bash/codex_review (13 total)
|
|
17096
|
+
*
|
|
17097
|
+
* `peer_review` is intentionally NOT wired in (peer critics aren't part of
|
|
17098
|
+
* the worker surface); `advisor` is the worker's consultation path.
|
|
16174
17099
|
*
|
|
16175
|
-
* Order matches the
|
|
16176
|
-
*
|
|
16177
|
-
*
|
|
17100
|
+
* Order matches the prompt-mode-note for stability — Pi's tool-injection
|
|
17101
|
+
* shape includes the list verbatim, so a stable order keeps the model's
|
|
17102
|
+
* tool-name prediction cache warm.
|
|
16178
17103
|
*
|
|
16179
17104
|
* Each call returns FRESH tool objects (workspace is closure-captured
|
|
16180
17105
|
* per call), so two concurrent worker runs against different
|
|
16181
17106
|
* workspaces don't share state.
|
|
16182
17107
|
*/
|
|
16183
17108
|
function buildWorkerTools(opts) {
|
|
16184
|
-
const { mode, workspace } = opts;
|
|
17109
|
+
const { mode, workspace, getMessages, planState } = opts;
|
|
16185
17110
|
const explore = [
|
|
16186
17111
|
readTool(workspace),
|
|
16187
17112
|
globTool(workspace),
|
|
16188
17113
|
grepTool(workspace),
|
|
16189
17114
|
codeSearchTool(workspace),
|
|
16190
17115
|
webSearchTool(),
|
|
16191
|
-
fetchUrlTool()
|
|
17116
|
+
fetchUrlTool(),
|
|
17117
|
+
toolbeltTool(workspace),
|
|
17118
|
+
advisorTool(getMessages),
|
|
17119
|
+
updatePlanTool(planState)
|
|
16192
17120
|
];
|
|
16193
17121
|
if (mode === "explore" || mode === "review") return explore;
|
|
16194
17122
|
return [
|
|
@@ -16499,19 +17427,29 @@ async function createWorktree(workspaceAbs, opts) {
|
|
|
16499
17427
|
*/
|
|
16500
17428
|
const WORKTREE_REGISTRY = new WorktreeRegistry();
|
|
16501
17429
|
registerExitHandlers(WORKTREE_REGISTRY);
|
|
16502
|
-
/** Default model + thinking
|
|
16503
|
-
*
|
|
16504
|
-
*
|
|
16505
|
-
*
|
|
16506
|
-
*
|
|
16507
|
-
*
|
|
16508
|
-
*
|
|
16509
|
-
*
|
|
16510
|
-
*
|
|
16511
|
-
*
|
|
16512
|
-
*
|
|
16513
|
-
|
|
17430
|
+
/** Default model + thinking for the READ-ONLY worker modes (`explore`,
|
|
17431
|
+
* `review`). `gemini-3.5-flash` at `high` (its top reasoning tier) — fast,
|
|
17432
|
+
* 1M-context, tool-call-capable.
|
|
17433
|
+
*
|
|
17434
|
+
* HISTORY / CAVEAT: an earlier iteration moved OFF flash to
|
|
17435
|
+
* `gemini-3.1-pro-preview` because *that* flash early-stopped with empty
|
|
17436
|
+
* turns on the function-calling loop. `gemini-3.5-flash` is a NEWER model
|
|
17437
|
+
* and is being re-evaluated for the read-only workload, where parallel
|
|
17438
|
+
* read/search batches and sound stop/continue decisions matter. If it
|
|
17439
|
+
* regresses to early-stopping, revert this to `gemini-3.1-pro-preview`.
|
|
17440
|
+
*
|
|
17441
|
+
* Exported so the MCP handler + the gate (`workerToolsEnabled`) read the
|
|
17442
|
+
* same constant — drift would ship a tool whose docs/gate disagree with
|
|
17443
|
+
* its runtime default. Caller can override per call via the `model` arg. */
|
|
17444
|
+
const DEFAULT_MODEL = "gemini-3.5-flash";
|
|
16514
17445
|
const DEFAULT_THINKING = "high";
|
|
17446
|
+
/** Default model + thinking for the READ+WRITE `implement` mode. `gpt-5.5`
|
|
17447
|
+
* at `xhigh` — the strongest reasoning tier in the catalog, 1M+ context,
|
|
17448
|
+
* routed through `/responses` by the stream-fn endpoint split. Coding edits
|
|
17449
|
+
* benefit from maximum reasoning; the higher per-call cost is justified for
|
|
17450
|
+
* autonomous implementation. An explicit `opts.model` still wins. */
|
|
17451
|
+
const IMPLEMENT_DEFAULT_MODEL = "gpt-5.5";
|
|
17452
|
+
const IMPLEMENT_DEFAULT_THINKING = "xhigh";
|
|
16515
17453
|
/** Default model for `browse` mode. `gpt-5.4-mini` — the Gate-B-winning
|
|
16516
17454
|
* browse model (small + fast enough to drive a tab at human pace, with
|
|
16517
17455
|
* enough tool-calling discipline to terminate). This is DISTINCT from the
|
|
@@ -16619,9 +17557,12 @@ async function runWorkerAgent(opts) {
|
|
|
16619
17557
|
};
|
|
16620
17558
|
try {
|
|
16621
17559
|
const isBrowse = opts.mode === "browse";
|
|
17560
|
+
const isImplement = opts.mode === "implement";
|
|
17561
|
+
const defaultModel = isBrowse ? BROWSE_DEFAULT_MODEL : isImplement ? IMPLEMENT_DEFAULT_MODEL : DEFAULT_MODEL;
|
|
17562
|
+
const defaultThinking = isBrowse ? BROWSE_DEFAULT_THINKING : isImplement ? IMPLEMENT_DEFAULT_THINKING : DEFAULT_THINKING;
|
|
16622
17563
|
const resolved = resolveModelAndThinking({
|
|
16623
|
-
model: opts.model ??
|
|
16624
|
-
thinking: opts.thinking ??
|
|
17564
|
+
model: opts.model ?? defaultModel,
|
|
17565
|
+
thinking: opts.thinking ?? defaultThinking
|
|
16625
17566
|
});
|
|
16626
17567
|
if (!resolved.ok) return {
|
|
16627
17568
|
text: resolved.error,
|
|
@@ -16657,9 +17598,14 @@ async function runWorkerAgent(opts) {
|
|
|
16657
17598
|
}
|
|
16658
17599
|
else ws = makeNoWorktreeHandle(workspaceAbs);
|
|
16659
17600
|
const budget = new Budget();
|
|
17601
|
+
const agentHolder = {};
|
|
17602
|
+
const planState = createPlanState();
|
|
17603
|
+
const getMessages = () => agentHolder.agent?.state.messages ?? [];
|
|
16660
17604
|
const tools = opts.mode === "browse" ? buildBrowseTools({ sessionId: opts.sessionId }) : buildWorkerTools({
|
|
16661
17605
|
mode: opts.mode,
|
|
16662
|
-
workspace: ws.dir
|
|
17606
|
+
workspace: ws.dir,
|
|
17607
|
+
getMessages,
|
|
17608
|
+
planState
|
|
16663
17609
|
});
|
|
16664
17610
|
const agent = new Agent$1({
|
|
16665
17611
|
initialState: {
|
|
@@ -16672,14 +17618,20 @@ async function runWorkerAgent(opts) {
|
|
|
16672
17618
|
resolved,
|
|
16673
17619
|
contextBudget: ctxBudget
|
|
16674
17620
|
}),
|
|
16675
|
-
toolExecution:
|
|
16676
|
-
transformContext:
|
|
17621
|
+
toolExecution: "parallel",
|
|
17622
|
+
transformContext: async (messages) => {
|
|
17623
|
+
let compacted = messages;
|
|
17624
|
+
if (ctxBudget) try {
|
|
17625
|
+
compacted = compactWorkerContext(messages, ctxBudget);
|
|
17626
|
+
} catch {
|
|
17627
|
+
compacted = messages;
|
|
17628
|
+
}
|
|
16677
17629
|
try {
|
|
16678
|
-
return
|
|
17630
|
+
return appendPlanReminder(compacted, planState);
|
|
16679
17631
|
} catch {
|
|
16680
|
-
return
|
|
17632
|
+
return compacted;
|
|
16681
17633
|
}
|
|
16682
|
-
}
|
|
17634
|
+
},
|
|
16683
17635
|
beforeToolCall: async (ctx) => {
|
|
16684
17636
|
logAudit({
|
|
16685
17637
|
mode: opts.mode,
|
|
@@ -16708,6 +17660,7 @@ async function runWorkerAgent(opts) {
|
|
|
16708
17660
|
budget.addTurn();
|
|
16709
17661
|
}
|
|
16710
17662
|
});
|
|
17663
|
+
agentHolder.agent = agent;
|
|
16711
17664
|
const abortHandler = () => agent?.abort();
|
|
16712
17665
|
if (opts.signal) if (opts.signal.aborted) agent.abort();
|
|
16713
17666
|
else opts.signal.addEventListener("abort", abortHandler, { once: true });
|
|
@@ -16777,6 +17730,35 @@ async function runWorkerAgent(opts) {
|
|
|
16777
17730
|
release();
|
|
16778
17731
|
}
|
|
16779
17732
|
}
|
|
17733
|
+
/**
|
|
17734
|
+
* Test-only exports. The public surface of the engine is
|
|
17735
|
+
* `runWorkerAgent` alone; everything else is internal. Tests use
|
|
17736
|
+
* the helpers below for direct extract-assistant-text assertions
|
|
17737
|
+
* without spinning up the full agent.
|
|
17738
|
+
*/
|
|
17739
|
+
/**
|
|
17740
|
+
* Append a single synthetic `user`-role plan reminder to a send-time
|
|
17741
|
+
* message view, so the current `update_plan` checklist survives context
|
|
17742
|
+
* compaction. Pure: returns the SAME array reference when there's nothing
|
|
17743
|
+
* to add, and a NEW array otherwise (never mutates the input). Appends
|
|
17744
|
+
* ONLY after a tool-result turn — that's the multi-step boundary where the
|
|
17745
|
+
* reminder is useful, and it can never double a `user` turn or split an
|
|
17746
|
+
* assistant→toolResult pair. Called inside the engine's `transformContext`,
|
|
17747
|
+
* whose output is a send-time view never persisted to the canonical
|
|
17748
|
+
* transcript.
|
|
17749
|
+
*/
|
|
17750
|
+
function appendPlanReminder(messages, planState) {
|
|
17751
|
+
if (planState.current.length === 0) return messages;
|
|
17752
|
+
const last = messages[messages.length - 1];
|
|
17753
|
+
const lastRole = last ? last.role : void 0;
|
|
17754
|
+
if (lastRole === "user" || lastRole === "assistant") return messages;
|
|
17755
|
+
const reminder = {
|
|
17756
|
+
role: "user",
|
|
17757
|
+
content: `Current plan (update via update_plan if it changed):\n${renderPlan(planState)}`,
|
|
17758
|
+
timestamp: Date.now()
|
|
17759
|
+
};
|
|
17760
|
+
return [...messages, reminder];
|
|
17761
|
+
}
|
|
16780
17762
|
|
|
16781
17763
|
//#endregion
|
|
16782
17764
|
//#region src/lib/stand-in.ts
|
|
@@ -17521,10 +18503,9 @@ function buildPeerAwarenessSnippet(opts) {
|
|
|
17521
18503
|
}
|
|
17522
18504
|
criticList.push("`opus_critic` (Opus 4.7)");
|
|
17523
18505
|
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." : "";
|
|
17524
|
-
const para2Parts = [`\`mcp__${searchKey}__code\`
|
|
18506
|
+
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\` for the exhaustive match set, \`ast_pattern\`+\`ast_lang\` for multi-line AST structures (via ast-grep), \`scan\` for a whole-workspace symbol outline, \`multiline\` for cross-line regex. Multiple independent 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.`];
|
|
17525
18507
|
if (opts.workerToolsAvailable) para2Parts.push(`\`mcp__${workersKey}__explore\` runs a Gemini-backed read-only worker that returns a summary, using its own context rather than yours; concurrent launches share the \`MAX_INFLIGHT_TOOLS_CALL=32\` cap with operator traffic.`, `\`mcp__${workersKey}__review\` is the same read-only worker framed as a code reviewer that reads the relevant code itself to verify a change or claim and reports findings with severity, so it checks surrounding context the \`peers\` critics (single stateless calls on the pasted artifact) cannot.`, `\`mcp__${workersKey}__implement\` is the same worker with edit/write/bash; \`worktree: true\` runs it in an isolated git worktree and returns the diff.`, "Workers themselves have `code_search` in their toolset.");
|
|
17526
18508
|
para2Parts.push(`\`mcp__${searchKey}__web\` surfaces citable sources for docs, errors, and upstream issues.`);
|
|
17527
|
-
if (opts.semanticSearchAvailable) para2Parts.push(`\`mcp__${searchKey}__semantic_search\` is ColBERT semantic code search over a per-workspace index and is the first search to try for intent/concept questions ("where is retry/backoff handled", "how does auth work") that a lexical \`code\`/grep search would miss; reserve lexical \`code\`/grep for exact symbols/strings. It returns honest \`building\`/\`stale\`/\`unavailable\` notices and never silently falls back to lexical.`);
|
|
17528
18509
|
if (opts.standInAvailable) para2Parts.push(`\`mcp__${decideKey}__stand_in\` provides three-lab consensus for decision tiebreak when the user is unavailable.`);
|
|
17529
18510
|
if (opts.browseAvailable) {
|
|
17530
18511
|
const powerNote = opts.powerBrowseAvailable ? ` Power mode is on: the L0/L1 primitives (\`mcp__${browserKey}__mouse\`, \`__drag\`, \`__type\`, \`__keyboard\`, \`__scroll\`, \`__eval_js\`, \`__read_page\`, \`__diagnostics\`, \`__find\`) are also available for direct DOM / coordinate control.` : "";
|
|
@@ -17606,7 +18587,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
17606
18587
|
{
|
|
17607
18588
|
toolNameHttp: "code",
|
|
17608
18589
|
group: "search",
|
|
17609
|
-
description: "Fast structured code search over a local workspace.
|
|
18590
|
+
description: "Fast structured code search over a local workspace. Default (`mode:\"semantic\"`, or omit `mode`) ranks by MEANING via ColBERT over a per-workspace index — best for intent/concept queries where the literal keywords may not appear (\"where do we rate-limit\", \"auth token refresh\"). When that index is building/stale/absent it TRANSPARENTLY returns lexical (BM25F) results and labels the response `source` (\"lexical-fallback\") so a degrade is never silent. On a `lexical-fallback` the `notice` says how to proceed: retry `mode:\"semantic\"` shortly (the index self-heals in the background) or re-query with specific symbols — the lexical engine matches keywords/symbols, not natural-language phrases. Other modes force the lexical engine: `lexical` (BM25F ranked, best for exact symbols), `exact` (fixed-string), `regex` (PCRE2), `ast` (ast-grep structural via `ast_pattern`+`ast_lang`). Lexical ranking refines a `symbol-context` field with tree-sitter AST analysis so definitions outrank incidental matches. Launch multiple code searches in parallel to triangulate — e.g. definition + callers + tests in one round-trip. Prefer this over Grep/Bash+grep for ranked discovery (\"where is X defined\", \"which files reference Y\", \"find code that does Z\"). Use Grep for exact-pattern enumeration when you need every hit unranked, and Glob for file-name patterns (no content match). `workspace` is any absolute path the proxy process can read — typically the project root or a sub-tree you're working in. Each response also carries a tree-sitter structural outline of the matched files (`summary` on by default; set it false to omit).",
|
|
17610
18591
|
inputSchema: {
|
|
17611
18592
|
type: "object",
|
|
17612
18593
|
required: ["query", "workspace"],
|
|
@@ -17614,7 +18595,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
17614
18595
|
properties: {
|
|
17615
18596
|
query: {
|
|
17616
18597
|
type: "string",
|
|
17617
|
-
description: "Search text. In
|
|
18598
|
+
description: "Search text. In the default 'semantic' mode it's natural-language intent (finds code by meaning even when the words don't appear literally). In 'lexical'/'exact' modes it's a literal string (single-identifier queries auto-expand across camelCase / snake_case / kebab-case / SCREAMING_SNAKE so `getUserName` also matches `get_user_name`). In 'regex' mode it's a PCRE2 regex."
|
|
17618
18599
|
},
|
|
17619
18600
|
workspace: {
|
|
17620
18601
|
type: "string",
|
|
@@ -17623,11 +18604,17 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
17623
18604
|
mode: {
|
|
17624
18605
|
type: "string",
|
|
17625
18606
|
enum: [
|
|
17626
|
-
"
|
|
17627
|
-
"
|
|
17628
|
-
"
|
|
18607
|
+
"semantic",
|
|
18608
|
+
"lexical",
|
|
18609
|
+
"exact",
|
|
18610
|
+
"regex",
|
|
18611
|
+
"ast"
|
|
17629
18612
|
],
|
|
17630
|
-
description: "
|
|
18613
|
+
description: "Search mode. 'semantic' (DEFAULT): ColBERT meaning-based ranking over a per-workspace index; transparently falls back to lexical when the index is building/stale/absent (the response `source` says which engine ran). 'lexical': BM25F + tree-sitter structural boost, ordered by score with shoulder pruning — best for exact symbols. 'exact': fixed-string, ripgrep document order. 'regex': PCRE2, ripgrep document order. 'ast': ast-grep structural match (requires `ast_pattern` + `ast_lang`)."
|
|
18614
|
+
},
|
|
18615
|
+
pattern: {
|
|
18616
|
+
type: "string",
|
|
18617
|
+
description: "Semantic mode only: regex pre-filter (colgrep -e) — grep first, then rank the matches semantically. Use to scope a semantic ranking to e.g. async fns. Ignored in lexical modes."
|
|
17631
18618
|
},
|
|
17632
18619
|
file_glob: {
|
|
17633
18620
|
type: "string",
|
|
@@ -17640,7 +18627,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
17640
18627
|
structural: {
|
|
17641
18628
|
type: "string",
|
|
17642
18629
|
enum: ["full", "topN"],
|
|
17643
|
-
description: "Structural-ranking depth (
|
|
18630
|
+
description: "Structural-ranking depth (lexical mode only). 'full' (default) runs tree-sitter on the top 50 BM25F hits — best signal, fine for typical repos. 'topN' restricts to the top 10 for tighter latency on very large workspaces. Both modes share a 200ms wall-clock budget; on budget exhaustion the response includes `notice` and remaining hits fall back to the regex symbol heuristic."
|
|
17644
18631
|
},
|
|
17645
18632
|
summary: {
|
|
17646
18633
|
type: "boolean",
|
|
@@ -17648,7 +18635,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
17648
18635
|
},
|
|
17649
18636
|
complete: {
|
|
17650
18637
|
type: "boolean",
|
|
17651
|
-
description: "Exhaustiveness. Default false —
|
|
18638
|
+
description: "Exhaustiveness (lexical mode). Default false — lexical mode applies a precision shoulder cut + a per-file cap so you aren't overwhelmed, and the response `notice` tells you when matches were hidden. Set true to disable both and return the COMPLETE match set (every line `grep` would find, reordered by relevance), capped only by `limit` — use it when you must not miss any occurrence (e.g. \"every caller of X\", a rename, an audit)."
|
|
17652
18639
|
},
|
|
17653
18640
|
multiline: {
|
|
17654
18641
|
type: "boolean",
|
|
@@ -17670,10 +18657,10 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
17670
18657
|
},
|
|
17671
18658
|
async handler(args, signal) {
|
|
17672
18659
|
try {
|
|
17673
|
-
const result = await
|
|
18660
|
+
const result = await runUnifiedCodeSearch({
|
|
17674
18661
|
query: typeof args.query === "string" ? args.query : "",
|
|
17675
18662
|
workspace: typeof args.workspace === "string" ? args.workspace : "",
|
|
17676
|
-
mode: args.mode === "
|
|
18663
|
+
mode: args.mode === "semantic" || args.mode === "lexical" || args.mode === "exact" || args.mode === "regex" || args.mode === "ast" ? args.mode : void 0,
|
|
17677
18664
|
file_glob: typeof args.file_glob === "string" ? args.file_glob : void 0,
|
|
17678
18665
|
limit: typeof args.limit === "number" ? args.limit : void 0,
|
|
17679
18666
|
structural: args.structural === "full" || args.structural === "topN" ? args.structural : void 0,
|
|
@@ -17682,7 +18669,8 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
17682
18669
|
multiline: typeof args.multiline === "boolean" ? args.multiline : void 0,
|
|
17683
18670
|
scan: typeof args.scan === "boolean" ? args.scan : void 0,
|
|
17684
18671
|
ast_pattern: typeof args.ast_pattern === "string" ? args.ast_pattern : void 0,
|
|
17685
|
-
ast_lang: typeof args.ast_lang === "string" ? args.ast_lang : void 0
|
|
18672
|
+
ast_lang: typeof args.ast_lang === "string" ? args.ast_lang : void 0,
|
|
18673
|
+
pattern: typeof args.pattern === "string" ? args.pattern : void 0
|
|
17686
18674
|
}, signal);
|
|
17687
18675
|
const SIZE_CAP_BYTES = 256 * 1024;
|
|
17688
18676
|
const trimmedHits = [];
|
|
@@ -17695,6 +18683,9 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
17695
18683
|
snippet: hit.snippet
|
|
17696
18684
|
};
|
|
17697
18685
|
if (hit.role) next.role = hit.role;
|
|
18686
|
+
if (hit.endLine !== void 0) next.endLine = hit.endLine;
|
|
18687
|
+
if (hit.name !== void 0) next.name = hit.name;
|
|
18688
|
+
if (hit.score !== void 0) next.score = hit.score;
|
|
17698
18689
|
const nextBytes = Buffer.byteLength(JSON.stringify(next), "utf8");
|
|
17699
18690
|
if (trimmedHits.length > 0 && totalBytes + nextBytes > SIZE_CAP_BYTES) {
|
|
17700
18691
|
sizeCapped = true;
|
|
@@ -17704,8 +18695,9 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
17704
18695
|
totalBytes += nextBytes;
|
|
17705
18696
|
}
|
|
17706
18697
|
const minimal = {
|
|
18698
|
+
source: result.source,
|
|
17707
18699
|
results: trimmedHits,
|
|
17708
|
-
truncated: result.truncated || sizeCapped
|
|
18700
|
+
truncated: (result.truncated ?? false) || sizeCapped
|
|
17709
18701
|
};
|
|
17710
18702
|
let outlinesDropped = false;
|
|
17711
18703
|
if (result.outlines && result.outlines.length > 0) {
|
|
@@ -17733,90 +18725,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
17733
18725
|
return {
|
|
17734
18726
|
content: [{
|
|
17735
18727
|
type: "text",
|
|
17736
|
-
text: `
|
|
17737
|
-
}],
|
|
17738
|
-
isError: true
|
|
17739
|
-
};
|
|
17740
|
-
}
|
|
17741
|
-
}
|
|
17742
|
-
},
|
|
17743
|
-
{
|
|
17744
|
-
toolNameHttp: "semantic_search",
|
|
17745
|
-
group: "search",
|
|
17746
|
-
capability: "semantic_search",
|
|
17747
|
-
description: "Semantic code search by MEANING, not text (ColBERT late-interaction over a per-workspace index). Best for natural-language intent queries where the literal keywords may not appear ('where do we rate-limit', 'auth token refresh', 'retry/backoff around the upstream fetch'). For exact symbol lookup ('where is X defined', 'callers of Y') prefer `code` (lexical) — it's faster and exact. Returns a `status` field (ready / building / stale / unavailable / failed); while the index is building or stale it returns a status + notice and NO results (it does NOT fall back to another search) — run `code` yourself if you need results immediately. `workspace` is any absolute path; the index is built and cached by the proxy on first use.",
|
|
17748
|
-
inputSchema: {
|
|
17749
|
-
type: "object",
|
|
17750
|
-
required: ["query"],
|
|
17751
|
-
additionalProperties: false,
|
|
17752
|
-
properties: {
|
|
17753
|
-
query: {
|
|
17754
|
-
type: "string",
|
|
17755
|
-
description: "Natural-language intent, e.g. 'where do we validate JWT expiry' or 'retry/backoff around the upstream fetch'. Semantic — finds code by meaning even when the words don't appear literally."
|
|
17756
|
-
},
|
|
17757
|
-
workspace: {
|
|
17758
|
-
type: "string",
|
|
17759
|
-
description: "Absolute path to the repo/subtree to search. Defaults to the proxy launch cwd. Must be absolute."
|
|
17760
|
-
},
|
|
17761
|
-
limit: {
|
|
17762
|
-
type: "integer",
|
|
17763
|
-
description: "Max results (default 15)."
|
|
17764
|
-
},
|
|
17765
|
-
pattern: {
|
|
17766
|
-
type: "string",
|
|
17767
|
-
description: "Optional regex pre-filter (colgrep -e): grep first, then rank the matches semantically. Use to scope a semantic ranking to e.g. async fns."
|
|
17768
|
-
}
|
|
17769
|
-
}
|
|
17770
|
-
},
|
|
17771
|
-
async handler(args, signal) {
|
|
17772
|
-
const query = typeof args.query === "string" ? args.query.trim() : "";
|
|
17773
|
-
if (!query) return {
|
|
17774
|
-
content: [{
|
|
17775
|
-
type: "text",
|
|
17776
|
-
text: "semantic_search: arguments.query is required (must be a non-empty string)"
|
|
17777
|
-
}],
|
|
17778
|
-
isError: true
|
|
17779
|
-
};
|
|
17780
|
-
let workspace;
|
|
17781
|
-
if (args.workspace === void 0) workspace = process.cwd();
|
|
17782
|
-
else if (typeof args.workspace === "string" && path.isAbsolute(args.workspace)) workspace = args.workspace;
|
|
17783
|
-
else return {
|
|
17784
|
-
content: [{
|
|
17785
|
-
type: "text",
|
|
17786
|
-
text: "semantic_search: arguments.workspace must be an ABSOLUTE path (or omitted to use the proxy launch cwd)"
|
|
17787
|
-
}],
|
|
17788
|
-
isError: true
|
|
17789
|
-
};
|
|
17790
|
-
const limit = typeof args.limit === "number" && Number.isFinite(args.limit) ? args.limit : void 0;
|
|
17791
|
-
const pattern = typeof args.pattern === "string" && args.pattern.length > 0 ? args.pattern : void 0;
|
|
17792
|
-
try {
|
|
17793
|
-
const result = await runSemanticSearch({
|
|
17794
|
-
query,
|
|
17795
|
-
workspace,
|
|
17796
|
-
limit,
|
|
17797
|
-
pattern,
|
|
17798
|
-
signal
|
|
17799
|
-
});
|
|
17800
|
-
const envelope = { status: result.status };
|
|
17801
|
-
if (result.results) envelope.results = result.results;
|
|
17802
|
-
if (result.source) envelope.source = result.source;
|
|
17803
|
-
if (result.notice) envelope.notice = result.notice;
|
|
17804
|
-
return {
|
|
17805
|
-
content: [{
|
|
17806
|
-
type: "text",
|
|
17807
|
-
text: JSON.stringify(envelope, null, 2)
|
|
17808
|
-
}],
|
|
17809
|
-
isError: result.isError === true
|
|
17810
|
-
};
|
|
17811
|
-
} catch (err) {
|
|
17812
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
17813
|
-
return {
|
|
17814
|
-
content: [{
|
|
17815
|
-
type: "text",
|
|
17816
|
-
text: JSON.stringify({
|
|
17817
|
-
status: "failed",
|
|
17818
|
-
notice: `semantic_search failed: ${msg}; use code (lexical) instead`
|
|
17819
|
-
}, null, 2)
|
|
18728
|
+
text: `code search failed: ${err instanceof Error ? err.message : String(err)}`
|
|
17820
18729
|
}],
|
|
17821
18730
|
isError: true
|
|
17822
18731
|
};
|
|
@@ -17827,7 +18736,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
17827
18736
|
toolNameHttp: "explore",
|
|
17828
18737
|
group: "workers",
|
|
17829
18738
|
capability: "worker",
|
|
17830
|
-
description: "Read-only investigation by an autonomous worker (Pi runtime; default model `gemini-3.
|
|
18739
|
+
description: "Read-only investigation by an autonomous worker (Pi runtime; default model `gemini-3.5-flash` at high reasoning, override via the `model` arg with any Copilot-catalog model that advertises `tool_calls`). Tools: read, glob, grep, code_search (semantic-first), web_search, fetch_url, advisor (consult a stronger cross-lab model), update_plan (planning checklist), and toolbelt (run a read-only analysis CLI: rg/fd/jq/yq/sg/gron/tokei/difft/git). The worker's system prompt sandboxes it and gives one-line descriptions of each tool, so brief it on the investigation, not on tool semantics. Offloads bounded research that would otherwise eat your context window — the worker plans its own tool calls and returns a single text answer. Examples: \"find files matching X then summarize\", \"how does library Y handle Z\", \"survey this codebase for usages of deprecated API\".",
|
|
17831
18740
|
inputSchema: {
|
|
17832
18741
|
type: "object",
|
|
17833
18742
|
required: ["prompt"],
|
|
@@ -17839,7 +18748,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
17839
18748
|
},
|
|
17840
18749
|
model: {
|
|
17841
18750
|
type: "string",
|
|
17842
|
-
description: "Optional Copilot catalog model id (defaults to gemini-3.
|
|
18751
|
+
description: "Optional Copilot catalog model id (defaults to gemini-3.5-flash). Must advertise tool_calls support; the engine emits an isError envelope listing the eligible catalog models on mismatch."
|
|
17843
18752
|
},
|
|
17844
18753
|
thinking: {
|
|
17845
18754
|
type: "string",
|
|
@@ -17871,7 +18780,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
17871
18780
|
toolNameHttp: "implement",
|
|
17872
18781
|
group: "workers",
|
|
17873
18782
|
capability: "worker",
|
|
17874
|
-
description: "Delegates a scoped coding task to an autonomous worker (Pi runtime; default model `
|
|
18783
|
+
description: "Delegates a scoped coding task to an autonomous worker (Pi runtime; default model `gpt-5.5` at xhigh reasoning, override via the `model` arg with any Copilot-catalog model that advertises `tool_calls`). Tools: the explore read-only set (read, glob, grep, code_search, web_search, fetch_url, advisor, update_plan, toolbelt) plus edit, write, bash, and codex_review (code review by codex-reviewer / gpt-5.3-codex). The worker's system prompt sandboxes it and gives one-line descriptions of each tool, so brief it on the task, not on tool semantics. With `worktree: false` (default) edits in place — concurrent worker_implement calls and Claude's own edits to the same files will race. With `worktree: true` runs in an isolated git worktree and returns the diff for review. HARD ERROR if true and the workspace is not a git repository.",
|
|
17875
18784
|
inputSchema: {
|
|
17876
18785
|
type: "object",
|
|
17877
18786
|
required: ["prompt"],
|
|
@@ -17887,7 +18796,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
17887
18796
|
},
|
|
17888
18797
|
model: {
|
|
17889
18798
|
type: "string",
|
|
17890
|
-
description: "Optional Copilot catalog model id (defaults to
|
|
18799
|
+
description: "Optional Copilot catalog model id (defaults to gpt-5.5). Must advertise tool_calls support; the engine emits an isError envelope listing the eligible catalog models on mismatch."
|
|
17891
18800
|
},
|
|
17892
18801
|
thinking: {
|
|
17893
18802
|
type: "string",
|
|
@@ -17899,7 +18808,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
17899
18808
|
"high",
|
|
17900
18809
|
"xhigh"
|
|
17901
18810
|
],
|
|
17902
|
-
description: "Optional reasoning depth (default
|
|
18811
|
+
description: "Optional reasoning depth (default xhigh). Silently clamped to the model's allowed range; \"off\" drops the parameter entirely."
|
|
17903
18812
|
},
|
|
17904
18813
|
workspace: {
|
|
17905
18814
|
type: "string",
|
|
@@ -17919,7 +18828,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
17919
18828
|
toolNameHttp: "review",
|
|
17920
18829
|
group: "workers",
|
|
17921
18830
|
capability: "worker",
|
|
17922
|
-
description: "Read-only code review by an autonomous worker (Pi runtime; default model `gemini-3.
|
|
18831
|
+
description: "Read-only code review by an autonomous worker (Pi runtime; default model `gemini-3.5-flash`, override via `model` with any Copilot-catalog model that advertises `tool_calls`). Same read-only toolset as `explore` (read, glob, grep, code_search, web_search, fetch_url, advisor, update_plan, toolbelt) — it CANNOT edit — but the worker is framed as a reviewer: it verifies correctness against the actual code itself rather than trusting a claim, and reports findings (bugs, edge cases, security / concurrency / resource risks, missing handling) with a severity and `file:line`. Brief it with the change / diff / claim to verify (paste it, or name the files) — it reads the code to confirm, so you get a self-verifying second opinion that doesn't depend on you having pre-extracted the relevant code. Unlike the `peers` critics (single stateless model calls on the artifact you paste), this worker can navigate the repo to check surrounding context for itself.",
|
|
17923
18832
|
inputSchema: {
|
|
17924
18833
|
type: "object",
|
|
17925
18834
|
required: ["prompt"],
|
|
@@ -17931,7 +18840,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
17931
18840
|
},
|
|
17932
18841
|
model: {
|
|
17933
18842
|
type: "string",
|
|
17934
|
-
description: "Optional Copilot catalog model id (defaults to gemini-3.
|
|
18843
|
+
description: "Optional Copilot catalog model id (defaults to gemini-3.5-flash). Must advertise tool_calls support; the engine emits an isError envelope listing the eligible catalog models on mismatch."
|
|
17935
18844
|
},
|
|
17936
18845
|
thinking: {
|
|
17937
18846
|
type: "string",
|
|
@@ -19606,49 +20515,6 @@ async function exposedCommands(binDir) {
|
|
|
19606
20515
|
return out;
|
|
19607
20516
|
}
|
|
19608
20517
|
|
|
19609
|
-
//#endregion
|
|
19610
|
-
//#region src/lib/colbert/index.ts
|
|
19611
|
-
/**
|
|
19612
|
-
* True unless the operator opted out via
|
|
19613
|
-
* `GH_ROUTER_DISABLE_SEMANTIC_SEARCH=1`. Semantic search is ON BY
|
|
19614
|
-
* DEFAULT (the proxy auto-provisions + background-indexes); the
|
|
19615
|
-
* capability gate additionally requires the artifacts to be present on
|
|
19616
|
-
* disk + smoke-passed, so in any environment where provisioning hasn't
|
|
19617
|
-
* completed the tool simply doesn't appear (no regression).
|
|
19618
|
-
*/
|
|
19619
|
-
function semanticSearchOptedIn() {
|
|
19620
|
-
return parseBoolEnv(process$1.env.GH_ROUTER_DISABLE_SEMANTIC_SEARCH) !== true;
|
|
19621
|
-
}
|
|
19622
|
-
let _started = false;
|
|
19623
|
-
/**
|
|
19624
|
-
* Fire-and-forget provision + background-index. Never throws; safe to
|
|
19625
|
-
* `void`-call from a launcher right after the server is listening.
|
|
19626
|
-
* Idempotent within a proxy run (subsequent calls no-op).
|
|
19627
|
-
*/
|
|
19628
|
-
async function provisionAndIndexColbert(opts = {}) {
|
|
19629
|
-
if (!semanticSearchOptedIn()) return;
|
|
19630
|
-
if (_started) return;
|
|
19631
|
-
_started = true;
|
|
19632
|
-
registerColbertExitHandlers();
|
|
19633
|
-
let provisioned = false;
|
|
19634
|
-
try {
|
|
19635
|
-
const result = await provisionColbert();
|
|
19636
|
-
provisioned = result.status === "ready";
|
|
19637
|
-
if (result.status === "unsupported") consola.debug("colbert: semantic search unsupported on this platform");
|
|
19638
|
-
else if (result.status !== "ready") consola.debug(`colbert: provision not ready (${result.status}: ${result.reason ?? ""})`);
|
|
19639
|
-
} catch (err) {
|
|
19640
|
-
consola.debug("colbert: provision threw (swallowed):", err);
|
|
19641
|
-
return;
|
|
19642
|
-
}
|
|
19643
|
-
if (!provisioned) return;
|
|
19644
|
-
const cwd = opts.cwd ?? process$1.cwd();
|
|
19645
|
-
try {
|
|
19646
|
-
if ((await gitState(cwd)).isRepo) kickBackgroundInit(cwd);
|
|
19647
|
-
} catch (err) {
|
|
19648
|
-
consola.debug("colbert: cwd git-detect skipped:", err);
|
|
19649
|
-
}
|
|
19650
|
-
}
|
|
19651
|
-
|
|
19652
20518
|
//#endregion
|
|
19653
20519
|
//#region src/lib/proxy.ts
|
|
19654
20520
|
function initProxyFromEnv() {
|
|
@@ -19698,7 +20564,7 @@ function initProxyFromEnv() {
|
|
|
19698
20564
|
//#endregion
|
|
19699
20565
|
//#region package.json
|
|
19700
20566
|
var name = "github-router";
|
|
19701
|
-
var version$1 = "0.3.
|
|
20567
|
+
var version$1 = "0.3.87";
|
|
19702
20568
|
|
|
19703
20569
|
//#endregion
|
|
19704
20570
|
//#region src/lib/approval.ts
|
|
@@ -21864,7 +22730,6 @@ const claude = defineCommand({
|
|
|
21864
22730
|
geminiAvailable: geminiAvailable$1,
|
|
21865
22731
|
workerToolsAvailable: workerToolsEnabled(),
|
|
21866
22732
|
standInAvailable: standInToolEnabled(),
|
|
21867
|
-
semanticSearchAvailable: semanticSearchEnabled(),
|
|
21868
22733
|
browseAvailable: state.browseEnabled,
|
|
21869
22734
|
powerBrowseAvailable: state.powerBrowseEnabled,
|
|
21870
22735
|
groupKeys
|