gitnexus 1.6.8-rc.4 → 1.6.8-rc.6

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 (35) hide show
  1. package/dist/cli/analyze.d.ts +8 -0
  2. package/dist/cli/analyze.js +31 -6
  3. package/dist/cli/clean.d.ts +1 -0
  4. package/dist/cli/clean.js +43 -1
  5. package/dist/cli/help-i18n.js +6 -0
  6. package/dist/cli/i18n/en.d.ts +11 -0
  7. package/dist/cli/i18n/en.js +11 -0
  8. package/dist/cli/i18n/resources.d.ts +22 -0
  9. package/dist/cli/i18n/zh-CN.d.ts +11 -0
  10. package/dist/cli/i18n/zh-CN.js +11 -0
  11. package/dist/cli/index.js +9 -0
  12. package/dist/cli/list.js +12 -0
  13. package/dist/cli/status.js +26 -5
  14. package/dist/cli/tool.d.ts +5 -0
  15. package/dist/cli/tool.js +5 -0
  16. package/dist/core/group/extractors/http-patterns/java.js +3 -50
  17. package/dist/core/ingestion/language-provider.d.ts +13 -0
  18. package/dist/core/ingestion/languages/java.js +3 -0
  19. package/dist/core/ingestion/route-extractors/spring-shared.d.ts +50 -0
  20. package/dist/core/ingestion/route-extractors/spring-shared.js +80 -0
  21. package/dist/core/ingestion/route-extractors/spring.d.ts +35 -0
  22. package/dist/core/ingestion/route-extractors/spring.js +136 -0
  23. package/dist/core/ingestion/workers/parse-worker.js +10 -0
  24. package/dist/core/run-analyze.d.ts +38 -0
  25. package/dist/core/run-analyze.js +174 -24
  26. package/dist/mcp/local/local-backend.d.ts +24 -2
  27. package/dist/mcp/local/local-backend.js +104 -15
  28. package/dist/mcp/tools.js +32 -0
  29. package/dist/storage/branch-index.d.ts +52 -0
  30. package/dist/storage/branch-index.js +65 -0
  31. package/dist/storage/git.d.ts +11 -0
  32. package/dist/storage/git.js +28 -0
  33. package/dist/storage/repo-manager.d.ts +57 -2
  34. package/dist/storage/repo-manager.js +132 -18
  35. package/package.json +1 -1
@@ -58,6 +58,14 @@ export interface AnalyzeOptions {
58
58
  * before being threaded into the generated AGENTS.md / CLAUDE.md content.
59
59
  */
60
60
  defaultBranch?: string;
61
+ /**
62
+ * Index-branch selector (#2106). From `--branch`. Distinct from
63
+ * `defaultBranch` (cosmetic base_ref): this routes the index to a per-branch
64
+ * slot. NOT sourced from `.gitnexusrc` — the `.gitnexusrc` `branch` key is an
65
+ * alias for `defaultBranch` and must not change index placement. Defaults to
66
+ * the checked-out branch inside `runFullAnalysis` when omitted.
67
+ */
68
+ branch?: string;
61
69
  /** Pure index mode: skip all file injection (AGENTS.md, CLAUDE.md, skills). */
62
70
  indexOnly?: boolean;
63
71
  /** Index the folder even when no .git directory is present. */
@@ -551,6 +551,21 @@ const analyzeCommandImpl = async (inputPath, cliOptions) => {
551
551
  return;
552
552
  }
553
553
  }
554
+ // Validate the index-branch selector (#2106) the same way, so a malformed
555
+ // `--branch` exits before any expensive analysis starts. Capture the TRIMMED
556
+ // return so a whitespace-padded value (e.g. " feature" from shell completion)
557
+ // normalizes before the checked-out-branch mismatch guard and slug — otherwise
558
+ // it would false-reject on-branch or create a ghost index when detached.
559
+ if (cliOptions?.branch !== undefined) {
560
+ try {
561
+ cliOptions.branch = validateBranchName(cliOptions.branch, '--branch');
562
+ }
563
+ catch (err) {
564
+ cliError(` ${err instanceof Error ? err.message : String(err)}\n`);
565
+ process.exitCode = 1;
566
+ return;
567
+ }
568
+ }
554
569
  // ── Load .gitnexusrc and merge: CLI flags override config (#243) ───
555
570
  // Parse/validate before the progress bar so a malformed config produces an
556
571
  // actionable error and exits before any expensive analysis starts.
