diff --git a/CLAUDE.md b/CLAUDE.md index 86e9ecb..33e98f8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,12 +1,8 @@ # Clayde -**Name:** Clayde -**Email:** clayde@vtettenborn.net -**GitHub:** @ClaydeCode +Clayde is a persistent autonomous AI software agent running in a Docker container. My purpose is to help with software development by working on GitHub issues assigned to me. When assigned an issue, I analyze the relevant codebase, implement a solution, open a pull request, and post a comment on the issue summarizing what I did. -Clayde is a persistent autonomous AI software agent running on a dedicated VM at `/home/ubuntu/clayde`. My purpose is to help with software development by working on GitHub issues assigned to me. When assigned an issue, I analyze the relevant codebase, implement a solution, open a pull request, and post a comment on the issue summarizing what I did. - -The `gh` CLI is authenticated as @ClaydeCode and git is configured with my name and email. +The `gh` CLI is authenticated as the configured bot GitHub account and git is configured with the identity from `CLAYDE_GIT_NAME` and `CLAYDE_GIT_EMAIL`. --- @@ -27,7 +23,7 @@ The `gh` CLI is authenticated as @ClaydeCode and git is configured with my name - **Container layout:** Application code at `/opt/clayde`, data at `/data` (single volume mount from host `./data`) - **Claude:** Dual backend — Anthropic Python SDK (`api`) or Claude Code CLI (`cli`), selected by `CLAYDE_CLAUDE_BACKEND` - **Git credential helper:** `gh auth git-credential` (configured globally in the container) -- **Git identity:** `user.name = Clayde`, `user.email = clayde@vtettenborn.net` +- **Git identity:** configured at container startup from `CLAYDE_GIT_NAME` and `CLAYDE_GIT_EMAIL` env vars --- @@ -39,7 +35,6 @@ pyproject.toml # hatchling build; console scripts: clayde, clayde-once CLAUDE.md # this file — identity + project context Dockerfile # Python 3.13-slim image with git, gh, uv docker-compose.yml # container deployment config -gh-issue.md # slash-command prompt for interactive issue work uv.lock src/clayde/ __init__.py @@ -99,10 +94,12 @@ Plain `KEY=VALUE` file (no shell quoting). All keys use `CLAYDE_` prefix and are | Key | Purpose | |-----|---------| -| `CLAYDE_GITHUB_TOKEN` | Fine-grained PAT with Issues R/W, Pull Requests R/W, Contents R/W | -| `CLAYDE_GITHUB_USERNAME` | `ClaydeCode` | +| `CLAYDE_GITHUB_TOKEN` | Classic PAT with full `repo` scope | +| `CLAYDE_GITHUB_USERNAME` | The bot account username (e.g. `YourBotName`) | | `CLAYDE_ENABLED` | Set to `true` to activate; any other value causes immediate exit | -| `CLAYDE_WHITELISTED_USERS` | Comma-separated list of trusted GitHub usernames (e.g. `max-tet,ClaydeCode`) | +| `CLAYDE_WHITELISTED_USERS` | Comma-separated list of trusted GitHub usernames | +| `CLAYDE_GIT_NAME` | Git commit author name (defaults to `CLAYDE_GITHUB_USERNAME` if not set) | +| `CLAYDE_GIT_EMAIL` | Git commit author email (required) | | `CLAYDE_CLAUDE_API_KEY` | Anthropic API key for Claude SDK calls (required when backend=`api`) | | `CLAYDE_CLAUDE_MODEL` | Model to use (default: `claude-opus-4-6`) | | `CLAYDE_CLAUDE_BACKEND` | `api` (default) or `cli` — selects Anthropic SDK or Claude Code CLI | @@ -290,16 +287,6 @@ Logger names: `clayde.orchestrator`, `clayde.tasks.plan`, `clayde.tasks.implemen --- -## Interactive Issue Work (`gh-issue.md`) - -The file `gh-issue.md` is a Claude Code slash-command prompt (`/gh-issue `) for working on issues interactively (outside cron). It runs as a multi-step subagent workflow: Plan → clarify → implement → self-review → address review → return PR URL. Sends push notifications via `apprise ntfy://7yuau0vyes`. - -Allowed tools for interactive work: `Bash(gh:*)`, `Bash(git:*)`, `Bash(just:*)`, `Bash(python:*)`, `Bash(pytest:*)`, `Bash(npm:*)`, `Bash(uv:*)`, `Bash(apprise:*)`, `Read`, `Write`, `Edit`, `Glob`, `Grep` - -Branch naming for interactive work: `issue/{number}-short-desc` - ---- - ## Testing Run the test suite after any feature development or bug fix: diff --git a/Dockerfile b/Dockerfile index 755e942..65a8894 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,14 +37,16 @@ COPY src/ src/ COPY CLAUDE.md ./ RUN uv sync --frozen --no-dev +# Copy entrypoint script +COPY entrypoint.sh /opt/clayde/entrypoint.sh +RUN chmod +x /opt/clayde/entrypoint.sh + # Create data directories and set ownership RUN mkdir -p /data/repos /data/logs && chown -R clayde:clayde /data -# Switch to non-root user and configure git +# Switch to non-root user and configure git credential helper USER clayde RUN mkdir -p /home/clayde/.claude && \ - git config --global credential.helper '!gh auth git-credential' && \ - git config --global user.name "Clayde" && \ - git config --global user.email "clayde@vtettenborn.net" + git config --global credential.helper '!gh auth git-credential' -ENTRYPOINT ["/opt/clayde/.venv/bin/clayde"] +ENTRYPOINT ["/opt/clayde/entrypoint.sh"] diff --git a/README.md b/README.md index 78d752d..5b875d5 100644 --- a/README.md +++ b/README.md @@ -143,10 +143,56 @@ Clayde will start its loop, checking for assigned issues every 5 minutes (config | Key | Purpose | |---|---| -| `CLAYDE_GITHUB_TOKEN` | Fine-grained PAT (Issues R/W, PRs R/W, Contents R/W) | -| `CLAYDE_GITHUB_USERNAME` | `ClaydeCode` | +| `CLAYDE_GITHUB_TOKEN` | Classic PAT with full `repo` scope | +| `CLAYDE_GITHUB_USERNAME` | The bot account username | +| `CLAYDE_GIT_NAME` | Git commit author name (defaults to `CLAYDE_GITHUB_USERNAME` if not set) | +| `CLAYDE_GIT_EMAIL` | Git commit author email (required) | | `CLAYDE_ENABLED` | Set to `true` to activate | | `CLAYDE_WHITELISTED_USERS` | Comma-separated trusted GitHub usernames | | `CLAYDE_CLAUDE_BACKEND` | `api` (default) or `cli` | | `CLAYDE_CLAUDE_API_KEY` | Anthropic API key (required when backend=`api`) | | `CLAYDE_CLAUDE_MODEL` | Model to use (default: `claude-opus-4-6`) | + +--- + +## Deploying Your Own Instance + +Clayde is designed to be deployed by anyone. To run your own instance: + +### 1. Create a dedicated bot GitHub account + +Create a GitHub account for your bot (e.g. `my-bot`). This is the account that will be assigned issues and open pull requests. + +### 2. Create a GitHub Personal Access Token for the bot + +From the bot account, create a classic personal access token with the full **`repo`** scope. + +### 3. Configure the instance + +```bash +mkdir -p data/logs data/repos +cp config.env.template data/config.env +``` + +Edit `data/config.env`: + +``` +CLAYDE_GITHUB_TOKEN=github_pat_... +CLAYDE_GITHUB_USERNAME=my-bot +CLAYDE_GIT_EMAIL=my-bot@example.com +CLAYDE_ENABLED=true +CLAYDE_WHITELISTED_USERS=your-username,my-bot +``` + +### 4. Choose a Claude backend and start + +Follow the backend instructions in the [Setup](#setup) section above, then run: + +```bash +docker compose up -d +``` + +### 5. Assign issues to your bot + +In any repository the bot has access to, assign issues to the bot account. Clayde will pick them up automatically on the next loop cycle. + diff --git a/config.env.template b/config.env.template index 89f67dd..38f3227 100644 --- a/config.env.template +++ b/config.env.template @@ -1,10 +1,20 @@ -# Clayde configuration — copy to config.env and fill in values +# Clayde configuration — copy to data/config.env and fill in your values + +# GitHub identity: the bot account Clayde runs as +CLAYDE_GITHUB_USERNAME=your-bot-username CLAYDE_GITHUB_TOKEN= -CLAYDE_GITHUB_USERNAME=ClaydeCode + +# Git identity for commits (name defaults to CLAYDE_GITHUB_USERNAME if not set) +CLAYDE_GIT_NAME= +CLAYDE_GIT_EMAIL=your-bot@example.com + +# Set to true to activate Clayde; any other value causes immediate exit CLAYDE_ENABLED=false -CLAYDE_WHITELISTED_USERS_RAW=max-tet,ClaydeCode + +# Comma-separated list of trusted GitHub usernames allowed to approve plans +CLAYDE_WHITELISTED_USERS=your-username,your-bot-username # Claude backend: "api" (Anthropic SDK, requires CLAYDE_CLAUDE_API_KEY) -# "cli" (Claude Code CLI, requires OAuth credentials mounted) +# "cli" (Claude Code CLI, requires OAuth credentials mounted) CLAYDE_CLAUDE_BACKEND=api CLAYDE_CLAUDE_API_KEY= diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..43fa768 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/sh +# Configure git identity from environment variables at container startup. +# CLAYDE_GIT_NAME defaults to CLAYDE_GITHUB_USERNAME if not set. +# CLAYDE_GIT_EMAIL is required. + +GIT_NAME="${CLAYDE_GIT_NAME:-$CLAYDE_GITHUB_USERNAME}" +GIT_EMAIL="${CLAYDE_GIT_EMAIL}" + +if [ -z "$GIT_NAME" ]; then + echo "ERROR: CLAYDE_GIT_NAME (or CLAYDE_GITHUB_USERNAME) must be set" >&2 + exit 1 +fi + +if [ -z "$GIT_EMAIL" ]; then + echo "ERROR: CLAYDE_GIT_EMAIL must be set" >&2 + exit 1 +fi + +git config --global user.name "$GIT_NAME" +git config --global user.email "$GIT_EMAIL" + +exec /opt/clayde/.venv/bin/clayde "$@" diff --git a/src/clayde/config.py b/src/clayde/config.py index c879566..7c1c7d9 100644 --- a/src/clayde/config.py +++ b/src/clayde/config.py @@ -20,12 +20,18 @@ class Settings(BaseSettings): ) github_token: str = "" - github_username: str = "ClaydeCode" + github_username: str = "" enabled: bool = False - whitelisted_users: str = "max-tet,ClaydeCode" + whitelisted_users: str = "" claude_api_key: str = "" claude_model: str = "claude-opus-4-6" claude_backend: str = "api" # "api" or "cli" + git_name: str = "" + git_email: str = "" + + @property + def effective_git_name(self) -> str: + return self.git_name or self.github_username # Claude invocation tuning claude_tool_loop_timeout_s: int = 1800 diff --git a/tests/test_config.py b/tests/test_config.py index abd6116..bb670fb 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -22,11 +22,24 @@ def test_defaults(self, monkeypatch): monkeypatch.delenv("CLAYDE_GITHUB_TOKEN", raising=False) monkeypatch.delenv("CLAYDE_ENABLED", raising=False) monkeypatch.delenv("CLAYDE_WHITELISTED_USERS", raising=False) + monkeypatch.delenv("CLAYDE_GITHUB_USERNAME", raising=False) s = Settings(_env_file=None) assert s.github_token == "" assert s.enabled is False - assert s.github_username == "ClaydeCode" - assert s.whitelisted_users_list == ["max-tet", "ClaydeCode"] + assert s.github_username == "" + assert s.whitelisted_users_list == [] + + def test_effective_git_name_falls_back_to_username(self, monkeypatch): + monkeypatch.setenv("CLAYDE_GITHUB_USERNAME", "my-bot") + monkeypatch.delenv("CLAYDE_GIT_NAME", raising=False) + s = Settings(_env_file=None) + assert s.effective_git_name == "my-bot" + + def test_effective_git_name_uses_explicit_value(self, monkeypatch): + monkeypatch.setenv("CLAYDE_GITHUB_USERNAME", "my-bot") + monkeypatch.setenv("CLAYDE_GIT_NAME", "My Bot") + s = Settings(_env_file=None) + assert s.effective_git_name == "My Bot" def test_loads_from_env_file(self, tmp_path, monkeypatch): env_file = tmp_path / "config.env"