feat: add disinto-factory skill for guided setup and operations
All checks were successful
ci/woodpecker/push/ci Pipeline was successful

Distributable skill file (SKILL.md) that walks an AI agent through:
- First-time factory setup with interactive [ASK] prompts
- Post-init verification checklist
- Mirror configuration to GitHub/Codeberg
- Backlog seeding and issue creation
- Ongoing monitoring: agent status, CI, PRs
- Unsticking blocked issues

Includes:
- scripts/factory-status.sh — one-command factory health check
- references/troubleshooting.md — common issues from real deployments
- Slimmed CLAUDE.md pointing to the skill

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
johba 2026-03-28 11:13:24 +00:00
parent ed43f9db11
commit cbe5df52b2
4 changed files with 318 additions and 237 deletions

248
CLAUDE.md
View file

@ -1,246 +1,20 @@
# CLAUDE.md — Skill file for disinto
# CLAUDE.md
## What is disinto?
This repo is **disinto** — an autonomous code factory.
Disinto is an autonomous code factory — bash scripts + Claude CLI that automate the full
software development lifecycle: picking up issues, implementing via Claude, creating PRs,
running CI, reviewing, merging, and mirroring to external forges.
For setup and operations, load the `disinto-factory` skill from `disinto-factory/SKILL.md`.
Read `VISION.md` for the project philosophy, `AGENTS.md` for architecture, and
`BOOTSTRAP.md` for setup instructions.
## Setting up a new factory instance
### Prerequisites
- An LXD container (Debian 12) with Docker, git, jq, curl, tmux, python3 (>=3.11)
- `claude` CLI installed and authenticated
- SSH key for mirror pushes (added to GitHub/Codeberg)
### First-time setup
1. **Clone the repo** and cd into it:
```bash
git clone https://codeberg.org/johba/disinto.git && cd disinto
```
2. **Run init** against the repo you want the factory to develop:
```bash
bin/disinto init https://codeberg.org/org/repo --yes
```
For self-development (factory develops itself):
```bash
bin/disinto init https://codeberg.org/johba/disinto --yes --repo-root $(pwd)
```
3. **Verify the stack** came up:
```bash
docker ps --format "table {{.Names}}\t{{.Status}}"
```
Expected: forgejo (Up), woodpecker (healthy), woodpecker-agent (healthy), agents (Up),
edge (Up), staging (Up).
4. **Check WOODPECKER_TOKEN** was generated:
```bash
grep WOODPECKER_TOKEN .env
```
If empty, see "Known issues" below.
5. **Verify agent cron** is running:
```bash
docker exec -u agent disinto-agents-1 crontab -l -u agent
```
6. **Set up mirrors** (optional):
Edit `projects/<name>.toml`:
```toml
[mirrors]
github = "git@github.com:Org/repo.git"
codeberg = "git@codeberg.org:user/repo.git"
```
Ensure `~/.ssh` is mounted into the agents container and SSH keys are added
to the remote forges. The compose template includes the mount; just add your
public key to GitHub/Codeberg.
### Post-init checklist
- [ ] Stack containers all running and healthy
- [ ] `WOODPECKER_TOKEN` in `.env` is non-empty
- [ ] `projects/<name>.toml` exists with correct `repo_root` and `primary_branch`
- [ ] Labels exist on Forgejo repo: backlog, in-progress, blocked, tech-debt, etc.
- [ ] Agent container can reach Forgejo API: `docker exec disinto-agents-1 bash -c "source /home/agent/disinto/.env && curl -sf http://forgejo:3000/api/v1/version"`
- [ ] Agent repo is cloned: `docker exec -u agent disinto-agents-1 ls /home/agent/repos/<name>`
- If not: `docker exec disinto-agents-1 chown -R agent:agent /home/agent/repos && docker exec -u agent disinto-agents-1 bash -c "source /home/agent/disinto/.env && git clone http://dev-bot:\${FORGE_TOKEN}@forgejo:3000/org/repo.git /home/agent/repos/<name>"`
- [ ] Create backlog issues on Forgejo for the factory to work on
## Checking on the factory
### Agent status
```bash
# Are agents running?
docker exec disinto-agents-1 bash -c "
for f in /proc/[0-9]*/cmdline; do
cmd=\$(tr '\0' ' ' < \$f 2>/dev/null)
echo \$cmd | grep -qi claude && echo PID \$(echo \$f | cut -d/ -f3): running
done
"
# Latest dev-agent activity
docker exec disinto-agents-1 tail -20 /home/agent/data/logs/dev/dev-agent.log
# Latest poll activity
docker exec disinto-agents-1 tail -20 /home/agent/data/logs/dev/dev-agent-<project>.log
```
### Issue and PR status
```bash
source .env
# Open issues
curl -sf "http://localhost:3000/api/v1/repos/<org>/<repo>/issues?state=open" \
-H "Authorization: token $FORGE_TOKEN" | jq -r '.[] | "#\(.number) [\(.labels | map(.name) | join(","))] \(.title)"'
# Open PRs
curl -sf "http://localhost:3000/api/v1/repos/<org>/<repo>/pulls?state=open" \
-H "Authorization: token $FORGE_TOKEN" | jq -r '.[] | "PR #\(.number) [\(.head.ref)] \(.title)"'
```
### CI status
```bash
source .env
# Check pipelines (requires session cookie + CSRF for WP v3 API)
WP_CSRF=$(curl -sf -b "user_sess=$WOODPECKER_TOKEN" http://localhost:8000/web-config.js \
| sed -n 's/.*WOODPECKER_CSRF = "\([^"]*\)".*/\1/p')
curl -sf -b "user_sess=$WOODPECKER_TOKEN" -H "X-CSRF-Token: $WP_CSRF" \
"http://localhost:8000/api/repos/1/pipelines?page=1&per_page=5" \
| jq '.[] | {number, status, event}'
```
### Unsticking a blocked issue
When a dev-agent run fails (CI timeout, implementation error), the issue gets labeled
`blocked`. To retry:
```bash
source .env
# 1. Close stale PR if any
curl -sf -X PATCH "http://localhost:3000/api/v1/repos/<org>/<repo>/pulls/<N>" \
-H "Authorization: token $FORGE_TOKEN" -H "Content-Type: application/json" \
-d '{"state":"closed"}'
# 2. Delete stale branch
curl -sf -X DELETE "http://localhost:3000/api/v1/repos/<org>/<repo>/branches/fix/issue-<N>" \
-H "Authorization: token $FORGE_TOKEN"
# 3. Remove locks
docker exec disinto-agents-1 rm -f /tmp/dev-agent-*.json /tmp/dev-agent-*.lock
# 4. Relabel issue to backlog
BACKLOG_ID=$(curl -sf "http://localhost:3000/api/v1/repos/<org>/<repo>/labels" \
-H "Authorization: token $FORGE_TOKEN" | jq -r '.[] | select(.name=="backlog") | .id')
curl -sf -X PUT "http://localhost:3000/api/v1/repos/<org>/<repo>/issues/<N>/labels" \
-H "Authorization: token $FORGE_TOKEN" -H "Content-Type: application/json" \
-d "{\"labels\":[$BACKLOG_ID]}"
# 5. Update agent repo to latest main
docker exec -u agent disinto-agents-1 bash -c \
"cd /home/agent/repos/<name> && git fetch origin && git reset --hard origin/main"
```
The next cron cycle (every 5 minutes) will pick it up.
### Triggering a poll manually
```bash
docker exec -u agent disinto-agents-1 bash -c \
"cd /home/agent/disinto && bash dev/dev-poll.sh projects/<name>.toml"
```
## Filing issues
The factory picks up issues labeled `backlog`. The dev-agent:
1. Claims the issue (labels it `in-progress`)
2. Creates a worktree on branch `fix/issue-<N>`
3. Runs Claude to implement the fix
4. Pushes, creates a PR, waits for CI
5. Requests review from review-bot
6. Merges on approval, pushes to mirrors
Issue body should contain enough context for Claude to implement it. Include:
- What's wrong or what needs to change
- Which files are affected
- Any design constraints
- Dependency references: `Depends-on: #N` (dev-agent checks these before starting)
Use labels:
- `backlog` — ready for the dev-agent to pick up
- `blocked` — not ready (missing dependency, needs investigation)
- `in-progress` — claimed by dev-agent (set automatically)
- No label — parked, not for the factory to touch
## Reverse tunnel access (for browser UI)
If running in an LXD container with a reverse SSH tunnel to a jump host:
```bash
# On the LXD container, add to /etc/systemd/system/reverse-tunnel.service:
# -R 127.0.0.1:13000:localhost:3000 (Forgejo)
# -R 127.0.0.1:18000:localhost:8000 (Woodpecker)
# From your machine:
ssh -L 3000:localhost:13000 user@jump-host
# Then open http://localhost:3000 in your browser
```
Forgejo admin login: `disinto-admin` / set during init (or reset with
`docker exec disinto-forgejo-1 su -c "forgejo admin user change-password --username disinto-admin --password <pw> --must-change-password=false" git`).
## Known issues & workarounds
### WP CI agent needs host networking in LXD
Docker bridge networking inside LXD breaks gRPC/HTTP2. The compose template uses
`network_mode: host` + `privileged: true` for the WP agent, connecting via
`localhost:9000`. This is baked into the template and works on regular VMs too.
### CI step containers need Docker network
The WP agent spawns CI containers that need to reach Forgejo for git clone.
`WOODPECKER_BACKEND_DOCKER_NETWORK: disinto_disinto-net` is set in the compose
template to put CI containers on the compose network.
### Forgejo webhook allowlist
Forgejo blocks outgoing webhooks by default. The compose template sets
`FORGEJO__webhook__ALLOWED_HOST_LIST: "private"` to allow delivery to
Docker-internal hosts.
### OAuth2 token generation during init
The init script drives a Forgejo OAuth2 flow to generate a Woodpecker token.
This requires rewriting URL-encoded Docker-internal hostnames and submitting
all Forgejo grant form fields. If token generation fails, check Forgejo logs
for "Unregistered Redirect URI" errors.
### Woodpecker UI not accessible via tunnel
The WP OAuth login redirects use Docker-internal hostnames that browsers can't
resolve. Use the Forgejo UI instead — CI results appear as commit statuses on PRs.
### PROJECT_REPO_ROOT inside agents container
The agents container needs `PROJECT_REPO_ROOT` set in its environment to
`/home/agent/repos/<name>` (not the host path from the TOML). The compose
template includes this. If the agent fails with "cd: no such file or directory",
check this env var.
Quick references:
- `AGENTS.md` — per-agent architecture and file-level docs
- `VISION.md` — project philosophy
- `BOOTSTRAP.md` — detailed init walkthrough
- `disinto-factory/references/troubleshooting.md` — common issues and fixes
- `disinto-factory/scripts/factory-status.sh` — quick status check
## Code conventions
See `AGENTS.md` for per-file architecture docs and coding conventions.
Key principles:
- Bash for checks, AI for judgment
- Zero LLM tokens when idle (cron checks are pure bash)
- Zero LLM tokens when idle (cron polls are pure bash)
- Fire-and-forget mirror pushes (never block the pipeline)
- Issues are the unit of work; PRs are the delivery mechanism
- See `AGENTS.md` for per-file watermarks and coding conventions

