@usewhisper/mcp-server 2.10.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/server.js +849 -156
  2. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -3799,6 +3799,7 @@ var API_KEY = process.env.WHISPER_API_KEY || "";
3799
3799
  var DEFAULT_PROJECT = process.env.WHISPER_PROJECT || "";
3800
3800
  var BASE_URL = process.env.WHISPER_BASE_URL;
3801
3801
  var RUNTIME_MODE = (process.env.WHISPER_MCP_MODE || "remote").toLowerCase();
3802
+ var MCP_RETRIEVAL_PRECISION_V1_DEFAULT = /^true$/i.test(process.env.MCP_RETRIEVAL_PRECISION_V1_DEFAULT || "false");
3802
3803
  var CLI_ARGS = process.argv.slice(2);
3803
3804
  var IS_MANAGEMENT_ONLY = CLI_ARGS.includes("--print-tool-map") || CLI_ARGS[0] === "scope";
3804
3805
  function createWhisperMcpClient(options) {
@@ -3833,6 +3834,16 @@ var server = new McpServer({
3833
3834
  function createMcpServer() {
3834
3835
  return server;
3835
3836
  }
3837
+ var WORKSPACE_HEALTH_VALUES = ["unbound", "unindexed", "stale", "drifted", "healthy"];
3838
+ var RETRIEVAL_READINESS_VALUES = [
3839
+ "no_project",
3840
+ "project_unverified",
3841
+ "project_bound_no_repo_source",
3842
+ "project_repo_source_stale",
3843
+ "project_repo_source_ready",
3844
+ "local_fallback"
3845
+ ];
3846
+ var RETRIEVAL_ROUTE_VALUES = ["project_repo", "local_workspace_fallback", "memory_only", "none"];
3836
3847
  var STATE_DIR = join(homedir(), ".whisper-mcp");
3837
3848
  var STATE_PATH = join(STATE_DIR, "state.json");
3838
3849
  var AUDIT_LOG_PATH = join(STATE_DIR, "forget-audit.log");
@@ -3874,6 +3885,55 @@ var ALIAS_TOOL_MAP = [
3874
3885
  { alias: "learn", target: "context.add_text | context.add_source | context.add_document" },
3875
3886
  { alias: "share_context", target: "context.share" }
3876
3887
  ];
3888
+ var MCP_RETRIEVAL_PROFILE_VALUES = ["legacy", "precision_v1"];
3889
+ var RECOMMENDED_NEXT_CALL_ALLOWLIST = /* @__PURE__ */ new Set([
3890
+ "index.workspace_resolve",
3891
+ "context.list_projects",
3892
+ "index.workspace_status",
3893
+ "index.workspace_run",
3894
+ "search_code",
3895
+ "grep",
3896
+ "context.list_sources",
3897
+ "index.local_scan_ingest",
3898
+ "context.get_relevant"
3899
+ ]);
3900
+ var UNTRUSTED_CODEBASE_HEALTH = /* @__PURE__ */ new Set(["unbound", "unindexed"]);
3901
+ var UNTRUSTED_CODEBASE_PROJECT_READINESS = /* @__PURE__ */ new Set([
3902
+ "project_unverified",
3903
+ "project_bound_no_repo_source"
3904
+ ]);
3905
+ function sanitizeRecommendedNextCalls(nextCalls) {
3906
+ return Array.from(
3907
+ new Set(
3908
+ nextCalls.map((entry) => String(entry || "").trim()).filter((entry) => RECOMMENDED_NEXT_CALL_ALLOWLIST.has(entry))
3909
+ )
3910
+ );
3911
+ }
3912
+ function resolveMcpRetrievalProfile(requested) {
3913
+ if (requested && MCP_RETRIEVAL_PROFILE_VALUES.includes(requested)) {
3914
+ return requested;
3915
+ }
3916
+ return MCP_RETRIEVAL_PRECISION_V1_DEFAULT ? "precision_v1" : "legacy";
3917
+ }
3918
+ function shouldAbstainForCodebaseTrust(args) {
3919
+ if (args.retrievalProfile !== "precision_v1" || !args.codebaseIntent) return false;
3920
+ return UNTRUSTED_CODEBASE_HEALTH.has(args.preflight.trust_state.health) || UNTRUSTED_CODEBASE_PROJECT_READINESS.has(args.preflight.project_retrieval_readiness);
3921
+ }
3922
+ function buildCodebaseTrustAbstainPayload(args) {
3923
+ return {
3924
+ status: "abstained",
3925
+ reason: "stale_index",
3926
+ message: args.preflight.warnings[0] || "Codebase retrieval is not trusted yet for this workspace/project. Resolve index trust before answering.",
3927
+ query: args.query,
3928
+ trust_state: args.preflight.trust_state,
3929
+ retrieval_readiness: args.preflight.retrieval_readiness,
3930
+ project_retrieval_readiness: args.preflight.project_retrieval_readiness,
3931
+ retrieval_route: args.preflight.retrieval_route,
3932
+ retrieval_profile: args.retrievalProfile,
3933
+ warnings: args.preflight.warnings,
3934
+ recommended_next_calls: sanitizeRecommendedNextCalls(args.preflight.recommended_next_calls)
3935
+ };
3936
+ }
3877
3937
  function ensureStateDir() {
3878
3938
  if (!existsSync(STATE_DIR)) {
3879
3939
  mkdirSync(STATE_DIR, { recursive: true });
@@ -3914,6 +3974,21 @@ function classifyWorkspaceHealth(args) {
3914
3974
  if (ageHours > (args.max_staleness_hours ?? 168)) return "stale";
3915
3975
  return "healthy";
3916
3976
  }
3977
+ function resolveWorkspaceIdentity(args) {
3978
+ const rootPath = canonicalizeWorkspacePath(args?.path, args?.cwd);
3979
+ const derivedWorkspaceId = getWorkspaceIdForPath(rootPath);
3980
+ const requestedWorkspaceId = args?.workspace_id?.trim();
3981
+ if (requestedWorkspaceId && requestedWorkspaceId !== derivedWorkspaceId) {
3982
+ throw new Error(
3983
+ `workspace_id '${requestedWorkspaceId}' does not match canonical workspace '${derivedWorkspaceId}' for ${rootPath}.`
3984
+ );
3985
+ }
3986
+ return {
3987
+ workspace_id: derivedWorkspaceId,
3988
+ root_path: rootPath,
3989
+ identity_source: args?.path?.trim() ? "path_canonical" : "cwd_canonical"
3990
+ };
3991
+ }
3917
3992
  function getWorkspaceId(workspaceId) {
3918
3993
  if (workspaceId?.trim()) return workspaceId.trim();
3919
3994
  const seed = `${canonicalizeWorkspacePath(process.cwd())}|${API_KEY.slice(0, 12) || "anon"}`;
@@ -3925,6 +4000,143 @@ function getWorkspaceIdForPath(path, workspaceId) {
3925
4000
  const seed = `${canonicalizeWorkspacePath(path)}|${API_KEY.slice(0, 12) || "anon"}`;
3926
4001
  return createHash("sha256").update(seed).digest("hex").slice(0, 20);
3927
4002
  }
4003
+ function normalizeLoosePath(value) {
4004
+ if (!value || !String(value).trim()) return null;
4005
+ return canonicalizeWorkspacePath(String(value));
4006
+ }
4007
+ function normalizeRepoName(value) {
4008
+ if (!value || !String(value).trim()) return null;
4009
+ return String(value).trim().replace(/\.git$/i, "").toLowerCase();
4010
+ }
4011
+ function parseGitHubRemote(remote) {
4012
+ if (!remote) return null;
4013
+ const normalized = remote.trim().replace(/\.git$/i, "");
4014
+ const httpsMatch = normalized.match(/github\.com[/:]([^/:\s]+)\/([^/\s]+)$/i);
4015
+ if (httpsMatch) {
4016
+ return { owner: httpsMatch[1].toLowerCase(), repo: httpsMatch[2].toLowerCase() };
4017
+ }
4018
+ const sshMatch = normalized.match(/git@github\.com:([^/:\s]+)\/([^/\s]+)$/i);
4019
+ if (sshMatch) {
4020
+ return { owner: sshMatch[1].toLowerCase(), repo: sshMatch[2].toLowerCase() };
4021
+ }
4022
+ return null;
4023
+ }
4024
+ function getGitRemoteUrl(searchPath) {
4025
+ const root = searchPath || process.cwd();
4026
+ const result = spawnSync("git", ["-C", root, "config", "--get", "remote.origin.url"], { encoding: "utf-8" });
4027
+ if (result.status !== 0) return void 0;
4028
+ const out = String(result.stdout || "").trim();
4029
+ return out || void 0;
4030
+ }
4031
+ function getGitBranch(searchPath) {
4032
+ const root = searchPath || process.cwd();
4033
+ const result = spawnSync("git", ["-C", root, "rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf-8" });
4034
+ if (result.status !== 0) return void 0;
4035
+ const out = String(result.stdout || "").trim();
4036
+ return out || void 0;
4037
+ }
4038
+ function sourceStatusLooksReady(status) {
4039
+ const normalized = String(status || "").trim().toLowerCase();
4040
+ if (!normalized) return true;
4041
+ if (["ready", "indexed", "completed", "complete", "active", "synced", "success"].includes(normalized)) return true;
4042
+ if (["processing", "queued", "syncing", "pending", "creating", "indexing", "error", "failed"].includes(normalized)) {
4043
+ return false;
4044
+ }
4045
+ return !normalized.includes("error") && !normalized.includes("fail") && !normalized.includes("queue");
4046
+ }
4047
+ function sourceMatchesWorkspacePath(source, rootPath) {
4048
+ const config = source.config || {};
4049
+ const candidates = [
4050
+ config.path,
4051
+ config.root_path,
4052
+ config.workspace_path,
4053
+ config.workspacePath,
4054
+ config.local_path,
4055
+ config.file_path,
4056
+ config.directory
4057
+ ].map((value) => normalizeLoosePath(typeof value === "string" ? value : null)).filter(Boolean);
4058
+ return candidates.some((candidate) => candidate === rootPath || rootPath.startsWith(`${candidate}/`) || candidate.startsWith(`${rootPath}/`));
4059
+ }
4060
+ function sourceMatchesWorkspaceRepo(source, repoIdentity, branch) {
4061
+ if (!repoIdentity) return false;
4062
+ const config = source.config || {};
4063
+ const owner = normalizeRepoName(config.owner || config.org || config.organization || null);
4064
+ const repo = normalizeRepoName(config.repo || config.repository || config.name || null);
4065
+ const sourceUrlRepo = parseGitHubRemote(typeof config.url === "string" ? config.url : null);
4066
+ const branchValue = normalizeRepoName(config.branch || config.ref || config.default_branch || null);
4067
+ const ownerMatches = owner === repoIdentity.owner || sourceUrlRepo?.owner === repoIdentity.owner;
4068
+ const repoMatches = repo === repoIdentity.repo || sourceUrlRepo?.repo === repoIdentity.repo;
4069
+ if (!ownerMatches || !repoMatches) return false;
4070
+ if (!branchValue || !branch) return true;
4071
+ return branchValue === normalizeRepoName(branch);
4072
+ }
4073
+ function classifyProjectRepoReadiness(args) {
4074
+ const remoteIdentity = parseGitHubRemote(args.remote_url || null);
4075
+ const matchedSources = args.sources.filter(
4076
+ (source) => sourceMatchesWorkspacePath(source, args.root_path) || sourceMatchesWorkspaceRepo(source, remoteIdentity, args.branch || void 0)
4077
+ );
4078
+ if (matchedSources.length === 0) {
4079
+ return {
4080
+ retrieval_readiness: "project_bound_no_repo_source",
4081
+ matched_sources: [],
4082
+ matched_source_ids: [],
4083
+ warnings: [`No verified local or GitHub source matches ${args.root_path}.`]
4084
+ };
4085
+ }
4086
+ const readySources = matchedSources.filter((source) => sourceStatusLooksReady(source.status));
4087
+ if (readySources.length === 0) {
4088
+ return {
4089
+ retrieval_readiness: "project_repo_source_stale",
4090
+ matched_sources: matchedSources,
4091
+ matched_source_ids: matchedSources.map((source) => source.id),
4092
+ warnings: [
4093
+ `Matching repo sources are present but not ready (${matchedSources.map((source) => `${source.name}:${source.status}`).join(", ")}).`
4094
+ ]
4095
+ };
4096
+ }
4097
+ return {
4098
+ retrieval_readiness: "project_repo_source_ready",
4099
+ matched_sources: readySources,
4100
+ matched_source_ids: readySources.map((source) => source.id),
4101
+ warnings: []
4102
+ };
4103
+ }
4104
+ function classifyRepoGroundedQuery(args) {
4105
+ const query = args.query.toLowerCase();
4106
+ let score = 0;
4107
+ if (/(^|[\s"'])where is\b|which file|what file|show me|wiring|handler|middleware|implementation|route|endpoint|function|class|module|repo|workspace|code|project/.test(query)) {
4108
+ score += 1;
4109
+ }
4110
+ if (/\bauth\b|\burl\b|\bcookie\b|\bsession\b|\bapi\b/.test(query)) {
4111
+ score += 1;
4112
+ }
4113
+ if (/\berror\b|\berrors\b|\bexception\b|\bexceptions\b/.test(query)) {
4114
+ score += 1;
4115
+ }
4116
+ if (/\bretry\b/.test(query)) {
4117
+ score += 1;
4118
+ }
4119
+ if (/\bflow\b|\blogic\b|\bstack\b|\btrace\b|\blogging\b|\bfailure\b|\bfailures\b|\bthrow\b|\bcatch\b|\bdebug\b/.test(query)) {
4120
+ score += 1;
4121
+ }
4122
+ if (/[A-Za-z0-9/_-]+\.[A-Za-z0-9]+/.test(args.query) || /\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]{2,}/.test(args.query)) {
4123
+ score += 2;
4124
+ }
4125
+ if (/[A-Za-z_$][A-Za-z0-9_$-]*\(/.test(args.query) || /\b[A-Z][a-z0-9]+[A-Z][A-Za-z0-9]*\b/.test(args.query)) {
4126
+ score += 1;
4127
+ }
4128
+ if ((args.chunk_types || []).some((chunkType) => ["code", "function", "class", "config", "schema", "api_spec"].includes(chunkType))) {
4129
+ score += 2;
4130
+ }
4131
+ const changedTokens = new Set((args.changed_path_tokens || []).map((token) => token.toLowerCase()));
4132
+ if (changedTokens.size > 0) {
4133
+ const queryTokens = tokenizeQueryForLexicalRescue(args.query);
4134
+ if (queryTokens.some((token) => changedTokens.has(token.toLowerCase()))) {
4135
+ score += 1;
4136
+ }
4137
+ }
4138
+ return score >= 2;
4139
+ }
3928
4140
  function clamp012(value) {
3929
4141
  if (Number.isNaN(value)) return 0;
3930
4142
  if (value < 0) return 0;
@@ -4038,9 +4250,9 @@ async function resolveProjectDescriptor(projectRef) {
4038
4250
  }
4039
4251
  }
4040
4252
  function getWorkspaceRecommendedNextCalls(health) {
4041
- if (health === "unbound") return ["index.workspace_resolve", "context.list_projects", "index.workspace_status"];
4042
- if (health === "unindexed") return ["index.workspace_status", "index.workspace_run", "search_code", "grep"];
4043
- if (health === "stale" || health === "drifted") return ["index.workspace_status", "index.workspace_run", "grep", "search_code"];
4253
+ if (health === "unbound") return sanitizeRecommendedNextCalls(["index.workspace_resolve", "context.list_projects", "index.workspace_status"]);
4254
+ if (health === "unindexed") return sanitizeRecommendedNextCalls(["index.workspace_status", "index.workspace_run", "search_code", "grep"]);
4255
+ if (health === "stale" || health === "drifted") return sanitizeRecommendedNextCalls(["index.workspace_status", "index.workspace_run", "grep", "search_code"]);
4044
4256
  return [];
4045
4257
  }
4046
4258
  function getWorkspaceWarnings(args) {
@@ -4066,8 +4278,9 @@ function getWorkspaceWarnings(args) {
4066
4278
  return [];
4067
4279
  }
4068
4280
  async function resolveWorkspaceTrust(args) {
4069
- const rootPath = canonicalizeWorkspacePath(args.path);
4070
- const workspaceId = getWorkspaceIdForPath(rootPath, args.workspace_id);
4281
+ const identity = resolveWorkspaceIdentity({ path: args.path, workspace_id: args.workspace_id });
4282
+ const rootPath = identity.root_path;
4283
+ const workspaceId = identity.workspace_id;
4071
4284
  const state = loadState();
4072
4285
  const workspace = getWorkspaceState(state, workspaceId);
4073
4286
  let mutated = false;
@@ -4125,6 +4338,7 @@ async function resolveWorkspaceTrust(args) {
4125
4338
  return {
4126
4339
  workspace_id: workspaceId,
4127
4340
  root_path: rootPath,
4341
+ identity_source: identity.identity_source,
4128
4342
  project_ref: projectRef,
4129
4343
  project_id: projectId,
4130
4344
  resolved_by: selected.resolved_by,
@@ -4156,6 +4370,99 @@ async function resolveProjectRef(explicit) {
4156
4370
  return void 0;
4157
4371
  }
4158
4372
  }
4373
+ function collectGitChangedPathTokens(searchPath) {
4374
+ const root = searchPath || process.cwd();
4375
+ const result = spawnSync("git", ["-C", root, "status", "--porcelain"], { encoding: "utf-8" });
4376
+ if (result.status !== 0) return [];
4377
+ const lines = String(result.stdout || "").split("\n").map((line) => line.trim()).filter(Boolean);
4378
+ const tokens = /* @__PURE__ */ new Set();
4379
+ for (const line of lines) {
4380
+ const filePath = line.slice(3).split(" -> ").at(-1)?.trim();
4381
+ if (!filePath) continue;
4382
+ for (const token of filePath.split(/[\\/._-]+/).filter((part) => part.length >= 3)) {
4383
+ tokens.add(token.toLowerCase());
4384
+ }
4385
+ }
4386
+ return Array.from(tokens);
4387
+ }
4388
+ async function inspectProjectRepoSources(args) {
4389
+ if (!args.project_ref) {
4390
+ return {
4391
+ retrieval_readiness: "no_project",
4392
+ matched_sources: [],
4393
+ matched_source_ids: [],
4394
+ warnings: ["No Whisper project is bound to this workspace."]
4395
+ };
4396
+ }
4397
+ try {
4398
+ const sourceData = await whisper.listSources(args.project_ref);
4399
+ const classified = classifyProjectRepoReadiness({
4400
+ sources: sourceData.sources || [],
4401
+ root_path: args.root_path,
4402
+ remote_url: getGitRemoteUrl(args.root_path) || null,
4403
+ branch: getGitBranch(args.root_path) || null
4404
+ });
4405
+ if (classified.retrieval_readiness === "project_bound_no_repo_source") {
4406
+ return {
4407
+ ...classified,
4408
+ warnings: [`Project ${args.project_ref} has no verified local or GitHub source for ${args.root_path}.`]
4409
+ };
4410
+ }
4411
+ if (classified.retrieval_readiness === "project_repo_source_stale") {
4412
+ return {
4413
+ ...classified,
4414
+ warnings: [
4415
+ `Project ${args.project_ref} has matching repo sources, but none are ready (${classified.matched_sources.map((source) => `${source.name}:${source.status}`).join(", ")}).`
4416
+ ]
4417
+ };
4418
+ }
4419
+ return classified;
4420
+ } catch (error) {
4421
+ return {
4422
+ retrieval_readiness: "project_unverified",
4423
+ matched_sources: [],
4424
+ matched_source_ids: [],
4425
+ warnings: [`Could not verify project sources for ${args.project_ref}: ${error.message}`]
4426
+ };
4427
+ }
4428
+ }
4429
+ async function resolveRepoGroundingPreflight(args) {
4430
+ const trust = await resolveWorkspaceTrust({ path: args.path, workspace_id: args.workspace_id, project: args.project });
4431
+ const repoGrounded = classifyRepoGroundedQuery({
4432
+ query: args.query,
4433
+ chunk_types: args.chunk_types,
4434
+ changed_path_tokens: collectGitChangedPathTokens(trust.root_path)
4435
+ });
4436
+ const sourceVerification = await inspectProjectRepoSources({
4437
+ project_ref: trust.project_ref,
4438
+ root_path: trust.root_path
4439
+ });
4440
+ const warnings = [...trust.warnings, ...sourceVerification.warnings];
4441
+ let retrievalReadiness = sourceVerification.retrieval_readiness;
4442
+ let retrievalRoute = "none";
4443
+ if (repoGrounded) {
4444
+ retrievalRoute = sourceVerification.retrieval_readiness === "project_repo_source_ready" ? "project_repo" : "local_workspace_fallback";
4445
+ if (retrievalRoute === "local_workspace_fallback") {
4446
+ retrievalReadiness = "local_fallback";
4447
+ warnings.push("Using live local workspace retrieval because project-backed repo retrieval is unavailable or unverified.");
4448
+ }
4449
+ }
4450
+ const recommendedNextCalls = [...trust.recommended_next_calls];
4451
+ if (sourceVerification.retrieval_readiness === "project_bound_no_repo_source" || sourceVerification.retrieval_readiness === "project_unverified") {
4452
+ recommendedNextCalls.push("context.list_sources", "index.local_scan_ingest");
4453
+ }
4454
+ return {
4455
+ repo_grounded: repoGrounded,
4456
+ trust_state: trust,
4457
+ project_retrieval_readiness: sourceVerification.retrieval_readiness,
4458
+ retrieval_readiness: retrievalReadiness,
4459
+ retrieval_route: retrievalRoute,
4460
+ matched_sources: sourceVerification.matched_sources,
4461
+ matched_source_ids: sourceVerification.matched_source_ids,
4462
+ warnings: Array.from(new Set(warnings)),
4463
+ recommended_next_calls: sanitizeRecommendedNextCalls(recommendedNextCalls)
4464
+ };
4465
+ }
4159
4466
  async function ingestSessionWithSyncFallback(params) {
4160
4467
  try {
4161
4468
  return await whisper.ingestSession({
@@ -4182,7 +4489,7 @@ function resolveMcpScope(params) {
4182
4489
  project: params?.project,
4183
4490
  userId: params?.user_id?.trim() || defaultMcpUserId(),
4184
4491
  sessionId: params?.session_id?.trim() || cachedMcpSessionId,
4185
- workspacePath: process.env.WHISPER_WORKSPACE_PATH || process.cwd()
4492
+ workspacePath: canonicalizeWorkspacePath(params?.path || process.env.WHISPER_WORKSPACE_PATH || process.cwd())
4186
4493
  };
4187
4494
  }
4188
4495
  async function prepareAutomaticQuery(params) {
@@ -4248,7 +4555,9 @@ function buildAbstain(args) {
4248
4555
  closest_evidence: args.closest_evidence,
4249
4556
  warnings: args.warnings || [],
4250
4557
  trust_state: args.trust_state,
4251
- recommended_next_calls: args.recommended_next_calls || ["index.workspace_status", "index.workspace_run", "grep", "context.get_relevant"],
4558
+ recommended_next_calls: sanitizeRecommendedNextCalls(
4559
+ args.recommended_next_calls || ["index.workspace_status", "index.workspace_run", "grep", "context.get_relevant"]
4560
+ ),
4252
4561
  diagnostics: {
4253
4562
  claims_evaluated: args.claims_evaluated,
4254
4563
  evidence_items_found: args.evidence_items_found,
@@ -4418,6 +4727,9 @@ async function queryWithDegradedFallback(params) {
4418
4727
  include_graph: params.include_graph,
4419
4728
  user_id: params.user_id,
4420
4729
  session_id: params.session_id,
4730
+ source_ids: params.source_ids,
4731
+ retrieval_profile: params.retrieval_profile,
4732
+ include_parent_content: params.include_parent_content,
4421
4733
  hybrid: true,
4422
4734
  rerank: true
4423
4735
  });
@@ -4432,6 +4744,9 @@ async function queryWithDegradedFallback(params) {
4432
4744
  include_graph: false,
4433
4745
  user_id: params.user_id,
4434
4746
  session_id: params.session_id,
4747
+ source_ids: params.source_ids,
4748
+ retrieval_profile: params.retrieval_profile,
4749
+ include_parent_content: params.include_parent_content,
4435
4750
  hybrid: false,
4436
4751
  rerank: false,
4437
4752
  vector_weight: 0,
@@ -4471,9 +4786,15 @@ function collectCodeFiles(rootPath, allowedExts, maxFiles) {
4471
4786
  }
4472
4787
  function tokenizeQueryForLexicalRescue(query) {
4473
4788
  const stopWords = /* @__PURE__ */ new Set(["where", "what", "show", "find", "logic", "code", "file", "files", "handled", "handling"]);
4474
- return Array.from(new Set(
4475
- query.toLowerCase().split(/[^a-z0-9/_-]+/).map((token) => token.trim()).filter((token) => token.length >= 3 && !stopWords.has(token))
4476
- ));
4789
+ const rawTokens = query.toLowerCase().split(/[^a-z0-9/._:-]+/).map((token) => token.trim()).filter(Boolean);
4790
+ const expanded = /* @__PURE__ */ new Set();
4791
+ for (const token of rawTokens) {
4792
+ if (token.length >= 3 && !stopWords.has(token)) expanded.add(token);
4793
+ for (const part of token.split(/[/:._-]+/).filter((value) => value.length >= 3 && !stopWords.has(value))) {
4794
+ expanded.add(part);
4795
+ }
4796
+ }
4797
+ return Array.from(expanded);
4477
4798
  }
4478
4799
  function buildLexicalRescueResults(args) {
4479
4800
  const tokens = tokenizeQueryForLexicalRescue(args.query);
@@ -4499,16 +4820,17 @@ ${doc.raw_content}`.toLowerCase();
4499
4820
  async function runSearchCodeSearch(args) {
4500
4821
  const topK = args.top_k ?? 10;
4501
4822
  const requestedThreshold = args.threshold ?? 0.2;
4823
+ const semanticTopK = Math.min(args.documents.length || topK, Math.max(topK * 3, topK + 5));
4502
4824
  const semanticDocuments = args.documents.map((doc) => ({ id: doc.id, content: doc.content }));
4503
4825
  const defaultResponse = await args.semantic_search({
4504
4826
  query: args.query,
4505
4827
  documents: semanticDocuments,
4506
- top_k: topK,
4828
+ top_k: semanticTopK,
4507
4829
  threshold: requestedThreshold
4508
4830
  });
4509
4831
  if (defaultResponse.results?.length) {
4510
4832
  return {
4511
- results: defaultResponse.results.map((result) => ({ ...result, search_mode: "semantic" })),
4833
+ results: defaultResponse.results.slice(0, topK).map((result) => ({ ...result, search_mode: "semantic" })),
4512
4834
  mode: "semantic",
4513
4835
  threshold_used: requestedThreshold,
4514
4836
  fallback_used: false,
@@ -4520,12 +4842,12 @@ async function runSearchCodeSearch(args) {
4520
4842
  const adaptiveResponse = await args.semantic_search({
4521
4843
  query: args.query,
4522
4844
  documents: semanticDocuments,
4523
- top_k: topK,
4845
+ top_k: semanticTopK,
4524
4846
  threshold: adaptiveThreshold
4525
4847
  });
4526
4848
  if (adaptiveResponse.results?.length) {
4527
4849
  return {
4528
- results: adaptiveResponse.results.map((result) => ({ ...result, search_mode: "adaptive_semantic" })),
4850
+ results: adaptiveResponse.results.slice(0, topK).map((result) => ({ ...result, search_mode: "adaptive_semantic" })),
4529
4851
  mode: "adaptive_semantic",
4530
4852
  threshold_used: adaptiveThreshold,
4531
4853
  fallback_used: true,
@@ -4546,36 +4868,8 @@ async function runSearchCodeSearch(args) {
4546
4868
  fallback_reason: lexicalResults.length ? "Semantic ranking returned no matches; lexical rescue over local file paths and content was used." : "Semantic ranking returned no matches and lexical rescue found no strong candidates."
4547
4869
  };
4548
4870
  }
4549
- async function runSearchCodeTool(args) {
4550
- const rootPath = canonicalizeWorkspacePath(args.path);
4551
- const workspace = await resolveWorkspaceTrust({ path: rootPath });
4552
- const allowedExts = args.file_types ? new Set(args.file_types) : CODE_EXTENSIONS;
4553
- const files = collectCodeFiles(rootPath, allowedExts, args.max_files ?? 150);
4554
- const sharedWarnings = [...workspace.warnings];
4555
- if (files.length === 0) {
4556
- return {
4557
- tool: "search_code",
4558
- query: args.query,
4559
- path: rootPath,
4560
- results: [],
4561
- count: 0,
4562
- warnings: sharedWarnings,
4563
- diagnostics: {
4564
- workspace_id: workspace.workspace_id,
4565
- root_path: workspace.root_path,
4566
- project_ref: workspace.project_ref,
4567
- project_id: workspace.project_id,
4568
- index_health: workspace.health,
4569
- grounded_to_workspace: workspace.grounded_to_workspace,
4570
- threshold_requested: args.threshold ?? 0.2,
4571
- threshold_used: null,
4572
- fallback_used: false,
4573
- fallback_reason: null,
4574
- search_mode: "semantic",
4575
- warnings: sharedWarnings
4576
- }
4577
- };
4578
- }
4871
+ function buildLocalWorkspaceDocuments(rootPath, allowedExts, maxFiles) {
4872
+ const files = collectCodeFiles(rootPath, allowedExts, maxFiles);
4579
4873
  const documents = [];
4580
4874
  for (const filePath of files) {
4581
4875
  try {
@@ -4587,6 +4881,38 @@ async function runSearchCodeTool(args) {
4587
4881
  } catch {
4588
4882
  }
4589
4883
  }
4884
+ return documents;
4885
+ }
4886
+ function extractRelevantSnippet(rawContent, query) {
4887
+ const lines = rawContent.split("\n");
4888
+ const tokens = tokenizeQueryForLexicalRescue(query);
4889
+ let matchIndex = -1;
4890
+ for (let index = 0; index < lines.length; index += 1) {
4891
+ const lower = lines[index].toLowerCase();
4892
+ if (tokens.some((token) => lower.includes(token))) {
4893
+ matchIndex = index;
4894
+ break;
4895
+ }
4896
+ }
4897
+ if (matchIndex < 0) {
4898
+ matchIndex = lines.findIndex((line) => line.trim().length > 0);
4899
+ }
4900
+ const start = Math.max(0, matchIndex < 0 ? 0 : matchIndex - 1);
4901
+ const end = Math.min(lines.length, start + 4);
4902
+ const snippet = lines.slice(start, end).join("\n").trim().slice(0, 500);
4903
+ return {
4904
+ snippet,
4905
+ line_start: start + 1,
4906
+ ...end > start + 1 ? { line_end: end } : {}
4907
+ };
4908
+ }
4909
+ async function runLocalWorkspaceRetrieval(args) {
4910
+ const identity = resolveWorkspaceIdentity({ path: args.path });
4911
+ const workspace = await resolveWorkspaceTrust({ path: identity.root_path, project: args.project });
4912
+ const allowedExts = args.file_types ? new Set(args.file_types) : CODE_EXTENSIONS;
4913
+ const candidateFiles = collectCodeFiles(identity.root_path, allowedExts, args.max_files ?? 150);
4914
+ const documents = buildLocalWorkspaceDocuments(identity.root_path, allowedExts, args.max_files ?? 150);
4915
+ const sharedWarnings = [...workspace.warnings];
4590
4916
  const searchResult = await runSearchCodeSearch({
4591
4917
  query: args.query,
4592
4918
  documents,
@@ -4602,29 +4928,140 @@ async function runSearchCodeTool(args) {
4602
4928
  if (!workspace.grounded_to_workspace && workspace.health !== "unbound" && workspace.health !== "unindexed") {
4603
4929
  sharedWarnings.push("Local code search is live, but project-backed retrieval may disagree until the workspace is re-indexed.");
4604
4930
  }
4931
+ const documentMap = new Map(documents.map((document) => [document.id, document]));
4932
+ const localResults = searchResult.results.map((result) => {
4933
+ const document = documentMap.get(result.id);
4934
+ const snippet = extractRelevantSnippet(document?.raw_content || result.snippet || "", args.query);
4935
+ return {
4936
+ ...result,
4937
+ snippet: snippet.snippet || result.snippet,
4938
+ raw_snippet: snippet.snippet || result.snippet,
4939
+ line_start: snippet.line_start,
4940
+ ...snippet.line_end ? { line_end: snippet.line_end } : {},
4941
+ retrieval_method: result.search_mode === "lexical_rescue" ? "lexical" : "semantic"
4942
+ };
4943
+ });
4605
4944
  return {
4606
- tool: "search_code",
4607
- query: args.query,
4608
- path: rootPath,
4609
- results: searchResult.results,
4610
- count: searchResult.results.length,
4945
+ workspace,
4946
+ documents,
4947
+ results: localResults,
4611
4948
  warnings: sharedWarnings,
4612
4949
  diagnostics: {
4613
4950
  workspace_id: workspace.workspace_id,
4614
4951
  root_path: workspace.root_path,
4615
4952
  project_ref: workspace.project_ref,
4616
4953
  project_id: workspace.project_id,
4954
+ identity_source: workspace.identity_source,
4617
4955
  index_health: workspace.health,
4618
- grounded_to_workspace: workspace.grounded_to_workspace,
4956
+ grounded_to_workspace: true,
4619
4957
  threshold_requested: args.threshold ?? 0.2,
4620
4958
  threshold_used: searchResult.threshold_used,
4621
4959
  fallback_used: searchResult.fallback_used,
4622
4960
  fallback_reason: searchResult.fallback_reason,
4623
4961
  search_mode: searchResult.mode,
4962
+ candidate_files_scanned: candidateFiles.length,
4963
+ semantic_candidate_count: documents.length,
4964
+ retrieval_route: "local_workspace",
4624
4965
  warnings: sharedWarnings
4625
4966
  }
4626
4967
  };
4627
4968
  }
4969
+ function isLikelyRepoBackedResult(result) {
4970
+ const metadata = result.metadata || {};
4971
+ const retrievalSource = String(result.retrieval_source || "").toLowerCase();
4972
+ if (retrievalSource.includes("memory")) return false;
4973
+ const pathCandidate = String(metadata.file_path || metadata.path || result.source || result.document || "");
4974
+ if (/[\\/]/.test(pathCandidate) || /\.[a-z0-9]+$/i.test(pathCandidate)) return true;
4975
+ const sourceType = String(metadata.source_type || metadata.connector_type || result.type || "").toLowerCase();
4976
+ return ["local", "github", "code", "file", "repo"].some((token) => sourceType.includes(token));
4977
+ }
4978
+ function filterProjectRepoResults(results, matchedSourceIds) {
4979
+ const scoped = results.filter((result) => {
4980
+ const sourceId = String(result.metadata?.source_id || result.metadata?.sourceId || result.metadata?.source || "");
4981
+ return matchedSourceIds.length === 0 || matchedSourceIds.includes(sourceId);
4982
+ });
4983
+ const repoResults = scoped.filter((result) => isLikelyRepoBackedResult(result));
4984
+ return repoResults.length > 0 ? repoResults : scoped;
4985
+ }
4986
+ function localHitsToEvidence(workspaceId, hits) {
4987
+ return hits.map(
4988
+ (hit) => toEvidenceRef(
4989
+ {
4990
+ id: hit.id,
4991
+ content: hit.raw_snippet,
4992
+ score: hit.score,
4993
+ retrieval_source: hit.retrieval_method,
4994
+ metadata: {
4995
+ file_path: hit.id,
4996
+ snippet: hit.raw_snippet,
4997
+ line_start: hit.line_start,
4998
+ ...hit.line_end ? { line_end: hit.line_end } : {}
4999
+ }
5000
+ },
5001
+ workspaceId,
5002
+ hit.retrieval_method
5003
+ )
5004
+ );
5005
+ }
5006
+ function renderLocalWorkspaceContext(args) {
5007
+ const lines = args.hits.map((hit, index) => {
5008
+ const location = hit.line_end && hit.line_end !== hit.line_start ? `${hit.id}:${hit.line_start}-${hit.line_end}` : `${hit.id}:${hit.line_start}`;
5009
+ return `${index + 1}. [${location}, score: ${hit.score.toFixed(2)}, mode: ${hit.search_mode}] ${hit.raw_snippet || hit.snippet || hit.id}`;
5010
+ });
5011
+ const header = `Found ${args.hits.length} local workspace result(s) (route=${args.route}, workspace=${args.diagnostics.workspace_id}, project=${args.project_ref || "none"}, user=${args.scope.userId}, session=${args.scope.sessionId}):`;
5012
+ const warnings = args.warnings.length ? `
5013
+
5014
+ [warnings]
5015
+ ${args.warnings.join("\n")}` : "";
5016
+ return `${header}
5017
+
5018
+ ${lines.join("\n\n")}${warnings}`;
5019
+ }
5020
+ async function runSearchCodeTool(args) {
5021
+ const rootPath = canonicalizeWorkspacePath(args.path);
5022
+ const allowedExts = args.file_types ? new Set(args.file_types) : CODE_EXTENSIONS;
5023
+ const files = collectCodeFiles(rootPath, allowedExts, args.max_files ?? 150);
5024
+ if (files.length === 0) {
5025
+ const workspace = await resolveWorkspaceTrust({ path: rootPath });
5026
+ const sharedWarnings = [...workspace.warnings];
5027
+ return {
5028
+ tool: "search_code",
5029
+ query: args.query,
5030
+ path: rootPath,
5031
+ results: [],
5032
+ count: 0,
5033
+ warnings: sharedWarnings,
5034
+ diagnostics: {
5035
+ workspace_id: workspace.workspace_id,
5036
+ root_path: workspace.root_path,
5037
+ project_ref: workspace.project_ref,
5038
+ project_id: workspace.project_id,
5039
+ identity_source: workspace.identity_source,
5040
+ index_health: workspace.health,
5041
+ grounded_to_workspace: workspace.grounded_to_workspace,
5042
+ threshold_requested: args.threshold ?? 0.2,
5043
+ threshold_used: null,
5044
+ fallback_used: false,
5045
+ fallback_reason: null,
5046
+ search_mode: "semantic",
5047
+ candidate_files_scanned: 0,
5048
+ semantic_candidate_count: 0,
5049
+ retrieval_route: "local_workspace",
5050
+ warnings: sharedWarnings
5051
+ }
5052
+ };
5053
+ }
5054
+ const localRetrieval = await runLocalWorkspaceRetrieval(args);
5055
+ return {
5056
+ tool: "search_code",
5057
+ query: args.query,
5058
+ path: rootPath,
5059
+ results: localRetrieval.results,
5060
+ count: localRetrieval.results.length,
5061
+ warnings: localRetrieval.warnings,
5062
+ diagnostics: localRetrieval.diagnostics
5063
+ };
5064
+ }
4628
5065
  function getLocalAllowlistRoots() {
4629
5066
  const fromEnv = (process.env.WHISPER_LOCAL_ALLOWLIST || "").split(",").map((v) => v.trim()).filter(Boolean);
4630
5067
  if (fromEnv.length > 0) return fromEnv;
@@ -4900,21 +5337,23 @@ server.tool(
4900
5337
  },
4901
5338
  async ({ path, workspace_id, project }) => {
4902
5339
  try {
4903
- const rootPath = canonicalizeWorkspacePath(path);
4904
- const workspaceId = getWorkspaceIdForPath(rootPath, workspace_id);
5340
+ const identity = resolveWorkspaceIdentity({ path, workspace_id });
4905
5341
  const state = loadState();
4906
- const existed = Boolean(state.workspaces[workspaceId]);
4907
- const trust = await resolveWorkspaceTrust({ path: rootPath, workspace_id, project });
5342
+ const existed = Boolean(state.workspaces[identity.workspace_id]);
5343
+ const trust = await resolveWorkspaceTrust({ path: identity.root_path, workspace_id, project });
5344
+ const preflight = await resolveRepoGroundingPreflight({ query: "workspace status", path, workspace_id, project });
4908
5345
  const payload = {
4909
- workspace_id: workspaceId,
5346
+ workspace_id: identity.workspace_id,
4910
5347
  root_path: trust.root_path,
5348
+ identity_source: trust.identity_source,
4911
5349
  project_ref: trust.project_ref,
4912
5350
  project_id: trust.project_id,
4913
5351
  created: !existed,
4914
5352
  resolved_by: trust.resolved_by,
4915
5353
  health: trust.health,
4916
- warnings: trust.warnings,
4917
- recommended_next_calls: trust.recommended_next_calls,
5354
+ retrieval_readiness: preflight.retrieval_readiness,
5355
+ warnings: preflight.warnings,
5356
+ recommended_next_calls: preflight.recommended_next_calls,
4918
5357
  index_state: {
4919
5358
  last_indexed_at: trust.freshness.last_indexed_at,
4920
5359
  last_indexed_commit: trust.last_indexed_commit,
@@ -4937,15 +5376,21 @@ server.tool(
4937
5376
  async ({ workspace_id, path }) => {
4938
5377
  try {
4939
5378
  const trust = await resolveWorkspaceTrust({ path, workspace_id });
5379
+ const repoVerification = await inspectProjectRepoSources({ project_ref: trust.project_ref, root_path: trust.root_path });
4940
5380
  const payload = {
4941
5381
  workspace_id: trust.workspace_id,
4942
5382
  root_path: trust.root_path,
5383
+ identity_source: trust.identity_source,
4943
5384
  project_ref: trust.project_ref,
4944
5385
  project_id: trust.project_id,
4945
5386
  resolved_by: trust.resolved_by,
4946
5387
  health: trust.health,
4947
- warnings: trust.warnings,
4948
- recommended_next_calls: trust.recommended_next_calls,
5388
+ retrieval_readiness: repoVerification.retrieval_readiness,
5389
+ warnings: Array.from(/* @__PURE__ */ new Set([...trust.warnings, ...repoVerification.warnings])),
5390
+ recommended_next_calls: sanitizeRecommendedNextCalls([
5391
+ ...trust.recommended_next_calls,
5392
+ ...repoVerification.retrieval_readiness === "project_bound_no_repo_source" ? ["context.list_sources", "index.local_scan_ingest"] : []
5393
+ ]),
4949
5394
  freshness: trust.freshness,
4950
5395
  coverage: trust.coverage,
4951
5396
  last_indexed_commit: trust.last_indexed_commit,
@@ -4970,8 +5415,9 @@ server.tool(
4970
5415
  },
4971
5416
  async ({ workspace_id, path, mode, max_files }) => {
4972
5417
  try {
4973
- const rootPath = canonicalizeWorkspacePath(path);
4974
- const workspaceId = getWorkspaceIdForPath(rootPath, workspace_id);
5418
+ const identity = resolveWorkspaceIdentity({ path, workspace_id });
5419
+ const rootPath = identity.root_path;
5420
+ const workspaceId = identity.workspace_id;
4975
5421
  const state = loadState();
4976
5422
  const workspace = getWorkspaceState(state, workspaceId);
4977
5423
  const fileStats = countCodeFiles(rootPath, max_files);
@@ -5038,62 +5484,143 @@ server.tool(
5038
5484
  );
5039
5485
  server.tool(
5040
5486
  "context.get_relevant",
5041
- "Core retrieval. Task goes in, ranked context chunks come out with structured evidence (file:line ready).",
5487
+ "Default grounded retrieval step for workspace/project questions. Call this before answering when you need ranked evidence with file:line citations instead of relying on model memory.",
5042
5488
  {
5043
5489
  question: z.string().describe("Task/question to retrieve context for"),
5490
+ path: z.string().optional().describe("Workspace path. Defaults to current working directory."),
5044
5491
  workspace_id: z.string().optional(),
5045
5492
  project: z.string().optional(),
5046
5493
  top_k: z.number().optional().default(12),
5047
5494
  include_memories: z.boolean().optional().default(true),
5048
5495
  include_graph: z.boolean().optional().default(true),
5049
5496
  session_id: z.string().optional(),
5050
- user_id: z.string().optional()
5497
+ user_id: z.string().optional(),
5498
+ include_parent_content: z.boolean().optional().default(false),
5499
+ retrieval_profile: z.enum(MCP_RETRIEVAL_PROFILE_VALUES).optional()
5051
5500
  },
5052
- async ({ question, workspace_id, project, top_k, include_memories, include_graph, session_id, user_id }) => {
5501
+ async ({ question, path, workspace_id, project, top_k, include_memories, include_graph, session_id, user_id, include_parent_content, retrieval_profile }) => {
5053
5502
  try {
5054
- const trust = await resolveWorkspaceTrust({ workspace_id, project });
5055
- if (trust.health === "unbound" || trust.health === "unindexed" || !trust.project_ref) {
5503
+ const preflight = await resolveRepoGroundingPreflight({ query: question, path, workspace_id, project });
5504
+ const retrievalProfile = resolveMcpRetrievalProfile(retrieval_profile);
5505
+ if (shouldAbstainForCodebaseTrust({
5506
+ retrievalProfile,
5507
+ codebaseIntent: preflight.repo_grounded,
5508
+ preflight
5509
+ })) {
5510
+ return toTextResult(buildCodebaseTrustAbstainPayload({
5511
+ query: question,
5512
+ preflight,
5513
+ retrievalProfile
5514
+ }));
5515
+ }
5516
+ if (preflight.retrieval_route === "local_workspace_fallback") {
5517
+ const localRetrieval = await runLocalWorkspaceRetrieval({
5518
+ query: question,
5519
+ path: preflight.trust_state.root_path,
5520
+ project: preflight.trust_state.project_ref || project,
5521
+ top_k
5522
+ });
5523
+ const evidence2 = localHitsToEvidence(preflight.trust_state.workspace_id, localRetrieval.results);
5524
+ const payload2 = {
5525
+ question,
5526
+ workspace_id: preflight.trust_state.workspace_id,
5527
+ root_path: preflight.trust_state.root_path,
5528
+ identity_source: preflight.trust_state.identity_source,
5529
+ trust_state: preflight.trust_state,
5530
+ retrieval_readiness: preflight.retrieval_readiness,
5531
+ retrieval_route: preflight.retrieval_route,
5532
+ grounded_to_workspace: true,
5533
+ total_results: evidence2.length,
5534
+ context: evidence2.map((item) => `[${renderCitation(item)}] ${item.snippet || "Relevant local workspace context found."}`).join("\n"),
5535
+ evidence: evidence2,
5536
+ used_context_ids: evidence2.map((item) => item.source_id),
5537
+ latency_ms: 0,
5538
+ warnings: Array.from(/* @__PURE__ */ new Set([...preflight.warnings, ...localRetrieval.warnings])),
5539
+ recommended_next_calls: preflight.recommended_next_calls
5540
+ };
5541
+ return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
5542
+ }
5543
+ if (!preflight.trust_state.project_ref) {
5056
5544
  const payload2 = {
5057
5545
  question,
5058
- workspace_id: trust.workspace_id,
5059
- trust_state: trust,
5546
+ workspace_id: preflight.trust_state.workspace_id,
5547
+ root_path: preflight.trust_state.root_path,
5548
+ identity_source: preflight.trust_state.identity_source,
5549
+ trust_state: preflight.trust_state,
5550
+ retrieval_readiness: preflight.retrieval_readiness,
5551
+ retrieval_route: "none",
5060
5552
  grounded_to_workspace: false,
5061
5553
  total_results: 0,
5062
5554
  context: "",
5063
5555
  evidence: [],
5064
5556
  used_context_ids: [],
5065
5557
  latency_ms: 0,
5066
- warnings: trust.warnings,
5067
- recommended_next_calls: trust.recommended_next_calls
5558
+ warnings: preflight.warnings,
5559
+ recommended_next_calls: preflight.recommended_next_calls
5068
5560
  };
5069
5561
  return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
5070
5562
  }
5071
5563
  const queryResult = await queryWithDegradedFallback({
5072
- project: trust.project_ref,
5564
+ project: preflight.trust_state.project_ref,
5073
5565
  query: question,
5074
5566
  top_k,
5075
- include_memories,
5567
+ include_memories: preflight.repo_grounded ? false : include_memories,
5076
5568
  include_graph,
5077
5569
  session_id,
5078
- user_id
5570
+ user_id,
5571
+ source_ids: preflight.repo_grounded ? preflight.matched_source_ids : void 0,
5572
+ retrieval_profile: retrievalProfile,
5573
+ include_parent_content
5079
5574
  });
5080
5575
  const response = queryResult.response;
5081
- const evidence = (response.results || []).map((r) => toEvidenceRef(r, trust.workspace_id, "semantic"));
5576
+ const rawResults = preflight.repo_grounded ? filterProjectRepoResults(response.results || [], preflight.matched_source_ids) : response.results || [];
5577
+ if (preflight.repo_grounded && rawResults.length === 0) {
5578
+ const localRetrieval = await runLocalWorkspaceRetrieval({
5579
+ query: question,
5580
+ path: preflight.trust_state.root_path,
5581
+ project: preflight.trust_state.project_ref,
5582
+ top_k
5583
+ });
5584
+ const evidence2 = localHitsToEvidence(preflight.trust_state.workspace_id, localRetrieval.results);
5585
+ const payload2 = {
5586
+ question,
5587
+ workspace_id: preflight.trust_state.workspace_id,
5588
+ root_path: preflight.trust_state.root_path,
5589
+ identity_source: preflight.trust_state.identity_source,
5590
+ trust_state: preflight.trust_state,
5591
+ retrieval_readiness: "local_fallback",
5592
+ retrieval_route: "local_workspace_fallback",
5593
+ grounded_to_workspace: true,
5594
+ total_results: evidence2.length,
5595
+ context: evidence2.map((item) => `[${renderCitation(item)}] ${item.snippet || "Relevant local workspace context found."}`).join("\n"),
5596
+ evidence: evidence2,
5597
+ used_context_ids: evidence2.map((item) => item.source_id),
5598
+ latency_ms: 0,
5599
+ warnings: Array.from(/* @__PURE__ */ new Set([...preflight.warnings, ...localRetrieval.warnings])),
5600
+ recommended_next_calls: preflight.recommended_next_calls
5601
+ };
5602
+ return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
5603
+ }
5604
+ const evidence = rawResults.map((r) => toEvidenceRef(r, preflight.trust_state.workspace_id, "semantic"));
5082
5605
  const payload = {
5083
5606
  question,
5084
- workspace_id: trust.workspace_id,
5085
- trust_state: trust,
5086
- grounded_to_workspace: trust.grounded_to_workspace,
5087
- total_results: response.meta?.total || evidence.length,
5607
+ workspace_id: preflight.trust_state.workspace_id,
5608
+ root_path: preflight.trust_state.root_path,
5609
+ identity_source: preflight.trust_state.identity_source,
5610
+ trust_state: preflight.trust_state,
5611
+ retrieval_readiness: preflight.retrieval_readiness,
5612
+ retrieval_route: preflight.repo_grounded ? "project_repo" : "none",
5613
+ grounded_to_workspace: preflight.repo_grounded ? true : preflight.trust_state.grounded_to_workspace,
5614
+ total_results: rawResults.length || response.meta?.total || evidence.length,
5088
5615
  context: response.context || "",
5089
5616
  evidence,
5090
- used_context_ids: (response.results || []).map((r) => String(r.id)),
5617
+ used_context_ids: rawResults.map((r) => String(r.id)),
5091
5618
  latency_ms: response.meta?.latency_ms || 0,
5092
5619
  degraded_mode: queryResult.degraded_mode,
5093
5620
  degraded_reason: queryResult.degraded_reason,
5094
5621
  recommendation: queryResult.recommendation,
5095
- warnings: trust.warnings,
5096
- recommended_next_calls: trust.recommended_next_calls
5622
+ warnings: preflight.warnings,
5623
+ recommended_next_calls: preflight.recommended_next_calls
5097
5624
  };
5098
5625
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
5099
5626
  } catch (error) {
@@ -5106,38 +5633,53 @@ server.tool(
5106
5633
  "Verify whether a claim is supported by retrieved context. Returns supported/partial/unsupported with evidence.",
5107
5634
  {
5108
5635
  claim: z.string().describe("Claim to verify"),
5636
+ path: z.string().optional().describe("Workspace path. Defaults to current working directory."),
5109
5637
  workspace_id: z.string().optional(),
5110
5638
  project: z.string().optional(),
5111
5639
  context_ids: z.array(z.string()).optional(),
5112
5640
  strict: z.boolean().optional().default(true)
5113
5641
  },
5114
- async ({ claim, workspace_id, project, context_ids, strict }) => {
5642
+ async ({ claim, path, workspace_id, project, context_ids, strict }) => {
5115
5643
  try {
5116
- const trust = await resolveWorkspaceTrust({ workspace_id, project });
5117
- if (trust.health !== "healthy" || !trust.project_ref) {
5644
+ const preflight = await resolveRepoGroundingPreflight({ query: claim, path, workspace_id, project });
5645
+ let evidence = [];
5646
+ if (preflight.retrieval_route === "local_workspace_fallback") {
5647
+ const localRetrieval = await runLocalWorkspaceRetrieval({
5648
+ query: claim,
5649
+ path: preflight.trust_state.root_path,
5650
+ project: preflight.trust_state.project_ref || project,
5651
+ top_k: strict ? 8 : 12
5652
+ });
5653
+ evidence = localHitsToEvidence(preflight.trust_state.workspace_id, localRetrieval.results).filter((item) => !context_ids || context_ids.length === 0 || context_ids.includes(item.source_id));
5654
+ } else if (preflight.trust_state.project_ref) {
5655
+ const response = await whisper.query({
5656
+ project: preflight.trust_state.project_ref,
5657
+ query: claim,
5658
+ top_k: strict ? 8 : 12,
5659
+ include_memories: preflight.repo_grounded ? false : true,
5660
+ include_graph: true,
5661
+ ...preflight.repo_grounded ? { source_ids: preflight.matched_source_ids } : {}
5662
+ });
5663
+ const filtered = (preflight.repo_grounded ? filterProjectRepoResults(response.results || [], preflight.matched_source_ids) : response.results || []).filter(
5664
+ (r) => !context_ids || context_ids.length === 0 || context_ids.includes(String(r.id))
5665
+ );
5666
+ evidence = filtered.map((r) => toEvidenceRef(r, preflight.trust_state.workspace_id, "semantic"));
5667
+ }
5668
+ if (evidence.length === 0) {
5118
5669
  const payload2 = {
5119
5670
  verdict: "unsupported",
5120
5671
  confidence: 0,
5121
5672
  evidence: [],
5122
- trust_state: trust,
5123
- warnings: trust.warnings,
5124
- recommended_next_calls: trust.recommended_next_calls,
5125
- missing_requirements: trust.warnings.length ? trust.warnings : ["Workspace trust requirements were not met."],
5126
- explanation: "Verifier did not run because workspace grounding is insufficient."
5673
+ trust_state: preflight.trust_state,
5674
+ retrieval_readiness: preflight.retrieval_readiness,
5675
+ retrieval_route: preflight.retrieval_route,
5676
+ warnings: preflight.warnings,
5677
+ recommended_next_calls: preflight.recommended_next_calls,
5678
+ missing_requirements: preflight.warnings.length ? preflight.warnings : ["No repo-grounded evidence was available for verification."],
5679
+ explanation: "Verifier did not find sufficient repo-grounded evidence."
5127
5680
  };
5128
5681
  return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
5129
5682
  }
5130
- const response = await whisper.query({
5131
- project: trust.project_ref,
5132
- query: claim,
5133
- top_k: strict ? 8 : 12,
5134
- include_memories: true,
5135
- include_graph: true
5136
- });
5137
- const filtered = (response.results || []).filter(
5138
- (r) => !context_ids || context_ids.length === 0 || context_ids.includes(String(r.id))
5139
- );
5140
- const evidence = filtered.map((r) => toEvidenceRef(r, trust.workspace_id, "semantic"));
5141
5683
  const directEvidence = evidence.filter((e) => e.score >= (strict ? 0.7 : 0.6));
5142
5684
  const weakEvidence = evidence.filter((e) => e.score >= (strict ? 0.45 : 0.35));
5143
5685
  let verdict = "unsupported";
@@ -5147,9 +5689,11 @@ server.tool(
5147
5689
  verdict,
5148
5690
  confidence: evidence.length ? Math.max(...evidence.map((e) => e.score)) : 0,
5149
5691
  evidence: verdict === "supported" ? directEvidence : weakEvidence,
5150
- trust_state: trust,
5151
- warnings: trust.warnings,
5152
- recommended_next_calls: trust.recommended_next_calls,
5692
+ trust_state: preflight.trust_state,
5693
+ retrieval_readiness: preflight.retrieval_readiness,
5694
+ retrieval_route: preflight.retrieval_route,
5695
+ warnings: preflight.warnings,
5696
+ recommended_next_calls: preflight.recommended_next_calls,
5153
5697
  missing_requirements: verdict === "supported" ? [] : verdict === "partial" ? ["No direct evidence spans met strict threshold."] : ["No sufficient supporting evidence found for the claim."],
5154
5698
  explanation: verdict === "supported" ? "At least one direct evidence span supports the claim." : verdict === "partial" ? "Some related evidence exists, but direct support is incomplete." : "Retrieved context did not contain sufficient support."
5155
5699
  };
@@ -5164,6 +5708,7 @@ server.tool(
5164
5708
  "Answer a question only when evidence requirements are met. Fails closed with an abstain payload when not verifiable.",
5165
5709
  {
5166
5710
  question: z.string(),
5711
+ path: z.string().optional().describe("Workspace path. Defaults to current working directory."),
5167
5712
  workspace_id: z.string().optional(),
5168
5713
  project: z.string().optional(),
5169
5714
  constraints: z.object({
@@ -5178,38 +5723,51 @@ server.tool(
5178
5723
  include_recent_decisions: z.boolean().optional().default(true)
5179
5724
  }).optional()
5180
5725
  },
5181
- async ({ question, workspace_id, project, constraints, retrieval }) => {
5726
+ async ({ question, path, workspace_id, project, constraints, retrieval }) => {
5182
5727
  try {
5183
5728
  const requireCitations = constraints?.require_citations ?? true;
5184
5729
  const minEvidenceItems = constraints?.min_evidence_items ?? 2;
5185
5730
  const minConfidence = constraints?.min_confidence ?? 0.65;
5186
5731
  const maxStalenessHours = constraints?.max_staleness_hours ?? 168;
5187
5732
  const topK = retrieval?.top_k ?? 12;
5188
- const trust = await resolveWorkspaceTrust({ workspace_id, project, max_staleness_hours: maxStalenessHours });
5189
- if (trust.health !== "healthy" || !trust.project_ref) {
5190
- const reason = trust.health === "stale" || trust.health === "drifted" ? "stale_index" : "no_retrieval_hits";
5733
+ const preflight = await resolveRepoGroundingPreflight({ query: question, path, workspace_id, project });
5734
+ let evidence = [];
5735
+ if (preflight.retrieval_route === "local_workspace_fallback") {
5736
+ const localRetrieval = await runLocalWorkspaceRetrieval({
5737
+ query: question,
5738
+ path: preflight.trust_state.root_path,
5739
+ project: preflight.trust_state.project_ref || project,
5740
+ top_k: topK
5741
+ });
5742
+ evidence = localHitsToEvidence(preflight.trust_state.workspace_id, localRetrieval.results);
5743
+ } else if (preflight.trust_state.project_ref) {
5744
+ const response = await whisper.query({
5745
+ project: preflight.trust_state.project_ref,
5746
+ query: question,
5747
+ top_k: topK,
5748
+ include_memories: preflight.repo_grounded ? false : true,
5749
+ include_graph: true,
5750
+ ...preflight.repo_grounded ? { source_ids: preflight.matched_source_ids } : {}
5751
+ });
5752
+ const filtered = preflight.repo_grounded ? filterProjectRepoResults(response.results || [], preflight.matched_source_ids) : response.results || [];
5753
+ evidence = filtered.map((r) => toEvidenceRef(r, preflight.trust_state.workspace_id, "semantic"));
5754
+ }
5755
+ if (evidence.length === 0) {
5756
+ const reason = preflight.trust_state.health === "stale" || preflight.trust_state.health === "drifted" ? "stale_index" : "no_retrieval_hits";
5191
5757
  const abstain = buildAbstain({
5192
5758
  reason,
5193
- message: trust.warnings[0] || "Workspace trust requirements were not met.",
5759
+ message: preflight.warnings[0] || "Workspace trust requirements were not met.",
5194
5760
  closest_evidence: [],
5195
5761
  claims_evaluated: 1,
5196
5762
  evidence_items_found: 0,
5197
5763
  min_required: minEvidenceItems,
5198
- index_fresh: trust.health === "healthy",
5199
- warnings: trust.warnings,
5200
- trust_state: trust,
5201
- recommended_next_calls: trust.recommended_next_calls
5764
+ index_fresh: preflight.trust_state.health === "healthy",
5765
+ warnings: preflight.warnings,
5766
+ trust_state: preflight.trust_state,
5767
+ recommended_next_calls: preflight.recommended_next_calls
5202
5768
  });
5203
5769
  return { content: [{ type: "text", text: JSON.stringify(abstain, null, 2) }] };
5204
5770
  }
5205
- const response = await whisper.query({
5206
- project: trust.project_ref,
5207
- query: question,
5208
- top_k: topK,
5209
- include_memories: true,
5210
- include_graph: true
5211
- });
5212
- const evidence = (response.results || []).map((r) => toEvidenceRef(r, trust.workspace_id, "semantic"));
5213
5771
  const sorted = evidence.sort((a, b) => b.score - a.score);
5214
5772
  const confidence = sorted.length ? sorted[0].score : 0;
5215
5773
  if (sorted.length === 0) {
@@ -5220,10 +5778,10 @@ server.tool(
5220
5778
  claims_evaluated: 1,
5221
5779
  evidence_items_found: 0,
5222
5780
  min_required: minEvidenceItems,
5223
- index_fresh: trust.health === "healthy",
5224
- warnings: trust.warnings,
5225
- trust_state: trust,
5226
- recommended_next_calls: trust.recommended_next_calls
5781
+ index_fresh: preflight.trust_state.health === "healthy",
5782
+ warnings: preflight.warnings,
5783
+ trust_state: preflight.trust_state,
5784
+ recommended_next_calls: preflight.recommended_next_calls
5227
5785
  });
5228
5786
  return { content: [{ type: "text", text: JSON.stringify(abstain, null, 2) }] };
5229
5787
  }
@@ -5238,9 +5796,9 @@ server.tool(
5238
5796
  evidence_items_found: supportedEvidence.length,
5239
5797
  min_required: minEvidenceItems,
5240
5798
  index_fresh: true,
5241
- warnings: trust.warnings,
5242
- trust_state: trust,
5243
- recommended_next_calls: trust.recommended_next_calls
5799
+ warnings: preflight.warnings,
5800
+ trust_state: preflight.trust_state,
5801
+ recommended_next_calls: preflight.recommended_next_calls
5244
5802
  });
5245
5803
  return { content: [{ type: "text", text: JSON.stringify(abstain, null, 2) }] };
5246
5804
  }
@@ -5253,9 +5811,11 @@ server.tool(
5253
5811
  answer: answerLines.join("\n"),
5254
5812
  citations,
5255
5813
  confidence,
5256
- trust_state: trust,
5257
- warnings: trust.warnings,
5258
- recommended_next_calls: trust.recommended_next_calls,
5814
+ trust_state: preflight.trust_state,
5815
+ retrieval_readiness: preflight.retrieval_readiness,
5816
+ retrieval_route: preflight.retrieval_route,
5817
+ warnings: preflight.warnings,
5818
+ recommended_next_calls: preflight.recommended_next_calls,
5259
5819
  verification: {
5260
5820
  verdict,
5261
5821
  supported_claims: verdict === "supported" ? 1 : 0,
@@ -5271,26 +5831,134 @@ server.tool(
5271
5831
  );
5272
5832
  server.tool(
5273
5833
  "context.query",
5274
- "Search your knowledge base for relevant context. Returns packed context ready for LLM consumption. Supports hybrid vector+keyword search, memory inclusion, and knowledge graph traversal.",
5834
+ "Use this when answering from project knowledge rather than general model memory. Retrieves packed context for a query using hybrid vector+keyword search with optional memory/graph expansion.",
5275
5835
  {
5276
5836
  project: z.string().optional().describe("Project name or slug (optional if WHISPER_PROJECT is set)"),
5277
5837
  query: z.string().describe("What are you looking for?"),
5838
+ path: z.string().optional().describe("Workspace path. Defaults to current working directory."),
5278
5839
  top_k: z.number().optional().default(10).describe("Number of results"),
5279
5840
  chunk_types: z.array(z.string()).optional().describe("Filter: code, function, class, documentation, api_spec, schema, config, text"),
5280
5841
  include_memories: z.boolean().optional().describe("Include relevant memories. Omit to use automatic runtime defaults."),
5281
5842
  include_graph: z.boolean().optional().default(false).describe("Include knowledge graph traversal"),
5282
5843
  user_id: z.string().optional().describe("User ID for memory scoping"),
5283
5844
  session_id: z.string().optional().describe("Session ID for memory scoping"),
5284
- max_tokens: z.number().optional().describe("Max tokens for packed context")
5845
+ max_tokens: z.number().optional().describe("Max tokens for packed context"),
5846
+ include_parent_content: z.boolean().optional().default(false),
5847
+ retrieval_profile: z.enum(MCP_RETRIEVAL_PROFILE_VALUES).optional()
5285
5848
  },
5286
- async ({ project, query, top_k, chunk_types, include_memories, include_graph, user_id, session_id, max_tokens }) => {
5849
+ async ({ project, query, path, top_k, chunk_types, include_memories, include_graph, user_id, session_id, max_tokens, include_parent_content, retrieval_profile }) => {
5287
5850
  try {
5288
- const resolvedProject = await resolveProjectRef(project);
5289
- if (!resolvedProject) {
5851
+ const preflight = await resolveRepoGroundingPreflight({ query, path, project, chunk_types });
5852
+ const retrievalProfile = resolveMcpRetrievalProfile(retrieval_profile);
5853
+ if (shouldAbstainForCodebaseTrust({
5854
+ retrievalProfile,
5855
+ codebaseIntent: preflight.repo_grounded,
5856
+ preflight
5857
+ })) {
5858
+ return toTextResult(buildCodebaseTrustAbstainPayload({
5859
+ query,
5860
+ preflight,
5861
+ retrievalProfile
5862
+ }));
5863
+ }
5864
+ const resolvedProject = preflight.trust_state.project_ref || await resolveProjectRef(project);
5865
+ if (!resolvedProject && !preflight.repo_grounded) {
5290
5866
  return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or pass project." }] };
5291
5867
  }
5292
- const scope = resolveMcpScope({ project: resolvedProject, user_id, session_id });
5293
- const automaticMode = include_memories !== false && include_graph !== true && !(chunk_types && chunk_types.length > 0) && max_tokens === void 0 && runtimeClient;
5868
+ const scope = resolveMcpScope({ project: resolvedProject, user_id, session_id, path: preflight.trust_state.root_path });
5869
+ if (preflight.repo_grounded && preflight.retrieval_route === "local_workspace_fallback") {
5870
+ const localRetrieval = await runLocalWorkspaceRetrieval({
5871
+ query,
5872
+ path: preflight.trust_state.root_path,
5873
+ project: resolvedProject || project,
5874
+ top_k
5875
+ });
5876
+ if (localRetrieval.results.length > 0) {
5877
+ return {
5878
+ content: [{
5879
+ type: "text",
5880
+ text: renderLocalWorkspaceContext({
5881
+ query,
5882
+ project_ref: resolvedProject || null,
5883
+ scope,
5884
+ route: "local_workspace_fallback",
5885
+ hits: localRetrieval.results,
5886
+ diagnostics: localRetrieval.diagnostics,
5887
+ warnings: Array.from(/* @__PURE__ */ new Set([...preflight.warnings, ...localRetrieval.warnings]))
5888
+ })
5889
+ }]
5890
+ };
5891
+ }
5892
+ }
5893
+ if (preflight.repo_grounded && resolvedProject) {
5894
+ const queryResult2 = await queryWithDegradedFallback({
5895
+ project: resolvedProject,
5896
+ query,
5897
+ top_k,
5898
+ include_memories: false,
5899
+ include_graph,
5900
+ user_id: user_id || scope.userId,
5901
+ session_id: session_id || scope.sessionId,
5902
+ source_ids: preflight.matched_source_ids,
5903
+ retrieval_profile: retrievalProfile,
5904
+ include_parent_content
5905
+ });
5906
+ const repoResults = filterProjectRepoResults(queryResult2.response.results || [], preflight.matched_source_ids);
5907
+ if (repoResults.length > 0) {
5908
+ const scopedResponse = { ...queryResult2.response, results: repoResults };
5909
+ const header2 = `Found ${repoResults.length} repo-grounded result(s) (${scopedResponse.meta.latency_ms}ms${scopedResponse.meta.cache_hit ? ", cached" : ""}, route=project_repo, workspace=${preflight.trust_state.workspace_id}, project=${resolvedProject}, user=${scope.userId}, session=${scope.sessionId}):
5910
+
5911
+ `;
5912
+ const suffix2 = [
5913
+ `[diagnostics] identity_source=${preflight.trust_state.identity_source} retrieval_readiness=${preflight.retrieval_readiness} retrieval_route=project_repo`,
5914
+ queryResult2.degraded_mode ? `[degraded_mode=true] ${queryResult2.degraded_reason}
5915
+ Recommendation: ${queryResult2.recommendation}` : "",
5916
+ preflight.warnings.length ? `[warnings]
5917
+ ${preflight.warnings.join("\n")}` : ""
5918
+ ].filter(Boolean).join("\n\n");
5919
+ return { content: [{ type: "text", text: `${header2}${scopedResponse.context}${suffix2 ? `
5920
+
5921
+ ${suffix2}` : ""}` }] };
5922
+ }
5923
+ }
5924
+ if (preflight.repo_grounded) {
5925
+ if (resolvedProject && include_memories !== false) {
5926
+ const memoryRescue = await runContextQueryMemoryRescue({
5927
+ project: resolvedProject,
5928
+ query,
5929
+ user_id: user_id ? scope.userId : void 0,
5930
+ session_id: session_id ? scope.sessionId : void 0,
5931
+ top_k
5932
+ });
5933
+ if (memoryRescue.results.length && memoryRescue.rescue_mode) {
5934
+ return {
5935
+ content: [{
5936
+ type: "text",
5937
+ text: `${renderContextQueryMemoryRescue({
5938
+ project: resolvedProject,
5939
+ query,
5940
+ scope,
5941
+ results: memoryRescue.results,
5942
+ rescue_mode: memoryRescue.rescue_mode
5943
+ })}
5944
+
5945
+ [diagnostics]
5946
+ retrieval_route=memory_only retrieval_readiness=${preflight.retrieval_readiness} workspace=${preflight.trust_state.workspace_id}`
5947
+ }]
5948
+ };
5949
+ }
5950
+ }
5951
+ return {
5952
+ content: [{
5953
+ type: "text",
5954
+ text: `No relevant repo-grounded context found.
5955
+
5956
+ [diagnostics]
5957
+ workspace=${preflight.trust_state.workspace_id} identity_source=${preflight.trust_state.identity_source} retrieval_readiness=${preflight.retrieval_readiness} retrieval_route=${preflight.retrieval_route}`
5958
+ }]
5959
+ };
5960
+ }
5961
+ const automaticMode = !preflight.repo_grounded && include_memories !== false && include_graph !== true && !(chunk_types && chunk_types.length > 0) && max_tokens === void 0 && runtimeClient;
5294
5962
  if (automaticMode) {
5295
5963
  try {
5296
5964
  const prepared = await prepareAutomaticQuery({
@@ -5298,14 +5966,15 @@ server.tool(
5298
5966
  query,
5299
5967
  top_k,
5300
5968
  user_id,
5301
- session_id
5969
+ session_id,
5970
+ path: preflight.trust_state.root_path
5302
5971
  });
5303
5972
  if (!prepared.items.length) {
5304
5973
  const memoryRescue = include_memories !== false ? await runContextQueryMemoryRescue({
5305
5974
  project: resolvedProject,
5306
5975
  query,
5307
- user_id: user_id ? scope2.userId : void 0,
5308
- session_id: session_id ? scope2.sessionId : void 0,
5976
+ user_id: user_id ? scope.userId : void 0,
5977
+ session_id: session_id ? scope.sessionId : void 0,
5309
5978
  top_k
5310
5979
  }) : { results: [], rescue_mode: null };
5311
5980
  if (memoryRescue.results.length && memoryRescue.rescue_mode) {
@@ -5315,7 +5984,7 @@ server.tool(
5315
5984
  text: renderContextQueryMemoryRescue({
5316
5985
  project: resolvedProject,
5317
5986
  query,
5318
- scope: scope2,
5987
+ scope,
5319
5988
  results: memoryRescue.results,
5320
5989
  rescue_mode: memoryRescue.rescue_mode
5321
5990
  })
@@ -5334,11 +6003,11 @@ ${prepared.retrieval.warnings.join("\n")}` : "";
5334
6003
  `deduped=${prepared.retrieval.dedupedCount}`,
5335
6004
  `dropped_below_floor=${prepared.retrieval.droppedBelowFloor}`
5336
6005
  ].join(" ");
5337
- const scope2 = `project=${prepared.scope.project} user=${prepared.scope.userId} session=${prepared.scope.sessionId}`;
6006
+ const scopeLabel = `project=${prepared.scope.project} user=${prepared.scope.userId} session=${prepared.scope.sessionId}`;
5338
6007
  return {
5339
6008
  content: [{
5340
6009
  type: "text",
5341
- text: `Found ${prepared.items.length} runtime-ranked items (${prepared.retrieval.durationMs}ms, ${scope2}, ${diagnostics}):
6010
+ text: `Found ${prepared.items.length} runtime-ranked items (${prepared.retrieval.durationMs}ms, ${scopeLabel}, ${diagnostics}):
5342
6011
 
5343
6012
  ${prepared.context}${warnings}`
5344
6013
  }]
@@ -5351,8 +6020,10 @@ ${prepared.context}${warnings}`
5351
6020
  top_k,
5352
6021
  include_memories: include_memories === true,
5353
6022
  include_graph,
5354
- user_id: user_id || resolveMcpScope({ user_id }).userId,
5355
- session_id: session_id || resolveMcpScope({ session_id }).sessionId
6023
+ user_id: user_id || scope.userId,
6024
+ session_id: session_id || scope.sessionId,
6025
+ retrieval_profile: retrievalProfile,
6026
+ include_parent_content
5356
6027
  });
5357
6028
  const response2 = queryResult2.response;
5358
6029
  if (response2.results.length === 0) {
@@ -5404,8 +6075,10 @@ ${automaticWarning}${suffix2}` }] };
5404
6075
  top_k,
5405
6076
  include_memories: include_memories === true,
5406
6077
  include_graph,
5407
- user_id: user_id || resolveMcpScope({ user_id }).userId,
5408
- session_id: session_id || resolveMcpScope({ session_id }).sessionId
6078
+ user_id: user_id || scope.userId,
6079
+ session_id: session_id || scope.sessionId,
6080
+ retrieval_profile: retrievalProfile,
6081
+ include_parent_content
5409
6082
  });
5410
6083
  const response = queryResult.response;
5411
6084
  if (response.results.length === 0) {
@@ -5483,7 +6156,7 @@ server.tool(
5483
6156
  );
5484
6157
  server.tool(
5485
6158
  "memory.search",
5486
- "Search stored memories by semantic similarity. Recall facts, preferences, past decisions from previous interactions.",
6159
+ "Call this before answering questions about user history (preferences, prior decisions, past tasks, or 'what did we discuss/search'). Returns memory context you would not otherwise know.",
5487
6160
  {
5488
6161
  project: z.string().optional().describe("Project name or slug"),
5489
6162
  query: z.string().describe("What to search for"),
@@ -6462,6 +7135,7 @@ var CODE_EXTENSIONS = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "py", "
6462
7135
  function extractSignature(filePath, content) {
6463
7136
  const lines = content.split("\n");
6464
7137
  const signature = /* @__PURE__ */ new Set([`// File: ${filePath}`]);
7138
+ signature.add(`relative_path:${filePath.replace(/\\/g, "/").toLowerCase()}`);
6465
7139
  for (const segment of filePath.split(/[\\/._-]+/).filter(Boolean)) {
6466
7140
  signature.add(`path:${segment}`);
6467
7141
  }
@@ -6477,29 +7151,39 @@ function extractSignature(filePath, content) {
6477
7151
  if (routeMatches) {
6478
7152
  for (const match of routeMatches.slice(0, 3)) signature.add(`route:${match.slice(0, 120)}`);
6479
7153
  }
7154
+ const envKeyMatches = trimmed.match(/[A-Z][A-Z0-9_]{2,}/g);
7155
+ if (envKeyMatches) {
7156
+ for (const match of envKeyMatches.slice(0, 3)) signature.add(`env:${match}`);
7157
+ }
6480
7158
  if (/^(import|from|require|use |pub use )/.test(trimmed)) {
6481
7159
  signature.add(trimmed.slice(0, 120));
6482
7160
  continue;
6483
7161
  }
6484
7162
  if (/^(export|async function|function|class|interface|type |const |let |def |pub fn |fn |struct |impl |enum )/.test(trimmed)) {
6485
7163
  signature.add(trimmed.slice(0, 120));
7164
+ const exportMatch = trimmed.match(/export\s+(?:const|function|class|type|interface)\s+([A-Za-z0-9_$]+)/);
7165
+ if (exportMatch?.[1]) signature.add(`export:${exportMatch[1]}`);
6486
7166
  continue;
6487
7167
  }
6488
7168
  if (trimmed.startsWith("@") || trimmed.startsWith("#[")) {
6489
7169
  signature.add(trimmed.slice(0, 80));
6490
7170
  }
6491
7171
  }
6492
- for (const line of lines.slice(60)) {
6493
- const trimmed = line.trim();
7172
+ for (let index = 60; index < lines.length; index += 1) {
7173
+ const trimmed = lines[index].trim();
6494
7174
  if (/^(export (default |async )?function|export (default )?class|export const|export type|export interface|async function|function |class |def |pub fn |fn )/.test(trimmed)) {
6495
7175
  signature.add(trimmed.slice(0, 120));
7176
+ const surrounding = lines.slice(index, Math.min(lines.length, index + 3)).map((line) => line.trim()).filter(Boolean);
7177
+ for (const line of surrounding) signature.add(line.slice(0, 120));
6496
7178
  continue;
6497
7179
  }
6498
7180
  if (/^[A-Za-z0-9_$]+\s*[:=]\s*["'`][^"'`]{3,}["'`]/.test(trimmed)) {
6499
7181
  signature.add(trimmed.slice(0, 120));
6500
7182
  }
7183
+ const exportMatch = trimmed.match(/export\s+(?:const|function|class|type|interface)\s+([A-Za-z0-9_$]+)/);
7184
+ if (exportMatch?.[1]) signature.add(`export:${exportMatch[1]}`);
6501
7185
  }
6502
- return Array.from(signature).join("\n").slice(0, 2500);
7186
+ return Array.from(signature).join("\n").slice(0, 3200);
6503
7187
  }
6504
7188
  server.tool(
6505
7189
  "code.search_semantic",
@@ -6766,7 +7450,7 @@ server.tool(
6766
7450
  );
6767
7451
  server.tool(
6768
7452
  "search",
6769
- "Search retrievable context by query, exact id, or both. Use `id` for exact fetch and `query` for semantic retrieval.",
7453
+ "Primary retrieval alias. Call this whenever the user asks to find or recall context: use `query` for semantic retrieval, `id` for exact memory fetch, or both for hybrid recall.",
6770
7454
  {
6771
7455
  project: z.string().optional().describe("Project name or slug"),
6772
7456
  query: z.string().optional().describe("Semantic retrieval query"),
@@ -6996,7 +7680,7 @@ server.tool(
6996
7680
  );
6997
7681
  server.tool(
6998
7682
  "index",
6999
- "Index a new source or refresh a workspace. Use action='source' to add GitHub/web/pdf/local/slack/video. Use action='workspace' to refresh local workspace metadata.",
7683
+ "Administrative indexing tool. Call this when retrieval is stale/missing or the user asks to connect new data. Use action='source' to add GitHub/web/pdf/local/slack/video, action='workspace' to refresh local workspace metadata.",
7000
7684
  {
7001
7685
  action: z.enum(["source", "workspace"]).default("source"),
7002
7686
  project: z.string().optional(),
@@ -7077,7 +7761,7 @@ server.tool(
7077
7761
  );
7078
7762
  server.tool(
7079
7763
  "remember",
7080
- "Store something the agent should keep across sessions: a fact, decision, preference, or instruction.",
7764
+ "Call this whenever the user states a durable preference, decision, instruction, or personal/project fact that should persist across sessions. Save proactively without waiting for an explicit 'remember this'.",
7081
7765
  {
7082
7766
  project: z.string().optional(),
7083
7767
  content: z.string().describe("Memory content"),
@@ -7156,7 +7840,7 @@ server.tool(
7156
7840
  );
7157
7841
  server.tool(
7158
7842
  "learn",
7159
- "Unified learning tool for conversation memory, text ingestion, and source indexing. Prefer this over the older learning-adjacent compatibility tools.",
7843
+ "Unified ingestion entrypoint. Call this when the user asks to import knowledge: mode='conversation' for chat logs, mode='text' for raw text, mode='source' for external sources to index. Prefer this over legacy compatibility tools.",
7160
7844
  {
7161
7845
  mode: z.enum(["conversation", "text", "source"]).describe("What kind of learning to perform"),
7162
7846
  project: z.string().optional(),
@@ -7287,8 +7971,14 @@ if (process.argv[1] && /server\.(mjs|cjs|js|ts)$/.test(process.argv[1])) {
7287
7971
  main().catch(console.error);
7288
7972
  }
7289
7973
  export {
7974
+ RECOMMENDED_NEXT_CALL_ALLOWLIST,
7975
+ RETRIEVAL_READINESS_VALUES,
7976
+ RETRIEVAL_ROUTE_VALUES,
7977
+ WORKSPACE_HEALTH_VALUES,
7290
7978
  canonicalizeWorkspacePath,
7291
7979
  chooseWorkspaceProjectSource,
7980
+ classifyProjectRepoReadiness,
7981
+ classifyRepoGroundedQuery,
7292
7982
  classifyWorkspaceHealth,
7293
7983
  createMcpServer,
7294
7984
  createWhisperMcpClient,
@@ -7296,5 +7986,8 @@ export {
7296
7986
  extractSignature,
7297
7987
  renderScopedMcpConfig,
7298
7988
  resolveForgetQueryCandidates,
7299
- runSearchCodeSearch
7989
+ resolveWorkspaceIdentity,
7990
+ runSearchCodeSearch,
7991
+ sanitizeRecommendedNextCalls,
7992
+ shouldAbstainForCodebaseTrust
7300
7993
  };