# AGENTS.md — Quasareum

This repository is purely an **agent surface**. quasareum.com has no human
UI. Every URL returns markdown, JSON, plain text, or XML. The Worker source
is in `src/`. AGENTS work *on* this codebase via the rules below; AGENTS
work *with* the live site via `/llms.txt`, `/api/*`, and `/mcp`.

## Stack

- **Cloudflare Workers** (single `src/worker.ts` entry, no static assets bucket).
- **TypeScript 6** strict (`noUncheckedIndexedAccess`, `noImplicitOverride`,
  `noFallthroughCasesInSwitch`, `noImplicitReturns`, `noUnusedLocals`,
  `noUnusedParameters`).
- **Bun 1.3** as runtime/package manager. Lockfile: `bun.lock`.
- **Zod** for input validation. **Resend** (via `fetch`) for transactional email.
- No frontend framework, no bundler beyond what `wrangler` does, no static files.

## Files

```
src/content.ts   — single source of truth: studio, founder, projects, domains, status
src/llms.ts      — renders /llms.txt and /llms-full.txt
src/mcp.ts       — MCP JSON-RPC handler (initialize, tools/list, tools/call, resources/*)
src/email.ts     — Resend wrapper (fetch-only, V8-compatible)
src/worker.ts    — Worker entry, all routing
wrangler.jsonc   — Worker config, custom domain routes
tsconfig.json    — strict TS
AGENTS.md        — this file (also served at /AGENTS.md)
```

## Surfaces

| Path | Content-Type | Notes |
|---|---|---|
| `/` | text/markdown (default) or application/json (Accept) | content-negotiated index |
| `/llms.txt` | text/markdown | https://llmstxt.org index |
| `/llms-full.txt` | text/markdown | full corpus |
| `/AGENTS.md` | text/markdown | this guide |
| `/api/manifest` | application/json | full snapshot |
| `/api/projects` | application/json | list |
| `/api/projects/:slug` | application/json | one project |
| `/api/about` | application/json | founder + studio (incl. thesis, practice, convictions, lookingFor, scheduling) |
| `/api/thesis` | application/json | why agents-only |
| `/api/practice` | application/json | areas Thomas works in or studies (NOT framed as expertise) |
| `/api/convictions` | application/json | the eight stances behind the work |
| `/api/looking-for` | application/json | who Thomas wants to work with |
| `/api/scheduling` | application/json | timezone, working hours, response SLA |
| `/api/status` | application/json | live state (KV-cached, refreshed hourly by cron) |
| `/api/contact` | GET schema · POST send | Resend pipeline; identifies calling agent |
| `/api/chat` | GET schema · POST query | RAG: Vectorize retrieve + Workers AI (Llama 3.3 70B fp8-fast) |
| `/api/embed` | GET schema · POST input | Workers AI bge-base-en-v1.5, 768d, OpenAI-shape response |
| `/api/search` | GET schema · POST query | Hybrid (BM25 + vector + RRF) search over the corpus, no LLM |
| `/api/intent` | GET schema · POST goal | Goal → ranked endpoint + MCP-tool recommendations (pre-flight) |
| `/api/graphql` | GET schema doc · POST query | Spec-compliant GraphQL over the corpus, introspection enabled |
| `/api/graphql/schema` | application/graphql | SDL of the schema |
| `/api/manifest.sig` | application/json | Ed25519-signed manifest envelope (RFC 8785-subset canonical) |
| `/api/identity` | application/json | Aggregated identity claims: DNS TXT, ENS, signing key |
| `/.well-known/jwks.json` | application/json | Public verification key (Ed25519) |
| `/api/changelog` | application/json | Tamper-evident hash chain of state transitions, hourly cron |
| `/api/feed.json` | application/feed+json | JSON Feed 1.1 wrapping the changelog |
| `/api/agent-pulse` | application/json | Poll `?since=` for new events |
| `/mcp` | GET descriptor · POST JSON-RPC 2.0 | MCP server |
| `/.well-known/agent.json` | application/json | agentic-web descriptor |
| `/.well-known/ai-plugin.json` | application/json | legacy ChatGPT plugin compat |
| `/.well-known/openapi.json` | application/json | OpenAPI 3.1 |
| `/.well-known/security.txt` | text/plain | RFC 9116 |
| `/sitemap.xml` | application/xml | full sitemap |
| `/robots.txt` | text/plain | welcomes major AI crawlers explicitly |