210
disinto-factory/SKILL.md Normal file
View file

@ -0,0 +1,210 @@
---
name: disinto-factory
description: Set up and operate a disinto autonomous code factory. Use when bootstrapping a new factory instance, checking on agents and CI, managing the backlog, or troubleshooting the stack.
---
# Disinto Factory
You are helping the user set up and operate a **disinto autonomous code factory** — a system
of bash scripts and Claude CLI that automates the full development lifecycle: picking up
issues, implementing via Claude, creating PRs, running CI, reviewing, merging, and mirroring.
## First-time setup
Walk the user through these steps interactively. Ask questions where marked with [ASK].
### 1. Environment
[ASK] Where will the factory run? Options:
- **LXD container** (recommended for isolation) — need Debian 12, Docker, nesting enabled
- **Bare VM or server** — need Debian/Ubuntu with Docker
- **Existing container** — check prerequisites
Verify prerequisites:
```bash
docker --version && git --version && jq --version && curl --version && tmux -V && python3 --version && claude --version
```
Any missing tool — help the user install it before continuing.
### 2. Clone and init
```bash
git clone https://codeberg.org/johba/disinto.git && cd disinto
```
[ASK] What repo should the factory develop? Options:
- **Itself** (self-development): `bin/disinto init https://codeberg.org/johba/disinto --yes --repo-root $(pwd)`
- **Another project**: `bin/disinto init <repo-url> --yes`
Run the init and watch for:
- All bot users created (dev-bot, review-bot, etc.)
- `WOODPECKER_TOKEN` generated and saved
- Stack containers all started
### 3. Post-init verification
Run this checklist — fix any failures before proceeding:
```bash
# Stack healthy?
docker ps --format "table {{.Names}}\t{{.Status}}"
# Expected: forgejo, woodpecker (healthy), woodpecker-agent (healthy), agents, edge, staging
# Token generated?
grep WOODPECKER_TOKEN .env | grep -v "^$" && echo "OK" || echo "MISSING — see references/troubleshooting.md"
# Agent cron active?
docker exec -u agent disinto-agents-1 crontab -l -u agent
# Agent can reach Forgejo?
docker exec disinto-agents-1 bash -c "source /home/agent/disinto/.env && curl -sf http://forgejo:3000/api/v1/version | jq .version"
# Agent repo cloned?
docker exec -u agent disinto-agents-1 ls /home/agent/repos/
```
If the agent repo is missing, clone it:
```bash
docker exec disinto-agents-1 chown -R agent:agent /home/agent/repos
docker exec -u agent disinto-agents-1 bash -c "source /home/agent/disinto/.env && git clone http://dev-bot:\${FORGE_TOKEN}@forgejo:3000/<org>/<repo>.git /home/agent/repos/<name>"
```
### 4. Mirrors (optional)
[ASK] Should the factory mirror to external forges? If yes, which?
- GitHub: need repo URL and SSH key added to GitHub account
- Codeberg: need repo URL and SSH key added to Codeberg account
Show the user their public key:
```bash
cat ~/.ssh/id_ed25519.pub
```
Test SSH access:
```bash
ssh -T git@github.com 2>&1; ssh -T git@codeberg.org 2>&1
```
If SSH host keys are missing: `ssh-keyscan github.com codeberg.org >> ~/.ssh/known_hosts 2>/dev/null`
Edit `projects/<name>.toml` to add mirrors:
```toml
[mirrors]
github = "git@github.com:Org/repo.git"
codeberg = "git@codeberg.org:user/repo.git"
```
Test with a manual push:
```bash
source .env && source lib/env.sh && export PROJECT_TOML=projects/<name>.toml && source lib/load-project.sh && source lib/mirrors.sh && mirror_push
```
### 5. Seed the backlog
[ASK] What should the factory work on first? Brainstorm with the user.
Help them create issues on the local Forgejo. Each issue needs:
- A clear title prefixed with `fix:`, `feat:`, or `chore:`
- A body describing what to change, which files, and any constraints
- The `backlog` label (so the dev-agent picks it up)
```bash
source .env
BACKLOG_ID=$(curl -sf "http://localhost:3000/api/v1/repos/<org>/<repo>/labels" \
-H "Authorization: token $FORGE_TOKEN" | jq -r '.[] | select(.name=="backlog") | .id')
curl -sf -X POST "http://localhost:3000/api/v1/repos/<org>/<repo>/issues" \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"title\": \"<title>\", \"body\": \"<body>\", \"labels\": [$BACKLOG_ID]}"
```
For issues with dependencies, add `Depends-on: #N` in the body — the dev-agent checks
these before starting.
Use labels:
- `backlog` — ready for the dev-agent
- `blocked` — parked, not for the factory
- No label — tracked but not for autonomous work
### 6. Watch it work
The dev-agent polls every 5 minutes. Trigger manually to see it immediately:
```bash
docker exec -u agent disinto-agents-1 bash -c "cd /home/agent/disinto && bash dev/dev-poll.sh projects/<name>.toml"
```
Then monitor:
```bash
# Watch the agent work
docker exec disinto-agents-1 tail -f /home/agent/data/logs/dev/dev-agent.log
# Check for Claude running
docker exec disinto-agents-1 bash -c "for f in /proc/[0-9]*/cmdline; do cmd=\$(tr '\0' ' ' < \$f 2>/dev/null); echo \$cmd | grep -q 'claude.*-p' && echo 'Claude is running'; done"
```
## Ongoing operations
### Check factory status
```bash
source .env
# Issues
curl -sf "http://localhost:3000/api/v1/repos/<org>/<repo>/issues?state=open" \
-H "Authorization: token $FORGE_TOKEN" \
| jq -r '.[] | "#\(.number) [\(.labels | map(.name) | join(","))] \(.title)"'
# PRs
curl -sf "http://localhost:3000/api/v1/repos/<org>/<repo>/pulls?state=open" \
-H "Authorization: token $FORGE_TOKEN" \
| jq -r '.[] | "PR #\(.number) [\(.head.ref)] \(.title)"'
# Agent logs
docker exec disinto-agents-1 tail -20 /home/agent/data/logs/dev/dev-agent.log
```
### Check CI
```bash
source .env
WP_CSRF=$(curl -sf -b "user_sess=$WOODPECKER_TOKEN" http://localhost:8000/web-config.js \
| sed -n 's/.*WOODPECKER_CSRF = "\([^"]*\)".*/\1/p')
curl -sf -b "user_sess=$WOODPECKER_TOKEN" -H "X-CSRF-Token: $WP_CSRF" \
"http://localhost:8000/api/repos/1/pipelines?page=1&per_page=5" \
| jq '.[] | {number, status, event}'
```
### Unstick a blocked issue
When a dev-agent run fails (CI timeout, implementation error), the issue gets labeled `blocked`:
1. Close stale PR and delete the branch
2. `docker exec disinto-agents-1 rm -f /tmp/dev-agent-*.json /tmp/dev-agent-*.lock`
3. Relabel the issue to `backlog`
4. Update agent repo: `docker exec -u agent disinto-agents-1 bash -c "cd /home/agent/repos/<name> && git fetch origin && git reset --hard origin/main"`
### Access Forgejo UI
If running in an LXD container with reverse tunnel:
```bash
# From your machine:
ssh -L 3000:localhost:13000 user@jump-host
# Open http://localhost:3000
```
Reset admin password if needed:
```bash
docker exec disinto-forgejo-1 su -c "forgejo admin user change-password --username disinto-admin --password <new-pw> --must-change-password=false" git
```
## Important context
- Read `AGENTS.md` for per-agent architecture and file-level docs
- Read `VISION.md` for project philosophy
- Read `BOOTSTRAP.md` for detailed init walkthrough
- The factory uses a single internal Forgejo as its forge, regardless of where mirrors go
- Dev-agent uses `claude -p --resume` for session continuity across CI/review cycles
- Mirror pushes happen automatically after every merge (fire-and-forget)
- Cron schedule: dev-poll every 5min, review-poll every 5min, gardener 4x/day

