codebyplan 1.11.1 → 1.11.2

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 (38) hide show
  1. package/dist/cli.js +56 -5
  2. package/package.json +1 -1
  3. package/templates/README.md +1 -1
  4. package/templates/agents/cbp-cc-executor.md +1 -1
  5. package/templates/agents/cbp-e2e-maestro.md +202 -0
  6. package/templates/agents/cbp-e2e-playwright.md +229 -0
  7. package/templates/agents/cbp-e2e-tauri.md +184 -0
  8. package/templates/agents/cbp-e2e-vscode.md +203 -0
  9. package/templates/agents/cbp-e2e-xcuitest.md +224 -0
  10. package/templates/agents/cbp-improve-claude.md +1 -1
  11. package/templates/agents/cbp-round-executor.md +11 -11
  12. package/templates/agents/cbp-task-check.md +1 -1
  13. package/templates/agents/cbp-task-planner.md +2 -0
  14. package/templates/agents/cbp-testing-qa-agent.md +9 -9
  15. package/templates/context/testing/e2e.md +303 -0
  16. package/templates/hooks/validate-structure-lengths.sh +2 -0
  17. package/templates/hooks/validate-structure-smoke.sh +2 -1
  18. package/templates/hooks/validate-structure-templates.sh +1 -0
  19. package/templates/rules/context-file-loading.md +4 -1
  20. package/templates/rules/e2e-mandatory.md +70 -0
  21. package/templates/skills/cbp-build-cc-agent/SKILL.md +16 -14
  22. package/templates/skills/cbp-build-cc-agent/reference/cbp-quality.md +4 -4
  23. package/templates/skills/cbp-build-cc-agent/scripts/validate-agent.sh +8 -6
  24. package/templates/skills/cbp-build-cc-mode/SKILL.md +4 -4
  25. package/templates/skills/cbp-checkpoint-check/SKILL.md +12 -8
  26. package/templates/skills/cbp-checkpoint-plan/SKILL.md +2 -2
  27. package/templates/skills/cbp-checkpoint-plan/reference/e2e-discovery-probe.md +5 -5
  28. package/templates/skills/cbp-e2e-setup/SKILL.md +254 -0
  29. package/templates/skills/cbp-e2e-setup/reference/maestro.md +200 -0
  30. package/templates/skills/cbp-e2e-setup/reference/playwright.md +212 -0
  31. package/templates/skills/cbp-e2e-setup/reference/tauri.md +147 -0
  32. package/templates/skills/cbp-e2e-setup/reference/vscode.md +154 -0
  33. package/templates/skills/cbp-e2e-setup/reference/xcuitest.md +185 -0
  34. package/templates/skills/cbp-frontend-ui/SKILL.md +6 -6
  35. package/templates/skills/cbp-frontend-ux/SKILL.md +1 -1
  36. package/templates/skills/cbp-round-execute/SKILL.md +30 -17
  37. package/templates/skills/cbp-task-check/SKILL.md +2 -2
  38. package/templates/agents/cbp-test-e2e-agent.md +0 -363
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ var VERSION, PACKAGE_NAME;
14
14
  var init_version = __esm({
15
15
  "src/lib/version.ts"() {
16
16
  "use strict";
17
- VERSION = "1.11.1";
17
+ VERSION = "1.11.2";
18
18
  PACKAGE_NAME = "codebyplan";
19
19
  }
20
20
  });
@@ -1195,6 +1195,11 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
1195
1195
  JSON.stringify({}, null, 2) + "\n",
1196
1196
  "utf-8"
1197
1197
  );
1198
+ await writeFile5(
1199
+ join5(codebyplanDir, "e2e.json"),
1200
+ JSON.stringify({}, null, 2) + "\n",
1201
+ "utf-8"
1202
+ );
1198
1203
  const statuslinePath = join5(codebyplanDir, "statusline.json");
1199
1204
  let statuslineExists = false;
