From b7319dced6b5feeefc3aefaeeaba364a453bc415 Mon Sep 17 00:00:00 2001 From: Carlo Costanzo Date: Sat, 11 Jul 2026 21:11:55 -0400 Subject: [PATCH] Move Codex skills to dedicated repository --- README.md | 2 +- codex_skills/README.md | 53 -- .../README.md | 124 ---- .../homeassistant-dashboard-designer/SKILL.md | 221 ------- .../agents/openai.yaml | 13 - .../references/dashboard_rules.md | 169 ------ .../scripts/validate_lovelace_view.py | 379 ------------ .../tests/test_validate_lovelace_view.py | 127 ---- .../homeassistant-yaml-dry-verifier/README.md | 91 --- .../homeassistant-yaml-dry-verifier/SKILL.md | 101 ---- .../agents/openai.yaml | 13 - .../references/refactor_playbook.md | 33 - .../scripts/verify_ha_yaml_dry.py | 565 ------------------ .../tests/test_verify_ha_yaml_dry.py | 302 ---------- codex_skills/infrastructure-doc-sync/SKILL.md | 85 --- .../agents/openai.yaml | 13 - .../network-architecture-diagrammer/SKILL.md | 91 --- .../agents/openai.yaml | 13 - .../references/excalidraw_mermaid_rules.md | 134 ----- .../scripts/validate_mermaid_excalidraw.py | 77 --- .../tests/test_validate_mermaid_excalidraw.py | 46 -- 21 files changed, 1 insertion(+), 2651 deletions(-) delete mode 100644 codex_skills/README.md delete mode 100644 codex_skills/homeassistant-dashboard-designer/README.md delete mode 100644 codex_skills/homeassistant-dashboard-designer/SKILL.md delete mode 100644 codex_skills/homeassistant-dashboard-designer/agents/openai.yaml delete mode 100644 codex_skills/homeassistant-dashboard-designer/references/dashboard_rules.md delete mode 100644 codex_skills/homeassistant-dashboard-designer/scripts/validate_lovelace_view.py delete mode 100644 codex_skills/homeassistant-dashboard-designer/tests/test_validate_lovelace_view.py delete mode 100644 codex_skills/homeassistant-yaml-dry-verifier/README.md delete mode 100644 codex_skills/homeassistant-yaml-dry-verifier/SKILL.md delete mode 100644 codex_skills/homeassistant-yaml-dry-verifier/agents/openai.yaml delete mode 100644 codex_skills/homeassistant-yaml-dry-verifier/references/refactor_playbook.md delete mode 100644 codex_skills/homeassistant-yaml-dry-verifier/scripts/verify_ha_yaml_dry.py delete mode 100644 codex_skills/homeassistant-yaml-dry-verifier/tests/test_verify_ha_yaml_dry.py delete mode 100644 codex_skills/infrastructure-doc-sync/SKILL.md delete mode 100644 codex_skills/infrastructure-doc-sync/agents/openai.yaml delete mode 100644 codex_skills/network-architecture-diagrammer/SKILL.md delete mode 100644 codex_skills/network-architecture-diagrammer/agents/openai.yaml delete mode 100644 codex_skills/network-architecture-diagrammer/references/excalidraw_mermaid_rules.md delete mode 100644 codex_skills/network-architecture-diagrammer/scripts/validate_mermaid_excalidraw.py delete mode 100644 codex_skills/network-architecture-diagrammer/tests/test_validate_mermaid_excalidraw.py 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) ![Home Assistant header](https://i.imgur.com/vjDH1LJ.png) 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 -
- Bear Stone Smart Home Documentation -

-

Be sure to :star: my configuration repo so you can keep up to date on any daily progress!

- -
- -[![X Follow](https://img.shields.io/static/v1?label=talk&message=3k&color=blue&logo=twitter&style=for-the-badge)](https://x.com/ccostan) -[![YouTube Subscribe](https://img.shields.io/youtube/channel/subscribers/UC301G8JJFzY0BZ_0lshpKpQ?label=VIEW&logo=Youtube&logoColor=%23DF5D44&style=for-the-badge)](https://www.youtube.com/vCloudInfo?sub_confirmation=1) -[![GitHub Stars](https://img.shields.io/github/stars/CCOSTAN/Home-AssistantConfig.svg?label=STARS&logo=github&style=for-the-badge)](https://github.com/CCOSTAN/Home-AssistantConfig/stargazers)
-[![HA Version Badge](https://raw.githubusercontent.com/ccostan/home-assistantconfig/master/ha-version-badge.svg)](https://github.com/CCOSTAN/Home-AssistantConfig/blob/master/config/.HA_VERSION) -[![Last Commit](https://img.shields.io/github/last-commit/CCOSTAN/Home-AssistantConfig/master?style=plastic)](https://github.com/CCOSTAN/Home-AssistantConfig/commits/master) -[![Commit Activity](https://img.shields.io/github/commit-activity/y/CCOSTAN/Home-AssistantConfig.svg?style=plastic)](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: [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/aFis2YPeSuY) -- Companion post: [![vCloudInfo Blog Post](https://img.shields.io/static/v1?label=vCloudInfo&message=Blog%20Post&color=21759B&logo=wordpress&logoColor=white)](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 :** [![Follow CCostan](https://img.shields.io/twitter/follow/CCostan)](https://www.x.com/ccostan) - -

-Buy me a coffeeYou can buy me a coffeeBuy 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 -
- Bear Stone Smart Home Documentation -

-

Be sure to :star: my configuration repo so you can keep up to date on any daily progress!

- -
- -[![X Follow](https://img.shields.io/static/v1?label=talk&message=3k&color=blue&logo=twitter&style=for-the-badge)](https://x.com/ccostan) -[![YouTube Subscribe](https://img.shields.io/youtube/channel/subscribers/UC301G8JJFzY0BZ_0lshpKpQ?label=VIEW&logo=Youtube&logoColor=%23DF5D44&style=for-the-badge)](https://www.youtube.com/vCloudInfo?sub_confirmation=1) -[![GitHub Stars](https://img.shields.io/github/stars/CCOSTAN/Home-AssistantConfig.svg?label=STARS&logo=github&style=for-the-badge)](https://github.com/CCOSTAN/Home-AssistantConfig/stargazers)
-[![HA Version Badge](https://raw.githubusercontent.com/ccostan/home-assistantconfig/master/ha-version-badge.svg)](https://github.com/CCOSTAN/Home-AssistantConfig/blob/master/config/.HA_VERSION) -[![Last Commit](https://img.shields.io/github/last-commit/CCOSTAN/Home-AssistantConfig/master?style=plastic)](https://github.com/CCOSTAN/Home-AssistantConfig/commits/master) -[![Commit Activity](https://img.shields.io/github/commit-activity/y/CCOSTAN/Home-AssistantConfig.svg?style=plastic)](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: [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/aFis2YPeSuY) -- Companion post: [![vCloudInfo Blog Post](https://img.shields.io/static/v1?label=vCloudInfo&message=Blog%20Post&color=21759B&logo=wordpress&logoColor=white)](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 :** [![Follow CCostan](https://img.shields.io/twitter/follow/CCostan)](https://www.x.com/ccostan) - -

-Buy me a coffeeYou can buy me a coffeeBuy 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 -
- Bear Stone Smart Home Documentation -

-

Be sure to :star: my configuration repo so you can keep up to date on any daily progress!

- -
- -[![X Follow](https://img.shields.io/static/v1?label=talk&message=3k&color=blue&logo=twitter&style=for-the-badge)](https://x.com/ccostan) -[![YouTube Subscribe](https://img.shields.io/youtube/channel/subscribers/UC301G8JJFzY0BZ_0lshpKpQ?label=VIEW&logo=Youtube&logoColor=%23DF5D44&style=for-the-badge)](https://www.youtube.com/vCloudInfo?sub_confirmation=1) -[![GitHub Stars](https://img.shields.io/github/stars/CCOSTAN/Home-AssistantConfig.svg?label=STARS&logo=github&style=for-the-badge)](https://github.com/CCOSTAN/Home-AssistantConfig/stargazers)
-[![HA Version Badge](https://raw.githubusercontent.com/ccostan/home-assistantconfig/master/ha-version-badge.svg)](https://github.com/CCOSTAN/Home-AssistantConfig/blob/master/config/.HA_VERSION) -[![Last Commit](https://img.shields.io/github/last-commit/CCOSTAN/Home-AssistantConfig/master?style=plastic)](https://github.com/CCOSTAN/Home-AssistantConfig/commits/master) -[![Commit Activity](https://img.shields.io/github/commit-activity/y/CCOSTAN/Home-AssistantConfig.svg?style=plastic)](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 :** [![Follow CCostan](https://img.shields.io/twitter/follow/CCostan)](https://www.x.com/ccostan) - -

-Buy me a coffeeYou can buy me a coffeeBuy 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()