View file

@ -0,0 +1,53 @@
# Troubleshooting
## WOODPECKER_TOKEN empty after init
The OAuth2 flow failed. Common causes:
1. **URL-encoded redirect_uri mismatch**: Forgejo logs show "Unregistered Redirect URI".
The init script must rewrite both plain and URL-encoded Docker hostnames.
2. **Forgejo must_change_password**: Admin user was created with forced password change.
The init script calls `--must-change-password=false` but Forgejo 11.x sometimes ignores it.
3. **WOODPECKER_OPEN not set**: WP refuses first-user OAuth registration without it.
Manual fix: reset admin password and re-run the token generation manually, or
use the Woodpecker UI to create a token.
## WP CI agent won't connect (DeadlineExceeded)
gRPC over Docker bridge fails in LXD (and possibly other nested container environments).
The compose template uses `network_mode: host` + `privileged: true` for the agent.
If you see this error, check:
- Server exposes port 9000: `grep "9000:9000" docker-compose.yml`
- Agent uses `localhost:9000`: `grep "WOODPECKER_SERVER" docker-compose.yml`
- Agent has `network_mode: host`
## CI clone fails (could not resolve host)
CI containers need to resolve Docker service names (e.g., `forgejo`).
Check `WOODPECKER_BACKEND_DOCKER_NETWORK` is set on the agent.
## Webhooks not delivered
Forgejo blocks outgoing webhooks by default. Check:
```bash
docker logs disinto-forgejo-1 2>&1 | grep "webhook.*ALLOWED_HOST_LIST"
```
Fix: add `FORGEJO__webhook__ALLOWED_HOST_LIST: "private"` to Forgejo environment.
Also verify the webhook exists:
```bash
curl -sf -u "disinto-admin:<password>" "http://localhost:3000/api/v1/repos/<org>/<repo>/hooks" | jq '.[].config.url'
```
If missing, deactivate and reactivate the repo in Woodpecker to auto-create it.
## Dev-agent fails with "cd: no such file or directory"
`PROJECT_REPO_ROOT` inside the agents container points to a host path that doesn't
exist in the container. Check the compose env:
```bash
docker inspect disinto-agents-1 --format '{{range .Config.Env}}{{println .}}{{end}}' | grep PROJECT_REPO_ROOT
```
Should be `/home/agent/repos/<name>`, not `/home/<user>/<name>`.

