ctx-cc 3.5.0 → 4.1.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 +375 -676
- package/agents/ctx-arch-mapper.md +5 -3
- package/agents/ctx-auditor.md +5 -3
- package/agents/ctx-codex-reviewer.md +214 -0
- package/agents/ctx-concerns-mapper.md +5 -3
- package/agents/ctx-criteria-suggester.md +6 -4
- package/agents/ctx-debugger.md +5 -3
- package/agents/ctx-designer.md +488 -114
- package/agents/ctx-discusser.md +5 -3
- package/agents/ctx-executor.md +5 -3
- package/agents/ctx-handoff.md +6 -4
- package/agents/ctx-learner.md +5 -3
- package/agents/ctx-mapper.md +4 -3
- package/agents/ctx-ml-analyst.md +600 -0
- package/agents/ctx-ml-engineer.md +933 -0
- package/agents/ctx-ml-reviewer.md +485 -0
- package/agents/ctx-ml-scientist.md +626 -0
- package/agents/ctx-parallelizer.md +4 -3
- package/agents/ctx-planner.md +5 -3
- package/agents/ctx-predictor.md +4 -3
- package/agents/ctx-qa.md +5 -3
- package/agents/ctx-quality-mapper.md +5 -3
- package/agents/ctx-researcher.md +5 -3
- package/agents/ctx-reviewer.md +6 -4
- package/agents/ctx-team-coordinator.md +5 -3
- package/agents/ctx-tech-mapper.md +5 -3
- package/agents/ctx-verifier.md +5 -3
- package/bin/ctx.js +199 -27
- package/commands/brand.md +309 -0
- package/commands/ctx.md +10 -10
- package/commands/design.md +304 -0
- package/commands/experiment.md +251 -0
- package/commands/help.md +57 -7
- package/commands/init.md +25 -0
- package/commands/metrics.md +1 -1
- package/commands/milestone.md +1 -1
- package/commands/ml-status.md +197 -0
- package/commands/monitor.md +1 -1
- package/commands/train.md +266 -0
- package/commands/visual-qa.md +559 -0
- package/commands/voice.md +1 -1
- package/hooks/post-tool-use.js +39 -0
- package/hooks/pre-tool-use.js +94 -0
- package/hooks/subagent-stop.js +32 -0
- package/package.json +9 -3
- package/plugin.json +46 -0
- package/skills/ctx-design-system/SKILL.md +572 -0
- package/skills/ctx-ml-experiment/SKILL.md +334 -0
- package/skills/ctx-ml-pipeline/SKILL.md +437 -0
- package/skills/ctx-orchestrator/SKILL.md +91 -0
- package/skills/ctx-review-gate/SKILL.md +147 -0
- package/skills/ctx-state/SKILL.md +100 -0
- package/skills/ctx-visual-qa/SKILL.md +587 -0
- package/src/agents.js +109 -0
- package/src/auto.js +287 -0
- package/src/capabilities.js +226 -0
- package/src/commits.js +94 -0
- package/src/config.js +112 -0
- package/src/context.js +241 -0
- package/src/handoff.js +156 -0
- package/src/hooks.js +218 -0
- package/src/install.js +125 -50
- package/src/lifecycle.js +194 -0
- package/src/metrics.js +198 -0
- package/src/pipeline.js +269 -0
- package/src/review-gate.js +338 -0
- package/src/runner.js +120 -0
- package/src/skills.js +143 -0
- package/src/state.js +267 -0
- package/src/worktree.js +244 -0
- package/templates/PRD.json +1 -1
- package/templates/config.json +4 -237
- package/workflows/ctx-router.md +0 -485
- package/workflows/map-codebase.md +0 -329
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ctx:visual-qa
|
|
3
|
+
description: Pixel-perfect visual QA — measurement-driven design parity, WCAG 2.2 AA accessibility audit, responsive matrix at 375/768/1440px. Uses Playwright + Figma MCP + Gemini.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<objective>
|
|
7
|
+
Run a complete visual QA pass: extract design specs from Figma, measure rendered output in the browser, compute precision diffs, audit WCAG 2.2 AA compliance, and produce a correction report with specific file/property/value changes.
|
|
8
|
+
</objective>
|
|
9
|
+
|
|
10
|
+
<usage>
|
|
11
|
+
```bash
|
|
12
|
+
/ctx:visual-qa # Full QA — parity + a11y + responsive
|
|
13
|
+
/ctx:visual-qa --component "Button" # Single component QA
|
|
14
|
+
/ctx:visual-qa --page "/login" # Full page QA
|
|
15
|
+
/ctx:visual-qa --a11y-only # Accessibility audit only
|
|
16
|
+
/ctx:visual-qa --parity-only # Design parity only (no a11y)
|
|
17
|
+
/ctx:visual-qa --baseline # Save new baseline screenshots (before change)
|
|
18
|
+
/ctx:visual-qa --regression # Compare current state against baselines
|
|
19
|
+
```
|
|
20
|
+
</usage>
|
|
21
|
+
|
|
22
|
+
<process>
|
|
23
|
+
|
|
24
|
+
## Step 1: Environment Check
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Load app URL
|
|
28
|
+
cat .ctx/.env 2>/dev/null | grep APP_URL || echo "APP_URL_MISSING"
|
|
29
|
+
|
|
30
|
+
# Check for Figma file key
|
|
31
|
+
cat .ctx/.env 2>/dev/null | grep FIGMA_FILE_KEY || echo "FIGMA_KEY_MISSING"
|
|
32
|
+
|
|
33
|
+
# Check for BRAND_KIT.md (needed for spec context)
|
|
34
|
+
ls BRAND_KIT.md 2>/dev/null && echo "BRAND_KIT_FOUND" || echo "BRAND_KIT_MISSING"
|
|
35
|
+
|
|
36
|
+
# Ensure output directories exist
|
|
37
|
+
mkdir -p .ctx/qa/visual .ctx/qa/baselines .ctx/qa/a11y
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
If APP_URL is missing:
|
|
41
|
+
```
|
|
42
|
+
APP_URL not found in .ctx/.env.
|
|
43
|
+
|
|
44
|
+
Add it:
|
|
45
|
+
echo "APP_URL=http://localhost:3000" >> .ctx/.env
|
|
46
|
+
|
|
47
|
+
Then re-run /ctx:visual-qa.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Step 2: Collect QA Target
|
|
51
|
+
|
|
52
|
+
If no flags provided, ask:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
What do you want to QA?
|
|
56
|
+
|
|
57
|
+
Component name or page route:
|
|
58
|
+
> [e.g., "Button", "LoginForm", "/dashboard"]
|
|
59
|
+
|
|
60
|
+
Figma node ID (optional — enables spec extraction):
|
|
61
|
+
> [e.g., "1234:5678" or leave blank]
|
|
62
|
+
|
|
63
|
+
Scope:
|
|
64
|
+
A Full QA (parity + a11y + responsive + Gemini)
|
|
65
|
+
B Accessibility only
|
|
66
|
+
C Design parity only
|
|
67
|
+
D Responsive layout only
|
|
68
|
+
E Regression check (compare against baselines)
|
|
69
|
+
|
|
70
|
+
> [A/B/C/D/E]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Step 3: Navigate to Target
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
// Load APP_URL from .ctx/.env
|
|
77
|
+
const appUrl = process.env.APP_URL || 'http://localhost:3000';
|
|
78
|
+
const targetPath = '<path from user input>';
|
|
79
|
+
|
|
80
|
+
await mcp__playwright__browser_navigate({ url: `${appUrl}${targetPath}` });
|
|
81
|
+
await mcp__playwright__browser_snapshot();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
If authentication is required:
|
|
85
|
+
```javascript
|
|
86
|
+
// Check if redirected to login
|
|
87
|
+
const snapshot = await mcp__playwright__browser_snapshot();
|
|
88
|
+
if (snapshot.includes('/login') || snapshot.includes('Sign in')) {
|
|
89
|
+
// Load credentials from .ctx/.env
|
|
90
|
+
await mcp__playwright__browser_navigate({ url: `${appUrl}/login` });
|
|
91
|
+
await mcp__playwright__browser_type({ ref: 'email-input', text: process.env.TEST_USER_EMAIL });
|
|
92
|
+
await mcp__playwright__browser_type({ ref: 'password-input', text: process.env.TEST_USER_PASSWORD });
|
|
93
|
+
await mcp__playwright__browser_click({ ref: 'submit-button' });
|
|
94
|
+
await mcp__playwright__browser_navigate({ url: `${appUrl}${targetPath}` });
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Step 4: Design Parity Check
|
|
99
|
+
|
|
100
|
+
### 4.1 Extract Figma Specs (if node ID provided)
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
const designContext = await mcp__figma__get_design_context({
|
|
104
|
+
fileKey: process.env.FIGMA_FILE_KEY,
|
|
105
|
+
nodeId: figmaNodeId
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const variables = await mcp__figma__get_variable_defs({
|
|
109
|
+
fileKey: process.env.FIGMA_FILE_KEY
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Save Figma reference screenshot
|
|
113
|
+
await mcp__figma__get_screenshot({
|
|
114
|
+
fileKey: process.env.FIGMA_FILE_KEY,
|
|
115
|
+
nodeId: figmaNodeId
|
|
116
|
+
});
|
|
117
|
+
// Copy output to .ctx/qa/visual/figma-spec-[component].png
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Build spec table from Figma context.
|
|
121
|
+
|
|
122
|
+
### 4.2 Measure Rendered Output (All Breakpoints)
|
|
123
|
+
|
|
124
|
+
Run at each breakpoint:
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
const breakpoints = [
|
|
128
|
+
{ name: 'mobile', width: 375, height: 812 },
|
|
129
|
+
{ name: 'tablet', width: 768, height: 1024 },
|
|
130
|
+
{ name: 'desktop', width: 1440, height: 900 }
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
for (const bp of breakpoints) {
|
|
134
|
+
await mcp__playwright__browser_resize({ width: bp.width, height: bp.height });
|
|
135
|
+
await mcp__playwright__browser_wait_for({ time: 0.3 });
|
|
136
|
+
|
|
137
|
+
const measurements = await mcp__playwright__browser_evaluate({
|
|
138
|
+
function: `
|
|
139
|
+
const el = document.querySelector('[data-testid="${selector}"]')
|
|
140
|
+
|| document.querySelector('${cssSelector}');
|
|
141
|
+
if (!el) return { error: 'Element not found' };
|
|
142
|
+
const s = getComputedStyle(el);
|
|
143
|
+
const r = el.getBoundingClientRect();
|
|
144
|
+
return {
|
|
145
|
+
fontSize: s.fontSize, fontWeight: s.fontWeight,
|
|
146
|
+
lineHeight: s.lineHeight, letterSpacing: s.letterSpacing,
|
|
147
|
+
fontFamily: s.fontFamily,
|
|
148
|
+
paddingTop: s.paddingTop, paddingRight: s.paddingRight,
|
|
149
|
+
paddingBottom: s.paddingBottom, paddingLeft: s.paddingLeft,
|
|
150
|
+
marginTop: s.marginTop, marginBottom: s.marginBottom,
|
|
151
|
+
width: r.width, height: r.height,
|
|
152
|
+
minHeight: s.minHeight, minWidth: s.minWidth,
|
|
153
|
+
backgroundColor: s.backgroundColor, color: s.color,
|
|
154
|
+
borderRadius: s.borderRadius, borderWidth: s.borderWidth,
|
|
155
|
+
borderColor: s.borderColor, boxShadow: s.boxShadow,
|
|
156
|
+
display: s.display, flexDirection: s.flexDirection,
|
|
157
|
+
gap: s.gap, alignItems: s.alignItems
|
|
158
|
+
};
|
|
159
|
+
`
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Take screenshot at this breakpoint
|
|
163
|
+
await mcp__playwright__browser_take_screenshot({
|
|
164
|
+
filename: `.ctx/qa/visual/${component}-${bp.name}.png`
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Store measurements keyed by breakpoint
|
|
168
|
+
results[bp.name] = measurements;
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 4.3 Generate Precision Diff Table
|
|
173
|
+
|
|
174
|
+
For each breakpoint, compare measured values against Figma spec:
|
|
175
|
+
|
|
176
|
+
```markdown
|
|
177
|
+
## Precision Diff — [Component] — [Breakpoint]
|
|
178
|
+
|
|
179
|
+
| Property | Spec | Rendered | Delta | Status |
|
|
180
|
+
|----------|------|----------|-------|--------|
|
|
181
|
+
| font-size | 16px | 16px | 0 | PASS |
|
|
182
|
+
| font-weight | 600 | 600 | 0 | PASS |
|
|
183
|
+
| min-height | 44px | 40px | -4px | FAIL |
|
|
184
|
+
| border-radius | 8px | 6px | -2px | FAIL |
|
|
185
|
+
| background-color | oklch(0.52 0.18 264) | rgb(46,120,227) | ~0 | PASS* |
|
|
186
|
+
| padding-right | 16px | 16px | 0 | PASS |
|
|
187
|
+
|
|
188
|
+
*Color values converted to same space for comparison.
|
|
189
|
+
Tolerance: ±1px for dimensions. Exact match for weights and radii.
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Step 5: Accessibility Audit
|
|
193
|
+
|
|
194
|
+
### WCAG 2.2 AA Automated Checks
|
|
195
|
+
|
|
196
|
+
Navigate to target at desktop breakpoint (1440px), then run:
|
|
197
|
+
|
|
198
|
+
**Color contrast:**
|
|
199
|
+
```javascript
|
|
200
|
+
const contrastViolations = await mcp__playwright__browser_evaluate({
|
|
201
|
+
function: `
|
|
202
|
+
function lum(str) {
|
|
203
|
+
const m = str.match(/rgb\\((\\d+),\\s*(\\d+),\\s*(\\d+)\\)/);
|
|
204
|
+
if (!m) return null;
|
|
205
|
+
return [m[1],m[2],m[3]].reduce((acc, v, i) => {
|
|
206
|
+
const s = parseInt(v)/255;
|
|
207
|
+
const lin = s <= 0.04045 ? s/12.92 : Math.pow((s+0.055)/1.055, 2.4);
|
|
208
|
+
return acc + lin * [0.2126,0.7152,0.0722][i];
|
|
209
|
+
}, 0);
|
|
210
|
+
}
|
|
211
|
+
function cr(a, b) {
|
|
212
|
+
const l = [lum(a), lum(b)];
|
|
213
|
+
if (l.includes(null)) return null;
|
|
214
|
+
return ((Math.max(...l)+0.05)/(Math.min(...l)+0.05)).toFixed(2);
|
|
215
|
+
}
|
|
216
|
+
const fails = [];
|
|
217
|
+
document.querySelectorAll('*').forEach(el => {
|
|
218
|
+
const s = getComputedStyle(el);
|
|
219
|
+
if (!el.textContent.trim() || el.children.length > 0) return;
|
|
220
|
+
const ratio = parseFloat(cr(s.color, s.backgroundColor));
|
|
221
|
+
if (!ratio) return;
|
|
222
|
+
const fs = parseFloat(s.fontSize);
|
|
223
|
+
const fw = parseInt(s.fontWeight);
|
|
224
|
+
const large = fs >= 18 || (fs >= 14 && fw >= 700);
|
|
225
|
+
const req = large ? 3.0 : 4.5;
|
|
226
|
+
if (ratio < req) fails.push({
|
|
227
|
+
tag: el.tagName, text: el.textContent.trim().substring(0,40),
|
|
228
|
+
fg: s.color, bg: s.backgroundColor, ratio, required: req
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
return fails.slice(0, 50);
|
|
232
|
+
`
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Touch targets (WCAG 2.2 SC 2.5.8):**
|
|
237
|
+
```javascript
|
|
238
|
+
const targetViolations = await mcp__playwright__browser_evaluate({
|
|
239
|
+
function: `
|
|
240
|
+
return [...document.querySelectorAll('a, button, input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="switch"]')]
|
|
241
|
+
.map(el => ({ el, r: el.getBoundingClientRect() }))
|
|
242
|
+
.filter(({ r }) => r.width < 24 || r.height < 24)
|
|
243
|
+
.map(({ el, r }) => ({
|
|
244
|
+
tag: el.tagName, role: el.getAttribute('role'),
|
|
245
|
+
text: (el.textContent || el.getAttribute('aria-label') || '').trim().substring(0,40),
|
|
246
|
+
w: Math.round(r.width), h: Math.round(r.height)
|
|
247
|
+
}));
|
|
248
|
+
`
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Focus visibility:**
|
|
253
|
+
```javascript
|
|
254
|
+
const focusViolations = await mcp__playwright__browser_evaluate({
|
|
255
|
+
function: `
|
|
256
|
+
const fails = [];
|
|
257
|
+
[...document.querySelectorAll('a, button, input, select, [tabindex]:not([tabindex="-1"])')].slice(0, 50).forEach(el => {
|
|
258
|
+
el.focus();
|
|
259
|
+
const s = getComputedStyle(el);
|
|
260
|
+
const ow = parseFloat(s.outlineWidth);
|
|
261
|
+
const ring = s.boxShadow !== 'none' && s.boxShadow.includes('0 0 0');
|
|
262
|
+
if (ow < 2 && !ring) {
|
|
263
|
+
fails.push({ tag: el.tagName, text: el.textContent.trim().substring(0,40), outlineWidth: ow, boxShadow: s.boxShadow });
|
|
264
|
+
}
|
|
265
|
+
el.blur();
|
|
266
|
+
});
|
|
267
|
+
return fails;
|
|
268
|
+
`
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Heading hierarchy:**
|
|
273
|
+
```javascript
|
|
274
|
+
const headingViolations = await mcp__playwright__browser_evaluate({
|
|
275
|
+
function: `
|
|
276
|
+
const hs = [...document.querySelectorAll('h1,h2,h3,h4,h5,h6')].map(h => ({
|
|
277
|
+
level: +h.tagName[1], text: h.textContent.trim().substring(0,50)
|
|
278
|
+
}));
|
|
279
|
+
const fails = [];
|
|
280
|
+
for (let i = 1; i < hs.length; i++) {
|
|
281
|
+
if (hs[i].level - hs[i-1].level > 1)
|
|
282
|
+
fails.push({ from: 'h'+hs[i-1].level+': '+hs[i-1].text, to: 'h'+hs[i].level+': '+hs[i].text });
|
|
283
|
+
}
|
|
284
|
+
const h1s = hs.filter(h => h.level === 1).length;
|
|
285
|
+
if (h1s !== 1) fails.push({ issue: 'h1 count is '+h1s+' (must be 1)' });
|
|
286
|
+
return { structure: hs, violations: fails };
|
|
287
|
+
`
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Image alt text:**
|
|
292
|
+
```javascript
|
|
293
|
+
const altViolations = await mcp__playwright__browser_evaluate({
|
|
294
|
+
function: `
|
|
295
|
+
return [...document.querySelectorAll('img')]
|
|
296
|
+
.filter(img => img.getAttribute('alt') === null)
|
|
297
|
+
.map(img => ({ src: img.src.substring(0,80), issue: 'Missing alt attribute' }));
|
|
298
|
+
`
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**ARIA names:**
|
|
303
|
+
```javascript
|
|
304
|
+
const ariaViolations = await mcp__playwright__browser_evaluate({
|
|
305
|
+
function: `
|
|
306
|
+
return [...document.querySelectorAll('button, [role="button"]')]
|
|
307
|
+
.filter(el => {
|
|
308
|
+
const text = el.textContent.trim();
|
|
309
|
+
const label = el.getAttribute('aria-label');
|
|
310
|
+
const lby = el.getAttribute('aria-labelledby');
|
|
311
|
+
return !text && !label && !lby;
|
|
312
|
+
})
|
|
313
|
+
.map(el => ({ tag: el.tagName, html: el.outerHTML.substring(0,100) }));
|
|
314
|
+
`
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Accessibility Summary Table
|
|
319
|
+
|
|
320
|
+
```markdown
|
|
321
|
+
## WCAG 2.2 AA — Automated Audit
|
|
322
|
+
|
|
323
|
+
| Criterion | Description | Violations | Status |
|
|
324
|
+
|-----------|-------------|-----------|--------|
|
|
325
|
+
| 1.1.1 Non-text Content | Alt text on images | {n} | PASS/FAIL |
|
|
326
|
+
| 1.4.3 Contrast (Minimum) | Text 4.5:1 / Large text 3:1 | {n} | PASS/FAIL |
|
|
327
|
+
| 2.4.7 Focus Visible | Focus indicator present | {n} | PASS/FAIL |
|
|
328
|
+
| 2.5.8 Target Size | Interactive targets 24x24px | {n} | PASS/FAIL |
|
|
329
|
+
| 3.1.2 Heading Hierarchy | No skipped heading levels | {n} | PASS/FAIL |
|
|
330
|
+
| 4.1.2 Name, Role, Value | ARIA labels on icon elements | {n} | PASS/FAIL |
|
|
331
|
+
|
|
332
|
+
WCAG 2.2 AA Score: {passing}/{total} criteria automated
|
|
333
|
+
Manual checks required: 2.4.11 Focus Not Obscured, 2.5.7 Dragging Alternatives, 2.1.1 Keyboard
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Step 6: Gemini Design Analysis
|
|
337
|
+
|
|
338
|
+
```javascript
|
|
339
|
+
// Analyze desktop screenshot with Gemini
|
|
340
|
+
const analysis = await mcp__gemini-design__gemini_analyze_design({
|
|
341
|
+
imagePath: `.ctx/qa/visual/${component}-desktop.png`,
|
|
342
|
+
prompt: `
|
|
343
|
+
You are a senior product designer reviewing a UI screenshot.
|
|
344
|
+
Evaluate each area precisely. No vague feedback.
|
|
345
|
+
|
|
346
|
+
1. VISUAL HIERARCHY (1-5)
|
|
347
|
+
Does the eye move naturally through the content?
|
|
348
|
+
What is the focal point? Is it correct?
|
|
349
|
+
|
|
350
|
+
2. SPACING CONSISTENCY (1-5)
|
|
351
|
+
Is spacing uniform and following a grid?
|
|
352
|
+
List any specific spacing inconsistencies with pixel estimates.
|
|
353
|
+
|
|
354
|
+
3. TYPOGRAPHY SCALE (1-5)
|
|
355
|
+
Do font sizes create clear hierarchy?
|
|
356
|
+
Are line heights appropriate for readability?
|
|
357
|
+
|
|
358
|
+
4. COLOR HARMONY (1-5)
|
|
359
|
+
Do colors complement each other?
|
|
360
|
+
Any jarring contrast issues beyond accessibility requirements?
|
|
361
|
+
|
|
362
|
+
5. ALIGNMENT (1-5)
|
|
363
|
+
Are elements aligned to a consistent grid?
|
|
364
|
+
Any orphaned or misaligned elements?
|
|
365
|
+
|
|
366
|
+
6. OVERALL POLISH (1-5)
|
|
367
|
+
Does this feel production-ready or prototype-level?
|
|
368
|
+
|
|
369
|
+
Format each as:
|
|
370
|
+
Rating: X/5
|
|
371
|
+
Observation: [specific observation]
|
|
372
|
+
Fix: [exact action to take, or "none needed"]
|
|
373
|
+
`
|
|
374
|
+
});
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Step 7: Regression Check (--regression flag)
|
|
378
|
+
|
|
379
|
+
```javascript
|
|
380
|
+
const breakpoints = ['mobile', 'tablet', 'desktop'];
|
|
381
|
+
|
|
382
|
+
for (const bp of breakpoints) {
|
|
383
|
+
// Take current screenshot
|
|
384
|
+
await mcp__playwright__browser_resize({
|
|
385
|
+
width: { mobile: 375, tablet: 768, desktop: 1440 }[bp],
|
|
386
|
+
height: { mobile: 812, tablet: 1024, desktop: 900 }[bp]
|
|
387
|
+
});
|
|
388
|
+
await mcp__playwright__browser_take_screenshot({
|
|
389
|
+
filename: `.ctx/qa/visual/${component}-${bp}-current.png`
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Compare against baseline with Gemini
|
|
393
|
+
const diff = await mcp__gemini-design__gemini_analyze_design({
|
|
394
|
+
images: [
|
|
395
|
+
`.ctx/qa/baselines/${component}-${bp}-baseline.png`,
|
|
396
|
+
`.ctx/qa/visual/${component}-${bp}-current.png`
|
|
397
|
+
],
|
|
398
|
+
prompt: `
|
|
399
|
+
Compare these two screenshots: [0] is the baseline, [1] is current.
|
|
400
|
+
List only UNINTENDED visual differences (regressions, not intentional changes).
|
|
401
|
+
For each difference:
|
|
402
|
+
- Element affected
|
|
403
|
+
- What changed (size, color, position, visibility)
|
|
404
|
+
- Severity: minor (cosmetic) or major (layout/functional)
|
|
405
|
+
If identical or only intentional changes, say "No regressions detected."
|
|
406
|
+
`
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
regressionResults[bp] = diff;
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## Step 8: Save Baseline (--baseline flag)
|
|
414
|
+
|
|
415
|
+
```javascript
|
|
416
|
+
for (const bp of breakpoints) {
|
|
417
|
+
await mcp__playwright__browser_resize({ width: bp.width, height: bp.height });
|
|
418
|
+
await mcp__playwright__browser_take_screenshot({
|
|
419
|
+
filename: `.ctx/qa/baselines/${component}-${bp.name}-baseline.png`
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
console.log('Baselines saved to .ctx/qa/baselines/');
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## Step 9: Write QA Report
|
|
426
|
+
|
|
427
|
+
Write to `.ctx/qa/VISUAL_QA_REPORT.md`:
|
|
428
|
+
|
|
429
|
+
```markdown
|
|
430
|
+
# Visual QA Report
|
|
431
|
+
|
|
432
|
+
**Target:** [component or page]
|
|
433
|
+
**Date:** [ISO-8601]
|
|
434
|
+
**App URL:** [URL]
|
|
435
|
+
**Figma Node:** [ID or "not provided"]
|
|
436
|
+
**Breakpoints:** 375px / 768px / 1440px
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## Executive Summary
|
|
441
|
+
|
|
442
|
+
| Check | Result | Issues |
|
|
443
|
+
|-------|--------|--------|
|
|
444
|
+
| Design Parity | PASS/FAIL | {n} deltas |
|
|
445
|
+
| Accessibility | PASS/FAIL | {n} violations |
|
|
446
|
+
| Responsive Layout | PASS/FAIL | {n} issues |
|
|
447
|
+
| Visual Regression | PASS/FAIL | {n} regressions |
|
|
448
|
+
| Gemini Design Score | {n}/5 | [summary] |
|
|
449
|
+
|
|
450
|
+
**Overall: PASS / FAIL**
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## Design Parity
|
|
455
|
+
|
|
456
|
+
### Mobile 375px
|
|
457
|
+
[precision diff table]
|
|
458
|
+
|
|
459
|
+
### Tablet 768px
|
|
460
|
+
[precision diff table]
|
|
461
|
+
|
|
462
|
+
### Desktop 1440px
|
|
463
|
+
[precision diff table]
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## Accessibility Audit
|
|
468
|
+
[full WCAG 2.2 AA table + violation details]
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## Gemini Design Analysis
|
|
473
|
+
[structured feedback per category]
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## Regression Analysis
|
|
478
|
+
[diff results per breakpoint]
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Corrections Required
|
|
483
|
+
|
|
484
|
+
Developer action items — apply in order:
|
|
485
|
+
|
|
486
|
+
| # | File | Property | Current | Change To |
|
|
487
|
+
|---|------|----------|---------|-----------|
|
|
488
|
+
| 1 | src/components/Button.tsx:24 | fontSize | 14px | 16px |
|
|
489
|
+
| 2 | src/components/Button.tsx:30 | minHeight | 40px | 44px |
|
|
490
|
+
| 3 | src/components/Button.css:12 | border-radius | 6px | 8px |
|
|
491
|
+
| 4 | src/components/Button.tsx:18 | focus outline | missing | add outline 2px solid var(--color-interactive-focus) |
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## Screenshots
|
|
496
|
+
|
|
497
|
+
.ctx/qa/visual/
|
|
498
|
+
├── [component]-mobile.png
|
|
499
|
+
├── [component]-tablet.png
|
|
500
|
+
├── [component]-desktop.png
|
|
501
|
+
└── figma-spec-[component].png (if Figma node provided)
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
## Step 10: Update STATE.json
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
python3 -c "
|
|
508
|
+
import json
|
|
509
|
+
from datetime import datetime
|
|
510
|
+
|
|
511
|
+
with open('.ctx/STATE.json', 'r') as f:
|
|
512
|
+
state = json.load(f)
|
|
513
|
+
|
|
514
|
+
state.setdefault('visualQA', {})
|
|
515
|
+
state['visualQA']['lastRun'] = datetime.utcnow().isoformat() + 'Z'
|
|
516
|
+
state['visualQA']['target'] = '$TARGET'
|
|
517
|
+
state['visualQA']['reportPath'] = '.ctx/qa/VISUAL_QA_REPORT.md'
|
|
518
|
+
state['visualQA']['pass'] = $OVERALL_PASS
|
|
519
|
+
|
|
520
|
+
with open('.ctx/STATE.json', 'w') as f:
|
|
521
|
+
json.dump(state, f, indent=2)
|
|
522
|
+
"
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
</process>
|
|
526
|
+
|
|
527
|
+
<output>
|
|
528
|
+
```
|
|
529
|
+
[CTX VISUAL QA]
|
|
530
|
+
|
|
531
|
+
Target: [component or page]
|
|
532
|
+
App URL: [URL]
|
|
533
|
+
Figma Node: [ID or not provided]
|
|
534
|
+
|
|
535
|
+
Running: parity check + accessibility + responsive + Gemini
|
|
536
|
+
|
|
537
|
+
Mobile 375px → [PASS/FAIL] {n} deltas
|
|
538
|
+
Tablet 768px → [PASS/FAIL] {n} deltas
|
|
539
|
+
Desktop 1440px → [PASS/FAIL] {n} deltas
|
|
540
|
+
|
|
541
|
+
Accessibility (WCAG 2.2 AA):
|
|
542
|
+
Contrast violations: {n}
|
|
543
|
+
Touch target violations: {n}
|
|
544
|
+
Focus indicator gaps: {n}
|
|
545
|
+
Heading hierarchy: {n}
|
|
546
|
+
Alt text missing: {n}
|
|
547
|
+
ARIA name missing: {n}
|
|
548
|
+
|
|
549
|
+
Gemini design score: {n}/5
|
|
550
|
+
|
|
551
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
552
|
+
VISUAL QA: [PASS / FAIL]
|
|
553
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
554
|
+
|
|
555
|
+
Corrections required: {n}
|
|
556
|
+
Report: .ctx/qa/VISUAL_QA_REPORT.md
|
|
557
|
+
Screenshots: .ctx/qa/visual/
|
|
558
|
+
```
|
|
559
|
+
</output>
|
package/commands/voice.md
CHANGED
|
@@ -4,7 +4,7 @@ description: Voice control for CTX - speak your requirements and commands using
|
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
<objective>
|
|
7
|
-
CTX
|
|
7
|
+
CTX 4.0 Voice Control - Speak your requirements instead of typing. Natural language processing converts speech to CTX commands and story descriptions.
|
|
8
8
|
</objective>
|
|
9
9
|
|
|
10
10
|
<usage>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CTX PostToolUse Hook
|
|
5
|
+
* Runs after tool execution. Logs file modifications for audit trail.
|
|
6
|
+
* Installed to .claude/hooks/ctx-post-tool-use.js
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
|
|
12
|
+
const ctxDir = path.join(process.cwd(), '.ctx');
|
|
13
|
+
const auditLog = path.join(ctxDir, 'audit.log');
|
|
14
|
+
|
|
15
|
+
// Read hook input from stdin
|
|
16
|
+
let input = {};
|
|
17
|
+
try {
|
|
18
|
+
const stdin = fs.readFileSync('/dev/stdin', 'utf-8');
|
|
19
|
+
if (stdin) input = JSON.parse(stdin);
|
|
20
|
+
} catch {
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const toolName = input.tool_name || '';
|
|
25
|
+
const toolInput = input.tool_input || '';
|
|
26
|
+
|
|
27
|
+
// Log file modifications for audit trail
|
|
28
|
+
if (['Write', 'Edit'].includes(toolName) && fs.existsSync(ctxDir)) {
|
|
29
|
+
const filePath = typeof toolInput === 'object' ? toolInput.file_path : '';
|
|
30
|
+
if (filePath) {
|
|
31
|
+
try {
|
|
32
|
+
fs.appendFileSync(auditLog,
|
|
33
|
+
`${new Date().toISOString()} | ${toolName} | ${filePath}\n`
|
|
34
|
+
);
|
|
35
|
+
} catch {
|
|
36
|
+
// Non-fatal
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CTX PreToolUse Hook
|
|
5
|
+
* Enforces TDD mode and capability restrictions.
|
|
6
|
+
* Installed to .claude/hooks/ctx-pre-tool-use.js
|
|
7
|
+
*
|
|
8
|
+
* Reads hook input from stdin (JSON with tool_name, tool_input, agent_name).
|
|
9
|
+
* Exit 0 = allow, Exit 2 = block.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { execSync } from 'child_process';
|
|
15
|
+
|
|
16
|
+
const ctxDir = path.join(process.cwd(), '.ctx');
|
|
17
|
+
|
|
18
|
+
// Read hook input from stdin
|
|
19
|
+
let input = {};
|
|
20
|
+
try {
|
|
21
|
+
const chunks = [];
|
|
22
|
+
const stdin = fs.readFileSync('/dev/stdin', 'utf-8');
|
|
23
|
+
if (stdin) input = JSON.parse(stdin);
|
|
24
|
+
} catch {
|
|
25
|
+
// No stdin or invalid JSON — allow
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const toolName = input.tool_name || '';
|
|
30
|
+
const toolInput = input.tool_input || '';
|
|
31
|
+
const agentName = input.agent_name || '';
|
|
32
|
+
|
|
33
|
+
// --- TDD Enforcement ---
|
|
34
|
+
if (toolName === 'Bash' && /git commit/.test(toolInput)) {
|
|
35
|
+
const tddMode = loadConfig('hooks.tddMode', 'off');
|
|
36
|
+
|
|
37
|
+
if (tddMode !== 'off') {
|
|
38
|
+
try {
|
|
39
|
+
const diff = execSync('git diff --cached --name-only', { encoding: 'utf-8', timeout: 5000 });
|
|
40
|
+
const files = diff.trim().split('\n').filter(Boolean);
|
|
41
|
+
const hasCode = files.some(f => /\.(js|ts|jsx|tsx|py|go|rs|rb|java|cs)$/.test(f) && !/\.(test|spec)\.|__tests__/.test(f));
|
|
42
|
+
const hasTest = files.some(f => /\.(test|spec)\.|__tests__/.test(f));
|
|
43
|
+
|
|
44
|
+
if (hasCode && !hasTest) {
|
|
45
|
+
if (tddMode === 'strict') {
|
|
46
|
+
process.stderr.write('CTX TDD: Commit blocked — code changes without tests.\n');
|
|
47
|
+
process.stderr.write('Files: ' + files.filter(f => /\.(js|ts|jsx|tsx|py|go|rs)$/.test(f)).join(', ') + '\n');
|
|
48
|
+
process.exit(2);
|
|
49
|
+
} else if (tddMode === 'warn') {
|
|
50
|
+
process.stderr.write('CTX TDD Warning: Code changes without corresponding tests.\n');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Git not available — skip check
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- Capability Enforcement ---
|
|
60
|
+
if (agentName && agentName.startsWith('ctx-')) {
|
|
61
|
+
const manifestPath = path.join(ctxDir, 'capability-manifest.json');
|
|
62
|
+
try {
|
|
63
|
+
if (fs.existsSync(manifestPath)) {
|
|
64
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
65
|
+
for (const [category, cfg] of Object.entries(manifest)) {
|
|
66
|
+
if (category.startsWith('_')) continue; // skip metadata keys like _version
|
|
67
|
+
if (cfg?.agents?.includes(agentName + '.md') && cfg.denied.includes(toolName)) {
|
|
68
|
+
process.stderr.write(`CTX: Tool "${toolName}" blocked for ${category} agent "${agentName}".\n`);
|
|
69
|
+
fs.appendFileSync(
|
|
70
|
+
path.join(ctxDir, 'violations.log'),
|
|
71
|
+
`${new Date().toISOString()} | ${agentName} | ${toolName} | BLOCKED\n`
|
|
72
|
+
);
|
|
73
|
+
process.exit(2);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// Manifest not found or invalid — allow
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
process.exit(0);
|
|
83
|
+
|
|
84
|
+
// --- Helpers ---
|
|
85
|
+
|
|
86
|
+
function loadConfig(key, defaultValue) {
|
|
87
|
+
try {
|
|
88
|
+
const configPath = path.join(ctxDir, 'config.json');
|
|
89
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
90
|
+
return key.split('.').reduce((o, k) => o?.[k], config) ?? defaultValue;
|
|
91
|
+
} catch {
|
|
92
|
+
return defaultValue;
|
|
93
|
+
}
|
|
94
|
+
}
|