e2e-ai 1.4.3 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -19
- package/agents/7.scanner-agent.md +146 -0
- package/dist/cli-h86f3dv5.js +3971 -0
- package/dist/cli-hjczkpxm.js +117 -0
- package/dist/cli.js +149 -3975
- package/dist/mcp.js +544 -10
- package/package.json +1 -1
- package/templates/workflow.md +10 -1
- /package/agents/{init-agent.md → 0.init-agent.md} +0 -0
- /package/agents/{transcript-agent.md → 1_1.transcript-agent.md} +0 -0
- /package/agents/{scenario-agent.md → 1_2.scenario-agent.md} +0 -0
- /package/agents/{playwright-generator-agent.md → 2.playwright-generator-agent.md} +0 -0
- /package/agents/{refactor-agent.md → 3.refactor-agent.md} +0 -0
- /package/agents/{self-healing-agent.md → 4.self-healing-agent.md} +0 -0
- /package/agents/{qa-testcase-agent.md → 5.qa-testcase-agent.md} +0 -0
- /package/agents/{feature-analyzer-agent.md → 6_1.feature-analyzer-agent.md} +0 -0
- /package/agents/{scenario-planner-agent.md → 6_2.scenario-planner-agent.md} +0 -0
package/dist/mcp.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
loadAgent
|
|
4
|
-
|
|
3
|
+
loadAgent,
|
|
4
|
+
runStage1
|
|
5
|
+
} from "./cli-h86f3dv5.js";
|
|
5
6
|
import {
|
|
6
7
|
getPackageRoot
|
|
7
8
|
} from "./cli-kx32qnf3.js";
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
} from "./cli-fgp618yt.js";
|
|
41
42
|
import {
|
|
42
43
|
__commonJS,
|
|
44
|
+
__require,
|
|
43
45
|
__toESM
|
|
44
46
|
} from "./cli-wckvcay0.js";
|
|
45
47
|
|
|
@@ -14856,9 +14858,282 @@ class StdioServerTransport {
|
|
|
14856
14858
|
}
|
|
14857
14859
|
|
|
14858
14860
|
// src/mcp.ts
|
|
14859
|
-
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
14860
|
-
import { join as join2 } from "node:path";
|
|
14861
14861
|
import { execSync } from "node:child_process";
|
|
14862
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
14863
|
+
import { join as join2 } from "node:path";
|
|
14864
|
+
|
|
14865
|
+
// src/scanner/summarize.ts
|
|
14866
|
+
import { dirname } from "node:path";
|
|
14867
|
+
function summarizeAST(ast, savedPath) {
|
|
14868
|
+
const dirGroups = {};
|
|
14869
|
+
for (const f of ast.files) {
|
|
14870
|
+
const dir = dirname(f.path).split("/")[0] || ".";
|
|
14871
|
+
dirGroups[dir] = (dirGroups[dir] ?? 0) + 1;
|
|
14872
|
+
}
|
|
14873
|
+
return {
|
|
14874
|
+
stats: ast.stats,
|
|
14875
|
+
routes: ast.routes.map((r) => ({
|
|
14876
|
+
path: r.path,
|
|
14877
|
+
filePath: r.filePath,
|
|
14878
|
+
isDynamic: r.isDynamic
|
|
14879
|
+
})),
|
|
14880
|
+
fileTree: ast.files.map((f) => f.path),
|
|
14881
|
+
componentNames: ast.components.map((c) => c.name),
|
|
14882
|
+
hookNames: ast.hooks.filter((h) => h.isCustom).map((h) => h.name),
|
|
14883
|
+
directoryGroups: dirGroups,
|
|
14884
|
+
astScanPath: savedPath
|
|
14885
|
+
};
|
|
14886
|
+
}
|
|
14887
|
+
function filterASTByCategory(ast, category, filter, limit) {
|
|
14888
|
+
const matchesFilter = (path) => {
|
|
14889
|
+
if (!filter)
|
|
14890
|
+
return true;
|
|
14891
|
+
const regex = new RegExp("^" + filter.replace(/\*\*/g, "__.DOUBLE__").replace(/\*/g, "[^/]*").replace(/__.DOUBLE__/g, ".*") + "$");
|
|
14892
|
+
return regex.test(path);
|
|
14893
|
+
};
|
|
14894
|
+
let items;
|
|
14895
|
+
switch (category) {
|
|
14896
|
+
case "routes":
|
|
14897
|
+
items = filter ? ast.routes.filter((r) => matchesFilter(r.path) || matchesFilter(r.filePath)) : ast.routes;
|
|
14898
|
+
break;
|
|
14899
|
+
case "components":
|
|
14900
|
+
items = filter ? ast.components.filter((c) => matchesFilter(c.filePath) || matchesFilter(c.name)) : ast.components;
|
|
14901
|
+
break;
|
|
14902
|
+
case "hooks":
|
|
14903
|
+
items = filter ? ast.hooks.filter((h) => matchesFilter(h.filePath) || matchesFilter(h.name)) : ast.hooks;
|
|
14904
|
+
break;
|
|
14905
|
+
case "dependencies":
|
|
14906
|
+
items = filter ? ast.dependencies.filter((d) => matchesFilter(d.from) || matchesFilter(d.to)) : ast.dependencies;
|
|
14907
|
+
break;
|
|
14908
|
+
case "files":
|
|
14909
|
+
items = filter ? ast.files.filter((f) => matchesFilter(f.path)) : ast.files;
|
|
14910
|
+
break;
|
|
14911
|
+
}
|
|
14912
|
+
if (limit && limit > 0) {
|
|
14913
|
+
items = items.slice(0, limit);
|
|
14914
|
+
}
|
|
14915
|
+
return {
|
|
14916
|
+
category,
|
|
14917
|
+
filter: filter ?? null,
|
|
14918
|
+
total: items.length,
|
|
14919
|
+
items
|
|
14920
|
+
};
|
|
14921
|
+
}
|
|
14922
|
+
|
|
14923
|
+
// src/scanner/validate.ts
|
|
14924
|
+
var WORKFLOW_TYPES = ["navigation", "crud", "multi-step", "configuration", "search-filter"];
|
|
14925
|
+
var COMPONENT_TYPES = ["form", "display", "navigation", "modal", "layout", "feedback"];
|
|
14926
|
+
var SCENARIO_CATEGORIES = ["happy-path", "permission", "validation", "error", "edge-case", "precondition"];
|
|
14927
|
+
var SCENARIO_PRIORITIES = ["critical", "high", "medium", "low"];
|
|
14928
|
+
var BRANCH_TYPES = ["validation", "permission", "error", "business-logic"];
|
|
14929
|
+
function validateQAMap(payload) {
|
|
14930
|
+
const errors = [];
|
|
14931
|
+
const warnings = [];
|
|
14932
|
+
if (!payload || typeof payload !== "object") {
|
|
14933
|
+
return { valid: false, errors: ["Payload must be a non-null object"], warnings };
|
|
14934
|
+
}
|
|
14935
|
+
const p = payload;
|
|
14936
|
+
const features = assertArray(p, "features", errors);
|
|
14937
|
+
const workflows = assertArray(p, "workflows", errors);
|
|
14938
|
+
const components = assertArray(p, "components", errors);
|
|
14939
|
+
const scenarios = assertArray(p, "scenarios", errors);
|
|
14940
|
+
if (errors.length > 0) {
|
|
14941
|
+
return { valid: false, errors, warnings };
|
|
14942
|
+
}
|
|
14943
|
+
const featureIds = new Set;
|
|
14944
|
+
const workflowIds = new Set;
|
|
14945
|
+
const componentIds = new Set;
|
|
14946
|
+
const scenarioIds = new Set;
|
|
14947
|
+
const workflowStepIds = new Set;
|
|
14948
|
+
for (const f of features) {
|
|
14949
|
+
requireString(f, "id", "feature", errors);
|
|
14950
|
+
requireString(f, "name", "feature", errors);
|
|
14951
|
+
requireString(f, "description", "feature", errors);
|
|
14952
|
+
requireArray(f, "routes", "feature", errors);
|
|
14953
|
+
requireArray(f, "workflowIds", "feature", errors);
|
|
14954
|
+
requireArray(f, "sourceFiles", "feature", errors);
|
|
14955
|
+
if (f.id) {
|
|
14956
|
+
if (featureIds.has(f.id))
|
|
14957
|
+
errors.push(`Duplicate feature id: ${f.id}`);
|
|
14958
|
+
featureIds.add(f.id);
|
|
14959
|
+
}
|
|
14960
|
+
}
|
|
14961
|
+
for (const w of workflows) {
|
|
14962
|
+
requireString(w, "id", "workflow", errors);
|
|
14963
|
+
requireString(w, "name", "workflow", errors);
|
|
14964
|
+
requireString(w, "featureId", "workflow", errors);
|
|
14965
|
+
requireEnum(w, "type", WORKFLOW_TYPES, "workflow", errors);
|
|
14966
|
+
requireArray(w, "preconditions", "workflow", errors);
|
|
14967
|
+
requireArray(w, "steps", "workflow", errors);
|
|
14968
|
+
requireArray(w, "componentIds", "workflow", errors);
|
|
14969
|
+
if (w.id) {
|
|
14970
|
+
if (workflowIds.has(w.id))
|
|
14971
|
+
errors.push(`Duplicate workflow id: ${w.id}`);
|
|
14972
|
+
workflowIds.add(w.id);
|
|
14973
|
+
}
|
|
14974
|
+
if (Array.isArray(w.steps)) {
|
|
14975
|
+
for (const s of w.steps) {
|
|
14976
|
+
requireString(s, "id", "workflowStep", errors);
|
|
14977
|
+
requireNumber(s, "order", "workflowStep", errors);
|
|
14978
|
+
requireString(s, "description", "workflowStep", errors);
|
|
14979
|
+
requireArray(s, "componentIds", "workflowStep", errors);
|
|
14980
|
+
requireArray(s, "apiCalls", "workflowStep", errors);
|
|
14981
|
+
requireArray(s, "conditionalBranches", "workflowStep", errors);
|
|
14982
|
+
if (s.id)
|
|
14983
|
+
workflowStepIds.add(s.id);
|
|
14984
|
+
if (Array.isArray(s.conditionalBranches)) {
|
|
14985
|
+
for (const b of s.conditionalBranches) {
|
|
14986
|
+
requireString(b, "condition", "conditionalBranch", errors);
|
|
14987
|
+
requireString(b, "outcome", "conditionalBranch", errors);
|
|
14988
|
+
requireEnum(b, "type", BRANCH_TYPES, "conditionalBranch", errors);
|
|
14989
|
+
}
|
|
14990
|
+
}
|
|
14991
|
+
}
|
|
14992
|
+
}
|
|
14993
|
+
}
|
|
14994
|
+
for (const c of components) {
|
|
14995
|
+
requireString(c, "id", "component", errors);
|
|
14996
|
+
requireString(c, "name", "component", errors);
|
|
14997
|
+
requireEnum(c, "type", COMPONENT_TYPES, "component", errors);
|
|
14998
|
+
requireArray(c, "sourceFiles", "component", errors);
|
|
14999
|
+
requireArray(c, "props", "component", errors);
|
|
15000
|
+
requireArray(c, "referencedByWorkflows", "component", errors);
|
|
15001
|
+
if (c.id) {
|
|
15002
|
+
if (componentIds.has(c.id))
|
|
15003
|
+
errors.push(`Duplicate component id: ${c.id}`);
|
|
15004
|
+
componentIds.add(c.id);
|
|
15005
|
+
}
|
|
15006
|
+
}
|
|
15007
|
+
for (const s of scenarios) {
|
|
15008
|
+
requireString(s, "id", "scenario", errors);
|
|
15009
|
+
requireString(s, "workflowId", "scenario", errors);
|
|
15010
|
+
requireString(s, "featureId", "scenario", errors);
|
|
15011
|
+
requireString(s, "name", "scenario", errors);
|
|
15012
|
+
requireString(s, "description", "scenario", errors);
|
|
15013
|
+
requireEnum(s, "category", SCENARIO_CATEGORIES, "scenario", errors);
|
|
15014
|
+
requireArray(s, "preconditions", "scenario", errors);
|
|
15015
|
+
requireArray(s, "steps", "scenario", errors);
|
|
15016
|
+
requireString(s, "expectedOutcome", "scenario", errors);
|
|
15017
|
+
requireArray(s, "componentIds", "scenario", errors);
|
|
15018
|
+
requireArray(s, "workflowStepIds", "scenario", errors);
|
|
15019
|
+
requireEnum(s, "priority", SCENARIO_PRIORITIES, "scenario", errors);
|
|
15020
|
+
if (s.id) {
|
|
15021
|
+
if (scenarioIds.has(s.id))
|
|
15022
|
+
errors.push(`Duplicate scenario id: ${s.id}`);
|
|
15023
|
+
scenarioIds.add(s.id);
|
|
15024
|
+
}
|
|
15025
|
+
if (Array.isArray(s.steps)) {
|
|
15026
|
+
for (const step of s.steps) {
|
|
15027
|
+
requireNumber(step, "order", "scenarioStep", errors);
|
|
15028
|
+
requireString(step, "action", "scenarioStep", errors);
|
|
15029
|
+
requireString(step, "expectedResult", "scenarioStep", errors);
|
|
15030
|
+
}
|
|
15031
|
+
}
|
|
15032
|
+
}
|
|
15033
|
+
for (const w of workflows) {
|
|
15034
|
+
if (w.featureId && !featureIds.has(w.featureId)) {
|
|
15035
|
+
errors.push(`Workflow "${w.id}" references unknown feature: ${w.featureId}`);
|
|
15036
|
+
}
|
|
15037
|
+
if (Array.isArray(w.componentIds)) {
|
|
15038
|
+
for (const cid of w.componentIds) {
|
|
15039
|
+
if (!componentIds.has(cid)) {
|
|
15040
|
+
errors.push(`Workflow "${w.id}" references unknown component: ${cid}`);
|
|
15041
|
+
}
|
|
15042
|
+
}
|
|
15043
|
+
}
|
|
15044
|
+
}
|
|
15045
|
+
for (const f of features) {
|
|
15046
|
+
if (Array.isArray(f.workflowIds)) {
|
|
15047
|
+
for (const wid of f.workflowIds) {
|
|
15048
|
+
if (!workflowIds.has(wid)) {
|
|
15049
|
+
errors.push(`Feature "${f.id}" references unknown workflow: ${wid}`);
|
|
15050
|
+
}
|
|
15051
|
+
}
|
|
15052
|
+
}
|
|
15053
|
+
}
|
|
15054
|
+
for (const s of scenarios) {
|
|
15055
|
+
if (s.workflowId && !workflowIds.has(s.workflowId)) {
|
|
15056
|
+
errors.push(`Scenario "${s.id}" references unknown workflow: ${s.workflowId}`);
|
|
15057
|
+
}
|
|
15058
|
+
if (s.featureId && !featureIds.has(s.featureId)) {
|
|
15059
|
+
errors.push(`Scenario "${s.id}" references unknown feature: ${s.featureId}`);
|
|
15060
|
+
}
|
|
15061
|
+
if (Array.isArray(s.componentIds)) {
|
|
15062
|
+
for (const cid of s.componentIds) {
|
|
15063
|
+
if (!componentIds.has(cid)) {
|
|
15064
|
+
errors.push(`Scenario "${s.id}" references unknown component: ${cid}`);
|
|
15065
|
+
}
|
|
15066
|
+
}
|
|
15067
|
+
}
|
|
15068
|
+
if (Array.isArray(s.workflowStepIds)) {
|
|
15069
|
+
for (const wsid of s.workflowStepIds) {
|
|
15070
|
+
if (!workflowStepIds.has(wsid)) {
|
|
15071
|
+
errors.push(`Scenario "${s.id}" references unknown workflow step: ${wsid}`);
|
|
15072
|
+
}
|
|
15073
|
+
}
|
|
15074
|
+
}
|
|
15075
|
+
}
|
|
15076
|
+
for (const c of components) {
|
|
15077
|
+
if (Array.isArray(c.referencedByWorkflows)) {
|
|
15078
|
+
for (const wid of c.referencedByWorkflows) {
|
|
15079
|
+
if (!workflowIds.has(wid)) {
|
|
15080
|
+
errors.push(`Component "${c.id}" references unknown workflow: ${wid}`);
|
|
15081
|
+
}
|
|
15082
|
+
}
|
|
15083
|
+
}
|
|
15084
|
+
}
|
|
15085
|
+
for (const f of features) {
|
|
15086
|
+
if (Array.isArray(f.workflowIds) && f.workflowIds.length === 0) {
|
|
15087
|
+
warnings.push(`Feature "${f.id}" has no workflows`);
|
|
15088
|
+
}
|
|
15089
|
+
}
|
|
15090
|
+
for (const w of workflows) {
|
|
15091
|
+
const hasScenarios = scenarios.some((s) => s.workflowId === w.id);
|
|
15092
|
+
if (!hasScenarios) {
|
|
15093
|
+
warnings.push(`Workflow "${w.id}" has no scenarios`);
|
|
15094
|
+
}
|
|
15095
|
+
}
|
|
15096
|
+
const referencedComponentIds = new Set;
|
|
15097
|
+
for (const w of workflows) {
|
|
15098
|
+
if (Array.isArray(w.componentIds)) {
|
|
15099
|
+
for (const cid of w.componentIds)
|
|
15100
|
+
referencedComponentIds.add(cid);
|
|
15101
|
+
}
|
|
15102
|
+
}
|
|
15103
|
+
for (const c of components) {
|
|
15104
|
+
if (c.id && !referencedComponentIds.has(c.id)) {
|
|
15105
|
+
warnings.push(`Component "${c.id}" is not referenced by any workflow`);
|
|
15106
|
+
}
|
|
15107
|
+
}
|
|
15108
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
15109
|
+
}
|
|
15110
|
+
function assertArray(obj, field, errors) {
|
|
15111
|
+
if (!Array.isArray(obj[field])) {
|
|
15112
|
+
errors.push(`Missing or invalid top-level array: ${field}`);
|
|
15113
|
+
return [];
|
|
15114
|
+
}
|
|
15115
|
+
return obj[field];
|
|
15116
|
+
}
|
|
15117
|
+
function requireString(obj, field, context, errors) {
|
|
15118
|
+
if (typeof obj?.[field] !== "string" || obj[field].length === 0) {
|
|
15119
|
+
errors.push(`${context} missing required string field: ${field} (id: ${obj?.id ?? "unknown"})`);
|
|
15120
|
+
}
|
|
15121
|
+
}
|
|
15122
|
+
function requireNumber(obj, field, context, errors) {
|
|
15123
|
+
if (typeof obj?.[field] !== "number") {
|
|
15124
|
+
errors.push(`${context} missing required number field: ${field} (id: ${obj?.id ?? "unknown"})`);
|
|
15125
|
+
}
|
|
15126
|
+
}
|
|
15127
|
+
function requireArray(obj, field, context, errors) {
|
|
15128
|
+
if (!Array.isArray(obj?.[field])) {
|
|
15129
|
+
errors.push(`${context} missing required array field: ${field} (id: ${obj?.id ?? "unknown"})`);
|
|
15130
|
+
}
|
|
15131
|
+
}
|
|
15132
|
+
function requireEnum(obj, field, allowed, context, errors) {
|
|
15133
|
+
if (typeof obj?.[field] !== "string" || !allowed.includes(obj[field])) {
|
|
15134
|
+
errors.push(`${context} invalid ${field}: "${obj?.[field]}" — expected one of: ${allowed.join(", ")} (id: ${obj?.id ?? "unknown"})`);
|
|
15135
|
+
}
|
|
15136
|
+
}
|
|
14862
15137
|
|
|
14863
15138
|
// src/utils/scan.ts
|
|
14864
15139
|
import { readdirSync, existsSync, readFileSync } from "node:fs";
|
|
@@ -14970,14 +15245,15 @@ NEVER run multiple pipeline steps at once. Each step is a separate job with its
|
|
|
14970
15245
|
## Protocol
|
|
14971
15246
|
|
|
14972
15247
|
1. **Plan first.** Call \`e2e_ai_plan_workflow\` with the user's goal. This returns a structured todo list of steps.
|
|
14973
|
-
2. **
|
|
14974
|
-
3. **
|
|
15248
|
+
2. **Check prerequisites.** The plan includes a \`ready\` boolean and \`missingPrerequisites\` array. If \`ready\` is false, show the user what's missing (API keys, config, etc.) and **wait for them to fix it** before proceeding. Do NOT attempt to execute any step while prerequisites are missing.
|
|
15249
|
+
3. **Present the plan.** Show the user the ordered step list with descriptions. Ask for confirmation or adjustments before proceeding.
|
|
15250
|
+
4. **Execute one step at a time.** For each step in the approved plan:
|
|
14975
15251
|
a. Tell the user which step you're about to run and why.
|
|
14976
15252
|
b. Call \`e2e_ai_execute_step\` with the step name and parameters.
|
|
14977
15253
|
c. Report the result to the user (success, key output, any warnings).
|
|
14978
15254
|
d. If the step fails, stop and discuss with the user before continuing.
|
|
14979
15255
|
e. Move to the next step only after the current one succeeds.
|
|
14980
|
-
|
|
15256
|
+
5. **Use subagents when available.** If your AI platform supports subagents (e.g., Claude Code Agent tool), dispatch each step as a dedicated subagent to preserve context. Each subagent should:
|
|
14981
15257
|
- Receive only the context it needs (step name, key, relevant file paths)
|
|
14982
15258
|
- Call \`e2e_ai_execute_step\` to do its work
|
|
14983
15259
|
- Return the result to the orchestrator
|
|
@@ -15010,6 +15286,20 @@ The \`record\` step opens a browser and requires user interaction. When the plan
|
|
|
15010
15286
|
- **Single step**: any individual command
|
|
15011
15287
|
|
|
15012
15288
|
Always use \`e2e_ai_plan_workflow\` to determine the right steps — don't guess.
|
|
15289
|
+
|
|
15290
|
+
## Scanner Analysis (Interactive QA Map)
|
|
15291
|
+
|
|
15292
|
+
For deep codebase analysis and QA map generation, use the interactive scanner workflow instead of the CLI pipeline:
|
|
15293
|
+
|
|
15294
|
+
1. **Load the protocol.** Call \`e2e_ai_read_agent("scanner-agent")\` — this returns the full interactive protocol.
|
|
15295
|
+
2. **Scan.** Call \`e2e_ai_scan_ast()\` to run the AST scanner and get a compact summary.
|
|
15296
|
+
3. **Explore.** Use \`e2e_ai_scan_ast_detail()\` to drill into routes, components, hooks, or files.
|
|
15297
|
+
4. **Propose & discuss.** Present candidate features to the user, ask clarifying questions.
|
|
15298
|
+
5. **Build.** Construct the QA map payload and validate with \`e2e_ai_build_qa_map({ dryRun: true })\`.
|
|
15299
|
+
6. **Write.** Once validated and approved, call \`e2e_ai_build_qa_map({ dryRun: false })\` to save.
|
|
15300
|
+
7. **Read existing.** Use \`e2e_ai_read_qa_map()\` to load a previously generated QA map for incremental updates.
|
|
15301
|
+
|
|
15302
|
+
This approach is preferred over \`scan → analyze\` CLI steps because it allows interactive refinement with the user.
|
|
15013
15303
|
`.trim();
|
|
15014
15304
|
var TEST_PIPELINE_STEPS = [
|
|
15015
15305
|
{
|
|
@@ -15095,6 +15385,87 @@ var SCANNER_PIPELINE_STEPS = [
|
|
|
15095
15385
|
}
|
|
15096
15386
|
];
|
|
15097
15387
|
var ALL_STEPS = [...TEST_PIPELINE_STEPS, ...SCANNER_PIPELINE_STEPS];
|
|
15388
|
+
var STEP_REQUIREMENTS = {
|
|
15389
|
+
record: { envVars: [] },
|
|
15390
|
+
transcribe: { envVars: [{ name: "OPENAI_API_KEY", reason: "Whisper transcription requires OpenAI API key" }] },
|
|
15391
|
+
scenario: { envVars: [
|
|
15392
|
+
{ name: "OPENAI_API_KEY", reason: "LLM calls require OpenAI API key", onlyIf: () => getProvider() === "openai" },
|
|
15393
|
+
{ name: "ANTHROPIC_API_KEY", reason: "LLM calls require Anthropic API key", onlyIf: () => getProvider() === "anthropic" }
|
|
15394
|
+
] },
|
|
15395
|
+
generate: { envVars: [
|
|
15396
|
+
{ name: "OPENAI_API_KEY", reason: "LLM calls require OpenAI API key", onlyIf: () => getProvider() === "openai" },
|
|
15397
|
+
{ name: "ANTHROPIC_API_KEY", reason: "LLM calls require Anthropic API key", onlyIf: () => getProvider() === "anthropic" }
|
|
15398
|
+
] },
|
|
15399
|
+
refine: { envVars: [
|
|
15400
|
+
{ name: "OPENAI_API_KEY", reason: "LLM calls require OpenAI API key", onlyIf: () => getProvider() === "openai" },
|
|
15401
|
+
{ name: "ANTHROPIC_API_KEY", reason: "LLM calls require Anthropic API key", onlyIf: () => getProvider() === "anthropic" }
|
|
15402
|
+
] },
|
|
15403
|
+
test: { envVars: [] },
|
|
15404
|
+
heal: { envVars: [
|
|
15405
|
+
{ name: "OPENAI_API_KEY", reason: "LLM calls require OpenAI API key", onlyIf: () => getProvider() === "openai" },
|
|
15406
|
+
{ name: "ANTHROPIC_API_KEY", reason: "LLM calls require Anthropic API key", onlyIf: () => getProvider() === "anthropic" }
|
|
15407
|
+
] },
|
|
15408
|
+
qa: { envVars: [
|
|
15409
|
+
{ name: "OPENAI_API_KEY", reason: "LLM calls require OpenAI API key", onlyIf: () => getProvider() === "openai" },
|
|
15410
|
+
{ name: "ANTHROPIC_API_KEY", reason: "LLM calls require Anthropic API key", onlyIf: () => getProvider() === "anthropic" }
|
|
15411
|
+
] },
|
|
15412
|
+
scan: { envVars: [] },
|
|
15413
|
+
analyze: { envVars: [
|
|
15414
|
+
{ name: "OPENAI_API_KEY", reason: "LLM calls require OpenAI API key", onlyIf: () => getProvider() === "openai" },
|
|
15415
|
+
{ name: "ANTHROPIC_API_KEY", reason: "LLM calls require Anthropic API key", onlyIf: () => getProvider() === "anthropic" }
|
|
15416
|
+
] },
|
|
15417
|
+
push: { envVars: [
|
|
15418
|
+
{ name: "E2E_AI_API_URL", reason: "Push requires API URL (set E2E_AI_API_URL or push.apiUrl in config)" },
|
|
15419
|
+
{ name: "E2E_AI_API_KEY", reason: "Push requires API key (set E2E_AI_API_KEY or push.apiKey in config)" }
|
|
15420
|
+
] }
|
|
15421
|
+
};
|
|
15422
|
+
function getProvider() {
|
|
15423
|
+
return process.env.AI_PROVIDER ?? "openai";
|
|
15424
|
+
}
|
|
15425
|
+
function checkPrerequisites(stepNames) {
|
|
15426
|
+
const issueMap = new Map;
|
|
15427
|
+
for (const stepName of stepNames) {
|
|
15428
|
+
const reqs = STEP_REQUIREMENTS[stepName];
|
|
15429
|
+
if (!reqs)
|
|
15430
|
+
continue;
|
|
15431
|
+
for (const envReq of reqs.envVars) {
|
|
15432
|
+
if (envReq.onlyIf && !envReq.onlyIf())
|
|
15433
|
+
continue;
|
|
15434
|
+
if (!process.env[envReq.name]) {
|
|
15435
|
+
const key = `env:${envReq.name}`;
|
|
15436
|
+
if (issueMap.has(key)) {
|
|
15437
|
+
issueMap.get(key).stepsAffected.push(stepName);
|
|
15438
|
+
} else {
|
|
15439
|
+
issueMap.set(key, {
|
|
15440
|
+
type: "env_var",
|
|
15441
|
+
name: envReq.name,
|
|
15442
|
+
reason: envReq.reason,
|
|
15443
|
+
stepsAffected: [stepName]
|
|
15444
|
+
});
|
|
15445
|
+
}
|
|
15446
|
+
}
|
|
15447
|
+
}
|
|
15448
|
+
if (reqs.files) {
|
|
15449
|
+
for (const fileReq of reqs.files) {
|
|
15450
|
+
if (!existsSync2(fileReq.path)) {
|
|
15451
|
+
const key = `file:${fileReq.path}`;
|
|
15452
|
+
if (issueMap.has(key)) {
|
|
15453
|
+
issueMap.get(key).stepsAffected.push(stepName);
|
|
15454
|
+
} else {
|
|
15455
|
+
issueMap.set(key, {
|
|
15456
|
+
type: "file",
|
|
15457
|
+
name: fileReq.label,
|
|
15458
|
+
reason: `File not found: ${fileReq.path}`,
|
|
15459
|
+
stepsAffected: [stepName]
|
|
15460
|
+
});
|
|
15461
|
+
}
|
|
15462
|
+
}
|
|
15463
|
+
}
|
|
15464
|
+
}
|
|
15465
|
+
}
|
|
15466
|
+
const missing = Array.from(issueMap.values());
|
|
15467
|
+
return { ready: missing.length === 0, missing };
|
|
15468
|
+
}
|
|
15098
15469
|
function planWorkflow(goal, options) {
|
|
15099
15470
|
const goalLower = goal.toLowerCase();
|
|
15100
15471
|
const notes = [];
|
|
@@ -15179,7 +15550,8 @@ function planWorkflow(goal, options) {
|
|
|
15179
15550
|
if (!options.key && pipeline2 === "test" && steps.length > 1) {
|
|
15180
15551
|
notes.push("No --key provided. Use --key <ISSUE-KEY> to organize files by issue.");
|
|
15181
15552
|
}
|
|
15182
|
-
|
|
15553
|
+
const prereqs = checkPrerequisites(steps.map((s) => s.name));
|
|
15554
|
+
return { goal, pipeline: pipeline2, ready: prereqs.ready, missingPrerequisites: prereqs.missing, steps, notes };
|
|
15183
15555
|
}
|
|
15184
15556
|
function executeStep(stepName, options) {
|
|
15185
15557
|
const args = [stepName];
|
|
@@ -15229,7 +15601,7 @@ ${stderr}`,
|
|
|
15229
15601
|
};
|
|
15230
15602
|
}
|
|
15231
15603
|
}
|
|
15232
|
-
var server = new McpServer({ name: "e2e-ai", version: "1.
|
|
15604
|
+
var server = new McpServer({ name: "e2e-ai", version: "1.5.0" }, { instructions: SERVER_INSTRUCTIONS });
|
|
15233
15605
|
server.registerTool("e2e_ai_scan_codebase", {
|
|
15234
15606
|
title: "Scan Codebase",
|
|
15235
15607
|
description: "Scan a project directory for test files, configs, fixtures, path aliases, and sample test content. Use this during project setup or to understand test infrastructure.",
|
|
@@ -15257,7 +15629,7 @@ server.registerTool("e2e_ai_validate_context", {
|
|
|
15257
15629
|
});
|
|
15258
15630
|
server.registerTool("e2e_ai_read_agent", {
|
|
15259
15631
|
title: "Read Agent",
|
|
15260
|
-
description: "Read an agent prompt definition by name. Returns the agent system prompt and config. Agents: transcript-agent, scenario-agent, playwright-generator-agent, refactor-agent, self-healing-agent, qa-testcase-agent, feature-analyzer-agent, scenario-planner-agent, init-agent.",
|
|
15632
|
+
description: "Read an agent prompt definition by name. Returns the agent system prompt and config. Agents: transcript-agent, scenario-agent, playwright-generator-agent, refactor-agent, self-healing-agent, qa-testcase-agent, feature-analyzer-agent, scenario-planner-agent, scanner-agent, init-agent.",
|
|
15261
15633
|
inputSchema: exports_external.object({
|
|
15262
15634
|
agentName: exports_external.string().describe("Agent name (e.g. scenario-agent, playwright-generator-agent)")
|
|
15263
15635
|
})
|
|
@@ -15343,6 +15715,27 @@ server.registerTool("e2e_ai_execute_step", {
|
|
|
15343
15715
|
isError: true
|
|
15344
15716
|
};
|
|
15345
15717
|
}
|
|
15718
|
+
const prereqs = checkPrerequisites([step]);
|
|
15719
|
+
if (!prereqs.ready) {
|
|
15720
|
+
const lines = prereqs.missing.map((m) => `- ${m.type === "env_var" ? `Set ${m.name}` : m.name}: ${m.reason}`);
|
|
15721
|
+
return {
|
|
15722
|
+
content: [{
|
|
15723
|
+
type: "text",
|
|
15724
|
+
text: JSON.stringify({
|
|
15725
|
+
step,
|
|
15726
|
+
success: false,
|
|
15727
|
+
blocked: true,
|
|
15728
|
+
missingPrerequisites: prereqs.missing,
|
|
15729
|
+
message: `Cannot run "${step}" — missing prerequisites:
|
|
15730
|
+
${lines.join(`
|
|
15731
|
+
`)}
|
|
15732
|
+
|
|
15733
|
+
Ask the user to provide these before retrying.`
|
|
15734
|
+
}, null, 2)
|
|
15735
|
+
}],
|
|
15736
|
+
isError: true
|
|
15737
|
+
};
|
|
15738
|
+
}
|
|
15346
15739
|
const result = executeStep(step, { key, voice, trace, scanDir, output, extraArgs });
|
|
15347
15740
|
return {
|
|
15348
15741
|
content: [{
|
|
@@ -15380,6 +15773,147 @@ server.registerTool("e2e_ai_get_workflow_guide", {
|
|
|
15380
15773
|
};
|
|
15381
15774
|
}
|
|
15382
15775
|
});
|
|
15776
|
+
server.registerTool("e2e_ai_scan_ast", {
|
|
15777
|
+
title: "Scan AST",
|
|
15778
|
+
description: "Run the deep AST scanner on the codebase. Returns a compact summary (stats, routes, components, hooks, directory groups). " + "The full AST is saved to .e2e-ai/ast-scan.json for follow-up queries via e2e_ai_scan_ast_detail.",
|
|
15779
|
+
inputSchema: exports_external.object({
|
|
15780
|
+
scanDir: exports_external.string().optional().describe('Directory to scan (defaults to "src")'),
|
|
15781
|
+
include: exports_external.array(exports_external.string()).optional().describe('Glob patterns to include (defaults to ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"])'),
|
|
15782
|
+
exclude: exports_external.array(exports_external.string()).optional().describe("Glob patterns to exclude (defaults to common non-source dirs)")
|
|
15783
|
+
})
|
|
15784
|
+
}, async ({ scanDir, include, exclude }) => {
|
|
15785
|
+
try {
|
|
15786
|
+
const cwd = process.cwd();
|
|
15787
|
+
const dir = scanDir ?? "src";
|
|
15788
|
+
const scanConfig = {
|
|
15789
|
+
scanDir: join2(cwd, dir),
|
|
15790
|
+
include: include ?? ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
|
|
15791
|
+
exclude: exclude ?? ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/*.test.*", "**/*.spec.*", "**/__tests__/**"],
|
|
15792
|
+
cacheDir: join2(cwd, ".e2e-ai", "cache")
|
|
15793
|
+
};
|
|
15794
|
+
const ast = await runStage1(scanConfig);
|
|
15795
|
+
const astPath = join2(cwd, ".e2e-ai", "ast-scan.json");
|
|
15796
|
+
const { mkdirSync, writeFileSync } = await import("node:fs");
|
|
15797
|
+
mkdirSync(join2(cwd, ".e2e-ai"), { recursive: true });
|
|
15798
|
+
writeFileSync(astPath, JSON.stringify(ast, null, 2));
|
|
15799
|
+
const summary = summarizeAST(ast, astPath);
|
|
15800
|
+
return {
|
|
15801
|
+
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
15802
|
+
};
|
|
15803
|
+
} catch (err) {
|
|
15804
|
+
return {
|
|
15805
|
+
content: [{ type: "text", text: `Error scanning AST: ${err.message}` }],
|
|
15806
|
+
isError: true
|
|
15807
|
+
};
|
|
15808
|
+
}
|
|
15809
|
+
});
|
|
15810
|
+
server.registerTool("e2e_ai_scan_ast_detail", {
|
|
15811
|
+
title: "Scan AST Detail",
|
|
15812
|
+
description: "Retrieve a filtered slice of the AST scan. Requires a prior e2e_ai_scan_ast call. " + "Use to drill into routes, components, hooks, dependencies, or files.",
|
|
15813
|
+
inputSchema: exports_external.object({
|
|
15814
|
+
category: exports_external.enum(["routes", "components", "hooks", "dependencies", "files"]).describe("Which AST category to retrieve"),
|
|
15815
|
+
filter: exports_external.string().optional().describe('Glob pattern to filter results (e.g. "src/app/**", "Dashboard*")'),
|
|
15816
|
+
limit: exports_external.number().optional().describe("Max number of items to return")
|
|
15817
|
+
})
|
|
15818
|
+
}, async ({ category, filter, limit }) => {
|
|
15819
|
+
try {
|
|
15820
|
+
const astPath = join2(process.cwd(), ".e2e-ai", "ast-scan.json");
|
|
15821
|
+
if (!existsSync2(astPath)) {
|
|
15822
|
+
return {
|
|
15823
|
+
content: [{ type: "text", text: "Error: No AST scan found. Run e2e_ai_scan_ast first." }],
|
|
15824
|
+
isError: true
|
|
15825
|
+
};
|
|
15826
|
+
}
|
|
15827
|
+
const ast = JSON.parse(readFileSync2(astPath, "utf-8"));
|
|
15828
|
+
const result = filterASTByCategory(ast, category, filter, limit);
|
|
15829
|
+
return {
|
|
15830
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
15831
|
+
};
|
|
15832
|
+
} catch (err) {
|
|
15833
|
+
return {
|
|
15834
|
+
content: [{ type: "text", text: `Error reading AST detail: ${err.message}` }],
|
|
15835
|
+
isError: true
|
|
15836
|
+
};
|
|
15837
|
+
}
|
|
15838
|
+
});
|
|
15839
|
+
server.registerTool("e2e_ai_build_qa_map", {
|
|
15840
|
+
title: "Build QA Map",
|
|
15841
|
+
description: "Validate and optionally write a QAMapV2Payload. Use dryRun: true to validate without writing. " + "Returns validation result with errors, warnings, and stats.",
|
|
15842
|
+
inputSchema: exports_external.object({
|
|
15843
|
+
payload: exports_external.any().describe("QAMapV2Payload JSON object with features, workflows, components, scenarios"),
|
|
15844
|
+
output: exports_external.string().optional().describe("Output file path (defaults to .e2e-ai/qa-map.json)"),
|
|
15845
|
+
dryRun: exports_external.boolean().optional().describe("If true, validate only without writing (default: false)")
|
|
15846
|
+
})
|
|
15847
|
+
}, async ({ payload, output, dryRun }) => {
|
|
15848
|
+
try {
|
|
15849
|
+
const validation = validateQAMap(payload);
|
|
15850
|
+
if (validation.valid && payload && typeof payload === "object") {
|
|
15851
|
+
try {
|
|
15852
|
+
const sha = execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
|
|
15853
|
+
payload.commitSha = sha;
|
|
15854
|
+
} catch {}
|
|
15855
|
+
}
|
|
15856
|
+
const outputPath = output ?? join2(process.cwd(), ".e2e-ai", "qa-map.json");
|
|
15857
|
+
let written = false;
|
|
15858
|
+
if (validation.valid && !dryRun) {
|
|
15859
|
+
const { mkdirSync, writeFileSync } = await import("node:fs");
|
|
15860
|
+
const { dirname: dirname2 } = await import("node:path");
|
|
15861
|
+
mkdirSync(dirname2(outputPath), { recursive: true });
|
|
15862
|
+
writeFileSync(outputPath, JSON.stringify(payload, null, 2));
|
|
15863
|
+
written = true;
|
|
15864
|
+
}
|
|
15865
|
+
const p = payload;
|
|
15866
|
+
const stats = validation.valid ? {
|
|
15867
|
+
features: Array.isArray(p?.features) ? p.features.length : 0,
|
|
15868
|
+
workflows: Array.isArray(p?.workflows) ? p.workflows.length : 0,
|
|
15869
|
+
components: Array.isArray(p?.components) ? p.components.length : 0,
|
|
15870
|
+
scenarios: Array.isArray(p?.scenarios) ? p.scenarios.length : 0
|
|
15871
|
+
} : null;
|
|
15872
|
+
return {
|
|
15873
|
+
content: [{
|
|
15874
|
+
type: "text",
|
|
15875
|
+
text: JSON.stringify({
|
|
15876
|
+
valid: validation.valid,
|
|
15877
|
+
errors: validation.errors,
|
|
15878
|
+
warnings: validation.warnings,
|
|
15879
|
+
written,
|
|
15880
|
+
outputPath: written ? outputPath : null,
|
|
15881
|
+
stats
|
|
15882
|
+
}, null, 2)
|
|
15883
|
+
}]
|
|
15884
|
+
};
|
|
15885
|
+
} catch (err) {
|
|
15886
|
+
return {
|
|
15887
|
+
content: [{ type: "text", text: `Error building QA map: ${err.message}` }],
|
|
15888
|
+
isError: true
|
|
15889
|
+
};
|
|
15890
|
+
}
|
|
15891
|
+
});
|
|
15892
|
+
server.registerTool("e2e_ai_read_qa_map", {
|
|
15893
|
+
title: "Read QA Map",
|
|
15894
|
+
description: "Read an existing QA map file. Returns the parsed QAMapV2Payload or null if not found.",
|
|
15895
|
+
inputSchema: exports_external.object({
|
|
15896
|
+
path: exports_external.string().optional().describe("Path to QA map file (defaults to .e2e-ai/qa-map.json)")
|
|
15897
|
+
})
|
|
15898
|
+
}, async ({ path }) => {
|
|
15899
|
+
try {
|
|
15900
|
+
const mapPath = path ?? join2(process.cwd(), ".e2e-ai", "qa-map.json");
|
|
15901
|
+
if (!existsSync2(mapPath)) {
|
|
15902
|
+
return {
|
|
15903
|
+
content: [{ type: "text", text: JSON.stringify(null) }]
|
|
15904
|
+
};
|
|
15905
|
+
}
|
|
15906
|
+
const content = readFileSync2(mapPath, "utf-8");
|
|
15907
|
+
return {
|
|
15908
|
+
content: [{ type: "text", text: content }]
|
|
15909
|
+
};
|
|
15910
|
+
} catch (err) {
|
|
15911
|
+
return {
|
|
15912
|
+
content: [{ type: "text", text: `Error reading QA map: ${err.message}` }],
|
|
15913
|
+
isError: true
|
|
15914
|
+
};
|
|
15915
|
+
}
|
|
15916
|
+
});
|
|
15383
15917
|
async function main() {
|
|
15384
15918
|
const transport = new StdioServerTransport;
|
|
15385
15919
|
await server.connect(transport);
|
package/package.json
CHANGED
package/templates/workflow.md
CHANGED
|
@@ -232,7 +232,16 @@ After running the pipeline for `PROJ-101`:
|
|
|
232
232
|
config.ts ← your configuration
|
|
233
233
|
context.md ← project context (teach AI your conventions)
|
|
234
234
|
workflow.md ← this file
|
|
235
|
-
agents/ ← AI agent prompts (
|
|
235
|
+
agents/ ← AI agent prompts (numbered by pipeline order)
|
|
236
|
+
0.init-agent.md
|
|
237
|
+
1_1.transcript-agent.md
|
|
238
|
+
1_2.scenario-agent.md
|
|
239
|
+
2.playwright-generator-agent.md
|
|
240
|
+
3.refactor-agent.md
|
|
241
|
+
4.self-healing-agent.md
|
|
242
|
+
5.qa-testcase-agent.md
|
|
243
|
+
6_1.feature-analyzer-agent.md
|
|
244
|
+
6_2.scenario-planner-agent.md
|
|
236
245
|
PROJ-101/ ← working files (codegen, recordings)
|
|
237
246
|
|
|
238
247
|
e2e/
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|