PowerShell utility scripts for managing the wkcollis1-eng GitHub repositories and Home Assistant Green deployment from a Windows PC.
Repos managed:
home-assistant-configResidential-HVAC-Performance-Baseline-Lifepo4-Battery-BanksDIY-LiFePO4-UPSTools(this repo)
cd C:\repos\Tools
.\session-start.ps1 # verify, pull, status — all in oneIf verify-system.ps1 exits with failures, resolve them before proceeding. Warnings (unpushed commits, non-main branch) are informational — the session can continue.
These rules are non-negotiable. The scripts enforce them where possible; the rest require discipline.
- Never commit data that fails HALT-level validation.
validate-all.ps1exits 1 on HALT.monthly-update.ps1Phase 2 aborts before pushing if validation fails. - Always run
pull-all-repos.ps1before any session. Working on stale local state causes merge conflicts. - Run
deploy-to-ha.ps1after any script file change. Six HA shell commands depend on the three deployed scripts. A missing deploy breaks them silently until the next automation fires. monthly-update.ps1Phase 2 requires a clean working tree. The script enforces this — uncommitted changes in any release repo abort the sequence.- Repo list changes go in
repos.ps1only. All scripts source it. Never hardcode a repo name in an individual script.
What this toolkit actively enforces — as opposed to what it relies on discipline for:
- No partial deploys to HA.
deploy-to-ha.ps1verifies SHA-256 hashes after every copy and aborts with a hard error if any mismatch is detected. HA shell commands are not reloadable until all copies verify clean. - No release tagging on invalid data.
monthly-update.ps1Phase 2 runsvalidate-all.ps1before tagging or pushing. A HALT-level failure stops the sequence entirely. - No cross-repo drift in repo list.
repos.ps1is the single source of truth. All scripts dot-source it — no script carries its own hardcoded repo list. - No silent tool failures. Every script calls
Assert-Environmentfromcommon.ps1before doing real work. Missing tools, missing git identity, or unauthenticatedghall throw immediately with a clear message. - Deterministic issue publishing. Local issue files carry their own frontmatter (repo, title, labels).
publish-issue.ps1reads it directly — no manual parameter entry, no copy-paste into the GitHub UI.
Anything outside these guarantees requires discipline rather than enforcement. Use verify-system.ps1 at the start of any session to confirm the full environment is clean before proceeding.
Defines how each type of failure is handled across all scripts. New scripts must conform to this model.
| Failure type | Exit | Examples | Script behavior |
|---|---|---|---|
| Hard failure | 1 | Missing tool, git identity not set, HALT validation, deploy hash mismatch, diverged repo | Throws immediately with actionable message. No partial state left. |
| Soft failure | 0 | Per-repo git failure in batch pull/push | Logged with color, script continues to remaining repos, summary shown at end. |
| Warning | 0 | Non-main branch, unpushed commits, python not installed | Printed in yellow. Session may proceed. |
Rules for script authors:
- Hard failures use
exit 1with aWrite-Hosterror message — neverthrowin scripts called by parents (throw produces noisy or swallowed output when called from a pipeline). Reservethrowfor strict-mode guard clauses at the top of standalone scripts only. - Soft failures accumulate in a
$failuresor$resultshash and are reported in the summary. - Warnings never block execution.
- Multi-repo scripts always print a summary at the end regardless of individual outcomes.
CLAUDE_TOOLS.md is an ultra-minified version of this README optimized for Claude Code sessions. It covers all contracts, workflows, scripts, and failure modes in ~150 lines instead of ~900 — reducing token consumption per lookup by ~85%.
Setup (one-time):
# 1. Copy CLAUDE_TOOLS.md to the tools repo (already done if you pulled recently)
# 2. Inject a reference into each repo's CLAUDE.md:
cd C:\repos\Tools
.\inject-tools-ref.ps1
.\push-all-repos.ps1How Claude Code uses it:
During any session, Claude Code reads CLAUDE_TOOLS.md as the authoritative reference instead of README.md. The Task → Script mapping table at the top lets it pattern-match directly from your request to the correct command without reading through documentation. The contracts section ensures it enforces the same invariants and failure model the scripts enforce.
README.md remains the human-readable reference for setup, detailed parameters, and troubleshooting.
Before using any script, confirm the following are installed and working on your Windows PC.
git --versionIf not installed: https://git-scm.com/download/win — accept all defaults. Git 2.x is the minimum; Assert-Environment will throw with the version it detected if the requirement is not met.
python --versionRequired for install-precommit-all.ps1. Python 3.x is required — Assert-Environment -RequirePython checks the major version and throws if it is not 3.
gh --versionIf not installed: https://cli.github.com — then authenticate:
gh auth loginFollow the prompts. Choose HTTPS and authenticate via browser.
The deploy script writes to \\homeassistant\config\scripts\. Confirm the share is reachable before running:
Test-Path \\homeassistant\config\scriptsShould return True. If not, open File Explorer → Map Network Drive and map \\homeassistant\config before proceeding.
On a fresh Windows machine, download bootstrap.ps1 from the repo and run it. It handles everything in steps 2–5 automatically:
# After installing git, python, and gh (see Prerequisites):
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
cd <directory where you downloaded bootstrap.ps1>
Unblock-File .\bootstrap.ps1
.\bootstrap.ps1 # standard — clones to C:\repos
.\bootstrap.ps1 -ReposRoot D:\repos # custom repos root
.\bootstrap.ps1 -SkipPrecommit # skip pre-commit install stepBootstrap handles: git 2.x and Python 3.x version checks, gh authentication, git identity, execution policy, cloning all repos, unblocking scripts, installing pre-commit hooks, and running verify-system.ps1 automatically at the end. It writes a bootstrap.log in the script directory for post-run review. Skip to Step 6 if bootstrap completed successfully.
Go to https://github.com/new and create a repo named tools under wkcollis1-eng. Add a README. Public or private — your choice.
Open PowerShell as a regular user (not administrator) and run:
mkdir C:\repos
cd C:\repos
git clone https://github.com/wkcollis1-eng/Tools.gitThen run the clone script to pull the other four repos:
cd C:\repos\Tools
.\clone-all-repos.ps1This is safe to run again at any time — it skips repos that already exist locally.
Windows marks files downloaded from the internet as untrusted and blocks them from running regardless of execution policy. Unblock all scripts in the tools repo with one command:
cd C:\repos\Tools
Get-ChildItem *.ps1 | Unblock-FileRun this again any time you download new or updated .ps1 files into the repo. No output means success.
Also set the PowerShell execution policy to allow local scripts if you haven't already:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUserTo run the scripts from any directory without typing the full path:
# Add to current session only:
$env:PATH += ";C:\repos\Tools"
# To make permanent, add to your PowerShell profile:
notepad $PROFILE
# Add this line at the bottom:
# $env:PATH += ";C:\repos\Tools"cd C:\repos\Tools
.\install-precommit-all.ps1This installs pre-commit in every repo. The hooks run automatically on every git commit and catch trailing whitespace, YAML errors, oversized files, and Python formatting issues before they reach GitHub.
After installing, update the hook versions to current:
cd C:\repos\home-assistant-config
python -m pre_commit autoupdate
git add .pre-commit-config.yaml
git commit -m "chore: update pre-commit hook versions"
# Repeat for each repo, or run autoupdate inside install-precommit-all.ps1Note: Always use
python -m pre_commitrather than thepre-commitbinary directly. On Windows, a freshly pip-installedpre-commitbinary may not appear on PATH in the same PowerShell session. The module invocation always works.
Shared environment validation. Never run directly — dot-sourced automatically by every script. Defines Assert-Environment, which checks for required tools and git identity before any script does real work. Also exports the $SambaSharePath constant (\\homeassistant\config\scripts) used by deploy-to-ha.ps1 and verify-system.ps1.
# Used inside scripts — not for direct invocation:
. "$PSScriptRoot\common.ps1"
Assert-Environment # git 2.x + git identity
Assert-Environment -RequireGh # + gh CLI authenticated
Assert-Environment -RequirePython # + python 3.x
Assert-Environment -RequireGh -RequirePython # all three
Assert-Environment -RequirePython -RequirePreCommit # + python -m pre_commit available
Assert-Environment -RequirePython -RequireSamba # + Samba share reachableEach script calls the appropriate variant. If git identity is not configured, any script that touches git will throw immediately with the exact commands to fix it. -RequirePreCommit uses python -m pre_commit (not the binary) to avoid the Windows PATH-not-updated-after-pip-install problem. -RequireSamba calls Test-Path $SambaSharePath and throws if the share is not mounted.
Single command to begin any work session. Runs verify-system.ps1, pull-all-repos.ps1, and status-all-repos.ps1 in sequence. Aborts on any hard failure — if the environment is not clean, the session does not start.
.\session-start.ps1 # Normal startup (pulls all repos)
.\session-start.ps1 -NoPull # Skip pull step (offline or air-gapped work)| Parameter | Description |
|---|---|
-NoPull |
Skip the pull step entirely — useful when working offline or on a local branch with no remote changes expected |
Single command to close any work session. Shows repo status, prompts for HA deploy if needed (auto-detects .py and .yaml changes in home-assistant-config), pushes all repos, and syncs notes.
.\session-end.ps1 # prompts whether to deploy
.\session-end.ps1 -Deploy # always deploy (skips prompt)
.\session-end.ps1 -NoDeploy # skip deploy entirely
.\session-end.ps1 -NoSync # skip sync-notesParameters:
| Parameter | Description |
|---|---|
-Deploy |
Always run deploy-to-ha.ps1 without prompting |
-NoDeploy |
Skip deploy step entirely |
-NoSync |
Skip sync-notes.ps1 (no doc changes this session) |
The deploy prompt is context-aware: if session-end.ps1 detects .py or .yaml file changes in home-assistant-config across all unpushed commits (using @{u}..HEAD), it defaults to Y for .py changes. If .yaml changes are detected, a reminder to reload the affected HA domain is shown before the prompt. Pass -Deploy or -NoDeploy to skip the prompt entirely.
If deploy fails, the session end aborts before pushing — preventing a push that references scripts not yet deployed to HA.
Full issue publishing workflow in one command: checks for GitHub duplicates, publishes the issue, then commits the updated file (with published_url written back) to the tools repo.
.\publish-and-sync.ps1 issues\ha-config_cooling-buildout.md
.\publish-and-sync.ps1 issues\ha-config_cooling-buildout.md -OpenInBrowser
.\publish-and-sync.ps1 issues\ha-config_cooling-buildout.md -SkipDuplicateCheckParameters:
| Parameter | Description |
|---|---|
-OpenInBrowser |
Open the new issue URL in your browser after publishing |
-SkipDuplicateCheck |
Skip the list-issues.ps1 -Remote step |
Replaces the three-step manual sequence: list-issues.ps1 -All, publish-issue.ps1, sync-notes.ps1.
Creates a tagged GitHub release with an automatic validation gate for the HVAC baseline repo. For Residential-HVAC-Performance-Baseline-, runs validate-all.ps1 before tagging — a HALT stops the release. For all other repos, tags immediately. Validates CalVer tag format before proceeding. Fetches tags locally after creation so git describe is immediately accurate. Auto-generates release notes from git log --oneline since the last tag if -Notes is omitted.
.\release.ps1 -Repo home-assistant-config -Tag v2026.03.1 -Title "March 2026 — cooling build-out"
.\release.ps1 -Repo Residential-HVAC-Performance-Baseline- -Tag v2026.03.1 -Title "March 2026" -Month 2026-03
.\release.ps1 -Repo home-assistant-config -Tag v2026.03.1 -Title "March 2026" -DraftParameters:
| Parameter | Required | Description |
|---|---|---|
-Repo |
Yes | Repo name |
-Tag |
Yes | CalVer tag — must match vYYYY.MM.N (e.g. v2026.03.1). Script rejects malformed tags before any GitHub call |
-Title |
Yes | Release title |
-Month |
HVAC repo | Month to validate, e.g. 2026-03. Required for HVAC repo to scope validation |
-Notes |
No | Release body text. Auto-generated from git log --oneline since last tag if omitted |
-Draft |
No | Create as draft for review before publishing |
-SkipValidation |
No | Skip validation even for HVAC repo (use with caution) |
Full first-time setup from a fresh Windows machine. Checks prerequisites (including git 2.x and Python 3.x version minimums), authenticates gh, configures git identity, sets execution policy, clones all repos, unblocks scripts, installs pre-commit hooks, and runs verify-system.ps1 automatically at the end. Writes bootstrap.log in the script directory for post-run review.
# On a fresh machine after installing git, python, and gh:
Unblock-File .\bootstrap.ps1
.\bootstrap.ps1 # standard — clones to C:\repos
.\bootstrap.ps1 -ReposRoot D:\repos # custom repos root
.\bootstrap.ps1 -SkipPrecommit # skip pre-commit install step| Parameter | Required | Description |
|---|---|---|
-ReposRoot |
No | Root directory for all repos. Defaults to C:\repos |
-SkipPrecommit |
No | Skip the pre-commit install step (useful on machines without pip access) |
This is the only script that can be run before the toolkit is set up — it has no dependency on common.ps1 or repos.ps1 being present. Both are dot-sourced automatically once the Tools repo is cloned. All steps are safe to re-run — completed steps are skipped.
Adds a CLAUDE_TOOLS.md reference block to each repo's CLAUDE.md. Safe to re-run — skips repos where the reference already exists. Run once after initial setup.
.\inject-tools-ref.ps1
.\push-all-repos.ps1 # commit the CLAUDE.md updatesFull environment health check. Run at the start of any session to confirm the system is in a known-good state before doing real work. Exits 1 if any check fails.
.\verify-system.ps1 # Standard health check
.\verify-system.ps1 -NoFetch # Skip git fetch (faster, offline-safe)
.\verify-system.ps1 -Json # Export results to verify-results.json| Parameter | Description |
|---|---|
-NoFetch |
Skip git fetch origin per repo — avoids network calls when offline or when divergence data is not needed |
-Json |
Export all check results to verify-results.json in the Tools repo root |
Checks performed:
| Check | Pass condition |
|---|---|
| git installed | git --version succeeds |
| git identity | user.name and user.email configured |
| gh installed | gh --version succeeds |
| gh authenticated | gh auth status exits 0 |
| python installed | python --version succeeds |
| All repos cloned | .git present in each repo directory |
| All repos on main | No repo on a non-main branch |
| All working trees clean | No uncommitted or unpushed changes |
| Repo divergence | origin/main exists and is not diverged (guarded — skipped if no remote tracking branch) |
| Pre-commit hooks installed | .git/hooks/pre-commit present per repo |
| HA Samba share reachable | \\homeassistant\config\scripts accessible |
| HA Samba share writable | Probe file write succeeds on share |
| Deployed scripts present | Expected .py files exist on share |
| Last deploy timestamp | Read from DEPLOY_VERSION.txt on HA share |
Output is color-coded: green for pass, yellow for warnings (session can proceed), red for failures (resolve before continuing).
Single source of truth for the managed repo list. Not run directly — dot-sourced by every other script. To add a repo to the toolkit, add one line here; no other script needs to change.
# repos.ps1 is sourced automatically — no manual invocation neededTo add a new repo: edit $Repos and $RepoUrls in repos.ps1, then run clone-all-repos.ps1. The unified $RepoMap hash (keyed by repo name, with .Path and .Url properties) is rebuilt automatically — scripts use $RepoMap[$repo].Path instead of constructing paths manually.
$ReposRoot defaults to C:\repos and can be overridden by setting $env:REPOS_ROOT before dot-sourcing:
$env:REPOS_ROOT = "D:\repos"
. "$PSScriptRoot\repos.ps1"Clones all repos defined in repos.ps1 into C:\repos\. Safe to re-run — skips repos that already exist locally (checks for a .git subdirectory, not just the directory). Shows a progress bar during cloning. After each successful clone, automatically unblocks all .ps1 files in the cloned repo so they can run without execution policy prompts. Exits 1 if any clone fails.
.\clone-all-repos.ps1 # standard full clone
.\clone-all-repos.ps1 -Shallow # --depth 1 shallow clone (faster, less history)| Parameter | Description |
|---|---|
-Shallow |
Pass --depth 1 to git clone — useful on slow connections or when full history is not needed |
Runs git pull --ff-only in every repo. Run this at the start of every session before opening Claude Code. Uses --ff-only to surface diverged branches instead of silently creating merge commits. Warns if any repo is not on main (does not abort — legitimate branch work is allowed). Shows diff stats for updated repos. Exits 1 if any repo fails; exits 0 if all repos succeed (updated or already up to date).
.\pull-all-repos.ps1Shows current branch, last commit hash and subject, unpushed commit count, and changed files for every repo. Run before committing or pushing.
.\status-all-repos.ps1 # Full per-repo display
.\status-all-repos.ps1 -Table # Compact formatted table| Parameter | Description |
|---|---|
-Table |
Output a compact Format-Table view with one row per repo — useful for a quick glance at all repo states |
Copies live Python scripts to the HA Green Samba share. Run after any commit that modifies a script file. Performs a hard pre-flight check before touching the share (aborts if any source file is missing or the share is unreachable), then verifies SHA-256 hashes after each copy to catch Samba partial writes.
.\deploy-to-ha.ps1Three scripts are deployed to HA Green — all three must be in the deploy map:
| Local path | HA Green destination | Used by shell command |
|---|---|---|
home-assistant-config\scripts\climate_norms_today.py |
/config/scripts/climate_norms_today.py |
climate_norms_today |
home-assistant-config\scripts\csv_manager.py |
/config/scripts/csv_manager.py |
appenddailycsv, appendmonthlycsv, rotatedailycsv, backup_input_numbers |
home-assistant-config\scripts\setback_csv.py |
/config/scripts/setback_csv.py |
appendsetbacklog_1f, appendsetbacklog_2f |
Critical:
csv_manager.pyandsetback_csv.pyare called by six shell commands inconfiguration.yaml. Deploying onlyclimate_norms_today.pywill leave those commands broken on HA Green without any visible error until the next time they fire.
After deploying, reload in HA: Developer Tools → YAML → Reload Shell Commands
To add a new script in the future, add one line to the $deployMap hash table in deploy-to-ha.ps1 — no other changes needed.
Runs git push in every repo. Warns if any repo is not on main. Reports unpushed commit count per repo and prints a summary. Run status-all-repos.ps1 first to confirm what will go out.
.\push-all-repos.ps1Stages, commits, and pushes README.md and issues\ changes in the Tools repo in one command. Use this for README updates and issue drafts. Deliberately scoped — .ps1 scripts and other files are never staged by this command (those belong in a proper code commit). Does nothing if neither README.md nor issues\ has changes.
.\sync-notes.ps1 # auto-generates commit message from changed files
.\sync-notes.ps1 -Message "docs: add cooling buildout draft" # custom messageParameters:
| Parameter | Required | Description |
|---|---|---|
-Message |
No | Commit message. Auto-generated from changed files if omitted (e.g. "docs: update issue draft (ha-config_sensor-fix.md)") |
Note: this only syncs
README.mdandissues\in theToolsrepo. For code changes or changes across all repos usepush-all-repos.ps1.
Creates a tagged GitHub release using the gh CLI. Validates CalVer tag format before proceeding, auto-generates release notes from git log --oneline since the last tag if -Notes is omitted, and fetches the new tag back locally after creation so git describe is immediately accurate. Warns if used on the HVAC baseline repo and prompts for confirmation (recommending release.ps1 instead for its HALT validation gate).
.\create-release.ps1 -Repo home-assistant-config -Tag v2026.03.1 -Title "March 2026 — cooling build-out"
.\create-release.ps1 -Repo home-assistant-config -Tag v2026.03.1 -Title "March 2026" -Draft
.\create-release.ps1 -Repo home-assistant-config -Tag v2026.03.1 -Title "March 2026" -Notes "Custom notes here"Parameters:
| Parameter | Required | Description |
|---|---|---|
-Repo |
Yes | Repo name (not the full URL — just e.g. home-assistant-config) |
-Tag |
Yes | CalVer tag — must match vYYYY.MM.N (e.g. v2026.03.1). Script rejects malformed tags before any GitHub call |
-Title |
Yes | Release title shown on GitHub |
-Notes |
No | Release body text. Auto-generated from git log --oneline since last tag if omitted |
-Draft |
No | Switch. Pass -Draft to save as draft instead of publishing immediately |
Example — draft release for review before publishing:
.\create-release.ps1 -Repo Residential-HVAC-Performance-Baseline- -Tag v2026.03.1 -Title "March 2026 monthly update" -DraftInstalls pre-commit hooks in all managed repos. Run once after initial clone. Installs pre-commit via pip install --user if not present, then runs python -m pre_commit autoupdate in each repo so you never depend on stale pinned versions. After install, runs python -m pre_commit run --all-files to confirm hooks execute correctly. If autoupdate modifies .pre-commit-config.yaml, the script prints the exact git add and git commit commands needed to save the update.
Note: All pre-commit calls use
python -m pre_commit(not thepre-commitbinary). On Windows, a freshly pip-installedpre-commitbinary may not appear on PATH in the same PowerShell session. The module invocation always works.
.\install-precommit-all.ps1 # install hooks + autoupdate
.\install-precommit-all.ps1 -UpdateOnly # autoupdate only, skip install| Parameter | Description |
|---|---|
-UpdateOnly |
Skip pre-commit install and only run autoupdate — useful on re-runs when hooks are already installed |
Hook configuration file. Copy this into each of your four main repos (a one-liner is in the file header). The tools repo also carries its own copy.
Hooks included:
trailing-whitespace— removes trailing spacesend-of-file-fixer— ensures files end with a newlinecheck-yaml— validates YAML syntax (catches HA config errors before push)check-added-large-files— blocks accidental commits of files >500 KB (protects against CSVs)ruff— Python linting with auto-fixruff-format— Python formatting (Black-compatible)
Creates a new local issue file in Tools\issues\ from the standard template. The filename is derived from the repo and a slug you provide. Edit the file, then publish it with publish-issue.ps1.
.\new-issue.ps1 -Repo home-assistant-config -Slug "cooling-buildout"
.\new-issue.ps1 -Repo home-assistant-config -Slug "cooling-buildout" -Open # auto-opens after createParameters:
| Parameter | Required | Description |
|---|---|---|
-Repo |
Yes | Exact repo name |
-Slug |
Yes | Short kebab-case description, e.g. cooling-buildout. Becomes part of the filename |
-Open |
No | Switch. Opens the new file in VS Code if available, otherwise Notepad |
Creates: issues\ha-config_cooling-buildout.md (repo prefix is derived automatically). Files are written with a trailing newline so the pre-commit end-of-file-fixer hook does not flag them on every commit.
Reads the YAML frontmatter from a local issue file and creates the GitHub issue via gh. No parameters to remember — everything is in the file.
.\publish-issue.ps1 issues\ha-config_cooling-buildout.md
.\publish-issue.ps1 issues\ha-config_cooling-buildout.md -OpenInBrowserFrontmatter format (required at the top of every issue file):
---
repo: home-assistant-config
title: "feat: AC cooling build-out"
labels: enhancement,hvac
---The labels field accepts a comma-separated list — each label is passed as a separate --label argument to gh, so all labels are applied correctly.
The script validates that repo and title are present, warns if the body still contains template placeholder comments, and aborts if the repo name is not in repos.ps1.
After a successful publish, the script writes published_url back into the file's frontmatter:
published_url: https://github.com/wkcollis1-eng/home-assistant-config/issues/2This creates a bidirectional trace — the local file records where it was published, and re-running publish-issue.ps1 on the same file will warn before creating a duplicate.
Lists local issue drafts and/or open GitHub issues across all managed repos. Useful for reviewing what's queued locally before publishing, and for checking GitHub for duplicates. Local drafts that have already been published show a [published] indicator alongside their filename.
.\list-issues.ps1 # local drafts only
.\list-issues.ps1 -Remote # GitHub open issues, all repos
.\list-issues.ps1 -Remote -Repo home-assistant-config # one repo only
.\list-issues.ps1 -All # local drafts + GitHub issues side by side
.\list-issues.ps1 -Remote -Label bug # filter GitHub issues by label
.\list-issues.ps1 -Remote -State closed # show closed instead of open issues
.\list-issues.ps1 -All -Export json # export all results to timestamped JSON
.\list-issues.ps1 -All -Export csv # export all results to timestamped CSVParameters:
| Parameter | Required | Description |
|---|---|---|
-Repo |
No | Filter to a single repo. Works with all modes |
-Remote |
No | Switch. Fetch and display issues from GitHub via gh |
-All |
No | Switch. Show both local drafts and GitHub issues |
-Label |
No | Filter GitHub issues by label (passed as --label to gh). Also filters local drafts by label field in frontmatter |
-State |
No | GitHub issue state to query — open (default) or closed |
-Export |
No | Export results to a timestamped file: json or csv. Written to the Tools repo root |
Remote mode requires gh authenticated. Color-coded by repo — same scheme as status-all-repos.ps1.
Standard template for new issue files. Copied automatically by new-issue.ps1 — do not edit directly. Contains frontmatter fields and section headers that match the project's issue style.
Opens a GitHub issue in one of the managed repos. Requires gh (see Prerequisites). Displays the latest CalVer tag in the target repo for release reference context.
.\create-issue.ps1 -Repo home-assistant-config -Title "Fix DST in climate_norms_today.py"
.\create-issue.ps1 -Repo Residential-HVAC-Performance-Baseline- -Title "Add March 2026 data" -OpenInBrowser
.\create-issue.ps1 -Repo home-assistant-config -Title "Cooling build-out" -BodyFile "C:\repos\Tools\issue_body.md"
.\create-issue.ps1 -Repo home-assistant-config -Title "Fix bug" -Labels "bug","urgent" -Assignee wkcollis1-engParameters:
| Parameter | Required | Description |
|---|---|---|
-Repo |
Yes | Exact repo name (case-sensitive — see valid list in script) |
-Title |
Yes | Issue title |
-Body |
No | Issue body as an inline string |
-BodyFile |
No | Path to a .md file to use as the issue body — use this for long issues like build-out plans |
-Labels |
No | Array of label strings — each passed as a separate --label argument to gh (e.g. "bug","urgent") |
-Assignee |
No | GitHub username to assign the issue to |
-OpenInBrowser |
No | Switch. Opens the new issue URL in your browser immediately |
Note: If neither
-Bodynor-BodyFileis provided,ghopens your configured editor so you can write the body interactively.-BodyFileis the recommended approach for issues that already exist as Markdown documents (e.g.GITHUB_ISSUE_1_COOLING_BUILDOUT.md).
Runs validate_month.py from the Residential-HVAC-Performance-Baseline- repo. Validates all months in the CSV history by default, or a specific month. Exits 1 if any HALT-level check fails — safe to use as a gate before committing monthly data. The Python call is wrapped in try/finally so the working directory is always restored even if Python throws an uncaught exception.
.\validate-all.ps1 # validate all months
.\validate-all.ps1 -Month 2026-03 # validate March 2026 only
.\validate-all.ps1 -Json # also export results to validate-results.jsonParameters:
| Parameter | Required | Description |
|---|---|---|
-Month |
No | Month to validate. Accepts YYYY-MM or YYYY-MM-01. Omit to validate all months |
-Json |
No | Export pass/fail result and metadata to validate-results.json in the Tools root |
Checks run (V-HVAC-1 through V-HVAC-8):
- V-HVAC-1 — Runtime/HDD within ±2σ of trailing 3-month history
- V-HVAC-2 — Heating efficiency within ±15% of 90.3 CCF/1kHDD baseline
- V-HVAC-3 — Space heat CCF / HDD coherence (implied UA ±5% of 480 BTU/hr-°F)
- V-HVAC-4 — Monthly HDD65 vs 5,270 annual normal
- V-HVAC-5 — Zone balance 1F/2F within 45–55% (heating season only)
- V-HVAC-6 — DHW CCF ≤ prior-year same month +5%
- V-HVAC-7 —
daily_temperature.csvrow count = days in month - V-HVAC-8 —
monthly_hvac_runtime.csvhas exactly 2 rows per month
Output levels: ✅ PASS, ⚠️ WARN, 🚩 FLAG, 🛑 HALT. HALT stops the exit code at 1; WARN and FLAG are informational and allow the commit to proceed.
Orchestrates the full 1st-of-month data entry workflow as two distinct phases. Requires gh.
Phase 1 — before the Claude Code session: Pulls all repos, validates the prior month's existing data (the target month data doesn't exist yet — this confirms the baseline is clean before new entry begins), and creates a work queue issue with a pre-filled checklist.
.\monthly-update.ps1 -Month 2026-03 -Phase 1Phase 2 — after the Claude Code session: Validates the newly entered month, tags releases for repos with new commits, and pushes everything.
.\monthly-update.ps1 -Month 2026-03 -Phase 2 -Tag v2026.03.1
.\monthly-update.ps1 -Month 2026-03 -Phase 2 -Tag v2026.03.1 -DraftRelease # review before publishing
.\monthly-update.ps1 -Month 2026-03 -Phase 2 -SkipRelease # push without taggingParameters:
| Parameter | Required | Description |
|---|---|---|
-Month |
Yes | Month being entered. Accepts YYYY-MM or YYYY-MM-01 |
-Phase |
Yes | 1 (pre-session) or 2 (post-session) |
-Tag |
Phase 2 | CalVer release tag, e.g. v2026.03.1. Required unless -SkipRelease |
-SkipRelease |
No | Switch. Skip release tagging in Phase 2 (push only) |
-DraftRelease |
No | Switch. Create releases as drafts for review before publishing |
Phase 2 is smart about releases — it validates the -Tag format as CalVer (vYYYY.MM.N) before any GitHub call, checks git rev-list for each repo and only tags repos that have new commits since their last tag, and fetches the new tag locally after each successful create. Repos with no changes are skipped automatically.
Local issue files let you draft, review, and version-control issues before they hit GitHub — useful for large build-out plans like the cooling infrastructure issue.
# Shortcut — full publish workflow in one command:
.\publish-and-sync.ps1 issues\ha-config_filter-runtime-fix.md -OpenInBrowser
# Or step by step:
# 1. Create a new local issue file
.\new-issue.ps1 -Repo home-assistant-config -Slug "filter-runtime-fix" -Open
# 2. Edit the file in tools\issues\ha-config_filter-runtime-fix.md
# Fill in the title, body sections, and labels in the frontmatter
# 3. Check for duplicates before publishing
.\list-issues.ps1 -All -Repo home-assistant-config
# 4. Publish to GitHub when ready
.\publish-issue.ps1 issues\ha-config_filter-runtime-fix.md -OpenInBrowser
# 5. Commit the issue file to the tools repo for reference
git add issues\
git commit -m "docs: add filter runtime fix issue draft"
git pushFor large issues (like build-out plans generated in a Claude session), copy the markdown body into a new issue file and set the frontmatter — then publish-issue.ps1 handles the rest with no copy-paste into the GitHub UI.
cd C:\repos\Tools
.\session-start.ps1 # verify environment, pull, status — aborts on failure
# ... do your Claude Code session ...
.\session-end.ps1 # status, deploy prompt, push, syncThat's it. The scripts encode the full workflow — nothing to remember.
For sessions where you know you won't be deploying scripts:
.\session-end.ps1 -NoDeployFor sessions with only code changes and no doc updates:
.\session-end.ps1 -NoSyncOn the 1st of each month after utility bills arrive, monthly-update.ps1 handles the full sequence:
# Before your Claude Code session:
.\monthly-update.ps1 -Month 2026-03 -Phase 1
# (pulls all repos, validates data, opens a work queue issue with checklist)
# Run your Claude Code session to enter bill data and update CSVs
# After your Claude Code session:
.\monthly-update.ps1 -Month 2026-03 -Phase 2 -Tag v2026.03.1
# (validates the new month, tags releases for changed repos, pushes everything)If validation fails in Phase 2 with a HALT, the script stops before tagging or pushing. Fix the data error, then re-run Phase 2.
Hooks are shell commands that Claude Code runs automatically at lifecycle events — before or after tool use, on session start/stop, etc. The most useful application for this repo is running validate_month.py automatically every time Claude Code writes a CSV in the Residential-HVAC-Performance-Baseline- data directory. This closes the loop without requiring you to remember to run validation manually.
PostToolUse— fires after a tool completes. Output goes back to Claude as context; exit code 1 is a non-blocking warning; exit code 0 is success.- Matcher — filters which tool triggers the hook. Uses pipe syntax:
"Write|Edit|MultiEdit". Case-sensitive:WriteandEditare correct;writewon't match. - Settings file — project-level hooks go in
.claude/settings.jsonin the repo root. User-level hooks go in~/.claude/settings.jsonand apply to every project.
This hook runs validate_month.py automatically after any write to a CSV file in the data/ directory. It uses a thin Python wrapper to read the hook's stdin JSON, check the file path, and only run validation when a data CSV is actually being written — not on every file write in the repo.
Step 1 — Create the hook directory:
mkdir C:\repos\Residential-HVAC-Performance-Baseline-\.claude\hooksStep 2 — Create the wrapper script:
Save this as C:\repos\Residential-HVAC-Performance-Baseline-\.claude\hooks\validate_on_csv_write.py:
#!/usr/bin/env python3
"""
PostToolUse hook — runs validate_month.py after any CSV write in data/.
Reads tool event JSON from stdin (provided by Claude Code).
Exits 0 (PASS/WARN) or 1 (HALT) to mirror validate_month.py exit codes.
"""
import json
import os
import subprocess
import sys
data = json.load(sys.stdin)
file_path = data.get("tool_input", {}).get("file_path", "")
# Only trigger for CSV files in the data directory
if not (file_path.endswith(".csv") and "data" in file_path):
sys.exit(0)
# Run validate_month.py from repo root
repo_root = os.environ.get("CLAUDE_PROJECT_DIR", ".")
result = subprocess.run(
[sys.executable, "Scripts/validate_month.py"],
cwd=repo_root
)
# Exit code propagates back to Claude Code:
# 0 = PASS/WARN (Claude continues normally)
# 1 = HALT (Claude sees failure output and can stop itself)
sys.exit(result.returncode)Step 3 — Create the settings file:
Save this as C:\repos\Residential-HVAC-Performance-Baseline-\.claude\settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "python .claude/hooks/validate_on_csv_write.py"
}
]
}
]
}
}Step 4 — Commit both files:
cd C:\repos\Residential-HVAC-Performance-Baseline-
git add .claude/
git commit -m "chore: add PostToolUse validation hook for data CSV writes"During a monthly update Claude Code session, every time Claude writes or edits a file in data/, the hook fires. If the file isn't a CSV, the wrapper exits 0 immediately (no overhead). If it is a CSV, validate_month.py runs and its full output — ✅ PASS, ⚠️ WARN, 🚩 FLAG, or 🛑 HALT — appears in the Claude Code session. On a HALT, Claude sees the failure before it moves to the next step, giving it the opportunity to stop and report the issue rather than continuing with bad data.
This means validate-all.ps1 is still useful for a deliberate pre-push check, but you no longer need to remember to run it during the session — validation is automatic.
The .claude/settings.json file is independent of .pre-commit-config.yaml and does not need updating when you run pre-commit autoupdate. They are separate systems.
When a new .py script is added to home-assistant-config\scripts\ and referenced by a shell command in configuration.yaml, add one entry to the $deployMap in deploy-to-ha.ps1. The map uses $ReposRoot (from repos.ps1) and $SambaSharePath (from common.ps1) so paths adapt automatically to any -ReposRoot override:
$deployMap = @{
"$ReposRoot\home-assistant-config\scripts\climate_norms_today.py" = "$SambaSharePath\climate_norms_today.py"
"$ReposRoot\home-assistant-config\scripts\csv_manager.py" = "$SambaSharePath\csv_manager.py"
"$ReposRoot\home-assistant-config\scripts\setback_csv.py" = "$SambaSharePath\setback_csv.py"
# Add new scripts here:
"$ReposRoot\home-assistant-config\scripts\new_script.py" = "$SambaSharePath\new_script.py"
}No other changes are needed.
Edit repos.ps1 — add the repo name to $Repos and its clone URL to $RepoUrls. The $RepoMap (with .Path and .Url properties per repo) is rebuilt automatically. Every script that dot-sources repos.ps1 will automatically include the new repo. Then run .\clone-all-repos.ps1 to pull it locally.
C:\repos\
├── Tools\ ← this repo
│ ├── README.md
│ ├── repos.ps1 ← single source of truth for repo list
│ ├── common.ps1 ← shared Assert-Environment, dot-sourced by all scripts
│ ├── .pre-commit-config.yaml
│ ├── CLAUDE_TOOLS.md ← ultra-min Claude Code reference
│ ├── inject-tools-ref.ps1
│ ├── bootstrap.ps1
│ ├── session-start.ps1
│ ├── session-end.ps1
│ ├── publish-and-sync.ps1
│ ├── release.ps1
│ ├── verify-system.ps1
│ ├── clone-all-repos.ps1
│ ├── pull-all-repos.ps1
│ ├── push-all-repos.ps1
│ ├── status-all-repos.ps1
│ ├── sync-notes.ps1
│ ├── deploy-to-ha.ps1
│ ├── install-precommit-all.ps1
│ ├── create-release.ps1
│ ├── create-issue.ps1
│ ├── new-issue.ps1
│ ├── publish-issue.ps1
│ ├── list-issues.ps1
│ ├── validate-all.ps1
│ ├── monthly-update.ps1
│ └── issues\ ← local issue drafts
│ ├── TEMPLATE.md
│ └── ...
├── home-assistant-config\
├── Residential-HVAC-Performance-Baseline-\
│ ├── .claude\
│ │ ├── settings.json ← PostToolUse validation hook config
│ │ └── hooks\
│ │ └── validate_on_csv_write.py
│ └── ...
├── Lifepo4-Battery-Banks\
└── DIY-LiFePO4-UPS\
bootstrap.ps1 fails at a prerequisite check
Install the missing tool (git, python, or gh — links are printed), then re-run bootstrap.ps1. It is safe to re-run — completed steps are skipped.
release.ps1 fails with HALT on HVAC repo
validate-all.ps1 found a data integrity error for the target month. Fix the CSV data, commit the fix, and re-run release.ps1. Do not use -SkipValidation to bypass a legitimate data error.
publish-and-sync.ps1 shows duplicates but I want to publish anyway
Answer n at the duplicate prompt to cancel, or pass -SkipDuplicateCheck to bypass it. Note that if you publish a duplicate, publish-issue.ps1 will still succeed — it's your responsibility to close the duplicate on GitHub afterward.
session-start.ps1 aborts at environment check
verify-system.ps1 found a hard failure — the output above it will show exactly which check failed. Resolve it (missing tool, git identity, unauthenticated gh, diverged repo) and re-run session-start.ps1.
session-end.ps1 aborts at deploy step
deploy-to-ha.ps1 failed — hash mismatch or Samba share unreachable. Do not push until resolved. Fix the deploy issue and re-run session-end.ps1 -Deploy to resume from the deploy step.
session-end.ps1 deploy prompt always shows N default but I changed a .py file
The auto-detect uses @{u}..HEAD to capture all unpushed commits, not just the last one. If the .py change hasn't been committed yet it won't be detected. Commit first, then run session-end.ps1 or pass -Deploy to skip detection. If the repo has no upstream tracking branch yet, the diff falls back to HEAD~1..HEAD (single-commit guard).
Any script throws "Git identity not configured"
Assert-Environment in common.ps1 checks git config user.name and user.email before proceeding. Fix with:
git config --global user.name "Bill Collis"
git config --global user.email "your@email.com"Any script throws "gh CLI is not authenticated"
Run gh auth login and complete the browser flow. Assert-Environment -RequireGh verifies authentication, not just installation.
verify-system.ps1 shows FAIL for HA share
The Samba share is not mounted. Open File Explorer, navigate to \\homeassistant\config, enter HA credentials. Re-run after connecting.
verify-system.ps1 shows WARN for unpushed commits
Not a blocker — the session can proceed. Push when the session is complete with .\push-all-repos.ps1.
deploy-to-ha.ps1 aborts with "missing source file"
The script now hard-aborts before touching the share if any source file is missing. Confirm the script is committed and pulled: .\pull-all-repos.ps1, then retry.
deploy-to-ha.ps1 aborts with "Samba share not accessible"
The share is not mounted. Open File Explorer, navigate to \\homeassistant\config, and enter HA credentials when prompted. Once connected, re-run the script.
deploy-to-ha.ps1 reports "HASH MISMATCH"
The file copied to the share does not match the source — likely a Samba partial write. Re-run deploy-to-ha.ps1 immediately. Do not reload HA shell commands until the script reports all hashes verified.
pull-all-repos.ps1 or push-all-repos.ps1 warns about a non-main branch
This is a warning, not an error — the script continues. If the branch is unintentional, cd C:\repos\<repo> and git checkout main before proceeding. If it is intentional, the warning can be ignored.
monthly-update.ps1 Phase 2 aborts with "Uncommitted changes"
The clean working tree guard fired. Commit or stash the changes in the flagged repo, then re-run Phase 2.
pre-commit blocks a commit with a YAML error
Fix the YAML in the flagged file, then git add the file again and retry the commit. The hook output will show the exact line number.
gh is not recognized
GitHub CLI is not installed or not on PATH. Install from https://cli.github.com and restart PowerShell.
create-release.ps1 fails with "release already exists"
A tag with that name was already pushed. Either delete the tag on GitHub and re-run, or increment the patch number (e.g. v2025.06.2).
validate-all.ps1 reports "validate_month.py not found"
The Residential-HVAC-Performance-Baseline- repo hasn't been cloned or pulled. Run .\pull-all-repos.ps1 first.
validate-all.ps1 exits with HALT
One or more months have data integrity issues — the output will identify which check (V-HVAC-1 through V-HVAC-8) failed and why. Fix the CSV data for the flagged month and re-run before committing. Do not bypass a HALT.
monthly-update.ps1 stops at Phase 2 before pushing
Validation failed with a HALT. Fix the data error in the relevant CSV, commit the fix, then re-run .\monthly-update.ps1 -Month YYYY-MM -Phase 2 -Tag vYYYY.MM.N.
publish-issue.ps1 fails with "Frontmatter missing repo or title"
The file is missing the --- frontmatter block or the fields are malformed. Open the file and confirm the top looks exactly like the template — --- on its own line, then repo:, title:, labels:, then --- again.
publish-issue.ps1 warns about template placeholders
The body still contains <!-- comment text from the template. Either fill in the sections or answer y at the prompt to publish anyway (useful for minimal issues).
create-issue.ps1 fails silently or creates an empty issue
If neither -Body nor -BodyFile is provided and no editor is configured, gh may create an issue with an empty body. Set a default editor: gh config set editor notepad and re-run.
Hook doesn't fire when Claude Code writes a CSV
Confirm .claude/settings.json is in the repo root (not a subdirectory). Confirm the matcher is "Write|Edit|MultiEdit" with exact casing. Confirm python is on your PATH (python --version in PowerShell). If Claude Code is writing via Bash rather than the Write tool, add Bash to the matcher: "Write|Edit|MultiEdit|Bash".
Hook fires but validate_month.py output doesn't appear in the session
The hook command must write to stdout for Claude Code to capture it. validate_month.py uses print() which goes to stdout — this is correct. If output is missing, the hook may be exiting before the script runs. Add a print(f"Hook triggered for: {file_path}") at the top of the wrapper script temporarily to confirm it's being called.
git pull reports merge conflicts
Do not force-push. Resolve conflicts manually in the affected file, then git add and git commit. If Claude Code made the conflicting commit, check CLAUDE.md for the session rules that govern how it handles existing content.