diff --git a/.claude/skills/hassio-addon-workflow/SKILL.md b/.claude/skills/hassio-addon-workflow/SKILL.md index 8b7b44fb38..20c61498ee 100644 --- a/.claude/skills/hassio-addon-workflow/SKILL.md +++ b/.claude/skills/hassio-addon-workflow/SKILL.md @@ -4,29 +4,30 @@ description: >- Workflow for alexbelgium/hassio-addons Home Assistant add-on work: diagnose with real measurements, independent Codex review, implement, open a PR, resolve CodeRabbit / Copilot / Codex bot review comments, verify in production. Use for any task touching an add-on in this - repo — bugs, RAM/CPU/performance tuning, Dockerfile, config.yaml, cont-init.d or s6 changes, - version bumps, opening or iterating PRs — and when asked to "check with codex", "verify with - chatgpt", or resolve bot comments. Cheap for small asks: a light path skips the heavy steps. + repo — bugs, RAM/CPU/performance tuning, Dockerfile, config.yaml, build.json, + updater.json, cont-init.d, services.d or s6 changes, version and CHANGELOG bumps, a failing + add-on CI check, opening or iterating PRs — and, on an add-on task, when asked to "check with + codex", "verify with chatgpt", or resolve bot comments. Cheap for small asks: a light path skips + the heavy steps. --- # Home Assistant add-on workflow -**Answer style.** Chat replies are terse: no pleasantries, no tool-call narration, no decorative -tables or emoji, no dumped logs — quote the shortest decisive line, and don't re-read or re-print -what is already in context. Fragments and dropped articles are fine. Never compressed: uncertainty -markers ("likely", "assumed", "not verified"), negations (`not`/`never`/`no`/`only`), numbers, -units, technical terms, code blocks, error strings — step 9's Verified/Checked/Assumed distinction -outranks brevity every time. Write in full prose, not fragments, for security warnings, -irreversible-action confirmations, and any multi-step sequence a fragment could make ambiguous. -Persisted text is prose too: commits, CHANGELOG entries, PR bodies, review-thread replies, the -step 10 report. +**Answer style.** Chat replies are terse — no pleasantries, tool-call narration, decorative tables, +emoji or dumped logs; quote the shortest decisive line and don't re-print what is already in +context. Telegraphic fragments are fine *there*. Two things outrank brevity, because dropping a +word from either changes the meaning rather than shortening it: never compress uncertainty markers +("likely", "assumed", "not verified"), negations, numbers, units, technical terms, code or error +strings — step 9's Verified/Checked/Assumed distinction wins every time; and write full prose +wherever a fragment could be read two ways — security warnings, irreversible-action confirmations, +multi-step sequences, and everything persisted (commits, CHANGELOG entries, PR bodies, review-thread +replies, the step 10 report). Triage first, then one of two paths: - **Light** — typo/doc fixes, CHANGELOG edits, version bumps, one-file edits at ladder levels - 1-3 (below), simple questions: scope → implement → validate (`$SKILL/scripts/validate.sh - --vs-master`; `$SKILL` defined below) → PR (version bump + CHANGELOG still required) → - resolve bot comments. + 1-3 (below), simple questions: scope → implement → validate (step 4) → PR (version bump + + CHANGELOG still required) → resolve bot comments. - **Full loop** — performance/RAM/CPU work, diagnosis, anything changing a shipped default, ladder levels 4-6, or an explicit Codex-check request: scope → measure → plan → Codex reviews the plan → implement → simplify → Codex reviews the code → **simplify again** → PR → resolve @@ -47,9 +48,8 @@ reasoning about a hypothetical *host* either. A defensive branch is complexity l the input that reaches it and the image or host where that happens, or delete it and let the case fail visibly instead. -**Repo layout.** `alexbelgium/hassio-addons`; each add-on is a top-level directory. This skill is -checked in at `.claude/skills/hassio-addon-workflow/` (canonical copy). Set the skill root once, -then every `scripts/…` and `references/…` path below is relative to it: +**Skill root.** The canonical copy lives at `.claude/skills/hassio-addon-workflow/`. Set it once; +every `scripts/…` and `references/…` path below is relative to it: ```bash SKILL="$(git rev-parse --show-toplevel)/.claude/skills/hassio-addon-workflow" @@ -63,9 +63,10 @@ bash "$SKILL/scripts/preflight.sh" # and likewise for the other scripts **Delegate heavy output to a subagent.** Codex reviews and multi-thread PR triage produce output you don't need verbatim in your own context. For Codex's plan review (step 3), Codex's code -review (step 6), and PR-comment listing when there are more than ~5 threads (step 8): launch a -subagent to run the command and report back only the objections/findings and your assessment of -each, not the raw transcript. +review (step 6), and PR-comment listing when there are more than ~5 threads (step 8): if subagents +are available, launch one to run the command and report back only the objections/findings and your +assessment of each, not the raw transcript. Otherwise redirect the output to a file and read the +parts you need. --- @@ -85,9 +86,11 @@ Measure the running add-on rather than reasoning from source (`$BUILD_VERSION` s - Is this flag/driver/package actually present? → inspect the artifact: `/proc//cmdline`, `command -v`, `/var/log/apt/history.log` -Verify you're reading the right revision first — `scripts/preflight.sh` catches a stale branch -before it costs a full analysis pass. Measurement methodology, gotchas, and real failure examples: -`references/evidence.md`. +Verify you're reading the right revision first — `scripts/preflight.sh` compares the checkout +against the running `$BUILD_VERSION`, which catches the usual stale branch before it costs a full +analysis pass (a version match does not prove the source is identical). Read +`references/evidence.md` before interpreting any number you did not get straight from +`measure.sh`, and for the failure modes this step exists to prevent. ## 3. Plan — choose the mechanism level, then Codex reviews it (full loop) @@ -117,45 +120,60 @@ Attack your own plan before implementing: - What am I **inferring** that I could instead **detect at runtime** or **record explicitly**? Highest-yield question here — see `references/evidence.md`'s failure-mode section. - For every branch that exists **only to survive something going wrong**: name the image or host - where that input actually arrives, and go and look. Naming is the bar, not reproducing it here — - `references/simplify.md` works the `/dev/shm` guard and the `s6-dumpenv` fallback through that - distinction. + where that input arrives (the standing rule above) and go and look, before you write it. -Full loop only, before writing code: get Codex's independent read on the plan. Spawn a subagent -whose prompt includes the path `references/codex-review.md` and tells it to follow that file's -invocation, then report back only Codex's objections and an assessment of each — not the raw -transcript. +Full loop only, before writing code: get Codex's independent read on the plan, delegated as above — +`references/codex-review.md` has the invocation and how to write the prompt. ## 4. Implement -Touching a shell script, Dockerfile, or env option? Read `references/traps.md` first — skim the -headings, read the sections you're about to touch; the bashio, s6-env, arch-guard and versioning -traps are all live. (The light-path facts it holds — versioning format, CHANGELOG heading — are -already inline in step 7.) Validate with `scripts/validate.sh --vs-master`. Write -behavioural tests for anything with branches, targeting **the regression a reviewer described**, -not just the happy path. +Read the `references/traps.md` section matching what you're about to touch. It is ~18 KB and all +but one section is irrelevant to any given edit, so print the one you need rather than reading the +file — run it with no argument to list the sections: + +| Touching | Run | +| --- | --- | +| an option or anything a base-image service reads | `bash "$SKILL/scripts/traps.sh" passing` | +| a file the app also writes itself | `bash "$SKILL/scripts/traps.sh" "app's own"` | +| shell, bashio, a symlinked script | `bash "$SKILL/scripts/traps.sh" bashio` | +| `Dockerfile`, `build.json`, an arch guard | `bash "$SKILL/scripts/traps.sh" dockerfile` | +| Chromium, Electron, Xvfb | `bash "$SKILL/scripts/traps.sh" chromium` | + +Then validate with `scripts/validate.sh --vs-master`, and write behavioural tests for +anything with branches, targeting **the regression a reviewer described**, not just the happy +path. ## 5. Simplify -Before requesting review, check: did the diff stay at the ladder level chosen in step 3? Can this -be solved by deleting instead of adding? Is the fix bigger than what it fixes? How does it fail in -three years? And on reuse: does any hunk reimplement something `.templates/`, another script in -this add-on, or a sibling add-on already does — and if a future add-on hits this same problem, -will it find one way to solve it or two? Fold a near-duplicate into the existing mechanism, or -justify the divergence in the PR body — but never at the cost of an isolation rule -`references/traps.md` documents: scripts shared by symlink with the webtop add-ons take a new -numbered script, not an edit. Case studies of what happens when this check is skipped: -`references/simplify.md`. +Six questions over your own diff, before anyone else reads it: + +- **Level** — did the diff stay at the ladder level chosen in step 3, or creep up one? +- **Deletion** — can this be solved by deleting instead of adding? +- **Size** — is the fix bigger than the thing it fixes? +- **Reuse** — does any hunk reimplement what `.templates/`, another script in this add-on, or a + sibling add-on already does? If a future add-on hits this problem, will it find one way to solve + it or two? Fold near-duplicates in, or justify the divergence in the PR body. +- **Depth** — is this a special case bolted onto shared infrastructure? Fix the shared mechanism + instead once more than one add-on hits it; generalising from a single case is how bespoke + designs get built, so below that bar the special case is the right call. +- **Longevity** — how does this fail in three years, when the base image or upstream has moved? + +The standing exception to Reuse and Depth: scripts shared by symlink with the webtop add-ons take a +new numbered script, never an edit (`references/traps.md#shell-and-bashio`). Case studies for the rest, including +what shipped when this pass was skipped: `references/simplify.md`. ## 6. Codex attacks the code, then simplify what the review added (full loop only) Same delegated invocation, pointed at `git diff origin/master...HEAD` plus your reasoning per hunk. Details in `references/codex-review.md`. -Then run step 5's checks again over the hunks the review changed. Adversarial review only ever -argues *for* another branch — that is its job — so accepting objections ratchets the diff upward, -and nothing else in the loop walks it back down. For each accepted objection: is the case it -defends one you have now demonstrated, or one you have merely been told about? Taking a +Then run step 5's checks again over the hunks the review changed. Adversarial review mostly argues +*for* another branch — that is what it is asked to do — so accepting objections tends to ratchet +the diff upward, and nothing else in the loop walks it back down. Sort each objection before you +write anything: +**"this is wrong"** is a bug and you fix it; **"this is undefended"** is a claim about some host, +and it needs the same demonstration you would demand of a measurement — is the case it defends one +you have now demonstrated, or one you have merely been told about? Taking a correctness objection often deletes the code that made it necessary, and a fix that collapses back to fewer lines than you started the review with is the normal outcome, not a suspicious one. @@ -165,20 +183,23 @@ kind of edit that leaves a stray `fi` behind. ## 7. Open the PR -CI gates on a PR: **`CHANGELOG.md` updated** (hard fail), the **HA add-on linter** -(`frenck/action-addon-linter`, blocking — not the weekly Super-Linter, which is non-blocking), and -the **add-on image build**. Bump `version` anyway (`X.Y.Z.N`, never `X.Y.Z-N`, see -`references/traps.md#versioning`) — Supervisor won't offer a rebuild without it. Update -`README.md` if you added options; write the CHANGELOG heading as `## ()`, -matching the date format already in that file — almost always ISO `YYYY-MM-DD`, see -`references/traps.md#ci-and-review-bots`. +Three hard gates — **`CHANGELOG.md` updated**, the **HA add-on linter** +(`frenck/action-addon-linter`), and the **add-on image build** — but only on a PR that changes a +top-level `config.*`. On a PR that doesn't (docs, `.github/`, `.claude/`) they *skip*, which is not +the same as passing. Super-Linter runs on every PR and is `continue-on-error`, so it never blocks; +fix its real findings anyway. Nothing checks the version bump, so bump it yourself — Supervisor +won't offer a rebuild without one, and `CLAUDE.md` has the format. Update `README.md` if you added +options; write the CHANGELOG heading as `## ()`, matching the date format already +in that file — almost always ISO `YYYY-MM-DD`, see `references/traps.md#ci-and-review-bots`. Write the body to a file, `gh pr create --body-file`: state what was measured, what changed, **what is not verified**, and how to roll back the riskiest hunk alone. ## 8. Resolve review comments -`scripts/pr_review.sh list|reply|resolve|status|watch `. For every comment, **reproduce the +`scripts/pr_review.sh list|status|watch ` to read, `reply ` and +`resolve ` to answer; run it with no arguments for the full usage. For every +comment, **reproduce the claim before agreeing or disagreeing** — reviewers are frequently right and occasionally confidently wrong; a reproduction takes a minute and decides it either way. Reply with the evidence, then resolve. **Push back when you're right**, on the thread — a resolved-but-wrong @@ -192,13 +213,12 @@ work" — either it was exercised, or say plainly it wasn't. Light path: verification is `validate.sh` plus CI; anything beyond that is Assumed. Full loop: CI passing proves the build works, not that the change does anything — re-run the measurement that -motivated the work once the rebuilt add-on is running. After merge, `git fetch origin master` -(the tracking ref is stale otherwise), then confirm the *changes* survived — `git diff -origin/master -- ` comes back empty. Ancestry is not the check: a revert -leaves your commit in history and undoes its tree, so `--contains` reports success either way. The -builder's revert-on-failure job can revert a merge for reasons unrelated to your diff (see -`references/traps.md#ci-and-review-bots`). Real "merged and inert" examples, and what -to do when a fix can't be self-verified: `references/evidence.md`. +motivated the work once the rebuilt add-on is running. Real "merged and inert" examples, and what +to do when a fix cannot be self-verified: `references/evidence.md`. Then confirm the change +survived the merge: `git fetch origin master` first (the tracking ref is stale otherwise), then +`git diff origin/master -- ` must come back empty. Ancestry is not the +check, and the builder reverts merges for reasons unrelated to your diff — both explained in +`references/traps.md#ci-and-review-bots`. ## 10. Calibrate and report diff --git a/.claude/skills/hassio-addon-workflow/references/codex-review.md b/.claude/skills/hassio-addon-workflow/references/codex-review.md index b204ed1e39..59e02c7d19 100644 --- a/.claude/skills/hassio-addon-workflow/references/codex-review.md +++ b/.claude/skills/hassio-addon-workflow/references/codex-review.md @@ -2,9 +2,7 @@ Used for step 3 (plan review) and step 6 (code review), full loop only. Codex is a genuinely different model reading the files itself; on this workload it has repeatedly been worth the -minutes. Delegate the invocation to a subagent (see SKILL.md's subagent-delegation note) so its -output doesn't land verbatim in your context — have the subagent return only Codex's objections -and your assessment of each. +minutes. Run it through a subagent, per SKILL.md's delegation note. ## Invocation @@ -12,11 +10,12 @@ and your assessment of each. on ~4 KB prompts (2026-08-03); the CLI with the same content succeeded. The MCP tool is still fine for short questions. -`--sandbox read-only` lets Codex read files but blocks writes and command execution, and -`approval_policy=never` means it will not be prompted for permission to run anything either — so -paste every number into the prompt rather than expecting Codex to gather it. `- <` feeds the -prompt file on stdin. Run it in the background so you are not blocked for the several minutes it -takes (`&` here, or your harness's background-task mechanism): +`--sandbox read-only` blocks writes, not reads, and `approval_policy=never` stops it asking for +permission rather than stopping it acting — so it can still run read-only commands and often +falls back to fetching the repo from GitHub instead of reading your worktree. Paste every number +into the prompt rather than expecting it to gather them, and treat what it reports about *local* +state as unverified. `- <` feeds the prompt file on stdin. Run it in the background so you are not +blocked for the several minutes it takes (`&` here, or your harness's background-task mechanism): ```bash codex exec --model gpt-5.6-sol --sandbox read-only --skip-git-repo-check \ @@ -40,9 +39,5 @@ codex exec --model gpt-5.6-sol --sandbox read-only --skip-git-repo-check \ confidently, and separately caught a genuine methodology error in the same review. Treat its confirmations with the same scepticism as its objections — especially about the build. -**Its objections ratchet complexity upward.** An adversarial reviewer is asked to find what could -go wrong, so its output is a list of arguments for more code; it is never asked whether the branch -it wants is reachable. Separate "this is wrong" from "this is undefended" before you write -anything: the first is a bug and you fix it, the second is a claim about some host, and it needs -the same demonstration you would demand of a measurement. That is what step 6's second simplify -pass is for. +**Its objections only ever argue for more code** — it is asked what could go wrong, never whether +the branch it wants is reachable. Sort them before writing anything; SKILL.md step 6 is that pass. diff --git a/.claude/skills/hassio-addon-workflow/references/evidence.md b/.claude/skills/hassio-addon-workflow/references/evidence.md index 49e535beed..22db9368d9 100644 --- a/.claude/skills/hassio-addon-workflow/references/evidence.md +++ b/.claude/skills/hassio-addon-workflow/references/evidence.md @@ -1,16 +1,24 @@ # Evidence — measurement methodology and case studies -## Why summed RSS and reserved-vs-resident both matter +## How to read the numbers -- **Summed RSS double-counts shared pages.** Removing a duplicate process frees its *private* - memory, not its RSS. `scripts/measure.sh` reports PSS and private alongside RSS — quote - **private** when arguing "removing this saves N MB". -- **A big mapping is not necessarily resident.** Large SysV/tmpfs segments are lazily populated; - reserved size is reported separately from resident for this reason. -- `/proc/meminfo` and `free` show **host** figures (no memory cgroup namespace here) — never - attribute those to the add-on. -- Sample duration matters: a 3 s CPU sample measured 2.3% where a 20 s sample measured 21.6% for - the same process. Use ≥20 s for anything you report. +**Summed RSS overstates savings.** Shared library pages are counted once per process, so removing +a duplicate frees its *private* memory, not its RSS. Measured example: four MCP shims summed to +882 MB RSS but 643 MB PSS / 564 MB private, and per-process private ranged 54 MB down to 2 MB — +which completely changes which duplicate is worth removing. Quote private when arguing "removing +this saves N MB". + +**A large mapping is often not resident.** SysV/tmpfs segments are lazily populated. Xvfb's +506 MB framebuffer shows `Rss: 0` in `/proc//smaps`. Check before calling anything a leak. + +**`/proc/meminfo` and `free` show host figures** — there is no memory cgroup namespace here. +Never attribute those totals to the add-on. + +**A short CPU sample is not a CPU measurement.** A 3 s sample measured 2.3% where a 20 s sample +measured 21.6% for the same process. Use >= 20 s for anything you report. + +`scripts/measure.sh` already reports PSS and private alongside RSS, and resident separately from +reserved, so these three only bite when you compute a figure yourself or quote one from `ps`. ## Before asserting anything, ask what would show it false diff --git a/.claude/skills/hassio-addon-workflow/references/simplify.md b/.claude/skills/hassio-addon-workflow/references/simplify.md index 2872131160..a9b3d38d28 100644 --- a/.claude/skills/hassio-addon-workflow/references/simplify.md +++ b/.claude/skills/hassio-addon-workflow/references/simplify.md @@ -30,28 +30,24 @@ Being able to build the complicated thing is not a reason to. It took the maintainer asking "is this the simplest way possible?" to run the pass that step 6 now requires. -## Checks worth running against your own diff +## Where SKILL.md step 5's questions get hard -- **Did the diff stay at the ladder level chosen in step 3?** If it crept up a level, either - justify that out loud or redo it at the level you chose. -- **Can this be solved by deleting instead of adding?** A flag that shouldn't be passed, a - process that shouldn't start, a registration that shouldn't be duplicated. Deleting usually - shrinks the regression surface — but not always: the `/dev/shm` case in - `references/evidence.md` is a removal that reintroduced a crash loop on hosts unlike this one. - A removal that depends on a host default still needs the same verification as an addition. -- **Is the fix bigger than the thing it fixes?** That is a smell, not a rule — but it usually - means the problem was framed one level too deep. -- **For each defensive branch: what input reaches it, on which image or host?** Go and check, - the way you would check a measurement. The bar is being able to **name** the case, not to - reproduce it here: Docker's 64 MB `/dev/shm` default is documented behaviour that HA does not - override, so the bullet above keeps that guard even though this host measured 7.7 GB. Nobody - could name a single image shipping `with-contenv` without `s6-dumpenv`, so that fallback went. - If you cannot name the case, delete the branch — the situation then fails the way it already - fails today, visibly, instead of through a second path that is never exercised and silently - rots as the base images move. Weigh the cost too: a one-flag guard against a crash you cannot - rule out is cheap, a second code path that degrades to the pre-fix behaviour anyway is not. - Write down in the PR body what you cut and why, so the next person does not re-add it from the - same reasoning. -- **How does this fail in three years**, when the base image, Electron, or upstream has moved? - Code that reads a documented knob keeps working. Code that reaches into private internals - does not. +**Deletion is not automatically the safe direction.** The `/dev/shm` case in `evidence.md` is a +removal that reintroduced a crash loop on hosts unlike this one. A removal that depends on a host +default needs the same verification as an addition. + +**Naming the case is the bar for a defensive branch, not reproducing it.** Docker's 64 MB +`/dev/shm` default is documented behaviour that Home Assistant does not override, so that guard +stays even though this host measured 7.7 GB. Nobody could name a single image shipping +`with-contenv` without `s6-dumpenv`, so that fallback went. If you cannot name the case, delete the +branch: the situation then fails the way it already fails today, visibly, instead of through a +second path that is never exercised and silently rots as the base images move. Weigh the cost both +ways — a one-flag guard against a crash you cannot rule out is cheap; a second code path that +degrades to the pre-fix behaviour anyway is not. Write in the PR body what you cut and why, so the +next person does not re-add it from the same reasoning. + +**"Bigger than the thing it fixes" is a smell, not a rule** — but it usually means the problem was +framed one level too deep. + +**Three-year failure** favours code that reads a documented knob. Code that reaches into private +internals does not survive the base image moving. diff --git a/.claude/skills/hassio-addon-workflow/references/traps.md b/.claude/skills/hassio-addon-workflow/references/traps.md index a6fefa7d7d..7ece6b86b3 100644 --- a/.claude/skills/hassio-addon-workflow/references/traps.md +++ b/.claude/skills/hassio-addon-workflow/references/traps.md @@ -9,7 +9,6 @@ workflows and lint rules — that is not repeated here. ## Contents - [Environment and workspace](#environment-and-workspace) -- [Measurement](#measurement) - [Passing values into base-image services](#passing-values-into-base-image-services) - [Writing into an app's own config](#writing-into-an-apps-own-config) - [Shell and bashio](#shell-and-bashio) @@ -46,20 +45,6 @@ gate. One observed run took ~3 hours, with 20+ runs queued against 2 executing runner contention, not the diff. Check `gh run list` before concluding your PR is stuck. Poll in a background task, and never claim the build is verified when it hasn't run. -## Measurement - -**Summed RSS overstates savings.** Shared library pages are counted once per process, so removing -a duplicate frees its *private* memory, not its RSS. Measured example: four MCP shims summed to -882 MB RSS but 643 MB PSS / 564 MB private, and per-process private ranged 54 MB down to 2 MB — -which completely changes which duplicate is worth removing. Quote private when arguing "removing -this saves N MB". - -**A large mapping is often not resident.** SysV/tmpfs segments are lazily populated. Xvfb's -506 MB framebuffer shows `Rss: 0` in `/proc//smaps`. Check before calling anything a leak. - -**`/proc/meminfo` and `free` show host figures** — there is no memory cgroup namespace here. -Never attribute those totals to the add-on. - **`rtk` filters some command output.** For a complete listing, redirect to a file and read that (`ps ... > $SP/ps.txt`), or use `rtk proxy `. @@ -190,11 +175,9 @@ build. ## Versioning -**`X.Y.Z.N`, never `X.Y.Z-N`.** A hyphen parses as a semver pre-release, which Supervisor treats -as *older* than `X.Y.Z` — the update is never offered. - -Date-based versions (`2026.08.03`) are common here. Check whether master has already moved to the -version you were about to use. +`CLAUDE.md` owns the format (`X.Y.Z.N`, never `X.Y.Z-N`, and why). The one thing it does not say: +date-based versions (`2026.08.03`) are common here, so check whether master has already moved to +the version you were about to use before you pick it. ## Chromium / Electron under Xvfb diff --git a/.claude/skills/hassio-addon-workflow/scripts/preflight.sh b/.claude/skills/hassio-addon-workflow/scripts/preflight.sh index a61495b00d..6c2ecdd73c 100755 --- a/.claude/skills/hassio-addon-workflow/scripts/preflight.sh +++ b/.claude/skills/hassio-addon-workflow/scripts/preflight.sh @@ -5,7 +5,14 @@ # Usage: preflight.sh [repo-path] [addon-slug] set -uo pipefail -REPO="${1:-/data/claude/hassio-addons}" +# Default to the checkout this is run from, never a fixed path: a fixed path silently inspected +# the main checkout while the caller worked in a worktree, reporting a branch nobody was editing — +# the exact stale-checkout trap this script exists to catch. Falling back to one when git cannot +# answer would recreate it, so refuse instead and make the caller say which repo they mean. +REPO="${1:-}" +if [ -z "$REPO" ]; then + REPO=$(git rev-parse --show-toplevel 2> /dev/null) || REPO="" +fi SLUG="${2:-}" echo "== tools ==" @@ -26,9 +33,14 @@ fi echo echo "== repo ==" # git-aware check: in a worktree .git is a file, not a directory +if [ -z "$REPO" ]; then + echo " not inside a git checkout, and no repo path given" + echo " -> pass one explicitly: preflight.sh [addon-slug]" + exit 1 +fi if ! git -C "$REPO" rev-parse --git-dir > /dev/null 2>&1; then echo " no git repo at $REPO" - exit 0 + exit 1 fi cd "$REPO" || exit 0 branch=$(git branch --show-current 2> /dev/null || echo "(detached)") diff --git a/.claude/skills/hassio-addon-workflow/scripts/traps.sh b/.claude/skills/hassio-addon-workflow/scripts/traps.sh new file mode 100755 index 0000000000..33e5f01dcd --- /dev/null +++ b/.claude/skills/hassio-addon-workflow/scripts/traps.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Print one section of references/traps.md. +# +# traps.md is ~18 KB and every section but one is irrelevant to any given edit: a shell fix needs +# 642 bytes of it, a Dockerfile fix 2.6 KB, and "CI and review bots" — 35% of the file — is needed +# at steps 7-8 and never at step 4. A markdown anchor cannot be loaded on its own, so reading the +# file to reach one section pays for all of them. This prints just the section, so the routing +# table in SKILL.md step 4 costs what it claims to. +# +# Usage: traps.sh # substring of a section heading, case-insensitive +# traps.sh # list the sections +set -uo pipefail + +TRAPS="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/references/traps.md" +[ -f "$TRAPS" ] || { echo "not found: $TRAPS" >&2; exit 1; } + +# The Contents list duplicates the headings; grep the headings themselves so the list cannot drift. +list() { + echo "sections (pass any substring):" + grep '^## ' "$TRAPS" | grep -v '^## Contents' | sed 's/^## / /' +} + +[ $# -eq 0 ] && { list; exit 0; } + +# awk over exact heading text: section names contain '/' and other characters that would need +# escaping in a sed address, and a keyword matching several headings should be an error, not a +# silent pick of the first. +mapfile -t matches < <(grep '^## ' "$TRAPS" | grep -v '^## Contents' | + grep -iF -- "$1" | sed 's/^## //') + +case "${#matches[@]}" in + 0) + echo "no section matching '$1'" >&2 + list >&2 + exit 1 + ;; + 1) ;; + *) + echo "'$1' matches ${#matches[@]} sections — be more specific:" >&2 + printf ' %s\n' "${matches[@]}" >&2 + exit 1 + ;; +esac + +awk -v want="## ${matches[0]}" ' + $0 == want { inside = 1; print; next } + inside && /^## / { exit } + inside { print } +' "$TRAPS"