gitnexus 1.6.8-rc.2 → 1.6.8-rc.4
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 +20 -0
- package/dist/core/embeddings/embedding-pipeline.js +27 -16
- package/dist/core/lbug/lbug-adapter.d.ts +19 -0
- package/dist/core/lbug/lbug-adapter.js +56 -1
- package/hooks/antigravity/gitnexus-antigravity-hook.cjs +24 -4
- package/hooks/claude/gitnexus-hook.cjs +18 -3
- package/package.json +1 -1
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
|
|
@@ -16,8 +16,8 @@ import { extractStructuralNames } from './structural-extractor.js';
|
|
|
16
16
|
import { EMBEDDABLE_LABELS, isShortLabel, LABEL_METHOD, LABELS_WITH_EXPORTED, STRUCTURAL_LABELS, collectBestChunks, } from './types.js';
|
|
17
17
|
import { resolveEmbeddingConfig } from './config.js';
|
|
18
18
|
import { rankExactEmbeddingRows } from './exact-search.js';
|
|
19
|
-
import { EMBEDDING_TABLE_NAME, EMBEDDING_INDEX_NAME,
|
|
20
|
-
import { loadVectorExtension } from '../lbug/lbug-adapter.js';
|
|
19
|
+
import { EMBEDDING_TABLE_NAME, EMBEDDING_INDEX_NAME, STALE_HASH_SENTINEL } from '../lbug/schema.js';
|
|
20
|
+
import { loadVectorExtension, createVectorIndex } from '../lbug/lbug-adapter.js';
|
|
21
21
|
import { getExactScanLimit } from '../platform/capabilities.js';
|
|
22
22
|
import { logger } from '../logger.js';
|
|
23
23
|
const isDev = process.env.NODE_ENV === 'development';
|
|
@@ -153,24 +153,35 @@ export const batchInsertEmbeddings = async (executeWithReusedStatement, updates)
|
|
|
153
153
|
await executeWithReusedStatement(cypher, paramsList);
|
|
154
154
|
};
|
|
155
155
|
/**
|
|
156
|
-
* Create the vector index for semantic search
|
|
157
|
-
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
|
|
156
|
+
* Create the vector index for semantic search (indexes the CodeEmbedding table).
|
|
157
|
+
*
|
|
158
|
+
* Keeps the embedding-specific extension-install policy gate here
|
|
159
|
+
* (ensureVectorExtensionAvailable → resolveEmbeddingInstallPolicy, default
|
|
160
|
+
* `auto` for the analyze write path), then delegates the actual
|
|
161
|
+
* `CALL CREATE_VECTOR_INDEX(...)` to the adapter, which runs it through the
|
|
162
|
+
* unprepared `conn.query()` path. It must NOT go through the injected
|
|
163
|
+
* `executeQuery` (prepared `conn.prepare()`): LadybugDB cannot prepare that
|
|
164
|
+
* procedure and fails with "We do not support prepare multiple statements" —
|
|
165
|
+
* the silent degrade in #2114.
|
|
162
166
|
*/
|
|
163
|
-
const
|
|
167
|
+
const buildVectorIndex = async () => {
|
|
168
|
+
// This pre-check applies the embedding-specific install policy
|
|
169
|
+
// (resolveEmbeddingInstallPolicy, default `auto` for analyze) before reaching
|
|
170
|
+
// the adapter. The adapter's createVectorIndex() calls loadVectorExtension()
|
|
171
|
+
// again, but that's a no-op here: once this gate loads VECTOR the module-level
|
|
172
|
+
// `vectorExtensionLoaded` flag is set, so the adapter's second call
|
|
173
|
+
// short-circuits without re-resolving the policy — no double install.
|
|
164
174
|
if (!(await ensureVectorExtensionAvailable()))
|
|
165
175
|
return false;
|
|
166
176
|
try {
|
|
167
|
-
await
|
|
168
|
-
return true;
|
|
177
|
+
return await createVectorIndex();
|
|
169
178
|
}
|
|
170
179
|
catch (error) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
180
|
+
// Surface this even outside dev: it silently downgrades a user-requested
|
|
181
|
+
// feature (semantic search) to exact scan. Log under `err` so pino's
|
|
182
|
+
// standard serializer captures the message/stack — logging under `error`
|
|
183
|
+
// serialized an Error to `{}` (the empty `{"error":{}}` reported in #2114).
|
|
184
|
+
logger.warn({ err: error }, 'Vector index creation failed; semantic search will use exact-scan fallback');
|
|
174
185
|
return false;
|
|
175
186
|
}
|
|
176
187
|
};
|
|
@@ -283,7 +294,7 @@ export const runEmbeddingPipeline = async (executeQuery, executeWithReusedStatem
|
|
|
283
294
|
// Ensure the vector index exists even when no new nodes need embedding.
|
|
284
295
|
// A prior crash or first-time incremental run may have left CodeEmbedding
|
|
285
296
|
// rows without ever reaching index creation.
|
|
286
|
-
const vectorIndexReady = await
|
|
297
|
+
const vectorIndexReady = await buildVectorIndex();
|
|
287
298
|
onProgress({
|
|
288
299
|
phase: 'ready',
|
|
289
300
|
percent: 100,
|
|
@@ -403,7 +414,7 @@ export const runEmbeddingPipeline = async (executeQuery, executeWithReusedStatem
|
|
|
403
414
|
if (isDev) {
|
|
404
415
|
logger.info('📇 Creating vector index...');
|
|
405
416
|
}
|
|
406
|
-
const vectorIndexReady = await
|
|
417
|
+
const vectorIndexReady = await buildVectorIndex();
|
|
407
418
|
onProgress({
|
|
408
419
|
phase: 'ready',
|
|
409
420
|
percent: 100,
|
|
@@ -233,6 +233,25 @@ export declare const loadVectorExtension: (targetConn?: lbug.Connection, opts?:
|
|
|
233
233
|
* @param stemmer - Stemming algorithm (default: 'porter')
|
|
234
234
|
*/
|
|
235
235
|
export declare const createFTSIndex: (tableName: string, indexName: string, properties: string[], stemmer?: string) => Promise<void>;
|
|
236
|
+
/**
|
|
237
|
+
* Create the HNSW vector index on the CodeEmbedding table.
|
|
238
|
+
*
|
|
239
|
+
* MUST run via `conn.query()` (here through `queryAndDrain`), NOT through the
|
|
240
|
+
* prepared `executeQuery`/`conn.prepare()` path: `CALL CREATE_VECTOR_INDEX(...)`
|
|
241
|
+
* compiles to multiple statements, which LadybugDB cannot prepare — it fails
|
|
242
|
+
* with "Connection Exception: We do not support prepare multiple statements."
|
|
243
|
+
* Routing index creation through `executeQuery` (prepared) is exactly what
|
|
244
|
+
* broke vector-index creation during `analyze` (#2114; the singleton
|
|
245
|
+
* `executeQuery` was switched to the prepared path in #1655 while FTS index
|
|
246
|
+
* creation kept using `conn.query()`, which is why FTS survived and VECTOR did
|
|
247
|
+
* not). Mirrors `createFTSIndex` above.
|
|
248
|
+
*
|
|
249
|
+
* Returns `true` on success (or when the index already exists — idempotent so
|
|
250
|
+
* incremental re-runs don't spuriously downgrade to exact scan), `false` when
|
|
251
|
+
* the VECTOR extension is unavailable or the connection is read-only. Any other
|
|
252
|
+
* failure propagates so the caller can log it.
|
|
253
|
+
*/
|
|
254
|
+
export declare const createVectorIndex: () => Promise<boolean>;
|
|
236
255
|
/**
|
|
237
256
|
* Lazy-create an FTS index, caching the fact in-process.
|
|
238
257
|
*
|
|
@@ -8,7 +8,7 @@ import os from 'os';
|
|
|
8
8
|
import crypto from 'crypto';
|
|
9
9
|
import lbug from '@ladybugdb/core';
|
|
10
10
|
import { closeQueryResults } from './query-result-utils.js';
|
|
11
|
-
import { NODE_TABLES, REL_TABLE_NAME, SCHEMA_QUERIES, EMBEDDING_TABLE_NAME, STALE_HASH_SENTINEL, } from './schema.js';
|
|
11
|
+
import { NODE_TABLES, REL_TABLE_NAME, SCHEMA_QUERIES, EMBEDDING_TABLE_NAME, CREATE_VECTOR_INDEX_QUERY, STALE_HASH_SENTINEL, } from './schema.js';
|
|
12
12
|
import { streamAllCSVsToDisk } from './csv-generator.js';
|
|
13
13
|
import { extensionManager } from './extension-loader.js';
|
|
14
14
|
import { closeLbugConnection, isDbBusyError, isOpenRetryExhausted, isWalCorruptionError, openLbugConnection, toNativeSafePath, WAL_RECOVERY_SUGGESTION, waitForWindowsHandleRelease, } from './lbug-config.js';
|
|
@@ -119,6 +119,11 @@ let currentDbPath = null;
|
|
|
119
119
|
let currentDbReadOnly = false;
|
|
120
120
|
let ftsLoaded = false;
|
|
121
121
|
let vectorExtensionLoaded = false;
|
|
122
|
+
// In-process guard so a repeated createVectorIndex() within one connection
|
|
123
|
+
// lifetime skips the DB round-trip (mirrors ensuredFTSIndexes). Reset wherever
|
|
124
|
+
// vectorExtensionLoaded resets, so it can never stay true against a swapped or
|
|
125
|
+
// closed connection.
|
|
126
|
+
let vectorIndexEnsured = false;
|
|
122
127
|
/**
|
|
123
128
|
* In-process cache of FTS indexes observed against the current singleton
|
|
124
129
|
* connection. Avoids repeated `CALL CREATE_FTS_INDEX` calls, which can trip
|
|
@@ -494,6 +499,7 @@ const resetOpenConnectionState = () => {
|
|
|
494
499
|
currentDbPath = null;
|
|
495
500
|
ftsLoaded = false;
|
|
496
501
|
vectorExtensionLoaded = false;
|
|
502
|
+
vectorIndexEnsured = false;
|
|
497
503
|
ensuredFTSIndexes.clear();
|
|
498
504
|
};
|
|
499
505
|
const runSchemaCreationQueries = async (dbPath) => {
|
|
@@ -572,6 +578,7 @@ export const withLbugDb = async (dbPath, operation, options = {}) => {
|
|
|
572
578
|
currentDbPath = null;
|
|
573
579
|
ftsLoaded = false;
|
|
574
580
|
vectorExtensionLoaded = false;
|
|
581
|
+
vectorIndexEnsured = false;
|
|
575
582
|
ensuredFTSIndexes.clear();
|
|
576
583
|
});
|
|
577
584
|
// Sleep outside the lock — no need to block others while waiting
|
|
@@ -596,6 +603,7 @@ const doInitLbug = async (dbPath, readOnly = false) => {
|
|
|
596
603
|
currentDbPath = null;
|
|
597
604
|
ftsLoaded = false;
|
|
598
605
|
vectorExtensionLoaded = false;
|
|
606
|
+
vectorIndexEnsured = false;
|
|
599
607
|
ensuredFTSIndexes.clear();
|
|
600
608
|
}
|
|
601
609
|
// ---------------------------------------------------------------------------
|
|
@@ -1476,6 +1484,7 @@ export const closeLbug = async () => {
|
|
|
1476
1484
|
currentDbPath = null;
|
|
1477
1485
|
ftsLoaded = false;
|
|
1478
1486
|
vectorExtensionLoaded = false;
|
|
1487
|
+
vectorIndexEnsured = false;
|
|
1479
1488
|
ensuredFTSIndexes.clear();
|
|
1480
1489
|
};
|
|
1481
1490
|
export const isLbugReady = () => conn !== null && db !== null;
|
|
@@ -1708,6 +1717,52 @@ export const createFTSIndex = async (tableName, indexName, properties, stemmer =
|
|
|
1708
1717
|
throw e;
|
|
1709
1718
|
}
|
|
1710
1719
|
};
|
|
1720
|
+
/**
|
|
1721
|
+
* Create the HNSW vector index on the CodeEmbedding table.
|
|
1722
|
+
*
|
|
1723
|
+
* MUST run via `conn.query()` (here through `queryAndDrain`), NOT through the
|
|
1724
|
+
* prepared `executeQuery`/`conn.prepare()` path: `CALL CREATE_VECTOR_INDEX(...)`
|
|
1725
|
+
* compiles to multiple statements, which LadybugDB cannot prepare — it fails
|
|
1726
|
+
* with "Connection Exception: We do not support prepare multiple statements."
|
|
1727
|
+
* Routing index creation through `executeQuery` (prepared) is exactly what
|
|
1728
|
+
* broke vector-index creation during `analyze` (#2114; the singleton
|
|
1729
|
+
* `executeQuery` was switched to the prepared path in #1655 while FTS index
|
|
1730
|
+
* creation kept using `conn.query()`, which is why FTS survived and VECTOR did
|
|
1731
|
+
* not). Mirrors `createFTSIndex` above.
|
|
1732
|
+
*
|
|
1733
|
+
* Returns `true` on success (or when the index already exists — idempotent so
|
|
1734
|
+
* incremental re-runs don't spuriously downgrade to exact scan), `false` when
|
|
1735
|
+
* the VECTOR extension is unavailable or the connection is read-only. Any other
|
|
1736
|
+
* failure propagates so the caller can log it.
|
|
1737
|
+
*/
|
|
1738
|
+
export const createVectorIndex = async () => {
|
|
1739
|
+
if (!conn) {
|
|
1740
|
+
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
1741
|
+
}
|
|
1742
|
+
// Already built on this connection — skip the round-trip (mirrors createFTSIndex).
|
|
1743
|
+
if (vectorIndexEnsured)
|
|
1744
|
+
return true;
|
|
1745
|
+
if (!(await loadVectorExtension())) {
|
|
1746
|
+
return false;
|
|
1747
|
+
}
|
|
1748
|
+
try {
|
|
1749
|
+
await queryAndDrain(conn, CREATE_VECTOR_INDEX_QUERY);
|
|
1750
|
+
vectorIndexEnsured = true;
|
|
1751
|
+
return true;
|
|
1752
|
+
}
|
|
1753
|
+
catch (e) {
|
|
1754
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1755
|
+
// Idempotent: a prior analyze already built the HNSW index.
|
|
1756
|
+
if (msg.includes('already exists')) {
|
|
1757
|
+
vectorIndexEnsured = true;
|
|
1758
|
+
return true;
|
|
1759
|
+
}
|
|
1760
|
+
// Read-only DB (e.g. the MCP query pool): writable analyze owns creation.
|
|
1761
|
+
if (isReadOnlyDbError(e))
|
|
1762
|
+
return false;
|
|
1763
|
+
throw e;
|
|
1764
|
+
}
|
|
1765
|
+
};
|
|
1711
1766
|
/**
|
|
1712
1767
|
* Lazy-create an FTS index, caching the fact in-process.
|
|
1713
1768
|
*
|
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
379
|
+
if (isDebugEnabled()) {
|
|
365
380
|
console.error('GitNexus hook error:', (err.message || '').slice(0, 200));
|
|
366
381
|
}
|
|
367
382
|
}
|
package/package.json
CHANGED