Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down Expand Up @@ -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 <name> [<branch>] # <branch> defaults to feat/<name>
```

It creates `../span-<name>` 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
Expand Down
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Claude Code

<!-- Claude Code reads CLAUDE.md, not AGENTS.md. This imports AGENTS.md so every agent works from the same instructions. -->

@AGENTS.md
12 changes: 12 additions & 0 deletions developer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name> [<branch>] # <branch> defaults to feat/<name>
```

It fetches `origin`, fast-forwards `main` where that is safe, creates `../span-<name>` 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:
Expand Down
112 changes: 112 additions & 0 deletions scripts/new-worktree.sh
Original file line number Diff line number Diff line change
@@ -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 <name> [<branch>]
#
# <name> The worktree is created at ../<repository>-<name>, beside the primary
# checkout. From there `../../span/span-panel-api` still resolves to the
# library checkout, so no bridge is needed.
# <branch> The branch to create. Defaults to feat/<name>.
#
# 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 <name> [<branch>]" >&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
Loading