@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.
Files changed (259) hide show
  1. package/README.md +95 -8
  2. package/dist/adapters/cypress.d.ts +10 -0
  3. package/dist/adapters/cypress.d.ts.map +1 -0
  4. package/dist/adapters/cypress.js +86 -0
  5. package/dist/adapters/framework_adapter.d.ts +41 -0
  6. package/dist/adapters/framework_adapter.d.ts.map +1 -0
  7. package/dist/adapters/framework_adapter.js +152 -0
  8. package/dist/adapters/playwright.d.ts +10 -0
  9. package/dist/adapters/playwright.d.ts.map +1 -0
  10. package/dist/adapters/playwright.js +86 -0
  11. package/dist/adapters/pytest.d.ts +10 -0
  12. package/dist/adapters/pytest.d.ts.map +1 -0
  13. package/dist/adapters/pytest.js +96 -0
  14. package/dist/adapters/supertest.d.ts +12 -0
  15. package/dist/adapters/supertest.d.ts.map +1 -0
  16. package/dist/adapters/supertest.js +85 -0
  17. package/dist/agent/config.d.ts +1 -1
  18. package/dist/agent/config.d.ts.map +1 -1
  19. package/dist/agent/git.d.ts +1 -0
  20. package/dist/agent/git.d.ts.map +1 -1
  21. package/dist/agent/git.js +3 -0
  22. package/dist/agentic/fix_loop.d.ts.map +1 -1
  23. package/dist/agentic/fix_loop.js +5 -4
  24. package/dist/agentic/runner.d.ts +2 -0
  25. package/dist/agentic/runner.d.ts.map +1 -1
  26. package/dist/agentic/runner.js +15 -12
  27. package/dist/agents/cross-impact.d.ts.map +1 -1
  28. package/dist/agents/cross-impact.js +6 -1
  29. package/dist/agents/executor.d.ts.map +1 -1
  30. package/dist/agents/executor.js +6 -1
  31. package/dist/agents/strategist.d.ts.map +1 -1
  32. package/dist/agents/strategist.js +6 -1
  33. package/dist/agents/test-designer.d.ts.map +1 -1
  34. package/dist/agents/test-designer.js +6 -1
  35. package/dist/anthropic_provider.d.ts.map +1 -1
  36. package/dist/anthropic_provider.js +1 -0
  37. package/dist/base_provider.d.ts +56 -0
  38. package/dist/base_provider.d.ts.map +1 -1
  39. package/dist/base_provider.js +123 -1
  40. package/dist/budget_ledger.d.ts +28 -0
  41. package/dist/budget_ledger.d.ts.map +1 -0
  42. package/dist/budget_ledger.js +62 -0
  43. package/dist/cache/cached_provider.d.ts +45 -0
  44. package/dist/cache/cached_provider.d.ts.map +1 -0
  45. package/dist/cache/cached_provider.js +88 -0
  46. package/dist/cache/response_cache.d.ts +79 -0
  47. package/dist/cache/response_cache.d.ts.map +1 -0
  48. package/dist/cache/response_cache.js +177 -0
  49. package/dist/cli/commands/bootstrap.d.ts +3 -0
  50. package/dist/cli/commands/bootstrap.d.ts.map +1 -0
  51. package/dist/cli/commands/bootstrap.js +109 -0
  52. package/dist/cli/commands/cost_report.d.ts +3 -0
  53. package/dist/cli/commands/cost_report.d.ts.map +1 -0
  54. package/dist/cli/commands/cost_report.js +115 -0
  55. package/dist/cli/commands/crew.d.ts.map +1 -1
  56. package/dist/cli/commands/crew.js +118 -1
  57. package/dist/cli/commands/gate.d.ts +3 -0
  58. package/dist/cli/commands/gate.d.ts.map +1 -0
  59. package/dist/cli/commands/gate.js +86 -0
  60. package/dist/cli/commands/init.d.ts.map +1 -1
  61. package/dist/cli/commands/init.js +7 -62
  62. package/dist/cli/commands/plan_crew.d.ts.map +1 -1
  63. package/dist/cli/commands/plan_crew.js +33 -21
  64. package/dist/cli/commands/train.d.ts.map +1 -1
  65. package/dist/cli/commands/train.js +16 -21
  66. package/dist/cli/defaults.d.ts +35 -0
  67. package/dist/cli/defaults.d.ts.map +1 -0
  68. package/dist/cli/defaults.js +125 -0
  69. package/dist/cli/errors.d.ts +27 -0
  70. package/dist/cli/errors.d.ts.map +1 -0
  71. package/dist/cli/errors.js +57 -0
  72. package/dist/cli/parse_args.d.ts.map +1 -1
  73. package/dist/cli/parse_args.js +24 -2
  74. package/dist/cli/types.d.ts +7 -1
  75. package/dist/cli/types.d.ts.map +1 -1
  76. package/dist/cli.js +47 -2
  77. package/dist/crew/context.d.ts +15 -0
  78. package/dist/crew/context.d.ts.map +1 -1
  79. package/dist/crew/orchestrator.d.ts +14 -0
  80. package/dist/crew/orchestrator.d.ts.map +1 -1
  81. package/dist/crew/orchestrator.js +162 -4
  82. package/dist/crew/protocol.d.ts +13 -0
  83. package/dist/crew/protocol.d.ts.map +1 -1
  84. package/dist/crew/provider.d.ts +15 -1
  85. package/dist/crew/provider.d.ts.map +1 -1
  86. package/dist/crew/provider.js +24 -4
  87. package/dist/custom_provider.d.ts.map +1 -1
  88. package/dist/custom_provider.js +1 -0
  89. package/dist/engine/diff_loader.d.ts.map +1 -1
  90. package/dist/engine/diff_loader.js +3 -14
  91. package/dist/engine/impact_engine.d.ts.map +1 -1
  92. package/dist/engine/impact_engine.js +9 -23
  93. package/dist/esm/adapters/cypress.js +49 -0
  94. package/dist/esm/adapters/framework_adapter.js +114 -0
  95. package/dist/esm/adapters/playwright.js +49 -0
  96. package/dist/esm/adapters/pytest.js +59 -0
  97. package/dist/esm/adapters/supertest.js +48 -0
  98. package/dist/esm/agent/git.js +3 -1
  99. package/dist/esm/agentic/fix_loop.js +5 -4
  100. package/dist/esm/agentic/runner.js +15 -12
  101. package/dist/esm/agents/cross-impact.js +6 -1
  102. package/dist/esm/agents/executor.js +6 -1
  103. package/dist/esm/agents/strategist.js +6 -1
  104. package/dist/esm/agents/test-designer.js +6 -1
  105. package/dist/esm/anthropic_provider.js +1 -0
  106. package/dist/esm/base_provider.js +121 -0
  107. package/dist/esm/budget_ledger.js +58 -0
  108. package/dist/esm/cache/cached_provider.js +82 -0
  109. package/dist/esm/cache/response_cache.js +140 -0
  110. package/dist/esm/cli/commands/bootstrap.js +106 -0
  111. package/dist/esm/cli/commands/cost_report.js +112 -0
  112. package/dist/esm/cli/commands/crew.js +118 -1
  113. package/dist/esm/cli/commands/gate.js +83 -0
  114. package/dist/esm/cli/commands/init.js +3 -58
  115. package/dist/esm/cli/commands/plan_crew.js +33 -21
  116. package/dist/esm/cli/commands/train.js +16 -21
  117. package/dist/esm/cli/defaults.js +118 -0
  118. package/dist/esm/cli/errors.js +52 -0
  119. package/dist/esm/cli/parse_args.js +24 -2
  120. package/dist/esm/cli.js +47 -2
  121. package/dist/esm/crew/orchestrator.js +162 -4
  122. package/dist/esm/crew/provider.js +24 -4
  123. package/dist/esm/custom_provider.js +1 -0
  124. package/dist/esm/engine/diff_loader.js +1 -12
  125. package/dist/esm/engine/impact_engine.js +9 -23
  126. package/dist/esm/index.js +21 -0
  127. package/dist/esm/knowledge/cluster_utils.js +60 -0
  128. package/dist/esm/knowledge/kg_bridge.js +381 -0
  129. package/dist/esm/knowledge/kg_types.js +3 -0
  130. package/dist/esm/knowledge/route_families.js +89 -0
  131. package/dist/esm/mcp-server.js +2 -4
  132. package/dist/esm/metrics/prometheus.js +149 -0
  133. package/dist/esm/model_router.js +59 -0
  134. package/dist/esm/ollama_provider.js +1 -0
  135. package/dist/esm/openai_provider.js +1 -0
  136. package/dist/esm/pipeline/orchestrator.js +6 -12
  137. package/dist/esm/pipeline/stage0_preprocess.js +12 -19
  138. package/dist/esm/pipeline/stage2_coverage.js +1 -0
  139. package/dist/esm/pipeline/stage3_generation.js +1 -0
  140. package/dist/esm/progress.js +112 -0
  141. package/dist/esm/prompts/coverage.js +7 -24
  142. package/dist/esm/prompts/cross-impact.js +3 -21
  143. package/dist/esm/prompts/generation.js +158 -36
  144. package/dist/esm/prompts/generation_profile.js +147 -0
  145. package/dist/esm/prompts/heal.js +33 -15
  146. package/dist/esm/prompts/impact.js +3 -22
  147. package/dist/esm/prompts/json_extract.js +36 -0
  148. package/dist/esm/prompts/strategist.js +2 -20
  149. package/dist/esm/prompts/test-designer.js +6 -21
  150. package/dist/esm/provider_factory.js +6 -4
  151. package/dist/esm/reporters/junit.js +86 -0
  152. package/dist/esm/reporters/reporter.js +3 -0
  153. package/dist/esm/reporters/sarif.js +131 -0
  154. package/dist/esm/resilience/circuit_breaker.js +78 -0
  155. package/dist/esm/resilience/retry.js +56 -0
  156. package/dist/esm/sanitize.js +66 -0
  157. package/dist/esm/training/kg_scanner.js +115 -0
  158. package/dist/esm/training/scanner.js +27 -34
  159. package/dist/esm/version.js +33 -0
  160. package/dist/index.d.ts +21 -1
  161. package/dist/index.d.ts.map +1 -1
  162. package/dist/index.js +45 -1
  163. package/dist/knowledge/cluster_utils.d.ts +28 -0
  164. package/dist/knowledge/cluster_utils.d.ts.map +1 -0
  165. package/dist/knowledge/cluster_utils.js +67 -0
  166. package/dist/knowledge/kg_bridge.d.ts +31 -0
  167. package/dist/knowledge/kg_bridge.d.ts.map +1 -0
  168. package/dist/knowledge/kg_bridge.js +388 -0
  169. package/dist/knowledge/kg_types.d.ts +75 -0
  170. package/dist/knowledge/kg_types.d.ts.map +1 -0
  171. package/dist/knowledge/kg_types.js +4 -0
  172. package/dist/knowledge/route_families.d.ts +18 -0
  173. package/dist/knowledge/route_families.d.ts.map +1 -1
  174. package/dist/knowledge/route_families.js +91 -0
  175. package/dist/mcp-server.d.ts.map +1 -1
  176. package/dist/mcp-server.js +2 -4
  177. package/dist/metrics/prometheus.d.ts +37 -0
  178. package/dist/metrics/prometheus.d.ts.map +1 -0
  179. package/dist/metrics/prometheus.js +153 -0
  180. package/dist/model_router.d.ts +28 -0
  181. package/dist/model_router.d.ts.map +1 -0
  182. package/dist/model_router.js +63 -0
  183. package/dist/ollama_provider.d.ts.map +1 -1
  184. package/dist/ollama_provider.js +1 -0
  185. package/dist/openai_provider.d.ts.map +1 -1
  186. package/dist/openai_provider.js +1 -0
  187. package/dist/pipeline/orchestrator.d.ts +2 -0
  188. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  189. package/dist/pipeline/orchestrator.js +6 -12
  190. package/dist/pipeline/stage0_preprocess.d.ts.map +1 -1
  191. package/dist/pipeline/stage0_preprocess.js +11 -18
  192. package/dist/pipeline/stage2_coverage.d.ts +2 -0
  193. package/dist/pipeline/stage2_coverage.d.ts.map +1 -1
  194. package/dist/pipeline/stage2_coverage.js +1 -0
  195. package/dist/pipeline/stage3_generation.d.ts +2 -0
  196. package/dist/pipeline/stage3_generation.d.ts.map +1 -1
  197. package/dist/pipeline/stage3_generation.js +1 -0
  198. package/dist/pipeline/stage4_heal.d.ts +2 -0
  199. package/dist/pipeline/stage4_heal.d.ts.map +1 -1
  200. package/dist/progress.d.ts +22 -0
  201. package/dist/progress.d.ts.map +1 -0
  202. package/dist/progress.js +116 -0
  203. package/dist/prompts/coverage.d.ts +2 -0
  204. package/dist/prompts/coverage.d.ts.map +1 -1
  205. package/dist/prompts/coverage.js +7 -24
  206. package/dist/prompts/cross-impact.d.ts +1 -0
  207. package/dist/prompts/cross-impact.d.ts.map +1 -1
  208. package/dist/prompts/cross-impact.js +3 -21
  209. package/dist/prompts/generation.d.ts +3 -1
  210. package/dist/prompts/generation.d.ts.map +1 -1
  211. package/dist/prompts/generation.js +158 -36
  212. package/dist/prompts/generation_profile.d.ts +29 -0
  213. package/dist/prompts/generation_profile.d.ts.map +1 -0
  214. package/dist/prompts/generation_profile.js +151 -0
  215. package/dist/prompts/heal.d.ts +3 -1
  216. package/dist/prompts/heal.d.ts.map +1 -1
  217. package/dist/prompts/heal.js +33 -15
  218. package/dist/prompts/impact.d.ts +1 -0
  219. package/dist/prompts/impact.d.ts.map +1 -1
  220. package/dist/prompts/impact.js +3 -22
  221. package/dist/prompts/json_extract.d.ts +14 -0
  222. package/dist/prompts/json_extract.d.ts.map +1 -0
  223. package/dist/prompts/json_extract.js +39 -0
  224. package/dist/prompts/strategist.d.ts.map +1 -1
  225. package/dist/prompts/strategist.js +2 -20
  226. package/dist/prompts/test-designer.d.ts +2 -0
  227. package/dist/prompts/test-designer.d.ts.map +1 -1
  228. package/dist/prompts/test-designer.js +6 -21
  229. package/dist/provider_factory.d.ts.map +1 -1
  230. package/dist/provider_factory.js +6 -4
  231. package/dist/reporters/junit.d.ts +6 -0
  232. package/dist/reporters/junit.d.ts.map +1 -0
  233. package/dist/reporters/junit.js +89 -0
  234. package/dist/reporters/reporter.d.ts +42 -0
  235. package/dist/reporters/reporter.d.ts.map +1 -0
  236. package/dist/reporters/reporter.js +4 -0
  237. package/dist/reporters/sarif.d.ts +7 -0
  238. package/dist/reporters/sarif.d.ts.map +1 -0
  239. package/dist/reporters/sarif.js +134 -0
  240. package/dist/resilience/circuit_breaker.d.ts +36 -0
  241. package/dist/resilience/circuit_breaker.d.ts.map +1 -0
  242. package/dist/resilience/circuit_breaker.js +82 -0
  243. package/dist/resilience/retry.d.ts +11 -0
  244. package/dist/resilience/retry.d.ts.map +1 -0
  245. package/dist/resilience/retry.js +59 -0
  246. package/dist/sanitize.d.ts +15 -0
  247. package/dist/sanitize.d.ts.map +1 -0
  248. package/dist/sanitize.js +71 -0
  249. package/dist/training/kg_scanner.d.ts +13 -0
  250. package/dist/training/kg_scanner.d.ts.map +1 -0
  251. package/dist/training/kg_scanner.js +118 -0
  252. package/dist/training/scanner.d.ts +7 -2
  253. package/dist/training/scanner.d.ts.map +1 -1
  254. package/dist/training/scanner.js +27 -34
  255. package/dist/version.d.ts +6 -0
  256. package/dist/version.d.ts.map +1 -0
  257. package/dist/version.js +36 -0
  258. package/package.json +7 -2
  259. 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 workflow.phases) {
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
- if (config.budgetUSD && ctx.usage.totalCost >= config.budgetUSD) {
78
- warnings.push(`Budget limit reached ($${ctx.usage.totalCost.toFixed(4)} >= $${config.budgetUSD}). Stopping workflow.`);
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
- export async function getCrewProvider(providerOverride) {
9
- if (providerOverride) {
10
- return LLMProviderFactory.createFromString(providerOverride);
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
- return LLMProviderFactory.createFromEnv();
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
  }
@@ -65,6 +65,7 @@ export class CustomProvider extends BaseProvider {
65
65
  };
66
66
  }
67
67
  async generateText(prompt, options) {
68
+ this.checkBudget();
68
69
  const startTime = Date.now();
69
70
  try {
70
71
  if (prompt.length > 10 * 1024 * 1024) {
@@ -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 { spawnSync } from 'child_process';
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.files.includes(fb.file)) {
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
- const manifest = loadRouteFamilyManifest(testsRoot, routeFamilies);
175
+ // Load manifest, fall back to heuristic families if not found
176
+ let manifest = loadRouteFamilyManifest(testsRoot, routeFamilies);
185
177
  if (!manifest) {
186
- return {
187
- changedFiles,
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 };