# Audit Fix Prompt + run_site_audit MCP — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Depuis `/flight-checker`, un bouton « Copier le prompt agent » génère un prompt prêt-à-coller qui branche l'agent de l'utilisateur sur notre MCP pour corriger les points d'audit, avec un nouveau tool `run_site_audit` (cooldown 60 s) pour boucler fix → re-audit.

**Architecture:** Helper de persistance partagé (`persist.ts`) extrait de la Server Action ; tool org-scopé dans `src/lib/mcp/tools.ts` + registration `server.ts` ; builder de prompt **pur et client-safe** (`fix-prompt.ts`, données passées en paramètres, aucun import server-only) ; bouton client dans `checklist.tsx` (pattern copie = `keyword-prompt-card.tsx`, toast sonner).

**Tech Stack:** Next.js 16 App Router, Drizzle/Postgres, mcp-handler + zod, bun test (DB tests gated `DB_TESTS=1`, base `seo_insight_test`).

**Spec:** `docs/superpowers/specs/2026-07-07-audit-fix-prompt-mcp-design.md` · **Branche:** `feature/audit-fix-prompt` (déjà créée, spec committée).

## Global Constraints

- Conventional Commits ; biome (`bunx biome check --write <fichiers>`) avant chaque commit.
- Gate verte = `bun run test:all` (jamais `bun test` global — mock.module fuit entre fichiers).
- Jamais de clé API réelle dans un prompt généré — placeholder `eli_xxxxxxxxxxxx` uniquement.
- Tool MCP : l'orgId vient TOUJOURS de `requireOrgId(extra)`/paramètre `orgId`, jamais de l'input du tool.
- Invariant DB : une seule ligne `site_audits` par site (unique `site_audits_site_unique`), écrite par upsert.
- Textes UI et détails en français (comme l'existant).

---

### Task 1: Helper de persistance partagé `persistSiteAudit`

**Files:**
- Create: `src/lib/spec/audit/persist.ts`
- Modify: `src/lib/spec/audit/actions.ts` (remplacer l'upsert inline)

**Interfaces:**
- Produces: `persistSiteAudit(siteId: string, result: AuditResult): Promise<void>` — upsert sur `site_audits` (unique site_id), importable hors `"use server"`.

- [ ] **Step 1: Créer le helper**

```ts
// src/lib/spec/audit/persist.ts — persistance du résultat d'audit, partagée
// entre la Server Action (actions.ts) et le tool MCP run_site_audit.
// Invariant : une seule ligne par site (unique site_audits_site_unique) —
// le re-run remplace score/results/ranAt, l'id d'origine est conservé.
import "server-only";
import { randomUUID } from "node:crypto";
import { db } from "@/lib/db/client";
import { siteAudits } from "@/lib/db/domain-schema";
import type { AuditResult } from "./run";

export async function persistSiteAudit(
  siteId: string,
  result: AuditResult,
): Promise<void> {
  await db
    .insert(siteAudits)
    .values({
      id: randomUUID(),
      siteId,
      score: result.score,
      results: result.results,
    })
    .onConflictDoUpdate({
      target: siteAudits.siteId,
      set: {
        score: result.score,
        results: result.results,
        ranAt: new Date(),
      },
    });
}
```

- [ ] **Step 2: L'utiliser dans actions.ts**

Dans `src/lib/spec/audit/actions.ts` : supprimer les imports devenus inutiles (`randomUUID` de `node:crypto`, `siteAudits` reste utilisé par les lectures — vérifier), et remplacer le bloc upsert de `runSiteAuditAction` par :

```ts
import { persistSiteAudit } from "./persist";
// … dans runSiteAuditAction, à la place du db.insert(...).onConflictDoUpdate(...) :
  await persistSiteAudit(siteId, result);
```

- [ ] **Step 3: Vérifier que la suite passe (les tests d'actions couvrent déjà l'upsert)**

Run: `bun run test:all`
Expected: « Suite complète verte » — notamment `src/lib/spec/audit` (le test « re-run remplace l'audit — une seule ligne par site » couvre le helper via l'action).

- [ ] **Step 4: Commit**

```bash
bunx biome check --write src/lib/spec/audit/persist.ts src/lib/spec/audit/actions.ts
git add src/lib/spec/audit/persist.ts src/lib/spec/audit/actions.ts
git commit -m "refactor(checklist): extract persistSiteAudit — upsert partagé action/MCP"
```

---

### Task 2: `runSiteAuditForOrg` dans tools.ts (TDD, DB)

**Files:**
- Modify: `src/lib/mcp/tools.ts` (nouvelle fn après `getSiteAudit`, ~l.125)
- Test: `src/lib/mcp/tools.test.ts`

**Interfaces:**
- Consumes: `persistSiteAudit` (Task 1), `fetchSiteSnapshot(url, fetchImpl?)`, `runSiteAudit(snapshot, ctx)`, `getSiteById`, `latestAuditRow` (privé existant dans tools.ts), `SiteAuditDetail` (type existant).
- Produces:
```ts
export type RunSiteAuditOutcome =
  | { kind: "not-found" }
  | { kind: "no-url"; name: string }
  | { kind: "ran"; detail: SiteAuditDetail }
  | { kind: "cooldown"; detail: SiteAuditDetail };
export const RUN_AUDIT_COOLDOWN_MS = 60_000;
export async function runSiteAuditForOrg(
  orgId: string,
  siteId: string,
  fetchImpl?: typeof fetch,
): Promise<RunSiteAuditOutcome>;
```

- [ ] **Step 1: Écrire les tests qui échouent**

Dans `src/lib/mcp/tools.test.ts`, dans le `describe.skipIf(!RUN_DB)` existant : le beforeEach seed le site **`mcp-site-a`** (org `demo-org`, `ORG`/`OTHER` constants en tête de fichier). Utiliser `"mcp-site-a"` partout où les extraits ci-dessous écrivent `"site-1"` — et vérifier dans le beforeEach que ce site a une `url` non nulle (sinon la lui donner dans le seed) :

```ts
describe("runSiteAuditForOrg", () => {
  // Un fetch canné qui renvoie une petite page HTML valide pour toutes les URLs.
  const fakeFetch = (async () =>
    new Response("<html><head><title>t</title></head><body>ok</body></html>", {
      status: 200,
      headers: { "content-type": "text/html" },
    })) as unknown as typeof fetch;

  test("cross-org → not-found", async () => {
    const out = await tools.runSiteAuditForOrg(OTHER, "site-1", fakeFetch);
    expect(out.kind).toBe("not-found");
  });

  test("site sans URL → no-url", async () => {
    await db.update(schema.sites).set({ url: null })
      .where(eq(schema.sites.id, "site-1"));
    const out = await tools.runSiteAuditForOrg(ORG, "site-1", fakeFetch);
    expect(out.kind).toBe("no-url");
  });

  test("run → audit persisté (une ligne), detail retourné", async () => {
    await db.delete(schema.siteAudits);
    const out = await tools.runSiteAuditForOrg(ORG, "site-1", fakeFetch);
    expect(out.kind).toBe("ran");
    if (out.kind !== "ran") throw new Error("unreachable");
    expect(out.detail.siteId).toBe("site-1");
    expect(out.detail.results.length).toBeGreaterThan(100);
    const rows = await db.select().from(schema.siteAudits);
    expect(rows).toHaveLength(1);
  });

  test("2e appel < 60 s → cooldown, résultat existant, AUCUN fetch", async () => {
    await db.delete(schema.siteAudits);
    await tools.runSiteAuditForOrg(ORG, "site-1", fakeFetch);
    let called = 0;
    const spyFetch = (async (...a: Parameters<typeof fetch>) => {
      called++;
      return fakeFetch(...a);
    }) as unknown as typeof fetch;
    const out = await tools.runSiteAuditForOrg(ORG, "site-1", spyFetch);
    expect(out.kind).toBe("cooldown");
    expect(called).toBe(0);
    const rows = await db.select().from(schema.siteAudits);
    expect(rows).toHaveLength(1);
  });
});
```

- [ ] **Step 2: Vérifier l'échec**

Run: `DB_TESTS=1 DATABASE_URL=postgresql://humperdoo@localhost:5432/seo_insight_test bun test src/lib/mcp/tools.test.ts`
Expected: FAIL — `runSiteAuditForOrg is not a function`.

- [ ] **Step 3: Implémenter dans tools.ts**

Après `getSiteAudit` :

```ts
export type RunSiteAuditOutcome =
  | { kind: "not-found" }
  | { kind: "no-url"; name: string }
  | { kind: "ran"; detail: SiteAuditDetail }
  | { kind: "cooldown"; detail: SiteAuditDetail };

/** Fenêtre pendant laquelle un re-run renvoie l'audit existant (anti-boucle agent). */
export const RUN_AUDIT_COOLDOWN_MS = 60_000;

/**
 * `run_site_audit` — audite le site (fetch réel de sa surface publique) et
 * remplace l'audit stocké (une ligne par site). Si le dernier audit date de
 * moins de RUN_AUDIT_COOLDOWN_MS, renvoie l'existant sans re-fetcher : un
 * agent qui boucle ne martèle ni le site cible ni la base.
 */
export async function runSiteAuditForOrg(
  orgId: string,
  siteId: string,
  fetchImpl: typeof fetch = fetch,
): Promise<RunSiteAuditOutcome> {
  const site = await getSiteById(siteId, orgId);
  if (!site) return { kind: "not-found" };
  if (!site.url) return { kind: "no-url", name: site.name };

  const toDetail = (row: {
    score: number;
    ranAt: Date;
    results: SiteAuditDetail["results"];
  }): SiteAuditDetail => ({
    siteId: site.id,
    name: site.name,
    url: site.url || null,
    score: row.score,
    ranAt: row.ranAt.toISOString(),
    results: row.results,
  });

  const existing = await latestAuditRow(siteId);
  if (existing && Date.now() - existing.ranAt.getTime() < RUN_AUDIT_COOLDOWN_MS) {
    return { kind: "cooldown", detail: toDetail(existing) };
  }

  const snapshot = await fetchSiteSnapshot(site.url, fetchImpl);
  const result = runSiteAudit(snapshot, {
    targetCountries: site.targetCountries ?? [],
  });
  await persistSiteAudit(siteId, result);
  const row = await latestAuditRow(siteId);
  if (!row) throw new Error("audit row missing after persist"); // impossible
  return { kind: "ran", detail: toDetail(row) };
}
```

Imports à ajouter en tête de `tools.ts` :

```ts
import { persistSiteAudit } from "@/lib/spec/audit/persist";
import { runSiteAudit } from "@/lib/spec/audit/run";
import { fetchSiteSnapshot } from "@/lib/spec/audit/snapshot";
```

(`getSiteById` (tools.ts l.~40 via queries) et `latestAuditRow` (tools.ts l.59) existent déjà ; `getSiteById` expose bien `targetCountries` — `src/lib/sites/queries.ts` le sélectionne.)

- [ ] **Step 4: Vérifier que les tests passent**

Run: `DB_TESTS=1 DATABASE_URL=postgresql://humperdoo@localhost:5432/seo_insight_test bun test src/lib/mcp/tools.test.ts`
Expected: PASS (4 nouveaux tests verts, aucun existant cassé).

- [ ] **Step 5: Commit**

```bash
bunx biome check --write src/lib/mcp/tools.ts src/lib/mcp/tools.test.ts
git add src/lib/mcp/tools.ts src/lib/mcp/tools.test.ts
git commit -m "feat(mcp): runSiteAuditForOrg — audit à la demande, cooldown 60s, upsert"
```

---

### Task 3: Registration du tool `run_site_audit` + liste des tools

**Files:**
- Modify: `src/lib/mcp/server.ts` (après le bloc `get_site_audit`, ~l.111)
- Modify: `src/lib/mcp/route.test.ts` (liste EXACTE des noms de tools, ~l.136 — elle casse sinon)

**Interfaces:**
- Consumes: `runSiteAuditForOrg`, `RUN_AUDIT_COOLDOWN_MS` (Task 2), `requireOrgId`/`text` (existants dans server.ts).

- [ ] **Step 1: Ajouter `run_site_audit` à la liste attendue de route.test.ts (test rouge)**

Dans le `expect(names).toEqual([...])` : ajouter `"run_site_audit",` (la liste est `.sort()`ée, l'ordre d'insertion importe peu).

Run: `DB_TESTS=1 DATABASE_URL=postgresql://humperdoo@localhost:5432/seo_insight_test bun test src/lib/mcp/route.test.ts`
Expected: FAIL — la liste réelle n'a pas `run_site_audit`.

- [ ] **Step 2: Enregistrer le tool dans server.ts**

Après le `server.registerTool("get_site_audit", …)` :

```ts
  server.registerTool(
    "run_site_audit",
    {
      description: `Lance un audit du site (fetch réel de sa surface publique : home, robots.txt, sitemap, security.txt, llms.txt) et remplace l'audit stocké. Si le dernier audit date de moins de 60 secondes, renvoie le résultat existant sans re-fetcher. À utiliser pour vérifier des corrections ; l'appel compte dans le quota MCP mensuel. ${SCOPE_NOTE}`,
      inputSchema: {
        siteId: z.string().describe("L'id du site (cf. list_sites)"),
      },
    },
    async ({ siteId }, extra) => {
      const out = await runSiteAuditForOrg(requireOrgId(extra), siteId);
      if (out.kind === "not-found")
        return text("Site introuvable dans cette organisation.");
      if (out.kind === "no-url")
        return text(
          `Le site « ${out.name} » n'a pas d'URL configurée — impossible d'auditer.`,
        );
      const { detail } = out;
      const note =
        out.kind === "cooldown"
          ? "Audit récent (moins de 60 s) — résultat existant renvoyé, le site n'a pas été re-fetché.\n\n"
          : "";
      const header = `${note}Audit de ${detail.name} (id: ${detail.siteId}, ${detail.url ?? "URL non configurée"})\nScore : ${detail.score}/100 · audité le ${detail.ranAt}\n`;
      const items = detail.results.map(
        (r) => `- [${r.status}] ${r.itemId}${r.detail ? ` — ${r.detail}` : ""}`,
      );
      return text(
        `${header}\nRésultats (${detail.results.length} items) :\n${items.join("\n")}`,
      );
    },
  );
