Files
Alexandre fc07a2d1b4 skill(hassio-addon-workflow): deduplicate, route traps.md by section, fix preflight path (#3057)
* skill(hassio-addon-workflow): deduplicate, route traps.md by section, fix preflight path

Reviewed the skill against Anthropic's skill-authoring guidance (progressive
disclosure, single ownership of each rule, explain the why rather than issue
rigid rules) with a second, independent pass from Codex (gpt-5.6-sol).

Real defect fixed: preflight.sh defaulted its repo argument to the hardcoded
/data/claude/hassio-addons, so when run from a worktree it inspected the main
checkout and reported that branch — the exact stale-checkout trap the script
exists to catch. It now defaults to `git rev-parse --show-toplevel`.

Deduplicated, one owner each:
- measurement methodology: evidence.md owns it; traps.md's Measurement section
  is gone (its rtk note moved to Environment and workspace)
- defensive-branch reachability: the standing rule owns it; step 3 points back
- subagent delegation: SKILL.md's delegation note owns it; codex-review.md points
- review-ratchet handling: SKILL.md step 6 owns it, absorbing codex-review.md's
  "this is wrong" vs "this is undefended" split
- post-merge revert mechanics: traps.md owns them; step 9 states the check only
- simplify checklist: step 5 owns the questions; simplify.md keeps the cases
- version format: CLAUDE.md owns it; traps.md keeps only what it does not say

Corrections found by review and verified against the workflows and scripts:
- Super-Linter runs on every PR (lint.yml `pull_request`), not weekly; it is
  continue-on-error at both call sites, which is the part that matters
- the three hard gates are matrixed over changed add-ons, so they skip rather
  than pass on a PR touching no top-level config.*
- `--sandbox read-only` blocks writes, not reads or command execution; Codex
  can and does fall back to fetching the repo from GitHub
- env_trace.sh's process argument is optional; pr_review.sh's reply and resolve
  take more than a PR number

Also: step 4 now routes to the traps.md section by anchor instead of asking for
the whole ~290-line file, and step 5 gains a Depth check (fix the shared
mechanism once more than one add-on hits it) adapted from the built-in
/simplify skill's altitude pass.

No new markdownlint findings; shellcheck clean; preflight.sh verified reporting
this worktree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* skill(hassio-addon-workflow): make step 4's traps.md routing actually save the tokens

The routing table added in the previous commit was advisory only: a markdown
anchor cannot be loaded on its own, so an agent told to read
references/traps.md#shell-and-bashio reads the file and pays for all 18 KB.

Measured overpay per edit, against what each section actually needs:
  shell / bashio            642 B needed, 18,070 B paid  (28x)
  Dockerfile / arch       2,655 B needed, 18,070 B paid  (7x)
  base-image env option   3,070 B needed, 18,070 B paid  (6x)
"CI and review bots" is 6,302 B of that — 35% of the file — and is needed at
steps 7-8, never at step 4.

scripts/traps.sh prints one section. Scripts are run rather than read, so the
helper itself costs no prompt tokens and the table now buys what it claims.
Kept traps.md whole rather than splitting it into six files: the split would
save the same bytes but fragments a file with a working table of contents and
breaks the references/traps.md#anchor cross-references that steps 5, 7 and 9
still use.

Ambiguous or unmatched keywords list the sections and exit non-zero rather than
silently printing the wrong one. Verified: every keyword in the step 4 table
resolves to exactly one section, headings containing '/' work, and the last
section reaches EOF. shellcheck clean; no new markdownlint findings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* skill(hassio-addon-workflow): fix review findings — PATH form, preflight fallback, config.json claim

All three reproduced before accepting.

Step 4's routing table used a bare `traps.sh <keyword>`, which is not on PATH:
every row failed with "traps.sh: command not found" from the repo root. Flagged
independently by Codex, Copilot and CodeRabbit. Now uses the
`bash "$SKILL/scripts/traps.sh"` form already established above it; all five
keywords verified to run and resolve to exactly one section.

preflight.sh fell back to the hardcoded /data/claude/hassio-addons when
`git rev-parse` could not answer, recreating the stale-checkout bug this commit
series set out to fix — and worse than CodeRabbit described: from a non-git
directory it reported an unrelated branch of the main checkout as though it were
the caller's own. It now refuses and asks for an explicit path (exit 1).
Explicit-argument and in-worktree behaviour unchanged, both re-verified.

Codex correctly noted the description advertised config.json while preflight.sh
and validate.sh parse only config.yaml. Exactly one add-on uses config.json
(zzz_archived_tor, archived) against 136 using config.yaml, so the proportionate
fix is Codex's own alternative — narrow the description — rather than teaching
two scripts JSON for an archived add-on. config.json dropped from the
description.

shellcheck clean; no new markdownlint findings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: BirdNET-Go Addon Builder <addon-builder@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 22:42:55 +02:00

4.1 KiB

Evidence — measurement methodology and case studies

How to read the numbers

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/<pid>/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

  • "This process is duplicated" → is it? ps -ef --forest, compare parents and start times.
  • "This costs 500 MB" → is it resident? grep Rss /proc/<pid>/smaps_rollup — plain smaps prints one Rss: line per mapping (dozens of them), not a process total.
  • "This block never runs" → is its payload in the image? command -v, apt history.
  • "The flag isn't set" → tr '\0' '\n' < /proc/<pid>/cmdline.

When you correct yourself mid-analysis, keep the correction visible in your notes and in what you report — a retracted claim that stays retracted is worth more than one quietly dropped.

The failure mode this loop keeps producing

Every bug shipped from the source session came from one move: measuring this host correctly, then generalising it to all hosts.

  • /dev/shm was 7.7 GB here, so a flag looked useless — but Home Assistant ignores shm_size, so elsewhere it is Docker's 64 MB default and removing the flag reintroduces a crash loop.
  • An MCP entry was identified by its URL — but that URL is the documented default, so the rule would have deleted a user's hand-written configuration.
  • A GPU probe created a hardware context — but that proved the driver worked, not that Chromium's GPU path did.
  • SABnzbd's source was grepped to see which proxy headers it reads, and X-Forwarded-For was forwarded because it reads that one — but verify_xff_header is on by default and makes it reject every address in the chain that is not local, so ingress answered 403 for anyone reaching Home Assistant from outside the LAN (#3019, fixed in #3023). Every check ran from inside the container, where no such header exists. That an app reads a header is not a reason to send it — find out what it does with it, and exercise the path a remote user takes.

The pattern is always inference standing in for detection. Before changing a default, ask what this is like on a host unlike yours. Prefer detecting the condition at runtime over asserting it. When ownership matters, record it rather than infer it.

"Merged and inert" — CI passing proves the build works, not that the change does anything

Both changes in the session that produced this skill passed CI, merged, and were inert:

  • The Xvfb resolution cap wrote its env file correctly and Xvfb still started at the base-image default — wrong env mechanism for that service.
  • The GPU flags reached Chromium's command line exactly as intended, and the GPU process still reported --use-gl=disabled, having overridden them after its own init failed.

Once the rebuilt add-on is running, re-run the measurement that motivated the work. Some fixes cannot be self-verified — a service that reads its environment only at start makes an env-var fix unproven until the add-on restarts, which needs the user or ha-cli with their agreement. If you cannot restart, the change is Assumed, not Verified, and must be reported that way.