ai newsSo., 1. Nov.
EilmeldungCloned a Stranger's Repo? Check 6 Agent Config Files Before Claude Code or Codex Opens It

Leafy Goose: A Human-Led AI Assistant for Maintainers’ Community Work

How I built a human-led community assistant with Goose Agent and Skill.md Every tech community knows this moment: a newcomer joins, says hi, and asks "What should I do first? Please help!" Welcoming one newcomer is easy. Welcoming new people every single week, with the same questions, is a differen

dev.toSonntag 16:28 · 01.11.9 Min. Lesezeit
Bild: dev.to

How I built a human-led community assistant with Goose Agent and Skill.md

Every tech community knows this moment: a newcomer joins, says hi, and asks "What should I do first? Please help!" Welcoming one newcomer is easy. Welcoming new people every single week, with the same questions, is a different story.

In a Discord open source community I help with, the same "how do I get started?" questions come back again and again: Do I need to attend every meeting? Where do I share my idea? The answers are already in our guides. But I get it: people want a quick answer in the place where they asked. They don't have time to dig through pages, and they want to feel there is a person on the other side.

Most moderators from this community are volunteers, so those answers sometimes wait days. A bot that answers on its own is not the fix either. It is fast, but it loses the human connection, and it can be wrong. So I kept thinking: how can we answer faster, right where people ask, and still keep a human in the loop?

My answer this time was a build with goose, a SKILL.md and Discord. I started with a small bot called Goose Bot v1 and ended with Leafy Goose 🍃🪿, an open source bot where AI drafts the answer and a human always decides what gets posted.

In this post, I share how I got there, step by step + a link to the public repo at the end to test it yourself with your community

I wanted to build a reusable workflow that other trusted community members could help review and maintain. I also wanted to adapt it to other communities I’m involved with by changing the resources goose reads, without touching the code.

That led me to use goose as the AI layer behind the Discord bot. The bot handles the Discord interaction, while goose uses the community’s resources and instructions to draft answers to questions phrased in different ways.

This is a fair question I got from folks who have been following my work. I could have connected the bot directly to a model API. None of the building blocks below is unique to goose, but goose gives me all of them in one open source tool, ready to use:

Today the bot is built for Discord, so other chat tools would need some extra work. But only one part of the architecture would change: the chat layer. The guide, goose and the answer checks can stay the same.

Leafy Goose has one rule: AI drafts, a human publishes. A reviewer (a moderator, maintainer or community manager) picks a member's question. The bot asks goose for a draft, using only the community guide. The reviewer reads the private draft and decides.

Only the prompt goes out to the model provider, and only a reviewer's click puts an answer in a public channel.

My first bot was simple. A slash command took the question, sent it to goose, and showed the answer. It worked and taught me fast what can go wrong when a model talks to real people:

Each of these became one rule in the next version

The fix for "it knows things you did not tell it" is to tell it everything it may say, and nothing else. All the facts live in one Markdown file: the community guide. Changing what the bot says means editing that file, not the code.

At first it was a plain.md file. Later I turned it into a goose skill, so the same guide could also works beyond the bot if more people want to make use of it (more in the side story below).

--- name: drf-community-guide description: " Approved facts, links and public Discord channels for the community. Use it to draft answers about getting started, community calls, events, contributing and conduct." ---... Enter fullscreen mode Exit fullscreen mode Plain.md vs SKILL.md With a plain.md, an agent does not know the file exists: you have to paste it or ask the agent to read it every time. A skill is found automatically: the agent sees its name and description in every session, and loads the full guide only when a question matches or when you ask for it.

So why does the bot not load it as a skill? goose loads a skill only when it decides the question matches the description. For a bot that must always stay on facts, "maybe" is not good enough. The bot reads the file itself, removes the top block, and puts the whole guide in every prompt

🪿 Side story: a skill beyond the bot. Because the guide is a standard SKILL.md, it is useful outside Discord too. Community managers, moderators, or anyone curious about the community can add it to their favorite AI agent (goose, or any other agent that supports skills). Then they can ask it for help with a welcome post, an answer in GitHub Discussions or a newsletter. Same facts and links, wherever they work.

This is the heart of Leafy Goose. Every answer starts as an ephemeral message: only the reviewer who asked can see it. Under the draft there are three buttons: Publish, Edit and Discard.

A reviewer can start a draft by right-click a member's message, then Apps, then Answer with Leafy Goose

Discord wants a first response within 3 seconds, and goose needs longer. So the bot defers the reply first, then asks goose:

