diff --git a/README.md b/README.md
index 93a8c7a0..1031a3bc 100755
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@ Start with the automation: one Home Assistant webhook records each water softene
- You are here: `/` (root repo guide)
- [Blog](https://www.vcloudinfo.com) | [Issues](https://github.com/CCOSTAN/Home-AssistantConfig/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) | [Diagram](#network-diagram) | [YouTube](https://youtube.com/vCloudInfo)
- Config readmes: [Config index](config/README.md) | [Packages](config/packages/README.md) | [Automations](config/automation/README.md) | [Scripts](config/script/README.md) | [Scenes](config/scene/README.md) | [Sounds](config/sounds/README.md) | [Package triggers](config/packages/triggers/README.md)
-- Codex skills (optional): [codex_skills](codex_skills)
+- Codex skills (optional): [Home Assistant Codex Skills](https://github.com/CCOSTAN/Home-Assistant-Codex-Skills)

diff --git a/codex_skills/README.md b/codex_skills/README.md
deleted file mode 100644
index 3e438522..00000000
--- a/codex_skills/README.md
+++ /dev/null
@@ -1,53 +0,0 @@
-
-
-
- Bear Stone Smart Home Documentation
-
-Be sure to :star: my configuration repo so you can keep up to date on any daily progress!
-
-
-
-[](https://x.com/ccostan)
-[](https://www.youtube.com/vCloudInfo?sub_confirmation=1)
-[](https://github.com/CCOSTAN/Home-AssistantConfig/stargazers)
-[](https://github.com/CCOSTAN/Home-AssistantConfig/blob/master/config/.HA_VERSION)
-[](https://github.com/CCOSTAN/Home-AssistantConfig/commits/master)
-[](https://github.com/CCOSTAN/Home-AssistantConfig/commits/master)
-
-
-
-Codex skills stored in-repo so they can be shared with the community. These are documentation + helper scripts only (no secrets).
-
-### Walkthroughs
-- Dashboard design skill: [](https://youtu.be/aFis2YPeSuY)
-- Companion post: [](https://www.vcloudinfo.com/2026/02/home-assistant-dashboard-design-system-button-card.html)
-
-### Quick navigation
-- You are here: `codex_skills/`
-- [Repo overview](../README.md) | [Dashboards](../config/dashboards/README.md) | [Issues](https://github.com/CCOSTAN/Home-AssistantConfig/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc)
-
-## Skills
-
-- `homeassistant-dashboard-designer/`: Constrained, button-card-first Lovelace dashboard design system + YAML lint helper.
-- `homeassistant-yaml-dry-verifier/`: Home Assistant YAML DRY verifier to detect redundant triggers/conditions/actions/sequence blocks and suggest refactors.
-- `infrastructure-doc-sync/`: Session closeout workflow to update AGENTS/README/Dashy shortcuts/BearClaw infrastructure snapshot consistently after infra changes.
-- `network-architecture-diagrammer/`: Mermaid-first homelab/network architecture diagram workflow for Excalidraw-friendly topology and service maps.
-
-### Notes
-- Codex loads skills from your local Codex home. See each skill folder for install steps.
-
-**All of my configuration files are tested against the most stable version of home-assistant.**
-
-
-
-**Still have questions on my Config?**
-**Message me on X :** [](https://www.x.com/ccostan)
-
-
-
You can buy me a coffee
-
-
-
-Affiliate Disclosure
-
-
diff --git a/codex_skills/homeassistant-dashboard-designer/README.md b/codex_skills/homeassistant-dashboard-designer/README.md
deleted file mode 100644
index 69c62911..00000000
--- a/codex_skills/homeassistant-dashboard-designer/README.md
+++ /dev/null
@@ -1,124 +0,0 @@
-
-
-
- Bear Stone Smart Home Documentation
-
-Be sure to :star: my configuration repo so you can keep up to date on any daily progress!
-
-
-
-[](https://x.com/ccostan)
-[](https://www.youtube.com/vCloudInfo?sub_confirmation=1)
-[](https://github.com/CCOSTAN/Home-AssistantConfig/stargazers)
-[](https://github.com/CCOSTAN/Home-AssistantConfig/blob/master/config/.HA_VERSION)
-[](https://github.com/CCOSTAN/Home-AssistantConfig/commits/master)
-[](https://github.com/CCOSTAN/Home-AssistantConfig/commits/master)
-
-
-
-# Home Assistant Dashboard Designer (Codex Skill)
-
-This directory contains the `homeassistant-dashboard-designer` Codex skill, stored in-repo so it can be shared with the community.
-
-### Walkthrough
-- Video: [](https://youtu.be/aFis2YPeSuY)
-- Companion post: [](https://www.vcloudinfo.com/2026/02/home-assistant-dashboard-design-system-button-card.html)
-
-### Quick navigation
-- You are here: `codex_skills/homeassistant-dashboard-designer/`
-- [Repo overview](../../README.md) | [Codex skills](../README.md) | [Dashboards](../../config/dashboards/README.md) | [Issues](https://github.com/CCOSTAN/Home-AssistantConfig/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc)
-
-## What This Skill Does
-
-- Enforces a constrained Lovelace design system (button-card first, minimal card-mod, grid/vertical-stack layout rules).
-- Encourages centralized templates and deterministic YAML output.
-- Treats Stitch MCP output as inspiration only and translates ideas into safe Lovelace YAML.
-
-Skill source of truth inside this folder:
-- `SKILL.md`
-- `agents/openai.yaml`
-- `references/dashboard_rules.md`
-- `scripts/validate_lovelace_view.py`
-
-## Install (Local Codex)
-
-Codex loads skills from your local Codex skills directory.
-
-1. Copy this folder into your Codex skills directory as:
- - `~/.codex/skills/homeassistant-dashboard-designer/` (Linux/macOS)
- - `%USERPROFILE%\\.codex\\skills\\homeassistant-dashboard-designer\\` (Windows)
-2. Restart your Codex session/editor so it re-indexes skills.
-
-## Stitch MCP Install (Design Inspiration)
-
-Stitch is optional and used only for design inspiration. To enable it, add a Stitch MCP server entry to your Codex config.
-
-1. Set an environment variable with your API key:
- - `STITCH_API_KEY=`
-2. Add this to your `~/.codex/config.toml`:
-
-```toml
-[mcp_servers.stitch]
-url = "https://stitch.googleapis.com/mcp"
-env_http_headers = { "X-Goog-Api-Key" = "STITCH_API_KEY" }
-http_headers = { "Accept" = "application/json" }
-```
-
-## Usage
-
-Invoke in chat:
-- `$homeassistant-dashboard-designer`
-
-Then describe what you want in natural language (what to change + where + any constraints). The skill will infer the structured intent internally and enforce the button-card-first / layout constraints defined in `SKILL.md`.
-
-Workflow notes:
-- This skill uses direct updates for `config/dashboards/**` (no staged rollout workflow in-skill).
-- It requires post-edit validation in this order:
-- `pwsh -NoProfile -File tools/validate_dashboards.ps1`
-- `pwsh -NoProfile -File tools/ha_ui_smoke.ps1`
-- `python codex_skills/homeassistant-dashboard-designer/scripts/validate_lovelace_view.py `
-
-Examples:
-- "Refactor `config/dashboards/infrastructure/partials/mariadb_sections.yaml` to match the existing Infrastructure design language. Preserve existing templates and keep diffs small."
-- "Add a new Infrastructure view for Docker containers using the same layout rules as the other views (4 columns desktop / 2 columns mobile)."
-
-Optional:
-- If you already have an entity list, include it.
-- If you do not, enable the Home Assistant MCP so Codex can validate entity IDs/services against your live HA instance (recommended).
-
-## Home Assistant MCP (Built-In) Enablement
-
-The Home Assistant MCP lets Codex validate entity IDs and service calls against your *real* HA instance before it edits YAML.
-
-1. Create a Home Assistant Long-Lived Access Token (Profile page in HA).
-2. Set an environment variable (do not commit tokens):
- - `HOMEASSISTANT_MCP_AUTH=Bearer `
-3. Add this to your `~/.codex/config.toml`:
-
-```toml
-[mcp_servers.homeassistant]
-url = "http://:8123/api/mcp"
-env_http_headers = { "Authorization" = "HOMEASSISTANT_MCP_AUTH" }
-```
-
-Notes:
-- Use `https://` if your HA is behind TLS.
-- Keep tokens in environment variables, not in files under git.
-
-### Notes
-- This skill intentionally contains no secrets. Configure MCP credentials via environment variables in your local Codex setup.
-
-**All of my configuration files are tested against the most stable version of home-assistant.**
-
-
-
-**Still have questions on my Config?**
-**Message me on X :** [](https://www.x.com/ccostan)
-
-
-
You can buy me a coffee
-
-
-
-Affiliate Disclosure
-
diff --git a/codex_skills/homeassistant-dashboard-designer/SKILL.md b/codex_skills/homeassistant-dashboard-designer/SKILL.md
deleted file mode 100644
index 18ff9d11..00000000
--- a/codex_skills/homeassistant-dashboard-designer/SKILL.md
+++ /dev/null
@@ -1,221 +0,0 @@
----
-name: homeassistant-dashboard-designer
-description: "Design, update, and refactor Home Assistant Lovelace dashboards (YAML views/partials) with a constrained, machine-safe design system: button-card-first structure, minimal card-mod styling, optional flex-horseshoe + mini-graph telemetry, strict grid/vertical-stack layout rules, centralized templates, deterministic ordering, and config validation. Use for Home Assistant dashboard work (especially config/dashboards/**), when refactoring views, adding infra/home/energy/environment panels, or translating Stitch design inspiration into safe Lovelace YAML."
----
-
-# Home Assistant Dashboard Designer
-
-Design dashboards as systems: predictable structure, reusable templates, minimal drift. Treat Stitch as inspiration only; translate to safe Lovelace YAML.
-
-## Primary Input: Natural Language (Default)
-
-You can give natural-language guidance. The skill must infer the structured intent internally (dashboard intent, view name, entity mapping, constraints) while enforcing the design system and validating entities/services via the Home Assistant MCP when available.
-
-Minimum helpful info to include in natural language:
-- What to change (add/remove/refactor) and why (the goal).
-- Where to change it: the exact view/partial path(s) under `config/dashboards/**`.
-- Any constraints you care about: desktop/mobile columns, time window preferences for graphs, "do not touch" templates/sections.
-
-If any of the above is missing, ask targeted clarifying questions (do not demand a full "intent block").
-
-Example natural-language request:
-- "Refactor `config/dashboards/infrastructure/views/06_mariadb.yaml` to match the button-card-first system used in other Infrastructure views. Keep the same entities, no new templates, 4 columns desktop / 2 columns mobile."
-
-## Optional Input: Structured Intent (Allowed, Not Required)
-
-If the user provides structured intent, accept it. Do not require it.
-
-```yaml
-dashboard_type: infra # infra|home|energy|environment
-view_name: "Infrastructure Overview"
-target_paths:
- dashboard_yaml: "config/dashboards/infrastructure/dashboard.yaml"
- view_yaml: "config/dashboards/infrastructure/views/07_docker_containers.yaml"
-entities:
- - name: "Proxmox01"
- cpu: "sensor.proxmox01_cpu"
- memory: "sensor.proxmox01_mem"
-constraints:
- desktop_columns: 4
- mobile_columns: 2
-notes:
- - "Preserve existing templates; minimize diff."
-```
-
-If updating an existing view and not obvious from context, ask:
-
-- Which view file (single view dict) is the source of truth?
-- Any "don't touch" areas/templates?
-- Any fallback cards already in use that must remain?
-
-## Output Contract (Always Produce)
-
-- One valid Lovelace view (single view dict)
-- Deterministic ordering (stable ordering of cards/sections)
-- Section comments (short, consistent)
-- Zero per-card styling variance (no one-off styles; use templates/snippets)
-- Fallback usage explicitly justified (YAML comments adjacent to the fallback card)
-
-## Card Priority Model
-
-Attempt Tier 1 first. Use Tier 2 only if Tier 1 cannot express the requirement safely.
-
-Tier 1 (primary):
-- `custom:button-card` (structure + tiles + headers + containers)
-- `card-mod` (shared/global polish only; shared snippets OK)
-- `custom:flex-horseshoe-card` (single-metric radial telemetry)
-- `custom:mini-graph-card` (single-metric trends)
-
-Tier 2 (fallback, must justify in comments):
-- `entities`, `markdown`, `history-graph`, `conditional`
-- `iframe` (last resort)
-
-## Layout Rules (Hard Constraints)
-
-- Allowed layout containers: `grid`, `vertical-stack`
-- Maximum nesting depth: 2 (example: `grid` -> `vertical-stack` -> cards)
-- No `horizontal-stack` inside grid cells (prefer: more grid columns, or vertical-stack)
-- No freeform positioning
-- No layout logic embedded in `card-mod`
-- For dense tile lists in `type: sections` views, keep the outer panel full-width (`column_span: 4`) and make the inner tile grid responsive using `custom:layout-card` (`4` desktop / `2` mobile unless the user asks otherwise).
-- In `type: sections` views, the first section immediately below top chips/badges must be a full-width container (`column_span: 4`) with a single wrapper card that also sets `grid_options.columns: full` to prevent skinny-column whitespace.
-- Consolidate upward after removals/moves: do not leave dead space, empty placeholders, or intentionally sparse rows unless the user explicitly asks for whitespace.
-
-Note: If the repo/view uses Home Assistant `type: sections`, treat `sections` as the top-level structure and enforce the same container rules inside each section (sections should contain only `grid`/`vertical-stack` cards and their children).
-
-## Design System Rules (Implementation Discipline)
-
-### Button Card (Primary Renderer)
-
-- Use `custom:button-card` as the structural primitive.
-- Templates required. Do not inline per-card `styles:`; keep styling inside centralized templates/snippets.
-- Logic via variables or template JS only (avoid duplicating logic per-card).
-
-### card-mod (Styling Constraint Layer)
-
-- Allowed only for shared/global polish:
- - padding normalization
- - border radius control
- - shadow suppression
- - typography consistency
-- No entity-specific styling
-- No per-card visual experimentation
-
-### Flex Horseshoe Card
-
-- One primary metric per card.
-- Explicit color thresholds.
-- No mixed units.
-
-### Mini Graph Card
-
-- One metric per graph.
-- Consistent time windows per dashboard.
-- Minimal legends (only when required).
-
-## Template Architecture (Required)
-
-- Views reference templates only (for `custom:button-card`: every instance must specify `template:`).
-- Templates accept variables; views pass variables.
-- Do not create new templates unless explicitly instructed.
-
-Repo reality check (important):
-- If the repo already has established templates (for example, Infrastructure uses `bearstone_*`), use what exists.
-- If the spec's "required template categories" are not present, map to the closest existing template and call out the gap, or ask for explicit permission to add templates.
-
-## Stitch MCP (Inspiration Only)
-
-Use Stitch only to learn:
-- visual hierarchy (headers, grouping, density)
-- spacing rhythm (padding/gaps)
-- grouping patterns (panels, sections)
-
-Never treat Stitch output as implementation code.
-
-When prompting Stitch, include these constraints verbatim:
-
-```
-Grid-only layout. No absolute positioning. No JS frameworks. No custom HTML/CSS outside card-mod.
-Card types limited to: custom:button-card, card-mod, custom:flex-horseshoe-card, custom:mini-graph-card, and HA core fallback cards only if unavoidable.
-Max layout nesting depth: 2. No horizontal-stack inside grid cells.
-```
-
-## Optional Stitch Handoff (Light Bridge)
-
-Use this only when the user explicitly asks for visual ideation, layout exploration, or variants.
-
-- Trigger Stitch path:
- - User asks for redesign inspiration, variants, or multiple visual directions.
-- Skip Stitch path:
- - User asks for direct dashboard refactor/update and no ideation is needed.
-- Preconditions:
- - Stitch is used for hierarchy/spacing/grouping ideation only.
- - Stitch output must never be copied as implementation code.
- - Final implementation must still obey all allowed card/layout rules in this skill.
-
-Handoff artifact (summary only, no code export):
-
-```yaml
-stitch_intent:
- layout_pattern: ""
- section_hierarchy: ""
- density_target: ""
- cards_to_translate:
- - ""
-```
-
-Translation requirement:
-- Convert `stitch_intent` into approved Lovelace structures only (`grid`, `vertical-stack`, Tier 1 cards first).
-- Re-map any unsupported Stitch concepts to compliant cards/containers and explain fallback choices inline when Tier 2 is required.
-
-Fallback behavior when Stitch is unavailable:
-- Continue with manual ideation using this skill's design system and rules.
-- State degraded mode clearly ("Stitch unavailable; proceeding with manual hierarchy/density planning").
-- Do not block dashboard work on Stitch availability.
-
-## Workflow (Do This In Order)
-
-1. Use direct-update mode for this repo:
- - Edit production YAML directly (no staged dashboard copies in this skill workflow).
- - Keep rollback safety by minimizing diffs and validating before HA restart/reload.
-2. Read the target dashboard/view/partials/templates and include targets before editing:
- - Confirm the exact source view/partial/template files.
- - Confirm referenced `!include` files/directories exist and are in-scope.
-3. Determine intent from the user's request and existing dashboard context: `infra` (NOC), `home`, `energy`, or `environment`. Keep one intent per view.
-4. Validate entities and services before editing:
- - Prefer the Home Assistant MCP for live entity/service validation (required when available).
- - Record the MCP validation step in the work notes before writing YAML.
- - If MCP is not available, ask the user to confirm entity IDs and services (do not guess).
-5. Draft layout with constraints: a top-level `grid` and optional `vertical-stack` groups.
- - If using Stitch, first summarize `stitch_intent` and treat it as advisory input to this step.
- - After removals, reflow cards/sections upward to collapse gaps and reduce empty rows.
-6. Implement using Tier 1 cards first; reuse existing templates; avoid one-off styles.
-7. If fallback cards are necessary, add an inline comment explaining why Tier 1 cannot satisfy the requirement.
-8. Validate in this order:
- - Run `pwsh -NoProfile -File tools/validate_dashboards.ps1`.
- - Run `pwsh -NoProfile -File tools/ha_ui_smoke.ps1`.
- - Run Home Assistant config validation (`ha core check` or `homeassistant --script check_config`) when available.
- - Run `scripts/validate_lovelace_view.py` from this skill against each changed view file.
-9. Failure behavior:
- - If requirements can't be met: state the violated rule and propose a compliant alternative.
- - If validation fails: stop, surface the error output, and propose corrected YAML. Do not leave invalid config applied.
-
-Example flow:
-- User asks for redesign inspiration -> Stitch ideation with required constraints -> summarize `stitch_intent` -> translate to Lovelace-safe YAML -> run validation checks.
-
-## References
-
-- Read `references/dashboard_rules.md` when you need the full constraint set and repo-specific mapping notes.
-
-## Home Assistant MCP Validation (Required When Available)
-
-Before writing Lovelace YAML, confirm:
-- Every `entity:` (and any entities referenced via variables) exists.
-- Every service you plan to call exists and the payload shape is correct (especially `service_data`).
-
-When Home Assistant MCP is enabled, use it to:
-- List/confirm entities (avoid typos like `sensor.foo_bar` vs `sensor.foobar`).
-- Confirm the dashboard card references match real entities.
-- Validate service names and fields before committing YAML changes.
-
-If MCP is not enabled or not reachable, stop and ask for the missing entity/service IDs rather than inventing them.
diff --git a/codex_skills/homeassistant-dashboard-designer/agents/openai.yaml b/codex_skills/homeassistant-dashboard-designer/agents/openai.yaml
deleted file mode 100644
index 71e9d4de..00000000
--- a/codex_skills/homeassistant-dashboard-designer/agents/openai.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-######################################################################
-# @CCOSTAN - Follow Me on X
-# For more info visit https://www.vcloudinfo.com/click-here
-# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
-# -------------------------------------------------------------------
-# Dashboard Designer Agent - Codex skill entry metadata.
-# Defines the OpenAI-facing launcher text for this local skill.
-# -------------------------------------------------------------------
-######################################################################
-interface:
- display_name: "Home Assistant Dashboard Designer"
- short_description: "Design/refactor HA Lovelace dashboards safely"
- default_prompt: "Use $homeassistant-dashboard-designer to design, update, or refactor a Home Assistant Lovelace dashboard view using button-card templates + safe grid/stack layouts."
diff --git a/codex_skills/homeassistant-dashboard-designer/references/dashboard_rules.md b/codex_skills/homeassistant-dashboard-designer/references/dashboard_rules.md
deleted file mode 100644
index 6175659b..00000000
--- a/codex_skills/homeassistant-dashboard-designer/references/dashboard_rules.md
+++ /dev/null
@@ -1,169 +0,0 @@
-# Dashboard Rules (Full)
-
-This file is reference material for $homeassistant-dashboard-designer. Prefer following the workflow in `../SKILL.md`, and read this only when you need details.
-
-## Card Priority Model
-
-Tier 1 (primary, attempt first):
-- `custom:button-card`
-- `card-mod` (shared/global styling only)
-- `custom:flex-horseshoe-card`
-- `custom:mini-graph-card`
-
-Tier 2 (fallback, only when Tier 1 cannot satisfy the requirement; justify inline):
-- `entities`
-- `markdown`
-- `history-graph`
-- `conditional`
-- `iframe` (last resort)
-
-## Layout Rules (Hard Constraints)
-
-- Allowed layout containers: `grid`, `vertical-stack`
-- Maximum nesting depth: 2
-- No `horizontal-stack` inside grid cells
-- No freeform positioning
-- No layout logic embedded in `card-mod`
-- For dense status/container lists in `type: sections` views, keep the panel full-width (`column_span: 4`) and use a responsive inner grid (`4` desktop / `2` mobile by default).
-- In `type: sections` views, always place a full-width wrapper section directly below top chips/badges and set the wrapper card to `grid_options.columns: full`.
-- Consolidate layout vertically after any move/remove; dead space is disallowed unless explicitly requested.
-
-Sections-mode note:
-- If a view uses `type: sections`, treat `sections` as the top-level structure and enforce the same container rules inside each section.
-- Prefer reducing section count and regrouping cards into earlier rows rather than leaving sparse trailing space.
-
-## Template Architecture (Required)
-
-Intent: centralize visuals and logic into templates; views pass only variables and remain uniform.
-
-Rules:
-- `custom:button-card` must use templates (no one-off per-card styles).
-- Templates accept variables.
-- Do not hardcode entity IDs inside templates.
-- Do not create new templates without explicit instruction.
-
-Reality note for CCOSTAN/Home-AssistantConfig:
-- Infrastructure templates already exist in:
- - `config/dashboards/infrastructure/templates/button_card_templates.yaml`
-- Existing template naming uses `bearstone_*` and includes styling inside templates (this is OK).
-- When asked to implement new conceptual categories (e.g., "status_pill"), first map to existing templates (e.g., `bearstone_infra_chip*`). If no suitable match exists, ask before adding templates.
-
-## Design System (Discipline)
-
-### Button Card
-
-Use it for:
-- Infrastructure tiles
-- Status pills
-- Section headers
-- Control footers
-- Card containers
-
-Rules:
-- Templates required.
-- Avoid per-card `styles:` keys.
-- Avoid per-card `card_mod:` blocks.
-
-### card-mod
-
-Allowed only for shared/global polish:
-- padding normalization
-- border radius control
-- shadow suppression
-- typography consistency
-
-Not allowed:
-- entity-specific styling
-- per-card visual experimentation
-
-### Flex Horseshoe Card
-
-Use it for:
-- Temperature
-- Battery
-- Load
-- Utilization
-- Percentage-based metrics
-
-Rules:
-- One primary metric per card.
-- Explicit color thresholds.
-- No mixed units.
-
-### Mini Graph Card
-
-Rules:
-- One metric per graph.
-- Consistent time windows per dashboard (keep uniform unless user asks otherwise).
-- Minimal legends (only if required for interpretation).
-
-## Stitch MCP Integration (Inspiration Only)
-
-Stitch is advisory, never authoritative.
-
-When using Stitch:
-- Provide feasibility constraints (cards + layout)
-- Use outputs only to inform hierarchy/grouping/density/spacing
-- Translate into approved Lovelace YAML
-
-Required constraints to include in Stitch prompt:
-
-```
-Grid-only layout. No absolute positioning. No JS frameworks. No custom HTML/CSS outside card-mod.
-Card types limited to: custom:button-card, card-mod, custom:flex-horseshoe-card, custom:mini-graph-card, and HA core fallback cards only if unavoidable.
-Max layout nesting depth: 2. No horizontal-stack inside grid cells.
-```
-
-## Optional Stitch Handoff (Light Bridge)
-
-Decision rule:
-- Use Stitch only when the user requests visual ideation, variants, or exploration.
-- For normal refactor/update requests, skip Stitch and implement directly with this skill's rules.
-
-Handoff artifact (advisory summary, not implementation code):
-
-```yaml
-stitch_intent:
- layout_pattern: ""
- section_hierarchy: ""
- density_target: ""
- cards_to_translate:
- - ""
-```
-
-Required translation back to Lovelace:
-- Treat `stitch_intent` as input to layout planning only.
-- Implement using approved containers/cards and template architecture from this document.
-- For any unsupported concept, re-map to compliant cards and justify Tier 2 fallback inline.
-
-Degraded mode:
-- If Stitch or Stitch skills are unavailable, continue with manual hierarchy/spacing ideation.
-- Do not block dashboard work on Stitch availability.
-
-Anti-drift checklist:
-- No HTML/CSS export artifacts from Stitch output.
-- No nesting-depth violations (max 2).
-- No card-type drift outside approved Tier 1/Tier 2 lists.
-
-## Repo-Specific Dashboard YAML Rules (config/dashboards/**)
-
-If working in this repo's `config/dashboards/` tree:
-- Do not edit `config/.storage` (runtime state).
-- Use direct-update workflow for dashboard YAML in this repo (no staged dashboard promotion flow in this skill).
-- Includes must use absolute container paths starting with `/config/`.
-- Views are one file per view, and the dashboard file uses `!include_dir_list`.
-- Files under `config/dashboards/**/*.yaml` must include the standard `@CCOSTAN` header block.
-
-## Validation (Home Assistant MCP)
-
-When available, use the Home Assistant MCP to validate:
-- Entity IDs referenced by Lovelace cards/templates.
-- Service names and payload fields used by actions (for example, `button.press`, `script.*`, etc.).
-
-If MCP is not available, do not guess entity IDs. Ask the user to confirm them.
-
-## Validation Chain (Required Before Restart/Reload)
-
-- Run `pwsh -NoProfile -File tools/validate_dashboards.ps1`.
-- Run `pwsh -NoProfile -File tools/ha_ui_smoke.ps1`.
-- Run `scripts/validate_lovelace_view.py` for each changed view file.
diff --git a/codex_skills/homeassistant-dashboard-designer/scripts/validate_lovelace_view.py b/codex_skills/homeassistant-dashboard-designer/scripts/validate_lovelace_view.py
deleted file mode 100644
index 3e61fa23..00000000
--- a/codex_skills/homeassistant-dashboard-designer/scripts/validate_lovelace_view.py
+++ /dev/null
@@ -1,379 +0,0 @@
-#!/usr/bin/env python
-"""
-Lightweight validator for Lovelace view YAML files.
-
-Goal: catch common violations of the homeassistant-dashboard-designer constraints
-before Home Assistant reloads.
-
-This is NOT a full Lovelace schema validator.
-"""
-
-from __future__ import annotations
-
-import argparse
-import sys
-from dataclasses import dataclass
-from pathlib import Path
-from typing import Any
-
-import yaml
-
-
-class _Tagged(str):
- pass
-
-
-class _Loader(yaml.SafeLoader):
- pass
-
-
-def _construct_undefined(loader: _Loader, node: yaml.Node) -> Any:
- # Preserve unknown tags (e.g., !include, !include_dir_list) as opaque strings.
- if isinstance(node, yaml.ScalarNode):
- return _Tagged(f"{node.tag} {node.value}")
- if isinstance(node, yaml.SequenceNode):
- seq = loader.construct_sequence(node)
- return _Tagged(f"{node.tag} {seq!r}")
- if isinstance(node, yaml.MappingNode):
- mapping = loader.construct_mapping(node)
- return _Tagged(f"{node.tag} {mapping!r}")
- return _Tagged(f"{node.tag}")
-
-
-_Loader.add_constructor(None, _construct_undefined) # type: ignore[arg-type]
-
-
-@dataclass(frozen=True)
-class Finding:
- level: str # "ERROR" | "WARN"
- path: str
- message: str
-
-
-ALLOWED_LAYOUT = {"grid", "vertical-stack"}
-DISALLOWED_CARD_TYPES = {"horizontal-stack"}
-
-# Cards allowed without justification (design-system tiering is enforced in SKILL.md; this is a lint).
-ALLOWED_CARD_TYPES = {
- "grid",
- "vertical-stack",
- "custom:button-card",
- "custom:layout-card",
- "custom:vertical-stack-in-card",
- "custom:flex-horseshoe-card",
- "custom:mini-graph-card",
- # Tier-2 fallbacks (validator does not enforce justification comments).
- "entities",
- "markdown",
- "history-graph",
- "conditional",
- "iframe",
-}
-
-
-def _is_mapping(v: Any) -> bool:
- return isinstance(v, dict)
-
-
-def _is_sequence(v: Any) -> bool:
- return isinstance(v, list)
-
-
-def _card_type(card: dict[str, Any]) -> str | None:
- t = card.get("type")
- if isinstance(t, str):
- return t
- return None
-
-
-def _validate_layout_depth(
- *,
- node: Any,
- cur_depth: int,
- max_depth: int,
- node_path: str,
- findings: list[Finding],
-) -> None:
- if not _is_mapping(node):
- return
-
- ctype = _card_type(node)
- if ctype in ALLOWED_LAYOUT:
- cur_depth += 1
- if cur_depth > max_depth:
- findings.append(
- Finding(
- level="ERROR",
- path=node_path,
- message=f"Layout nesting depth {cur_depth} exceeds max {max_depth}.",
- )
- )
-
- cards = node.get("cards")
- if _is_sequence(cards):
- for idx, child in enumerate(cards):
- _validate_layout_depth(
- node=child,
- cur_depth=cur_depth,
- max_depth=max_depth,
- node_path=f"{node_path}.cards[{idx}]",
- findings=findings,
- )
-
-
-def _walk_cards(node: Any, node_path: str, findings: list[Finding]) -> None:
- if _is_mapping(node):
- ctype = _card_type(node)
- if ctype in DISALLOWED_CARD_TYPES:
- findings.append(
- Finding(
- level="ERROR",
- path=node_path,
- message=f"Disallowed card type: {ctype}",
- )
- )
- if ctype and ctype not in ALLOWED_CARD_TYPES:
- findings.append(
- Finding(
- level="WARN",
- path=node_path,
- message=f"Unknown/unlisted card type: {ctype}",
- )
- )
-
- if ctype == "custom:button-card":
- tmpl = node.get("template")
- if tmpl is None:
- findings.append(
- Finding(
- level="ERROR",
- path=node_path,
- message="custom:button-card must set template: (string or list).",
- )
- )
- if "styles" in node:
- findings.append(
- Finding(
- level="ERROR",
- path=node_path,
- message="Per-card styles: are not allowed; move styling into centralized templates.",
- )
- )
- if "style" in node:
- findings.append(
- Finding(
- level="ERROR",
- path=node_path,
- message="Per-card style: is not allowed; move styling into centralized templates.",
- )
- )
- if "extra_styles" in node:
- findings.append(
- Finding(
- level="ERROR",
- path=node_path,
- message="Per-card extra_styles: is not allowed; move styling into centralized templates.",
- )
- )
- if "card_mod" in node:
- findings.append(
- Finding(
- level="ERROR",
- path=node_path,
- message="Per-card card_mod: is not allowed on button-card instances; use shared snippets or templates.",
- )
- )
- state_entries = node.get("state")
- if _is_sequence(state_entries):
- for sidx, state_entry in enumerate(state_entries):
- if _is_mapping(state_entry) and "styles" in state_entry:
- findings.append(
- Finding(
- level="ERROR",
- path=f"{node_path}.state[{sidx}]",
- message="Per-card state styles are not allowed; move styling into centralized templates.",
- )
- )
-
- cards = node.get("cards")
- if _is_sequence(cards):
- for idx, child in enumerate(cards):
- _walk_cards(child, f"{node_path}.cards[{idx}]", findings)
-
- elif _is_sequence(node):
- for idx, child in enumerate(node):
- _walk_cards(child, f"{node_path}[{idx}]", findings)
-
-
-def _load_yaml(path: Path) -> Any:
- txt = path.read_text(encoding="utf-8")
- return yaml.load(txt, Loader=_Loader)
-
-
-def _validate_sections_wrapper(sections: list[Any], findings: list[Finding]) -> None:
- """Validate first sections wrapper constraints from dashboard rules."""
- if not sections:
- findings.append(
- Finding(
- level="ERROR",
- path="$.sections",
- message="sections list cannot be empty for a sections view.",
- )
- )
- return
-
- first_section = sections[0]
- if not _is_mapping(first_section):
- findings.append(
- Finding(
- level="ERROR",
- path="$.sections[0]",
- message="First section must be a mapping.",
- )
- )
- return
-
- column_span = first_section.get("column_span")
- if column_span != 4:
- findings.append(
- Finding(
- level="ERROR",
- path="$.sections[0].column_span",
- message="First section must set column_span: 4.",
- )
- )
-
- cards = first_section.get("cards")
- if not _is_sequence(cards):
- findings.append(
- Finding(
- level="ERROR",
- path="$.sections[0].cards",
- message="First section must define cards as a list.",
- )
- )
- return
-
- if len(cards) != 1:
- findings.append(
- Finding(
- level="ERROR",
- path="$.sections[0].cards",
- message="First section must contain exactly one wrapper card.",
- )
- )
- return
-
- wrapper_card = cards[0]
- if not _is_mapping(wrapper_card):
- findings.append(
- Finding(
- level="ERROR",
- path="$.sections[0].cards[0]",
- message="Wrapper card must be a mapping.",
- )
- )
- return
-
- grid_options = wrapper_card.get("grid_options")
- if not _is_mapping(grid_options) or grid_options.get("columns") != "full":
- findings.append(
- Finding(
- level="ERROR",
- path="$.sections[0].cards[0].grid_options.columns",
- message='Wrapper card must set grid_options.columns: "full".',
- )
- )
-
-
-def main(argv: list[str]) -> int:
- ap = argparse.ArgumentParser()
- ap.add_argument("view_yaml", type=Path, help="Path to a Lovelace view YAML file")
- ap.add_argument("--max-layout-depth", type=int, default=2)
- ap.add_argument("--strict", action="store_true", help="Treat WARN findings as errors (non-zero exit).")
- args = ap.parse_args(argv)
-
- if not args.view_yaml.exists():
- print(f"ERROR: file not found: {args.view_yaml}", file=sys.stderr)
- return 2
-
- try:
- doc = _load_yaml(args.view_yaml)
- except Exception as e:
- print(f"ERROR: failed to parse YAML: {e}", file=sys.stderr)
- return 2
-
- findings: list[Finding] = []
-
- # The repo uses "one YAML file per view (single view dict)".
- if not _is_mapping(doc):
- findings.append(Finding(level="ERROR", path="$", message="View file must be a single YAML mapping (one view dict)."))
- else:
- # Support both classic views (`cards:`) and sections views (`type: sections`, `sections:`).
- if "cards" in doc:
- _validate_layout_depth(
- node=doc,
- cur_depth=0,
- max_depth=args.max_layout_depth,
- node_path="$",
- findings=findings,
- )
- _walk_cards(doc.get("cards", []), "$.cards", findings)
- elif doc.get("type") == "sections" and "sections" in doc:
- sections = doc.get("sections")
- if isinstance(sections, _Tagged):
- findings.append(
- Finding(
- level="WARN",
- path="$.sections",
- message="sections is an !include/unknown tag; validator cannot inspect included sections content.",
- )
- )
- elif _is_sequence(sections):
- _validate_sections_wrapper(sections, findings)
- for sidx, section in enumerate(sections):
- spath = f"$.sections[{sidx}]"
- if not _is_mapping(section):
- findings.append(Finding(level="ERROR", path=spath, message="Section must be a mapping."))
- continue
-
- _validate_layout_depth(
- node=section,
- cur_depth=0,
- max_depth=args.max_layout_depth,
- node_path=spath,
- findings=findings,
- )
- _walk_cards(section.get("cards", []), f"{spath}.cards", findings)
- else:
- findings.append(
- Finding(
- level="ERROR",
- path="$.sections",
- message="sections must be a list (or an !include tag).",
- )
- )
- else:
- findings.append(
- Finding(
- level="ERROR",
- path="$",
- message="View dict must contain cards: or be type: sections with sections:.",
- )
- )
-
- errors = [f for f in findings if f.level == "ERROR"]
- warns = [f for f in findings if f.level == "WARN"]
-
- for f in errors + warns:
- print(f"{f.level}: {f.path}: {f.message}")
-
- if errors:
- return 1
- if warns and args.strict:
- return 1
- return 0
-
-
-if __name__ == "__main__":
- raise SystemExit(main(sys.argv[1:]))
diff --git a/codex_skills/homeassistant-dashboard-designer/tests/test_validate_lovelace_view.py b/codex_skills/homeassistant-dashboard-designer/tests/test_validate_lovelace_view.py
deleted file mode 100644
index f3c2335d..00000000
--- a/codex_skills/homeassistant-dashboard-designer/tests/test_validate_lovelace_view.py
+++ /dev/null
@@ -1,127 +0,0 @@
-import contextlib
-import importlib.util
-import io
-import sys
-import tempfile
-import unittest
-from pathlib import Path
-
-
-MODULE_PATH = (
- Path(__file__).resolve().parents[1] / "scripts" / "validate_lovelace_view.py"
-)
-MODULE_NAME = "validate_lovelace_view"
-SPEC = importlib.util.spec_from_file_location(MODULE_NAME, MODULE_PATH)
-if SPEC is None or SPEC.loader is None:
- raise RuntimeError(f"Unable to load module spec from {MODULE_PATH}")
-MODULE = importlib.util.module_from_spec(SPEC)
-sys.modules[MODULE_NAME] = MODULE
-SPEC.loader.exec_module(MODULE)
-
-
-class ValidateLovelaceViewTests(unittest.TestCase):
- def _run_validator(self, yaml_text: str, *extra_args: str) -> tuple[int, str, str]:
- with tempfile.TemporaryDirectory() as tmpdir:
- view_path = Path(tmpdir) / "view.yaml"
- view_path.write_text(yaml_text, encoding="utf-8")
-
- stdout = io.StringIO()
- stderr = io.StringIO()
- with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
- rc = MODULE.main([str(view_path), *extra_args])
- return rc, stdout.getvalue(), stderr.getvalue()
-
- def test_valid_classic_view_passes(self) -> None:
- yaml_text = """
-title: Main
-path: main
-cards:
- - type: custom:button-card
- template:
- - status_chip
-"""
- rc, out, err = self._run_validator(yaml_text)
- self.assertEqual(rc, 0, out + err)
- self.assertEqual(out.strip(), "")
- self.assertEqual(err.strip(), "")
-
- def test_horizontal_stack_fails(self) -> None:
- yaml_text = """
-title: Main
-path: main
-cards:
- - type: horizontal-stack
- cards:
- - type: custom:button-card
- template: status_chip
-"""
- rc, out, _ = self._run_validator(yaml_text)
- self.assertEqual(rc, 1)
- self.assertIn("Disallowed card type: horizontal-stack", out)
-
- def test_button_card_missing_template_fails(self) -> None:
- yaml_text = """
-title: Main
-path: main
-cards:
- - type: custom:button-card
- entity: light.kitchen
-"""
- rc, out, _ = self._run_validator(yaml_text)
- self.assertEqual(rc, 1)
- self.assertIn("custom:button-card must set template", out)
-
- def test_sections_wrapper_rule_fails_when_full_width_missing(self) -> None:
- yaml_text = """
-title: Sections
-path: sections
-type: sections
-sections:
- - type: grid
- column_span: 4
- cards:
- - type: custom:vertical-stack-in-card
- cards:
- - type: custom:button-card
- template: status_chip
-"""
- rc, out, _ = self._run_validator(yaml_text)
- self.assertEqual(rc, 1)
- self.assertIn('Wrapper card must set grid_options.columns: "full".', out)
-
- def test_sections_wrapper_rule_passes_when_full_width_wrapper_present(self) -> None:
- yaml_text = """
-title: Sections
-path: sections
-type: sections
-sections:
- - type: grid
- column_span: 4
- cards:
- - type: custom:vertical-stack-in-card
- grid_options:
- columns: "full"
- cards:
- - type: custom:button-card
- template: status_chip
-"""
- rc, out, err = self._run_validator(yaml_text)
- self.assertEqual(rc, 0, out + err)
- self.assertEqual(out.strip(), "")
- self.assertEqual(err.strip(), "")
-
- def test_unknown_include_tag_is_parse_safe(self) -> None:
- yaml_text = """
-title: Included Sections
-path: included_sections
-type: sections
-sections: !include ../sections/core.yaml
-"""
- rc, out, err = self._run_validator(yaml_text)
- self.assertEqual(rc, 0, out + err)
- self.assertIn("validator cannot inspect included sections content", out)
- self.assertEqual(err.strip(), "")
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/codex_skills/homeassistant-yaml-dry-verifier/README.md b/codex_skills/homeassistant-yaml-dry-verifier/README.md
deleted file mode 100644
index b90b8386..00000000
--- a/codex_skills/homeassistant-yaml-dry-verifier/README.md
+++ /dev/null
@@ -1,91 +0,0 @@
-
-
-
- Bear Stone Smart Home Documentation
-
-Be sure to :star: my configuration repo so you can keep up to date on any daily progress!
-
-
-
-[](https://x.com/ccostan)
-[](https://www.youtube.com/vCloudInfo?sub_confirmation=1)
-[](https://github.com/CCOSTAN/Home-AssistantConfig/stargazers)
-[](https://github.com/CCOSTAN/Home-AssistantConfig/blob/master/config/.HA_VERSION)
-[](https://github.com/CCOSTAN/Home-AssistantConfig/commits/master)
-[](https://github.com/CCOSTAN/Home-AssistantConfig/commits/master)
-
-
-
-# HA YAML DRY Verifier (Codex Skill)
-
-This directory contains the `homeassistant-yaml-dry-verifier` skill and the CLI used to detect repeated YAML structures in Home Assistant automations/scripts/packages.
-
-### Quick navigation
-- You are here: `codex_skills/homeassistant-yaml-dry-verifier/`
-- [Repo overview](../../README.md) | [Codex skills](../README.md) | [Packages](../../config/packages/README.md) | [Scripts](../../config/script/README.md)
-
-## What This Skill Does
-
-- Detects repeated `trigger`, `condition`, `action`, and `sequence` blocks.
-- Detects repeated entries inside those blocks.
-- Detects duplicate entries within a single block (`INTRA`).
-- Detects package-defined scripts called from multiple files (`CENTRAL_SCRIPT`).
-- Collapses noisy ENTRY reports when they are already fully explained by an identical `FULL_BLOCK` finding.
-- Adds workflow guardrails for automation refactors that rename/remove entity references or introduce cleanup behavior: stale-reference checks, dry-run/preview expectations, explicit confirmation, and audit/backup output.
-
-## CLI Usage
-
-Run on one file:
-
-```bash
-python codex_skills/homeassistant-yaml-dry-verifier/scripts/verify_ha_yaml_dry.py config/packages/bearclaw.yaml
-```
-
-Run on broader scope:
-
-```bash
-python codex_skills/homeassistant-yaml-dry-verifier/scripts/verify_ha_yaml_dry.py config/packages config/script
-```
-
-Strict mode (non-zero exit if findings exist):
-
-```bash
-python codex_skills/homeassistant-yaml-dry-verifier/scripts/verify_ha_yaml_dry.py config/packages config/script --strict
-```
-
-## Output Model
-
-The CLI prints:
-- Scan summary counts
-- `FULL_BLOCK` findings
-- `ENTRY` findings
-- `INTRA` findings
-- `CENTRAL_SCRIPT` findings
-
-Exit codes:
-- `0`: success (or findings in non-strict mode)
-- `1`: findings present in strict mode
-- `2`: parse/path errors
-
-## Notes
-
-- This verifier intentionally keeps text output and a small CLI surface.
-- It does not implement suppression files, severity scoring, JSON output, or diff-only mode.
-- It is not an orphan entity cleaner and should not delete Home Assistant registry entries during normal DRY runs.
-- Treat generic `unavailable`, disabled, or no-`config_entry_id` entities as audit signals only; YAML, helper, REST, command-line, MQTT, finance, YouTube, and local infrastructure telemetry can be intentional.
-- Use it as a fast pre-refactor signal and pair with Home Assistant config validation before restart/reload.
-
-**All of my configuration files are tested against the most stable version of home-assistant.**
-
-
-
-**Still have questions on my Config?**
-**Message me on X :** [](https://www.x.com/ccostan)
-
-
-
You can buy me a coffee
-
-
-
-Affiliate Disclosure
-
diff --git a/codex_skills/homeassistant-yaml-dry-verifier/SKILL.md b/codex_skills/homeassistant-yaml-dry-verifier/SKILL.md
deleted file mode 100644
index a5fed600..00000000
--- a/codex_skills/homeassistant-yaml-dry-verifier/SKILL.md
+++ /dev/null
@@ -1,101 +0,0 @@
----
-name: homeassistant-yaml-dry-verifier
-description: "Verify Home Assistant YAML for DRY and efficiency issues by detecting redundant trigger/condition/action/sequence structures and repeated blocks across automations, scripts, and packages. Use when creating, reviewing, or refactoring YAML in config/packages, config/automations, config/scripts, or dashboard-related YAML where duplication risk is high. Include a read-only entity/reference safety pass when automation changes rename/remove entities or introduce maintenance cleanup behavior."
----
-
-# Home Assistant YAML DRY Verifier
-
-Use this skill to lint Home Assistant YAML for repeat logic before or after edits, then refactor repeated blocks into reusable helpers.
-
-## Mandatory Resolution Policy
-
-- If the verifier reports findings for files touched in the current task, do not stop at reporting.
-- Resolve the findings in the same task by refactoring YAML to remove duplication.
-- Re-run the verifier after refactoring and iterate until targeted findings are cleared.
-- If a finding cannot be safely resolved, explicitly document the blocker and the smallest safe follow-up.
-- If touched YAML performs cleanup, registry hygiene, purge, deletion, or other destructive maintenance, require preview/dry-run behavior, explicit confirmation, and audit/backup output before any destructive action is considered complete.
-
-## Quick Start
-
-1. Run the verifier script on the file(s) you edited.
-2. Review repeated block findings first (highest confidence).
-3. Refactor into shared scripts/helpers/templates where appropriate.
-4. If the change renames/removes entity references or adds maintenance cleanup behavior, perform the read-only entity/reference safety pass.
-5. Re-run the verifier and then run your normal Home Assistant config check.
-
-```bash
-python codex_skills/homeassistant-yaml-dry-verifier/scripts/verify_ha_yaml_dry.py config/packages/life360.yaml --strict
-```
-
-Scan a full directory when doing wider cleanup:
-
-```bash
-python codex_skills/homeassistant-yaml-dry-verifier/scripts/verify_ha_yaml_dry.py config/packages config/automations
-```
-
-## Workflow
-
-1. Identify target YAML:
-- Prefer changed files first.
-- Include adjacent package/script files when the change might duplicate existing logic.
-
-2. Run verifier:
-- Use `--min-occurrences 2` (default) for normal checks.
-- Use `--strict` when you want non-zero exit if duplication is found.
-
-3. Prioritize findings in this order:
-- `FULL_BLOCK`: repeated full trigger/condition/action/sequence blocks.
-- `ENTRY`: repeated individual entries inside those blocks (excluding entries already fully covered by a `FULL_BLOCK` duplicate).
-- `INTRA`: duplicate entries inside a single block.
-- `CENTRAL_SCRIPT`: script is defined in `config/packages` but called from 2+ YAML files.
-
-4. Refactor with intent:
-- Repeated actions/sequence: move to a reusable `script.*`, pass variables.
-- Repeated conditions: extract to template binary sensors or helper entities.
-- Repeated triggers: consolidate where behavior is equivalent, or split by intent if readability improves.
-- For cooldown/throttle behavior, prefer automation-local `this.attributes.last_triggered` with custom event handoff before adding new helper entities, unless shared persistent state is required across automations.
-
-5. Entity/Registry Safety Pass:
-- Keep this pass read-only during normal DRY work. Report possible stale references or risky cleanup behavior; do not delete Home Assistant registry entries as part of this skill.
-- When refactors rename, remove, or consolidate automations, scripts, helpers, entities, service calls, or dashboard targets, search touched and adjacent YAML for stale references before closing the task.
-- Use live Home Assistant context or registry/state evidence when available and in scope, especially before changing entity IDs or automations that depend on device/integration state.
-- Do not treat generic `unavailable`, disabled, or no-`config_entry_id` entities as safe deletion candidates. In YAML-heavy setups these are often intentional.
-- Treat these platforms as common false-positive sources unless stronger evidence proves otherwise: `automation`, `script`, `scene`, `template`, helpers (`input_*`, `group`, `timer`, `counter`, `schedule`, `zone`, `person`, `tag`), `command_line`, `rest`, `mqtt`, `yahoofinance`, `youtube`, and local infrastructure telemetry.
-- For cleanup or maintenance automations, prefer a preview/report action first, persistent ignore rules where repeated noise is expected, and an audit artifact that records what would change or did change.
-- Destructive cleanup must be gated by explicit operator confirmation and should have backup/audit output. A dry-run-only recommendation is acceptable when the evidence is not strong enough.
-
-6. Validate after edits:
-- Re-run this verifier.
-- Run Home Assistant config validation before reload/restart.
-
-7. Enforce closure:
-- Treat unresolved `FULL_BLOCK`/`ENTRY` findings in touched files as incomplete work unless a blocker is documented.
-- Prefer consolidating duplicated automation triggers/conditions/actions into shared logic or a single branching automation.
-- Treat unresolved `CENTRAL_SCRIPT` findings in touched scope as incomplete unless documented as deferred-with-blocker.
-- Move shared package scripts to `config/script/.yaml` when they are used cross-file.
-- Treat unresolved stale-reference or cleanup-safety concerns in touched scope as incomplete unless documented as `deferred-with-blocker`.
-
-## Dashboard Designer Integration
-
-When dashboard or automation work includes YAML edits beyond card layout, use this verifier after generation to catch duplicated logic that may have been introduced during fast refactors.
-
-## Output Contract
-
-Always report:
-- Total files scanned.
-- Parse errors (if any).
-- Duplicate groups by kind (`trigger`, `condition`, `action`, `sequence`).
-- Central script placement findings (`CENTRAL_SCRIPT`) with definition + caller files.
-- Script caller detection should include direct `service: script.` and `script.turn_on`-style entity targeting when present.
-- Concrete refactor recommendation per group.
-- Resolution status for each finding (`resolved`, `deferred-with-blocker`).
-- Entity/reference hygiene status when this pass is in scope (`checked`, `not-in-scope`, or `deferred-with-blocker`).
-- For cleanup or destructive maintenance YAML, preview/dry-run, confirmation, and audit/backup status.
-
-Strict behavior:
-- `--strict` returns non-zero for any reported finding (`FULL_BLOCK`, `ENTRY`, `INTRA`, `CENTRAL_SCRIPT`).
-- Without `--strict`, findings are reported but exit remains zero unless parse errors occur.
-
-## References
-
-- Read `references/refactor_playbook.md` for concise DRY refactor patterns.
diff --git a/codex_skills/homeassistant-yaml-dry-verifier/agents/openai.yaml b/codex_skills/homeassistant-yaml-dry-verifier/agents/openai.yaml
deleted file mode 100644
index 86d46db8..00000000
--- a/codex_skills/homeassistant-yaml-dry-verifier/agents/openai.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-######################################################################
-# @CCOSTAN - Follow Me on X
-# For more info visit https://www.vcloudinfo.com/click-here
-# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
-# -------------------------------------------------------------------
-# YAML DRY Verifier Agent - Codex skill entry metadata.
-# Defines the OpenAI-facing launcher text for this local skill.
-# -------------------------------------------------------------------
-######################################################################
-interface:
- display_name: "HA YAML DRY Verifier"
- short_description: "Find redundant HA YAML logic"
- default_prompt: "Use $homeassistant-yaml-dry-verifier to verify Home Assistant YAML for DRY violations, redundant triggers/actions/conditions, and refactor opportunities."
diff --git a/codex_skills/homeassistant-yaml-dry-verifier/references/refactor_playbook.md b/codex_skills/homeassistant-yaml-dry-verifier/references/refactor_playbook.md
deleted file mode 100644
index 71bfe422..00000000
--- a/codex_skills/homeassistant-yaml-dry-verifier/references/refactor_playbook.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Home Assistant YAML DRY Refactor Playbook
-
-Use these patterns after verifier findings.
-
-## Repeated Actions or Sequence
-
-- Move common action chains into a reusable script (`script.`).
-- Use script fields/variables instead of duplicating payload variants.
-- Keep each automation focused on trigger + routing, not full action implementation.
-
-## Repeated Conditions
-
-- Promote shared logic into helper entities (`input_boolean`, helper groups) or template sensors.
-- Replace long repeated `and` chains with a single meaningful condition entity when practical.
-- Keep per-automation overrides small and explicit.
-
-## Repeated Triggers
-
-- Merge equivalent triggers into one automation when the resulting behavior stays clear.
-- If actions diverge, keep separate automations but centralize shared actions in scripts.
-- Avoid copy-paste time/state triggers that only differ by one minor field; parameterize if possible.
-
-## Intra-Block Duplicates
-
-- Remove exact duplicate entries inside the same trigger/condition/action list.
-- Keep only one canonical copy of each entry.
-
-## Validation Loop
-
-1. Run verifier.
-2. Refactor.
-3. Run verifier again.
-4. Run Home Assistant config check before reload/restart.
diff --git a/codex_skills/homeassistant-yaml-dry-verifier/scripts/verify_ha_yaml_dry.py b/codex_skills/homeassistant-yaml-dry-verifier/scripts/verify_ha_yaml_dry.py
deleted file mode 100644
index b9a2e460..00000000
--- a/codex_skills/homeassistant-yaml-dry-verifier/scripts/verify_ha_yaml_dry.py
+++ /dev/null
@@ -1,565 +0,0 @@
-#!/usr/bin/env python
-"""
-Detect DRY violations in Home Assistant YAML by finding repeated structures.
-
-Focus areas:
-- Repeated trigger/condition/action blocks across automations
-- Repeated sequence blocks across scripts
-- Repeated entries inside those blocks
-- Duplicate entries within a single block
-"""
-
-from __future__ import annotations
-
-import argparse
-import json
-import os
-import re
-import sys
-from collections import defaultdict
-from dataclasses import dataclass
-from pathlib import Path
-from typing import Any, Iterable
-
-import yaml
-
-
-class _Tagged(str):
- """Opaque holder for unknown YAML tags such as !include or !secret."""
-
-
-class _Loader(yaml.SafeLoader):
- pass
-
-
-def _construct_undefined(loader: _Loader, node: yaml.Node) -> Any:
- if isinstance(node, yaml.ScalarNode):
- return _Tagged(f"{node.tag} {node.value}")
- if isinstance(node, yaml.SequenceNode):
- seq = loader.construct_sequence(node)
- return _Tagged(f"{node.tag} {seq!r}")
- if isinstance(node, yaml.MappingNode):
- mapping = loader.construct_mapping(node)
- return _Tagged(f"{node.tag} {mapping!r}")
- return _Tagged(f"{node.tag}")
-
-
-_Loader.add_constructor(None, _construct_undefined) # type: ignore[arg-type]
-
-
-AUTOMATION_KEYS: dict[str, tuple[str, ...]] = {
- "trigger": ("trigger", "triggers"),
- "condition": ("condition", "conditions"),
- "action": ("action", "actions"),
-}
-
-SCRIPT_KEYS: dict[str, tuple[str, ...]] = {
- "sequence": ("sequence",),
-}
-
-SKIP_DIRS = {".git", ".venv", ".codex_tmp", "__pycache__", ".mypy_cache"}
-
-
-@dataclass(frozen=True)
-class Candidate:
- kind: str # "automation" | "script"
- name: str
- file_path: str
- path: str
- data: dict[str, Any]
-
-
-@dataclass(frozen=True)
-class Occurrence:
- file_path: str
- candidate_name: str
- candidate_path: str
- block_path: str
-
-
-@dataclass(frozen=True)
-class ParseError:
- file_path: str
- error: str
-
-
-@dataclass(frozen=True)
-class CentralScriptFinding:
- script_id: str
- definition_files: tuple[str, ...]
- caller_files: tuple[str, ...]
-
-
-def _discover_yaml_files(paths: Iterable[str]) -> list[Path]:
- found: set[Path] = set()
- for raw in paths:
- p = Path(raw)
- if not p.exists():
- continue
- if p.is_file() and p.suffix.lower() in {".yaml", ".yml"}:
- found.add(p.resolve())
- continue
- if p.is_dir():
- for root, dirs, files in os.walk(p):
- dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
- root_path = Path(root)
- for name in files:
- if Path(name).suffix.lower() in {".yaml", ".yml"}:
- found.add((root_path / name).resolve())
- return sorted(found)
-
-
-def _load_yaml_docs(path: Path) -> list[Any]:
- text = path.read_text(encoding="utf-8")
- return list(yaml.load_all(text, Loader=_Loader))
-
-
-def _looks_like_automation(v: Any) -> bool:
- if not isinstance(v, dict):
- return False
- has_trigger = "trigger" in v or "triggers" in v
- has_action = "action" in v or "actions" in v
- return has_trigger and has_action
-
-
-def _looks_like_script(v: Any) -> bool:
- return isinstance(v, dict) and "sequence" in v
-
-
-def _candidate_name(kind: str, v: dict[str, Any], fallback: str) -> str:
- alias = v.get("alias")
- if isinstance(alias, str) and alias.strip():
- return alias.strip()
- cid = v.get("id")
- if isinstance(cid, str) and cid.strip():
- return cid.strip()
- return f"{kind}:{fallback}"
-
-
-def _iter_container_items(container: Any) -> Iterable[tuple[str, dict[str, Any]]]:
- if isinstance(container, list):
- for idx, item in enumerate(container):
- if isinstance(item, dict):
- yield f"[{idx}]", item
- return
- if isinstance(container, dict):
- for key, item in container.items():
- if isinstance(item, dict):
- yield f".{key}", item
-
-
-def _extract_candidates_from_doc(doc: Any, file_path: str, doc_idx: int) -> list[Candidate]:
- out: list[Candidate] = []
- root_path = "$" if doc_idx == 0 else f"$doc[{doc_idx}]"
-
- if isinstance(doc, list):
- for suffix, item in _iter_container_items(doc):
- if _looks_like_automation(item):
- name = _candidate_name("automation", item, f"{root_path}{suffix}")
- out.append(
- Candidate(
- kind="automation",
- name=name,
- file_path=file_path,
- path=f"{root_path}{suffix}",
- data=item,
- )
- )
- return out
-
- if not isinstance(doc, dict):
- return out
-
- if _looks_like_automation(doc):
- name = _candidate_name("automation", doc, root_path)
- out.append(Candidate("automation", name, file_path, root_path, doc))
-
- if _looks_like_script(doc):
- name = _candidate_name("script", doc, root_path)
- out.append(Candidate("script", name, file_path, root_path, doc))
-
- if "automation" in doc:
- for suffix, item in _iter_container_items(doc["automation"]):
- if _looks_like_automation(item):
- name = _candidate_name("automation", item, f"{root_path}.automation{suffix}")
- out.append(
- Candidate(
- kind="automation",
- name=name,
- file_path=file_path,
- path=f"{root_path}.automation{suffix}",
- data=item,
- )
- )
-
- if "script" in doc:
- for suffix, item in _iter_container_items(doc["script"]):
- if _looks_like_script(item):
- name = _candidate_name("script", item, f"{root_path}.script{suffix}")
- out.append(
- Candidate(
- kind="script",
- name=name,
- file_path=file_path,
- path=f"{root_path}.script{suffix}",
- data=item,
- )
- )
-
- if out:
- return out
-
- for key, item in doc.items():
- if isinstance(item, dict) and _looks_like_automation(item):
- name = _candidate_name("automation", item, f"{root_path}.{key}")
- out.append(Candidate("automation", name, file_path, f"{root_path}.{key}", item))
- if isinstance(item, dict) and _looks_like_script(item):
- name = _candidate_name("script", item, f"{root_path}.{key}")
- out.append(Candidate("script", name, file_path, f"{root_path}.{key}", item))
-
- return out
-
-
-def _normalize(value: Any) -> Any:
- if isinstance(value, _Tagged):
- return str(value)
- if isinstance(value, dict):
- normalized_items: list[tuple[str, Any]] = []
- for k, v in value.items():
- normalized_items.append((str(k), _normalize(v)))
- normalized_items.sort(key=lambda i: i[0])
- return {k: v for k, v in normalized_items}
- if isinstance(value, list):
- return [_normalize(v) for v in value]
- return value
-
-
-def _fingerprint(value: Any) -> str:
- return json.dumps(_normalize(value), sort_keys=True, separators=(",", ":"), ensure_ascii=True)
-
-
-def _first_present_key(mapping: dict[str, Any], aliases: tuple[str, ...]) -> str | None:
- for key in aliases:
- if key in mapping:
- return key
- return None
-
-
-def _iter_entries(value: Any) -> Iterable[tuple[str, Any]]:
- if isinstance(value, list):
- for idx, entry in enumerate(value):
- yield f"[{idx}]", entry
- return
- yield "", value
-
-
-def _block_keys_for_candidate(candidate: Candidate) -> dict[str, tuple[str, ...]]:
- if candidate.kind == "automation":
- return AUTOMATION_KEYS
- return SCRIPT_KEYS
-
-
-def _recommendation(block_label: str) -> str:
- if block_label in {"action", "sequence"}:
- return (
- "Move repeated logic to config/script/.yaml and call it "
- "via service: script. with variables."
- )
- if block_label == "condition":
- return (
- "Extract shared condition logic into helper/template entities or "
- "merge condition blocks when behavior is equivalent."
- )
- return (
- "Consolidate equivalent trigger patterns and keep shared actions in a "
- "single reusable script when possible."
- )
-
-
-def _render_occurrences(occurrences: list[Occurrence], max_rows: int = 6) -> str:
- lines: list[str] = []
- for occ in occurrences[:max_rows]:
- lines.append(
- f" - {occ.file_path} :: {occ.block_path} ({occ.candidate_name})"
- )
- if len(occurrences) > max_rows:
- lines.append(f" - ... {len(occurrences) - max_rows} more")
- return "\n".join(lines)
-
-
-def _normalize_path(path: str) -> str:
- return path.replace("\\", "/").lower()
-
-
-def _entry_parent_block_path(block_path: str) -> str:
- """Return parent block path for entry occurrences (strip trailing [idx])."""
- return re.sub(r"\[\d+\]$", "", block_path)
-
-
-def _occurrence_key(
- occurrence: Occurrence, *, treat_as_entry: bool = False
-) -> tuple[str, str, str]:
- block_path = (
- _entry_parent_block_path(occurrence.block_path)
- if treat_as_entry
- else occurrence.block_path
- )
- return (occurrence.file_path, occurrence.candidate_path, block_path)
-
-
-def _infer_script_id(candidate: Candidate) -> str | None:
- if candidate.kind != "script":
- return None
- marker = ".script."
- if marker in candidate.path:
- return candidate.path.split(marker, 1)[1]
- if "/config/script/" in _normalize_path(candidate.file_path):
- if candidate.path.startswith("$."):
- return candidate.path[2:]
- match = re.match(r"^\$doc\[\d+\]\.(.+)$", candidate.path)
- if match:
- return match.group(1)
- return None
-
-
-def _collect_script_service_calls(node: Any, script_ids: set[str]) -> None:
- """Collect called script IDs from common HA service invocation patterns."""
- script_domain_meta_services = {"turn_on", "toggle", "reload", "stop"}
-
- def _add_script_entity_ids(value: Any) -> None:
- if isinstance(value, str):
- if value.startswith("script."):
- entity_script_id = value.split(".", 1)[1].strip()
- if entity_script_id:
- script_ids.add(entity_script_id)
- return
- if isinstance(value, list):
- for item in value:
- _add_script_entity_ids(item)
-
- if isinstance(node, dict):
- service_name_raw = node.get("service")
- action_name_raw = node.get("action")
- service_name = None
- if isinstance(service_name_raw, str):
- service_name = service_name_raw.strip()
- elif isinstance(action_name_raw, str):
- service_name = action_name_raw.strip()
-
- if service_name and service_name.startswith("script."):
- tail = service_name.split(".", 1)[1].strip()
- if tail and tail not in script_domain_meta_services:
- script_ids.add(tail)
- else:
- _add_script_entity_ids(node.get("entity_id"))
- for key in ("target", "data", "service_data"):
- container = node.get(key)
- if isinstance(container, dict):
- _add_script_entity_ids(container.get("entity_id"))
-
- for value in node.values():
- _collect_script_service_calls(value, script_ids)
- return
- if isinstance(node, list):
- for item in node:
- _collect_script_service_calls(item, script_ids)
-
-
-def main(argv: list[str]) -> int:
- ap = argparse.ArgumentParser(description="Detect duplicated Home Assistant YAML structures.")
- ap.add_argument("paths", nargs="+", help="YAML file(s) or directory path(s) to scan")
- ap.add_argument("--min-occurrences", type=int, default=2, help="Minimum duplicate count to report (default: 2)")
- ap.add_argument("--max-groups", type=int, default=50, help="Maximum duplicate groups to print (default: 50)")
- ap.add_argument("--strict", action="store_true", help="Return non-zero when duplicates are found")
- args = ap.parse_args(argv)
-
- if args.min_occurrences < 2:
- print("ERROR: --min-occurrences must be >= 2", file=sys.stderr)
- return 2
-
- files = _discover_yaml_files(args.paths)
- if not files:
- print("ERROR: no YAML files found for the provided paths", file=sys.stderr)
- return 2
-
- parse_errors: list[ParseError] = []
- candidates: list[Candidate] = []
- script_calls_by_id: dict[str, set[str]] = defaultdict(set)
-
- for path in files:
- try:
- docs = _load_yaml_docs(path)
- except Exception as exc:
- parse_errors.append(ParseError(file_path=str(path), error=str(exc)))
- continue
-
- script_calls_in_file: set[str] = set()
- for doc_idx, doc in enumerate(docs):
- candidates.extend(_extract_candidates_from_doc(doc, str(path), doc_idx))
- _collect_script_service_calls(doc, script_calls_in_file)
- for script_id in script_calls_in_file:
- script_calls_by_id[script_id].add(str(path))
-
- full_index: dict[tuple[str, str, str], list[Occurrence]] = defaultdict(list)
- entry_index: dict[tuple[str, str, str], list[Occurrence]] = defaultdict(list)
- intra_duplicate_notes: list[str] = []
-
- for candidate in candidates:
- block_key_map = _block_keys_for_candidate(candidate)
- for block_label, aliases in block_key_map.items():
- source_key = _first_present_key(candidate.data, aliases)
- if not source_key:
- continue
-
- block_value = candidate.data[source_key]
- if block_value in (None, [], {}):
- continue
-
- block_fp = _fingerprint(block_value)
- full_index[(candidate.kind, block_label, block_fp)].append(
- Occurrence(
- file_path=candidate.file_path,
- candidate_name=candidate.name,
- candidate_path=candidate.path,
- block_path=f"{candidate.path}.{source_key}",
- )
- )
-
- seen_in_candidate: dict[str, list[str]] = defaultdict(list)
- for suffix, entry in _iter_entries(block_value):
- entry_fp = _fingerprint(entry)
- entry_occ = Occurrence(
- file_path=candidate.file_path,
- candidate_name=candidate.name,
- candidate_path=candidate.path,
- block_path=f"{candidate.path}.{source_key}{suffix}",
- )
- entry_index[(candidate.kind, block_label, entry_fp)].append(entry_occ)
- seen_in_candidate[entry_fp].append(entry_occ.block_path)
-
- for entry_fp, block_paths in seen_in_candidate.items():
- if len(block_paths) >= args.min_occurrences:
- intra_duplicate_notes.append(
- (
- f"INTRA {candidate.kind}.{block_label}: {candidate.name} has "
- f"{len(block_paths)} duplicated entries in {candidate.path}.{source_key}"
- )
- )
-
- def _filter_groups(index: dict[tuple[str, str, str], list[Occurrence]]) -> list[tuple[tuple[str, str, str], list[Occurrence]]]:
- groups = [(k, v) for k, v in index.items() if len(v) >= args.min_occurrences]
- groups.sort(key=lambda item: (-len(item[1]), item[0][0], item[0][1]))
- return groups
-
- full_groups = _filter_groups(full_index)
- entry_groups = _filter_groups(entry_index)
-
- # Drop ENTRY groups that are fully subsumed by an identical FULL_BLOCK group.
- full_group_member_sets: dict[tuple[str, str], list[set[tuple[str, str, str]]]] = defaultdict(list)
- for (kind, block_label, _), occurrences in full_groups:
- full_group_member_sets[(kind, block_label)].append(
- {_occurrence_key(occ) for occ in occurrences}
- )
-
- filtered_entry_groups: list[tuple[tuple[str, str, str], list[Occurrence]]] = []
- for entry_group_key, entry_occurrences in entry_groups:
- kind, block_label, _ = entry_group_key
- entry_member_set = {
- _occurrence_key(occ, treat_as_entry=True) for occ in entry_occurrences
- }
- full_sets = full_group_member_sets.get((kind, block_label), [])
- is_subsumed = any(entry_member_set.issubset(full_set) for full_set in full_sets)
- if not is_subsumed:
- filtered_entry_groups.append((entry_group_key, entry_occurrences))
-
- entry_groups = filtered_entry_groups
- intra_duplicate_notes = sorted(set(intra_duplicate_notes))
- script_definitions_by_id: dict[str, set[str]] = defaultdict(set)
-
- for candidate in candidates:
- script_id = _infer_script_id(candidate)
- if script_id:
- script_definitions_by_id[script_id].add(candidate.file_path)
-
- central_script_findings: list[CentralScriptFinding] = []
- for script_id, definition_files in script_definitions_by_id.items():
- normalized_definitions = {_normalize_path(path): path for path in definition_files}
- if not any("/config/packages/" in n for n in normalized_definitions):
- continue
- if any("/config/script/" in n for n in normalized_definitions):
- continue
- caller_files = sorted(script_calls_by_id.get(script_id, set()))
- if len(caller_files) < 2:
- continue
- central_script_findings.append(
- CentralScriptFinding(
- script_id=script_id,
- definition_files=tuple(sorted(definition_files)),
- caller_files=tuple(caller_files),
- )
- )
-
- central_script_findings.sort(key=lambda item: (-len(item.caller_files), item.script_id))
-
- print(f"Scanned files: {len(files)}")
- print(f"Parsed candidates: {len(candidates)}")
- print(f"Parse errors: {len(parse_errors)}")
- print(f"Duplicate full-block groups: {len(full_groups)}")
- print(f"Duplicate entry groups: {len(entry_groups)}")
- print(f"Intra-block duplicates: {len(intra_duplicate_notes)}")
- print(f"Central-script findings: {len(central_script_findings)}")
-
- if parse_errors:
- print("\nParse errors:")
- for err in parse_errors:
- print(f" - {err.file_path}: {err.error}")
-
- if full_groups:
- print("\nFULL_BLOCK findings:")
- for idx, ((kind, block_label, _), occurrences) in enumerate(full_groups[: args.max_groups], start=1):
- print(f"{idx}. {kind}.{block_label} repeated {len(occurrences)} times")
- print(_render_occurrences(occurrences))
- print(f" suggestion: {_recommendation(block_label)}")
-
- if entry_groups:
- print("\nENTRY findings:")
- for idx, ((kind, block_label, _), occurrences) in enumerate(entry_groups[: args.max_groups], start=1):
- print(f"{idx}. {kind}.{block_label} entry repeated {len(occurrences)} times")
- print(_render_occurrences(occurrences))
- print(f" suggestion: {_recommendation(block_label)}")
-
- if intra_duplicate_notes:
- print("\nINTRA findings:")
- for idx, note in enumerate(intra_duplicate_notes[: args.max_groups], start=1):
- print(f"{idx}. {note}")
-
- if central_script_findings:
- print("\nCENTRAL_SCRIPT findings:")
- for idx, finding in enumerate(central_script_findings[: args.max_groups], start=1):
- print(
- f"{idx}. script.{finding.script_id} is package-defined and called from "
- f"{len(finding.caller_files)} files"
- )
- for definition_file in finding.definition_files:
- print(f" - definition: {definition_file}")
- for caller_file in finding.caller_files[:6]:
- print(f" - caller: {caller_file}")
- if len(finding.caller_files) > 6:
- print(f" - ... {len(finding.caller_files) - 6} more callers")
- print(f" suggestion: Move definition to config/script/{finding.script_id}.yaml")
-
- finding_count = (
- len(full_groups)
- + len(entry_groups)
- + len(intra_duplicate_notes)
- + len(central_script_findings)
- )
- if args.strict and finding_count > 0:
- return 1
- if parse_errors:
- return 2
- return 0
-
-
-if __name__ == "__main__":
- raise SystemExit(main(sys.argv[1:]))
diff --git a/codex_skills/homeassistant-yaml-dry-verifier/tests/test_verify_ha_yaml_dry.py b/codex_skills/homeassistant-yaml-dry-verifier/tests/test_verify_ha_yaml_dry.py
deleted file mode 100644
index dfcdb2c9..00000000
--- a/codex_skills/homeassistant-yaml-dry-verifier/tests/test_verify_ha_yaml_dry.py
+++ /dev/null
@@ -1,302 +0,0 @@
-import contextlib
-import importlib.util
-import io
-import re
-import sys
-import tempfile
-import unittest
-from pathlib import Path
-
-
-MODULE_PATH = (
- Path(__file__).resolve().parents[1] / "scripts" / "verify_ha_yaml_dry.py"
-)
-MODULE_NAME = "verify_ha_yaml_dry"
-SPEC = importlib.util.spec_from_file_location(MODULE_NAME, MODULE_PATH)
-if SPEC is None or SPEC.loader is None:
- raise RuntimeError(f"Unable to load module spec from {MODULE_PATH}")
-MODULE = importlib.util.module_from_spec(SPEC)
-sys.modules[MODULE_NAME] = MODULE
-SPEC.loader.exec_module(MODULE)
-
-
-class VerifyHaYamlDryTests(unittest.TestCase):
- def _run_verifier(
- self,
- files: dict[str, str],
- *extra_args: str,
- scan_subpath: str = ".",
- ) -> tuple[int, str, str]:
- with tempfile.TemporaryDirectory() as tmpdir:
- root = Path(tmpdir)
- for rel_path, content in files.items():
- file_path = root / rel_path
- file_path.parent.mkdir(parents=True, exist_ok=True)
- file_path.write_text(content, encoding="utf-8")
-
- scan_path = root / scan_subpath
- stdout = io.StringIO()
- stderr = io.StringIO()
- with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
- rc = MODULE.main([str(scan_path), *extra_args])
- return rc, stdout.getvalue(), stderr.getvalue()
-
- @staticmethod
- def _full_block_group_order(output: str) -> list[str]:
- marker = "\nFULL_BLOCK findings:\n"
- if marker not in output:
- return []
- section = output.split(marker, 1)[1]
- for stop in ("\nENTRY findings:\n", "\nINTRA findings:\n", "\nCENTRAL_SCRIPT findings:\n"):
- if stop in section:
- section = section.split(stop, 1)[0]
- break
-
- groups: list[str] = []
- for line in section.splitlines():
- text = line.strip()
- match = re.match(r"^\d+\.\s+([a-z]+\.[a-z_]+)\s+repeated\s+\d+\s+times$", text)
- if match:
- groups.append(match.group(1))
- return groups
-
- def test_full_block_detection_with_fixture(self) -> None:
- files = {
- "automations.yaml": """
-- alias: A1
- trigger:
- - platform: state
- entity_id: binary_sensor.one
- to: "on"
- action:
- - service: light.turn_on
- target:
- entity_id: light.kitchen
-- alias: A2
- trigger:
- - platform: state
- entity_id: binary_sensor.two
- to: "on"
- action:
- - service: light.turn_on
- target:
- entity_id: light.kitchen
-"""
- }
- rc, out, err = self._run_verifier(files)
- self.assertEqual(rc, 0, out + err)
- self.assertIn("Duplicate full-block groups: 1", out)
- self.assertIn("FULL_BLOCK findings:", out)
- self.assertIn("automation.action repeated 2 times", out)
-
- def test_entry_detection_with_fixture(self) -> None:
- files = {
- "automations.yaml": """
-- alias: A1
- trigger:
- - platform: state
- entity_id: binary_sensor.one
- to: "on"
- action:
- - service: script.shared_handler
-- alias: A2
- trigger:
- - platform: state
- entity_id: binary_sensor.two
- to: "on"
- action:
- - service: script.shared_handler
- - delay: "00:00:01"
-"""
- }
- rc, out, err = self._run_verifier(files)
- self.assertEqual(rc, 0, out + err)
- self.assertIn("Duplicate entry groups: 1", out)
- self.assertIn("ENTRY findings:", out)
- self.assertIn("automation.action entry repeated 2 times", out)
-
- def test_intra_detection_with_fixture(self) -> None:
- files = {
- "automations.yaml": """
-- alias: Repeat Inside Block
- trigger:
- - platform: state
- entity_id: binary_sensor.one
- to: "on"
- action:
- - service: light.turn_off
- target:
- entity_id: light.den
- - service: light.turn_off
- target:
- entity_id: light.den
-"""
- }
- rc, out, err = self._run_verifier(files)
- self.assertEqual(rc, 0, out + err)
- self.assertIn("INTRA findings:", out)
- self.assertIn("INTRA automation.action: Repeat Inside Block has 2 duplicated entries", out)
-
- def test_central_script_detection_with_package_definition_and_multi_caller(self) -> None:
- files = {
- "config/packages/shared_scripts.yaml": """
-script:
- my_shared_script:
- alias: Shared Script
- sequence:
- - service: logbook.log
- data:
- name: Shared
- message: Hello
-""",
- "config/automations/caller_one.yaml": """
-automation:
- - alias: Caller One
- trigger:
- - platform: state
- entity_id: binary_sensor.one
- to: "on"
- action:
- - service: script.my_shared_script
-""",
- "config/automations/caller_two.yaml": """
-automation:
- - alias: Caller Two
- trigger:
- - platform: state
- entity_id: binary_sensor.two
- to: "on"
- action:
- - service: script.turn_on
- target:
- entity_id: script.my_shared_script
-""",
- }
- rc, out, err = self._run_verifier(files)
- self.assertEqual(rc, 0, out + err)
- self.assertIn("Central-script findings: 1", out)
- self.assertIn("script.my_shared_script is package-defined and called from 2 files", out)
- self.assertIn("suggestion: Move definition to config/script/my_shared_script.yaml", out)
-
- def test_subsumed_entry_groups_are_collapsed(self) -> None:
- files = {
- "automations.yaml": """
-- alias: A1
- trigger:
- - platform: state
- entity_id: binary_sensor.one
- to: "on"
- action:
- - service: light.turn_on
- target:
- entity_id: light.kitchen
-- alias: A2
- trigger:
- - platform: state
- entity_id: binary_sensor.two
- to: "on"
- action:
- - service: light.turn_on
- target:
- entity_id: light.kitchen
-"""
- }
- rc, out, err = self._run_verifier(files)
- self.assertEqual(rc, 0, out + err)
- self.assertIn("Duplicate full-block groups: 1", out)
- self.assertIn("Duplicate entry groups: 0", out)
-
- def test_full_block_findings_order_is_deterministic(self) -> None:
- files = {
- "automations.yaml": """
-- alias: A1
- trigger:
- - platform: state
- entity_id: binary_sensor.same
- to: "on"
- action:
- - service: light.turn_on
- target:
- entity_id: light.kitchen
-- alias: A2
- trigger:
- - platform: state
- entity_id: binary_sensor.same
- to: "on"
- action:
- - service: light.turn_on
- target:
- entity_id: light.kitchen
-""",
- "scripts.yaml": """
-script:
- script_one:
- sequence:
- - service: light.turn_off
- target:
- entity_id: light.kitchen
- script_two:
- sequence:
- - service: light.turn_off
- target:
- entity_id: light.kitchen
-""",
- }
- rc, out, err = self._run_verifier(files)
- self.assertEqual(rc, 0, out + err)
- order = self._full_block_group_order(out)
- self.assertGreaterEqual(len(order), 3, out)
- self.assertEqual(order[:3], ["automation.action", "automation.trigger", "script.sequence"])
-
- def test_exit_codes_for_strict_and_non_strict_modes(self) -> None:
- files = {
- "automations.yaml": """
-- alias: A1
- trigger:
- - platform: state
- entity_id: binary_sensor.one
- to: "on"
- action:
- - service: light.turn_on
- target:
- entity_id: light.kitchen
-- alias: A2
- trigger:
- - platform: state
- entity_id: binary_sensor.two
- to: "on"
- action:
- - service: light.turn_on
- target:
- entity_id: light.kitchen
-"""
- }
- rc_non_strict, _, _ = self._run_verifier(files)
- rc_strict, _, _ = self._run_verifier(files, "--strict")
- self.assertEqual(rc_non_strict, 0)
- self.assertEqual(rc_strict, 1)
-
- def test_parse_error_path_returns_exit_code_two(self) -> None:
- files = {
- "good.yaml": """
-automation:
- - alias: Good
- trigger:
- - platform: state
- entity_id: binary_sensor.one
- to: "on"
- action:
- - service: light.turn_on
- target:
- entity_id: light.kitchen
-""",
- "bad.yaml": "automation: [\n",
- }
- rc, out, err = self._run_verifier(files)
- self.assertEqual(rc, 2, out + err)
- self.assertIn("Parse errors:", out)
- self.assertIn("bad.yaml", out)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/codex_skills/infrastructure-doc-sync/SKILL.md b/codex_skills/infrastructure-doc-sync/SKILL.md
deleted file mode 100644
index c74d971f..00000000
--- a/codex_skills/infrastructure-doc-sync/SKILL.md
+++ /dev/null
@@ -1,85 +0,0 @@
----
-name: infrastructure-doc-sync
-description: "Use when infra/container placement changes require synchronized AGENTS, docs, Dashy, and BearClaw infrastructure snapshot updates while keeping AGENTS concise, scoped, and non-runbook."
----
-
-# Infrastructure Doc Sync
-
-Keep infrastructure documentation aligned after operational changes.
-Keep `AGENTS.md` short and task-scoped; move long runbooks to dedicated docs.
-
-## When to Use
-
-- Docker service moved between hosts.
-- Network mode/port mapping changed.
-- Host role changed (core, camera, edge, backup, etc.).
-- Any update to an `AGENTS.md` file that affects infra context.
-- Any user-facing shortcut URL changed (Dashy).
-
-## Required Update Targets (As Applicable)
-
-1. Workspace infra truth:
- - `../AGENTS.md` (workspace-level host/topology source)
-2. Repo-scoped `AGENTS.md` files touched by the change:
- - Keep only hard constraints and applicability gates.
- - Remove/move long runbooks into dedicated docs.
-3. Relevant repo docs:
- - `README.md`
- - `codex_skills/README.md` (if adding/updating skills)
-4. Ops/runbook doc (if AGENTS runbook content is reduced):
- - Example: `docs/agent_ops_baselines.md` (repo-local operational baseline)
-5. Dashy shortcuts (if any service URL/host changed):
- - `h:\hass\docker_files\dashy/conf.yml`
- - Reload Dashy on docker_17 after edits: `ssh hass@192.168.10.17 "cd ~/docker_files && docker compose up -d dashy"`
-6. BearClaw infrastructure snapshot:
- - `docker_17/codex_appliance` environment map and `/api/admin/infrastructure`
-
-## Workflow
-
-1. Collect current runtime truth from hosts (containers, network mode, ports, and workload role).
-2. Update workspace `AGENTS.md` first for host/topology truth.
-3. Update repo-level `AGENTS.md` with concise, scoped constraints only.
-4. Move long operational/runbook details out of `AGENTS.md` into a dedicated doc when needed.
-5. If end-user entry points changed, update Dashy shortcuts (`dashy/conf.yml`) to match reality.
-6. Update README/skill docs impacted by the change (short, factual, no drift).
-7. Refresh/check BearClaw infrastructure context so it mirrors the same outcome at a high level.
-8. Validate:
- - JSON is valid (`python -m json.tool` equivalent).
- - Dashy `conf.yml` references the intended hostname(s)/ports (no stale LAN IPs unless intentionally required).
- - AGENTS and README statements do not conflict with runtime.
- - Repo-level AGENTS do not contain long runbooks duplicated from dedicated docs.
- - BearClaw `/api/admin/infrastructure` returns the intended topology/context.
-
-## AGENTS Quality Rules
-
-- Prefer short checklists over narrative paragraphs.
-- Keep only non-discoverable, high-impact constraints in AGENTS.
-- Add explicit applicability gates where a file has mixed-scope guidance.
-- Keep specialized/deeper-scoped AGENTS concise and task-specific.
-- De-duplicate repeated policy lines across global/workspace/repo scopes.
-
-## BearClaw Snapshot Content Rules
-
-- Keep infrastructure snapshot content high-level and planning-focused.
-- Do not include secrets, tokens, passwords, or internal file paths.
-- Avoid step-by-step runbooks.
-- Prefer host IDs and roles over low-level implementation detail.
-
-## Dashy Content Rules
-
-- Prefer stable hostnames (ex: `docker17`) over raw IPs when available.
-- Prefer Cloudflare/public URLs for internet-facing apps where appropriate.
-- Keep "Vibe Apps" grouped under the existing Dashy section unless the user asks for taxonomy changes.
-- After edits, reload only Dashy (avoid restarting other docker_17 services).
-
-## Output Contract
-
-Always report:
-
-- What changed (files + purpose).
-- Final intended topology/placement.
-- Any Dashy shortcuts touched (or explicitly state "no Dashy updates needed").
-- Whether runbook content was moved from AGENTS into a dedicated ops doc.
-- BearClaw infrastructure snapshot validation result.
-- Any unresolved follow-up items.
-
diff --git a/codex_skills/infrastructure-doc-sync/agents/openai.yaml b/codex_skills/infrastructure-doc-sync/agents/openai.yaml
deleted file mode 100644
index 5a7c44af..00000000
--- a/codex_skills/infrastructure-doc-sync/agents/openai.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-######################################################################
-# @CCOSTAN - Follow Me on X
-# For more info visit https://www.vcloudinfo.com/click-here
-# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
-# -------------------------------------------------------------------
-# Infrastructure Doc Sync Agent - Codex skill entry metadata.
-# Defines the OpenAI-facing launcher text for this local skill.
-# -------------------------------------------------------------------
-######################################################################
-interface:
- display_name: "Infrastructure Doc Sync"
- short_description: "Sync infra docs with concise AGENTS"
- default_prompt: "Use $infrastructure-doc-sync to keep AGENTS concise and scoped, move runbook content into dedicated docs when needed, and sync README/Dashy/BearClaw infrastructure snapshot context after infra changes."
diff --git a/codex_skills/network-architecture-diagrammer/SKILL.md b/codex_skills/network-architecture-diagrammer/SKILL.md
deleted file mode 100644
index f52578dc..00000000
--- a/codex_skills/network-architecture-diagrammer/SKILL.md
+++ /dev/null
@@ -1,91 +0,0 @@
----
-name: network-architecture-diagrammer
-description: "Create homelab and network architecture diagrams from plain-English prompts by producing Mermaid-first artifacts that import cleanly into Excalidraw. Use for Docker host topology, service dependency maps, Cloudflare/public edge diagrams, and before/after infrastructure sketches."
----
-
-# Network Architecture Diagrammer
-
-Use text-first architecture diagrams that round-trip well: Mermaid source first, Excalidraw second.
-
-## When to Use
-
-- The user asks for a network diagram, architecture sketch, topology map, or service relationship view.
-- You need a reviewable artifact that belongs in git, docs, or an issue comment.
-- You need a fast homelab diagram from AGENTS, compose files, README notes, or runtime inventory.
-
-## Primary Artifact
-
-- Produce Mermaid flowcharts first.
-- Keep the output Excalidraw-importable instead of Mermaid-fancy.
-- If the user wants a file, write a `.mmd` file.
-- Add a short `.md` legend or assumptions file only when it materially helps.
-
-## Workflow
-
-1. Collect topology truth from `AGENTS.md`, compose files, repo docs, and MCP/runtime inventory when available.
-2. Pick one view:
- - context: user, cloud, edge, host
- - deployment: hosts, containers/apps, data stores
- - flow: request/data/event path
- - delta: before/after architecture change
-3. Normalize nodes into a small set:
- - users
- - internet/cloud
- - edge/tunnel/proxy
- - hosts
- - containers/apps
- - data stores
- - external systems
-4. Split large requests into two diagrams instead of forcing one dense canvas.
-5. Write Mermaid with one subgraph per host or trust zone and short, stable labels.
-6. Run `scripts/validate_mermaid_excalidraw.py` on any Mermaid file you create.
-7. Deliver the Mermaid artifact plus short assumptions and the Excalidraw import hint when useful.
-
-## Excalidraw Compatibility Rules
-
-- Use only Mermaid `flowchart` or `graph` syntax.
-- Prefer plain labels, simple arrows, and `subgraph`.
-- Avoid features that commonly import poorly into Excalidraw:
- - non-flowchart diagram types such as `sequenceDiagram`, `classDiagram`, `stateDiagram`, `erDiagram`, `journey`, `gantt`, `mindmap`, `timeline`, `pie`, `gitGraph`, `block-beta`, `packet-beta`, `sankey-beta`, `quadrantChart`, and `zenuml`
- - expanded shape syntax like `@{ ... }`
- - `classDef`, `class`, `style`, `linkStyle`, `click`, and HTML labels/tags
-- Prefer stable hostnames over LAN IPs unless the IPs are the point of the diagram.
-- Keep edge labels short: protocol, purpose, or port only when it changes understanding.
-- Keep each diagram roughly under 15 nodes. Split by audience or scope when needed.
-
-## Output Contract
-
-Always produce:
-
-1. A Mermaid code block.
-2. A short assumptions or omissions list when anything was inferred.
-3. An optional recommended filename such as `docs/diagrams/docker-host-topology.mmd`.
-4. A short note that the Mermaid can be imported into Excalidraw for cleanup when the user wants a polished visual.
-
-If asked to write files, prefer:
-
-- `docs/diagrams/.mmd`
-- `docs/diagrams/.md` for legend/notes only when helpful
-
-## Prompt Pattern
-
-Natural-language prompts are the default. Helpful details include:
-
-- scope
-- audience
-- desired granularity
-- whether public vs LAN edge matters
-- whether a before/after comparison is needed
-
-Example:
-
-- "Create a deployment diagram for docker10/docker14/docker17/docker69, show Cloudflare edge publishing, and make clear which services are LAN-only."
-
-## References
-
-- Read `references/excalidraw_mermaid_rules.md` for the diagram playbook and example patterns.
-
-## Validation
-
-- Run `python scripts/validate_mermaid_excalidraw.py ` when a Mermaid file is created.
-- If the validator flags unsupported syntax, simplify the diagram instead of forcing Mermaid-only features.
diff --git a/codex_skills/network-architecture-diagrammer/agents/openai.yaml b/codex_skills/network-architecture-diagrammer/agents/openai.yaml
deleted file mode 100644
index 3914ed70..00000000
--- a/codex_skills/network-architecture-diagrammer/agents/openai.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-######################################################################
-# @CCOSTAN - Follow Me on X
-# For more info visit https://www.vcloudinfo.com/click-here
-# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
-# -------------------------------------------------------------------
-# Network Diagrammer Agent - Codex skill entry metadata.
-# Defines the OpenAI-facing launcher text for this local skill.
-# -------------------------------------------------------------------
-######################################################################
-interface:
- display_name: "Network Architecture Diagrammer"
- short_description: "Generate Excalidraw-ready homelab diagrams"
- default_prompt: "Use $network-architecture-diagrammer to turn a plain-English homelab or network architecture request into a Mermaid-first diagram that imports cleanly into Excalidraw."
diff --git a/codex_skills/network-architecture-diagrammer/references/excalidraw_mermaid_rules.md b/codex_skills/network-architecture-diagrammer/references/excalidraw_mermaid_rules.md
deleted file mode 100644
index 933b9524..00000000
--- a/codex_skills/network-architecture-diagrammer/references/excalidraw_mermaid_rules.md
+++ /dev/null
@@ -1,134 +0,0 @@
-# Excalidraw Mermaid Playbook
-
-Use this reference when you need a stable, reviewable architecture diagram artifact.
-
-## Goal
-
-Produce Mermaid that:
-
-- is easy to diff in git
-- is readable in markdown or issue comments
-- imports into Excalidraw without heavy cleanup
-
-## Recommended Views
-
-### Context diagram
-
-Use when the audience only needs major boundaries.
-
-```mermaid
-flowchart LR
- User[Carlo]
- Internet[Internet]
- Cloudflare[Cloudflare Tunnel]
- subgraph docker17[docker17]
- Dashy[Dashy]
- Codex[Codex Appliance]
- end
-
- User --> Internet --> Cloudflare --> Dashy
- Cloudflare --> Codex
-```
-
-### Deployment diagram
-
-Use when the audience needs host and service placement.
-
-```mermaid
-flowchart TD
- subgraph docker10[docker10]
- HA[Home Assistant]
- MQTT[MQTT]
- UniFi[UniFi]
- end
- subgraph docker14[docker14]
- Frigate[Frigate]
- end
- subgraph docker17[docker17]
- Appliance[Codex Appliance]
- Dashy[Dashy]
- end
- subgraph docker69[docker69]
- Tunnel[Cloudflared]
- PublicApps[Public Apps]
- end
-
- Tunnel --> Appliance
- Tunnel --> PublicApps
- HA --> MQTT
- Frigate --> HA
-```
-
-### Flow diagram
-
-Use when the audience cares about one path, not the full estate.
-
-```mermaid
-flowchart LR
- User[User]
- Telegram[Telegram]
- Joanna[Joanna]
- HA[Home Assistant]
- Notify[notify_engine]
-
- User --> Telegram --> Joanna --> HA --> Notify
-```
-
-## Label Rules
-
-- Prefer `docker17` over raw IP addresses.
-- Prefer `Cloudflare Tunnel` over product-internal nouns unless the audience already knows them.
-- Keep labels human-readable; IDs belong in notes, not in node titles.
-
-## Edge Rules
-
-- Use unlabeled arrows for obvious containment or traffic.
-- Add short labels only when they matter:
- - `RTSP`
- - `MQTT`
- - `HTTPS`
- - `Webhook`
- - `8124`
-
-## Split Rules
-
-Split into multiple diagrams when any of these are true:
-
-- more than four hosts
-- more than 15 nodes
-- mixed audiences (operators vs app users)
-- both runtime placement and request flow are important
-
-Good split example:
-
-- diagram 1: public edge and user entry points
-- diagram 2: host/container placement
-
-## Excalidraw Guardrails
-
-Avoid these Mermaid features in Excalidraw-bound artifacts:
-
-- non-flowchart diagram types
-- expanded shape syntax (`@{ ... }`)
-- CSS classes or class attachments
-- inline style directives
-- HTML inside labels
-- click handlers
-
-If you want polish, do it after import inside Excalidraw.
-
-## File Naming
-
-Prefer short, obvious filenames:
-
-- `docs/diagrams/bear-stone-context.mmd`
-- `docs/diagrams/docker-host-topology.mmd`
-- `docs/diagrams/cloudflare-edge-flow.mmd`
-
-## Review Checklist
-
-- Does the diagram answer one question clearly?
-- Are host boundaries obvious?
-- Are public entry points distinguishable from LAN-only services?
-- Is the artifact still readable as raw Mermaid text?
-- Will Excalidraw import it without advanced Mermaid features?
diff --git a/codex_skills/network-architecture-diagrammer/scripts/validate_mermaid_excalidraw.py b/codex_skills/network-architecture-diagrammer/scripts/validate_mermaid_excalidraw.py
deleted file mode 100644
index 308c70e0..00000000
--- a/codex_skills/network-architecture-diagrammer/scripts/validate_mermaid_excalidraw.py
+++ /dev/null
@@ -1,77 +0,0 @@
-from __future__ import annotations
-
-import argparse
-import re
-import sys
-from pathlib import Path
-
-
-DISALLOWED_PATTERNS: list[tuple[re.Pattern[str], str]] = [
- (
- re.compile(
- r"^\s*(sequenceDiagram|classDiagram|stateDiagram|erDiagram|journey|gantt|pie|mindmap|timeline|gitGraph|quadrantChart|zenuml|block-beta|packet-beta|sankey-beta)\b",
- re.MULTILINE,
- ),
- "Use Mermaid flowchart syntax only for Excalidraw import.",
- ),
- (
- re.compile(r"@\{"),
- "Avoid expanded Mermaid shape syntax (`@{ ... }`); simplify the node shape.",
- ),
- (
- re.compile(r"^\s*(classDef|class|style|linkStyle|click)\b", re.MULTILINE),
- "Avoid Mermaid styling/class directives; keep the diagram plain for Excalidraw.",
- ),
- (
- re.compile(r":::\w+"),
- "Avoid Mermaid class attachments (`:::`); keep the diagram plain for Excalidraw.",
- ),
- (
- re.compile(r"<[^>\n]+>"),
- "Avoid HTML inside Mermaid labels; use plain text labels instead.",
- ),
-]
-
-
-def validate_mermaid(text: str) -> list[str]:
- findings: list[str] = []
-
- if not re.search(r"^\s*(flowchart|graph)\s+(LR|RL|TB|TD|BT)\b", text, re.MULTILINE):
- findings.append("Expected a Mermaid flowchart header such as `flowchart LR` or `flowchart TD`.")
-
- for pattern, message in DISALLOWED_PATTERNS:
- if pattern.search(text):
- findings.append(message)
-
- return findings
-
-
-def _read_text(path: str | None) -> str:
- if path:
- return Path(path).read_text(encoding="utf-8")
-
- return sys.stdin.read()
-
-
-def main(argv: list[str] | None = None) -> int:
- parser = argparse.ArgumentParser(
- description="Validate Mermaid syntax for clean Excalidraw import."
- )
- parser.add_argument("path", nargs="?", help="Optional Mermaid file to validate")
- args = parser.parse_args(argv)
-
- text = _read_text(args.path)
- findings = validate_mermaid(text)
-
- if findings:
- print("Mermaid is not Excalidraw-friendly:")
- for finding in findings:
- print(f"- {finding}")
- return 1
-
- print("Mermaid looks compatible with the Mermaid-to-Excalidraw workflow.")
- return 0
-
-
-if __name__ == "__main__":
- raise SystemExit(main())
diff --git a/codex_skills/network-architecture-diagrammer/tests/test_validate_mermaid_excalidraw.py b/codex_skills/network-architecture-diagrammer/tests/test_validate_mermaid_excalidraw.py
deleted file mode 100644
index 3b1cb125..00000000
--- a/codex_skills/network-architecture-diagrammer/tests/test_validate_mermaid_excalidraw.py
+++ /dev/null
@@ -1,46 +0,0 @@
-from __future__ import annotations
-
-import sys
-import unittest
-from pathlib import Path
-
-
-sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
-
-import validate_mermaid_excalidraw as validator
-
-
-class ValidateMermaidExcalidrawTests(unittest.TestCase):
- def test_simple_flowchart_passes(self) -> None:
- text = """flowchart LR
-User[User] --> Tunnel[Cloudflare Tunnel] --> App[Dashy]
-"""
- self.assertEqual(validator.validate_mermaid(text), [])
-
- def test_non_flowchart_diagram_fails(self) -> None:
- findings = validator.validate_mermaid(
- """sequenceDiagram
-Alice->>Bob: hello
-"""
- )
- self.assertTrue(any("flowchart syntax only" in finding for finding in findings))
-
- def test_expanded_shape_fails(self) -> None:
- findings = validator.validate_mermaid(
- """flowchart TD
-A@{ shape: rect, label: "App" }
-"""
- )
- self.assertTrue(any("expanded Mermaid shape syntax" in finding for finding in findings))
-
- def test_html_label_fails(self) -> None:
- findings = validator.validate_mermaid(
- """flowchart TD
-A["App
UI"] --> B[API]
-"""
- )
- self.assertTrue(any("Avoid HTML" in finding for finding in findings))
-
-
-if __name__ == "__main__":
- unittest.main()