gitnexus 1.6.11-rc.8 → 1.6.11-rc.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -204,7 +204,7 @@ Your AI agent gets **17 tools** (15 per-repo + 2 group) automatically:
204
204
  | `group_list` | List configured repository groups |
205
205
  | `group_sync` | Rebuild a group's Contract Registry and cross-repo links |
206
206
 
207
- > With one indexed repo, the `repo` param is optional. With multiple, specify which: `query({search_query: "auth", repo: "my-app"})`. Per-repo tools also take an optional `branch` for indexes pinned with `gitnexus analyze --branch`; omitting it queries the workspace index, which follows your checked-out working tree. `explain` and `pdg_query` need an index built with `gitnexus analyze --pdg`.
207
+ > Read-only tools can omit `repo` when one repo is indexed, an MCP default is configured, or the GitNexus process cwd is inside a registered path without crossing into an unindexed nested Git checkout. Otherwise—and for mutating tools with multiple indexed repos and no MCP default—specify it explicitly: `query({search_query: "auth", repo: "my-app"})`. Per-repo tools also take an optional `branch` for indexes pinned with `gitnexus analyze --branch`; omitting it queries the workspace index, which follows your checked-out working tree. `explain` and `pdg_query` need an index built with `gitnexus analyze --pdg`.
208
208
 
209
209
  ## MCP Resources
210
210
 
