
Dr. Kernel’s classes · Lesson 1
How to build a building full of agents (with Claude)
Ahem. Sit down, Dicey. Nothing in this building is pretend: every neighbour is a program that really runs on richi.codes, and what they say in the group chat comes from what they really did today. Here is how I built them.
1. Inky is a Claude routine
A routine is a Claude Code task that runs on Anthropic’s cloud on a schedule, so it keeps working with my laptop closed. Inky’s schedule is stored as a cron expression in UTC: that’s why Nauty jokes about time zones.
Each run clones the repo, researches a topic, writes the note in English and Spanish with a live demo, and checks it. By default a routine pushes to a branch starting with claude/; Inky’s prompt tells her to push to main only after everything passes, or to touch nothing if she can’t do it properly.
name: richi.codes — daily note (EN+ES)
schedule: "0 5 * * *" # cron in UTC: 07:00 in Madrid in summer, 06:00 in winter
model: claude-opus-5-5
repo: github.com/richigallego/richicodes
tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch2. Mr. Sourcey is not a second agent
He’s the part of Inky’s prompt that nobody likes: every concrete claim has to be in a primary source she has read (MDN, the spec, the browser’s own blog). Every link is opened with curl and dropped if it doesn’t return 200. Fewer than two primary sources means no note that day. Then the demo runs in Chromium, Firefox and WebKit, and the site has to build.
His count in the chat (“I counted N links”) is real: the page counts the links in today’s note.
3. The couriers are n8n workflows
Bluebell and Tusk live in Nauty’s boiler room (a self-hosted n8n). Each one: a schedule trigger (17:00 and 17:15, Madrid time) → read richi.codes/feed.json → ask my own account what it already posted → a small Code node picks the oldest note that isn’t out yet → post it. One a day, no repeats.
Gotchas we hit: on Bluesky a hashtag is only a link if you send it as a facet, with its position counted in UTF-8 bytes, not characters, and a post is 300 graphemes max. On Mastodon every link counts as 23 characters whatever its length, and the Idempotency-Key header stops a retry from tooting twice.
POST https://bsky.social/xrpc/com.atproto.repo.createRecord
Authorization: Bearer <accessJwt from createSession>
{
"repo": "<your DID>",
"collection": "app.bsky.feed.post",
"record": {
"$type": "app.bsky.feed.post",
"text": "CSS can count now…\n\n#css #layout",
"facets": [{ "index": { "byteStart": 22, "byteEnd": 26 },
"features": [{ "$type": "app.bsky.richtext.facet#tag", "tag": "css" }] }],
"embed": { "$type": "app.bsky.embed.external", "external": { "uri": "https://richi.codes/article/…", "title": "…", "description": "…" } },
"createdAt": "2026-10-19T15:00:00.000Z"
}
}
POST https://mastodon.social/api/v1/statuses
Authorization: Bearer <access token>
Idempotency-Key: <one per note> # a retry never posts the same toot twice
status=CSS can count now…&language=en4. How they talk to each other
The neighbours don’t chat freely: I wrote the script of the day, and real facts fill in the blanks. When the page is generated (and again every 10 minutes) it reads the latest note, today’s game and robots.txt from the repo, and asks the public APIs of dev.to, Bluesky and Mastodon what was posted. If an API doesn’t answer, Bugsy eats the error in the chat. Then your browser reads the Madrid clock and shows whoever is talking now.
// components/Edificio/script.js (simplified)
const scenes = [
{ at: fromUtc('05:00', offset), who: 'inky', text: t.inkyWake },
{ at: toMinutes('17:00'), who: 'bluebell', text: t.bluebell(bskyState, title) },
// …
];
// The page asks dev.to, Bluesky and Mastodon what was really posted today,
// rebuilds itself every 10 minutes, and your browser picks the scene for the Madrid clock.5. How I drew them
With FLUX.2 [dev] on Cloudflare Workers AI, whose free plan gives 10,000 “neurons” a day. That was enough for about ten portraits before it said “come back tomorrow”, which is why Devvy is still in a box.
Lessons learned: asking for all eleven neighbours in one picture gave me clones with the wrong name tags, so each one got their own picture with the same style paragraph. The safety filter rejected the word “kid” in one description; “cartoon courier” went through. The white background was removed with a flood fill from the edges, so the white lab coat survived.
const form = new FormData();
form.append('prompt', 'Single character design, full body, white background. Style: retro 1980s Japanese gag cartoon… Character: a tiny robot writer with a teal antenna…');
form.append('width', '1024');
form.append('height', '1024');
const res = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${ACCOUNT}/ai/run/@cf/black-forest-labs/flux-2-dev`,
{ method: 'POST', headers: { authorization: `Bearer ${TOKEN}` }, body: form }
);And Nana? She is the robots.txt, and she lets everybody in on purpose. The reasoning is in this note: Say no to AI training, not to AI search.
Homework: come back at 17:00 and watch Bluebell deliver. Class dismissed.








