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.
Files changed (74) hide show
  1. package/README.md +375 -676
  2. package/agents/ctx-arch-mapper.md +5 -3
  3. package/agents/ctx-auditor.md +5 -3
  4. package/agents/ctx-codex-reviewer.md +214 -0
  5. package/agents/ctx-concerns-mapper.md +5 -3
  6. package/agents/ctx-criteria-suggester.md +6 -4
  7. package/agents/ctx-debugger.md +5 -3
  8. package/agents/ctx-designer.md +488 -114
  9. package/agents/ctx-discusser.md +5 -3
  10. package/agents/ctx-executor.md +5 -3
  11. package/agents/ctx-handoff.md +6 -4
  12. package/agents/ctx-learner.md +5 -3
  13. package/agents/ctx-mapper.md +4 -3
  14. package/agents/ctx-ml-analyst.md +600 -0
  15. package/agents/ctx-ml-engineer.md +933 -0
  16. package/agents/ctx-ml-reviewer.md +485 -0
  17. package/agents/ctx-ml-scientist.md +626 -0
  18. package/agents/ctx-parallelizer.md +4 -3
  19. package/agents/ctx-planner.md +5 -3
  20. package/agents/ctx-predictor.md +4 -3
  21. package/agents/ctx-qa.md +5 -3
  22. package/agents/ctx-quality-mapper.md +5 -3
  23. package/agents/ctx-researcher.md +5 -3
  24. package/agents/ctx-reviewer.md +6 -4
  25. package/agents/ctx-team-coordinator.md +5 -3
  26. package/agents/ctx-tech-mapper.md +5 -3
  27. package/agents/ctx-verifier.md +5 -3
  28. package/bin/ctx.js +199 -27
  29. package/commands/brand.md +309 -0
  30. package/commands/ctx.md +10 -10
  31. package/commands/design.md +304 -0
  32. package/commands/experiment.md +251 -0
  33. package/commands/help.md +57 -7
  34. package/commands/init.md +25 -0
  35. package/commands/metrics.md +1 -1
  36. package/commands/milestone.md +1 -1
  37. package/commands/ml-status.md +197 -0
  38. package/commands/monitor.md +1 -1
  39. package/commands/train.md +266 -0
  40. package/commands/visual-qa.md +559 -0
  41. package/commands/voice.md +1 -1
  42. package/hooks/post-tool-use.js +39 -0
  43. package/hooks/pre-tool-use.js +94 -0
  44. package/hooks/subagent-stop.js +32 -0
  45. package/package.json +9 -3
  46. package/plugin.json +46 -0
  47. package/skills/ctx-design-system/SKILL.md +572 -0
  48. package/skills/ctx-ml-experiment/SKILL.md +334 -0
  49. package/skills/ctx-ml-pipeline/SKILL.md +437 -0
  50. package/skills/ctx-orchestrator/SKILL.md +91 -0
  51. package/skills/ctx-review-gate/SKILL.md +147 -0
  52. package/skills/ctx-state/SKILL.md +100 -0
  53. package/skills/ctx-visual-qa/SKILL.md +587 -0
  54. package/src/agents.js +109 -0
  55. package/src/auto.js +287 -0
  56. package/src/capabilities.js +226 -0
  57. package/src/commits.js +94 -0
  58. package/src/config.js +112 -0
  59. package/src/context.js +241 -0
  60. package/src/handoff.js +156 -0
  61. package/src/hooks.js +218 -0
  62. package/src/install.js +125 -50
  63. package/src/lifecycle.js +194 -0
  64. package/src/metrics.js +198 -0
  65. package/src/pipeline.js +269 -0
  66. package/src/review-gate.js +338 -0
  67. package/src/runner.js +120 -0
  68. package/src/skills.js +143 -0
  69. package/src/state.js +267 -0
  70. package/src/worktree.js +244 -0
  71. package/templates/PRD.json +1 -1
  72. package/templates/config.json +4 -237
  73. package/workflows/ctx-router.md +0 -485
  74. 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 3.3 Voice Control - Speak your requirements instead of typing. Natural language processing converts speech to CTX commands and story descriptions.
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
+ }