Dev.to · 11 min read

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

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 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 Build the workflow once, reuse it anywhere 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. Why goose over a direct API call? 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: Provider changes without code changes: the model provider is configured in goose's settings, so switching providers doesn't require editing the bot. Reusable instructions: the same SKILL.md can be used outside Discord, making the workflow useful beyond the bot (more on this in step 2). Tools when needed: extensions add capabilities as the workflow grows. For example, a calendar integration could let the bot check when the next community call is scheduled, something I could add in the future. 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. The Architecture 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. Before you start You need four things: Node.js 20 or later. goose CLI signed in to a model provider A Discord app from the Developer Portal Manage Server permission on your Discord server, so you can add the bot. Only the person who installs it needs this. Other reviewers don't need it (the person who runs the bot adds their Discord user IDs to the settings) 1. The prototype 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: Acronyms are ambiguous: the community name is an acronym, and the model at first answered about a popular web framework with the same initials The model has its own style and that lack of customization was odd in a community channel. There are old links, meeting times, channels that are private that the model cannot tell which facts are still true unless you give them proper instructions Each of these became one rule in the next version 2. One guide, one source of truth 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. The guide has a few simple rules: Public information only. The bot may quote anything in it, so no passwords, private links or personal data. A Last reviewed: line. Drafts show a warning when the guide is older than 30 days. Every link in the guide is an approved link, and every mention is an approved public channel. The bot builds its allowlists from this file (more on that in step 5). 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." --- ... 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. 3. Keeping a human-review in every answer 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) }); } 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); } A few small details that matter in a real community: The draft is deleted from memory before posting, so a double click cannot post twice. Only the reviewer who made a draft can press its buttons or open its edit form. allowedMentions blocks @everyone, @here and role pings. Only the member being answered is mentioned. Link previews are off, so one answer does not fill the channel with cards. Every published answer ends with a small footer: AI-assisted answer, reviewed by @reviewer. 4. Running Goose 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(), }); 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; } 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: It escapes every < and > in the message, so it can't close its marker and pretend to be part of the guide (code below) The prompt tells goose to treat the message as untrusted and to reply NO_ANSWER: off-topic request to these attempts A human reviews every draft before anything is posted function cleanQuestion(question) { return question .replace(//g, ">") .replace(/\s+/g, " ") .trim() .slice(0, MAX_QUESTION_CHARS); } 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: . 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. 6. Tests 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"] } 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. 7. Set it up for your community 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 Final remarks 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! 👋

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More AI & Machine Learning News