levante 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +263 -83
  2. package/dist/cli.js +64 -54
  3. package/dist/mcp.js +206 -76
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -1,11 +1,13 @@
1
1
  # Levante
2
2
 
3
- AI-powered end-to-end test pipeline for Playwright. Record, transcribe, generate, refine, heal, and document your tests — all driven by LLM agents.
3
+ AI-powered end-to-end test pipeline for Playwright. Record interactions, transcribe narration, generate Playwright tests, self-heal failures, and produce QA documentation — all driven by LLM agents.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
8
  npm install -g levante
9
+ # or
10
+ bun add -g levante
9
11
  ```
10
12
 
11
13
  Levante requires Playwright as a peer dependency:
@@ -14,82 +16,109 @@ Levante requires Playwright as a peer dependency:
14
16
  npm install -D @playwright/test
15
17
  ```
16
18
 
17
- ### Environment Variables
18
-
19
- | Variable | Required | Description |
20
- |----------|----------|-------------|
21
- | `OPENAI_API_KEY` | Yes (if using OpenAI) | OpenAI API key |
22
- | `ANTHROPIC_API_KEY` | Yes (if using Anthropic) | Anthropic API key |
23
- | `E2E_AI_API_URL` | No | Remote API URL for QA map push |
24
- | `E2E_AI_API_KEY` | No | API key for push authentication |
25
-
26
19
  ## Quick Start
27
20
 
28
21
  ```bash
29
- # Initialize config and agents
22
+ # 1. Initialize config and agents
30
23
  levante init
31
24
 
32
- # Run full pipeline for a test case
25
+ # 2. Run full pipeline for a test case
33
26
  levante run --key PROJ-101
34
27
 
35
- # Or run individual steps
28
+ # Or step by step
36
29
  levante record --key PROJ-101
37
30
  levante transcribe --key PROJ-101
38
31
  levante scenario --key PROJ-101
39
32
  levante generate --key PROJ-101
33
+ levante refine --key PROJ-101
40
34
  levante test --key PROJ-101
35
+ levante heal --key PROJ-101 # only if test fails
36
+ levante qa --key PROJ-101
41
37
  ```
42
38
 
43
- ## CLI Usage
39
+ ---
44
40
 
45
- ### Global Options
41
+ ## Environment Variables
46
42
 
47
- ```
48
- -k, --key <KEY> Issue key (e.g., PROJ-101)
49
- --provider <provider> LLM provider: openai | anthropic
50
- --model <model> LLM model override
51
- -v, --verbose Verbose output
52
- --no-voice Disable voice recording
53
- --no-trace Disable trace replay
54
- ```
43
+ | Variable | Description |
44
+ |----------|-------------|
45
+ | `OPENAI_API_KEY` | OpenAI API key |
46
+ | `ANTHROPIC_API_KEY` | Anthropic API key |
47
+ | `QAI_BASE_URL` | QA Intelligence API base URL |
48
+ | `QAI_API_URL` | Full push endpoint (overrides base URL) |
49
+ | `QAI_API_KEY` | API key for authenticated push |
55
50
 
56
- ### Commands
51
+ ---
57
52
 
58
- #### `levante init`
53
+ ## Commands
59
54
 
60
- Initialize levante in your project. Creates `.qai/levante/` with config, agents, and workflow guide.
55
+ ### `levante init`
56
+
57
+ Initialize levante in your project. Generates `.qai/levante/config.ts`, copies agent templates, and optionally connects to QA Intelligence.
61
58
 
62
59
  ```bash
63
60
  levante init
64
- levante init --non-interactive
61
+ levante init --non-interactive # skip prompts, use defaults
65
62
  ```
66
63
 
67
- #### `levante record [session]`
64
+ On re-run, preserves your existing config and context, and only updates agents.
65
+
66
+ **After init:**
67
+ 1. Generate `.qai/levante/context.md` using the `init-agent` prompt in your AI tool, or via MCP (`levante_scan_codebase`)
68
+ 2. Review the generated context
69
+ 3. Start recording: `levante record --key PROJ-101`
70
+
71
+ ---
68
72
 
69
- Launch Playwright codegen with optional audio narration capture.
73
+ ### `levante record [session]`
74
+
75
+ Launch Playwright codegen with optional voice narration recording.
70
76
 
71
77
  ```bash
72
78
  levante record --key PROJ-101
73
- levante record --key PROJ-101 --no-voice
79
+ levante record --key PROJ-101 --no-companion # voice only, no companion UI
80
+ levante record --key PROJ-101 --no-voice # codegen only, no audio
74
81
  ```
75
82
 
76
- #### `levante transcribe [session]`
83
+ | Option | Description |
84
+ |--------|-------------|
85
+ | `--no-companion` | Disable companion UI (voice recording only) |
86
+ | `--no-voice` | Disable voice recording entirely |
77
87
 
78
- Transcribe `.wav` voice recording via OpenAI Whisper.
88
+ **Output:** codegen TypeScript file + `.wav` audio (if voice enabled)
89
+
90
+ ---
91
+
92
+ ### `levante transcribe [session]`
93
+
94
+ Transcribe the `.wav` voice recording via OpenAI Whisper. If a live transcript from the recording session exists, Whisper is skipped.
79
95
 
80
96
  ```bash
81
97
  levante transcribe --key PROJ-101
98
+ levante transcribe --key PROJ-101 --force # re-transcribe even if transcript exists
82
99
  ```
83
100
 
84
- #### `levante scenario [session]`
101
+ Merges voice annotations (with timestamps) back into the codegen file as inline comments.
102
+
103
+ **Output:** `*-transcript.json`, `*-transcript.md`, annotated codegen file
104
+
105
+ ---
106
+
107
+ ### `levante scenario [session]`
85
108
 
86
- Generate a structured YAML scenario from codegen output and transcript.
109
+ Generate a structured YAML scenario from codegen output and voice transcript.
87
110
 
88
111
  ```bash
89
112
  levante scenario --key PROJ-101
90
113
  ```
91
114
 
92
- #### `levante generate [scenario]`
115
+ Uses `transcript-agent` (if transcript available) to extract narrative and action intents, then `scenario-agent` to produce a YAML scenario with title, precondition, steps, and postcondition. Jira/Linear issue context is included automatically if `--key` is set.
116
+
117
+ **Output:** `.yaml` scenario file in `e2e/tests/[key]/`
118
+
119
+ ---
120
+
121
+ ### `levante generate [scenario]`
93
122
 
94
123
  Generate a Playwright `.test.ts` file from a YAML scenario.
95
124
 
@@ -97,118 +126,238 @@ Generate a Playwright `.test.ts` file from a YAML scenario.
97
126
  levante generate --key PROJ-101
98
127
  ```
99
128
 
100
- #### `levante refine [test]`
129
+ Uses `playwright-generator-agent` with scenario + project context. If Zephyr is configured, also generates a Zephyr test case export.
130
+
131
+ **Output:** `[key].test.ts` in `e2e/tests/[key]/`
132
+
133
+ ---
101
134
 
102
- Refactor a generated test with AI — replaces raw selectors, adds best practices.
135
+ ### `levante refine [test]`
136
+
137
+ Refactor a generated test with AI — replaces raw selectors, improves structure, applies project patterns.
103
138
 
104
139
  ```bash
