@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
@@ -1,9 +1,9 @@
1
1
  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
2
  // See LICENSE.txt for license information.
3
- import { existsSync, writeFileSync, readFileSync } from 'fs';
4
- import { execFileSync } from 'child_process';
5
- import { join, resolve } from 'path';
3
+ import { existsSync, writeFileSync } from 'fs';
4
+ import { join } from 'path';
6
5
  import * as readline from 'readline';
6
+ import { detectFramework, detectTestsRoot, detectGitDefaultBranch } from '../defaults.js';
7
7
  const CONFIG_FILENAME = 'e2e-ai-agents.config.json';
8
8
  function createInterface() {
9
9
  return readline.createInterface({
@@ -19,61 +19,6 @@ function ask(rl, question, defaultValue) {
19
19
  });
20
20
  });
21
21
  }
22
- function detectFramework(appPath) {
23
- const resolvedPath = resolve(appPath);
24
- const pkgPath = join(resolvedPath, 'package.json');
25
- if (!existsSync(pkgPath)) {
26
- return 'auto';
27
- }
28
- try {
29
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
30
- const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
31
- if (allDeps['@playwright/test'] || allDeps.playwright) {
32
- return 'playwright';
33
- }
34
- if (allDeps.cypress) {
35
- return 'cypress';
36
- }
37
- if (allDeps['selenium-webdriver'] || allDeps.webdriverio) {
38
- return 'selenium';
39
- }
40
- }
41
- catch {
42
- // ignore
43
- }
44
- return 'auto';
45
- }
46
- function detectGitDefaultBranch(appPath) {
47
- try {
48
- const result = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
49
- cwd: resolve(appPath),
50
- encoding: 'utf-8',
51
- stdio: ['pipe', 'pipe', 'pipe'],
52
- }).trim();
53
- return `origin/${result}`;
54
- }
55
- catch {
56
- return 'origin/main';
57
- }
58
- }
59
- function detectTestsRoot(appPath) {
60
- const resolvedPath = resolve(appPath);
61
- const candidates = [
62
- 'e2e-tests/playwright',
63
- 'e2e-tests',
64
- 'e2e',
65
- 'tests/e2e',
66
- 'test/e2e',
67
- 'tests',
68
- 'test',
69
- ];
70
- for (const candidate of candidates) {
71
- if (existsSync(join(resolvedPath, candidate))) {
72
- return candidate;
73
- }
74
- }
75
- return undefined;
76
- }
77
22
  function buildConfig(answers) {
78
23
  const config = {
79
24
  path: answers.path,
@@ -88,11 +88,11 @@ export function buildCrewMarkdown(crew, plan) {
88
88
  const totalCases = crew.testDesigns.reduce((n, td) => n + td.testCases.length, 0);
89
89
  const gapFamilies = new Set((plan?.gapDetails ?? []).map((g) => g.id));
90
90
  const lines = [
91
- '### Crew Insights',
91
+ '### Crew Analysis — What to Test',
92
92
  '',
93
- `Workflow: \`${crew.workflow}\``,
94
- `Impacted flows: **${crew.summary.impactedFlows}**`,
95
- `Strategy entries: **${crew.summary.strategyEntries}**`,
93
+ `Crew analyzed the diff and recommends what to verify before merging.`,
94
+ '',
95
+ `Impacted flows: **${crew.summary.impactedFlows}** | Strategy entries: **${crew.summary.strategyEntries}**`,
96
96
  ];
97
97
  if (totalCases > 0) {
98
98
  const gapDesigns = gapFamilies.size > 0
@@ -177,37 +177,48 @@ export function buildCrewTestPlan(crew, plan) {
177
177
  const gapCases = gapDesigns.reduce((n, td) => n + td.testCases.length, 0);
178
178
  const coveredCases = coveredDesigns.reduce((n, td) => n + td.testCases.length, 0);
179
179
  const lines = [
180
- '# Crew Test Plan',
180
+ '# Crew Test Plan — What to Verify',
181
+ '',
182
+ '> **This is a test recommendation, not a test execution report.**',
183
+ '> Crew analyzed the code diff and identified what needs to be tested.',
184
+ '> Use this plan to guide manual QA or write automated E2E tests.',
181
185
  '',
182
- `> Auto-generated by e2e-agents crew (\`${crew.workflow}\` workflow)`,
186
+ `_Auto-generated by e2e-agents crew (\`${crew.workflow}\` workflow)_`,
183
187
  '',
184
188
  '## Summary',
185
189
  '',
186
190
  `| Metric | Count |`,
187
191
  `|--------|-------|`,
188
- `| Gap flows (missing tests) | ${gapStrategies.length} flows${hasTestDesigns ? `, **${gapCases} test cases**` : ''} |`,
189
- `| Covered flows (expansion) | ${coveredStrategies.length} flows${hasTestDesigns ? `, ${coveredCases} test cases` : ''} |`,
190
- `| Total strategy entries | ${crew.strategyEntries.length} flows${hasTestDesigns ? `, ${totalCases} test cases` : ''} |`,
192
+ `| Gap flows **no existing tests, must verify** | ${gapStrategies.length} flows${hasTestDesigns ? `, **${gapCases} test cases**` : ''} |`,
193
+ `| Covered flows **has tests, verify no regressions** | ${coveredStrategies.length} flows${hasTestDesigns ? `, ${coveredCases} test cases` : ''} |`,
194
+ `| Total | ${crew.strategyEntries.length} flows${hasTestDesigns ? `, ${totalCases} test cases` : ''} |`,
191
195
  `| High-risk cross-impacts | ${crew.summary.highRiskCrossImpacts} |`,
192
196
  `| AI cost | $${crew.summary.totalCostUSD.toFixed(4)} |`,
193
197
  '',
194
198
  ];
195
199
  // ── Gap flows ──
196
200
  if (gapStrategies.length > 0) {
197
- lines.push('## Priority: Gap Flows (Missing Tests)');
201
+ lines.push('## Action Required: Gap Flows (No Existing Tests)');
198
202
  lines.push('');
199
- lines.push('These flows have **no existing E2E coverage** and should be addressed first.');
203
+ lines.push('These flows have **no automated E2E coverage**. Before merging, either:');
204
+ lines.push('1. **Write E2E tests** for the critical scenarios below, or');
205
+ lines.push('2. **Manually verify** each flow works as expected');
200
206
  lines.push('');
201
207
  for (const strategy of gapStrategies) {
202
208
  const td = crew.testDesigns.find((d) => d.flowId === strategy.flowId);
203
209
  lines.push(`### ${strategy.flowName}`);
204
210
  lines.push('');
205
- lines.push(`Strategy: **${strategy.approach}** | Priority: **${strategy.priority}** | Cross-impact risk: **${strategy.crossImpactRisk}**`);
206
- if (strategy.rationale) {
207
- lines.push(`> ${strategy.rationale}`);
211
+ const actionVerb = strategy.approach === 'full-test' ? 'Write full E2E test or verify manually'
212
+ : strategy.approach === 'smoke-test' ? 'Smoke-test manually or add basic E2E coverage'
213
+ : strategy.approach === 'manual-review' ? 'Manual review required'
214
+ : 'Can skip — low risk';
215
+ lines.push(`**${strategy.priority}** | Recommended: **${strategy.approach}** | Cross-impact risk: **${strategy.crossImpactRisk}**`);
216
+ lines.push(`> ${actionVerb}`);
217
+ if (strategy.rationale && !strategy.rationale.includes('Default strategy')) {
218
+ lines.push(`> Rationale: ${strategy.rationale}`);
208
219
  }
209
220
  if (strategy.testCategories.length > 0) {
210
- lines.push(`Test categories: ${strategy.testCategories.join(', ')}`);
221
+ lines.push(`> Test types to cover: ${strategy.testCategories.join(', ')}`);
211
222
  }
212
223
  lines.push('');
213
224
  // If test designs exist, show P0/P1 cases
@@ -242,9 +253,10 @@ export function buildCrewTestPlan(crew, plan) {
242
253
  }
243
254
  // ── Covered flows ──
244
255
  if (coveredStrategies.length > 0) {
245
- lines.push('## Covered Flows (Regression / Expansion)');
256
+ lines.push('## Regression Check: Covered Flows (Already Have Tests)');
246
257
  lines.push('');
247
- lines.push('These flows already have specs. Verify changes haven\'t introduced regressions.');
258
+ lines.push('These flows have existing E2E specs. The existing tests should catch regressions automatically.');
259
+ lines.push('**No manual action required** unless CI tests fail on these flows.');
248
260
  lines.push('');
249
261
  for (const strategy of coveredStrategies) {
250
262
  const td = crew.testDesigns.find((d) => d.flowId === strategy.flowId);
@@ -252,8 +264,8 @@ export function buildCrewTestPlan(crew, plan) {
252
264
  const detail = caseCount > 0 ? ` | ${caseCount} cases` : '';
253
265
  lines.push(`<details><summary><strong>${strategy.flowName}</strong> — ${strategy.approach}${detail} (${strategy.priority})</summary>`);
254
266
  lines.push('');
255
- lines.push(`Cross-impact risk: ${strategy.crossImpactRisk}`);
256
- if (strategy.rationale) {
267
+ lines.push(`Existing tests should cover this. Cross-impact risk: ${strategy.crossImpactRisk}`);
268
+ if (strategy.rationale && !strategy.rationale.includes('Default strategy')) {
257
269
  lines.push(`> ${strategy.rationale}`);
258
270
  }
259
271
  if (strategy.testCategories.length > 0) {
@@ -273,9 +285,9 @@ export function buildCrewTestPlan(crew, plan) {
273
285
  // ── Cross-impacts ──
274
286
  const highRisk = crew.crossImpacts.filter((ci) => ci.riskLevel === 'high');
275
287
  if (highRisk.length > 0) {
276
- lines.push('## High-Risk Cross-Impacts');
288
+ lines.push('## High-Risk Cross-Impacts — Verify Before Release');
277
289
  lines.push('');
278
- lines.push('These cross-family dependencies should be verified during release testing:');
290
+ lines.push('Changes in one area may break these related areas. Manually verify or ensure E2E tests cover both sides:');
279
291
  lines.push('');
280
292
  for (const ci of highRisk) {
281
293
  lines.push(`- **${ci.sourceFamily}** → **${ci.affectedFamily}**: ${ci.sharedDependency}`);
@@ -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
  });