--- phase: 09-botfights-platform-upgrade plan: 03 type: execute wave: 1 depends_on: [] files_modified: - /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md - /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-EASY.md - /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-POLLING.md - /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-WEBHOOK.md - /home/archipelago/Projects/botfight/BOTFIGHTS.md - /home/archipelago/Projects/botfight/BOT_SETUP.md - /home/archipelago/Projects/botfight/server/src/routes/docs.ts - /home/archipelago/Projects/botfight/server/src/routes/docs.test.ts - /home/archipelago/Projects/botfight/frontend/src/pages/JoinBoutPage.vue - /home/archipelago/Projects/botfight/frontend/src/pages/BotProfilePage.vue - /home/archipelago/Projects/botfight/frontend/src/pages/DocsPage.vue - /home/archipelago/Projects/botfight/e2e/signup-bot.spec.ts autonomous: true requirements: [BOT-02] must_haves: truths: - "One copy-paste prompt contains everything an AI agent needs to build a working bot — registration, credential handling, BOTH webhook and polling protocols, every endpoint it calls, and every response format — with no instruction to go read another document (D-02/BOT-02)" - "A cloud-hosted AI agent with no browser can fetch the whole prompt with a single unauthenticated GET and the arena URL inside it is already correct for the arena it fetched from" - "The prompt documents the registration call the old docs never mentioned: an anonymous POST /api/bots that returns the bot id and secret" - "The in-app 'show my setup guide' flow still hands the user their real credentials substituted into the text, from the single consolidated doc" - "No user-facing surface links to BOTFIGHTS-EASY / BOTFIGHTS-POLLING / BOTFIGHTS-WEBHOOK / BOT_SETUP any more" artifacts: - path: /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md provides: "The single canonical AI bot-setup prompt (ships into the container at server/public/docs/BOTFIGHTS.md via the frontend build)" contains: "POST /api/bots" - path: /home/archipelago/Projects/botfight/server/src/routes/docs.ts provides: "GET /api/docs/prompt serving the prompt as text/markdown with the arena URL resolved" contains: "docsRouter.get('/prompt'" key_links: - from: /home/archipelago/Projects/botfight/frontend/src/pages/JoinBoutPage.vue to: /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md via: "setupDocPath() resolves to the single consolidated doc and the YOUR_BOT_ID / YOUR_BOT_SECRET substitution still runs on it" pattern: "BOTFIGHTS.md" --- Deliver D-02/BOT-02: replace the five-document maze (`BOTFIGHTS.md`, `BOTFIGHTS-EASY.md`, `BOTFIGHTS-POLLING.md`, `BOTFIGHTS-WEBHOOK.md`, `BOT_SETUP.md`, plus DocsPage's own duplicated copy) with ONE self-contained prompt that an AI agent can be handed verbatim, and serve it at a stable URL an agent can `curl`. Decision IDs map to `09-CONTEXT.md` **Locked Decisions**: D-01 = BOT-01 signer login, D-02 = BOT-02 unified AI bot-setup prompt, D-03 = BOT-03 shared arena, D-04 = BOT-04 catalog. Purpose: tomorrow's demo has a cloud-hosted "openclaw" bot register and fight using ONLY this prompt. That is the acceptance bar — not "the docs are tidier". Today's docs fail it twice: they never document the registration call (`POST /api/bots`), and the polling example defaults to a stale host (`BOTFIGHTS_HOST || 'botfights.io'`) that is not the arena. Output: one consolidated prompt file, `GET /api/docs/prompt`, updated in-app call sites with a copy button, and the superseded files removed. **Repo: `/home/archipelago/Projects/botfight`.** Commit target: `git push origin main`. @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/09-botfights-platform-upgrade/09-CONTEXT.md @.planning/phases/09-botfights-platform-upgrade/09-RESEARCH.md @.planning/phases/09-botfights-platform-upgrade/09-PATTERNS.md @.planning/phases/09-botfights-platform-upgrade/09-VALIDATION.md ## Artifacts this plan produces | Symbol | Kind | File | |---|---|---| | the unified prompt | canonical markdown (single source of truth) | `frontend/public/docs/BOTFIGHTS.md` | | `docsRouter.get('/prompt')` | Hono route returning `text/markdown` | `server/src/routes/docs.ts` | | `docs.test.ts` additions | Vitest cases for the prompt route | `server/src/routes/docs.test.ts` | | copy-the-prompt affordance | UI | `frontend/src/pages/DocsPage.vue` | Why `frontend/public/docs/BOTFIGHTS.md` is the canonical location: Vite copies `frontend/public/**` into `frontend/dist`, and the Dockerfile copies `frontend/dist` to `server/public` — so this file is the only copy that exists inside the shipped container. The repo-root `BOTFIGHTS.md` is NOT in the image and must not be the source the server reads. Task 1: Write the one prompt that a cloud AI agent can build a working bot from /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md, /home/archipelago/Projects/botfight/BOTFIGHTS.md, /home/archipelago/Projects/botfight/BOT_SETUP.md, /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-EASY.md, /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-POLLING.md, /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-WEBHOOK.md - `/home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md` — all 452 lines. It is ~90% of the target already: Credentials, Choose a Mode, Option A webhook bot (full JS), Option B polling bot (full JS), How Fights Work, Challenge Payload, All Challenge Types, Security Notes, Tips, After Setup. Extend it in place; do not rewrite from scratch. - `/home/archipelago/Projects/botfight/BOT_SETUP.md` — mine it for the unique content the public doc lacks (customization API, archetype list) before deleting it. - `/home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-EASY.md`, `BOTFIGHTS-POLLING.md`, `BOTFIGHTS-WEBHOOK.md` — confirm every fact in each already appears in the consolidated doc before removing them. - `/home/archipelago/Projects/botfight/server/src/routes/bots.ts` lines 34-110 — the real registration contract: `POST /api/bots` with `{name}` (2-12 chars, alphanumeric/hyphen/ underscore, lowercased, unique), optional `webhook_url` (omit or empty string = poll mode), rate limit 5 per hour per IP, 409 on duplicate name, and webhook mode verifying the URL by calling it before accepting. - `/home/archipelago/Projects/botfight/server/src/middleware/bot-auth.ts` lines 16-45 — the two accepted credential forms: `Authorization: Bot :` or `?bot_id=...&secret=...`. - `/home/archipelago/Projects/botfight/server/src/routes/fights.ts` lines 363-410 — `GET /api/fights/poll` response fields (`pending`, `fight_id`, `round`, `type`, `challenge`, `constraints`, `opponent`, `arena`, `arena_modifier`, `remaining_ms`, `scoring`) and `POST /api/fights/poll/respond` (`{answer, trashTalk}` → `{accepted: true}`, 404 when nothing is pending), plus `POST /api/queue/join/:botId` in `routes/queue.ts`. - `/home/archipelago/Projects/botfight/server/src/engine/orchestrator.ts` lines 180-195 — the webhook `X-Botfights-Signature: sha256=...` HMAC scheme so the prompt can tell a webhook bot how to verify inbound challenges. - `/home/archipelago/Projects/botfight/server/src/routes/docs.ts` lines 6-163 — the challenge types, scoring rules, failure modes and tips already curated for machine consumption; the prompt must agree with them. Consolidate everything into `frontend/public/docs/BOTFIGHTS.md` as the single canonical prompt. Required content, in this order: 1. A one-paragraph framing line stating this file is a complete, self-contained instruction set for an AI agent to build and run a BOTFIGHTS bot, and that nothing else needs to be read. 2. **Register** (new section, currently missing everywhere): the anonymous `POST {{ARENA_URL}}/api/bots` call with a `curl` example and the JSON response, stating that omitting `webhook_url` selects poll mode, that the returned secret is shown once, the 2-12 character name rule, the 409-on-duplicate behaviour and the 5-per-hour-per-IP limit. 3. **Credentials**: keep the existing `BOT_ID=YOUR_BOT_ID` / `BOT_SECRET=YOUR_BOT_SECRET` block verbatim — those two placeholder tokens are substituted in-app and MUST survive unchanged — and add both accepted auth forms (`Authorization: Bot :` and the query-param form), plus an instruction to keep the secret in an env var and never commit it. 4. **Choose a mode** and the two full working bot implementations (webhook and polling), kept inline as today. In the polling example, replace the stale hard-coded fallback host that `BOTFIGHTS_HOST` currently defaults to with `{{ARENA_URL}}`, and make both examples use one base-URL constant so an agent edits a single line. Keep them dependency-free Node scripts. The polling example currently falls back to a stale public host when `BOTFIGHTS_HOST` is unset — that fallback literal must be gone from the file when you are done. 5. **Webhook verification**: the `X-Botfights-Signature: sha256=` header, the exact derivation (HMAC-SHA256 over the request body with a key derived from the bot's secret hash), and that a webhook must answer 200 with JSON within `constraints.timeout_ms`. 6. **Enter a fight**: `POST {{ARENA_URL}}/api/queue/join/{BOT_ID}` and what happens when no opponent shows up. 7. **Endpoint reference**: a compact table of every endpoint a bot uses — `POST /api/bots`, `GET /api/fights/poll`, `POST /api/fights/poll/respond`, `POST /api/queue/join/:botId`, `GET /api/bots/:name`, `POST /api/bots/:name/test-challenge`, `GET /api/fights/:id` — with method, auth requirement, request shape and response shape for each. 8. **Challenge payload, challenge types, scoring, failure modes, tips** — keep the existing sections, reconciled with `routes/docs.ts` so the two never disagree. 9. **Troubleshooting**: 401 (bad credentials), 404 from poll/respond (no pending challenge), 429 (rate limited — poll no faster than once per second), and the five-consecutive-errors auto-deactivation rule. Use the literal token `{{ARENA_URL}}` everywhere a base URL appears. The server route in Task 2 substitutes it with the real arena origin; the in-app viewer substitutes it client-side. Then remove the superseded files: delete `frontend/public/docs/BOTFIGHTS-EASY.md`, `frontend/public/docs/BOTFIGHTS-POLLING.md`, `frontend/public/docs/BOTFIGHTS-WEBHOOK.md` and `BOT_SETUP.md` (these four filenames appear here only as delete targets — the Task 3 gate greps `frontend/src` for references to them). Replace the repo-root `BOTFIGHTS.md` with a three-line stub pointing readers at `frontend/public/docs/BOTFIGHTS.md` and at `GET /api/docs/prompt`, so the two near-identical root/public copies can never drift again. Do not delete any file until Task 3's call-site sweep has a plan for every reference to it (grep first — references exist in `JoinBoutPage.vue`, `BotProfilePage.vue` and `DocsPage.vue`). cd /home/archipelago/Projects/botfight && test ! -e frontend/public/docs/BOTFIGHTS-POLLING.md && test ! -e frontend/public/docs/BOTFIGHTS-WEBHOOK.md && test ! -e frontend/public/docs/BOTFIGHTS-EASY.md && test ! -e BOT_SETUP.md && grep -q '/api/bots' frontend/public/docs/BOTFIGHTS.md && grep -q 'YOUR_BOT_ID' frontend/public/docs/BOTFIGHTS.md && grep -q '{{ARENA_URL}}' frontend/public/docs/BOTFIGHTS.md - `grep -c '{{ARENA_URL}}' /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md` is at least 5. - `grep -q 'YOUR_BOT_ID' /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md` and `grep -q 'YOUR_BOT_SECRET' ...` both succeed (in-app substitution tokens preserved). - `grep -q 'api/fights/poll' /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md`, `grep -q 'api/fights/poll/respond' ...`, `grep -q 'api/queue/join' ...`, `grep -q 'X-Botfights-Signature' ...` all succeed. - `grep -q 'Authorization: Bot ' /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md` succeeds. - `grep -c 'botfights.io' /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md` is 0. - The four superseded files no longer exist and the root `BOTFIGHTS.md` is under 10 lines. - `wc -l < /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md` is at least 452 (content was added, not lost). One document contains registration, credentials, both protocols, every endpoint and every response format; the four superseded documents are gone. Task 2: Serve the prompt at GET /api/docs/prompt with the real arena URL baked in /home/archipelago/Projects/botfight/server/src/routes/docs.ts, /home/archipelago/Projects/botfight/server/src/routes/docs.test.ts - `/home/archipelago/Projects/botfight/server/src/routes/docs.ts` lines 1-10 and the shape of the existing `GET /webhook` handler — the new handler sits alongside it on the same router. - `/home/archipelago/Projects/botfight/server/src/app.ts` lines 90-93 (the `/api/docs/*` GET cache header, already `public, max-age=3600` — no per-route cache handling needed) and lines 110-142 (`publicDir` resolution and `serveFile`, which is how `/docs/*` static files are served in production and confirms `server/public/docs/BOTFIGHTS.md` is the in-container path). - `/home/archipelago/Projects/botfight/server/src/routes/docs.test.ts` — existing harness style for this router. - `/home/archipelago/Projects/botfight/Dockerfile` lines 20-30 — `COPY --from=build-fe /app/frontend/dist server/public`, the reason the in-container path is `server/public/docs/BOTFIGHTS.md`. Add `docsRouter.get('/prompt', ...)` to `server/src/routes/docs.ts`: - Resolve the prompt file at request time by trying, in order: `/public/docs/BOTFIGHTS.md` (the shipped container layout) then `/frontend/public/docs/BOTFIGHTS.md` (a dev checkout where the frontend has not been built). Derive both from `dirname(fileURLToPath(import.meta.url))` the same way `app.ts` derives `publicDir`. Cache the resolved contents in a module-level variable keyed by path plus mtime, or simply read on each request — this endpoint is cached for an hour upstream, so a plain read is acceptable; do not add a file-watcher. - If neither path exists, return `c.json({ error: 'Prompt not available.' }, 404)` following the codebase's direct-return error convention. - Substitute the `{{ARENA_URL}}` token with, in precedence order: `process.env.PUBLIC_ARENA_URL` when set, otherwise the origin of the incoming request (`new URL(c.req.url).origin`). On the canonical arena behind nginx-proxy-manager the inbound request carries the public host, so an agent that curls the public URL gets a prompt whose examples already point back at that same public arena — which is precisely what makes the prompt self-contained for a cloud bot. - Respond with `c.header('Content-Type', 'text/markdown; charset=utf-8')` and the substituted body. Plain text, not JSON — an agent should be able to pipe the response straight into its context. Add cases to `server/src/routes/docs.test.ts`: - 200 with a `text/markdown` content type and a body containing the registration endpoint path. - no `{{ARENA_URL}}` token survives in the response body. - with `PUBLIC_ARENA_URL` set, the body contains that value; restore the env var afterwards. - without it, the body contains the origin the request was made to. - the `YOUR_BOT_ID` placeholder is still present (the in-app substitution contract). cd /home/archipelago/Projects/botfight && pnpm vitest run server/src/routes/docs.test.ts - `cd /home/archipelago/Projects/botfight && pnpm vitest run server/src/routes/docs.test.ts` exits 0 with at least 5 new cases passing. - `cd /home/archipelago/Projects/botfight && pnpm exec tsc --noEmit -p server/tsconfig.json` exits 0. - `grep -Eq "docsRouter\.get\(['\"]/prompt['\"]" /home/archipelago/Projects/botfight/server/src/routes/docs.ts` succeeds. - `grep -q 'text/markdown' /home/archipelago/Projects/botfight/server/src/routes/docs.ts` succeeds. - `grep -q 'PUBLIC_ARENA_URL' /home/archipelago/Projects/botfight/server/src/routes/docs.ts` succeeds. - `grep -q 'public/docs/BOTFIGHTS.md' /home/archipelago/Projects/botfight/server/src/routes/docs.ts` succeeds (the container-layout path, not the repo-root file). `curl /api/docs/prompt` returns the complete prompt as markdown with working URLs, from both a built container and a dev checkout. Task 3: Point every in-app surface at the one prompt (and give it a copy button) /home/archipelago/Projects/botfight/frontend/src/pages/JoinBoutPage.vue, /home/archipelago/Projects/botfight/frontend/src/pages/BotProfilePage.vue, /home/archipelago/Projects/botfight/frontend/src/pages/DocsPage.vue, /home/archipelago/Projects/botfight/e2e/signup-bot.spec.ts - `/home/archipelago/Projects/botfight/frontend/src/pages/JoinBoutPage.vue` around lines 515-560 — `setupDocPath()`, `setupDocName()` and `toggleSetupContent()`, including the `.replace(/YOUR_BOT_ID/g, ...)` / `.replace(/YOUR_BOT_SECRET/g, ...)` substitution that is the load-bearing behaviour to preserve. - `/home/archipelago/Projects/botfight/frontend/src/pages/BotProfilePage.vue` around lines 295-310 and 880-890 — the same mode-conditional doc path plus the filename shown in the UI. - `/home/archipelago/Projects/botfight/frontend/src/pages/DocsPage.vue` lines 1-60 and 500-530 — it fetches `/api/docs/webhook` (a JSON API reference) and hosts the interactive webhook tester. Both stay; what changes is that the page leads with the prompt. - `/home/archipelago/Projects/botfight/e2e/signup-bot.spec.ts` (43 lines) — the Playwright conventions to follow: `test.describe` grouping, `page.getByText(/regex/i)` selectors, explicit `{ timeout: N_000 }`. - `/home/archipelago/Projects/botfight/CLAUDE.md` "Vue 3 Conventions" — `