diff --git a/.gitignore b/.gitignore index 3cc740c..b3b0cfe 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ daemon/.pi-scratch/ coverage daemon/lvmh-daemon e2e-integration-report.md +docker/pi-agent/ diff --git a/daemon/docker.go b/daemon/docker.go index 11168d8..a8686b6 100644 --- a/daemon/docker.go +++ b/daemon/docker.go @@ -41,6 +41,8 @@ const ( labelSession string = "lvmh.session" volumeRepoPrefix string = "lvmh-repo-" volumeSessions string = "lvmh-sessions" + volumePiCache string = "lvmh-pi-cache" // pi package cache (git:/npm:), shared across spawns + cacheMount string = "/root/.pi/agent/cache" workspaceMount string = "/workspace" sessionsMount string = "/pi-sessions" @@ -382,8 +384,15 @@ func (s *Spawner) createAndStart(ctx context.Context, repo, slug, sessionID stri if _, err := s.cli.VolumeCreate(ctx, volume.CreateOptions{Name: volumeSessions}); err != nil { return "", fmt.Errorf("volume %s: %w", volumeSessions, err) } + if _, err := s.cli.VolumeCreate(ctx, volume.CreateOptions{Name: volumePiCache}); err != nil { + return "", fmt.Errorf("volume %s: %w", volumePiCache, err) + } - binds := []string{repoVolume + ":" + workspaceMount, volumeSessions + ":" + sessionsMount} + binds := []string{ + repoVolume + ":" + workspaceMount, + volumeSessions + ":" + sessionsMount, + volumePiCache + ":" + cacheMount, + } cfg := &container.Config{ Image: imageRefWorker, Env: []string{ diff --git a/daemon/spawner_test.go b/daemon/spawner_test.go index 6c7cf46..78221b7 100644 --- a/daemon/spawner_test.go +++ b/daemon/spawner_test.go @@ -77,9 +77,10 @@ func TestSpawnerStartHappyPath(t *testing.T) { wantBinds := []string{ "lvmh-repo-group-project:" + workspaceMount, volumeSessions + ":" + sessionsMount, + volumePiCache + ":" + cacheMount, } - if len(c.HostConfig.Binds) != 2 { - t.Fatalf("binds = %v, want %v (no models file)", c.HostConfig.Binds, wantBinds) + if len(c.HostConfig.Binds) != 3 { + t.Fatalf("binds = %v, want %v", c.HostConfig.Binds, wantBinds) } for i, b := range wantBinds { if c.HostConfig.Binds[i] != b { diff --git a/deploy/deploy.sh b/deploy/deploy.sh index aaff7c4..7b332b8 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -19,6 +19,9 @@ if [ ! -f web/dist/index.html ]; then (cd web && bun install && bun run build) fi +# Bake the user's pi config (dotfiles) into the worker image context. +bash "$(dirname "$0")/rsync-pi-agent.sh" + rsync -az --delete \ --exclude .git --exclude node_modules --exclude web/node_modules \ --exclude .env --exclude .scratch --exclude .pi --exclude coverage \ diff --git a/deploy/rsync-pi-agent.sh b/deploy/rsync-pi-agent.sh new file mode 100755 index 0000000..ba248d9 --- /dev/null +++ b/deploy/rsync-pi-agent.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Sync the user's pi config (from dotfiles) into docker/pi-agent/ so the +# worker image bakes it. Filtered: no secrets, no machine-local state. +set -euo pipefail + +SRC="${LVMH_PI_AGENT_DIR:-$HOME/.dotfiles/pi/agent}" +DEST="$(dirname "$0")/../docker/pi-agent" + +if [ ! -f "$SRC/settings.json" ]; then + echo "rsync-pi-agent: $SRC/settings.json not found — skipping dotfiles sync" >&2 + exit 0 +fi + +mkdir -p "$DEST" +rsync -a --delete \ + --exclude 'auth.json' \ + --exclude 'models.json' \ + --exclude 'models-store.json' \ + --exclude 'sessions/' \ + --exclude 'cache/' \ + --exclude 'npm/' \ + --exclude 'git/' \ + --exclude 'trust.json' \ + --exclude 'mcp-*' \ + --exclude 'run-history*' \ + --exclude 'vibes/' \ + --exclude '*.log' \ + --exclude 'scratch/' \ + --exclude '.pi/' \ + "$SRC/" "$DEST/" +echo "rsync-pi-agent: synced $(find "$DEST" -type f | wc -l) files from $SRC" diff --git a/docker/worker.Dockerfile b/docker/worker.Dockerfile index df7fdf2..f98c6ef 100644 --- a/docker/worker.Dockerfile +++ b/docker/worker.Dockerfile @@ -11,12 +11,19 @@ RUN apt-get update \ RUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent -# lvmh plugin: dial-home mirror (built from plugin/ at repo root) +# User's pi config from dotfiles (settings, skills, agents, extensions, +# APPEND_SYSTEM.md) — synced by deploy.sh from ~/.dotfiles/pi/agent (filtered: +# no auth.json/sessions/cache/npm). If docker/pi-agent/ is absent this layer +# is skipped (mkdir keeps later COPY targets valid). +COPY docker/pi-agent/ /root/.pi/agent/ + +# lvmh overlays: dial-home plugin + env-ref models.json win over dotfiles copies. COPY plugin/lvmh-agent.ts /root/.pi/agent/extensions/lvmh-agent.ts COPY docker/worker-models.json /root/.pi/agent/models.json COPY docker/bridge /bridge -# Per-session pi sessions persist here (volume lvmh-sessions) +# Per-session pi sessions persist here (volume lvmh-sessions); package cache +# shared across spawns via volume lvmh-pi-cache at /root/.pi/agent/cache. ENV PI_SESSION_DIR=/pi-sessions WORKDIR /workspace CMD ["node", "/bridge/index.mjs"]