diff --git a/apps/scout-bot/src/index.ts b/apps/scout-bot/src/index.ts index 915c69a..680dbac 100644 --- a/apps/scout-bot/src/index.ts +++ b/apps/scout-bot/src/index.ts @@ -11,6 +11,7 @@ const SCOUT_AGENT_ID = process.env.SCOUT_AGENT_ID || "scout_official_1"; const SCOUT_CRON_EXPRESSION = process.env.SCOUT_CRON_EXPRESSION || "*/10 * * * *"; const SCOUT_ISSUE_LABEL = process.env.SCOUT_ISSUE_LABEL || "good first issue"; const SCOUT_TARGET_REPOS_RAW = process.env.SCOUT_TARGET_REPOS || "vibe-work/test-bounty-repo"; + const SCOUT_TARGET_REPOS = SCOUT_TARGET_REPOS_RAW .split(",") .map((entry) => entry.trim()) @@ -24,6 +25,9 @@ const SCOUT_TARGET_REPOS = SCOUT_TARGET_REPOS_RAW const fallbackRepo = { owner: "vibe-work", repo: "test-bounty-repo" }; const TARGET_REPOS = SCOUT_TARGET_REPOS.length > 0 ? SCOUT_TARGET_REPOS : [fallbackRepo]; +if (!SCOUT_API_KEY) { + console.warn("WARNING: SCOUT_API_KEY is not set. Draft API may be rejected by server."); +} if (!GITHUB_TOKEN) { console.warn("WARNING: GITHUB_TOKEN is not set. Scout bot cannot post comments."); } @@ -32,35 +36,65 @@ const octokit = new Octokit({ auth: GITHUB_TOKEN, }); -async function draftBountyTask(issueTitle: string, issueBody: string, issueUrl: string) { - try { - const response = await fetch(`${VIBEWORK_API_URL}/scout/draft`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${SCOUT_API_KEY}` - }, - body: JSON.stringify({ - scout_id: SCOUT_AGENT_ID, - title: `GitHub Issue: ${issueTitle}`, - description: `${issueBody}\n\nSource: ${issueUrl}`, - reward_amount: 1000, // $10.00 Beta Promo subsidy (<= 2000 triggers free promo logic) - reward_currency: "USD", - required_stack: ["TypeScript", "GitHub"], - test_file_content: "// Needs manual test writing based on issue" - }) - }); +async function wait(seconds: number) { + return new Promise((resolve) => setTimeout(resolve, seconds * 1000)); +} - if (!response.ok) { - throw new Error(`Draft API failed: ${await response.text()}`); +async function postDraftWithRetry(payload: { + scout_id: string; + title: string; + description: string; + reward_amount: number; + reward_currency: string; + required_stack: string[]; + test_file_content: string; +}) { + let lastError: unknown = null; + + for (let attempt = 0; attempt < 3; attempt++) { + try { + const response = await fetch(`${VIBEWORK_API_URL}/scout/draft`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${SCOUT_API_KEY}` + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const bodyText = await response.text(); + throw new Error(`Draft API failed (HTTP ${response.status}): ${bodyText}`); + } + + return await response.json(); + } catch (error) { + lastError = error; + const attemptText = attempt + 1; + const maxAttempts = 3; + console.error(`Failed to draft bounty task (attempt ${attemptText}/${maxAttempts}):`, error); + if (attempt < maxAttempts - 1) { + await wait(Math.min(5 * (attempt + 1), 15)); + } } - - const data = await response.json(); - return data; - } catch (error) { - console.error("Failed to draft bounty task:", error); - return null; } + + console.error("Failed to draft bounty task after retries:", lastError); + return null; +} + +async function draftBountyTask(issueTitle: string, issueBody: string, issueUrl: string) { + const payload = { + scout_id: SCOUT_AGENT_ID, + title: `GitHub Issue: ${issueTitle}`, + description: `${issueBody}\n\nSource: ${issueUrl}`, + reward_amount: 1000, // $10.00 Beta Promo subsidy (<= 2000 triggers free promo logic) + reward_currency: "USD", + required_stack: ["TypeScript", "GitHub"], + test_file_content: "// Needs manual test writing based on issue" + }; + + return postDraftWithRetry(payload); } async function processIssue(owner: string, repo: string, issue: any) { @@ -74,7 +108,7 @@ async function processIssue(owner: string, repo: string, issue: any) { issue_number: issue.number, }); - const alreadyCommented = comments.data.some(c => c.body?.includes("agent.wooo.work")); + const alreadyCommented = comments.data.some((c) => c.body?.includes("agent.wooo.work")); if (alreadyCommented) { console.log(`Already commented on #${issue.number}, skipping.`); return; @@ -83,7 +117,7 @@ async function processIssue(owner: string, repo: string, issue: any) { // Generate draft task on VibeWork const draft = await draftBountyTask(issue.title, issue.body || "", issue.html_url); - + if (!draft) { console.log(`Failed to generate draft for #${issue.number}, skipping comment.`); return; @@ -123,7 +157,7 @@ npx -y @agent-bounty/mcp-server --endpoint https://agent.wooo.work async function scanRepositories() { console.log("Starting GitHub scan..."); - + for (const target of TARGET_REPOS) { try { const issues = await octokit.issues.listForRepo({ @@ -135,7 +169,7 @@ async function scanRepositories() { }); console.log(`Found ${issues.data.length} open issues in ${target.owner}/${target.repo}`); - + for (const issue of issues.data) { if (!issue.pull_request) { // Ignore PRs await processIssue(target.owner, target.repo, issue); @@ -147,8 +181,13 @@ async function scanRepositories() { } } -// Run immediately on startup for testing -scanRepositories(); +async function bootstrap() { + // Delay first scan a bit so main web API is ready after startup. + await wait(10); + await scanRepositories(); +} + +bootstrap(); // Default: every 10 minutes for higher volume inbound traffic cron.schedule(SCOUT_CRON_EXPRESSION, () => { diff --git a/docker-compose.yml b/docker-compose.yml index e42dea4..820557d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,6 +41,13 @@ services: # We use a command override to run database push before starting next.js command: > sh -c "npx prisma@6.4.1 db push --schema=apps/web/prisma/schema.prisma --skip-generate && node apps/web/server.js" + healthcheck: + test: ["CMD-SHELL", "node -e \"const http=require('http');const req=http.get('http://127.0.0.1:3000/api/v1/health',(res)=>process.exit(res.statusCode===200?0:1));req.on('error',()=>process.exit(1));\""] + interval: 10s + timeout: 5s + retries: 30 + start_period: 10s + stop_grace_period: 30s networks: - agent-bounty-network @@ -52,11 +59,11 @@ services: restart: unless-stopped depends_on: web: - condition: service_started + condition: service_healthy environment: - NODE_ENV=production - VIBEWORK_API_URL=http://agent_bounty_web:3000/api - - SCOUT_API_KEY=${SCOUT_API_KEY:-dev_scout_key} + - SCOUT_API_KEY=${SCOUT_API_KEY:-super-secret-mcp-key} - SCOUT_AGENT_ID=scout_official_1 # Optional: add more discovery repos via env (comma separated "owner/repo"), e.g. openai/swarm,significant-gravitas/autogpt - SCOUT_TARGET_REPOS=${SCOUT_TARGET_REPOS}