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/dist/mcp.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- loadAgent
4
- } from "./cli-98db6h2q.js";
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. **Present the plan.** Show the user the ordered step list with descriptions. Ask for confirmation or adjustments before proceeding.
14974
- 3. **Execute one step at a time.** For each step in the approved plan:
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
- 4. **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:
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
- return { goal, pipeline: pipeline2, steps, notes };
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.2.0" }, { instructions: SERVER_INSTRUCTIONS });
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "e2e-ai",
3
3
  "description": "AI-powered test automation pipeline — record, transcribe, generate, heal and ship Playwright tests from a single CLI",
4
- "version": "1.4.3",
4
+ "version": "1.5.1",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "bin": {
@@ -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 (customizable)
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