@kennethsolomon/shipkit 3.2.0 → 3.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.
@@ -0,0 +1,287 @@
1
+ # React + Vite + Tailwind — Stack Reference
2
+
3
+ ## Scaffold
4
+
5
+ ```bash
6
+ npm create vite@latest {project-name} -- --template react-ts
7
+ cd {project-name}
8
+ npm install
9
+ npm install -D tailwindcss @tailwindcss/vite
10
+ npm install react-router-dom
11
+ ```
12
+
13
+ ## Directory Structure
14
+
15
+ ```
16
+ {project-name}/
17
+ ├── src/
18
+ │ ├── main.tsx ← entry point (router setup)
19
+ │ ├── App.tsx ← route definitions
20
+ │ ├── index.css ← Tailwind directives + custom CSS
21
+ │ ├── pages/
22
+ │ │ ├── Landing.tsx ← landing page
23
+ │ │ ├── Dashboard.tsx ← dashboard
24
+ │ │ ├── {Feature1}.tsx
25
+ │ │ ├── {Feature2}.tsx
26
+ │ │ └── Settings.tsx
27
+ │ ├── components/
28
+ │ │ ├── landing/
29
+ │ │ │ ├── Navbar.tsx
30
+ │ │ │ ├── Hero.tsx
31
+ │ │ │ ├── Features.tsx
32
+ │ │ │ ├── HowItWorks.tsx
33
+ │ │ │ ├── Pricing.tsx
34
+ │ │ │ ├── Testimonials.tsx
35
+ │ │ │ ├── WaitlistForm.tsx
36
+ │ │ │ └── Footer.tsx
37
+ │ │ ├── app/
38
+ │ │ │ ├── Sidebar.tsx
39
+ │ │ │ ├── AppLayout.tsx
40
+ │ │ │ ├── DashboardCards.tsx
41
+ │ │ │ └── {feature components}
42
+ │ │ └── ui/
43
+ │ │ ├── Button.tsx
44
+ │ │ ├── Input.tsx
45
+ │ │ ├── Card.tsx
46
+ │ │ ├── Modal.tsx
47
+ │ │ └── Toast.tsx
48
+ │ ├── data/
49
+ │ │ └── mock.ts ← all fake data centralized
50
+ │ └── lib/
51
+ │ └── utils.ts ← shared helpers (cn, etc.)
52
+ ├── public/
53
+ │ └── {static assets}
54
+ ├── index.html
55
+ ├── vite.config.ts
56
+ ├── tailwind.config.ts
57
+ └── package.json
58
+ ```
59
+
60
+ ## Vite Config
61
+
62
+ `vite.config.ts`:
63
+
64
+ ```ts
65
+ import { defineConfig } from 'vite'
66
+ import react from '@vitejs/plugin-react'
67
+ import tailwindcss from '@tailwindcss/vite'
68
+
69
+ export default defineConfig({
70
+ plugins: [react(), tailwindcss()],
71
+ })
72
+ ```
73
+
74
+ ## Tailwind Config
75
+
76
+ `tailwind.config.ts`:
77
+
78
+ ```ts
79
+ export default {
80
+ content: ['./index.html', './src/**/*.{ts,tsx}'],
81
+ theme: {
82
+ extend: {
83
+ colors: {
84
+ bg: 'var(--color-bg)',
85
+ fg: 'var(--color-fg)',
86
+ accent: 'var(--color-accent)',
87
+ muted: 'var(--color-muted)',
88
+ },
89
+ fontFamily: {
90
+ display: ['var(--font-display)', 'serif'],
91
+ body: ['var(--font-body)', 'sans-serif'],
92
+ },
93
+ },
94
+ },
95
+ }
96
+ ```
97
+
98
+ CSS variables and font imports in `src/index.css`:
99
+
100
+ ```css
101
+ @import url('https://fonts.googleapis.com/css2?family={DisplayFont}:wght@400;600;700;800&family={BodyFont}:wght@400;500;600&display=swap');
102
+
103
+ @tailwind base;
104
+ @tailwind components;
105
+ @tailwind utilities;
106
+
107
+ :root {
108
+ --color-bg: #xxxxxx;
109
+ --color-fg: #xxxxxx;
110
+ --color-accent: #xxxxxx;
111
+ --color-muted: #xxxxxx;
112
+ --font-display: '{DisplayFont}', serif;
113
+ --font-body: '{BodyFont}', sans-serif;
114
+ }
115
+
116
+ body {
117
+ font-family: var(--font-body);
118
+ }
119
+ ```
120
+
121
+ ## Router Setup
122
+
123
+ `src/App.tsx`:
124
+
125
+ ```tsx
126
+ import { createBrowserRouter, RouterProvider } from 'react-router-dom'
127
+ import Landing from './pages/Landing'
128
+ import AppLayout from './components/app/AppLayout'
129
+ import Dashboard from './pages/Dashboard'
130
+ import Feature1 from './pages/{Feature1}'
131
+ import Feature2 from './pages/{Feature2}'
132
+ import Settings from './pages/Settings'
133
+
134
+ const router = createBrowserRouter([
135
+ { path: '/', element: <Landing /> },
136
+ {
137
+ element: <AppLayout />,
138
+ children: [
139
+ { path: '/dashboard', element: <Dashboard /> },
140
+ { path: '/{feature-1}', element: <Feature1 /> },
141
+ { path: '/{feature-2}', element: <Feature2 /> },
142
+ { path: '/settings', element: <Settings /> },
143
+ ],
144
+ },
145
+ ])
146
+
147
+ export default function App() {
148
+ return <RouterProvider router={router} />
149
+ }
150
+ ```
151
+
152
+ ## App Layout
153
+
154
+ `src/components/app/AppLayout.tsx`:
155
+
156
+ ```tsx
157
+ import { Outlet } from 'react-router-dom'
158
+ import Sidebar from './Sidebar'
159
+
160
+ export default function AppLayout() {
161
+ return (
162
+ <div className="flex min-h-screen bg-bg text-fg">
163
+ <Sidebar />
164
+ <main className="flex-1 p-6 lg:p-8">
165
+ <Outlet />
166
+ </main>
167
+ </div>
168
+ )
169
+ }
170
+ ```
171
+
172
+ ## Waitlist — Formspree Integration
173
+
174
+ Since React + Vite has no backend, use Formspree for email collection.
175
+
176
+ `src/components/landing/WaitlistForm.tsx`:
177
+
178
+ ```tsx
179
+ import { useState, FormEvent } from 'react'
180
+
181
+ // Replace YOUR_FORM_ID with your Formspree form ID
182
+ // Create one free at https://formspree.io
183
+ const FORMSPREE_URL = 'https://formspree.io/f/YOUR_FORM_ID'
184
+
185
+ export default function WaitlistForm() {
186
+ const [email, setEmail] = useState('')
187
+ const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
188
+ const [message, setMessage] = useState('')
189
+
190
+ async function handleSubmit(e: FormEvent) {
191
+ e.preventDefault()
192
+ setStatus('loading')
193
+
194
+ try {
195
+ const res = await fetch(FORMSPREE_URL, {
196
+ method: 'POST',
197
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
198
+ body: JSON.stringify({ email }),
199
+ })
200
+
201
+ if (res.ok) {
202
+ setStatus('success')
203
+ setMessage("You're on the list! We'll notify you when we launch.")
204
+ } else {
205
+ throw new Error()
206
+ }
207
+ } catch {
208
+ setStatus('error')
209
+ setMessage('Something went wrong. Please try again.')
210
+ }
211
+ }
212
+
213
+ if (status === 'success') {
214
+ return <p className="text-green-600 font-medium text-lg">{message}</p>
215
+ }
216
+
217
+ return (
218
+ <form onSubmit={handleSubmit} className="flex flex-col sm:flex-row gap-3 max-w-md">
219
+ <input
220
+ type="email"
221
+ value={email}
222
+ onChange={e => setEmail(e.target.value)}
223
+ placeholder="you@example.com"
224
+ required
225
+ className="flex-1 px-4 py-3 rounded-lg border border-muted/30 bg-bg focus:ring-2 focus:ring-accent focus:outline-none"
226
+ />
227
+ <button
228
+ type="submit"
229
+ disabled={status === 'loading'}
230
+ className="px-6 py-3 bg-accent text-white rounded-xl font-medium hover:opacity-90 transition-all disabled:opacity-50"
231
+ >
232
+ {status === 'loading' ? 'Joining...' : 'Join Waitlist'}
233
+ </button>
234
+ {status === 'error' && <p className="text-red-500 text-sm">{message}</p>}
235
+ </form>
236
+ )
237
+ }
238
+ ```
239
+
240
+ ## Mock Data
241
+
242
+ Centralize all fake data in `src/data/mock.ts`:
243
+
244
+ ```ts
245
+ export const features = [
246
+ { icon: '🎯', title: 'Feature One', description: 'Short benefit-driven description.' },
247
+ { icon: '⚡', title: 'Feature Two', description: 'Short benefit-driven description.' },
248
+ { icon: '🔒', title: 'Feature Three', description: 'Short benefit-driven description.' },
249
+ ]
250
+
251
+ export const testimonials = [
252
+ { quote: 'Realistic testimonial here.', name: 'Jane Smith', role: 'CTO, TechCo' },
253
+ // ...
254
+ ]
255
+
256
+ export const pricingPlans = [
257
+ { name: 'Free', price: '$0', features: ['Feature A', 'Feature B'], cta: 'Get Started' },
258
+ { name: 'Pro', price: '$29/mo', features: ['Everything in Free', 'Feature C'], cta: 'Get Pro', popular: true },
259
+ { name: 'Enterprise', price: 'Custom', features: ['Everything in Pro', 'Priority support'], cta: 'Contact Us' },
260
+ ]
261
+
262
+ // Dashboard mock data
263
+ export const dashboardStats = [
264
+ { label: 'Total Users', value: '2,847', change: '+12%' },
265
+ // ...
266
+ ]
267
+
268
+ export const recentActivity = [
269
+ { user: 'Jane Smith', action: 'created a new project', time: '2 hours ago' },
270
+ // ...
271
+ ]
272
+ ```
273
+
274
+ ## Component Patterns
275
+
276
+ - Use functional components with TypeScript.
277
+ - Use `useState` for local state, no state management library needed.
278
+ - Navigation: `<Link to="/dashboard">` from react-router-dom.
279
+ - Keep components focused — one file per component.
280
+ - Props typed with interfaces or inline.
281
+
282
+ ## Dev Server
283
+
284
+ ```bash
285
+ npm run dev
286
+ # Runs on http://localhost:5173
287
+ ```
@@ -58,16 +58,23 @@ If `tasks/security-findings.md` exists, read the most recent audit. Use any unre
58
58
  Critical/High findings as additional targeted checks — verify the current diff doesn't
