@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.
Files changed (256) 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/train.d.ts.map +1 -1
  63. package/dist/cli/commands/train.js +16 -21
  64. package/dist/cli/defaults.d.ts +35 -0
  65. package/dist/cli/defaults.d.ts.map +1 -0
  66. package/dist/cli/defaults.js +125 -0
  67. package/dist/cli/errors.d.ts +27 -0
  68. package/dist/cli/errors.d.ts.map +1 -0
  69. package/dist/cli/errors.js +57 -0
  70. package/dist/cli/parse_args.d.ts.map +1 -1
  71. package/dist/cli/parse_args.js +24 -2
  72. package/dist/cli/types.d.ts +7 -1
  73. package/dist/cli/types.d.ts.map +1 -1
  74. package/dist/cli.js +47 -2
  75. package/dist/crew/context.d.ts +15 -0
  76. package/dist/crew/context.d.ts.map +1 -1
  77. package/dist/crew/orchestrator.d.ts +14 -0
  78. package/dist/crew/orchestrator.d.ts.map +1 -1
  79. package/dist/crew/orchestrator.js +162 -4
  80. package/dist/crew/protocol.d.ts +13 -0
  81. package/dist/crew/protocol.d.ts.map +1 -1
  82. package/dist/crew/provider.d.ts +15 -1
  83. package/dist/crew/provider.d.ts.map +1 -1
  84. package/dist/crew/provider.js +24 -4
  85. package/dist/custom_provider.d.ts.map +1 -1
  86. package/dist/custom_provider.js +1 -0
  87. package/dist/engine/diff_loader.d.ts.map +1 -1
  88. package/dist/engine/diff_loader.js +3 -14
  89. package/dist/engine/impact_engine.d.ts.map +1 -1
  90. package/dist/engine/impact_engine.js +9 -23
  91. package/dist/esm/adapters/cypress.js +49 -0
  92. package/dist/esm/adapters/framework_adapter.js +114 -0
  93. package/dist/esm/adapters/playwright.js +49 -0
  94. package/dist/esm/adapters/pytest.js +59 -0
  95. package/dist/esm/adapters/supertest.js +48 -0
  96. package/dist/esm/agent/git.js +3 -1
  97. package/dist/esm/agentic/fix_loop.js +5 -4
  98. package/dist/esm/agentic/runner.js +15 -12
  99. package/dist/esm/agents/cross-impact.js +6 -1
  100. package/dist/esm/agents/executor.js +6 -1
  101. package/dist/esm/agents/strategist.js +6 -1
  102. package/dist/esm/agents/test-designer.js +6 -1
  103. package/dist/esm/anthropic_provider.js +1 -0
  104. package/dist/esm/base_provider.js +121 -0
  105. package/dist/esm/budget_ledger.js +58 -0
  106. package/dist/esm/cache/cached_provider.js +82 -0
  107. package/dist/esm/cache/response_cache.js +140 -0
  108. package/dist/esm/cli/commands/bootstrap.js +106 -0
  109. package/dist/esm/cli/commands/cost_report.js +112 -0
  110. package/dist/esm/cli/commands/crew.js +118 -1
  111. package/dist/esm/cli/commands/gate.js +83 -0
  112. package/dist/esm/cli/commands/init.js +3 -58
  113. package/dist/esm/cli/commands/train.js +16 -21
  114. package/dist/esm/cli/defaults.js +118 -0
  115. package/dist/esm/cli/errors.js +52 -0
  116. package/dist/esm/cli/parse_args.js +24 -2
  117. package/dist/esm/cli.js +47 -2
  118. package/dist/esm/crew/orchestrator.js +162 -4
  119. package/dist/esm/crew/provider.js +24 -4
  120. package/dist/esm/custom_provider.js +1 -0
  121. package/dist/esm/engine/diff_loader.js +1 -12
  122. package/dist/esm/engine/impact_engine.js +9 -23
  123. package/dist/esm/index.js +21 -0
  124. package/dist/esm/knowledge/cluster_utils.js +60 -0
  125. package/dist/esm/knowledge/kg_bridge.js +381 -0
  126. package/dist/esm/knowledge/kg_types.js +3 -0
  127. package/dist/esm/knowledge/route_families.js +89 -0
  128. package/dist/esm/mcp-server.js +2 -4
  129. package/dist/esm/metrics/prometheus.js +149 -0
  130. package/dist/esm/model_router.js +59 -0
  131. package/dist/esm/ollama_provider.js +1 -0
  132. package/dist/esm/openai_provider.js +1 -0
  133. package/dist/esm/pipeline/orchestrator.js +6 -12
  134. package/dist/esm/pipeline/stage0_preprocess.js +12 -19
  135. package/dist/esm/pipeline/stage2_coverage.js +1 -0
  136. package/dist/esm/pipeline/stage3_generation.js +1 -0
  137. package/dist/esm/progress.js +112 -0
  138. package/dist/esm/prompts/coverage.js +7 -24
  139. package/dist/esm/prompts/cross-impact.js +3 -21
  140. package/dist/esm/prompts/generation.js +158 -36
  141. package/dist/esm/prompts/generation_profile.js +147 -0
  142. package/dist/esm/prompts/heal.js +33 -15
  143. package/dist/esm/prompts/impact.js +3 -22
  144. package/dist/esm/prompts/json_extract.js +36 -0
  145. package/dist/esm/prompts/strategist.js +2 -20
  146. package/dist/esm/prompts/test-designer.js +6 -21
  147. package/dist/esm/provider_factory.js +6 -4
  148. package/dist/esm/reporters/junit.js +86 -0
  149. package/dist/esm/reporters/reporter.js +3 -0
  150. package/dist/esm/reporters/sarif.js +131 -0
  151. package/dist/esm/resilience/circuit_breaker.js +78 -0
  152. package/dist/esm/resilience/retry.js +56 -0
  153. package/dist/esm/sanitize.js +66 -0
  154. package/dist/esm/training/kg_scanner.js +115 -0
  155. package/dist/esm/training/scanner.js +27 -34
  156. package/dist/esm/version.js +33 -0
  157. package/dist/index.d.ts +21 -1
  158. package/dist/index.d.ts.map +1 -1
  159. package/dist/index.js +45 -1
  160. package/dist/knowledge/cluster_utils.d.ts +28 -0
  161. package/dist/knowledge/cluster_utils.d.ts.map +1 -0
  162. package/dist/knowledge/cluster_utils.js +67 -0
  163. package/dist/knowledge/kg_bridge.d.ts +31 -0
  164. package/dist/knowledge/kg_bridge.d.ts.map +1 -0
  165. package/dist/knowledge/kg_bridge.js +388 -0
  166. package/dist/knowledge/kg_types.d.ts +75 -0
  167. package/dist/knowledge/kg_types.d.ts.map +1 -0
  168. package/dist/knowledge/kg_types.js +4 -0
  169. package/dist/knowledge/route_families.d.ts +18 -0
  170. package/dist/knowledge/route_families.d.ts.map +1 -1
  171. package/dist/knowledge/route_families.js +91 -0
  172. package/dist/mcp-server.d.ts.map +1 -1
  173. package/dist/mcp-server.js +2 -4
  174. package/dist/metrics/prometheus.d.ts +37 -0
  175. package/dist/metrics/prometheus.d.ts.map +1 -0
  176. package/dist/metrics/prometheus.js +153 -0
  177. package/dist/model_router.d.ts +28 -0
  178. package/dist/model_router.d.ts.map +1 -0
  179. package/dist/model_router.js +63 -0
  180. package/dist/ollama_provider.d.ts.map +1 -1
  181. package/dist/ollama_provider.js +1 -0
  182. package/dist/openai_provider.d.ts.map +1 -1
  183. package/dist/openai_provider.js +1 -0
  184. package/dist/pipeline/orchestrator.d.ts +2 -0
  185. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  186. package/dist/pipeline/orchestrator.js +6 -12
  187. package/dist/pipeline/stage0_preprocess.d.ts.map +1 -1
  188. package/dist/pipeline/stage0_preprocess.js +11 -18
  189. package/dist/pipeline/stage2_coverage.d.ts +2 -0
  190. package/dist/pipeline/stage2_coverage.d.ts.map +1 -1
  191. package/dist/pipeline/stage2_coverage.js +1 -0
  192. package/dist/pipeline/stage3_generation.d.ts +2 -0
  193. package/dist/pipeline/stage3_generation.d.ts.map +1 -1
  194. package/dist/pipeline/stage3_generation.js +1 -0
  195. package/dist/pipeline/stage4_heal.d.ts +2 -0
  196. package/dist/pipeline/stage4_heal.d.ts.map +1 -1
  197. package/dist/progress.d.ts +22 -0
  198. package/dist/progress.d.ts.map +1 -0
  199. package/dist/progress.js +116 -0
  200. package/dist/prompts/coverage.d.ts +2 -0
  201. package/dist/prompts/coverage.d.ts.map +1 -1
  202. package/dist/prompts/coverage.js +7 -24
  203. package/dist/prompts/cross-impact.d.ts +1 -0
  204. package/dist/prompts/cross-impact.d.ts.map +1 -1
  205. package/dist/prompts/cross-impact.js +3 -21
  206. package/dist/prompts/generation.d.ts +3 -1
  207. package/dist/prompts/generation.d.ts.map +1 -1
  208. package/dist/prompts/generation.js +158 -36
  209. package/dist/prompts/generation_profile.d.ts +29 -0
  210. package/dist/prompts/generation_profile.d.ts.map +1 -0
  211. package/dist/prompts/generation_profile.js +151 -0
  212. package/dist/prompts/heal.d.ts +3 -1
  213. package/dist/prompts/heal.d.ts.map +1 -1
  214. package/dist/prompts/heal.js +33 -15
  215. package/dist/prompts/impact.d.ts +1 -0
  216. package/dist/prompts/impact.d.ts.map +1 -1
  217. package/dist/prompts/impact.js +3 -22
  218. package/dist/prompts/json_extract.d.ts +14 -0
  219. package/dist/prompts/json_extract.d.ts.map +1 -0
  220. package/dist/prompts/json_extract.js +39 -0
  221. package/dist/prompts/strategist.d.ts.map +1 -1
  222. package/dist/prompts/strategist.js +2 -20
  223. package/dist/prompts/test-designer.d.ts +2 -0
  224. package/dist/prompts/test-designer.d.ts.map +1 -1
  225. package/dist/prompts/test-designer.js +6 -21
  226. package/dist/provider_factory.d.ts.map +1 -1
  227. package/dist/provider_factory.js +6 -4
  228. package/dist/reporters/junit.d.ts +6 -0
  229. package/dist/reporters/junit.d.ts.map +1 -0
  230. package/dist/reporters/junit.js +89 -0
  231. package/dist/reporters/reporter.d.ts +42 -0
  232. package/dist/reporters/reporter.d.ts.map +1 -0
  233. package/dist/reporters/reporter.js +4 -0
  234. package/dist/reporters/sarif.d.ts +7 -0
  235. package/dist/reporters/sarif.d.ts.map +1 -0
  236. package/dist/reporters/sarif.js +134 -0
  237. package/dist/resilience/circuit_breaker.d.ts +36 -0
  238. package/dist/resilience/circuit_breaker.d.ts.map +1 -0
  239. package/dist/resilience/circuit_breaker.js +82 -0
  240. package/dist/resilience/retry.d.ts +11 -0
  241. package/dist/resilience/retry.d.ts.map +1 -0
  242. package/dist/resilience/retry.js +59 -0
  243. package/dist/sanitize.d.ts +15 -0
  244. package/dist/sanitize.d.ts.map +1 -0
  245. package/dist/sanitize.js +71 -0
  246. package/dist/training/kg_scanner.d.ts +13 -0
  247. package/dist/training/kg_scanner.d.ts.map +1 -0
  248. package/dist/training/kg_scanner.js +118 -0
  249. package/dist/training/scanner.d.ts +7 -2
  250. package/dist/training/scanner.d.ts.map +1 -1
  251. package/dist/training/scanner.js +27 -34
  252. package/dist/version.d.ts +6 -0
  253. package/dist/version.d.ts.map +1 -0
  254. package/dist/version.js +36 -0
  255. package/package.json +7 -2
  256. package/schemas/route-families.schema.json +31 -1
