diff --git a/.codex/skills/available_skills.xml b/.codex/skills/available_skills.xml
new file mode 100644
index 000000000..8748fac83
--- /dev/null
+++ b/.codex/skills/available_skills.xml
@@ -0,0 +1,67 @@
+
+
+ cloc
+ Count lines of code, analyze codebase size and composition, compare code between versions. Use when the user asks about lines of code, codebase size, language breakdown, or code statistics.
+ /Users/ksemenenko/Developer/markitdown/.codex/skills/cloc/SKILL.md
+
+
+ dotnet-strict
+ Apply strict .NET/C# coding standards to a project or solution. Adds Roslynator and Meziantou analyzers, a comprehensive .editorconfig with 80+ diagnostic rules, naming conventions, and performance warnings. Use when the user wants to enforce strict code quality, set up analyzers, or add an .editorconfig to a .NET project.
+ /Users/ksemenenko/Developer/markitdown/.codex/skills/dotnet-strict/SKILL.md
+
+
+ frontend-design
+ Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
+ /Users/ksemenenko/Developer/markitdown/.codex/skills/frontend-design/SKILL.md
+
+
+ mcaf-adr-writing
+ Create or update an ADR under `docs/ADR/` using `docs/templates/ADR-Template.md`, capturing context, decision, alternatives, consequences, rollout, and verification. Use when changing architecture, boundaries, dependencies, data model, or cross-cutting patterns; ensure the ADR is self-contained, has a Mermaid diagram, and defines testable invariants.
+ /Users/ksemenenko/Developer/markitdown/.codex/skills/mcaf-adr-writing/SKILL.md
+
+
+ mcaf-architecture-overview
+ Create or update `docs/Architecture/Overview.md` for a repository: map modules and boundaries, add a Mermaid module diagram, document dependency rules, and link to ADRs/features. Use when onboarding, refactoring, adding modules, or when the repo lacks a clear global architecture map.
+ /Users/ksemenenko/Developer/markitdown/.codex/skills/mcaf-architecture-overview/SKILL.md
+
+
+ mcaf-feature-spec
+ Create or update a feature document under `docs/Features/` using `docs/templates/Feature-Template.md`, including business rules, user flows, system behaviour, Mermaid diagram, verification plan, and Definition of Done. Use before implementing a non-trivial feature or when behaviour changes; make the spec executable (test flows + traceability to tests).
+ /Users/ksemenenko/Developer/markitdown/.codex/skills/mcaf-feature-spec/SKILL.md
+
+
+ mcaf-formatting
+ Format code and keep style consistent using the repository’s canonical formatting/lint commands from AGENTS.md. Use after implementing changes or when formatting drift causes noisy diffs; keep formatting changes intentional and verified with build/tests.
+ /Users/ksemenenko/Developer/markitdown/.codex/skills/mcaf-formatting/SKILL.md
+
+
+ mcaf-skill-curation
+ Create, update, and validate repository skills under your agent’s skills directory (Codex: `.codex/skills/`, Claude: `.claude/skills/`) so they match the real codebase and `AGENTS.md` rules; tune YAML `description` triggers, apply feedback, and generate `<available_skills>` metadata blocks.
+ /Users/ksemenenko/Developer/markitdown/.codex/skills/mcaf-skill-curation/SKILL.md
+
+
+ mcaf-testing
+ Add or update automated tests for a change (bugfix, feature, refactor) using the repository’s testing rules in AGENTS.md. Use TDD (test fails → implement → pass) where applicable; derive scenarios from docs/Features/* and ADR invariants; prefer stable integration/API/UI tests, run build before tests, collect coverage, and verify meaningful assertions for happy/negative/edge cases.
+ /Users/ksemenenko/Developer/markitdown/.codex/skills/mcaf-testing/SKILL.md
+
+
+ pre-pr
+ Run mandatory pre-PR quality checks for .NET projects before creating a pull request. Use before creating any PR, or when the user says "prepare for PR", "pre-PR checks", or "quality gate".
+ /Users/ksemenenko/Developer/markitdown/.codex/skills/pre-pr/SKILL.md
+
+
+ profile
+ Profile .NET applications for CPU performance, memory allocations, lock contention, exceptions, heap analysis, and JIT inlining. Use when the user asks about performance bottlenecks, memory leaks, high CPU, slow code, lock contention, excessive exceptions, GC pressure, heap growth, or JIT compilation in .NET projects.
+ /Users/ksemenenko/Developer/markitdown/.codex/skills/profile/SKILL.md
+
+
+ quickdup
+ Find and reduce code duplication, clean up redundant code, detect code clones, reduce codebase size, DRY violations, copy-paste detection. Use when the user asks about duplicate code, code cleanup, reducing code size, DRY principles, or finding copy-pasted code.
+ /Users/ksemenenko/Developer/markitdown/.codex/skills/quickdup/SKILL.md
+
+
+ roslynator
+ Run C# static analysis, auto-fix code issues, format code, find unused code, and enforce coding standards in .NET projects. Use when the user asks about code quality, linting, static analysis, code cleanup, unused code, or formatting in C# / .NET projects.
+ /Users/ksemenenko/Developer/markitdown/.codex/skills/roslynator/SKILL.md
+
+
diff --git a/.codex/skills/cloc/SKILL.md b/.codex/skills/cloc/SKILL.md
new file mode 100644
index 000000000..75498459c
--- /dev/null
+++ b/.codex/skills/cloc/SKILL.md
@@ -0,0 +1,86 @@
+---
+name: cloc
+description: Count lines of code, analyze codebase size and composition, compare code between versions. Use when the user asks about lines of code, codebase size, language breakdown, or code statistics.
+argument-hint: "[path or git ref]"
+---
+
+## Prerequisites
+
+Before running cloc, check if it is installed:
+
+```
+which cloc
+```
+
+If not found, install it:
+- macOS: `brew install cloc`
+- npm: `npm install -g cloc`
+- Debian/Ubuntu: `sudo apt install cloc`
+- Red Hat/Fedora: `sudo yum install cloc`
+
+## About cloc
+
+cloc counts blank lines, comment lines, and physical lines of source code in 200+ programming languages. It can analyze files, directories, archives (tar, zip, .whl, .ipynb), and git commits/branches.
+
+## Common Usage
+
+**Count a directory (default: current project):**
+```
+cloc .
+```
+
+**Count using git file list (respects .gitignore):**
+```
+cloc --vcs=git
+```
+
+**Count a specific path:**
+```
+cloc $ARGUMENTS
+```
+
+**Count by file (detailed breakdown):**
+```
+cloc --by-file --vcs=git
+```
+
+**Diff between two git refs:**
+```
+cloc --git --diff
+```
+
+**Exclude directories:**
+```
+cloc --exclude-dir=node_modules,vendor,.git .
+```
+
+## Output Formats
+
+Use these flags for machine-readable output:
+- `--json` — JSON
+- `--yaml` — YAML
+- `--csv` — CSV
+- `--md` — Markdown table
+- `--xml` — XML
+
+## Useful Flags
+
+| Flag | Purpose |
+|------|---------|
+| `--by-file` | Per-file breakdown instead of per-language |
+| `--vcs=git` | Use git to get file list (respects .gitignore) |
+| `--git` | Treat arguments as git targets (commits, branches) |
+| `--diff ` | Show added/removed/modified lines between two sources |
+| `--exclude-dir=` | Skip directories |
+| `--exclude-lang=` | Skip languages |
+| `--include-lang=` | Only count these languages |
+| `--force-lang=,` | Map file extension to a language |
+| `--processes=N` | Parallel processing (N cores) |
+| `--quiet` | Suppress progress output |
+
+## Guidelines
+
+- Prefer `--vcs=git` over bare `.` to avoid counting generated/vendored files
+- Use `--json` when the user needs to process the output programmatically
+- When comparing versions, use `--git --diff `
+- For large repos, consider `--processes=N` to speed things up
diff --git a/.codex/skills/dotnet-strict/SKILL.md b/.codex/skills/dotnet-strict/SKILL.md
new file mode 100644
index 000000000..79e74db54
--- /dev/null
+++ b/.codex/skills/dotnet-strict/SKILL.md
@@ -0,0 +1,334 @@
+---
+name: dotnet-strict
+description: Apply strict .NET/C# coding standards to a project or solution. Adds Roslynator and Meziantou analyzers, a comprehensive .editorconfig with 80+ diagnostic rules, naming conventions, and performance warnings. Use when the user wants to enforce strict code quality, set up analyzers, or add an .editorconfig to a .NET project.
+disable-model-invocation: true
+argument-hint: "[solution or project path]"
+---
+
+## What This Does
+
+Applies a strict, production-grade coding standard to a .NET project by:
+
+1. Adding analyzer NuGet packages to the project(s)
+2. Creating a comprehensive `.editorconfig` at the solution root
+3. Verifying the setup compiles
+
+## Step 1: Determine Target
+
+If `$ARGUMENTS` is provided, use it as the solution/project path. Otherwise, look for a `.sln` or `.csproj` in the current directory.
+
+Identify all `.csproj` files that should get analyzers (typically `src/` projects, not test projects).
+
+## Step 2: Add Analyzer Packages
+
+Add these analyzer packages to each source project (not test projects):
+
+```bash
+dotnet add package Roslynator.Analyzers
+dotnet add package Meziantou.Analyzer
+```
+
+These should be added as development-only dependencies. After adding, verify the package references include `PrivateAssets="all"` so they don't flow to consumers:
+
+```xml
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+```
+
+## Step 3: Create .editorconfig
+
+Create a `.editorconfig` file at the solution root (next to the `.sln` file, or at the repo root). If one already exists, merge the rules — do not overwrite existing customizations.
+
+The .editorconfig should contain the following rules:
+
+```ini
+; Strict .NET coding standards
+; Based on ASYNKRON production configuration
+root = true
+
+[*]
+indent_style = space
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.cs]
+indent_size = 4
+dotnet_sort_system_directives_first = true
+
+; --- Code Style ---
+
+; Don't use 'this.' qualifier
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+
+; Use int over Int32
+dotnet_style_predefined_type_for_locals_parameters_members = true:warning
+dotnet_style_predefined_type_for_member_access = true:warning
+
+; Require var
+csharp_style_var_for_built_in_types = true:warning
+csharp_style_var_when_type_is_apparent = true:warning
+csharp_style_var_elsewhere = true:suggestion
+
+; Disallow throw expressions
+csharp_style_throw_expression = false:suggestion
+
+; Braces on new lines (Allman style)
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+
+; File-scoped namespaces
+csharp_style_namespace_declarations = file_scoped
+
+; Always use braces
+csharp_prefer_braces = true
+
+; Modifier order
+csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:suggestion
+
+; No multiple blank lines
+dotnet_style_allow_multiple_blank_lines_experimental = false
+dotnet_diagnostic.IDE2000.severity = warning
+
+; Unused parameters (non-public only)
+dotnet_code_quality_unused_parameters = non_public
+
+; --- Naming Conventions ---
+
+; Constants: PascalCase
+dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
+dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
+dotnet_naming_symbols.constant_fields.applicable_kinds = field
+dotnet_naming_symbols.constant_fields.required_modifiers = const
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+
+; Static fields: PascalCase
+dotnet_naming_rule.pascal_case_for_static_fields.severity = suggestion
+dotnet_naming_rule.pascal_case_for_static_fields.symbols = static_fields
+dotnet_naming_rule.pascal_case_for_static_fields.style = pascal_case_style
+dotnet_naming_rule.pascal_case_for_static_fields.priority = 1
+dotnet_naming_symbols.static_fields.applicable_kinds = field
+dotnet_naming_symbols.static_fields.applicable_accessibilities = public, private, internal
+dotnet_naming_symbols.static_fields.required_modifiers = static
+
+; Private/internal fields: _camelCase
+dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
+dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
+dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
+dotnet_naming_rule.camel_case_for_private_internal_fields.priority = 3
+dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
+dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
+dotnet_naming_style.camel_case_underscore_style.required_prefix = _
+dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
+
+; --- Roslynator Rules ---
+
+; Resource can be disposed asynchronously
+dotnet_diagnostic.RCS1261.severity = warning
+; File contains no code
+dotnet_diagnostic.RCS1093.severity = error
+; Implement exception constructors (noisy — disabled)
+dotnet_diagnostic.RCS1194.severity = none
+; Add parentheses when necessary (noisy — disabled)
+dotnet_diagnostic.RCS1123.severity = none
+; Unused parameter
+dotnet_diagnostic.RCS1163.severity = warning
+; Unused type parameter
+dotnet_diagnostic.RCS1164.severity = warning
+; Unused 'this' parameter
+dotnet_diagnostic.RCS1175.severity = warning
+; Remove unused member declaration
+dotnet_diagnostic.RCS1213.severity = warning
+
+; --- Meziantou Rules ---
+
+; Use explicit enum value instead of 0
+dotnet_diagnostic.MA0099.severity = warning
+; Local variables should not hide other symbols
+dotnet_diagnostic.MA0084.severity = warning
+; Specify the parameter name in ArgumentException
+dotnet_diagnostic.MA0015.severity = warning
+; Method is too long
+dotnet_diagnostic.MA0051.severity = warning
+; Use String.Equals instead of equality operator
+dotnet_diagnostic.MA0006.severity = warning
+; Avoid implicit culture-sensitive methods
+dotnet_diagnostic.MA0074.severity = warning
+; IEqualityComparer or IComparer is missing
+dotnet_diagnostic.MA0002.severity = warning
+
+; --- IDE Rules ---
+
+; Remove unnecessary usings
+dotnet_diagnostic.IDE0005.severity = none
+; Add braces
+dotnet_diagnostic.IDE0011.severity = warning
+; Use pattern matching
+dotnet_diagnostic.IDE0020.severity = warning
+; Use coalesce expression (non-nullable)
+dotnet_diagnostic.IDE0029.severity = warning
+; Use coalesce expression (nullable)
+dotnet_diagnostic.IDE0030.severity = warning
+; Use null propagation
+dotnet_diagnostic.IDE0031.severity = warning
+; Remove unreachable code
+dotnet_diagnostic.IDE0035.severity = warning
+; Order modifiers
+dotnet_diagnostic.IDE0036.severity = warning
+; Use pattern matching
+dotnet_diagnostic.IDE0038.severity = warning
+; Format string contains invalid placeholder
+dotnet_diagnostic.IDE0043.severity = warning
+; Make field readonly
+dotnet_diagnostic.IDE0044.severity = warning
+; Remove unused private members
+dotnet_diagnostic.IDE0051.severity = warning
+; Fix formatting
+dotnet_diagnostic.IDE0055.severity = suggestion
+; Unnecessary assignment
+dotnet_diagnostic.IDE0059.severity = warning
+; Make local function static
+dotnet_diagnostic.IDE0062.severity = warning
+; Convert to file-scoped namespace
+dotnet_diagnostic.IDE0161.severity = warning
+; Remove unnecessary lambda expression
+dotnet_diagnostic.IDE0200.severity = warning
+
+; --- Compiler Warnings ---
+
+; Unreachable code detected
+dotnet_diagnostic.CS0162.severity = warning
+; Field is never used
+dotnet_diagnostic.CS0169.severity = warning
+
+; --- Microsoft Code Analysis (CA) Rules ---
+
+; Performance
+dotnet_diagnostic.CA1822.severity = warning
+dotnet_code_quality.CA1822.api_surface = private, internal
+dotnet_diagnostic.CA1825.severity = warning
+dotnet_diagnostic.CA1826.severity = warning
+dotnet_diagnostic.CA1827.severity = warning
+dotnet_diagnostic.CA1828.severity = warning
+dotnet_diagnostic.CA1829.severity = warning
+dotnet_diagnostic.CA1830.severity = warning
+dotnet_diagnostic.CA1831.severity = warning
+dotnet_diagnostic.CA1832.severity = warning
+dotnet_diagnostic.CA1833.severity = warning
+dotnet_diagnostic.CA1834.severity = warning
+dotnet_diagnostic.CA1835.severity = warning
+dotnet_diagnostic.CA1836.severity = warning
+dotnet_diagnostic.CA1837.severity = warning
+dotnet_diagnostic.CA1838.severity = warning
+dotnet_diagnostic.CA1839.severity = warning
+dotnet_diagnostic.CA1840.severity = warning
+dotnet_diagnostic.CA1841.severity = warning
+dotnet_diagnostic.CA1842.severity = warning
+dotnet_diagnostic.CA1843.severity = warning
+dotnet_diagnostic.CA1844.severity = warning
+dotnet_diagnostic.CA1845.severity = warning
+dotnet_diagnostic.CA1846.severity = warning
+dotnet_diagnostic.CA1847.severity = warning
+dotnet_diagnostic.CA1852.severity = warning
+dotnet_diagnostic.CA1854.severity = warning
+dotnet_diagnostic.CA1855.severity = warning
+dotnet_diagnostic.CA1856.severity = error
+dotnet_diagnostic.CA1857.severity = warning
+dotnet_diagnostic.CA1858.severity = warning
+
+; Unused code
+dotnet_diagnostic.CA1801.severity = warning
+dotnet_diagnostic.CA1811.severity = warning
+dotnet_diagnostic.CA1823.severity = warning
+dotnet_diagnostic.CA1802.severity = warning
+dotnet_diagnostic.CA1805.severity = warning
+dotnet_diagnostic.CA1810.severity = warning
+dotnet_diagnostic.CA1821.severity = warning
+
+; Correctness
+dotnet_diagnostic.CA1018.severity = warning
+dotnet_diagnostic.CA1047.severity = warning
+dotnet_diagnostic.CA1305.severity = warning
+dotnet_diagnostic.CA1507.severity = warning
+dotnet_diagnostic.CA1510.severity = warning
+dotnet_diagnostic.CA1511.severity = warning
+dotnet_diagnostic.CA1512.severity = warning
+dotnet_diagnostic.CA1513.severity = warning
+dotnet_diagnostic.CA1725.severity = suggestion
+dotnet_diagnostic.CA2008.severity = warning
+dotnet_diagnostic.CA2009.severity = warning
+dotnet_diagnostic.CA2011.severity = warning
+dotnet_diagnostic.CA2012.severity = warning
+dotnet_diagnostic.CA2013.severity = warning
+dotnet_diagnostic.CA2014.severity = warning
+dotnet_diagnostic.CA2016.severity = warning
+dotnet_diagnostic.CA2022.severity = warning
+dotnet_diagnostic.CA2200.severity = warning
+dotnet_diagnostic.CA2201.severity = warning
+dotnet_diagnostic.CA2208.severity = warning
+dotnet_diagnostic.CA2245.severity = warning
+dotnet_diagnostic.CA2246.severity = warning
+dotnet_diagnostic.CA2249.severity = warning
+
+[*.{xml,config,*proj,nuspec,props,resx,targets,yml,tasks}]
+indent_size = 2
+
+[*.json]
+indent_size = 2
+
+[*.sh]
+indent_size = 4
+end_of_line = lf
+
+[tests/**/*.cs]
+; Allow parameter capture in test constructors
+dotnet_diagnostic.CS9107.severity = silent
+```
+
+## Step 4: Build and Verify
+
+```bash
+dotnet build
+```
+
+The first build after adding analyzers will likely produce many warnings. This is expected — it shows the analyzers are working.
+
+Report a summary of the warnings found, grouped by category:
+- How many performance warnings (CA18xx)
+- How many unused code warnings (RCS1163, RCS1213, CA1811, etc.)
+- How many style warnings (IDE0xxx)
+- How many Meziantou warnings (MA0xxx)
+
+## Step 5: Optional — Auto-fix
+
+Suggest running roslynator to auto-fix what it can:
+
+```bash
+roslynator fix
+```
+
+## What the Rules Cover
+
+**Performance (CA18xx):** Use Span/Memory over substring, prefer Count property over Count(), use char overloads, seal internal types, prefer TryGetValue, avoid zero-length arrays.
+
+**Dead Code (RCS1163, RCS1213, CA1811, IDE0051):** Unused parameters, unused type parameters, unused members, uncalled private code.
+
+**Correctness (CA2xxx):** Rethrow to preserve stack, don't raise reserved exceptions, forward CancellationToken, avoid infinite recursion, use ValueTask correctly.
+
+**Culture Safety (MA0074, MA0006, CA1305):** Avoid implicit culture-sensitive methods, use String.Equals, specify IFormatProvider.
+
+**Modern C# Style:** File-scoped namespaces, pattern matching, null propagation, coalesce expressions, var everywhere, braces always.
+
+**Naming:** Constants PascalCase, static fields PascalCase, private fields _camelCase.
diff --git a/.codex/skills/frontend-design/SKILL.md b/.codex/skills/frontend-design/SKILL.md
new file mode 100644
index 000000000..4b2a345a4
--- /dev/null
+++ b/.codex/skills/frontend-design/SKILL.md
@@ -0,0 +1,42 @@
+---
+name: frontend-design
+description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
+license: Complete terms in LICENSE.txt
+---
+
+This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
+
+The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
+
+## Design Thinking
+
+Before coding, understand the context and commit to a BOLD aesthetic direction:
+- **Purpose**: What problem does this interface solve? Who uses it?
+- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
+- **Constraints**: Technical requirements (framework, performance, accessibility).
+- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
+
+**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
+
+Then implement working code (HTML/CSS/JS, Blazor) that is:
+- Production-grade and functional
+- Visually striking and memorable
+- Cohesive with a clear aesthetic point-of-view
+- Meticulously refined in every detail
+
+## Frontend Aesthetics Guidelines
+
+Focus on:
+- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
+- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
+- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
+- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
+- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
+
+NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
+
+Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
+
+**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
+
+Remember: Codex is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
diff --git a/.codex/skills/mcaf-adr-writing/SKILL.md b/.codex/skills/mcaf-adr-writing/SKILL.md
new file mode 100644
index 000000000..d704d54d2
--- /dev/null
+++ b/.codex/skills/mcaf-adr-writing/SKILL.md
@@ -0,0 +1,57 @@
+---
+name: mcaf-adr-writing
+description: "Create or update an ADR under `docs/ADR/` using `docs/templates/ADR-Template.md`, capturing context, decision, alternatives, consequences, rollout, and verification. Use when changing architecture, boundaries, dependencies, data model, or cross-cutting patterns; ensure the ADR is self-contained, has a Mermaid diagram, and defines testable invariants."
+compatibility: "Requires repository write access; produces Markdown docs with Mermaid diagrams."
+---
+
+# MCAF: ADR Writing
+
+## Outputs
+
+- `docs/ADR/ADR-XXXX-.md` (create or update)
+- Update `docs/Architecture/Overview.md` when boundaries/interactions change
+
+## Decision Quality (anti-guesswork checklist)
+
+Before writing, make the ADR executable (no placeholders, no hand-waving):
+
+- **Decision**: one sentence. If you can’t write it, you don’t have a decision yet.
+- **Scope**: what changes / what does not + which module(s) are affected (match `docs/Architecture/Overview.md` names).
+- **No invented reality**: every component you mention exists in the repo today, or is explicitly part of this change (named + where it will live).
+- **Invariants**: write as **MUST / MUST NOT** statements and say how we prove each (test or static analysis).
+- **Verification**: use exact commands from `AGENTS.md` and link scenarios → test IDs.
+- **Stakeholders**: Product / Dev / DevOps / QA — what each role must know to execute safely.
+
+## Workflow
+
+1. Confirm the decision scope:
+ - what changes (and what does not)
+ - what module(s) are affected
+ - follow `AGENTS.md` scoping rules: Architecture map → linked ADR/Feature → entry points (do not scan everything)
+2. Start from `docs/templates/ADR-Template.md`.
+ - keep the ADR’s `## Implementation plan (step-by-step)` updated while executing
+3. Write the ADR as a decision record:
+ - **Context**: constraints + why this is needed now
+ - **Decision**: a short, direct statement
+ - **Diagram** (mandatory): include at least one Mermaid diagram for the decision (boundaries/modules/interactions)
+ - **Alternatives**: 1–3 realistic options with pros/cons
+ - **Consequences**: trade-offs, risks, mitigations
+4. Make it executable for the team:
+ - follow `AGENTS.md` Task Delivery rules (analysis → plan → execute → verify)
+ - include the invariants that must be proven by tests
+ - include verification commands copied from `AGENTS.md`
+ - include rollout/rollback and “how we know it’s safe”
+5. Make impacts explicit:
+ - code/modules affected
+ - data/config changes (including migration/rollback)
+ - backwards compatibility strategy
+6. Add verification that proves the decision:
+ - which tests must exist/change
+ - which suites must stay green
+7. If the decision changes boundaries, update `docs/Architecture/Overview.md` (diagram, modules table, dependency rules).
+
+## Guardrails
+
+- ADRs are self-contained: no hidden context, no “as discussed”.
+- ADRs justify *why*; feature docs describe *what the system does*.
+- If you can’t state the decision in 1–2 sentences, the ADR is not ready.
diff --git a/.codex/skills/mcaf-adr-writing/references/ADR-FORMATS.md b/.codex/skills/mcaf-adr-writing/references/ADR-FORMATS.md
new file mode 100644
index 000000000..966526ab8
--- /dev/null
+++ b/.codex/skills/mcaf-adr-writing/references/ADR-FORMATS.md
@@ -0,0 +1,269 @@
+# ADR Formats (templates)
+
+This file is for **copy/paste**. Pick one of the templates below and fill it with real repo facts.
+
+Rules (MCAF):
+
+- ADRs are self-contained (no “as discussed”).
+- At least one Mermaid diagram is mandatory.
+- ADRs define testable invariants + verification commands (not vibes).
+
+---
+
+## Template 1: MCAF ADR (full)
+
+Use this as the default template. Save as `docs/ADR/ADR-XXXX-title-in-kebab-case.md`.
+
+````md
+# ADR-XXXX: Title
+
+Status: Proposed | Accepted | Implemented | Rejected | Superseded
+Date: YYYY-MM-DD
+Related Features: `docs/Features/...` (recommended)
+Supersedes: `docs/ADR/ADR-....md` (delete if none)
+Superseded by: `docs/ADR/ADR-....md` (delete if none)
+
+Rules:
+
+- This ADR is **self-contained** — avoid “as discussed”; include all critical context and links.
+- At least **one Mermaid diagram is mandatory** (boundaries/modules/interactions for this decision).
+
+---
+
+## Context
+
+- Current situation (what exists today).
+- Constraints (tech/legal/time/org constraints that matter).
+- Problem statement (what is failing / what you must enable).
+- Goals (what success looks like).
+- Non-goals (what this ADR is not trying to solve).
+
+---
+
+## Stakeholders (who needs this to be clear)
+
+| Role | What they need to know | Questions this ADR must answer |
+| --- | --- | --- |
+| Product / Owner | User/business impact, scope, rollout risk | What changes for users? What’s out of scope? |
+| Engineering | Boundaries/modules, data/contract changes, edge cases | What do we change, where, and why? |
+| DevOps / SRE | Deployability, config, monitoring, rollback | How do we ship safely and observe it? |
+| QA | Test scenarios + environment assumptions | What must be proven by automated tests? |
+
+---
+
+## Decision
+
+- One sentence decision statement.
+
+Key points:
+
+- Key point 1
+- Key point 2
+
+---
+
+## Diagram (Mandatory)
+
+```mermaid
+%% Show the boundaries/modules that change, and how they interact.
+%% Prefer 1 clear diagram over many noisy ones.
+```
+
+---
+
+## Alternatives considered
+
+### Option A
+
+- Pros:
+- Cons:
+- Rejected because:
+
+### Option B
+
+- Pros:
+- Cons:
+- Rejected because:
+
+---
+
+## Consequences
+
+### Positive
+
+- Benefit
+
+### Negative / risks
+
+- Risk
+- Mitigation:
+
+---
+
+## Impact
+
+### Code
+
+- Affected modules / services:
+- New boundaries / responsibilities:
+- Feature flags / toggles (names, defaults, removal plan):
+
+### Data / configuration
+
+- Data model / schema changes:
+- Config changes (keys, defaults, secrets handling):
+- Backwards compatibility strategy:
+
+### Documentation
+
+- Feature docs to update:
+- Testing docs to update:
+- Architecture docs to update:
+- `docs/Architecture/Overview.md` updates (what must change):
+- Notes for `AGENTS.md` (new rules/patterns):
+
+---
+
+## Verification (Mandatory: describe how to test this decision)
+
+### Objectives
+
+- What behaviour / qualities must be proven.
+- Which invariants from this ADR must be encoded as tests (happy path + negative/forbidden + edge cases).
+- Link each objective/scenario to the specific automated test(s) that prove it.
+
+### Test environment
+
+- Environment (local compose / staging / prod-like):
+- Data and reset strategy (seed data, migrations, rollback plan):
+- External dependencies (real / sandbox / fake services required):
+
+### Test commands
+
+- build: (paste from `AGENTS.md`)
+- test: (paste from `AGENTS.md`)
+- format: (paste from `AGENTS.md`)
+- coverage: (paste from `AGENTS.md` if separate; otherwise delete)
+
+### New or changed tests
+
+| ID | Scenario | Level (Unit / Int / API / UI) | Expected result | Notes / Data |
+| --- | --- | --- | --- | --- |
+| TST-001 | Happy path / negative / edge | Integration | Observable outcome | Fixtures / seed data |
+
+### Regression and analysis
+
+- Regression suites to run (must stay green):
+- Static analysis (tools/configs that must pass):
+- Monitoring during rollout (logs/metrics/alerts to watch):
+
+---
+
+## Rollout and migration
+
+- Migration steps:
+- Backwards compatibility:
+- Rollback:
+
+---
+
+## References
+
+- Issues / tickets:
+- External docs / specs:
+- Related ADRs:
+
+---
+
+## Filing checklist
+
+- [ ] File saved under `docs/ADR/ADR-XXXX-title-in-kebab-case.md` (not in `docs/templates/`).
+- [ ] Status reflects real state (`Proposed`, `Accepted`, `Rejected`, `Superseded`).
+- [ ] Links to related features, tests, and ADRs are filled in.
+- [ ] Diagram section contains at least one Mermaid diagram.
+- [ ] `docs/Architecture/Overview.md` updated if module boundaries or interactions changed.
+````
+
+---
+
+## Template 2: Mini ADR (small, safe decision)
+
+Use this when the decision is real but the scope is contained (still needs a diagram + verification).
+
+````md
+# ADR-XXXX: Title
+
+Status: Proposed | Accepted | Implemented | Rejected | Superseded
+Date: YYYY-MM-DD
+Related Features: `docs/Features/...` (delete if none)
+
+## Decision
+
+- One sentence.
+
+## Why (context + constraints)
+
+- Why now:
+- Constraints:
+
+## Diagram (Mandatory)
+
+```mermaid
+%% Boundaries/modules that change + their interaction.
+```
+
+## Alternatives (at least one)
+
+- Alternative A (why not):
+
+## Consequences
+
+- Positive:
+- Negative / risks + mitigations:
+
+## Verification (Mandatory)
+
+- Invariants to test:
+- Tests to add/update:
+- Commands to run (from `AGENTS.md`):
+````
+
+---
+
+## Template 3: Options Matrix ADR (when choosing between 2–4 options)
+
+Use this when you need to evaluate trade-offs explicitly and keep the decision auditable.
+
+````md
+# ADR-XXXX: Title
+
+Status: Proposed | Accepted | Implemented | Rejected | Superseded
+Date: YYYY-MM-DD
+
+## Decision
+
+- One sentence.
+
+## Decision drivers (what matters)
+
+- Driver 1:
+- Driver 2:
+
+## Options
+
+| Option | Summary | Pros | Cons | Risk | Why/why not |
+| --- | --- | --- | --- | --- | --- |
+| A | | | | | |
+| B | | | | | |
+
+## Diagram (Mandatory)
+
+```mermaid
+%% Diagram the chosen option (and the most important alternative if helpful).
+```
+
+## Verification (Mandatory)
+
+- Invariants to protect:
+- Test plan:
+````
diff --git a/.codex/skills/mcaf-architecture-overview/SKILL.md b/.codex/skills/mcaf-architecture-overview/SKILL.md
new file mode 100644
index 000000000..11fae049a
--- /dev/null
+++ b/.codex/skills/mcaf-architecture-overview/SKILL.md
@@ -0,0 +1,56 @@
+---
+name: mcaf-architecture-overview
+description: "Create or update `docs/Architecture/Overview.md` for a repository: map modules and boundaries, add a Mermaid module diagram, document dependency rules, and link to ADRs/features. Use when onboarding, refactoring, adding modules, or when the repo lacks a clear global architecture map."
+compatibility: "Requires repository write access; produces Markdown docs with Mermaid diagrams."
+---
+
+# MCAF: Architecture Overview
+
+## Output
+
+- `docs/Architecture/Overview.md` (create or update)
+
+## Architecture Thinking (keep it a map)
+
+This doc is the **global map**: boundaries, modules, and dependency rules.
+
+- Keep it lean and structural:
+ - modules/boundaries + responsibility + dependency direction
+ - at least one Mermaid module/boundary diagram
+- Keep behaviour out of the overview:
+ - feature flows live in `docs/Features/*`
+ - decision-specific diagrams/invariants live in `docs/ADR/*`
+- Anti-“AI slop” rule: never invent components/services/DBs — only document what exists (or what this change will explicitly add).
+
+## Workflow
+
+1. Open `docs/Architecture/Overview.md` if it exists; otherwise start from `docs/templates/Architecture-Template.md`.
+ - Ensure it contains a short `## Scoping (read first)` section (this is how we prevent “scan everything” behaviour).
+2. Identify the **real** top-level boundaries:
+ - entry points (HTTP/API, CLI, UI, jobs, events)
+ - modules/layers (group by folders/namespaces, not individual files)
+ - external dependencies (only those that actually exist)
+3. Fill the **Summary** so a new engineer can orient in ~1 minute.
+4. Draw the Mermaid diagram as a **module map**:
+ - keep it small (roughly 8–15 nodes)
+ - label arrows with meaning (calls, events, reads/writes)
+ - don’t invent DB/queues/services that aren’t present
+5. Fill the modules table:
+ - one row per module/service
+ - responsibilities and “depends on” must be concrete
+6. Write explicit dependency rules:
+ - what is allowed
+ - what is forbidden
+ - how integration happens (sync / async / shared lib)
+7. Add a short “Key decisions (ADRs)” section:
+ - link to the ADRs that define boundaries, dependencies, and major cross-cutting patterns
+ - keep it link-based (no detailed flows here)
+8. Link out to deeper docs:
+ - ADRs for key decisions
+ - Features for behaviour details
+ - Testing/Development for how to run and verify
+
+## Guardrails
+
+- Do not list every file/class. This is a **map**, not an inventory.
+- Keep the document stable: update it when boundaries or interactions change.
diff --git a/.codex/skills/mcaf-feature-spec/SKILL.md b/.codex/skills/mcaf-feature-spec/SKILL.md
new file mode 100644
index 000000000..242e93789
--- /dev/null
+++ b/.codex/skills/mcaf-feature-spec/SKILL.md
@@ -0,0 +1,50 @@
+---
+name: mcaf-feature-spec
+description: "Create or update a feature document under `docs/Features/` using `docs/templates/Feature-Template.md`, including business rules, user flows, system behaviour, Mermaid diagram, verification plan, and Definition of Done. Use before implementing a non-trivial feature or when behaviour changes; make the spec executable (test flows + traceability to tests)."
+compatibility: "Requires repository write access; produces Markdown docs with Mermaid diagrams and executable verification steps."
+---
+
+# MCAF: Feature Spec
+
+## Outputs
+
+- `docs/Features/.md` (create or update)
+- Update links from/to ADRs and architecture map when needed
+
+## Spec Quality (anti-guesswork checklist)
+
+Write a spec that can be implemented and verified **without guessing**:
+
+- **No placeholders**: avoid “TBD”, “later”, “etc.”; if something is unknown, list it as an explicit question.
+- **Concrete modules**: use real module/boundary names from `docs/Architecture/Overview.md`.
+- **Rules are testable**: numbered business rules with clear inputs → outputs (no vague adjectives).
+- **Flows are executable**: scenarios include preconditions, steps, expected results (happy + negative + edge).
+- **Verification is real**: commands copied from `AGENTS.md`, and scenarios mapped to test IDs.
+- **Stakeholders covered**: Product / Dev / DevOps / QA each get the information they need to ship safely.
+
+## Workflow
+
+1. Start from `docs/Architecture/Overview.md` to pick the affected module(s).
+2. Create/update the feature doc using `docs/templates/Feature-Template.md`.
+ - follow `AGENTS.md` scoping rules (do not scan the whole repo; use the architecture map to stay focused)
+ - keep the feature’s `## Implementation plan (step-by-step)` updated while executing
+3. Define behaviour precisely:
+ - purpose and scope (in/out)
+ - business rules (numbered, testable)
+ - primary flow + edge cases
+4. Describe system behaviour in terms of **entry points, reads/writes, side effects, idempotency, and errors**.
+5. Add a Mermaid diagram for the main flow (modules + interactions; keep it readable).
+6. Write verification that can be executed:
+ - test environment assumptions
+ - concrete test flows (positive/negative/edge)
+ - mapping to where tests live (or will live)
+ - traceability: rules/flows → test IDs (so tests reflect the spec)
+7. Keep Definition of Done strict:
+ - behaviour covered by automated tests
+ - static analysis clean
+ - docs updated (feature + ADR + architecture overview if boundaries changed)
+
+## Guardrails
+
+- If the feature introduces a new dependency/boundary, write an ADR and update `docs/Architecture/Overview.md`.
+- Don’t hide decisions inside the feature doc: decisions go to ADRs.
diff --git a/.codex/skills/mcaf-formatting/SKILL.md b/.codex/skills/mcaf-formatting/SKILL.md
new file mode 100644
index 000000000..78869108c
--- /dev/null
+++ b/.codex/skills/mcaf-formatting/SKILL.md
@@ -0,0 +1,32 @@
+---
+name: mcaf-formatting
+description: "Format code and keep style consistent using the repository’s canonical formatting/lint commands from AGENTS.md. Use after implementing changes or when formatting drift causes noisy diffs; keep formatting changes intentional and verified with build/tests."
+compatibility: "Requires the repository’s formatter/linter tools; uses commands from AGENTS.md."
+---
+
+# MCAF: Formatting
+
+## Outputs
+
+- Formatted code changes (consistent with repo style)
+- Evidence: formatting command(s) run and any follow-up build/tests
+
+## Workflow
+
+1. Use the canonical `format` command from `AGENTS.md` (do not invent commands).
+2. Run the formatter on the smallest scope possible (if your tools allow it).
+3. Review the diff:
+ - ensure changes are formatting-only
+ - if the formatter touched many files, separate the change or confirm it was explicitly requested
+4. Verify (follow `AGENTS.md` for sequencing + required commands):
+ - for formatting-only changes: run the smallest meaningful verification the repo requires (build/tests/analyze as applicable)
+ - for formatting as part of a behaviour change: follow the repo’s normal order (don’t reorder the pipeline)
+5. If `format`/linters are missing or flaky:
+ - fix `AGENTS.md` to point to a real, repeatable command
+ - only then rerun formatting
+6. Report what was run and what changed.
+
+## Guardrails
+
+- Do not introduce unrelated refactors under the cover of formatting.
+- Keep formatting changes and behaviour changes reviewable.
diff --git a/.codex/skills/mcaf-skill-curation/SKILL.md b/.codex/skills/mcaf-skill-curation/SKILL.md
new file mode 100644
index 000000000..878823a97
--- /dev/null
+++ b/.codex/skills/mcaf-skill-curation/SKILL.md
@@ -0,0 +1,65 @@
+---
+name: mcaf-skill-curation
+description: "Create, update, and validate repository skills under your agent’s skills directory (Codex: `.codex/skills/`, Claude: `.claude/skills/`) so they match the real codebase and `AGENTS.md` rules; tune YAML `description` triggers, apply feedback, and generate `` metadata blocks."
+compatibility: "Requires repository write access; uses a shell and Python 3 for validation/scripts."
+---
+
+# MCAF: Skill Curation
+
+## Outputs
+
+- New/updated skill folders under your agent’s skills directory with valid `SKILL.md` frontmatter and lean workflows
+- Updated YAML `description` triggers for correct matching
+- A generated `` block (metadata only) for your agent runtime
+- A validation report (script output)
+
+## Workflow
+
+1. Read the repo’s sources of truth:
+ - `AGENTS.md` (commands + hard rules)
+ - `docs/Architecture/Overview.md` (modules/boundaries) if present
+2. Inventory skills and identify drift:
+ - list all `*/SKILL.md` under your skills directory (Codex: `.codex/skills/*/SKILL.md`, Claude Code: `.claude/skills/*/SKILL.md`)
+ - verify folder name == YAML `name`
+ - look for “template cruft” inside skill folders (README/INSTALL/CHANGELOG) and remove it
+3. Update skills to match reality (no guessing):
+ - ensure each skill references **real** commands (from `AGENTS.md`)
+ - ensure each skill’s YAML `description` includes trigger keywords that match how users ask for the task
+ - ensure workflows reference real modules/boundaries (from architecture overview)
+4. Create new skills for repeated/fragile workflows:
+ - keep one workflow per skill (split mega-skills)
+ - copy the closest existing skill and adapt (folder/name, triggers, outputs, workflow)
+ - use `scripts/` for deterministic or fragile steps
+ - use `references/` only for copy/paste templates and structured checklists (avoid “reading lists”)
+5. Validate skills (fix errors before shipping):
+ - run the bundled validator from the repo root:
+ - `python3 /mcaf-skill-curation/scripts/validate_skills.py `
+ - Codex: `python3 .codex/skills/mcaf-skill-curation/scripts/validate_skills.py .codex/skills`
+ - Claude: `python3 .claude/skills/mcaf-skill-curation/scripts/validate_skills.py .claude/skills`
+6. Generate a metadata-only skills block for your agent runtime:
+ - run the bundled generator from the repo root:
+ - `python3 /mcaf-skill-curation/scripts/generate_available_skills.py --absolute`
+ - Codex: `python3 .codex/skills/mcaf-skill-curation/scripts/generate_available_skills.py .codex/skills --absolute`
+ - Claude: `python3 .claude/skills/mcaf-skill-curation/scripts/generate_available_skills.py .claude/skills --absolute`
+ - paste the output into your agent configuration (system/developer prompt)
+7. When user feedback is about skills:
+ - update the relevant `//SKILL.md` (especially YAML `description` triggers) so the fix is permanent
+
+## Bundled scripts
+
+- `scripts/validate_skills.py` — validates frontmatter + folder/name rules and flags common spec violations.
+- `scripts/generate_available_skills.py` — prints an `` XML block from your skills directory `*/SKILL.md` metadata.
+
+## Guardrails
+
+- Don’t turn a skill into a wiki: keep `SKILL.md` procedural and short.
+- Don’t add extra docs inside skill folders (`README.md`, `INSTALLATION_GUIDE.md`, `CHANGELOG.md`, etc.).
+- Prefer updating triggers (`description`) over adding more and more body text.
+- YAML is strict: if a value contains `:` or other YAML-significant characters, wrap it in quotes.
+
+## Examples (trigger phrases)
+
+- "update our skills to match the repo"
+- "this skill triggers wrong, fix the description"
+- "generate available_skills block"
+- "validate skills frontmatter"
diff --git a/.codex/skills/mcaf-skill-curation/scripts/generate_available_skills.py b/.codex/skills/mcaf-skill-curation/scripts/generate_available_skills.py
new file mode 100755
index 000000000..ae7b42ec5
--- /dev/null
+++ b/.codex/skills/mcaf-skill-curation/scripts/generate_available_skills.py
@@ -0,0 +1,187 @@
+#!/usr/bin/env python3
+
+from __future__ import annotations
+
+import argparse
+import html
+import re
+import sys
+from dataclasses import dataclass
+from pathlib import Path
+
+try:
+ import yaml # type: ignore
+except Exception: # pragma: no cover - optional dependency
+ yaml = None
+
+
+@dataclass(frozen=True)
+class SkillMetadata:
+ name: str
+ description: str
+ skill_md: Path
+
+
+class SkillMetadataError(Exception):
+ pass
+
+
+def _parse_frontmatter(skill_md: Path) -> tuple[str, str]:
+ text = skill_md.read_text(encoding="utf-8")
+ lines = text.splitlines()
+ if not lines or lines[0].strip() != "---":
+ raise SkillMetadataError("Missing YAML frontmatter start ('---') at line 1.")
+
+ end_index = None
+ for i in range(1, len(lines)):
+ if lines[i].strip() == "---":
+ end_index = i
+ break
+ if end_index is None:
+ raise SkillMetadataError("Missing YAML frontmatter end ('---').")
+
+ frontmatter_text = "\n".join(lines[1:end_index]).strip()
+
+ if yaml is not None:
+ try:
+ data = yaml.safe_load(frontmatter_text) or {}
+ except Exception as e:
+ raise SkillMetadataError(f"Invalid YAML frontmatter: {e}") from e
+
+ if not isinstance(data, dict):
+ raise SkillMetadataError("YAML frontmatter must be a mapping (key/value object).")
+
+ name_value = data.get("name")
+ description_value = data.get("description")
+
+ if not isinstance(name_value, str) or not name_value.strip():
+ raise SkillMetadataError("Missing YAML 'name'.")
+ if not isinstance(description_value, str) or not description_value.strip():
+ raise SkillMetadataError("Missing YAML 'description'.")
+
+ if "\n" in name_value or "\r" in name_value:
+ raise SkillMetadataError("YAML 'name' must be a single line.")
+ if "\n" in description_value or "\r" in description_value:
+ raise SkillMetadataError("YAML 'description' must be a single line.")
+
+ return name_value, description_value
+
+ # Fallback parser (no PyYAML installed): only supports single-line `key: value` scalars.
+ name_value: str | None = None
+ description_value: str | None = None
+
+ for raw in lines[1:end_index]:
+ line = raw.strip()
+ if not line or line.startswith("#"):
+ continue
+
+ match = re.match(r"^([A-Za-z0-9_-]+):\s*(.*)$", line)
+ if not match:
+ continue
+ key, value = match.group(1), match.group(2).strip()
+
+ if value in {"|", ">", "|-", ">-"}:
+ raise SkillMetadataError(
+ f"Unsupported multi-line YAML value for '{key}'. Use a single-line scalar."
+ )
+
+ if len(value) >= 2 and value[0] in {"'", '"'} and value[-1] == value[0]:
+ value = value[1:-1]
+
+ if key == "name":
+ name_value = value
+ elif key == "description":
+ description_value = value
+
+ if not name_value:
+ raise SkillMetadataError("Missing YAML 'name'.")
+ if not description_value:
+ raise SkillMetadataError("Missing YAML 'description'.")
+
+ return name_value, description_value
+
+
+def _collect_skills(skills_root: Path) -> list[SkillMetadata]:
+ skills: list[SkillMetadata] = []
+ for child in sorted(skills_root.iterdir()):
+ if not child.is_dir():
+ continue
+ skill_md = child / "SKILL.md"
+ if not skill_md.exists():
+ continue
+ name, description = _parse_frontmatter(skill_md)
+ skills.append(SkillMetadata(name=name, description=description, skill_md=skill_md))
+ return sorted(skills, key=lambda s: s.name)
+
+
+def _detect_skills_dir() -> str:
+ candidates = (Path(".codex/skills"), Path(".claude/skills"), Path("skills"))
+
+ def contains_skills(dir_path: Path) -> bool:
+ if not dir_path.exists() or not dir_path.is_dir():
+ return False
+ for child in dir_path.iterdir():
+ if not child.is_dir():
+ continue
+ if (child / "SKILL.md").is_file():
+ return True
+ return False
+
+ for candidate in candidates:
+ if contains_skills(candidate):
+ return str(candidate)
+
+ for candidate in candidates:
+ if candidate.exists() and candidate.is_dir():
+ return str(candidate)
+
+ return "skills"
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(
+ description="Generate an XML block from */SKILL.md metadata under a skills directory."
+ )
+ parser.add_argument(
+ "skills_dir",
+ nargs="?",
+ default=_detect_skills_dir(),
+ help="Path to skills directory (default: auto-detect .codex/skills, .claude/skills, or ./skills).",
+ )
+ parser.add_argument(
+ "--absolute",
+ action="store_true",
+ help="Use absolute paths in (recommended for filesystem-based agents).",
+ )
+ args = parser.parse_args()
+
+ skills_root = Path(args.skills_dir).resolve()
+ if not skills_root.exists() or not skills_root.is_dir():
+ print(f"ERROR: Skills directory does not exist: {skills_root}", file=sys.stderr)
+ return 2
+
+ try:
+ skills = _collect_skills(skills_root)
+ except SkillMetadataError as e:
+ print(f"ERROR: {e}", file=sys.stderr)
+ return 2
+
+ if not skills:
+ print(f"ERROR: No skills found under: {skills_root}", file=sys.stderr)
+ return 2
+
+ print("")
+ for skill in skills:
+ location = str(skill.skill_md.resolve() if args.absolute else skill.skill_md.relative_to(Path.cwd()))
+ print(" ")
+ print(f" {html.escape(skill.name)}")
+ print(f" {html.escape(skill.description)}")
+ print(f" {html.escape(location)}")
+ print(" ")
+ print("")
+
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/.codex/skills/mcaf-skill-curation/scripts/validate_skills.py b/.codex/skills/mcaf-skill-curation/scripts/validate_skills.py
new file mode 100755
index 000000000..9609ae3df
--- /dev/null
+++ b/.codex/skills/mcaf-skill-curation/scripts/validate_skills.py
@@ -0,0 +1,261 @@
+#!/usr/bin/env python3
+
+from __future__ import annotations
+
+import argparse
+import re
+import sys
+from dataclasses import dataclass
+from pathlib import Path
+
+try:
+ import yaml # type: ignore
+except Exception: # pragma: no cover - optional dependency
+ yaml = None
+
+
+SKILL_NAME_RE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
+MAX_NAME_LEN = 64
+MAX_DESCRIPTION_LEN = 1024
+
+FORBIDDEN_FILES = {
+ "README.md",
+ "CHANGELOG.md",
+ "INSTALLATION_GUIDE.md",
+ "QUICK_REFERENCE.md",
+}
+
+
+@dataclass(frozen=True)
+class Frontmatter:
+ name: str | None
+ description: str | None
+
+
+class SkillValidationError(Exception):
+ pass
+
+
+def _parse_frontmatter(skill_md: Path) -> Frontmatter:
+ text = skill_md.read_text(encoding="utf-8")
+ lines = text.splitlines()
+ if not lines or lines[0].strip() != "---":
+ raise SkillValidationError("Missing YAML frontmatter start ('---') at line 1.")
+
+ end_index = None
+ for i in range(1, len(lines)):
+ if lines[i].strip() == "---":
+ end_index = i
+ break
+ if end_index is None:
+ raise SkillValidationError("Missing YAML frontmatter end ('---').")
+
+ frontmatter_text = "\n".join(lines[1:end_index]).strip()
+
+ if yaml is not None:
+ try:
+ data = yaml.safe_load(frontmatter_text) or {}
+ except Exception as e:
+ raise SkillValidationError(f"Invalid YAML frontmatter: {e}") from e
+
+ if not isinstance(data, dict):
+ raise SkillValidationError("YAML frontmatter must be a mapping (key/value object).")
+
+ name_value = data.get("name")
+ description_value = data.get("description")
+
+ if name_value is not None and not isinstance(name_value, str):
+ raise SkillValidationError("YAML 'name' must be a string.")
+ if description_value is not None and not isinstance(description_value, str):
+ raise SkillValidationError("YAML 'description' must be a string.")
+
+ # Enforce single-line scalars (Codex expects concise metadata).
+ if isinstance(name_value, str) and ("\n" in name_value or "\r" in name_value):
+ raise SkillValidationError("YAML 'name' must be a single line.")
+ if isinstance(description_value, str) and ("\n" in description_value or "\r" in description_value):
+ raise SkillValidationError("YAML 'description' must be a single line.")
+
+ return Frontmatter(name=name_value, description=description_value)
+
+ # Fallback parser (no PyYAML installed): only supports single-line `key: value` scalars.
+ name_value: str | None = None
+ description_value: str | None = None
+
+ for raw in lines[1:end_index]:
+ line = raw.strip()
+ if not line or line.startswith("#"):
+ continue
+
+ match = re.match(r"^([A-Za-z0-9_-]+):\s*(.*)$", line)
+ if not match:
+ continue
+ key, value = match.group(1), match.group(2).strip()
+
+ if value in {"|", ">", "|-", ">-"}:
+ raise SkillValidationError(
+ f"Unsupported multi-line YAML value for '{key}'. Use a single-line scalar."
+ )
+
+ if len(value) >= 2 and value[0] in {"'", '"'} and value[-1] == value[0]:
+ value = value[1:-1]
+
+ if key == "name":
+ name_value = value
+ elif key == "description":
+ description_value = value
+
+ return Frontmatter(name=name_value, description=description_value)
+
+
+def _validate_name(value: str, *, skill_dir_name: str) -> list[str]:
+ errors: list[str] = []
+
+ if not value:
+ errors.append("YAML 'name' is empty.")
+ return errors
+
+ if len(value) > MAX_NAME_LEN:
+ errors.append(f"YAML 'name' is too long ({len(value)} > {MAX_NAME_LEN}).")
+
+ if not SKILL_NAME_RE.fullmatch(value):
+ errors.append(
+ "YAML 'name' must match ^[a-z0-9]+(-[a-z0-9]+)*$ (lowercase, digits, hyphens; no leading/trailing hyphen; no '--')."
+ )
+
+ if value != skill_dir_name:
+ errors.append(f"YAML 'name' ('{value}') must match folder name ('{skill_dir_name}').")
+
+ return errors
+
+
+def _validate_description(value: str) -> list[str]:
+ errors: list[str] = []
+
+ if not value or not value.strip():
+ errors.append("YAML 'description' is missing or empty.")
+ return errors
+
+ if len(value) > MAX_DESCRIPTION_LEN:
+ errors.append(
+ f"YAML 'description' is too long ({len(value)} > {MAX_DESCRIPTION_LEN})."
+ )
+
+ return errors
+
+
+def _find_skill_dirs(skills_root: Path) -> list[Path]:
+ if not skills_root.exists():
+ raise SkillValidationError(f"Skills directory does not exist: {skills_root}")
+ if not skills_root.is_dir():
+ raise SkillValidationError(f"Skills path is not a directory: {skills_root}")
+
+ skill_dirs: list[Path] = []
+ for child in sorted(skills_root.iterdir()):
+ if not child.is_dir():
+ continue
+ if (child / "SKILL.md").exists():
+ skill_dirs.append(child)
+ return skill_dirs
+
+
+def _detect_skills_dir() -> str:
+ candidates = (Path(".codex/skills"), Path(".claude/skills"), Path("skills"))
+
+ def contains_skills(dir_path: Path) -> bool:
+ if not dir_path.exists() or not dir_path.is_dir():
+ return False
+ for child in dir_path.iterdir():
+ if not child.is_dir():
+ continue
+ if (child / "SKILL.md").is_file():
+ return True
+ return False
+
+ for candidate in candidates:
+ if contains_skills(candidate):
+ return str(candidate)
+
+ for candidate in candidates:
+ if candidate.exists() and candidate.is_dir():
+ return str(candidate)
+
+ return "skills"
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description="Validate Agent Skills under a directory (for example: .codex/skills).")
+ parser.add_argument(
+ "skills_dir",
+ nargs="?",
+ default=_detect_skills_dir(),
+ help="Path to skills directory (default: auto-detect .codex/skills, .claude/skills, or ./skills).",
+ )
+ args = parser.parse_args()
+
+ skills_root = Path(args.skills_dir).resolve()
+
+ try:
+ skill_dirs = _find_skill_dirs(skills_root)
+ except SkillValidationError as e:
+ print(f"ERROR: {e}", file=sys.stderr)
+ return 2
+
+ if not skill_dirs:
+ print(f"No skills found under: {skills_root}", file=sys.stderr)
+ return 2
+
+ errors_count = 0
+ warnings_count = 0
+
+ for skill_dir in skill_dirs:
+ skill_md = skill_dir / "SKILL.md"
+ rel = skill_md.relative_to(skills_root.parent) if skills_root.parent in skill_md.parents else skill_md
+ print(f"\n==> {rel}")
+
+ try:
+ fm = _parse_frontmatter(skill_md)
+ except SkillValidationError as e:
+ errors_count += 1
+ print(f"ERROR: {e}", file=sys.stderr)
+ continue
+
+ if fm.name is None:
+ errors_count += 1
+ print("ERROR: YAML 'name' is missing.", file=sys.stderr)
+ else:
+ name_errors = _validate_name(fm.name, skill_dir_name=skill_dir.name)
+ for err in name_errors:
+ errors_count += 1
+ print(f"ERROR: {err}", file=sys.stderr)
+
+ if fm.description is None:
+ errors_count += 1
+ print("ERROR: YAML 'description' is missing.", file=sys.stderr)
+ else:
+ description_errors = _validate_description(fm.description)
+ for err in description_errors:
+ errors_count += 1
+ print(f"ERROR: {err}", file=sys.stderr)
+
+ # Additional hygiene checks.
+ forbidden_present = []
+ for name in FORBIDDEN_FILES:
+ if (skill_dir / name).exists():
+ forbidden_present.append(name)
+ if forbidden_present:
+ warnings_count += len(forbidden_present)
+ print(
+ f"WARNING: Remove extra docs from skill folder: {', '.join(sorted(forbidden_present))}",
+ file=sys.stderr,
+ )
+
+ print("\n---")
+ print(f"Skills validated: {len(skill_dirs)}")
+ print(f"Errors: {errors_count}")
+ print(f"Warnings: {warnings_count}")
+
+ return 1 if errors_count else 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/.codex/skills/mcaf-testing/SKILL.md b/.codex/skills/mcaf-testing/SKILL.md
new file mode 100644
index 000000000..f635153f4
--- /dev/null
+++ b/.codex/skills/mcaf-testing/SKILL.md
@@ -0,0 +1,74 @@
+---
+name: mcaf-testing
+description: "Add or update automated tests for a change (bugfix, feature, refactor) using the repository’s testing rules in AGENTS.md. Use TDD (test fails → implement → pass) where applicable; derive scenarios from docs/Features/* and ADR invariants; prefer stable integration/API/UI tests, run build before tests, collect coverage, and verify meaningful assertions for happy/negative/edge cases."
+compatibility: "Requires the repository’s build/test tooling; uses commands from AGENTS.md."
+---
+
+# MCAF: Testing
+
+## Outputs
+
+- New/updated automated tests that encode **documented behaviour** (happy path + negative + edge), with integration/API/UI preferred
+- For new behaviour and bugfixes: tests drive the change (TDD: reproduce/specify → test fails → implement → test passes)
+- Updated verification sections in relevant docs (`docs/Features/*`, `docs/ADR/*`) when needed (tests + commands must match reality)
+- Evidence of verification: commands run (`build`/`test`/`coverage`/`analyze`) + result + the report/artifact path written by the tool (when applicable)
+
+## Workflow
+
+1. Read `AGENTS.md`:
+ - commands: `build`, `test`, `format`, `analyze`, and the repo’s coverage path (either a dedicated `coverage` command or a `test` command that generates coverage)
+ - testing rules (levels, mocks policy, suites to run, containers, etc.)
+2. Start from the docs that define behaviour (no guessing):
+ - `docs/Features/*` for user/system flows and business rules
+ - `docs/ADR/*` for architectural decisions and invariants that must remain true
+ - if the docs are missing/contradict, fix the docs first (or write a minimal spec + test plan in the task/PR)
+ - follow `AGENTS.md` scoping rules (Architecture map → relevant docs → relevant module code; avoid repo-wide scanning)
+3. Follow `AGENTS.md` verification timing (optimize time + tokens):
+ - run tests/coverage only when you have a reason (changed code/tests, bug reproduction, baseline confirmation)
+ - start with the smallest scope (new/changed tests), then expand to required suites
+4. For "fix failing tests" tasks, triage in batches before coding:
+ - run the target full suite first (not one test at a time)
+ - collect the complete failing-test list
+ - write/refresh a root `*.plan.md` checklist with each failing test and status
+ - fix from that checklist in required order (integration -> API -> UI)
+ - for each layer, repeat this loop until full-suite `x0`: full suite -> plan update -> fixes -> targeted retests -> full suite again
+ - after all three layers are `x0`, run one final full regression
+5. Define the scenarios you must prove (map them back to docs):
+ - **positive** (happy path)
+ - **negative** (validation/forbidden/unauthorized/error paths)
+ - **edge** (limits, concurrency, retries/idempotency, time-sensitive behaviour)
+ - for ADRs: test the **invariants** and the “must not happen” behaviours the decision relies on
+6. Choose the highest meaningful test level:
+ - prefer integration/API/UI when the behaviour crosses boundaries
+ - use unit tests only when logic is isolated and higher-level coverage is impractical
+7. Implement via a TDD loop (per scenario):
+ - write the test first and make sure it fails for the **right reason**
+ - implement the minimum change to make it pass
+ - refactor safely (keep tests green)
+8. Write tests that assert outcomes (not “it runs”):
+ - assert returned values/responses
+ - assert DB state / emitted events / observable side effects
+ - include negative and edge cases when relevant
+9. Keep tests stable (treat flakiness as a bug):
+ - deterministic data/fixtures, no hidden dependencies
+ - avoid `sleep`-based timing; prefer “wait until condition”/polling with a timeout
+ - keep test setup/teardown reliable (reset state between tests)
+10. Coverage (follow `AGENTS.md`, optimize time/tokens):
+ - run coverage only if it’s part of the repo’s required verification path or if you need it to find gaps
+ - run coverage once per change (it is heavier than tests)
+ - capture where the report/artifacts were written (path, summary) if generated
+11. If the repo has UI:
+ - run UI/E2E tests
+ - inspect screenshots/videos/traces produced by the runner for failures and obvious UI regressions
+12. Run verification in layers (as required by `AGENTS.md`):
+ - new/changed tests first
+ - then the related suite
+ - then broader regressions if required
+ - run `analyze` if required
+13. Keep docs and skills consistent:
+ - ensure `docs/Features/*` and `docs/ADR/*` verification sections point to the real tests and real commands
+ - if you change test/coverage commands or rules, update `AGENTS.md` and this skill in the same PR
+
+## Guardrails
+
+- All test discipline and prohibitions come from `AGENTS.md`. Do not contradict it in this skill.
diff --git a/.codex/skills/pre-pr/SKILL.md b/.codex/skills/pre-pr/SKILL.md
new file mode 100644
index 000000000..64d8a3632
--- /dev/null
+++ b/.codex/skills/pre-pr/SKILL.md
@@ -0,0 +1,100 @@
+---
+name: pre-pr
+description: Run mandatory pre-PR quality checks for .NET projects before creating a pull request. Use before creating any PR, or when the user says "prepare for PR", "pre-PR checks", or "quality gate".
+disable-model-invocation: true
+argument-hint: "[solution or project path]"
+---
+
+## Prerequisites
+
+Check that required tools are available:
+
+```
+which roslynator
+which quickdup
+```
+
+If `roslynator` is not found:
+```
+dotnet tool install -g roslynator.dotnet.cli
+```
+
+If `quickdup` is not found:
+- macOS/Linux: `curl -sSL https://raw.githubusercontent.com/asynkron/Asynkron.QuickDup/main/install.sh | bash`
+- From source: `go install github.com/asynkron/Asynkron.QuickDup/cmd/quickdup@latest`
+
+## About
+
+This is a mandatory quality gate to run before creating any pull request in a .NET project. It ensures code is analyzed, compiles cleanly, tests pass, duplication is checked, and code is formatted — in that order.
+
+Do NOT skip any steps. Do NOT create a PR if any step fails. Iterate until all checks pass.
+
+## Determine Paths
+
+If `$ARGUMENTS` is provided, use it as the project/solution path. Otherwise, look for a `.sln` or `.csproj` in the current directory.
+
+Also determine the source directory for quickdup scanning (typically `src/` or the project root).
+
+## Step 1: Roslynator Fix
+
+Run static analysis and auto-fix code issues:
+
+```bash
+roslynator fix $PROJECT_PATH
+```
+
+Then verify compilation:
+
+```bash
+dotnet build $PROJECT_PATH
+```
+
+**If build fails:** Fix the issues and rerun roslynator before proceeding.
+
+## Step 2: Run Tests
+
+```bash
+dotnet test $PROJECT_PATH
+```
+
+**Handle failures:**
+- **Flaky tests:** Rerun to confirm
+- **Broken tests:** Fix before proceeding
+- **All tests MUST pass** before moving to step 3
+
+## Step 3: Check for Code Duplication
+
+```bash
+quickdup -path $SOURCE_DIR -ext .cs -select 0..20 -min 2 -exclude ".g.,.generated."
+```
+
+**If new duplications are found:**
+1. Refactor to eliminate duplications (see `/quickdup` for refactoring patterns)
+2. Go back to Step 1 (roslynator fix)
+3. Repeat until no new duplications
+
+## Step 4: Format Code
+
+```bash
+dotnet format $PROJECT_PATH
+```
+
+## Step 5: Final Verification
+
+Confirm all checks passed:
+- [ ] Roslynator fix applied
+- [ ] Code compiles cleanly
+- [ ] All tests pass
+- [ ] No new code duplications
+- [ ] Code formatted
+
+## Step 6: Create PR
+
+Only after ALL checks pass, create the pull request.
+
+## Important
+
+- Do NOT skip any steps
+- Do NOT create PR if any step fails
+- Steps are ordered intentionally — roslynator before build, build before test, duplication check may loop back to step 1
+- Iterate until all checks pass
diff --git a/.codex/skills/profile/SKILL.md b/.codex/skills/profile/SKILL.md
new file mode 100644
index 000000000..eb05d60cc
--- /dev/null
+++ b/.codex/skills/profile/SKILL.md
@@ -0,0 +1,392 @@
+---
+name: profile
+description: Profile .NET applications for CPU performance, memory allocations, lock contention, exceptions, heap analysis, and JIT inlining. Use when the user asks about performance bottlenecks, memory leaks, high CPU, slow code, lock contention, excessive exceptions, GC pressure, heap growth, or JIT compilation in .NET projects.
+argument-hint: "[--cpu|--memory|--contention|--exception|--heap] [path]"
+---
+
+## Prerequisites
+
+This tool requires .NET 10+ SDK (for `dnx` support).
+
+Check if `dnx` is available:
+```
+dnx --help
+```
+
+The profiler also depends on `dotnet-trace` and `dotnet-gcdump`. Install them if missing:
+```
+dotnet tool install -g dotnet-trace
+dotnet tool install -g dotnet-gcdump
+```
+
+## About Asynkron.Profiler
+
+A CLI profiler for .NET that outputs structured text — no GUI needed. Designed for both human inspection and AI-assisted analysis. It wraps `dotnet-trace` and `dotnet-gcdump` and presents call trees, hot functions, allocation tables, and contention rankings as plain text.
+
+Results are written to `profile-output/` in the current directory.
+
+## Running via dnx (no install needed)
+
+```
+dnx asynkron-profiler [flags] -- [target]
+```
+
+On first run, `dnx` will prompt to download the package.
+
+## Profiling Modes
+
+### CPU Profiling (`--cpu`)
+Sampled CPU profiling. Shows call trees and hot function tables.
+```
+dnx asynkron-profiler --cpu -- ./MyApp.csproj
+dnx asynkron-profiler --cpu -- ./bin/Release/net10.0/MyApp
+```
+
+### Memory Allocation Profiling (`--memory`)
+Tracks GC allocation tick events. Shows per-type allocation call trees and allocation sources.
+```
+dnx asynkron-profiler --memory -- ./MyApp.csproj
+```
+
+### Lock Contention Profiling (`--contention`)
+Shows wait-time call trees and contended method rankings. Use for diagnosing lock congestion and thread starvation.
+```
+dnx asynkron-profiler --contention -- ./MyApp.csproj
+```
+
+### Exception Profiling (`--exception`)
+Counts thrown exceptions, shows throw-site call trees. Filter by type with `--exception-type`.
+```
+dnx asynkron-profiler --exception -- ./MyApp.csproj
+dnx asynkron-profiler --exception --exception-type InvalidOperationException -- ./MyApp.csproj
+```
+
+### Heap Snapshot (`--heap`)
+Takes a GC heap snapshot using `dotnet-gcdump`. Shows retained objects by type and size.
+```
+dnx asynkron-profiler --heap -- ./MyApp.csproj
+```
+
+### JIT / Inlining Analysis
+The profiler can capture JIT-to-native compilation events, showing what methods get JIT compiled and which calls get inlined. This is useful for understanding runtime code generation and verifying that hot paths are being optimized by the JIT.
+
+## Analyzing Existing Traces
+
+You can analyze previously captured trace files without re-running the app:
+```
+dnx asynkron-profiler --input /path/to/trace.nettrace
+dnx asynkron-profiler --input /path/to/trace.speedscope.json --cpu
+dnx asynkron-profiler --input /path/to/heap.gcdump --heap
+```
+
+## Key Flags
+
+| Flag | Purpose |
+|------|---------|
+| `--cpu` | CPU profiling |
+| `--memory` | Memory allocation profiling |
+| `--contention` | Lock contention analysis |
+| `--exception` | Exception profiling |
+| `--heap` | Heap snapshot |
+| `--root ` | Root call tree at first matching method |
+| `--filter ` | Filter function tables by substring |
+| `--exception-type ` | Filter exceptions by type name |
+| `--calltree-depth ` | Max call tree depth (default: 30) |
+| `--calltree-width ` | Max children per node (default: 4) |
+| `--calltree-self` | Include self-time tree |
+| `--calltree-sibling-cutoff ` | Hide siblings below X% (default: 5) |
+| `--include-runtime` | Include runtime/framework frames |
+| `--input ` | Analyze existing trace file |
+| `--tfm ` | Target framework for .csproj/.sln |
+
+## Supported Input Formats
+
+| Mode | Formats |
+|------|---------|
+| CPU | `.speedscope.json`, `.nettrace` |
+| Memory | `.nettrace`, `.etlx` |
+| Exceptions | `.nettrace`, `.etlx` |
+| Contention | `.nettrace`, `.etlx` |
+| Heap | `.gcdump` |
+
+---
+
+## Profiling Methodology
+
+Profiling is iterative. Follow this workflow to systematically identify and eliminate bottlenecks.
+
+### Step 1: Build Release First
+
+Always build Release before profiling. Debug builds have disabled optimizations, extra checks, and no inlining — profiling them gives misleading results.
+
+```
+dotnet build -c Release
+```
+
+### Step 2: Choose the Right Mode
+
+| Symptom | Start with |
+|---------|------------|
+| High CPU / slow execution | `--cpu` |
+| High memory / GC pressure | `--memory` |
+| High latency but low CPU | `--contention` |
+| Too many exceptions in logs | `--exception` |
+| Memory keeps growing (leak) | `--heap` |
+| Want to verify JIT optimization | JIT/inlining analysis |
+
+### Step 3: Read the Hot Function Table
+
+The profiler outputs a hot function table showing where time or allocations are concentrated:
+
+```
+=== HOT FUNCTIONS ===
+ Time (ms) Calls Function
+-------------------------------------------------
+ 38805.39 19533 MyApp.Core.ProcessItem...
+ 19769.23 9897 MyApp.Core.TransformData...
+```
+
+Focus on the top 3-5 entries. These are your optimization targets.
+
+### Step 4: Read the Allocation Call Graph
+
+For memory profiling, the allocation call graph shows *where* allocations originate:
+
+```
+CreateEnvironment
+ Calls: 1048
+ Allocated by:
+ <- ProcessLoop (1048x, 100%)
+ <- RunMain (4x)
+```
+
+This traces allocations back to their source — the method that triggered them, not just where `new` was called.
+
+### Step 5: Iterate
+
+1. Profile to find the top bottleneck
+2. Fix it (optimize hot path, reduce allocations, remove contention)
+3. Profile again to measure improvement
+4. Repeat until performance targets are met
+
+Track progress across rounds:
+```
+Round 1: 322 MB, 172 ms
+Round 2: 173 MB, 150 ms (pooling)
+Round 3: 107 MB, 116 ms (fast paths)
+```
+
+### Step 6: Use Manual Tracing for Deep Dives
+
+When the profiler summary isn't enough, capture a detailed trace for manual analysis:
+
+```bash
+# Capture detailed GC trace
+dotnet-trace collect \
+ --profile gc-verbose \
+ --format NetTrace \
+ -o trace.nettrace \
+ -- dotnet run -c Release --project ./MyApp
+
+# Analyze with the profiler
+dnx asynkron-profiler --input trace.nettrace --memory
+
+# Or convert for external tools
+dotnet-trace convert trace.nettrace --format Speedscope
+```
+
+### Common Optimization Patterns
+
+**Reduce allocations in hot loops:**
+- Use object pooling for frequently created/disposed objects
+- Use `Span` / `stackalloc` for short-lived buffers
+- Avoid boxing value types (use generic overloads)
+
+**Reduce CPU in hot paths:**
+- Use `[MethodImpl(MethodImplOptions.AggressiveInlining)]` on small hot methods
+- Split into fast path (inlined, common case) and slow path (`NoInlining`, rare case)
+- Cache computed values instead of recomputing
+
+**Reduce contention:**
+- Use lock-free patterns (`Interlocked`, `ConcurrentDictionary`)
+- Reduce lock scope — hold locks for the shortest time possible
+- Use `ReaderWriterLockSlim` for read-heavy workloads
+
+**Reduce exceptions:**
+- Use `TryParse` / `TryGet` patterns instead of catching exceptions
+- Exceptions are expensive — never use them for control flow
+
+---
+
+## JIT-Aware Optimization Patterns
+
+These patterns work *with* the .NET JIT compiler to produce faster native code. Use the profiler's JIT/inlining analysis to verify these optimizations take effect.
+
+### Fast/Slow Path Split
+
+The most impactful pattern for hot methods. The JIT inlines small methods into their callers, eliminating call overhead and enabling further optimizations. But it won't inline large methods. The trick: keep the common case tiny and inlineable, push the rare case into a separate non-inlined method.
+
+```csharp
+[MethodImpl(MethodImplOptions.AggressiveInlining)]
+private static Result HandleHotPath(Data data)
+{
+ // Fast path: ~20-30 lines max, handles the common case
+ if (data.IsSimpleCase)
+ {
+ // Direct, minimal work
+ return Result.From(data.Value);
+ }
+
+ // Rare/complex case — delegate to non-inlined method
+ return HandleHotPathSlow(data);
+}
+
+[MethodImpl(MethodImplOptions.NoInlining)]
+private static Result HandleHotPathSlow(Data data)
+{
+ // Complex logic: type coercion, error handling, edge cases
+ // This can be as large as needed — it won't bloat the call site
+}
+```
+
+**Why this works:**
+- The JIT inlines the fast path directly into the hot loop
+- The slow path stays as a regular call — it doesn't bloat the inlined code
+- CPU branch prediction favors the fast path since it's the common case
+- Instruction cache stays hot because the loop body is small
+
+**When to apply:**
+- Method shows up in profiler hot function table
+- Method has a common fast case and rare complex case
+- The fast case is under ~30 lines
+- The method is called in a tight loop
+
+**How to verify:** Use the profiler's JIT/inlining analysis to confirm the fast path is being inlined at the call site.
+
+### Dispatch Tables vs Switch Statements
+
+For hot dispatch (e.g., instruction interpreters, message handlers, event processors), a delegate array indexed by enum is faster than a switch:
+
+```csharp
+// Define handler signature
+delegate Result Handler(Context ctx, Instruction instr);
+
+// Build dispatch table once (static constructor)
+private static readonly Handler[] _dispatch = new Handler[64];
+
+static MyRunner()
+{
+ _dispatch[(int)Kind.Add] = HandleAdd;
+ _dispatch[(int)Kind.Call] = HandleCall;
+ _dispatch[(int)Kind.Branch] = HandleBranch;
+ // ...
+}
+
+// Hot loop — direct delegate invocation, no switch overhead
+while (running)
+{
+ var instr = instructions[pc];
+ var result = _dispatch[(int)instr.Kind](ctx, instr);
+ // ...
+}
+```
+
+**Why this works:**
+- Direct indexed lookup — O(1), no comparisons
+- Each handler can have its own inlining attributes
+- Scales better than switch as the number of cases grows
+
+**When to apply:**
+- Dispatch over an enum with 10+ cases
+- Called in a tight loop (interpreter, event loop, message pump)
+- Switch shows up in profiler as a hot function
+
+### Object Pooling for Hot Allocations
+
+When the profiler shows a type being allocated millions of times in a loop, pool it instead.
+
+```csharp
+// 1. Define what poolable objects look like
+interface IRentable
+{
+ void Activate(); // Called when rented — initialize state
+ void Reset(); // Called when returned — clear state for reuse
+}
+
+// 2. Lock-free pool using Interlocked.CompareExchange
+class ObjectPool where T : class, IRentable
+{
+ private readonly T?[] _items;
+ private readonly Func _factory;
+
+ public T Rent()
+ {
+ for (int i = 0; i < _items.Length; i++)
+ {
+ var item = Interlocked.Exchange(ref _items[i], null);
+ if (item is not null) { item.Activate(); return item; }
+ }
+ var created = _factory();
+ created.Activate();
+ return created;
+ }
+
+ public void Return(T item)
+ {
+ item.Reset();
+ for (int i = 0; i < _items.Length; i++)
+ {
+ if (Interlocked.CompareExchange(ref _items[i], item, null) == null)
+ return;
+ }
+ // Pool full — abandon to GC (graceful degradation)
+ }
+}
+
+// 3. RAII wrapper ensures objects are returned
+using var handle = pool.Rent();
+var obj = handle.Value;
+// ... use obj ...
+// Automatically returned on dispose
+```
+
+**Impact:** A tight loop creating 1M scoped objects goes from 1M allocations to ~32 (pool size). Dramatically reduces GC pressure.
+
+**When to apply:**
+- A type shows up in the memory profiler's top allocations
+- It's created and disposed in a tight loop
+- Its lifetime is short and predictable
+- The type can be cleanly reset for reuse
+
+### Thread-Safe Lazy Initialization
+
+For cached computed values that are expensive to create but read frequently:
+
+```csharp
+static TCache GetOrCreate(ref TCache? field, Func factory)
+ where TCache : class
+{
+ var existing = Volatile.Read(ref field);
+ if (existing is not null) return existing;
+
+ var created = factory();
+ var prior = Interlocked.CompareExchange(ref field, created, null);
+ return prior ?? created;
+}
+```
+
+**Why not `Lazy`:** This pattern avoids the `Lazy` allocation itself, and the `Volatile.Read` fast path is a single instruction on x86/ARM. The worst case (two threads create simultaneously) wastes one creation but is still correct — no locks needed.
+
+---
+
+## Guidelines
+
+- Always build Release first (`dotnet build -c Release`) before profiling — profiling Debug builds gives misleading results
+- Profile the compiled binary directly rather than via `dotnet run` for accurate measurements
+- Use `--root` to focus on a specific call path when the tree is too broad
+- Use `--filter` to narrow function tables to your own code, excluding framework noise
+- Use `--calltree-sibling-cutoff` to hide insignificant branches
+- For memory issues, start with `--memory` to find allocation sources, then `--heap` for retained object analysis
+- For performance, start with `--cpu`, then drill into contention if CPU usage is low but latency is high
+- For exception-heavy apps, use `--exception` with `--exception-type` to focus on specific exception categories
+- Profile iteratively — fix one bottleneck at a time and measure the improvement before moving on
diff --git a/.codex/skills/quickdup/SKILL.md b/.codex/skills/quickdup/SKILL.md
new file mode 100644
index 000000000..e23a1dfef
--- /dev/null
+++ b/.codex/skills/quickdup/SKILL.md
@@ -0,0 +1,236 @@
+---
+name: quickdup
+description: Find and reduce code duplication, clean up redundant code, detect code clones, reduce codebase size, DRY violations, copy-paste detection. Use when the user asks about duplicate code, code cleanup, reducing code size, DRY principles, or finding copy-pasted code.
+argument-hint: "[path or extension]"
+---
+
+## Prerequisites
+
+Before running quickdup, check if it is installed:
+
+```
+which quickdup
+```
+
+If not found, install it:
+- macOS/Linux: `curl -sSL https://raw.githubusercontent.com/asynkron/Asynkron.QuickDup/main/install.sh | bash`
+- Windows: `iwr -useb https://raw.githubusercontent.com/asynkron/Asynkron.QuickDup/main/install.ps1 | iex`
+- From source: `go install github.com/asynkron/Asynkron.QuickDup/cmd/quickdup@latest`
+
+## About QuickDup
+
+QuickDup is a fast structural code clone detector (~100k lines in 500ms). It uses indent-delta fingerprinting to find duplicate code patterns. It is designed as a candidate generator — it optimizes for speed and recall, surfacing candidates fast for AI-assisted review.
+
+Results are written to `.quickdup/results.json` and cached in `.quickdup/cache.gob` for fast incremental re-runs.
+
+## Workflow
+
+1. **Run quickdup** to find duplication candidates
+2. **Review the results** with `-select` to inspect specific patterns
+3. **Classify each pattern** — determine which type of duplication it is (see below)
+4. **Refactor using the right pattern** — not all duplication should be removed the same way
+5. **Re-run** to verify duplication is reduced
+
+## Determine File Extension
+
+Before running, determine the primary language/extension of the project. Look at the files in the target path and use the appropriate `-ext` flag (e.g. `.go`, `.ts`, `.cs`, `.py`, `.java`, `.rs`).
+
+## Common Usage
+
+**Scan current project:**
+```
+quickdup -path . -ext .go
+```
+
+**Scan with specific extension (adapt to project language):**
+```
+quickdup -path $ARGUMENTS -ext .ts
+```
+
+**Show top 20 patterns:**
+```
+quickdup -path . -ext .go -top 20
+```
+
+**Inspect specific patterns in detail:**
+```
+quickdup -path . -ext .go -select 0..5
+```
+
+**Strict similarity (fewer false positives):**
+```
+quickdup -path . -ext .go -min-similarity 0.9
+```
+
+**Require more occurrences:**
+```
+quickdup -path . -ext .go -min 5
+```
+
+**Exclude generated files:**
+```
+quickdup -path . -ext .go -exclude "*.pb.go,*_gen.go,*_test.go"
+```
+
+**Compare duplication between branches:**
+```
+quickdup -path . -ext .go -compare origin/main..HEAD
+```
+
+## Key Flags
+
+| Flag | Default | Purpose |
+|------|---------|---------|
+| `-path` | `.` | Directory to scan |
+| `-ext` | `.go` | File extension to match |
+| `-min` | `2` | Minimum occurrences to report |
+| `-min-size` | `3` | Minimum pattern size (lines) |
+| `-max-size` | `0` | Max pattern size (0 = unlimited) |
+| `-min-score` | `5` | Minimum score threshold |
+| `-min-similarity` | `0.75` | Token similarity threshold (0.0-1.0) |
+| `-top` | `10` | Show top N patterns |
+| `-select` | — | Detailed view (e.g. `0..5`) |
+| `-exclude` | — | Exclude file globs (comma-separated) |
+| `-no-cache` | `false` | Force full re-parse |
+| `-strategy` | `normalized-indent` | Detection strategy |
+| `-compare` | — | Compare between commits (`base..head`) |
+
+## Detection Strategies
+
+- **normalized-indent** (default) — indent deltas + first word per line
+- **word-indent** — raw indentation + first word
+- **word-only** — ignores indentation, first words only
+- **inlineable** — detects small patterns suitable for inline extraction
+
+## Suppressing Known Patterns
+
+If a pattern is intentional duplication, suppress it by adding its hash to `.quickdup/ignore.json`:
+
+```json
+{
+ "description": "Patterns to ignore",
+ "ignored": ["56c2f5f9b27ed5a0"]
+}
+```
+
+---
+
+## How to Refactor Duplicated Code
+
+QuickDup finds duplication candidates. The next step is knowing *how* to fix them. Not all duplication is the same — each type has a different refactoring strategy.
+
+### Type 1: Structural Duplication with Contextual Variation
+
+Two blocks share the same control flow and structure, but differ in injected behavior (which function is called, which parameters are passed, whether some context like an index exists).
+
+**How to recognize it:**
+- Same loops, same branching, same method calls in the same order
+- Only the behavior inside the structure differs
+
+**How to refactor:**
+
+1. **Ignore the differences** — mentally replace the differing lines with placeholders. If the code still "reads the same", you have structural duplication.
+2. **Identify the invariant structure** — same loops, branching, method calls in the same order. That is the structure to extract.
+3. **Identify the varying behavior** — what methods differ? What parameters differ? What context exists in one version but not the other? These become delegates, lambdas, or strategy objects.
+4. **Extract, do not merge** — do NOT write `if (variant) { ... } else { ... }`. Instead, extract the shared structure and pass behavior in.
+5. **Preserve meaning at call sites** — the call site should still clearly express intent.
+
+Good:
+```
+IterateAndResolve(array, i => CreateResolve(i), ...)
+```
+
+Bad:
+```
+IterateAndResolve(array, withIndex: true)
+```
+
+**Rule of thumb:** If two methods differ only in what they do inside a shared structure, extract the structure. If they differ in structure, keep them separate.
+
+### Type 2: Structural Switch Duplication
+
+Repeated switch/pattern match blocks on the same domain structures. Common in AST walkers, serializers, interpreters, and compilers.
+
+**How to recognize it:**
+- Repeated `switch` or pattern matching blocks
+- Short, trivial case bodies
+- Each case extracts or forwards structure
+- No real algorithm, just classification
+
+**When to leave it alone:**
+- Each case is one or two lines
+- The code is stable and unlikely to drift
+- The abstraction would just wrap a switch
+- The switch *documents the domain shape* — removing it hides meaning
+
+**When to refactor:**
+- Same switch appears 3+ times
+- Case list starts diverging between copies
+- Case logic grows beyond trivial access
+
+**How to refactor:**
+- Extract the *smallest possible* helper
+- Return data, do not hide control flow
+- Use pattern matching, not flags
+
+Good: `Statement? TryUnwrapBody(Statement s)`
+Bad: `HandleStatement(s, mode)`
+
+**Rule of thumb:** If the switch *describes structure*, duplication is documentation. If the switch *implements behavior*, consider extraction.
+
+### Type 3: Parameter Bundle Duplication
+
+The same set of context values passed together in multiple places — same parameters, same order, same meaning.
+
+**How to recognize it:**
+- Same group of 4+ parameters passed together repeatedly
+- Usually forwarded to constructors or methods
+- Reads like a "context snapshot"
+- Adding a new parameter would require editing many call sites
+
+**How to refactor:**
+
+Extract a value object:
+```
+new ExecutionContext(thisValue, realmState, isStrict, homeObject, privateScope)
+```
+
+Then pass the single object instead of 5+ individual arguments.
+
+**When to leave it:** Appears only once, is highly localized, or has extremely short lifetime.
+
+**Rule of thumb:** If the same parameter list appears 2-3+ times, it wants to become a named type.
+
+### Type 4: Argument Unpacking Duplication
+
+Duplicated defensive parsing of callback arguments — same local variables, same guards, same positional decoding.
+
+**How to recognize it:**
+- Same argument index checks (`args.Count >= 1`, `args[0]`)
+- Same type unwrapping logic
+- Same local variable names
+- Appears inside lambdas, which amplifies the noise
+
+**How to refactor:**
+
+Extract a small helper whose only job is to unpack the arguments:
+```
+UnwrapResolveReject(args, out var resolve, out var reject);
+```
+
+This is purely mechanical code — the intent is obscured by boilerplate, and any bug fix would need to be applied to every copy.
+
+**Rule of thumb:** If you copy the same argument decoding logic twice, extract it. If the extraction reads like English, you picked the right abstraction.
+
+---
+
+## Guidelines
+
+- Always determine the correct `-ext` for the project before running
+- Start with defaults, then tighten `-min-similarity` if too many false positives
+- Use `-select 0..5` to inspect the top candidates before refactoring
+- After identifying duplicates, classify them by type before refactoring
+- Not all duplication should be removed — structural switch duplication is often intentional documentation
+- After refactoring, re-run to confirm duplication is reduced
+- For large projects, quickdup caches results — subsequent runs are near-instant
+- When the user wants to clean up or reduce code size, run quickdup first to identify targets, then refactor the highest-scoring patterns using the appropriate strategy for each type
diff --git a/.codex/skills/roslynator/SKILL.md b/.codex/skills/roslynator/SKILL.md
new file mode 100644
index 000000000..da992b25c
--- /dev/null
+++ b/.codex/skills/roslynator/SKILL.md
@@ -0,0 +1,161 @@
+---
+name: roslynator
+description: Run C# static analysis, auto-fix code issues, format code, find unused code, and enforce coding standards in .NET projects. Use when the user asks about code quality, linting, static analysis, code cleanup, unused code, or formatting in C# / .NET projects.
+argument-hint: "[solution or project path]"
+---
+
+## Prerequisites
+
+Check if roslynator is installed:
+
+```
+which roslynator
+```
+
+If not found, try running via `dnx` (no install needed, .NET 10+):
+
+```
+dnx Roslynator.DotNet.Cli [command] [options]
+```
+
+Or install globally:
+
+```
+dotnet tool install -g roslynator.dotnet.cli
+```
+
+## About Roslynator
+
+Roslynator is a set of code analysis tools for C# powered by Roslyn. It provides 500+ analyzers, refactorings, and code fixes. The CLI tool can analyze, fix, and format entire solutions from the command line — no IDE needed.
+
+The CLI itself contains no analyzers — they come from NuGet packages referenced in your project (e.g. `Roslynator.Analyzers`) or via `--analyzer-assemblies`.
+
+## Common Usage
+
+**Fix all diagnostics in a solution:**
+```
+roslynator fix MySolution.sln
+```
+
+**Fix all diagnostics in a project:**
+```
+roslynator fix MyProject.csproj
+```
+
+**Analyze without fixing (report only):**
+```
+roslynator analyze MySolution.sln
+```
+
+**Format code:**
+```
+roslynator format MySolution.sln
+```
+
+**Find unused code (dead code detection):**
+```
+roslynator find-unused MySolution.sln
+```
+
+**List symbols:**
+```
+roslynator list-symbols MySolution.sln
+```
+
+**Count lines of code:**
+```
+roslynator lloc MySolution.sln
+```
+
+## Commands
+
+| Command | Purpose |
+|---------|---------|
+| `fix` | Auto-fix diagnostics in project/solution |
+| `analyze` | Report diagnostics without fixing |
+| `format` | Format whitespace |
+| `find-unused` | Find unused declarations (dead code) |
+| `list-symbols` | List types, members, and symbols |
+| `lloc` | Count logical lines of code |
+| `loc` | Count physical lines of code |
+| `spellcheck` | Check spelling in comments and strings |
+
+## Key Flags (shared across commands)
+
+| Flag | Purpose |
+|------|---------|
+| `--projects ` | Only process named projects |
+| `--ignored-projects ` | Skip specific projects |
+| `--include ` | Include matching files/folders |
+| `--exclude ` | Exclude matching files/folders |
+| `-v, --verbosity ` | Output level: quiet, minimal, normal, detailed, diagnostic |
+| `-p, --properties ` | MSBuild properties (e.g. `Configuration=Release`) |
+| `-m, --msbuild-path ` | Path to MSBuild directory |
+| `--language ` | Language: `cs` or `vb` |
+| `-g, --include-generated-code` | Include generated code |
+| `--file-log ` | Write output to file |
+| `-h, --help` | Show help |
+
+## Analyze-specific Flags
+
+| Flag | Purpose |
+|------|---------|
+| `-a, --analyzer-assemblies ` | Paths to additional analyzer assemblies |
+| `--supported-diagnostics ` | Report only these diagnostic IDs |
+| `--ignored-diagnostics ` | Skip these diagnostic IDs |
+| `--severity-level ` | Minimum severity: hidden, info, warning, error |
+| `--ignore-compiler-diagnostics` | Hide compiler messages |
+| `-o, --output ` | Save diagnostics to file |
+| `--output-format` | Report format: xml or gitlab |
+| `--execution-time` | Measure analyzer performance |
+
+## Configuration
+
+Roslynator is configured via `.editorconfig` in your project:
+
+**Set severity for all Roslynator analyzers:**
+```ini
+[*.cs]
+dotnet_analyzer_diagnostic.category-roslynator.severity = warning
+```
+
+**Enable/disable specific analyzers:**
+```ini
+[*.cs]
+dotnet_diagnostic.RCS1001.severity = none # Disable specific rule
+dotnet_diagnostic.RCS1036.severity = error # Upgrade to error
+```
+
+**Enable/disable refactorings:**
+```ini
+[*.cs]
+roslynator_refactoring.add_braces.enabled = false
+```
+
+## Exit Codes
+
+| Code | Meaning |
+|------|---------|
+| `0` | Success — no diagnostics found or all fixed |
+| `1` | Diagnostics found or not all fixed |
+| `2` | Error or execution canceled |
+
+## Workflow with pre-pr
+
+Roslynator is used in the `/pre-pr` quality gate as the first step:
+
+1. `roslynator fix` — auto-fix code issues
+2. `dotnet build` — verify compilation
+3. `dotnet test` — run tests
+4. `quickdup` — check for duplication
+5. `dotnet format` — format code
+
+## Guidelines
+
+- Always build the solution before running `roslynator analyze` — it needs compiled output
+- Use `roslynator fix` for auto-fixing, `roslynator analyze` for CI reporting
+- Use `--severity-level warning` to skip info/hidden diagnostics in CI
+- Use `--ignored-diagnostics` to suppress known false positives
+- Use `roslynator find-unused` periodically to catch dead code
+- Configure rules in `.editorconfig` rather than via CLI flags for consistency across team
+- When the user mentions "lint", "static analysis", or "code quality" in a .NET context, this is the tool to use
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
deleted file mode 100644
index e13e299d6..000000000
--- a/.devcontainer/devcontainer.json
+++ /dev/null
@@ -1,32 +0,0 @@
-// For format details, see https://aka.ms/devcontainer.json. For config options, see the
-// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile
-{
- "name": "Existing Dockerfile",
- "build": {
- // Sets the run context to one level up instead of the .devcontainer folder.
- "context": "..",
- // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
- "dockerfile": "../Dockerfile",
- "args": {
- "INSTALL_GIT": "true"
- }
- },
-
- // Features to add to the dev container. More info: https://containers.dev/features.
- // "features": {},
- "features": {
- "ghcr.io/devcontainers-extra/features/hatch:2": {}
- },
-
- // Use 'forwardPorts' to make a list of ports inside the container available locally.
- // "forwardPorts": [],
-
- // Uncomment the next line to run commands after the container is created.
- // "postCreateCommand": "cat /etc/os-release",
-
- // Configure tool-specific properties.
- // "customizations": {},
-
- // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
- "remoteUser": "root"
-}
diff --git a/.dockerignore b/.dockerignore
deleted file mode 100644
index 319b9327a..000000000
--- a/.dockerignore
+++ /dev/null
@@ -1,2 +0,0 @@
-*
-!packages/
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 000000000..f9db92d07
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,327 @@
+# GitHub Copilot Instructions for MarkItDown C# .NET Project
+
+## Project Overview
+
+MarkItDown is a C# .NET 8 library for converting various document formats (HTML, PDF, DOCX, XLSX, etc.) into clean Markdown suitable for Large Language Models (LLMs) and text analysis pipelines. This project is a conversion from the original Python implementation to C# while maintaining API compatibility and adding modern async/await patterns.
+
+## Architecture and Design Principles
+
+### Core Components
+
+```
+src/
+├── MarkItDown.Core/ # Main library project
+│ ├── IDocumentConverter.cs # Converter interface
+│ ├── MarkItDown.cs # Main orchestration class
+│ ├── StreamInfo.cs # File metadata handling
+│ ├── DocumentConverterResult.cs # Conversion results
+│ ├── Exceptions/ # Exception hierarchy
+│ └── Converters/ # Format-specific converters
+├── MarkItDown.Cli/ # Command line tool
+tests/
+└── MarkItDown.Tests/ # Unit tests with xUnit
+```
+
+### Key Design Patterns
+
+1. **Interface-Based Architecture**: All converters implement `IDocumentConverter`
+2. **Async/Await Throughout**: Modern C# async patterns for I/O operations
+3. **Priority-Based Registration**: Converters are ordered by priority for format detection
+4. **Stream-Based Processing**: Avoid temporary files, work with streams
+5. **Comprehensive Error Handling**: Specific exception types for different failure modes
+
+## Code Quality Standards
+
+### C# Coding Conventions
+
+- **Target Framework**: .NET 8.0 (net8.0)
+- **Language Version**: C# 12
+- **Nullable Reference Types**: Enabled
+- **Async Patterns**: Use async/await, ConfigureAwait(false) for library code
+- **Exception Handling**: Specific exception types, never swallow exceptions
+
+### Naming Conventions
+
+- **Classes**: PascalCase (`DocumentConverter`, `StreamInfo`)
+- **Methods**: PascalCase (`ConvertAsync`, `AcceptsInput`)
+- **Properties**: PascalCase (`Markdown`, `MimeType`)
+- **Fields**: _camelCase with underscore prefix (`_logger`, _converters`)
+- **Constants**: PascalCase (`DefaultPriority`)
+- **Interfaces**: IPascalCase (`IDocumentConverter`)
+
+### Method Signatures
+
+```csharp
+// Async methods should always return Task or Task
+public async Task ConvertAsync(
+ Stream stream,
+ StreamInfo streamInfo,
+ CancellationToken cancellationToken = default)
+
+// Interface implementations should be explicit about async
+bool AcceptsInput(StreamInfo streamInfo);
+```
+
+### Error Handling Patterns
+
+```csharp
+// Custom exceptions for specific failure modes
+public class UnsupportedFormatException : MarkItDownException
+{
+ public UnsupportedFormatException(string format)
+ : base($"Unsupported format: {format}") { }
+}
+
+// Proper async exception handling
+try
+{
+ var result = await converter.ConvertAsync(stream, info, cancellationToken);
+ return result;
+}
+catch (UnsupportedFormatException)
+{
+ throw; // Re-throw specific exceptions
+}
+catch (Exception ex)
+{
+ throw new MarkItDownException("Conversion failed", ex);
+}
+```
+
+### Testing Standards
+
+- **Framework**: xUnit with standard assertions
+- **Async Testing**: Proper async test methods
+- **Test Naming**: `MethodName_Scenario_ExpectedResult`
+- **Coverage**: All public APIs must have tests
+- **Edge Cases**: Test null inputs, empty streams, invalid data
+
+```csharp
+[Fact]
+public async Task ConvertAsync_ValidHtml_ReturnsCorrectMarkdown()
+{
+ // Arrange
+ var converter = new HtmlConverter();
+ var html = "Test
Content
";
+ var bytes = Encoding.UTF8.GetBytes(html);
+ using var stream = new MemoryStream(bytes);
+ var streamInfo = new StreamInfo(mimeType: "text/html");
+
+ // Act
+ var result = await converter.ConvertAsync(stream, streamInfo);
+
+ // Assert
+ Assert.Contains("# Test", result.Markdown);
+ Assert.Contains("Content", result.Markdown);
+}
+```
+
+## Converter Implementation Guidelines
+
+### Creating New Converters
+
+1. **Inherit from Base**: Consider if a base converter class would help
+2. **Implement Interface**: All converters must implement `IDocumentConverter`
+3. **Priority Assignment**: Lower numbers = higher priority (HTML = 100, Plain Text = 1000)
+4. **Format Detection**: Be specific in `AcceptsInput` - check MIME type AND extension
+5. **Error Handling**: Wrap third-party exceptions in `MarkItDownException`
+
+### Standard Converter Structure
+
+```csharp
+public class YourFormatConverter : IDocumentConverter
+{
+ public int Priority => 200; // Between HTML(100) and PlainText(1000)
+
+ public bool AcceptsInput(StreamInfo streamInfo)
+ {
+ return streamInfo.MimeType?.StartsWith("application/your-format") == true ||
+ streamInfo.Extension?.ToLowerInvariant() == ".your-ext";
+ }
+
+ public async Task ConvertAsync(
+ Stream stream,
+ StreamInfo streamInfo,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ // Reset stream position
+ if (stream.CanSeek)
+ stream.Position = 0;
+
+ // Your conversion logic here
+ var markdown = await ConvertToMarkdownAsync(stream, cancellationToken);
+
+ return new DocumentConverterResult(
+ markdown: markdown,
+ title: ExtractTitle(markdown) // Optional
+ );
+ }
+ catch (Exception ex) when (!(ex is MarkItDownException))
+ {
+ throw new MarkItDownException($"Failed to convert {streamInfo.Extension} file", ex);
+ }
+ }
+}
+```
+
+## Package Management and Dependencies
+
+### NuGet Package References
+
+- **Core Dependencies**: Keep minimal - only what's absolutely needed
+- **Version Pinning**: Use specific versions for reproducible builds
+- **License Compatibility**: Ensure all dependencies are MIT-compatible
+- **Security**: Regularly update packages for security fixes
+
+### Current Key Dependencies
+
+```xml
+
+
+
+```
+
+## Testing Philosophy
+
+### Test Coverage Requirements
+
+- **Every Public Method**: Must have at least basic functionality tests
+- **Error Conditions**: Test exception scenarios and edge cases
+- **Integration Tests**: Test the full MarkItDown workflow
+- **Format-Specific Tests**: Each converter needs comprehensive tests
+
+### Test Data Strategy
+
+```csharp
+// Use test data that mirrors the original Python test vectors
+public static class TestVectors
+{
+ public static readonly FileTestVector[] GeneralTestVectors = {
+ new FileTestVector(
+ filename: "test.html",
+ mimeType: "text/html",
+ mustInclude: new[] { "# Header", "**bold text**" },
+ mustNotInclude: new[] { "", "Run