AGENTS.md — Aquila build contract
Read this before touching anything. It is the interface contract between workstreams.
Rationale for every choice lives in DECISIONS.md. Work plan lives in PLAN.md.
Ground rules for all agents
- Stay in your directory. You own one workstream. Do not edit files outside it unless the
orchestrator explicitly asks. Shared files (
README.md,DECISIONS.md,PLAN.md, rootpackage.json,scripts/,.github/) belong to the orchestrator. - The interfaces in §2 are frozen. If you need a change, say so in your delivery notes — do not change both sides silently.
- Nothing runs in the deklo k3s cluster. Plain Docker Compose only. Resource budget: the
whole stack fits in 8GB (
deploy/compose.ymlenforces per-container limits). - Resource discipline: Python 3.11 slim images; no Chromium/Playwright/Electron anywhere; idle bot target ≤250MB RAM. One dep that adds 300MB is a bug.
- Safety posture (D008): bots detect → summarize → draft. They never send email, pay, file, or act on the owner's behalf. Do not build features that violate this.
- No secrets in git. Env files only (
.env, gitignored), with.env.examplecommitted. - Every delivery = branch
ws/<name>→ tests/smoke evidence in the delivery notes → orchestrator review → merge. Append decisions (yours and reversals) toDECISIONS.mdvia the orchestrator — propose entries in your summary. - Language: code and docs in English. All user-facing copy is English only — classy,
plain, never Hinglish (D035). Personas write in English. Tests must not pin copy strings
verbatim where avoidable (see
docs/agent-improvements.mdL04). - Skills are part of the contract: experts exist via installed skills (
~/.claude/skills/: superpowers + anthropic sets). Useverification-before-completionbefore any "done" claim,systematic-debuggingfor failures,requesting/receiving-code-reviewfor reviews. - The improvement loop is mandatory: every significant mistake gets an entry in
docs/agent-improvements.mdvia/retro; briefs and rules evolve from evidence, not vibes.
§1 Repo map & ownership
| Path | Workstream | Stack |
|---|---|---|
core/ |
WS1 | Python 3.11, FastAPI, uvicorn, APScheduler, SQLite (FTS5), httpx, imapclient |
gateway/ |
WS2 | LiteLLM proxy (container), thin FastAPI admin for keys/budgets |
manager/ |
WS3 | Python 3.11, FastAPI, Jinja2, docker SDK, psycopg/SQLite for control-plane state |
channels/telegram/ |
WS4 | Python 3.11, FastAPI, python-telegram-bot (or raw Bot API via httpx) |
deploy/ |
orchestrator | docker-compose, Caddy, relay runbook, k8s mapping notes |
site/, brand/, docs/ |
WS5 | static HTML/CSS/JS, marked for MD→HTML in scripts/build.mjs |
e2e/ |
orchestrator | pytest + httpx smoke tests, density harness |
§2 Frozen interfaces
2.1 core/ ↔ gateway/ (LLM)
- OpenAI-compatible:
POST {LLM_BASE_URL}/chat/completions, model aliasaquila-default, SSE streaming enabled ("stream": true). - Auth:
Authorization: Bearer $BOT_LLM_KEY(virtual key minted by WS2 per bot). - Env in bot container:
LLM_BASE_URL(e.g.http://gateway:4000/v1),BOT_LLM_KEY,BOT_ID. - Gateway may return
429withRetry-Afterwhen a bot's budget is exhausted; core must surface a friendly Hinglish message to the owner instead of crashing.
2.2 core/ (bot runtime HTTP API, port 8080)
| Endpoint | Method | Behavior |
|---|---|---|
/health |
GET | {status:"ok", bot_id, uptime_s, memory_mb} |
/persona |
GET/PUT | read/replace persona.md content (PUT is orchestrator/admin only) |
/chat |
POST | body {message, channel, user_ref} → SSE stream of {delta} chunks, then {done:true, usage} |
/routine/{name}/run |
POST | force-run a routine (used by tests and manager debugging) |
/routines |
GET | list configured routines with next-fire times |
- Manager spawns: container name
aquila-bot-<bot_id>, envBOT_ID, BOT_NAME, PERSONA_FILE, LLM_BASE_URL, BOT_LLM_KEY, TELEGRAM_CHAT_ID+ volumebotdata_<bot_id>:/data. /data/persona.mdis the single source of persona truth;/data/aquila.dbis SQLite (chat history + memory + FTS5 index).
2.3 manager/ (control plane, port 8090)
- Auth model: phone-number identity; session cookie after Telegram OTP verification.
GET /w/<phone>— welcome page (pre-provisioned, unauthenticated).POST /api/otp/start— body{phone}→ creates challenge, returns Telegram deep link + QR payload.POST /api/otp/verify— body{phone, code}→ sets session cookie.GET /api/session— current user + bots.POST /api/bots— body{persona_key, name}→ provisions container (docker SDK), returns bot.POST /api/bots/{id}/start|stop|deleteGET /api/bots/{id}/cost?month=YYYY-MM— from gateway spend API.GET /api/spend/summary— all bots, current month, ₹ equivalent (configurable FX env).- Host-header routing:
<phone>.BASE_DOMAIN→ that user's workspace; unknown phone → welcome page. - All phone numbers normalized to digits-only international format (e.g.
919876543210).
2.4 channels/telegram/ (port 8091)
- One Aquila bot token (
TELEGRAM_BOT_TOKEN). Internally a FastAPI webhook service. - OTP flow:
/start verify_<nonce>→ bot asks contact-share → verifies phone matches the nonce's phone → POSTs to manager/internal/otp/confirm(shared-secret header) → sends the OTP code to the user in Telegram. - Chat flow: user message → POST manager
/internal/route{telegram_chat_id, text}→ manager maps chat→bot → calls core/chat→ streams reply; telegram edits message as chunks arrive (or sends final replies in batches ≤2s). - Delivery flow: manager calls telegram
POST /internal/send{chat_id, text}for routine output. - Shared secret:
AQUILA_INTERNAL_TOKENenv on manager + telegram + core admin calls.
2.5 Data & state
- Control plane state (users, bots, OTP challenges, sessions): SQLite at
manager/data/aquila.db(single-writer, WAL). Enough for POC; Postgres is the k8s-phase upgrade. - Per-bot state: Docker volume
botdata_<bot_id>(/data). - Spend log: gateway writes
gateway/data/spend.jsonl+ exposes/spendAPI; manager reads API.
§3 Workstream briefs
WS1 — core (bot runtime)
Deliver: core/Dockerfile (python:3.11-slim, ≤180MB image), core/app/*, core/personas/ with
vega.md, nova.md, pulsar.md, sirius.md (baseline), tool layer:
web_search (SearXNG at SEARXNG_URL), web_fetch (httpx + readability-ish extraction),
imap_read (imapclient; env IMAP_HOST/USER/PASSWORD, read-only mailbox access), calculator.
Routines: APScheduler, cron strings from persona front-matter (routines: block), all times
IST by default (Asia/Kolkata). Memory: recent transcript + FTS5 recall injected into system
prompt (token-budgeted ≤2k tokens). Tests: core/tests/ pytest (persona load, memory round-trip,
SSE chat against a mock OpenAI-compatible server, routine parsing).
WS2 — gateway
Deliver: LiteLLM config.yaml with model alias aquila-default; upstreams from env:
OPENCODE_GO_API_BASE/OPENCODE_GO_API_KEY (auto-detect placeholder until orchestrator fills),
fallback OPENROUTER_API_KEY free models list; gateway/admin.py (FastAPI): create/revoke virtual
keys, set per-bot ₹ budget (BOT_BUDGET_INR default 200), read spend (/spend, /spend/summary),
/health. Persist keys+budgets in gateway/data/gateway.db (SQLite). On budget exhaustion return
429 as per §2.1. Tests: key lifecycle, budget math, spend parsing. Runs inside compose at
gateway:4000.
WS3 — manager + microsites
Deliver: manager/app/* FastAPI, SQLite schema (users, bots, otp_challenges, sessions), Docker SDK
provisioning (spawn core/ image as aquila-bot-<id> with env+volume+limits 256MB/0.25CPU, only
attachable to the aquila compose network), Jinja templates: welcome, workspace (bot list, chat
debug, cost), admin minimal. No JS build step; vanilla JS + fetch for SSE chat. Tests: OTP lifecycle
(mocked telegram), bot CRUD with mocked docker client, host-routing unit tests.
WS4 — telegram
Deliver: channels/telegram/app.py FastAPI webhook + polling fallback (TELEGRAM_MODE=webhook|poll),
OTP contact-share flow per §2.4, chat routing to manager, /internal/send, QR payload generation
(deep link https://t.me/<bot>?start=verify_<nonce>), friendly Hinglish copy. Tests: nonce parsing,
contact-share verification logic, routing with mocked manager.
WS5 — site + docs
Deliver: site/index.html (night-sky homepage: hero "The eagle that works while you sleep",
how-it-works, 3 personas showcase, pricing ₹250/₹299/₹499 tiers placeholder, FAQ, footer), docs/*.md
rendered at /docs by scripts/build.mjs (pages: plan, decisions, killer-apps, economics,
demo-script, comparisons index), brand/tokens.css (night-sky palette: deep navy grounds, star-white,
eagle-gold accent) + brand/BRAND.md, /docs index nav. Self-contained pages, no CDN except Google
Fonts. npm run verify + npm run build clean.
WS6 — orchestrator (main session)
Owns: deploy/ (compose stack, Caddy configs, relay runbook, k8s mapping notes), e2e/ (smoke +
density harness), economics report, demo script, reviews/merges, DECISIONS.md upkeep, final demo.
§4 Conventions
- Python: ruff (line 100), pytest, type hints on public functions, no
printin services (logging). - Frontend: mobile-first, system fonts fallback, works on a ₹8k Android phone; no frameworks.
- Commits:
ws<N>: <what>(e.g.ws1: chat SSE endpoint); merges via orchestrator withmerge:. - Branching:
ws/<name>; orchestrator merges tomainafter review + smoke evidence. - Keep images small; pin versions in requirements/compose; document any new env var in
.env.exampleat repo root and the workstream README.
§5 Definition of done (per workstream)
- Tests pass (
pytest/npm run verify) and evidence pasted into delivery notes. docker compose configclean; container boots;/healthgreen.- No TODOs without a DECISIONS.md entry proposal.
- Delivery notes: what shipped, how tested, contract deviations (if any), proposed decision entries.