@@ -5,13 +5,16 @@ import { existsSync, mkdirSync, renameSync, writeFileSync } from 'fs';
5
5
  import { dirname, join, resolve } from 'path';
6
6
  import * as readline from 'readline';
7
7
  import { resolveConfig } from '../../agent/config.js';
8
- import { loadRouteFamilyManifest } from '../../knowledge/route_families.js';
8
+ import { loadRouteFamilyManifest, serializeManifest } from '../../knowledge/route_families.js';
9
9
  import { LLMProviderFactory } from '../../provider_factory.js';
10
10
  import { logger, LogLevel } from '../../logger.js';
11
+ import { getVersion } from '../../version.js';
11
12
  import { scanProject } from '../../training/scanner.js';
13
+ import { scanFromKnowledgeGraph } from '../../training/kg_scanner.js';
12
14
  import { mergeFamilies, detectStaleFamilies } from '../../training/merger.js';
13
15
  import { enrichFamilies } from '../../training/enricher.js';
14
16
  import { getCommitFiles, validateCommit, buildValidationReport, formatValidationReport } from '../../training/validator.js';
17
+ import { loadKnowledgeGraph } from '../../knowledge/kg_bridge.js';
15
18
  class TrainError extends Error {
16
19
  constructor(message) {
17
20
  super(message);
@@ -121,24 +124,6 @@ function ask(rl, question, defaultValue) {
121
124
  });
122
125
  });
