agentic-qe 3.7.20 → 3.7.22

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 (115) hide show
  1. package/.claude/agents/v3/qe-deployment-advisor.md +14 -0
  2. package/.claude/agents/v3/qe-gap-detector.md +8 -0
  3. package/.claude/agents/v3/qe-impact-analyzer.md +11 -0
  4. package/.claude/agents/v3/qe-queen-coordinator.md +45 -0
  5. package/.claude/agents/v3/qe-root-cause-analyzer.md +11 -0
  6. package/.claude/agents/v3/qe-security-scanner.md +25 -16
  7. package/.claude/helpers/brain-checkpoint.cjs +7 -4
  8. package/.claude/helpers/statusline-v3.cjs +7 -4
  9. package/.claude/skills/skills-manifest.json +1 -1
  10. package/CHANGELOG.md +34 -0
  11. package/README.md +0 -12
  12. package/assets/agents/v3/qe-deployment-advisor.md +14 -0
  13. package/assets/agents/v3/qe-gap-detector.md +8 -0
  14. package/assets/agents/v3/qe-impact-analyzer.md +11 -0
  15. package/assets/agents/v3/qe-queen-coordinator.md +45 -0
  16. package/assets/agents/v3/qe-root-cause-analyzer.md +11 -0
  17. package/assets/agents/v3/qe-security-scanner.md +25 -16
  18. package/assets/helpers/statusline-v3.cjs +7 -4
  19. package/dist/adapters/claude-flow/model-router-bridge.d.ts +0 -6
  20. package/dist/adapters/claude-flow/model-router-bridge.js +4 -17
  21. package/dist/adapters/claude-flow/pretrain-bridge.d.ts +0 -6
  22. package/dist/adapters/claude-flow/pretrain-bridge.js +6 -19
  23. package/dist/adapters/claude-flow/trajectory-bridge.d.ts +0 -6
  24. package/dist/adapters/claude-flow/trajectory-bridge.js +21 -23
  25. package/dist/cli/brain-commands.js +6 -10
  26. package/dist/cli/bundle.js +3124 -3622
  27. package/dist/cli/commands/hooks.js +29 -6
  28. package/dist/cli/commands/init.js +1 -73
  29. package/dist/cli/commands/learning.js +164 -12
  30. package/dist/cli/handlers/init-handler.d.ts +0 -1
  31. package/dist/cli/handlers/init-handler.js +0 -6
  32. package/dist/cli/index.js +0 -2
  33. package/dist/context/sources/defect-source.js +2 -2
  34. package/dist/context/sources/memory-source.js +2 -2
  35. package/dist/context/sources/requirements-source.js +2 -2
  36. package/dist/coordination/protocols/security-audit.d.ts +3 -6
  37. package/dist/coordination/protocols/security-audit.js +8 -88
  38. package/dist/coordination/queen-coordinator.d.ts +13 -0
  39. package/dist/coordination/queen-coordinator.js +76 -0
  40. package/dist/coordination/queen-task-management.d.ts +2 -0
  41. package/dist/coordination/queen-task-management.js +10 -0
  42. package/dist/coordination/queen-types.d.ts +3 -0
  43. package/dist/coordination/task-executor.js +7 -5
  44. package/dist/domains/security-compliance/services/scanners/sast-scanner.d.ts +25 -1
  45. package/dist/domains/security-compliance/services/scanners/sast-scanner.js +140 -11
  46. package/dist/domains/security-compliance/services/scanners/scanner-types.d.ts +2 -0
  47. package/dist/domains/security-compliance/services/scanners/scanner-types.js +1 -0
  48. package/dist/domains/test-execution/services/mincut-test-optimizer.js +2 -0
  49. package/dist/init/agents-installer.d.ts +2 -0
  50. package/dist/init/agents-installer.js +13 -0
  51. package/dist/init/enhancements/claude-flow-adapter.js +51 -24
  52. package/dist/init/index.d.ts +0 -2
  53. package/dist/init/index.js +0 -1
  54. package/dist/init/init-wizard-steps.d.ts +10 -0
  55. package/dist/init/init-wizard-steps.js +87 -1
  56. package/dist/init/init-wizard.d.ts +1 -9
  57. package/dist/init/init-wizard.js +3 -69
  58. package/dist/init/orchestrator.js +0 -1
  59. package/dist/init/phases/01-detection.js +0 -27
  60. package/dist/init/phases/07-hooks.js +12 -10
  61. package/dist/init/phases/phase-interface.d.ts +0 -1
  62. package/dist/init/settings-merge.js +1 -1
  63. package/dist/integrations/ruvector/brain-rvf-exporter.js +14 -2
  64. package/dist/kernel/unified-memory.js +5 -6
  65. package/dist/learning/experience-capture-middleware.js +23 -1
  66. package/dist/learning/index.d.ts +0 -2
  67. package/dist/learning/index.js +0 -4
  68. package/dist/learning/metrics-tracker.js +15 -13
  69. package/dist/learning/pattern-lifecycle.d.ts +1 -1
  70. package/dist/learning/pattern-lifecycle.js +18 -20
  71. package/dist/learning/qe-reasoning-bank.js +3 -3
  72. package/dist/learning/qe-unified-memory.js +1 -28
  73. package/dist/learning/sqlite-persistence.js +16 -0
  74. package/dist/learning/token-tracker.js +4 -2
  75. package/dist/mcp/bundle.js +1162 -478
  76. package/dist/routing/agent-dependency-graph.d.ts +77 -0
  77. package/dist/routing/agent-dependency-graph.js +359 -0
  78. package/dist/routing/co-execution-repository.d.ts +68 -0
  79. package/dist/routing/co-execution-repository.js +184 -0
  80. package/dist/routing/index.d.ts +6 -0
  81. package/dist/routing/index.js +6 -0
  82. package/dist/routing/qe-task-router.d.ts +7 -0
  83. package/dist/routing/qe-task-router.js +63 -1
  84. package/dist/routing/signal-merger.d.ts +81 -0
  85. package/dist/routing/signal-merger.js +136 -0
  86. package/dist/routing/types.d.ts +1 -0
  87. package/dist/shared/llm/providers/azure-openai.js +3 -2
  88. package/dist/shared/llm/providers/bedrock.js +3 -2
  89. package/dist/shared/llm/providers/claude.js +3 -2
  90. package/dist/shared/llm/providers/gemini.js +3 -2
  91. package/dist/shared/llm/providers/openai.js +3 -2
  92. package/dist/shared/llm/providers/openrouter.js +3 -2
  93. package/dist/shared/llm/retry.d.ts +10 -0
  94. package/dist/shared/llm/retry.js +16 -0
  95. package/dist/shared/llm/router/agent-router-config.d.ts +2 -1
  96. package/dist/shared/llm/router/agent-router-config.js +38 -88
  97. package/dist/validation/index.d.ts +2 -0
  98. package/dist/validation/index.js +4 -0
  99. package/dist/validation/steps/agent-mcp-validator.d.ts +88 -0
  100. package/dist/validation/steps/agent-mcp-validator.js +254 -0
  101. package/package.json +1 -1
  102. package/dist/cli/commands/migrate.d.ts +0 -9
  103. package/dist/cli/commands/migrate.js +0 -566
  104. package/dist/init/init-wizard-migration.d.ts +0 -52
  105. package/dist/init/init-wizard-migration.js +0 -345
  106. package/dist/init/migration/config-migrator.d.ts +0 -31
  107. package/dist/init/migration/config-migrator.js +0 -149
  108. package/dist/init/migration/data-migrator.d.ts +0 -72
  109. package/dist/init/migration/data-migrator.js +0 -232
  110. package/dist/init/migration/detector.d.ts +0 -44
  111. package/dist/init/migration/detector.js +0 -105
  112. package/dist/init/migration/index.d.ts +0 -8
  113. package/dist/init/migration/index.js +0 -8
  114. package/dist/learning/v2-to-v3-migration.d.ts +0 -86
  115. package/dist/learning/v2-to-v3-migration.js +0 -529
