From e2962e44567889c52553c745f688eb47226e2676 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Sun, 13 Sep 2026 16:44:44 -0700 Subject: [PATCH 1/2] feat(scripts): add new-worktree.sh scripts/new-worktree.sh [] fetches origin, fast-forwards local main where that is safe and refuses when main holds commits origin/main lacks, creates ../span- on a branch that tracks no remote so a bare push can never land on main, links .env from the primary checkout, and runs the new worktree's setup-hooks.sh. From beside the primary checkout, ../../span/span-panel-api still resolves to the library, so no bridge is needed. The worktree gets a virtual environment of its own: editable installs record one checkout's source path, so a shared environment imports whichever checkout synced last. When a library worktree of the same name exists, the script prints the three-distribution `uv pip install -e` command rather than running it, because the next `uv sync` silently undoes that override. Main is moved with update-ref rather than `branch --force`, which would also rewrite its upstream, and a checked-out main is fast-forwarded only when it has no uncommitted tracked changes. --- developer.md | 12 +++++ scripts/new-worktree.sh | 112 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100755 scripts/new-worktree.sh diff --git a/developer.md b/developer.md index c790f5c7..39565b95 100644 --- a/developer.md +++ b/developer.md @@ -75,6 +75,18 @@ echo 'dotenv' > .envrc direnv allow ``` +## Worktrees + +Create worktrees with the script rather than `git worktree add`: + +```bash +./scripts/new-worktree.sh [] # defaults to feat/ +``` + +It fetches `origin`, fast-forwards `main` where that is safe, creates `../span-` on a branch that does not track a remote, links `.env` from the primary +checkout, and runs `setup-hooks.sh` inside it. The worktree gets a virtual environment of its own, which resolves the library from `../../span/span-panel-api` +like the primary checkout does. When a library worktree of the same name exists, the script ends by printing the command to install it, described below. + ## Working against unreleased library code `pyproject.toml` installs `span-panel-api` and both adapters from `../../span/span-panel-api` as editable path dependencies. That is a committed, shared file: diff --git a/scripts/new-worktree.sh b/scripts/new-worktree.sh new file mode 100755 index 00000000..b287edca --- /dev/null +++ b/scripts/new-worktree.sh @@ -0,0 +1,112 @@ +#!/bin/bash +# +# Create a worktree for a branch of work beside the primary checkout, and prepare +# it with setup-hooks.sh. +# +# Usage: scripts/new-worktree.sh [] +# +# The worktree is created at ../-, beside the primary +# checkout. From there `../../span/span-panel-api` still resolves to the +# library checkout, so no bridge is needed. +# The branch to create. Defaults to feat/. +# +# The branch starts from origin/main as just fetched and does not track it, so a +# bare `git push` can never land on main. Local main is fast-forwarded as well where +# that is safe, and the script refuses when local main holds commits origin/main +# lacks, because branching from origin/main would silently leave them out. +# +# The worktree gets a virtual environment of its own rather than a link to the +# primary checkout's. The environment holds editable installs bound to one checkout's +# sources, so a shared one imports whichever checkout synced last and tests pass +# against the wrong code without an error. uv clones packages from its cache, so the +# second environment costs little. Local configuration that is not bound to a path, +# `.env`, is linked instead. +# +# When a library worktree of the same name exists, the command to test against it is +# printed rather than run: it is a local override that the next `uv sync` silently +# undoes, so it should be a step you take knowingly. See developer.md, "Working +# against unreleased library code". + +set -euo pipefail + +usage() { + echo "Usage: $0 []" >&2 + exit 2 +} + +[[ $# -ge 1 && $# -le 2 ]] || usage +name=$1 +if [[ ! "$name" =~ ^[A-Za-z0-9._-]+$ ]]; then + echo "Worktree name '$name' may use only letters, digits, dots, underscores and hyphens." >&2 + exit 2 +fi +branch=${2:-feat/$name} + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +checkout=$(cd "$script_dir/.." && pwd) +common_dir=$(git -C "$checkout" rev-parse --path-format=absolute --git-common-dir) +primary_root=$(dirname "$common_dir") +target="$(dirname "$primary_root")/$(basename "$primary_root")-$name" + +if [[ -e "$target" ]]; then + echo "$target already exists." >&2 + exit 1 +fi +if git -C "$primary_root" show-ref --verify --quiet "refs/heads/$branch"; then + echo "Branch $branch already exists." >&2 + exit 1 +fi + +echo "Fetching origin..." +git -C "$primary_root" fetch origin + +if git -C "$primary_root" show-ref --verify --quiet refs/heads/main; then + ahead=$(git -C "$primary_root" rev-list --count origin/main..main) + if ((ahead > 0)); then + echo "Local main has $ahead commit(s) that origin/main does not. Reconcile main first;" >&2 + echo "branching from origin/main would silently leave them out." >&2 + exit 1 + fi + + # Where main is checked out, move it only if that checkout is clean. + main_checkout=$(git -C "$primary_root" worktree list --porcelain | + awk '/^worktree /{wt = substr($0, 10)} /^branch refs\/heads\/main$/{print wt}') + if [[ -z "$main_checkout" ]]; then + # update-ref rather than `branch --force`: that would also rewrite main's + # upstream to origin/main, silently re-pointing a main that tracks another + # remote. The old value guards against main moving since it was checked. + git -C "$primary_root" update-ref -m "new-worktree.sh: fast-forward main to origin/main" \ + refs/heads/main "$(git -C "$primary_root" rev-parse refs/remotes/origin/main)" \ + "$(git -C "$primary_root" rev-parse refs/heads/main)" + elif [[ -n "$(git -C "$main_checkout" status --porcelain --untracked-files=no)" ]]; then + echo "main is checked out with uncommitted changes at $main_checkout; leaving it as it is." + elif ! git -C "$main_checkout" merge --ff-only --quiet origin/main; then + # Untracked files are not counted as changes above, because a fast-forward + # refuses to overwrite one; this is where that refusal lands. + echo "Could not fast-forward main at $main_checkout; leaving it as it is." + fi +fi + +echo "Creating $target on $branch from origin/main..." +git -C "$primary_root" worktree add --no-track -b "$branch" "$target" origin/main + +if [[ -f "$primary_root/.env" && ! -e "$target/.env" ]]; then + ln -s "$primary_root/.env" "$target/.env" + echo "Linked .env from the primary checkout." +fi + +echo "Preparing the worktree..." +(cd "$target" && bash ./setup-hooks.sh) + +echo +echo "Worktree ready: $target" +echo "Branch: $branch, from $(git -C "$target" rev-parse --short HEAD), not tracking a remote" + +if [[ -d "$target/../../span/span-panel-api-$name" ]]; then + echo + echo "A library worktree of the same name exists. To test against it, install all three of" + echo "its distributions into this worktree's environment; the next \`uv sync\` undoes it:" + echo " uv pip install -e ../../span/span-panel-api-$name \\" + echo " -e ../../span/span-panel-api-$name/packages/schema-0 \\" + echo " -e ../../span/span-panel-api-$name/packages/schema-1" +fi From 368101ee07187bf23b76281d0a989f56e6f69839 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Sun, 13 Sep 2026 16:45:46 -0700 Subject: [PATCH 2/2] docs: add worktree instructions and import AGENTS.md into CLAUDE.md AGENTS.md gains a Worktrees section: create worktrees with scripts/new-worktree.sh rather than `git worktree add` or a built-in worktree tool, never share a virtual environment between checkouts, and install a library worktree explicitly when testing against it. The script joins the permitted operations. CLAUDE.md imports AGENTS.md. Claude Code reads CLAUDE.md and not AGENTS.md, so until now none of these instructions reached it, including the translation workflow, where documentation artifacts belong, and the service registration requirements. --- AGENTS.md | 18 +++++++++++++++++- CLAUDE.md | 5 +++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md index 01e96c86..05ac9bc7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ The following operations are benign and should be executed without confirmation. - Compile or syntax-check Python (`python -c`, `python -m py_compile`) - Activate or use the virtual environment (`.venv/bin/python`, `source .venv/bin/activate`) - Run pre-commit hooks (`git commit` triggers hooks automatically) -- Run project scripts (`./scripts/fix-markdown.sh`) +- Run project scripts (`./scripts/fix-markdown.sh`, `./scripts/new-worktree.sh`) - Standard git operations (`git status`, `git diff`, `git log`, `git add`, `git commit`) - Package management (`poetry`, `uv`, `pip install`) - File inspection (`ls`, `find`, `wc`, `head`, `tail`, `diff`, `cat`) @@ -64,6 +64,22 @@ those files in the `SpanPanel_Docs/span/docs/` folder in the workspace — not i Do not commit documentation artifacts to the `span` integration repo itself. +## Worktrees + +Create worktrees with `scripts/new-worktree.sh`, never with `git worktree add` or an agent's built-in worktree tool: + +```bash +./scripts/new-worktree.sh [] # defaults to feat/ +``` + +It creates `../span-` beside the primary checkout from a freshly fetched `origin/main`, prepares it with `setup-hooks.sh`, and links `.env`. From there +`../../span/span-panel-api` still resolves to the library checkout, so no bridge is needed. + +- **Never share a virtual environment between checkouts.** Editable installs record one checkout's source path, so a shared `.venv` imports whichever checkout + synced last and tests pass against the wrong code without an error. +- **To test against a library worktree, install it explicitly.** When a library worktree of the same name exists, the script prints the `uv pip install -e` + command rather than running it, because the next `uv sync` silently undoes it. See `developer.md`, "Working against unreleased library code". + ## Service Registration Requirements All services registered by this integration MUST conform to the following requirements. These align with Home Assistant core's service architecture and ensure diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..0010d05b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +# Claude Code + + + +@AGENTS.md