@yasserkhanorg/e2e-agents 1.8.5 → 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/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/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
|
@@ -9,6 +9,8 @@ exports.CrewOrchestrator = void 0;
|
|
|
9
9
|
const git_js_1 = require("../agent/git.js");
|
|
10
10
|
const stage0_preprocess_js_1 = require("../pipeline/stage0_preprocess.js");
|
|
11
11
|
const logger_js_1 = require("../logger.js");
|
|
12
|
+
const base_provider_js_1 = require("../base_provider.js");
|
|
13
|
+
const budget_ledger_js_1 = require("../budget_ledger.js");
|
|
12
14
|
const context_js_1 = require("./context.js");
|
|
13
15
|
const workflows_js_1 = require("./workflows.js");
|
|
14
16
|
class CrewOrchestrator {
|
|
@@ -18,10 +20,61 @@ class CrewOrchestrator {
|
|
|
18
20
|
registerAgent(agent) {
|
|
19
21
|
this.agents.set(agent.role, agent);
|
|
20
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Load and register plugins from file paths.
|
|
25
|
+
* Each module must default-export an object satisfying AgentPlugin.
|
|
26
|
+
*/
|
|
27
|
+
async loadPlugins(pluginPaths) {
|
|
28
|
+
const loaded = [];
|
|
29
|
+
for (const pluginPath of pluginPaths) {
|
|
30
|
+
try {
|
|
31
|
+
// Security: Only allow relative paths (starting with . or ..) to prevent loading arbitrary modules.
|
|
32
|
+
// Absolute paths, URLs, and node_modules references are rejected.
|
|
33
|
+
if (!pluginPath.startsWith('.')) {
|
|
34
|
+
logger_js_1.logger.warn(`Plugin path must be relative (start with ./): ${pluginPath} — skipped`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const resolved = new URL(pluginPath, `file://${process.cwd()}/`).href;
|
|
38
|
+
// Security: reject paths that resolve outside the workspace (e.g., ../../etc/evil.js)
|
|
39
|
+
const cwd = `file://${process.cwd()}/`;
|
|
40
|
+
if (!resolved.startsWith(cwd)) {
|
|
41
|
+
logger_js_1.logger.warn(`Plugin path '${pluginPath}' resolves outside workspace — skipped`);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const mod = await import(resolved);
|
|
45
|
+
const plugin = mod.default || mod;
|
|
46
|
+
if (!plugin.role || typeof plugin.execute !== 'function') {
|
|
47
|
+
logger_js_1.logger.warn(`Plugin at ${pluginPath} missing required role/execute — skipped`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
// Warn if plugin overrides a built-in agent
|
|
51
|
+
if (this.agents.has(plugin.role)) {
|
|
52
|
+
logger_js_1.logger.warn(`Plugin '${plugin.role}' overrides built-in agent — ensure this is intentional`);
|
|
53
|
+
}
|
|
54
|
+
this.agents.set(plugin.role, plugin);
|
|
55
|
+
loaded.push(plugin.role);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
59
|
+
logger_js_1.logger.warn(`Failed to load plugin ${pluginPath}: ${msg}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return loaded;
|
|
63
|
+
}
|
|
21
64
|
async run(config) {
|
|
22
65
|
const workflow = workflows_js_1.WORKFLOWS[config.workflow || 'full-qa'];
|
|
23
66
|
const timings = {};
|
|
24
67
|
const warnings = [];
|
|
68
|
+
// Load plugins if configured, then inject them into workflow phases
|
|
69
|
+
const pluginRoles = [];
|
|
70
|
+
if (config.plugins && config.plugins.length > 0) {
|
|
71
|
+
const loaded = await this.loadPlugins(config.plugins);
|
|
72
|
+
pluginRoles.push(...loaded);
|
|
73
|
+
if (loaded.length > 0) {
|
|
74
|
+
logger_js_1.logger.info(`Loaded ${loaded.length} plugins: ${loaded.join(', ')}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const effectivePhases = this.injectPluginsIntoPhases(workflow.phases, pluginRoles);
|
|
25
78
|
// Step 1: Get changed files
|
|
26
79
|
const gitResult = (0, git_js_1.getChangedFiles)(config.appPath, config.gitSince, {
|
|
27
80
|
includeUncommitted: config.gitIncludeUncommitted,
|
|
@@ -35,6 +88,8 @@ class CrewOrchestrator {
|
|
|
35
88
|
if (changedFiles.length === 0) {
|
|
36
89
|
warnings.push('No changed application files detected.');
|
|
37
90
|
}
|
|
91
|
+
// Create shared budget ledger for aggregate cost tracking across all agents
|
|
92
|
+
const budgetLedger = config.budgetUSD ? new budget_ledger_js_1.BudgetLedger(config.budgetUSD) : undefined;
|
|
38
93
|
// Initialize context (will be populated during preprocess phase)
|
|
39
94
|
const ctx = {
|
|
40
95
|
changedFiles,
|
|
@@ -49,6 +104,8 @@ class CrewOrchestrator {
|
|
|
49
104
|
testsRoot: config.testsRoot,
|
|
50
105
|
gitSince: config.gitSince,
|
|
51
106
|
providerOverride: config.providerOverride,
|
|
107
|
+
budgetUSD: config.budgetUSD,
|
|
108
|
+
budgetLedger,
|
|
52
109
|
impactedFlows: [],
|
|
53
110
|
strategyEntries: [],
|
|
54
111
|
testDesigns: [],
|
|
@@ -57,14 +114,21 @@ class CrewOrchestrator {
|
|
|
57
114
|
findings: [],
|
|
58
115
|
generatedSpecs: [],
|
|
59
116
|
usage: (0, context_js_1.createEmptyUsageStats)(),
|
|
117
|
+
agentUsage: [],
|
|
60
118
|
messages: [],
|
|
61
119
|
warnings,
|
|
62
120
|
};
|
|
63
121
|
// Execute each phase
|
|
64
|
-
for (const phase of
|
|
122
|
+
for (const phase of effectivePhases) {
|
|
65
123
|
const timer = logger_js_1.logger.timer(`crew:${phase.name}`);
|
|
66
124
|
if (phase.handler === 'built-in') {
|
|
67
125
|
await this.runBuiltInPhase(phase.name, ctx, config);
|
|
126
|
+
// Dry-run: after preprocess, return summary without running agents
|
|
127
|
+
if (config.dryRun && phase.name === 'preprocess') {
|
|
128
|
+
timings[phase.name] = timer.end();
|
|
129
|
+
ctx.warnings.push('Dry run — no LLM calls were made.');
|
|
130
|
+
return { context: ctx, warnings, timings, dryRun: true };
|
|
131
|
+
}
|
|
68
132
|
}
|
|
69
133
|
else if (phase.parallel && phase.parallel.length > 0) {
|
|
70
134
|
await this.runParallel(phase.parallel, phase.name, ctx);
|
|
@@ -76,9 +140,10 @@ class CrewOrchestrator {
|
|
|
76
140
|
warnings.push(`Phase '${phase.name}' has no handler, parallel, or sequential agents — skipped.`);
|
|
77
141
|
}
|
|
78
142
|
timings[phase.name] = timer.end();
|
|
79
|
-
// Budget check
|
|
80
|
-
|
|
81
|
-
|
|
143
|
+
// Budget check — prefer ledger (aggregate across all providers) over ctx.usage
|
|
144
|
+
const currentCost = budgetLedger ? budgetLedger.totalCost : ctx.usage.totalCost;
|
|
145
|
+
if (config.budgetUSD && currentCost >= config.budgetUSD) {
|
|
146
|
+
warnings.push(`Budget limit reached ($${currentCost.toFixed(4)} >= $${config.budgetUSD}). Stopping workflow.`);
|
|
82
147
|
break;
|
|
83
148
|
}
|
|
84
149
|
}
|
|
@@ -95,10 +160,19 @@ class CrewOrchestrator {
|
|
|
95
160
|
};
|
|
96
161
|
}
|
|
97
162
|
const task = { role, action, input: null };
|
|
163
|
+
const startMs = Date.now();
|
|
98
164
|
try {
|
|
99
165
|
const result = await agent.execute(task, ctx);
|
|
166
|
+
const durationMs = Date.now() - startMs;
|
|
100
167
|
if (result.usage) {
|
|
101
168
|
(0, context_js_1.mergeUsageStats)(ctx.usage, result.usage);
|
|
169
|
+
ctx.agentUsage.push({
|
|
170
|
+
agent: role,
|
|
171
|
+
inputTokens: result.usage.totalInputTokens,
|
|
172
|
+
outputTokens: result.usage.totalOutputTokens,
|
|
173
|
+
cost: result.usage.totalCost,
|
|
174
|
+
durationMs,
|
|
175
|
+
});
|
|
102
176
|
}
|
|
103
177
|
if (result.warnings && result.warnings.length > 0) {
|
|
104
178
|
ctx.warnings.push(...result.warnings);
|
|
@@ -106,6 +180,10 @@ class CrewOrchestrator {
|
|
|
106
180
|
return result;
|
|
107
181
|
}
|
|
108
182
|
catch (error) {
|
|
183
|
+
if (error instanceof base_provider_js_1.BudgetExceededError) {
|
|
184
|
+
ctx.warnings.push(`Budget exceeded ($${error.currentCost.toFixed(4)} >= $${error.budgetUSD}). Agent '${role}' skipped.`);
|
|
185
|
+
return { role, status: 'failed', output: null, warnings: [error.message] };
|
|
186
|
+
}
|
|
109
187
|
const message = error instanceof Error ? error.message : String(error);
|
|
110
188
|
ctx.warnings.push(`Agent '${role}' failed: ${message}`);
|
|
111
189
|
return { role, status: 'failed', output: null, warnings: [message] };
|
|
@@ -161,6 +239,86 @@ class CrewOrchestrator {
|
|
|
161
239
|
}
|
|
162
240
|
this.checkPhaseResults(phaseName, results, ctx);
|
|
163
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Inject loaded plugins into workflow phases based on their `phase` and `runAfter` fields.
|
|
244
|
+
* Plugins with `runAfter` dependencies are appended to the sequential list of the matching phase;
|
|
245
|
+
* plugins without `runAfter` are appended to the parallel list.
|
|
246
|
+
* Returns a new array of phases (does not mutate the original workflow definition).
|
|
247
|
+
*/
|
|
248
|
+
injectPluginsIntoPhases(phases, pluginRoles) {
|
|
249
|
+
if (pluginRoles.length === 0)
|
|
250
|
+
return phases;
|
|
251
|
+
// Build mutable copies keyed by phase name
|
|
252
|
+
const phaseMap = new Map();
|
|
253
|
+
const ordered = [];
|
|
254
|
+
for (const p of phases) {
|
|
255
|
+
ordered.push(p.name);
|
|
256
|
+
if (p.handler === 'built-in') {
|
|
257
|
+
phaseMap.set(p.name, { handler: 'built-in' });
|
|
258
|
+
}
|
|
259
|
+
else if (p.parallel) {
|
|
260
|
+
phaseMap.set(p.name, { parallel: [...p.parallel] });
|
|
261
|
+
}
|
|
262
|
+
else if (p.sequential) {
|
|
263
|
+
phaseMap.set(p.name, { sequential: [...p.sequential] });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
for (const role of pluginRoles) {
|
|
267
|
+
const agent = this.agents.get(role);
|
|
268
|
+
if (!agent)
|
|
269
|
+
continue;
|
|
270
|
+
const plugin = agent;
|
|
271
|
+
if (!plugin.phase)
|
|
272
|
+
continue;
|
|
273
|
+
const target = phaseMap.get(plugin.phase);
|
|
274
|
+
if (!target) {
|
|
275
|
+
logger_js_1.logger.warn(`Plugin '${role}' targets phase '${plugin.phase}' which does not exist in workflow — skipped`);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (target.handler === 'built-in') {
|
|
279
|
+
logger_js_1.logger.warn(`Plugin '${role}' targets built-in phase '${plugin.phase}' — not supported, skipped`);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const pluginRole = role;
|
|
283
|
+
if (plugin.runAfter && plugin.runAfter.length > 0) {
|
|
284
|
+
// Validate that runAfter dependencies are either in this phase or a prior phase
|
|
285
|
+
const phaseRoles = target.parallel || target.sequential || [];
|
|
286
|
+
const missingDeps = plugin.runAfter.filter((dep) => !phaseRoles.includes(dep) && !this.agents.has(dep));
|
|
287
|
+
if (missingDeps.length > 0) {
|
|
288
|
+
logger_js_1.logger.warn(`Plugin '${role}' has unresolved runAfter deps [${missingDeps.join(', ')}] — injecting anyway`);
|
|
289
|
+
}
|
|
290
|
+
// Plugin has dependencies — must run sequentially
|
|
291
|
+
if (target.sequential) {
|
|
292
|
+
target.sequential.push(pluginRole);
|
|
293
|
+
}
|
|
294
|
+
else if (target.parallel) {
|
|
295
|
+
// Convert to sequential to respect dependency ordering
|
|
296
|
+
target.sequential = [...target.parallel, pluginRole];
|
|
297
|
+
delete target.parallel;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
if (target.parallel) {
|
|
302
|
+
target.parallel.push(pluginRole);
|
|
303
|
+
}
|
|
304
|
+
else if (target.sequential) {
|
|
305
|
+
target.sequential.push(pluginRole);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
logger_js_1.logger.info(`Plugin '${role}' injected into phase '${plugin.phase}'`);
|
|
309
|
+
}
|
|
310
|
+
// Rebuild WorkflowPhase array
|
|
311
|
+
return ordered.map((name) => {
|
|
312
|
+
const entry = phaseMap.get(name);
|
|
313
|
+
if (entry.handler === 'built-in')
|
|
314
|
+
return { name, handler: 'built-in' };
|
|
315
|
+
if (entry.parallel)
|
|
316
|
+
return { name, parallel: entry.parallel };
|
|
317
|
+
if (entry.sequential)
|
|
318
|
+
return { name, sequential: entry.sequential };
|
|
319
|
+
throw new Error(`Phase '${name}' has no handler, parallel, or sequential agents after plugin injection`);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
164
322
|
checkPhaseResults(phaseName, results, ctx) {
|
|
165
323
|
const allFailed = results.length > 0 && results.every((r) => r.status === 'failed');
|
|
166
324
|
if (allFailed) {
|
package/dist/crew/protocol.d.ts
CHANGED
|
@@ -30,4 +30,17 @@ export interface Agent {
|
|
|
30
30
|
execute(task: AgentTask, ctx: CrewContext): Promise<AgentResult>;
|
|
31
31
|
onMessage?(msg: AgentMessage): Promise<void>;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* AgentPlugin — interface for external agent plugins loaded from config.
|
|
35
|
+
*
|
|
36
|
+
* Plugins register into crew workflow phases alongside built-in agents.
|
|
37
|
+
* The CrewContext interface is a public API contract once plugins exist:
|
|
38
|
+
* field additions are non-breaking, field removals or type changes are breaking.
|
|
39
|
+
*/
|
|
40
|
+
export interface AgentPlugin extends Agent {
|
|
41
|
+
/** Which workflow phase to run in (e.g., 'strategize', 'understand') */
|
|
42
|
+
phase: string;
|
|
43
|
+
/** Run after these agents complete (dependency ordering) */
|
|
44
|
+
runAfter?: AgentRole[];
|
|
45
|
+
}
|
|
33
46
|
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../src/crew/protocol.ts"],"names":[],"mappings":"AAGA;;GAEG;AAEH,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,YAAY,CAAC;AAC1C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,cAAc,CAAC;AAE9C,MAAM,WAAW,YAAY;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,CAAC;IAChB,EAAE,EAAE,SAAS,GAAG,WAAW,CAAC;IAC5B,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,YAAY,GAAG,SAAS,CAAC;IACnD,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,SAAS;IACtB,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;IACzC,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,QAAQ,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,KAAK;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACjE,SAAS,CAAC,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD"}
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../src/crew/protocol.ts"],"names":[],"mappings":"AAGA;;GAEG;AAEH,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,YAAY,CAAC;AAC1C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,cAAc,CAAC;AAE9C,MAAM,WAAW,YAAY;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,CAAC;IAChB,EAAE,EAAE,SAAS,GAAG,WAAW,CAAC;IAC5B,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,YAAY,GAAG,SAAS,CAAC;IACnD,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,SAAS;IACtB,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;IACzC,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,QAAQ,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,KAAK;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACjE,SAAS,CAAC,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,WAAY,SAAQ,KAAK;IACtC,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,SAAS,EAAE,CAAC;CAC1B"}
|
package/dist/crew/provider.d.ts
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
1
|
import type { LLMProvider } from '../provider_interface.js';
|
|
2
|
-
|
|
2
|
+
import type { BudgetLedger } from '../budget_ledger.js';
|
|
3
|
+
import type { AgentRole } from './types.js';
|
|
4
|
+
export interface CrewProviderOptions {
|
|
5
|
+
providerOverride?: string;
|
|
6
|
+
budgetUSD?: number;
|
|
7
|
+
agentRole?: AgentRole;
|
|
8
|
+
modelRoutingProviderType?: string;
|
|
9
|
+
modelRoutingOverrides?: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
export declare function getCrewProvider(providerOverride?: string, budgetUSD?: number, opts?: {
|
|
12
|
+
agentRole?: AgentRole;
|
|
13
|
+
modelRoutingProviderType?: string;
|
|
14
|
+
modelRoutingOverrides?: Record<string, string>;
|
|
15
|
+
budgetLedger?: BudgetLedger;
|
|
16
|
+
}): Promise<LLMProvider>;
|
|
3
17
|
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/crew/provider.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/crew/provider.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;AAG1D,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,qBAAqB,CAAC;AACtD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,YAAY,CAAC;AAE1C,MAAM,WAAW,mBAAmB;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,qBAAqB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClD;AAED,wBAAsB,eAAe,CAAC,gBAAgB,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;IACxF,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,qBAAqB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/C,YAAY,CAAC,EAAE,YAAY,CAAC;CAC/B,GAAG,OAAO,CAAC,WAAW,CAAC,CA0BvB"}
|
package/dist/crew/provider.js
CHANGED
|
@@ -8,9 +8,29 @@ exports.getCrewProvider = getCrewProvider;
|
|
|
8
8
|
* instantiation and prevents usage stats fragmentation.
|
|
9
9
|
*/
|
|
10
10
|
const provider_factory_js_1 = require("../provider_factory.js");
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const base_provider_js_1 = require("../base_provider.js");
|
|
12
|
+
const model_router_js_1 = require("../model_router.js");
|
|
13
|
+
async function getCrewProvider(providerOverride, budgetUSD, opts) {
|
|
14
|
+
let effectiveOverride = providerOverride;
|
|
15
|
+
// Apply model routing if configured and agent role is provided
|
|
16
|
+
if (opts?.agentRole && opts?.modelRoutingProviderType) {
|
|
17
|
+
const router = new model_router_js_1.ModelRouter(opts.modelRoutingProviderType, opts.modelRoutingOverrides);
|
|
18
|
+
const model = router.getModel(opts.agentRole);
|
|
19
|
+
if (model) {
|
|
20
|
+
// Override uses provider:model format (e.g., "anthropic:claude-haiku-4-5-20251001")
|
|
21
|
+
effectiveOverride = `${opts.modelRoutingProviderType}:${model}`;
|
|
22
|
+
}
|
|
14
23
|
}
|
|
15
|
-
|
|
24
|
+
const provider = effectiveOverride
|
|
25
|
+
? await provider_factory_js_1.LLMProviderFactory.createFromString(effectiveOverride)
|
|
26
|
+
: await provider_factory_js_1.LLMProviderFactory.createFromEnv();
|
|
27
|
+
if (provider instanceof base_provider_js_1.BaseProvider) {
|
|
28
|
+
if (opts?.budgetLedger) {
|
|
29
|
+
provider.setBudgetLedger(opts.budgetLedger);
|
|
30
|
+
}
|
|
31
|
+
else if (budgetUSD !== undefined) {
|
|
32
|
+
provider.setBudget(budgetUSD);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return provider;
|
|
16
36
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"custom_provider.d.ts","sourceRoot":"","sources":["../src/custom_provider.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACR,YAAY,EACZ,eAAe,EACf,UAAU,EACV,WAAW,EACX,oBAAoB,EACvB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AA6DhD,qBAAa,cAAe,SAAQ,YAAY;IAC5C,IAAI,SAAY;IAChB,OAAO,CAAC,MAAM,CAAe;IAE7B,YAAY,EAAE,oBAAoB,CAAC;gBAEvB,MAAM,EAAE,YAAY;IAyB1B,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"custom_provider.d.ts","sourceRoot":"","sources":["../src/custom_provider.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACR,YAAY,EACZ,eAAe,EACf,UAAU,EACV,WAAW,EACX,oBAAoB,EACvB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AA6DhD,qBAAa,cAAe,SAAQ,YAAY;IAC5C,IAAI,SAAY;IAChB,OAAO,CAAC,MAAM,CAAe;IAE7B,YAAY,EAAE,oBAAoB,CAAC;gBAEvB,MAAM,EAAE,YAAY;IAyB1B,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,WAAW,CAAC;IAmC7E,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,WAAW,CAAC;YAyC3F,eAAe;YAiBf,aAAa;YA8Eb,gBAAgB;YA6EhB,aAAa;IAqBpB,UAAU,IAAI,cAAc,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC;IAKpD,WAAW,IAAI,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAC,CAAC;CAOpE"}
|
package/dist/custom_provider.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diff_loader.d.ts","sourceRoot":"","sources":["../../src/engine/diff_loader.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"diff_loader.d.ts","sourceRoot":"","sources":["../../src/engine/diff_loader.ts"],"names":[],"mappings":"AASA;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CA0CrG;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAUvE"}
|
|
@@ -4,21 +4,10 @@
|
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.loadDiffs = loadDiffs;
|
|
6
6
|
exports.formatDiffsForPrompt = formatDiffsForPrompt;
|
|
7
|
-
const
|
|
7
|
+
const git_js_1 = require("../agent/git.js");
|
|
8
8
|
const MAX_DIFF_CHARS = 8000;
|
|
9
9
|
const MAX_TOTAL_CHARS = 60000;
|
|
10
10
|
const TRUNCATION_NOTICE = '\n... (diff truncated)';
|
|
11
|
-
function runGitRaw(args, cwd) {
|
|
12
|
-
const result = (0, child_process_1.spawnSync)('git', args, {
|
|
13
|
-
cwd,
|
|
14
|
-
encoding: 'utf-8',
|
|
15
|
-
timeout: 30000,
|
|
16
|
-
});
|
|
17
|
-
if (result.error || result.status !== 0) {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
return result.stdout;
|
|
21
|
-
}
|
|
22
11
|
/**
|
|
23
12
|
* Loads git diffs for the given changed files relative to the given since ref.
|
|
24
13
|
* Uses `git merge-base` to find the accurate base ref first.
|
|
@@ -31,7 +20,7 @@ function loadDiffs(appRoot, since, changedFiles) {
|
|
|
31
20
|
}
|
|
32
21
|
// Try to get accurate merge base
|
|
33
22
|
let baseRef = since;
|
|
34
|
-
const mergeBaseOutput = runGitRaw(['merge-base', since, 'HEAD'], appRoot);
|
|
23
|
+
const mergeBaseOutput = (0, git_js_1.runGitRaw)(['merge-base', since, 'HEAD'], appRoot);
|
|
35
24
|
if (mergeBaseOutput) {
|
|
36
25
|
const candidate = mergeBaseOutput
|
|
37
26
|
.split('\n')
|
|
@@ -46,7 +35,7 @@ function loadDiffs(appRoot, since, changedFiles) {
|
|
|
46
35
|
if (totalChars >= MAX_TOTAL_CHARS) {
|
|
47
36
|
break;
|
|
48
37
|
}
|
|
49
|
-
const diffOutput = runGitRaw(['diff', `${baseRef}..HEAD`, '--', file], appRoot);
|
|
38
|
+
const diffOutput = (0, git_js_1.runGitRaw)(['diff', `${baseRef}..HEAD`, '--', file], appRoot);
|
|
50
39
|
if (diffOutput === null) {
|
|
51
40
|
continue;
|
|
52
41
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"impact_engine.d.ts","sourceRoot":"","sources":["../../src/engine/impact_engine.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGR,eAAe,EAClB,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"impact_engine.d.ts","sourceRoot":"","sources":["../../src/engine/impact_engine.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGR,eAAe,EAClB,MAAM,gCAAgC,CAAC;AAUxC,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,oBAAoB,CAAC;AAG5D,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,CAAC;AAEjE,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,eAAe,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,qBAAqB,EAAE,iBAAiB,EAAE,CAAC;IAC3C,kBAAkB,EAAE,iBAAiB,EAAE,CAAC;IACxC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,cAAc,EAAE,cAAc,CAAC;CAClC;AAED,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,SAAS,GAAG,MAAM,GAAG,UAAU,CAAC;AAE5E,MAAM,WAAW,UAAU;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IACzB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gBAAgB,EAAE,eAAe,EAAE,CAAC;IACpC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,oFAAoF;IACpF,mBAAmB,EAAE,UAAU,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,mBAAmB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,yHAAyH;IACzH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC;AA+CD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,GAAG,SAAS,GAAG,MAAM,EAAE,CAgBhG;AAgGD,wBAAgB,aAAa,CACzB,YAAY,EAAE,MAAM,EAAE,EACtB,OAAO,EAAE,mBAAmB,GAC7B,YAAY,CAoFd;AAYD,MAAM,WAAW,SAAS;IACtB,oDAAoD;IACpD,IAAI,EAAE,eAAe,EAAE,CAAC;IACxB,0IAA0I;IAC1I,cAAc,EAAE,eAAe,EAAE,CAAC;CACrC;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,YAAY,GAAG,SAAS,CA2BrE;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,YAAY,GAAG,eAAe,EAAE,CAE/D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,YAAY,GAAG,eAAe,EAAE,CAItE"}
|
|
@@ -10,6 +10,7 @@ exports.getPartialGaps = getPartialGaps;
|
|
|
10
10
|
const fs_1 = require("fs");
|
|
11
11
|
const path_1 = require("path");
|
|
12
12
|
const route_families_js_1 = require("../knowledge/route_families.js");
|
|
13
|
+
const git_js_1 = require("../agent/git.js");
|
|
13
14
|
function scanDirForSpecs(baseDir, specDir, extension) {
|
|
14
15
|
const fullDir = (0, path_1.join)(baseDir, specDir);
|
|
15
16
|
if (!(0, fs_1.existsSync)(fullDir)) {
|
|
@@ -132,7 +133,8 @@ function groupBindings(fileBindings) {
|
|
|
132
133
|
const key = binding.feature || binding.family;
|
|
133
134
|
const existing = groups.get(key);
|
|
134
135
|
if (existing) {
|
|
135
|
-
if (!existing.
|
|
136
|
+
if (!existing._seen.has(fb.file)) {
|
|
137
|
+
existing._seen.add(fb.file);
|
|
136
138
|
existing.files.push(fb.file);
|
|
137
139
|
}
|
|
138
140
|
}
|
|
@@ -141,23 +143,13 @@ function groupBindings(fileBindings) {
|
|
|
141
143
|
familyId: binding.family,
|
|
142
144
|
featureId: binding.feature,
|
|
143
145
|
files: [fb.file],
|
|
146
|
+
_seen: new Set([fb.file]),
|
|
144
147
|
});
|
|
145
148
|
}
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
151
|
return groups;
|
|
149
152
|
}
|
|
150
|
-
/** Filter out test files that should not be treated as application changes. */
|
|
151
|
-
function isTestFile(file) {
|
|
152
|
-
const normalized = file.replace(/\\/g, '/');
|
|
153
|
-
return /\.(spec|test)\.(ts|tsx|js|jsx)$/.test(normalized) ||
|
|
154
|
-
/\.snap$/.test(normalized) ||
|
|
155
|
-
/_test\.go$/.test(normalized) ||
|
|
156
|
-
normalized.includes('__tests__/') ||
|
|
157
|
-
normalized.includes('__snapshots__/') ||
|
|
158
|
-
normalized.includes('/tests/') ||
|
|
159
|
-
normalized.includes('/test/');
|
|
160
|
-
}
|
|
161
153
|
/** Classify filtered test files by type for downstream decision-making. */
|
|
162
154
|
function classifyPrTestFiles(allFiles, sourceFiles) {
|
|
163
155
|
const sourceSet = new Set(sourceFiles);
|
|
@@ -185,19 +177,13 @@ function analyzeImpact(changedFiles, options) {
|
|
|
185
177
|
// (b) test files pre-filtered by the caller (filteredTestFiles from git.ts).
|
|
186
178
|
const preFilteredTests = options.filteredTestFiles ?? [];
|
|
187
179
|
const allOriginalFiles = [...new Set([...changedFiles, ...preFilteredTests])];
|
|
188
|
-
changedFiles = changedFiles.filter((f) => !isTestFile(f));
|
|
180
|
+
changedFiles = changedFiles.filter((f) => !(0, git_js_1.isTestFile)(f));
|
|
189
181
|
const prIncludedTestFiles = classifyPrTestFiles(allOriginalFiles, changedFiles);
|
|
190
|
-
// Load manifest
|
|
191
|
-
|
|
182
|
+
// Load manifest, fall back to heuristic families if not found
|
|
183
|
+
let manifest = (0, route_families_js_1.loadRouteFamilyManifest)(testsRoot, routeFamilies);
|
|
192
184
|
if (!manifest) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
expandedFiles: options.expandedFiles || [],
|
|
196
|
-
impactedFeatures: [],
|
|
197
|
-
unboundFiles: [...changedFiles],
|
|
198
|
-
warnings: ['Route family manifest not found. All files are unbound.'],
|
|
199
|
-
prIncludedTestFiles,
|
|
200
|
-
};
|
|
185
|
+
manifest = (0, route_families_js_1.buildHeuristicFamilies)(changedFiles, testsRoot);
|
|
186
|
+
warnings.push('Route family manifest not found. Using directory-based heuristics (lower accuracy).', 'Tip: Run `e2e-ai-agents train` to generate a proper manifest.');
|
|
201
187
|
}
|
|
202
188
|
// Combine original + expanded files
|
|
203
189
|
const allFiles = [...new Set([...changedFiles, ...(options.expandedFiles || [])])];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
/**
|
|
4
|
+
* Cypress Adapter — FrameworkAdapter implementation for Cypress.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
export class CypressAdapter {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.name = 'cypress';
|
|
11
|
+
this.specGlob = '**/*.cy.{ts,js,tsx,jsx}';
|
|
12
|
+
this.extractTestPattern = /\b(?:it|describe|context)\s*\(/g;
|
|
13
|
+
this.configFileNames = ['cypress.config.ts', 'cypress.config.js'];
|
|
14
|
+
}
|
|
15
|
+
detect(projectRoot) {
|
|
16
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
17
|
+
if (!fs.existsSync(pkgPath)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const raw = fs.readFileSync(pkgPath, 'utf-8');
|
|
22
|
+
const pkg = JSON.parse(raw);
|
|
23
|
+
const allDeps = {
|
|
24
|
+
...pkg.dependencies,
|
|
25
|
+
...pkg.devDependencies,
|
|
26
|
+
};
|
|
27
|
+
return 'cypress' in allDeps;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
buildRunCommand(specPath, options) {
|
|
34
|
+
const args = ['cypress', 'run', '--spec', specPath];
|
|
35
|
+
if (options?.headed) {
|
|
36
|
+
args.push('--headed');
|
|
37
|
+
}
|
|
38
|
+
if (options?.browser) {
|
|
39
|
+
args.push('--browser', options.browser);
|
|
40
|
+
}
|
|
41
|
+
if (options?.project) {
|
|
42
|
+
args.push('--project', options.project);
|
|
43
|
+
}
|
|
44
|
+
if (options?.timeout != null) {
|
|
45
|
+
args.push('--config', `defaultCommandTimeout=${options.timeout}`);
|
|
46
|
+
}
|
|
47
|
+
return { executable: 'npx', args };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
/**
|
|
4
|
+
* Framework Adapter Interface — abstracts test-framework-specific logic
|
|
5
|
+
* behind a uniform contract so the rest of the pipeline is framework-agnostic.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import { PlaywrightAdapter } from './playwright.js';
|
|
10
|
+
import { CypressAdapter } from './cypress.js';
|
|
11
|
+
import { SupertestAdapter } from './supertest.js';
|
|
12
|
+
import { PytestAdapter } from './pytest.js';
|
|
13
|
+
/** Shared framework name lists used for test-mode detection across the codebase. */
|
|
14
|
+
export const UI_FRAMEWORKS = ['playwright', '@playwright/test', 'cypress', 'selenium'];
|
|
15
|
+
export const API_FRAMEWORKS = ['supertest', 'pytest', 'requests', 'vitest', 'jest'];
|
|
16
|
+
/**
|
|
17
|
+
* Auto-detect which test framework a project uses by inspecting its
|
|
18
|
+
* package.json dependencies. Falls back to Playwright when detection
|
|
19
|
+
* is inconclusive.
|
|
20
|
+
*/
|
|
21
|
+
export function detectFramework(projectRoot) {
|
|
22
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
23
|
+
if (fs.existsSync(pkgPath)) {
|
|
24
|
+
try {
|
|
25
|
+
const raw = fs.readFileSync(pkgPath, 'utf-8');
|
|
26
|
+
const pkg = JSON.parse(raw);
|
|
27
|
+
const allDeps = {
|
|
28
|
+
...pkg.dependencies,
|
|
29
|
+
...pkg.devDependencies,
|
|
30
|
+
};
|
|
31
|
+
if ('@playwright/test' in allDeps) {
|
|
32
|
+
return new PlaywrightAdapter();
|
|
33
|
+
}
|
|
34
|
+
if ('cypress' in allDeps) {
|
|
35
|
+
return new CypressAdapter();
|
|
36
|
+
}
|
|
37
|
+
// Backend API test frameworks
|
|
38
|
+
if ('supertest' in allDeps) {
|
|
39
|
+
const runner = 'vitest' in allDeps ? 'vitest' : 'jest';
|
|
40
|
+
return new SupertestAdapter(runner);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Malformed package.json — fall through to default.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Check for Python project
|
|
48
|
+
const pyprojectPath = path.join(projectRoot, 'pyproject.toml');
|
|
49
|
+
if (fs.existsSync(pyprojectPath)) {
|
|
50
|
+
try {
|
|
51
|
+
const content = fs.readFileSync(pyprojectPath, 'utf-8');
|
|
52
|
+
if (content.includes('pytest')) {
|
|
53
|
+
return new PytestAdapter();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// fall through
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Default to Playwright when we cannot determine the framework.
|
|
61
|
+
return new PlaywrightAdapter();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Detect the test mode for a project: UI testing, API testing, or both.
|
|
65
|
+
* Uses package.json / pyproject.toml dependencies and optional KG metadata.
|
|
66
|
+
*/
|
|
67
|
+
export function detectTestMode(projectRoot, kg) {
|
|
68
|
+
// If KG provides framework hints, use them
|
|
69
|
+
if (kg) {
|
|
70
|
+
const frameworks = kg.project.frameworks.map((f) => f.toLowerCase());
|
|
71
|
+
const uiSet = new Set(UI_FRAMEWORKS);
|
|
72
|
+
const apiSet = new Set(API_FRAMEWORKS);
|
|
73
|
+
const hasUi = frameworks.some((f) => uiSet.has(f));
|
|
74
|
+
const hasApi = frameworks.some((f) => apiSet.has(f));
|
|
75
|
+
if (hasUi && hasApi)
|
|
76
|
+
return 'both';
|
|
77
|
+
if (hasApi)
|
|
78
|
+
return 'api';
|
|
79
|
+
if (hasUi)
|
|
80
|
+
return 'ui';
|
|
81
|
+
}
|
|
82
|
+
// Fall back to package.json inspection
|
|
83
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
84
|
+
if (fs.existsSync(pkgPath)) {
|
|
85
|
+
try {
|
|
86
|
+
const raw = fs.readFileSync(pkgPath, 'utf-8');
|
|
87
|
+
const pkg = JSON.parse(raw);
|
|
88
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
89
|
+
const hasUi = '@playwright/test' in allDeps || 'cypress' in allDeps;
|
|
90
|
+
const hasApi = 'supertest' in allDeps;
|
|
91
|
+
if (hasUi && hasApi)
|
|
92
|
+
return 'both';
|
|
93
|
+
if (hasApi)
|
|
94
|
+
return 'api'; // supertest alone means API-only when no UI framework is present
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// fall through
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Check for Python API testing
|
|
101
|
+
const pyprojectPath = path.join(projectRoot, 'pyproject.toml');
|
|
102
|
+
if (fs.existsSync(pyprojectPath)) {
|
|
103
|
+
try {
|
|
104
|
+
const content = fs.readFileSync(pyprojectPath, 'utf-8');
|
|
105
|
+
if (content.includes('pytest') && !fs.existsSync(path.join(projectRoot, 'package.json'))) {
|
|
106
|
+
return 'api';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// fall through
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return 'ui';
|
|
114
|
+
}
|