```

Import à compléter dans le bloc `from "./tools"` : `runSiteAuditForOrg`.

- [ ] **Step 3: Vérifier**

Run: `DB_TESTS=1 DATABASE_URL=postgresql://humperdoo@localhost:5432/seo_insight_test bun test src/lib/mcp/route.test.ts`
Expected: PASS — `tools/list` montre 9 tools.

- [ ] **Step 4: Commit**

```bash
bunx biome check --write src/lib/mcp/server.ts src/lib/mcp/route.test.ts
git add src/lib/mcp/server.ts src/lib/mcp/route.test.ts
git commit -m "feat(mcp): tool run_site_audit — boucle fix → re-audit pour les agents"
```

---

### Task 4: Builder de prompt `buildAuditFixPrompt` (pur, client-safe, TDD)

**Files:**
- Create: `src/lib/spec/audit/fix-prompt.ts`
- Test: `src/lib/spec/audit/fix-prompt.test.ts`

**Interfaces:**
- Consumes: rien de server-only — toutes les données arrivent en paramètres (le composant client a déjà `categories` et l'audit en props).
- Produces:
```ts
export type FixPromptItem = {
  itemId: string;      // "security/hsts"
  title: string;       // titre checklist
  specStatus: string;  // required|recommended|optional|avoid
  status: string;      // pass|fail|warn|na|manual (verdict audit)
  detail: string;      // détail FR du check ("" si manual)
};
export type AuditFixPromptInput = {
  siteId: string;
  siteName: string;
  siteUrl: string;
  appUrl: string;      // ex. https://dashboard.seo-insight.com
  score: number;
  ranAt: string | null; // ISO
  items: FixPromptItem[]; // TOUS les items de l'audit, le builder filtre
};
export function buildAuditFixPrompt(input: AuditFixPromptInput): string;
```

- [ ] **Step 1: Écrire les tests qui échouent**

```ts
// src/lib/spec/audit/fix-prompt.test.ts
import { describe, expect, test } from "bun:test";
import { buildAuditFixPrompt } from "./fix-prompt";

const INPUT = {
  siteId: "site-123",
  siteName: "Mon Site",
  siteUrl: "https://mon-site.fr",
  appUrl: "https://dashboard.seo-insight.com",
  score: 62,
  ranAt: "2026-07-07T09:00:00.000Z",
  items: [
    { itemId: "foundations/doctype", title: "The HTML doctype", specStatus: "required", status: "pass", detail: "Doctype présent." },
    { itemId: "security/hsts", title: "HSTS", specStatus: "required", status: "fail", detail: "Pas d'en-tête HSTS." },
    { itemId: "security/trusted-types", title: "Trusted Types", specStatus: "recommended", status: "warn", detail: "Pas de CSP (Trusted Types inactifs)." },
    { itemId: "accessibility/color-contrast", title: "Colour contrast", specStatus: "required", status: "manual", detail: "" },
    { itemId: "performance/server-timing", title: "Server-Timing header", specStatus: "optional", status: "na", detail: "Pas de Server-Timing (optionnel)." },
  ],
};

describe("buildAuditFixPrompt", () => {
  const prompt = buildAuditFixPrompt(INPUT);

  test("embarque la connexion MCP avec placeholder, jamais de clé réelle", () => {
    expect(prompt).toContain("https://dashboard.seo-insight.com/api/mcp");
    expect(prompt).toContain("eli_xxxxxxxxxxxx");
    expect(prompt).toContain("Réglages → Clés API");
    expect(prompt).not.toMatch(/eli_[A-Za-z0-9_-]{20,}/); // pas de vraie clé
  });

  test("embarque siteId + contexte", () => {
    expect(prompt).toContain("site-123");
    expect(prompt).toContain("https://mon-site.fr");
    expect(prompt).toContain("62/100");
  });

  test("détaille les fail/warn, pas les pass", () => {
    expect(prompt).toContain("security/hsts");
    expect(prompt).toContain("Pas d'en-tête HSTS.");
    expect(prompt).toContain("security/trusted-types");
    expect(prompt).not.toContain("foundations/doctype"); // pass → absent
  });

  test("manual → consigne MCP, pas d'embed ; na → ids listés à part", () => {
    expect(prompt).not.toContain("accessibility/color-contrast — "); // pas de ligne détaillée
    expect(prompt).toContain("get_site_audit");
    expect(prompt).toContain("1 point(s) à vérifier manuellement");
    expect(prompt).toContain("performance/server-timing"); // na listé en id
  });

  test("boucle de vérification via run_site_audit + garde-fous", () => {
    expect(prompt).toContain("run_site_audit");
    expect(prompt).toContain("60 s");
    expect(prompt).toContain("un commit par point corrigé");
  });

  test("déterministe", () => {
    expect(buildAuditFixPrompt(INPUT)).toBe(prompt);
  });
});
```

- [ ] **Step 2: Vérifier l'échec**

Run: `bun test src/lib/spec/audit/fix-prompt.test.ts`
Expected: FAIL — module inexistant.

- [ ] **Step 3: Implémenter**

```ts
// src/lib/spec/audit/fix-prompt.ts — builder PUR du prompt « corriger avec
// votre agent ». Client-safe : aucune I/O, aucun import server-only — toutes
// les données (audit + items enrichis) arrivent en paramètres depuis le
// composant. Pattern : src/lib/keywords/prompt/build-prompt.ts.

export type FixPromptItem = {
  itemId: string;
  title: string;
  specStatus: string;
  status: string;
  detail: string;
};

export type AuditFixPromptInput = {
  siteId: string;
  siteName: string;
  siteUrl: string;
  appUrl: string;
  score: number;
  ranAt: string | null;
  items: FixPromptItem[];
};

export function buildAuditFixPrompt(input: AuditFixPromptInput): string {
  const toFix = input.items.filter(
    (i) => i.status === "fail" || i.status === "warn",
  );
  const manualCount = input.items.filter((i) => i.status === "manual").length;
  const naIds = input.items.filter((i) => i.status === "na").map((i) => i.itemId);

  const lines: string[] = [
    "# MISSION",
    `Corriger le site « ${input.siteName} » (${input.siteUrl}) pour le mettre en conformité avec The Website Specification, en t'appuyant sur le serveur MCP SEO Insight ci-dessous.`,
    `Audit du ${input.ranAt ?? "?"} — score actuel : ${input.score}/100. siteId : ${input.siteId}`,
    "",
    "# CONNEXION MCP",
    "Ajoute ce serveur MCP (l'utilisateur crée sa clé dans SEO Insight, Réglages → Clés API) :",
    "```json",
    JSON.stringify(
      {
        mcpServers: {
          "seo-insight": {
            url: `${input.appUrl}/api/mcp`,
            headers: { Authorization: "Bearer eli_xxxxxxxxxxxx" },
          },
        },
      },
      null,
      2,
    ),
    "```",
    "Remplace eli_xxxxxxxxxxxx par la clé API réelle fournie par l'utilisateur. Ne la committe jamais.",
    "",
    `# POINTS À CORRIGER (${toFix.length})`,
    "Pour CHAQUE point : appelle seo-insight.get_spec_topic(category, slug) pour la marche à suivre détaillée, applique la correction minimale dans le code du site, puis passe au suivant.",
    ...toFix.map(
      (i) =>
        `- [${i.status}] ${i.itemId} (${i.specStatus}) — ${i.title}${i.detail ? ` · constat : ${i.detail}` : ""}`,
    ),
    "",
    `# À VÉRIFIER MANUELLEMENT (${manualCount} point(s) à vérifier manuellement)`,
    `L'audit automatique ne couvre pas tout : appelle seo-insight.get_site_audit("${input.siteId}") pour la liste des items en statut manual, traite ceux qui s'appliquent au code du site (contraste, alt text, ARIA, formulaires…) via get_spec_topic, et signale à l'utilisateur ceux qui relèvent du contenu ou de l'éditorial.`,
    "",
  ];
  if (naIds.length > 0) {
    lines.push(
      `# NON APPLICABLES AUJOURD'HUI (${naIds.length}) — à évaluer si pertinent`,
      ...naIds.map((id) => `- ${id}`),
      "",
    );
  }
  lines.push(
    "# BOUCLE DE VÉRIFICATION",
    `Après avoir corrigé et déployé un lot de points : appelle seo-insight.run_site_audit("${input.siteId}") et compare les verdicts. Répète jusqu'à zéro fail/warn. Note : un re-audit à moins de 60 s du précédent renvoie le résultat existant (cooldown).`,
    "",
    "# GARDE-FOUS",
    "- Modifications minimales : ne réécris pas ce qui fonctionne.",
    "- Ne casse rien : vérifie que le site build/tourne après chaque changement.",
    "- Discipline git : un commit par point corrigé, message clair.",
  );
  return lines.join("\n");
}
```

- [ ] **Step 4: Vérifier**

Run: `bun test src/lib/spec/audit/fix-prompt.test.ts`
Expected: PASS (6 tests). Ajuster les libellés exacts si un `toContain` diverge — le test est la source de vérité du wording.

- [ ] **Step 5: Commit**

```bash
bunx biome check --write src/lib/spec/audit/fix-prompt.ts src/lib/spec/audit/fix-prompt.test.ts
git add src/lib/spec/audit/fix-prompt.ts src/lib/spec/audit/fix-prompt.test.ts
git commit -m "feat(checklist): buildAuditFixPrompt — prompt agent pur et client-safe"
```

---

### Task 5: Bouton « Copier le prompt agent » sur /flight-checker

**Files:**
- Modify: `src/app/(app)/flight-checker/page.tsx` (prop `appUrl`)
- Modify: `src/components/flight-check/checklist.tsx` (props + bouton à côté de « Relancer l'audit », ~l.350-370)

**Interfaces:**
- Consumes: `buildAuditFixPrompt`, `FixPromptItem` (Task 4) ; `audit` (état existant), `categories` (prop existante), `siteId`/`siteName`/`siteUrl` (props existantes) ; `toast` de `sonner` (déjà dans le projet — cf. `keyword-prompt-card.tsx`).

- [ ] **Step 1: Passer `appUrl` depuis le RSC**

Dans `page.tsx` (même source que `settings/api-keys/page.tsx:34`) :

```ts
import { env } from "@/lib/env"; // vérifier le chemin exact utilisé par settings/api-keys/page.tsx et reprendre le même
// …
const appUrl = env.BETTER_AUTH_URL ?? "https://app.seo-insight.com";
// …
<Checklist … appUrl={appUrl} />
```

- [ ] **Step 2: Bouton + construction du prompt dans checklist.tsx**

Ajouter `appUrl: string` aux props du composant. Construire les `FixPromptItem` en joignant l'audit aux items de la checklist (les deux sont déjà là) :

```tsx
import { toast } from "sonner";
import { Copy } from "lucide-react";
import {
  buildAuditFixPrompt,
  type FixPromptItem,
} from "@/lib/spec/audit/fix-prompt";

