pi-harness-runtime 0.3.2-beta.2 → 0.4.0

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/CHANGELOG.md CHANGED
@@ -1,11 +1,25 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to `pi-harness-runtime` will be documented in this file.
3
+ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
+ ## [0.4.0](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.3.1...v0.4.0) (2026-07-05)
7
6
 
8
- ## [Unreleased]
7
+
8
+ ### Features
9
+
10
+ * **auth:** Add curator-mode MiniMax browser authentication ([4914517](https://github.com/ManotLuijiu/pi-harness-runtime/commit/49145171d0205e78454472f5f4e540ff7c70a711))
11
+ * **auth:** Add MiniMax browser authentication prototype ([75e294e](https://github.com/ManotLuijiu/pi-harness-runtime/commit/75e294e31f582521a48b66d1ce05e1cc3f581750))
12
+ * **auth:** Add persistent browser profile auth for MiniMax ([f725d2b](https://github.com/ManotLuijiu/pi-harness-runtime/commit/f725d2b89aa61d6e1cd5de0458b4d2c80efb6e7a))
13
+ * Fixed Chrome ([98becb8](https://github.com/ManotLuijiu/pi-harness-runtime/commit/98becb8bed6b94612db648d4bdd58f8a45d23040))
14
+ * implement RFC-0019 through RFC-0022 ([4468dcb](https://github.com/ManotLuijiu/pi-harness-runtime/commit/4468dcb1cfcb320f8eeab50c8ef590de74de979a))
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * **auth:** Don't re-navigate after login detection ([133e6d5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/133e6d5ac852cc3048881eb4e28700c3aceca792))
20
+ * **auth:** Poll for content AND URL, log URL changes for debugging ([4a197fe](https://github.com/ManotLuijiu/pi-harness-runtime/commit/4a197febec24efe85568c3ce35286f45602f5e23))
21
+ * **auth:** Poll URL instead of stdin for login detection ([04ced9a](https://github.com/ManotLuijiu/pi-harness-runtime/commit/04ced9a76753e6b6d4b8e43c97fe19830ab08af1))
22
+ * **skill:** Add description field to harness-runtime SKILL.md ([d06d37b](https://github.com/ManotLuijiu/pi-harness-runtime/commit/d06d37becfd92e17ffe27dfbeac04da61f0d89df))
9
23
 
10
24
  ## [0.2.0] - 2026-06-29
11
25
 
package/README.md CHANGED
@@ -178,3 +178,23 @@ MIT © 2026 MooCoding
178
178
  - [pi-coding-agent](https://github.com/earendil-works/pi-coding-agent) — Underlying pi agent
179
179
  - [context-mode](https://github.com/MiniMax-AI/context-mode) — Context window tracking
180
180
  - [pi-web-access](https://github.com/nicobailon/pi-web-access) — Web search for pi
181
+
182
+ Day 6 adds runtime recovery and remote-observability architecture.
183
+
184
+ ## RFCs
185
+
186
+ - RFC-0019 Auto Compact and Continue
187
+ - RFC-0020 Output Token Limit Handler
188
+ - RFC-0021 Partial Response Recovery
189
+ - RFC-0022 Notification Center
190
+ - RFC-0023 Local Browser Agent
191
+
192
+ ## Why this matters
193
+
194
+ These RFCs address real runtime failures:
195
+
196
+ - MiniMax auto-compacts but does not automatically resume.
197
+ - A model may stop because max output token limit is reached.
198
+ - Partial responses must be preserved and continued.
199
+ - The human needs mobile/tablet notifications.
200
+ - Headless servers cannot open interactive login windows for MiniMax usage console.
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Auto Compact and Continue — RFC-0019
3
+ *
4
+ * Automatically resume work after model/session compaction without requiring
5
+ * human intervention. Keeps the human out of the message-bus role.
6
+ *
7
+ * Artifact Layout:
8
+ * harness/context/
9
+ * compaction_events.jsonl
10
+ * latest_compaction_summary.md
11
+ * continue_prompt.md
12
+ */
13
+
14
+ import {
15
+ appendFileSync,
16
+ existsSync,
17
+ mkdirSync,
18
+ readFileSync,
19
+ writeFileSync,
20
+ } from "node:fs";
21
+ import { join } from "node:path";
22
+ import { homedir } from "node:os";
23
+
24
+ export interface CompactionEvent {
25
+ timestamp: string;
26
+ jobId: string;
27
+ taskId: string;
28
+ compactedFromTokens: number;
29
+ reason: string;
30
+ errorMessage?: string;
31
+ partialOutput?: string;
32
+ continuePrompt?: string;
33
+ }
34
+
35
+ export interface CompactionConfig {
36
+ jobId: string;
37
+ rootDir?: string;
38
+ maxContinueAttempts?: number;
39
+ continuationBackoffMs?: number;
40
+ }
41
+
42
+ export interface ContinuePrompt {
43
+ taskId: string;
44
+ originalRequirement: string;
45
+ whatWasCompleted: string;
46
+ whatNeedsToBeDone: string;
47
+ partialFiles: string[];
48
+ nextSteps: string;
49
+ }
50
+
51
+ const COMPACTION_PATTERNS = [
52
+ /\[compaction\]/i,
53
+ /compacted from \d+[,.]?\d* tokens/i,
54
+ /model stopped because it reached the maximum output token limit/i,
55
+ /error.*output.*token.*limit/i,
56
+ /context.*truncated/i,
57
+ /session.*compact/i,
58
+ ];
59
+
60
+ const CONTINUE_MARKERS = [
61
+ /would you like me to continue/i,
62
+ /should i continue/i,
63
+ /type.*continue/i,
64
+ /\[continue\]/i,
65
+ /waiting for your response/i,
66
+ ];
67
+
68
+ export class AutoCompactEngine {
69
+ private readonly rootDir: string;
70
+ private readonly jobId: string;
71
+ private readonly maxAttempts: number;
72
+ private readonly backoffMs: number;
73
+ private continueAttempts = 0;
74
+
75
+ constructor(config: CompactionConfig) {
76
+ this.rootDir =
77
+ config.rootDir ??
78
+ join(homedir(), ".pi", "harness", config.jobId, "context");
79
+ this.jobId = config.jobId;
80
+ this.maxAttempts = config.maxContinueAttempts ?? 5;
81
+ this.backoffMs = config.continuationBackoffMs ?? 1000;
82
+ }
83
+
84
+ /**
85
+ * Detect if output contains compaction markers
86
+ */
87
+ detectCompaction(output: string): boolean {
88
+ return COMPACTION_PATTERNS.some((pattern) => pattern.test(output));
89
+ }
90
+
91
+ /**
92
+ * Detect if output is waiting for continue signal
93
+ */
94
+ isWaitingForContinue(output: string): boolean {
95
+ return CONTINUE_MARKERS.some((pattern) => pattern.test(output));
96
+ }
97
+
98
+ /**
99
+ * Parse compaction details from output
100
+ */
101
+ parseCompactionEvent(output: string, taskId: string): CompactionEvent | null {
102
+ const lines = output.split("\n");
103
+ let compactedFromTokens = 0;
104
+ let reason = "unknown";
105
+ let errorMessage: string | undefined;
106
+
107
+ // Extract compacted token count
108
+ const tokenMatch = output.match(
109
+ /compacted from ([,\d]+(?:\.\d+)?)\s*tokens/i,
110
+ );
111
+ if (tokenMatch) {
112
+ compactedFromTokens = parseInt(tokenMatch[1].replace(/,/g, ""), 10);
113
+ }
114
+
115
+ // Extract reason
116
+ if (/output.*token.*limit/i.test(output)) {
117
+ reason = "output_token_limit";
118
+ } else if (/context.*truncated/i.test(output)) {
119
+ reason = "context_truncated";
120
+ } else if (/session.*compact/i.test(output)) {
121
+ reason = "session_compact";
122
+ }
123
+
124
+ // Extract error message
125
+ const errorMatch = output.match(/error[:\s]+(.+)/i);
126
+ if (errorMatch) {
127
+ errorMessage = errorMatch[1].trim();
128
+ }
129
+
130
+ return {
131
+ timestamp: new Date().toISOString(),
132
+ jobId: this.jobId,
133
+ taskId,
134
+ compactedFromTokens,
135
+ reason,
136
+ errorMessage,
137
+ partialOutput: output,
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Save compaction artifact
143
+ */
144
+ saveCompactionArtifact(event: CompactionEvent): void {
145
+ this.ensureDir();
146
+
147
+ // Append to events log
148
+ const eventsPath = join(this.rootDir, "compaction_events.jsonl");
149
+ appendFileSync(eventsPath, JSON.stringify(event) + "\n", "utf-8");
150
+
151
+ // Write latest summary
152
+ const summaryPath = join(this.rootDir, "latest_compaction_summary.md");
153
+ const summary = this.generateSummary(event);
154
+ writeFileSync(summaryPath, summary, "utf-8");
155
+ }
156
+
157
+ /**
158
+ * Generate continue prompt
159
+ */
160
+ generateContinuePrompt(
161
+ taskId: string,
162
+ completedWork: string,
163
+ remainingWork: string,
164
+ ): ContinuePrompt {
165
+ const prompt: ContinuePrompt = {
166
+ taskId,
167
+ originalRequirement: this.getRequirement(),
168
+ whatWasCompleted: completedWork,
169
+ whatNeedsToBeDone: remainingWork,
170
+ partialFiles: this.findPartialFiles(),
171
+ nextSteps: remainingWork,
172
+ };
173
+
174
+ const promptPath = join(this.rootDir, "continue_prompt.md");
175
+ const promptContent = this.formatContinuePrompt(prompt);
176
+ writeFileSync(promptPath, promptContent, "utf-8");
177
+
178
+ return prompt;
179
+ }
180
+
181
+ /**
182
+ * Check if we should continue (respects max attempts)
183
+ */
184
+ shouldContinue(): boolean {
185
+ this.continueAttempts++;
186
+ return this.continueAttempts <= this.maxAttempts;
187
+ }
188
+
189
+ /**
190
+ * Get current continue attempt count
191
+ */
192
+ getContinueAttempts(): number {
193
+ return this.continueAttempts;
194
+ }
195
+
196
+ /**
197
+ * Reset continue attempts
198
+ */
199
+ resetAttempts(): void {
200
+ this.continueAttempts = 0;
201
+ }
202
+
203
+ /**
204
+ * Build the continue message for the next turn
205
+ */
206
+ buildContinueMessage(): string {
207
+ const promptPath = join(this.rootDir, "continue_prompt.md");
208
+ if (existsSync(promptPath)) {
209
+ const prompt = readFileSync(promptPath, "utf-8");
210
+ return `continue\n\n${prompt}`;
211
+ }
212
+ return "continue";
213
+ }
214
+
215
+ // ─── Private Methods ────────────────────────────────────────────────
216
+
217
+ private ensureDir(): void {
218
+ if (!existsSync(this.rootDir)) {
219
+ mkdirSync(this.rootDir, { recursive: true });
220
+ }
221
+ }
222
+
223
+ private generateSummary(event: CompactionEvent): string {
224
+ return `# Compaction Summary
225
+
226
+ **Time:** ${event.timestamp}
227
+ **Job:** ${event.jobId}
228
+ **Task:** ${event.taskId}
229
+ **Reason:** ${event.reason}
230
+ **Tokens Compacted:** ${event.compactedFromTokens.toLocaleString()}
231
+
232
+ ${event.errorMessage ? `**Error:** ${event.errorMessage}` : ""}
233
+
234
+ ## What Happened
235
+
236
+ Session was compacted due to ${event.reason}.
237
+
238
+ ${
239
+ event.compactedFromTokens > 0
240
+ ? `Context was reduced from approximately ${event.compactedFromTokens.toLocaleString()} tokens.`
241
+ : "Context was compacted."
242
+ }
243
+
244
+ ## Next Step
245
+
246
+ Runtime will automatically continue this task.
247
+
248
+ Continue attempts: ${this.continueAttempts}/${this.maxAttempts}
249
+ `;
250
+ }
251
+
252
+ private formatContinuePrompt(prompt: ContinuePrompt): string {
253
+ const lines = [
254
+ "# Continue Previous Task",
255
+ "",
256
+ "## Task Context",
257
+ "",
258
+ "**Requirement:** " + prompt.originalRequirement,
259
+ "",
260
+ "## What Was Completed",
261
+ "",
262
+ prompt.whatWasCompleted ||
263
+ "Work was in progress when session was compacted.",
264
+ "",
265
+ ];
266
+
267
+ if (prompt.partialFiles.length > 0) {
268
+ lines.push("## Partial Files Created");
269
+ lines.push("");
270
+ for (const file of prompt.partialFiles) {
271
+ lines.push(`- ${file}`);
272
+ }
273
+ lines.push("");
274
+ }
275
+
276
+ lines.push("## What Needs To Be Done");
277
+ lines.push("");
278
+ lines.push(
279
+ prompt.whatNeedsToBeDone || "Continue the task from where it left off.",
280
+ );
281
+ lines.push("");
282
+ lines.push("## Instructions");
283
+ lines.push("");
284
+ lines.push("1. Review any partial files created");
285
+ lines.push("2. Continue from where the previous session ended");
286
+ lines.push("3. Complete the remaining work");
287
+ lines.push("4. Ensure all tests pass");
288
+ lines.push("");
289
+ lines.push("**Do not repeat work that was already completed.**");
290
+
291
+ return lines.join("\n");
292
+ }
293
+
294
+ private getRequirement(): string {
295
+ const checkpointPath = join(
296
+ homedir(),
297
+ ".pi",
298
+ "harness",
299
+ this.jobId,
300
+ "checkpoint.json",
301
+ );
302
+ if (existsSync(checkpointPath)) {
303
+ try {
304
+ const checkpoint = JSON.parse(readFileSync(checkpointPath, "utf-8"));
305
+ return checkpoint.requirement ?? "Unknown requirement";
306
+ } catch {
307
+ return "Unknown requirement";
308
+ }
309
+ }
310
+ return "Unknown requirement";
311
+ }
312
+
313
+ private findPartialFiles(): string[] {
314
+ const partialDir = join(this.rootDir, "..", "partial", this.jobId);
315
+ if (!existsSync(partialDir)) {
316
+ return [];
317
+ }
318
+
319
+ try {
320
+ const files = readFileSync(join(partialDir, "files.json"), "utf-8");
321
+ return JSON.parse(files) as string[];
322
+ } catch {
323
+ return [];
324
+ }
325
+ }
326
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * E2E Test Engine — RFC-0013
3
+ *
4
+ * Browser-based end-to-end testing before work is marked ready for client.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { E2ETestEngine, PlaywrightE2ERunner } from "./e2e/index.js";
9
+ *
10
+ * const runner = new PlaywrightE2ERunner({ headless: true });
11
+ * await runner.start();
12
+ *
13
+ * const engine = new E2ETestEngine("/tmp/e2e", {
14
+ * baseUrl: "http://localhost:3000",
15
+ * screenshotOnFailure: true,
16
+ * });
17
+ * engine.setRunner(runner);
18
+ *
19
+ * const scenario = engine.createScenario(
20
+ * "homepage-loads",
21
+ * "Homepage Loads",
22
+ * "Verify the homepage loads correctly",
23
+ * );
24
+ * engine.addStep(scenario, "navigate", { value: "http://localhost:3000" });
25
+ * engine.addStep(scenario, "wait", { selector: "h1" });
26
+ * engine.addStep(scenario, "assert", { assertCondition: "document.querySelector('h1') !== null" });
27
+ *
28
+ * const result = await engine.runScenario(scenario);
29
+ * console.log(result);
30
+ *
31
+ * await runner.stop();
32
+ * ```
33
+ */
34
+
35
+ export { E2ETestEngine } from "./test-engine.js";
36
+ export {
37
+ PlaywrightE2ERunner,
38
+ MiniMaxQuotaScraper,
39
+ } from "./playwright-runner.js";
40
+ export type {
41
+ E2ERunner,
42
+ PlaywrightRunnerConfig,
43
+ QuotaPageData,
44
+ } from "./playwright-runner.js";
@@ -7,35 +7,40 @@
7
7
  * Also used as the E2E runner for testing workflows.
8
8
  */
9
9
 
10
+ // Dynamic import for Playwright (optional dependency)
11
+ let playwrightModule: typeof import("playwright") | null = null;
12
+
13
+ async function getPlaywright() {
14
+ if (!playwrightModule) {
15
+ try {
16
+ playwrightModule = await import("playwright");
17
+ } catch {
18
+ console.warn(
19
+ "[PlaywrightRunner] Playwright not installed. E2E tests will be skipped.",
20
+ );
21
+ return null;
22
+ }
23
+ }
24
+ return playwrightModule;
25
+ }
26
+
10
27
  export interface PlaywrightRunnerConfig {
11
28
  headless?: boolean;
12
29
  slowMo?: number;
13
30
  timeout?: number;
14
31
  browser?: "chromium" | "firefox" | "webkit";
15
32
  profileDir?: string;
33
+ viewport?: { width: number; height: number };
16
34
  }
17
35
 
18
- export interface PlaywrightBrowser {
19
- new (config?: PlaywrightRunnerConfig): PlaywrightBrowserInstance;
20
- }
21
-
22
- export interface PlaywrightBrowserInstance {
23
- page(): PlaywrightPage;
24
- close(): Promise<void>;
25
- }
26
-
27
- export interface PlaywrightPage {
28
- goto(url: string): Promise<void>;
36
+ // Forward-compatible runner interface for E2E Test Engine
37
+ export interface E2ERunner {
38
+ navigate(url: string): Promise<void>;
29
39
  click(selector: string): Promise<void>;
30
- fill(selector: string, value: string): Promise<void>;
31
- waitForSelector(
32
- selector: string,
33
- options?: { timeout?: number },
34
- ): Promise<void>;
35
- screenshot(options?: { path?: string }): Promise<void>;
36
- evaluate<T>(fn: (...args: unknown[]) => T, ...args: unknown[]): Promise<T>;
37
- content(): Promise<string>;
38
- url(): Promise<string>;
40
+ type(selector: string, text: string): Promise<void>;
41
+ wait(selector: string, timeout?: number): Promise<void>;
42
+ screenshot(path: string): Promise<void>;
43
+ assert(condition: string, message?: string): Promise<boolean>;
39
44
  }
40
45
 
41
46
  export interface QuotaPageData {
@@ -81,10 +86,7 @@ export class MiniMaxQuotaScraper {
81
86
  /**
82
87
  * Wait for quota data to load
83
88
  */
84
- async waitForQuotaLoad(
85
- page: PlaywrightPage,
86
- timeoutMs?: number,
87
- ): Promise<void> {
89
+ async waitForQuotaLoad(page: any, timeoutMs?: number): Promise<void> {
88
90
  const selectors = [
89
91
  ".quota-used",
90
92
  ".usage-percentage",
@@ -133,11 +135,14 @@ export class MiniMaxQuotaScraper {
133
135
 
134
136
  /**
135
137
  * Playwright E2E Runner for testing workflows
138
+ *
139
+ * Real implementation using Playwright library.
136
140
  */
137
- export class PlaywrightE2ERunner {
141
+ export class PlaywrightE2ERunner implements E2ERunner {
138
142
  private config: PlaywrightRunnerConfig;
139
- private browser: PlaywrightBrowserInstance | null = null;
140
- private page: PlaywrightPage | null = null;
143
+ private browser: any = null;
144
+ private context: any = null;
145
+ private page: any = null;
141
146
 
142
147
  constructor(config: PlaywrightRunnerConfig = {}) {
143
148
  this.config = {
@@ -153,9 +158,30 @@ export class PlaywrightE2ERunner {
153
158
  * Start the browser
154
159
  */
155
160
  async start(): Promise<void> {
156
- // In a real implementation, this would launch Playwright
157
- // this.browser = await chromium.launch({ headless: this.config.headless });
158
- // this.page = await this.browser.newPage();
161
+ const pw = await getPlaywright();
162
+ if (!pw) {
163
+ throw new Error(
164
+ "Playwright not available. Install with: bun add playwright",
165
+ );
166
+ }
167
+
168
+ const browserType =
169
+ this.config.browser === "firefox"
170
+ ? pw.firefox
171
+ : this.config.browser === "webkit"
172
+ ? pw.webkit
173
+ : pw.chromium;
174
+
175
+ this.browser = await browserType.launch({
176
+ headless: this.config.headless,
177
+ slowMo: this.config.slowMo,
178
+ });
179
+
180
+ this.context = await this.browser.newContext({
181
+ viewport: this.config.viewport ?? { width: 1280, height: 720 },
182
+ });
183
+
184
+ this.page = await this.context.newPage();
159
185
  }
160
186
 
161
187
  /**
@@ -163,11 +189,15 @@ export class PlaywrightE2ERunner {
163
189
  */
164
190
  async stop(): Promise<void> {
165
191
  if (this.page) {
166
- // await this.page.close();
192
+ await this.page.close();
167
193
  this.page = null;
168
194
  }
195
+ if (this.context) {
196
+ await this.context.close();
197
+ this.context = null;
198
+ }
169
199
  if (this.browser) {
170
- // await this.browser.close();
200
+ await this.browser.close();
171
201
  this.browser = null;
172
202
  }
173
203
  }
@@ -177,7 +207,7 @@ export class PlaywrightE2ERunner {
177
207
  */
178
208
  async navigate(url: string): Promise<void> {
179
209
  if (!this.page) throw new Error("Browser not started");
180
- await this.page.goto(url);
210
+ await this.page.goto(url, { timeout: this.config.timeout });
181
211
  }
182
212
 
183
213
  /**
@@ -190,12 +220,12 @@ export class PlaywrightE2ERunner {
190
220
  }
191
221
 
192
222
  /**
193
- * Fill an input
223
+ * Type text into an input
194
224
  */
195
- async fill(selector: string, value: string): Promise<void> {
225
+ async type(selector: string, text: string): Promise<void> {
196
226
  if (!this.page) throw new Error("Browser not started");
197
227
  await this.page.waitForSelector(selector, { timeout: this.config.timeout });
198
- await this.page.fill(selector, value);
228
+ await this.page.fill(selector, text);
199
229
  }
200
230
 
201
231
  /**
@@ -213,23 +243,7 @@ export class PlaywrightE2ERunner {
213
243
  */
214
244
  async screenshot(path: string): Promise<void> {
215
245
  if (!this.page) throw new Error("Browser not started");
216
- await this.page.screenshot({ path });
217
- }
218
-
219
- /**
220
- * Evaluate JavaScript
221
- */
222
- async evaluate<T>(fn: () => T): Promise<T> {
223
- if (!this.page) throw new Error("Browser not started");
224
- return this.page.evaluate(fn);
225
- }
226
-
227
- /**
228
- * Get current URL
229
- */
230
- async getUrl(): Promise<string> {
231
- if (!this.page) throw new Error("Browser not started");
232
- return this.page.url();
246
+ await this.page.screenshot({ path, fullPage: true });
233
247
  }
234
248
 
235
249
  /**
@@ -249,4 +263,29 @@ export class PlaywrightE2ERunner {
249
263
 
250
264
  return result;
251
265
  }
266
+
267
+ /**
268
+ * Get the underlying page for advanced operations
269
+ */
270
+ getPage() {
271
+ return this.page;
272
+ }
273
+
274
+ /**
275
+ * Start tracing for debugging
276
+ */
277
+ async startTracing(_outputPath: string): Promise<void> {
278
+ if (!this.page) throw new Error("Browser not started");
279
+ await this.page
280
+ .context()
281
+ .tracing.start({ screenshots: true, snapshots: true });
282
+ }
283
+
284
+ /**
285
+ * Stop tracing and save
286
+ */
287
+ async stopTracing(outputPath: string): Promise<void> {
288
+ if (!this.page) throw new Error("Browser not started");
289
+ await this.page.context().tracing.stop({ path: outputPath });
290
+ }
252
291
  }
@@ -18,19 +18,15 @@ import type {
18
18
  E2EStep,
19
19
  E2EResult,
20
20
  E2EReport,
21
- } from "../../packages/types/src/runtime-types.ts";
22
- import { writeJson, appendJsonl } from "../../cli.ts";
23
- // @ts-expect-error - Bun has built-in Node.js types
21
+ } from "../../packages/types/src/runtime-types.js";
22
+ import { writeJson, appendJsonl } from "../../cli.js";
24
23
  import { join } from "node:path";
25
24
 
26
- export interface PlaywrightRunner {
27
- navigate(url: string): Promise<void>;
28
- click(selector: string): Promise<void>;
29
- type(selector: string, text: string): Promise<void>;
30
- wait(selector: string, timeout?: number): Promise<void>;
31
- screenshot(path: string): Promise<void>;
32
- assert(condition: string, message?: string): Promise<boolean>;
33
- }
25
+ // Re-export from playwright-runner for backwards compatibility
26
+ export type { E2ERunner as PlaywrightRunner } from "./playwright-runner.js";
27
+
28
+ // Also import locally for use in this file
29
+ import type { E2ERunner } from "./playwright-runner.js";
34
30
 
35
31
  export interface E2EConfig {
36
32
  baseUrl: string;
@@ -44,7 +40,7 @@ export interface E2EConfig {
44
40
  export class E2ETestEngine {
45
41
  private readonly rootDir: string;
46
42
  private readonly config: E2EConfig;
47
- private runner: PlaywrightRunner | null = null;
43
+ private runner: E2ERunner | null = null;
48
44
 
49
45
  constructor(rootDir: string, config: E2EConfig) {
50
46
  this.rootDir = rootDir;
@@ -61,7 +57,7 @@ export class E2ETestEngine {
61
57
  /**
62
58
  * Set the Playwright runner
63
59
  */
64
- setRunner(runner: PlaywrightRunner): void {
60
+ setRunner(runner: E2ERunner): void {
65
61
  this.runner = runner;
66
62
  }
67
63