@haystackeditor/cli 0.5.0 → 0.7.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/README.md +2 -2
- package/dist/commands/config.d.ts +19 -0
- package/dist/commands/config.js +133 -0
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +11 -9
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +24 -4
- package/dist/types.d.ts +69 -2
- package/dist/types.js +1 -1
- package/dist/utils/config.d.ts +1 -1
- package/dist/utils/config.js +4 -8
- package/dist/utils/detect.d.ts +35 -2
- package/dist/utils/detect.js +204 -5
- package/dist/utils/secrets.d.ts +1 -1
- package/dist/utils/secrets.js +1 -1
- package/dist/utils/skill.d.ts +2 -2
- package/dist/utils/skill.js +655 -446
- package/package.json +1 -1
package/dist/utils/skill.js
CHANGED
|
@@ -6,236 +6,487 @@ import * as fs from 'fs/promises';
|
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
/**
|
|
8
8
|
* Claude Code slash command - invoked with /haystack
|
|
9
|
-
* This is the "one command" entry point for users.
|
|
10
|
-
* Uses task decomposition - complete one step, validate, then next step.
|
|
11
9
|
*/
|
|
12
10
|
const CLAUDE_COMMAND_CONTENT = `# Set Up Haystack Verification
|
|
13
11
|
|
|
14
|
-
|
|
12
|
+
Follow .agents/skills/setup-haystack.md to set up Haystack verification for this repo.
|
|
13
|
+
`;
|
|
14
|
+
const SKILL_CONTENT = `# Haystack Verification Setup
|
|
15
|
+
|
|
16
|
+
**Your job**: Help the verification system understand this app so it can visually verify PRs work correctly.
|
|
17
|
+
|
|
18
|
+
The verification system has two parts:
|
|
19
|
+
1. **Planner** - An AI that explores the codebase to figure out what to test
|
|
20
|
+
2. **Executor** - Takes screenshots based on the plan
|
|
21
|
+
|
|
22
|
+
Your \`.haystack.json\` flows feed into the **corpus** - they're hints for the Planner about what routes exist, what selectors to use, and what the core user journey looks like.
|
|
15
23
|
|
|
16
24
|
---
|
|
17
25
|
|
|
18
|
-
##
|
|
26
|
+
## Step 1: Generate Base Config
|
|
19
27
|
|
|
20
28
|
\`\`\`bash
|
|
21
29
|
npx @haystackeditor/cli init --yes
|
|
22
30
|
\`\`\`
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
This detects your dev server command, port, and service dependencies.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Step 2: Understand the App's Core Feature
|
|
37
|
+
|
|
38
|
+
Before writing any flows, answer this:
|
|
39
|
+
|
|
40
|
+
**What is the ONE main thing users do in this app?**
|
|
41
|
+
|
|
42
|
+
Read the codebase to understand the core user journey:
|
|
43
|
+
- E-commerce: browse products → add to cart → checkout
|
|
44
|
+
- Dashboard: view metrics → filter data → export
|
|
45
|
+
- Editor: create document → edit → save
|
|
46
|
+
- Social: view feed → create post → interact
|
|
47
|
+
- SaaS: sign up → configure → use feature
|
|
48
|
+
|
|
49
|
+
Your flows should describe THIS journey, not just "pages load".
|
|
25
50
|
|
|
26
51
|
---
|
|
27
52
|
|
|
28
|
-
##
|
|
53
|
+
## Step 3: Assess Data Needs
|
|
29
54
|
|
|
30
|
-
Find every route in the app:
|
|
31
55
|
\`\`\`bash
|
|
32
|
-
|
|
33
|
-
|
|
56
|
+
# Find API calls
|
|
57
|
+
grep -r "fetch(\\|useQuery\\|useSWR\\|axios" src/ --include="*.tsx" --include="*.ts" | wc -l
|
|
58
|
+
|
|
59
|
+
# Find dynamic routes
|
|
60
|
+
grep -r "useParams\\|router.query\\|\\[.*\\]" src/ --include="*.tsx" | head -10
|
|
61
|
+
|
|
62
|
+
# Find external domains
|
|
63
|
+
grep -r "https://" src/ --include="*.ts" --include="*.tsx" | grep -v node_modules | head -10
|
|
34
64
|
\`\`\`
|
|
35
65
|
|
|
36
|
-
|
|
66
|
+
---
|
|
37
67
|
|
|
38
|
-
|
|
68
|
+
## STOP - Ask About Test Data Strategy
|
|
69
|
+
|
|
70
|
+
**You MUST ask the user before proceeding:**
|
|
71
|
+
|
|
72
|
+
> I analyzed the codebase and found:
|
|
73
|
+
> - X API fetch calls
|
|
74
|
+
> - Y dynamic routes with parameters
|
|
75
|
+
> - These external domains: [list them]
|
|
76
|
+
>
|
|
77
|
+
> **How should I handle test data for verification?**
|
|
78
|
+
>
|
|
79
|
+
> 1. **Passthrough** - Let API calls through to real servers (add domains to \`network.allow\`)
|
|
80
|
+
> 2. **Staging URL** - Point flows at your staging/demo environment
|
|
81
|
+
> 3. **Local fixtures** - JSON files in \`fixtures/\` directory
|
|
82
|
+
> 4. **Skip data pages** - Only test static pages that don't need API data
|
|
83
|
+
|
|
84
|
+
**Wait for the user's response before continuing.**
|
|
39
85
|
|
|
40
86
|
---
|
|
41
87
|
|
|
42
|
-
##
|
|
88
|
+
## Step 4: Write Flows
|
|
43
89
|
|
|
44
|
-
|
|
90
|
+
Flows tell the Planner about your app's routes, UI elements, and user journeys.
|
|
45
91
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
92
|
+
### Structure
|
|
93
|
+
|
|
94
|
+
\`\`\`yaml
|
|
95
|
+
flows:
|
|
96
|
+
- name: "Descriptive name of what this tests"
|
|
97
|
+
trigger: always # or on_change with watch_patterns
|
|
98
|
+
steps:
|
|
99
|
+
- action: navigate
|
|
100
|
+
url: "/"
|
|
101
|
+
- action: wait_for
|
|
102
|
+
selector: "[data-testid='specific-element']"
|
|
103
|
+
- action: click
|
|
104
|
+
selector: "[data-testid='button']"
|
|
105
|
+
- action: screenshot
|
|
106
|
+
name: "result"
|
|
50
107
|
\`\`\`
|
|
51
108
|
|
|
52
|
-
|
|
53
|
-
- \`[data-testid='dashboard']\`
|
|
54
|
-
- \`.dashboard-content\`
|
|
55
|
-
- \`[role='main']\`
|
|
109
|
+
### What to Include
|
|
56
110
|
|
|
57
|
-
|
|
111
|
+
**Core journey flow** (trigger: always):
|
|
112
|
+
- The main thing users do in your app
|
|
113
|
+
- Multiple steps with interactions (click, type)
|
|
114
|
+
- Waits for meaningful state changes
|
|
58
115
|
|
|
59
|
-
|
|
116
|
+
**Route coverage flows**:
|
|
117
|
+
- One flow per major route
|
|
118
|
+
- Uses specific selectors the Planner can learn from
|
|
119
|
+
- \`watch_patterns\` to only run when relevant files change
|
|
60
120
|
|
|
61
|
-
|
|
121
|
+
### Finding Good Selectors
|
|
62
122
|
|
|
63
|
-
Find interactive elements:
|
|
64
123
|
\`\`\`bash
|
|
65
|
-
|
|
124
|
+
# Find data-testid attributes
|
|
125
|
+
grep -r "data-testid" src/ --include="*.tsx" | head -20
|
|
126
|
+
|
|
127
|
+
# Find aria-labels
|
|
128
|
+
grep -r "aria-label" src/ --include="*.tsx" | head -20
|
|
129
|
+
|
|
130
|
+
# Find component class names
|
|
131
|
+
grep -r "className=" src/components/ --include="*.tsx" | head -20
|
|
66
132
|
\`\`\`
|
|
67
133
|
|
|
68
|
-
|
|
134
|
+
Use specific selectors like:
|
|
135
|
+
- \`[data-testid='dashboard-chart']\`
|
|
136
|
+
- \`[aria-label='Submit form']\`
|
|
137
|
+
- \`.pricing-table\`
|
|
138
|
+
- \`button[type='submit']\`
|
|
139
|
+
|
|
140
|
+
If good selectors don't exist, add \`data-testid\` to key components.
|
|
141
|
+
|
|
142
|
+
### watch_patterns
|
|
143
|
+
|
|
144
|
+
For flows that only matter when certain files change:
|
|
69
145
|
|
|
70
146
|
\`\`\`yaml
|
|
71
|
-
- name: "
|
|
147
|
+
- name: "Settings page works"
|
|
148
|
+
trigger: on_change
|
|
149
|
+
watch_patterns:
|
|
150
|
+
- "src/pages/settings/**"
|
|
151
|
+
- "src/components/settings/**"
|
|
72
152
|
steps:
|
|
73
153
|
- action: navigate
|
|
74
|
-
url: "/"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
- action: screenshot
|
|
78
|
-
name: "after-toggle"
|
|
154
|
+
url: "/settings"
|
|
155
|
+
# ...
|
|
156
|
+
\`\`\`
|
|
79
157
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Step 5: Configure Fixtures (if needed)
|
|
161
|
+
|
|
162
|
+
Based on user's answer in Step 3:
|
|
163
|
+
|
|
164
|
+
**Passthrough**:
|
|
165
|
+
\`\`\`yaml
|
|
166
|
+
network:
|
|
167
|
+
allow:
|
|
168
|
+
- "api.example.com"
|
|
169
|
+
- "cdn.example.com"
|
|
170
|
+
\`\`\`
|
|
171
|
+
|
|
172
|
+
**Staging URL**:
|
|
173
|
+
\`\`\`yaml
|
|
174
|
+
flows:
|
|
175
|
+
- name: "Dashboard with real data"
|
|
176
|
+
steps:
|
|
177
|
+
- action: navigate
|
|
178
|
+
url: "https://staging.example.com/dashboard"
|
|
90
179
|
\`\`\`
|
|
91
180
|
|
|
92
|
-
|
|
181
|
+
**Local fixtures**:
|
|
182
|
+
\`\`\`yaml
|
|
183
|
+
fixtures:
|
|
184
|
+
- pattern: "/api/user"
|
|
185
|
+
source: "file://fixtures/user.json"
|
|
186
|
+
- pattern: "/api/data/*"
|
|
187
|
+
source: "file://fixtures/data.json"
|
|
188
|
+
\`\`\`
|
|
93
189
|
|
|
94
190
|
---
|
|
95
191
|
|
|
96
|
-
##
|
|
192
|
+
## Step 6: Commit
|
|
97
193
|
|
|
98
|
-
Find pages that fetch data:
|
|
99
194
|
\`\`\`bash
|
|
100
|
-
|
|
195
|
+
git add .haystack.json fixtures/
|
|
196
|
+
git commit -m "Add Haystack verification"
|
|
101
197
|
\`\`\`
|
|
102
198
|
|
|
103
|
-
|
|
199
|
+
Done! The Planner will use your flows to understand the app and create verification plans for PRs.
|
|
200
|
+
`;
|
|
201
|
+
const REFERENCE_CONTENT = `# Haystack Reference
|
|
202
|
+
|
|
203
|
+
Reference material for \`.haystack.json\` configuration. Only consult when needed for a specific step.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Fixture Patterns
|
|
208
|
+
|
|
209
|
+
### Option 1: Passthrough (Recommended - Easiest!)
|
|
104
210
|
\`\`\`yaml
|
|
211
|
+
# Let API calls through to real servers - no mocking needed
|
|
105
212
|
fixtures:
|
|
106
|
-
- pattern: "/api
|
|
107
|
-
source:
|
|
108
|
-
|
|
109
|
-
|
|
213
|
+
- pattern: "/api/*"
|
|
214
|
+
source: passthrough
|
|
215
|
+
|
|
216
|
+
network:
|
|
217
|
+
allow:
|
|
218
|
+
- "api.example.com"
|
|
219
|
+
- "cdn.example.com"
|
|
110
220
|
\`\`\`
|
|
111
221
|
|
|
112
|
-
|
|
222
|
+
**This is the easiest option.** Just allow the domains your app calls and let real APIs handle requests. No fixtures to maintain.
|
|
113
223
|
|
|
114
|
-
|
|
224
|
+
### Option 2: Staging/Demo URL
|
|
225
|
+
\`\`\`yaml
|
|
226
|
+
# Point flows directly at staging - no fixtures needed
|
|
227
|
+
flows:
|
|
228
|
+
- name: "Dashboard loads"
|
|
229
|
+
url: "https://staging.example.com/dashboard"
|
|
230
|
+
wait_for_selector: ".dashboard-content"
|
|
115
231
|
|
|
116
|
-
|
|
232
|
+
network:
|
|
233
|
+
allow:
|
|
234
|
+
- "staging.example.com"
|
|
235
|
+
\`\`\`
|
|
117
236
|
|
|
118
|
-
|
|
119
|
-
\`\`\`
|
|
120
|
-
|
|
121
|
-
|
|
237
|
+
### Option 3: Pre-signed URLs (S3/Cloud Storage)
|
|
238
|
+
\`\`\`yaml
|
|
239
|
+
fixtures:
|
|
240
|
+
- pattern: "/api/data"
|
|
241
|
+
source: "$FIXTURE_DATA_URL" # Pre-signed URL from CI
|
|
242
|
+
- pattern: "/api/users"
|
|
243
|
+
source: "$FIXTURE_USERS_URL"
|
|
244
|
+
|
|
245
|
+
secrets:
|
|
246
|
+
- FIXTURE_DATA_URL
|
|
247
|
+
- FIXTURE_USERS_URL
|
|
248
|
+
\`\`\`
|
|
122
249
|
|
|
123
|
-
|
|
124
|
-
grep -c "action: click\\|action: type" .haystack.yml
|
|
250
|
+
See "Pre-signed URL Setup" below for CI configuration.
|
|
125
251
|
|
|
126
|
-
|
|
127
|
-
|
|
252
|
+
### Option 4: Local Fixtures (Simple Apps Only)
|
|
253
|
+
\`\`\`yaml
|
|
254
|
+
fixtures:
|
|
255
|
+
- pattern: "/api/user"
|
|
256
|
+
source: "file://fixtures/user.json"
|
|
257
|
+
- pattern: "/api/settings"
|
|
258
|
+
source: "file://fixtures/settings.json"
|
|
128
259
|
\`\`\`
|
|
129
260
|
|
|
130
|
-
|
|
261
|
+
Create matching JSON files in \`fixtures/\` directory. Only use for small, stable data.
|
|
262
|
+
|
|
263
|
+
### Option 5: HTTP Endpoint (Self-hosted)
|
|
264
|
+
\`\`\`yaml
|
|
265
|
+
fixtures:
|
|
266
|
+
- pattern: "/api/*"
|
|
267
|
+
source: "https://fixtures.yourcompany.com/api"
|
|
268
|
+
headers:
|
|
269
|
+
Authorization: "Bearer $FIXTURES_TOKEN"
|
|
270
|
+
|
|
271
|
+
network:
|
|
272
|
+
allow:
|
|
273
|
+
- "fixtures.yourcompany.com"
|
|
274
|
+
\`\`\`
|
|
131
275
|
|
|
132
276
|
---
|
|
133
277
|
|
|
134
|
-
##
|
|
278
|
+
## Pre-signed URL Setup
|
|
135
279
|
|
|
280
|
+
Generate temporary URLs in CI, pass to Haystack. No cloud credentials in sandbox.
|
|
281
|
+
|
|
282
|
+
**Note:** URLs valid for 24 hours. Run on push to main to keep URLs fresh.
|
|
283
|
+
|
|
284
|
+
### AWS S3
|
|
136
285
|
\`\`\`bash
|
|
137
|
-
|
|
138
|
-
|
|
286
|
+
# Generate pre-signed URL (valid 24 hours)
|
|
287
|
+
aws s3 presign s3://my-bucket/fixtures/data.json --expires-in 86400
|
|
139
288
|
\`\`\`
|
|
140
289
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
290
|
+
### Google Cloud Storage
|
|
291
|
+
\`\`\`bash
|
|
292
|
+
# Generate signed URL (valid 24 hours)
|
|
293
|
+
gcloud storage sign-url gs://my-bucket/fixtures/data.json --duration=24h
|
|
294
|
+
\`\`\`
|
|
144
295
|
|
|
145
|
-
|
|
296
|
+
### Azure Blob Storage
|
|
297
|
+
\`\`\`bash
|
|
298
|
+
# Generate SAS URL (valid 24 hours)
|
|
299
|
+
az storage blob generate-sas --account-name myaccount --container fixtures \\
|
|
300
|
+
--name data.json --permissions r --expiry $(date -u -d '+1 day' +%Y-%m-%dT%H:%MZ) \\
|
|
301
|
+
--full-uri
|
|
302
|
+
\`\`\`
|
|
146
303
|
|
|
147
|
-
|
|
304
|
+
### Cloudflare R2 / DigitalOcean Spaces / MinIO
|
|
305
|
+
\`\`\`bash
|
|
306
|
+
# S3-compatible - use aws cli with custom endpoint
|
|
307
|
+
aws s3 presign s3://my-bucket/data.json --expires-in 86400 \\
|
|
308
|
+
--endpoint-url https://your-r2-endpoint.com
|
|
309
|
+
\`\`\`
|
|
148
310
|
|
|
149
|
-
|
|
150
|
-
2. An AI agent reads the "flows" in \`.haystack.yml\`
|
|
151
|
-
3. The agent executes each flow to verify changes work correctly
|
|
152
|
-
4. Results (screenshots, API responses, errors) are posted to the PR
|
|
311
|
+
---
|
|
153
312
|
|
|
154
|
-
|
|
313
|
+
### GitHub Actions Example
|
|
314
|
+
\`\`\`yaml
|
|
315
|
+
# Run on push to main to keep URLs fresh
|
|
316
|
+
on:
|
|
317
|
+
push:
|
|
318
|
+
branches: [main]
|
|
319
|
+
|
|
320
|
+
jobs:
|
|
321
|
+
update-fixture-urls:
|
|
322
|
+
runs-on: ubuntu-latest
|
|
323
|
+
permissions:
|
|
324
|
+
id-token: write # OIDC - no long-lived secrets
|
|
325
|
+
steps:
|
|
326
|
+
- uses: aws-actions/configure-aws-credentials@v4
|
|
327
|
+
with:
|
|
328
|
+
role-to-assume: arn:aws:iam::123456789:role/haystack-fixtures
|
|
329
|
+
aws-region: us-east-1
|
|
330
|
+
|
|
331
|
+
- name: Generate pre-signed URLs (valid 24 hours)
|
|
332
|
+
run: |
|
|
333
|
+
URL=$(aws s3 presign s3://my-bucket/fixtures/data.json --expires-in 86400)
|
|
334
|
+
haystack secrets set FIXTURE_DATA_URL "$URL"
|
|
335
|
+
\`\`\`
|
|
155
336
|
|
|
156
|
-
|
|
337
|
+
### General Approach
|
|
338
|
+
1. Use OIDC to get temporary cloud credentials (no long-lived secrets)
|
|
339
|
+
2. Generate pre-signed/signed URL for each fixture file (24 hour expiry)
|
|
340
|
+
3. Store with \`haystack secrets set FIXTURE_URL "$URL"\`
|
|
341
|
+
4. Run on push to main to keep URLs fresh
|
|
157
342
|
|
|
158
|
-
|
|
343
|
+
---
|
|
159
344
|
|
|
160
|
-
##
|
|
345
|
+
## Flow Examples
|
|
161
346
|
|
|
162
|
-
###
|
|
163
|
-
\`\`\`
|
|
164
|
-
|
|
347
|
+
### Basic Page Flow
|
|
348
|
+
\`\`\`yaml
|
|
349
|
+
flows:
|
|
350
|
+
- name: "Dashboard loads"
|
|
351
|
+
description: "Verify dashboard renders correctly"
|
|
352
|
+
trigger: always
|
|
353
|
+
watch_patterns:
|
|
354
|
+
- "src/components/dashboard/**"
|
|
355
|
+
steps:
|
|
356
|
+
- action: navigate
|
|
357
|
+
url: "/dashboard"
|
|
358
|
+
- action: wait_for
|
|
359
|
+
selector: "[data-testid='dashboard-content']"
|
|
360
|
+
- action: assert_no_errors
|
|
361
|
+
- action: screenshot
|
|
362
|
+
name: "dashboard"
|
|
165
363
|
\`\`\`
|
|
166
|
-
This auto-detects framework, ports, package manager and creates \`.haystack.yml\`.
|
|
167
364
|
|
|
168
|
-
###
|
|
169
|
-
|
|
365
|
+
### Interactive Flow (click)
|
|
366
|
+
\`\`\`yaml
|
|
367
|
+
- name: "Modal opens"
|
|
368
|
+
trigger: on_change
|
|
369
|
+
watch_patterns:
|
|
370
|
+
- "src/components/settings/**"
|
|
371
|
+
steps:
|
|
372
|
+
- action: navigate
|
|
373
|
+
url: "/settings"
|
|
374
|
+
- action: wait_for
|
|
375
|
+
selector: ".settings-page"
|
|
376
|
+
- action: click
|
|
377
|
+
selector: "button[aria-label='Settings']"
|
|
378
|
+
- action: wait_for
|
|
379
|
+
selector: "[role='dialog']"
|
|
380
|
+
- action: screenshot
|
|
381
|
+
name: "settings-modal"
|
|
382
|
+
\`\`\`
|
|
170
383
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
384
|
+
### Form Flow (type)
|
|
385
|
+
\`\`\`yaml
|
|
386
|
+
- name: "Contact form works"
|
|
387
|
+
trigger: on_change
|
|
388
|
+
watch_patterns:
|
|
389
|
+
- "src/components/contact/**"
|
|
390
|
+
steps:
|
|
391
|
+
- action: navigate
|
|
392
|
+
url: "/contact"
|
|
393
|
+
- action: wait_for
|
|
394
|
+
selector: "form"
|
|
395
|
+
- action: type
|
|
396
|
+
selector: "input[name='email']"
|
|
397
|
+
value: "test@example.com"
|
|
398
|
+
- action: click
|
|
399
|
+
selector: "button[type='submit']"
|
|
400
|
+
- action: wait_for
|
|
401
|
+
selector: ".success-message"
|
|
402
|
+
\`\`\`
|
|
176
403
|
|
|
177
|
-
|
|
404
|
+
### Backend API Flow
|
|
405
|
+
\`\`\`yaml
|
|
406
|
+
- name: "API health check"
|
|
407
|
+
trigger: always
|
|
408
|
+
steps:
|
|
409
|
+
- action: http_request
|
|
410
|
+
method: GET
|
|
411
|
+
url: "http://localhost:3001/health"
|
|
412
|
+
- action: assert_status
|
|
413
|
+
status: 200
|
|
414
|
+
\`\`\`
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## Finding Good Selectors
|
|
178
419
|
|
|
179
|
-
|
|
420
|
+
**Priority order:**
|
|
421
|
+
1. \`[data-testid='feature-name']\` - Best
|
|
422
|
+
2. \`[role='main']\`, \`[aria-label='...']\` - Semantic
|
|
423
|
+
3. \`.specific-class-name\` - Component-specific
|
|
424
|
+
4. Avoid: \`#root\`, \`div\`, \`h1\` - Too generic
|
|
425
|
+
|
|
426
|
+
**How to find:**
|
|
180
427
|
\`\`\`bash
|
|
181
|
-
|
|
182
|
-
|
|
428
|
+
# Find data-testid attributes
|
|
429
|
+
grep -r "data-testid" src/ --include="*.tsx"
|
|
430
|
+
|
|
431
|
+
# Find class names
|
|
432
|
+
grep -r "className=" src/components/Dashboard.tsx
|
|
433
|
+
|
|
434
|
+
# Find semantic roles
|
|
435
|
+
grep -r "role=\\|aria-label=" src/ --include="*.tsx"
|
|
183
436
|
\`\`\`
|
|
184
437
|
|
|
185
|
-
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## Config Structure
|
|
441
|
+
|
|
442
|
+
⚠️ **IMPORTANT**: \`flows\` must be at TOP LEVEL, not nested under \`verification\`!
|
|
186
443
|
|
|
187
444
|
\`\`\`yaml
|
|
188
445
|
version: "1"
|
|
189
446
|
name: my-app
|
|
190
447
|
|
|
191
|
-
# Dev server configuration
|
|
192
448
|
dev_server:
|
|
193
449
|
command: pnpm dev
|
|
194
450
|
port: 3000
|
|
195
|
-
ready_pattern: "Local
|
|
196
|
-
env:
|
|
197
|
-
SKIP_AUTH: "true" # Auth bypass for testing
|
|
451
|
+
ready_pattern: "ready|Local:|started" # Regex for server ready
|
|
198
452
|
|
|
199
|
-
# Verification commands
|
|
453
|
+
# Verification commands - MUST include build
|
|
200
454
|
verification:
|
|
201
455
|
commands:
|
|
202
456
|
- name: build
|
|
203
|
-
run: pnpm build
|
|
457
|
+
run: pnpm build # ← REQUIRED
|
|
204
458
|
- name: lint
|
|
205
459
|
run: pnpm lint
|
|
206
460
|
- name: typecheck
|
|
207
461
|
run: pnpm tsc --noEmit
|
|
208
462
|
|
|
209
|
-
|
|
210
|
-
|
|
463
|
+
network:
|
|
464
|
+
allow:
|
|
465
|
+
- "api.example.com"
|
|
466
|
+
|
|
467
|
+
# ⚠️ flows at TOP LEVEL - NOT under verification!
|
|
211
468
|
flows:
|
|
212
|
-
- name: "
|
|
213
|
-
description: "Verify the landing page renders without errors"
|
|
469
|
+
- name: "Page loads"
|
|
214
470
|
trigger: always
|
|
215
471
|
steps:
|
|
216
472
|
- action: navigate
|
|
217
473
|
url: "/"
|
|
218
474
|
- action: wait_for
|
|
219
|
-
selector: "[data-testid='
|
|
475
|
+
selector: "[data-testid='main']"
|
|
220
476
|
- action: screenshot
|
|
221
|
-
name: "
|
|
477
|
+
name: "home"
|
|
222
478
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
- action: navigate
|
|
230
|
-
url: "/dashboard"
|
|
231
|
-
- action: wait_for
|
|
232
|
-
selector: ".dashboard-content"
|
|
233
|
-
- action: assert_no_errors
|
|
479
|
+
secrets:
|
|
480
|
+
- API_TOKEN
|
|
481
|
+
|
|
482
|
+
fixtures:
|
|
483
|
+
- pattern: "/api/*"
|
|
484
|
+
source: passthrough
|
|
234
485
|
\`\`\`
|
|
235
486
|
|
|
236
|
-
|
|
487
|
+
---
|
|
237
488
|
|
|
238
|
-
|
|
489
|
+
## Monorepo Configuration
|
|
239
490
|
|
|
240
491
|
\`\`\`yaml
|
|
241
492
|
version: "1"
|
|
@@ -247,8 +498,6 @@ services:
|
|
|
247
498
|
command: pnpm dev
|
|
248
499
|
port: 3000
|
|
249
500
|
ready_pattern: "Local:"
|
|
250
|
-
env:
|
|
251
|
-
VITE_SKIP_AUTH: "true"
|
|
252
501
|
|
|
253
502
|
api:
|
|
254
503
|
root: packages/api
|
|
@@ -260,410 +509,370 @@ services:
|
|
|
260
509
|
root: infra/worker
|
|
261
510
|
command: pnpm wrangler dev
|
|
262
511
|
port: 8787
|
|
263
|
-
|
|
264
|
-
# Batch jobs (run once, don't stay running)
|
|
265
|
-
analysis:
|
|
266
|
-
root: packages/analysis
|
|
267
|
-
type: batch
|
|
268
|
-
command: pnpm start
|
|
269
|
-
|
|
270
|
-
verification:
|
|
271
|
-
commands:
|
|
272
|
-
- name: build
|
|
273
|
-
run: pnpm build
|
|
274
|
-
- name: lint
|
|
275
|
-
run: pnpm lint
|
|
276
512
|
\`\`\`
|
|
277
513
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
When services live in separate git repositories (not a monorepo), each repo gets its own \`.haystack.yml\`:
|
|
281
|
-
|
|
282
|
-
**Frontend repo** - Mock the API it depends on:
|
|
283
|
-
\`\`\`yaml
|
|
284
|
-
version: "1"
|
|
285
|
-
name: frontend
|
|
286
|
-
|
|
287
|
-
dev_server:
|
|
288
|
-
command: pnpm dev
|
|
289
|
-
port: 3000
|
|
290
|
-
env:
|
|
291
|
-
VITE_API_URL: "http://localhost:8080" # Will be mocked
|
|
514
|
+
---
|
|
292
515
|
|
|
293
|
-
|
|
294
|
-
fixtures:
|
|
295
|
-
- pattern: "/api/*"
|
|
296
|
-
source: "file://fixtures/api-responses.json"
|
|
297
|
-
\`\`\`
|
|
516
|
+
## Flow Triggers
|
|
298
517
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
518
|
+
| Trigger | When it runs |
|
|
519
|
+
|---------|--------------|
|
|
520
|
+
| \`always\` | Every PR |
|
|
521
|
+
| \`on_change\` | Only when \`watch_patterns\` match changed files |
|
|
522
|
+
`;
|
|
523
|
+
const PREPARE_VERIFICATION_CONTENT = `# Prepare Codebase for Verification
|
|
303
524
|
|
|
304
|
-
|
|
305
|
-
command: pnpm dev
|
|
306
|
-
port: 8080
|
|
525
|
+
**Your job**: Make this codebase easy to verify by adding semantic identifiers that the verification system can target.
|
|
307
526
|
|
|
308
|
-
verification
|
|
309
|
-
commands:
|
|
310
|
-
- name: test
|
|
311
|
-
run: pnpm test
|
|
312
|
-
\`\`\`
|
|
527
|
+
The verification Planner needs to find UI elements by selectors. Generic selectors like \`div\` or \`.flex\` are useless. Your job is to add meaningful identifiers throughout the codebase.
|
|
313
528
|
|
|
314
|
-
|
|
529
|
+
---
|
|
315
530
|
|
|
316
|
-
##
|
|
531
|
+
## What to Add
|
|
317
532
|
|
|
318
|
-
|
|
533
|
+
### 1. \`aria-label\` on Interactive Elements
|
|
319
534
|
|
|
320
|
-
|
|
321
|
-
fixtures:
|
|
322
|
-
# Local file (small data, commit to repo)
|
|
323
|
-
- pattern: "/api/user"
|
|
324
|
-
source: "file://fixtures/user.json"
|
|
325
|
-
|
|
326
|
-
# From staging server
|
|
327
|
-
- pattern: "/api/dashboard"
|
|
328
|
-
source: "https://staging.example.com/api/dashboard"
|
|
329
|
-
headers:
|
|
330
|
-
Authorization: "Bearer $STAGING_TOKEN"
|
|
535
|
+
Every clickable/interactive element should have an aria-label describing what it does:
|
|
331
536
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
537
|
+
\`\`\`tsx
|
|
538
|
+
// Before
|
|
539
|
+
<button onClick={onSave}>💾</button>
|
|
540
|
+
<button onClick={() => setOpen(true)}>
|
|
541
|
+
<MenuIcon />
|
|
542
|
+
</button>
|
|
335
543
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
544
|
+
// After
|
|
545
|
+
<button onClick={onSave} aria-label="Save document">💾</button>
|
|
546
|
+
<button onClick={() => setOpen(true)} aria-label="Open menu">
|
|
547
|
+
<MenuIcon />
|
|
548
|
+
</button>
|
|
339
549
|
\`\`\`
|
|
340
550
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
- \`navigate\` - Go to URL
|
|
367
|
-
- \`wait_for\` - Wait for element
|
|
368
|
-
- \`click\` - Click element
|
|
369
|
-
- \`type\` - Enter text
|
|
370
|
-
- \`screenshot\` - Capture screenshot
|
|
371
|
-
- \`assert_no_errors\` - Check for error states
|
|
551
|
+
**Target elements:**
|
|
552
|
+
- Buttons (especially icon-only buttons)
|
|
553
|
+
- Links without descriptive text
|
|
554
|
+
- Toggle switches
|
|
555
|
+
- Dropdown triggers
|
|
556
|
+
- Modal open/close buttons
|
|
557
|
+
- Form submit buttons
|
|
558
|
+
|
|
559
|
+
### 2. \`data-testid\` on Key Sections
|
|
560
|
+
|
|
561
|
+
Major UI sections should have data-testid for easy targeting:
|
|
562
|
+
|
|
563
|
+
\`\`\`tsx
|
|
564
|
+
// Before
|
|
565
|
+
<div className="flex flex-col p-4">
|
|
566
|
+
<h1>Dashboard</h1>
|
|
567
|
+
{/* content */}
|
|
568
|
+
</div>
|
|
569
|
+
|
|
570
|
+
// After
|
|
571
|
+
<div className="flex flex-col p-4" data-testid="dashboard-container">
|
|
572
|
+
<h1>Dashboard</h1>
|
|
573
|
+
{/* content */}
|
|
574
|
+
</div>
|
|
575
|
+
\`\`\`
|
|
372
576
|
|
|
373
|
-
**
|
|
374
|
-
-
|
|
375
|
-
-
|
|
376
|
-
-
|
|
577
|
+
**Target sections:**
|
|
578
|
+
- Page containers (dashboard, settings, profile)
|
|
579
|
+
- Navigation bars/sidebars
|
|
580
|
+
- Modal/dialog content
|
|
581
|
+
- Form containers
|
|
582
|
+
- Data tables/lists
|
|
583
|
+
- Card components
|
|
584
|
+
- Loading states
|
|
585
|
+
- Error states
|
|
586
|
+
- Empty states
|
|
587
|
+
|
|
588
|
+
### 3. \`role\` Attributes for Semantic Structure
|
|
589
|
+
|
|
590
|
+
Add ARIA roles where HTML semantics aren't clear:
|
|
591
|
+
|
|
592
|
+
\`\`\`tsx
|
|
593
|
+
// Before
|
|
594
|
+
<div className="modal-overlay">
|
|
595
|
+
<div className="modal-content">
|
|
596
|
+
|
|
597
|
+
// After
|
|
598
|
+
<div className="modal-overlay" role="presentation">
|
|
599
|
+
<div className="modal-content" role="dialog" aria-modal="true">
|
|
600
|
+
\`\`\`
|
|
377
601
|
|
|
378
|
-
|
|
602
|
+
**Common roles:**
|
|
603
|
+
- \`role="dialog"\` - Modals/dialogs
|
|
604
|
+
- \`role="navigation"\` - Nav sections
|
|
605
|
+
- \`role="main"\` - Main content area
|
|
606
|
+
- \`role="alert"\` - Error/success messages
|
|
607
|
+
- \`role="status"\` - Loading indicators
|
|
608
|
+
- \`role="tablist"\`, \`role="tab"\`, \`role="tabpanel"\` - Tab interfaces
|
|
609
|
+
|
|
610
|
+
### 4. State Indicators
|
|
611
|
+
|
|
612
|
+
Add attributes that indicate UI state:
|
|
613
|
+
|
|
614
|
+
\`\`\`tsx
|
|
615
|
+
// Before
|
|
616
|
+
<button onClick={toggle}>
|
|
617
|
+
{isOpen ? 'Close' : 'Open'}
|
|
618
|
+
</button>
|
|
619
|
+
|
|
620
|
+
// After
|
|
621
|
+
<button
|
|
622
|
+
onClick={toggle}
|
|
623
|
+
aria-expanded={isOpen}
|
|
624
|
+
aria-label={isOpen ? 'Close panel' : 'Open panel'}
|
|
625
|
+
>
|
|
626
|
+
{isOpen ? 'Close' : 'Open'}
|
|
627
|
+
</button>
|
|
628
|
+
\`\`\`
|
|
379
629
|
|
|
380
|
-
|
|
630
|
+
**State attributes:**
|
|
631
|
+
- \`aria-expanded\` - Collapsible sections
|
|
632
|
+
- \`aria-selected\` - Selected items in lists
|
|
633
|
+
- \`aria-checked\` - Checkboxes/toggles
|
|
634
|
+
- \`aria-disabled\` - Disabled elements
|
|
635
|
+
- \`aria-busy\` - Loading states
|
|
636
|
+
- \`data-state="loading|error|success"\` - Custom states
|
|
637
|
+
|
|
638
|
+
### 5. Form Accessibility
|
|
639
|
+
|
|
640
|
+
Forms should have proper labels and descriptions:
|
|
641
|
+
|
|
642
|
+
\`\`\`tsx
|
|
643
|
+
// Before
|
|
644
|
+
<input type="email" placeholder="Email" />
|
|
645
|
+
<span className="text-red-500">{error}</span>
|
|
646
|
+
|
|
647
|
+
// After
|
|
648
|
+
<input
|
|
649
|
+
type="email"
|
|
650
|
+
placeholder="Email"
|
|
651
|
+
aria-label="Email address"
|
|
652
|
+
aria-describedby={error ? "email-error" : undefined}
|
|
653
|
+
aria-invalid={!!error}
|
|
654
|
+
/>
|
|
655
|
+
{error && <span id="email-error" role="alert" className="text-red-500">{error}</span>}
|
|
656
|
+
\`\`\`
|
|
381
657
|
|
|
382
|
-
|
|
383
|
-
|-----------|---------|
|
|
384
|
-
| Vite/React | \`VITE_SKIP_AUTH=true\` |
|
|
385
|
-
| Next.js | \`NEXT_PUBLIC_SKIP_AUTH=true\` |
|
|
386
|
-
| Express | \`SKIP_AUTH=true\` |
|
|
387
|
-
| Rails | \`SKIP_AUTH=true\` |
|
|
658
|
+
---
|
|
388
659
|
|
|
389
|
-
|
|
660
|
+
## Step 1: Scan for Missing Identifiers
|
|
390
661
|
|
|
391
|
-
|
|
662
|
+
\`\`\`bash
|
|
663
|
+
# Find buttons without aria-label
|
|
664
|
+
grep -rn "<button" src/ --include="*.tsx" | grep -v "aria-label" | head -20
|
|
392
665
|
|
|
393
|
-
|
|
666
|
+
# Find icon-only buttons (likely missing labels)
|
|
667
|
+
grep -rn "<button.*Icon\\|<button.*>.*</.*Icon>" src/ --include="*.tsx" | head -20
|
|
394
668
|
|
|
395
|
-
|
|
669
|
+
# Find modals/dialogs without role
|
|
670
|
+
grep -rn "modal\\|dialog\\|Modal\\|Dialog" src/ --include="*.tsx" | grep -v "role=" | head -20
|
|
396
671
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
3. [ ] **Interactive flows**: At least 3 flows that click buttons, open modals, or submit forms
|
|
400
|
-
4. [ ] **Fixtures**: Pages with \`:id\` params or API fetches have fixtures (staging URL or local file)
|
|
401
|
-
5. [ ] **Backend API flows**: If app has API endpoints, add http_request flows to test them
|
|
402
|
-
6. [ ] **Watch patterns**: Each flow's \`watch_patterns\` matches the component file paths
|
|
672
|
+
# Find forms without proper labeling
|
|
673
|
+
grep -rn "<input\\|<select\\|<textarea" src/ --include="*.tsx" | grep -v "aria-label\\|id=" | head -20
|
|
403
674
|
|
|
404
|
-
|
|
675
|
+
# Find major components (likely need data-testid)
|
|
676
|
+
ls src/components/ src/pages/ 2>/dev/null
|
|
677
|
+
\`\`\`
|
|
405
678
|
|
|
406
679
|
---
|
|
407
680
|
|
|
408
|
-
|
|
681
|
+
## Step 2: Prioritize by Impact
|
|
409
682
|
|
|
410
|
-
|
|
683
|
+
Focus on elements the verification system is most likely to need:
|
|
411
684
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
685
|
+
**High Priority (do first):**
|
|
686
|
+
1. Navigation elements (header, sidebar, menu buttons)
|
|
687
|
+
2. Primary actions (submit buttons, save buttons, CTAs)
|
|
688
|
+
3. Modal triggers and dialogs
|
|
689
|
+
4. Form inputs and submit buttons
|
|
690
|
+
5. Page-level containers
|
|
415
691
|
|
|
416
|
-
|
|
417
|
-
|
|
692
|
+
**Medium Priority:**
|
|
693
|
+
1. Toggle switches and checkboxes
|
|
694
|
+
2. Dropdown menus
|
|
695
|
+
3. Tab interfaces
|
|
696
|
+
4. Cards and list items
|
|
697
|
+
5. Loading/error states
|
|
418
698
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
699
|
+
**Lower Priority:**
|
|
700
|
+
1. Decorative elements
|
|
701
|
+
2. Static content sections
|
|
702
|
+
3. Footer links
|
|
422
703
|
|
|
423
|
-
|
|
704
|
+
---
|
|
424
705
|
|
|
425
|
-
|
|
706
|
+
## Step 3: Add Identifiers Systematically
|
|
426
707
|
|
|
427
|
-
|
|
428
|
-
1. \`[data-testid='feature-name']\` - Best, explicit test hooks
|
|
429
|
-
2. \`[role='main']\`, \`[role='navigation']\` - Semantic roles
|
|
430
|
-
3. \`.feature-specific-class\` - Component-specific classes
|
|
431
|
-
4. \`h1\`, \`.page-title\` - Unique page identifiers
|
|
708
|
+
Go component by component. For each component:
|
|
432
709
|
|
|
433
|
-
**
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
710
|
+
1. **Check the component's purpose** - What does it DO?
|
|
711
|
+
2. **Add aria-label** to interactive elements describing the ACTION
|
|
712
|
+
3. **Add data-testid** to the container if it's a major section
|
|
713
|
+
4. **Add role** if the semantic HTML isn't clear
|
|
714
|
+
5. **Add state attributes** if the component has dynamic states
|
|
437
715
|
|
|
438
|
-
|
|
439
|
-
grep -r "className=" src/components/Dashboard.tsx
|
|
716
|
+
### Naming Conventions
|
|
440
717
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
718
|
+
**aria-label**: Describe the action, not the element
|
|
719
|
+
- ✅ \`aria-label="Close modal"\`
|
|
720
|
+
- ✅ \`aria-label="Submit contact form"\`
|
|
721
|
+
- ❌ \`aria-label="Button"\`
|
|
722
|
+
- ❌ \`aria-label="Click here"\`
|
|
444
723
|
|
|
445
|
-
**
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
-
|
|
449
|
-
|
|
724
|
+
**data-testid**: Use kebab-case, describe the section
|
|
725
|
+
- ✅ \`data-testid="user-profile-card"\`
|
|
726
|
+
- ✅ \`data-testid="search-results-list"\`
|
|
727
|
+
- ❌ \`data-testid="div1"\`
|
|
728
|
+
- ❌ \`data-testid="container"\`
|
|
450
729
|
|
|
451
|
-
|
|
452
|
-
- action: wait_for
|
|
453
|
-
selector: "[data-testid='dashboard-content']"
|
|
454
|
-
# or
|
|
455
|
-
- action: wait_for
|
|
456
|
-
selector: ".dashboard-stats-grid"
|
|
457
|
-
# or
|
|
458
|
-
- action: wait_for
|
|
459
|
-
selector: "h1:has-text('Dashboard')"
|
|
460
|
-
\`\`\`
|
|
730
|
+
---
|
|
461
731
|
|
|
462
|
-
|
|
732
|
+
## Step 4: Verify Coverage
|
|
463
733
|
|
|
464
|
-
|
|
734
|
+
After adding identifiers, check coverage:
|
|
465
735
|
|
|
466
|
-
**Look for interactive elements:**
|
|
467
736
|
\`\`\`bash
|
|
468
|
-
#
|
|
469
|
-
grep -r "
|
|
737
|
+
# Count aria-labels added
|
|
738
|
+
grep -r "aria-label" src/ --include="*.tsx" | wc -l
|
|
470
739
|
|
|
471
|
-
#
|
|
472
|
-
grep -r "
|
|
740
|
+
# Count data-testid added
|
|
741
|
+
grep -r "data-testid" src/ --include="*.tsx" | wc -l
|
|
473
742
|
|
|
474
|
-
#
|
|
475
|
-
grep -r "
|
|
743
|
+
# Count role attributes
|
|
744
|
+
grep -r "role=" src/ --include="*.tsx" | wc -l
|
|
476
745
|
|
|
477
|
-
#
|
|
478
|
-
grep -
|
|
746
|
+
# List all data-testid values (check for meaningful names)
|
|
747
|
+
grep -oh 'data-testid="[^"]*"' src/ -r --include="*.tsx" | sort -u
|
|
479
748
|
\`\`\`
|
|
480
749
|
|
|
481
|
-
|
|
482
|
-
\`\`\`yaml
|
|
483
|
-
# Theme toggle
|
|
484
|
-
- name: "Theme toggle works"
|
|
485
|
-
steps:
|
|
486
|
-
- action: navigate
|
|
487
|
-
url: "/"
|
|
488
|
-
- action: click
|
|
489
|
-
selector: "[data-testid='theme-toggle']"
|
|
490
|
-
- action: screenshot
|
|
491
|
-
name: "dark-mode"
|
|
492
|
-
- action: click
|
|
493
|
-
selector: "[data-testid='theme-toggle']"
|
|
494
|
-
- action: screenshot
|
|
495
|
-
name: "light-mode"
|
|
496
|
-
|
|
497
|
-
# Modal open/close
|
|
498
|
-
- name: "Settings modal opens"
|
|
499
|
-
steps:
|
|
500
|
-
- action: navigate
|
|
501
|
-
url: "/dashboard"
|
|
502
|
-
- action: click
|
|
503
|
-
selector: "[aria-label='Settings']"
|
|
504
|
-
- action: wait_for
|
|
505
|
-
selector: "[role='dialog']"
|
|
506
|
-
- action: screenshot
|
|
507
|
-
name: "settings-modal"
|
|
508
|
-
|
|
509
|
-
# Form submission
|
|
510
|
-
- name: "Contact form submits"
|
|
511
|
-
steps:
|
|
512
|
-
- action: navigate
|
|
513
|
-
url: "/contact"
|
|
514
|
-
- action: type
|
|
515
|
-
selector: "input[name='email']"
|
|
516
|
-
value: "test@example.com"
|
|
517
|
-
- action: click
|
|
518
|
-
selector: "button[type='submit']"
|
|
519
|
-
- action: wait_for
|
|
520
|
-
selector: ".success-message"
|
|
521
|
-
\`\`\`
|
|
750
|
+
---
|
|
522
751
|
|
|
523
|
-
|
|
752
|
+
## Step 5: Commit
|
|
524
753
|
|
|
525
|
-
**How to identify pages that need fixtures:**
|
|
526
754
|
\`\`\`bash
|
|
527
|
-
|
|
528
|
-
|
|
755
|
+
git add src/
|
|
756
|
+
git commit -m "Add accessibility attributes for verification
|
|
529
757
|
|
|
530
|
-
|
|
531
|
-
|
|
758
|
+
- Added aria-labels to interactive elements
|
|
759
|
+
- Added data-testid to major sections
|
|
760
|
+
- Added ARIA roles for semantic structure
|
|
761
|
+
- Added state indicators (aria-expanded, etc.)"
|
|
532
762
|
\`\`\`
|
|
533
763
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
**Option A: Pull from staging (recommended for large/dynamic data)**
|
|
537
|
-
\`\`\`yaml
|
|
538
|
-
fixtures:
|
|
539
|
-
# Pull real data from staging API
|
|
540
|
-
- pattern: "/api/pr/*"
|
|
541
|
-
source: "https://staging.example.com/api/pr/sample"
|
|
542
|
-
headers:
|
|
543
|
-
Authorization: "Bearer $STAGING_TOKEN"
|
|
764
|
+
---
|
|
544
765
|
|
|
545
|
-
|
|
546
|
-
- pattern: "/api/analytics/*"
|
|
547
|
-
source: "s3://my-fixtures-bucket/analytics-sample.json"
|
|
548
|
-
\`\`\`
|
|
766
|
+
## Quick Reference: Common Patterns
|
|
549
767
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
{"id": 1, "name": "Test User", "email": "test@example.com"}
|
|
556
|
-
EOF
|
|
768
|
+
### Icon Button
|
|
769
|
+
\`\`\`tsx
|
|
770
|
+
<button onClick={onAction} aria-label="Descriptive action name">
|
|
771
|
+
<Icon />
|
|
772
|
+
</button>
|
|
557
773
|
\`\`\`
|
|
558
774
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
775
|
+
### Modal
|
|
776
|
+
\`\`\`tsx
|
|
777
|
+
<div role="dialog" aria-modal="true" aria-labelledby="modal-title" data-testid="settings-modal">
|
|
778
|
+
<h2 id="modal-title">Settings</h2>
|
|
779
|
+
<button onClick={onClose} aria-label="Close settings">×</button>
|
|
780
|
+
</div>
|
|
563
781
|
\`\`\`
|
|
564
782
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
| Auth tokens, sessions | Passthrough or mock inline |
|
|
571
|
-
|
|
572
|
-
**Option B: Use demo/example routes**
|
|
573
|
-
\`\`\`bash
|
|
574
|
-
# Look for demo or example routes in the router
|
|
575
|
-
grep -r "demo\|example\|sample" src/ --include="*.tsx"
|
|
783
|
+
### Navigation
|
|
784
|
+
\`\`\`tsx
|
|
785
|
+
<nav aria-label="Main navigation" data-testid="main-nav">
|
|
786
|
+
<a href="/dashboard" aria-current={isActive ? "page" : undefined}>Dashboard</a>
|
|
787
|
+
</nav>
|
|
576
788
|
\`\`\`
|
|
577
789
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
790
|
+
### Toggle
|
|
791
|
+
\`\`\`tsx
|
|
792
|
+
<button
|
|
793
|
+
onClick={toggle}
|
|
794
|
+
aria-pressed={isOn}
|
|
795
|
+
aria-label={\`\${isOn ? 'Disable' : 'Enable'} notifications\`}
|
|
796
|
+
>
|
|
797
|
+
{isOn ? 'On' : 'Off'}
|
|
798
|
+
</button>
|
|
587
799
|
\`\`\`
|
|
588
800
|
|
|
589
|
-
###
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
801
|
+
### Loading State
|
|
802
|
+
\`\`\`tsx
|
|
803
|
+
<div data-testid="content-area" aria-busy={isLoading}>
|
|
804
|
+
{isLoading ? (
|
|
805
|
+
<div role="status" aria-label="Loading content">
|
|
806
|
+
<Spinner />
|
|
807
|
+
</div>
|
|
808
|
+
) : (
|
|
809
|
+
content
|
|
810
|
+
)}
|
|
811
|
+
</div>
|
|
597
812
|
\`\`\`
|
|
598
813
|
|
|
599
|
-
|
|
600
|
-
\`\`\`
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
- name: "API returns valid data"
|
|
613
|
-
trigger: on_change
|
|
614
|
-
watch_patterns:
|
|
615
|
-
- "src/api/**"
|
|
616
|
-
steps:
|
|
617
|
-
- action: http_request
|
|
618
|
-
method: GET
|
|
619
|
-
url: "http://localhost:3001/api/users"
|
|
620
|
-
- action: assert_status
|
|
621
|
-
status: 200
|
|
814
|
+
### Form Field
|
|
815
|
+
\`\`\`tsx
|
|
816
|
+
<div data-testid="email-field">
|
|
817
|
+
<label htmlFor="email">Email</label>
|
|
818
|
+
<input
|
|
819
|
+
id="email"
|
|
820
|
+
type="email"
|
|
821
|
+
aria-describedby={error ? "email-error" : "email-hint"}
|
|
822
|
+
aria-invalid={!!error}
|
|
823
|
+
/>
|
|
824
|
+
<span id="email-hint">We'll never share your email</span>
|
|
825
|
+
{error && <span id="email-error" role="alert">{error}</span>}
|
|
826
|
+
</div>
|
|
622
827
|
\`\`\`
|
|
623
828
|
|
|
624
|
-
###
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
829
|
+
### Expandable Section
|
|
830
|
+
\`\`\`tsx
|
|
831
|
+
<div data-testid="faq-section">
|
|
832
|
+
<button
|
|
833
|
+
onClick={() => setExpanded(!expanded)}
|
|
834
|
+
aria-expanded={expanded}
|
|
835
|
+
aria-controls="faq-content"
|
|
836
|
+
>
|
|
837
|
+
FAQ
|
|
838
|
+
</button>
|
|
839
|
+
<div id="faq-content" hidden={!expanded}>
|
|
840
|
+
{content}
|
|
841
|
+
</div>
|
|
842
|
+
</div>
|
|
633
843
|
\`\`\`
|
|
844
|
+
`;
|
|
845
|
+
const PREPARE_HAYSTACK_COMMAND = `# Prepare Codebase for Verification
|
|
634
846
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
Before you finish, verify:
|
|
638
|
-
|
|
639
|
-
1. [ ] **Page flows**: Every route has a flow
|
|
640
|
-
2. [ ] **Specific selectors**: All use \`[data-testid='x']\` or \`.class-name\`, NOT \`#root\`/\`div\`/\`h1\`
|
|
641
|
-
3. [ ] **Interactive flows**: At least 3 flows with \`click\` or \`type\` actions
|
|
642
|
-
4. [ ] **Fixtures configured**: Data-dependent pages have fixtures (staging URL preferred, or local JSON)
|
|
643
|
-
5. [ ] **Backend API flows**: API endpoints have \`http_request\` flows (if app has backend)
|
|
644
|
-
6. [ ] **Watch patterns**: Each \`watch_patterns\` matches component file paths
|
|
847
|
+
Follow .agents/skills/prepare-haystack.md to add accessibility attributes that make verification easier.
|
|
645
848
|
|
|
646
|
-
|
|
849
|
+
Run this BEFORE /setup-haystack to ensure your codebase has good selectors.
|
|
647
850
|
`;
|
|
648
851
|
export async function createSkillFile() {
|
|
649
852
|
const skillDir = path.join(process.cwd(), '.agents', 'skills');
|
|
650
|
-
const
|
|
853
|
+
const setupPath = path.join(skillDir, 'setup-haystack.md');
|
|
854
|
+
const refPath = path.join(skillDir, 'haystack-reference.md');
|
|
855
|
+
const prepPath = path.join(skillDir, 'prepare-haystack.md');
|
|
651
856
|
// Create directory if needed
|
|
652
857
|
await fs.mkdir(skillDir, { recursive: true });
|
|
653
|
-
// Write skill
|
|
654
|
-
await fs.writeFile(
|
|
655
|
-
|
|
858
|
+
// Write all skill files
|
|
859
|
+
await fs.writeFile(setupPath, SKILL_CONTENT, 'utf-8');
|
|
860
|
+
await fs.writeFile(refPath, REFERENCE_CONTENT, 'utf-8');
|
|
861
|
+
await fs.writeFile(prepPath, PREPARE_VERIFICATION_CONTENT, 'utf-8');
|
|
862
|
+
return setupPath;
|
|
656
863
|
}
|
|
657
864
|
/**
|
|
658
|
-
* Create the .claude/commands/
|
|
659
|
-
* Users can invoke with /haystack
|
|
865
|
+
* Create the .claude/commands/ files for Claude Code slash commands
|
|
866
|
+
* Users can invoke with /setup-haystack or /prepare-haystack
|
|
660
867
|
*/
|
|
661
868
|
export async function createClaudeCommand() {
|
|
662
869
|
const commandDir = path.join(process.cwd(), '.claude', 'commands');
|
|
663
|
-
const
|
|
870
|
+
const setupPath = path.join(commandDir, 'setup-haystack.md');
|
|
871
|
+
const prepPath = path.join(commandDir, 'prepare-haystack.md');
|
|
664
872
|
// Create directory if needed
|
|
665
873
|
await fs.mkdir(commandDir, { recursive: true });
|
|
666
|
-
// Write command
|
|
667
|
-
await fs.writeFile(
|
|
668
|
-
|
|
874
|
+
// Write command files
|
|
875
|
+
await fs.writeFile(setupPath, CLAUDE_COMMAND_CONTENT, 'utf-8');
|
|
876
|
+
await fs.writeFile(prepPath, PREPARE_HAYSTACK_COMMAND, 'utf-8');
|
|
877
|
+
return setupPath;
|
|
669
878
|
}
|