@@ -829,6 +844,10 @@ const analyzeCommandImpl = async (inputPath, cliOptions) => {
829
844
  // Resolved default branch (CLI > .gitnexusrc > auto-detect > "main")
830
845
  // threaded into the generated regression-compare example (#243).
831
846
  defaultBranch: resolvedDefaultBranch,
847
+ // Index-branch selector (#2106). Read straight from the CLI flag (not
848
+ // the .gitnexusrc-merged options) so the cosmetic defaultBranch config
849
+ // can never change index placement. Undefined → auto-detect in pipeline.
850
+ branch: cliOptions?.branch,
832
851
  // commander.js `.option('--no-stats', …)` registers the flag as
833
852
  // `options.stats` (boolean, default true; `false` when the user
834
853
  // passed --no-stats). Reading `options.noStats` here returns
@@ -863,13 +882,19 @@ const analyzeCommandImpl = async (inputPath, cliOptions) => {
863
882
  // preserving the rest of the block (incl. --skills community rows). No-op
864
883
  // when the value already matches, so a routine up-to-date run is silent
865
884
  // (#1996 tri-review P2).
885
+ // Only refresh the repo-root AGENTS.md/CLAUDE.md base_ref for the
886
+ // PRIMARY/flat index (#2106 R2). A non-primary branch's up-to-date
887
+ // analyze must not churn the committed AGENTS.md — this mirrors the
888
+ // in-pipeline `if (!placement.branch)` gate around generateAIContextFiles.
866
889
  let baseRefRefreshed = [];
867
- try {
868
- const { refreshBaseRefLine } = await import('./ai-context.js');
869
- baseRefRefreshed = (await refreshBaseRefLine(repoPath, resolvedDefaultBranch, { skipAgentsMd })).files;
870
- }
871
- catch {
872
- /* best-effort — never fail the fast path over a context refresh */
890
+ if (result.isPrimaryBranch !== false) {
891
+ try {
892
+ const { refreshBaseRefLine } = await import('./ai-context.js');
893
+ baseRefRefreshed = (await refreshBaseRefLine(repoPath, resolvedDefaultBranch, { skipAgentsMd })).files;
894
+ }
895
+ catch {
896
+ /* best-effort — never fail the fast path over a context refresh */
897
+ }
873
898
  }
874
899
  clearInterval(elapsedTimer);
875
900
  process.removeListener('SIGINT', sigintHandler);
@@ -8,4 +8,5 @@ export declare const cleanCommand: (options?: {
8
8
  force?: boolean;
9
9
  all?: boolean;
10
10
  lbugSidecars?: boolean;
11
+ branch?: string;
11
12
  }) => Promise<void>;
package/dist/cli/clean.js CHANGED
@@ -7,10 +7,52 @@
7
7
  import fs from 'fs/promises';
8
8
  import path from 'path';
9
9
  import { logger } from '../core/logger.js';
10
- import { findRepo, unregisterRepo, listRegisteredRepos, assertSafeStoragePath, UnsafeStoragePathError, } from '../storage/repo-manager.js';
10
+ import { findRepo, unregisterRepo, listRegisteredRepos, assertSafeStoragePath, getStoragePaths, removeBranchIndex, UnsafeStoragePathError, } from '../storage/repo-manager.js';
11
11
  import { cleanQuarantinedMissingShadowWals, inspectLbugSidecars, listQuarantinedMissingShadowWals, } from '../core/lbug/sidecar-recovery.js';
12
12
  import { t } from './i18n/index.js';
13
13
  export const cleanCommand = async (options) => {
14
+ // --branch <name>: remove a single non-primary branch's index (#2106 R7).
15
+ // Resolve against the RECORDED branches[] summary (never by slugging the
16
+ // user's raw input, which can disagree with the index-time-sanitized label).
17
+ if (options?.branch) {
18
+ const cwd = process.cwd();
19
+ const repo = await findRepo(cwd);
20
+ if (!repo) {
21
+ console.log(t('clean.notFoundHere'));
22
+ return;
23
+ }
24
+ const entries = await listRegisteredRepos();
25
+ const entry = entries.find((e) => path.resolve(e.path) === path.resolve(repo.repoPath));
26
+ const summary = entry?.branches?.find((b) => b.branch === options.branch);
27
+ if (!summary) {
28
+ console.log(t('clean.branchNotIndexed', { branch: options.branch }));
29
+ return;
30
+ }
31
+ const { storagePath, lbugPath } = getStoragePaths(repo.repoPath, summary.branch);
32
+ const branchDir = path.dirname(lbugPath);
33
+ // Safety guard: the target MUST live under <repo>/.gitnexus/branches/.
34
+ // assertSafeStoragePath only validates the flat `<repo>/.gitnexus`, so this
35
+ // is a dedicated branches-sub-dir check before any destructive fs.rm.
36
+ const branchesRoot = path.join(storagePath, 'branches') + path.sep;
37
+ if (!branchDir.startsWith(branchesRoot)) {
38
+ logger.error(`Refusing to clean branch index outside .gitnexus/branches: ${branchDir}`);
39
+ return;
40
+ }
41
+ if (!options.force) {
42
+ console.log(t('clean.deleteBranch', { branch: summary.branch, path: branchDir }));
43
+ console.log(`\n${t('common.runForceConfirm')}`);
44
+ return;
45
+ }
46
+ try {
47
+ await fs.rm(branchDir, { recursive: true, force: true });
48
+ await removeBranchIndex(repo.repoPath, summary.branch);
49
+ console.log(t('clean.deletedBranch', { branch: summary.branch }));
50
+ }
51
+ catch (err) {
52
+ logger.error({ err }, 'Failed to delete branch index:');
53
+ }
54
+ return;
55
+ }
14
56
  if (options?.lbugSidecars) {
15
57
  const cwd = process.cwd();
16
58
  const repo = await findRepo(cwd);
@@ -69,6 +69,7 @@ const OPTION_DESCRIPTION_KEYS = {
69
69
  'uninstall|-f, --force': 'help.option.uninstall.force',
70
70
  'clean|-f, --force': 'help.option.force.confirmation',
71
71
  'clean|--all': 'help.option.clean.all',
72
+ 'clean|--branch <name>': 'help.option.clean.branch',
72
73
  'clean|--lbug-sidecars': 'help.option.clean.lbugSidecars',
73
74
  'remove|-f, --force': 'help.option.force.confirmation',
74
75
  'wiki|-f, --force': 'help.option.wiki.force',
@@ -89,16 +90,19 @@ const OPTION_DESCRIPTION_KEYS = {
89
90
  'publish|--id <owner/repo>': 'help.option.publish.id',
90
91
  'publish|--skip-git': 'help.option.skipGit',
91
92
  'query|-r, --repo <name>': 'help.option.repo.targetOmitOne',
93
+ 'query|--branch <name>': 'help.option.branch',
92
94
  'query|-c, --context <text>': 'help.option.query.context',
93
95
  'query|-g, --goal <text>': 'help.option.query.goal',
94
96
  'query|-l, --limit <n>': 'help.option.query.limit',
95
97
  'query|--content': 'help.option.content',
96
98
  'context|-r, --repo <name>': 'help.option.repo.target',
99
+ 'context|--branch <name>': 'help.option.branch',
97
100
  'context|-u, --uid <uid>': 'help.option.context.uid',
98
101
  'context|-f, --file <path>': 'help.option.context.file',
99
102
  'context|--content': 'help.option.content',
100
103
  'impact|-d, --direction <dir>': 'help.option.impact.direction',
101
104
  'impact|-r, --repo <name>': 'help.option.repo.target',
105
+ 'impact|--branch <name>': 'help.option.branch',
102
106
  'impact|-u, --uid <uid>': 'help.option.context.uid',
103
107
  'impact|-f, --file <path>': 'help.option.context.file',
104
108
  'impact|--kind <kind>': 'help.option.impact.kind',
@@ -108,9 +112,11 @@ const OPTION_DESCRIPTION_KEYS = {
108
112
  'impact|--offset <n>': 'help.option.impact.offset',
109
113
  'impact|--summary-only': 'help.option.impact.summaryOnly',
110
114
  'cypher|-r, --repo <name>': 'help.option.repo.target',
115
+ 'cypher|--branch <name>': 'help.option.branch',
111
116
  'detect-changes|-s, --scope <scope>': 'help.option.detectChanges.scope',
112
117
  'detect-changes|-b, --base-ref <ref>': 'help.option.detectChanges.baseRef',
113
118
  'detect-changes|-r, --repo <name>': 'help.option.repo.target',
119
+ 'detect-changes|--branch <name>': 'help.option.branch',
114
120
  'eval-server|-p, --port <port>': 'help.option.port',
115
121
  'eval-server|--host <host>': 'help.option.evalServer.host',
116
122
  'eval-server|--idle-timeout <seconds>': 'help.option.evalServer.idleTimeout',
@@ -10,6 +10,9 @@ export declare const en: {
10
10
  readonly 'list.title': "Indexed Repositories ({{count}})";
11
11
  readonly 'list.indexed': "Indexed";
12
12
  readonly 'list.commit': "Commit";
13
+ readonly 'list.branch': "Branch";
14
+ readonly 'list.branchIndexes': "Branch indexes";
15
+ readonly 'list.branchLine': "{{branch}} ({{commit}}, {{indexed}})";
13
16
  readonly 'list.stats': "Stats";
14
17
  readonly 'list.statsValue': "{{files}} files, {{symbols}} symbols, {{edges}} edges";
15
18
  readonly 'list.clusters': "Clusters";
@@ -23,6 +26,9 @@ export declare const en: {
23
26
  readonly 'status.indexed': "Indexed";
24
27
  readonly 'status.indexedCommit': "Indexed commit";
25
28
  readonly 'status.currentCommit': "Current commit";
29
+ readonly 'status.branch': "Branch";
30
+ readonly 'status.detached': "(detached HEAD)";
31
+ readonly 'status.branchNotIndexed': "⚠️ current branch not indexed (primary index is for '{{primary}}'; run gitnexus analyze)";
26
32
  readonly 'status.status': "Status";
27
33
  readonly 'status.upToDate': "✅ up-to-date";
28
34
  readonly 'status.stale': "⚠️ stale (re-run gitnexus analyze)";
@@ -30,6 +36,9 @@ export declare const en: {
30
36
  readonly 'clean.deletedRepo': "Deleted: {{name}} ({{storagePath}})";
31
37
  readonly 'clean.notFoundHere': "No indexed repository found in this directory.";
32
38
  readonly 'clean.deleteCurrent': "This will delete the GitNexus index for: {{repoName}}";
39
+ readonly 'clean.branchNotIndexed': "No indexed branch named \"{{branch}}\" for this repository.";
40
+ readonly 'clean.deleteBranch': "This will delete the branch index \"{{branch}}\" at: {{path}}";
41
+ readonly 'clean.deletedBranch': "Deleted branch index: {{branch}}";
33
42
  readonly 'clean.lbugSidecars.state': "LadybugDB sidecar state: {{state}}";
34
43
  readonly 'clean.lbugSidecars.none': "No quarantined LadybugDB missing-shadow WAL sidecars found.";
35
44
  readonly 'clean.lbugSidecars.preview': "This will delete {{count}} quarantined LadybugDB missing-shadow WAL sidecar(s):";
@@ -156,6 +165,7 @@ export declare const en: {
156
165
  readonly 'help.option.force.confirmation': "Skip confirmation prompt";
157
166
  readonly 'help.option.uninstall.force': "Apply the changes (default is a dry-run preview)";
158
167
  readonly 'help.option.clean.all': "Clean all indexed repos";
168
+ readonly 'help.option.clean.branch': "Delete only the named branch index (not the primary)";
159
169
  readonly 'help.option.clean.lbugSidecars': "Clean quarantined LadybugDB missing-shadow WAL sidecars";
160
170
  readonly 'help.option.wiki.force': "Force full regeneration even if up to date";
161
171
  readonly 'help.option.wiki.provider': "LLM provider: openai, openrouter, azure, custom, cursor, claude, codex, or opencode (default: openai)";
@@ -178,6 +188,7 @@ export declare const en: {
178
188
  readonly 'help.option.query.limit': "Max processes to return (default: 5)";
179
189
  readonly 'help.option.content': "Include full symbol source code";
180
190
  readonly 'help.option.repo.target': "Target repository";
191
+ readonly 'help.option.branch': "Scope to a specific branch index (multi-branch repos)";
181
192
  readonly 'help.option.context.uid': "Direct symbol UID (zero-ambiguity lookup)";
182
193
  readonly 'help.option.context.file': "File path to disambiguate common names";
183
194
  readonly 'help.option.impact.kind': "Kind filter to disambiguate common names (e.g. Function, Class, Method)";
@@ -10,6 +10,9 @@ export const en = {
10
10
  'list.title': 'Indexed Repositories ({{count}})',
11
11
  'list.indexed': 'Indexed',
12
12
  'list.commit': 'Commit',
13
+ 'list.branch': 'Branch',
14
+ 'list.branchIndexes': 'Branch indexes',
15
+ 'list.branchLine': '{{branch}} ({{commit}}, {{indexed}})',
13
16
  'list.stats': 'Stats',
14
17
  'list.statsValue': '{{files}} files, {{symbols}} symbols, {{edges}} edges',
15
18
  'list.clusters': 'Clusters',
@@ -23,6 +26,9 @@ export const en = {
23
26
  'status.indexed': 'Indexed',
24
27
  'status.indexedCommit': 'Indexed commit',
25
28
  'status.currentCommit': 'Current commit',
29
+ 'status.branch': 'Branch',
30
+ 'status.detached': '(detached HEAD)',
31
+ 'status.branchNotIndexed': "⚠️ current branch not indexed (primary index is for '{{primary}}'; run gitnexus analyze)",
26
32
  'status.status': 'Status',
27
33
  'status.upToDate': '✅ up-to-date',
28
34
  'status.stale': '⚠️ stale (re-run gitnexus analyze)',
@@ -30,6 +36,9 @@ export const en = {
30
36
  'clean.deletedRepo': 'Deleted: {{name}} ({{storagePath}})',
31
37
  'clean.notFoundHere': 'No indexed repository found in this directory.',
32
38
  'clean.deleteCurrent': 'This will delete the GitNexus index for: {{repoName}}',
39
+ 'clean.branchNotIndexed': 'No indexed branch named "{{branch}}" for this repository.',
40
+ 'clean.deleteBranch': 'This will delete the branch index "{{branch}}" at: {{path}}',
41
+ 'clean.deletedBranch': 'Deleted branch index: {{branch}}',
33
42
  'clean.lbugSidecars.state': 'LadybugDB sidecar state: {{state}}',
34
43
  'clean.lbugSidecars.none': 'No quarantined LadybugDB missing-shadow WAL sidecars found.',
35
44
  'clean.lbugSidecars.preview': 'This will delete {{count}} quarantined LadybugDB missing-shadow WAL sidecar(s):',
@@ -156,6 +165,7 @@ export const en = {
156
165
  'help.option.force.confirmation': 'Skip confirmation prompt',
157
166
  'help.option.uninstall.force': 'Apply the changes (default is a dry-run preview)',
158
167
  'help.option.clean.all': 'Clean all indexed repos',
168
+ 'help.option.clean.branch': 'Delete only the named branch index (not the primary)',
159
169
  'help.option.clean.lbugSidecars': 'Clean quarantined LadybugDB missing-shadow WAL sidecars',
160
170
  'help.option.wiki.force': 'Force full regeneration even if up to date',
161
171
  'help.option.wiki.provider': 'LLM provider: openai, openrouter, azure, custom, cursor, claude, codex, or opencode (default: openai)',
@@ -178,6 +188,7 @@ export const en = {
178
188
  'help.option.query.limit': 'Max processes to return (default: 5)',
179
189
  'help.option.content': 'Include full symbol source code',
180
190
  'help.option.repo.target': 'Target repository',
191
+ 'help.option.branch': 'Scope to a specific branch index (multi-branch repos)',
181
192
  'help.option.context.uid': 'Direct symbol UID (zero-ambiguity lookup)',
182
193
  'help.option.context.file': 'File path to disambiguate common names',
183
194
  'help.option.impact.kind': 'Kind filter to disambiguate common names (e.g. Function, Class, Method)',
@@ -11,6 +11,9 @@ export declare const cliResources: {
11
11
  readonly 'list.title': "Indexed Repositories ({{count}})";
12
12
  readonly 'list.indexed': "Indexed";
13
13
  readonly 'list.commit': "Commit";
14
+ readonly 'list.branch': "Branch";
15
+ readonly 'list.branchIndexes': "Branch indexes";
16
+ readonly 'list.branchLine': "{{branch}} ({{commit}}, {{indexed}})";
14
17
  readonly 'list.stats': "Stats";
15
18
  readonly 'list.statsValue': "{{files}} files, {{symbols}} symbols, {{edges}} edges";
16
19
  readonly 'list.clusters': "Clusters";
@@ -24,6 +27,9 @@ export declare const cliResources: {
24
27
  readonly 'status.indexed': "Indexed";
25
28
  readonly 'status.indexedCommit': "Indexed commit";
26
29
  readonly 'status.currentCommit': "Current commit";
30
+ readonly 'status.branch': "Branch";
31
+ readonly 'status.detached': "(detached HEAD)";
32
+ readonly 'status.branchNotIndexed': "⚠️ current branch not indexed (primary index is for '{{primary}}'; run gitnexus analyze)";
27
33
  readonly 'status.status': "Status";
28
34
  readonly 'status.upToDate': "✅ up-to-date";
29
35
  readonly 'status.stale': "⚠️ stale (re-run gitnexus analyze)";
@@ -31,6 +37,9 @@ export declare const cliResources: {
31
37
  readonly 'clean.deletedRepo': "Deleted: {{name}} ({{storagePath}})";
32
38
  readonly 'clean.notFoundHere': "No indexed repository found in this directory.";
33
39
  readonly 'clean.deleteCurrent': "This will delete the GitNexus index for: {{repoName}}";
40
+ readonly 'clean.branchNotIndexed': "No indexed branch named \"{{branch}}\" for this repository.";
41
+ readonly 'clean.deleteBranch': "This will delete the branch index \"{{branch}}\" at: {{path}}";
42
+ readonly 'clean.deletedBranch': "Deleted branch index: {{branch}}";
34
43
  readonly 'clean.lbugSidecars.state': "LadybugDB sidecar state: {{state}}";
35
44
  readonly 'clean.lbugSidecars.none': "No quarantined LadybugDB missing-shadow WAL sidecars found.";
36
45
  readonly 'clean.lbugSidecars.preview': "This will delete {{count}} quarantined LadybugDB missing-shadow WAL sidecar(s):";
@@ -157,6 +166,7 @@ export declare const cliResources: {
157
166
  readonly 'help.option.force.confirmation': "Skip confirmation prompt";
158
167
  readonly 'help.option.uninstall.force': "Apply the changes (default is a dry-run preview)";
159
168
  readonly 'help.option.clean.all': "Clean all indexed repos";
169
+ readonly 'help.option.clean.branch': "Delete only the named branch index (not the primary)";
160
170
  readonly 'help.option.clean.lbugSidecars': "Clean quarantined LadybugDB missing-shadow WAL sidecars";
161
171
  readonly 'help.option.wiki.force': "Force full regeneration even if up to date";
162
172
  readonly 'help.option.wiki.provider': "LLM provider: openai, openrouter, azure, custom, cursor, claude, codex, or opencode (default: openai)";
@@ -179,6 +189,7 @@ export declare const cliResources: {
179
189
  readonly 'help.option.query.limit': "Max processes to return (default: 5)";
180
190
  readonly 'help.option.content': "Include full symbol source code";
181
191
  readonly 'help.option.repo.target': "Target repository";
192
+ readonly 'help.option.branch': "Scope to a specific branch index (multi-branch repos)";
182
193
  readonly 'help.option.context.uid': "Direct symbol UID (zero-ambiguity lookup)";
183
194
  readonly 'help.option.context.file': "File path to disambiguate common names";
184
195
  readonly 'help.option.impact.kind': "Kind filter to disambiguate common names (e.g. Function, Class, Method)";
@@ -224,6 +235,9 @@ export declare const cliResources: {
224
235
  'list.title': string;
225
236
  'list.indexed': string;
226
237
  'list.commit': string;
238
+ 'list.branch': string;
239
+ 'list.branchIndexes': string;
240
+ 'list.branchLine': string;
227
241
  'list.stats': string;
228
242
  'list.statsValue': string;
229
243
  'list.clusters': string;
@@ -237,6 +251,9 @@ export declare const cliResources: {
237
251
  'status.indexed': string;
238
252
  'status.indexedCommit': string;
239
253
  'status.currentCommit': string;
254
+ 'status.branch': string;
255
+ 'status.detached': string;
256
+ 'status.branchNotIndexed': string;
240
257
  'status.status': string;
241
258
  'status.upToDate': string;
242
259
  'status.stale': string;
@@ -244,6 +261,9 @@ export declare const cliResources: {
244
261
  'clean.deletedRepo': string;
245
262
  'clean.notFoundHere': string;
246
263
  'clean.deleteCurrent': string;
264
+ 'clean.branchNotIndexed': string;
265
+ 'clean.deleteBranch': string;
266
+ 'clean.deletedBranch': string;
247
267
  'clean.lbugSidecars.state': string;
248
268
  'clean.lbugSidecars.none': string;
249
269
  'clean.lbugSidecars.preview': string;
@@ -370,6 +390,7 @@ export declare const cliResources: {
370
390
  'help.option.force.confirmation': string;
371
391
  'help.option.uninstall.force': string;
372
392
  'help.option.clean.all': string;
393
+ 'help.option.clean.branch': string;
373
394
  'help.option.clean.lbugSidecars': string;
374
395
  'help.option.wiki.force': string;
375
396
  'help.option.wiki.provider': string;
@@ -392,6 +413,7 @@ export declare const cliResources: {
392
413
  'help.option.query.limit': string;
393
414
  'help.option.content': string;
394
415
  'help.option.repo.target': string;
416
+ 'help.option.branch': string;
395
417
  'help.option.context.uid': string;
396
418
  'help.option.context.file': string;
397
419
  'help.option.impact.kind': string;
@@ -10,6 +10,9 @@ export declare const zhCN: {
10
10
  'list.title': string;
11
11
  'list.indexed': string;
12
12
  'list.commit': string;
13
+ 'list.branch': string;
14
+ 'list.branchIndexes': string;
15
+ 'list.branchLine': string;
13
16
  'list.stats': string;
14
17
  'list.statsValue': string;
15
18
  'list.clusters': string;
@@ -23,6 +26,9 @@ export declare const zhCN: {
23
26
  'status.indexed': string;
24
27
  'status.indexedCommit': string;
25
28
  'status.currentCommit': string;
29
+ 'status.branch': string;
30
+ 'status.detached': string;
31
+ 'status.branchNotIndexed': string;
26
32
  'status.status': string;
27
33
  'status.upToDate': string;
28
34
  'status.stale': string;
@@ -30,6 +36,9 @@ export declare const zhCN: {
30
36
  'clean.deletedRepo': string;
31
37
  'clean.notFoundHere': string;
32
38
  'clean.deleteCurrent': string;
39
+ 'clean.branchNotIndexed': string;
40
+ 'clean.deleteBranch': string;
41
+ 'clean.deletedBranch': string;
33
42
  'clean.lbugSidecars.state': string;
34
43
  'clean.lbugSidecars.none': string;
35
44
  'clean.lbugSidecars.preview': string;
@@ -156,6 +165,7 @@ export declare const zhCN: {
156
165
  'help.option.force.confirmation': string;
157
166
  'help.option.uninstall.force': string;
158
167
  'help.option.clean.all': string;
168
+ 'help.option.clean.branch': string;
159
169
  'help.option.clean.lbugSidecars': string;
160
170
  'help.option.wiki.force': string;
161
171
  'help.option.wiki.provider': string;
@@ -178,6 +188,7 @@ export declare const zhCN: {
178
188
  'help.option.query.limit': string;
179
189
  'help.option.content': string;
180
190
  'help.option.repo.target': string;
191
+ 'help.option.branch': string;
181
192
  'help.option.context.uid': string;
182
193
  'help.option.context.file': string;
183
194
  'help.option.impact.kind': string;
@@ -10,6 +10,9 @@ export const zhCN = {
10
10
  'list.title': '已索引仓库({{count}})',
11
11
  'list.indexed': '索引时间',
12
12
  'list.commit': '提交',
13
+ 'list.branch': '分支',
14
+ 'list.branchIndexes': '分支索引',
15
+ 'list.branchLine': '{{branch}}({{commit}},{{indexed}})',
13
16
  'list.stats': '统计',
14
17
  'list.statsValue': '{{files}} 个文件,{{symbols}} 个符号,{{edges}} 条边',
15
18
  'list.clusters': '聚类',
@@ -23,6 +26,9 @@ export const zhCN = {
23
26
  'status.indexed': '索引时间',
24
27
  'status.indexedCommit': '索引提交',
25
28
  'status.currentCommit': '当前提交',
29
+ 'status.branch': '分支',
30
+ 'status.detached': '(分离 HEAD)',
31
+ 'status.branchNotIndexed': "⚠️ 当前分支未索引(主索引对应 '{{primary}}';请运行 gitnexus analyze)",
26
32
  'status.status': '状态',
27
33
  'status.upToDate': '✅ 已是最新',
28
34
  'status.stale': '⚠️ 已过期(重新运行 gitnexus analyze)',
@@ -30,6 +36,9 @@ export const zhCN = {
30
36
  'clean.deletedRepo': '已删除:{{name}}({{storagePath}})',
31
37
  'clean.notFoundHere': '当前目录未找到已索引仓库。',
32
38
  'clean.deleteCurrent': '将删除该仓库的 GitNexus 索引:{{repoName}}',
39
+ 'clean.branchNotIndexed': '该仓库没有名为 “{{branch}}” 的已索引分支。',
40
+ 'clean.deleteBranch': '将删除分支索引 “{{branch}}”,路径:{{path}}',
41
+ 'clean.deletedBranch': '已删除分支索引:{{branch}}',
33
42
  'clean.lbugSidecars.state': 'LadybugDB sidecar 状态:{{state}}',
34
43
  'clean.lbugSidecars.none': '未找到已隔离的 LadybugDB missing-shadow WAL sidecar。',
35
44
  'clean.lbugSidecars.preview': '将删除 {{count}} 个已隔离的 LadybugDB missing-shadow WAL sidecar:',
@@ -156,6 +165,7 @@ export const zhCN = {
156
165
  'help.option.force.confirmation': '跳过确认提示',
157
166
  'help.option.uninstall.force': '应用更改(默认仅为预演预览)',
158
167
  'help.option.clean.all': '清理所有已索引仓库',
168
+ 'help.option.clean.branch': '仅删除指定分支的索引(不影响主索引)',
159
169
  'help.option.clean.lbugSidecars': '清理已隔离的 LadybugDB missing-shadow WAL sidecar',
160
170
  'help.option.wiki.force': '即使已是最新也强制完整重新生成',
161
171
  'help.option.wiki.provider': 'LLM 提供商:openai、openrouter、azure、custom、cursor、claude、codex 或 opencode(默认:openai)',
@@ -178,6 +188,7 @@ export const zhCN = {
178
188
  'help.option.query.limit': '最多返回的流程数(默认:5)',
179
189
  'help.option.content': '包含完整符号源码',
180
190
  'help.option.repo.target': '目标仓库',
191
+ 'help.option.branch': '将查询限定到指定分支的索引(多分支仓库)',
181
192
  'help.option.context.uid': '直接符号 UID(零歧义查找)',
182
193
  'help.option.context.file': '用于消除常见名称歧义的文件路径',
183
194
  'help.option.impact.kind': '用于消除常见名称歧义的类型过滤(如 Function、Class、Method)',
package/dist/cli/index.js CHANGED
@@ -34,6 +34,9 @@ program
34
34
  .option('--skip-agents-md', 'Skip updating the gitnexus section in AGENTS.md and CLAUDE.md')
35
35
  .option('--default-branch <branch>', 'Default branch used in the generated regression-compare example (base_ref). ' +
36
36
  'Falls back to .gitnexusrc, then auto-detected origin/HEAD, then "main".')
37
+ .option('--branch <name>', 'Index the working tree under a specific branch slot (multi-branch indexing). ' +
38
+ 'Defaults to the checked-out branch; the primary/first-indexed branch keeps the ' +
39
+ 'flat index and others get their own. Distinct from --default-branch (cosmetic base_ref).')
37
40
  .option('--no-stats', 'Omit volatile file/symbol counts from AGENTS.md and CLAUDE.md')
38
41
  .option('--skip-skills', 'Skip installing standard GitNexus skill files under .claude/skills/gitnexus/. ' +
39
42
  'Does not suppress community skills from --skills (those use .claude/skills/generated/). ' +
@@ -89,6 +92,7 @@ program
89
92
  .description('Delete GitNexus index for current repo')
90
93
  .option('-f, --force', 'Skip confirmation prompt')
91
94
  .option('--all', 'Clean all indexed repos')
95
+ .option('--branch <name>', 'Delete only the named branch index (not the primary)')
92
96
  .option('--lbug-sidecars', 'Clean quarantined LadybugDB missing-shadow WAL sidecars')
93
97
  .action(createLazyAction(() => import('./clean.js'), 'cleanCommand'));
94
98
  program
@@ -135,6 +139,7 @@ program
135
139
  .command('query <search_query>')
136
140
  .description('Search the knowledge graph for execution flows related to a concept')
137
141
  .option('-r, --repo <name>', 'Target repository (omit if only one indexed)')
142
+ .option('--branch <name>', 'Scope to a specific branch index (multi-branch repos)')
138
143
  .option('-c, --context <text>', 'Task context to improve ranking')
139
144
  .option('-g, --goal <text>', 'What you want to find')
140
145
  .option('-l, --limit <n>', 'Max processes to return (default: 5)')
@@ -144,6 +149,7 @@ program
144
149
  .command('context [name]')
145
150
  .description('360-degree view of a code symbol: callers, callees, processes')
146
151
  .option('-r, --repo <name>', 'Target repository')
152
+ .option('--branch <name>', 'Scope to a specific branch index (multi-branch repos)')
147
153
  .option('-u, --uid <uid>', 'Direct symbol UID (zero-ambiguity lookup)')
148
154
  .option('-f, --file <path>', 'File path to disambiguate common names')
149
155
  .option('--content', 'Include full symbol source code')
@@ -153,6 +159,7 @@ program
153
159
  .description('Blast radius analysis: what breaks if you change a symbol')
154
160
  .option('-d, --direction <dir>', 'upstream (dependants) or downstream (dependencies)', 'upstream')
155
161
  .option('-r, --repo <name>', 'Target repository')
162
+ .option('--branch <name>', 'Scope to a specific branch index (multi-branch repos)')
156
163
  .option('-u, --uid <uid>', 'Direct symbol UID (zero-ambiguity lookup)')
157
164
  .option('-f, --file <path>', 'File path to disambiguate common names')
158
165
  .option('--kind <kind>', 'Kind filter to disambiguate common names (e.g. Function, Class, Method)')
@@ -166,6 +173,7 @@ program
166
173
  .command('cypher <query>')
167
174
  .description('Execute raw Cypher query against the knowledge graph')
168
175
  .option('-r, --repo <name>', 'Target repository')
176
+ .option('--branch <name>', 'Scope to a specific branch index (multi-branch repos)')
169
177
  .action(createLbugLazyAction(() => import('./tool.js'), 'cypherCommand'));
170
178
  program
171
179
  .command('detect-changes')
@@ -174,6 +182,7 @@ program
174
182
  .option('-s, --scope <scope>', 'What to analyze: unstaged, staged, all, or compare', 'unstaged')
175
183
  .option('-b, --base-ref <ref>', 'Branch/commit for compare scope (e.g. main)')
176
184
  .option('-r, --repo <name>', 'Target repository')
185
+ .option('--branch <name>', 'Scope to a specific branch index (multi-branch repos)')
177
186
  .action(createLbugLazyAction(() => import('./tool.js'), 'detectChangesCommand'));
178
187
  // ─── Eval Server (persistent daemon for SWE-bench) ─────────────────
179
188
  program
package/dist/cli/list.js CHANGED
@@ -31,6 +31,8 @@ export const listCommand = async () => {
31
31
  console.log(` ${t('common.path')}: ${entry.path}`);
32
32
  console.log(` ${t('list.indexed')}: ${indexedDate}`);
33
33
  console.log(` ${t('list.commit')}: ${commitShort}`);
34
+ if (entry.branch)
35
+ console.log(` ${t('list.branch')}: ${entry.branch}`);
34
36
  console.log(` ${t('list.stats')}: ${t('list.statsValue', {
35
37
  files: stats.files ?? 0,
36
38
  symbols: stats.nodes ?? 0,
@@ -40,6 +42,16 @@ export const listCommand = async () => {
40
42
  console.log(` ${t('list.clusters')}: ${stats.communities}`);
41
43
  if (stats.processes)
42
44
  console.log(` ${t('list.processes')}: ${stats.processes}`);
45
+ // Per-branch indexes (#2106). Only rendered when extra branches were
46
+ // indexed for this path, so single-branch output is unchanged.
47
+ if (entry.branches && entry.branches.length > 0) {
48
+ console.log(` ${t('list.branchIndexes')}:`);
49
+ for (const b of entry.branches) {
50
+ const bCommit = b.lastCommit?.slice(0, 7) || t('list.unknown');
51
+ const bIndexed = new Date(b.indexedAt).toLocaleString();
52
+ console.log(` ${t('list.branchLine', { branch: b.branch, commit: bCommit, indexed: bIndexed })}`);
53
+ }
54
+ }
43
55
  console.log('');
44
56
  }
45
57
  };
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * Shows the indexing status of the current repository.
5
5
  */
6
- import { findRepo, getStoragePaths, hasKuzuIndex } from '../storage/repo-manager.js';
7
- import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
6
+ import path from 'path';
7
+ import { findRepo, getStoragePaths, loadMeta, hasKuzuIndex } from '../storage/repo-manager.js';
8
+ import { getCurrentCommit, getCurrentBranch, isGitRepo, getGitRoot } from '../storage/git.js';
8
9
  import { t } from './i18n/index.js';
9
10
  export const statusCommand = async () => {
10
11
  const cwd = process.cwd();
@@ -28,10 +29,30 @@ export const statusCommand = async () => {
28
29
  return;
29
30
  }
30
31
  const currentCommit = getCurrentCommit(repo.repoPath);
31
- const isUpToDate = currentCommit === repo.meta.lastCommit;
32
+ const currentBranch = getCurrentBranch(repo.repoPath);
33
+ // Pick the index matching the checked-out branch (#2106). The flat index
34
+ // belongs to the primary branch (repo.meta.branch); when the current branch
35
+ // differs and has its own index, report that one. Legacy/no-branch metas and
36
+ // detached HEAD fall through to the flat index (unchanged behavior).
37
+ let activeMeta = repo.meta;
38
+ let currentBranchIndexed = true;
39
+ if (currentBranch && repo.meta.branch && currentBranch !== repo.meta.branch) {
40
+ const { metaPath } = getStoragePaths(repo.repoPath, currentBranch);
41
+ const branchMeta = await loadMeta(path.dirname(metaPath));
42
+ if (branchMeta)
43
+ activeMeta = branchMeta;
44
+ else
45
+ currentBranchIndexed = false;
46
+ }
32
47
  console.log(`${t('status.repository')}: ${repo.repoPath}`);
33
- console.log(`${t('status.indexed')}: ${new Date(repo.meta.indexedAt).toLocaleString()}`);
34
- console.log(`${t('status.indexedCommit')}: ${repo.meta.lastCommit?.slice(0, 7)}`);
48
+ console.log(`${t('status.branch')}: ${currentBranch ?? t('status.detached')}`);
49
+ if (!currentBranchIndexed) {
50
+ console.log(`${t('status.status')}: ${t('status.branchNotIndexed', { primary: repo.meta.branch ?? '' })}`);
51
+ return;
52
+ }
53
+ const isUpToDate = currentCommit === activeMeta.lastCommit;
54
+ console.log(`${t('status.indexed')}: ${new Date(activeMeta.indexedAt).toLocaleString()}`);
55
+ console.log(`${t('status.indexedCommit')}: ${activeMeta.lastCommit?.slice(0, 7)}`);
35
56
  console.log(`${t('status.currentCommit')}: ${currentCommit?.slice(0, 7)}`);
36
57
  console.log(`${t('status.status')}: ${isUpToDate ? t('status.upToDate') : t('status.stale')}`);
37
58
  };
@@ -16,6 +16,7 @@
16
16
  */
17
17
  export declare function queryCommand(queryText: string, options?: {
18
18
  repo?: string;
19
+ branch?: string;
19
20
  context?: string;
20
21
  goal?: string;
21
22
  limit?: string;
@@ -23,6 +24,7 @@ export declare function queryCommand(queryText: string, options?: {
23
24
  }): Promise<void>;
24
25
  export declare function contextCommand(name: string, options?: {
25
26
  repo?: string;
27
+ branch?: string;
26
28
  file?: string;
27
29
  uid?: string;
28
30
  content?: boolean;
@@ -30,6 +32,7 @@ export declare function contextCommand(name: string, options?: {
30
32
  export declare function impactCommand(target?: string, options?: {
31
33
  direction?: string;
32
34
  repo?: string;
35
+ branch?: string;
33
36
  uid?: string;
34
37
  file?: string;
35
38
  kind?: string;
@@ -41,9 +44,11 @@ export declare function impactCommand(target?: string, options?: {
41
44
  }): Promise<void>;
42
45
  export declare function cypherCommand(query: string, options?: {
43
46
  repo?: string;
47
+ branch?: string;
44
48
  }): Promise<void>;
45
49
  export declare function detectChangesCommand(options?: {
46
50
  scope?: string;
47
51
  baseRef?: string;
48
52
  repo?: string;
53
+ branch?: string;
49
54
  }): Promise<void>;