arkaos 2.25.0 → 2.28.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 (27) hide show
  1. package/VERSION +1 -1
  2. package/arka/skills/bootstrap-agent/SKILL.md +76 -0
  3. package/arka/skills/design-ops/SKILL.md +68 -0
  4. package/arka/skills/design-ops/scripts/extract-colors.py +86 -0
  5. package/arka/skills/design-ops/scripts/shadcn-tokens.py +59 -0
  6. package/arka/skills/design-ops/scripts/wcag-contrast.py +92 -0
  7. package/arka/skills/research/SKILL.md +117 -0
  8. package/config/constitution.yaml +4 -0
  9. package/config/hooks/post-tool-use.sh +17 -0
  10. package/config/hooks/user-prompt-submit.sh +16 -0
  11. package/core/agents/__pycache__/loader.cpython-313.pyc +0 -0
  12. package/core/agents/__pycache__/schema.cpython-313.pyc +0 -0
  13. package/core/agents/loader.py +4 -1
  14. package/core/agents/schema.py +29 -0
  15. package/core/cognition/__pycache__/retrieval.cpython-313.pyc +0 -0
  16. package/core/cognition/capture/__pycache__/collector.cpython-313.pyc +0 -0
  17. package/core/cognition/retrieval.py +383 -0
  18. package/core/cognition/scheduler/__pycache__/daemon.cpython-313.pyc +0 -0
  19. package/core/obsidian/__pycache__/writer.cpython-313.pyc +0 -0
  20. package/core/runtime/__pycache__/path_resolver.cpython-313.pyc +0 -0
  21. package/departments/brand/agents/design-ops/design-ops-lead.yaml +78 -0
  22. package/departments/brand/agents/design-ops/extraction-script-writer.yaml +74 -0
  23. package/departments/brand/agents/design-ops/shadcn-padronizer.yaml +76 -0
  24. package/departments/brand/agents/design-ops/wcag-auditor.yaml +76 -0
  25. package/departments/dev/skills/mcp/SKILL.md +16 -0
  26. package/package.json +1 -1
  27. package/pyproject.toml +1 -1
