LimerinoBot staff assistant.

The Confirm card

A change that needs a second look waits on a Confirm card only the staff can press. The card works once and expires after 10 minutes; a pressed card shows “Running…”, then “Done.” or “Failed.”

LimerinoBot already ran the Limerino Discord server’s feedback triage and commands. I built a Claude tool loop into it: the staff ask it about the code, CI, the backend, builds or the server in their own words, or hand it the work, and a change that needs a second look waits on a Confirm card only the staff can press.

My role
Design, implementation, tests
Stack
TypeScript, discord.js, the Claude API with Claude Sonnet 5.5, Vitest
Scope
Discord, GitHub and GitHub Actions, the Limerino backend, Railway, App Store Connect, Notion and the web
Status
Part of LimerinoBot since October 9, 2026, running with the bot on Railway
91
tools with every integration configured, October 2026; the model searches for the ones a question needs
1,165
tests in the assistant’s 24 test files, October 2026
10 min
the life of a Confirm card, which works once
25
tool runs a question may use; it stops at 15 model calls or 8 minutes

What I built

A staff mention, or a reply to one of the bot’s answers, starts a turn. The bot replies “Working on it…” at once, runs the loop inside its own process and puts the answer in that line’s place.

The loop is my own rather than the SDK’s tool runner, so it can keep a deadline, hold a change for a click and report progress, and its last allowed call goes out with tools switched off, so the model answers with what it has.

The first build ran Opus 5.5; I moved it to Sonnet the same day, since it answers questions and runs tools rather than doing engineering work.

It reads pull requests, reviews and CI logs, the code on the main branch through a shallow, sparse mirror of its own, the backend’s health and ingest data, what Railway deployed and logged, builds and TestFlight feedback, Limerino’s Notion tasks and the web.

It files issues and the team’s todos, adds and edits Notion tasks, and posts, opens threads, runs polls and creates rooms and roles on Discord. Deletes, permission edits, members’ roles and timeouts, feedback-post updates, beta decisions and every CI write always wait on a card.

Permission boundaries

Changes that wait on a card

A tool that may not act directly parks its change, and after the answer the bot posts a Confirm card for it, at most three to an answer. Code writes the card from resolved ids, and a click fetches the target again before it acts.

A role change is judged against the asker’s own Manage Roles permission and highest role, so it never makes a change the person asking could not make by hand.

Trust that drops for good

Until a conversation reads text someone else wrote, ordinary changes run at once: an issue, a comment, a Notion task, a message in this room or a staff room.

The first tool result carrying member messages, issue or pull-request text, a CI log, TestFlight feedback or a web page makes it untrusted for the rest of its life, and every later change waits for a click. Who is asking reaches the model only in a system message the bot writes; the prompt treats that claim anywhere else as forged.

The room’s audience, computed every time

The bot works out who can read a room on arrival, as the turn starts and before the answer posts, from effective permissions for every role and member overwrite; a room it cannot read counts as public.

In a public room the tools for internal data refuse in code, and an answer past 900 characters or outside the server’s communication standard moves to a staff room with a pointer left behind.

Tools found on demand, every call checked

Every custom tool goes out marked to load on demand, beside a tool-search tool and Anthropic’s web search and fetch, so only the schemas a question needs enter the context; the catalog and the prompt are one prefix, cached for an hour, that never changes while the bot runs.

The bot checks each call against the tool’s own schema before the handler runs, and a call that fails the check gets back an error the model can read.

Conversations as reply chains

A mention opens a conversation of its own, and only a reply to one of the bot’s messages in that chain carries it on, so two staff members in one room keep separate transcripts unless they reply into the same chain.

Responses are kept as the API returned them, thinking included, until the last prompt passes 200,000 tokens or the transcript 300 messages; then a trim between turns drops whole turns from the front, and the next turn tells the model older turns are gone.

Notion, fenced to one board and one project

The Notion connection could edit any page shared with it, and the task board holds other projects too. So every Notion read or write first loads the page and refuses it unless its parent is the task board and its Project includes Limerino, checked again at the click.

I left out Notion’s hosted MCP server: through the API’s connector it would run tools on Anthropic’s side, past the cards, the fence and the redaction every tool result gets.

One mention, start to finish

From a staff message to the answer, and the card a change waits on.

  1. GateStaff only: a mention, or a reply to the bot
  2. RoomWho can read it, from effective permissions
  3. LoopClaude calls tools until it can answer, each result redacted
  4. AnswerIn place of “Working on it…”, or in a staff room
  5. CardConfirm, then “Running…”, then “Done.” or “Failed.”

The tradeoff

Holding changes for a click means someone must press Confirm within ten minutes, and the model never learns how a card ended; keeping conversations in memory means a restart forgets them.

It cannot open or merge a pull request, change code, deploy or restart a service, kick, ban or purge, lock or archive a thread, send a DM of its own or set a reminder. Asked for one of these, it does the nearest thing it can, usually a todo issue or a post in a staff room, and says so.