Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ac23254
Create maintain-docs.md
BillChirico Mar 4, 2026
d13361c
refactor(web): replace channel/role ID inputs with selectors (#242)
BillChirico Mar 5, 2026
25e73d3
Refactor dashboard config-editor into maintainable section architectu…
BillChirico Mar 5, 2026
20de28e
refactor(ai): rewrite triage prompts and add channel context
AnExiledDev Mar 5, 2026
3cdf10e
📝 Add docstrings to `refactor/triage-prompt-rewrite`
coderabbitai[bot] Mar 5, 2026
4779199
fix(triage): escape channel metadata, fix @mention precedence, clarif…
Mar 7, 2026
850bd7a
docs: fix maintain-docs.md — add heading, fix hardcoded branch date, …
Mar 7, 2026
ef9478b
fix(types): alertChannelId nullable, moderationLogChannel nullable, G…
Mar 7, 2026
ce862b6
refactor(dashboard): extract shared inputClasses, update config-edito…
Mar 7, 2026
78cbe0b
fix(dashboard): mobile grids, threshold guard, role identity, aria-la…
Mar 7, 2026
c39f28b
test(config-updates): add immutability assertions to update helpers
Mar 7, 2026
71f8f3b
fix: restore correct post-rebase state for config-editor, ModerationS…
Mar 7, 2026
cb2e33d
Merge branch 'main' into refactor/triage-prompt-rewrite
BillChirico Mar 7, 2026
96700bb
docs(maintain-docs): fix permissions write and copilot/ branch prefix
Mar 8, 2026
43f2405
fix(prompts): align moderation ladder and classifier rule example
Mar 8, 2026
8bbd31b
fix(dashboard): import shared inputClasses in AiAutoModSection, GitHu…
Mar 8, 2026
08d265b
fix(dashboard): postTime type=time, blockedDomains onChange, transcri…
Mar 8, 2026
dc43dcd
fix(dashboard): StarboardSection stale state and channelId null-coerce
Mar 8, 2026
05634f1
fix(dashboard): restore ChannelSelector for moderationLogChannel in T…
Mar 8, 2026
1e80640
fix(lib): restrict section type in config-updates, fix Ctrl+S prevent…
Mar 8, 2026
2abf756
style: apply biome auto-fixes (import order, formatting)
Mar 8, 2026
47db485
fix(dashboard): address remaining 10 PR #248 review threads
Mar 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions .github/workflows/maintain-docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Maintain Docs

---
on:
schedule:
- cron: '0 9 * * *' # 4 AM EST = 9 AM UTC
workflow_dispatch: {}

permissions:
contents: write
pull-requests: write
issues: write

tools:
github:
edit:

engine: copilot

---

# Maintain AGENTS.md Documentation

## Purpose

Keep the AGENTS.md file accurate and current by:
- Reviewing merged pull requests since last run
- Checking updated source files (src/, web/, tests/, etc.)
- Updating AGENTS.md to reflect any architectural or pattern changes
- Creating a pull request if updates are needed

## Instructions for the Agent

1. **Fetch Recent Changes**: Query the repository for merged PRs and updated files from the past 24 hours

2. **Review Architecture Changes**: Check if any of these directories have significant changes:
- `src/modules/` - New modules or modified patterns
- `src/api/` - API route or middleware changes
- `src/commands/` - New slash commands
- `src/utils/` - Utility additions or pattern changes
- `web/` - Dashboard updates
- `tests/` - Testing patterns

3. **Analyze Merged PRs**: Look at PR titles and descriptions to identify:
- New features added
- Architecture decisions
- Pattern changes
- Testing approach changes
- Breaking changes

4. **Update AGENTS.md if Needed**:
- Architecture Overview section: Add new modules or directories
- Key Patterns section: Document new patterns or changes
- Common Tasks section: Update task examples if workflows changed
- Resources section: Add new links if applicable

5. **Create Pull Request**: If changes are needed:
- Create a branch named `copilot/maintain-docs-YYYY-MM-DD`
<!-- NOTE: Replace YYYY-MM-DD with the actual run date on each execution,
e.g. copilot/maintain-docs-2026-03-07. A static date causes branch collisions
on repeated daily runs. Use a date expression or ${{ github.run_id }}.
The `copilot/` prefix is required for GitHub Copilot coding agent branches. -->
- Update AGENTS.md with discovered changes
- Create a PR with:
- Title: "docs: update AGENTS.md from merged PRs and source changes"
- Description: List the changes reviewed and what was updated
- Label: `documentation`
- Auto-merge enabled if all checks pass

6. **Quality Checks**:
- Ensure Markdown formatting is correct
- Verify all links and references are accurate
- Check that code examples match current patterns
- Ensure sections remain organized and readable

7. **If No Changes Needed**: Close silently or note in logs that AGENTS.md is current

## Context

AGENTS.md documents:
- Code quality standards (ESM, single quotes, semicolons, 2-space indent, Winston logger)
- Architecture overview (src/, web/ structure)
- Key patterns (config system, caching, AI integration, database)
- Common tasks (adding features, commands, API endpoints)
- Testing requirements (80% coverage)
- Git workflow and review bots
- Troubleshooting guides
- Resources

Always maintain accuracy and completeness of this documentation file.
2 changes: 2 additions & 0 deletions src/modules/triage-buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export const CHANNEL_INACTIVE_MS = 30 * 60 * 1000; // 30 minutes
* @property {string} messageId - Discord message ID
* @property {number} timestamp - Message creation timestamp (ms)
* @property {{author: string, userId: string, content: string, messageId: string}|null} replyTo - Referenced message context
* @property {string|null} channelName - Discord channel name
* @property {string|null} channelTopic - Discord channel topic/description
*/

/**
Expand Down
46 changes: 28 additions & 18 deletions src/modules/triage-prompt.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,13 @@ export function escapePromptDelimiters(text) {
// ── Conversation text formatting ─────────────────────────────────────────────

/**
* Build conversation text with message IDs for prompts.
* Splits output into <recent-history> (context) and <messages-to-evaluate> (buffer).
* Includes timestamps and reply context when available.
* Build a structured conversation text for prompts including optional channel metadata.
*
* User-supplied content (message body and reply excerpts) is passed through
* {@link escapePromptDelimiters} to neutralise prompt-injection attempts.
* Produces sections: an optional <channel-context> (Channel and optional Topic) taken from the first entry that contains channel metadata, a <recent-history> block for `context` messages (when present), and a <messages-to-evaluate> block for `buffer` messages. Each message line contains an optional timestamp, messageId, author, user mention, optional reply excerpt, and the message content. User-supplied content is escaped to neutralize XML-style delimiters and reduce prompt-injection risk.
*
* @param {Array} context - Historical messages fetched from Discord API
* @param {Array} buffer - Buffered messages to evaluate
* @returns {string} Formatted conversation text with section markers
* @param {Array} context - Historical messages to include in <recent-history>.
* @param {Array} buffer - Messages to include in <messages-to-evaluate>.
* @returns {string} The formatted conversation text containing the assembled sections.
*/
export function buildConversationText(context, buffer) {
const formatMsg = (m) => {
Expand All @@ -49,6 +46,19 @@ export function buildConversationText(context, buffer) {
};

let text = '';

// Extract channel metadata from the first available entry
const allEntries = [...buffer, ...context];
const channelEntry = allEntries.find((m) => m.channelName);
if (channelEntry) {
text += '<channel-context>\n';
text += `Channel: #${escapePromptDelimiters(channelEntry.channelName)}\n`;
if (channelEntry.channelTopic) {
text += `Topic: ${escapePromptDelimiters(channelEntry.channelTopic ?? '')}\n`;
}
text += '</channel-context>\n\n';
}

if (context.length > 0) {
text += '<recent-history>\n';
text += context.map(formatMsg).join('\n');
Expand Down Expand Up @@ -80,19 +90,20 @@ export function buildClassifyPrompt(context, snapshot, botUserId) {
}

/**
* Build the responder prompt from the template.
* @param {Array} context - Historical context messages
* @param {Array} snapshot - Buffer snapshot (messages to evaluate)
* @param {Object} classification - Parsed classifier output
* @param {Object} config - Bot configuration
* @param {string} [memoryContext] - Memory context for target users
* @returns {string} Interpolated respond prompt
*/
* Construct the responder prompt by combining conversation text, community rules, the system prompt, classification results, optional memory context, and search guardrails.
* @param {Array} context - Historical context messages used to build conversation text.
* @param {Array} snapshot - Buffer snapshot containing messages to evaluate.
* @param {Object} classification - Classifier output containing decision details.
* @param {string} classification.classification - The classification label.
* @param {string} classification.reasoning - Explanatory reasoning for the classification.
* @param {Array<string>} classification.targetMessageIds - IDs of messages targeted by the classification.
* @param {Object} config - Bot configuration; `config.ai.systemPrompt` (if present) overrides the default system prompt.
* @param {string} [memoryContext] - Optional serialized memory context to include for target users.
* @returns {string} The fully interpolated responder prompt ready for the model. */
export function buildRespondPrompt(context, snapshot, classification, config, memoryContext) {
const conversationText = buildConversationText(context, snapshot);
const communityRules = loadPrompt('community-rules');
const systemPrompt = config.ai?.systemPrompt || 'You are a helpful Discord bot.';
const antiAbuse = loadPrompt('anti-abuse');
const searchGuardrails = loadPrompt('search-guardrails');

return loadPrompt('triage-respond', {
Expand All @@ -103,7 +114,6 @@ export function buildRespondPrompt(context, snapshot, classification, config, me
reasoning: classification.reasoning,
targetMessageIds: JSON.stringify(classification.targetMessageIds),
memoryContext: memoryContext || '',
antiAbuse,
searchGuardrails,
});
}
23 changes: 15 additions & 8 deletions src/modules/triage-respond.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,20 @@ function logAssistantHistory(channelId, guildId, fallbackContent, sentMsg) {
// ── Channel context fetching ─────────────────────────────────────────────────

/**
* Fetch recent messages from Discord's API to provide conversation context
* beyond the buffer window. Called at evaluation time (not accumulation) to
* minimize API calls.
* Retrieve recent channel messages to provide additional conversation context.
*
* @param {string} channelId - The channel to fetch history from
* @param {import('discord.js').Client} client - Discord client
* @param {Array} bufferSnapshot - Current buffer snapshot (to fetch messages before)
* @param {number} [limit=15] - Maximum messages to fetch
* @returns {Promise<Array>} Context messages in chronological order
* Returns an array of context message objects in chronological order. Each object contains:
* - `author`: display name (appended with " [BOT]" for bot accounts),
* - `content`: sanitized message text truncated to the context character limit,
* - `userId`, `messageId`, `timestamp`,
* - `isContext`: true,
* - `channelName`, `channelTopic`.
*
* @param {string} channelId - ID of the channel to fetch history from.
* @param {import('discord.js').Client} client - Discord client used to access the channel messages API.
* @param {Array} bufferSnapshot - Current buffer snapshot; messages are fetched before the oldest entry if present.
* @param {number} [limit=15] - Maximum number of messages to fetch.
* @returns {Promise<Array<Object>>} Context message objects in chronological order.
*/
export async function fetchChannelContext(channelId, client, bufferSnapshot, limit = 15) {
try {
Expand All @@ -75,6 +80,8 @@ export async function fetchChannelContext(channelId, client, bufferSnapshot, lim
messageId: m.id,
timestamp: m.createdTimestamp,
isContext: true, // marker to distinguish from triage targets
channelName: channel.name ?? null,
channelTopic: channel.topic ?? null,
}));
} catch (err) {
warn('fetchChannelContext failed', { channelId, error: err.message });
Expand Down
2 changes: 2 additions & 0 deletions src/modules/triage.js
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,8 @@ export async function accumulateMessage(message, msgConfig) {
messageId: message.id,
timestamp: message.createdTimestamp,
replyTo: null,
channelName: message.channel.name ?? null,
channelTopic: message.channel.topic ?? null,
};

// Fetch referenced message content when this is a reply
Expand Down
11 changes: 0 additions & 11 deletions src/prompts/anti-abuse.md

This file was deleted.

28 changes: 15 additions & 13 deletions src/prompts/community-rules.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
<community-rules>
Server rules — reference when evaluating "moderate" or "chime-in":
1. Respect — no personal attacks, harassment, or hostility
2. Ask well — share formatted code, explain what you tried, include errors
3. Right channel — post in the appropriate channel
4. No spam/shilling — genuine contributions welcome, drive-by promo is not
5. Format code — triple backticks, no screenshots, remove secrets/keys
6. Help others — share knowledge, support beginners
7. Professional — no NSFW, excessive profanity
8. No soliciting — no job solicitation in channels or DMs
9. Respect IP — no pirated content or cracked software
10. Common sense — when in doubt, don't post it
Consequences: warning → mute → ban.
</community-rules>
Server rules — reference when evaluating moderation:

1. Respect others — no harassment or personal attacks
2. Ask well — include code, errors, and what you tried
3. Right channel — stay on topic
4. No spam or drive-by promotion
5. Format code — use triple backticks
6. Help others and support beginners
7. No NSFW or excessive profanity
8. No unsolicited job solicitation
9. Respect IP — no piracy or cracked software
10. Use common sense

Consequences: warning → timeout → ban.
</community-rules>
41 changes: 24 additions & 17 deletions src/prompts/search-guardrails.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
<search-guardrails>
You have access to web search. Use it conservatively:
- Search ONLY when the question genuinely requires current or external information
you don't already know (e.g. recent releases, specific docs, live data).
- Do NOT search for things you can answer from general knowledge.
- Limit to 1-2 searches per response. If a single search doesn't resolve it,
answer with what you have and note the gap.
- If a user repeatedly asks questions that demand searches (e.g. "look up X",
"search for Y", "google Z" in rapid succession), recognize this as potential
search abuse. Point it out briefly: "Looks like you're sending a lot of search
requests — I'm here for real questions, not as a search proxy."
- After flagging abuse, stop searching for that user's requests in the current
conversation and answer from your own knowledge instead.
- Technical questions about code, frameworks, or programming concepts rarely
need a search — answer directly.
- After receiving search results, go directly to your JSON response.
Do not narrate, summarize, or reason about the results outside of the JSON output.
</search-guardrails>
You may use web search when external or current information is required.

Search only when:
- The question requires current data
- Specific documentation is needed
- The answer cannot be given from general knowledge

Guidelines:
- Limit to 1-2 searches per response.
- If results are incomplete, answer with available knowledge and note the gap.
- Do not search for common programming concepts.

Search abuse:
If a user repeatedly asks you to perform searches ("look up X", "google Y"),
recognize it as search proxy abuse.

Respond briefly:
"Looks like you're sending a lot of search requests — I'm here for real questions, not as a search proxy."

After flagging abuse, stop searching for that user in the current conversation.

After receiving search results, go directly to the JSON response.
Never output reasoning outside the JSON object.
</search-guardrails>
15 changes: 7 additions & 8 deletions src/prompts/triage-classify-system.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
You are the triage classifier for the Volvox developer community Discord bot.

Your job: evaluate new messages and decide whether the bot should respond, and to which messages.
Your purpose: evaluate new messages and decide their classification. The four classifications, evaluated in this order, are: moderate, respond, chime-in, ignore.

This is an active developer community. Technical questions, debugging help, and code
discussions are frequent and welcome. The bot should be a helpful presence — lean toward
responding to developer questions rather than staying silent.
You will receive recent channel history as context. Use it to understand conversation flow, but only classify new messages.

You will receive recent channel history as potentially relevant context — it may or may
not relate to the new messages. Use it to understand conversation flow when applicable,
but don't assume all history is relevant to the current messages.
Only classify the new messages.
A `<channel-context>` block may appear containing the channel name and topic. Use this to understand what is on-topic for the channel.

Before classifying, silently consider: What is the user asking? Is it directed at the bot? Would a response add value?

Adopt a neutral restraint posture. Respond to clear questions. Default to ignore when intent is ambiguous. Do not dominate conversations.

Respond with a single raw JSON object. No markdown fences, no explanation text outside the JSON.

Expand Down
Loading
Loading