105
140
  levante refine --key PROJ-101
106
141
  ```
107
142
 
108
- #### `levante test [test]`
143
+ Uses `refactor-agent`. Rewrites the test file in-place.
144
+
145
+ ---
146
+
147
+ ### `levante test [test]`
109
148
 
110
- Run the Playwright test with trace/video/screenshot capture.
149
+ Run the Playwright test with trace, video, and screenshot capture.
111
150
 
112
151
  ```bash
113
152
  levante test --key PROJ-101
153
+ levante test --key PROJ-101 --no-trace
114
154
  ```
115
155
 
116
- #### `levante heal [test]`
156
+ **Output:** Test execution logs, trace files in `e2e/traces/`
117
157
 
118
- Self-heal a failing test (up to 3 retries). Diagnoses failures and patches automatically.
158
+ ---
159
+
160
+ ### `levante heal [test]`
161
+
162
+ Self-heal a failing test. Diagnoses the failure, patches the test, and re-runs — up to 3 retries.
119
163
 
120
164
  ```bash
121
165
  levante heal --key PROJ-101
122
166
  ```
123
167
 
124
- #### `levante qa [test]`
168
+ Each attempt uses `self-healing-agent` with test content + error output + trace data to produce:
169
+ - `diagnosis` (failure type, root cause, confidence)
170
+ - `patchedTest` (updated test file)
171
+ - `changes` (summary of what was fixed)
172
+
173
+ Exits with error if all 3 attempts fail.
125
174
 
126
- Generate QA documentation (markdown and/or Zephyr XML).
175
+ ---
176
+
177
+ ### `levante qa [test]`
178
+
179
+ Generate QA documentation from the test and scenario.
127
180
 
128
181
  ```bash
129
182
  levante qa --key PROJ-101
130
183
  ```
131
184
 
132
- #### `levante run [session]`
185
+ Uses `qa-testcase-agent` with test + scenario + Jira context. Produces markdown and/or Zephyr export depending on `outputTarget`.
186
+
187
+ **Output:** `qa/[testId].md` and/or Zephyr JSON
188
+
189
+ ---
190
+
191
+ ### `levante run [session]`
133
192
 
134
- Run the full pipeline: record → transcribe → scenario → generate → refine → test → heal → qa.
193
+ Run the full pipeline: `record → transcribe → scenario → generate → refine → test → heal → qa`.
135
194
 
136
195
  ```bash
137
196
  levante run --key PROJ-101
138
- levante run --key PROJ-101 --from generate
197
+ levante run --key PROJ-101 --from generate # resume from a specific step
139
198
  levante run --key PROJ-101 --skip transcribe,heal
199
+ levante run --key PROJ-101 --no-voice # skip recording + transcription
140
200
  ```
141
201
 
142
202
  | Option | Description |
143
203
  |--------|-------------|
144
- | `--from <step>` | Start from a specific step |
145
- | `--skip <steps>` | Skip comma-separated steps |
204
+ | `--from <step>` | Start from a specific step (skips earlier steps) |
205
+ | `--skip <steps>` | Comma-separated step names to skip |
206
+ | `--no-voice` | Disable voice recording (skips transcription too) |
207
+ | `--no-trace` | Disable trace capture |
146
208
 
147
- ## MCP Server
209
+ **Steps (in order):** `record` → `transcribe` → `scenario` → `generate` → `refine` → `test` → `heal` → `qa`
148
210
 
149
- Levante includes an MCP server for use with AI coding assistants.
211
+ ---
150
212
 
151
- ### Claude Code
213
+ ### `levante auth`
214
+
215
+ Manage authentication with QA Intelligence.
152
216
 
153
217
  ```bash
154
- claude mcp add levante -- npx levante-mcp
218
+ levante auth login # open browser for OAuth sign-in
219
+ levante auth logout # revoke CLI token
220
+ levante auth status # show current auth status
221
+ levante auth switch # re-authenticate with a different org/project/app
155
222
  ```
156
223
 
157
- ### Manual Configuration
224
+ ---
225
+
226
+ ### `levante jira`
227
+
228
+ Interactive Jira integration for issue discovery and workflow launch.
158
229
 
159
- Add to your MCP client config (e.g., `claude_desktop_config.json`):
230
+ #### `levante jira browse`
160
231
 
232
+ Interactive browser — search, view, and select Jira issues to start a test workflow.
233
+
234
+ ```bash
235
+ levante jira browse
236
+ ```
237
+
238
+ Select an issue to:
239
+ - **run** — save context and launch `levante run --key <KEY>`
240
+ - **save** — save issue context without running
241
+ - **view** — display full issue details
242
+
243
+ #### `levante jira search <query>`
244
+
245
+ Search Jira issues by text or raw JQL.
246
+
247
+ ```bash
248
+ levante jira search "login flow"
249
+ levante jira search --jql "project = QA AND status = 'In Progress'"
250
+ levante jira search "dashboard" --max 10
251
+ ```
252
+
253
+ | Option | Description |
254
+ |--------|-------------|
255
+ | `<query>` | Text search query |
256
+ | `--jql <jql>` | Use raw JQL instead of text search |
257
+ | `--max <n>` | Maximum results to return (default: 20) |
258
+
259
+ #### `levante jira show <issueKey>`
260
+
261
+ Display full details for a Jira issue.
262
+
263
+ ```bash
264
+ levante jira show PROJ-101
265
+ levante jira show PROJ-101 --save # also save context for pipeline use
266
+ ```
267
+
268
+ | Option | Description |
269
+ |--------|-------------|
270
+ | `--save` | Save issue context to `.qai/levante/issues/[KEY].json` |
271
+
272
+ ---
273
+
274
+ ### `levante mcp`
275
+
276
+ Print MCP server setup instructions.
277
+
278
+ ```bash
279
+ levante mcp
280
+ ```
281
+
282
+ **Claude Code:**
283
+ ```bash
284
+ claude mcp add levante -- levante-mcp # project-scoped
285
+ claude mcp add levante -s user -- levante-mcp # global
286
+ ```
287
+
288
+ **Claude Desktop / other clients:**
161
289
  ```json
162
290
  {
163
291
  "mcpServers": {
164
- "levante": {
165
- "command": "npx",
166
- "args": ["levante-mcp"]
167
- }
292
+ "levante": { "command": "levante-mcp" }
168
293
  }
169
294
  }
