@yasserkhanorg/e2e-agents 1.8.1 → 1.8.3

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.
@@ -1,9 +1,10 @@
1
1
  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
2
  // See LICENSE.txt for license information.
3
- import { appendFileSync } from 'fs';
3
+ import { appendFileSync, writeFileSync } from 'fs';
4
4
  import { join } from 'path';
5
5
  import { writeCiSummary } from '../../engine/plan_builder.js';
6
6
  import { recommendTestsAI, recommendTestsDeterministic } from '../../api.js';
7
+ import { appendCrewToSummary, runPlanCrewAnalysis, writeCrewArtifacts } from './plan_crew.js';
7
8
  export async function runPlanCommand(args, autoConfig, config) {
8
9
  const reportRoot = config.testsRoot || config.path;
9
10
  const apiOptions = {
@@ -40,41 +41,77 @@ export async function runPlanCommand(args, autoConfig, config) {
40
41
  const { aiEnrichment } = result;
41
42
  console.log(`AI enrichment: ${aiEnrichment.enrichedFeatures.length} features enriched (${aiEnrichment.tokenUsage.input + aiEnrichment.tokenUsage.output} tokens)`);
42
43
  }
43
- else if (!process.env.ANTHROPIC_API_KEY) {
44
- console.log('Tip: set ANTHROPIC_API_KEY to enable AI-powered enrichment');
44
+ else if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY && !process.env.LLM_PROVIDER) {
45
+ console.log('Tip: configure ANTHROPIC_API_KEY, OPENAI_API_KEY, or LLM_PROVIDER to enable AI-powered enrichment');
45
46
  }
46
47
  }
47
48
  const { plan, planPath, ciSummaryMarkdown, ciSummaryPath } = result;
48
- // Write CI summary to an additional path if --ci-comment-path was specified
49
- if (args.ciCommentPath) {
50
- writeCiSummary(reportRoot, ciSummaryMarkdown, args.ciCommentPath);
49
+ let planReport = plan;
50
+ let combinedSummaryMarkdown = ciSummaryMarkdown;
51
+ let crewSummaryPath = '';
52
+ let crewMarkdownPath = '';
53
+ let crewTestPlanPath = '';
54
+ if (args.crew) {
55
+ try {
56
+ const crew = await runPlanCrewAnalysis(plan, config, args);
57
+ planReport = {
58
+ ...plan,
59
+ crew,
60
+ };
61
+ combinedSummaryMarkdown = appendCrewToSummary(ciSummaryMarkdown, crew, plan);
62
+ const artifacts = writeCrewArtifacts(reportRoot, crew, plan);
63
+ crewSummaryPath = artifacts.crewSummaryPath;
64
+ crewMarkdownPath = artifacts.crewMarkdownPath;
65
+ crewTestPlanPath = artifacts.crewTestPlanPath;
66
+ writeFileSync(planPath, JSON.stringify(planReport, null, 2), 'utf-8');
67
+ }
68
+ catch (error) {
69
+ const message = error instanceof Error ? error.message : String(error);
70
+ console.warn(`Crew analysis unavailable: ${message}`);
71
+ }
51
72
  }
52
- const summaryPath = ciSummaryPath;
73
+ writeCiSummary(reportRoot, combinedSummaryMarkdown);
74
+ const summaryPath = args.ciCommentPath
75
+ ? writeCiSummary(reportRoot, combinedSummaryMarkdown, args.ciCommentPath)
76
+ : ciSummaryPath;
53
77
  // Compute metrics paths (api already wrote metrics; derive paths for GHA output)
54
78
  const metricsEventsPath = join(reportRoot, '.e2e-ai-agents/metrics.jsonl');
55
79
  const metricsSummaryPath = join(reportRoot, '.e2e-ai-agents/metrics-summary.json');
56
80
  const ghaOutput = args.githubOutputPath || process.env.GITHUB_OUTPUT;
57
81
  if (ghaOutput) {
58
- appendFileSync(ghaOutput, `run_set=${plan.runSet}\n`);
59
- appendFileSync(ghaOutput, `action=${plan.decision.action}\n`);
60
- appendFileSync(ghaOutput, `confidence=${plan.confidence}\n`);
61
- appendFileSync(ghaOutput, `enforcement_mode=${plan.enforcement.mode}\n`);
62
- appendFileSync(ghaOutput, `enforcement_should_fail=${plan.enforcement.shouldFail}\n`);
63
- appendFileSync(ghaOutput, `recommended_tests_count=${plan.recommendedTests.length}\n`);
64
- appendFileSync(ghaOutput, `required_new_tests_count=${plan.requiredNewTests.length}\n`);
82
+ appendFileSync(ghaOutput, `run_set=${planReport.runSet}\n`);
83
+ appendFileSync(ghaOutput, `action=${planReport.decision.action}\n`);
84
+ appendFileSync(ghaOutput, `confidence=${planReport.confidence}\n`);
85
+ appendFileSync(ghaOutput, `enforcement_mode=${planReport.enforcement.mode}\n`);
86
+ appendFileSync(ghaOutput, `enforcement_should_fail=${planReport.enforcement.shouldFail}\n`);
87
+ appendFileSync(ghaOutput, `recommended_tests_count=${planReport.recommendedTests.length}\n`);
88
+ appendFileSync(ghaOutput, `required_new_tests_count=${planReport.requiredNewTests.length}\n`);
65
89
  appendFileSync(ghaOutput, `plan_path=${planPath}\n`);
66
90
  appendFileSync(ghaOutput, `summary_path=${summaryPath}\n`);
67
91
  appendFileSync(ghaOutput, `metrics_events_path=${metricsEventsPath}\n`);
68
92
  appendFileSync(ghaOutput, `metrics_summary_path=${metricsSummaryPath}\n`);
93
+ appendFileSync(ghaOutput, `crew_enabled=${planReport.crew ? 'true' : 'false'}\n`);
94
+ appendFileSync(ghaOutput, `crew_workflow=${planReport.crew?.workflow || ''}\n`);
95
+ appendFileSync(ghaOutput, `crew_summary_path=${crewSummaryPath}\n`);
96
+ appendFileSync(ghaOutput, `crew_markdown_path=${crewMarkdownPath}\n`);
97
+ appendFileSync(ghaOutput, `crew_test_plan_path=${crewTestPlanPath}\n`);
98
+ appendFileSync(ghaOutput, `crew_impacted_flows=${planReport.crew?.summary.impactedFlows || 0}\n`);
99
+ appendFileSync(ghaOutput, `crew_strategy_entries=${planReport.crew?.summary.strategyEntries || 0}\n`);
100
+ appendFileSync(ghaOutput, `crew_test_designs=${planReport.crew?.summary.testDesigns || 0}\n`);
69
101
  }
70
- console.log(`Suggested run set: ${plan.runSet} (confidence ${plan.confidence})`);
71
- console.log(`Decision: ${plan.decision.action} - ${plan.decision.summary}`);
72
- console.log(`Enforcement: ${plan.enforcement.mode} (shouldFail=${plan.enforcement.shouldFail})`);
102
+ console.log(`Suggested run set: ${planReport.runSet} (confidence ${planReport.confidence})`);
103
+ console.log(`Decision: ${planReport.decision.action} - ${planReport.decision.summary}`);
104
+ console.log(`Enforcement: ${planReport.enforcement.mode} (shouldFail=${planReport.enforcement.shouldFail})`);
73
105
  console.log(`Plan data: ${planPath}`);
74
106
  console.log(`CI summary: ${summaryPath}`);
107
+ if (planReport.crew) {
108
+ console.log(`Crew workflow: ${planReport.crew.workflow} (impactedFlows=${planReport.crew.summary.impactedFlows}, strategyEntries=${planReport.crew.summary.strategyEntries}, testDesigns=${planReport.crew.summary.testDesigns})`);
109
+ console.log(`Crew summary: ${crewSummaryPath}`);
110
+ console.log(`Crew test plan: ${crewTestPlanPath}`);
111
+ }
75
112
  console.log(`Plan metrics: ${metricsSummaryPath}`);
76
- const failOnLegacyFlag = args.failOnMustAddTests && plan.decision.action === 'must-add-tests';
77
- if (failOnLegacyFlag || plan.enforcement.shouldFail) {
113
+ const failOnLegacyFlag = args.failOnMustAddTests && planReport.decision.action === 'must-add-tests';
114
+ if (failOnLegacyFlag || planReport.enforcement.shouldFail) {
78
115
  process.exit(2);
79
116
  }
80
117
  }
@@ -0,0 +1,266 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { mkdirSync, writeFileSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { CrossImpactAgent } from '../../agents/cross-impact.js';
6
+ import { ImpactAnalystAgent } from '../../agents/impact-analyst.js';
7
+ import { RegressionAdvisorAgent } from '../../agents/regression-advisor.js';
8
+ import { StrategistAgent } from '../../agents/strategist.js';
9
+ import { TestDesignerAgent } from '../../agents/test-designer.js';
10
+ import { CrewOrchestrator } from '../../crew/orchestrator.js';
11
+ const VALID_WORKFLOWS = new Set(['full-qa', 'quick-check', 'design-only']);
12
+ function uniqueStrings(values) {
13
+ return Array.from(new Set(values.filter(Boolean)));
14
+ }
15
+ function singleLine(value) {
16
+ return value.replace(/\s+/g, ' ').trim();
17
+ }
18
+ function chooseCrewWorkflow(explicitWorkflow, plan) {
19
+ if (explicitWorkflow && VALID_WORKFLOWS.has(explicitWorkflow)) {
20
+ return explicitWorkflow;
21
+ }
22
+ if (plan.decision.action === 'must-add-tests' || plan.metrics.uncoveredP0P1Flows > 0) {
23
+ return 'design-only';
24
+ }
25
+ return 'quick-check';
26
+ }
27
+ function registerCrewAgents(orchestrator) {
28
+ orchestrator.registerAgent(new ImpactAnalystAgent());
29
+ orchestrator.registerAgent(new StrategistAgent());
30
+ orchestrator.registerAgent(new TestDesignerAgent());
31
+ orchestrator.registerAgent(new CrossImpactAgent());
32
+ orchestrator.registerAgent(new RegressionAdvisorAgent());
33
+ }
34
+ export async function runPlanCrewAnalysis(plan, config, args) {
35
+ const reportRoot = config.testsRoot || config.path;
36
+ const workflow = chooseCrewWorkflow(args.crewWorkflow, plan);
37
+ const normalizedProvider = config.llm.provider?.trim().toLowerCase();
38
+ const providerOverride = normalizedProvider && normalizedProvider !== 'auto' ? normalizedProvider : 'auto';
39
+ const orchestrator = new CrewOrchestrator();
40
+ registerCrewAgents(orchestrator);
41
+ const result = await orchestrator.run({
42
+ appPath: config.path,
43
+ testsRoot: reportRoot,
44
+ gitSince: args.gitSince || config.git.since,
45
+ routeFamilies: config.routeFamilies,
46
+ apiSurface: config.apiSurface,
47
+ workflow,
48
+ providerOverride: providerOverride === 'auto' ? undefined : providerOverride,
49
+ budgetUSD: args.budgetUSD,
50
+ dryRun: args.dryRun,
51
+ });
52
+ const ctx = result.context;
53
+ const highRiskCrossImpacts = ctx.crossImpacts.filter((entry) => entry.riskLevel === 'high');
54
+ const manualReviewEntries = ctx.strategyEntries.filter((entry) => entry.approach === 'manual-review');
55
+ return {
56
+ workflow,
57
+ providerOverride,
58
+ summary: {
59
+ impactedFlows: ctx.impactedFlows.length,
60
+ strategyEntries: ctx.strategyEntries.length,
61
+ testDesigns: ctx.testDesigns.length,
62
+ crossImpacts: ctx.crossImpacts.length,
63
+ highRiskCrossImpacts: highRiskCrossImpacts.length,
64
+ regressionRisks: ctx.regressionRisks.length,
65
+ findings: ctx.findings.length,
66
+ generatedSpecs: ctx.generatedSpecs.length,
67
+ manualReviewEntries: manualReviewEntries.length,
68
+ totalCostUSD: Number(ctx.usage.totalCost.toFixed(4)),
69
+ totalTokens: ctx.usage.totalTokens,
70
+ },
71
+ impactedFlows: ctx.impactedFlows,
72
+ strategyEntries: ctx.strategyEntries,
73
+ testDesigns: ctx.testDesigns,
74
+ crossImpacts: ctx.crossImpacts,
75
+ regressionRisks: ctx.regressionRisks,
76
+ findings: ctx.findings,
77
+ warnings: uniqueStrings([...ctx.warnings, ...result.warnings]),
78
+ timings: result.timings,
79
+ };
80
+ }
81
+ export function buildCrewMarkdown(crew, plan) {
82
+ const totalCases = crew.testDesigns.reduce((n, td) => n + td.testCases.length, 0);
83
+ const gapFamilies = new Set((plan?.gapDetails ?? []).map((g) => g.id));
84
+ const gapDesigns = gapFamilies.size > 0
85
+ ? crew.testDesigns.filter((td) => Array.from(gapFamilies).some((fam) => td.flowId.startsWith(fam) || td.flowName.toLowerCase().includes(fam.replace(/_/g, ' '))))
86
+ : [];
87
+ const gapCases = gapDesigns.reduce((n, td) => n + td.testCases.length, 0);
88
+ const gapP0Cases = gapDesigns.reduce((n, td) => n + td.testCases.filter((tc) => tc.priority === 'P0').length, 0);
89
+ const lines = [
90
+ '### Crew Insights',
91
+ '',
92
+ `Workflow: \`${crew.workflow}\``,
93
+ `Impacted flows: **${crew.summary.impactedFlows}**`,
94
+ `Structured test designs: **${crew.summary.testDesigns}** flows, **${totalCases}** test cases`,
95
+ ];
96
+ if (gapFamilies.size > 0 && gapDesigns.length > 0) {
97
+ lines.push(`Gap-focused: **${gapDesigns.length}** flows, **${gapCases}** test cases (**${gapP0Cases}** P0)`);
98
+ }
99
+ lines.push(`Cross-impacts: **${crew.summary.crossImpacts}** (${crew.summary.highRiskCrossImpacts} high risk)`, `Estimated AI cost: **$${crew.summary.totalCostUSD.toFixed(4)}**`);
100
+ if (crew.strategyEntries.length > 0) {
101
+ lines.push('');
102
+ lines.push('Top Strategy Recommendations:');
103
+ for (const entry of crew.strategyEntries.slice(0, 5)) {
104
+ lines.push(`- ${entry.priority} ${entry.flowName} -> ${entry.approach} (${entry.crossImpactRisk} cross-impact risk)`);
105
+ }
106
+ }
107
+ if (crew.testDesigns.length > 0) {
108
+ lines.push('');
109
+ lines.push('Structured Test Designs:');
110
+ for (const design of crew.testDesigns.slice(0, 3)) {
111
+ lines.push(`- ${design.flowName}: ${design.testCases.length} designed test case(s)`);
112
+ }
113
+ }
114
+ const riskyCrossImpacts = crew.crossImpacts.filter((entry) => entry.riskLevel === 'high');
115
+ if (riskyCrossImpacts.length > 0) {
116
+ lines.push('');
117
+ lines.push('High-Risk Cross-Impacts:');
118
+ for (const entry of riskyCrossImpacts.slice(0, 5)) {
119
+ lines.push(`- ${entry.sourceFamily} -> ${entry.affectedFamily}: ${entry.sharedDependency}`);
120
+ }
121
+ }
122
+ if (crew.findings.length > 0) {
123
+ lines.push('');
124
+ lines.push('Crew Findings:');
125
+ for (const finding of crew.findings.slice(0, 5)) {
126
+ lines.push(`- ${finding.severity} ${finding.type}: ${finding.summary}`);
127
+ }
128
+ }
129
+ if (crew.warnings.length > 0) {
130
+ lines.push('');
131
+ lines.push('Crew Warnings:');
132
+ for (const warning of crew.warnings.slice(0, 5)) {
133
+ lines.push(`- ${singleLine(warning)}`);
134
+ }
135
+ }
136
+ return lines.join('\n');
137
+ }
138
+ export function appendCrewToSummary(baseMarkdown, crew, plan) {
139
+ return `${baseMarkdown}\n\n---\n\n${buildCrewMarkdown(crew, plan)}`;
140
+ }
141
+ /**
142
+ * Build a full structured test plan as a Markdown document.
143
+ * Sections: gap flows first (P0 cases expanded), then covered-flow smoke tests.
144
+ */
145
+ export function buildCrewTestPlan(crew, plan) {
146
+ const gapFamilies = new Set((plan?.gapDetails ?? []).map((g) => g.id));
147
+ const totalCases = crew.testDesigns.reduce((n, td) => n + td.testCases.length, 0);
148
+ // Split designs into gap-related and coverage-expansion
149
+ const gapDesigns = [];
150
+ const coveredDesigns = [];
151
+ for (const td of crew.testDesigns) {
152
+ // Match by flowId prefix against gap family ids
153
+ const isGap = Array.from(gapFamilies).some((fam) => td.flowId.startsWith(fam) || td.flowName.toLowerCase().includes(fam.replace(/_/g, ' ')));
154
+ if (isGap) {
155
+ gapDesigns.push(td);
156
+ }
157
+ else {
158
+ coveredDesigns.push(td);
159
+ }
160
+ }
161
+ const gapCases = gapDesigns.reduce((n, td) => n + td.testCases.length, 0);
162
+ const coveredCases = coveredDesigns.reduce((n, td) => n + td.testCases.length, 0);
163
+ const lines = [
164
+ '# Crew Test Plan',
165
+ '',
166
+ `> Auto-generated by e2e-agents crew (\`${crew.workflow}\` workflow)`,
167
+ '',
168
+ '## Summary',
169
+ '',
170
+ `| Metric | Count |`,
171
+ `|--------|-------|`,
172
+ `| Gap flows (missing tests) | ${gapDesigns.length} flows, **${gapCases} test cases** |`,
173
+ `| Covered flows (expansion) | ${coveredDesigns.length} flows, ${coveredCases} test cases |`,
174
+ `| Total | ${crew.testDesigns.length} flows, ${totalCases} test cases |`,
175
+ `| High-risk cross-impacts | ${crew.summary.highRiskCrossImpacts} |`,
176
+ `| AI cost | $${crew.summary.totalCostUSD.toFixed(4)} |`,
177
+ '',
178
+ ];
179
+ // Priority action items
180
+ if (gapDesigns.length > 0) {
181
+ lines.push('## Priority: Gap Flows (Missing Tests)');
182
+ lines.push('');
183
+ lines.push('These flows have **no existing E2E coverage** and should be addressed first.');
184
+ lines.push('');
185
+ for (const td of gapDesigns) {
186
+ const strategy = crew.strategyEntries.find((s) => s.flowId === td.flowId);
187
+ const approach = strategy?.approach ?? 'full-test';
188
+ const risk = strategy?.crossImpactRisk ?? 'unknown';
189
+ const p0Cases = td.testCases.filter((tc) => tc.priority === 'P0');
190
+ const p1Cases = td.testCases.filter((tc) => tc.priority === 'P1');
191
+ lines.push(`### ${td.flowName}`);
192
+ lines.push('');
193
+ lines.push(`Strategy: **${approach}** | Cross-impact risk: **${risk}** | ${td.testCases.length} cases (${p0Cases.length} P0, ${p1Cases.length} P1)`);
194
+ lines.push('');
195
+ // Show P0 cases expanded
196
+ if (p0Cases.length > 0) {
197
+ lines.push('**P0 — Must test:**');
198
+ lines.push('');
199
+ for (const tc of p0Cases) {
200
+ lines.push(`- [ ] **${tc.name}** (${tc.type})`);
201
+ if (tc.preconditions.length > 0) {
202
+ lines.push(` - Preconditions: ${tc.preconditions.join('; ')}`);
203
+ }
204
+ lines.push(` - Steps: ${tc.steps.join(' → ')}`);
205
+ lines.push(` - Expected: ${tc.expectedOutcome}`);
206
+ }
207
+ lines.push('');
208
+ }
209
+ // Show P1 as a collapsed checklist
210
+ if (p1Cases.length > 0) {
211
+ lines.push(`<details><summary>P1 — Should test (${p1Cases.length})</summary>`);
212
+ lines.push('');
213
+ for (const tc of p1Cases) {
214
+ lines.push(`- [ ] **${tc.name}** (${tc.type}) — ${tc.expectedOutcome}`);
215
+ }
216
+ lines.push('');
217
+ lines.push('</details>');
218
+ lines.push('');
219
+ }
220
+ }
221
+ }
222
+ // Covered flow expansion — collapsed by default
223
+ if (coveredDesigns.length > 0) {
224
+ lines.push('## Covered Flows (Regression / Expansion)');
225
+ lines.push('');
226
+ lines.push('These flows already have specs. The test cases below extend coverage for changes in this PR.');
227
+ lines.push('');
228
+ for (const td of coveredDesigns) {
229
+ const strategy = crew.strategyEntries.find((s) => s.flowId === td.flowId);
230
+ const approach = strategy?.approach ?? 'smoke-test';
231
+ const p0Count = td.testCases.filter((tc) => tc.priority === 'P0').length;
232
+ lines.push(`<details><summary><strong>${td.flowName}</strong> — ${approach} | ${td.testCases.length} cases (${p0Count} P0)</summary>`);
233
+ lines.push('');
234
+ for (const tc of td.testCases) {
235
+ lines.push(`- [ ] **${tc.name}** (${tc.priority}, ${tc.type}) — ${tc.expectedOutcome}`);
236
+ }
237
+ lines.push('');
238
+ lines.push('</details>');
239
+ lines.push('');
240
+ }
241
+ }
242
+ // Cross-impacts section
243
+ const highRisk = crew.crossImpacts.filter((ci) => ci.riskLevel === 'high');
244
+ if (highRisk.length > 0) {
245
+ lines.push('## High-Risk Cross-Impacts');
246
+ lines.push('');
247
+ lines.push('These cross-family dependencies should be verified during release testing:');
248
+ lines.push('');
249
+ for (const ci of highRisk) {
250
+ lines.push(`- **${ci.sourceFamily}** → **${ci.affectedFamily}**: ${ci.sharedDependency}`);
251
+ }
252
+ lines.push('');
253
+ }
254
+ return lines.join('\n');
255
+ }
256
+ export function writeCrewArtifacts(reportRoot, crew, plan) {
257
+ const outputDir = join(reportRoot, '.e2e-ai-agents');
258
+ mkdirSync(outputDir, { recursive: true });
259
+ const crewSummaryPath = join(outputDir, 'crew-summary.json');
260
+ const crewMarkdownPath = join(outputDir, 'crew-summary.md');
261
+ const crewTestPlanPath = join(outputDir, 'crew-test-plan.md');
262
+ writeFileSync(crewSummaryPath, JSON.stringify(crew, null, 2), 'utf-8');
263
+ writeFileSync(crewMarkdownPath, buildCrewMarkdown(crew, plan), 'utf-8');
264
+ writeFileSync(crewTestPlanPath, buildCrewTestPlan(crew, plan), 'utf-8');
265
+ return { crewSummaryPath, crewMarkdownPath, crewTestPlanPath };
266
+ }
@@ -56,6 +56,7 @@ const FLAGS = {
56
56
  '--pipeline-parallel': { key: 'pipelineParallel', type: 'boolean' },
57
57
  '--pipeline-dry-run': { key: 'pipelineDryRun', type: 'boolean' },
58
58
  '--fail-on-must-add-tests': { key: 'failOnMustAddTests', type: 'boolean' },
59
+ '--crew': { key: 'crew', type: 'boolean' },
59
60
  '--create-pr': { key: 'createPr', type: 'boolean' },
60
61
  '--dry-run': { key: 'dryRun', type: 'boolean' },
61
62
  '--generate': { key: 'analyzeGenerate', type: 'boolean' },
@@ -99,6 +100,7 @@ const FLAGS = {
99
100
  '--output': { key: 'trainOutput', type: 'string' },
100
101
  '--server-path': { key: 'serverPath', type: 'string' },
101
102
  '--workflow': { key: 'crewWorkflow', type: 'string' },
103
+ '--crew-workflow': { key: 'crewWorkflow', type: 'string' },
102
104
  // -- number flags (with isFinite guard) --
103
105
  '--pipeline-scenarios': { key: 'pipelineScenarios', type: 'number' },
104
106
  '--time': { key: 'timeLimitMinutes', type: 'number' },
@@ -56,6 +56,8 @@ export function printUsage() {
56
56
  ' --policy-risky-patterns <globs> Comma-separated risky file globs',
57
57
  ' --policy-enforcement-mode <mode> advisory | warn | block',
58
58
  ' --policy-block-actions <actions> Comma-separated CI actions to block/warn',
59
+ ' --crew Run Crew enrichment and attach insights to plan output',
60
+ ' --crew-workflow <name> full-qa | quick-check | design-only',
59
61
  ' --ci-comment-path <path> Write CI markdown summary',
60
62
  ' --github-output <path> Write GitHub Actions outputs',
61
63
  ' --fail-on-must-add-tests Exit non-zero on must-add-tests decision',
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
3
  // See LICENSE.txt for license information.
3
4
  /**
@@ -6,7 +7,7 @@
6
7
  */
7
8
  import { spawnSync } from 'child_process';
8
9
  import { readFileSync, writeFileSync, existsSync, realpathSync } from 'fs';
9
- import { resolve } from 'path';
10
+ import { join, resolve, dirname } from 'path';
10
11
  import { globSync } from 'glob';
11
12
  /**
12
13
  * SECURITY: Path validation helper
@@ -471,12 +472,152 @@ export class E2EAgentsMCPServer {
471
472
  }
472
473
  }
473
474
  /**
474
- * Start MCP server
475
- * Usage: node dist/mcp-server.js
475
+ * Read the package version at runtime so the MCP initialize response
476
+ * always reflects the installed version.
476
477
  */
478
+ function getPackageVersion() {
479
+ try {
480
+ const pkgPath = join(dirname(__dirname), 'package.json');
481
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
482
+ return pkg.version || '0.0.0';
483
+ }
484
+ catch {
485
+ return '0.0.0';
486
+ }
487
+ }
488
+ /**
489
+ * Encode a JSON-RPC message with Content-Length framing.
490
+ * Exported for testability.
491
+ */
492
+ export function encodeJsonRpcMessage(message) {
493
+ const body = JSON.stringify(message);
494
+ return `Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n${body}`;
495
+ }
496
+ /**
497
+ * Parse Content-Length framed JSON-RPC messages from a buffer.
498
+ * Returns parsed messages and the remaining (unconsumed) buffer.
499
+ * Exported for testability.
500
+ */
501
+ export function parseJsonRpcFrames(input) {
502
+ const messages = [];
503
+ let buffer = Buffer.from(input);
504
+ while (true) {
505
+ const headerEnd = buffer.indexOf('\r\n\r\n');
506
+ if (headerEnd === -1)
507
+ break;
508
+ const headerText = buffer.slice(0, headerEnd).toString('utf8');
509
+ const match = headerText.match(/Content-Length:\s*(\d+)/i);
510
+ if (!match) {
511
+ buffer = Buffer.alloc(0);
512
+ break;
513
+ }
514
+ const contentLength = Number(match[1]);
515
+ const messageEnd = headerEnd + 4 + contentLength;
516
+ if (buffer.length < messageEnd)
517
+ break;
518
+ const body = buffer.slice(headerEnd + 4, messageEnd).toString('utf8');
519
+ buffer = buffer.slice(messageEnd);
520
+ messages.push(JSON.parse(body));
521
+ }
522
+ return { messages, remainder: buffer };
523
+ }
524
+ /**
525
+ * Handle a single JSON-RPC message against the server.
526
+ * Returns the response message (or null for notifications).
527
+ * Exported for testability.
528
+ */
529
+ export async function handleJsonRpcMessage(server, message) {
530
+ const { id, method, params } = message;
531
+ const version = getPackageVersion();
532
+ if (method === 'initialize') {
533
+ return {
534
+ jsonrpc: '2.0',
535
+ id,
536
+ result: {
537
+ protocolVersion: typeof params?.protocolVersion === 'string' ? params.protocolVersion : '2024-11-05',
538
+ capabilities: { tools: {}, resources: {}, prompts: {} },
539
+ serverInfo: { name: 'e2e-agents-mcp', version },
540
+ },
541
+ };
542
+ }
543
+ if (method === 'notifications/initialized' || method === 'initialized') {
544
+ return null;
545
+ }
546
+ if (method === 'tools/list') {
547
+ return {
548
+ jsonrpc: '2.0',
549
+ id,
550
+ result: {
551
+ tools: server.getTools().map((tool) => ({
552
+ name: tool.name,
553
+ description: tool.description,
554
+ inputSchema: tool.inputSchema,
555
+ })),
556
+ },
557
+ };
558
+ }
559
+ if (method === 'tools/call') {
560
+ const resultText = await server.callTool(typeof params?.name === 'string' ? params.name : '', typeof params?.arguments === 'object' && params.arguments !== null ? params.arguments : {});
561
+ let isError = false;
562
+ try {
563
+ const parsed = JSON.parse(resultText);
564
+ isError = Boolean(parsed.error);
565
+ }
566
+ catch {
567
+ isError = false;
568
+ }
569
+ return {
570
+ jsonrpc: '2.0',
571
+ id,
572
+ result: { content: [{ type: 'text', text: resultText }], isError },
573
+ };
574
+ }
575
+ if (method === 'resources/list') {
576
+ return { jsonrpc: '2.0', id, result: { resources: [] } };
577
+ }
578
+ if (method === 'prompts/list') {
579
+ return { jsonrpc: '2.0', id, result: { prompts: [] } };
580
+ }
581
+ if (method === 'ping') {
582
+ return { jsonrpc: '2.0', id, result: {} };
583
+ }
584
+ return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } };
585
+ }
586
+ /**
587
+ * Start MCP server over stdio using Content-Length framed JSON-RPC messages.
588
+ */
589
+ export function startStdioServer(repoRoot = process.cwd()) {
590
+ const server = new E2EAgentsMCPServer(repoRoot);
591
+ let buffer = Buffer.alloc(0);
592
+ const sendMessage = (message) => {
593
+ process.stdout.write(encodeJsonRpcMessage(message));
594
+ };
595
+ const sendError = (id, code, msg) => {
596
+ sendMessage({ jsonrpc: '2.0', id, error: { code, message: msg } });
597
+ };
598
+ const processBuffer = () => {
599
+ const { messages, remainder } = parseJsonRpcFrames(buffer);
600
+ buffer = remainder;
601
+ for (const parsed of messages) {
602
+ void handleJsonRpcMessage(server, parsed)
603
+ .then((response) => {
604
+ if (response)
605
+ sendMessage(response);
606
+ })
607
+ .catch((error) => {
608
+ sendError(parsed.id ?? null, -32603, error instanceof Error ? error.message : String(error));
609
+ });
610
+ }
611
+ };
612
+ process.stdin.on('data', (chunk) => {
613
+ buffer = Buffer.concat([buffer, chunk]);
614
+ processBuffer();
615
+ });
616
+ process.stdin.on('end', () => {
617
+ process.exit(0);
618
+ });
619
+ }
477
620
  if (require.main === module) {
478
- const server = new E2EAgentsMCPServer();
479
- console.log('E2E Agents MCP Server started');
480
- console.log('Tools:', server.getTools().map((t) => t.name).join(', '));
621
+ startStdioServer();
481
622
  }
482
623
  export default E2EAgentsMCPServer;
@@ -141,6 +141,17 @@ export class LLMProviderFactory {
141
141
  '3. Set OPENAI_API_KEY environment variable\n' +
142
142
  '4. Set LLM_PROVIDER environment variable');
143
143
  }
144
+ /**
145
+ * Create provider from an explicit preference when supplied, otherwise
146
+ * fall back to environment auto-detection.
147
+ */
148
+ static async createFromPreference(providerPreference) {
149
+ const normalized = providerPreference?.trim().toLowerCase();
150
+ if (!normalized || normalized === 'auto') {
151
+ return this.createFromEnv();
152
+ }
153
+ return this.createFromString(normalized);
154
+ }
144
155
  /**
145
156
  * Create provider from simple string format
146
157
  *
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  interface Tool {
2
3
  name: string;
3
4
  description: string;
@@ -31,5 +32,37 @@ export declare class E2EAgentsMCPServer {
31
32
  */
32
33
  getTools(): Tool[];
33
34
  }
35
+ /**
36
+ * Encode a JSON-RPC message with Content-Length framing.
37
+ * Exported for testability.
38
+ */
39
+ export declare function encodeJsonRpcMessage(message: Record<string, unknown>): string;
40
+ /**
41
+ * Parse Content-Length framed JSON-RPC messages from a buffer.
42
+ * Returns parsed messages and the remaining (unconsumed) buffer.
43
+ * Exported for testability.
44
+ */
45
+ export declare function parseJsonRpcFrames(input: Buffer): {
46
+ messages: Array<{
47
+ id?: unknown;
48
+ method?: string;
49
+ params?: Record<string, unknown>;
50
+ }>;
51
+ remainder: Buffer<ArrayBuffer>;
52
+ };
53
+ /**
54
+ * Handle a single JSON-RPC message against the server.
55
+ * Returns the response message (or null for notifications).
56
+ * Exported for testability.
57
+ */
58
+ export declare function handleJsonRpcMessage(server: E2EAgentsMCPServer, message: {
59
+ id?: unknown;
60
+ method?: string;
61
+ params?: Record<string, unknown>;
62
+ }): Promise<Record<string, unknown> | null>;
63
+ /**
64
+ * Start MCP server over stdio using Content-Length framed JSON-RPC messages.
65
+ */
66
+ export declare function startStdioServer(repoRoot?: string): void;
34
67
  export default E2EAgentsMCPServer;
35
68
  //# sourceMappingURL=mcp-server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AAaA,UAAU,IAAI;IACV,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AA4GD;;;GAGG;AACH,qBAAa,kBAAkB;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,WAAW,CAAc;gBAErB,QAAQ,GAAE,MAAsB;IAM5C,OAAO,CAAC,WAAW;IAmGnB;;;OAGG;IACG,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAwB5E,OAAO,CAAC,aAAa;IA6BrB,OAAO,CAAC,QAAQ;IAmBhB,OAAO,CAAC,SAAS;IAwCjB,OAAO,CAAC,QAAQ;IAyDhB,OAAO,CAAC,aAAa;IA2BrB,OAAO,CAAC,oBAAoB;IAqD5B,OAAO,CAAC,eAAe;IAwBvB,OAAO,CAAC,sBAAsB;IAW9B;;OAEG;IACH,QAAQ,IAAI,IAAI,EAAE;CAGrB;AAYD,eAAe,kBAAkB,CAAC"}
1
+ {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":";AAeA,UAAU,IAAI;IACV,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AA4GD;;;GAGG;AACH,qBAAa,kBAAkB;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,WAAW,CAAc;gBAErB,QAAQ,GAAE,MAAsB;IAM5C,OAAO,CAAC,WAAW;IAmGnB;;;OAGG;IACG,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAwB5E,OAAO,CAAC,aAAa;IA6BrB,OAAO,CAAC,QAAQ;IAmBhB,OAAO,CAAC,SAAS;IAwCjB,OAAO,CAAC,QAAQ;IAyDhB,OAAO,CAAC,aAAa;IA2BrB,OAAO,CAAC,oBAAoB;IAqD5B,OAAO,CAAC,eAAe;IAwBvB,OAAO,CAAC,sBAAsB;IAW9B;;OAEG;IACH,QAAQ,IAAI,IAAI,EAAE;CAGrB;AAgBD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAG7E;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG;IAAC,QAAQ,EAAE,KAAK,CAAC;QAAC,EAAE,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAC,CAAC,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC,WAAW,CAAC,CAAA;CAAC,CA0BtK;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACtC,MAAM,EAAE,kBAAkB,EAC1B,OAAO,EAAE;IAAC,EAAE,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAC,GAC3E,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAmEzC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,GAAE,MAAsB,GAAG,IAAI,CAmCvE;AAMD,eAAe,kBAAkB,CAAC"}