Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ integration.test

# Tailwind CLI binary cache (downloaded by `make tailwind`)
tools/
docker/.env.experiment
26 changes: 26 additions & 0 deletions docker/.env.experiment.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copy to .env.experiment and fill in. .env.experiment is gitignored.
# Tokens may reference repo-root .env vars (CODEBERG_TOKEN_FULL,
# ANTHROPIC_TOKEN) when sourced together:
# set -a && . .env && . docker/.env.experiment && set +a
#
# Target project on Codeberg/Gitea.
WAVE_PROJECT_HOST=codeberg.org
WAVE_PROJECT_OWNER=libretech
WAVE_PROJECT_REPO=wave-testing
# Token with at minimum: repo:read, issue:read+write. Get from:
# https://codeberg.org/-/user/settings/applications
WAVE_PROJECT_TOKEN=${CODEBERG_TOKEN_FULL}

# Adapter + model defaults (per memory feedback_model_run_ladder).
WAVE_ADAPTER=claude
WAVE_MODEL=balanced

# Anthropic API key for claude-code subprocesses. Get from:
# https://console.anthropic.com/settings/keys
ANTHROPIC_API_KEY=${ANTHROPIC_TOKEN}

# First smoke: skip onboard-project (zero LLM tokens). Unset to enable.
WAVE_SKIP_ONBOARD=1

# Host port mapping. Change if 8080 is taken.
WAVE_HOST_PORT=8080
67 changes: 67 additions & 0 deletions docker/Dockerfile.experiment
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# syntax=docker/dockerfile:1.7

# Reproducible Wave experiment VM — clones a target project via tea, runs
# wave init + onboard-project, exposes the webui on :8080. Used for the
# Phase 4 end-to-end smoke described in
# docs/scope/codecrispies-walkthrough.md.

# ---------- Stage 1: build wave from source ----------
FROM golang:1.25-bookworm AS wave-build

WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download

COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=1 go build -tags=webui_preview -o /out/wave ./cmd/wave

# ---------- Stage 2: tea CLI (gitea/forgejo client) ----------
FROM debian:bookworm-slim AS tea-fetch
ARG TEA_VERSION=0.11.1
ARG TEA_ARCH=amd64
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /out \
&& curl -fsSL "https://gitea.com/gitea/tea/releases/download/v${TEA_VERSION}/tea-${TEA_VERSION}-linux-${TEA_ARCH}" \
-o /out/tea \
&& chmod +x /out/tea

# ---------- Stage 3: runtime image ----------
FROM node:22-bookworm-slim AS runtime