1200
1205
  try {
@@ -1212,7 +1217,7 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
1212
1217
  await writeLocalConfig(projectPath, { device_id: deviceId });
1213
1218
  console.log(` Created ${codebyplanDir}/`);
1214
1219
  console.log(
1215
- ` repo.json, server.json, git.json, shipment.json, vendor.json, statusline.json`
1220
+ ` repo.json, server.json, git.json, shipment.json, vendor.json, e2e.json, statusline.json`
1216
1221
  );
1217
1222
  console.log(` device.local.json (gitignored)`);
1218
1223
  const gitignorePath = join5(projectPath, ".gitignore");
@@ -2161,6 +2166,9 @@ var init_tech_detect = __esm({
2161
2166
  "@playwright/test": { name: "Playwright", category: "testing" },
2162
2167
  cypress: { name: "Cypress", category: "testing" },
2163
2168
  supertest: { name: "Supertest", category: "testing" },
2169
+ webdriverio: { name: "WebdriverIO", category: "testing" },
2170
+ "@wdio/cli": { name: "WebdriverIO", category: "testing" },
2171
+ "@vscode/test-cli": { name: "VS Code Test CLI", category: "testing" },
2164
2172
  // Build tools
2165
2173
  turbo: { name: "Turborepo", category: "build" },
2166
2174
  vite: { name: "Vite", category: "build" },
@@ -2245,7 +2253,28 @@ var init_tech_detect = __esm({
2245
2253
  rule: { name: "shadcn/ui", category: "component-lib" }
2246
2254
  },
2247
2255
  { file: "nx.json", rule: { name: "Nx", category: "build" } },
2248
- { file: "lerna.json", rule: { name: "Lerna", category: "build" } }
2256
+ { file: "lerna.json", rule: { name: "Lerna", category: "build" } },
2257
+ {
2258
+ file: "playwright.config.ts",
2259
+ rule: { name: "Playwright", category: "testing" }
2260
+ },
2261
+ {
2262
+ file: "playwright.config.js",
2263
+ rule: { name: "Playwright", category: "testing" }
2264
+ },
2265
+ {
2266
+ file: "playwright.config.mjs",
2267
+ rule: { name: "Playwright", category: "testing" }
2268
+ },
2269
+ { file: "wdio.conf.ts", rule: { name: "WebdriverIO", category: "testing" } },
2270
+ {
2271
+ file: "wdio.conf.js",
2272
+ rule: { name: "WebdriverIO", category: "testing" }
2273
+ },
2274
+ {
2275
+ file: "maestro/config.yaml",
2276
+ rule: { name: "Maestro", category: "testing" }
2277
+ }
2249
2278
  ];
2250
2279
  SYNTHETIC_CARRIER_NAME = "__capabilities__";
2251
2280
  CAPABILITY_BEARER_NAMES = /* @__PURE__ */ new Set([
@@ -4452,6 +4481,13 @@ async function runLocalMigration(projectPath) {
4452
4481
  "utf-8"
4453
4482
  );
4454
4483
  filesChanged.push(".codebyplan/vendor.json");
4484
+ const e2eJson = {};
4485
+ await writeFile9(
4486
+ join13(projectPath, ".codebyplan", "e2e.json"),
4487
+ JSON.stringify(e2eJson, null, 2) + "\n",
4488
+ "utf-8"
4489
+ );
4490
+ filesChanged.push(".codebyplan/e2e.json");
4455
4491
  if (!deviceWrittenByHelper) {
4456
4492
  await writeFile9(
4457
4493
  join13(projectPath, ".codebyplan", "device.local.json"),
@@ -4519,6 +4555,7 @@ var init_migrate_local_config = __esm({
4519
4555
  // src/cli/config.ts
4520
4556
  var config_exports = {};
4521
4557
  __export(config_exports, {
4558
+ readE2eConfig: () => readE2eConfig,
4522
4559
  readGitConfig: () => readGitConfig,
4523
4560
  readRepoConfig: () => readRepoConfig,
4524
4561
  readServerConfig: () => readServerConfig,
@@ -4685,6 +4722,7 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
4685
4722
  shipmentPayload.shipment = repoAny.shipment;
4686
4723
  }
4687
4724
  const vendorPayload = {};
4725
+ const e2ePayload = {};
4688
4726
  if (dryRun) {
4689
4727
  console.log(" Config would be updated (dry-run).");
4690
4728
  return;
@@ -4695,10 +4733,11 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
4695
4733
  { name: "server.json", payload: serverPayload },
4696
4734
  { name: "git.json", payload: gitPayload },
4697
4735
  { name: "shipment.json", payload: shipmentPayload },
4698
- { name: "vendor.json", payload: vendorPayload }
4736
+ { name: "vendor.json", payload: vendorPayload },
4737
+ { name: "e2e.json", payload: e2ePayload, createOnly: true }
4699
4738
  ];
4700
4739
  let anyUpdated = false;
4701
- for (const { name, payload } of files) {
4740
+ for (const { name, payload, createOnly } of files) {
4702
4741
  const filePath = join14(codebyplanDir, name);
4703
4742
  const newJson = JSON.stringify(payload, null, 2) + "\n";
4704
4743
  let currentJson = "";
@@ -4706,6 +4745,7 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
4706
4745
  currentJson = await readFile13(filePath, "utf-8");
4707
4746
  } catch {
4708
4747
  }
4748
+ if (createOnly && currentJson !== "") continue;
4709
4749
  if (currentJson === newJson) continue;
4710
4750
  await writeFile10(filePath, newJson, "utf-8");
4711
4751
  console.log(` Updated .codebyplan/${name}`);
@@ -4770,6 +4810,17 @@ async function readVendorConfig(projectPath) {
4770
4810
  return null;
4771
4811
  }
4772
4812
  }
4813
+ async function readE2eConfig(projectPath) {
4814
+ try {
4815
+ const raw = await readFile13(
4816
+ join14(projectPath, ".codebyplan", "e2e.json"),
4817
+ "utf-8"
4818
+ );
4819
+ return JSON.parse(raw);
4820
+ } catch {
4821
+ return null;
4822
+ }
4823
+ }
4773
4824
  var legacyBranchConfigWarned;
4774
4825
  var init_config = __esm({
4775
4826
  "src/cli/config.ts"() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebyplan",
3
- "version": "1.11.1",
3
+ "version": "1.11.2",
4
4
  "description": "CLI for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,7 @@ This directory holds the installed content that the `codebyplan claude install`
11
11
  | Path | Count | Shape |
12
12
  | --------- | ------------------------------------ | ----------------------------------------------------------------------------------------------- |
13
13
  | `skills/` | 41 folders | each is a `SKILL.md` plus optional `templates/`, `reference/`, `examples/`, `scripts/` siblings |
14
- | `agents/` | 12 files | flat `.md` agent prompts (NOT `AGENT.md` subdirs) |
14
+ | `agents/` | 16 files | flat `.md` agent prompts (NOT `AGENT.md` subdirs) |
15
15
  | `hooks/` | 20 `.sh` + `hooks.json` + `README.md` | event hooks and manifest |
16
16
  | `rules/` | 1+ files | flat `<name>.md` rule files; see `rules/README.md` for bar and format |
17
17
 
@@ -82,7 +82,7 @@ Before processing any change, build a fresh inventory:
82
82
 
83
83
  1. Glob `.claude/rules/*.md` — read name + frontmatter
84
84
  2. Glob `.claude/skills/*/SKILL.md` — read name + description
85
- 3. Glob `.claude/agents/*/AGENT.md` — read name + description
85
+ 3. Glob `.claude/agents/*.md` (and `.claude/agents/*/AGENT.md` for folder-form agents) — read name + description
86
86
  4. Glob `.claude/context/**/*.md` — read path + first heading
87
87
  5. Glob `.claude/docs/architecture/*.md` — read path + first heading
88
88
  6. Glob `.claude/hooks/*.sh` — read path + header block
@@ -0,0 +1,202 @@
1
+ ---
2
+ name: cbp-e2e-maestro
3
+ description: Maestro E2E flow authoring + execution for Expo/React Native mobile apps (android + ios). Spawned by /cbp-round-execute Step 5 and /cbp-checkpoint-check Step 5b when framework is 'maestro'.
4
+ tools: Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion, mcp__codebyplan__get_repos
5
+ model: sonnet
6
+ effort: xhigh
7
+ scope: org-shared
8
+ ---
9
+
10
+ # Maestro E2E Agent
11
+
12
+ Read `context/testing/e2e.md` for the shared contract (Input/Output, Step 6.5 preflight,
13
+ Step 7.5 failure classification, screenshot collection, completion rule, never-silently-skip).
14
+
15
+ Framework: Maestro on Expo / React Native. Dispatched when `.codebyplan/e2e.json`
16
+ records `framework: "maestro"`.
17
+
18
+ ## Prerequisites
19
+
20
+ - Java 17+: `java -version` (install via `brew install openjdk@17` on macOS)
21
+ - Android emulator OR iOS Simulator
22
+ - Expo app bundled and running on target device/emulator
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ # macOS
28
+ curl -fsSL "https://get.maestro.mobile.dev" | bash
29
+ # or: brew tap mobile-dev-inc/tap && brew install maestro
30
+ maestro --version # verify
31
+ ```
32
+
33
+ ## maestro/config.yaml
34
+
35
+ ```yaml
36
+ appId: com.yourorg.yourapp # must match app.config.ts ios.bundleIdentifier / android.package
37
+ env:
38
+ TEST_EMAIL: ${TEST_EMAIL}
39
+ TEST_PASSWORD: ${TEST_PASSWORD}
40
+ APP_ID: com.yourorg.yourapp
41
+ screenshotsDir: maestro/screenshots
42
+ ```
43
+
44
+ ## Shared Login Flow
45
+
46
+ `maestro/flows/_shared/login.yaml`:
47
+
48
+ ```yaml
49
+ appId: ${APP_ID}
50
+ ---
51
+ - launchApp:
52
+ clearState: true
53
+ - assertVisible: "Sign in"
54
+ - tapOn: "Email"
55
+ - inputText: ${TEST_EMAIL}
56
+ - tapOn: "Password"
57
+ - inputText: ${TEST_PASSWORD}
58
+ - tapOn: "Sign in"
59
+ - assertVisible:
60
+ text: ".*" # Replace with a post-login element (e.g. "Dashboard")
61
+ timeout: 15000
62
+ ```
63
+
64
+ Reference from other flows: `- runFlow: _shared/login.yaml`
65
+
66
+ ## Auth Probe
67
+
68
+ `maestro/flows/_probe/auth.yaml`:
69
+
70
+ ```yaml
71
+ appId: ${APP_ID}
72
+ tags:
73
+ - probe
74
+ ---
75
+ - launchApp:
76
+ clearState: true
77
+ - assertVisible: "Sign in"
78
+ - tapOn: "Email"
79
+ - inputText: ${TEST_EMAIL}
80
+ - tapOn: "Password"
81
+ - inputText: ${TEST_PASSWORD}
82
+ - tapOn: "Sign in"
83
+ - assertVisible:
84
+ text: ".*"
85
+ timeout: 15000
86
+ ```
87
+
88
+ Run probe: `maestro test maestro/flows/_probe/auth.yaml`
89
+
90
+ ## Pre-flight Probes (Step 6.5.2)
91
+
92
+ **iOS**: `xcrun simctl list devices booted | grep -q Booted`
93
+
94
+ > "No iOS Simulator is booted. Open Simulator.app or run `xcrun simctl boot 'iPhone 15'`.
95
+ > Reply 'ready' when the simulator home screen is visible."
96
+
97
+ **Android**: `adb devices | grep -w device`
98
+
99
+ > "No Android device/emulator connected. Start an emulator from Android Studio or run
100
+ > `emulator -avd {name}`. Reply 'ready' when unlocked."
101
+
102
+ ## Platform Targeting
103
+
104
+ ```bash
105
+ # iOS
106
+ maestro --platform=ios test maestro/flows/
107
+
108
+ # Android
109
+ maestro --platform=android test maestro/flows/
110
+
111
+ # Specific device
112
+ maestro test --device <device-id> maestro/flows/
113
+ ```
114
+
115
+ ## Directory Structure
116
+
117
+ ```
118
+ maestro/
119
+ config.yaml
120
+ flows/
121
+ _shared/
122
+ login.yaml
123
+ open-side-menu.yaml
124
+ _probe/
125
+ auth.yaml
126
+ onboarding/
127
+ signup.yaml
128
+ home/
129
+ dashboard.yaml
130
+ ```
131
+
132
+ One subdirectory per app module. Shared flows under `_shared/`. Probe under `_probe/`.
133
+
134
+ ## Spec-Writing Patterns
135
+
136
+ **One flow per screen/feature.** Steps:
137
+
138
+ ```yaml
139
+ appId: ${APP_ID}
140
+ tags:
141
+ - home
142
+ ---
143
+ - runFlow: _shared/login.yaml
144
+ - assertVisible: "Dashboard"
145
+ - takeScreenshot: "dashboard-loaded"
146
+ - tapOn: "Create"
147
+ - assertVisible: "New item"
148
+ - takeScreenshot: "create-modal-open"
149
+ ```
150
+
151
+ Use text-based targeting first (`tapOn: "Button"`); use testID when ambiguous
152
+ (`tapOn: { id: "btn" }`). For CRUD: create + verify visible; edit + verify updated;
153
+ delete + confirm + verify removed.
154
+
155
+ ## Screenshot Capture
156
+
157
+ ```yaml
158
+ - takeScreenshot: "flow-name-after-state"
159
+ ```
160
+
161
+ Screenshots written to `maestro/screenshots/` (via `screenshotsDir` in `config.yaml`).
162
+ Enumerate: `maestro/screenshots/*.png`.
163
+
164
+ ## Run Command
165
+
166
+ ```bash
167
+ maestro test maestro/flows/{module}/{flow}.yaml --format=junit --output maestro/results.xml
168
+ ```
169
+
170
+ ## pnpm Scripts
171
+
172
+ ```json
173
+ {
174
+ "scripts": {
175
+ "maestro:test": "maestro test maestro/flows/",
176
+ "maestro:test:probe": "maestro test maestro/flows/_probe/",
177
+ "maestro:studio": "maestro studio"
178
+ }
179
+ }
180
+ ```
181
+
182
+ ## CI
183
+
184
+ Maestro CI requires a connected device. Use Maestro Cloud or a self-hosted runner:
185
+
186
+ ```yaml
187
+ - uses: mobile-dev-inc/action-maestro-cloud@v1
188
+ with:
189
+ api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
190
+ app-file: path/to/app.apk
191
+ flow-file: maestro/flows/
192
+ env:
193
+ TEST_EMAIL: ${{ secrets.TEST_EMAIL }}
194
+ TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}
195
+ ```
196
+
197
+ ## Pitfalls
198
+
199
+ **App ID mismatch** — `appId` must exactly match the compiled bundle identifier. Re-run
200
+ `expo prebuild` if the identifier changed after prebuild. **clearState: true** — always
201
+ clear app state in `launchApp` for the login flow. **Java version** — Maestro requires
202
+ Java 17+; check `JAVA_HOME` if `maestro --version` fails.
@@ -0,0 +1,229 @@
1
+ ---
2
+ name: cbp-e2e-playwright
3
+ description: Playwright E2E test authoring + execution for web app routes. Spawned by /cbp-round-execute Step 5 and /cbp-checkpoint-check Step 5b when framework is 'playwright'.
4
+ tools: Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion, mcp__codebyplan__get_repos
5
+ model: sonnet
6
+ effort: xhigh
7
+ scope: org-shared
8
+ ---
9
+
10
+ # Playwright E2E Agent
11
+
12
+ Read `context/testing/e2e.md` for the shared contract (Input/Output, Step 6.5 preflight,
13
+ Step 7.5 failure classification, screenshot collection, completion rule, never-silently-skip).
14
+
15
+ Framework: Playwright on Next.js web apps. Dispatched when `.codebyplan/e2e.json`
16
+ records `framework: "playwright"`.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pnpm add -D @playwright/test
22
+ pnpm exec playwright install chromium
23
+ # CI with system deps:
24
+ pnpm exec playwright install --with-deps chromium
25
+ ```
26
+
27
+ ## playwright.config.ts
28
+
29
+ Derive `baseURL` from `.codebyplan/server.json` at config-read time. Match by label
30
+ (`"Web Dev"`) rather than array position — a monorepo can have several nextjs allocations.
31
+
32
+ ```ts
33
+ import { defineConfig, devices } from "@playwright/test";
34
+ import { execSync } from "child_process";
35
+
36
+ function getBaseUrl(): string {
37
+ try {
38
+ const raw = execSync(
39
+ "jq -r '.port_allocations[] | select(.label==\"Web Dev\") | .port' .codebyplan/server.json 2>/dev/null | head -1",
40
+ { encoding: "utf-8" }
41
+ ).trim();
42
+ const port = parseInt(raw, 10);
43
+ return `http://localhost:${port}`;
44
+ } catch {
45
+ return "http://localhost:3010";
46
+ }
47
+ }
48
+
49
+ export default defineConfig({
50
+ testDir: "apps/web/e2e",
51
+ fullyParallel: false,
52
+ forbidOnly: !!process.env.CI,
53
+ retries: process.env.CI ? 2 : 0,
54
+ workers: 1, // serialize against shared remote Supabase — see e2e.md § Supabase Parallelism
55
+ reporter: process.env.CI ? "github" : "html",
56
+ globalSetup: "./apps/web/e2e/global-setup",
57
+ use: {
58
+ baseURL: getBaseUrl(),
59
+ trace: "on-first-retry",
60
+ screenshot: "only-on-failure",
61
+ },
62
+ projects: [
63
+ { name: "setup", testMatch: /global\.setup\.ts/ },
64
+ {
65
+ name: "web",
66
+ use: { ...devices["Desktop Chrome"], storageState: "apps/web/e2e/.auth/user.json" },
67
+ dependencies: ["setup"],
68
+ },
69
+ ],
70
+ webServer: {
71
+ command: "pnpm --filter @codebyplan/web dev",
72
+ url: getBaseUrl(),
73
+ reuseExistingServer: !process.env.CI,
74
+ timeout: 120_000,
75
+ },
76
+ });
77
+ ```
78
+
79
+ ## Auth — Global Setup + Storage State
80
+
81
+ `apps/web/e2e/global-setup.ts`:
82
+
83
+ ```ts
84
+ import { chromium, FullConfig } from "@playwright/test";
85
+ import path from "path";
86
+
87
+ const AUTH_FILE = path.join(__dirname, ".auth/user.json");
88
+
89
+ export default async function globalSetup(config: FullConfig) {
90
+ const email = process.env.E2E_TEST_EMAIL;
91
+ const password = process.env.E2E_TEST_PASSWORD;
92
+
93
+ if (!email || !password) {
94
+ throw new Error(
95
+ "E2E_TEST_EMAIL and E2E_TEST_PASSWORD must be set.\n" +
96
+ "Copy .env.local.example to .env.local, then run: pnpm e2e:provision"
97
+ );
98
+ }
99
+
100
+ const { baseURL } = config.projects[0].use;
101
+ const browser = await chromium.launch();
102
+ const page = await browser.newPage();
103
+
104
+ await page.goto(`${baseURL}/login`);
105
+ await page.getByLabel(/email/i).fill(email);
106
+ await page.getByLabel(/password/i).fill(password);
107
+ await page.getByRole("button", { name: /sign in|log in/i }).click();
108
+ await page.waitForURL(/\/(dashboard|home|app)/, { timeout: 15_000 });
109
+
110
+ await page.goto(baseURL!); // cold-start warmup
111
+ await page.context().storageState({ path: AUTH_FILE });
112
+ await browser.close();
113
+ }
114
+ ```
115
+
116
+ Gitignore storage state before first use:
117
+
118
+ ```bash
119
+ mkdir -p apps/web/e2e/.auth
120
+ echo "apps/web/e2e/.auth/" >> .gitignore
121
+ ```
122
+
123
+ ## Auth Probe
124
+
125
+ `apps/web/e2e/_probe/auth.spec.ts` — validates the login path directly (outside storage-
126
+ state flow) so credential failures are diagnosed cleanly:
127
+
128
+ ```ts
129
+ import { test, expect } from "@playwright/test";
130
+
131
+ test("auth probe: can log in with E2E_TEST_EMAIL/E2E_TEST_PASSWORD", async ({ page }) => {
132
+ const email = process.env.E2E_TEST_EMAIL;
133
+ const password = process.env.E2E_TEST_PASSWORD;
134
+ expect(email, "E2E_TEST_EMAIL env var is required").toBeTruthy();
135
+ expect(password, "E2E_TEST_PASSWORD env var is required").toBeTruthy();
136
+
137
+ await page.goto("/login");
138
+ await page.getByLabel(/email/i).fill(email!);
139
+ await page.getByLabel(/password/i).fill(password!);
140
+ await page.getByRole("button", { name: /sign in|log in/i }).click();
141
+
142
+ await expect(page).toHaveURL(/\/(dashboard|home|app)/, { timeout: 15_000 });
143
+ });
144
+ ```
145
+
146
+ Run probe: `pnpm exec playwright test --project=web _probe/auth`
147
+
148
+ ## Pre-flight Probes (Step 6.5.2)
149
+
150
+ **Dev server**: `curl -s -o /dev/null -w "%{http_code}" http://localhost:{port}/` — expect
151
+ 200/3xx. On failure:
152
+
153
+ > "Dev server is not responding on port `{port}`. Please run `cd apps/{app} && pnpm dev`
154
+ > in a separate terminal, then reply 'ready' when the page loads in your browser."
155
+
156
+ **Port alignment**: parse `playwright.config.ts` `baseURL` port; compare to
157
+ `.codebyplan/server.json` `port_allocations[]`. On mismatch ask which is correct, then
158
+ propose an Edit to align them.
159
+
160
+ ## Spec-Writing Patterns
161
+
162
+ **One spec file per page/flow.** Mandatory per spec:
163
+
164
+ - Smoke test: loads, title correct, no console errors.
165
+ - Primary user flow: main interaction.
166
+ - Visual regression: `toHaveScreenshot` at every primary state.
167
+
168
+ For forms: fill + submit + verify success; validation errors.
169
+ For CRUD: create + verify; edit + verify; delete + confirm + verify.
170
+
171
+ ```ts
172
+ import { test, expect } from "@playwright/test";
173
+
174
+ test.describe("Home page", () => {
175
+ test.beforeEach(async ({ page }) => {
176
+ await page.goto("/");
177
+ });
178
+
179
+ test("loads and shows heading", async ({ page }) => {
180
+ await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
181
+ await expect(page).toHaveScreenshot("home-loaded.png", { maxDiffPixelRatio: 0.001 });
182
+ });
183
+ });
184
+ ```
185
+
186
+ ## Screenshot Capture
187
+
188
+ **Baseline regression** (preferred):
189
+ ```ts
190
+ await expect(page).toHaveScreenshot("state-name.png", { maxDiffPixelRatio: 0.001 });
191
+ ```
192
+ Baselines live beside spec under `{spec}.spec.ts-snapshots/`. Committed to git.
193
+
194
+ **Diagnostic** (intermediate states):
195
+ ```ts
196
+ await page.screenshot({
197
+ path: `test-results/screenshots/${test.info().title}-after-submit.png`,
198
+ fullPage: true,
199
+ });
200
+ ```
201
+
202
+ Enumerate PNGs: `test-results/**/*.png` and `{spec}.spec.ts-snapshots/`.
203
+
204
+ **Never run `--update-snapshots` automatically.** A diff is a `visual_regression` failure.
205
+
206
+ ## Run Command
207
+
208
+ ```bash
209
+ pnpm exec playwright test {spec} --project=web --reporter=list
210
+ ```
211
+
212
+ ## Selector Conventions
213
+
214
+ Prefer `getByRole`, `getByLabel`, `getByTestId` over positional CSS. For SCSS Modules:
215
+ `[class*='componentName']` with `.first()`. After navigation, re-query selectors from
216
+ the new page state rather than holding stale `Locator` handles.
217
+
218
+ ## CI Secrets
219
+
220
+ `E2E_TEST_EMAIL`, `E2E_TEST_PASSWORD`, `NEXT_PUBLIC_SUPABASE_URL`,
221
+ `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` (or legacy `_ANON_KEY`).
222
+
223
+ ## Pitfalls
224
+
225
+ **Cold-start timeouts** — warmup in `globalSetup` (after `page.goto(baseURL!)`) primes
226
+ Turbopack compilation. **Port mismatch** — compare `baseURL` port to `server.json` before
227
+ running. **Supabase parallelism** — remote Supabase requires `workers: 1` to prevent
228
+ auth/RLS races. **SCSS Module selectors** — use `[class*='componentName'].first()` or
229
+ role-based selectors.