gitnexus 1.6.13-rc.29 → 1.6.13-rc.30

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 (74) hide show
  1. package/dist/cli/clean.js +98 -107
  2. package/dist/cli/doctor.d.ts +1 -1
  3. package/dist/cli/doctor.js +7 -9
  4. package/dist/cli/i18n/en.d.ts +1 -0
  5. package/dist/cli/i18n/en.js +1 -0
  6. package/dist/cli/i18n/resources.d.ts +2 -0
  7. package/dist/cli/i18n/zh-CN.d.ts +1 -0
  8. package/dist/cli/i18n/zh-CN.js +1 -0
  9. package/dist/core/group/service.js +13 -2
  10. package/dist/mcp/local/local-backend.js +5 -30
  11. package/dist/mcp/local/query-process-attaches.d.ts +49 -0
  12. package/dist/mcp/local/query-process-attaches.js +52 -0
  13. package/dist/mcp/tools.js +2 -2
  14. package/dist/server/analyze-job.d.ts +35 -1
  15. package/dist/server/analyze-job.js +139 -18
  16. package/dist/server/analyze-launch.js +90 -17
  17. package/dist/server/api.d.ts +33 -1
  18. package/dist/server/api.js +225 -46
  19. package/dist/server/ops-snapshot.d.ts +99 -0
  20. package/dist/server/ops-snapshot.js +240 -0
  21. package/dist/server/public-repo-id.d.ts +1 -0
  22. package/dist/server/public-repo-id.js +21 -0
  23. package/dist/server/repo-projection.d.ts +1 -0
  24. package/dist/server/repo-projection.js +3 -0
  25. package/dist/server/sse-progress.d.ts +5 -3
  26. package/dist/server/sse-progress.js +18 -8
  27. package/dist/storage/git.js +1 -3
  28. package/dist/storage/map-pool.d.ts +2 -0
  29. package/dist/storage/map-pool.js +18 -0
  30. package/dist/storage/repo-manager.d.ts +11 -0
  31. package/dist/storage/repo-manager.js +12 -18
  32. package/dist/storage/stale-branch-slots.d.ts +1 -1
  33. package/dist/storage/stale-branch-slots.js +232 -105
  34. package/package.json +1 -1
  35. package/web/assets/{abnfDiagram-PAR5IXJI-8u563MEU.js → abnfDiagram-PAR5IXJI-4yVR-hMp.js} +1 -1
  36. package/web/assets/{agent-DyMwsoMH.js → agent-3KMucoRj.js} +6 -4
  37. package/web/assets/{architectureDiagram-2PNWDENW-BjWbN2PO.js → architectureDiagram-2PNWDENW-5WR38bGK.js} +1 -1
  38. package/web/assets/{chunk-32MCUW2P-DEek-0Ht.js → chunk-32MCUW2P-9JwhsLAQ.js} +1 -1
  39. package/web/assets/{chunk-CLS4B6BI-D8hARACd.js → chunk-CLS4B6BI-cE_tHXKy.js} +1 -1
  40. package/web/assets/{chunk-NF366YI6-B-9n0uvs.js → chunk-NF366YI6-DgPaL5Pp.js} +1 -1
  41. package/web/assets/{chunk-SLDQ463B-BskBwVBC.js → chunk-SLDQ463B-Da9sF2jK.js} +1 -1
  42. package/web/assets/{classDiagram-FGAMPOII-DaSs7-5O.js → classDiagram-FGAMPOII-D5xpy-7A.js} +1 -1
  43. package/web/assets/{classDiagram-v2-AI5RJEK4-DaSs7-5O.js → classDiagram-v2-AI5RJEK4-D5xpy-7A.js} +1 -1
  44. package/web/assets/{cynefinDiagram-2OOWUY4M-DjbooVji.js → cynefinDiagram-2OOWUY4M-DABJiD-b.js} +1 -1
  45. package/web/assets/{dagre-MPVFI544-B-wqpxUN.js → dagre-MPVFI544-pYMKtWlh.js} +1 -1
  46. package/web/assets/{diagram-AE4NWXFW-Bkmf_O6L.js → diagram-AE4NWXFW-CZ04hcNr.js} +1 -1
  47. package/web/assets/{diagram-BUJID5UG-CbUC2Fie.js → diagram-BUJID5UG-D0nf-uew.js} +1 -1
  48. package/web/assets/{diagram-C44CZ5NI-dwPTAnfj.js → diagram-C44CZ5NI-ypaw31pk.js} +1 -1
  49. package/web/assets/{diagram-EYOLEXXS-OD8zf2mA.js → diagram-EYOLEXXS-BxDZOcdK.js} +1 -1
  50. package/web/assets/{ebnfDiagram-FTPZ3PV6-u4D1E694.js → ebnfDiagram-FTPZ3PV6-Oe7rPBmK.js} +1 -1
  51. package/web/assets/{erDiagram-H3ERR26V-BBtjOXBa.js → erDiagram-H3ERR26V-DSuDKH7X.js} +1 -1
  52. package/web/assets/{flowDiagram-YHGXBVSY-C7uNeQLW.js → flowDiagram-YHGXBVSY-DQisgp3A.js} +1 -1
  53. package/web/assets/index-6K-W6Rj-.css +2 -0
  54. package/web/assets/{index-BvcE-KTJ.js → index-fyU3YunG.js} +69 -68
  55. package/web/assets/{infoDiagram-LV547AFH-BHH9OuWU.js → infoDiagram-LV547AFH-Ce8Ylfhp.js} +1 -1
  56. package/web/assets/{ishikawaDiagram-PUJQGNDS-uat_n043.js → ishikawaDiagram-PUJQGNDS-Cf1XsKfx.js} +1 -1
  57. package/web/assets/{kanban-definition-JSQERAZB-Cc-n7-iv.js → kanban-definition-JSQERAZB-D1ygctat.js} +1 -1
  58. package/web/assets/{mindmap-definition-VBJCLQLM-CcnYi775.js → mindmap-definition-VBJCLQLM-EB7Llk3q.js} +1 -1
  59. package/web/assets/{node.browser-Dx1ppNym.js → node.browser-BISeS8dh.js} +1 -1
  60. package/web/assets/{pegDiagram-7WILOQTJ-CjsU2E8I.js → pegDiagram-7WILOQTJ-CyGZpmyT.js} +1 -1
  61. package/web/assets/{pieDiagram-EFA7CBSB-DFarVbT5.js → pieDiagram-EFA7CBSB-fl4j0AVe.js} +1 -1
  62. package/web/assets/{railroadDiagram-5NIQVNLQ-BOc4TKmb.js → railroadDiagram-5NIQVNLQ-Bztg21cQ.js} +1 -1
  63. package/web/assets/{requirementDiagram-SXREGOKZ-IYeU8wzI.js → requirementDiagram-SXREGOKZ-Dt_6vjm_.js} +1 -1
  64. package/web/assets/{sequenceDiagram-52ZFIFDL-Bvb237FL.js → sequenceDiagram-52ZFIFDL-Dl4-Ga10.js} +1 -1
  65. package/web/assets/{stateDiagram-3QAKPIVI-Bj5eSjtk.js → stateDiagram-3QAKPIVI-BJGGrzEz.js} +1 -1
  66. package/web/assets/{stateDiagram-v2-HN2P4RDZ-CgNVPo1K.js → stateDiagram-v2-HN2P4RDZ-BDlFhXzb.js} +1 -1
  67. package/web/assets/{swimlanes-QLVP42QD-BBXlMTuI.js → swimlanes-QLVP42QD-CCly8LhV.js} +1 -1
  68. package/web/assets/{swimlanesDiagram-JKPDBFCN-g8DoalWG.js → swimlanesDiagram-JKPDBFCN-bJms4nVX.js} +1 -1
  69. package/web/assets/{timeline-definition-GS45XAXW-Dkl4ie3U.js → timeline-definition-GS45XAXW-BsUXbFPa.js} +1 -1
  70. package/web/assets/{vennDiagram-QIQU2ZRJ-8swT0k92.js → vennDiagram-QIQU2ZRJ-C4UUmZOE.js} +1 -1
  71. package/web/assets/{wardleyDiagram-U6YNM66N-CtH6gYM2.js → wardleyDiagram-U6YNM66N-CUCmW8Uk.js} +1 -1
  72. package/web/assets/{xychartDiagram-XJAZEDNM-BkWLFLb6.js → xychartDiagram-XJAZEDNM-BB5mKEY8.js} +1 -1
  73. package/web/index.html +2 -2
  74. package/web/assets/index-NLQtvayY.css +0 -2