View file

@ -0,0 +1,44 @@
#!/usr/bin/env bash
# factory-status.sh — Quick status check for a running disinto factory
set -euo pipefail
FACTORY_ROOT="${1:-$(cd "$(dirname "$0")/../.." && pwd)}"
source "${FACTORY_ROOT}/.env" 2>/dev/null || { echo "No .env found at ${FACTORY_ROOT}"; exit 1; }
FORGE_URL="${FORGE_URL:-http://localhost:3000}"
REPO=$(grep '^repo ' "${FACTORY_ROOT}/projects/"*.toml 2>/dev/null | head -1 | sed 's/.*= *"//;s/"//')
[ -z "$REPO" ] && { echo "No project TOML found"; exit 1; }
echo "=== Stack ==="
docker ps --format "table {{.Names}}\t{{.Status}}" 2>/dev/null | grep disinto
echo ""
echo "=== Open Issues ==="
curl -sf "${FORGE_URL}/api/v1/repos/${REPO}/issues?state=open&limit=20" \
-H "Authorization: token ${FORGE_TOKEN}" \
| jq -r '.[] | "#\(.number) [\(.labels | map(.name) | join(","))] \(.title)"' 2>/dev/null || echo "(API error)"
echo ""
echo "=== Open PRs ==="
curl -sf "${FORGE_URL}/api/v1/repos/${REPO}/pulls?state=open&limit=10" \
-H "Authorization: token ${FORGE_TOKEN}" \
| jq -r '.[] | "PR #\(.number) [\(.head.ref)] \(.title)"' 2>/dev/null || echo "none"
echo ""
echo "=== Agent Activity ==="
docker exec disinto-agents-1 bash -c "tail -5 /home/agent/data/logs/dev/dev-agent.log 2>/dev/null" || echo "(no logs)"
echo ""
echo "=== Claude Running? ==="
docker exec disinto-agents-1 bash -c "
found=false
for f in /proc/[0-9]*/cmdline; do
cmd=\$(tr '\0' ' ' < \"\$f\" 2>/dev/null)
if echo \"\$cmd\" | grep -q 'claude.*-p'; then found=true; echo 'Yes — Claude is actively working'; break; fi
done
\$found || echo 'No — idle'
" 2>/dev/null
echo ""
echo "=== Mirrors ==="
cd "${FACTORY_ROOT}" 2>/dev/null && git remote -v | grep -E 'github|codeberg' | grep push || echo "none configured"