59
59
  reintroduce previously flagged vulnerabilities.
60
60
 
61
- ### 2. Collect All Changes
61
+ ### 2. Collect Changes + Blast Radius
62
+
63
+ Instead of reading the entire codebase or only the diff, build a **blast radius** — the minimal set of files that could be affected by the changes. This produces focused, high-signal context that leads to better review quality.
64
+
65
+ **2a — Baseline git info:**
62
66
 
63
67
  ```bash
64
68
  # Determine base branch
65
- git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main"
69
+ BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main")
70
+
71
+ # Changed files and stats
72
+ CHANGED_FILES=$(git diff $BASE..HEAD --name-only)
73
+ git diff $BASE..HEAD --stat
74
+ git log $BASE..HEAD --oneline
66
75
 
67
- # All changes on this branch
68
- git diff main..HEAD
69
- git diff main..HEAD --stat
70
- git log main..HEAD --oneline
76
+ # Full diff for reference
77
+ git diff $BASE..HEAD
71
78
 
72
79
  # Check for uncommitted changes
73
80
  git status --short
@@ -76,12 +83,103 @@ git status --short
76
83
  If there are uncommitted changes, warn:
77
84
  > **Warning:** You have uncommitted changes. These will NOT be included in the review. Commit or stash them first.
78
85
 
79
- Read the full content of every changed file (not just the diff hunks) to understand context around the changes.
86
+ **2b Extract changed symbols:**
87
+
88
+ Use **git hunk headers** as the primary extraction method. Git already parses the enclosing function/class name into every `@@` header — this is more reliable than regex or AST tools:
89
+
90
+ ```bash
91
+ # Phase 1: Enclosing scope names from hunk headers (free from git, no parsing needed)
92
+ git diff $BASE..HEAD -U0 | grep '^@@' | sed 's/.*@@\s*//' | \
93
+ grep -oE '[A-Za-z_][A-Za-z0-9_]*\s*\(' | sed 's/\s*(//' | sort -u
94
+ ```
95
+
96
+ Then supplement with **new/modified definitions** from added lines using language-specific patterns. Only match definition keywords — not `const`, `export`, `type`, or other high-noise terms:
97
+
98
+ ```bash
99
+ # Phase 2: Definitions from added lines (supplement, not replace)
100
+ # JS/TS: function foo(, class Foo, interface Foo
101
+ # Python: def foo(, class Foo
102
+ # Go: func foo(, func (r *T) foo(
103
+ # PHP: function foo(, class Foo
104
+ # Rust: fn foo(, struct Foo, impl Foo, trait Foo
105
+ git diff $BASE..HEAD | grep '^+' | grep -v '^+++' | \
106
+ grep -oE '(function|class|interface|def|fn|func|struct|trait|impl)\s+[A-Za-z_][A-Za-z0-9_]+' | \
107
+ awk '{print $2}' | sort -u
108
+ ```
109
+
110
+ Combine both phases. Filter out symbols shorter than 3 characters (too generic for blast-radius search).
111
+
112
+ Classify each symbol:
113
+ - **Modified/removed** — existed before the branch, changed or deleted now. These can break callers. **Run blast radius on these.**
114
+ - **New** — added in this branch, no prior callers exist. **Skip blast radius** (nothing to break).
115
+
116
+ To classify, check if the symbol appears in the base branch:
117
+ ```bash
118
+ # If symbol exists in base branch files, it's modified/removed → needs blast radius
119
+ git show $BASE:$FILE 2>/dev/null | grep -q "\b$SYMBOL\b"
120
+ ```
121
+
122
+ **2c — Find blast radius (modified/removed symbols only):**
123
+
124
+ For each modified/removed symbol, use **import-chain narrowing** to find dependents with minimal false positives:
125
+
126
+ ```bash
127
+ # Step 1: Find files that import the module containing the changed symbol
128
+ CHANGED_MODULE_PATHS=$(echo "$CHANGED_FILES" | sed 's/\.[^.]*$//' | sed 's/\/index$//')
129
+ for module_path in $CHANGED_MODULE_PATHS; do
130
+ rg -l "(import|require|from|use)\s.*$(basename $module_path)" \
131
+ --glob '!node_modules/**' --glob '!vendor/**' --glob '!dist/**' \
132
+ --glob '!build/**' --glob '!*.lock' --glob '!*.md' \
133
+ 2>/dev/null
134
+ done | sort -u > /tmp/importers.txt
135
+
136
+ # Step 2: Within importers, find which ones reference the specific changed symbols
137
+ for symbol in $MODIFIED_SYMBOLS; do
138
+ rg -wl "$symbol" $(cat /tmp/importers.txt) 2>/dev/null
139
+ done | sort -u > /tmp/dependents.txt
140
+
141
+ # Remove files already in the changed set
142
+ comm -23 /tmp/dependents.txt <(echo "$CHANGED_FILES" | sort) > /tmp/blast_radius.txt
143
+ ```
144
+
145
+ **Noise guard:** If a symbol produces >100 matches, it's too generic for grep-based analysis. Note it in the review as "unable to determine blast radius for `symbol` — manual verification recommended."
146
+
147
+ Log the blast radius before reading:
148
+ ```
149
+ Blast Radius Summary
150
+ ──────────────────────────────────
151
+ Changed files: X
152
+ Blast-radius dependents: Y (files importing changed symbols)
153
+ Total review scope: X+Y files
154
+ Symbols analyzed: N modified, M new (skipped)
155
+
156
+ Symbol → Dependents:
157
+ processOrder → src/checkout/cart.ts, src/api/orders.ts
158
+ validateInput → src/middleware/auth.ts
159
+ ──────────────────────────────────
160
+ ```
161
+
162
+ **2d — Read context (focused, not exhaustive):**
163
+
164
+ Read in this priority order:
165
+ 1. **Changed files in full** — not just the diff. The full file provides surrounding context (imports, related functions, class-level state) needed to judge whether the change is correct. For files >500 lines, read the changed function + 30 lines of surrounding context instead.
166
+ 2. **The diff** — for precise change tracking (already collected above).
167
+ 3. **Blast-radius dependent files** — read only the call sites that reference changed symbols. Use `rg -B5 -A10 "\bsymbol\b" dependent_file` to get the call site with surrounding context, not the entire file.
168
+ 4. **Test files** for changed symbols — verify existing tests still cover the changed behavior.
169
+
170
+ Do **not** read unchanged files outside the blast radius.
171
+
172
+ Carry the blast-radius mapping (symbol → dependents) forward into Steps 3-9. When analyzing a changed function, always cross-reference its dependents.
80
173
 
81
174
  ### 3. Analyze — Correctness & Bugs
82
175
 
83
176
  The most important dimension. A bug that ships is worse than ugly code that works.
84
177
 
178
+ **Blast-radius check (mandatory):** For every modified/removed symbol, verify its dependents (from Step 2c) are still compatible:
179
+ - Do callers pass arguments the changed function still accepts?
180
+ - Do callers depend on return values whose shape/type changed?
181
+ - Do callers rely on side effects the changed code no longer produces?
182
+
85
183
  **Logic errors:**
86
184
  - Wrong operator (`&&` vs `||`, `==` vs `===`, `<` vs `<=`)
87
185
  - Inverted conditions, missing negation
@@ -114,7 +212,11 @@ The most important dimension. A bug that ships is worse than ugly code that work
114
212
 
115
213
  ### 4. Analyze — Security
116
214
 
117
- Load `references/security-checklist.md` and apply its grep patterns systematically. Check for:
215
+ Load `references/security-checklist.md` and apply its grep patterns against the **diff and blast-radius files** (not the entire codebase). Only flag patterns **newly introduced** in the diff — pre-existing issues are out of scope unless they interact with the changed code.
216
+
217
+ **Blast-radius check:** If a validation or auth function was modified, check all its callers (from Step 2c) — a weakened check affects every endpoint that depends on it.
218
+
219
+ Check for:
118
220
 
119
221
  **Injection (OWASP A03):**
120
222
  - SQL, NoSQL, OS command, LDAP, template injection
@@ -184,6 +286,8 @@ Think about what happens at 10x, 100x current scale. Performance bugs are often
184
286
 
185
287
  Production code must handle failure gracefully. The question isn't "does it work?" but "what happens when things go wrong?"
186
288
 
289
+ **Blast-radius check:** If error handling changed (e.g., function now throws instead of returning null, or error type changed), check all callers from Step 2c — they may not have matching try/catch or null checks.
290
+
187
291
  **Error handling quality:**
188
292
  - Swallowed errors (empty catch blocks, `.catch(() => {})`)
189
293
  - Generic catch blocks that hide the actual error type
@@ -217,8 +321,9 @@ Think about the next engineer who reads this code. Is the intent clear? Does the
217
321
  - Components doing too many things (should be split)
218
322
  - Side effects in pure functions or constructors
219
323
 
220
- **API design (if endpoints changed):**
324
+ **API design (if endpoints or function signatures changed):**
221
325
  - Breaking changes to existing API contracts without versioning
326
+ - **Blast-radius check:** If a function signature changed, the blast radius from Step 2c is the definitive answer to whether it's a breaking change — every dependent file that calls the old signature will break
222
327
  - Inconsistent response format across endpoints
223
328
  - Missing or inconsistent HTTP status codes
224
329
  - Unclear or missing error response schema
@@ -294,7 +399,8 @@ Format findings with severity levels and review dimensions:
294
399
 
295
400
  **Changes:** X files changed, +Y/-Z lines
296
401
  **Commits:** N commits
297
- **Review dimensions:** Correctness, Security, Performance, Reliability, Design, Best Practices, Testing
402
+ **Blast radius:** X changed files + Y dependents = Z total review scope
403
+ **Review dimensions:** Correctness, Security, Performance, Reliability, Design, Best Practices, Testing, Blast Radius
298
404
 
299
405
  ### Critical (must fix before merge)
300
406
  - **[Correctness]** [FILE:LINE] Description of critical issue
@@ -323,7 +429,8 @@ Format findings with severity levels and review dimensions:
323
429
 
324
430
  **Rules:**
325
431
  - Maximum 20 items total (prioritize by severity, then by category)
326
- - Every item must tag its review dimension: `[Correctness]`, `[Security]`, `[Performance]`, `[Reliability]`, `[Design]`, `[Best Practices]`, `[Testing]`
432
+ - Every item must tag its review dimension: `[Correctness]`, `[Security]`, `[Performance]`, `[Reliability]`, `[Design]`, `[Best Practices]`, `[Testing]`, `[Blast Radius]`
433
+ - Use `[Blast Radius]` for issues found in dependent files — callers broken by changed signatures, importers affected by removed exports, tests that no longer cover the changed behavior
327
434
  - Every item must reference a specific file and line
328
435
  - Every item must explain **why** it matters — the impact, not just the symptom
329
436
  - Include a brief "What Looks Good" section (2-3 items) — acknowledge strong patterns so they're reinforced. This isn't cheerleading — it's calibrating signal.
@@ -145,16 +145,54 @@ If a frontend stack was detected, generate FE test files:
145
145
 
146
146
  Skip this step if no FE stack was detected.
147
147
 
148
- ### 8b. Playwright-Specific (conditional)
148
+ ### 8b. Write E2E Spec Files (conditional)
149
+
150
+ **Only if `playwright.config.ts` or `playwright.config.js` is detected in the project root:**
151
+
152
+ Write `e2e/<feature>.spec.ts` files covering the acceptance criteria from `tasks/todo.md`. Follow these rules:
153
+
154
+ - Use `test.describe` / `test` blocks — not `describe`/`it`
155
+ - Use role-based locators: `getByRole`, `getByLabel`, `getByText`, `getByPlaceholder` — never CSS selectors
156
+ - Use `test.beforeEach` for shared setup (auth, navigation)
157
+ - Use `test.skip(!email, 'ENV_VAR not set — skipping')` guards for credential-dependent tests
158
+ - Auth credentials from env vars via `e2e/helpers/auth.ts` — never hardcode credentials
159
+ - Soft assertions (`expect.soft`) for non-critical checks; hard `expect` for gate conditions
160
+
161
+ E2E spec example structure:
162
+ ```ts
163
+ import { test, expect } from '@playwright/test'
164
+ import { signIn, TEST_USERS } from './helpers/auth'
165
+
166
+ test.describe('[Feature] — [scenario]', () => {
167
+ test.beforeEach(async ({ page }) => {
168
+ const { email, password } = TEST_USERS.regular
169
+ test.skip(!email, 'E2E_USER_EMAIL not set — skipping')
170
+ await signIn(page, email, password)
171
+ })
172
+
173
+ test('[behavior description]', async ({ page }) => {
174
+ await page.goto('/dashboard/feature')
175
+ await expect(page.getByRole('heading', { name: /title/i })).toBeVisible()
176
+ })
177
+ })
178
+ ```
179
+
180
+ Create `e2e/helpers/auth.ts` if it doesn't exist (see `/sk:e2e` Playwright Setup Reference).
181
+
182
+ **Run the E2E spec to confirm tests fail or skip** (they should fail until implementation, or skip if env vars aren't set — both are acceptable for the RED phase):
183
+ ```bash
184
+ npx playwright test e2e/<feature>.spec.ts --reporter=list
185
+ ```
186
+
187
+ ### 8c. Playwright MCP Inspection (optional)
149
188
 
150
- **Only if `@playwright/sk:test` is detected:**
189
+ **Only if the Playwright MCP plugin is active in the session AND live selectors are needed:**
151
190
 
152
191
  Use the Playwright MCP plugin to inspect live page state for more accurate selectors:
153
192
 
154
193
  1. Navigate to target URL
155
194
  2. Capture accessibility snapshot for role-based selectors
156
195
  3. Screenshot for visual reference
157
- 4. Optionally run inline assertions for complex interactions
158
196
 
159
197
  ### 9. Verify Tests Fail (Red Phase)
160
198
 
@@ -170,6 +208,7 @@ Output:
170
208
  ```
171
209
  BE tests written: X tests in Y files ([framework])
172
210
  FE tests written: X tests in Y files ([framework]) ← omit if no FE stack
211
+ E2E specs written: X tests in Y files (Playwright) ← omit if no playwright.config.ts
173
212
  Existing tests updated: X files
174
213
  Status: RED (tests fail as expected — ready for implementation)
175
214
  ```