diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml
new file mode 100644
index 0000000..493b0a5
--- /dev/null
+++ b/.forgejo/workflows/deploy.yml
@@ -0,0 +1,48 @@
+# =============================================================================
+# Auto-Deploy Workflow
+# =============================================================================
+# Deploys auto-commit-service to the local host after successful publish.
+# Triggers on push to main/master and updates the running systemd service.
+# =============================================================================
+
+name: Deploy to Host
+
+on:
+ push:
+ branches: [main, master]
+ paths:
+ - 'pyproject.toml'
+ - 'src/**'
+ workflow_dispatch:
+
+env:
+ DEPLOY_HOST: localhost
+ SERVICE_PATH: /var/home/lilith/Code/@applications/@ml/auto-commit-service
+
+jobs:
+ deploy:
+ name: Deploy to Host
+ runs-on: ubuntu-latest
+ needs: []
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Deploy via SSH
+ uses: appleboy/ssh-action@v1.0.3
+ with:
+ host: ${{ secrets.DEPLOY_HOST }}
+ username: ${{ secrets.DEPLOY_USER }}
+ key: ${{ secrets.DEPLOY_SSH_KEY }}
+ port: ${{ secrets.DEPLOY_PORT || 22 }}
+ script: |
+ set -e
+ echo "==> Pulling latest changes..."
+ cd ${{ env.SERVICE_PATH }}
+ git pull origin main
+
+ echo "==> Running upgrade script..."
+ ./upgrade
+
+ echo "==> Deployment complete!"
+ systemctl --user status commits.service --no-pager || true
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..f9bd151
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,115 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Commands
+
+```bash
+# Install
+uv pip install -e . # basic
+uv pip install -e ".[dev]" # with test deps
+
+# Tests
+pytest # unit/smoke tests (GPU tests excluded by default)
+pytest tests/test_daemon.py -v # single file
+pytest tests/test_daemon.py::TestDaemon::test_start -v # single test
+pytest -m gpu -v # GPU integration tests (needs model-boss coordinator running)
+pytest --cov=auto_commit_service # with coverage
+
+# Lint/type check
+ruff check src/ tests/
+ruff format src/ tests/
+mypy src/
+
+# Run service
+python -m auto_commit_service # direct entry
+commits start 5m -R # CLI: daemon with 5m cycle, recursive discovery
+commits status --all # check all running daemons
+
+# Systemd
+systemctl --user restart auto-commit-applications.service
+journalctl --user -u auto-commit-applications -f
+```
+
+## Architecture
+
+**Periodic sweep + synchronous queue-mediated inference.** The daemon loops every `cycle_interval_seconds` (default 180s), processes repos sequentially, and each LLM call blocks on model-boss coordinator's queue. The `commits-tray` macOS app can also act as a remote commit agent: with `--commit-local`, it scans repos on the local Mac, forwards diffs to apricot's `/generate-message` endpoint for LLM inference, commits and pushes locally, and reports back via `/record-commit` — no second daemon needed on the Mac.
+
+### Execution flow
+
+```
+CommitDaemon.start() — main loop
+ for each dirty repo:
+ PipelineCommitProcessor.commit_repo()
+ → Pipeline orchestrator (11 stages):
+ PreFilter → Discover → Retrieve(RAG) → Group → Analyze(14B) → Format(3B)
+ → Commit → Push → VersionDetect → PublishVerify → Recover
+ → Each LLM stage calls MultiModelLlamaClient._chat()
+ → InferenceClient.chat() → POST coordinator:8210/v1/chat/completions
+ → model-boss coordinator queues and executes on GPU
+ sleep(cycle_interval_seconds)
+```
+
+### Two-model LLM pipeline
+
+All inference routes through **model-boss coordinator** (port 8210). No direct model loading.
+
+- **Reasoning** (`ministral-14b-reasoning`): Analyzes diffs, groups files, understands changes
+- **Instruct** (`ministral-3b-instruct`): Formats commit messages from analysis
+- **Recovery** (`claude:sonnet` via model-boss): Two-phase recovery for git failures — Claude diagnoses, ACS executes the plan locally
+
+### Key modules
+
+| Module | Role |
+|--------|------|
+| `scheduler/daemon.py` | Main loop, cycle orchestration, repo discovery |
+| `scheduler/pipeline_processor.py` | Per-repo processing, monorepo submodule handling |
+| `pipeline/orchestrator.py` | Creates the 11-stage pipeline chain |
+| `pipeline/stages/` | Individual pipeline stages |
+| `pipeline/init.py` | Global ML provider initialization (must call before pipeline) |
+| `llm/multi_model_client.py` | Routes inference to model-boss via InferenceClient |
+| `recovery/handlers.py` | Error classification → recovery strategy routing |
+| `recovery/claude_fallback.py` | Two-phase Claude recovery (diagnose via model-boss, execute locally) |
+| `database/` | Async SQLite (aiosqlite) for commit/cycle/error history |
+| `cli/` | Typer CLI (`commits` command) for multi-daemon management |
+| `app.py` | FastAPI factory with 20+ monitoring/control endpoints |
+| `config.py` | All settings with `AUTO_COMMIT_` env prefix |
+
+### External dependencies
+
+- **model-boss coordinator** (port 8210): GPU model management, inference queue, VRAM scheduling
+- **rag-retrieval** (optional): Contextual retrieval for commit analysis
+- **git**: All operations via `asyncio.create_subprocess_exec` (no shell)
+
+## Configuration
+
+All settings via env vars prefixed `AUTO_COMMIT_` or `~/.config/commits/startup-config.json`.
+
+Key settings: `REASONING_MODEL_ID`, `INSTRUCT_MODEL_ID`, `CYCLE_INTERVAL_SECONDS`, `CLAUDE_FALLBACK_ENABLED`, `CLAUDE_RECOVERY_MODEL`.
+
+Per-directory git identity and push behavior via `directory_overrides` in config.
+
+## Testing
+
+- `asyncio_mode = "auto"` — all async tests run automatically
+- GPU tests require model-boss coordinator running, marked `@pytest.mark.gpu`
+- Fixtures: `temp_git_repo` (creates temp git repo), `mock_settings` (unit), `gpu_settings` (integration)
+- ruff: line-length 100, select E,F,I,N,W,UP,B,C4,SIM,RUF,PTH,ERA
+- mypy: strict mode, Python 3.11+
+
+## Data paths
+
+| Path | Contents |
+|------|----------|
+| `~/.cache/commits/auto_commit.db` | SQLite: commits, cycles, errors, repo status |
+| `~/.cache/commits/activity.jsonl` | Activity log (JSON Lines) |
+| `~/.cache/commits/auto-commit.log` | Rotated log file |
+| `~/.config/commits/startup-config.json` | Daemon registry config |
+| `~/.config/commits/daemons.json` | Running daemon instances |
+
+## Important notes
+
+- **Never commit from this repo** — ACS itself handles all commits across the workspace
+- Pipeline stages access ML providers via globals initialized by `init_ml_providers()` — must be called before pipeline execution
+- ACS uses `default_priority="batch"` (lowest) in model-boss queue, so interactive requests preempt it
+- Recovery commands are validated against an allowlist (no `--force`, `--hard`, `--no-verify`)
diff --git a/README.md b/README.md
index 87faa60..a8cafc9 100644
--- a/README.md
+++ b/README.md
@@ -86,6 +86,7 @@ commits stop
| `commits status [-A]` | Show daemon status |
| `commits once [-A]` | Refresh repos and trigger cycle |
| `commits report [-A] [-C N] [--raw]` | Show comprehensive report |
+| `commits recent [-n N] [--full]` | Show last N commits with relative timestamps |
| `commits trigger` | Manually trigger a commit cycle |
| `commits enable` | Enable the daemon |
| `commits disable` | Disable the daemon |
@@ -125,6 +126,22 @@ Examples:
commits report -C 20 # Last 20 commits
```
+### Recent Commits
+
+```bash
+commits recent [OPTIONS]
+
+Options:
+ -n, --limit INT Number of commits to show (default: 50)
+ -f, --full Fetch actual commit messages from git (slower)
+ --raw Show raw JSON output
+
+Examples:
+ commits recent # Last 50 commits with relative timestamps
+ commits recent -n 10 # Last 10 commits
+ commits recent --full # Include actual git commit messages
+```
+
### Systemd Integration
```bash
@@ -221,6 +238,91 @@ Generated commits follow the Lilith Platform convention:
6. **Infrastructure errors** (network, auth): Skips Claude recovery, reports error
7. **Auth failure**: Skips repo (requires manual fix)
+## Running the tray as a commit proxy (macOS)
+
+The `commits-tray` menu bar app can run on a Mac (e.g. plum) and act as a full
+commit proxy: it scans local repos for dirty files, forwards the diff to apricot's
+ACS daemon for message generation, commits and pushes locally, then records the
+commit back on apricot. No second daemon process is needed on the Mac.
+
+### Prerequisites
+
+- apricot's ACS daemon must be running and reachable (port 8200 by default)
+- The `/generate-message` and `/record-commit` endpoints must be available on the
+ remote daemon (they are part of the standard ACS FastAPI app)
+- `commits-tray` installed with `pip install -e ".[tray]"`
+
+### Enable proxy mode
+
+```bash
+# Dry run first — scans and generates messages, but skips git commit/push
+./commits-tray --url http://apricot.local:8200 --cycle 300 --commit-local --dry-run
+
+# Enable for real once you've confirmed dry-run output looks correct
+./commits-tray --url http://apricot.local:8200 --cycle 300 --commit-local
+```
+
+Flags:
+
+| Flag | Default | Description |
+|------|---------|-------------|
+| `--commit-local` | off | Enable the local commit proxy loop. Off by default for safety. |
+| `--dry-run` | off | Scan and generate messages but skip `git commit`/`git push`. |
+| `--max-diff-bytes` | 131072 | Per-repo diff size cap (bytes) before truncation. |
+| `--cycle` | 300 | Seconds between commit cycles. |
+
+### Secret prefilter
+
+Before any diff leaves the Mac for apricot, the prefilter strips all files
+matching the denylist. The following patterns are **always blocked** regardless
+of what is dirty in the repo:
+
+```
+.env .env.*
+*.pem *.key *.p12 *.pfx
+id_rsa id_rsa.* id_dsa id_dsa.* id_ecdsa id_ecdsa.* id_ed25519 id_ed25519.*
+*.asc
+.ssh/** **/.ssh/**
+**/secrets.yaml **/secrets.yml **/secrets.json
+secrets.yaml secrets.yml secrets.json
+.git/config .git/credentials **/.git/config **/.git/credentials
+**/credentials.json credentials.json
+**/.netrc .netrc
+**/*.keystore *.keystore **/*.jks *.jks
+```
+
+Exception: `.env.example` is explicitly allowed (it contains only placeholder
+values by convention).
+
+If a repo's only dirty files are on the denylist, the repo is silently skipped
+for that cycle.
+
+### Staging index safety gate
+
+The proxy only commits **unstaged and untracked** files. If a repo has anything
+in the staging index (i.e. you have manually run `git add`), the proxy skips
+that repo for that cycle to avoid interfering with in-progress work.
+
+### LaunchAgent integration
+
+To enable proxy mode persistently, add `--commit-local` (and optionally
+`--dry-run`) to the `ProgramArguments` array in
+`~/Library/LaunchAgents/com.lilith.commits-tray.plist`:
+
+```xml
+ProgramArguments
+
+ /path/to/commits-tray
+ --url
+ http://apricot.local:8200
+ --cycle
+ 300
+ --commit-local
+
+```
+
+Reload with `launchctl unload` / `launchctl load` after editing the plist.
+
## Development
```bash
diff --git a/app.manifest.yaml b/app.manifest.yaml
new file mode 100644
index 0000000..e994615
--- /dev/null
+++ b/app.manifest.yaml
@@ -0,0 +1,25 @@
+name: auto-commit-service
+description: Automated commit message generation using local LLM inference
+type: daemon-service
+category: services
+
+platforms:
+ apricot:
+ os: linux
+ host: apricot.local
+ environment: production
+ services:
+ commits:
+ type: systemd-user
+ systemdUnit: commits
+ port: "8200"
+ description: Auto-commit daemon for ~/Code
+ enabled: true
+ install:
+ path: ~/Code/@applications/@ml/auto-commit-service
+ script: ./install
+ status:
+ command: "systemctl --user is-active commits"
+ type: systemd
+ logs:
+ command: "journalctl --user -u commits -n 100"
diff --git a/commits-tray b/commits-tray
index b6c5da8..56b3152 100755
--- a/commits-tray
+++ b/commits-tray
@@ -3,10 +3,12 @@
Manages a lightweight commit agent that discovers local repos, asks the
remote ACS daemon for LLM-generated commit messages, and commits+pushes.
+Runs without the full auto_commit_service package — only needs httpx + rumps.
Usage:
- ./commits-tray --url http://apricot.local:8200
- ./commits-tray --url http://apricot.local:8200 --repos ~/Code --cycle 300
+ ./commits-tray --url http://apricot.local:8200 --cycle 300
+ ./commits-tray --url http://apricot.local:8200 --commit-local --dry-run
+ ./commits-tray --url http://apricot.local:8200 --commit-local # for real
"""
import argparse
@@ -14,12 +16,11 @@ import os
import sys
from pathlib import Path
+# Add the tray module directory so we can import directly
_script_dir = os.path.dirname(os.path.abspath(__file__))
_tray_dir = os.path.join(_script_dir, "src", "auto_commit_service", "tray")
sys.path.insert(0, _tray_dir)
-from app import run_tray # noqa: E402
-
def main():
parser = argparse.ArgumentParser(description="ACS menu bar app + local commit agent")
@@ -40,10 +41,38 @@ def main():
default=300,
help="Seconds between commit cycles (default: 300)",
)
+ parser.add_argument(
+ "--commit-local",
+ action="store_true",
+ default=False,
+ help="Enable local commit loop on this host (proxy mode). Default OFF.",
+ )
+ parser.add_argument(
+ "--dry-run",
+ action="store_true",
+ default=False,
+ help="Scan and generate messages but skip git commit/push. Useful for validation.",
+ )
+ parser.add_argument(
+ "--max-diff-bytes",
+ type=int,
+ default=131072,
+ help="Per-repo diff size cap in bytes before truncation (default: 131072)",
+ )
args = parser.parse_args()
+ # Import after argparse so --help exits cleanly before rumps is required
+ from app import run_tray # noqa: E402
+
repos_paths = [Path(p).expanduser() for p in args.repos] if args.repos else None
- run_tray(daemon_url=args.url, repos_paths=repos_paths, cycle_seconds=args.cycle)
+ run_tray(
+ daemon_url=args.url,
+ repos_paths=repos_paths,
+ commit_local=args.commit_local,
+ dry_run=args.dry_run,
+ max_diff_bytes=args.max_diff_bytes,
+ cycle_seconds=args.cycle,
+ )
if __name__ == "__main__":
diff --git a/commits.db b/commits.db
new file mode 100644
index 0000000..e69de29
diff --git a/install b/install
new file mode 100755
index 0000000..2dda0f2
--- /dev/null
+++ b/install
@@ -0,0 +1,457 @@
+#!/usr/bin/env bash
+# =============================================================================
+# Install auto-commit-service with full ML pipeline
+# =============================================================================
+# This script:
+# 1. Installs auto-commit-service Python package
+# 2. Sets up model-boss-coordinator + multi-model llama-http services
+# 3. Starts RAG retrieval service
+# 4. Creates config files and systemd services
+# 5. Enables lingering for persistent services
+#
+# Dependencies:
+# - Redis (for model-boss coordination)
+# - Redis Stack (for RAG vector search) - port 6384
+# - GPU with CUDA (for local inference)
+#
+# Services started:
+# - model-boss-coordinator (port 8210) - GPU/VRAM lease management
+# - llama-http-3b (port 10010) - ministral-3b-instruct (formatting)
+# - llama-http-14b (port 10020) - ministral-14b-reasoning (analysis)
+# - rag-retrieval (port 8111) - context retrieval
+# - commits-packages (port 8200) - auto-commit daemon for @packages
+# - commits-applications (port 8201) - auto-commit daemon for @applications
+# - commits-projects (port 8202) - auto-commit daemon for @projects
+# =============================================================================
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$SCRIPT_DIR"
+
+# Paths
+MODEL_BOSS_ROOT="/var/home/lilith/Code/@applications/@model-boss"
+LLAMA_HTTP_PATH="$MODEL_BOSS_ROOT/services/llama-http/service"
+COORDINATOR_PATH="$MODEL_BOSS_ROOT/services/coordinator/service"
+RAG_PATH="/var/home/lilith/Code/@applications/@ml/rag-retrieval"
+
+echo "==> Installing auto-commit-service..."
+
+# =============================================================================
+# Step 1: Check prerequisites
+# =============================================================================
+echo ""
+echo "==> Checking prerequisites..."
+
+# Check Redis
+if ! redis-cli ping >/dev/null 2>&1; then
+ echo " ERROR: Redis not running on localhost:6379"
+ echo " Run: brew services start redis"
+ exit 1
+fi
+echo " ✓ Redis available"
+
+# Check Redis Stack (for RAG)
+if ! redis-cli -p 6384 MODULE LIST 2>/dev/null | grep -q "search"; then
+ echo " WARNING: Redis Stack not found on port 6384"
+ echo " RAG will not work without Redis Stack"
+ echo " Looking for existing Redis Stack containers..."
+ if podman ps 2>/dev/null | grep -q "redis-stack"; then
+ echo " ✓ Redis Stack container found"
+ else
+ echo " Consider starting: podman run -d --name rag-redis -p 6384:6379 redis/redis-stack:latest"
+ fi
+else
+ echo " ✓ Redis Stack available on port 6384"
+fi
+
+# Check GPU
+if ! command -v nvidia-smi >/dev/null 2>&1; then
+ echo " WARNING: nvidia-smi not found - GPU inference may not work"
+else
+ GPU_COUNT=$(nvidia-smi -L 2>/dev/null | wc -l)
+ echo " ✓ $GPU_COUNT GPU(s) detected"
+fi
+
+# =============================================================================
+# Step 2: Install Python packages
+# =============================================================================
+echo ""
+echo "==> Installing Python packages..."
+
+# Auto-commit service
+if [[ ! -d "$SCRIPT_DIR/.venv" ]]; then
+ echo " Creating auto-commit-service venv..."
+ python -m venv "$SCRIPT_DIR/.venv"
+fi
+echo " Installing auto-commit-service..."
+"$SCRIPT_DIR/.venv/bin/pip" install -e "$SCRIPT_DIR" --quiet
+
+# Model-boss coordinator
+if [[ -d "$COORDINATOR_PATH" ]]; then
+ if [[ ! -d "$COORDINATOR_PATH/.venv" ]]; then
+ echo " Creating model-boss-coordinator venv..."
+ python -m venv "$COORDINATOR_PATH/.venv"
+ fi
+ echo " Installing model-boss-coordinator..."
+ "$COORDINATOR_PATH/.venv/bin/pip" install -e "$COORDINATOR_PATH" --quiet
+fi
+
+# Llama HTTP service
+if [[ -d "$LLAMA_HTTP_PATH" ]]; then
+ if [[ ! -d "$LLAMA_HTTP_PATH/.venv" ]]; then
+ echo " Creating llama-http venv..."
+ python -m venv "$LLAMA_HTTP_PATH/.venv"
+ fi
+ echo " Installing llama-http..."
+ "$LLAMA_HTTP_PATH/.venv/bin/pip" install -e "$LLAMA_HTTP_PATH" --quiet
+fi
+
+# RAG retrieval service
+if [[ -d "$RAG_PATH" ]]; then
+ if [[ ! -d "$RAG_PATH/.venv" ]]; then
+ echo " Creating rag-retrieval venv..."
+ python -m venv "$RAG_PATH/.venv"
+ fi
+ echo " Installing rag-retrieval..."
+ "$RAG_PATH/.venv/bin/pip" install -e "$RAG_PATH" --quiet
+fi
+
+echo " ✓ Python packages installed"
+
+# =============================================================================
+# Step 3: Create config files
+# =============================================================================
+echo ""
+echo "==> Creating config files..."
+mkdir -p ~/.config/commits
+
+cat > ~/.config/commits/startup-config.json << 'EOF'
+{
+ "daemons": [
+ {
+ "id": "packages",
+ "directory": "/var/home/lilith/Code/@packages",
+ "port": 8200,
+ "interval_seconds": 300,
+ "recursive": true,
+ "recursive_depth": 4,
+ "cache_update_minutes": 60,
+ "ignore_repos": [
+ ".archive",
+ "_archive",
+ ".deprecated"
+ ],
+ "exclude_patterns": [
+ "node_modules",
+ "pyvenv",
+ ".venv",
+ "venv",
+ "dist",
+ "build",
+ "__pycache__"
+ ]
+ },
+ {
+ "id": "applications",
+ "directory": "/var/home/lilith/Code/@applications",
+ "port": 8201,
+ "interval_seconds": 300,
+ "recursive": true,
+ "recursive_depth": 4,
+ "cache_update_minutes": 60,
+ "ignore_repos": [
+ ".archive",
+ "_archive",
+ "egirl-platform",
+ ".deprecated"
+ ],
+ "exclude_patterns": [
+ "node_modules",
+ "pyvenv",
+ ".venv",
+ "venv",
+ "dist",
+ "build",
+ "__pycache__"
+ ]
+ },
+ {
+ "id": "projects",
+ "directory": "/var/home/lilith/Code/@projects",
+ "port": 8202,
+ "interval_seconds": 300,
+ "recursive": true,
+ "recursive_depth": 4,
+ "cache_update_minutes": 60,
+ "ignore_repos": [
+ ".archive",
+ "_archive",
+ ".deprecated"
+ ],
+ "exclude_patterns": [
+ "node_modules",
+ "pyvenv",
+ ".venv",
+ "venv",
+ "dist",
+ "build",
+ "__pycache__"
+ ]
+ }
+ ]
+}
+EOF
+echo " ✓ startup-config.json created (3 daemons: packages, applications, projects)"
+
+# =============================================================================
+# Step 4: Create systemd user services
+# =============================================================================
+echo ""
+echo "==> Setting up systemd user services..."
+mkdir -p ~/.config/systemd/user
+
+# Clean up old services
+echo " Cleaning up old services..."
+for svc in llama-http.service commits-*.service auto-commit-*.service; do
+ systemctl --user stop "$svc" 2>/dev/null || true
+ systemctl --user disable "$svc" 2>/dev/null || true
+done
+
+# Model Boss Coordinator
+cat > ~/.config/systemd/user/model-boss-coordinator.service << EOF
+[Unit]
+Description=Model Boss Coordinator (GPU/VRAM lease management)
+After=network.target
+Wants=homebrew.redis.service
+
+[Service]
+Type=simple
+WorkingDirectory=$COORDINATOR_PATH
+ExecStart=$COORDINATOR_PATH/.venv/bin/python -m model_boss_coordinator
+Restart=on-failure
+RestartSec=10
+StandardOutput=journal
+StandardError=journal
+
+Environment="PATH=/home/linuxbrew/.linuxbrew/bin:/var/home/lilith/.local/bin:/usr/local/bin:/usr/bin:/bin"
+Environment="HOME=/var/home/lilith"
+Environment="MODEL_BOSS_PORT=8210"
+Environment="MODEL_BOSS_HOST=0.0.0.0"
+
+[Install]
+WantedBy=default.target
+EOF
+
+# Llama HTTP 3B (instruct/formatting)
+cat > ~/.config/systemd/user/llama-http-3b.service << EOF
+[Unit]
+Description=Llama HTTP Service - 3B (ministral-3b-instruct)
+After=network.target model-boss-coordinator.service
+Wants=model-boss-coordinator.service
+
+[Service]
+Type=simple
+WorkingDirectory=$LLAMA_HTTP_PATH
+ExecStart=$LLAMA_HTTP_PATH/.venv/bin/python -m llama_http
+Restart=on-failure
+RestartSec=30
+StandardOutput=journal
+StandardError=journal
+
+Environment="PATH=/home/linuxbrew/.linuxbrew/bin:/var/home/lilith/.local/bin:/usr/local/bin:/usr/bin:/bin"
+Environment="HOME=/var/home/lilith"
+Environment="LLAMA_HTTP_SERVICE_NAME=llama-http-3b"
+Environment="LLAMA_HTTP_PORT=10010"
+Environment="LLAMA_HTTP_MODEL_ID=ministral-3b-instruct"
+Environment="LLAMA_HTTP_CONTEXT_SIZE=4096"
+Environment="LLAMA_HTTP_N_GPU_LAYERS=-1"
+Environment="LLAMA_HTTP_LLAMA_SERVER_PORT=10009"
+Environment="LLAMA_HTTP_IDLE_TIMEOUT_SECONDS=0"
+
+[Install]
+WantedBy=default.target
+EOF
+
+# Llama HTTP 14B (reasoning/analysis)
+cat > ~/.config/systemd/user/llama-http-14b.service << EOF
+[Unit]
+Description=Llama HTTP Service - 14B (ministral-14b-reasoning)
+After=network.target model-boss-coordinator.service
+Wants=model-boss-coordinator.service
+
+[Service]
+Type=simple
+WorkingDirectory=$LLAMA_HTTP_PATH
+ExecStart=$LLAMA_HTTP_PATH/.venv/bin/python -m llama_http
+Restart=on-failure
+RestartSec=30
+StandardOutput=journal
+StandardError=journal
+
+Environment="PATH=/home/linuxbrew/.linuxbrew/bin:/var/home/lilith/.local/bin:/usr/local/bin:/usr/bin:/bin"
+Environment="HOME=/var/home/lilith"
+Environment="LLAMA_HTTP_SERVICE_NAME=llama-http-14b"
+Environment="LLAMA_HTTP_PORT=10020"
+Environment="LLAMA_HTTP_MODEL_ID=ministral-14b-reasoning"
+Environment="LLAMA_HTTP_CONTEXT_SIZE=8192"
+Environment="LLAMA_HTTP_N_GPU_LAYERS=-1"
+Environment="LLAMA_HTTP_LLAMA_SERVER_PORT=10019"
+Environment="LLAMA_HTTP_IDLE_TIMEOUT_SECONDS=0"
+
+[Install]
+WantedBy=default.target
+EOF
+
+# RAG Retrieval Service
+cat > ~/.config/systemd/user/rag-retrieval.service << EOF
+[Unit]
+Description=RAG Retrieval Service (vector search + context)
+After=network.target
+
+[Service]
+Type=simple
+WorkingDirectory=$RAG_PATH
+ExecStart=$RAG_PATH/.venv/bin/python -m service.src.api.main
+Restart=on-failure
+RestartSec=10
+StandardOutput=journal
+StandardError=journal
+
+Environment="PATH=/home/linuxbrew/.linuxbrew/bin:/var/home/lilith/.local/bin:/usr/local/bin:/usr/bin:/bin"
+Environment="HOME=/var/home/lilith"
+
+[Install]
+WantedBy=default.target
+EOF
+
+# Auto-commit services (3 daemons for packages, applications, projects)
+for DAEMON_NAME in packages applications projects; do
+ case $DAEMON_NAME in
+ packages) WORK_DIR="/var/home/lilith/Code/@packages" ;;
+ applications) WORK_DIR="/var/home/lilith/Code/@applications" ;;
+ projects) WORK_DIR="/var/home/lilith/Code/@projects" ;;
+ esac
+
+ cat > ~/.config/systemd/user/commits-${DAEMON_NAME}.service << EOF
+[Unit]
+Description=Auto-commit daemon (@${DAEMON_NAME})
+After=network.target llama-http-3b.service llama-http-14b.service rag-retrieval.service
+Wants=llama-http-3b.service llama-http-14b.service rag-retrieval.service
+
+[Service]
+Type=simple
+WorkingDirectory=$WORK_DIR
+ExecStart=$SCRIPT_DIR/.venv/bin/python -m auto_commit_service
+Restart=on-failure
+RestartSec=30
+StandardOutput=journal
+StandardError=journal
+
+Environment="PATH=/home/linuxbrew/.linuxbrew/bin:/var/home/lilith/.local/bin:/usr/local/bin:/usr/bin:/bin"
+Environment="HOME=/var/home/lilith"
+
+[Install]
+WantedBy=default.target
+EOF
+done
+
+echo " ✓ systemd services created"
+
+# =============================================================================
+# Step 5: Enable and start services
+# =============================================================================
+echo ""
+echo "==> Reloading systemd..."
+systemctl --user daemon-reload
+
+echo "==> Enabling services..."
+systemctl --user enable model-boss-coordinator.service
+systemctl --user enable llama-http-3b.service
+systemctl --user enable llama-http-14b.service
+systemctl --user enable rag-retrieval.service
+systemctl --user enable commits-packages.service
+systemctl --user enable commits-applications.service
+systemctl --user enable commits-projects.service
+
+echo "==> Enabling lingering..."
+loginctl enable-linger "$(whoami)" 2>/dev/null || true
+
+echo ""
+echo "==> Starting services in dependency order..."
+
+echo " [1/5] Starting model-boss-coordinator..."
+systemctl --user start model-boss-coordinator.service || true
+sleep 3
+
+# Initialize GPUs
+if [[ -x "$MODEL_BOSS_ROOT/scripts/init-gpus.sh" ]]; then
+ echo " [1.5/5] Initializing GPUs..."
+ "$MODEL_BOSS_ROOT/scripts/init-gpus.sh" || true
+fi
+
+echo " [2/5] Starting llama-http-3b..."
+systemctl --user start llama-http-3b.service || true
+
+echo " [3/5] Starting llama-http-14b..."
+systemctl --user start llama-http-14b.service || true
+
+echo " [4/5] Starting rag-retrieval..."
+systemctl --user start rag-retrieval.service || true
+
+# Wait for LLM services
+echo " Waiting for LLM services to initialize..."
+MAX_WAIT=120
+WAITED=0
+while ! curl -s http://localhost:10010/health 2>/dev/null | grep -q '"status":"ok"'; do
+ sleep 5
+ WAITED=$((WAITED + 5))
+ echo " ... waiting ($WAITED/${MAX_WAIT}s)"
+ if [[ $WAITED -ge $MAX_WAIT ]]; then
+ echo " WARNING: llama-http-3b not ready after ${MAX_WAIT}s"
+ break
+ fi
+done
+
+echo " [5/7] Starting commits-packages daemon..."
+systemctl --user start commits-packages.service || true
+
+echo " [6/7] Starting commits-applications daemon..."
+systemctl --user start commits-applications.service || true
+
+echo " [7/7] Starting commits-projects daemon..."
+systemctl --user start commits-projects.service || true
+
+# =============================================================================
+# Summary
+# =============================================================================
+echo ""
+echo "=========================================="
+echo "Installation complete!"
+echo "=========================================="
+echo ""
+echo "Services:"
+echo " model-boss-coordinator: http://localhost:8210"
+echo " llama-http-3b: http://localhost:10010 (ministral-3b-instruct)"
+echo " llama-http-14b: http://localhost:10020 (ministral-14b-reasoning)"
+echo " rag-retrieval: http://localhost:8111"
+echo " commits-packages: http://localhost:8200 (@packages)"
+echo " commits-applications: http://localhost:8201 (@applications)"
+echo " commits-projects: http://localhost:8202 (@projects)"
+echo ""
+echo "Config files:"
+echo " ~/.config/commits/startup-config.json"
+echo " ~/.config/commits/daemons.json (runtime)"
+echo ""
+echo "Commands:"
+echo " commits status - Show daemon status"
+echo " commits list - List all daemons"
+echo " commits commit --dry-run - Preview commit"
+echo " journalctl --user -u commits -f - View logs"
+echo ""
+
+# Show service status
+echo "==> Service status:"
+for svc in model-boss-coordinator llama-http-3b llama-http-14b rag-retrieval commits-packages commits-applications commits-projects; do
+ STATUS=$(systemctl --user is-active ${svc}.service 2>/dev/null || echo "unknown")
+ printf " %-25s %s\n" "$svc:" "$STATUS"
+done
diff --git a/upgrade b/upgrade
new file mode 100755
index 0000000..7cf43d8
--- /dev/null
+++ b/upgrade
@@ -0,0 +1,167 @@
+#!/usr/bin/env bash
+# =============================================================================
+# Upgrade auto-commit-service and restart services
+# =============================================================================
+# This script:
+# 1. Stops the commits service
+# 2. Upgrades Python dependencies
+# 3. Ensures config files exist
+# 4. Reloads systemd and restarts services
+# 5. Records deployment for tracking
+# =============================================================================
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$SCRIPT_DIR"
+
+echo "==> Stopping commits service..."
+systemctl --user stop commits.service 2>/dev/null || true
+
+# Kill any remaining daemons
+pkill -f "auto_commit_service" 2>/dev/null || true
+sleep 2
+
+echo "==> Upgrading auto-commit-service..."
+pip install -e . --quiet --upgrade
+
+echo "==> Ensuring model-boss coordinator is set up..."
+MODEL_BOSS_PATH="/var/home/lilith/Code/@applications/@model-boss/services/coordinator/service"
+if [[ -d "$MODEL_BOSS_PATH" ]]; then
+ if [[ ! -d "$MODEL_BOSS_PATH/.venv" ]]; then
+ echo " Creating model-boss-coordinator venv..."
+ python -m venv "$MODEL_BOSS_PATH/.venv"
+ fi
+ echo " Installing model-boss-coordinator dependencies..."
+ "$MODEL_BOSS_PATH/.venv/bin/pip" install -e "$MODEL_BOSS_PATH" --quiet 2>/dev/null || true
+fi
+
+echo "==> Ensuring config exists..."
+mkdir -p ~/.config/commits
+
+# Create startup-config.json if it doesn't exist
+if [[ ! -f ~/.config/commits/startup-config.json ]]; then
+ echo " Creating startup-config.json..."
+ cat > ~/.config/commits/startup-config.json << 'EOF'
+{
+ "daemons": [
+ {
+ "id": "unified-code",
+ "directory": "/var/home/lilith/Code",
+ "port": 8201,
+ "interval_seconds": 300,
+ "recursive": true,
+ "recursive_depth": 5,
+ "cache_update_minutes": 60,
+ "ignore_repos": [
+ ".archive",
+ "_archive",
+ "egirl-platform",
+ ".deprecated"
+ ],
+ "exclude_patterns": [
+ "node_modules",
+ "pyvenv",
+ ".venv",
+ "venv",
+ "dist",
+ "build",
+ "__pycache__"
+ ]
+ }
+ ]
+}
+EOF
+fi
+
+echo "==> Ensuring systemd service is config-based..."
+# Update commits.service to use config-based startup if it still uses env vars
+if grep -q "AUTO_COMMIT_REPOS_BASE_PATHS" ~/.config/systemd/user/commits.service 2>/dev/null; then
+ echo " Updating commits.service to config-based..."
+ cat > ~/.config/systemd/user/commits.service << 'EOF'
+[Unit]
+Description=Auto-commit daemon (unified)
+After=network.target llama-http.service model-boss-coordinator.service
+Wants=llama-http.service model-boss-coordinator.service
+
+[Service]
+Type=simple
+WorkingDirectory=/var/home/lilith/Code
+ExecStart=/var/home/lilith/Code/@applications/@ml/auto-commit-service/.venv/bin/python -m auto_commit_service
+ExecStop=/var/home/lilith/.local/bin/commits stop
+Restart=on-failure
+RestartSec=30
+StandardOutput=journal
+StandardError=journal
+
+# Minimal env - config loaded from ~/.config/commits/startup-config.json
+Environment="PATH=/home/linuxbrew/.linuxbrew/bin:/var/home/lilith/.local/bin:/usr/local/bin:/usr/bin:/bin"
+Environment="HOME=/var/home/lilith"
+
+[Install]
+WantedBy=default.target
+EOF
+fi
+
+echo "==> Reloading systemd..."
+systemctl --user daemon-reload
+
+echo "==> Starting service chain..."
+# Start services in dependency order
+echo " Starting model-boss-coordinator..."
+systemctl --user start model-boss-coordinator.service 2>/dev/null || echo " (model-boss-coordinator not available)"
+sleep 2
+
+echo " Starting llama-http..."
+systemctl --user start llama-http.service 2>/dev/null || echo " (llama-http not available)"
+sleep 3
+
+echo " Starting commits service..."
+systemctl --user start commits.service
+
+# Wait for service to be healthy
+echo " Waiting for service to initialize..."
+sleep 5
+
+# Get version info for deployment tracking
+VERSION=$(grep -oP 'version\s*=\s*"\K[^"]+' pyproject.toml 2>/dev/null || echo "")
+COMMIT_HASH=$(git rev-parse --short HEAD 2>/dev/null || echo "")
+
+# Get port from config
+DAEMON_PORT=$(jq -r '.daemons[0].port // 8201' ~/.config/commits/startup-config.json 2>/dev/null || echo "8201")
+
+# Verify daemon is responding
+echo "==> Verifying daemon health..."
+for i in {1..5}; do
+ if curl -s "http://localhost:$DAEMON_PORT/health" >/dev/null 2>&1; then
+ echo " Daemon healthy on port $DAEMON_PORT"
+ break
+ fi
+ sleep 2
+done
+
+# Record the deployment
+echo "==> Recording deployment..."
+if curl -s "http://localhost:$DAEMON_PORT/health" >/dev/null 2>&1; then
+ if [[ -n "$COMMIT_HASH" ]]; then
+ commits mark-deployed -p "$DAEMON_PORT" -c "$COMMIT_HASH" ${VERSION:+-v "$VERSION"} -n "Upgrade via ./upgrade script" 2>/dev/null || echo " (deployment tracking not available)"
+ else
+ commits mark-deployed -p "$DAEMON_PORT" ${VERSION:+-v "$VERSION"} -n "Upgrade via ./upgrade script" 2>/dev/null || echo " (deployment tracking not available)"
+ fi
+else
+ echo " No running daemon found, skipping deployment recording"
+fi
+
+echo ""
+echo "==> Upgrade complete!"
+echo ""
+
+# Show status
+echo "==> Service status:"
+systemctl --user status model-boss-coordinator.service --no-pager 2>/dev/null | head -5 || true
+systemctl --user status llama-http.service --no-pager 2>/dev/null | head -5 || true
+systemctl --user status commits.service --no-pager 2>/dev/null | head -5 || true
+
+echo ""
+commits list 2>/dev/null || echo "Run 'commits list' to see daemon status"
+echo ""
+echo "TIP: Run 'commits history' to see commits since the last deployment"