async function createDraft ( interaction, question, target ) { //...allowlist, cooldown and "one request at a time" checks... await interaction. deferReply ({ flags: MessageFlags. Ephemeral }); const raw = await guideLib. askGoose ( guide, question, { timeoutMs: GOOSE_TIMEOUT_MS }); const result = guideLib. checkAnswer ( raw, guide ); if (! result. answer ) { return interaction. editReply ( `🤔 Leafy Goose has no approved answer for this ( ${ result. noAnswerReason } ).` ); } const id = interaction. id; drafts. set ( id, { id, answer: result. answer, warnings: result. warnings, reviewerId: interaction. user. id, interaction, target, timer: setTimeout (() => expireDraft ( id ), DRAFT_TTL_MS ), // 14 minutes }); await interaction. editReply ({ content: renderDraft, components: draftButtons ( id ) }); } Enter fullscreen mode Exit fullscreen mode Drafts expire after 14 minutes, because Discord interaction tokens last 15.

This is what a reviewer sees. Only they can read it:

When the reviewer clicks Publish, the bot replies to the member's message when there is one. That way the answer stays next to the question:

const channel = await interaction. client. channels. fetch ( draft. interaction. channelId ); if ( draft. target ) { const message = await channel. messages. fetch ( draft. target. messageId ); sent = await message. reply ( options ); } else { sent = await channel. send ( options ); } Enter fullscreen mode Exit fullscreen mode A few small details that matter in a real community:

goose is an agent: with extensions it can run shell commands and read files. For a Discord bot I want the opposite. goose should read one prompt and write one answer, nothing more.

The bot calls the goose CLI with execFile (no shell) and four flags:

const args = [ " run ", " --no-session ", " --no-profile ", " --quiet ", " --max-turns ", " 2 ", " --text ", buildPrompt ( guide, question ), ]; await execFileAsync ( process. env. GOOSE_BIN || " goose ", args, { timeout: timeoutMs, env: safeGooseEnv (), }); Enter fullscreen mode Exit fullscreen mode safeGooseEnv() does two jobs. It sets GOOSE_MODE=chat, so goose does not call tools. And it removes every DISCORD_* variable, so the Discord token never reaches goose:

function safeGooseEnv () { const env = {... process. env, GOOSE_MODE: " chat " }; for ( const key of Object. keys ( env )) { if ( key. startsWith ( " DISCORD_ " )) delete env [ key ]; } return env; } Enter fullscreen mode Exit fullscreen mode The prompt has two clearly marked parts: the guide, which the bot trusts, and the member's message, which it doesn't. Member text could try a prompt injection to ignore the rules, reveal the prompt or act as something else. No single trick stops that, so the bot uses layers:

function cleanQuestion ( question ) { return question. replace ( /</g, " < " ). replace ( />/g, " > " ). replace ( / \s +/g, " " ). trim (). slice ( 0, MAX_QUESTION_CHARS ); } Enter fullscreen mode Exit fullscreen mode The prompt rules are short and strict: always English, at most 120 words, one clear next step, only links from the guide, never a date for a future meeting. When the guide has no answer, the model replies with one line: NO_ANSWER: <reason>. The bot then tells the reviewer to answer by hand, instead of guessing.

💡 Watch your error logs: One of the things I did to avoid leaking members' questions was to never log goose's raw error. When goose fails, Node's error repeats the full command, question included. Leafy Goose only reports goose failed (exit code 1).

Before a draft reaches the reviewer, the bot also flags links or channels that are not in the guide, meta comments and very long answers.

There are two kinds of tests in the repo, and they answer different questions:

Unit tests check the code: is the guide loaded? are bad links flagged? do Discord secrets stay out of the goose environment? They use a fake guide and goose script, so they need no Discord, no goose and no network

Evals check the answers: Each eval is a real question, plus words the answer must or must not contain:

{ "question": "how can i contribuite to drf", "mustInclude": [ "touchpoint" ], "mustNotInclude": [ "if you mean", "contribuite" ] } Enter fullscreen mode Exit fullscreen mode This one checks two things. The answer points to a community call, and the bot does not comment on the member's typo. Other evals check that the bot never invents a date, never links a private channel, answers in English, and replies NO_ANSWER to off-topic questions and injection attempts.

Leafy Goose started as a bot for one community, but nothing in the code is specific to it. To use it in another Discord community, you write a new guide, change a few settings and update your own.env

One running bot serves all your reviewers: add their Discord user IDs to DISCORD_ALLOWED_USER_ID. The code is at github.com/anajsana/leafy-goose

As a maintainer in open source communities, I know the community load first-hand, and there is still a lot of talk about AI adding to maintainer burnout. However, this time, with Leafy Goose, I wanted to show the other side: the side where AI can take on the repetitive and boring part of community work, while maintainers keep the voice, the judgment and the human connection.

I also wanted another community manager to be able to take this, point goose to their own community's guide, and review the answers without having to rebuild the Discord bot. If you try it in your community, I'd love to hear how it goes! 👋

Quelle: dev.to (dev.to). Original öffnen