package/dist/cli/clean.js CHANGED
@@ -14,136 +14,127 @@ import { listLocalHeads } from '../storage/git.js';
14
14
  import { isContainedBranchDir, isDeleteCandidate, listStaleBranchSlots, removeBranchSlot, staleListingBlock, } from '../storage/stale-branch-slots.js';
15
15
  import { cleanParkedLbugSidecars, inspectLbugSidecars, listParkedLbugSidecars, } from '../core/lbug/sidecar-recovery.js';
16
16
  import { t } from './i18n/index.js';
17
- export const cleanCommand = async (options) => {
18
- // --stale: reclaim leftover per-branch slots whose recorded branch is not
19
- // a live local head (#3331). Exclusive arm before --branch.
20
- if (options?.stale) {
21
- const cwd = process.cwd();
22
- const repo = await findRepo(cwd);
23
- if (!repo) {
24
- console.log(t('clean.notFoundHere'));
25
- return;
26
- }
27
- const entries = await listRegisteredRepos();
28
- const entry = findRegistryEntryByRepoPath(entries, repo.repoPath);
29
- let storagePath;
30
- try {
31
- storagePath = await requireDeletableStoragePath({
17
+ const resolveOwnedCwdStorage = async (refusePrefix) => {
18
+ const repo = await findRepo(process.cwd());
19
+ if (!repo) {
20
+ console.log(t('clean.notFoundHere'));
21
+ return null;
22
+ }
23
+ try {
24
+ const [entries, storagePath] = await Promise.all([
25
+ listRegisteredRepos(),
26
+ requireDeletableStoragePath({
32
27
  path: repo.repoPath,
33
28
  storagePath: repo.storagePath,
34
- });
35
- }
36
- catch (err) {
37
- if (err instanceof StorageDeletionError) {
38
- logger.error(`Refusing to clean leftover branch indexes: ${err.message}`);
39
- return;
40
- }
41
- throw err;
42
- }
43
- const slots = await listStaleBranchSlots({
44
- repoPath: repo.repoPath,
29
+ }),
30
+ ]);
31
+ return {
32
+ repo,
33
+ entry: findRegistryEntryByRepoPath(entries, repo.repoPath),
45
34
  storagePath,
46
- branches: entry?.branches,
47
- includeSize: !options.force,
48
- });
49
- const listingBlock = staleListingBlock(slots);
50
- if (listingBlock === 'heads-unavailable') {
51
- console.log(t('clean.stale.headsUnavailable'));
52
- for (const slot of slots.filter((row) => row.reason === 'heads-unavailable')) {
53
- console.log(` - ${formatStaleSlotLine(slot)}`);
54
- }
55
- return;
35
+ };
36
+ }
37
+ catch (err) {
38
+ if (err instanceof StorageDeletionError) {
39
+ logger.error(`${refusePrefix}${err.message}`);
40
+ return null;
56
41
  }
57
- if (listingBlock === 'listing-failed') {
58
- console.log(t('clean.stale.listingFailed'));
42
+ throw err;
43
+ }
44
+ };
45
+ const printStaleSlotLines = (message, slots) => {
46
+ console.log(message);
47
+ for (const slot of slots) {
48
+ console.log(` - ${formatStaleSlotLine(slot)}`);
49
+ }
50
+ };
51
+ const cleanStaleBranchSlots = async (force) => {
52
+ const owned = await resolveOwnedCwdStorage('Refusing to clean leftover branch indexes: ');
53
+ if (!owned)
54
+ return;
55
+ const { repo, entry, storagePath } = owned;
56
+ const slots = await listStaleBranchSlots({
57
+ repoPath: repo.repoPath,
58
+ storagePath,
59
+ branches: entry?.branches,
60
+ includeSize: !force,
61
+ });
62
+ const listingBlock = staleListingBlock(slots);
63
+ if (listingBlock === 'heads-unavailable') {
64
+ printStaleSlotLines(t('clean.stale.headsUnavailable'), slots.filter((row) => row.reason === 'heads-unavailable'));
65
+ return;
66
+ }
67
+ if (listingBlock === 'listing-failed') {
68
+ console.log(t('clean.stale.listingFailed'));
69
+ return;
70
+ }
71
+ const candidates = slots.filter(isDeleteCandidate);
72
+ const probeFailed = slots.filter((slot) => slot.reason === 'probe-failed');
73
+ if (candidates.length === 0) {
74
+ if (probeFailed.length > 0) {
75
+ printStaleSlotLines(t('clean.stale.probeFailed'), probeFailed);
59
76
  return;
60
77
  }
61
- const candidates = slots.filter(isDeleteCandidate);
62
- const probeFailed = slots.filter((slot) => slot.reason === 'probe-failed');
63
- const printProbeFailed = () => {
64
- console.log(t('clean.stale.probeFailed'));
65
- for (const slot of probeFailed) {
66
- console.log(` - ${formatStaleSlotLine(slot)}`);
67
- }
68
- };
69
- if (candidates.length === 0) {
70
- if (probeFailed.length > 0) {
71
- printProbeFailed();
72
- return;
73
- }
74
- console.log(t('clean.stale.none'));
75
- return;
78
+ console.log(t('clean.stale.none'));
79
+ return;
80
+ }
81
+ if (!force) {
82
+ printStaleSlotLines(t('clean.stale.preview', { count: candidates.length }), candidates);
83
+ if (probeFailed.length > 0) {
84
+ printStaleSlotLines(t('clean.stale.probeFailed'), probeFailed);
76
85
  }
77
- if (!options.force) {
78
- console.log(t('clean.stale.preview', { count: candidates.length }));
79
- for (const slot of candidates) {
80
- console.log(` - ${formatStaleSlotLine(slot)}`);
81
- }
82
- if (probeFailed.length > 0) {
83
- printProbeFailed();
84
- }
85
- console.log(`\n${t('common.runForceConfirm')}`);
86
+ console.log(`\n${t('common.runForceConfirm')}`);
87
+ return;
88
+ }
89
+ let deletedAny = false;
90
+ for (const slot of candidates) {
91
+ const heads = listLocalHeads(repo.repoPath);
92
+ if (heads === null) {
93
+ console.log(deletedAny ? t('clean.stale.remainingSkipped') : t('clean.stale.headsUnavailable'));
86
94
  return;
87
95
  }
88
- for (const slot of candidates) {
89
- const heads = listLocalHeads(repo.repoPath);
90
- if (heads === null) {
91
- console.log(t('clean.stale.headsUnavailable'));
92
- return;
93
- }
94
- if (heads.includes(slot.branch)) {
95
- console.log(t('clean.stale.skippedLive', { branch: slot.branch }));
96
- continue;
97
- }
98
- const result = await removeBranchSlot({
99
- repoPath: repo.repoPath,
100
- storagePath,
101
- branch: slot.branch,
102
- dir: slot.dir,
103
- });
104
- if (!result.ok) {
105
- console.log(t('clean.stale.failed', { branch: slot.branch }));
106
- logger.error({ err: result.error }, 'Failed to delete leftover branch index:');
107
- continue;
108
- }
109
- console.log(t('clean.stale.deleted', { branch: slot.branch }));
96
+ if (heads.includes(slot.branch)) {
97
+ console.log(t('clean.stale.skippedLive', { branch: slot.branch }));
98
+ continue;
110
99
  }
111
- if (probeFailed.length > 0) {
112
- printProbeFailed();
100
+ const result = await removeBranchSlot({
101
+ repoPath: repo.repoPath,
102
+ storagePath,
103
+ branch: slot.branch,
104
+ dir: slot.dir,
105
+ });
106
+ if (!result.ok) {
107
+ console.log(t('clean.stale.failed', { branch: slot.branch }));
108
+ logger.error({ err: result.error }, 'Failed to delete leftover branch index:');
109
+ continue;
113
110
  }
111
+ deletedAny = true;
112
+ console.log(t('clean.stale.deleted', { branch: slot.branch }));
113
+ }
114
+ if (probeFailed.length > 0) {
115
+ printStaleSlotLines(t('clean.stale.probeFailed'), probeFailed);
116
+ }
117
+ };
118
+ export const cleanCommand = async (options) => {
119
+ // --stale: reclaim leftover per-branch slots whose recorded branch is not
120
+ // a live local head (#3331). Exclusive arm before --branch.
121
+ if (options?.stale) {
122
+ await cleanStaleBranchSlots(options.force === true);
114
123
  return;
115
124
  }
116
125
  // --branch <name>: remove a single non-primary branch's index (#2106 R7).
117
126
  // Resolve against the RECORDED branches[] summary (never by slugging the
118
127
  // user's raw input, which can disagree with the index-time-sanitized label).
119
128
  if (options?.branch) {
120
- const cwd = process.cwd();
121
- const repo = await findRepo(cwd);
122
- if (!repo) {
123
- console.log(t('clean.notFoundHere'));
129
+ const owned = await resolveOwnedCwdStorage('Refusing to clean branch index: ');
130
+ if (!owned)
124
131
  return;
125
- }
126
- const entries = await listRegisteredRepos();
127
- const entry = findRegistryEntryByRepoPath(entries, repo.repoPath);
132
+ const { repo, entry, storagePath } = owned;
128
133
  const summary = entry?.branches?.find((b) => b.branch === options.branch);
129
134
  if (!summary) {
130
135
  console.log(t('clean.branchNotIndexed', { branch: options.branch }));
131
136
  return;
132
137
  }
133
- let storagePath;
134
- try {
135
- storagePath = await requireDeletableStoragePath({
136
- path: repo.repoPath,
137
- storagePath: repo.storagePath,
138
- });
139
- }
140
- catch (err) {
141
- if (err instanceof StorageDeletionError) {
142
- logger.error(`Refusing to clean branch index: ${err.message}`);
143
- return;
144
- }
145
- throw err;
146
- }
147
138
  const { lbugPath } = getStoragePaths(repo.repoPath, summary.branch, storagePath);
148
139
  const branchDir = path.dirname(lbugPath);
149
140
  if (!isContainedBranchDir(storagePath, branchDir)) {
@@ -66,7 +66,7 @@ export declare function nativeStatusLine(check: NativeCheckResult): string;
66
66
  * When heads cannot be listed, do not title rows as orphaned or name
67
67
  * `clean --stale` (#3337): that command refuses to delete in the same state.
68
68
  */
69
- export declare function orphanedBranchSlotDoctorLines(slots: StaleBranchSlot[]): string[];
69
+ export declare function leftoverBranchSlotDoctorLines(slots: StaleBranchSlot[]): string[];
70
70
  export declare function cachedUpdateDoctorLine(options: {
71
71
  installedVersion: string;
72
72
  eligible: boolean;
@@ -153,7 +153,7 @@ export function nativeStatusLine(check) {
153
153
  * When heads cannot be listed, do not title rows as orphaned or name
154
154
  * `clean --stale` (#3337): that command refuses to delete in the same state.
155
155
  */
156
- export function orphanedBranchSlotDoctorLines(slots) {
156
+ export function leftoverBranchSlotDoctorLines(slots) {
157
157
  if (slots.length === 0)
158
158
  return [];
159
159
  const listingBlock = staleListingBlock(slots);
@@ -315,24 +315,22 @@ export const doctorCommand = async () => {
315
315
  console.log(` ${padDisplayEnd('', 12)}${cudaRedirect.detail}`);
316
316
  }
317
317
  }
318
- // Doctor stays runtime-global. Add only a cwd leftover-slot section when
319
- // this process is inside an indexed repo. Look up that repo's registry row
320
- // for recorded branch slugs; do not report leftovers for every registered
321
- // repo, and never delete.
322
- const [cwdRepo, entries] = await Promise.all([findRepo(process.cwd()), listRegisteredRepos()]);
318
+ // Cwd leftover-slot report only; never delete.
319
+ const cwdRepo = await findRepo(process.cwd());
323
320
  if (!cwdRepo)
324
321
  return;
322
+ const entries = await listRegisteredRepos();
325
323
  const entry = findRegistryEntryByRepoPath(entries, cwdRepo.repoPath);
326
324
  const slots = await listStaleBranchSlots({
327
325
  repoPath: cwdRepo.repoPath,
328
326
  storagePath: cwdRepo.storagePath,
329
327
  branches: entry?.branches,
330
328
  });
331
- const orphanLines = orphanedBranchSlotDoctorLines(slots);
332
- if (orphanLines.length === 0)
329
+ const leftoverLines = leftoverBranchSlotDoctorLines(slots);
330
+ if (leftoverLines.length === 0)
333
331
  return;
334
332
  console.log('');
335
- for (const line of orphanLines) {
333
+ for (const line of leftoverLines) {
336
334
  console.log(line);
337
335
  }
338
336
  };
@@ -59,6 +59,7 @@ export declare const en: {
59
59
  readonly 'clean.stale.item': '{{branch}} {{reason}} {{path}} {{size}}';
60
60
  readonly 'clean.stale.registryOnlyPath': '(registry only)';
61
61
  readonly 'clean.stale.headsUnavailable': 'Could not list local heads; leftover branch indexes were not deleted. Re-run `gitnexus clean --stale` when git is available.';
62
+ readonly 'clean.stale.remainingSkipped': 'Could not list local heads; remaining leftover branch indexes were skipped. Re-run `gitnexus clean --stale` when git is available.';
62
63
  readonly 'clean.stale.listingFailed': 'Could not read leftover branch index directories; leftover indexes were not deleted. Check permissions on the branches/ directory and re-run `gitnexus clean --stale`.';
63
64
  readonly 'clean.stale.probeFailed': 'Could not inspect leftover branch index path(s); those slots were not deleted.';
64
65
  readonly 'clean.stale.deleted': 'Deleted leftover branch index: {{branch}}';
@@ -59,6 +59,7 @@ export const en = {
59
59
  'clean.stale.item': '{{branch}} {{reason}} {{path}} {{size}}',
60
60
  'clean.stale.registryOnlyPath': '(registry only)',
61
61
  'clean.stale.headsUnavailable': 'Could not list local heads; leftover branch indexes were not deleted. Re-run `gitnexus clean --stale` when git is available.',
62
+ 'clean.stale.remainingSkipped': 'Could not list local heads; remaining leftover branch indexes were skipped. Re-run `gitnexus clean --stale` when git is available.',
62
63
  'clean.stale.listingFailed': 'Could not read leftover branch index directories; leftover indexes were not deleted. Check permissions on the branches/ directory and re-run `gitnexus clean --stale`.',
63
64
  'clean.stale.probeFailed': 'Could not inspect leftover branch index path(s); those slots were not deleted.',
64
65
  'clean.stale.deleted': 'Deleted leftover branch index: {{branch}}',
@@ -60,6 +60,7 @@ export declare const cliResources: {
60
60
  readonly 'clean.stale.item': '{{branch}} {{reason}} {{path}} {{size}}';
61
61
  readonly 'clean.stale.registryOnlyPath': '(registry only)';
62
62
  readonly 'clean.stale.headsUnavailable': 'Could not list local heads; leftover branch indexes were not deleted. Re-run `gitnexus clean --stale` when git is available.';
63
+ readonly 'clean.stale.remainingSkipped': 'Could not list local heads; remaining leftover branch indexes were skipped. Re-run `gitnexus clean --stale` when git is available.';
63
64
  readonly 'clean.stale.listingFailed': 'Could not read leftover branch index directories; leftover indexes were not deleted. Check permissions on the branches/ directory and re-run `gitnexus clean --stale`.';
64
65
  readonly 'clean.stale.probeFailed': 'Could not inspect leftover branch index path(s); those slots were not deleted.';
65
66
  readonly 'clean.stale.deleted': 'Deleted leftover branch index: {{branch}}';
@@ -366,6 +367,7 @@ export declare const cliResources: {
366
367
  'clean.stale.item': string;
367
368
  'clean.stale.registryOnlyPath': string;
368
369
  'clean.stale.headsUnavailable': string;
370
+ 'clean.stale.remainingSkipped': string;
369
371
  'clean.stale.listingFailed': string;
370
372
  'clean.stale.probeFailed': string;
371
373
  'clean.stale.deleted': string;
@@ -59,6 +59,7 @@ export declare const zhCN: {
59
59
  'clean.stale.item': string;
60
60
  'clean.stale.registryOnlyPath': string;
61
61
  'clean.stale.headsUnavailable': string;
62
+ 'clean.stale.remainingSkipped': string;
62
63
  'clean.stale.listingFailed': string;
63
64
  'clean.stale.probeFailed': string;
64
65
  'clean.stale.deleted': string;
@@ -59,6 +59,7 @@ export const zhCN = {
59
59
  'clean.stale.item': '{{branch}} {{reason}} {{path}} {{size}}',
60
60
  'clean.stale.registryOnlyPath': '(仅注册表)',
61
61
  'clean.stale.headsUnavailable': '无法列出本地分支,因此未删除残留分支索引。请在 git 可用后重新运行 `gitnexus clean --stale`。',
62
+ 'clean.stale.remainingSkipped': '无法列出本地分支,因此已跳过其余残留分支索引。请在 git 可用后重新运行 `gitnexus clean --stale`。',
62
63
  'clean.stale.listingFailed': '无法读取残留分支索引目录,因此未删除残留索引。请检查 branches/ 目录权限后重新运行 `gitnexus clean --stale`。',
63
64
  'clean.stale.probeFailed': '无法检查残留分支索引路径,因此未删除这些槽位。',
64
65
  'clean.stale.deleted': '已删除残留分支索引:{{branch}}',
@@ -41,8 +41,19 @@ function isStoredContract(raw) {
41
41
  }
42
42
  function filterQueryByServicePrefix(queryResult, servicePrefix) {
43
43
  const symbols = (queryResult.process_symbols || []).filter((s) => fileMatchesServicePrefix(typeof s.filePath === 'string' ? s.filePath : undefined, servicePrefix));
44
- const allowed = new Set(symbols.map((s) => String(s.process_id ?? '')).filter(Boolean));
45
- const processes = (queryResult.processes || []).filter((p) => allowed.has(String(p.id)));
44
+ const countByProcess = new Map();
45
+ for (const symbol of symbols) {
46
+ const pid = String(symbol.process_id ?? '');
47
+ if (!pid)
48
+ continue;
49
+ countByProcess.set(pid, (countByProcess.get(pid) ?? 0) + 1);
50
+ }
51
+ const processes = (queryResult.processes || []).filter((p) => countByProcess.has(String(p.id)));
52
+ for (const process of processes) {
53
+ const id = String(process.id ?? '');
54
+ if (id)
55
+ process.symbol_count = countByProcess.get(id) ?? 0;
56
+ }
46
57
  return { processes, process_symbols: symbols };
47
58
  }
48
59
  function isCrossLink(raw) {
@@ -15,6 +15,7 @@ import { querySpringAopMetadata } from './aop-metadata.js';
15
15
  import { queryConvexDispatchMetadata } from './convex-metadata.js';
16
16
  import { isValidQueryParams } from '../../core/lbug/query-params.js';
17
17
  import { toDisplayLine } from './line-display.js';
18
+ import { shapeQueryProcessAttaches } from './query-process-attaches.js';
18
19
  import { LBUG_ID_PROBE_BATCH_SIZE, LBUG_QUERY_BATCH_SIZE } from '../../core/lbug/query-batch.js';
19
20
  import { chunk, mapConcurrent } from '../../lib/utils.js';
20
21
  import { pathSuffixOf } from './path-predicate.js';
@@ -2750,35 +2751,9 @@ export class LocalBackend {
2750
2751
  }
2751
2752
  // Step 4: Build response
2752
2753
  timer.start('formatting');
2753
- const processes = rankedProcesses.map((p) => ({
2754
- id: p.id,
2755
- summary: p.heuristicLabel || p.label,
2756
- priority: Math.round(p.priority * 1000) / 1000,
2757
- symbol_count: p.symbols.length,
2758
- process_type: p.processType,
2759
- step_count: p.stepCount,
2760
- ...(p.routes && p.routes.length > 0
2761
- ? {
2762
- route: p.routes[0].url,
2763
- method: p.routes[0].method || undefined,
2764
- routes: p.routes,
2765
- }
2766
- : {}),
2767
- ...(chainByProcessId.has(p.id) ? { chain: chainByProcessId.get(p.id) } : {}),
2768
- }));
2769
- const processSymbols = rankedProcesses.flatMap((p) => p.symbols.slice(0, maxSymbolsPerProcess).map((s) => ({
2770
- ...s,
2771
- // mark the entry-point symbol so an agent reading the
2772
- // process can tell procedure vs. workflow vs. helper at a glance.
2773
- ...(p.entryPointId && s.id === p.entryPointId ? { is_entry_point: true } : {}),
2774
- })));
2775
- // Deduplicate process_symbols by id
2776
- const seen = new Set();
2777
- const dedupedSymbols = processSymbols.filter((s) => {
2778
- if (seen.has(s.id))
2779
- return false;
2780
- seen.add(s.id);
2781
- return true;
2754
+ const { processes, process_symbols } = shapeQueryProcessAttaches(rankedProcesses, {
2755
+ maxSymbolsPerProcess,
2756
+ chainByProcessId,
2782
2757
  });
2783
2758
  timer.stop(); // formatting
2784
2759
  // End-to-end wall time — deliberately a separate mark so callers can
@@ -2941,7 +2916,7 @@ export class LocalBackend {
2941
2916
  const ftsPartial = ftsUsed && !!ftsQueryErrors;
2942
2917
  return {
2943
2918
  processes,
2944
- process_symbols: dedupedSymbols,
2919
+ process_symbols,
2945
2920
  definitions: definitions.slice(0, 20), // cap standalone definitions
2946
2921
  timing,
2947
2922
  ...(contentAvailability ? { contentAvailability } : {}),
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Shape ranked query processes into the MCP `process_symbols` attach list
3
+ * and honest per-process `symbol_count` (#3351).
4
+ *
5
+ * Aggregation already pushes a hub onto every owning process. This helper
6
+ * is the last shaping step: slice each process to `max_symbols`, keep one
7
+ * row per `(id, process_id)`, then set `symbol_count` from those rows.
8
+ */
9
+ export type QueryProcessAttach = {
10
+ id: string;
11
+ process_id: string;
12
+ is_entry_point?: boolean;
13
+ } & Record<string, unknown>;
14
+ export type RankedQueryProcess<S extends QueryProcessAttach = QueryProcessAttach> = {
15
+ id: string;
16
+ label?: string;
17
+ heuristicLabel?: string;
18
+ processType?: string;
19
+ stepCount?: number;
20
+ entryPointId?: string;
21
+ priority: number;
22
+ symbols: S[];
23
+ routes?: Array<{
24
+ url: string;
25
+ method?: string;
26
+ }>;
27
+ };
28
+ export type QueryProcessCard = {
29
+ id: string;
30
+ summary: string;
31
+ priority: number;
32
+ symbol_count: number;
33
+ process_type?: string;
34
+ step_count?: number;
35
+ route?: string;
36
+ method?: string;
37
+ routes?: Array<{
38
+ url: string;
39
+ method?: string;
40
+ }>;
41
+ chain?: unknown;
42
+ };
43
+ export declare function shapeQueryProcessAttaches<S extends QueryProcessAttach>(rankedProcesses: Array<RankedQueryProcess<S>>, options: {
44
+ maxSymbolsPerProcess: number;
45
+ chainByProcessId?: ReadonlyMap<string, unknown>;
46
+ }): {
47
+ processes: QueryProcessCard[];
48
+ process_symbols: S[];
49
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Shape ranked query processes into the MCP `process_symbols` attach list
3
+ * and honest per-process `symbol_count` (#3351).
4
+ *
5
+ * Aggregation already pushes a hub onto every owning process. This helper
6
+ * is the last shaping step: slice each process to `max_symbols`, keep one
7
+ * row per `(id, process_id)`, then set `symbol_count` from those rows.
8
+ */
9
+ /**
10
+ * Pair key for attach rows. Both `id` and `process_id` routinely contain
11
+ * colons (`func:validate`, `proc:login-flow`), so a `:` join would collide.
12
+ */
13
+ function attachPairKey(id, processId) {
14
+ return `${id}\0${processId}`;
15
+ }
16
+ export function shapeQueryProcessAttaches(rankedProcesses, options) {
17
+ const { maxSymbolsPerProcess, chainByProcessId } = options;
18
+ const seen = new Set();
19
+ const process_symbols = [];
20
+ const countByProcess = new Map();
21
+ for (const p of rankedProcesses) {
22
+ for (const s of p.symbols.slice(0, maxSymbolsPerProcess)) {
23
+ const key = attachPairKey(s.id, s.process_id);
24
+ if (seen.has(key))
25
+ continue;
26
+ seen.add(key);
27
+ const row = p.entryPointId && s.id === p.entryPointId ? { ...s, is_entry_point: true } : s;
28
+ process_symbols.push(row);
29
+ countByProcess.set(s.process_id, (countByProcess.get(s.process_id) ?? 0) + 1);
30
+ }
31
+ }
32
+ const processes = rankedProcesses.map((p) => {
33
+ const card = {
34
+ id: p.id,
35
+ summary: p.heuristicLabel || p.label || '',
36
+ priority: Math.round(p.priority * 1000) / 1000,
37
+ symbol_count: countByProcess.get(p.id) ?? 0,
38
+ process_type: p.processType,
39
+ step_count: p.stepCount,
40
+ };
41
+ if (p.routes && p.routes.length > 0) {
42
+ card.route = p.routes[0].url;
43
+ card.method = p.routes[0].method || undefined;
44
+ card.routes = p.routes;
45
+ }
46
+ const chain = chainByProcessId?.get(p.id);
47
+ if (chain !== undefined)
48
+ card.chain = chain;
49
+ return card;
50
+ });
51
+ return { processes, process_symbols };
52
+ }
package/dist/mcp/tools.js CHANGED
@@ -100,14 +100,14 @@ specify the "repo" parameter explicitly.`,
100
100
  {
101
101
  name: 'query',
102
102
  description: `Query the code knowledge graph for execution flows related to a concept.
103
- Returns processes (call chains) ranked by relevance, each with its symbols and file locations.
103
+ Returns ranked processes plus a flat process_symbols list. Join processes[].id to process_symbols[].process_id.
104
104
 
105
105
  WHEN TO USE: Understanding how code works together. Use this when you need execution flows and relationships, not just file matches. Complements grep/IDE search.
106
106
  AFTER THIS: Use context() on a specific symbol for 360-degree view (callers, callees, categorized refs).
107
107
 
108
108
  Returns results grouped by process (execution flow):
109
109
  - processes: ranked execution flows with relevance priority. When a process has an HTTP endpoint, each item includes route and method string aliases plus routes: [{ url, method? }] (same shape as context). When chain_depth > 0, each item also includes chain — layered upstream callers + downstream callees from the process entry symbol (same BFS as context({chain_depth})).
110
- - process_symbols: search-hit symbols in those flows with file locations and module (functional area). When the process entry is among those hits, it is marked is_entry_point: true.
110
+ - process_symbols: search-hit symbols in those flows with file locations and module (functional area). On the single-repo envelope { processes, process_symbols, definitions }: One row per (id, process_id) — the same symbol id may appear under more than one process. Join a process to its rows by process_id; symbol_count is the number of those rows. When the process entry is among those hits, it is marked is_entry_point: true. A repo of "@<group>" returns { group, query, results, per_repo } and does not include process_symbols. results[].symbol_count is the member's post-slice attach count; when service is set, it counts only attaches under that prefix. To get process_symbols for one member, query again with repo "@<group>/<memberPath>" (member path from group.yaml, or results[]._repo).
111
111
  - definitions: standalone types/interfaces not in any process. Keyword hits on Route URLs (route_fts) are bridged to their handler via HANDLES_ROUTE (handlerSymbolId, routes) when the edge exists; use route_map({route}) for the full HTTP surface.
112
112
 
113
113
  Hybrid ranking: BM25 keyword + semantic vector search, ranked by Reciprocal Rank Fusion.
@@ -74,6 +74,9 @@ export declare class JobManager {
74
74
  private children;
75
75
  private abortControllers;
76
76
  private timeouts;
77
+ private cancelGraceTimers;
78
+ /** Cancel reason to apply when a still-running worker exits. */
79
+ private pendingCancelReasons;
77
80
  private emitter;
78
81
  private cleanupTimer;
79
82
  constructor();
@@ -99,13 +102,44 @@ export declare class JobManager {
99
102
  updateJob(id: string, update: Partial<Pick<AnalyzeJob, 'status' | 'progress' | 'error' | 'partial' | 'repoPath' | 'repoName' | 'completedAt'>>): void;
100
103
  /** Register a child process for a job — enables cancellation and timeout. */
101
104
  registerChild(jobId: string, child: ChildProcess): void;
105
+ /** True while cancel was requested and the worker has not exited yet. */
106
+ hasPendingCancel(jobId: string): boolean;
107
+ /**
108
+ * Drop a registered child without waiting for `exit`. Spawn failures emit
109
+ * `error` and never `exit`, which would otherwise leave `isSlotOccupied`
110
+ * true forever after the job is already failed.
111
+ */
112
+ releaseChild(jobId: string): void;
113
+ /**
114
+ * Apply a stored cancel reason if one is pending. Returns true when a reason
115
+ * was consumed. Used by the worker-exit handler and by analyze-launch when
116
+ * the child reports a generic cancel IPC — that message must not overwrite
117
+ * the caller's reason (timeout vs user cancel).
118
+ */
119
+ applyPendingCancel(jobId: string): boolean;
102
120
  /** Register cancellable in-process work for a job. */
103
121
  registerAbortController(jobId: string, controller: AbortController): void;
104
- /** Cancel a running job — sends SIGTERM to child process. */
122
+ /**
123
+ * Cancel a running job.
124
+ *
125
+ * The worker is asked to stop over IPC first (`{ type: 'cancel' }`), which
126
+ * lets it reach a JS-visible safe point and checkpoint before exiting —
127
+ * the same cross-platform control path `core/auto-sync` uses. A signal is
128
+ * sent only as a bounded fallback: on Windows `child.kill('SIGTERM')` is a
129
+ * forceful termination (Node ignores the signal name there), so leading
130
+ * with it could kill the worker mid LadybugDB write.
131
+ */
105
132
  cancelJob(jobId: string, reason?: string): boolean;
106
133
  /** Subscribe to progress events for a job. Returns unsubscribe function. */
107
134
  onProgress(jobId: string, listener: (progress: AnalyzeJobProgress) => void): () => void;
135
+ /**
136
+ * Ask a worker to shut down: IPC cancel now, signal after a grace period if
137
+ * it has not exited on its own. The grace timer is cleared by the child's
138
+ * `exit` handler registered in `registerChild`.
139
+ */
140
+ private requestChildShutdown;
108
141
  dispose(): void;
109
142
  private isTerminal;
143
+ private isSlotOccupied;
110
144
  private cleanup;
111
145
  }