// … dans le composant, près de runAudit :
const copyFixPrompt = async () => {
  if (!audit) return;
  const byId = new Map(
    categories.flatMap((c) => c.items).map((it) => [it.id, it]),
  );
  const items: FixPromptItem[] = audit.results.flatMap((r) => {
    const item = byId.get(r.itemId);
    if (!item) return []; // item disparu de la checklist courante
    return [{
      itemId: r.itemId,
      title: item.title,
      specStatus: item.status,
      status: r.status,
      detail: r.detail,
    }];
  });
  const prompt = buildAuditFixPrompt({
    siteId,
    siteName,
    siteUrl,
    appUrl,
    score: audit.score,
    ranAt: audit.ranAt,
    items,
  });
  try {
    await navigator.clipboard.writeText(prompt);
    toast.success("Prompt copié", {
      description:
        "Collez-le dans votre agent (Claude Code, Cursor…) avec votre clé API (Réglages → Clés API).",
    });
  } catch {
    toast.error("Impossible de copier le prompt");
  }
};
```

À côté du bouton « Relancer l'audit » (même conteneur flex) :

```tsx
<Button
  variant="outline"
  onClick={copyFixPrompt}
  disabled={!audit || running}
  className="shrink-0"
  title={
    !audit
      ? "Lancez d'abord un audit pour générer le prompt."
      : "Prompt prêt-à-coller pour votre agent (nécessite une clé API — Réglages → Clés API)."
  }
