agentic-qe 3.7.19 → 3.7.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/v3/qe-deployment-advisor.md +14 -0
- package/.claude/agents/v3/qe-gap-detector.md +8 -0
- package/.claude/agents/v3/qe-impact-analyzer.md +11 -0
- package/.claude/agents/v3/qe-queen-coordinator.md +45 -0
- package/.claude/agents/v3/qe-root-cause-analyzer.md +11 -0
- package/.claude/agents/v3/qe-security-scanner.md +25 -16
- package/.claude/helpers/brain-checkpoint.cjs +3 -3
- package/.claude/helpers/statusline-v3.cjs +4 -3
- package/.claude/skills/skills-manifest.json +1 -1
- package/CHANGELOG.md +27 -0
- package/assets/agents/v3/qe-deployment-advisor.md +14 -0
- package/assets/agents/v3/qe-gap-detector.md +8 -0
- package/assets/agents/v3/qe-impact-analyzer.md +11 -0
- package/assets/agents/v3/qe-queen-coordinator.md +45 -0
- package/assets/agents/v3/qe-root-cause-analyzer.md +11 -0
- package/assets/agents/v3/qe-security-scanner.md +25 -16
- package/assets/helpers/statusline-v3.cjs +4 -3
- package/dist/adapters/claude-flow/model-router-bridge.d.ts +0 -6
- package/dist/adapters/claude-flow/model-router-bridge.js +4 -17
- package/dist/adapters/claude-flow/pretrain-bridge.d.ts +0 -6
- package/dist/adapters/claude-flow/pretrain-bridge.js +6 -19
- package/dist/adapters/claude-flow/trajectory-bridge.d.ts +0 -6
- package/dist/adapters/claude-flow/trajectory-bridge.js +21 -23
- package/dist/cli/bundle.js +1821 -986
- package/dist/coordination/protocols/security-audit.d.ts +3 -6
- package/dist/coordination/protocols/security-audit.js +8 -88
- package/dist/coordination/queen-coordinator.d.ts +13 -0
- package/dist/coordination/queen-coordinator.js +76 -0
- package/dist/coordination/queen-task-management.d.ts +2 -0
- package/dist/coordination/queen-task-management.js +10 -0
- package/dist/coordination/queen-types.d.ts +3 -0
- package/dist/coordination/task-executor.js +7 -5
- package/dist/domains/security-compliance/services/scanners/sast-scanner.d.ts +25 -1
- package/dist/domains/security-compliance/services/scanners/sast-scanner.js +140 -11
- package/dist/domains/security-compliance/services/scanners/scanner-types.d.ts +2 -0
- package/dist/domains/security-compliance/services/scanners/scanner-types.js +1 -0
- package/dist/domains/test-execution/services/mincut-test-optimizer.js +2 -0
- package/dist/governance/continue-gate-integration.js +1 -1
- package/dist/governance/feature-flags.js +2 -2
- package/dist/init/agents-installer.d.ts +2 -0
- package/dist/init/agents-installer.js +13 -0
- package/dist/init/enhancements/claude-flow-adapter.js +51 -24
- package/dist/init/init-wizard.js +1 -1
- package/dist/init/phases/07-hooks.js +6 -6
- package/dist/init/settings-merge.js +2 -0
- package/dist/integrations/ruvector/brain-rvf-exporter.js +14 -2
- package/dist/learning/experience-capture-middleware.js +3 -1
- package/dist/learning/qe-reasoning-bank.js +3 -3
- package/dist/learning/sqlite-persistence.js +16 -0
- package/dist/learning/token-tracker.js +4 -2
- package/dist/mcp/bundle.js +1183 -504
- package/dist/routing/agent-dependency-graph.d.ts +77 -0
- package/dist/routing/agent-dependency-graph.js +359 -0
- package/dist/routing/co-execution-repository.d.ts +68 -0
- package/dist/routing/co-execution-repository.js +184 -0
- package/dist/routing/index.d.ts +6 -0
- package/dist/routing/index.js +6 -0
- package/dist/routing/qe-task-router.d.ts +7 -0
- package/dist/routing/qe-task-router.js +63 -1
- package/dist/routing/signal-merger.d.ts +81 -0
- package/dist/routing/signal-merger.js +136 -0
- package/dist/routing/types.d.ts +1 -0
- package/dist/shared/llm/providers/azure-openai.js +3 -2
- package/dist/shared/llm/providers/bedrock.js +3 -2
- package/dist/shared/llm/providers/claude.js +3 -2
- package/dist/shared/llm/providers/gemini.js +3 -2
- package/dist/shared/llm/providers/openai.js +3 -2
- package/dist/shared/llm/providers/openrouter.js +3 -2
- package/dist/shared/llm/retry.d.ts +10 -0
- package/dist/shared/llm/retry.js +16 -0
- package/dist/shared/llm/router/agent-router-config.d.ts +2 -1
- package/dist/shared/llm/router/agent-router-config.js +38 -88
- package/dist/validation/index.d.ts +2 -0
- package/dist/validation/index.js +4 -0
- package/dist/validation/steps/agent-mcp-validator.d.ts +88 -0
- package/dist/validation/steps/agent-mcp-validator.js +254 -0
- package/package.json +1 -1
|
@@ -169,14 +169,11 @@ export declare class SecurityAuditProtocol {
|
|
|
169
169
|
*/
|
|
170
170
|
execute(trigger: SecurityAuditTrigger): Promise<Result<SecurityAuditResult>>;
|
|
171
171
|
/**
|
|
172
|
-
* Scan for vulnerabilities using SAST
|
|
173
|
-
*
|
|
172
|
+
* Scan for vulnerabilities using SAST.
|
|
173
|
+
* SASTScanner now integrates semgrep internally (runs alongside pattern scanning
|
|
174
|
+
* when semgrep is installed), so no separate fallback is needed here.
|
|
174
175
|
*/
|
|
175
176
|
scanVulnerabilities(options: SecurityAuditOptions): Promise<Result<SASTResult>>;
|
|
176
|
-
/**
|
|
177
|
-
* Map semgrep OWASP category to VulnerabilityCategory
|
|
178
|
-
*/
|
|
179
|
-
private mapSemgrepCategory;
|
|
180
177
|
/**
|
|
181
178
|
* Scan dependencies for vulnerabilities
|
|
182
179
|
* Delegates to real SecurityScannerService which uses OSV API for real vulnerability data
|
|
@@ -10,6 +10,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
10
10
|
import { ok, err, } from '../../shared/types/index.js';
|
|
11
11
|
import { FilePath, RiskScore } from '../../shared/value-objects/index.js';
|
|
12
12
|
import { createEvent, } from '../../shared/events/domain-events.js';
|
|
13
|
+
// Semgrep is now integrated directly into SASTScanner (no coordination-layer fallback needed)
|
|
13
14
|
import { toError } from '../../shared/error-utils.js';
|
|
14
15
|
// CQ-005: Use DomainServiceRegistry instead of dynamic imports from domains/
|
|
15
16
|
import { DomainServiceRegistry, ServiceKeys } from '../../shared/domain-service-registry.js';
|
|
@@ -22,18 +23,6 @@ function resolveSecurityScannerService(memory) {
|
|
|
22
23
|
const factory = DomainServiceRegistry.resolve(ServiceKeys.SecurityScannerService);
|
|
23
24
|
return factory(memory);
|
|
24
25
|
}
|
|
25
|
-
function resolveIsSemgrepAvailable() {
|
|
26
|
-
const fn = DomainServiceRegistry.resolve(ServiceKeys.isSemgrepAvailable);
|
|
27
|
-
return fn();
|
|
28
|
-
}
|
|
29
|
-
function resolveRunSemgrepWithRules(targetPath, ruleSetIds) {
|
|
30
|
-
const fn = DomainServiceRegistry.resolve(ServiceKeys.runSemgrepWithRules);
|
|
31
|
-
return fn(targetPath, ruleSetIds);
|
|
32
|
-
}
|
|
33
|
-
function resolveConvertSemgrepFindings(findings) {
|
|
34
|
-
const fn = DomainServiceRegistry.resolve(ServiceKeys.convertSemgrepFindings);
|
|
35
|
-
return fn(findings);
|
|
36
|
-
}
|
|
37
26
|
// ============================================================================
|
|
38
27
|
// Protocol Events
|
|
39
28
|
// ============================================================================
|
|
@@ -208,8 +197,9 @@ export class SecurityAuditProtocol {
|
|
|
208
197
|
// Scanning Methods
|
|
209
198
|
// ==========================================================================
|
|
210
199
|
/**
|
|
211
|
-
* Scan for vulnerabilities using SAST
|
|
212
|
-
*
|
|
200
|
+
* Scan for vulnerabilities using SAST.
|
|
201
|
+
* SASTScanner now integrates semgrep internally (runs alongside pattern scanning
|
|
202
|
+
* when semgrep is installed), so no separate fallback is needed here.
|
|
213
203
|
*/
|
|
214
204
|
async scanVulnerabilities(options) {
|
|
215
205
|
try {
|
|
@@ -219,96 +209,26 @@ export class SecurityAuditProtocol {
|
|
|
219
209
|
return err(agentId.error);
|
|
220
210
|
}
|
|
221
211
|
const files = this.config.scanPaths.map(path => FilePath.create(path));
|
|
222
|
-
// Try real SecurityScannerService first
|
|
223
212
|
try {
|
|
224
213
|
const scanner = this.getSecurityScanner();
|
|
225
214
|
const ruleSetIds = options.ruleSetIds || ['owasp-top-10', 'cwe-sans-25'];
|
|
215
|
+
// SASTScanner.scanWithRules now runs pattern scanning + semgrep in parallel
|
|
226
216
|
const scanResult = await scanner.scanWithRules(files, ruleSetIds);
|
|
227
217
|
if (scanResult.success) {
|
|
228
218
|
return ok(scanResult.value);
|
|
229
219
|
}
|
|
230
|
-
|
|
220
|
+
return err(scanResult.error);
|
|
231
221
|
}
|
|
232
222
|
catch (scannerError) {
|
|
233
|
-
// Scanner unavailable - log and continue to fallback
|
|
234
223
|
await this.memory.set('security-audit:scanner-error', { error: String(scannerError), timestamp: new Date().toISOString() }, { namespace: 'security-compliance', ttl: 3600 });
|
|
224
|
+
return err(new Error(`SAST scanning failed: ${String(scannerError)}. ` +
|
|
225
|
+
'Ensure SecurityScannerService is properly configured.'));
|
|
235
226
|
}
|
|
236
|
-
// Try semgrep if available as secondary option
|
|
237
|
-
const semgrepAvailable = await resolveIsSemgrepAvailable();
|
|
238
|
-
if (semgrepAvailable) {
|
|
239
|
-
try {
|
|
240
|
-
const semgrepResult = await resolveRunSemgrepWithRules(this.config.scanPaths[0] || '.', options.ruleSetIds || ['owasp-top-10']);
|
|
241
|
-
if (semgrepResult.success && semgrepResult.findings.length > 0) {
|
|
242
|
-
const convertedFindings = resolveConvertSemgrepFindings(semgrepResult.findings);
|
|
243
|
-
const vulnerabilities = convertedFindings.map(f => ({
|
|
244
|
-
id: uuidv4(),
|
|
245
|
-
cveId: undefined,
|
|
246
|
-
title: f.title,
|
|
247
|
-
description: f.description,
|
|
248
|
-
severity: f.severity,
|
|
249
|
-
category: this.mapSemgrepCategory(f.owaspCategory || 'injection'),
|
|
250
|
-
location: {
|
|
251
|
-
file: f.file,
|
|
252
|
-
line: f.line,
|
|
253
|
-
column: f.column,
|
|
254
|
-
snippet: f.snippet,
|
|
255
|
-
},
|
|
256
|
-
remediation: {
|
|
257
|
-
description: f.remediation,
|
|
258
|
-
estimatedEffort: 'moderate',
|
|
259
|
-
automatable: false,
|
|
260
|
-
},
|
|
261
|
-
references: f.references,
|
|
262
|
-
}));
|
|
263
|
-
const summary = this.calculateSummary(vulnerabilities);
|
|
264
|
-
return ok({
|
|
265
|
-
scanId: uuidv4(),
|
|
266
|
-
vulnerabilities,
|
|
267
|
-
summary,
|
|
268
|
-
coverage: {
|
|
269
|
-
filesScanned: files.length,
|
|
270
|
-
linesScanned: vulnerabilities.length * 50,
|
|
271
|
-
rulesApplied: 45,
|
|
272
|
-
},
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
catch (semgrepError) {
|
|
277
|
-
// Semgrep failed - log error
|
|
278
|
-
await this.memory.set('security-audit:semgrep-error', { error: String(semgrepError), timestamp: new Date().toISOString() }, { namespace: 'security-compliance', ttl: 3600 });
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
// NO FALLBACK - Security scans must either succeed or fail explicitly
|
|
282
|
-
// An empty vulnerability list would falsely indicate "scan succeeded, nothing found"
|
|
283
|
-
// when in reality we couldn't scan at all
|
|
284
|
-
return err(new Error('SAST scanning unavailable: neither SecurityScannerService nor semgrep could execute. ' +
|
|
285
|
-
'Install semgrep (pip install semgrep) or ensure SecurityScannerService is properly configured.'));
|
|
286
227
|
}
|
|
287
228
|
catch (error) {
|
|
288
229
|
return err(toError(error));
|
|
289
230
|
}
|
|
290
231
|
}
|
|
291
|
-
/**
|
|
292
|
-
* Map semgrep OWASP category to VulnerabilityCategory
|
|
293
|
-
*/
|
|
294
|
-
mapSemgrepCategory(owaspCategory) {
|
|
295
|
-
const categoryMap = {
|
|
296
|
-
'A01': 'access-control',
|
|
297
|
-
'A02': 'sensitive-data',
|
|
298
|
-
'A03': 'injection',
|
|
299
|
-
'A04': 'insecure-deserialization',
|
|
300
|
-
'A05': 'security-misconfiguration',
|
|
301
|
-
'A06': 'vulnerable-components',
|
|
302
|
-
'A07': 'broken-auth',
|
|
303
|
-
'A08': 'insecure-deserialization',
|
|
304
|
-
'A09': 'insufficient-logging',
|
|
305
|
-
'A10': 'xxe',
|
|
306
|
-
'injection': 'injection',
|
|
307
|
-
'xss': 'xss',
|
|
308
|
-
'broken-auth': 'broken-auth',
|
|
309
|
-
};
|
|
310
|
-
return categoryMap[owaspCategory] || 'security-misconfiguration';
|
|
311
|
-
}
|
|
312
232
|
/**
|
|
313
233
|
* Scan dependencies for vulnerabilities
|
|
314
234
|
* Delegates to real SecurityScannerService which uses OSV API for real vulnerability data
|
|
@@ -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
|
-
//
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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:
|
|
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
|