# node:22-bookworm-slim ships a `node` user at UID 1000 — reuse it.
ENV DEBIAN_FRONTEND=noninteractive \
HOME=/home/node \
WAVE_HOME=/home/node \
PATH=/usr/local/bin:/usr/bin:/bin:/home/node/.local/bin

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
git \
curl \
jq \
sqlite3 \
ca-certificates \
bash \
&& rm -rf /var/lib/apt/lists/*

# Claude Code CLI ships via npm.
RUN npm install -g @anthropic-ai/claude-code \
&& npm cache clean --force

COPY --from=wave-build /out/wave /usr/local/bin/wave
COPY --from=tea-fetch /out/tea /usr/local/bin/tea

COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

USER node
WORKDIR /work
EXPOSE 8080

# compose init:true injects Docker's own tini as PID1 — no separate tini binary needed.
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
52 changes: 52 additions & 0 deletions docker/docker-compose.experiment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Reproducible Wave experiment VM compose. Boots wave + claude-code + tea
# against a target repo and exposes the webui on :8080.
#
# Usage:
# cp docker/.env.experiment.example docker/.env.experiment
# $EDITOR docker/.env.experiment # set tokens, target host/owner/repo
# docker compose -f docker/docker-compose.experiment.yml \
# --env-file docker/.env.experiment up --build
#
# After boot, browse http://localhost:8080 — the entry redirect goes to
# /onboard on first run (no sentinel) or /work afterwards.

services:
wave:
build:
context: ..
dockerfile: docker/Dockerfile.experiment
image: wave-experiment:latest
container_name: wave-experiment
init: true
environment:
WAVE_PROJECT_HOST: "${WAVE_PROJECT_HOST}"
WAVE_PROJECT_OWNER: "${WAVE_PROJECT_OWNER}"
WAVE_PROJECT_REPO: "${WAVE_PROJECT_REPO}"
WAVE_PROJECT_TOKEN: "${WAVE_PROJECT_TOKEN}"
WAVE_ADAPTER: "${WAVE_ADAPTER:-claude}"
WAVE_MODEL: "${WAVE_MODEL:-balanced}"
WAVE_PORT: "8080"
# Anthropic API auth — claude-code reads this. Rotate via .env.experiment.
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}"
ports:
- "${WAVE_HOST_PORT:-8080}:8080"
volumes:
# /work survives container rebuilds; the project clone + wave.yaml +
# .agents/state.db live here.
- wave-work:/work
# claude-code OAuth + tea login state. Mounting a host dir here lets
# `claude /login` happen once and persist across `docker compose up`.
- wave-config:/home/wave/.config
healthcheck:
test: ["CMD-SHELL", "curl -fs http://localhost:8080/health || exit 1"]
interval: 30s
timeout: 5s
retries: 5
start_period: 60s
restart: unless-stopped

volumes:
wave-work:
name: wave-experiment-work
wave-config:
name: wave-experiment-config
111 changes: 111 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
# Wave experiment VM entrypoint. Clones a target project via tea, runs
# wave init + onboard-project, then boots the webui.
#
# Required env:
# WAVE_PROJECT_HOST — e.g. codeberg.org
# WAVE_PROJECT_OWNER — e.g. libretech
# WAVE_PROJECT_REPO — e.g. wave-testing
# WAVE_PROJECT_TOKEN — token with read access (passes to tea)
#
# Optional env:
# WAVE_ADAPTER — claude (default) | opencode
# WAVE_MODEL — balanced (default) | opus | cheapest
# WAVE_PORT — 8080 (default)
# WAVE_SKIP_ONBOARD — set non-empty to skip onboard-project (manifest-only)
#
# Volume layout (compose mounts these):
# /work — project clone (target repo's working tree)
# /home/wave/.agents — Wave state + outputs (persists across container restarts)
# /home/wave/.config — claude-code auth + tea config

set -e

WAVE_PROJECT_HOST="${WAVE_PROJECT_HOST:-}"
WAVE_PROJECT_OWNER="${WAVE_PROJECT_OWNER:-}"
WAVE_PROJECT_REPO="${WAVE_PROJECT_REPO:-}"
WAVE_PROJECT_TOKEN="${WAVE_PROJECT_TOKEN:-}"
WAVE_ADAPTER="${WAVE_ADAPTER:-claude}"
WAVE_MODEL="${WAVE_MODEL:-balanced}"
WAVE_PORT="${WAVE_PORT:-8080}"

log() { printf '[entrypoint %s] %s\n' "$(date -u +%H:%M:%S)" "$*" >&2; }

require_env() {
for var in WAVE_PROJECT_HOST WAVE_PROJECT_OWNER WAVE_PROJECT_REPO WAVE_PROJECT_TOKEN; do
if [ -z "${!var}" ]; then
log "missing required env: $var"
exit 1
fi
done
}

# Idempotent: if /work/.git exists, skip clone — container restarts reuse the
# clone instead of fetching cold every time.
clone_project() {
if [ -d /work/.git ]; then
log "/work already a git tree; skipping clone"
return 0
fi
log "configuring tea login for ${WAVE_PROJECT_HOST}"
tea login add \
--name wave-vm \
--url "https://${WAVE_PROJECT_HOST}" \
--token "${WAVE_PROJECT_TOKEN}" \
>/dev/null

log "cloning ${WAVE_PROJECT_OWNER}/${WAVE_PROJECT_REPO}"
cd /work
git clone \
"https://oauth2:${WAVE_PROJECT_TOKEN}@${WAVE_PROJECT_HOST}/${WAVE_PROJECT_OWNER}/${WAVE_PROJECT_REPO}.git" \
.

# Embed the token in remote so wave/tea can push later. Replaced on each
# boot so a rotated token takes effect without a fresh clone.
git remote set-url origin \
"https://oauth2:${WAVE_PROJECT_TOKEN}@${WAVE_PROJECT_HOST}/${WAVE_PROJECT_OWNER}/${WAVE_PROJECT_REPO}.git"
}

init_wave() {
cd /work
if [ -f wave.yaml ] && [ -d .agents ]; then
log "wave.yaml + .agents already present; skipping wave init"
return 0
fi
log "running wave init --adapter ${WAVE_ADAPTER}"
wave init --adapter "${WAVE_ADAPTER}" >/dev/null
}

run_onboard_project() {
if [ -n "${WAVE_SKIP_ONBOARD:-}" ]; then
log "WAVE_SKIP_ONBOARD set; skipping onboard-project"
return 0
fi
if [ -f /work/.agents/.onboarding-done ]; then
log "onboarding sentinel present; skipping onboard-project"
return 0
fi
log "running onboard-project (adapter=${WAVE_ADAPTER} model=${WAVE_MODEL})"
cd /work
wave run onboard-project \
--adapter "${WAVE_ADAPTER}" \
--model "${WAVE_MODEL}" \
--auto-approve \
--no-tui
}

boot_webui() {
log "booting webui on :${WAVE_PORT}"
cd /work
exec wave serve --bind 0.0.0.0 --port "${WAVE_PORT}"
}

main() {
require_env
clone_project
init_wave
run_onboard_project
boot_webui
}

main "$@"
112 changes: 112 additions & 0 deletions docs/scope/2026-05-02-next-session.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Next Session Plan — 2026-05-02

## Current branch: `feat/phase4-docker-vm`

### What's on this branch (3 commits ahead of main)

```
77dee784 fix(docker): golang:1.25, drop tini+gh, reuse node user
523c9199 fix(docker): mkdir /out before tea fetch + map ANTHROPIC_TOKEN env
ed33923d feat(docker): Phase 4 reproducible experiment VM (Dockerfile + entrypoint + compose)
```

Files: `docker/Dockerfile.experiment`, `docker/entrypoint.sh`, `docker/docker-compose.experiment.yml`

Branch is **5 commits behind main** (contract guards + rollback + docs merged while branch was open).
Must rebase before merge.

### Untracked files (safe to ignore, not in .gitignore)

Session-planning docs left over from prior work — no code, no impact:
- `docs/2026-04-28-issue-1452-plan.md`
- `docs/2026-04-28-session-plan.md`
- `docs/scope/2026-04-29-phase1-execution-plan.md`
- `docs/scope/2026-04-30-remaining-work.md`
- `docs/scope/2026-04-30-session-pause.md`

Either commit them as `docs: session planning artifacts` or `git clean -f` them. Low stakes.

`docker/.env.experiment` — gitignored, has real tokens. Keep. Required for smoke run.

---

## What needs doing (in order)

### 1. Rebase branch on main
```bash
git rebase main
```
5 commits, no overlap with docker/ files — should be clean.

### 2. docker compose build (needs unmetered connection)
```bash
set -a && . .env && set +a
docker compose -f docker/docker-compose.experiment.yml \
--env-file docker/.env.experiment build
```
Pulls `golang:1.25-bookworm` (~700MB) + `node:22-bookworm-slim` (~250MB). First pull only.
Layer cache means rebuilds are fast after.

### 3. Smoke boot — WAVE_SKIP_ONBOARD=1 (zero LLM tokens)
```bash
docker compose -f docker/docker-compose.experiment.yml \
--env-file docker/.env.experiment up
```
`docker/.env.experiment` already has `WAVE_SKIP_ONBOARD=1`.

Verify:
- Container starts, tea clones `codeberg.org/libretech/wave-testing` into `wave-work` volume
- `wave init` runs, `wave.yaml` + `.agents/` appear in `/work`
- webui boots on `:8080`, root redirects to `/onboard` (no sentinel yet)
- `curl http://localhost:8080/health` → 200

### 4. Full onboard smoke (uses LLM tokens, needs unmetered)
Edit `docker/.env.experiment`, comment out `WAVE_SKIP_ONBOARD=1`, restart:
```bash
docker compose -f docker/docker-compose.experiment.yml \
--env-file docker/.env.experiment up
```

Verify (per epic gate):
- `docker exec wave-experiment ls /work/.agents/` shows personas, pipelines, prompts
- `docker exec wave-experiment cat /work/.agents/.onboarding-done` exists
- Browser: `http://localhost:8080` → `/work` board (sentinel present, redirect switches)
- `/work` board shows issues from `codeberg.org/libretech/wave-testing`

### 5. Comment results on epic #1565
```bash
gh issue comment 1565 --repo re-cinq/wave --body "Phase 4 4.4 smoke: ..."
```
Update epic checklist boxes 4.1/4.2/4.3/4.4.

### 6. Merge PR #1617
After smoke passes. PR is `MERGEABLE`, not draft.
```bash
gh pr merge 1617 --repo re-cinq/wave --squash --delete-branch
```

---

## Known issues already fixed (on branch)

| Issue | Fix | Commit |
|---|---|---|
| `golang:1.22` < go.mod requirement (1.25.5) | → `golang:1.25-bookworm` | 77dee784 |
| `tini` absent from `node:22-bookworm-slim` | removed; compose `init:true` handles PID1 | 77dee784 |
| `gh` CLI absent from slim + not needed (tea handles Codeberg) | removed | 77dee784 |
| `useradd` absent (no `passwd` pkg in slim) | reuse pre-existing `node` user (UID 1000) | 77dee784 |
| `mkdir /out` missing in tea-fetch stage | pre-create dir before curl | 523c9199 |
| `ANTHROPIC_TOKEN` env not forwarded to claude-code | mapped as `ANTHROPIC_API_KEY` | 523c9199 |

## Open questions before merge

- Does `node:22-bookworm-slim`'s `node` user home default to `/home/node`? Verify `echo $HOME` inside container.
- `wave init` on a fresh clone with no `wave.yaml` — does it work non-interactively? (`--adapter claude` flag in entrypoint is passed but `wave init` may still prompt)
- Volume `wave-work` persists across `docker compose down` — correct by design (idempotent clone). `docker compose down -v` to reset.

---

## Epic state

Phases 0/1/1.5/2/3 + all follow-ups: **MERGED**.
Phase 4: PR open, build fixes on branch, smoke deferred to unmetered.
Loading