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