| name | local-stack |
| description | Run a regelrecht branch locally end-to-end (worktree + Docker stack + DB), with the workarounds needed when developing inside the dev container against Docker Desktop. Accepts a branch name or a PR number. Subcommands: bare arg = start, `stop <arg>` = shut down, `clean <arg>` = shut down and remove worktree. |
| user_invocable | true |
Local Stack
Spin up a regelrecht branch in a self-contained Docker stack so the user can test it from their browser. Works on any branch — a PR number is just a convenient way to look one up. Handles the quirks that show up when Claude Code is running inside the dev container against Docker Desktop on Windows.
Context: why this skill exists
just local (introduced in PR #274) assumes you run it on a host Docker daemon — when you run it from the dev container against Docker Desktop, three things bite:
- Bind mounts break for
prometheus + grafana. Docker Desktop's daemon doesn't see paths under /workspace/... (the dev container's filesystem); it silently creates empty directories at the mount source. The two services that bind-mount config files (./dev/prometheus.yml, ./dev/grafana-datasource-local.yaml, the dashboards/alerting dirs) crash on start. The other services have no bind mounts.
- Ports 7100-7300, 3000-3001, 8000 are already claimed by the dev container on the Windows host, so any
docker-compose ports: mapping in those ranges fails with "port is already allocated". We use 18000+ instead (free on the Windows host, reachable from Windows).
~/.docker/config.json may reference a credsStore helper that doesn't exist inside this container (e.g. dev-containers-...). Even pulling a public image then fails with error getting credentials - err: exit status 255.
Instructions
1. Parse argument
The argument is either:
- A branch name (anything containing
/ or matching a local/remote ref) → use directly.
- An integer → look up the corresponding PR's branch.
Action depends on the leading verb:
<arg> (no verb) → start the stack for that branch.
stop <arg> → shut down the stack (worktree kept).
clean <arg> → shut down and remove the worktree.
If the user invoked the skill without an argument, ask which branch or PR.
2. Resolve to a branch name
If the argument is a pure integer, treat as a PR:
gh pr view <N> --json number,title,headRefName,state,author
Confirm state == "OPEN" (warn but proceed otherwise). Use headRefName as the branch.
If the argument is a branch name, use it as-is. Verify it exists on origin:
git -C /workspace/regelrecht ls-remote --exit-code --heads origin <branch>
For stop/clean: if the worktree already exists at the predictable path, skip resolution and reuse the existing path; otherwise still resolve to derive the worktree directory.
3. (start only) Pre-flight: Docker credential helper
Read ~/.docker/config.json. If it has a credsStore field, run a quick public-image pull to test:
docker pull alpine:3 >/dev/null 2>&1
If that fails with error getting credentials, back up the file and reset:
cp ~/.docker/config.json ~/.docker/config.json.bak
echo '{}' > ~/.docker/config.json
Tell the user: "Reset broken Docker credsStore (backup at config.json.bak)."
4. (start only) Fetch + worktree
Per project CLAUDE.md, always fetch before working with any branch:
git -C /workspace/regelrecht fetch origin <branch>
Derive a worktree slug from the branch: replace / with -, cap at ~40 chars. The worktree path is .worktrees/<slug>. Create it on the branch (not detached):
git -C /workspace/regelrecht worktree add .worktrees/<slug> -B <branch> origin/<branch>
cp -r /workspace/regelrecht/.claude /workspace/regelrecht/.worktrees/<slug>/
If the worktree already exists, reuse it but git -C <worktree> pull --ff-only origin <branch> to bring it up to date — abort if there are local uncommitted changes (the user may be mid-fix).
5. (start only) Pick free ports
Default ports: FRONTEND_PORT=18130, ADMIN_PORT=18180. Before using them, check they aren't held:
docker ps --format '{{.Ports}}' | grep -E ':(18130|18180)->'
If either is taken (e.g. another PR is already running), increment by 100 (18230, 18280, …) until a free pair is found. Record the chosen pair for the final URL output.
6. (start only) Bring up the stack
Skip prometheus and grafana (bind-mount issue — see Context). Start exactly the runtime services:
FRONTEND_PORT=<frontend> ADMIN_PORT=<admin> \
docker compose \
-f /workspace/regelrecht/.worktrees/<wt>/docker-compose.dev.yml \
-f /workspace/regelrecht/.worktrees/<wt>/dev/compose.local.yaml \
--project-directory /workspace/regelrecht/.worktrees/<wt> \
up -d --build postgres admin harvester-worker enrich-worker pipelineapi frontend
--project-directory is required when invoking compose from outside the worktree — without it, the compose project name collides with other PR worktrees.
The first build is slow (Rust release build for 5+ services, ~5-10 min on a clean cache). Run in the background and stream key events: Building, Built, Started, Healthy, Error, error:, failed, panic, unhealthy. If a build fails, surface the failing target and the last error block — don't blindly retry.
7. (start only) Verify the stack
After compose returns, check container health:
docker ps --filter 'name=<slug>' --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
postgres should report (healthy). pipelineapi may briefly show (unhealthy) while its first healthcheck runs — that's only a problem if it stays unhealthy for >30s. Tail the editor-api logs to confirm migrations applied and the OIDC mode:
docker logs --tail 30 <slug>-frontend-1 2>&1 | tail -10
Look for editor-api ... listening on 0.0.0.0:8000. The line OIDC authentication is DISABLED — editor is unprotected is expected locally — both admin and editor are designed to run auth-loose without OIDC env vars.
Verify the latest migration:
docker exec <slug>-postgres-1 \
psql -U regelrecht regelrecht_pipeline \
-c 'SELECT version, description FROM _sqlx_migrations ORDER BY version DESC LIMIT 3;'
If the PR adds a migration, point out whether it's the top row.
8. (start only) Report URLs and warnings
Print:
✅ Branch <branch> running locally
Editor: http://localhost:<frontend>
Admin: http://localhost:<admin>
Stop: /local-stack stop <arg>
Cleanup: /local-stack clean <arg> (also removes the worktree)
If the branch's diff vs origin/main suggests an auth-gated feature (anything touching person_sub, useAuth, login menu items, OIDC), warn explicitly:
⚠️ This branch's feature appears to require an authenticated user (person_sub). Locally OIDC is off, so anonymous calls will hit 401. Auth-gated UI elements (e.g. account-menu items keyed on authenticated) will not appear locally.
Detection heuristic: git -C <worktree> diff origin/main...HEAD | grep -E 'person_sub|useAuth|authenticated|OIDC|session' returning matches → emit the warning.
9. (stop) Shut down
docker compose \
-f /workspace/regelrecht/.worktrees/<wt>/docker-compose.dev.yml \
-f /workspace/regelrecht/.worktrees/<wt>/dev/compose.local.yaml \
--project-directory /workspace/regelrecht/.worktrees/<wt> \
down
Volumes (pgdata, repo-clones, etc.) are preserved by default — pass -v only if the user explicitly asks for a clean DB next time.
10. (clean) Shut down + remove worktree
After down, also:
git -C /workspace/regelrecht worktree remove .worktrees/<wt>
If the worktree has uncommitted changes, stop and ask before removing — the user may have in-flight edits to commit. Do not pass --force.
What this skill does not do
- Doesn't enable OIDC locally. If the user wants to test auth-gated behavior, they need real OIDC creds + a Keycloak client whose redirect URI includes
http://localhost:<frontend>/auth/callback. That's out of scope here.
- Doesn't run prometheus/grafana. If observability is needed, that's a separate workflow (e.g.
just dev on a host Docker daemon outside the dev container).
- Doesn't run the test suite, lint, or formatters — use
just check or the relevant just recipes for that.
- Doesn't push, comment, or modify the PR. Read-only locally.
Failure recovery
port is already allocated: another stack is running. Run docker ps to identify it, then either stop that stack (/local-stack stop <other>) or pick a higher port pair manually.
bind for ... failed on a service other than prometheus/grafana: a regression — investigate which new bind mount was added; don't silently work around it.
- Migrations didn't apply: check
pipelineapi and editor-api logs for acquiring migration lock / migrations completed. A schema mismatch (renumbered migration) requires down -v to wipe pgdata, then start again.
- Build fails on a Rust target: report the actual cargo error. Don't suggest
--no-cache or retry without a real cause.