agentsys 5.0.0 → 5.0.2

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 (202) hide show
  1. package/.claude-plugin/marketplace.json +13 -13
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +19 -2
  4. package/README.md +1 -0
  5. package/adapters/README.md +1 -1
  6. package/adapters/codex/skills/consult/SKILL.md +3 -2
  7. package/adapters/codex/skills/next-task/SKILL.md +8 -8
  8. package/adapters/opencode/agents/consult-agent.md +1 -1
  9. package/adapters/opencode/agents/delivery-validator.md +1 -1
  10. package/adapters/opencode/agents/implementation-agent.md +1 -1
  11. package/adapters/opencode/agents/worktree-manager.md +3 -3
  12. package/adapters/opencode/commands/consult.md +3 -2
  13. package/adapters/opencode/commands/next-task.md +7 -7
  14. package/adapters/opencode/skills/agnix/SKILL.md +1 -1
  15. package/adapters/opencode/skills/consult/SKILL.md +16 -4
  16. package/adapters/opencode/skills/deslop/SKILL.md +1 -1
  17. package/adapters/opencode/skills/discover-tasks/SKILL.md +2 -2
  18. package/adapters/opencode/skills/drift-analysis/SKILL.md +1 -1
  19. package/adapters/opencode/skills/enhance-agent-prompts/SKILL.md +1 -1
  20. package/adapters/opencode/skills/enhance-claude-memory/SKILL.md +1 -1
  21. package/adapters/opencode/skills/enhance-cross-file/SKILL.md +1 -1
  22. package/adapters/opencode/skills/enhance-docs/SKILL.md +1 -1
  23. package/adapters/opencode/skills/enhance-hooks/SKILL.md +1 -1
  24. package/adapters/opencode/skills/enhance-orchestrator/SKILL.md +1 -1
  25. package/adapters/opencode/skills/enhance-plugins/SKILL.md +1 -1
  26. package/adapters/opencode/skills/enhance-prompts/SKILL.md +1 -1
  27. package/adapters/opencode/skills/enhance-skills/SKILL.md +1 -1
  28. package/adapters/opencode/skills/learn/SKILL.md +1 -1
  29. package/adapters/opencode/skills/orchestrate-review/SKILL.md +1 -1
  30. package/adapters/opencode/skills/perf-analyzer/SKILL.md +1 -1
  31. package/adapters/opencode/skills/perf-baseline-manager/SKILL.md +1 -1
  32. package/adapters/opencode/skills/perf-benchmarker/SKILL.md +1 -1
  33. package/adapters/opencode/skills/perf-code-paths/SKILL.md +1 -1
  34. package/adapters/opencode/skills/perf-investigation-logger/SKILL.md +1 -1
  35. package/adapters/opencode/skills/perf-profiler/SKILL.md +1 -1
  36. package/adapters/opencode/skills/perf-theory-gatherer/SKILL.md +1 -1
  37. package/adapters/opencode/skills/perf-theory-tester/SKILL.md +1 -1
  38. package/adapters/opencode/skills/sync-docs/SKILL.md +1 -1
  39. package/adapters/opencode/skills/validate-delivery/SKILL.md +2 -2
  40. package/bin/cli.js +42 -8
  41. package/bin/dev-cli.js +16 -6
  42. package/lib/collectors/github.js +76 -12
  43. package/lib/perf/benchmark-runner.js +11 -6
  44. package/lib/perf/investigation-state.js +12 -13
  45. package/lib/perf/profiling-runner.js +23 -4
  46. package/lib/repo-map/concurrency.js +29 -0
  47. package/lib/repo-map/runner.js +218 -19
  48. package/lib/repo-map/updater.js +115 -27
  49. package/lib/state/workflow-state.js +31 -30
  50. package/lib/utils/command-parser.js +0 -0
  51. package/lib/utils/state-helpers.js +61 -0
  52. package/package.json +2 -1
  53. package/plugins/agnix/.claude-plugin/plugin.json +1 -1
  54. package/plugins/agnix/skills/agnix/SKILL.md +1 -1
  55. package/plugins/audit-project/.claude-plugin/plugin.json +1 -1
  56. package/plugins/audit-project/lib/collectors/github.js +76 -12
  57. package/plugins/audit-project/lib/perf/benchmark-runner.js +11 -6
  58. package/plugins/audit-project/lib/perf/investigation-state.js +12 -13
  59. package/plugins/audit-project/lib/perf/profiling-runner.js +23 -4
  60. package/plugins/audit-project/lib/repo-map/concurrency.js +29 -0
  61. package/plugins/audit-project/lib/repo-map/runner.js +218 -19
  62. package/plugins/audit-project/lib/repo-map/updater.js +115 -27
  63. package/plugins/audit-project/lib/state/workflow-state.js +31 -30
  64. package/plugins/audit-project/lib/utils/command-parser.js +0 -0
  65. package/plugins/audit-project/lib/utils/state-helpers.js +61 -0
  66. package/plugins/consult/.claude-plugin/plugin.json +1 -1
  67. package/plugins/consult/agents/consult-agent.md +1 -1
  68. package/plugins/consult/commands/consult.md +3 -2
  69. package/plugins/consult/skills/consult/SKILL.md +16 -4
  70. package/plugins/deslop/.claude-plugin/plugin.json +1 -1
  71. package/plugins/deslop/lib/collectors/github.js +76 -12
  72. package/plugins/deslop/lib/perf/benchmark-runner.js +11 -6
  73. package/plugins/deslop/lib/perf/investigation-state.js +12 -13
  74. package/plugins/deslop/lib/perf/profiling-runner.js +23 -4
  75. package/plugins/deslop/lib/repo-map/concurrency.js +29 -0
  76. package/plugins/deslop/lib/repo-map/runner.js +218 -19
  77. package/plugins/deslop/lib/repo-map/updater.js +115 -27
  78. package/plugins/deslop/lib/state/workflow-state.js +31 -30
  79. package/plugins/deslop/lib/utils/command-parser.js +0 -0
  80. package/plugins/deslop/lib/utils/state-helpers.js +61 -0
  81. package/plugins/deslop/skills/deslop/SKILL.md +1 -1
  82. package/plugins/drift-detect/.claude-plugin/plugin.json +1 -1
  83. package/plugins/drift-detect/lib/collectors/github.js +76 -12
  84. package/plugins/drift-detect/lib/perf/benchmark-runner.js +11 -6
  85. package/plugins/drift-detect/lib/perf/investigation-state.js +12 -13
  86. package/plugins/drift-detect/lib/perf/profiling-runner.js +23 -4
  87. package/plugins/drift-detect/lib/repo-map/concurrency.js +29 -0
  88. package/plugins/drift-detect/lib/repo-map/runner.js +218 -19
  89. package/plugins/drift-detect/lib/repo-map/updater.js +115 -27
  90. package/plugins/drift-detect/lib/state/workflow-state.js +31 -30
  91. package/plugins/drift-detect/lib/utils/command-parser.js +0 -0
  92. package/plugins/drift-detect/lib/utils/state-helpers.js +61 -0
  93. package/plugins/drift-detect/skills/drift-analysis/SKILL.md +1 -1
  94. package/plugins/enhance/.claude-plugin/plugin.json +1 -1
  95. package/plugins/enhance/lib/collectors/github.js +76 -12
  96. package/plugins/enhance/lib/perf/benchmark-runner.js +11 -6
  97. package/plugins/enhance/lib/perf/investigation-state.js +12 -13
  98. package/plugins/enhance/lib/perf/profiling-runner.js +23 -4
  99. package/plugins/enhance/lib/repo-map/concurrency.js +29 -0
  100. package/plugins/enhance/lib/repo-map/runner.js +218 -19
  101. package/plugins/enhance/lib/repo-map/updater.js +115 -27
  102. package/plugins/enhance/lib/state/workflow-state.js +31 -30
  103. package/plugins/enhance/lib/utils/command-parser.js +0 -0
  104. package/plugins/enhance/lib/utils/state-helpers.js +61 -0
  105. package/plugins/enhance/skills/enhance-agent-prompts/SKILL.md +1 -1
  106. package/plugins/enhance/skills/enhance-claude-memory/SKILL.md +1 -1
  107. package/plugins/enhance/skills/enhance-cross-file/SKILL.md +1 -1
  108. package/plugins/enhance/skills/enhance-docs/SKILL.md +1 -1
  109. package/plugins/enhance/skills/enhance-hooks/SKILL.md +1 -1
  110. package/plugins/enhance/skills/enhance-orchestrator/SKILL.md +1 -1
  111. package/plugins/enhance/skills/enhance-plugins/SKILL.md +1 -1
  112. package/plugins/enhance/skills/enhance-prompts/SKILL.md +1 -1
  113. package/plugins/enhance/skills/enhance-skills/SKILL.md +1 -1
  114. package/plugins/learn/.claude-plugin/plugin.json +1 -1
  115. package/plugins/learn/lib/collectors/github.js +76 -12
  116. package/plugins/learn/lib/perf/benchmark-runner.js +11 -6
  117. package/plugins/learn/lib/perf/investigation-state.js +12 -13
  118. package/plugins/learn/lib/perf/profiling-runner.js +23 -4
  119. package/plugins/learn/lib/repo-map/concurrency.js +29 -0
  120. package/plugins/learn/lib/repo-map/runner.js +218 -19
  121. package/plugins/learn/lib/repo-map/updater.js +115 -27
  122. package/plugins/learn/lib/state/workflow-state.js +31 -30
  123. package/plugins/learn/lib/utils/command-parser.js +0 -0
  124. package/plugins/learn/lib/utils/state-helpers.js +61 -0
  125. package/plugins/learn/skills/learn/SKILL.md +1 -1
  126. package/plugins/next-task/.claude-plugin/plugin.json +1 -1
  127. package/plugins/next-task/agents/delivery-validator.md +1 -1
  128. package/plugins/next-task/agents/implementation-agent.md +2 -2
  129. package/plugins/next-task/agents/worktree-manager.md +3 -3
  130. package/plugins/next-task/commands/next-task.md +8 -8
  131. package/plugins/next-task/hooks/hooks.json +1 -1
  132. package/plugins/next-task/lib/collectors/github.js +76 -12
  133. package/plugins/next-task/lib/perf/benchmark-runner.js +11 -6
  134. package/plugins/next-task/lib/perf/investigation-state.js +12 -13
  135. package/plugins/next-task/lib/perf/profiling-runner.js +23 -4
  136. package/plugins/next-task/lib/repo-map/concurrency.js +29 -0
  137. package/plugins/next-task/lib/repo-map/runner.js +218 -19
  138. package/plugins/next-task/lib/repo-map/updater.js +115 -27
  139. package/plugins/next-task/lib/state/workflow-state.js +31 -30
  140. package/plugins/next-task/lib/utils/command-parser.js +0 -0
  141. package/plugins/next-task/lib/utils/state-helpers.js +61 -0
  142. package/plugins/next-task/skills/discover-tasks/SKILL.md +2 -2
  143. package/plugins/next-task/skills/orchestrate-review/SKILL.md +1 -1
  144. package/plugins/next-task/skills/validate-delivery/SKILL.md +2 -2
  145. package/plugins/perf/.claude-plugin/plugin.json +1 -1
  146. package/plugins/perf/lib/collectors/github.js +76 -12
  147. package/plugins/perf/lib/perf/benchmark-runner.js +11 -6
  148. package/plugins/perf/lib/perf/investigation-state.js +12 -13
  149. package/plugins/perf/lib/perf/profiling-runner.js +23 -4
  150. package/plugins/perf/lib/repo-map/concurrency.js +29 -0
  151. package/plugins/perf/lib/repo-map/runner.js +218 -19
  152. package/plugins/perf/lib/repo-map/updater.js +115 -27
  153. package/plugins/perf/lib/state/workflow-state.js +31 -30
  154. package/plugins/perf/lib/utils/command-parser.js +0 -0
  155. package/plugins/perf/lib/utils/state-helpers.js +61 -0
  156. package/plugins/perf/skills/perf-analyzer/SKILL.md +1 -1
  157. package/plugins/perf/skills/perf-baseline-manager/SKILL.md +1 -1
  158. package/plugins/perf/skills/perf-benchmarker/SKILL.md +1 -1
  159. package/plugins/perf/skills/perf-code-paths/SKILL.md +1 -1
  160. package/plugins/perf/skills/perf-investigation-logger/SKILL.md +1 -1
  161. package/plugins/perf/skills/perf-profiler/SKILL.md +1 -1
  162. package/plugins/perf/skills/perf-theory-gatherer/SKILL.md +1 -1
  163. package/plugins/perf/skills/perf-theory-tester/SKILL.md +1 -1
  164. package/plugins/repo-map/.claude-plugin/plugin.json +1 -1
  165. package/plugins/repo-map/lib/collectors/github.js +76 -12
  166. package/plugins/repo-map/lib/perf/benchmark-runner.js +11 -6
  167. package/plugins/repo-map/lib/perf/investigation-state.js +12 -13
  168. package/plugins/repo-map/lib/perf/profiling-runner.js +23 -4
  169. package/plugins/repo-map/lib/repo-map/concurrency.js +29 -0
  170. package/plugins/repo-map/lib/repo-map/runner.js +218 -19
  171. package/plugins/repo-map/lib/repo-map/updater.js +115 -27
  172. package/plugins/repo-map/lib/state/workflow-state.js +31 -30
  173. package/plugins/repo-map/lib/utils/command-parser.js +0 -0
  174. package/plugins/repo-map/lib/utils/state-helpers.js +61 -0
  175. package/plugins/ship/.claude-plugin/plugin.json +1 -1
  176. package/plugins/ship/lib/collectors/github.js +76 -12
  177. package/plugins/ship/lib/perf/benchmark-runner.js +11 -6
  178. package/plugins/ship/lib/perf/investigation-state.js +12 -13
  179. package/plugins/ship/lib/perf/profiling-runner.js +23 -4
  180. package/plugins/ship/lib/repo-map/concurrency.js +29 -0
  181. package/plugins/ship/lib/repo-map/runner.js +218 -19
  182. package/plugins/ship/lib/repo-map/updater.js +115 -27
  183. package/plugins/ship/lib/state/workflow-state.js +31 -30
  184. package/plugins/ship/lib/utils/command-parser.js +0 -0
  185. package/plugins/ship/lib/utils/state-helpers.js +61 -0
  186. package/plugins/sync-docs/.claude-plugin/plugin.json +1 -1
  187. package/plugins/sync-docs/lib/collectors/github.js +76 -12
  188. package/plugins/sync-docs/lib/perf/benchmark-runner.js +11 -6
  189. package/plugins/sync-docs/lib/perf/investigation-state.js +12 -13
  190. package/plugins/sync-docs/lib/perf/profiling-runner.js +23 -4
  191. package/plugins/sync-docs/lib/repo-map/concurrency.js +29 -0
  192. package/plugins/sync-docs/lib/repo-map/runner.js +218 -19
  193. package/plugins/sync-docs/lib/repo-map/updater.js +115 -27
  194. package/plugins/sync-docs/lib/state/workflow-state.js +31 -30
  195. package/plugins/sync-docs/lib/utils/command-parser.js +0 -0
  196. package/plugins/sync-docs/lib/utils/state-helpers.js +61 -0
  197. package/plugins/sync-docs/skills/sync-docs/SKILL.md +1 -1
  198. package/scripts/bump-version.js +4 -1
  199. package/scripts/dev-install.js +9 -0
  200. package/scripts/validate-opencode-install.js +17 -4
  201. package/site/content.json +1 -1
  202. package/site/index.html +1 -1