170
295
  ```
171
296
 
172
- ### Available MCP Tools
297
+ **Available MCP tools:**
173
298
 
174
299
  | Tool | Description |
175
300
  |------|-------------|
176
301
  | `levante_plan_workflow` | Get ordered step list with prerequisite checks |
177
302
  | `levante_execute_step` | Execute a single pipeline step |
178
- | `levante_get_workflow_guide` | Get the full workflow guide |
303
+ | `levante_get_workflow_guide` | Read the full workflow guide |
179
304
  | `levante_scan_codebase` | Scan project for test infrastructure |
180
305
  | `levante_validate_context` | Validate context.md completeness |
181
306
  | `levante_read_agent` | Load an agent prompt by name |
182
- | `levante_get_example` | Get example context.md template |
307
+ | `levante_get_example` | Get an example context.md template |
183
308
  | `levante_scan_ast` | Run AST scanner |
184
309
  | `levante_scan_ast_detail` | Drill into routes/components/hooks |
185
310
  | `levante_build_qa_map` | Build and validate QA map |
186
311
  | `levante_read_qa_map` | Load existing QA map |
187
312
 
313
+ ---
314
+
315
+ ## Global Options
316
+
317
+ ```
318
+ -k, --key <KEY> Issue key (e.g. PROJ-101, LIN-42)
319
+ --provider <provider> LLM provider: openai | anthropic
320
+ --model <model> LLM model override
321
+ --verbose Verbose output
322
+ --no-voice Disable voice recording
323
+ --no-trace Disable trace capture
324
+ -v, --version Show version
325
+ -h, --help Show help
326
+ ```
327
+
328
+ ---
329
+
188
330
  ## Configuration
189
331
 
190
- Configuration lives in `.qai/levante/config.ts`:
332
+ Configuration lives in `.qai/levante/config.ts` (generated by `levante init`):
191
333
 
192
334
  ```typescript
193
- import { defineConfig } from 'levante/config';
335
+ import { defineConfig } from 'levante';
194
336
 