@@ -371,10 +371,26 @@ export declare class LocalBackend {
371
371
  * - If only 1 repo, use it
372
372
  * - If 0 or multiple without param, throw with helpful message
373
373
  *
374
- * On a miss, re-reads the registry once in case a new repo was indexed
375
- * while the MCP server was running.
374
+ * Re-reads the registry before an omitted implicit target or after an
375
+ * explicit miss, so long-running servers see newly indexed repositories.
376
376
  */
377
377
  resolveRepo(repoParam?: string, branch?: string): Promise<RepoHandle>;
378
+ /**
379
+ * Internal resolver variant for CLI/MCP tool routing and discovery.
380
+ * - If repoParam is given, match by name or path
381
+ * - If only 1 repo, use it
382
+ * - If multiple repos exist and repoParam is omitted, callers may opt in to
383
+ * the registered repo containing process.cwd()
384
+ * - If 0 repos exist, or cwd cannot disambiguate multiple repos, throw
385
+ *
386
+ * Omitted-repo resolution re-reads the registry before accepting any
387
+ * implicit target, including a cached singleton. A caller that just obtained
388
+ * a fresh registry snapshot may disable that refresh explicitly.
389
+ */
390
+ selectToolRepository(repoParam?: string, branch?: string, options?: {
391
+ allowCwdDefault?: boolean;
392
+ refreshRegistry?: boolean;
393
+ }): Promise<RepoHandle>;
378
394
  /**
379
395
  * Re-point a resolved repo handle at a specific branch index (#2106).
380
396
  *
@@ -409,12 +425,13 @@ export declare class LocalBackend {
409
425
  */
410
426
  private resolveRepoFromCache;
411
427
  /**
412
- * Prefer the indexed repo whose path matches the git root of process.cwd().
428
+ * Match process.cwd() against indexed repositories.
413
429
  *
414
- * In MCP stdio server mode, `process.cwd()` is the server's launch directory,
415
- * not the agent client's cwd. If the server was started from an unrelated
416
- * directory, `getGitRoot` returns null and duplicate-name resolution throws
417
- * {@link RegistryAmbiguousTargetError} — callers should pass an absolute path.
430
+ * Explicit duplicate aliases use exact Git-root matching only. Omitted
431
+ * read-only calls opt into deepest containing-path selection. In that mode a
432
+ * candidate must not sit above cwd's Git root, so an unindexed nested checkout
433
+ * cannot fall through to an indexed ancestor. The `.git` ancestor fallback
434
+ * preserves that boundary when the git executable is unavailable.
418
435
  */
419
436
  private pickRepoHandleForCwd;
420
437
  private handleToRegistryEntry;
@@ -24,7 +24,7 @@ import { isWalCorruptionError, WAL_RECOVERY_SUGGESTION } from '../../core/lbug/l
24
24
  // at MCP server startup — crashes on unsupported Node ABI versions (#89)
25
25
  // git utilities available if needed
26
26
  // import { isGitRepo, getCurrentCommit, getGitRoot } from '../../storage/git.js';
27
- import { parseDiffHunks, coalesceHunksByPath, hunksOverlapRange, getCanonicalRepoRoot, getGitRoot, } from '../../storage/git.js';
27
+ import { parseDiffHunks, coalesceHunksByPath, hunksOverlapRange, findGitRootByDotGit, getCanonicalRepoRoot, getGitRoot, } from '../../storage/git.js';
28
28
  import { realpathSync } from 'fs';
29
29
  import { listRegisteredRepos, cleanupOldKuzuFiles, canonicalizePath, getStoragePaths, loadMeta, RegistryAmbiguousTargetError, } from '../../storage/repo-manager.js';
30
30
  import { GroupService, } from '../../core/group/service.js';
@@ -1179,23 +1179,56 @@ export class LocalBackend {
1179
1179
  * - If only 1 repo, use it
1180
1180
  * - If 0 or multiple without param, throw with helpful message
1181
1181
  *
1182
- * On a miss, re-reads the registry once in case a new repo was indexed
1183
- * while the MCP server was running.
1182
+ * Re-reads the registry before an omitted implicit target or after an
1183
+ * explicit miss, so long-running servers see newly indexed repositories.
1184
1184
  */
1185
1185
  async resolveRepo(repoParam, branch) {
1186
- let refreshedAfterAmbiguity = false;
1186
+ return this.selectToolRepository(repoParam, branch);
1187
+ }
1188
+ /**
1189
+ * Internal resolver variant for CLI/MCP tool routing and discovery.
1190
+ * - If repoParam is given, match by name or path
1191
+ * - If only 1 repo, use it
1192
+ * - If multiple repos exist and repoParam is omitted, callers may opt in to
1193
+ * the registered repo containing process.cwd()
1194
+ * - If 0 repos exist, or cwd cannot disambiguate multiple repos, throw
1195
+ *
1196
+ * Omitted-repo resolution re-reads the registry before accepting any
1197
+ * implicit target, including a cached singleton. A caller that just obtained
1198
+ * a fresh registry snapshot may disable that refresh explicitly.
1199
+ */
1200
+ async selectToolRepository(repoParam, branch, options = {}) {
1201
+ const allowCwdDefault = options.allowCwdDefault === true;
1202
+ const mayRefresh = options.refreshRegistry !== false;
1203
+ let refreshed = false;
1204
+ // A cached singleton is also an implicit choice: another process may have
1205
+ // registered a second repo since init, which must not let a repo-less
1206
+ // mutating call bypass the multi-repo ambiguity guard.
1207
+ if (!repoParam && mayRefresh) {
1208
+ await this.refreshRepos();
1209
+ refreshed = true;
1210
+ }
1187
1211
  let result;
1188
1212
  try {
1189
- result = this.resolveRepoFromCache(repoParam);
1213
+ result = this.resolveRepoFromCache(repoParam, allowCwdDefault);
1190
1214
  }
1191
1215
  catch (err) {
1192
1216
  if (!(err instanceof RegistryAmbiguousTargetError))
1193
1217
  throw err;
1218
+ if (!mayRefresh || refreshed)
1219
+ throw err;
1194
1220
  // Stale in-memory duplicate siblings can linger after unregister; refresh
1195
1221
  // once before re-throwing so a resolved registry can disambiguate (#1658).
1196
1222
  await this.refreshRepos();
1197
- refreshedAfterAmbiguity = true;
1198
- result = this.resolveRepoFromCache(repoParam);
1223
+ refreshed = true;
1224
+ result = this.resolveRepoFromCache(repoParam, allowCwdDefault);
1225
+ }
1226
+ // Explicit misses retain the existing one-refresh retry. Omitted targets
1227
+ // already refreshed above unless a same-snapshot caller opted out.
1228
+ if (!result && mayRefresh && !refreshed) {
1229
+ await this.refreshRepos();
1230
+ refreshed = true;
1231
+ result = this.resolveRepoFromCache(repoParam, allowCwdDefault);
1199
1232
  }
1200
1233
  if (result) {
1201
1234
  // Issue: silent graph drift across sibling clones.
@@ -1209,15 +1242,6 @@ export class LocalBackend {
1209
1242
  });
1210
1243
  return this.applyBranchScope(result, branch);
1211
1244
  }
1212
- // Miss — refresh registry and try once more (skip if already refreshed above)
1213
- if (!refreshedAfterAmbiguity) {
1214
- await this.refreshRepos();
1215
- }
1216
- const retried = this.resolveRepoFromCache(repoParam);
1217
- if (retried) {
1218
- this.maybeWarnSiblingDrift(retried).catch(() => { });
1219
- return this.applyBranchScope(retried, branch);
1220
- }
1221
1245
  // Still no match — throw with helpful message
1222
1246
  if (this.repos.size === 0) {
1223
1247
  throw new Error('No indexed repositories. Run: gitnexus analyze');
@@ -1350,7 +1374,7 @@ export class LocalBackend {
1350
1374
  * Throws {@link RegistryAmbiguousTargetError} when `repoParam` matches
1351
1375
  * multiple handles by name and cwd cannot disambiguate (#1658).
1352
1376
  */
1353
- resolveRepoFromCache(repoParam) {
1377
+ resolveRepoFromCache(repoParam, allowCwdDefault = false) {
1354
1378
  if (this.repos.size === 0)
1355
1379
  return null;
1356
1380
  if (repoParam) {
@@ -1379,6 +1403,9 @@ export class LocalBackend {
1379
1403
  if (nameMatches.length === 1)
1380
1404
  return nameMatches[0];
1381
1405
  if (nameMatches.length > 1) {
1406
+ // Explicit duplicate aliases retain the legacy fail-closed contract:
1407
+ // only an exact cwd Git-root match may disambiguate them. Deepest path
1408
+ // containment is reserved for an omitted read-only repo (#3073).
1382
1409
  const cwdPick = this.pickRepoHandleForCwd(nameMatches);
1383
1410
  if (cwdPick)
1384
1411
  return cwdPick;
@@ -1403,26 +1430,46 @@ export class LocalBackend {
1403
1430
  if (this.repos.size === 1) {
1404
1431
  return this.repos.values().next().value;
1405
1432
  }
1433
+ if (allowCwdDefault) {
1434
+ const cwdPick = this.pickRepoHandleForCwd([...this.repos.values()], true);
1435
+ if (cwdPick)
1436
+ return cwdPick;
1437
+ }
1406
1438
  return null; // Multiple repos, no param — ambiguous
1407
1439
  }
1408
1440
  /**
1409
- * Prefer the indexed repo whose path matches the git root of process.cwd().
1441
+ * Match process.cwd() against indexed repositories.
1410
1442
  *
1411
- * In MCP stdio server mode, `process.cwd()` is the server's launch directory,
1412
- * not the agent client's cwd. If the server was started from an unrelated
1413
- * directory, `getGitRoot` returns null and duplicate-name resolution throws
1414
- * {@link RegistryAmbiguousTargetError} — callers should pass an absolute path.
1443
+ * Explicit duplicate aliases use exact Git-root matching only. Omitted
1444
+ * read-only calls opt into deepest containing-path selection. In that mode a
1445
+ * candidate must not sit above cwd's Git root, so an unindexed nested checkout
1446
+ * cannot fall through to an indexed ancestor. The `.git` ancestor fallback
1447
+ * preserves that boundary when the git executable is unavailable.
1415
1448
  */
1416
- pickRepoHandleForCwd(candidates) {
1417
- const cwdRoot = getGitRoot(process.cwd());
1418
- if (!cwdRoot)
1449
+ pickRepoHandleForCwd(candidates, allowContaining = false) {
1450
+ const cwd = process.cwd();
1451
+ const normalize = (value) => {
1452
+ const canonical = canonicalizePath(value);
1453
+ return process.platform === 'win32' ? canonical.toLowerCase() : canonical;
1454
+ };
1455
+ const isSameOrDescendant = (parent, child) => child === parent ||
1456
+ child.startsWith(parent.endsWith(path.sep) ? parent : `${parent}${path.sep}`);
1457
+ const canonicalCwd = normalize(cwd);
1458
+ const cwdRoot = getGitRoot(cwd) ?? findGitRootByDotGit(cwd);
1459
+ const canonicalRoot = cwdRoot ? normalize(cwdRoot) : null;
1460
+ if (allowContaining) {
1461
+ const containing = candidates
1462
+ .map((handle) => ({ handle, repoPath: normalize(handle.repoPath) }))
1463
+ .filter(({ repoPath }) => isSameOrDescendant(repoPath, canonicalCwd))
1464
+ .filter(({ repoPath }) => !canonicalRoot || isSameOrDescendant(canonicalRoot, repoPath))
1465
+ .sort((a, b) => b.repoPath.length - a.repoPath.length);
1466
+ if (containing.length > 0)
1467
+ return containing[0].handle;
1468
+ }
1469
+ if (!canonicalRoot)
1419
1470
  return null;
1420
- const canonicalCwd = canonicalizePath(cwdRoot);
1421
1471
  const cwdMatches = candidates.filter((handle) => {
1422
- const stored = canonicalizePath(handle.repoPath);
1423
- return process.platform === 'win32'
1424
- ? stored.toLowerCase() === canonicalCwd.toLowerCase()
1425
- : stored === canonicalCwd;
1472
+ return normalize(handle.repoPath) === canonicalRoot;
1426
1473
  });
1427
1474
  return cwdMatches.length === 1 ? cwdMatches[0] : null;
1428
1475
  }
@@ -1819,7 +1866,7 @@ export class LocalBackend {
1819
1866
  }
1820
1867
  // Resolve repo from optional param (re-reads registry on miss). An optional
1821
1868
  // `branch` param scopes the resolved handle to that branch's index (#2106).
1822
- const repo = await this.resolveRepo(p.repo, p.branch);
1869
+ const repo = await this.selectToolRepository(p.repo, p.branch, { allowCwdDefault: method !== 'rename' });
1823
1870
  switch (method) {
1824
1871
  case 'query':
1825
1872
  return this.withToolStaleness(repo, await this.query(repo, p));
@@ -20,10 +20,14 @@ export declare class McpRepositoryPolicy {
20
20
  private repoForArgs;
21
21
  private normalizeToolArgs;
22
22
  private listAllowedRepos;
23
- requiresExplicitRepo(backend: LocalBackend): Promise<boolean>;
23
+ toolSchemaRepoRequirements(backend: LocalBackend): Promise<{
24
+ readOnlyRequiresRepo: boolean;
25
+ mutatingRequiresRepo: boolean;
26
+ }>;
24
27
  private listReposPage;
25
28
  private callTool;
26
29
  private resolveRepo;
30
+ private selectToolRepository;
27
31
  assertResourceUri(uri: string): void;
28
32
  resourceTemplateAllowed(uriTemplate: string): boolean;
29
33
  toolAllowed(toolName: string): boolean;
@@ -134,10 +134,40 @@ export class McpRepositoryPolicy {
134
134
  };
135
135
  });
136
136
  }
137
- async requiresExplicitRepo(backend) {
138
- if (this.defaultRepo)
139
- return false;
140
- return (await this.listAllowedRepos(backend)).length > 1;
137
+ async toolSchemaRepoRequirements(backend) {
138
+ if (this.defaultRepo) {
139
+ return { readOnlyRequiresRepo: false, mutatingRequiresRepo: false };
140
+ }
141
+ // Runtime selection is based on the configured allowlist, not on which
142
+ // entries happen to remain visible in a later registry refresh. Keep the
143
+ // advertised schema aligned with repoForArgs() when that listing shrinks.
144
+ if (this.restricted) {
145
+ const requiresRepo = this.allowed.length > 1;
146
+ return {
147
+ readOnlyRequiresRepo: requiresRepo,
148
+ mutatingRequiresRepo: requiresRepo,
149
+ };
150
+ }
151
+ // One fresh listing supplies both schema decisions. Besides keeping the
152
+ // advertised contract internally consistent, this avoids doing two full
153
+ // per-repo staleness fan-outs for every tools/list request.
154
+ const visibleRepos = await this.listAllowedRepos(backend);
155
+ if (visibleRepos.length <= 1) {
156
+ return { readOnlyRequiresRepo: false, mutatingRequiresRepo: false };
157
+ }
158
+ try {
159
+ // listAllowedRepos() refreshed this backend immediately above. Resolve
160
+ // against that exact cache snapshot instead of racing another registry
161
+ // read; only read-only schemas may advertise the cwd-derived default.
162
+ await backend.selectToolRepository(undefined, undefined, {
163
+ allowCwdDefault: true,
164
+ refreshRegistry: false,
165
+ });
166
+ return { readOnlyRequiresRepo: false, mutatingRequiresRepo: true };
167
+ }
168
+ catch {
169
+ return { readOnlyRequiresRepo: true, mutatingRequiresRepo: true };
170
+ }
141
171
  }
142
172
  async listReposPage(backend, params) {
143
173
  const { limit, offset } = parseListReposPagination(params, {
@@ -186,6 +216,17 @@ export class McpRepositoryPolicy {
186
216
  const selected = this.repoForArgs(repo === undefined ? undefined : { repo });
187
217
  return backend.resolveRepo(selected?.path, branch);
188
218
  }
219
+ async selectToolRepository(backend, repo, branch, options) {
220
+ if (!this.configured)
221
+ return backend.selectToolRepository(repo, branch, options);
222
+ if (!this.restricted) {
223
+ return backend.selectToolRepository(repo ?? this.defaultRepo?.path, branch, options);
224
+ }
225
+ const selected = this.repoForArgs(repo === undefined ? undefined : { repo });
226
+ // Restricted policies never allow cwd to select outside the configured
227
+ // set; once policy supplies an explicit path, the public resolver is enough.
228
+ return backend.resolveRepo(selected?.path, branch);
229
+ }
189
230
  assertResourceUri(uri) {
190
231
  if (!this.restricted)
191
232
  return;
@@ -255,6 +296,9 @@ export class McpRepositoryPolicy {
255
296
  if (property === 'resolveRepo') {
256
297
  return (repo, branch) => policy.resolveRepo(target, repo, branch);
257
298
  }
299
+ if (property === 'selectToolRepository') {
300
+ return (repo, branch, options) => policy.selectToolRepository(target, repo, branch, options);
301
+ }
258
302
  if (property === 'getContext' && policy.restricted) {
259
303
  return (repoId) => {
260
304
  if (!repoId || !policy.uniqueAllowedContextNames.has(repoId.toLowerCase()))
@@ -254,7 +254,8 @@ async function getReposResource(backend) {
254
254
  }
255
255
  if (repos.length > 1) {
256
256
  lines.push('');
257
- lines.push('# Multiple repos indexed. Use repo parameter in tool calls:');
257
+ lines.push('# Multiple repos indexed. Read-only tools may omit repo when an MCP default is configured or GitNexus process.cwd() is inside one listed path without crossing an unindexed nested Git checkout.');
258
+ lines.push('# Otherwise—and for mutating tools without an MCP default—pass repo explicitly:');
258
259
  lines.push(`# query({search_query: "auth", repo: "${repos[0].name}"})`);
259
260
  }
260
261
  return lines.join('\n');
@@ -136,11 +136,11 @@ export function createMCPServer(backend, options = {}) {
136
136
  };
137
137
  }
138
138
  });
139
- // With multiple visible repositories and no process-wide default, make the
140
- // routing requirement machine-readable. Agents then supply `repo` before the
141
- // call instead of discovering the ambiguity through a failed tool response.
139
+ // Make the effective routing contract machine-readable. Read-only tools may
140
+ // use a cwd-derived default; mutating rename remains explicit unless policy
141
+ // supplies a single/default repository.
142
142
  server.setRequestHandler(ListToolsRequestSchema, async () => {
143
- const requireRepo = await repositoryPolicy.requiresExplicitRepo(backend);
143
+ const { readOnlyRequiresRepo, mutatingRequiresRepo } = await repositoryPolicy.toolSchemaRepoRequirements(backend);
144
144
  return {
145
145
  tools: GITNEXUS_TOOLS.filter((tool) => (!readOnly || MCP_READ_ONLY_TOOLS.has(tool.name)) &&
146
146
  repositoryPolicy.toolAllowed(tool.name))
@@ -148,7 +148,8 @@ export function createMCPServer(backend, options = {}) {
148
148
  .map((tool) => ({
149
149
  name: tool.name,
150
150
  description: tool.description,
151
- inputSchema: requireRepo && REPO_SCOPED_TOOLS.has(tool.name)
151
+ inputSchema: (tool.name === 'rename' ? mutatingRequiresRepo : readOnlyRequiresRepo) &&
152
+ REPO_SCOPED_TOOLS.has(tool.name)
152
153
  ? {
153
154
  ...tool.inputSchema,
154
155
  required: [...new Set([...tool.inputSchema.required, 'repo'])],
package/dist/mcp/tools.js CHANGED
@@ -49,6 +49,8 @@ export const PDG_QUERY_MAX_LIMIT = 200;
49
49
  // Shared impact traversal depth cap. The MCP schema advertises this bound;
50
50
  // PDG direct backend callers also enforce it before running traversal.
51
51
  export const IMPACT_MAX_DEPTH = 32;
52
+ const CWD_AWARE_REPO_OMISSION = 'Omit when only one repo is indexed, an MCP default is configured, or the GitNexus process cwd is inside a registered path without crossing an unindexed nested Git checkout; otherwise specify it explicitly.';
53
+ const MUTATING_REPO_OMISSION = 'Omit only when one repo is indexed or an MCP default is configured; otherwise mutating tools require an explicit repo.';
52
54
  export const GITNEXUS_TOOLS = [
53
55
  {
54
56
  name: 'list_repos',
@@ -61,8 +63,10 @@ PAGINATION: Results are paginated so a large registry is not truncated by MCP/LL
61
63
  WHEN TO USE: First step when multiple repos are indexed, or to discover available repos.
62
64
  AFTER THIS: READ gitnexus://repo/{name}/context for the repo you want to work with.
63
65
 
64
- When multiple repos are indexed, you MUST specify the "repo" parameter
65
- on other tools (query, context, impact, etc.) to target the correct one.`,
66
+ When multiple repos are indexed, repo-scoped read-only tools use the configured
67
+ MCP default or the registered path containing the GitNexus process cwd, unless
68
+ cwd has crossed into an unindexed nested Git checkout. If neither applies,
69
+ specify the "repo" parameter explicitly.`,
66
70
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
67
71
  inputSchema: {
68
72
  type: 'object',
@@ -148,7 +152,7 @@ SERVICE: optional monorepo path prefix (POSIX-style, case-sensitive segments). W
148
152
  },
149
153
  repo: {
150
154
  type: 'string',
151
- description: 'Indexed repository name or path, or group mode "@<groupName>" / "@<groupName>/<memberPath>" (member path keys from group.yaml). Omit when only one indexed repo exists.',
155
+ description: `Indexed repository name or path, or group mode "@<groupName>" / "@<groupName>/<memberPath>" (member path keys from group.yaml). ${CWD_AWARE_REPO_OMISSION}`,
152
156
  },
153
157
  service: {
154
158
  type: 'string',
@@ -227,7 +231,7 @@ TIPS:
227
231
  },
228
232
  repo: {
229
233
  type: 'string',
230
- description: 'Repository name or path. Omit if only one repo is indexed.',
234
+ description: `Repository name or path. ${CWD_AWARE_REPO_OMISSION}`,
231
235
  },
232
236
  },
233
237
  required: ['statement'],
@@ -290,7 +294,7 @@ SERVICE: optional monorepo path prefix (case-sensitive path segments). When "rep
290
294
  },
291
295
  repo: {
292
296
  type: 'string',
293
- description: 'Indexed repository name or path, or group mode "@<groupName>" / "@<groupName>/<memberPath>". Omit if only one repo is indexed.',
297
+ description: `Indexed repository name or path, or group mode "@<groupName>" / "@<groupName>/<memberPath>". ${CWD_AWARE_REPO_OMISSION}`,
294
298
  },
295
299
  service: {
296
300
  type: 'string',
@@ -334,7 +338,7 @@ Returns: changed symbols, affected processes, and a risk summary.
334
338
  },
335
339
  repo: {
336
340
  type: 'string',
337
- description: 'Repository name or path. Omit if only one repo is indexed.',
341
+ description: `Repository name or path. ${CWD_AWARE_REPO_OMISSION}`,
338
342
  },
339
343
  },
340
344
  required: [],
@@ -373,7 +377,7 @@ A graph too large to analyze at all returns \`{ error, truncated: true }\` with
373
377
  },
374
378
  repo: {
375
379
  type: 'string',
376
- description: 'Repository name or path. Omit if only one repo is indexed.',
380
+ description: `Repository name or path. ${CWD_AWARE_REPO_OMISSION}`,
377
381
  },
378
382
  },
379
383
  required: [],
@@ -410,7 +414,7 @@ Handles disambiguation via context()'s payload verbatim: an ambiguous symbol_nam
410
414
  },
411
415
  repo: {
412
416
  type: 'string',
413
- description: 'Repository name or path. Omit if only one repo is indexed.',
417
+ description: `Repository name or path. ${MUTATING_REPO_OMISSION}`,
414
418
  },
415
419
  },
416
420
  required: ['new_name'],
@@ -542,7 +546,7 @@ SERVICE: optional monorepo path prefix (case-sensitive path segments). When "rep
542
546
  },
543
547
  repo: {
544
548
  type: 'string',
545
- description: 'Indexed repository name or path, or group mode "@<groupName>" / "@<groupName>/<memberPath>". Omit if only one repo is indexed.',
549
+ description: `Indexed repository name or path, or group mode "@<groupName>" / "@<groupName>/<memberPath>". ${CWD_AWARE_REPO_OMISSION}`,
546
550
  },
547
551
  service: {
548
552
  type: 'string',
@@ -630,7 +634,7 @@ Findings are deliberately NOT part of impact()'s traversal or the web schema —
630
634
  },
631
635
  repo: {
632
636
  type: 'string',
633
- description: 'Repository name or path. Omit if only one repo is indexed.',
637
+ description: `Repository name or path. ${CWD_AWARE_REPO_OMISSION}`,
634
638
  },
635
639
  },
636
640
  required: [],
@@ -679,7 +683,7 @@ CONTRACT CAVEATS:
679
683
  },
680
684
  repo: {
681
685
  type: 'string',
682
- description: 'Repository name or path. Omit if only one repo is indexed.',
686
+ description: `Repository name or path. ${CWD_AWARE_REPO_OMISSION}`,
683
687
  },
684
688
  },
685
689
  required: ['mode', 'target'],
@@ -703,7 +707,7 @@ Returns: route nodes with their handlers, middleware wrapper chains (e.g., withA
703
707
  },
704
708
  repo: {
705
709
  type: 'string',
706
- description: 'Repository name or path. Omit if only one repo is indexed.',
710
+ description: `Repository name or path. ${CWD_AWARE_REPO_OMISSION}`,
707
711
  },
708
712
  },
709
713
  required: [],
@@ -721,7 +725,10 @@ Returns: tool nodes with their handler files and descriptions.`,
721
725
  type: 'object',
722
726
  properties: {
723
727
  tool: { type: 'string', description: 'Filter by tool name. Omit for all tools.' },
724
- repo: { type: 'string', description: 'Repository name or path.' },
728
+ repo: {
729
+ type: 'string',
730
+ description: `Repository name or path. ${CWD_AWARE_REPO_OMISSION}`,
731
+ },
725
732
  },
726
733
  required: [],
727
734
  },
@@ -744,7 +751,7 @@ Returns routes that have both detected response keys AND consumers. Shows top-le
744
751
  },
745
752
  repo: {
746
753
  type: 'string',
747
- description: 'Repository name or path. Omit if only one repo is indexed.',
754
+ description: `Repository name or path. ${CWD_AWARE_REPO_OMISSION}`,
748
755
  },
749
756
  },
750
757
  required: [],
@@ -769,7 +776,10 @@ Response shape is keyed on how many routes match, not on the data: exactly one m
769
776
  type: 'string',
770
777
  description: 'Optional HTTP verb — GET, POST, PUT, PATCH, DELETE, etc. — to narrow a multi-verb route or file lookup to a single method. Returns an error if no matched route uses that verb.',
771
778
  },
772
- repo: { type: 'string', description: 'Repository name or path.' },
779
+ repo: {
780
+ type: 'string',
781
+ description: `Repository name or path. ${CWD_AWARE_REPO_OMISSION}`,
782
+ },
773
783
  },
774
784
  required: [],
775
785
  },
@@ -879,7 +889,7 @@ DESTINATION TRACE (cross-repo): for an "@groupName" trace, OMIT to/to_uid/to_fil
879
889
  },
880
890
  repo: {
881
891
  type: 'string',
882
- description: 'Repository name or path, or "@groupName" / "@groupName/memberPath" for a cross-repo trace over a group. Omit if only one repo is indexed.',
892
+ description: `Repository name or path, or "@groupName" / "@groupName/memberPath" for a cross-repo trace over a group. ${CWD_AWARE_REPO_OMISSION}`,
883
893
  },
884
894
  },
885
895
  required: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.11-rc.8",
3
+ "version": "1.6.11-rc.9",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -114,6 +114,9 @@ const PLATFORM_LOGIC = [
114
114
  // POSIX and Windows — the fail-closed path-claim semantics must hold on the
115
115
  // real windows-latest path implementation (#2419/#2420).
116
116
  'test/unit/server-api-repo-resolution.test.ts',
117
+ // #3073: cwd-based repository selection canonicalizes real paths, compares
118
+ // platform separators/case, and rejects nested Git-boundary fallthrough.
119
+ 'test/unit/calltool-dispatch.test.ts',
117
120
  // The index write-lock (#2658) selects its backend by process.platform — the
118
121
  // OS socket lock (Windows named pipe / Linux abstract socket) vs the file
119
122
  // fallback — and its socket-backend describe block is gated to linux/win32.