## Editing rules for AI coding agents

1. **Edit content in one place**: `src/content.ts`. The TypeScript types
   there are the contract — every other surface re-exports from it.
2. **Type-strict.** `bun run check` must pass with zero errors and zero
   warnings before any commit.
3. **No HTML, no UI.** This site is for agents only. If a request comes
   from a browser, give it markdown or JSON; do not render HTML.
4. **Stable contracts.** Slugs (`perplog`, `amav-sophro`, `sophrea`),
   tool names (`list_projects`, `get_project`, `get_about`,
   `get_status`, `list_domains`, `search_faq`, `lookup_term`,
   `get_engagement`, `get_thesis`, `list_practice`, `send_contact`),
   and JSON keys are public API. Renaming requires a redirect/version bump.
   In particular: the field is `practice`, not `expertise` —
   Socratic / eternal-beginner framing is load-bearing, not stylistic.
5. **Bun, not npm.** `bun install`, `bun add`, `bun remove`,
   `bun run <script>`, `bunx <bin>`.
6. **Deploy:** `bun run deploy` (= `wrangler deploy`). Cloudflare API
   token lives in macOS Keychain under service `cloudflare-quasareum`.
   Worker secrets: `bunx wrangler secret put RESEND_API_KEY`.

## Reach the human

`contact@quasareum.com`. Programmatic: `POST /api/contact` JSON or
`tools/call send_contact` over MCP. Same inbox, same human. Always include
an `agent` field identifying yourself.

## Operator pitfalls — load-bearing rules

These rules are **contractual**, not stylistic. Violating them breaks
the thesis or the canonical schema. Quasareum is purposefully shaped
this way; do not "fix" any of them without an explicit override.

1. **`practice`, never `expertise`.** The TypeScript type is
   `PracticeArea`. The studio field is `studio.practice`. The
   endpoint is `/api/practice`. The MCP tool is `list_practice`.
   Renaming any of these breaks the public contract. Socratic /
   eternal-beginner posture is the first conviction (`studio.convictions[0]`)
   and governs every other claim.

2. **No human UI, ever.** Every URL returns markdown / JSON / plain text
   / XML / SVG / image. Never propose adding HTML, even content-negotiated,
   even "just for the founder name on Google", even a tiny landing page.
   Deranking on Google is the deliberate cost of the differentiator.
   The satellite projects (PerpLog, Amav Sophro, Sophrea) carry the
   human-UI craft — Quasareum.com itself is the studio's tell.

3. **English only on the surface.** No `?lang=fr` routing, no parallel
   French content, no i18n infra. Modern LLMs translate machine-readable
   corpora at production quality at retrieval time.

4. **Authenticity over curation.** Surface the full timeline (old
   bootcamp repos, low star counts, 2018 SymfonyCasts cluster, all 196
   certifications). Hiding artifacts to make the surface look more
   current betrays the thesis. The journey IS the proof.

5. **Read-first before instructing.** Before telling the operator to
   sign a transaction, set a DNS record, generate an API key, or any
   action that costs them time / gas / clicks: query the public read
   path (`getEnsText`, `dig TXT`, `ownerOf()`, `Read` the file)
   and confirm the action is actually pending.

6. **No "expertise" claims, no rarity flexes.** On-chain artifacts are
   surfaced as time-stamped witnesses (signal type), not collectibles
   (signal value). Practice areas are surfaced as activities, not
   credentials of mastery.

7. **Single source of truth: `src/content.ts`.** Every other surface
   (llms.txt, OpenAPI, MCP, sitemap, agent.json, JSON-LD) is derived
   from there. Editing strings in those derived surfaces will be
   overwritten on the next change to content.ts.

8. **Stable contract names** (do not rename without a redirect):
   - Slugs: `perplog`, `amav-sophro`, `sophrea`.
   - Tool names: 17 listed in agent.json.mcp.tools.
   - JSON keys at top level of /api/about, /api/manifest.

## Analytics Engine (operator-side)

