@yasserkhanorg/e2e-agents 1.8.4 → 1.9.5
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/README.md +95 -8
- package/dist/adapters/cypress.d.ts +10 -0
- package/dist/adapters/cypress.d.ts.map +1 -0
- package/dist/adapters/cypress.js +86 -0
- package/dist/adapters/framework_adapter.d.ts +41 -0
- package/dist/adapters/framework_adapter.d.ts.map +1 -0
- package/dist/adapters/framework_adapter.js +152 -0
- package/dist/adapters/playwright.d.ts +10 -0
- package/dist/adapters/playwright.d.ts.map +1 -0
- package/dist/adapters/playwright.js +86 -0
- package/dist/adapters/pytest.d.ts +10 -0
- package/dist/adapters/pytest.d.ts.map +1 -0
- package/dist/adapters/pytest.js +96 -0
- package/dist/adapters/supertest.d.ts +12 -0
- package/dist/adapters/supertest.d.ts.map +1 -0
- package/dist/adapters/supertest.js +85 -0
- package/dist/agent/config.d.ts +1 -1
- package/dist/agent/config.d.ts.map +1 -1
- package/dist/agent/git.d.ts +1 -0
- package/dist/agent/git.d.ts.map +1 -1
- package/dist/agent/git.js +3 -0
- package/dist/agentic/fix_loop.d.ts.map +1 -1
- package/dist/agentic/fix_loop.js +5 -4
- package/dist/agentic/runner.d.ts +2 -0
- package/dist/agentic/runner.d.ts.map +1 -1
- package/dist/agentic/runner.js +15 -12
- package/dist/agents/cross-impact.d.ts.map +1 -1
- package/dist/agents/cross-impact.js +6 -1
- package/dist/agents/executor.d.ts.map +1 -1
- package/dist/agents/executor.js +6 -1
- package/dist/agents/strategist.d.ts.map +1 -1
- package/dist/agents/strategist.js +6 -1
- package/dist/agents/test-designer.d.ts.map +1 -1
- package/dist/agents/test-designer.js +6 -1
- package/dist/anthropic_provider.d.ts.map +1 -1
- package/dist/anthropic_provider.js +1 -0
- package/dist/base_provider.d.ts +56 -0
- package/dist/base_provider.d.ts.map +1 -1
- package/dist/base_provider.js +123 -1
- package/dist/budget_ledger.d.ts +28 -0
- package/dist/budget_ledger.d.ts.map +1 -0
- package/dist/budget_ledger.js +62 -0
- package/dist/cache/cached_provider.d.ts +45 -0
- package/dist/cache/cached_provider.d.ts.map +1 -0
- package/dist/cache/cached_provider.js +88 -0
- package/dist/cache/response_cache.d.ts +79 -0
- package/dist/cache/response_cache.d.ts.map +1 -0
- package/dist/cache/response_cache.js +177 -0
- package/dist/cli/commands/bootstrap.d.ts +3 -0
- package/dist/cli/commands/bootstrap.d.ts.map +1 -0
- package/dist/cli/commands/bootstrap.js +109 -0
- package/dist/cli/commands/cost_report.d.ts +3 -0
- package/dist/cli/commands/cost_report.d.ts.map +1 -0
- package/dist/cli/commands/cost_report.js +115 -0
- package/dist/cli/commands/crew.d.ts.map +1 -1
- package/dist/cli/commands/crew.js +118 -1
- package/dist/cli/commands/gate.d.ts +3 -0
- package/dist/cli/commands/gate.d.ts.map +1 -0
- package/dist/cli/commands/gate.js +86 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +7 -62
- package/dist/cli/commands/plan_crew.d.ts.map +1 -1
- package/dist/cli/commands/plan_crew.js +33 -21
- package/dist/cli/commands/train.d.ts.map +1 -1
- package/dist/cli/commands/train.js +16 -21
- package/dist/cli/defaults.d.ts +35 -0
- package/dist/cli/defaults.d.ts.map +1 -0
- package/dist/cli/defaults.js +125 -0
- package/dist/cli/errors.d.ts +27 -0
- package/dist/cli/errors.d.ts.map +1 -0
- package/dist/cli/errors.js +57 -0
- package/dist/cli/parse_args.d.ts.map +1 -1
- package/dist/cli/parse_args.js +24 -2
- package/dist/cli/types.d.ts +7 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli.js +47 -2
- package/dist/crew/context.d.ts +15 -0
- package/dist/crew/context.d.ts.map +1 -1
- package/dist/crew/orchestrator.d.ts +14 -0
- package/dist/crew/orchestrator.d.ts.map +1 -1
- package/dist/crew/orchestrator.js +162 -4
- package/dist/crew/protocol.d.ts +13 -0
- package/dist/crew/protocol.d.ts.map +1 -1
- package/dist/crew/provider.d.ts +15 -1
- package/dist/crew/provider.d.ts.map +1 -1
- package/dist/crew/provider.js +24 -4
- package/dist/custom_provider.d.ts.map +1 -1
- package/dist/custom_provider.js +1 -0
- package/dist/engine/diff_loader.d.ts.map +1 -1
- package/dist/engine/diff_loader.js +3 -14
- package/dist/engine/impact_engine.d.ts.map +1 -1
- package/dist/engine/impact_engine.js +9 -23
- package/dist/esm/adapters/cypress.js +49 -0
- package/dist/esm/adapters/framework_adapter.js +114 -0
- package/dist/esm/adapters/playwright.js +49 -0
- package/dist/esm/adapters/pytest.js +59 -0
- package/dist/esm/adapters/supertest.js +48 -0
- package/dist/esm/agent/git.js +3 -1
- package/dist/esm/agentic/fix_loop.js +5 -4
- package/dist/esm/agentic/runner.js +15 -12
- package/dist/esm/agents/cross-impact.js +6 -1
- package/dist/esm/agents/executor.js +6 -1
- package/dist/esm/agents/strategist.js +6 -1
- package/dist/esm/agents/test-designer.js +6 -1
- package/dist/esm/anthropic_provider.js +1 -0
- package/dist/esm/base_provider.js +121 -0
- package/dist/esm/budget_ledger.js +58 -0
- package/dist/esm/cache/cached_provider.js +82 -0
- package/dist/esm/cache/response_cache.js +140 -0
- package/dist/esm/cli/commands/bootstrap.js +106 -0
- package/dist/esm/cli/commands/cost_report.js +112 -0
- package/dist/esm/cli/commands/crew.js +118 -1
- package/dist/esm/cli/commands/gate.js +83 -0
- package/dist/esm/cli/commands/init.js +3 -58
- package/dist/esm/cli/commands/plan_crew.js +33 -21
- package/dist/esm/cli/commands/train.js +16 -21
- package/dist/esm/cli/defaults.js +118 -0
- package/dist/esm/cli/errors.js +52 -0
- package/dist/esm/cli/parse_args.js +24 -2
- package/dist/esm/cli.js +47 -2
- package/dist/esm/crew/orchestrator.js +162 -4
- package/dist/esm/crew/provider.js +24 -4
- package/dist/esm/custom_provider.js +1 -0
- package/dist/esm/engine/diff_loader.js +1 -12
- package/dist/esm/engine/impact_engine.js +9 -23
- package/dist/esm/index.js +21 -0
- package/dist/esm/knowledge/cluster_utils.js +60 -0
- package/dist/esm/knowledge/kg_bridge.js +381 -0
- package/dist/esm/knowledge/kg_types.js +3 -0
- package/dist/esm/knowledge/route_families.js +89 -0
- package/dist/esm/mcp-server.js +2 -4
- package/dist/esm/metrics/prometheus.js +149 -0
- package/dist/esm/model_router.js +59 -0
- package/dist/esm/ollama_provider.js +1 -0
- package/dist/esm/openai_provider.js +1 -0
- package/dist/esm/pipeline/orchestrator.js +6 -12
- package/dist/esm/pipeline/stage0_preprocess.js +12 -19
- package/dist/esm/pipeline/stage2_coverage.js +1 -0
- package/dist/esm/pipeline/stage3_generation.js +1 -0
- package/dist/esm/progress.js +112 -0
- package/dist/esm/prompts/coverage.js +7 -24
- package/dist/esm/prompts/cross-impact.js +3 -21
- package/dist/esm/prompts/generation.js +158 -36
- package/dist/esm/prompts/generation_profile.js +147 -0
- package/dist/esm/prompts/heal.js +33 -15
- package/dist/esm/prompts/impact.js +3 -22
- package/dist/esm/prompts/json_extract.js +36 -0
- package/dist/esm/prompts/strategist.js +2 -20
- package/dist/esm/prompts/test-designer.js +6 -21
- package/dist/esm/provider_factory.js +6 -4
- package/dist/esm/reporters/junit.js +86 -0
- package/dist/esm/reporters/reporter.js +3 -0
- package/dist/esm/reporters/sarif.js +131 -0
- package/dist/esm/resilience/circuit_breaker.js +78 -0
- package/dist/esm/resilience/retry.js +56 -0
- package/dist/esm/sanitize.js +66 -0
- package/dist/esm/training/kg_scanner.js +115 -0
- package/dist/esm/training/scanner.js +27 -34
- package/dist/esm/version.js +33 -0
- package/dist/index.d.ts +21 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -1
- package/dist/knowledge/cluster_utils.d.ts +28 -0
- package/dist/knowledge/cluster_utils.d.ts.map +1 -0
- package/dist/knowledge/cluster_utils.js +67 -0
- package/dist/knowledge/kg_bridge.d.ts +31 -0
- package/dist/knowledge/kg_bridge.d.ts.map +1 -0
- package/dist/knowledge/kg_bridge.js +388 -0
- package/dist/knowledge/kg_types.d.ts +75 -0
- package/dist/knowledge/kg_types.d.ts.map +1 -0
- package/dist/knowledge/kg_types.js +4 -0
- package/dist/knowledge/route_families.d.ts +18 -0
- package/dist/knowledge/route_families.d.ts.map +1 -1
- package/dist/knowledge/route_families.js +91 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +2 -4
- package/dist/metrics/prometheus.d.ts +37 -0
- package/dist/metrics/prometheus.d.ts.map +1 -0
- package/dist/metrics/prometheus.js +153 -0
- package/dist/model_router.d.ts +28 -0
- package/dist/model_router.d.ts.map +1 -0
- package/dist/model_router.js +63 -0
- package/dist/ollama_provider.d.ts.map +1 -1
- package/dist/ollama_provider.js +1 -0
- package/dist/openai_provider.d.ts.map +1 -1
- package/dist/openai_provider.js +1 -0
- package/dist/pipeline/orchestrator.d.ts +2 -0
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +6 -12
- package/dist/pipeline/stage0_preprocess.d.ts.map +1 -1
- package/dist/pipeline/stage0_preprocess.js +11 -18
- package/dist/pipeline/stage2_coverage.d.ts +2 -0
- package/dist/pipeline/stage2_coverage.d.ts.map +1 -1
- package/dist/pipeline/stage2_coverage.js +1 -0
- package/dist/pipeline/stage3_generation.d.ts +2 -0
- package/dist/pipeline/stage3_generation.d.ts.map +1 -1
- package/dist/pipeline/stage3_generation.js +1 -0
- package/dist/pipeline/stage4_heal.d.ts +2 -0
- package/dist/pipeline/stage4_heal.d.ts.map +1 -1
- package/dist/progress.d.ts +22 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +116 -0
- package/dist/prompts/coverage.d.ts +2 -0
- package/dist/prompts/coverage.d.ts.map +1 -1
- package/dist/prompts/coverage.js +7 -24
- package/dist/prompts/cross-impact.d.ts +1 -0
- package/dist/prompts/cross-impact.d.ts.map +1 -1
- package/dist/prompts/cross-impact.js +3 -21
- package/dist/prompts/generation.d.ts +3 -1
- package/dist/prompts/generation.d.ts.map +1 -1
- package/dist/prompts/generation.js +158 -36
- package/dist/prompts/generation_profile.d.ts +29 -0
- package/dist/prompts/generation_profile.d.ts.map +1 -0
- package/dist/prompts/generation_profile.js +151 -0
- package/dist/prompts/heal.d.ts +3 -1
- package/dist/prompts/heal.d.ts.map +1 -1
- package/dist/prompts/heal.js +33 -15
- package/dist/prompts/impact.d.ts +1 -0
- package/dist/prompts/impact.d.ts.map +1 -1
- package/dist/prompts/impact.js +3 -22
- package/dist/prompts/json_extract.d.ts +14 -0
- package/dist/prompts/json_extract.d.ts.map +1 -0
- package/dist/prompts/json_extract.js +39 -0
- package/dist/prompts/strategist.d.ts.map +1 -1
- package/dist/prompts/strategist.js +2 -20
- package/dist/prompts/test-designer.d.ts +2 -0
- package/dist/prompts/test-designer.d.ts.map +1 -1
- package/dist/prompts/test-designer.js +6 -21
- package/dist/provider_factory.d.ts.map +1 -1
- package/dist/provider_factory.js +6 -4
- package/dist/reporters/junit.d.ts +6 -0
- package/dist/reporters/junit.d.ts.map +1 -0
- package/dist/reporters/junit.js +89 -0
- package/dist/reporters/reporter.d.ts +42 -0
- package/dist/reporters/reporter.d.ts.map +1 -0
- package/dist/reporters/reporter.js +4 -0
- package/dist/reporters/sarif.d.ts +7 -0
- package/dist/reporters/sarif.d.ts.map +1 -0
- package/dist/reporters/sarif.js +134 -0
- package/dist/resilience/circuit_breaker.d.ts +36 -0
- package/dist/resilience/circuit_breaker.d.ts.map +1 -0
- package/dist/resilience/circuit_breaker.js +82 -0
- package/dist/resilience/retry.d.ts +11 -0
- package/dist/resilience/retry.d.ts.map +1 -0
- package/dist/resilience/retry.js +59 -0
- package/dist/sanitize.d.ts +15 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +71 -0
- package/dist/training/kg_scanner.d.ts +13 -0
- package/dist/training/kg_scanner.d.ts.map +1 -0
- package/dist/training/kg_scanner.js +118 -0
- package/dist/training/scanner.d.ts +7 -2
- package/dist/training/scanner.d.ts.map +1 -1
- package/dist/training/scanner.js +27 -34
- package/dist/version.d.ts +6 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +36 -0
- package/package.json +7 -2
- package/schemas/route-families.schema.json +31 -1
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import { getChangedFiles, isTestFile } from '../agent/git.js';
|
|
7
7
|
import { preprocess } from '../pipeline/stage0_preprocess.js';
|
|
8
8
|
import { logger } from '../logger.js';
|
|
9
|
+
import { BudgetExceededError } from '../base_provider.js';
|
|
10
|
+
import { BudgetLedger } from '../budget_ledger.js';
|
|
9
11
|
import { createEmptyUsageStats, mergeUsageStats } from './context.js';
|
|
10
12
|
import { WORKFLOWS } from './workflows.js';
|
|
11
13
|
export class CrewOrchestrator {
|
|
@@ -15,10 +17,61 @@ export class CrewOrchestrator {
|
|
|
15
17
|
registerAgent(agent) {
|
|
16
18
|
this.agents.set(agent.role, agent);
|
|
17
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Load and register plugins from file paths.
|
|
22
|
+
* Each module must default-export an object satisfying AgentPlugin.
|
|
23
|
+
*/
|
|
24
|
+
async loadPlugins(pluginPaths) {
|
|
25
|
+
const loaded = [];
|
|
26
|
+
for (const pluginPath of pluginPaths) {
|
|
27
|
+
try {
|
|
28
|
+
// Security: Only allow relative paths (starting with . or ..) to prevent loading arbitrary modules.
|
|
29
|
+
// Absolute paths, URLs, and node_modules references are rejected.
|
|
30
|
+
if (!pluginPath.startsWith('.')) {
|
|
31
|
+
logger.warn(`Plugin path must be relative (start with ./): ${pluginPath} — skipped`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const resolved = new URL(pluginPath, `file://${process.cwd()}/`).href;
|
|
35
|
+
// Security: reject paths that resolve outside the workspace (e.g., ../../etc/evil.js)
|
|
36
|
+
const cwd = `file://${process.cwd()}/`;
|
|
37
|
+
if (!resolved.startsWith(cwd)) {
|
|
38
|
+
logger.warn(`Plugin path '${pluginPath}' resolves outside workspace — skipped`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const mod = await import(resolved);
|
|
42
|
+
const plugin = mod.default || mod;
|
|
43
|
+
if (!plugin.role || typeof plugin.execute !== 'function') {
|
|
44
|
+
logger.warn(`Plugin at ${pluginPath} missing required role/execute — skipped`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
// Warn if plugin overrides a built-in agent
|
|
48
|
+
if (this.agents.has(plugin.role)) {
|
|
49
|
+
logger.warn(`Plugin '${plugin.role}' overrides built-in agent — ensure this is intentional`);
|
|
50
|
+
}
|
|
51
|
+
this.agents.set(plugin.role, plugin);
|
|
52
|
+
loaded.push(plugin.role);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
56
|
+
logger.warn(`Failed to load plugin ${pluginPath}: ${msg}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return loaded;
|
|
60
|
+
}
|
|
18
61
|
async run(config) {
|
|
19
62
|
const workflow = WORKFLOWS[config.workflow || 'full-qa'];
|
|
20
63
|
const timings = {};
|
|
21
64
|
const warnings = [];
|
|
65
|
+
// Load plugins if configured, then inject them into workflow phases
|
|
66
|
+
const pluginRoles = [];
|
|
67
|
+
if (config.plugins && config.plugins.length > 0) {
|
|
68
|
+
const loaded = await this.loadPlugins(config.plugins);
|
|
69
|
+
pluginRoles.push(...loaded);
|
|
70
|
+
if (loaded.length > 0) {
|
|
71
|
+
logger.info(`Loaded ${loaded.length} plugins: ${loaded.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const effectivePhases = this.injectPluginsIntoPhases(workflow.phases, pluginRoles);
|
|
22
75
|
// Step 1: Get changed files
|
|
23
76
|
const gitResult = getChangedFiles(config.appPath, config.gitSince, {
|
|
24
77
|
includeUncommitted: config.gitIncludeUncommitted,
|
|
@@ -32,6 +85,8 @@ export class CrewOrchestrator {
|
|
|
32
85
|
if (changedFiles.length === 0) {
|
|
33
86
|
warnings.push('No changed application files detected.');
|
|
34
87
|
}
|
|
88
|
+
// Create shared budget ledger for aggregate cost tracking across all agents
|
|
89
|
+
const budgetLedger = config.budgetUSD ? new BudgetLedger(config.budgetUSD) : undefined;
|
|
35
90
|
// Initialize context (will be populated during preprocess phase)
|
|
36
91
|
const ctx = {
|
|
37
92
|
changedFiles,
|
|
@@ -46,6 +101,8 @@ export class CrewOrchestrator {
|
|
|
46
101
|
testsRoot: config.testsRoot,
|
|
47
102
|
gitSince: config.gitSince,
|
|
48
103
|
providerOverride: config.providerOverride,
|
|
104
|
+
budgetUSD: config.budgetUSD,
|
|
105
|
+
budgetLedger,
|
|
49
106
|
impactedFlows: [],
|
|
50
107
|
strategyEntries: [],
|
|
51
108
|
testDesigns: [],
|
|
@@ -54,14 +111,21 @@ export class CrewOrchestrator {
|
|
|
54
111
|
findings: [],
|
|
55
112
|
generatedSpecs: [],
|
|
56
113
|
usage: createEmptyUsageStats(),
|
|
114
|
+
agentUsage: [],
|
|
57
115
|
messages: [],
|
|
58
116
|
warnings,
|
|
59
117
|
};
|
|
60
118
|
// Execute each phase
|
|
61
|
-
for (const phase of
|
|
119
|
+
for (const phase of effectivePhases) {
|
|
62
120
|
const timer = logger.timer(`crew:${phase.name}`);
|
|
63
121
|
if (phase.handler === 'built-in') {
|
|
64
122
|
await this.runBuiltInPhase(phase.name, ctx, config);
|
|
123
|
+
// Dry-run: after preprocess, return summary without running agents
|
|
124
|
+
if (config.dryRun && phase.name === 'preprocess') {
|
|
125
|
+
timings[phase.name] = timer.end();
|
|
126
|
+
ctx.warnings.push('Dry run — no LLM calls were made.');
|
|
127
|
+
return { context: ctx, warnings, timings, dryRun: true };
|
|
128
|
+
}
|
|
65
129
|
}
|
|
66
130
|
else if (phase.parallel && phase.parallel.length > 0) {
|
|
67
131
|
await this.runParallel(phase.parallel, phase.name, ctx);
|
|
@@ -73,9 +137,10 @@ export class CrewOrchestrator {
|
|
|
73
137
|
warnings.push(`Phase '${phase.name}' has no handler, parallel, or sequential agents — skipped.`);
|
|
74
138
|
}
|
|
75
139
|
timings[phase.name] = timer.end();
|
|
76
|
-
// Budget check
|
|
77
|
-
|
|
78
|
-
|
|
140
|
+
// Budget check — prefer ledger (aggregate across all providers) over ctx.usage
|
|
141
|
+
const currentCost = budgetLedger ? budgetLedger.totalCost : ctx.usage.totalCost;
|
|
142
|
+
if (config.budgetUSD && currentCost >= config.budgetUSD) {
|
|
143
|
+
warnings.push(`Budget limit reached ($${currentCost.toFixed(4)} >= $${config.budgetUSD}). Stopping workflow.`);
|
|
79
144
|
break;
|
|
80
145
|
}
|
|
81
146
|
}
|
|
@@ -92,10 +157,19 @@ export class CrewOrchestrator {
|
|
|
92
157
|
};
|
|
93
158
|
}
|
|
94
159
|
const task = { role, action, input: null };
|
|
160
|
+
const startMs = Date.now();
|
|
95
161
|
try {
|
|
96
162
|
const result = await agent.execute(task, ctx);
|
|
163
|
+
const durationMs = Date.now() - startMs;
|
|
97
164
|
if (result.usage) {
|
|
98
165
|
mergeUsageStats(ctx.usage, result.usage);
|
|
166
|
+
ctx.agentUsage.push({
|
|
167
|
+
agent: role,
|
|
168
|
+
inputTokens: result.usage.totalInputTokens,
|
|
169
|
+
outputTokens: result.usage.totalOutputTokens,
|
|
170
|
+
cost: result.usage.totalCost,
|
|
171
|
+
durationMs,
|
|
172
|
+
});
|
|
99
173
|
}
|
|
100
174
|
if (result.warnings && result.warnings.length > 0) {
|
|
101
175
|
ctx.warnings.push(...result.warnings);
|
|
@@ -103,6 +177,10 @@ export class CrewOrchestrator {
|
|
|
103
177
|
return result;
|
|
104
178
|
}
|
|
105
179
|
catch (error) {
|
|
180
|
+
if (error instanceof BudgetExceededError) {
|
|
181
|
+
ctx.warnings.push(`Budget exceeded ($${error.currentCost.toFixed(4)} >= $${error.budgetUSD}). Agent '${role}' skipped.`);
|
|
182
|
+
return { role, status: 'failed', output: null, warnings: [error.message] };
|
|
183
|
+
}
|
|
106
184
|
const message = error instanceof Error ? error.message : String(error);
|
|
107
185
|
ctx.warnings.push(`Agent '${role}' failed: ${message}`);
|
|
108
186
|
return { role, status: 'failed', output: null, warnings: [message] };
|
|
@@ -158,6 +236,86 @@ export class CrewOrchestrator {
|
|
|
158
236
|
}
|
|
159
237
|
this.checkPhaseResults(phaseName, results, ctx);
|
|
160
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Inject loaded plugins into workflow phases based on their `phase` and `runAfter` fields.
|
|
241
|
+
* Plugins with `runAfter` dependencies are appended to the sequential list of the matching phase;
|
|
242
|
+
* plugins without `runAfter` are appended to the parallel list.
|
|
243
|
+
* Returns a new array of phases (does not mutate the original workflow definition).
|
|
244
|
+
*/
|
|
245
|
+
injectPluginsIntoPhases(phases, pluginRoles) {
|
|
246
|
+
if (pluginRoles.length === 0)
|
|
247
|
+
return phases;
|
|
248
|
+
// Build mutable copies keyed by phase name
|
|
249
|
+
const phaseMap = new Map();
|
|
250
|
+
const ordered = [];
|
|
251
|
+
for (const p of phases) {
|
|
252
|
+
ordered.push(p.name);
|
|
253
|
+
if (p.handler === 'built-in') {
|
|
254
|
+
phaseMap.set(p.name, { handler: 'built-in' });
|
|
255
|
+
}
|
|
256
|
+
else if (p.parallel) {
|
|
257
|
+
phaseMap.set(p.name, { parallel: [...p.parallel] });
|
|
258
|
+
}
|
|
259
|
+
else if (p.sequential) {
|
|
260
|
+
phaseMap.set(p.name, { sequential: [...p.sequential] });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
for (const role of pluginRoles) {
|
|
264
|
+
const agent = this.agents.get(role);
|
|
265
|
+
if (!agent)
|
|
266
|
+
continue;
|
|
267
|
+
const plugin = agent;
|
|
268
|
+
if (!plugin.phase)
|
|
269
|
+
continue;
|
|
270
|
+
const target = phaseMap.get(plugin.phase);
|
|
271
|
+
if (!target) {
|
|
272
|
+
logger.warn(`Plugin '${role}' targets phase '${plugin.phase}' which does not exist in workflow — skipped`);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (target.handler === 'built-in') {
|
|
276
|
+
logger.warn(`Plugin '${role}' targets built-in phase '${plugin.phase}' — not supported, skipped`);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const pluginRole = role;
|
|
280
|
+
if (plugin.runAfter && plugin.runAfter.length > 0) {
|
|
281
|
+
// Validate that runAfter dependencies are either in this phase or a prior phase
|
|
282
|
+
const phaseRoles = target.parallel || target.sequential || [];
|
|
283
|
+
const missingDeps = plugin.runAfter.filter((dep) => !phaseRoles.includes(dep) && !this.agents.has(dep));
|
|
284
|
+
if (missingDeps.length > 0) {
|
|
285
|
+
logger.warn(`Plugin '${role}' has unresolved runAfter deps [${missingDeps.join(', ')}] — injecting anyway`);
|
|
286
|
+
}
|
|
287
|
+
// Plugin has dependencies — must run sequentially
|
|
288
|
+
if (target.sequential) {
|
|
289
|
+
target.sequential.push(pluginRole);
|
|
290
|
+
}
|
|
291
|
+
else if (target.parallel) {
|
|
292
|
+
// Convert to sequential to respect dependency ordering
|
|
293
|
+
target.sequential = [...target.parallel, pluginRole];
|
|
294
|
+
delete target.parallel;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
if (target.parallel) {
|
|
299
|
+
target.parallel.push(pluginRole);
|
|
300
|
+
}
|
|
301
|
+
else if (target.sequential) {
|
|
302
|
+
target.sequential.push(pluginRole);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
logger.info(`Plugin '${role}' injected into phase '${plugin.phase}'`);
|
|
306
|
+
}
|
|
307
|
+
// Rebuild WorkflowPhase array
|
|
308
|
+
return ordered.map((name) => {
|
|
309
|
+
const entry = phaseMap.get(name);
|
|
310
|
+
if (entry.handler === 'built-in')
|
|
311
|
+
return { name, handler: 'built-in' };
|
|
312
|
+
if (entry.parallel)
|
|
313
|
+
return { name, parallel: entry.parallel };
|
|
314
|
+
if (entry.sequential)
|
|
315
|
+
return { name, sequential: entry.sequential };
|
|
316
|
+
throw new Error(`Phase '${name}' has no handler, parallel, or sequential agents after plugin injection`);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
161
319
|
checkPhaseResults(phaseName, results, ctx) {
|
|
162
320
|
const allFailed = results.length > 0 && results.every((r) => r.status === 'failed');
|
|
163
321
|
if (allFailed) {
|
|
@@ -5,9 +5,29 @@
|
|
|
5
5
|
* instantiation and prevents usage stats fragmentation.
|
|
6
6
|
*/
|
|
7
7
|
import { LLMProviderFactory } from '../provider_factory.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
import { BaseProvider } from '../base_provider.js';
|
|
9
|
+
import { ModelRouter } from '../model_router.js';
|
|
10
|
+
export async function getCrewProvider(providerOverride, budgetUSD, opts) {
|
|
11
|
+
let effectiveOverride = providerOverride;
|
|
12
|
+
// Apply model routing if configured and agent role is provided
|
|
13
|
+
if (opts?.agentRole && opts?.modelRoutingProviderType) {
|
|
14
|
+
const router = new ModelRouter(opts.modelRoutingProviderType, opts.modelRoutingOverrides);
|
|
15
|
+
const model = router.getModel(opts.agentRole);
|
|
16
|
+
if (model) {
|
|
17
|
+
// Override uses provider:model format (e.g., "anthropic:claude-haiku-4-5-20251001")
|
|
18
|
+
effectiveOverride = `${opts.modelRoutingProviderType}:${model}`;
|
|
19
|
+
}
|
|
11
20
|
}
|
|
12
|
-
|
|
21
|
+
const provider = effectiveOverride
|
|
22
|
+
? await LLMProviderFactory.createFromString(effectiveOverride)
|
|
23
|
+
: await LLMProviderFactory.createFromEnv();
|
|
24
|
+
if (provider instanceof BaseProvider) {
|
|
25
|
+
if (opts?.budgetLedger) {
|
|
26
|
+
provider.setBudgetLedger(opts.budgetLedger);
|
|
27
|
+
}
|
|
28
|
+
else if (budgetUSD !== undefined) {
|
|
29
|
+
provider.setBudget(budgetUSD);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return provider;
|
|
13
33
|
}
|
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
2
|
// See LICENSE.txt for license information.
|
|
3
|
-
import {
|
|
3
|
+
import { runGitRaw } from '../agent/git.js';
|
|
4
4
|
const MAX_DIFF_CHARS = 8000;
|
|
5
5
|
const MAX_TOTAL_CHARS = 60000;
|
|
6
6
|
const TRUNCATION_NOTICE = '\n... (diff truncated)';
|
|
7
|
-
function runGitRaw(args, cwd) {
|
|
8
|
-
const result = spawnSync('git', args, {
|
|
9
|
-
cwd,
|
|
10
|
-
encoding: 'utf-8',
|
|
11
|
-
timeout: 30000,
|
|
12
|
-
});
|
|
13
|
-
if (result.error || result.status !== 0) {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
return result.stdout;
|
|
17
|
-
}
|
|
18
7
|
/**
|
|
19
8
|
* Loads git diffs for the given changed files relative to the given since ref.
|
|
20
9
|
* Uses `git merge-base` to find the accurate base ref first.
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// See LICENSE.txt for license information.
|
|
3
3
|
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
4
4
|
import { join } from 'path';
|
|
5
|
-
import { loadRouteFamilyManifest, bindFilesToFamilies, getSpecDirsForBinding, getCypressSpecDirsForBinding, getPriorityForBinding, getUserFlowsForBinding, } from '../knowledge/route_families.js';
|
|
5
|
+
import { loadRouteFamilyManifest, buildHeuristicFamilies, bindFilesToFamilies, getSpecDirsForBinding, getCypressSpecDirsForBinding, getPriorityForBinding, getUserFlowsForBinding, } from '../knowledge/route_families.js';
|
|
6
|
+
import { isTestFile } from '../agent/git.js';
|
|
6
7
|
function scanDirForSpecs(baseDir, specDir, extension) {
|
|
7
8
|
const fullDir = join(baseDir, specDir);
|
|
8
9
|
if (!existsSync(fullDir)) {
|
|
@@ -125,7 +126,8 @@ function groupBindings(fileBindings) {
|
|
|
125
126
|
const key = binding.feature || binding.family;
|
|
126
127
|
const existing = groups.get(key);
|
|
127
128
|
if (existing) {
|
|
128
|
-
if (!existing.
|
|
129
|
+
if (!existing._seen.has(fb.file)) {
|
|
130
|
+
existing._seen.add(fb.file);
|
|
129
131
|
existing.files.push(fb.file);
|
|
130
132
|
}
|
|
131
133
|
}
|
|
@@ -134,23 +136,13 @@ function groupBindings(fileBindings) {
|
|
|
134
136
|
familyId: binding.family,
|
|
135
137
|
featureId: binding.feature,
|
|
136
138
|
files: [fb.file],
|
|
139
|
+
_seen: new Set([fb.file]),
|
|
137
140
|
});
|
|
138
141
|
}
|
|
139
142
|
}
|
|
140
143
|
}
|
|
141
144
|
return groups;
|
|
142
145
|
}
|
|
143
|
-
/** Filter out test files that should not be treated as application changes. */
|
|
144
|
-
function isTestFile(file) {
|
|
145
|
-
const normalized = file.replace(/\\/g, '/');
|
|
146
|
-
return /\.(spec|test)\.(ts|tsx|js|jsx)$/.test(normalized) ||
|
|
147
|
-
/\.snap$/.test(normalized) ||
|
|
148
|
-
/_test\.go$/.test(normalized) ||
|
|
149
|
-
normalized.includes('__tests__/') ||
|
|
150
|
-
normalized.includes('__snapshots__/') ||
|
|
151
|
-
normalized.includes('/tests/') ||
|
|
152
|
-
normalized.includes('/test/');
|
|
153
|
-
}
|
|
154
146
|
/** Classify filtered test files by type for downstream decision-making. */
|
|
155
147
|
function classifyPrTestFiles(allFiles, sourceFiles) {
|
|
156
148
|
const sourceSet = new Set(sourceFiles);
|
|
@@ -180,17 +172,11 @@ export function analyzeImpact(changedFiles, options) {
|
|
|
180
172
|
const allOriginalFiles = [...new Set([...changedFiles, ...preFilteredTests])];
|
|
181
173
|
changedFiles = changedFiles.filter((f) => !isTestFile(f));
|
|
182
174
|
const prIncludedTestFiles = classifyPrTestFiles(allOriginalFiles, changedFiles);
|
|
183
|
-
// Load manifest
|
|
184
|
-
|
|
175
|
+
// Load manifest, fall back to heuristic families if not found
|
|
176
|
+
let manifest = loadRouteFamilyManifest(testsRoot, routeFamilies);
|
|
185
177
|
if (!manifest) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
expandedFiles: options.expandedFiles || [],
|
|
189
|
-
impactedFeatures: [],
|
|
190
|
-
unboundFiles: [...changedFiles],
|
|
191
|
-
warnings: ['Route family manifest not found. All files are unbound.'],
|
|
192
|
-
prIncludedTestFiles,
|
|
193
|
-
};
|
|
178
|
+
manifest = buildHeuristicFamilies(changedFiles, testsRoot);
|
|
179
|
+
warnings.push('Route family manifest not found. Using directory-based heuristics (lower accuracy).', 'Tip: Run `e2e-ai-agents train` to generate a proper manifest.');
|
|
194
180
|
}
|
|
195
181
|
// Combine original + expanded files
|
|
196
182
|
const allFiles = [...new Set([...changedFiles, ...(options.expandedFiles || [])])];
|
package/dist/esm/index.js
CHANGED
|
@@ -44,8 +44,29 @@ export { StrategistAgent } from './agents/strategist.js';
|
|
|
44
44
|
export { TestDesignerAgent } from './agents/test-designer.js';
|
|
45
45
|
export { CrossImpactAgent } from './agents/cross-impact.js';
|
|
46
46
|
export { RegressionAdvisorAgent } from './agents/regression-advisor.js';
|
|
47
|
+
// Base provider (for extending with custom providers)
|
|
48
|
+
export { BaseProvider, BudgetExceededError } from './base_provider.js';
|
|
49
|
+
// Budget tracking
|
|
50
|
+
export { BudgetLedger } from './budget_ledger.js';
|
|
51
|
+
// Model routing
|
|
52
|
+
export { ModelRouter } from './model_router.js';
|
|
53
|
+
// Resilience
|
|
54
|
+
export { withRetry } from './resilience/retry.js';
|
|
55
|
+
export { CircuitBreaker } from './resilience/circuit_breaker.js';
|
|
56
|
+
// Metrics
|
|
57
|
+
export { PrometheusMetrics } from './metrics/prometheus.js';
|
|
58
|
+
// Secret scanning
|
|
59
|
+
export { sanitizeSecrets, containsSecrets, sanitizeObject } from './sanitize.js';
|
|
60
|
+
// CLI errors
|
|
61
|
+
export { CliError, classifyError, EXIT_CODES } from './cli/errors.js';
|
|
47
62
|
// Training (route-families bootstrap and maintenance)
|
|
48
63
|
export { scanProject } from './training/scanner.js';
|
|
49
64
|
export { mergeFamilies, detectStaleFamilies } from './training/merger.js';
|
|
50
65
|
export { enrichFamilies } from './training/enricher.js';
|
|
51
66
|
export { getCommitFiles, validateCommit, buildValidationReport, formatValidationReport } from './training/validator.js';
|
|
67
|
+
export { loadKnowledgeGraph, classifyProjectType, transformKGToFamilies, loadDiffOverlay } from './knowledge/kg_bridge.js';
|
|
68
|
+
export { scanFromKnowledgeGraph } from './training/kg_scanner.js';
|
|
69
|
+
export { resolveGenerationProfile, isMattermostProfile } from './prompts/generation_profile.js';
|
|
70
|
+
export { detectFramework, detectTestMode } from './adapters/framework_adapter.js';
|
|
71
|
+
// Route families (additional)
|
|
72
|
+
export { serializeManifest } from './knowledge/route_families.js';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
/**
|
|
4
|
+
* Shared cluster ID derivation for knowledge graph processing.
|
|
5
|
+
* Used by both kg_bridge.ts and kg_scanner.ts.
|
|
6
|
+
*/
|
|
7
|
+
/** Default directories to skip when deriving cluster IDs from file paths. */
|
|
8
|
+
const DEFAULT_SKIP_DIRS = new Set([
|
|
9
|
+
'src', 'app', 'lib', 'packages', 'server', 'api', 'pages',
|
|
10
|
+
'components', 'features', 'modules',
|
|
11
|
+
]);
|
|
12
|
+
/** Extended skip set that also excludes test directories. */
|
|
13
|
+
const SKIP_DIRS_WITH_TESTS = new Set([
|
|
14
|
+
...DEFAULT_SKIP_DIRS,
|
|
15
|
+
'test', 'tests', 'e2e', 'spec', 'specs',
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* Normalize a name to a snake_case cluster ID.
|
|
19
|
+
* Handles camelCase conversion, then strips non-alphanumeric characters.
|
|
20
|
+
*/
|
|
21
|
+
export function normalizeToClusterId(name) {
|
|
22
|
+
return name
|
|
23
|
+
.replace(/[A-Z]/g, (c, idx) => (idx > 0 ? `_${c.toLowerCase()}` : c.toLowerCase()))
|
|
24
|
+
.replace(/[^a-z0-9_]/g, '_')
|
|
25
|
+
.replace(/_+/g, '_')
|
|
26
|
+
.replace(/^_|_$/g, '');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Derive a cluster ID from a node that has an optional filePath and name.
|
|
30
|
+
* Prefers file path grouping for consistency.
|
|
31
|
+
*/
|
|
32
|
+
export function deriveClusterId(node, skipDirs) {
|
|
33
|
+
if (node.filePath) {
|
|
34
|
+
return deriveClusterIdFromPath(node.filePath, skipDirs);
|
|
35
|
+
}
|
|
36
|
+
const name = normalizeToClusterId(node.name);
|
|
37
|
+
return name && name.length > 1 ? name : null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Derive a cluster ID from a file path by finding the first meaningful
|
|
41
|
+
* directory segment after skipping common structural prefixes.
|
|
42
|
+
*/
|
|
43
|
+
export function deriveClusterIdFromPath(filePath, skipDirs = DEFAULT_SKIP_DIRS) {
|
|
44
|
+
const parts = filePath.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
45
|
+
for (const part of parts) {
|
|
46
|
+
if (skipDirs.has(part))
|
|
47
|
+
continue;
|
|
48
|
+
if (part.includes('.'))
|
|
49
|
+
continue; // skip files
|
|
50
|
+
const normalized = part.toLowerCase()
|
|
51
|
+
.replace(/[^a-z0-9_]/g, '_')
|
|
52
|
+
.replace(/_+/g, '_')
|
|
53
|
+
.replace(/^_|_$/g, '');
|
|
54
|
+
if (normalized && normalized.length > 1) {
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
export { DEFAULT_SKIP_DIRS, SKIP_DIRS_WITH_TESTS };
|