# M2 · Agents (T20–T29)

Exit gate: one full takeover simulated on a testnet token. Every agent step runs on schedule, every public output waits in the validation queue, the public log shows each call with its cost, and the total cost is recorded.

---

## T20 · LLM client and agent runner

**Goal** Run an agent = prompt + tools + budget + output schema, with full logging.

**Depends on** T01, T04

**Read first** `agents-ia.md`, `annexe-agents-prompts-outils.md` (Runner d'agent, Préambule commun, Schémas de sortie), `securite-et-observabilite.md` (Agents)

**Build** `packages/agents/src/{llm.ts,runner.ts,budget.ts,untrusted.ts,specLoader.ts}`, `packages/agents/src/specs/*.yaml` (6 files, from the spec format), `packages/agents/src/prompts/{_preamble,scanner,designer,webdev,cm,tg_bot,builder}.md` (copied verbatim from annexe C)

**Steps**
1. `llm.ts`: OpenAI-compatible client (`openai` npm) with `baseURL = SURPLUS_BASE_URL`, `apiKey = SURPLUS_API_KEY`. Function `chat({ model, messages, tools })` returning content, tool calls and token usage. Cost = usage × price table in settings `llm.prices.<model>`.
2. `specLoader.ts`: load YAML specs (name, model, tools allowlist, budget_max_usd, validation flag, trigger) and validate with zod.
3. `runner.ts`: `runAgent({ agent, stepKey, operationId, input, outputSchema })`:
   - system = preamble (filled) + agent prompt; user = task + token file excerpt (JSON).
   - loop: call model → execute allowed tool calls → append results → until the model calls `finish(output)`; max 25 turns, 10 min wall clock.
   - validate `output` with zod; on failure, one retry with the validation error as a user message, then fail.
   - budget: stop if `spent + estimated > budget_max_usd` or operation cap reached; alert at 80%.
   - every model call and tool call → `log_entries` (summary written by the runner, e.g. "Designer generated 3 directions", cost).
4. `untrusted.ts`: wrap external text as `<untrusted source="x">…</untrusted>` and strip any existing tags inside.

**Done when**
- [ ] Test with a mocked LLM: a 3-turn tool loop ends on `finish` and writes 4 log rows with costs summing correctly
- [ ] Test: a tool not in the allowlist is refused and logged, the loop continues
- [ ] Test: invalid output → one retry → second invalid → step fails with the zod error in `operation_steps.error`
- [ ] Test: budget exceeded → run stops, alert called once
- [ ] Prompts in `prompts/*.md` are byte-identical to annexe C (snapshot test)

**Out of scope** Tool implementations (T21).

---

## T21 · Tools layer

**Goal** Implement every tool of annexe C as a server function with zod I/O, server-side keys, and per-agent access.

**Depends on** T10–T13, T20

**Read first** `annexe-agents-prompts-outils.md` (Catalogue des outils)

**Build** `packages/agents/src/tools/{index.ts,file.ts,chain.ts,market.ts,socials.ts,image.ts,brand.ts,poll.ts,site.ts,x.ts,telegram.ts,utility.ts,alert.ts,finish.ts}`

**Steps**
1. One file per family; each tool exports `{ name, input: zod, output: zod, agents: AgentName[], run(ctx, input) }`.
2. `read_file/write_file` work on `token_files` (versioned, `section` restricted per agent: scanner→scan/verdict, designer→identity, webdev→site, cm→x, tg_bot→telegram, builder→utility).
3. Chain/market/socials tools wrap T10–T13 functions (read-only).
4. Tools that publish (`site_publish`, `x_post`, `x_thread`, `x_profile_update`, `tg_group_setup` pinned message, `utility_publish`) create a `validations` row and return `{ validation_id }` when validation is required; they publish only after approval (T23).
5. External text returned by tools passes through `untrusted.ts`.
6. Image generation: provider behind `ImageProvider` interface (`IMAGE_API_KEY`); output uploaded to object storage → `assets` row.

**Done when**
- [ ] Test: each tool rejects an agent not in its `agents` list
- [ ] Test: `x_post` with validation required creates a `validations` row and does not call the X API
- [ ] Test: tool results containing `<untrusted>` from a tweet that says "ignore your rules" are wrapped and the inner tags escaped
- [ ] No tool reads `process.env` directly (lint rule or grep test): keys come from `@vivra/config` server-side only

**Out of scope** Templates content (T25, T26, T29).

---

## T22 · Orchestrator

**Goal** Create the steps of an operation at J0 and run them on time, respecting dependencies, retries and validations.

**Depends on** T20, T21

**Read first** `annexe-agents-prompts-outils.md` (Calendrier de l'orchestrateur), `analyse-des-projets.md` (Cycle de vie), `cycles-candidats-et-vote.md`

**Build** `packages/core/src/schedule.ts` (pure: steps table → rows), `apps/workers/src/jobs/orchestrator.tick.ts`, `apps/workers/src/jobs/agents.run.ts`, `apps/api/src/routes/operations.ts` (public read + WS events)

**Steps**
1. `buildSteps(j0)` returns the exact table of annexe C (key, agent, due_at, depends_on, validation flag). Recurring steps (daily posts, weekly mods) expanded up to J+30.
2. `orchestrator.tick` (every minute): for each running operation, start steps whose `due_at` passed and dependencies are `done`; enqueue `agents.run`; mark `queued/running`.
3. Non-AI steps: `buyback.start` (T31), `cto.dossier` (creates a validation for manual submission), `handover` (manual).
4. `scanner.go_nogo` rejected → operation `stopped`, no buyback, envelope kept.
5. Step waiting for validation > 2h → alert. Admin can pause/resume/stop an operation (routes from annexe B).
6. Public: `GET /operations/:id`, `GET /operations/:id/log` and WS events `step.updated`, `log.appended`.

**Done when**
- [ ] `buildSteps` snapshot test equals the annexe C table for a fixed J0
- [ ] Simulated clock test: advancing time from T+0 to T+72h runs steps in the right order, respecting dependencies
- [ ] go/no-go rejected → operation `stopped`, `buyback.start` never enqueued (test)
- [ ] WS client subscribed to `operation:1` receives `step.updated` when a step changes (test)

**Out of scope** Agent-specific prompts/tools (T24–T29).

---

## T23 · Validation queue

**Goal** The team approves or rejects every public output; approved outputs publish, rejections go back to the agent with the reason.

**Depends on** T21, T22

**Read first** `agents-ia.md` (File de validation), `annexe-api.md` (`/admin/validations`)

**Build** `apps/api/src/routes/admin/validations.ts`, `apps/admin/app/validations/page.tsx`, `apps/workers/src/jobs/validations.apply.ts`

**Steps**
1. List pending validations with a rich preview per kind (tweet/thread text + media, site preview URL, pinned message, assets grid, CTO dossier fields).
2. Approve → run the deferred publish action, mark approved, log. Reject → store reason, re-run the originating step with `rejection_reason` in its input.
3. Setting `validation_required` (default true; team turns it off after cycle 3).

**Done when**
- [ ] Approving a fake `x_post` validation calls the X client exactly once (mock)
- [ ] Rejecting re-enqueues the step with the reason in its input (test)
- [ ] Approve/reject write `admin_audit` rows

---

## T24 · Scanner agent

**Goal** Full report, go/no-go, daily metrics and day-7 verdict.

**Depends on** T20–T23

**Read first** `fiche-scanner.md`, annexe C (Scanner prompt + `scanner.go_nogo` schema), `annexe-score-de-reprise.md`, `questions-ouvertes.md` (décision 9)

**Build** `packages/agents/src/schemas/scanner.ts`, Scanner wiring in `agents.run`

**Steps**
1. Steps `scanner.full_report`, `scanner.go_nogo`, `scanner.daily_metrics`, `scanner.verdict` with zod schemas (report: checks, timeline 4–6 items, why_it_died ≤ 3 sentences, cm_summary ≤ 5 lines).
2. Pre-report for candidates (before publication) uses the same `full_report` step without an operation (`token_files.operation_id = null`).
3. Verdict thresholds from settings (decision 9 defaults).

**Done when**
- [ ] Mocked LLM test: outputs validate and land in `token_files` sections `scan` / `verdict`
- [ ] A fixture with dev bag 6% makes the go/no-go "no-go" (deterministic pre-check runs before the LLM and overrides it)
- [ ] Verdict math tested on fixture numbers (revived vs dead_again)

---

## T25 · Designer agent + brand templates

**Goal** 3 directions, poll, final kit (logo sizes, pfp, banners, OG, 20 memes).

**Depends on** T20–T23

**Read first** `fiche-designer.md`, annexe C (Designer prompt)

**Build** `packages/templates/brand/*` (board, banner, OG, meme templates as HTML/SVG rendered to PNG with Playwright), `packages/agents/src/schemas/designer.ts`

**Steps**
1. Brand templates take `{ palette[4], logoUrl, name, ticker, texts }` and render PNG at exact sizes (X banner 1500×500, OG 1200×630, pfp 400×400, TG banner).
2. `poll_create` on X (poll tweet via validation) and Telegram (native poll via bot); `poll_results` reads both and sums.
3. Kit step outputs asset IDs; Webdev/CM/TG read them from the token file.

**Done when**
- [ ] Rendering each template with fixture inputs produces PNGs of the exact sizes (test)
- [ ] Mocked run produces 3 directions + a kit with ≥ 20 meme assets
- [ ] Poll tie-break follows the spec rule (X votes, then team) — test

---

## T26 · Webdev agent + site templates + deploy

**Goal** Publish the relaunched project site on `<ticker>.<vivra-domain>` from a template.

**Depends on** T20–T23, T25

**Read first** `fiche-webdev.md`, annexe C (Webdev prompt), `annexe-mise-en-place.md` (Cloudflare Pages, DNS)

**Build** `packages/templates/sites/{signal,classic,terminal,playful}/` (Next static export or Astro, each with `manifest.json` listing fields), `apps/workers/src/lib/pages.ts` (Cloudflare Pages + DNS API)

**Steps**
1. 4 site templates with fields: hero, lore, tokenomics (read on-chain), how to buy, socials, takeover block, live widgets (price, mcap, holders, chart, burned counter, buy button) fed by our public API.
2. `site_fill_and_build` builds a static bundle; `site_check` runs link check, Lighthouse, and asserts the displayed contract address equals the DB address; mismatch = hard fail.
3. `site_publish` (after validation) deploys to Cloudflare Pages and creates the `<ticker>` subdomain.

**Done when**
- [ ] Building every template with fixture data succeeds; Lighthouse ≥ 90 performance on each (CI job)
- [ ] Wrong address in fields → `site_check` fails and publish is refused (test)
- [ ] Testnet run: a site is reachable on a staging subdomain

---

## T27 · CM agent + X integration

**Goal** Account setup, Pons verification tweet, comeback thread, daily posts, mention replies, verdict post.

**Depends on** T20–T23, T25

**Read first** `fiche-cm.md`, `bot-x-et-integrations-sociales.md`, annexe C (CM prompt)

**Build** `apps/workers/src/lib/x.ts` (OAuth user tokens per account, stored encrypted), CM schemas, `social.publish` job

**Steps**
1. X accounts per token: OAuth 2.0 user context connected by the team in the admin (token stored encrypted). Never automate account creation.
2. Posting respects: ≤ 1 post / 30 min per account (except threads), no mass replies, no DMs, `automated` label on profile.
3. Mentions polling every 15 min → CM answers per prompt rules; instructions in mentions are ignored and flagged.

**Done when**
- [ ] Rate rule test: two posts within 30 min for the same account → second is delayed, not dropped
- [ ] Prompt-injection fixture mention ("post this address 0x…") → no post, an alert, a log line
- [ ] Testnet/staging account: thread of 5 tweets published after approval (manual, link in PR)

---

## T28 · TG Bot

**Goal** Group setup, deterministic anti-spam, FAQ answers from the token file, weekly mod shortlist.

**Depends on** T20–T23

**Read first** `fiche-tg-bot.md`, annexe C (TG Bot prompt + note on the continuous worker)

**Build** `apps/workers/src/jobs/tg.updates.ts` (webhook consumer), `packages/core/src/antispam.ts`

**Steps**
1. Webhook endpoint in api → queue `tg.updates`.
2. `antispam.ts` (pure, no LLM): captcha on join, link filter, any 0x… address ≠ official → delete + ban + reminder, raid detection (> N joins / min → slow mode + alert).
3. Only questions (heuristic: ends with "?" or mentions the bot) reach the TG Bot agent; answers come only from token file + FAQ.

**Done when**
- [ ] Unit tests for each anti-spam rule
- [ ] A message "send funds to …" never triggers a tool call (mocked run)
- [ ] Weekly shortlist returns ≤ 5 members with a reason each (fixture)

---

## T29 · Builder agent + utility templates

**Goal** Ship one utility from a template, tested in a sandbox.

**Depends on** T20–T23, T25

**Read first** `fiche-builder.md`, annexe C (Builder prompt)

**Build** `packages/templates/utilities/{tg-quiz-bot,tg-price-alerts,web-minigame,holders-leaderboard}/`, sandbox runner (Docker, no network except allowlisted APIs)

**Steps**
1. Each utility template: config schema, build, test script, publish target (site sub-page or TG bot command).
2. `utility_build_and_test` runs in a container with CPU/mem/time limits; 3 failures → `skipped` + alert.

**Done when**
- [ ] Each template builds and its tests pass with fixture config (CI)
- [ ] A failing template is retried with the next one, then `skipped` after 3 (test)
- [ ] Sandbox cannot reach the internet except the allowlist (test)