@@ -0,0 +1,383 @@
1
+ """Hooks-as-retrieval prototype for the ArkaOS Cognitive Layer.
2
+
3
+ After each tool call, the PostToolUse hook calls into this module to
4
+ extract likely entities from the tool output, find relevant notes in the
5
+ Obsidian vault, and write a small JSON cache that the UserPromptSubmit
6
+ hook injects into the next turn as an ``[arka:context]`` advisory.
7
+
8
+ This is the prototype unblocked by Claude Code 2.1.122 (late-binding
9
+ ``ToolSearch`` + hook-block isolation, see KB note
10
+ 2026-04-29-claude-code-2-1-122-and-2-1-123). It uses filesystem grep
11
+ (ripgrep when available, Python fallback otherwise) instead of running
12
+ a separate MCP server with embeddings — a deliberate "smallest thing
13
+ that could possibly work" choice per the 2026-05-13 Conclave ADR.
14
+
15
+ Performance budget: ≤ 800 ms p95 per ``capture_context`` call. The
16
+ PostToolUse hook enforces this with a hard timeout; the function itself
17
+ caps entities (20), vault hits (5), and ripgrep wall time (1 s).
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import os
24
+ import re
25
+ import subprocess
26
+ from dataclasses import asdict, dataclass, field
27
+ from datetime import datetime, timezone
28
+ from pathlib import Path
29
+
30
+ from core.runtime.user_paths import user_data_root
31
+
32
+ _CACHE_DIRNAME = "context-cache"
33
+ _DEFAULT_TTL_SECONDS = 600
34
+ _REAPER_MAX_AGE_SECONDS = 24 * 60 * 60 # cache files older than 24h get pruned
35
+ _MAX_ENTITIES = 20
36
+ _MAX_VAULT_HITS = 5
37
+ _RIPGREP_TIMEOUT_S = 1.0
38
+ _PY_FALLBACK_MAX_FILES = 500
39
+
40
+ # Common-word stoplist — kept tiny on purpose; we want to capture short
41
+ # domain terms like "auth" or "DSL" but cut out the highest-frequency
42
+ # English filler tokens that dominate any tool output.
43
+ _STOPLIST = frozenset(
44
+ {
45
+ "The", "And", "But", "For", "Not", "With", "From", "This", "That",
46
+ "When", "Where", "Then", "Else", "True", "False", "None", "Null",
47
+ "Error", "Warning", "Info", "Debug", "Trace", "Yes", "No", "Run",
48
+ "Read", "Write", "Edit", "Delete", "Show", "Find", "List",
49
+ }
50
+ )
51
+
52
+ _FILE_PATH = re.compile(r"(?:^|\s)((?:[\w.-]+/)+[\w.-]+\.[a-zA-Z]{1,6})")
53
+ _CAMEL_OR_PASCAL = re.compile(r"\b([A-Z][a-z]+(?:[A-Z][a-z]+)+)\b")
54
+ _AT_MENTION = re.compile(r"@([\w./_-]+)")
55
+ _PROPER_NOUN = re.compile(r"\b([A-Z][a-zA-Z]{2,})\b")
56
+
57
+
58
+ @dataclass
59
+ class ContextHit:
60
+ """One vault note that matches one or more extracted entities."""
61
+
62
+ entity: str
63
+ vault_path: str
64
+ snippet: str
65
+ score: float = 1.0
66
+
67
+ def to_dict(self) -> dict:
68
+ return asdict(self)
69
+
70
+
71
+ @dataclass
72
+ class ContextCache:
73
+ """The JSON payload persisted per session."""
74
+
75
+ session_id: str
76
+ captured_at: str
77
+ ttl_seconds: int
78
+ hits: list[ContextHit] = field(default_factory=list)
79
+
80
+ def to_dict(self) -> dict:
81
+ return {
82
+ "session_id": self.session_id,
83
+ "captured_at": self.captured_at,
84
+ "ttl_seconds": self.ttl_seconds,
85
+ "hits": [h.to_dict() for h in self.hits],
86
+ }
87
+
88
+
89
+ def extract_entities(text: str) -> list[str]:
90
+ """Pull file paths, identifiers, and proper nouns out of *text*.
91
+
92
+ Returns a deduplicated, capped list ready for vault search. Cheap
93
+ enough to call on every PostToolUse invocation.
94
+ """
95
+ if not text:
96
+ return []
97
+ seen: set[str] = set()
98
+ out: list[str] = []
99
+
100
+ def push(tok: str) -> None:
101
+ tok = tok.strip(" .,;:!?\"'()[]{}")
102
+ if not tok or tok in _STOPLIST or tok in seen:
103
+ return
104
+ if tok.isdigit():
105
+ return
106
+ seen.add(tok)
107
+ out.append(tok)
108
+ return
109
+
110
+ for m in _FILE_PATH.finditer(text):
111
+ push(m.group(1))
112
+ for m in _AT_MENTION.finditer(text):
113
+ push(m.group(1))
114
+ for m in _CAMEL_OR_PASCAL.finditer(text):
115
+ push(m.group(1))
116
+ for m in _PROPER_NOUN.finditer(text):
117
+ if len(out) >= _MAX_ENTITIES:
118
+ break
119
+ push(m.group(1))
120
+ return out[:_MAX_ENTITIES]
121
+
122
+
123
+ def search_vault(entities: list[str], vault_path: str) -> list[ContextHit]:
124
+ """Find Obsidian notes that mention any of the extracted entities.
125
+
126
+ Uses ripgrep when available (sub-second across thousands of notes),
127
+ falls back to a pure-Python scan capped at ``_PY_FALLBACK_MAX_FILES``
128
+ files so the hook timeout stays safe on Windows or minimal Linux
129
+ installs without ripgrep.
130
+ """
131
+ vault = Path(vault_path).expanduser()
132
+ if not entities or not vault.is_dir():
133
+ return []
134
+ if _ripgrep_available():
135
+ hits = _search_ripgrep(entities, vault)
136
+ else:
137
+ hits = _search_python(entities, vault)
138
+ return hits[:_MAX_VAULT_HITS]
139
+
140
+
141
+ def capture_context(
142
+ session_id: str,
143
+ tool_output: str,
144
+ vault_path: str,
145
+ cache_dir: Path | None = None,
146
+ ) -> ContextCache:
147
+ """Run extract + search and persist the cache. Returns the cache.
148
+
149
+ Idempotent across overlapping calls in the same second: a second
150
+ invocation overwrites the first. Empty hits still produce a file
151
+ so the UserPromptSubmit reader sees a fresh ``captured_at`` and
152
+ doesn't mistake a quiet turn for stale data.
153
+ """
154
+ entities = extract_entities(tool_output)
155
+ hits = search_vault(entities, vault_path) if entities else []
156
+ cache = ContextCache(
157
+ session_id=session_id,
158
+ captured_at=datetime.now(timezone.utc).isoformat(),
159
+ ttl_seconds=_DEFAULT_TTL_SECONDS,
160
+ hits=hits,
161
+ )
162
+ _write_cache(cache, cache_dir or _default_cache_dir())
163
+ return cache
164
+
165
+
166
+ def read_context(
167
+ session_id: str,
168
+ ttl_seconds: int = _DEFAULT_TTL_SECONDS,
169
+ cache_dir: Path | None = None,
170
+ ) -> list[ContextHit]:
171
+ """Read cached hits for *session_id* if still within TTL."""
172
+ path = (cache_dir or _default_cache_dir()) / f"{session_id}.json"
173
+ if not path.exists():
174
+ return []
175
+ try:
176
+ data = json.loads(path.read_text(encoding="utf-8"))
177
+ except (json.JSONDecodeError, OSError):
178
+ return []
179
+ captured = data.get("captured_at", "")
180
+ if not _within_ttl(captured, ttl_seconds):
181
+ return []
182
+ return [ContextHit(**h) for h in data.get("hits", [])]
183
+
184
+
185
+ def format_advisory(hits: list[ContextHit], max_chars: int = 1200) -> str:
186
+ """Render hits into a compact `[arka:context]` advisory string."""
187
+ if not hits:
188
+ return ""
189
+ lines = ["[arka:context] KB matches from last turn:"]
190
+ for h in hits:
191
+ line = f"- {h.entity} → {h.vault_path}: {h.snippet[:160]}"
192
+ if sum(len(l) for l in lines) + len(line) > max_chars:
193
+ break
194
+ lines.append(line)
195
+ return "\n".join(lines)
196
+
197
+
198
+ def _default_cache_dir() -> Path:
199
+ return user_data_root() / _CACHE_DIRNAME
200
+
201
+
202
+ def _write_cache(cache: ContextCache, cache_dir: Path) -> None:
203
+ cache_dir.mkdir(parents=True, exist_ok=True)
204
+ path = cache_dir / f"{cache.session_id}.json"
205
+ tmp = cache_dir / f"{cache.session_id}.json.tmp"
206
+ tmp.write_text(json.dumps(cache.to_dict(), indent=2), encoding="utf-8")
207
+ os.replace(tmp, path)
208
+ _prune_stale_cache(cache_dir)
209
+
210
+
211
+ def _prune_stale_cache(cache_dir: Path) -> int:
212
+ """Delete cache files older than 24h to bound disk growth.
213
+
214
+ Called from ``_write_cache`` so cleanup amortises across normal use
215
+ without requiring a separate cron job. Per Marta's PR4 follow-up
216
+ observation: the read-side TTL hides stale data but on-disk files
217
+ accumulate one-per-session forever. Returns the count of pruned files.
218
+ """
219
+ if not cache_dir.is_dir():
220
+ return 0
221
+ cutoff = datetime.now(timezone.utc).timestamp() - _REAPER_MAX_AGE_SECONDS
222
+ pruned = 0
223
+ for child in cache_dir.glob("*.json"):
224
+ try:
225
+ if child.stat().st_mtime < cutoff:
226
+ child.unlink()
227
+ pruned += 1
228
+ except OSError:
229
+ continue
230
+ return pruned
231
+
232
+
233
+ def _within_ttl(captured_iso: str, ttl_seconds: int) -> bool:
234
+ try:
235
+ captured = datetime.fromisoformat(captured_iso)
236
+ except ValueError:
237
+ return False
238
+ now = datetime.now(timezone.utc)
239
+ if captured.tzinfo is None:
240
+ captured = captured.replace(tzinfo=timezone.utc)
241
+ return (now - captured).total_seconds() <= ttl_seconds
242
+
243
+
244
+ def _ripgrep_available() -> bool:
245
+ try:
246
+ subprocess.run(
247
+ ["rg", "--version"], check=True, capture_output=True, timeout=1.0
248
+ )
249
+ return True
250
+ except (FileNotFoundError, subprocess.SubprocessError):
251
+ return False
252
+
253
+
254
+ def _search_ripgrep(entities: list[str], vault: Path) -> list[ContextHit]:
255
+ pattern = "|".join(re.escape(e) for e in entities)
256
+ cmd = [
257
+ "rg", "--json", "--max-count", "1", "--smart-case",
258
+ "-tmd", "-e", pattern, str(vault),
259
+ ]
260
+ try:
261
+ result = subprocess.run(
262
+ cmd, capture_output=True, text=True, timeout=_RIPGREP_TIMEOUT_S
263
+ )
264
+ except subprocess.SubprocessError:
265
+ return []
266
+ return _parse_ripgrep_json(result.stdout, vault, entities)
267
+
268
+
269
+ def _parse_ripgrep_json(stdout: str, vault: Path, entities: list[str]) -> list[ContextHit]:
270
+ hits: list[ContextHit] = []
271
+ seen: set[str] = set()
272
+ for line in stdout.splitlines():
273
+ try:
274
+ event = json.loads(line)
275
+ except json.JSONDecodeError:
276
+ continue
277
+ if event.get("type") != "match":
278
+ continue
279
+ data = event.get("data", {})
280
+ path = data.get("path", {}).get("text", "")
281
+ if not path or path in seen:
282
+ continue
283
+ seen.add(path)
284
+ text = data.get("lines", {}).get("text", "").strip()
285
+ matched_entity = _pick_matched_entity(text, entities)
286
+ rel = _relative_to_vault(path, vault)
287
+ hits.append(ContextHit(entity=matched_entity, vault_path=rel, snippet=text))
288
+ return hits
289
+
290
+
291
+ def _search_python(entities: list[str], vault: Path) -> list[ContextHit]:
292
+ pattern = re.compile("|".join(re.escape(e) for e in entities), re.IGNORECASE)
293
+ files = list(vault.rglob("*.md"))[:_PY_FALLBACK_MAX_FILES]
294
+ hits: list[ContextHit] = []
295
+ for f in files:
296
+ try:
297
+ text = f.read_text(encoding="utf-8", errors="ignore")
298
+ except OSError:
299
+ continue
300
+ match = pattern.search(text)
301
+ if not match:
302
+ continue
303
+ snippet_start = max(0, match.start() - 40)
304
+ snippet_end = min(len(text), match.end() + 80)
305
+ snippet = text[snippet_start:snippet_end].replace("\n", " ").strip()
306
+ hits.append(
307
+ ContextHit(
308
+ entity=match.group(0),
309
+ vault_path=str(f.relative_to(vault)),
310
+ snippet=snippet,
311
+ )
312
+ )
313
+ if len(hits) >= _MAX_VAULT_HITS:
314
+ break
315
+ return hits
316
+
317
+
318
+ def _pick_matched_entity(text: str, entities: list[str]) -> str:
319
+ lower = text.lower()
320
+ for e in entities:
321
+ if e.lower() in lower:
322
+ return e
323
+ return entities[0] if entities else ""
324
+
325
+
326
+ def _relative_to_vault(absolute: str, vault: Path) -> str:
327
+ try:
328
+ return str(Path(absolute).relative_to(vault))
329
+ except ValueError:
330
+ return absolute
331
+
332
+
333
+ def _vault_path_or_none() -> str | None:
334
+ """Resolve the configured vault path or return None when missing.
335
+
336
+ Hooks call this; they must never crash because profile.json is absent.
337
+ """
338
+ try:
339
+ from core.runtime.path_resolver import load_profile
340
+ return load_profile().vault_path
341
+ except Exception:
342
+ return None
343
+
344
+
345
+ def _cli_capture(argv: list[str]) -> int:
346
+ """Stdin → capture_context. Used by the PostToolUse hook."""
347
+ import sys
348
+ if len(argv) < 2:
349
+ return 2
350
+ session_id = argv[1]
351
+ vault = _vault_path_or_none()
352
+ if not vault:
353
+ return 0 # silent skip when no profile
354
+ tool_output = sys.stdin.read()
355
+ capture_context(session_id, tool_output, vault)
356
+ return 0
357
+
358
+
359
+ def _cli_inject(argv: list[str]) -> int:
360
+ """Print advisory for *session_id*. Used by the UserPromptSubmit hook."""
361
+ if len(argv) < 2:
362
+ return 2
363
+ session_id = argv[1]
364
+ advisory = format_advisory(read_context(session_id))
365
+ if advisory:
366
+ print(advisory)
367
+ return 0
368
+
369
+
370
+ def main(argv: list[str]) -> int:
371
+ if len(argv) < 2:
372
+ return 2
373
+ cmd = argv[1]
374
+ if cmd == "capture":
375
+ return _cli_capture(argv[1:])
376
+ if cmd == "inject":
377
+ return _cli_inject(argv[1:])
378
+ return 2
379
+
380
+
381
+ if __name__ == "__main__":
382
+ import sys
383
+ sys.exit(main(sys.argv))
@@ -0,0 +1,78 @@
1
+ id: design-ops-lead-iris
2
+ name: Iris
3
+ role: Design Ops Lead
4
+ department: brand
5
+ tier: 1
6
+ model: sonnet
7
+
8
+ parent_squad: brand
9
+ sub_squad_role: lead
10
+
11
+ behavioral_dna:
12
+ disc:
13
+ primary: D
14
+ secondary: C
15
+ communication_style: "Direct, blueprint-driven, defends production quality without smothering creative intent"
16
+ under_pressure: "Pushes back on shortcuts; protects the design system from drift"
17
+ motivator: "A design system that ships consistently across every surface"
18
+ enneagram:
19
+ type: 8
20
+ wing: 9
21
+ core_motivation: "Make the right thing also be the easy thing for designers and engineers"
22
+ core_fear: "Inconsistent UI shipped because nobody owned the production rails"
23
+ subtype: self-preservation
24
+ big_five:
25
+ openness: 60
26
+ conscientiousness: 88
27
+ extraversion: 52
28
+ agreeableness: 55
29
+ neuroticism: 28
30
+ mbti:
31
+ type: ESTJ
32
+
33
+ mental_models:
34
+ primary:
35
+ - "Atomic Design (Frost)"
36
+ - "Design Tokens W3C Spec"
37
+ - "Single Source of Truth (Wheeler)"
38
+ secondary:
39
+ - "Theory of Constraints (Goldratt)"
40
+ - "Cynefin (Snowden)"
41
+ - "WCAG 2.2 AA"
42
+
43
+ authority:
44
+ orchestrate: true
45
+ approve_quality: true
46
+ delegates_to:
47
+ - extraction-script-writer
48
+ - wcag-auditor
49
+ - shadcn-padronizer
50
+ escalates_to: brand-director-valentina
51
+
52
+ expertise:
53
+ domains:
54
+ - design tokens (JSON + CSS variables)
55
+ - component libraries (shadcn/ui, Radix, Headless UI)
56
+ - design system governance
57
+ - figma → code pipelines
58
+ - accessibility compliance (WCAG 2.2 AA/AAA)
59
+ - cross-platform tokenisation (Style Dictionary, Tailwind)
60
+ frameworks:
61
+ - Atomic Design (Frost)
62
+ - Design Tokens Community Group
63
+ - WCAG 2.2
64
+ - Style Dictionary
65
+ - shadcn/ui conventions
66
+ - Tailwind CSS configuration
67
+ depth: master
68
+ years_equivalent: 12
69
+
70
+ communication:
71
+ language: en
72
+ tone: "precise, blueprint-driven, low-drama"
73
+ vocabulary_level: advanced
74
+ preferred_format: "tables, JSON specs, before/after diffs"
75
+ avoid:
76
+ - "design by feel without a token spec"
77
+ - "ad-hoc one-off components"
78
+ - "shipping without accessibility audit"
@@ -0,0 +1,74 @@
1
+ id: extraction-script-writer-nia
2
+ name: Nia
3
+ role: Design Extraction Engineer
4
+ department: brand
5
+ tier: 2
6
+ model: sonnet
7
+
8
+ parent_squad: brand
9
+ sub_squad_role: extraction-script-writer
10
+
11
+ behavioral_dna:
12
+ disc:
13
+ primary: C
14
+ secondary: D
15
+ communication_style: "Investigative, evidence-first, writes the script before opining"
16
+ under_pressure: "Goes deeper into the source; refuses to guess"
17
+ motivator: "Reproducible extraction pipelines that turn a live site into structured tokens"
18
+ enneagram:
19
+ type: 5
20
+ wing: 6
21
+ core_motivation: "Understand a design system well enough to rebuild it from primitives"
22
+ core_fear: "Shipping a tokenisation that looks right but is silently wrong"
23
+ subtype: self-preservation
24
+ big_five:
25
+ openness: 78
26
+ conscientiousness: 82
27
+ extraversion: 35
28
+ agreeableness: 50
29
+ neuroticism: 30
30
+ mbti:
31
+ type: INTP
32
+
33
+ mental_models:
34
+ primary:
35
+ - "Design Tokens W3C Spec"
36
+ - "Unix Philosophy (Pike & Kernighan)"
37
+ - "Reverse-Engineering Discipline"
38
+ secondary:
39
+ - "AST-based code analysis"
40
+ - "CSS Specificity Model"
41
+ - "Color Theory (Albers)"
42
+
43
+ authority:
44
+ orchestrate: false
45
+ approve_quality: false
46
+ delegates_to: []
47
+ escalates_to: design-ops-lead-iris
48
+
49
+ expertise:
50
+ domains:
51
+ - CSS extraction (computed styles, custom properties)
52
+ - color palette reverse-engineering from screenshots / live DOM
53
+ - typography token harvesting (font-family, size, weight, line-height scales)
54
+ - spacing and grid token inference
55
+ - figma file parsing
56
+ - chromium-headless capture pipelines
57
+ frameworks:
58
+ - W3C Design Tokens spec
59
+ - Style Dictionary
60
+ - Tailwind CSS token format
61
+ - shadcn/ui CSS variable convention
62
+ - DTCG JSON schema
63
+ depth: expert
64
+ years_equivalent: 9
65
+
66
+ communication:
67
+ language: en
68
+ tone: "concise, citation-heavy, almost terse"
69
+ vocabulary_level: advanced
70
+ preferred_format: "JSON specs, before/after color swatches, script snippets"
71
+ avoid:
72
+ - "guessing token values without inspecting the source"
73
+ - "exposing hardcoded RGB instead of named tokens"
74
+ - "premature opinions on visual quality (that's brand-director territory)"
@@ -0,0 +1,76 @@
1
+ id: shadcn-padronizer-leo
2
+ name: Leo
3
+ role: Component Library Padronizer
4
+ department: brand
5
+ tier: 2
6
+ model: sonnet
7
+
8
+ parent_squad: brand
9
+ sub_squad_role: shadcn-padronizer
10
+
11
+ behavioral_dna:
12
+ disc:
13
+ primary: C
14
+ secondary: I
15
+ communication_style: "Pattern-oriented, shows the canonical component before refactoring"
16
+ under_pressure: "Stays in the design system; refuses to fork components for one-offs"
17
+ motivator: "A shadcn/ui-shaped library that every project in the org can adopt verbatim"
18
+ enneagram:
19
+ type: 1
20
+ wing: 9
21
+ core_motivation: "One canonical component, used everywhere, evolving cleanly"
22
+ core_fear: "Five teams shipping five Button components that diverge over a quarter"
23
+ subtype: social
24
+ big_five:
25
+ openness: 65
26
+ conscientiousness: 84
27
+ extraversion: 50
28
+ agreeableness: 58
29
+ neuroticism: 30
30
+ mbti:
31
+ type: ISTP
32
+
33
+ mental_models:
34
+ primary:
35
+ - "shadcn/ui composition philosophy"
36
+ - "Headless UI patterns (Radix, React Aria)"
37
+ - "Atomic Design (Frost)"
38
+ secondary:
39
+ - "Tailwind CSS configuration"
40
+ - "CVA (class-variance-authority) variants"
41
+ - "Theming via CSS variables"
42
+
43
+ authority:
44
+ orchestrate: false
45
+ approve_quality: false
46
+ delegates_to: []
47
+ escalates_to: design-ops-lead-iris
48
+
49
+ expertise:
50
+ domains:
51
+ - shadcn/ui component generation and customisation
52
+ - Radix UI primitives integration
53
+ - Tailwind configuration and theme tokens
54
+ - CVA variants and slot patterns
55
+ - dark mode and theme switching
56
+ - component API surface design (props, slots, polymorphic components)
57
+ - migration from MUI / Chakra / Ant Design to shadcn
58
+ frameworks:
59
+ - shadcn/ui
60
+ - Radix UI
61
+ - React Aria (Adobe)
62
+ - Tailwind CSS
63
+ - class-variance-authority
64
+ - tailwind-merge
65
+ depth: expert
66
+ years_equivalent: 8
67
+
68
+ communication:
69
+ language: en
70
+ tone: "code-first, pragmatic, mild humour"
71
+ vocabulary_level: advanced
72
+ preferred_format: "tsx snippets, before/after component diffs, migration guides"
73
+ avoid:
74
+ - "rebuilding a primitive that Radix already ships"
75
+ - "inline styles that bypass the theme tokens"
76
+ - "magic numbers in className strings"
@@ -0,0 +1,76 @@
1
+ id: wcag-auditor-oren
2
+ name: Oren
3
+ role: Accessibility Auditor
4
+ department: brand
5
+ tier: 2
6
+ model: sonnet
7
+
8
+ parent_squad: brand
9
+ sub_squad_role: wcag-auditor
10
+
11
+ behavioral_dna:
12
+ disc:
13
+ primary: C
14
+ secondary: S
15
+ communication_style: "Methodical, cites the exact WCAG criterion, never bluffs"
16
+ under_pressure: "Reports findings in priority order with severity; refuses to skip levels"
17
+ motivator: "Interfaces every human can actually use, including with assistive tech"
18
+ enneagram:
19
+ type: 1
20
+ wing: 2
21
+ core_motivation: "Make accessibility the default rather than a retrofit"
22
+ core_fear: "A real user blocked by a contrast ratio or focus-trap nobody caught"
23
+ subtype: social
24
+ big_five:
25
+ openness: 55
26
+ conscientiousness: 92
27
+ extraversion: 38
28
+ agreeableness: 68
29
+ neuroticism: 32
30
+ mbti:
31
+ type: ISTJ
32
+
33
+ mental_models:
34
+ primary:
35
+ - "WCAG 2.2 (AA + AAA)"
36
+ - "POUR Principles (Perceivable, Operable, Understandable, Robust)"
37
+ - "ARIA Authoring Practices"
38
+ secondary:
39
+ - "Inclusive Design Microsoft toolkit"
40
+ - "Cognitive Load Theory (Sweller)"
41
+ - "Universal Design (Mace)"
42
+
43
+ authority:
44
+ orchestrate: false
45
+ approve_quality: false
46
+ delegates_to: []
47
+ escalates_to: design-ops-lead-iris
48
+
49
+ expertise:
50
+ domains:
51
+ - WCAG 2.2 AA / AAA conformance auditing
52
+ - color contrast ratio analysis (4.5:1 text, 3:1 large text and UI)
53
+ - keyboard navigation and focus management
54
+ - screen reader compatibility (NVDA, JAWS, VoiceOver)
55
+ - ARIA pattern review (no-redundant-roles, correct landmarks)
56
+ - cognitive accessibility (reading level, error recovery)
57
+ - accessibility statement drafting
58
+ frameworks:
59
+ - WCAG 2.2
60
+ - WAI-ARIA 1.2
61
+ - ATAG 2.0
62
+ - EAA / EN 301 549
63
+ - axe-core ruleset
64
+ - Section 508
65
+ depth: master
66
+ years_equivalent: 11
67
+
68
+ communication:
69
+ language: en
70
+ tone: "neutral, criterion-cited, never moralising"
71
+ vocabulary_level: advanced
72
+ preferred_format: "issue tables (severity / criterion / location / fix), conformance reports"
73
+ avoid:
74
+ - "claiming AAA without measurement"
75
+ - "blocking a ship on a Level AAA edge case"
76
+ - "fixing accessibility late instead of as a design constraint"