---
name: setup
description: "Use when configuring a new machine to run the Valor Telegram bridge. Installs all dependencies, authentication, and service startup. Triggered by 'setup', 'configure this machine', or 'new machine setup'."
disable-model-invocation: true
---

# Setup - New Machine Configuration

Configure this machine to run the Valor Telegram bridge. You do everything except the interactive Telegram login step.

## Prerequisites

**PREREQUISITE: Must be on latest main branch before running.**

```bash
cd ~/src/ai && git checkout main && git pull
```

Before starting, confirm the user has:
- Python 3.12+ installed
- The `ai` repo cloned at `~/src/ai` **on the main branch with latest changes pulled**
- Telegram API credentials (api_id and api_hash from https://my.telegram.org). If they don't have these, pause and explain how to get them before continuing.

### Ensure bare `python` resolves to Python 3.12+

Claude Code hooks invoke bare `python` under `/bin/sh`, which does not honor zsh aliases. macOS does not ship a `python` binary by default — only `python3`. Without this symlink every hook that uses `python` silently fails with `command not found`, surfacing errors in the UI and disabling validators (no-raw-redis-delete, plan-section checks, SDLC reminders, etc.).

```bash
# Verify python3 is 3.12+
python3 --version

# Create the symlink in a user-writable PATH dir (no sudo)
ln -sf "$(command -v python3)" /opt/homebrew/bin/python

# Confirm /bin/sh resolves it
/bin/sh -c 'python --version'  # expected: Python 3.12.x or newer
```

The update orchestrator (`scripts/update/run.py`) verifies this via `check_python_alias()` and fails loudly if missing.

### Bootstrap cross-machine shell env loader

Cross-machine secrets and shell config live in `~/Desktop/Valor/` (iCloud-synced). `~/.zshenv` itself does NOT sync (it's in `$HOME`), so each new machine needs a one-line bootstrap that sources the vault loader. The update script self-heals this on every run via `scripts/update/zshenv_sync.py`, but on a fresh machine the easiest path is to run that module directly before the first `/update`:

```bash
cd ~/src/ai
.venv/bin/python -c "from scripts.update.zshenv_sync import sync_zshenv; r = sync_zshenv(); print(r)"
```

That:
- Seeds `~/Desktop/Valor/zshenv.sh` with a default loader if missing (only the very first machine ever does this — subsequent machines inherit the file via iCloud).
- Appends a `[ -f ~/Desktop/Valor/zshenv.sh ] && source ...` guard to `~/.zshenv` if missing.

After it runs, open a fresh shell and confirm a shared secret is loaded (e.g., `echo "${SENTRY_PERSONAL_TOKEN:+set}"` — should print `set` if the vault `.env` defines it). If the vault hasn't synced yet, the guard line is still safe (it's `[ -f ]`-gated) and will activate as soon as iCloud lands the file.

If you need to add new cross-machine shell config later (PATH tweaks shared across all Valor machines, shell functions, etc.), edit `~/Desktop/Valor/zshenv.sh` directly — it syncs everywhere automatically. Keep host-specific config in the local `~/.zshenv` or `~/.zshrc`.

## Step 1: Install uv Package Manager

We use `uv` for fast, reliable Python package management (much faster than pip).

```bash
# Check if uv is already installed
if ! command -v uv &> /dev/null; then
  echo "Installing uv package manager..."
  curl -LsSf https://astral.sh/uv/install.sh | sh
  export PATH="$HOME/.local/bin:$PATH"
fi

# Verify installation
uv --version
```

## Step 2: Virtual Environment & Dependencies

```bash
cd ~/src/ai

# Create virtual environment with uv (auto-creates with pip support)
uv venv

# Sync all dependencies including dev tools from pyproject.toml
uv sync --all-extras

# Install package in editable mode (registers CLI tools)
uv pip install -e .
```

This will:
- Create `.venv/` with Python 3.12 (or latest)
- Install all dependencies including:
  - Telegram bridge (telethon, httpx)
  - Claude SDK integration (anthropic, claude-agent-sdk)
  - Google Calendar (google-auth-oauthlib, google-api-python-client)
  - Job queue (popoto, redis)
  - Summarization (ollama)
  - Dev tools (pytest, ruff, mypy)
- Register CLI tools (`valor-calendar`, `valor-telegram`)

Verify key imports work:

```bash
.venv/bin/python -c "import telethon; import httpx; import dotenv; import anthropic; import google_auth_oauthlib; print('Dependencies OK')"
```

If this fails, debug before continuing.

## Step 3: Environment File (.env)

Check if `.env` exists. If not:

```bash
cp .env.example .env
```

**Ask the user** which project(s) this machine should monitor. The available projects are defined in `~/Desktop/Valor/projects.json` -- check the full list there. Common options:
- Single project: `ACTIVE_PROJECTS=psyoptimal`
- Multiple: `ACTIVE_PROJECTS=valor,popoto`
- All: `ACTIVE_PROJECTS=valor,django-project-template,popoto,psyoptimal,flutter-project-template,cuttlefish,yudame-research`

Edit `.env` and ensure these are set:

| Variable | Required | Notes |
|----------|----------|-------|
| `ACTIVE_PROJECTS` | Yes | Comma-separated project keys |
| `ANTHROPIC_API_KEY` | Yes | Starts with `sk-ant-` |
| `TELEGRAM_API_ID` | Yes | Numeric, from my.telegram.org |
| `TELEGRAM_API_HASH` | Yes | Hex string, from my.telegram.org |
| `TELEGRAM_PHONE` | Yes | With country code, e.g. `+1234567890` |
| `TELEGRAM_PASSWORD` | If 2FA on | Telegram 2FA password |
| `TELEGRAM_SESSION_NAME` | No | Defaults to `valor_bridge` |

If any required values are placeholder/missing, ask the user to provide them. The shared API keys file at `~/src/.env` may have `ANTHROPIC_API_KEY` and other keys -- check there first.

## Step 4: Google Calendar Configuration

Set up Google Calendar integration for work time tracking.

### 4.1 Get Google OAuth credentials

Check if credentials exist:

```bash
ls ~/Desktop/Valor/google_credentials.json
```

If missing, ask the user to:
1. Go to Google Cloud Console (project: Yudame General)
2. Enable Google Calendar API
3. Create OAuth 2.0 Client ID (Desktop app)
4. Download JSON and save to `~/Desktop/Valor/google_credentials.json`

### 4.2 Run OAuth consent flow

Check if token already exists:

```bash
ls ~/Desktop/Valor/google_token.json 2>/dev/null
```

If no token exists, run the OAuth flow:

```bash
cd ~/src/ai

# This will open browser for Google OAuth consent
.venv/bin/valor-calendar --reauth
```

The user must complete the OAuth consent in their browser. After completion, verify the token is valid:

```bash
.venv/bin/valor-calendar --check
```

### 4.3 Create calendar mappings

The calendar config is auto-generated by the `/update` command. For now, ensure the Google Calendars exist with matching names:

**Required calendars in Google Calendar:**
- **"Internal Projects"** (default for Claude Code sessions and internal projects)
- Optional: Create dedicated calendars for client projects that need separate time tracking
  - Example: For "Dev: PsyOPTIMAL" group -> create "PsyOPTIMAL" calendar
  - Internal projects (valor, popoto, flutter-template, etc.) use the default calendar

After setup, run `/update` to auto-generate `config/calendar_config.json`.

## Step 5: Authentication Configuration

The SDK uses Max subscription OAuth via the Claude Desktop app (no API credits needed).

```bash
# Check if Claude Desktop app is running (provides OAuth for CLI)
if pgrep -f "Claude.app" > /dev/null; then
  echo "Claude Desktop is running (provides subscription auth)"
else
  echo "Claude Desktop is not running"
  echo "Start /Applications/Claude.app to enable subscription auth"
  echo "Without it, the bridge will fall back to API key billing"
fi

# Verify API key exists as fallback
if grep -q 'ANTHROPIC_API_KEY=sk-ant-' .env 2>/dev/null; then
  echo "API key configured (fallback if Desktop auth fails)"
else
  echo "No API key fallback configured"
fi
```

**How authentication works:**
- **Primary**: Claude Desktop app (if running) provides OAuth authentication
- **Fallback**: API key from `.env` (`ANTHROPIC_API_KEY`)
- **Force API billing**: Set `USE_API_BILLING=true` in `.env`

The SDK spawns Claude Code CLI subprocesses that inherit authentication from the running Claude Desktop app. No separate login command is needed.

### Sentry CLI Authentication

`sentry-cli` is installed automatically by `/update`. After installation, authenticate:

```bash
# Login to Sentry (generates auth token)
sentry-cli login

# Or set token directly in ~/Desktop/Valor/.env
# SENTRY_PERSONAL_TOKEN=sntrys_...
# The SDK automatically injects this as SENTRY_AUTH_TOKEN for Eng (and Teammate) sessions
```

The token is stored in `~/Desktop/Valor/.env` as `SENTRY_PERSONAL_TOKEN` and auto-injected into agent sessions by `sdk_client.py`.

## Step 6: Project Configuration (~/Desktop/Valor/projects.json)

Project configuration lives in `~/Desktop/Valor/projects.json` (iCloud-synced, private). This directory is shared across machines via iCloud.

Check if `~/Desktop/Valor/projects.json` exists. If not, create from the repo example:

```bash
mkdir -p ~/Desktop/Valor
cp config/projects.example.json ~/Desktop/Valor/projects.json
```

Edit `~/Desktop/Valor/projects.json` for this machine's projects.

**Critical rules when editing projects.json:**

1. **Every project MUST have `working_directory`** -- absolute path to the repo on this machine
2. **Every project MUST have `machine`** -- the exact `ComputerName` of the single machine that owns it (`scutil --get ComputerName`). This is the source of truth for ownership; whitelists, groups, and email patterns all inherit from it. Two projects on different machines must never share a Telegram group, email contact, or DM whitelist contact id — see [Single-Machine Ownership](../../../docs/features/single-machine-ownership.md).
3. **Always include the full `defaults` section** -- copy it from the example if missing
4. **DO NOT set `respond_to_all: false`** -- the default is `true`, which is correct. Omit the field entirely from project-level telegram config.
5. **Keep project telegram config minimal** -- usually just `"groups": {"Eng: ProjectName": {"persona": "engineer"}}` is sufficient
6. **Verify paths exist on disk** -- run `ls` on each `working_directory` to confirm

**No per-contact ownership edits.** When adding this machine, you do not edit `dms.whitelist`, individual `telegram.groups` entries, or `email.contacts/domains` to "exclude" other machines. Just set each project's `machine` field once. The validator (`bridge/config_validation.py`) and the update gate (`scripts/update/run.py` Step 4.6) will enforce that no contact is owned by two machines.

Example minimal project entry:

```json
{
  "projects": {
    "myproject": {
      "name": "My Project",
      "working_directory": "~/src/myproject",
      "telegram": {
        "groups": {
          "Eng: My Project": {"persona": "engineer"}
        }
      },
      "github": {
        "org": "orgname",
        "repo": "reponame"
      },
      "context": {
        "tech_stack": ["Python"],
        "description": "What the agent should focus on"
      }
    }
  },
  "defaults": {
    "working_directory": "~/src/ai",
    "telegram": {
      "respond_to_all": true,
      "respond_to_mentions": true,
      "respond_to_dms": true,
      "mention_triggers": ["@valor", "valor", "hey valor"]
    },
    "response": {
      "typing_indicator": true,
      "max_response_length": 4000,
      "timeout_seconds": 300
    }
  }
}
```

### Persona overlays

Persona overlay files live in `~/Desktop/Valor/personas/`. The loader (`agent.sdk_client.load_persona_prompt`) prefers the private overlay when present and falls back to the in-repo template (`config/personas/<persona>.md`) otherwise. Seeding the private overlays from the in-repo defaults at setup time gives the agent identical behavior on every fresh machine without waiting for iCloud propagation from another box.

The engineer and customer-service personas have in-repo templates that are version-controlled and PR-reviewable:
- `config/personas/engineer.md` — Engineer SDLC-owner playbook (CRITIQUE/REVIEW gates, Mode 3 parallel orchestrator, `merge_authorized` bypass)
- `config/personas/customer-service.md` — Customer-service overlay for `customer-service`-persona sessions

Seed them into the vault if not already present (do NOT overwrite — existing overlays may carry per-machine customizations):

```bash
mkdir -p ~/Desktop/Valor/personas

for persona in engineer customer-service; do
  src="config/personas/${persona}.md"
  dst="$HOME/Desktop/Valor/personas/${persona}.md"
  if [ ! -f "$dst" ]; then
    cp "$src" "$dst"
    echo "Seeded $dst from $src"
  else
    echo "$dst already exists — leaving in place (run \`diff\` to compare with $src)"
  fi
done
```

The `teammate` persona has no in-repo template — there is no `config/personas/teammate.md`. A teammate overlay is purely operator-authored under `~/Desktop/Valor/personas/teammate.md`; if absent, the loader has no fallback for that persona, so a teammate-using machine must author its own overlay.

If the machine is already running and you want to inspect drift between the in-repo template and the private overlay:

```bash
diff config/personas/engineer.md ~/Desktop/Valor/personas/engineer.md
diff config/personas/customer-service.md ~/Desktop/Valor/personas/customer-service.md
```

The persona loader emits a WARNING log line if a known load-bearing substring is missing from the private engineer overlay (e.g., `CRITIQUE` for the pipeline gate, `Mode 3` for the parallel orchestrator, `merge_authorized` for the stale-baseline bypass). The `/update` script also runs an engineer-overlay drift check (`scripts/update/persona_drift.py`, Step 4.10). Watch `logs/bridge.log` after the first session for these warnings — they signal that the private overlay has rolled back and should be re-synced.

### Cross-machine reuse

If the project is already defined on another machine's `~/Desktop/Valor/projects.json`, copy its entry rather than writing from scratch (iCloud syncs this file across machines).

After editing, verify all working directories exist:

```bash
# For each project's working_directory, confirm it exists
ls ~/src/<project_dir>
```

## Step 7: Telegram Login (USER ACTION REQUIRED)

Check for an existing session:

```bash
ls data/*.session 2>/dev/null
```

**If a session file exists**: Skip to Step 8.

**If no session file exists**: The user must complete an interactive login. Tell them:

> I've finished all the automated setup. One step requires your input -- the Telegram login sends a verification code to your phone.
>
> Please run this in a terminal:
> ```
> cd ~/src/ai && source .venv/bin/activate && python scripts/telegram_login.py
> ```
> Let me know when you're done.

**STOP HERE. Do not proceed until the user confirms the login is complete.**

After they confirm, verify the session was created:

```bash
ls data/*.session
```

If no session file appeared, something went wrong. Ask the user what happened and help debug.

## Step 8: Install Reflections Scheduler

Install the reflections daily maintenance plist (runs at 6 AM Pacific):

```bash
cd ~/src/ai
./scripts/install_reflections.sh
```

Verify it loaded:

```bash
launchctl list | grep com.valor.reflections
```

If the output shows the `com.valor.reflections` label, the scheduler is installed. It will run `scripts/reflections.py` daily at 6 AM, performing log review, session analysis, LLM reflection, and memory consolidation.

## Step 8.5: Optional BYOB + Computer-Use Install (macOS only)

These two surfaces are operator-opt-in. Skip on non-macOS hosts.

### BYOB (real-Chrome control)

BYOB lets the agent read and act on the user's already-logged-in Chrome via MCP tools (`byob_navigate`, `byob_click`, etc.) -- no `state.json` files in the repo, no per-session re-auth.

```bash
# 1. Install bun if not already present
command -v bun >/dev/null || curl -fsSL https://bun.sh/install | bash

# 2. Clone BYOB to ~/.byob/ and check out the pinned commit
PIN=$(python3 -c "import json; print(json.load(open('config/byob_pin.json'))['commit'])")
if [ ! -d ~/.byob ]; then
  git clone https://github.com/wxtsky/byob ~/.byob
fi
git -C ~/.byob fetch
git -C ~/.byob checkout "$PIN"

# 3. Build + register the native messaging host
cd ~/.byob && bun install && bun run setup
cd ~/src/ai

# 4. Register the BYOB MCP server in ~/.claude.json (idempotent, self-healing)
python -c "from scripts.update import mcp_byob; r = mcp_byob.verify_byob_mcp(write=True); print(r.message)"
```

After install, the user must:
1. Open Chrome → `chrome://extensions` → toggle **Developer mode** ON (top-right) → click **Load unpacked** (top-left) → select `~/.byob/packages/extension/output/chrome-mv3/` (the BYOB extension cannot be auto-installed; this is an operator click-through).
2. **Quit Chrome completely** (`⌘Q` on macOS — closing windows is not enough). Reopen Chrome. Chrome only re-reads the Native Messaging config on full restart.

Verify with BYOB's own diagnostic — this is authoritative across BYOB versions and tells you exactly what's wrong if anything's off:

```bash
cd ~/.byob && bun run doctor
```

Expected output (all green checkmarks):
- ✓ Native Messaging manifest registered
- ✓ Launcher script present
- ✓ Bridge process: pid N, deviceId UUID, uptime Ns
- ✓ IPC socket: `~/.byob/bridges/<deviceId>.sock`

If any line is red, the message points at the exact fix. The most common case is "no live bridge — extension never connected" which means the user hasn't loaded the extension yet, or loaded it into a different Chrome profile than the one being tested.

Note: the IPC socket path is **per-device** (UUID-keyed under `~/.byob/bridges/`), not a fixed `~/.byob/run/byob.sock`. The MCP server discovers the socket at startup; callers should never hardcode the path.

### Computer-Use (bcu, native macOS app control)

bcu drives Slack, Notes, Telegram Desktop, etc. via the macOS Accessibility API without moving the user's cursor. **Prompt the user before installing**:

> Do you want to enable computer-use (lets the agent drive native macOS apps -- Slack, Notes, etc. -- without moving your cursor)?

On **yes**:
```bash
# Write the opt-in sentinel
mkdir -p ~/.config/valor && touch ~/.config/valor/computer-use-enabled

# Resolve the pinned bcu release
TAG=$(python3 -c "import json; print(json.load(open('config/bcu_pin.json'))['release_tag'])")

# Download + verify SHA + install -- /update handles this on every run too,
# so the SETUP-time fetch is just bootstrap. See scripts/update/run.py.
echo "bcu pinned tag: $TAG"
echo "Run: python scripts/update/run.py --full to fetch + install + permission-prompt."
```

After install, the user must grant **two** permissions in System Settings:
- Privacy & Security -> Accessibility -> add `BackgroundComputerUse.app`
- Privacy & Security -> Screen Recording -> add `BackgroundComputerUse.app`

These permissions cannot be granted programmatically.

On **no**: skip everything. Don't write the sentinel; `/update` will leave bcu alone.

## Step 8.6: Generation Model Selection (RAM-based)

Free-text generation (memory titles, the test AI judge, knowledge-doc
summarization) runs on a larger `gemma4:31b` model. Classification (bridge
routing, memory-audit, email triage) runs on the resident `granite4.1:3b` and
needs no choice here. Pick the generation variant from this machine's RAM:

- **RAM ≥ `MIN_LOCAL_GEN_RAM_GB` (48 GB)** → local Apple-Silicon MLX variant
  `gemma4:31b-mlx` (the ~18-20 GB MLX 32B coexists with granite + nomic-embed + OS).
- **RAM < 48 GB** → Ollama Cloud variant `gemma4:31b-cloud` (a lightweight hosted
  pointer that fits any machine, including a 16 GB host).

Write the choice to `~/.zshenv` — **machine-local**, NOT the iCloud-synced
`~/Desktop/Valor/.env` (the vault `.env` would propagate one machine's variant to
every other machine via iCloud and break per-machine semantics). The write is
grep-before-append idempotent:

```bash
RAM_GB=$(( $(sysctl -n hw.memsize) / 1024 / 1024 / 1024 ))
if [ "$RAM_GB" -ge 48 ]; then
  GEN_MODEL="gemma4:31b-mlx"
else
  GEN_MODEL="gemma4:31b-cloud"
fi
LINE="export MODELS__OLLAMA_GENERATION_MODEL=$GEN_MODEL"
grep -qxF "$LINE" ~/.zshenv 2>/dev/null || echo "$LINE" >> ~/.zshenv
echo "Generation model: $GEN_MODEL (RAM=${RAM_GB}GB)"
```

Then ensure the chosen tag (the RAM guard inside `ensure_generation_model()`
re-checks and degrades a misconfigured mlx tag to a soft warning — it never pulls
18 GB on a small host):

```bash
python -c "from config.models import ensure_generation_model; ok,d=ensure_generation_model('$GEN_MODEL'); print(('OK' if ok else 'WARN'), d)"
```

**Cloud-signin warning:** when `GEN_MODEL` ends in `:cloud`, the machine must be
signed in to Ollama Cloud (`ollama list` shows a `:cloud` entry). If not, warn the
user to run `ollama signin` — generation is fail-soft, so this does not block setup.

The launchd worker does not read the shell, so `scripts/install_worker.sh` parses
`MODELS__*` lines from `~/.zshenv` and injects them into the plist
`EnvironmentVariables` block — no extra action needed here.

## Step 9: Start the Bridge

Ensure the logs directory exists, then start the bridge as a background process:

```bash
mkdir -p logs
```

Start the bridge using the service script:

```bash
./scripts/valor-service.sh start
```

Wait a few seconds, then verify it started:

```bash
sleep 4 && tail -20 logs/bridge.log 2>/dev/null
```

Check for these indicators in the logs:
- `Agent backend: Claude Agent SDK` -- correct backend
- `Active projects: [...]` -- the projects you configured
- `Monitored groups: [...]` -- the Telegram groups
- `Connected to Telegram` -- successful connection

Also verify the process is running:

```bash
pgrep -f telegram_bridge.py
```

## Step 10: Final Verification

Run a comprehensive health check:

```bash
cd ~/src/ai

echo "=== System Tools ==="
claude --version
gh --version
git --version
uv --version

echo ""
echo "=== Python Environment ==="
.venv/bin/python --version
.venv/bin/python -c "import telethon; import anthropic; import google_auth_oauthlib; print('Dependencies OK')"

echo ""
echo "=== CLI Tools ==="
.venv/bin/valor-calendar --version 2>/dev/null || echo "valor-calendar: Not found (run 'uv pip install -e .' again)"
.venv/bin/python -m tools.sms_reader.cli recent --limit 1 | grep -q "rowid" && echo "SMS reader: OK" || echo "SMS reader: FAIL"

echo ""
echo "=== Bridge Status ==="
./scripts/valor-service.sh status
```

## Step 11: Confirm to User

Report the final status to the user with:

- **Bridge status**: Running (with PID)
- **Agent backend**: Claude Agent SDK
- **Active projects**: Which projects are configured
- **Monitored groups**: Which Telegram groups are being watched
- **Next steps**:
  1. Send a test message in the Telegram group to verify it responds
  2. Run `/update` to generate calendar config

## Rules

- **You** do everything: deps, config, starting the bridge, verification
- **User** only does:
  1. Interactive Telegram login (Step 7)
  2. Claude login (if not already done)
  3. Google OAuth consent (if no token exists)
- Never ask the user to start the bridge or check logs -- do it yourself
- Never set `respond_to_all: false` in project configs
- Always include the `defaults` section in `projects.json`
- Always verify `working_directory` paths exist on disk before starting
- The bridge must be confirmed running before you report success
- If anything fails, debug it yourself. Only escalate to the user if it requires their credentials or interactive input.

## Troubleshooting

### uv not found after install
```bash
export PATH="$HOME/.local/bin:$PATH"
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
```

### Dependencies won't install
```bash
rm -rf .venv
uv venv
uv sync --all-extras
```

### Calendar OAuth fails
1. Verify credentials file exists: `ls ~/Desktop/Valor/google_credentials.json`
2. Ensure Google Calendar API is enabled in Cloud Console
3. Re-run OAuth: `.venv/bin/valor-calendar --reauth`

### Bridge won't start
1. Check logs: `tail -50 logs/bridge.log`
2. Verify Telegram session: `ls data/*.session`
3. Test imports: `.venv/bin/python -c "import telethon; print('OK')"`