>
  <Copy className="size-4" />
  Copier le prompt agent
</Button>
```

- [ ] **Step 3: Vérifier la suite + lint**

Run: `bun run test:all && bunx biome check src/components/flight-check/checklist.tsx src/app/\(app\)/flight-checker/page.tsx`
Expected: suite verte, lint propre.

- [ ] **Step 4: Vérification live (navigateur QA logué)**

- `/flight-checker` : le bouton apparaît à côté de « Relancer l'audit », désactivé si jamais audité.
- Cliquer → toast « Prompt copié » ; lire le presse-papier via `d3k agent-browser eval "navigator.clipboard.readText()"` et vérifier : config mcpServers + siteId réel + items fail/warn + section run_site_audit.

- [ ] **Step 5: Commit**

```bash
git add src/components/flight-check/checklist.tsx "src/app/(app)/flight-checker/page.tsx"
git commit -m "feat(checklist): bouton « Copier le prompt agent » sur /flight-checker"
```

---

### Task 6: Vérification endpoint réelle + roadmap + fin de branche

**Files:**
- Modify: `docs/project/roadmap.md` (section « The Checklist (audit + MCP) »)

- [ ] **Step 1: Smoke test endpoint `run_site_audit`**

Réutiliser le script scratchpad `mcp-smoke.ts` (clé éphémère + org `SMOKE_ORG_ID=0668c152-…`) en remplaçant l'appel `search_spec_topics` par :

```json
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"run_site_audit","arguments":{"siteId":"<id du site de l'org smoke, cf. list_sites>"}}}
```

Expected: 200, score + verdicts ; un second appel immédiat renvoie la note cooldown. `tools/list` = 9 tools.

- [ ] **Step 2: Roadmap**

Ajouter en tête de « The Checklist (audit + MCP) » :

```markdown
- ✅ **Prompt de correction agent + `run_site_audit`** (v1.1 ELEVA-93) : bouton « Copier le prompt agent » sur `/flight-checker` (prompt prêt-à-coller : connexion MCP placeholder, fail/warn détaillés, consigne MCP pour les manual, N/A à part, boucle fix → re-audit, garde-fous — builder pur `fix-prompt.ts`) ; tool MCP **`run_site_audit(siteId)`** (première mutation : fetch du site + upsert une-ligne-par-site, **cooldown 60 s** anti-boucle, 9 tools au total) ; helper partagé `persistSiteAudit`. Spec : `docs/superpowers/specs/2026-07-07-audit-fix-prompt-mcp-design.md`. *Livré 2026-07-07.*
```

- [ ] **Step 3: Gate finale + merge**

```bash
bun run test:all && bun run lint
git add docs/project/roadmap.md
git commit -m "docs(project): roadmap — prompt de correction agent + run_site_audit"
git push -u origin feature/audit-fix-prompt
git checkout dev && git merge feature/audit-fix-prompt --ff-only && git push
git branch -d feature/audit-fix-prompt && git push origin --delete feature/audit-fix-prompt
```

(Jamais de merge `main` — dev uniquement.)