123
126
  }
124
- function serializeManifest(manifest) {
125
- const output = {
126
- families: manifest.families.map((f) => {
127
- // Remove undefined/empty optional fields for clean JSON
128
- const cleaned = { ...f };
129
- const optionalArrays = ['pageObjects', 'components', 'webappPaths', 'serverPaths', 'specDirs', 'cypressSpecDirs', 'tags', 'userFlows', 'features'];
130
- for (const key of optionalArrays) {
131
- if (!cleaned[key] || (Array.isArray(cleaned[key]) && cleaned[key].length === 0)) {
132
- delete cleaned[key];
133
- }
134
- }
135
- if (!cleaned.priority)
136
- delete cleaned.priority;
137
- return cleaned;
138
- }),
139
- };
140
- return JSON.stringify(output, null, 2) + '\n';
141
- }
142
127
  export async function runTrainCommand(args, autoConfig) {
143
128
  const opts = resolveTrainOptions(args, autoConfig);
144
129
  const totalTimer = logger.timer('train-total');
@@ -151,12 +136,22 @@ export async function runTrainCommand(args, autoConfig) {
151
136
  logger.info('e2e-ai-agents train');
152
137
  logger.info('===================');
153
138
  // ---------- Phase 1: Deterministic scan ----------
139
+ // Prefer knowledge graph when available
140
+ const kg = loadKnowledgeGraph(opts.appPath);
154
141
  logger.info('Scanning project structure...');
155
142
  if (opts.serverRoot) {
156
143
  logger.info(`Server root: ${opts.serverRoot}`);
157
144
  }
158
145
  const scanTimer = logger.timer('scan');
159
- const scanResult = scanProject(opts.appPath, opts.testsRoot !== opts.appPath ? opts.testsRoot : undefined, opts.serverRoot, opts.gitRepoRoot);
146
+ let scanResult;
147
+ if (kg) {
148
+ logger.info('Using knowledge graph for scanning (found .understand-anything/knowledge-graph.json)');
149
+ scanResult = scanFromKnowledgeGraph(kg);
150
+ logger.info(`KG: ${kg.nodes.length} nodes, ${kg.edges.length} edges`);
151
+ }
152
+ else {
153
+ scanResult = scanProject(opts.appPath, opts.testsRoot !== opts.appPath ? opts.testsRoot : undefined, opts.serverRoot, opts.gitRepoRoot);
154
+ }
160
155
  timings.scan = scanTimer.end();
161
156
  logger.info(`Found ${scanResult.stats.totalSourceFiles} source files, ${scanResult.stats.totalTestFiles} test files`);
162
157
  logger.info(`Discovered ${scanResult.families.length} candidate families`);
@@ -332,7 +327,7 @@ export async function runTrainCommand(args, autoConfig) {
332
327
  const reportDir = dirname(opts.outputPath);
333
328
  const trainReport = {
334
329
  timestamp: new Date().toISOString(),
335
- version: '1.7.0',
330
+ version: getVersion(),
336
331
  timings,
337
332
  families: {
338
333
  total: mergeResult.manifest.families.length,
@@ -0,0 +1,118 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import { execFileSync } from 'child_process';
5
+ import { join, resolve } from 'path';
6
+ /**
7
+ * Detect the test framework from package.json dependencies.
8
+ */
9
+ export function detectFramework(appPath) {
10
+ const resolvedPath = resolve(appPath);
11
+ const pkgPath = join(resolvedPath, 'package.json');
12
+ if (!existsSync(pkgPath)) {
13
+ return 'auto';
14
+ }
15
+ try {
16
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
17
+ const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
18
+ if (allDeps['@playwright/test'] || allDeps.playwright) {
19
+ return 'playwright';
20
+ }
21
+ if (allDeps.cypress) {
22
+ return 'cypress';
23
+ }
24
+ if (allDeps['selenium-webdriver'] || allDeps.webdriverio) {
25
+ return 'selenium';
26
+ }
27
+ }
28
+ catch {
29
+ // ignore malformed package.json
30
+ }
31
+ return 'auto';
32
+ }
33
+ /**
34
+ * Detect the tests root directory by scanning common conventions.
35
+ */
36
+ export function detectTestsRoot(appPath) {
37
+ const resolvedPath = resolve(appPath);
38
+ const candidates = [
39
+ 'e2e-tests/playwright',
40
+ 'e2e-tests',
41
+ 'e2e',
42
+ 'tests/e2e',
43
+ 'test/e2e',
44
+ 'tests',
45
+ 'test',
46
+ 'specs',
47
+ 'playwright',
48
+ 'cypress',
49
+ ];
50
+ for (const candidate of candidates) {
51
+ if (existsSync(join(resolvedPath, candidate))) {
52
+ return candidate;
53
+ }
54
+ }
55
+ return undefined;
56
+ }
57
+ /**
58
+ * Detect the git default branch for diffing.
59
+ * Returns origin/<branch> format.
60
+ */
61
+ export function detectGitDefaultBranch(appPath) {
62
+ try {
63
+ // Try to find the remote HEAD branch first
64
+ const remoteInfo = execFileSync('git', ['remote', 'show', 'origin'], {
65
+ cwd: resolve(appPath),
66
+ encoding: 'utf-8',
67
+ stdio: ['pipe', 'pipe', 'pipe'],
68
+ timeout: 5000,
69
+ });
70
+ const headMatch = remoteInfo.match(/HEAD branch:\s*(.+)/);
71
+ if (headMatch) {
72
+ return `origin/${headMatch[1].trim()}`;
73
+ }
74
+ }
75
+ catch {
76
+ // fallback to current branch
77
+ }
78
+ try {
79
+ const result = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
80
+ cwd: resolve(appPath),
81
+ encoding: 'utf-8',
82
+ stdio: ['pipe', 'pipe', 'pipe'],
83
+ timeout: 5000,
84
+ }).trim();
85
+ return `origin/${result}`;
86
+ }
87
+ catch {
88
+ return 'origin/main';
89
+ }
90
+ }
91
+ /**
92
+ * Detect the project root by walking up to find package.json or .git.
93
+ */
94
+ export function detectProjectRoot(startDir) {
95
+ let current = resolve(startDir);
96
+ while (true) {
97
+ if (existsSync(join(current, 'package.json')) || existsSync(join(current, '.git'))) {
98
+ return current;
99
+ }
100
+ const parent = resolve(current, '..');
101
+ if (parent === current) {
102
+ break;
103
+ }
104
+ current = parent;
105
+ }
106
+ return startDir;
107
+ }
108
+ /**
109
+ * Resolve defaults for CLI commands that need path/testsRoot/framework/since.
110
+ * Explicit values from CLI flags take precedence over detected values.
111
+ */
112
+ export function resolveDefaults(explicit) {
113
+ const path = explicit.path || detectProjectRoot(process.cwd());
114
+ const testsRoot = explicit.testsRoot || detectTestsRoot(path) || '.';
115
+ const framework = explicit.framework || detectFramework(path);
116
+ const since = explicit.gitSince || detectGitDefaultBranch(path);
117
+ return { path, testsRoot, framework, since };
118
+ }
@@ -0,0 +1,52 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ /**
4
+ * CLI Error types with structured exit codes.
5
+ *
6
+ * Exit codes:
7
+ * 0 = success
8
+ * 1 = general/user error (bad args, missing config, invalid input)
9
+ * 2 = budget exceeded
10
+ * 3 = LLM provider unavailable (API down, auth failure)
11
+ * 4 = invalid manifest or config file
12
+ */
13
+ export const EXIT_CODES = {
14
+ SUCCESS: 0,
15
+ GENERAL_ERROR: 1,
16
+ BUDGET_EXCEEDED: 2,
17
+ PROVIDER_UNAVAILABLE: 3,
18
+ INVALID_CONFIG: 4,
19
+ };
20
+ export class CliError extends Error {
21
+ constructor(message, exitCode = EXIT_CODES.GENERAL_ERROR) {
22
+ super(message);
23
+ this.exitCode = exitCode;
24
+ this.name = 'CliError';
25
+ }
26
+ }
27
+ /**
28
+ * Classify an unknown error into the appropriate exit code.
29
+ */
30
+ export function classifyError(error) {
31
+ if (error instanceof CliError)
32
+ return error.exitCode;
33
+ const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
34
+ // Budget errors
35
+ if (msg.includes('budget exceeded') || msg.includes('budget limit')) {
36
+ return EXIT_CODES.BUDGET_EXCEEDED;
37
+ }
38
+ // Provider/auth errors
39
+ if (msg.includes('api key') || msg.includes('authentication') ||
40
+ msg.includes('unauthorized') || msg.includes('403') ||
41
+ (msg.includes('provider') && msg.includes('unavailable')) ||
42
+ msg.includes('econnrefused') || msg.includes('econnreset')) {
43
+ return EXIT_CODES.PROVIDER_UNAVAILABLE;
44
+ }
45
+ // Config/manifest errors
46
+ if ((msg.includes('manifest') && (msg.includes('invalid') || msg.includes('not found') || msg.includes('parse'))) ||
47
+ (msg.includes('config') && msg.includes('invalid')) ||
48
+ (msg.includes('route-families') && msg.includes('invalid'))) {
49
+ return EXIT_CODES.INVALID_CONFIG;
50
+ }
51
+ return EXIT_CODES.GENERAL_ERROR;
52
+ }
@@ -2,6 +2,7 @@
2
2
  // See LICENSE.txt for license information.
3
3
  import { existsSync } from 'fs';
4
4
  import { dirname, join, resolve } from 'path';
5
+ import { logger } from '../logger.js';
5
6
  export const CONFIG_CANDIDATES = ['e2e-ai-agents.config.json', '.e2e-ai-agents.config.json'];
6
7
  export function findConfigUpwards(startDir) {
7
8
  if (!startDir) {
@@ -62,6 +63,7 @@ const FLAGS = {
62
63
  '--generate': { key: 'analyzeGenerate', type: 'boolean' },
63
64
  '--heal': { key: 'analyzeHeal', type: 'boolean' },
64
65
  '--no-ai': { key: 'noAi', type: 'boolean' },
66
+ '--degraded-mode': { key: 'degradedMode', type: 'boolean' },
65
67
  '--enrich': { key: 'trainEnrich', type: 'boolean' },
66
68
  '--no-enrich': { key: 'trainEnrich', type: 'boolean-false' },
67
69
  '--validate': { key: 'trainValidate', type: 'boolean' },
@@ -131,6 +133,13 @@ const FLAGS = {
131
133
  type: 'csv',
132
134
  transform: (v) => csvSplit(v).filter((s) => s === 'run-now' || s === 'must-add-tests' || s === 'safe-to-merge'),
133
135
  },
136
+ // -- gate command --
137
+ '--threshold': { key: 'gateThreshold', type: 'number' },
138
+ // -- bootstrap command --
139
+ '--kg-path': { key: 'bootstrapKgPath', type: 'string' },
140
+ '--scaffold-framework': { key: 'bootstrapScaffoldFramework', type: 'boolean' },
141
+ '--test-mode': { key: 'bootstrapTestMode', type: 'enum', enumValues: ['ui', 'api', 'both'] },
142
+ '--max-families': { key: 'bootstrapMaxFamilies', type: 'number' },
134
143
  };
135
144
  // Build a lookup from alias -> canonical flag name
136
145
  const ALIAS_MAP = {};
@@ -146,7 +155,8 @@ const COMMANDS = new Set([
146
155
  'init', 'impact', 'plan', 'heal', 'suggest', 'generate',
147
156
  'finalize-generated-tests', 'feedback',
148
157
  'traceability-capture', 'traceability-ingest',
149
- 'analyze', 'llm-health', 'train', 'crew',
158
+ 'analyze', 'llm-health', 'train', 'crew', 'cost-report', 'gate',
159
+ 'bootstrap',
150
160
  ]);
151
161
  // ---------------------------------------------------------------------------
152
162
  // Parser
@@ -168,6 +178,9 @@ export function parseArgs(argv) {
168
178
  const arg = argv[i];
169
179
  const canonical = ALIAS_MAP[arg];
170
180
  if (!canonical) {
181
+ if (arg.startsWith('--')) {
182
+ logger.warn(`Unknown flag "${arg}" (ignored)`);
183
+ }
171
184
  continue;
172
185
  }
173
186
  const def = FLAGS[canonical];
@@ -196,7 +209,16 @@ export function parseArgs(argv) {
196
209
  break;
197
210
  case 'number-raw':
198
211
  if (next) {
199
- setField(parsed, def.key, def.transform ? def.transform(next) : Number(next));
212
+ const rawValue = def.transform ? def.transform(next) : Number(next);
213
+ // Allow non-number transforms through; reject NaN/Infinity for numbers
214
+ if (typeof rawValue === 'number') {
215
+ if (Number.isFinite(rawValue)) {
216
+ setField(parsed, def.key, rawValue);
217
+ }
218
+ }
219
+ else {
220
+ setField(parsed, def.key, rawValue);
221
+ }
200
222
  i += 1;
201
223
  }
202
224
  break;
package/dist/esm/cli.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // See LICENSE.txt for license information.
4
4
  import { resolveConfig } from './agent/config.js';
5
5
  import { parseArgs, resolveAutoConfig } from './cli/parse_args.js';
6
+ import { resolveDefaults } from './cli/defaults.js';
6
7
  import { printUsage } from './cli/usage.js';
7
8
  import { runLlmHealth } from './cli/commands/llm_health.js';
8
9
  import { runAnalyzeCommand } from './cli/commands/analyze.js';
@@ -16,14 +17,42 @@ import { runGenerateCommand } from './cli/commands/generate.js';
16
17
  import { runInitCommand } from './cli/commands/init.js';
17
18
  import { runTrainCommand } from './cli/commands/train.js';
18
19
  import { runCrewCommand } from './cli/commands/crew.js';
20
+ import { runCostReportCommand } from './cli/commands/cost_report.js';
21
+ import { runGateCommand } from './cli/commands/gate.js';
22
+ import { runBootstrapCommand } from './cli/commands/bootstrap.js';
23
+ import { classifyError, EXIT_CODES } from './cli/errors.js';
24
+ // Commands that skip default resolution (they handle their own setup)
25
+ const SKIP_DEFAULTS_COMMANDS = new Set(['init', 'llm-health', 'cost-report', 'bootstrap']);
26
+ // Commands that need path/testsRoot/framework/since
27
+ const NEEDS_DEFAULTS_COMMANDS = new Set([
28
+ 'impact', 'plan', 'suggest', 'crew', 'generate', 'heal', 'analyze', 'train',
29
+ 'feedback', 'traceability-capture', 'traceability-ingest', 'finalize-generated-tests',
30
+ ]);
19
31
  async function main() {
20
32
  const args = parseArgs(process.argv.slice(2));
21
33
  const autoConfig = resolveAutoConfig(args);
34
+ // Auto-detect defaults for commands that need them (when no config file found)
35
+ if (args.command && NEEDS_DEFAULTS_COMMANDS.has(args.command) && !SKIP_DEFAULTS_COMMANDS.has(args.command)) {
36
+ const defaults = resolveDefaults({
37
+ path: args.path,
38
+ testsRoot: args.testsRoot,
39
+ framework: args.framework,
40
+ gitSince: args.gitSince,
41
+ });
42
+ args.path = args.path || defaults.path;
43
+ args.testsRoot = args.testsRoot || defaults.testsRoot;
44
+ args.framework = args.framework || defaults.framework;
45
+ args.gitSince = args.gitSince || defaults.since;
46
+ }
22
47
  if (args.command === 'init') {
23
48
  const hasYes = process.argv.includes('--yes') || process.argv.includes('-y');
24
49
  await runInitCommand(hasYes);
25
50
  return;
26
51
  }
52
+ if (args.command === 'bootstrap') {
53
+ await runBootstrapCommand(args);
54
+ return;
55
+ }
27
56
  if (args.command === 'train') {
28
57
  await runTrainCommand(args, autoConfig);
29
58
  return;
@@ -64,6 +93,14 @@ async function main() {
64
93
  await runCrewCommand(args, autoConfig);
65
94
  return;
66
95
  }
96
+ if (args.command === 'cost-report') {
97
+ runCostReportCommand(args);
98
+ return;
99
+ }
100
+ if (args.command === 'gate') {
101
+ await runGateCommand(args, autoConfig);
102
+ return;
103
+ }
67
104
  if (!args.path && !autoConfig) {
68
105
  console.error('Error: --path is required (or provide a config file with path set)');
69
106
  printUsage();
@@ -138,6 +175,14 @@ async function main() {
138
175
  process.exit(1);
139
176
  }
140
177
  main().catch((error) => {
141
- console.error(error instanceof Error ? error.message : String(error));
142
- process.exit(1);
178
+ const exitCode = classifyError(error);
179
+ const message = error instanceof Error ? error.message : String(error);
180
+ console.error(message);
181
+ if (exitCode === EXIT_CODES.BUDGET_EXCEEDED) {
182
+ console.error('Hint: Increase --budget or use --degraded-mode to skip AI features.');
183
+ }
184
+ else if (exitCode === EXIT_CODES.PROVIDER_UNAVAILABLE) {
185
+ console.error('Hint: Check API key or use --degraded-mode for deterministic analysis only.');
186
+ }
187
+ process.exit(exitCode);
143
188
  });
@@ -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
  }