@@ -6,7 +6,7 @@
6
6
 
7
7
  'use strict';
8
8
 
9
- const { execFileSync, spawnSync } = require('child_process');
9
+ const { execFileSync, spawnSync, spawn } = require('child_process');
10
10
  const path = require('path');
11
11
  const fs = require('fs');
12
12
  const fsPromises = require('fs').promises;
@@ -15,6 +15,7 @@ const crypto = require('crypto');
15
15
  const installer = require('./installer');
16
16
  const queries = require('./queries');
17
17
  const slopAnalyzers = require('../patterns/slop-analyzers');
18
+ const { runWithConcurrency } = require('./concurrency');
18
19
 
19
20
  // Language file extensions mapping
20
21
  const LANGUAGE_EXTENSIONS = {
@@ -33,6 +34,7 @@ const EXCLUDE_DIRS = Array.from(new Set([
33
34
  ]));
34
35
 
35
36
  const AST_GREP_BATCH_SIZE = 100;
37
+ const AST_GREP_CONCURRENCY = 4;
36
38
  const LANGUAGE_EXTENSION_SCAN_LIMIT = 500;
37
39
  const FILE_READ_BATCH_SIZE = 50; // Concurrent file reads
38
40
 
@@ -253,8 +255,12 @@ async function fullScan(basePath, languages, options = {}) {
253
255
  const pattern = typeof patternDef === 'string' ? patternDef : patternDef.pattern;
254
256
  if (!pattern) continue;
255
257
 
256
- for (const chunk of chunks) {
257
- const matches = runAstGrepPattern(cmd, pattern, sgLang, basePath, chunk);
258
+ const matchesByChunk = await runAstGrepPatternBatches(cmd, pattern, sgLang, basePath, chunks, {
259
+ onError: (error) => map.stats.errors.push(error),
260
+ concurrency: options.astGrepConcurrency
261
+ });
262
+
263
+ for (const matches of matchesByChunk) {
258
264
  for (const match of matches) {
259
265
  const matchedPath = normalizeMatchPath(match.file, basePath);
260
266
  if (!matchedPath) continue;
@@ -495,6 +501,127 @@ function chunkArray(items, size) {
495
501
  return chunks;
496
502
  }
497
503
 
504
+ function truncatePattern(pattern, max = 120) {
505
+ if (typeof pattern !== 'string') return '';
506
+ if (pattern.length <= max) return pattern;
507
+ return `${pattern.slice(0, max - 3)}...`;
508
+ }
509
+
510
+ function buildAstGrepError({ reason, pattern, lang, filePaths, basePath, stderr }) {
511
+ const batchLabel = Array.isArray(filePaths) && filePaths.length === 1
512
+ ? normalizeMatchPath(filePaths[0], basePath)
513
+ : '[batch]';
514
+
515
+ const details = stderr && String(stderr).trim()
516
+ ? ` (${String(stderr).trim()})`
517
+ : '';
518
+
519
+ return {
520
+ file: batchLabel,
521
+ error: `ast-grep ${reason} for ${lang}${details}`,
522
+ pattern: truncatePattern(pattern)
523
+ };
524
+ }
525
+
526
+ function runAstGrepPatternAsync(cmd, pattern, lang, basePath, filePaths, options = {}) {
527
+ if (!pattern || !filePaths || filePaths.length === 0) {
528
+ return Promise.resolve([]);
529
+ }
530
+
531
+ return new Promise((resolve) => {
532
+ const child = spawn(cmd, [
533
+ 'run',
534
+ '--pattern', pattern,
535
+ '--lang', lang,
536
+ '--json=stream',
537
+ ...filePaths
538
+ ], {
539
+ cwd: basePath,
540
+ windowsHide: true,
541
+ stdio: ['ignore', 'pipe', 'pipe']
542
+ });
543
+
544
+ let stdout = '';
545
+ let stderr = '';
546
+ let settled = false;
547
+
548
+ const timeoutHandle = setTimeout(() => {
549
+ if (settled) return;
550
+ settled = true;
551
+ child.kill();
552
+ if (typeof options.onError === 'function') {
553
+ options.onError(buildAstGrepError({
554
+ reason: 'timed out after 300000ms',
555
+ pattern,
556
+ lang,
557
+ filePaths,
558
+ basePath,
559
+ stderr
560
+ }));
561
+ }
562
+ resolve([]);
563
+ }, 300000);
564
+
565
+ child.stdout.on('data', (chunk) => {
566
+ stdout += chunk.toString();
567
+ });
568
+
569
+ child.stderr.on('data', (chunk) => {
570
+ stderr += chunk.toString();
571
+ });
572
+
573
+ child.on('error', (error) => {
574
+ if (settled) return;
575
+ settled = true;
576
+ clearTimeout(timeoutHandle);
577
+ if (typeof options.onError === 'function') {
578
+ options.onError(buildAstGrepError({
579
+ reason: 'execution failed',
580
+ pattern,
581
+ lang,
582
+ filePaths,
583
+ basePath,
584
+ stderr: error.message
585
+ }));
586
+ }
587
+ resolve([]);
588
+ });
589
+
590
+ child.on('close', (code) => {
591
+ if (settled) return;
592
+ settled = true;
593
+ clearTimeout(timeoutHandle);
594
+
595
+ if (typeof code === 'number' && code > 1) {
596
+ if (typeof options.onError === 'function') {
597
+ options.onError(buildAstGrepError({
598
+ reason: `returned exit code ${code}`,
599
+ pattern,
600
+ lang,
601
+ filePaths,
602
+ basePath,
603
+ stderr
604
+ }));
605
+ }
606
+ resolve([]);
607
+ return;
608
+ }
609
+
610
+ resolve(parseNdjson(stdout));
611
+ });
612
+ });
613
+ }
614
+
615
+ async function runAstGrepPatternBatches(cmd, pattern, lang, basePath, chunks, options = {}) {
616
+ const concurrency = Number.isFinite(options.concurrency)
617
+ ? Math.max(1, Math.floor(options.concurrency))
618
+ : AST_GREP_CONCURRENCY;
619
+
620
+ return runWithConcurrency(chunks, concurrency, async (chunk) => {
621
+ return runAstGrepPatternAsync(cmd, pattern, lang, basePath, chunk, options);
622
+ });
623
+ }
624
+
498
625
  function normalizeMatchPath(matchFile, basePath) {
499
626
  if (!matchFile) return null;
500
627
  const absolutePath = path.isAbsolute(matchFile) ? matchFile : path.join(basePath, matchFile);
@@ -513,7 +640,7 @@ function addSymbolToMap(map, name, match, kind, extra = {}) {
513
640
  }
514
641
  }
515
642
 
516
- function runAstGrepPattern(cmd, pattern, lang, basePath, filePaths) {
643
+ function runAstGrepPattern(cmd, pattern, lang, basePath, filePaths, options = {}) {
517
644
  if (!pattern || !filePaths || filePaths.length === 0) return [];
518
645
 
519
646
  try {
@@ -532,15 +659,45 @@ function runAstGrepPattern(cmd, pattern, lang, basePath, filePaths) {
532
659
  });
533
660
 
534
661
  if (result.error) {
662
+ if (typeof options.onError === 'function') {
663
+ options.onError(buildAstGrepError({
664
+ reason: 'execution failed',
665
+ pattern,
666
+ lang,
667
+ filePaths,
668
+ basePath,
669
+ stderr: result.error.message
670
+ }));
671
+ }
535
672
  return [];
536
673
  }
537
674
 
538
675
  if (typeof result.status === 'number' && result.status > 1) {
676
+ if (typeof options.onError === 'function') {
677
+ options.onError(buildAstGrepError({
678
+ reason: `returned exit code ${result.status}`,
679
+ pattern,
680
+ lang,
681
+ filePaths,
682
+ basePath,
683
+ stderr: result.stderr
684
+ }));
685
+ }
539
686
  return [];
540
687
  }
541
688
 
542
689
  return parseNdjson(result.stdout);
543
- } catch {
690
+ } catch (error) {
691
+ if (typeof options.onError === 'function') {
692
+ options.onError(buildAstGrepError({
693
+ reason: 'threw an exception',
694
+ pattern,
695
+ lang,
696
+ filePaths,
697
+ basePath,
698
+ stderr: error.message
699
+ }));
700
+ }
544
701
  return [];
545
702
  }
546
703
  }
@@ -555,7 +712,7 @@ function runAstGrepPattern(cmd, pattern, lang, basePath, filePaths) {
555
712
  * @param {string} content - File content
556
713
  * @returns {Object} - Extracted symbols
557
714
  */
558
- function extractSymbols(cmd, file, language, langQueries, basePath, content) {
715
+ function extractSymbols(cmd, file, language, langQueries, basePath, content, options = {}) {
559
716
  const symbols = {
560
717
  exports: [],
561
718
  functions: [],
@@ -588,7 +745,7 @@ function extractSymbols(cmd, file, language, langQueries, basePath, content) {
588
745
  if (!patterns) return;
589
746
  for (const patternDef of patterns) {
590
747
  const pattern = patternDef.pattern || patternDef;
591
- const results = runAstGrep(cmd, file, pattern, sgLang, basePath);
748
+ const results = runAstGrep(cmd, file, pattern, sgLang, basePath, options);
592
749
  for (const match of results) {
593
750
  const names = extractNamesFromMatch(match, patternDef);
594
751
  for (const name of names) {
@@ -640,7 +797,7 @@ function extractSymbols(cmd, file, language, langQueries, basePath, content) {
640
797
  * @param {string} basePath - Repository root (for cwd)
641
798
  * @returns {Array} - Extracted imports
642
799
  */
643
- function extractImports(cmd, file, language, langQueries, basePath) {
800
+ function extractImports(cmd, file, language, langQueries, basePath, options = {}) {
644
801
  const imports = [];
645
802
 
646
803
  if (!langQueries.imports) return imports;
@@ -650,7 +807,7 @@ function extractImports(cmd, file, language, langQueries, basePath) {
650
807
 
651
808
  for (const patternDef of langQueries.imports) {
652
809
  const pattern = patternDef.pattern || patternDef;
653
- const results = runAstGrep(cmd, file, pattern, sgLang, basePath);
810
+ const results = runAstGrep(cmd, file, pattern, sgLang, basePath, options);
654
811
  for (const match of results) {
655
812
  const sourceResult = extractSourceFromMatch(match, patternDef);
656
813
  const sources = Array.isArray(sourceResult) ? sourceResult : [sourceResult];
@@ -680,7 +837,7 @@ function extractImports(cmd, file, language, langQueries, basePath) {
680
837
  * @param {string} basePath - Working directory
681
838
  * @returns {Array} - Match results
682
839
  */
683
- function runAstGrep(cmd, file, pattern, lang, basePath) {
840
+ function runAstGrep(cmd, file, pattern, lang, basePath, options = {}) {
684
841
  try {
685
842
  const result = spawnSync(cmd, [
686
843
  'run',
@@ -697,16 +854,46 @@ function runAstGrep(cmd, file, pattern, lang, basePath) {
697
854
  });
698
855
 
699
856
  if (result.error) {
857
+ if (typeof options.onError === 'function') {
858
+ options.onError(buildAstGrepError({
859
+ reason: 'execution failed',
860
+ pattern,
861
+ lang,
862
+ filePaths: [file],
863
+ basePath,
864
+ stderr: result.error.message
865
+ }));
866
+ }
700
867
  return [];
701
868
  }
702
869
 
703
870
  // ast-grep exits with 1 when no matches
704
871
  if (typeof result.status === 'number' && result.status > 1) {
872
+ if (typeof options.onError === 'function') {
873
+ options.onError(buildAstGrepError({
874
+ reason: `returned exit code ${result.status}`,
875
+ pattern,
876
+ lang,
877
+ filePaths: [file],
878
+ basePath,
879
+ stderr: result.stderr
880
+ }));
881
+ }
705
882
  return [];
706
883
  }
707
884
 
708
885
  return parseNdjson(result.stdout);
709
- } catch {
886
+ } catch (error) {
887
+ if (typeof options.onError === 'function') {
888
+ options.onError(buildAstGrepError({
889
+ reason: 'threw an exception',
890
+ pattern,
891
+ lang,
892
+ filePaths: [file],
893
+ basePath,
894
+ stderr: error.message
895
+ }));
896
+ }
710
897
  return [];
711
898
  }
712
899
  }
@@ -1070,7 +1257,7 @@ function detectProjectType(languages) {
1070
1257
  * @param {string} basePath - Repository root
1071
1258
  * @returns {Object|null} - File data or null if failed
1072
1259
  */
1073
- function scanSingleFile(cmd, file, basePath) {
1260
+ function scanSingleFile(cmd, file, basePath, options = {}) {
1074
1261
  const ext = path.extname(file).toLowerCase();
1075
1262
 
1076
1263
  // Find language for this extension
@@ -1091,8 +1278,8 @@ function scanSingleFile(cmd, file, basePath) {
1091
1278
  const content = fs.readFileSync(file, 'utf8');
1092
1279
  const hash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
1093
1280
 
1094
- const symbols = extractSymbols(cmd, file, language, langQueries, basePath, content);
1095
- const imports = extractImports(cmd, file, language, langQueries, basePath);
1281
+ const symbols = extractSymbols(cmd, file, language, langQueries, basePath, content, options);
1282
+ const imports = extractImports(cmd, file, language, langQueries, basePath, options);
1096
1283
 
1097
1284
  return {
1098
1285
  hash,
@@ -1101,7 +1288,13 @@ function scanSingleFile(cmd, file, basePath) {
1101
1288
  symbols,
1102
1289
  imports
1103
1290
  };
1104
- } catch {
1291
+ } catch (error) {
1292
+ if (typeof options.onError === 'function') {
1293
+ options.onError({
1294
+ file: normalizeMatchPath(file, basePath) || file,
1295
+ error: `Failed to scan file: ${error.message}`
1296
+ });
1297
+ }
1105
1298
  return null;
1106
1299
  }
1107
1300
  }
@@ -1114,7 +1307,7 @@ function scanSingleFile(cmd, file, basePath) {
1114
1307
  * @param {string} basePath - Repository root
1115
1308
  * @returns {Promise<Object|null>} - File data or null if failed
1116
1309
  */
1117
- async function scanSingleFileAsync(cmd, file, basePath) {
1310
+ async function scanSingleFileAsync(cmd, file, basePath, options = {}) {
1118
1311
  const ext = path.extname(file).toLowerCase();
1119
1312
 
1120
1313
  // Find language for this extension
@@ -1135,8 +1328,8 @@ async function scanSingleFileAsync(cmd, file, basePath) {
1135
1328
  const content = await fsPromises.readFile(file, 'utf8');
1136
1329
  const hash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
1137
1330
 
1138
- const symbols = extractSymbols(cmd, file, language, langQueries, basePath, content);
1139
- const imports = extractImports(cmd, file, language, langQueries, basePath);
1331
+ const symbols = extractSymbols(cmd, file, language, langQueries, basePath, content, options);
1332
+ const imports = extractImports(cmd, file, language, langQueries, basePath, options);
1140
1333
 
1141
1334
  return {
1142
1335
  hash,
@@ -1145,7 +1338,13 @@ async function scanSingleFileAsync(cmd, file, basePath) {
1145
1338
  symbols,
1146
1339
  imports
1147
1340
  };
1148
- } catch {
1341
+ } catch (error) {
1342
+ if (typeof options.onError === 'function') {
1343
+ options.onError({
1344
+ file: normalizeMatchPath(file, basePath) || file,
1345
+ error: `Failed to scan file: ${error.message}`
1346
+ });
1347
+ }
1149
1348
  return null;
1150
1349
  }
1151
1350
  }
@@ -6,7 +6,6 @@
6
6
 
7
7
  'use strict';
8
8
 
9
- const fs = require('fs');
10
9
  const fsPromises = require('fs').promises;
11
10
  const path = require('path');
12
11
  const { execFileSync } = require('child_process');
@@ -14,6 +13,15 @@ const { execFileSync } = require('child_process');
14
13
  const runner = require('./runner');
15
14
  const cache = require('./cache');
16
15
  const installer = require('./installer');
16
+ const { runWithConcurrency } = require('./concurrency');
17
+
18
+ const SCAN_CONCURRENCY = 8;
19
+ const SCANNABLE_EXTENSIONS = new Set(Object.values(runner.LANGUAGE_EXTENSIONS).flat());
20
+
21
+ function isScannableFile(filePath) {
22
+ const ext = path.extname(filePath).toLowerCase();
23
+ return SCANNABLE_EXTENSIONS.has(ext);
24
+ }
17
25
 
18
26
  /**
19
27
  * Perform incremental update based on git diff
@@ -47,6 +55,10 @@ async function incrementalUpdate(basePath, map) {
47
55
  needsFullRebuild: true
48
56
  };
49
57
  }
58
+ map.stats = map.stats || {};
59
+ if (!Array.isArray(map.stats.errors)) {
60
+ map.stats.errors = [];
61
+ }
50
62
  if (map.docs) {
51
63
  delete map.docs;
52
64
  }
@@ -118,19 +130,46 @@ async function incrementalUpdate(basePath, map) {
118
130
  })
119
131
  );
120
132
 
121
- // Process files that exist
122
- for (const { file, fullPath, exists } of existenceChecks) {
123
- if (!exists) continue;
133
+ // Process files that exist with bounded concurrency
134
+ const scanTargets = existenceChecks.filter(({ file, exists }) => exists && isScannableFile(file));
135
+ const scanResults = await runWithConcurrency(scanTargets, SCAN_CONCURRENCY, async ({ file, fullPath }) => {
136
+ const astErrors = [];
137
+ const fileData = await runner.scanSingleFileAsync(installed.command, fullPath, basePath, {
138
+ onError: (error) => astErrors.push(error)
139
+ });
140
+ return { file, fileData, astErrors };
141
+ });
124
142
 
125
- const fileData = await runner.scanSingleFileAsync(installed.command, fullPath, basePath);
126
- if (fileData) {
127
- map.files[file] = fileData;
128
- if (fileData.imports && fileData.imports.length > 0) {
129
- map.dependencies[file] = Array.from(new Set(fileData.imports.map(imp => imp.source)));
130
- } else {
131
- delete map.dependencies[file];
143
+ const scanFailures = [];
144
+ for (const result of scanResults) {
145
+ if (!result) continue;
146
+
147
+ if (result.astErrors.length > 0) {
148
+ map.stats.errors.push(...result.astErrors);
149
+ }
150
+
151
+ if (!result.fileData) {
152
+ if (result.astErrors.length > 0) {
153
+ scanFailures.push(result.file);
132
154
  }
155
+ continue;
133
156
  }
157
+
158
+ map.files[result.file] = result.fileData;
159
+ if (result.fileData.imports && result.fileData.imports.length > 0) {
160
+ map.dependencies[result.file] = Array.from(new Set(result.fileData.imports.map(imp => imp.source)));
161
+ } else {
162
+ delete map.dependencies[result.file];
163
+ }
164
+ }
165
+
166
+ if (scanFailures.length > 0) {
167
+ return {
168
+ success: false,
169
+ error: `Failed to rescan ${scanFailures.length} file(s) during incremental update`,
170
+ needsFullRebuild: true,
171
+ failedFiles: scanFailures
172
+ };
134
173
  }
135
174
 
136
175
  // Recalculate stats
@@ -163,6 +202,10 @@ async function incrementalUpdate(basePath, map) {
163
202
  async function updateWithoutGit(basePath, map, cmd) {
164
203
  const currentFiles = new Set();
165
204
  const languages = map.project?.languages || [];
205
+ map.stats = map.stats || {};
206
+ if (!Array.isArray(map.stats.errors)) {
207
+ map.stats.errors = [];
208
+ }
166
209
  if (map.docs) {
167
210
  delete map.docs;
168
211
  }
@@ -204,31 +247,76 @@ async function updateWithoutGit(basePath, map, cmd) {
204
247
  }
205
248
 
206
249
  // Process existing files for modifications (async file reads)
207
- for (const file of filesToCheck) {
250
+ const checkResults = await runWithConcurrency(filesToCheck, SCAN_CONCURRENCY, async (file) => {
208
251
  const fullPath = path.join(basePath, file);
209
- const fileData = await runner.scanSingleFileAsync(cmd, fullPath, basePath);
210
- if (fileData && fileData.hash !== map.files[file].hash) {
211
- map.files[file] = fileData;
212
- if (fileData.imports && fileData.imports.length > 0) {
213
- map.dependencies[file] = Array.from(new Set(fileData.imports.map(imp => imp.source)));
252
+ const astErrors = [];
253
+ const fileData = await runner.scanSingleFileAsync(cmd, fullPath, basePath, {
254
+ onError: (error) => astErrors.push(error)
255
+ });
256
+ return { file, fileData, astErrors };
257
+ });
258
+
259
+ const scanFailures = [];
260
+ for (const result of checkResults) {
261
+ if (!result) continue;
262
+
263
+ if (result.astErrors.length > 0) {
264
+ map.stats.errors.push(...result.astErrors);
265
+ }
266
+
267
+ if (!result.fileData) {
268
+ scanFailures.push(result.file);
269
+ continue;
270
+ }
271
+
272
+ if (result.fileData.hash !== map.files[result.file].hash) {
273
+ map.files[result.file] = result.fileData;
274
+ if (result.fileData.imports && result.fileData.imports.length > 0) {
275
+ map.dependencies[result.file] = Array.from(new Set(result.fileData.imports.map(imp => imp.source)));
214
276
  } else {
215
- delete map.dependencies[file];
277
+ delete map.dependencies[result.file];
216
278
  }
217
- changes.modified.push(file);
279
+ changes.modified.push(result.file);
218
280
  }
219
281
  }
220
282
 
221
283
  // Process new files (async file reads)
222
- for (const file of currentFiles) {
284
+ const addedFiles = Array.from(currentFiles);
285
+ const addResults = await runWithConcurrency(addedFiles, SCAN_CONCURRENCY, async (file) => {
223
286
  const fullPath = path.join(basePath, file);
224
- const fileData = await runner.scanSingleFileAsync(cmd, fullPath, basePath);
225
- if (fileData) {
226
- map.files[file] = fileData;
227
- if (fileData.imports && fileData.imports.length > 0) {
228
- map.dependencies[file] = Array.from(new Set(fileData.imports.map(imp => imp.source)));
229
- }
230
- changes.added.push(file);
287
+ const astErrors = [];
288
+ const fileData = await runner.scanSingleFileAsync(cmd, fullPath, basePath, {
289
+ onError: (error) => astErrors.push(error)
290
+ });
291
+ return { file, fileData, astErrors };
292
+ });
293
+
294
+ for (const result of addResults) {
295
+ if (!result) continue;
296
+
297
+ if (result.astErrors.length > 0) {
298
+ map.stats.errors.push(...result.astErrors);
231
299
  }
300
+
301
+ if (!result.fileData) {
302
+ scanFailures.push(result.file);
303
+ continue;
304
+ }
305
+
306
+ map.files[result.file] = result.fileData;
307
+ if (result.fileData.imports && result.fileData.imports.length > 0) {
308
+ map.dependencies[result.file] = Array.from(new Set(result.fileData.imports.map(imp => imp.source)));
309
+ }
310
+ changes.added.push(result.file);
311
+ }
312
+
313
+ if (scanFailures.length > 0) {
314
+ return {
315
+ success: false,
316
+ error: `Failed to rescan ${scanFailures.length} file(s) during non-git update`,
317
+ needsFullRebuild: true,
318
+ failedFiles: scanFailures
319
+ };
232
320
  }
233
321
 
234
322
  changes.total = changes.added.length + changes.modified.length + changes.deleted.length;