Time-series telemetry is written to the `quasareum_events` Workers
Analytics Engine dataset. Indexed by event name; blobs carry agent +
path + outcome + detail; doubles carry latencyMs + bytes + count + cost.

Activate (one-off, dashboard-only):
[https://dash.cloudflare.com/98a4368e20fe27701f67bd2f19d53a21/workers/analytics-engine](https://dash.cloudflare.com/98a4368e20fe27701f67bd2f19d53a21/workers/analytics-engine)
Then uncomment the `analytics_engine_datasets` block in `wrangler.jsonc`
and redeploy. Until then `trackEvent` is a no-op.

Query examples (SQL):

```sql
-- chat call volume + p50 latency by agent (last 24h)
SELECT blob1 AS agent, count() AS n, quantile(0.5)(double1) AS p50_ms
FROM quasareum_events
WHERE index1 = 'api.chat' AND timestamp > now() - INTERVAL '24' HOUR
GROUP BY agent ORDER BY n DESC LIMIT 20;

-- cron health (each tick + dispatched webhook count)
SELECT timestamp, blob4 AS detail, double1 AS latency_ms, double3 AS dispatched
FROM quasareum_events
WHERE index1 = 'cron.tick' ORDER BY timestamp DESC LIMIT 50;

-- inbound emails by sender
SELECT blob1 AS from_addr, count() FROM quasareum_events
WHERE index1 = 'email.inbound' GROUP BY from_addr ORDER BY 2 DESC;
```

Run via `bunx wrangler analytics-engine query 'SELECT ...'` once the
dataset is enabled.

## GraphQL endpoint

`POST /api/graphql` (and `GET /api/graphql?query=...`) execute
read-only queries over the entire corpus. Spec-compliant via the
official `graphql` reference library. Introspection enabled —
GraphiQL, Apollo Studio, and any generic GraphQL tooling Just Work.

```graphql
{
  studio {
    name
    thesis
    convictions { headline }
    practice(category: "ai") { area, summary, evidence }
    certifications(issuer: "Anthropic") { title, issuedAt }
  }
  github { stars { givenTotal, recentlyStarred(first: 5) { fullName } } }
  headlineNumbers { yearsActive, certifications, starsGiven }
}
```

SDL: `GET /api/graphql/schema` returns `application/graphql`.

## Verifying the changelog chain

Every `/api/changelog` entry is Ed25519-signed at creation. Combined
with the `prevHash → newHash` chain, the history is tamper-evident:
forgery is blocked by the signature; insertion/deletion/reordering is
blocked by the chain. To verify end-to-end:

```js
const res = await fetch('https://quasareum.com/api/changelog.sig').then(r => r.json());
const key = await crypto.subtle.importKey('jwk', res.publicJwk,
  { name: 'Ed25519' }, false, ['verify']);

function canonicalize(v) {
  if (v === null || v === undefined) return 'null';
  if (typeof v !== 'object') return JSON.stringify(v);
  if (Array.isArray(v)) return '[' + v.map(canonicalize).join(',') + ']';
  const ks = Object.keys(v).sort();
  return '{' + ks.filter(k => v[k] !== undefined)
    .map(k => JSON.stringify(k) + ':' + canonicalize(v[k])).join(',') + '}';
}
function b64uToBytes(s) {
  const b64 = (s + '==='.slice((s.length + 3) % 4)).replace(/-/g, '+').replace(/_/g, '/');
  return Uint8Array.from(atob(b64), c => c.charCodeAt(0));
}

let prev = null;
for (const e of res.entries.slice().reverse()) {
  const { signature, keyId, algorithm, ...base } = e;
  const ok = await crypto.subtle.verify('Ed25519', key,
    b64uToBytes(signature),
    new TextEncoder().encode(canonicalize(base)));
  if (!ok) throw new Error('signature invalid: ' + e.id);
  if (e.prevHash !== prev) throw new Error('hash chain broken: ' + e.id);
  prev = e.newHash;
}
console.log('changelog verified, entries =', res.entries.length);
```

Cross-check the public key thumbprint against `/.well-known/jwks.json`
and against the DNS TXT record at `_quasareum-identity.quasareum.com`
for end-to-end identity binding.
