gitnexus 1.6.8-rc.3 → 1.6.8-rc.5

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
@@ -436,6 +436,26 @@ After scope resolution, analyze prunes inert block-local value symbols (a functi
436
436
 
437
437
  Programmatic callers can pass `keepLocalValueSymbols: true` in `PipelineOptions` instead of setting the env var.
438
438
 
439
+ ### Hook augmentation/notifications are silently skipped
440
+
441
+ The Claude Code / Antigravity hooks intentionally stay **silent** on normal skip
442
+ paths so strict hook runners (e.g. Codex `PreToolUse`) never see unexpected
443
+ output. A search may not be augmented — or a stale-index reminder may not appear
444
+ on stderr — when the GitNexus MCP server owns the repo DB, when the DB-lock probe
445
+ times out and fails closed, or when the index is already current.
446
+
447
+ To see why a hook skipped, set `GITNEXUS_DEBUG=1` and re-run the action — the hook
448
+ writes the reason (e.g. `[GitNexus] augment skipped: MCP server owns DB`) and the
449
+ stale-index hint to its stderr:
450
+
451
+ ```bash
452
+ GITNEXUS_DEBUG=1 <your command> # surfaces hook skip/diagnostic reasons on stderr
453
+ ```
454
+
455
+ Only `GITNEXUS_DEBUG=1` and `GITNEXUS_DEBUG=true` enable diagnostics; every other
456
+ value (including `0` and `false`) is treated as off. Diagnostics go to stderr
457
+ only — the hook's structured stdout (the JSON the agent consumes) is unaffected.
458
+
439
459
  ## Privacy
440
460
 
441
461
  - All processing happens locally on your machine
@@ -1,31 +1,6 @@
1
1
  import Java from 'tree-sitter-java';
2
2
  import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
3
- /**
4
- * Java HTTP plugin. Handles:
5
- * - Spring `@RequestMapping` class prefixes + `@(Get|Post|...)Mapping` method annotations
6
- * - Spring `RestTemplate.getForObject/...`, `exchange(...)`
7
- * - Spring `WebClient.method(HttpMethod.X, ...)`, `WebClient.get().uri(...)`
8
- * - OkHttp `new Request.Builder().url("...")`
9
- * - OpenFeign interfaces with Spring MVC method annotations or
10
- * native `@RequestLine("METHOD /path")` annotations
11
- * - Java / Apache HttpClient literal request construction
12
- *
13
- * Every route-defining annotation (class/interface `@RequestMapping`
14
- * prefixes, `@FeignClient(path)` prefixes, `@(Get|...)Mapping` method
15
- * routes and native `@RequestLine`s) is matched by a single consolidated
16
- * query (`JAVA_ROUTE_ANNOTATION_PATTERNS`) in one pass via
17
- * `scanRouteAnnotations`. The `scan` function then walks up from each
18
- * matched method to its enclosing class/interface to combine the prefix
19
- * with the method path. Call-site consumers (RestTemplate, WebClient,
20
- * OkHttp, Java/Apache HttpClient) keep their own focused queries.
21
- */
22
- const METHOD_ANNOTATION_TO_HTTP = {
23
- GetMapping: 'GET',
24
- PostMapping: 'POST',
25
- PutMapping: 'PUT',
26
- DeleteMapping: 'DELETE',
27
- PatchMapping: 'PATCH',
28
- };
3
+ import { METHOD_ANNOTATION_TO_HTTP, isRouteMemberKey, findEnclosingClass, } from '../../../ingestion/route-extractors/spring-shared.js';
29
4
  // ─── Route-defining annotations (one generic query, one pass) ─────────
30
5
  // Every Java route-mapper annotation shares one shape: an annotation carrying a
31
6
  // single string argument — positional `"..."` or named `key = "..."` — on a
@@ -300,19 +275,9 @@ const APACHE_HTTP_CLIENT_PATTERNS = compilePatterns({
300
275
  ],
301
276
  });
302
277
  /**
303
- * Find the nearest enclosing class/interface declaration ancestor for
304
- * a node, or null if the node is top-level. Tree-sitter's
305
- * SyntaxNode.parent walks one level at a time.
278
+ * Find the nearest enclosing interface declaration ancestor for a node, or
279
+ * null if the node is top-level.
306
280
  */