195
337
  export default defineConfig({
196
- inputSource: 'jira', // 'none' | 'jira' | 'linear'
197
- outputTarget: 'both', // 'markdown' | 'zephyr' | 'both'
338
+ inputSource: 'jira', // 'none' | 'jira' | 'linear'
339
+ outputTarget: 'both', // 'markdown' | 'zephyr' | 'both'
198
340
  baseUrl: 'http://localhost:3000',
199
341
 
200
342
  llm: {
201
- provider: 'openai', // 'openai' | 'anthropic'
202
- model: 'gpt-4o', // Override default model
203
- agentModels: { // Per-agent overrides
343
+ provider: 'openai', // 'openai' | 'anthropic'
344
+ model: 'gpt-4o',
345
+ agentModels: { // per-agent model overrides
204
346
  'scenario-agent': 'claude-sonnet-4-20250514',
205
347
  },
206
348
  },
207
349
 
208
350
  playwright: {
209
- browser: 'chromium',
351
+ browser: 'chromium', // 'chromium' | 'firefox' | 'webkit'
210
352
  timeout: 120_000,
211
- traceMode: 'on',
353
+ retries: 0,
354
+ traceMode: 'on', // 'on' | 'off' | 'retain-on-failure'
355
+ },
356
+
357
+ voice: {
358
+ enabled: true,
359
+ engine: 'webspeech', // 'webspeech' | 'whisper'
360
+ language: 'en-US',
212
361
  },
213
362
 
214
363
  paths: {
@@ -221,36 +370,67 @@ export default defineConfig({
221
370
  },
222
371
 
223
372
  integrations: {
224
- jira: { /* ... */ },
373
+ jira: { /* Jira config */ },
225
374
  zephyr: { titlePrefix: 'UI Automation' },
226
375
  },
227
376
 
228
377
  push: {
229
- apiUrl: 'https://your-api.com/qa-map',
378
+ apiUrl: 'https://qaligent.space/api',
230
379
  apiKey: 'your-key',
231
380
  },
232
381
  });
233
382
  ```
234
383
 
235
- ## Project Structure
384
+ ---
385
+
386
+ ## AI Agents
387
+
388
+ Levante ships with agents copied to `.qai/levante/agents/` on `init`. Edit them to tune AI behavior for your project.
236
389
 
237
- After initialization, levante creates:
390
+ | Agent | Purpose |
391
+ |-------|---------|
392
+ | `0.init-agent` | Generate project `context.md` |
393
+ | `1_1.transcript-agent` | Analyze voice transcript + codegen into narrative |
394
+ | `1_2.scenario-agent` | Generate YAML scenario from narrative and actions |
395
+ | `2.playwright-generator-agent` | Generate Playwright test from YAML scenario |
396
+ | `3.refactor-agent` | Refactor test code for quality and conventions |
397
+ | `4.self-healing-agent` | Diagnose and patch failing tests |
398
+ | `5.qa-testcase-agent` | Generate QA documentation and Zephyr export |
399
+
400
+ Add `.qai/levante/context.md` to inject project-specific context into every agent call.
401
+
402
+ ---
403
+
404
+ ## Project Structure
238
405
 
239
406
  ```
240
407
  your-project/
241
408
  .qai/levante/
242
- config.ts # Configuration
243
- context.md # Project context for AI agents
244
- agents/ # Agent prompts (customizable)
245
- workflow.md # Pipeline workflow guide
409
+ config.ts # Configuration
410
+ context.md # Project context for AI agents
411
+ agents/ # Agent prompts (customizable)
412
+ workflow.md # Pipeline workflow guide
413
+ issues/ # Saved Jira issue contexts
246
414
  e2e/
247
- tests/{key}/ # Generated test files
248
- recordings/ # Codegen + voice recordings
249
- transcripts/ # Transcription output
250
- traces/ # Playwright traces
251
- qa/ # QA documentation output
415
+ tests/{key}/ # Generated test files + scenario YAML
416
+ recordings/ # Codegen + voice recordings
417
+ transcripts/ # Transcription output
418
+ traces/ # Playwright traces
419
+ qa/ # QA documentation output
252
420
  ```
253
421
 
422
+ ---
423
+
424
+ ## Interactive TUI
425
+
426
+ Running `levante` with no arguments launches an interactive terminal UI (requires TTY, ≥ 60×20).
427
+
428
+ ```bash
429
+ levante
430
+ ```
431
+
432
+ ---
433
+
254
434
  ## License
255
435
 
256
436
  MIT
package/dist/cli.js CHANGED
@@ -57497,19 +57497,22 @@ async function launchTui(program2) {
57497
57497
  version: version2,
57498
57498
  onRunCommand: (argv) => {
57499
57499
  runningCommand = true;
57500
- instance.waitUntilExit().then(async () => {
57501
- process.stdin.resume();
57502
- process.stdout.write(`
57500
+ instance.waitUntilExit().then(() => {
57501
+ setTimeout(async () => {
57502
+ process.stdin.resume();
57503
+ process.stdin.ref();
57504
+ process.stdout.write(`
57503
57505
  $ levante ${argv.join(" ")}
57504
57506
 
57505
57507
  `);
57506
- try {
57507
- await program2.parseAsync(argv, { from: "user" });
57508
- } catch (err) {
57509
- if (err?.name !== "ExitPromptError")
57510
- throw err;
57511
- }
57512
- done();
57508
+ try {
57509
+ await program2.parseAsync(argv, { from: "user" });
57510
+ } catch (err) {
57511
+ if (err?.name !== "ExitPromptError")
57512
+ throw err;
57513
+ }
57514
+ done();
57515
+ }, 50);
57513
57516
  });
57514
57517
  },
57515
57518
  onQuit: done
@@ -75507,54 +75510,61 @@ async function migrateFromLegacy(projectRoot, nonInteractive) {
75507
75510
  // src/commands/init.ts
75508
75511
  function registerInit(program2) {
75509
75512
  program2.command("init").description("Initialize levante configuration for your project").option("--non-interactive", "Skip interactive prompts, use defaults").action(async (cmdOpts) => {
75510
- const projectRoot = getProjectRoot();
75511
- const nonInteractive = !!cmdOpts?.nonInteractive;
75512
- header("levante init");
75513
- await migrateFromLegacy(projectRoot, nonInteractive);
75514
- const qaiDir = join17(projectRoot, CONFIG_DIR);
75515
- const configPath = join17(qaiDir, "config.ts");
75516
- const isReInit = fileExists(configPath);
75517
- if (isReInit) {
75518
- info(`Existing ${CONFIG_DIR}/ detected — preserving config and context.
75513
+ try {
75514
+ const projectRoot = getProjectRoot();
75515
+ const nonInteractive = !!cmdOpts?.nonInteractive;
75516
+ header("levante init");
75517
+ await migrateFromLegacy(projectRoot, nonInteractive);
75518
+ const qaiDir = join17(projectRoot, CONFIG_DIR);
75519
+ const configPath = join17(qaiDir, "config.ts");
75520
+ const isReInit = fileExists(configPath);
75521
+ if (isReInit) {
75522
+ info(`Existing ${CONFIG_DIR}/ detected — preserving config and context.
75519
75523
  `);
75520
- await copyAgentsToLocal(projectRoot, nonInteractive);
75521
- await copyWorkflowGuide(projectRoot, nonInteractive);
75522
- } else {
75523
- const answers = nonInteractive ? getDefaultAnswers() : await askConfigQuestions();
75524
- const config2 = buildConfigFromAnswers(answers);
75525
- writeFile(configPath, generateConfigFile(config2));
75526
- success2(`Config written: ${configPath}`);
75527
- await copyAgentsToLocal(projectRoot, nonInteractive);
75528
- await copyWorkflowGuide(projectRoot, nonInteractive);
75529
- }
75530
- if (!nonInteractive) {
75531
- const { confirm: confirmPrompt } = await Promise.resolve().then(() => (init_dist9(), exports_dist));
75532
- const connectAuth = await confirmPrompt({
75533
- message: "Connect to QA Intelligence?",
75534
- default: false
75535
- });
75536
- if (connectAuth) {
75537
- try {
75538
- const { login: login2 } = await Promise.resolve().then(() => (init_dist10(), exports_dist2));
75539
- const session = await login2({ webappUrl: "https://qaligent.space" });
75540
- success2(`Authenticated as ${session.user.email}`);
75541
- success2(`Linked to: ${session.scope.orgSlug} / ${session.scope.projectSlug} / ${session.scope.appSlug}`);
75542
- } catch (err) {
75543
- warn(`Authentication skipped: ${err instanceof Error ? err.message : "unknown error"}`);
75524
+ await copyAgentsToLocal(projectRoot, nonInteractive);
75525
+ await copyWorkflowGuide(projectRoot, nonInteractive);
75526
+ } else {
75527
+ const answers = nonInteractive ? getDefaultAnswers() : await askConfigQuestions();
75528
+ const config2 = buildConfigFromAnswers(answers);
75529
+ writeFile(configPath, generateConfigFile(config2));
75530
+ success2(`Config written: ${configPath}`);
75531
+ await copyAgentsToLocal(projectRoot, nonInteractive);
75532
+ await copyWorkflowGuide(projectRoot, nonInteractive);
75533
+ }
75534
+ if (!nonInteractive) {
75535
+ const { confirm: confirmPrompt } = await Promise.resolve().then(() => (init_dist9(), exports_dist));
75536
+ const connectAuth = await confirmPrompt({
75537
+ message: "Connect to QA Intelligence?",
75538
+ default: false
75539
+ });
75540
+ if (connectAuth) {
75541
+ try {
75542
+ const { login: login2 } = await Promise.resolve().then(() => (init_dist10(), exports_dist2));
75543
+ const session = await login2({ webappUrl: "https://qaligent.space" });
75544
+ success2(`Authenticated as ${session.user.email}`);
75545
+ success2(`Linked to: ${session.scope.orgSlug} / ${session.scope.projectSlug} / ${session.scope.appSlug}`);
75546
+ } catch (err) {
75547
+ warn(`Authentication skipped: ${err instanceof Error ? err.message : "unknown error"}`);
75548
+ }
75544
75549
  }
75545
75550
  }
75546
- }
75547
- console.log("");
75548
- success2(`Initialization complete!
75551
+ console.log("");
75552
+ success2(`Initialization complete!
75549
75553
  `);
75550
- if (!isReInit) {
75551
- console.log(import_picocolors5.default.bold("Next steps:"));
75552
- console.log(` 1. Use the ${import_picocolors5.default.cyan("init-agent")} in your AI tool to generate ${import_picocolors5.default.cyan(`${CONFIG_DIR}/context.md`)}`);
75553
- console.log(` (or use the MCP server: ${import_picocolors5.default.cyan("levante_scan_codebase")} + ${import_picocolors5.default.cyan("levante_read_agent")})`);
75554
- console.log(` 2. Review the generated ${import_picocolors5.default.cyan(`${CONFIG_DIR}/context.md`)}`);
75555
- console.log(` 3. Run: ${import_picocolors5.default.cyan("levante run --key PROJ-101")}`);
75556
- } else {
75557
- console.log(import_picocolors5.default.dim("Config and context.md were preserved. Only agents and workflow were checked."));
75554
+ if (!isReInit) {
75555
+ console.log(import_picocolors5.default.bold("Next steps:"));
75556
+ console.log(` 1. Use the ${import_picocolors5.default.cyan("init-agent")} in your AI tool to generate ${import_picocolors5.default.cyan(`${CONFIG_DIR}/context.md`)}`);
75557
+ console.log(` (or use the MCP server: ${import_picocolors5.default.cyan("levante_scan_codebase")} + ${import_picocolors5.default.cyan("levante_read_agent")})`);
75558
+ console.log(` 2. Review the generated ${import_picocolors5.default.cyan(`${CONFIG_DIR}/context.md`)}`);
75559
+ console.log(` 3. Run: ${import_picocolors5.default.cyan("levante run --key PROJ-101")}`);
75560
+ } else {
75561
+ console.log(import_picocolors5.default.dim("Config and context.md were preserved. Only agents and workflow were checked."));
75562
+ }
75563
+ } catch (err) {
75564
+ if (err?.name === "ExitPromptError") {
75565
+ process.exit(0);
75566
+ }
75567
+ throw err;
75558
75568
  }
75559
75569
  });
75560
75570
  }
package/dist/mcp.js CHANGED
@@ -28540,7 +28540,7 @@ class StdioServerTransport {
28540
28540
  // src/mcp.ts
28541
28541
  import { execSync } from "node:child_process";
28542
28542
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
28543
- import { join as join5 } from "node:path";
28543
+ import { join as join6 } from "node:path";
28544
28544
 
28545
28545
  // src/agents/loadAgent.ts
28546
28546
  import { readFileSync, existsSync as existsSync2, readdirSync } from "node:fs";
@@ -28726,6 +28726,44 @@ function parseSections(body) {
28726
28726
  return sections;
28727
28727
  }
28728
28728
 
28729
+ // src/config/paths.ts
28730
+ import { join as join3 } from "node:path";
28731
+ function resolvePaths(config2, key) {
28732
+ const root = getProjectRoot();
28733
+ const workingDir = join3(root, config2.paths.workingDir);
28734
+ const testsDir = join3(root, config2.paths.tests);
28735
+ const recordingsDir = join3(root, config2.paths.recordings);
28736
+ const transcriptsDir = join3(root, config2.paths.transcripts);
28737
+ const tracesDir = join3(root, config2.paths.traces);
28738
+ const qaDir = join3(root, config2.paths.qaOutput);
28739
+ const keyDir = key ? join3(workingDir, key) : null;
28740
+ const testDir = key ? join3(testsDir, key) : null;
28741
+ const testFile = key ? join3(testsDir, key, `${key}.test.ts`) : null;
28742
+ const scenarioDir = key ? join3(testsDir, key) : null;
28743
+ const scenarioFile = key ? join3(testsDir, key, `${key}.yaml`) : null;
28744
+ const qaFile = key ? join3(qaDir, `${key}.md`) : null;
28745
+ const needsZephyr = config2.outputTarget === "zephyr" || config2.outputTarget === "both";
28746
+ const zephyrJsonFile = needsZephyr && key ? join3(workingDir, key, `${key}-zephyr-test-case.json`) : null;
28747
+ const zephyrXmlFile = needsZephyr && key ? join3(testsDir, key, `${key}-zephyr-import.xml`) : null;
28748
+ return {
28749
+ projectRoot: root,
28750
+ workingDir,
28751
+ keyDir,
28752
+ testsDir,
28753
+ testDir,
28754
+ testFile,
28755
+ scenarioDir,
28756
+ scenarioFile,
28757
+ recordingsDir,
28758
+ transcriptsDir,
28759
+ tracesDir,
28760
+ qaDir,
28761
+ qaFile,
28762
+ zephyrJsonFile,
28763
+ zephyrXmlFile
28764
+ };
28765
+ }
28766
+
28729
28767
  // src/scanner/scanner.ts
28730
28768
  import { readFileSync as readFileSync3, existsSync as existsSync3, mkdirSync, writeFileSync } from "node:fs";
28731
28769
  import { resolve as resolve2, relative as relative2 } from "node:path";
@@ -32218,10 +32256,10 @@ class TypeScriptParser {
32218
32256
 
32219
32257
  // src/scanner/extractors/routes.ts
32220
32258
  import { readdirSync as readdirSync2, statSync, readFileSync as readFileSync2 } from "node:fs";
32221
- import { join as join3, relative, basename, dirname as dirname2 } from "node:path";
32259
+ import { join as join4, relative, basename, dirname as dirname2 } from "node:path";
32222
32260
  function extractRoutes(scanDir) {
32223
32261
  const routes = [];
32224
- const appDirs = ["app", "src/app"].map((d) => join3(scanDir, d));
32262
+ const appDirs = ["app", "src/app"].map((d) => join4(scanDir, d));
32225
32263
  for (const appDir of appDirs) {
32226
32264
  try {
32227
32265
  if (statSync(appDir).isDirectory()) {
@@ -32229,7 +32267,7 @@ function extractRoutes(scanDir) {
32229
32267
  }
32230
32268
  } catch {}
32231
32269
  }
32232
- const pagesDirs = ["pages", "src/pages"].map((d) => join3(scanDir, d));
32270
+ const pagesDirs = ["pages", "src/pages"].map((d) => join4(scanDir, d));
32233
32271
  for (const pagesDir of pagesDirs) {
32234
32272
  try {
32235
32273
  if (statSync(pagesDir).isDirectory()) {
@@ -32242,7 +32280,7 @@ function extractRoutes(scanDir) {
32242
32280
  function extractAppRouterRoutes(dir, baseDir, routes) {
32243
32281
  const entries = readdirSync2(dir, { withFileTypes: true });
32244
32282
  for (const entry of entries) {
32245
- const fullPath = join3(dir, entry.name);
32283
+ const fullPath = join4(dir, entry.name);
32246
32284
  if (entry.isDirectory()) {
32247
32285
  extractAppRouterRoutes(fullPath, baseDir, routes);
32248
32286
  continue;
@@ -32278,7 +32316,7 @@ function extractAppRouterRoutes(dir, baseDir, routes) {
32278
32316
  function extractPagesRouterRoutes(dir, baseDir, routes) {
32279
32317
  const entries = readdirSync2(dir, { withFileTypes: true });
32280
32318
  for (const entry of entries) {
32281
- const fullPath = join3(dir, entry.name);
32319
+ const fullPath = join4(dir, entry.name);
32282
32320
  if (entry.isDirectory()) {
32283
32321
  if (entry.name === "api") {
32284
32322
  extractPagesApiRoutes(fullPath, baseDir, routes);
@@ -32304,7 +32342,7 @@ function extractPagesRouterRoutes(dir, baseDir, routes) {
32304
32342
  function extractPagesApiRoutes(dir, baseDir, routes) {
32305
32343
  const entries = readdirSync2(dir, { withFileTypes: true });
32306
32344
  for (const entry of entries) {
32307
- const fullPath = join3(dir, entry.name);
32345
+ const fullPath = join4(dir, entry.name);
32308
32346
  if (entry.isDirectory()) {
32309
32347
  extractPagesApiRoutes(fullPath, baseDir, routes);
32310
32348
  continue;
@@ -32332,7 +32370,7 @@ function findLayoutFile(dir, baseDir) {
32332
32370
  let current = dir;
32333
32371
  while (current.startsWith(baseDir)) {
32334
32372
  for (const ext of [".tsx", ".ts", ".jsx", ".js"]) {
32335
- const layoutPath = join3(current, `layout${ext}`);
32373
+ const layoutPath = join4(current, `layout${ext}`);
32336
32374
  try {
32337
32375
  statSync(layoutPath);
32338
32376
  return layoutPath;
@@ -32793,7 +32831,7 @@ function requireEnum(obj, field, allowed, context, errors3) {
32793
32831
 
32794
32832
  // src/utils/scan.ts
32795
32833
  import { readdirSync as readdirSync3, existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
32796
- import { join as join4, relative as relative3 } from "node:path";
32834
+ import { join as join5, relative as relative3 } from "node:path";
32797
32835
  async function scanCodebase(root) {
32798
32836
  const scan = {
32799
32837
  testFiles: [],
@@ -32812,7 +32850,7 @@ async function scanCodebase(root) {
32812
32850
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
32813
32851
  if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist")
32814
32852
  continue;
32815
- const full = join4(dir, entry.name);
32853
+ const full = join5(dir, entry.name);
32816
32854
  if (entry.isDirectory()) {
32817
32855
  files.push(...walk(full, depth + 1));
32818
32856
  } else {
@@ -32849,7 +32887,7 @@ async function scanCodebase(root) {
32849
32887
  }
32850
32888
  }
32851
32889
  for (const name of ["playwright.config.ts", "vitest.config.ts", "jest.config.ts", "tsconfig.json", "package.json"]) {
32852
- if (existsSync4(join4(root, name)))
32890
+ if (existsSync4(join5(root, name)))
32853
32891
  scan.configFiles.push(name);
32854
32892
  }
32855
32893
  return scan;
@@ -32898,38 +32936,60 @@ You have access to Levante, an AI-powered E2E test automation tool. Follow this
32898
32936
 
32899
32937
  NEVER run multiple pipeline steps at once. Each step is a separate job with its own context.
32900
32938
 
32939
+ ## Two Modes of Execution
32940
+
32941
+ Steps fall into two categories:
32942
+
32943
+ ### 1. Tool Steps — use \`levante_execute_step\`
32944
+ These steps do NOT require an LLM. Run them via \`levante_execute_step\`:
32945
+ - \`record\` — opens Playwright browser codegen (interactive)
32946
+ - \`test\` — runs Playwright tests
32947
+ - \`push\` — pushes QA map to remote API
32948
+ - \`scan\` — runs the filesystem/AST scan
32949
+
32950
+ ### 2. Agent Steps — YOU are the AI, do these yourself
32951
+ These steps require LLM reasoning. Do NOT call \`levante_execute_step\` for them. Instead, load the agent prompt and act on it directly:
32952
+
32953
+ | Step | Agent to load | Input to read | Output to write |
32954
+ |------|--------------|---------------|-----------------|
32955
+ | \`transcribe\` | \`transcript-agent\` | codegen file via \`levante_read_pipeline_file(key, "codegen")\` | transcript JSON via \`levante_write_pipeline_file\` |
32956
+ | \`scenario\` | \`scenario-agent\` | codegen + optional transcript | scenario YAML via \`levante_write_pipeline_file(key, "scenario", ...)\` |
32957
+ | \`generate\` | \`playwright-generator-agent\` | scenario YAML | test file via \`levante_write_pipeline_file(key, "test", ...)\` |
32958
+ | \`refine\` | \`refactor-agent\` | test file | updated test file (overwrite via \`levante_write_pipeline_file\`) |
32959
+ | \`heal\` | \`self-healing-agent\` | test file + error output | patched test file |
32960
+ | \`qa\` | \`qa-testcase-agent\` | test file + scenario | QA markdown via \`levante_write_pipeline_file(key, "qa", ...)\` |
32961
+ | \`analyze\` | \`feature-analyzer-agent\` | AST via \`levante_scan_ast_detail\` | QA map via \`levante_build_qa_map\` |
32962
+
32963
+ **Workflow for each agent step:**
32964
+ 1. Call \`levante_read_agent("<agent-name>")\` to load the system prompt and config.
32965
+ 2. Call \`levante_read_pipeline_file(key, "<type>")\` to load the input artifact.
32966
+ 3. Read \`.qai/levante/context.md\` if it exists — this is project context the agent needs.
32967
+ 4. Apply the agent system prompt to your own reasoning and produce the output.
32968
+ 5. Call \`levante_write_pipeline_file(key, "<type>", content)\` to save the output.
32969
+
32901
32970
  ## Protocol
32902
32971
 
32903
- 1. **Plan first.** Call \`levante_plan_workflow\` with the user's goal. This returns a structured todo list of steps.
32904
- 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.
32905
- 3. **Present the plan.** Show the user the ordered step list with descriptions. Ask for confirmation or adjustments before proceeding.
32906
- 4. **Execute one step at a time.** For each step in the approved plan:
32907
- a. Tell the user which step you're about to run and why.
32908
- b. Call \`levante_execute_step\` with the step name and parameters.
32909
- c. Report the result to the user (success, key output, any warnings).
32910
- d. If the step fails, stop and discuss with the user before continuing.
32911
- e. Move to the next step only after the current one succeeds.
32912
- 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:
32913
- - Receive only the context it needs (step name, key, relevant file paths)
32914
- - Call \`levante_execute_step\` to do its work
32915
- - Return the result to the orchestrator
32972
+ 1. **Plan first.** Call \`levante_plan_workflow\` with the user's goal to get a structured step list.
32973
+ 2. **Present the plan.** Show the user the ordered steps. Ask for confirmation before proceeding.
32974
+ 3. **Execute one step at a time.** For each step:
32975
+ a. Announce which step you're running and why.
32976
+ b. Use **Tool Steps** via \`levante_execute_step\`, or handle **Agent Steps** yourself (see above).
32977
+ c. Report the result. If a step fails, stop and discuss before continuing.
32978
+ 4. **Use subagents when available.** If your AI platform supports subagents (Claude Code Agent tool), dispatch each step as a dedicated subagent. Each subagent should receive only the context it needs.
32916
32979
 
32917
32980
  ## Step Dependencies
32918
32981
 
32919
- Steps produce artifacts that feed into later steps. The pipeline handles this automatically — each step picks up where the previous one left off. Do not skip steps unless the plan says a step can be skipped.
32982
+ Steps produce artifacts consumed by later steps. Do not skip steps unless the plan says a step can be skipped.
32920
32983
 
32921
32984
  ## Interactive Steps
32922
32985
 
32923
- The \`record\` step opens a browser and requires user interaction. When the plan includes \`record\`:
32924
- - Tell the user they need to interact with the browser window
32925
- - The step will block until they close the codegen window
32926
- - After recording completes, proceed with the next step
32986
+ The \`record\` step opens a browser. Tell the user to interact with it — the step blocks until they close the codegen window.
32927
32987
 
32928
32988
  ## When Things Fail
32929
32989
 
32930
- - If \`test\` fails and \`heal\` is in the plan, that's expected — heal will attempt to fix it
32931
- - If \`heal\` exhausts all retries, stop and show the user the last error output
32932
- - For any other failure, stop and ask the user how to proceed
32990
+ - If \`test\` fails and \`heal\` is in the plan, that's expected — do the \`heal\` agent step yourself.
32991
+ - If healing fails after multiple attempts, stop and show the user the last error.
32992
+ - For any other failure, stop and ask the user how to proceed.
32933
32993
 
32934
32994
  ## Available Workflows
32935
32995
 
@@ -32945,7 +33005,7 @@ Always use \`levante_plan_workflow\` to determine the right steps — don't gues
32945
33005
 
32946
33006
  ## Scanner Analysis (Interactive QA Map)
32947
33007
 
32948
- For deep codebase analysis and QA map generation, use the interactive scanner workflow instead of the CLI pipeline:
33008
+ For deep codebase analysis and QA map generation, use the interactive scanner workflow:
32949
33009
 
32950
33010
  1. **Load the protocol.** Call \`levante_read_agent("scanner-agent")\` — this returns the full interactive protocol.
32951
33011
  2. **Scan.** Call \`levante_scan_ast()\` to run the AST scanner and get a compact summary.
@@ -32954,8 +33014,6 @@ For deep codebase analysis and QA map generation, use the interactive scanner wo
32954
33014
  5. **Build.** Construct the QA map payload and validate with \`levante_build_qa_map({ dryRun: true })\`.
32955
33015
  6. **Write.** Once validated and approved, call \`levante_build_qa_map({ dryRun: false })\` to save.
32956
33016
  7. **Read existing.** Use \`levante_read_qa_map()\` to load a previously generated QA map for incremental updates.
32957
-
32958
- This approach is preferred over \`scan → analyze\` CLI steps because it allows interactive refinement with the user.
32959
33017
  `.trim();
32960
33018
  var TEST_PIPELINE_STEPS = [
32961
33019
  {
@@ -33043,41 +33101,20 @@ var SCANNER_PIPELINE_STEPS = [
33043
33101
  var ALL_STEPS = [...TEST_PIPELINE_STEPS, ...SCANNER_PIPELINE_STEPS];
33044
33102
  var STEP_REQUIREMENTS = {
33045
33103
  record: { envVars: [] },
33046
- transcribe: { envVars: [{ name: "OPENAI_API_KEY", reason: "Whisper transcription requires OpenAI API key" }] },
33047
- scenario: { envVars: [
33048
- { name: "OPENAI_API_KEY", reason: "LLM calls require OpenAI API key", onlyIf: () => getProvider() === "openai" },
33049
- { name: "ANTHROPIC_API_KEY", reason: "LLM calls require Anthropic API key", onlyIf: () => getProvider() === "anthropic" }
33050
- ] },
33051
- generate: { envVars: [
33052
- { name: "OPENAI_API_KEY", reason: "LLM calls require OpenAI API key", onlyIf: () => getProvider() === "openai" },
33053
- { name: "ANTHROPIC_API_KEY", reason: "LLM calls require Anthropic API key", onlyIf: () => getProvider() === "anthropic" }
33054
- ] },
33055
- refine: { envVars: [
33056
- { name: "OPENAI_API_KEY", reason: "LLM calls require OpenAI API key", onlyIf: () => getProvider() === "openai" },
33057
- { name: "ANTHROPIC_API_KEY", reason: "LLM calls require Anthropic API key", onlyIf: () => getProvider() === "anthropic" }
33058
- ] },
33104
+ transcribe: { envVars: [] },
33105
+ scenario: { envVars: [] },
33106
+ generate: { envVars: [] },
33107
+ refine: { envVars: [] },
33059
33108
  test: { envVars: [] },
33060
- heal: { envVars: [
33061
- { name: "OPENAI_API_KEY", reason: "LLM calls require OpenAI API key", onlyIf: () => getProvider() === "openai" },
33062
- { name: "ANTHROPIC_API_KEY", reason: "LLM calls require Anthropic API key", onlyIf: () => getProvider() === "anthropic" }
33063
- ] },
33064
- qa: { envVars: [
33065
- { name: "OPENAI_API_KEY", reason: "LLM calls require OpenAI API key", onlyIf: () => getProvider() === "openai" },
33066
- { name: "ANTHROPIC_API_KEY", reason: "LLM calls require Anthropic API key", onlyIf: () => getProvider() === "anthropic" }
33067
- ] },
33109
+ heal: { envVars: [] },
33110
+ qa: { envVars: [] },
33068
33111
  scan: { envVars: [] },
33069
- analyze: { envVars: [
33070
- { name: "OPENAI_API_KEY", reason: "LLM calls require OpenAI API key", onlyIf: () => getProvider() === "openai" },
33071
- { name: "ANTHROPIC_API_KEY", reason: "LLM calls require Anthropic API key", onlyIf: () => getProvider() === "anthropic" }
33072
- ] },
33112
+ analyze: { envVars: [] },
33073
33113
  push: { envVars: [
33074
33114
  { name: "E2E_AI_API_URL", reason: "Push requires API URL (set E2E_AI_API_URL or push.apiUrl in config)" },
33075
33115
  { name: "E2E_AI_API_KEY", reason: "Push requires API key (set E2E_AI_API_KEY or push.apiKey in config)" }
33076
33116
  ] }
33077
33117
  };
33078
- function getProvider() {
33079
- return process.env.AI_PROVIDER ?? "openai";
33080
- }
33081
33118
  function checkPrerequisites(stepNames) {
33082
33119
  const issueMap = new Map;
33083
33120
  for (const stepName of stepNames) {
@@ -33085,8 +33122,6 @@ function checkPrerequisites(stepNames) {
33085
33122
  if (!reqs)
33086
33123
  continue;
33087
33124
  for (const envReq of reqs.envVars) {
33088
- if (envReq.onlyIf && !envReq.onlyIf())
33089
- continue;
33090
33125
  if (!process.env[envReq.name]) {
33091
33126
  const key = `env:${envReq.name}`;
33092
33127
  if (issueMap.has(key)) {
@@ -33230,7 +33265,7 @@ function executeStep(stepName, options) {
33230
33265
  args.push(...options.extraArgs);
33231
33266
  }
33232
33267
  const pkgRoot = getPackageRoot();
33233
- const cliBin = join5(pkgRoot, "dist", "cli.js");
33268
+ const cliBin = join6(pkgRoot, "dist", "cli.js");
33234
33269
  const command = `node ${cliBin} ${args.join(" ")}`;
33235
33270
  try {
33236
33271
  const stdout = execSync(command, {
@@ -33257,7 +33292,7 @@ ${stderr}`,
33257
33292
  };
33258
33293
  }
33259
33294
  }
33260
- var server = new McpServer({ name: "levante", version: "1.5.1" }, { instructions: SERVER_INSTRUCTIONS });
33295
+ var server = new McpServer({ name: "levante", version: "1.6.0" }, { instructions: SERVER_INSTRUCTIONS });
33261
33296
  server.registerTool("levante_scan_codebase", {
33262
33297
  title: "Scan Codebase",
33263
33298
  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.",
@@ -33309,13 +33344,108 @@ server.registerTool("levante_read_agent", {
33309
33344
  };
33310
33345
  }
33311
33346
  });
33347
+ server.registerTool("levante_read_pipeline_file", {
33348
+ title: "Read Pipeline File",
33349
+ description: "Read a pipeline artifact for a given issue key. Use this before an agent step to load the input. " + 'Types: "codegen" (raw Playwright codegen), "transcript" (voice transcript JSON), ' + '"scenario" (YAML test scenario), "test" (Playwright .test.ts), "qa" (QA markdown), "context" (project context.md).',
33350
+ inputSchema: exports_external.object({
33351
+ key: exports_external.string().optional().describe('Issue key (e.g. PROJ-101). Not required for "context" type.'),
33352
+ type: exports_external.enum(["codegen", "transcript", "scenario", "test", "qa", "context"]).describe("Which artifact to read")
33353
+ })
33354
+ }, async ({ key, type }) => {
33355
+ try {
33356
+ let filePath = null;
33357
+ if (type === "context") {
33358
+ filePath = join6(process.cwd(), CONFIG_DIR, "context.md");
33359
+ } else {
33360
+ if (!key) {
33361
+ return {
33362
+ content: [{ type: "text", text: "Error: key is required for this file type" }],
33363
+ isError: true
33364
+ };
33365
+ }
33366
+ const config2 = await loadConfig();
33367
+ const paths = resolvePaths(config2, key);
33368
+ if (type === "codegen") {
33369
+ filePath = paths.keyDir ? join6(paths.keyDir, `${key}.codegen.ts`) : null;
33370
+ } else if (type === "transcript") {
33371
+ filePath = paths.keyDir ? join6(paths.keyDir, `${key}.transcript.json`) : null;
33372
+ } else if (type === "scenario") {
33373
+ filePath = paths.scenarioFile;
33374
+ } else if (type === "test") {
33375
+ filePath = paths.testFile;
33376
+ } else if (type === "qa") {
33377
+ filePath = paths.qaFile;
33378
+ }
33379
+ }
33380
+ if (!filePath) {
33381
+ return {
33382
+ content: [{ type: "text", text: JSON.stringify({ found: false, path: null, reason: "Could not resolve path" }) }]
33383
+ };
33384
+ }
33385
+ if (!existsSync5(filePath)) {
33386
+ return {
33387
+ content: [{ type: "text", text: JSON.stringify({ found: false, path: filePath }) }]
33388
+ };
33389
+ }
33390
+ const fileContent = readFileSync5(filePath, "utf-8");
33391
+ return {
33392
+ content: [{ type: "text", text: JSON.stringify({ found: true, path: filePath, content: fileContent }) }]
33393
+ };
33394
+ } catch (err) {
33395
+ const message = err instanceof Error ? err.message : String(err);
33396
+ return {
33397
+ content: [{ type: "text", text: `Error: ${message}` }],
33398
+ isError: true
33399
+ };
33400
+ }
33401
+ });
33402
+ server.registerTool("levante_write_pipeline_file", {
33403
+ title: "Write Pipeline File",
33404
+ description: "Write a pipeline artifact after completing an agent step. " + 'Types: "transcript" (voice transcript JSON), "scenario" (YAML test scenario), ' + '"test" (Playwright .test.ts), "qa" (QA markdown).',
33405
+ inputSchema: exports_external.object({
33406
+ key: exports_external.string().describe("Issue key (e.g. PROJ-101)"),
33407
+ type: exports_external.enum(["transcript", "scenario", "test", "qa"]).describe("Which artifact to write"),
33408
+ content: exports_external.string().describe("The file content to write")
33409
+ })
33410
+ }, async ({ key, type, content: fileContent }) => {
33411
+ try {
33412
+ const config2 = await loadConfig();
33413
+ const paths = resolvePaths(config2, key);
33414
+ const fileMap = {
33415
+ transcript: paths.keyDir ? join6(paths.keyDir, `${key}.transcript.json`) : null,
33416
+ scenario: paths.scenarioFile,
33417
+ test: paths.testFile,
33418
+ qa: paths.qaFile
33419
+ };
33420
+ const filePath = fileMap[type];
33421
+ if (!filePath) {
33422
+ return {
33423
+ content: [{ type: "text", text: `Error: Could not resolve path for type "${type}" with key "${key}"` }],
33424
+ isError: true
33425
+ };
33426
+ }
33427
+ const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync2 } = await import("node:fs");
33428
+ const { dirname: dirname4 } = await import("node:path");
33429
+ mkdirSync2(dirname4(filePath), { recursive: true });
33430
+ writeFileSync2(filePath, fileContent, "utf-8");
33431
+ return {
33432
+ content: [{ type: "text", text: JSON.stringify({ written: true, path: filePath }) }]
33433
+ };
33434
+ } catch (err) {
33435
+ const message = err instanceof Error ? err.message : String(err);
33436
+ return {
33437
+ content: [{ type: "text", text: `Error: ${message}` }],
33438
+ isError: true
33439
+ };
33440
+ }
33441
+ });
33312
33442
  server.registerTool("levante_get_example", {
33313
33443
  title: "Get Example Context",
33314
33444
  description: `Returns the full example context markdown file that shows the expected format for ${CONFIG_DIR}/context.md.`,
33315
33445
  inputSchema: exports_external.object({})
33316
33446
  }, async () => {
33317
33447
  try {
33318
- const examplePath = join5(getPackageRoot(), "templates", "e2e-ai.context.example.md");
33448
+ const examplePath = join6(getPackageRoot(), "templates", "e2e-ai.context.example.md");
33319
33449
  const content = readFileSync5(examplePath, "utf-8");
33320
33450
  return {
33321
33451
  content: [{ type: "text", text: content }]
@@ -33411,7 +33541,7 @@ server.registerTool("levante_get_workflow_guide", {
33411
33541
  inputSchema: exports_external.object({})
33412
33542
  }, async () => {
33413
33543
  try {
33414
- const guidePath = join5(getPackageRoot(), "templates", "workflow.md");
33544
+ const guidePath = join6(getPackageRoot(), "templates", "workflow.md");
33415
33545
  if (!existsSync5(guidePath)) {
33416
33546
  return {
33417
33547
  content: [{ type: "text", text: "Error: workflow.md not found in templates" }],
@@ -33442,15 +33572,15 @@ server.registerTool("levante_scan_ast", {
33442
33572
  const cwd = process.cwd();
33443
33573
  const dir = scanDir ?? "src";
33444
33574
  const scanConfig = {
33445
- scanDir: join5(cwd, dir),
33575
+ scanDir: join6(cwd, dir),
33446
33576
  include: include ?? ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
33447
33577
  exclude: exclude ?? ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/*.test.*", "**/*.spec.*", "**/__tests__/**"],
33448
- cacheDir: join5(cwd, CONFIG_DIR, "cache")
33578
+ cacheDir: join6(cwd, CONFIG_DIR, "cache")
33449
33579
  };
33450
33580
  const ast = await runStage1(scanConfig);
33451
- const astPath = join5(cwd, CONFIG_DIR, "ast-scan.json");
33581
+ const astPath = join6(cwd, CONFIG_DIR, "ast-scan.json");
33452
33582
  const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync2 } = await import("node:fs");
33453
- mkdirSync2(join5(cwd, CONFIG_DIR), { recursive: true });
33583
+ mkdirSync2(join6(cwd, CONFIG_DIR), { recursive: true });
33454
33584
  writeFileSync2(astPath, JSON.stringify(ast, null, 2));
33455
33585
  const summary2 = summarizeAST(ast, astPath);
33456
33586
  return {
@@ -33473,7 +33603,7 @@ server.registerTool("levante_scan_ast_detail", {
33473
33603
  })
33474
33604
  }, async ({ category, filter, limit }) => {
33475
33605
  try {
33476
- const astPath = join5(process.cwd(), CONFIG_DIR, "ast-scan.json");
33606
+ const astPath = join6(process.cwd(), CONFIG_DIR, "ast-scan.json");
33477
33607
  if (!existsSync5(astPath)) {
33478
33608
  return {
33479
33609
  content: [{ type: "text", text: "Error: No AST scan found. Run levante_scan_ast first." }],
@@ -33509,7 +33639,7 @@ server.registerTool("levante_build_qa_map", {
33509
33639
  payload.commitSha = sha;
33510
33640
  } catch {}
33511
33641
  }
33512
- const outputPath = output ?? join5(process.cwd(), CONFIG_DIR, "qa-map.json");
33642
+ const outputPath = output ?? join6(process.cwd(), CONFIG_DIR, "qa-map.json");
33513
33643
  let written = false;
33514
33644
  if (validation.valid && !dryRun) {
33515
33645
  const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync2 } = await import("node:fs");
@@ -33553,7 +33683,7 @@ server.registerTool("levante_read_qa_map", {
33553
33683
  })
33554
33684
  }, async ({ path }) => {
33555
33685
  try {
33556
- const mapPath = path ?? join5(process.cwd(), CONFIG_DIR, "qa-map.json");
33686
+ const mapPath = path ?? join6(process.cwd(), CONFIG_DIR, "qa-map.json");
33557
33687
  if (!existsSync5(mapPath)) {
33558
33688
  return {
33559
33689
  content: [{ type: "text", text: JSON.stringify(null) }]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "levante",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "levante": "./dist/cli.js",