@@ -21,6 +21,7 @@ import { EventBus, AgentCoordinator, AgentInfo, DomainPlugin, DomainHealth, Memo
21
21
  import { CrossDomainRouter, ProtocolExecutor, WorkflowExecutor } from './interfaces';
22
22
  import { QueenMinCutBridge } from './mincut/queen-integration';
23
23
  import { QueenRouterAdapter } from '../routing/queen-integration.js';
24
+ import { type DependencyGraphResult } from '../routing/agent-dependency-graph.js';
24
25
  import { AgentTeamsAdapter } from './agent-teams/index.js';
25
26
  import { DomainTeamManager } from './agent-teams/domain-team-manager.js';
26
27
  import { DomainBreakerRegistry } from './circuit-breaker/index.js';
@@ -73,6 +74,9 @@ export declare class QueenCoordinator implements IQueenCoordinator {
73
74
  private hypothesisManager;
74
75
  private federationMailbox;
75
76
  private dynamicScaler;
77
+ private coExecutionRepo;
78
+ private dependencyGraph;
79
+ private availableMcpServers;
76
80
  constructor(eventBus: EventBus, agentCoordinator: AgentCoordinator, memory: MemoryBackend, router: CrossDomainRouter, protocolExecutor?: ProtocolExecutor | undefined, workflowExecutor?: WorkflowExecutor | undefined, domainPlugins?: Map<DomainName, DomainPlugin> | undefined, config?: Partial<QueenConfig>);
77
81
  initialize(): Promise<void>;
78
82
  dispose(): Promise<void>;
@@ -84,6 +88,15 @@ export declare class QueenCoordinator implements IQueenCoordinator {
84
88
  getDomainLoad(domain: DomainName): number;
85
89
  getIdleDomains(): DomainName[];
86
90
  getBusyDomains(): DomainName[];
91
+ /**
92
+ * Get a phased spawn plan that respects hard agent dependencies.
93
+ * Use this when spawning multiple agents for a swarm task.
94
+ */
95
+ getSpawnPlan(agentNames: string[]): import("..").SpawnPlan;
96
+ /**
97
+ * Get the dependency graph for the agent fleet.
98
+ */
99
+ getDependencyGraph(): DependencyGraphResult | null;
87
100
  enableWorkStealing(): void;
88
101
  disableWorkStealing(): void;
89
102
  triggerWorkStealing(): Promise<number>;
@@ -24,6 +24,10 @@ import { createQueenMinCutBridge, } from './mincut/queen-integration';
24
24
  import { getSharedMinCutGraph } from './mincut/shared-singleton';
25
25
  // V3 Integration: TinyDancer intelligent model routing (TD-004, TD-005, TD-006)
26
26
  import { QueenRouterAdapter, } from '../routing/queen-integration.js';
27
+ // Issue #342: Dependency intelligence integration
28
+ import { getCoExecutionRepository } from '../routing/co-execution-repository.js';
29
+ import { getAvailableMcpServers } from '../validation/steps/agent-mcp-validator.js';
30
+ import { buildDependencyGraph, createSpawnPlan } from '../routing/agent-dependency-graph.js';
27
31
  // V3 Integration: @claude-flow/guidance governance (ADR-058)
28
32
  import { queenGovernanceAdapter } from '../governance/index.js';
29
33
  // ADR-064 Integration: Agent Teams, Circuit Breakers, Fleet Tiers
@@ -102,6 +106,10 @@ export class QueenCoordinator {
102
106
  hypothesisManager = null;
103
107
  federationMailbox = null;
104
108
  dynamicScaler = null;
109
+ // Issue #342: Dependency intelligence
110
+ coExecutionRepo = null;
111
+ dependencyGraph = null;
112
+ availableMcpServers = [];
105
113
  constructor(eventBus, agentCoordinator, memory, router, protocolExecutor, workflowExecutor, domainPlugins, config = {}) {
106
114
  this.eventBus = eventBus;
107
115
  this.agentCoordinator = agentCoordinator;
@@ -177,6 +185,32 @@ export class QueenCoordinator {
177
185
  }
178
186
  // ADR-064: Initialize subsystems (non-fatal failures)
179
187
  this.initializeSubsystems();
188
+ // Issue #342: Initialize dependency intelligence (non-fatal)
189
+ try {
190
+ // Build dependency graph from agent definitions
191
+ const { join } = await import('path');
192
+ const agentsDir = join(process.cwd(), '.claude', 'agents', 'v3');
193
+ this.dependencyGraph = buildDependencyGraph(agentsDir);
194
+ this.availableMcpServers = getAvailableMcpServers(process.cwd());
195
+ // Initialize co-execution repository for behavioral learning
196
+ const { getUnifiedMemory } = await import('../kernel/unified-memory.js');
197
+ const unifiedMemory = getUnifiedMemory();
198
+ await unifiedMemory.initialize();
199
+ this.coExecutionRepo = getCoExecutionRepository();
200
+ this.coExecutionRepo.initialize(unifiedMemory.getDatabase());
201
+ if (this.dependencyGraph.warnings.length > 0) {
202
+ logger.warn(`Dependency graph: ${this.dependencyGraph.warnings.length} warning(s)`, {
203
+ warnings: this.dependencyGraph.warnings.slice(0, 5),
204
+ });
205
+ }
206
+ logger.info('Dependency intelligence initialized', {
207
+ agents: this.dependencyGraph.nodes.size,
208
+ mcpServers: this.availableMcpServers.length,
209
+ });
210
+ }
211
+ catch (depError) {
212
+ logger.warn('Dependency intelligence initialization failed (continuing)', { error: depError });
213
+ }
180
214
  // Publish initialization event
181
215
  await this.publishEvent('QueenInitialized', {
182
216
  timestamp: new Date(),
@@ -365,6 +399,24 @@ export class QueenCoordinator {
365
399
  return ALL_DOMAINS.filter(domain => this.getDomainLoad(domain) > this.config.workStealing.loadThreshold);
366
400
  }
367
401
  // ============================================================================
402
+ // Issue #342 Item 2: Dependency-Aware Spawn Planning
403
+ // ============================================================================
404
+ /**
405
+ * Get a phased spawn plan that respects hard agent dependencies.
406
+ * Use this when spawning multiple agents for a swarm task.
407
+ */
408
+ getSpawnPlan(agentNames) {
409
+ if (!this.dependencyGraph)
410
+ return { phases: [agentNames], warnings: [], unsatisfiedHardDeps: [] };
411
+ return createSpawnPlan(agentNames, this.dependencyGraph);
412
+ }
413
+ /**
414
+ * Get the dependency graph for the agent fleet.
415
+ */
416
+ getDependencyGraph() {
417
+ return this.dependencyGraph;
418
+ }
419
+ // ============================================================================
368
420
  // Work Stealing
369
421
  // ============================================================================
370
422
  enableWorkStealing() {
@@ -394,6 +446,29 @@ export class QueenCoordinator {
394
446
  if (!this.agentCoordinator.canSpawn()) {
395
447
  return err(new Error('Maximum concurrent agents reached (15)'));
396
448
  }
449
+ // Issue #342 Item 1: Pre-spawn MCP validation (advisory only — never blocks).
450
+ // Check dependency graph nodes in this domain for declared MCP server requirements.
451
+ if (this.dependencyGraph && this.availableMcpServers.length > 0) {
452
+ try {
453
+ // Find agents in this domain that have MCP server declarations
454
+ for (const [, node] of this.dependencyGraph.nodes) {
455
+ const mcpDeps = node.dependencies.mcpServers;
456
+ if (!mcpDeps || mcpDeps.length === 0)
457
+ continue;
458
+ const missing = mcpDeps
459
+ .filter(s => s.required && !this.availableMcpServers.includes(s.name));
460
+ if (missing.length > 0) {
461
+ logger.warn(`Pre-spawn MCP advisory: ${node.agentName} needs ${missing.map(m => m.name).join(', ')}`, {
462
+ agent: node.agentName, domain, missing: missing.map(m => m.name),
463
+ });
464
+ break; // Log once per spawn, not per agent
465
+ }
466
+ }
467
+ }
468
+ catch {
469
+ // Advisory validation must never block spawning
470
+ }
471
+ }
397
472
  const result = await this.agentCoordinator.spawn({
398
473
  name: `${domain}-${type}-${Date.now()}`, domain, type, capabilities,
399
474
  });
@@ -738,6 +813,7 @@ export class QueenCoordinator {
738
813
  get tierSelector() { return self.tierSelector; },
739
814
  get traceCollector() { return self.traceCollector; },
740
815
  taskTraceContexts: self.taskTraceContexts,
816
+ get coExecutionRepo() { return self.coExecutionRepo; },
741
817
  requestAgentSpawn: (d, t, c) => self.requestAgentSpawn(d, t, c),
742
818
  publishEvent: (t, p) => self.publishEvent(t, p),
743
819
  getDomainLoad: (d) => self.getDomainLoad(d),
@@ -7,6 +7,7 @@ import type { DomainName, Priority, Result } from '../shared/types';
7
7
  import type { AgentCoordinator, DomainPlugin } from '../kernel/interfaces';
8
8
  import type { TaskAuditLogger } from './services';
9
9
  import type { QueenRouterAdapter } from '../routing/queen-integration.js';
10
+ import type { CoExecutionRepository } from '../routing/co-execution-repository.js';
10
11
  import type { DomainBreakerRegistry } from './circuit-breaker/index.js';
11
12
  import type { DomainTeamManager } from './agent-teams/domain-team-manager.js';
12
13
  import type { TierSelector } from './fleet-tiers/index.js';
@@ -41,6 +42,7 @@ export interface QueenTaskContext {
41
42
  readonly tierSelector: TierSelector | null;
42
43
  readonly traceCollector: TraceCollector | null;
43
44
  readonly taskTraceContexts: Map<string, TraceContext>;
45
+ readonly coExecutionRepo: CoExecutionRepository | null;
44
46
  requestAgentSpawn(domain: DomainName, type: string, capabilities: string[]): Promise<Result<string, Error>>;
45
47
  publishEvent(type: string, payload: Record<string, unknown>): Promise<void>;
46
48
  getDomainLoad(domain: DomainName): number;
@@ -310,6 +310,16 @@ async function handleTaskCompletionCallback(ctx, result) {
310
310
  }
311
311
  // CC-002: Decrement running task counter
312
312
  ctx.runningTaskCounter = Math.max(0, ctx.runningTaskCounter - 1);
313
+ // Issue #342 Item 3: Record co-execution behavioral data for all agents in this task.
314
+ // This feeds the behavioral signal into the signal merger for future routing decisions.
315
+ if (ctx.coExecutionRepo && execution.assignedAgents.length >= 2) {
316
+ try {
317
+ ctx.coExecutionRepo.recordSwarmCoExecution(execution.assignedAgents, execution.assignedDomain || 'unknown', result.success, execution.task.type);
318
+ }
319
+ catch {
320
+ // Non-blocking: behavioral recording failure should not affect task completion
321
+ }
322
+ }
313
323
  // Stop assigned agents
314
324
  for (const agentId of execution.assignedAgents) {
315
325
  await ctx.agentCoordinator.stop(agentId);
@@ -9,6 +9,7 @@ import type { DomainBreakerRegistry } from './circuit-breaker/index.js';
9
9
  import type { DomainTeamManager } from './agent-teams/domain-team-manager.js';
10
10
  import type { TierSelector } from './fleet-tiers/index.js';
11
11
  import type { TraceCollector } from './agent-teams/tracing.js';
12
+ import type { DependencyGraphResult, SpawnPlan } from '../routing/agent-dependency-graph.js';
12
13
  import type { HypothesisManager } from './competing-hypotheses/index.js';
13
14
  import type { FederationMailbox } from './federation/index.js';
14
15
  import type { DynamicScaler } from './dynamic-scaling/index.js';
@@ -155,6 +156,8 @@ export interface IQueenCoordinator {
155
156
  getHypothesisManager(): HypothesisManager | null;
156
157
  getFederationMailbox(): FederationMailbox | null;
157
158
  getDynamicScaler(): DynamicScaler | null;
159
+ getSpawnPlan(agentNames: string[]): SpawnPlan;
160
+ getDependencyGraph(): DependencyGraphResult | null;
158
161
  }
159
162
  export interface TaskFilter {
160
163
  status?: TaskExecution['status'];
@@ -15,6 +15,7 @@
15
15
  import { v4 as uuidv4 } from 'uuid';
16
16
  import { toErrorMessage } from '../shared/error-utils.js';
17
17
  import { createResultSaver } from './result-saver';
18
+ import { createLogger } from '../logging/logger-factory.js';
18
19
  // ADR-051: Agent Booster integration for Tier 0 tasks
19
20
  import { createAgentBoosterAdapter, } from '../integrations/agentic-flow/agent-booster';
20
21
  // ADR-051: Task Router for outcome recording
@@ -23,6 +24,7 @@ import { getTaskRouter } from '../mcp/services/task-router';
23
24
  import { DomainServiceRegistry, ServiceKeys } from '../shared/domain-service-registry';
24
25
  // Handler registration functions (extracted from the monolithic registerHandlers)
25
26
  import { registerTestExecutionHandlers, registerCoverageHandlers, registerSecurityHandlers, registerQualityHandlers, registerRequirementsHandlers, registerCodeIntelligenceHandlers, registerMiscHandlers, } from './handlers/index';
27
+ const logger = createLogger('TaskExecutor');
26
28
  // ============================================================================
27
29
  // CQ-005: Domain Service Resolution via Registry (no coordination -> domains imports)
28
30
  // Domain modules register their factories in their index.ts files.
@@ -339,7 +341,7 @@ export class DomainTaskExecutor {
339
341
  const boosterResult = await this.executeWithAgentBooster(task, startTime, domain);
340
342
  if (boosterResult) {
341
343
  // Agent Booster succeeded - record outcome and return
342
- this.recordOutcome(task, 0, true, Date.now() - startTime).catch(() => { });
344
+ this.recordOutcome(task, 0, true, Date.now() - startTime).catch((e) => { logger.warn('recordOutcome failed', { error: e instanceof Error ? e.message : String(e), taskId: task.id }); });
343
345
  await this.publishTaskCompleted(task.id, boosterResult.data, domain);
344
346
  return boosterResult;
345
347
  }
@@ -355,7 +357,7 @@ export class DomainTaskExecutor {
355
357
  duration: Date.now() - startTime,
356
358
  domain,
357
359
  };
358
- this.recordOutcome(task, routingTier, false, Date.now() - startTime).catch(() => { });
360
+ this.recordOutcome(task, routingTier, false, Date.now() - startTime).catch((e) => { logger.warn('recordOutcome failed', { error: e instanceof Error ? e.message : String(e), taskId: task.id }); });
359
361
  return result;
360
362
  }
361
363
  // Execute with timeout
@@ -367,7 +369,7 @@ export class DomainTaskExecutor {
367
369
  const errorMsg = 'error' in result ? result.error.message : 'Unknown error';
368
370
  await this.publishTaskFailed(task.id, errorMsg, domain);
369
371
  // ADR-051: Record failed outcome
370
- this.recordOutcome(task, routingTier, false, Date.now() - startTime).catch(() => { });
372
+ this.recordOutcome(task, routingTier, false, Date.now() - startTime).catch((e) => { logger.warn('recordOutcome failed', { error: e instanceof Error ? e.message : String(e), taskId: task.id }); });
371
373
  return {
372
374
  taskId: task.id,
373
375
  success: false,
@@ -378,7 +380,7 @@ export class DomainTaskExecutor {
378
380
  }
379
381
  await this.publishTaskCompleted(task.id, result.value, domain);
380
382
  // ADR-051: Record successful outcome
381
- this.recordOutcome(task, routingTier, true, Date.now() - startTime).catch(() => { });
383
+ this.recordOutcome(task, routingTier, true, Date.now() - startTime).catch((e) => { logger.warn('recordOutcome failed', { error: e instanceof Error ? e.message : String(e), taskId: task.id }); });
382
384
  // Save results to files if enabled
383
385
  let savedFiles;
384
386
  if (this._config.saveResults) {
@@ -417,7 +419,7 @@ export class DomainTaskExecutor {
417
419
  const errorMessage = toErrorMessage(error);
418
420
  await this.publishTaskFailed(task.id, errorMessage, domain);
419
421
  // ADR-051: Record failed outcome
420
- this.recordOutcome(task, routingTier, false, Date.now() - startTime).catch(() => { });
422
+ this.recordOutcome(task, routingTier, false, Date.now() - startTime).catch((e) => { logger.warn('recordOutcome failed', { error: e instanceof Error ? e.message : String(e), taskId: task.id }); });
421
423
  return {
422
424
  taskId: task.id,
423
425
  success: false,
@@ -20,9 +20,33 @@ export declare class SASTScanner {
20
20
  */
21
21
  scanFiles(files: FilePath[]): Promise<Result<SASTResult>>;
22
22
  /**
23
- * Scan with specific rule sets
23
+ * Scan with specific rule sets.
24
+ * Runs pattern-based scanning and semgrep (when available) in parallel,
25
+ * then merges and deduplicates results.
24
26
  */
25
27
  scanWithRules(files: FilePath[], ruleSetIds: string[]): Promise<Result<SASTResult>>;
28
+ /**
29
+ * Run pattern-based scanning on all files
30
+ */
31
+ private runPatternScanning;
32
+ /**
33
+ * Run semgrep scanning when enabled and available.
34
+ * Returns converted vulnerabilities or empty array on failure/unavailability.
35
+ */
36
+ private runSemgrepScanning;
37
+ /**
38
+ * Resolve the common parent directory from a set of file paths
39
+ */
40
+ private resolveTargetDirectory;
41
+ /**
42
+ * Map semgrep OWASP category string to VulnerabilityCategory
43
+ */
44
+ private mapSemgrepCategory;
45
+ /**
46
+ * Merge pattern-based and semgrep vulnerabilities, deduplicating
47
+ * findings that overlap on the same file and line.
48
+ */
49
+ private mergeVulnerabilities;
26
50
  /**
27
51
  * Get available rule sets
28
52
  */
@@ -7,6 +7,7 @@ import { ok, err } from '@shared/types/index.js';
7
7
  import { ALL_SECURITY_PATTERNS, BUILT_IN_RULE_SETS } from './security-patterns.js';
8
8
  import { toError } from '@shared/error-utils.js';
9
9
  import { safeJsonParse } from '@shared/safe-json.js';
10
+ import { isSemgrepAvailable, runSemgrepWithRules, convertSemgrepFindings, } from '../semgrep-integration.js';
10
11
  // ============================================================================
11
12
  // SAST Scanner Service
12
13
  // ============================================================================
@@ -35,7 +36,9 @@ export class SASTScanner {
35
36
  return this.scanWithRules(files, this.config.defaultRuleSets);
36
37
  }
37
38
  /**
38
- * Scan with specific rule sets
39
+ * Scan with specific rule sets.
40
+ * Runs pattern-based scanning and semgrep (when available) in parallel,
41
+ * then merges and deduplicates results.
39
42
  */
40
43
  async scanWithRules(files, ruleSetIds) {
41
44
  const scanId = uuidv4();
@@ -50,22 +53,23 @@ export class SASTScanner {
50
53
  if (ruleSets.length === 0) {
51
54
  return err(new Error(`No valid rule sets found: ${ruleSetIds.join(', ')}`));
52
55
  }
53
- // Perform static analysis on each file
54
- const vulnerabilities = [];
55
- let linesScanned = 0;
56
- for (const file of files) {
57
- const fileVulns = await this.analyzeFile(file, ruleSets);
58
- vulnerabilities.push(...fileVulns.vulnerabilities);
59
- linesScanned += fileVulns.linesScanned;
60
- }
56
+ // Run pattern-based scanning and semgrep in parallel
57
+ const [patternResult, semgrepVulns] = await Promise.all([
58
+ this.runPatternScanning(files, ruleSets),
59
+ this.runSemgrepScanning(files, ruleSetIds),
60
+ ]);
61
+ // Merge pattern-based and semgrep findings, deduplicating by file+line
62
+ const vulnerabilities = this.mergeVulnerabilities(patternResult.vulnerabilities, semgrepVulns);
63
+ const linesScanned = patternResult.linesScanned;
61
64
  const scanDurationMs = Date.now() - startTime;
62
65
  // Calculate summary
63
66
  const summary = this.calculateSummary(vulnerabilities, files.length, scanDurationMs);
64
- // Calculate coverage
67
+ // Calculate coverage — include semgrep rules when they ran
68
+ const patternRules = ruleSets.reduce((acc, rs) => acc + rs.ruleCount, 0);
65
69
  const coverage = {
66
70
  filesScanned: files.length,
67
71
  linesScanned,
68
- rulesApplied: ruleSets.reduce((acc, rs) => acc + rs.ruleCount, 0),
72
+ rulesApplied: patternRules + (semgrepVulns.length > 0 ? semgrepVulns.length : 0),
69
73
  };
70
74
  // Store scan results in memory
71
75
  await this.storeScanResults(scanId, 'sast', vulnerabilities, summary);
@@ -82,6 +86,131 @@ export class SASTScanner {
82
86
  return err(toError(error));
83
87
  }
84
88
  }
89
+ /**
90
+ * Run pattern-based scanning on all files
91
+ */
92
+ async runPatternScanning(files, ruleSets) {
93
+ const vulnerabilities = [];
94
+ let linesScanned = 0;
95
+ for (const file of files) {
96
+ const fileVulns = await this.analyzeFile(file, ruleSets);
97
+ vulnerabilities.push(...fileVulns.vulnerabilities);
98
+ linesScanned += fileVulns.linesScanned;
99
+ }
100
+ return { vulnerabilities, linesScanned };
101
+ }
102
+ /**
103
+ * Run semgrep scanning when enabled and available.
104
+ * Returns converted vulnerabilities or empty array on failure/unavailability.
105
+ */
106
+ async runSemgrepScanning(files, ruleSetIds) {
107
+ if (!this.config.enableSemgrep) {
108
+ return [];
109
+ }
110
+ try {
111
+ const available = await isSemgrepAvailable();
112
+ if (!available) {
113
+ return [];
114
+ }
115
+ // Determine target directory from files (use common parent)
116
+ const targetDir = this.resolveTargetDirectory(files);
117
+ const semgrepResult = await runSemgrepWithRules(targetDir, ruleSetIds);
118
+ if (!semgrepResult.success || semgrepResult.findings.length === 0) {
119
+ return [];
120
+ }
121
+ // Convert semgrep findings to our Vulnerability format
122
+ const converted = convertSemgrepFindings(semgrepResult.findings);
123
+ return converted.map(f => ({
124
+ id: uuidv4(),
125
+ cveId: undefined,
126
+ title: f.title,
127
+ description: `[semgrep] ${f.description}`,
128
+ severity: f.severity,
129
+ category: this.mapSemgrepCategory(f.owaspCategory),
130
+ location: {
131
+ file: f.file,
132
+ line: f.line,
133
+ column: f.column,
134
+ snippet: f.snippet,
135
+ },
136
+ remediation: {
137
+ description: f.remediation,
138
+ estimatedEffort: 'moderate',
139
+ automatable: false,
140
+ },
141
+ references: f.references,
142
+ }));
143
+ }
144
+ catch {
145
+ // Semgrep failure is non-fatal — pattern scanning still covers us
146
+ return [];
147
+ }
148
+ }
149
+ /**
150
+ * Resolve the common parent directory from a set of file paths
151
+ */
152
+ resolveTargetDirectory(files) {
153
+ if (files.length === 0)
154
+ return '.';
155
+ if (files.length === 1)
156
+ return files[0].directory || '.';
157
+ // Find common prefix of all directories
158
+ const dirs = files.map(f => f.directory || '.');
159
+ const first = dirs[0];
160
+ let commonLen = first.length;
161
+ for (let i = 1; i < dirs.length; i++) {
162
+ const dir = dirs[i];
163
+ const maxLen = Math.min(commonLen, dir.length);
164
+ let j = 0;
165
+ while (j < maxLen && first[j] === dir[j])
166
+ j++;
167
+ commonLen = j;
168
+ }
169
+ const common = first.substring(0, commonLen);
170
+ // Trim to last path separator
171
+ const lastSep = common.lastIndexOf('/');
172
+ return lastSep > 0 ? common.substring(0, lastSep) : common || '.';
173
+ }
174
+ /**
175
+ * Map semgrep OWASP category string to VulnerabilityCategory
176
+ */
177
+ mapSemgrepCategory(owaspCategory) {
178
+ if (!owaspCategory)
179
+ return 'injection';
180
+ const categoryMap = {
181
+ 'A01': 'access-control',
182
+ 'A02': 'sensitive-data',
183
+ 'A03': 'injection',
184
+ 'A04': 'insecure-deserialization',
185
+ 'A05': 'security-misconfiguration',
186
+ 'A06': 'vulnerable-components',
187
+ 'A07': 'broken-auth',
188
+ 'A08': 'insecure-deserialization',
189
+ 'A09': 'insufficient-logging',
190
+ 'A10': 'xxe',
191
+ };
192
+ // Try exact match or prefix match (e.g. "A03:2021-Injection")
193
+ for (const [key, value] of Object.entries(categoryMap)) {
194
+ if (owaspCategory.startsWith(key))
195
+ return value;
196
+ }
197
+ return 'injection';
198
+ }
199
+ /**
200
+ * Merge pattern-based and semgrep vulnerabilities, deduplicating
201
+ * findings that overlap on the same file and line.
202
+ */
203
+ mergeVulnerabilities(patternVulns, semgrepVulns) {
204
+ if (semgrepVulns.length === 0)
205
+ return patternVulns;
206
+ if (patternVulns.length === 0)
207
+ return semgrepVulns;
208
+ // Build a set of file:line keys from pattern results
209
+ const patternKeys = new Set(patternVulns.map(v => `${v.location.file}:${v.location.line ?? 0}:${v.category}`));
210
+ // Only add semgrep findings that don't overlap with pattern findings
211
+ const uniqueSemgrep = semgrepVulns.filter(v => !patternKeys.has(`${v.location.file}:${v.location.line ?? 0}:${v.category}`));
212
+ return [...patternVulns, ...uniqueSemgrep];
213
+ }
85
214
  /**
86
215
  * Get available rule sets
87
216
  */
@@ -19,6 +19,8 @@ export interface SecurityScannerConfig {
19
19
  enableLLMAnalysis: boolean;
20
20
  /** ADR-051: Model tier for LLM calls (1=Haiku, 2=Sonnet, 4=Opus) */
21
21
  llmModelTier: number;
22
+ /** Enable semgrep integration when semgrep is installed (default: true) */
23
+ enableSemgrep: boolean;
22
24
  }
23
25
  /**
24
26
  * Dependencies for SecurityScannerService
@@ -11,5 +11,6 @@ export const DEFAULT_CONFIG = {
11
11
  dastActiveScanning: false,
12
12
  enableLLMAnalysis: true, // On by default - opt-out (ADR-051)
13
13
  llmModelTier: 4, // Opus for security analysis (needs expert reasoning)
14
+ enableSemgrep: true, // Use semgrep when installed for real SAST
14
15
  };
15
16
  //# sourceMappingURL=scanner-types.js.map
@@ -263,6 +263,8 @@ export class MinCutTestOptimizerImpl {
263
263
  criticalTests.add(promotedId);
264
264
  skippableTests.delete(promotedId);
265
265
  const promotedTest = testMap.get(promotedId);
266
+ if (!promotedTest)
267
+ continue;
266
268
  for (const file of promotedTest.coveredFiles) {
267
269
  covered.add(file);
268
270
  }
@@ -21,6 +21,8 @@ export interface AgentsInstallResult {
21
21
  overlaysApplied?: string[];
22
22
  /** Warnings from overlay loading */
23
23
  overlayWarnings?: string[];
24
+ /** MCP dependency validation warnings (advisory only) */
25
+ mcpValidationWarnings?: string[];
24
26
  }
25
27
  export interface AgentsInstallerOptions {
26
28
  /** Project root directory */
@@ -10,6 +10,7 @@ import { join, dirname } from 'path';
10
10
  import { fileURLToPath } from 'url';
11
11
  import { toErrorMessage } from '../shared/error-utils.js';
12
12
  import { loadOverlays, applyOverlayToContent } from '../agents/overlay-loader.js';
13
+ import { validateFleetMcpDeps } from '../validation/steps/agent-mcp-validator.js';
13
14
  // ============================================================================
14
15
  // Agent Categories
15
16
  // ============================================================================
@@ -298,6 +299,18 @@ export class AgentsInstaller {
298
299
  if (result.overlaysApplied.length > 0) {
299
300
  console.error(`[AgentsInstaller] Applied ${result.overlaysApplied.length} agent overlay(s): ${result.overlaysApplied.join(', ')}`);
300
301
  }
302
+ // Validate MCP dependencies (advisory only -- Issue #342 Item 1)
303
+ const mcpValidation = validateFleetMcpDeps(targetAgentsDir, this.projectRoot);
304
+ result.mcpValidationWarnings = mcpValidation.warnings;
305
+ if (mcpValidation.warnings.length > 0) {
306
+ console.error(`[AgentsInstaller] MCP dependency warnings (${mcpValidation.agentsWithMissingDeps.length} agents affected):`);
307
+ for (const warning of mcpValidation.warnings.slice(0, 10)) {
308
+ console.error(` ${warning}`);
309
+ }
310
+ if (mcpValidation.warnings.length > 10) {
311
+ console.error(` ... and ${mcpValidation.warnings.length - 10} more warnings`);
312
+ }
313
+ }
301
314
  // Create agents index file
302
315
  await this.createAgentsIndex(targetAgentsDir, result.installed);
303
316
  return result;