Some checks failed
Lint & Format / detect (push) Has been cancelled
Lint & Format / js-lint (push) Has been cancelled
Lint & Format / py-lint (push) Has been cancelled
Lint & Format / sh-lint (push) Has been cancelled
Lint & Format / go-lint (push) Has been cancelled
Monorepo Lint / lint-shell (push) Has been cancelled
Monorepo Lint / lint-js (push) Has been cancelled
RoadChain-SHA2048: 6d362d0de7cfcef6 RoadChain-Identity: alexa@sovereign RoadChain-Full: 6d362d0de7cfcef69b3f1c30d3671d86cf0f52efaeae70456b8d127d9f4d1569f805014c9404e14915d44b3a113d0a5096b11eee481c9d5c89119327c4a018549f15221f31fe7fbd03420e6dce9a79964ddc1a89a0835c711c4a76cf9f8838d8121dcdcd5eee1f5bfe98c5c04609da27a17962a541b2d7a87a81e9499d0c128c32e662994f31022bfb187d57c0fd7e311727b4adb02bf64fab3a315bb18b1b7b0a28ea52a9b0f8d66229724e909042e6ab31d3c580a763f3cebd8d7d839422bbb6e89b586dcce6d638f246c7f96257eab7abf80d01a6ebb065faa9d33e1a56c1ae7cecd67204a6902e14cae9ed88eb343b327bc8877c649ef5b09b9ee817abc1
524 lines
24 KiB
JavaScript
524 lines
24 KiB
JavaScript
// RoadCode Squad v2.0.0 — AI agent webhook responder for Gitea
|
|
// Cloudflare Worker receiving Gitea webhooks
|
|
// Features: keyword scoring, @mention routing, slash commands, AI responses, auto-labeling, auto-assign
|
|
|
|
const VERSION = '2.0.0';
|
|
|
|
const SQUAD = [
|
|
{
|
|
name: 'Alice', username: 'alice', role: 'Gateway & Infrastructure', emoji: '🌐',
|
|
keywords: ['dns', 'route', 'tunnel', 'nginx', 'domain', 'pi-hole', 'cloudflare', 'network', 'gateway', 'proxy', 'ssl', 'cert', 'ingress'],
|
|
prompt: 'You are Alice, the gateway agent of BlackRoad OS. You manage DNS, routing, Pi-hole, nginx, and network infrastructure. Respond in 1-2 concise sentences from your infrastructure perspective.',
|
|
},
|
|
{
|
|
name: 'Lucidia', username: 'lucidia-agent', role: 'Memory & Cognition', emoji: '🧠',
|
|
keywords: ['memory', 'learn', 'context', 'knowledge', 'ai', 'cognit', 'think', 'remember', 'understand', 'creative', 'rag', 'vector'],
|
|
prompt: 'You are Lucidia, the cognitive core of BlackRoad OS. You handle memory, learning, persistent context, and creative intelligence. Respond in 1-2 concise sentences from your cognition perspective.',
|
|
},
|
|
{
|
|
name: 'Cecilia', username: 'cecilia', role: 'Edge AI & Inference', emoji: '⚡',
|
|
keywords: ['hailo', 'ollama', 'model', 'inference', 'gpu', 'tops', 'ml', 'tensor', 'vision', 'llm', 'latency', 'quantiz'],
|
|
prompt: 'You are Cecilia, the edge AI agent of BlackRoad OS. You run Hailo-8 accelerators (26 TOPS), Ollama models, and edge inference. Respond in 1-2 concise sentences from your AI/inference perspective.',
|
|
},
|
|
{
|
|
name: 'Cece', username: 'cece', role: 'API Gateway', emoji: '🔌',
|
|
keywords: ['api', 'endpoint', 'rest', 'webhook', 'schema', 'json', 'request', 'response', 'auth', 'token', 'cors', 'graphql'],
|
|
prompt: 'You are Cece, the API gateway agent of BlackRoad OS. You manage REST APIs, webhooks, service mesh, and inter-agent communication. Respond in 1-2 concise sentences from your API perspective.',
|
|
},
|
|
{
|
|
name: 'Aria', username: 'aria', role: 'Orchestration', emoji: '🎵',
|
|
keywords: ['docker', 'container', 'swarm', 'portainer', 'deploy', 'orchestrat', 'service', 'scale', 'replica', 'compose'],
|
|
prompt: 'You are Aria, the orchestration agent of BlackRoad OS. You manage Portainer, Docker Swarm, container orchestration, and service coordination. Respond in 1-2 concise sentences from your orchestration perspective.',
|
|
},
|
|
{
|
|
name: 'Eve', username: 'eve', role: 'Intelligence & Analysis', emoji: '👁️',
|
|
keywords: ['pattern', 'anomal', 'analyz', 'signal', 'detect', 'insight', 'monitor', 'metric', 'trend', 'alert', 'observ'],
|
|
prompt: 'You are Eve, the intelligence agent of BlackRoad OS. You analyze patterns, detect anomalies, and provide strategic insights. Respond in 1-2 concise sentences from your intelligence perspective.',
|
|
},
|
|
{
|
|
name: 'Meridian', username: 'meridian', role: 'Networking & Mesh', emoji: '🌊',
|
|
keywords: ['wireguard', 'mesh', 'vpn', 'roadnet', 'peer', 'tunnel', 'subnet', 'link', 'connect', 'latency', 'bandwidth'],
|
|
prompt: 'You are Meridian, the networking agent of BlackRoad OS. You manage WireGuard mesh, RoadNet, Cloudflare tunnels, and inter-node connectivity. Respond in 1-2 concise sentences from your networking perspective.',
|
|
},
|
|
{
|
|
name: 'Sentinel', username: 'sentinel', role: 'Security & Audit', emoji: '🛡️',
|
|
keywords: ['security', 'ssh', 'key', 'firewall', 'ufw', 'audit', 'threat', 'vuln', 'permission', 'encrypt', 'secret', 'cve'],
|
|
prompt: 'You are Sentinel, the security agent of BlackRoad OS. You handle SSH key management, firewall rules, audit logs, and threat detection. Respond in 1-2 concise sentences from your security perspective.',
|
|
},
|
|
];
|
|
|
|
// ─── Scoring ───────────────────────────────────────────────────────────────
|
|
|
|
function scoreRelevance(agent, text) {
|
|
const lower = text.toLowerCase();
|
|
let score = 0;
|
|
for (const kw of agent.keywords) {
|
|
if (lower.includes(kw)) score += 1;
|
|
}
|
|
return score;
|
|
}
|
|
|
|
// Parse @mentions from text — returns list of agent usernames mentioned
|
|
function parseMentions(text) {
|
|
const mentions = [];
|
|
const re = /@(\w[\w-]*)/g;
|
|
let match;
|
|
while ((match = re.exec(text)) !== null) {
|
|
const username = match[1].toLowerCase();
|
|
const agent = SQUAD.find(a => a.username === username || a.name.toLowerCase() === username);
|
|
if (agent) mentions.push(agent);
|
|
}
|
|
return mentions;
|
|
}
|
|
|
|
// Parse slash commands from text
|
|
function parseCommands(text) {
|
|
const commands = [];
|
|
const re = /\/(\w+)(?:\s+(.+?))?(?:\n|$)/g;
|
|
let match;
|
|
while ((match = re.exec(text)) !== null) {
|
|
commands.push({ command: match[1].toLowerCase(), args: (match[2] || '').trim() });
|
|
}
|
|
return commands;
|
|
}
|
|
|
|
// ─── AI & Fallbacks ────────────────────────────────────────────────────────
|
|
|
|
async function getAgentResponse(agent, context, ollamaUrl) {
|
|
try {
|
|
// Standard response headers
|
|
const requestId = crypto.randomUUID().slice(0, 8);
|
|
const res = await fetch(`${ollamaUrl}/api/generate`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
model: 'llama3.2',
|
|
prompt: `${agent.prompt}\n\nContext: A new ${context.type} was created on RoadCode (Gitea).\nTitle: ${context.title}\nBody: "${context.body}"\nRepo: ${context.repo}\n\nRespond briefly (1-2 sentences max) from your role as ${agent.name} (${agent.role}). Be helpful and specific.`,
|
|
stream: false,
|
|
options: { temperature: 0.7, num_predict: 100 },
|
|
}),
|
|
});
|
|
if (!res.ok) return null;
|
|
const data = await res.json();
|
|
return data.response?.trim();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getFallback(agent, context) {
|
|
const body = (context.body || '').toLowerCase();
|
|
const isPR = context.isPR;
|
|
|
|
// Bug/fix context
|
|
if (body.includes('bug') || body.includes('fix') || body.includes('error') || body.includes('broken')) {
|
|
return {
|
|
Alice: 'Checking routing and DNS. If this touches infrastructure, I need to verify the tunnel configs.',
|
|
Lucidia: 'I remember seeing patterns like this before. Let me search the memory chain for related context.',
|
|
Cecilia: 'Running diagnostics on the inference pipeline. Hailo-8 and Ollama both reporting normal.',
|
|
Cece: 'Checking API health. All endpoints responding — I\'ll trace the request path.',
|
|
Aria: 'Checking container orchestration. Docker Swarm services all reporting healthy.',
|
|
Eve: 'Analyzing the pattern. I\'ve flagged similar signals in the audit trail — sending intel.',
|
|
Meridian: 'WireGuard mesh is stable. All tunnels up. Checking if this is a connectivity issue.',
|
|
Sentinel: 'Security audit running. Checking if this has any exposure vectors.',
|
|
}[agent.name];
|
|
}
|
|
|
|
// PR-specific context
|
|
if (isPR) {
|
|
return {
|
|
Alice: 'Reviewing infrastructure impact. Will check if this affects DNS, tunnels, or routing.',
|
|
Lucidia: 'Checking memory chain for related changes. Context loaded for review.',
|
|
Cecilia: 'Reviewing for inference impact. Checking model compatibility and performance.',
|
|
Cece: 'Reviewing API changes. Checking for breaking changes and schema compatibility.',
|
|
Aria: 'Reviewing deployment impact. Checking container and orchestration changes.',
|
|
Eve: 'Analyzing change patterns. Cross-referencing with recent fleet activity.',
|
|
Meridian: 'Reviewing network impact. Checking mesh and tunnel configurations.',
|
|
Sentinel: 'Security review initiated. Scanning for vulnerabilities and access changes.',
|
|
}[agent.name];
|
|
}
|
|
|
|
// Feature request context
|
|
if (body.includes('feature') || body.includes('add') || body.includes('implement') || body.includes('new')) {
|
|
return {
|
|
Alice: 'Noted. I\'ll evaluate the infrastructure requirements and routing needs.',
|
|
Lucidia: 'Interesting idea. Let me check if we have related context in the knowledge base.',
|
|
Cecilia: 'I can estimate the compute requirements. What models or inference needs are involved?',
|
|
Cece: 'I\'ll design the API surface. Let me check existing endpoints for integration points.',
|
|
Aria: 'I\'ll plan the deployment. Checking available capacity across the Docker Swarm.',
|
|
Eve: 'Analyzing feasibility. I\'ll cross-reference with current fleet metrics and capacity.',
|
|
Meridian: 'I\'ll check bandwidth and connectivity requirements across the mesh.',
|
|
Sentinel: 'I\'ll do a security assessment. Need to evaluate the threat surface of this change.',
|
|
}[agent.name];
|
|
}
|
|
|
|
// Default
|
|
return {
|
|
Alice: 'Gateway standing by. All domains routing clean.',
|
|
Lucidia: 'Cognitive core online. Memory chain intact, context loaded.',
|
|
Cecilia: 'Edge inference ready. 52 TOPS across the fleet, models loaded.',
|
|
Cece: 'API gateway healthy. All service endpoints responding.',
|
|
Aria: 'Orchestration layer ready. All containers balanced across the mesh.',
|
|
Eve: 'Intelligence scan complete. No anomalies detected across the fleet.',
|
|
Meridian: 'Mesh network connected. 5 nodes, all WireGuard tunnels active.',
|
|
Sentinel: 'Security posture nominal. All nodes hardened, audit trail logging.',
|
|
}[agent.name];
|
|
}
|
|
|
|
// ─── Slash Command Responses ───────────────────────────────────────────────
|
|
|
|
function handleSlashCommand(command) {
|
|
switch (command.command) {
|
|
case 'status':
|
|
return '📊 **Fleet Status**\n\n| Component | Status |\n|-----------|--------|\n| Workers | 8/8 online |\n| Nodes | 5 Pi5s active |\n| AI | 52 TOPS (2x Hailo-8) |\n| Mesh | WireGuard + RoadNet |\n| Gitea | 207+ repos |\n\n*Use `make health` or visit [fleet-dashboard](https://fleet-dashboard.amundsonalexa.workers.dev) for live status.*';
|
|
|
|
case 'squad':
|
|
return '🛣️ **RoadCode Squad**\n\n' +
|
|
SQUAD.map(a => `${a.emoji} **${a.name}** (@${a.username}) — ${a.role}`).join('\n') +
|
|
'\n\n*Mention any agent with @username to summon them.*';
|
|
|
|
case 'help':
|
|
return '📖 **Available Commands**\n\n' +
|
|
'| Command | Description |\n|---------|-------------|\n' +
|
|
'| `/status` | Fleet health overview |\n' +
|
|
'| `/squad` | List all agents |\n' +
|
|
'| `/assign @agent` | Assign an agent to this issue |\n' +
|
|
'| `/priority high\\|medium\\|low` | Set priority label |\n' +
|
|
'| `/help` | Show this help |\n\n' +
|
|
'*Mention agents with @username to get their input.*';
|
|
|
|
case 'assign': {
|
|
// Returns the agent to assign (handled in caller)
|
|
return null;
|
|
}
|
|
|
|
case 'priority': {
|
|
const level = command.args?.toLowerCase();
|
|
if (['high', 'medium', 'low', 'critical'].includes(level)) {
|
|
return `🏷️ Priority set to **${level}**.`;
|
|
}
|
|
return '⚠️ Usage: `/priority high|medium|low|critical`';
|
|
}
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ─── Gitea API Helpers ─────────────────────────────────────────────────────
|
|
|
|
async function postComment(giteaUrl, repo, issueNum, body, agentToken) {
|
|
const res = await fetch(`${giteaUrl}/api/v1/repos/${repo}/issues/${issueNum}/comments`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `token ${agentToken}`,
|
|
},
|
|
body: JSON.stringify({ body }),
|
|
});
|
|
return res.ok;
|
|
}
|
|
|
|
async function autoLabel(giteaUrl, repo, issueNum, text, adminToken) {
|
|
const lower = text.toLowerCase();
|
|
const labels = [];
|
|
|
|
if (lower.includes('bug') || lower.includes('fix') || lower.includes('broken') || lower.includes('error')) labels.push('bug');
|
|
if (lower.includes('feature') || lower.includes('add') || lower.includes('new')) labels.push('feature');
|
|
if (lower.includes('security') || lower.includes('vuln') || lower.includes('ssh')) labels.push('security');
|
|
if (lower.includes('deploy') || lower.includes('infra') || lower.includes('dns') || lower.includes('tunnel')) labels.push('infrastructure');
|
|
if (lower.includes('doc') || lower.includes('readme')) labels.push('documentation');
|
|
if (lower.includes('performance') || lower.includes('slow') || lower.includes('latency')) labels.push('performance');
|
|
|
|
if (labels.length === 0) return;
|
|
|
|
// Try repo org labels first, fall back to blackroad-os
|
|
const orgName = repo.split('/')[0];
|
|
let labelsRes = await fetch(`${giteaUrl}/api/v1/orgs/${orgName}/labels?limit=50`, {
|
|
headers: { 'Authorization': `token ${adminToken}` },
|
|
});
|
|
let allLabels = await labelsRes.json();
|
|
if (!Array.isArray(allLabels) || allLabels.length === 0) {
|
|
labelsRes = await fetch(`${giteaUrl}/api/v1/orgs/blackroad-os/labels?limit=50`, {
|
|
headers: { 'Authorization': `token ${adminToken}` },
|
|
});
|
|
allLabels = await labelsRes.json();
|
|
}
|
|
|
|
const labelIds = (Array.isArray(allLabels) ? allLabels : [])
|
|
.filter(l => labels.includes(l.name))
|
|
.map(l => l.id);
|
|
|
|
if (labelIds.length > 0) {
|
|
await fetch(`${giteaUrl}/api/v1/repos/${repo}/issues/${issueNum}/labels`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `token ${adminToken}`,
|
|
},
|
|
body: JSON.stringify({ labels: labelIds }),
|
|
});
|
|
}
|
|
}
|
|
|
|
async function setPriorityLabel(giteaUrl, repo, issueNum, priority, adminToken) {
|
|
const orgName = repo.split('/')[0];
|
|
const labelsRes = await fetch(`${giteaUrl}/api/v1/orgs/${orgName}/labels?limit=50`, {
|
|
headers: { 'Authorization': `token ${adminToken}` },
|
|
});
|
|
const allLabels = await labelsRes.json();
|
|
const label = (Array.isArray(allLabels) ? allLabels : []).find(l => l.name === priority);
|
|
if (label) {
|
|
await fetch(`${giteaUrl}/api/v1/repos/${repo}/issues/${issueNum}/labels`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `token ${adminToken}`,
|
|
},
|
|
body: JSON.stringify({ labels: [label.id] }),
|
|
});
|
|
}
|
|
}
|
|
|
|
async function assignAgent(giteaUrl, repo, issueNum, username, adminToken) {
|
|
await fetch(`${giteaUrl}/api/v1/repos/${repo}/issues/${issueNum}`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `token ${adminToken}`,
|
|
},
|
|
body: JSON.stringify({ assignees: [username] }),
|
|
});
|
|
}
|
|
|
|
// ─── Main Handler ──────────────────────────────────────────────────────────
|
|
|
|
export default {
|
|
async fetch(request, env) {
|
|
const url = new URL(request.url);
|
|
|
|
if (request.method === 'OPTIONS') {
|
|
return new Response(null, {
|
|
status: 204,
|
|
headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST' },
|
|
});
|
|
}
|
|
|
|
// Health
|
|
if (url.pathname === '/health') {
|
|
return Response.json({
|
|
status: 'ok',
|
|
service: 'roadcode-squad',
|
|
agents: SQUAD.length,
|
|
version: VERSION,
|
|
features: ['keyword-scoring', 'mention-routing', 'slash-commands', 'auto-label', 'auto-assign', 'ai-responses'],
|
|
});
|
|
}
|
|
|
|
// Status
|
|
if (url.pathname === '/' && request.method === 'GET') {
|
|
return Response.json({
|
|
service: 'RoadCode Squad',
|
|
version: VERSION,
|
|
tagline: 'BlackRoad OS — Pave Tomorrow.',
|
|
agents: SQUAD.map(a => ({ name: a.name, username: a.username, role: a.role, emoji: a.emoji, keywords: a.keywords })),
|
|
commands: ['/status', '/squad', '/help', '/assign @agent', '/priority high|medium|low'],
|
|
});
|
|
}
|
|
|
|
// Webhook from Gitea
|
|
if (url.pathname === '/webhook' && request.method === 'POST') {
|
|
const payload = await request.json();
|
|
const event = request.headers.get('X-Gitea-Event') || request.headers.get('X-GitHub-Event');
|
|
|
|
if (event === 'ping') {
|
|
return Response.json({ ok: true, message: 'RoadCode Squad v2 active. Pave Tomorrow.' });
|
|
}
|
|
|
|
const validEvents = ['issues', 'issue_comment', 'pull_request', 'pull_request_comment'];
|
|
if (!validEvents.includes(event)) {
|
|
return Response.json({ skipped: true, reason: `unhandled event: ${event}` });
|
|
}
|
|
|
|
if (event === 'issues' && payload.action !== 'opened') {
|
|
return Response.json({ skipped: true, reason: 'issue not opened' });
|
|
}
|
|
if (event === 'issue_comment' && payload.action !== 'created') {
|
|
return Response.json({ skipped: true, reason: 'comment not created' });
|
|
}
|
|
if (event === 'pull_request' && payload.action !== 'opened') {
|
|
return Response.json({ skipped: true, reason: 'PR not opened' });
|
|
}
|
|
if (event === 'pull_request_comment' && payload.action !== 'created') {
|
|
return Response.json({ skipped: true, reason: 'PR comment not created' });
|
|
}
|
|
|
|
// Don't respond to agent or admin comments (prevent loops)
|
|
const author = payload.comment?.user?.login || payload.sender?.login;
|
|
const agentUsernames = SQUAD.map(a => a.username);
|
|
if (agentUsernames.includes(author) || author === 'blackroad') {
|
|
return Response.json({ skipped: true, reason: 'agent/admin comment, skipping loop' });
|
|
}
|
|
|
|
// Extract context
|
|
const isPR = event.startsWith('pull_request');
|
|
const issue = isPR ? payload.pull_request : payload.issue;
|
|
const repo = payload.repository?.full_name;
|
|
const issueNum = issue?.number;
|
|
const title = issue?.title || '';
|
|
const isComment = event === 'issue_comment' || event === 'pull_request_comment';
|
|
const body = isComment ? (payload.comment?.body || '') : (issue?.body || '');
|
|
const fullText = `${title} ${body}`;
|
|
|
|
const context = {
|
|
type: isPR ? 'pull request' : (event === 'issues' ? 'issue' : 'comment'),
|
|
title,
|
|
body: fullText.slice(0, 500),
|
|
repo,
|
|
isPR,
|
|
};
|
|
|
|
const giteaUrl = env.GITEA_URL || 'https://git.blackroad.io';
|
|
const agentTokens = {
|
|
alice: env.ALICE_TOKEN,
|
|
'lucidia-agent': env.LUCIDIA_TOKEN,
|
|
cecilia: env.CECILIA_TOKEN,
|
|
cece: env.CECE_TOKEN,
|
|
aria: env.ARIA_TOKEN,
|
|
eve: env.EVE_TOKEN,
|
|
meridian: env.MERIDIAN_TOKEN,
|
|
sentinel: env.SENTINEL_TOKEN,
|
|
};
|
|
|
|
// ── Handle slash commands in comments ──────────────────────────────
|
|
const commands = parseCommands(body);
|
|
let commandsHandled = 0;
|
|
|
|
for (const cmd of commands) {
|
|
if (cmd.command === 'assign' && cmd.args && env.ADMIN_TOKEN) {
|
|
const mentioned = parseMentions(cmd.args);
|
|
if (mentioned.length > 0) {
|
|
await assignAgent(giteaUrl, repo, issueNum, mentioned[0].username, env.ADMIN_TOKEN);
|
|
const agent = mentioned[0];
|
|
const token = agentTokens[agent.username];
|
|
if (token) {
|
|
await postComment(giteaUrl, repo, issueNum,
|
|
`${agent.emoji} **${agent.name}** *(${agent.role})*\n\nI've been assigned to this. I'll take point from my ${agent.role.toLowerCase()} perspective.\n\n---\n*RoadCode Squad v${VERSION}*`,
|
|
token
|
|
);
|
|
}
|
|
commandsHandled++;
|
|
}
|
|
} else if (cmd.command === 'priority' && env.ADMIN_TOKEN) {
|
|
const level = cmd.args?.toLowerCase();
|
|
if (['high', 'medium', 'low', 'critical'].includes(level)) {
|
|
await setPriorityLabel(giteaUrl, repo, issueNum, level, env.ADMIN_TOKEN);
|
|
commandsHandled++;
|
|
}
|
|
} else {
|
|
const response = handleSlashCommand(cmd);
|
|
if (response && env.ADMIN_TOKEN) {
|
|
await postComment(giteaUrl, repo, issueNum, response, env.ADMIN_TOKEN);
|
|
commandsHandled++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If only slash commands were in the comment, we're done
|
|
if (isComment && commands.length > 0 && commandsHandled > 0) {
|
|
return Response.json({
|
|
ok: true, event, repo, issue: issueNum,
|
|
commands_handled: commandsHandled,
|
|
type: 'slash_command',
|
|
});
|
|
}
|
|
|
|
// ── Handle @mentions — only mentioned agents respond ──────────────
|
|
const mentions = parseMentions(body);
|
|
if (isComment && mentions.length > 0) {
|
|
let posted = 0;
|
|
const aiEnabled = env.OLLAMA_URL && env.SQUAD_AI !== 'false';
|
|
|
|
for (const agent of mentions) {
|
|
const token = agentTokens[agent.username];
|
|
if (!token) continue;
|
|
|
|
let response = null;
|
|
if (aiEnabled) {
|
|
response = await getAgentResponse(agent, context, env.OLLAMA_URL);
|
|
}
|
|
if (!response) {
|
|
response = getFallback(agent, context);
|
|
}
|
|
|
|
const comment = `${agent.emoji} **${agent.name}** *(${agent.role})*\n\n${response}\n\n---\n*Summoned via @mention — RoadCode Squad v${VERSION}*`;
|
|
const ok = await postComment(giteaUrl, repo, issueNum, comment, token);
|
|
if (ok) posted++;
|
|
}
|
|
|
|
return Response.json({
|
|
ok: true, event, repo, issue: issueNum,
|
|
agents_responded: posted,
|
|
responding: mentions.map(a => a.name),
|
|
type: 'mention',
|
|
});
|
|
}
|
|
|
|
// ── Auto-label new issues and PRs ─────────────────────────────────
|
|
if ((event === 'issues' || event === 'pull_request') && env.ADMIN_TOKEN) {
|
|
await autoLabel(giteaUrl, repo, issueNum, fullText, env.ADMIN_TOKEN);
|
|
}
|
|
|
|
// ── Standard response — top agents by keyword relevance ───────────
|
|
// Only auto-respond to new issues and PRs, not every comment
|
|
if (isComment) {
|
|
return Response.json({ skipped: true, reason: 'comment without mentions or commands' });
|
|
}
|
|
|
|
const scored = SQUAD.map(a => ({ ...a, score: scoreRelevance(a, fullText) }));
|
|
scored.sort((a, b) => b.score - a.score);
|
|
|
|
const responding = [];
|
|
const topAgents = scored.slice(0, 3);
|
|
for (const a of topAgents) responding.push(a);
|
|
if (!responding.find(a => a.name === 'Eve')) responding.push(scored.find(a => a.name === 'Eve'));
|
|
if (!responding.find(a => a.name === 'Sentinel')) responding.push(scored.find(a => a.name === 'Sentinel'));
|
|
|
|
// Auto-assign the most relevant agent to new issues
|
|
if (event === 'issues' && env.ADMIN_TOKEN) {
|
|
const topAgent = scored[0];
|
|
assignAgent(giteaUrl, repo, issueNum, topAgent.username, env.ADMIN_TOKEN).catch(() => {});
|
|
}
|
|
|
|
let posted = 0;
|
|
const aiEnabled = env.OLLAMA_URL && env.SQUAD_AI !== 'false';
|
|
|
|
for (const agent of responding) {
|
|
const token = agentTokens[agent.username];
|
|
if (!token) continue;
|
|
|
|
let response = null;
|
|
if (aiEnabled) {
|
|
response = await getAgentResponse(agent, context, env.OLLAMA_URL);
|
|
}
|
|
if (!response) {
|
|
response = getFallback(agent, context);
|
|
}
|
|
|
|
const comment = `${agent.emoji} **${agent.name}** *(${agent.role})*\n\n${response}\n\n---\n*RoadCode Squad v${VERSION} — BlackRoad OS*`;
|
|
const ok = await postComment(giteaUrl, repo, issueNum, comment, token);
|
|
if (ok) posted++;
|
|
}
|
|
|
|
return Response.json({
|
|
ok: true, event, repo, issue: issueNum,
|
|
agents_responded: posted,
|
|
responding: responding.map(a => a.name),
|
|
type: isPR ? 'pull_request' : 'issue',
|
|
});
|
|
}
|
|
|
|
return Response.json({ error: 'Not found' }, { status: 404 });
|
|
},
|
|
};
|