307
- function findEnclosingClass(node) {
308
- let cur = node.parent;
309
- while (cur) {
310
- if (cur.type === 'class_declaration')
311
- return cur;
312
- cur = cur.parent;
313
- }
314
- return null;
315
- }
316
281
  function findEnclosingInterface(node) {
317
282
  let cur = node.parent;
318
283
  while (cur) {
@@ -369,18 +334,6 @@ function hasAnnotation(node, names) {
369
334
  }
370
335
  return false;
371
336
  }
372
- /**
373
- * A named annotation argument contributes a route only when its member key is
374
- * `path` or `value`; a positional argument (no key node) always qualifies.
375
- * This is the JS-side replacement for the in-query `^(path|value)$` filter and
376
- * drops Spring's non-route string attributes (`produces`, `consumes`,
377
- * `headers`, `name`, `params`) that would otherwise be mis-read as routes.
378
- */
379
- function isRouteMemberKey(keyNode) {
380
- if (!keyNode)
381
- return true;
382
- return keyNode.text === 'path' || keyNode.text === 'value';
383
- }
384
337
  /**
385
338
  * Resolve every Java route-defining annotation in a single tree-sitter pass.
386
339
  *
@@ -20,6 +20,8 @@ import type { VariableExtractor } from './variable-types.js';
20
20
  import type { ImportResolverFn } from './import-resolvers/types.js';
21
21
  import type { SyntaxNode } from './utils/ast-helpers.js';
22
22
  import type { NodeLabel } from '../../_shared/index.js';
23
+ import type Parser from 'tree-sitter';
24
+ import type { ExtractedDecoratorRoute } from './workers/parse-worker.js';
23
25
  /** Tree-sitter query captures: capture name → AST node (or undefined if not captured). */
24
26
  export type CaptureMap = Record<string, SyntaxNode | undefined>;
25
27
  /** Configuration for AST-based framework detection patterns. */
@@ -185,6 +187,17 @@ interface LanguageProviderConfig {
185
187
  * When true, the worker extracts routes via the language's route extraction logic.
186
188
  * Default: undefined (no route files). */
187
189
  readonly isRouteFile?: (filePath: string) => boolean;
190
+ /**
191
+ * Extract decorator-style route annotations from a parsed file.
192
+ *
193
+ * When defined, the parse worker calls this after per-file capture processing
194
+ * to extract framework route definitions that require AST-level analysis beyond
195
+ * generic `@decorator` captures (e.g., Java Spring class-level prefix joining,
196
+ * multi-class handling). The returned routes are appended to `decoratorRoutes`.
197
+ *
198
+ * Default: undefined (no language-specific decorator route extraction).
199
+ */
200
+ readonly extractDecoratorRoutes?: (tree: Parser.Tree, filePath: string, lineOffset: number) => ExtractedDecoratorRoute[];
188
201
  /** Built-in/stdlib names that should be filtered from the call graph for this language.
189
202
  * Default: undefined (no language-specific filtering). */
190
203
  readonly builtInNames?: ReadonlySet<string>;
@@ -11,6 +11,7 @@ import { createClassExtractor } from '../class-extractors/generic.js';
11
11
  import { javaClassConfig } from '../class-extractors/configs/jvm.js';
12
12
  import { defineLanguage } from '../language-provider.js';
13
13
  import { javaTypeConfig } from '../type-extractors/jvm.js';
14
+ import { extractSpringRoutes } from '../route-extractors/spring.js';
14
15
  import { javaExportChecker } from '../export-detection.js';
15
16
  import { createImportResolver } from '../import-resolvers/resolver-factory.js';
16
17
  import { javaImportConfig } from '../import-resolvers/configs/jvm.js';
@@ -103,4 +104,6 @@ export const javaProvider = defineLanguage({
103
104
  arityCompatibility: javaArityCompatibility,
104
105
  resolveImportTarget: resolveJavaImportTarget,
105
106
  orderSameNameTypeCandidates: orderJavaSameNameTypeCandidates,
107
+ // ── Route extraction ──
108
+ extractDecoratorRoutes: extractSpringRoutes,
106
109
  });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Shared Spring route-annotation primitives.
3
+ *
4
+ * These are the low-level building blocks the two Spring route extractors —
5
+ * the ingestion-layer `route-extractors/spring.ts` (produces graph `Route`
6
+ * nodes) and the group-layer `group/extractors/http-patterns/java.ts`
7
+ * (produces cross-repo HTTP contracts) — would otherwise each maintain
8
+ * independently. Centralising the annotation→method map, the enclosing-class
9
+ * lookup, and the route-key filter keeps those semantics in one place so the
10
+ * two extractors can't drift apart.
11
+ *
12
+ * This module lives in `ingestion/` (the lower layer); the group layer imports
13
+ * from it, matching the existing `group → ingestion` dependency direction
14
+ * (e.g. `group/extractors/include-extractor.ts` already imports
15
+ * `ingestion/import-resolvers/utils.ts`). It MUST NOT import anything from
16
+ * `group/` to avoid a dependency cycle.
17
+ */
18
+ import type Parser from 'tree-sitter';
19
+ /**
20
+ * Spring shortcut method-annotation → HTTP verb.
21
+ *
22
+ * `@RequestMapping` is intentionally absent: on a method it carries no implicit
23
+ * verb (the verb lives in its `method = RequestMethod.X` attribute), and on a
24
+ * class it is a URL prefix rather than a route. Callers handle `@RequestMapping`
25
+ * separately.
26
+ */
27
+ export declare const METHOD_ANNOTATION_TO_HTTP: Record<string, string>;
28
+ /**
29
+ * A named annotation argument contributes a route only when its member key is
30
+ * `path` or `value`; a positional argument (no key node) always qualifies.
31
+ * Drops Spring's non-route string attributes (`produces`, `consumes`,
32
+ * `headers`, `name`, `params`) that would otherwise be mis-read as routes.
33
+ */
34
+ export declare function isRouteMemberKey(keyNode: Parser.SyntaxNode | undefined): boolean;
35
+ /**
36
+ * Find the nearest enclosing `class_declaration` ancestor for a node, or null
37
+ * if the node is top-level. Tree-sitter's `SyntaxNode.parent` walks one level
38
+ * at a time.
39
+ */
40
+ export declare function findEnclosingClass(node: Parser.SyntaxNode): Parser.SyntaxNode | null;
41
+ /**
42
+ * Strip enclosing quotes from a tree-sitter string-literal node's text.
43
+ * Handles single / double / template (backtick) quotes and triple-quoted
44
+ * strings. Mirrors the safer semantics of the group layer's `unquoteLiteral`:
45
+ * returns `null` for empty / nullish input so callers can uniformly skip
46
+ * captures whose value is missing, and returns the text unchanged when it
47
+ * carries no recognisable surrounding quotes (some grammars expose string
48
+ * content without quotes already).
49
+ */
50
+ export declare function unquoteSpringLiteral(raw: string): string | null;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Shared Spring route-annotation primitives.
3
+ *
4
+ * These are the low-level building blocks the two Spring route extractors —
5
+ * the ingestion-layer `route-extractors/spring.ts` (produces graph `Route`
6
+ * nodes) and the group-layer `group/extractors/http-patterns/java.ts`
7
+ * (produces cross-repo HTTP contracts) — would otherwise each maintain
8
+ * independently. Centralising the annotation→method map, the enclosing-class
9
+ * lookup, and the route-key filter keeps those semantics in one place so the
10
+ * two extractors can't drift apart.
11
+ *
12
+ * This module lives in `ingestion/` (the lower layer); the group layer imports
13
+ * from it, matching the existing `group → ingestion` dependency direction
14
+ * (e.g. `group/extractors/include-extractor.ts` already imports
15
+ * `ingestion/import-resolvers/utils.ts`). It MUST NOT import anything from
16
+ * `group/` to avoid a dependency cycle.
17
+ */
18
+ /**
19
+ * Spring shortcut method-annotation → HTTP verb.
20
+ *
21
+ * `@RequestMapping` is intentionally absent: on a method it carries no implicit
22
+ * verb (the verb lives in its `method = RequestMethod.X` attribute), and on a
23
+ * class it is a URL prefix rather than a route. Callers handle `@RequestMapping`
24
+ * separately.
25
+ */
26
+ export const METHOD_ANNOTATION_TO_HTTP = {
27
+ GetMapping: 'GET',
28
+ PostMapping: 'POST',
29
+ PutMapping: 'PUT',
30
+ DeleteMapping: 'DELETE',
31
+ PatchMapping: 'PATCH',
32
+ };
33
+ /**
34
+ * A named annotation argument contributes a route only when its member key is
35
+ * `path` or `value`; a positional argument (no key node) always qualifies.
36
+ * Drops Spring's non-route string attributes (`produces`, `consumes`,
37
+ * `headers`, `name`, `params`) that would otherwise be mis-read as routes.
38
+ */
39
+ export function isRouteMemberKey(keyNode) {
40
+ if (!keyNode)
41
+ return true;
42
+ return keyNode.text === 'path' || keyNode.text === 'value';
43
+ }
44
+ /**
45
+ * Find the nearest enclosing `class_declaration` ancestor for a node, or null
46
+ * if the node is top-level. Tree-sitter's `SyntaxNode.parent` walks one level
47
+ * at a time.
48
+ */
49
+ export function findEnclosingClass(node) {
50
+ let cur = node.parent;
51
+ while (cur) {
52
+ if (cur.type === 'class_declaration')
53
+ return cur;
54
+ cur = cur.parent;
55
+ }
56
+ return null;
57
+ }
58
+ /**
59
+ * Strip enclosing quotes from a tree-sitter string-literal node's text.
60
+ * Handles single / double / template (backtick) quotes and triple-quoted
61
+ * strings. Mirrors the safer semantics of the group layer's `unquoteLiteral`:
62
+ * returns `null` for empty / nullish input so callers can uniformly skip
63
+ * captures whose value is missing, and returns the text unchanged when it
64
+ * carries no recognisable surrounding quotes (some grammars expose string
65
+ * content without quotes already).
66
+ */
67
+ export function unquoteSpringLiteral(raw) {
68
+ if (!raw)
69
+ return null;
70
+ if ((raw.startsWith('"""') && raw.endsWith('"""')) ||
71
+ (raw.startsWith("'''") && raw.endsWith("'''"))) {
72
+ return raw.slice(3, -3);
73
+ }
74
+ const first = raw[0];
75
+ const last = raw[raw.length - 1];
76
+ if ((first === '"' || first === "'" || first === '`') && last === first && raw.length >= 2) {
77
+ return raw.slice(1, -1);
78
+ }
79
+ return raw;
80
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Spring route annotation extractor for the ingestion pipeline.
3
+ *
4
+ * Extracts `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`,
5
+ * `@PatchMapping`, and `@RequestMapping` annotations from Java source files
6
+ * and returns `ExtractedDecoratorRoute[]` with class-level `@RequestMapping`
7
+ * prefixes already resolved per-class.
8
+ *
9
+ * This module is the ingestion-layer counterpart of
10
+ * `group/extractors/http-patterns/java.ts` (which extracts HTTP contracts
11
+ * for cross-repo matching). It uses the same tree-sitter capture approach:
12
+ * a single predicate-free query matches all route annotations generically,
13
+ * then a for-loop discriminates class-level prefixes from method-level routes
14
+ * by reading `@node.type` and the annotation name.
15
+ *
16
+ * The query is predicate-free to avoid the tree-sitter 0.21.x hazard where
17
+ * `#match?` / `#eq?` predicates in a top-level `[...]` alternation silently
18
+ * drop sibling-branch matches (see group-layer `JAVA_ROUTE_ANNOTATION_PATTERNS`
19
+ * header comment for details).
20
+ */
21
+ import Parser from 'tree-sitter';
22
+ import type { ExtractedDecoratorRoute } from '../workers/parse-worker.js';
23
+ /**
24
+ * Extract Spring route annotations from a parsed Java file.
25
+ *
26
+ * Uses a single tree-sitter query pass to capture all annotations, then
27
+ * discriminates class-level prefixes from method-level routes in a loop.
28
+ * Handles multiple classes per file, each with its own prefix.
29
+ *
30
+ * @param tree - tree-sitter parse tree
31
+ * @param filePath - relative file path (for `ExtractedDecoratorRoute.filePath`)
32
+ * @param lineOffset - line offset for pre-processing (usually 0)
33
+ * @returns Decorator routes with prefix already set per-class
34
+ */
35
+ export declare function extractSpringRoutes(tree: Parser.Tree, filePath: string, lineOffset?: number): ExtractedDecoratorRoute[];
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Spring route annotation extractor for the ingestion pipeline.
3
+ *
4
+ * Extracts `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`,
5
+ * `@PatchMapping`, and `@RequestMapping` annotations from Java source files
6
+ * and returns `ExtractedDecoratorRoute[]` with class-level `@RequestMapping`
7
+ * prefixes already resolved per-class.
8
+ *
9
+ * This module is the ingestion-layer counterpart of
10
+ * `group/extractors/http-patterns/java.ts` (which extracts HTTP contracts
11
+ * for cross-repo matching). It uses the same tree-sitter capture approach:
12
+ * a single predicate-free query matches all route annotations generically,
13
+ * then a for-loop discriminates class-level prefixes from method-level routes
14
+ * by reading `@node.type` and the annotation name.
15
+ *
16
+ * The query is predicate-free to avoid the tree-sitter 0.21.x hazard where
17
+ * `#match?` / `#eq?` predicates in a top-level `[...]` alternation silently
18
+ * drop sibling-branch matches (see group-layer `JAVA_ROUTE_ANNOTATION_PATTERNS`
19
+ * header comment for details).
20
+ */
21
+ import Parser from 'tree-sitter';
22
+ import Java from 'tree-sitter-java';
23
+ import { METHOD_ANNOTATION_TO_HTTP, isRouteMemberKey, findEnclosingClass, unquoteSpringLiteral, } from './spring-shared.js';
24
+ /**
25
+ * Single predicate-free tree-sitter query that captures all route annotations
26
+ * on classes and methods. Discrimination by annotation name and node type
27
+ * happens in the loop below.
28
+ *
29
+ * Captures:
30
+ * @ann → annotation name identifier (RequestMapping, GetMapping, etc.)
31
+ * @node → enclosing declaration (class_declaration | method_declaration)
32
+ * @value → the string-literal argument
33
+ * @key → the named-argument member key (absent for positional form)
34
+ */
35
+ const ROUTE_ANNOTATION_QUERY = new Parser.Query(Java, `
36
+ [
37
+ (class_declaration
38
+ (modifiers
39
+ (annotation
40
+ name: (identifier) @ann
41
+ arguments: (annotation_argument_list (string_literal) @value)))) @node
42
+ (class_declaration
43
+ (modifiers
44
+ (annotation
45
+ name: (identifier) @ann
46
+ arguments: (annotation_argument_list
47
+ (element_value_pair
48
+ key: (identifier) @key
49
+ value: (string_literal) @value))))) @node
50
+ (method_declaration
51
+ (modifiers
52
+ (annotation
53
+ name: (identifier) @ann
54
+ arguments: (annotation_argument_list (string_literal) @value)))) @node
55
+ (method_declaration
56
+ (modifiers
57
+ (annotation
58
+ name: (identifier) @ann
59
+ arguments: (annotation_argument_list
60
+ (element_value_pair
61
+ key: (identifier) @key
62
+ value: (string_literal) @value))))) @node
63
+ ]
64
+ `);
65
+ /**
66
+ * Extract Spring route annotations from a parsed Java file.
67
+ *
68
+ * Uses a single tree-sitter query pass to capture all annotations, then
69
+ * discriminates class-level prefixes from method-level routes in a loop.
70
+ * Handles multiple classes per file, each with its own prefix.
71
+ *
72
+ * @param tree - tree-sitter parse tree
73
+ * @param filePath - relative file path (for `ExtractedDecoratorRoute.filePath`)
74
+ * @param lineOffset - line offset for pre-processing (usually 0)
75
+ * @returns Decorator routes with prefix already set per-class
76
+ */
77
+ export function extractSpringRoutes(tree, filePath, lineOffset = 0) {
78
+ const matches = ROUTE_ANNOTATION_QUERY.matches(tree.rootNode);
79
+ // Phase 1: collect class-level @RequestMapping prefixes keyed by node id
80
+ const prefixByClassId = new Map();
81
+ for (const match of matches) {
82
+ const caps = {};
83
+ for (const { name, node } of match.captures) {
84
+ caps[name] = node;
85
+ }
86
+ const annNode = caps['ann'];
87
+ const node = caps['node'];
88
+ const valueNode = caps['value'];
89
+ const keyNode = caps['key'];
90
+ if (!annNode || !node || !valueNode)
91
+ continue;
92
+ if (node.type === 'class_declaration' && annNode.text === 'RequestMapping') {
93
+ if (!isRouteMemberKey(keyNode))
94
+ continue;
95
+ const prefix = unquoteSpringLiteral(valueNode.text);
96
+ if (prefix !== null)
97
+ prefixByClassId.set(node.id, prefix);
98
+ }
99
+ }
100
+ // Phase 2: collect method-level routes and resolve their class prefix
101
+ const routes = [];
102
+ for (const match of matches) {
103
+ const caps = {};
104
+ for (const { name, node } of match.captures) {
105
+ caps[name] = node;
106
+ }
107
+ const annNode = caps['ann'];
108
+ const node = caps['node'];
109
+ const valueNode = caps['value'];
110
+ const keyNode = caps['key'];
111
+ if (!annNode || !node || !valueNode)
112
+ continue;
113
+ if (node.type !== 'method_declaration')
114
+ continue;
115
+ const ann = annNode.text;
116
+ const httpMethod = METHOD_ANNOTATION_TO_HTTP[ann];
117
+ if (!httpMethod)
118
+ continue; // skip @RequestMapping on methods (ambiguous verb)
119
+ if (!isRouteMemberKey(keyNode))
120
+ continue;
121
+ const routePath = unquoteSpringLiteral(valueNode.text);
122
+ if (routePath === null)
123
+ continue;
124
+ const enclosingClass = findEnclosingClass(node);
125
+ const classPrefix = enclosingClass ? (prefixByClassId.get(enclosingClass.id) ?? '') : '';
126
+ routes.push({
127
+ filePath,
128
+ routePath,
129
+ httpMethod,
130
+ decoratorName: ann,
131
+ lineNumber: annNode.startPosition.row + lineOffset,
132
+ ...(classPrefix ? { prefix: classPrefix } : {}),
133
+ });
134
+ }
135
+ return routes;
136
+ }
@@ -678,6 +678,7 @@ const ROUTE_DECORATOR_NAMES = new Set([
678
678
  'PostMapping',
679
679
  'PutMapping',
680
680
  'DeleteMapping',
681
+ 'PatchMapping',
681
682
  ]);
682
683
  // ============================================================================
683
684
  // ORM Query Detection (Prisma + Supabase)
@@ -1743,6 +1744,15 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1743
1744
  if (language === SupportedLanguages.Python) {
1744
1745
  extractFastAPIRouterBindings(file.path, parseContent, result.routerIncludes, result.routerImports, (result.routerModuleAliases ??= []));
1745
1746
  }
1747
+ // Language-specific decorator route extraction via provider hook.
1748
+ // The provider's extractDecoratorRoutes walks the AST for framework-specific
1749
+ // route patterns (e.g., Java Spring class-level prefix joining). Routes are
1750
+ // appended to decoratorRoutes for the routes phase to emit as Route nodes.
1751
+ if (provider.extractDecoratorRoutes) {
1752
+ const frameworkRoutes = provider.extractDecoratorRoutes(tree, file.path, lineOffset);
1753
+ for (const r of frameworkRoutes)
1754
+ result.decoratorRoutes.push(r);
1755
+ }
1746
1756
  // Vue: emit CALLS edges for components used in <template>
1747
1757
  if (language === SupportedLanguages.Vue) {
1748
1758
  const templateComponents = extractTemplateComponents(file.content);
@@ -91,10 +91,20 @@ function hasGitNexusServerOwner(gitNexusDir) {
91
91
  return hasGitNexusDbLockedByGitNexusServer(path.join(gitNexusDir, 'lbug'), process.pid);
92
92
  }
93
93
 
94
+ /**
95
+ * Whether opt-in diagnostics should be written to the hook's stderr. Strict
96
+ * hook runners validate hook output, so normal, non-error skip paths must stay
97
+ * silent unless the operator explicitly asks for diagnostics via GITNEXUS_DEBUG.
98
+ * See issue #1913.
99
+ */
100
+ function isDebugEnabled() {
101
+ return process.env.GITNEXUS_DEBUG === '1' || process.env.GITNEXUS_DEBUG === 'true';
102
+ }
103
+
94
104
  function extractAugmentContext(stderr) {
95
105
  const output = (stderr || '').trim();
96
106
  const marker = output.indexOf('[GitNexus]');
97
- const debug = process.env.GITNEXUS_DEBUG === '1' || process.env.GITNEXUS_DEBUG === 'true';
107
+ const debug = isDebugEnabled();
98
108
  if (debug && output.length > 0) {
99
109
  // Emit the FULL discarded prefix (everything before the marker, or all of
100
110
  // it when no marker is present) so suppressed diagnostics — LadybugDB lock
@@ -258,8 +268,14 @@ function buildAfterToolContext(input) {
258
268
  if (/\bgit\s+(commit|merge|rebase|cherry-pick|pull)(\s|$)/.test(command)) {
259
269
  const hint = buildStaleIndexHint(gitNexusDir, cwd);
260
270
  if (hint) {
261
- process.stderr.write(`${hint}\n`);
271
+ // The hint always reaches the agent via additionalContext (parts). Mirror
272
+ // it to stderr (for terminal users) only under GITNEXUS_DEBUG, so strict
273
+ // hook runners see no unexpected output on this normal path (#1913). The
274
+ // claude hook never mirrored this to stderr — this aligns the two adapters.
262
275
  parts.push(hint);
276
+ if (isDebugEnabled()) {
277
+ process.stderr.write(`${hint}\n`);
278
+ }
263
279
  }
264
280
  }
265
281
  }
@@ -269,7 +285,11 @@ function buildAfterToolContext(input) {
269
285
 
270
286
  function runAugment(gitNexusDir, cwd, pattern) {
271
287
  if (hasGitNexusServerOwner(gitNexusDir)) {
272
- process.stderr.write('[GitNexus] augment skipped: MCP server owns DB\n');
288
+ // Normal skip path: the MCP server owns the DB. Stay silent for strict
289
+ // hook runners (issue #1913); surface the reason only under GITNEXUS_DEBUG.
290
+ if (isDebugEnabled()) {
291
+ process.stderr.write('[GitNexus] augment skipped: MCP server owns DB\n');
292
+ }
273
293
  return '';
274
294
  }
275
295
  const release = acquireHookSlot(gitNexusDir);
@@ -338,7 +358,7 @@ function main() {
338
358
  const handler = handlers[input.hook_event_name || ''];
339
359
  if (handler) handler(input);
340
360
  } catch (err) {
341
- if (process.env.GITNEXUS_DEBUG) {
361
+ if (isDebugEnabled()) {
342
362
  console.error('GitNexus antigravity hook error:', (err.message || '').slice(0, 200));
343
363
  }
344
364
  }
@@ -110,10 +110,20 @@ function hasGitNexusServerOwner(gitNexusDir) {
110
110
  return hasGitNexusDbLockedByGitNexusServer(path.join(gitNexusDir, 'lbug'), process.pid);
111
111
  }
112
112
 
113
+ /**
114
+ * Whether opt-in diagnostics should be written to the hook's stderr. Strict
115
+ * hook runners (e.g. Codex `PreToolUse`) validate hook output, so normal,
116
+ * non-error skip paths must stay silent unless the operator explicitly asks
117
+ * for diagnostics via GITNEXUS_DEBUG. See issue #1913.
118
+ */
119
+ function isDebugEnabled() {
120
+ return process.env.GITNEXUS_DEBUG === '1' || process.env.GITNEXUS_DEBUG === 'true';
121
+ }
122
+
113
123
  function extractAugmentContext(stderr) {
114
124
  const output = (stderr || '').trim();
115
125
  const marker = output.indexOf('[GitNexus]');
116
- const debug = process.env.GITNEXUS_DEBUG === '1' || process.env.GITNEXUS_DEBUG === 'true';
126
+ const debug = isDebugEnabled();
117
127
  if (debug && output.length > 0) {
118
128
  // Emit the FULL discarded prefix (everything before the marker, or all of
119
129
  // it when no marker is present) so suppressed diagnostics — KuzuDB lock
@@ -250,7 +260,12 @@ function handlePreToolUse(input) {
250
260
  const pattern = extractPattern(toolName, toolInput);
251
261
  if (!pattern || pattern.length < 3) return;
252
262
  if (hasGitNexusServerOwner(gitNexusDir)) {
253
- process.stderr.write('[GitNexus] augment skipped: MCP server owns DB\n');
263
+ // Normal skip path: the MCP server owns the DB, so the CLI augment would
264
+ // contend on the lock. Stay silent for strict hook runners (issue #1913);
265
+ // surface the reason only when diagnostics are explicitly requested.
266
+ if (isDebugEnabled()) {
267
+ process.stderr.write('[GitNexus] augment skipped: MCP server owns DB\n');
268
+ }
254
269
  return;
255
270
  }
256
271
 
@@ -361,7 +376,7 @@ function main() {
361
376
  const handler = handlers[input.hook_event_name || ''];
362
377
  if (handler) handler(input);
363
378
  } catch (err) {
364
- if (process.env.GITNEXUS_DEBUG) {
379
+ if (isDebugEnabled()) {
365
380
  console.error('GitNexus hook error:', (err.message || '').slice(0, 200));
366
381
  }
367
382
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.8-rc.3",
3
+ "version": "1.6.8-rc.5",
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",