Fresh Claude Code agent prompt. You are starting from an empty repo (or a repo with only
SPEC.mdand an emptyPROGRESS.md). Estimated effort: medium (mostly boilerplate; the goal is solid foundations).
Create the monorepo layout, set up the toolchain (composer, npm, PHPUnit, PHPStan, php-cs-fixer), and produce buildable Docker images for both the api and ui containers. Both containers start under compose, return placeholder healthchecks, and migrate runs an empty Phinx migration set and exits 0. No business logic in this milestone.
SPEC.md end-to-end once. Even if it feels long, do it — every later milestone assumes you understand the architecture.Verify the working tree:
ls -la # should see SPEC.md and PROGRESS.md
git status # clean
Confirm tooling: docker --version, docker compose version, php --version (8.3+), composer --version, node --version (20+), npm --version.
Execute in this order. Commit nothing until acceptance passes.
Create the directory structure exactly as in SPEC.md §11. Empty placeholders are fine where files come later (e.g. api/src/Domain/.gitkeep).
.env.example — every env var from SPEC.md §9, grouped into "Shared", "API container", "UI container" sections, with comments..gitignore — sensible defaults: vendor/, node_modules/, public/assets/, .env, .phpunit.cache/, .phpunit.result.cache, data/, IDE files.docker-compose.yml — exactly as in SPEC.md §10.compose.scheduler.yml — exactly as in SPEC.md §10. You don't need to make it work end-to-end yet; you only need it to be valid YAML.README.md — minimal: project name, one-paragraph description, "see SPEC.md and milestones/ for details".api/ subprojectapi/composer.json — Slim 4, doctrine/dbal, robmorgan/phinx, monolog, php-di, vlucas/phpdotenv (dev), guzzlehttp/psr7. Dev: phpunit ^11, phpstan ^1.10, friendsofphp/php-cs-fixer ^3. Set "type": "project" and PSR-4 autoload mapping App\\ → src/.api/public/index.php — Slim app bootstrap. Two routes for now: GET /healthz returning {"status": "ok"} (JSON), and a 404 fallback. Wire structured JSON logging via Monolog to stdout.api/config/settings.php — builds a config array from environment variables. Don't read .env in production; do read it in development via phpdotenv.api/config/phinx.php — Phinx config that reads the same env vars. Migrations dir db/migrations, seeds dir db/seeds. Both SQLite and MySQL adapters configured.api/db/migrations/.gitkeep and api/db/seeds/.gitkeep.api/bin/console — minimal Symfony Console app with one command: db:migrate that delegates to vendor/bin/phinx migrate. Make it executable.api/docker/entrypoint.sh — dispatcher script that switches on $1 (api default → starts FrankenPHP serving public/; migrate → runs migrations and exits). Make it executable.api/docker/Caddyfile — FrankenPHP/Caddy config serving on :8081. Configure /internal/* location with the remote_ip matcher from SPEC.md §6 (you don't need any internal routes yet, just the protective Caddy match).api/Dockerfile — multi-stage as described in SPEC.md §10, dunglas/frankenphp:1-php8.3-alpine base. Install pdo_sqlite, pdo_mysql, mbstring, intl, opcache, bcmath extensions.api/phpunit.xml — testsuite includes tests/Unit and tests/Integration.api/phpstan.neon — level 8 on src/.api/.php-cs-fixer.dist.php — PSR-12 + strict types.api/tests/Unit/SmokeTest.php — one trivial assertion ($this->assertTrue(true)) so the suite runs.ui/ subprojectSame shape, but:
ui/composer.json — Slim 4, twig/twig, slim/twig-view, guzzlehttp/guzzle, jumbojett/openid-connect-php, monolog, php-di, vlucas/phpdotenv (dev). No DBAL, no Phinx. Same dev deps as api.ui/package.json — tailwindcss ^3, postcss, autoprefixer, alpinejs, htmx.org. Build script: tailwindcss -i resources/css/app.css -o public/assets/app.css --minify.ui/tailwind.config.js — content paths covering resources/views/**/*.twig and resources/js/**/*.js. darkMode: 'class'.ui/postcss.config.js — autoprefixer + tailwindcss.ui/resources/css/app.css — Tailwind directives only.ui/resources/js/app.js — Alpine + htmx imports.ui/resources/views/layout.twig — minimal HTML skeleton with Tailwind classes, <html class="dark:bg-slate-900">, dark-mode toggle button (no JS yet, just the markup).ui/resources/views/pages/hello.twig — extends layout, says "IRDB UI — milestone 1".ui/public/index.php — Slim with Twig. One route GET / renders pages/hello.twig. One route GET /healthz returns {"status":"ok","api_reachable":null,"last_api_check_at":null}.ui/docker/Caddyfile — serves on :8080.ui/docker/entrypoint.sh — single mode (ui).ui/Dockerfile — multi-stage as in SPEC.md §10.ui/phpunit.xml, ui/phpstan.neon, ui/.php-cs-fixer.dist.php, ui/tests/Unit/SmokeTest.php.examples/ and doc/ placeholdersexamples/scheduler/host.crontab — a comment-only stub for now (real content lands in M13).doc/.gitkeep — empty (docs land in M13).Do not create any GitHub Actions workflow. CI runs locally on this server (which has Docker installed). Create scripts/ci.sh that an operator can invoke manually:
chmod +x scripts/ci.sh) and start with #!/usr/bin/env bash and set -euo pipefail.api/: composer install --no-interaction --prefer-dist, then composer stan, composer cs, composer test.ui/: composer install --no-interaction --prefer-dist, then composer stan, composer cs, composer test.ui/: npm ci then npm run build; assert ui/public/assets/app.css exists.docker compose build from the repo root to verify both images build.DB_DRIVERS (default "sqlite mysql") and loop the api test stage once per driver, exporting DB_DRIVER so phpunit can pick it up. For mysql, skip gracefully with a warning if no local MySQL is reachable (this milestone has no DB-touching tests yet, so the loop is mostly scaffolding for later milestones).Also add a one-line invocation note in README.md under a "Local CI" heading: ./scripts/ci.sh.
exec the FrankenPHP process for proper signal handling. exec frankenphp run --config /etc/Caddyfile.:8081 for api, :8080 for ui — match SPEC.md §10. Don't expose the api on 8080.{"status":"ok"} for now; full payload (db, jobs) lands in later milestones. Comment in the code where additional fields will go.UI_SERVICE_TOKEN exists on startup — that lands in M03 when api_tokens table exists.entrypoint.sh should mkdir -p /data before launching, in case the SQLite path is set but the dir wasn't created.test (phpunit), stan (phpstan), cs (cs-fixer), cs-fix (cs-fixer fix). Lets the agent in later milestones run composer test instead of remembering paths./healthz. The ui should render only the hello page and /healthz.doc/ beyond .gitkeep.SPEC.md. It is read-only for milestone agents.Run all of these. Every one must pass. If any fails, fix and re-run; do not commit until all green.
# 1. Static analysis and style
cd api && composer install && composer stan && composer cs && composer test && cd ..
cd ui && composer install && composer stan && composer cs && composer test && cd ..
# 2. Frontend build
cd ui && npm ci && npm run build && test -f public/assets/app.css && cd ..
# 3. Compose build
docker compose build
# 4. Compose up — migrate exits 0, api and ui become healthy
cp .env.example .env
# Set required secrets in .env so containers start (use placeholder values for now):
# APP_SECRET, UI_SECRET, UI_SERVICE_TOKEN, INTERNAL_JOB_TOKEN — any 32 hex chars each.
docker compose up -d
sleep 15
# Migrate must have exited 0:
test "$(docker compose ps -a --format '{{.Service}} {{.State}} {{.ExitCode}}' | grep migrate | awk '{print $3}')" = "0"
# Healthchecks (poll up to 60s):
for i in {1..30}; do
curl -sf http://localhost:8081/healthz && curl -sf http://localhost:8080/healthz && break
sleep 2
done
curl -sf http://localhost:8081/healthz | grep -q '"status":"ok"'
curl -sf http://localhost:8080/healthz | grep -q '"status":"ok"'
# Hello page renders:
curl -sf http://localhost:8080/ | grep -q "milestone 1"
docker compose down -v
Commit:
feat(M01): monorepo skeleton, toolchain, docker compose builds clean
- api/ and ui/ subprojects with composer + slim 4
- frankenphp dockerfiles, multi-stage builds
- phpunit, phpstan level 8, php-cs-fixer wired into composer scripts
- tailwind build pipeline in ui/
- empty phinx migrations directory
- scripts/ci.sh runs the full test/lint matrix locally (no GitHub Actions)
Append to PROGRESS.md:
## M01 — Monorepo skeleton (done)
**Built:** repo layout per SPEC §11, both Dockerfiles, compose stack, toolchain.
**Notes for next milestone:**
- DB schema empty; M02 owns all tables and seeds.
- `entrypoint.sh` for api supports `migrate` mode and calls `vendor/bin/phinx`.
- Healthcheck payloads are stubs; later milestones extend them.
- Service-token bootstrap deferred to M03 (needs `api_tokens` table first).
**Deviations from SPEC:** none.
**Added dependencies beyond SPEC §2:** none.
Stop. Do not start M02.