From 0a4d56d3cc7b06fcdba66bb90e806fed297c9115 Mon Sep 17 00:00:00 2001 From: spawn-bot Date: Thu, 2 Apr 2026 06:49:58 +0000 Subject: [PATCH 1/6] feat: add Reddit growth discovery agent Adds an automated agent that scans Reddit for threads where Spawn solves someone's problem, qualifies the poster, and surfaces the best candidate to Slack for human review. Does not auto-reply. - growth.sh: service script (same pattern as refactor.sh) - growth-prompt.md: Claude prompt for Reddit scanning + Slack posting - growth.yml: GitHub Actions workflow (daily trigger) - start-growth.sh: gitignored template for VM secrets Co-Authored-By: Claude Opus 4.6 (1M context) --- .../skills/setup-agent-team/growth-prompt.md | 272 ++++++++++++++++++ .claude/skills/setup-agent-team/growth.sh | 141 +++++++++ .github/workflows/growth.yml | 38 +++ 3 files changed, 451 insertions(+) create mode 100644 .claude/skills/setup-agent-team/growth-prompt.md create mode 100644 .claude/skills/setup-agent-team/growth.sh create mode 100644 .github/workflows/growth.yml diff --git a/.claude/skills/setup-agent-team/growth-prompt.md b/.claude/skills/setup-agent-team/growth-prompt.md new file mode 100644 index 000000000..7b0ad2dcf --- /dev/null +++ b/.claude/skills/setup-agent-team/growth-prompt.md @@ -0,0 +1,272 @@ +You are the Reddit growth discovery agent for Spawn (https://github.com/OpenRouterTeam/spawn). + +Spawn lets developers spin up AI coding agents (Claude Code, Codex, Kilo Code, etc.) on cloud servers with one command: `curl -fsSL openrouter.ai/labs/spawn | bash` + +Your job: find the ONE best Reddit thread where someone is asking for something Spawn solves, verify the poster looks like a real developer who could use it, and surface the finding to Slack for the team to review. You do NOT post replies. Humans decide what to do. + +## Credentials + +Reddit OAuth (script grant): +- Client ID: `REDDIT_CLIENT_ID_PLACEHOLDER` +- Client Secret: `REDDIT_CLIENT_SECRET_PLACEHOLDER` +- Username: `REDDIT_USERNAME_PLACEHOLDER` +- Password: `REDDIT_PASSWORD_PLACEHOLDER` + +Slack: +- Bot Token: `SLACK_BOT_TOKEN_PLACEHOLDER` +- Channel ID: `SLACK_CHANNEL_ID_PLACEHOLDER` + +GitHub issue for audit log: #GROWTH_LOG_ISSUE_PLACEHOLDER + +## Step 1: Authenticate with Reddit + +Get an OAuth token using the script grant type: + +```bash +bun -e " +const auth = Buffer.from('REDDIT_CLIENT_ID_PLACEHOLDER:REDDIT_CLIENT_SECRET_PLACEHOLDER').toString('base64'); +const res = await fetch('https://www.reddit.com/api/v1/access_token', { + method: 'POST', + headers: { + 'Authorization': 'Basic ' + auth, + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': 'spawn-growth:v1.0.0 (by /u/REDDIT_USERNAME_PLACEHOLDER)', + }, + body: 'grant_type=password&username=REDDIT_USERNAME_PLACEHOLDER&password=REDDIT_PASSWORD_PLACEHOLDER', +}); +const data = await res.json(); +console.log(JSON.stringify(data)); +" +``` + +Save the `access_token`. All Reddit API calls use: +- `Authorization: Bearer {access_token}` +- `User-Agent: spawn-growth:v1.0.0 (by /u/REDDIT_USERNAME_PLACEHOLDER)` +- Base URL: `https://oauth.reddit.com` + +## Step 2: Search for "feature ask" threads + +You are looking for a very specific type of post: someone asking how to do something that Spawn directly solves. Not general AI discussion. Not news. Not opinions. A concrete ask. + +**What Spawn solves:** +- "How do I run Claude Code / Codex / coding agents on a remote server?" +- "What's the cheapest way to get a cloud VM for AI coding?" +- "How do I set up a dev environment with AI tools on Hetzner/AWS/GCP?" +- "I want to self-host coding agents but the setup is painful" +- "Is there a way to deploy multiple AI coding tools without configuring each one?" + +**Subreddits to scan:** +- r/Vibecoding +- r/AIAgents +- r/LocalLLaMA +- r/ChatGPT +- r/SelfHosted +- r/programming +- r/commandline +- r/devops + +**Search queries** (run against each subreddit, wait 1s between calls): +- "coding agent cloud" +- "coding agent server" +- "self host AI coding" +- "remote dev AI" +- "vibe coding setup" +- "deploy coding agent" +- "cloud dev environment AI" + +``` +GET https://oauth.reddit.com/r/{subreddit}/search?q={query}&sort=new&t=week&restrict_sr=true&limit=25 +``` + +Also check for direct mentions: +``` +GET https://oauth.reddit.com/search?q=openrouter+spawn&sort=new&t=week&limit=25 +``` + +Collect all unique posts. Deduplicate by post ID. + +## Step 3: Check previous findings + +Before scoring, check what we've already surfaced: + +```bash +gh issue view GROWTH_LOG_ISSUE_PLACEHOLDER --repo OpenRouterTeam/spawn --json comments --jq '.comments[].body' +``` + +Extract all Reddit URLs from previous comments. Skip any post already surfaced. + +## Step 4: Score for relevance + +For each new post, score it on these criteria: + +**Is it a "feature ask"?** (0-5 points) +- 5: Explicitly asking how to do something Spawn does +- 3: Describing a pain point Spawn addresses +- 1: Tangentially related discussion +- 0: News, opinion, or not a question + +**Is the thread alive?** (0-2 points) +- 2: Posted in last 48h with 3+ comments or 5+ upvotes +- 1: Posted in last week, some engagement +- 0: Dead thread or very old + +**Is Spawn the right answer?** (0-3 points) +- 3: Spawn directly solves their stated problem +- 2: Spawn partially helps +- 1: Spawn is tangentially relevant +- 0: Spawn doesn't fit + +Only consider posts scoring 7+ out of 10. + +## Step 5: Qualify the poster + +For the top candidates (scored 7+), check if the poster is a real developer who could actually use Spawn. Fetch their recent comments: + +``` +GET https://oauth.reddit.com/user/{username}/comments?limit=25&sort=new +``` + +**Positive signals (look for ANY of these):** +- Mentions cloud providers (AWS, Hetzner, GCP, DigitalOcean, Azure, Vultr, Linode) +- Mentions SSH, VPS, servers, self-hosting, Docker, containers +- Posts in developer subreddits (r/programming, r/webdev, r/devops, r/SelfHosted) +- Mentions CI/CD, GitHub, deployment, infrastructure +- Has technical vocabulary in their comments +- Mentions paying for services or having accounts + +**Disqualifying signals:** +- Account is < 30 days old (likely bot/throwaway) +- Only posts in non-tech subreddits +- Posting history suggests they're not a developer +- Already uses Spawn or OpenRouter (check for mentions) + +## Step 6: Pick the ONE best candidate + +From all qualified, high-scoring posts, pick exactly 1. The best one. If nothing scores 7+ after qualification, that's fine. Report "no candidates this cycle" and stop. + +## Step 7: Post to Slack + +Post the finding to Slack for team review. Use a curl call to the Slack Web API: + +```bash +bun -e " +const payload = { + channel: 'SLACK_CHANNEL_ID_PLACEHOLDER', + text: 'Growth: Found a Reddit candidate', + blocks: [ + { + type: 'header', + text: { type: 'plain_text', text: ':mag: Reddit Growth - Candidate Found' } + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: '*Thread:* \n*Subreddit:* r/{subreddit} | *Score:* {upvotes} | *Comments:* {num_comments}\n*Posted:* {time_ago}' + } + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: '*What they asked:*\n> {brief_summary_of_their_question}' + } + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: '*Why Spawn fits:*\n{1-2_sentence_reasoning}' + } + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: '*Poster qualification:*\n{signals_found_in_their_history}' + } + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: '*Relevance score:* {score}/10' + } + }, + { + type: 'divider' + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: '*Draft reply (edit as needed):*\n\`\`\`{draft_reply_text}\`\`\`' + } + }, + { + type: 'context', + elements: [ + { type: 'mrkdwn', text: 'React :white_check_mark: to approve posting, :x: to skip, or reply in thread to edit' } + ] + } + ] +}; + +const res = await fetch('https://slack.com/api/chat.postMessage', { + method: 'POST', + headers: { + 'Authorization': 'Bearer SLACK_BOT_TOKEN_PLACEHOLDER', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), +}); +const data = await res.json(); +if (!data.ok) console.error('Slack error:', data.error); +else console.log('Posted to Slack:', data.ts); +" +``` + +**Draft reply style (for the team to edit):** +- Write like a real dev on Reddit. Casual, short, lowercase okay +- 2-3 sentences max answering their actual question +- Mention Spawn naturally if it fits +- End with "disclosure: i help build this" if mentioning Spawn +- NO em dashes, NO "I'd be happy to", NO bullet lists, NO corporate speak + +## Step 8: Log to GitHub issue + +After each cycle, append a comment to the tracking issue: + +```bash +gh issue comment GROWTH_LOG_ISSUE_PLACEHOLDER --repo OpenRouterTeam/spawn --body "## Growth scan $(date -u +'%Y-%m-%d %H:%M UTC') + +**Scanned:** {N} subreddits, {M} queries +**Posts found:** {total} ({new_count} new) + +### Selected candidate +- [{post_title}](https://reddit.com{permalink}) (r/{subreddit}, {upvotes} pts) +- Score: {score}/10 +- Poster signals: {qualification_summary} +- Status: Surfaced to Slack for review + +### Other candidates considered +{for each scored 5+:} +- [{title}](https://reddit.com{permalink}) - {score}/10: {why_not_selected} + +### No candidates +{if nothing qualified, say: 'No threads scored 7+ after qualification. This is fine.'} +" +``` + +## Safety rules + +1. **Pick exactly 1 candidate per cycle.** No more. +2. **Do NOT post replies to Reddit.** You only surface findings. +3. **Never surface the same thread twice.** Check GH issue history. +4. **No candidates is a valid outcome.** Don't force bad matches. +5. **Respect Reddit rate limits.** 1 second between API calls minimum. +6. **Don't surface threads from Spawn/OpenRouter team members.** + +## Time budget + +Complete within 25 minutes. If still searching at 20 minutes, stop and report what you have. diff --git a/.claude/skills/setup-agent-team/growth.sh b/.claude/skills/setup-agent-team/growth.sh new file mode 100644 index 000000000..4b8adf4d7 --- /dev/null +++ b/.claude/skills/setup-agent-team/growth.sh @@ -0,0 +1,141 @@ +#!/bin/bash +set -eo pipefail + +# Reddit Growth Agent — Single Cycle (Discovery Only) +# Triggered by trigger-server.ts via GitHub Actions (daily) +# +# Scans Reddit for "feature ask" threads that Spawn solves, +# qualifies the poster, picks the 1 best candidate, and surfaces it +# to Slack for human review. Does NOT auto-reply. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" +cd "${REPO_ROOT}" + +SPAWN_REASON="${SPAWN_REASON:-manual}" +TEAM_NAME="spawn-growth" +CYCLE_TIMEOUT=1800 # 30 min +HARD_TIMEOUT=2400 # 40 min grace + +LOG_FILE="${REPO_ROOT}/.docs/${TEAM_NAME}.log" +PROMPT_FILE="" + +# Ensure .docs directory exists +mkdir -p "$(dirname "${LOG_FILE}")" + +log() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] [growth] $*" | tee -a "${LOG_FILE}" +} + +# --- Safe sed substitution (escapes sed metacharacters in replacement) --- +safe_substitute() { + local placeholder="$1" + local value="$2" + local file="$3" + if printf '%s' "$value" | grep -qP '\x01'; then + log "ERROR: safe_substitute value contains illegal \\x01 character" + return 1 + fi + local escaped + escaped=$(printf '%s' "$value" | sed -e 's/[\\]/\\&/g' -e 's/[&]/\\&/g') + escaped="${escaped//$'\n'/\\$'\n'}" + sed -i.bak "s$(printf '\x01')${placeholder}$(printf '\x01')${escaped}$(printf '\x01')g" "$file" + rm -f "${file}.bak" +} + +# Cleanup function +cleanup() { + if [[ -n "${_cleanup_done:-}" ]]; then return; fi + _cleanup_done=1 + + local exit_code=$? + log "Running cleanup (exit_code=${exit_code})..." + + rm -f "${PROMPT_FILE:-}" 2>/dev/null || true + if [[ -n "${CLAUDE_PID:-}" ]] && kill -0 "${CLAUDE_PID}" 2>/dev/null; then + kill -TERM "${CLAUDE_PID}" 2>/dev/null || true + fi + + log "=== Cycle Done (exit_code=${exit_code}) ===" + exit ${exit_code} +} + +trap cleanup EXIT SIGTERM SIGINT + +log "=== Starting growth cycle ===" +log "Working directory: ${REPO_ROOT}" +log "Reason: ${SPAWN_REASON}" +log "Timeout: ${CYCLE_TIMEOUT}s" + +# Fetch latest refs +log "Fetching latest refs..." +git fetch --prune origin 2>&1 | tee -a "${LOG_FILE}" || true +git reset --hard origin/main 2>&1 | tee -a "${LOG_FILE}" || true + +# Update Claude Code to latest version +log "Updating Claude Code..." +claude update --yes 2>&1 | tee -a "${LOG_FILE}" || log "WARNING: Claude Code update failed (continuing with current version)" + +# Prepare prompt +log "Launching growth cycle..." + +PROMPT_FILE=$(mktemp /tmp/growth-prompt-XXXXXX.md) +PROMPT_TEMPLATE="${SCRIPT_DIR}/growth-prompt.md" + +if [[ ! -f "$PROMPT_TEMPLATE" ]]; then + log "ERROR: growth-prompt.md not found at $PROMPT_TEMPLATE" + exit 1 +fi + +cat "$PROMPT_TEMPLATE" > "${PROMPT_FILE}" + +# Substitute env vars into prompt +safe_substitute "REDDIT_CLIENT_ID_PLACEHOLDER" "${REDDIT_CLIENT_ID:-}" "${PROMPT_FILE}" +safe_substitute "REDDIT_CLIENT_SECRET_PLACEHOLDER" "${REDDIT_CLIENT_SECRET:-}" "${PROMPT_FILE}" +safe_substitute "REDDIT_USERNAME_PLACEHOLDER" "${REDDIT_USERNAME:-}" "${PROMPT_FILE}" +safe_substitute "REDDIT_PASSWORD_PLACEHOLDER" "${REDDIT_PASSWORD:-}" "${PROMPT_FILE}" +safe_substitute "GROWTH_LOG_ISSUE_PLACEHOLDER" "${GROWTH_LOG_ISSUE:-}" "${PROMPT_FILE}" +safe_substitute "SLACK_BOT_TOKEN_PLACEHOLDER" "${SLACK_BOT_TOKEN:-}" "${PROMPT_FILE}" +safe_substitute "SLACK_CHANNEL_ID_PLACEHOLDER" "${SLACK_CHANNEL_ID:-}" "${PROMPT_FILE}" + +log "Hard timeout: ${HARD_TIMEOUT}s" + +# Run claude in background +claude -p "$(cat "${PROMPT_FILE}")" --dangerously-skip-permissions --model sonnet >> "${LOG_FILE}" 2>&1 & +CLAUDE_PID=$! +log "Claude started (pid=${CLAUDE_PID})" + +# Kill claude and its full process tree +kill_claude() { + if kill -0 "${CLAUDE_PID}" 2>/dev/null; then + log "Killing claude (pid=${CLAUDE_PID}) and its process tree" + pkill -TERM -P "${CLAUDE_PID}" 2>/dev/null || true + kill -TERM "${CLAUDE_PID}" 2>/dev/null || true + sleep 5 + pkill -KILL -P "${CLAUDE_PID}" 2>/dev/null || true + kill -KILL "${CLAUDE_PID}" 2>/dev/null || true + fi +} + +# Watchdog: wall-clock timeout +WALL_START=$(date +%s) + +while kill -0 "${CLAUDE_PID}" 2>/dev/null; do + sleep 30 + WALL_ELAPSED=$(( $(date +%s) - WALL_START )) + + if [[ "${WALL_ELAPSED}" -ge "${HARD_TIMEOUT}" ]]; then + log "Hard timeout: ${WALL_ELAPSED}s elapsed — killing process" + kill_claude + break + fi +done + +wait "${CLAUDE_PID}" 2>/dev/null +CLAUDE_EXIT=$? + +if [[ "${CLAUDE_EXIT}" -eq 0 ]]; then + log "Cycle completed successfully" +else + log "Cycle failed (exit_code=${CLAUDE_EXIT})" +fi diff --git a/.github/workflows/growth.yml b/.github/workflows/growth.yml new file mode 100644 index 000000000..846f32b23 --- /dev/null +++ b/.github/workflows/growth.yml @@ -0,0 +1,38 @@ +name: Trigger Growth + +on: + schedule: + - cron: '37 14 * * *' + workflow_dispatch: + +jobs: + trigger: + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - name: Trigger growth cycle + env: + SPRITE_URL: ${{ secrets.GROWTH_SPRITE_URL }} + TRIGGER_SECRET: ${{ secrets.GROWTH_TRIGGER_SECRET }} + run: | + HTTP_CODE=$(curl -sS --connect-timeout 15 --max-time 30 \ + -o /tmp/response.json -w "%{http_code}" -X POST \ + "${SPRITE_URL}/trigger?reason=${{ github.event_name }}" \ + -H "Authorization: Bearer ${TRIGGER_SECRET}") + BODY=$(cat /tmp/response.json 2>/dev/null || echo '{}') + echo "$BODY" + case "$HTTP_CODE" in + 2*) + echo "::notice::Trigger accepted (HTTP $HTTP_CODE)" + ;; + 409) + echo "::notice::Run already in progress (HTTP 409)" + ;; + 429) + echo "::warning::Server at capacity (HTTP 429)" + ;; + *) + echo "::error::Trigger failed (HTTP $HTTP_CODE)" + exit 1 + ;; + esac From 63b3179b664e9c21042cabdebff5482f693758c9 Mon Sep 17 00:00:00 2001 From: spawn-bot Date: Thu, 2 Apr 2026 07:04:27 +0000 Subject: [PATCH 2/6] refactor: strip Slack/GH issue from growth agent, output to log only Simplifies the growth agent to just scan Reddit + score + qualify + output to stdout/log. Slack (via spa) and GH issue logging will be wired up separately. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../skills/setup-agent-team/growth-prompt.md | 167 ++++-------------- .claude/skills/setup-agent-team/growth.sh | 7 +- 2 files changed, 40 insertions(+), 134 deletions(-) diff --git a/.claude/skills/setup-agent-team/growth-prompt.md b/.claude/skills/setup-agent-team/growth-prompt.md index 7b0ad2dcf..c02cb29eb 100644 --- a/.claude/skills/setup-agent-team/growth-prompt.md +++ b/.claude/skills/setup-agent-team/growth-prompt.md @@ -2,7 +2,7 @@ You are the Reddit growth discovery agent for Spawn (https://github.com/OpenRout Spawn lets developers spin up AI coding agents (Claude Code, Codex, Kilo Code, etc.) on cloud servers with one command: `curl -fsSL openrouter.ai/labs/spawn | bash` -Your job: find the ONE best Reddit thread where someone is asking for something Spawn solves, verify the poster looks like a real developer who could use it, and surface the finding to Slack for the team to review. You do NOT post replies. Humans decide what to do. +Your job: find the ONE best Reddit thread where someone is asking for something Spawn solves, verify the poster looks like a real developer who could use it, and output a summary. You do NOT post replies. You only find and report. ## Credentials @@ -12,12 +12,6 @@ Reddit OAuth (script grant): - Username: `REDDIT_USERNAME_PLACEHOLDER` - Password: `REDDIT_PASSWORD_PLACEHOLDER` -Slack: -- Bot Token: `SLACK_BOT_TOKEN_PLACEHOLDER` -- Channel ID: `SLACK_CHANNEL_ID_PLACEHOLDER` - -GitHub issue for audit log: #GROWTH_LOG_ISSUE_PLACEHOLDER - ## Step 1: Authenticate with Reddit Get an OAuth token using the script grant type: @@ -85,19 +79,9 @@ GET https://oauth.reddit.com/search?q=openrouter+spawn&sort=new&t=week&limit=25 Collect all unique posts. Deduplicate by post ID. -## Step 3: Check previous findings - -Before scoring, check what we've already surfaced: - -```bash -gh issue view GROWTH_LOG_ISSUE_PLACEHOLDER --repo OpenRouterTeam/spawn --json comments --jq '.comments[].body' -``` +## Step 3: Score for relevance -Extract all Reddit URLs from previous comments. Skip any post already surfaced. - -## Step 4: Score for relevance - -For each new post, score it on these criteria: +For each post, score it on these criteria: **Is it a "feature ask"?** (0-5 points) - 5: Explicitly asking how to do something Spawn does @@ -118,7 +102,7 @@ For each new post, score it on these criteria: Only consider posts scoring 7+ out of 10. -## Step 5: Qualify the poster +## Step 4: Qualify the poster For the top candidates (scored 7+), check if the poster is a real developer who could actually use Spawn. Fetch their recent comments: @@ -140,132 +124,57 @@ GET https://oauth.reddit.com/user/{username}/comments?limit=25&sort=new - Posting history suggests they're not a developer - Already uses Spawn or OpenRouter (check for mentions) -## Step 6: Pick the ONE best candidate +## Step 5: Pick the ONE best candidate -From all qualified, high-scoring posts, pick exactly 1. The best one. If nothing scores 7+ after qualification, that's fine. Report "no candidates this cycle" and stop. +From all qualified, high-scoring posts, pick exactly 1. The best one. If nothing scores 7+ after qualification, that's fine. Say "no candidates this cycle" and stop. -## Step 7: Post to Slack +## Step 6: Output summary -Post the finding to Slack for team review. Use a curl call to the Slack Web API: +Print a structured summary of what you found. This goes to the log file. -```bash -bun -e " -const payload = { - channel: 'SLACK_CHANNEL_ID_PLACEHOLDER', - text: 'Growth: Found a Reddit candidate', - blocks: [ - { - type: 'header', - text: { type: 'plain_text', text: ':mag: Reddit Growth - Candidate Found' } - }, - { - type: 'section', - text: { - type: 'mrkdwn', - text: '*Thread:* \n*Subreddit:* r/{subreddit} | *Score:* {upvotes} | *Comments:* {num_comments}\n*Posted:* {time_ago}' - } - }, - { - type: 'section', - text: { - type: 'mrkdwn', - text: '*What they asked:*\n> {brief_summary_of_their_question}' - } - }, - { - type: 'section', - text: { - type: 'mrkdwn', - text: '*Why Spawn fits:*\n{1-2_sentence_reasoning}' - } - }, - { - type: 'section', - text: { - type: 'mrkdwn', - text: '*Poster qualification:*\n{signals_found_in_their_history}' - } - }, - { - type: 'section', - text: { - type: 'mrkdwn', - text: '*Relevance score:* {score}/10' - } - }, - { - type: 'divider' - }, - { - type: 'section', - text: { - type: 'mrkdwn', - text: '*Draft reply (edit as needed):*\n\`\`\`{draft_reply_text}\`\`\`' - } - }, - { - type: 'context', - elements: [ - { type: 'mrkdwn', text: 'React :white_check_mark: to approve posting, :x: to skip, or reply in thread to edit' } - ] - } - ] -}; - -const res = await fetch('https://slack.com/api/chat.postMessage', { - method: 'POST', - headers: { - 'Authorization': 'Bearer SLACK_BOT_TOKEN_PLACEHOLDER', - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), -}); -const data = await res.json(); -if (!data.ok) console.error('Slack error:', data.error); -else console.log('Posted to Slack:', data.ts); -" -``` +**If a candidate was found:** -**Draft reply style (for the team to edit):** -- Write like a real dev on Reddit. Casual, short, lowercase okay -- 2-3 sentences max answering their actual question -- Mention Spawn naturally if it fits -- End with "disclosure: i help build this" if mentioning Spawn -- NO em dashes, NO "I'd be happy to", NO bullet lists, NO corporate speak +``` +=== GROWTH CANDIDATE FOUND === +Thread: {post_title} +URL: https://reddit.com{permalink} +Subreddit: r/{subreddit} +Upvotes: {score} | Comments: {num_comments} +Posted: {time_ago} -## Step 8: Log to GitHub issue +What they asked: +{brief summary of their question} -After each cycle, append a comment to the tracking issue: +Why Spawn fits: +{1-2 sentences} -```bash -gh issue comment GROWTH_LOG_ISSUE_PLACEHOLDER --repo OpenRouterTeam/spawn --body "## Growth scan $(date -u +'%Y-%m-%d %H:%M UTC') +Poster qualification: +{signals found in their history} -**Scanned:** {N} subreddits, {M} queries -**Posts found:** {total} ({new_count} new) +Relevance score: {score}/10 -### Selected candidate -- [{post_title}](https://reddit.com{permalink}) (r/{subreddit}, {upvotes} pts) -- Score: {score}/10 -- Poster signals: {qualification_summary} -- Status: Surfaced to Slack for review +Draft reply: +{a short casual reply the team could use, written like a real dev on reddit. 2-3 sentences, no em dashes, no corporate speak, lowercase ok. end with "disclosure: i help build this" if mentioning spawn} +=== END CANDIDATE === +``` -### Other candidates considered -{for each scored 5+:} -- [{title}](https://reddit.com{permalink}) - {score}/10: {why_not_selected} +**If no candidates found:** -### No candidates -{if nothing qualified, say: 'No threads scored 7+ after qualification. This is fine.'} -" +``` +=== GROWTH SCAN COMPLETE === +Posts scanned: {total} +Scored 7+: 0 +No candidates this cycle. +=== END SCAN === ``` ## Safety rules 1. **Pick exactly 1 candidate per cycle.** No more. -2. **Do NOT post replies to Reddit.** You only surface findings. -3. **Never surface the same thread twice.** Check GH issue history. -4. **No candidates is a valid outcome.** Don't force bad matches. -5. **Respect Reddit rate limits.** 1 second between API calls minimum. -6. **Don't surface threads from Spawn/OpenRouter team members.** +2. **Do NOT post replies to Reddit.** You only scan and report. +3. **No candidates is a valid outcome.** Don't force bad matches. +4. **Respect Reddit rate limits.** 1 second between API calls minimum. +5. **Don't surface threads from Spawn/OpenRouter team members.** ## Time budget diff --git a/.claude/skills/setup-agent-team/growth.sh b/.claude/skills/setup-agent-team/growth.sh index 4b8adf4d7..fa6de1a6a 100644 --- a/.claude/skills/setup-agent-team/growth.sh +++ b/.claude/skills/setup-agent-team/growth.sh @@ -5,8 +5,8 @@ set -eo pipefail # Triggered by trigger-server.ts via GitHub Actions (daily) # # Scans Reddit for "feature ask" threads that Spawn solves, -# qualifies the poster, picks the 1 best candidate, and surfaces it -# to Slack for human review. Does NOT auto-reply. +# qualifies the poster, picks the 1 best candidate, and outputs +# a summary to the log. Does NOT post replies or notify externally. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" @@ -94,9 +94,6 @@ safe_substitute "REDDIT_CLIENT_ID_PLACEHOLDER" "${REDDIT_CLIENT_ID:-}" "${PROMPT safe_substitute "REDDIT_CLIENT_SECRET_PLACEHOLDER" "${REDDIT_CLIENT_SECRET:-}" "${PROMPT_FILE}" safe_substitute "REDDIT_USERNAME_PLACEHOLDER" "${REDDIT_USERNAME:-}" "${PROMPT_FILE}" safe_substitute "REDDIT_PASSWORD_PLACEHOLDER" "${REDDIT_PASSWORD:-}" "${PROMPT_FILE}" -safe_substitute "GROWTH_LOG_ISSUE_PLACEHOLDER" "${GROWTH_LOG_ISSUE:-}" "${PROMPT_FILE}" -safe_substitute "SLACK_BOT_TOKEN_PLACEHOLDER" "${SLACK_BOT_TOKEN:-}" "${PROMPT_FILE}" -safe_substitute "SLACK_CHANNEL_ID_PLACEHOLDER" "${SLACK_CHANNEL_ID:-}" "${PROMPT_FILE}" log "Hard timeout: ${HARD_TIMEOUT}s" From 43098b2754ded9fafba6ae5cd75ae3a7479a1f35 Mon Sep 17 00:00:00 2001 From: spawn-bot Date: Thu, 2 Apr 2026 07:19:57 +0000 Subject: [PATCH 3/6] fix: replace Pi agent icon with correct logo from shittycodingagent.ai Previous icon was a wrong GitHub avatar (Korean characters). Now uses the official Pi logo (pixelated P with dot) from the project website. Co-Authored-By: Claude Opus 4.6 (1M context) --- assets/agents/.sources.json | 2 +- assets/agents/pi.png | Bin 43259 -> 2426 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/agents/.sources.json b/assets/agents/.sources.json index 9535332a6..100af3a22 100644 --- a/assets/agents/.sources.json +++ b/assets/agents/.sources.json @@ -32,7 +32,7 @@ "ext": "png" }, "pi": { - "url": "https://avatars.githubusercontent.com/u/506932?s=200&v=4", + "url": "custom:shittycodingagent.ai/logo.svg (official Pi logo, converted to PNG with dark background)", "ext": "png" } } diff --git a/assets/agents/pi.png b/assets/agents/pi.png index d59c6b02a2f695ad35502cd7da7c74147a873ce5..55b47afa95e5713e9ea7eae19d31414f7dd91ca0 100644 GIT binary patch literal 2426 zcmdT`i8tF>8;-;_mK1}w8pLO*sp_KC($b$LZK4N75tY)8Av9ei_OwDeZ4sY|w$sLh zTGKFsG?tH|n6X!>HYL;4PAlrOMjB$9pMT;z^WAga_uliK^W1yx`<(MUZ~6t#bLuL3 zDi8=n9fNlB0r&3RP*wzU1-3E|0{PAv!n@d zNt{+NG2)Kb2Sg2+p-LlQT;#M4!aEp9`}M=|e{EUnYthZD(J70ze?^%sjqSsJs{ST5 z?wen0$QhJ|Fkhd?r2X@f7q)_a4_gzTU%4Te{4RLlM2~%N4+aw=QTENIOm^Y8&j=|vX@;A&+D~b);Isv zZ~n+)d|ccRE~nNq3rqJpL)lCy>?%Z0vdYTJsw!8KXvzmx&25VHrz+Syn8VvebFt9d-{!3wi+Ib|*LN9Qr#wpZ2a-7V zU9M6H1Ph`xW)aY9d_XT|$=#a-8hb*dBOVJnY5jVO*J_33FzEB@p7*1pqfP91QVHQ>gtXo zqob7(qmQ1#jNq$juz8b~x`xL!+GyOF=sRLxrRfmV!_zlRAEAx?aP+oCM3{O(z{PJ; zso@;+D}%apXzwd`ZVE%9lHlpEZFk@yoZ{PO|O^pB1MV_MHp6oJHtMNA)oZFzE& zw(^YviRuh$`+p5m;=}f)TlX)2Q7I)*ievYuHA~l6y)`gdE_`k=7MmYSGC)rSiIZ&< z3T_D`**0YaM-Bb{^*)^ew^WIEEaI6`Q5VN%X4p^<;zkebCBI=dDzn(CDLa0v*%CWu zc6PRtqvIAUS*`%X!y+Q8#~o3t_r>TN}=9^1j#=zy#)5VTNdr zXow)mOr!NI5}@4!QnclvETje!bp#rS!^P||I6a^?za*0o>Ug}u{gkH}fD>g?ly^F@ zXuoh`T|BeU%Q(uMz{^9S7-u9h3q=2~4-eYf+A25i_dnZSTvcvAi+sQhX=%W}&5Ilg zynXw&43F1L%LLTqq4a-Bf}S{elARpqZ#z^Mv;%9fgnp++y>}mgx+1R{WwmOcvC1aL zGJ?h7JHIOGHCBhj&TVXLd=>*S@WA)>)3-e2&@7k?)ZyVo z|5|X%8a!|)3nGcb5g!8b64`N6K z>(2?)1c!{J4kMYnIENE%3%+%@s;pO4y+DvO@G_$;BUl)2{|68OA!dbdF_KfGowiqCj6tNvO$YfA+cB!d1WTn!4^PjhUY?#q zh=<7hGFMku{-;l$bozow9H6Pb`Ej|uL0#6-IiWrKv2Oe?s&(-8jCA>RQi)yDDFqTW zpjG*@1((Dhw$R`%hfUI^aCcu^l$efR7cx>R>f@2YQ}`dwGn)q>4fC#r7qY7Ki-c^Qa%2n!tlErKYAvJLJ4^aB;?8KkHp%4chpl;R$4( znHXS3o?A}tO!`ApMHuQk^KXS`C;V|aH8xqQ+gxoQ^J<}A_$sedZrIPSv7_uyY|o1K z>-P^qP{42A=6s4>^Eqe2kWL5FP-y8dc^T?pfUd^QJSi?Nu8Lz+?UY7r4rb`n!?wbG rjfq7X;LIX4Jp>Bwlk&GZb<2jD(lFJs;q3}=rh;Hlo^F*cSML4|N%1^5 literal 43259 zcmV(xLFm4TP)B27z0dfJ@AQ2XRYf5v7J-!@g8J9L-7!%{wGBx$YSPiA&#}*@sBbwMEL_sO20>1j*>6y>Cd!79|=lYJ-k67#d-uHRVbI#uP zzK83+?)#2cS0AbCx@npyimbKHIcsg(woTJG=b|Vw#uPh7^83GZkwjz4MkBL4u?37CzHv3zb}d+P18Kj4~GL6 zOp=7Va?Ww7D2l483X9-;>-PISA92nZW7@V{Z`Skqyevx|%6q@t?G}rLv39rJ#&Mjc zY1_7O99wIzuCB5yYulEach2#!&bjS&>zvE;JY2Rd%hG$FWtnrXZQH7*_}2d*4N(XihwALI%M7Luwr7-O<5Lz0^C;tk$=zQQ@@y+`=C zR2-*Ck{k|)EX%^OHf`G&+bz*@x#a!U+QZ@Cy-$Y6L5#SE>2x|?6NH9?!RoPFX_~U0 zbzO4>Hok4!EX(3J<|Ecxjxl0dRTW>6B#HO_5MHy{44FoP`CwU=lgWg2K+ySt8`C{= zW$%3zUTiuK!a~JyjEw3@IW1|LB3_5X!8tdbPOGYlUipfzPm+YC=fxg#Ib%#!Rfoet z;>D^VOnQFK5R1q=*;?+BpX<6t86Z;Jt3JY;>$=u25yv==(I|PI>y|jpdZ)fxKeHl1 zIAYxoJ69CNe!u4)kPKv07oSe2Wmz_jx26;2`C`F&>$F-^)pfo0rjxTIL6>;%Q3-4YTBj^a&LroyEX)0V&$4G(#+&)lG)*~UtOkaHgFQZq=xp?# z-p^gJ<=sw)uR|z#UY3Y;<5rU-;hdwxnx;7%4tbt)0FZc2IQyQasZKacR8>{4&R_P@ zuhY@zNO9=VdsS8GaPwilQeT6lONRKF3+hwoc|M5f@ce0-@{s&eCYYm%u(A*wT*)8H zDpkbeaYnN&lR;;1SSIu(*XMh=C5)XhrYuXAxEI#CajtthsnsNjTi>KCXxWo zZknbjMKn;FrdY9}C^&@s{T@w#l&{z8$z+lw2__p&$(r-=>2z8YMI6USa9!7BS@NY= zb9TWPb9HqU$1&C*ilT5Z8WgZTju3M*)9I9z;r3WD)D>Gco6Y2qun2WsZ#EklE1m*L zmB1i>&be;CCX>n7#lf_3TP!sC3}wz~kTGhShI5Gs^JUr_zK>U3*DSZ3A!Jm36<6cr zAqsh(vzmyhlp+Ta&CHF-QtS94${Zkl1XnX*{m^Yh^P9R

*`IBG8o$$TVvRG&24SJ}+tG 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 P00000NkvXXu0mjfq2cP} From 97af66ea50a75ea92230a10e0bda009ebe921da8 Mon Sep 17 00:00:00 2001 From: spawn-bot Date: Thu, 2 Apr 2026 07:21:42 +0000 Subject: [PATCH 4/6] Revert "fix: replace Pi agent icon with correct logo from shittycodingagent.ai" This reverts commit 43098b2754ded9fafba6ae5cd75ae3a7479a1f35. --- assets/agents/.sources.json | 2 +- assets/agents/pi.png | Bin 2426 -> 43259 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/agents/.sources.json b/assets/agents/.sources.json index 100af3a22..9535332a6 100644 --- a/assets/agents/.sources.json +++ b/assets/agents/.sources.json @@ -32,7 +32,7 @@ "ext": "png" }, "pi": { - "url": "custom:shittycodingagent.ai/logo.svg (official Pi logo, converted to PNG with dark background)", + "url": "https://avatars.githubusercontent.com/u/506932?s=200&v=4", "ext": "png" } } diff --git a/assets/agents/pi.png b/assets/agents/pi.png index 55b47afa95e5713e9ea7eae19d31414f7dd91ca0..d59c6b02a2f695ad35502cd7da7c74147a873ce5 100644 GIT binary patch literal 43259 zcmV(xLFm4TP)B27z0dfJ@AQ2XRYf5v7J-!@g8J9L-7!%{wGBx$YSPiA&#}*@sBbwMEL_sO20>1j*>6y>Cd!79|=lYJ-k67#d-uHRVbI#uP zzK83+?)#2cS0AbCx@npyimbKHIcsg(woTJG=b|Vw#uPh7^83GZkwjz4MkBL4u?37CzHv3zb}d+P18Kj4~GL6 zOp=7Va?Ww7D2l483X9-;>-PISA92nZW7@V{Z`Skqyevx|%6q@t?G}rLv39rJ#&Mjc zY1_7O99wIzuCB5yYulEach2#!&bjS&>zvE;JY2Rd%hG$FWtnrXZQH7*_}2d*4N(XihwALI%M7Luwr7-O<5Lz0^C;tk$=zQQ@@y+`=C zR2-*Ck{k|)EX%^OHf`G&+bz*@x#a!U+QZ@Cy-$Y6L5#SE>2x|?6NH9?!RoPFX_~U0 zbzO4>Hok4!EX(3J<|Ecxjxl0dRTW>6B#HO_5MHy{44FoP`CwU=lgWg2K+ySt8`C{= zW$%3zUTiuK!a~JyjEw3@IW1|LB3_5X!8tdbPOGYlUipfzPm+YC=fxg#Ib%#!Rfoet z;>D^VOnQFK5R1q=*;?+BpX<6t86Z;Jt3JY;>$=u25yv==(I|PI>y|jpdZ)fxKeHl1 zIAYxoJ69CNe!u4)kPKv07oSe2Wmz_jx26;2`C`F&>$F-^)pfo0rjxTIL6>;%Q3-4YTBj^a&LroyEX)0V&$4G(#+&)lG)*~UtOkaHgFQZq=xp?# z-p^gJ<=sw)uR|z#UY3Y;<5rU-;hdwxnx;7%4tbt)0FZc2IQyQasZKacR8>{4&R_P@ zuhY@zNO9=VdsS8GaPwilQeT6lONRKF3+hwoc|M5f@ce0-@{s&eCYYm%u(A*wT*)8H zDpkbeaYnN&lR;;1SSIu(*XMh=C5)XhrYuXAxEI#CajtthsnsNjTi>KCXxWo zZknbjMKn;FrdY9}C^&@s{T@w#l&{z8$z+lw2__p&$(r-=>2z8YMI6USa9!7BS@NY= zb9TWPb9HqU$1&C*ilT5Z8WgZTju3M*)9I9z;r3WD)D>Gco6Y2qun2WsZ#EklE1m*L zmB1i>&be;CCX>n7#lf_3TP!sC3}wz~kTGhShI5Gs^JUr_zK>U3*DSZ3A!Jm36<6cr zAqsh(vzmyhlp+Ta&CHF-QtS94${Zkl1XnX*{m^Yh^P9R

*`IBG8o$$TVvRG&24SJ}+tG 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 P00000NkvXXu0mjfq2cP} literal 2426 zcmdT`i8tF>8;-;_mK1}w8pLO*sp_KC($b$LZK4N75tY)8Av9ei_OwDeZ4sY|w$sLh zTGKFsG?tH|n6X!>HYL;4PAlrOMjB$9pMT;z^WAga_uliK^W1yx`<(MUZ~6t#bLuL3 zDi8=n9fNlB0r&3RP*wzU1-3E|0{PAv!n@d zNt{+NG2)Kb2Sg2+p-LlQT;#M4!aEp9`}M=|e{EUnYthZD(J70ze?^%sjqSsJs{ST5 z?wen0$QhJ|Fkhd?r2X@f7q)_a4_gzTU%4Te{4RLlM2~%N4+aw=QTENIOm^Y8&j=|vX@;A&+D~b);Isv zZ~n+)d|ccRE~nNq3rqJpL)lCy>?%Z0vdYTJsw!8KXvzmx&25VHrz+Syn8VvebFt9d-{!3wi+Ib|*LN9Qr#wpZ2a-7V zU9M6H1Ph`xW)aY9d_XT|$=#a-8hb*dBOVJnY5jVO*J_33FzEB@p7*1pqfP91QVHQ>gtXo zqob7(qmQ1#jNq$juz8b~x`xL!+GyOF=sRLxrRfmV!_zlRAEAx?aP+oCM3{O(z{PJ; zso@;+D}%apXzwd`ZVE%9lHlpEZFk@yoZ{PO|O^pB1MV_MHp6oJHtMNA)oZFzE& zw(^YviRuh$`+p5m;=}f)TlX)2Q7I)*ievYuHA~l6y)`gdE_`k=7MmYSGC)rSiIZ&< z3T_D`**0YaM-Bb{^*)^ew^WIEEaI6`Q5VN%X4p^<;zkebCBI=dDzn(CDLa0v*%CWu zc6PRtqvIAUS*`%X!y+Q8#~o3t_r>TN}=9^1j#=zy#)5VTNdr zXow)mOr!NI5}@4!QnclvETje!bp#rS!^P||I6a^?za*0o>Ug}u{gkH}fD>g?ly^F@ zXuoh`T|BeU%Q(uMz{^9S7-u9h3q=2~4-eYf+A25i_dnZSTvcvAi+sQhX=%W}&5Ilg zynXw&43F1L%LLTqq4a-Bf}S{elARpqZ#z^Mv;%9fgnp++y>}mgx+1R{WwmOcvC1aL zGJ?h7JHIOGHCBhj&TVXLd=>*S@WA)>)3-e2&@7k?)ZyVo z|5|X%8a!|)3nGcb5g!8b64`N6K z>(2?)1c!{J4kMYnIENE%3%+%@s;pO4y+DvO@G_$;BUl)2{|68OA!dbdF_KfGowiqCj6tNvO$YfA+cB!d1WTn!4^PjhUY?#q zh=<7hGFMku{-;l$bozow9H6Pb`Ej|uL0#6-IiWrKv2Oe?s&(-8jCA>RQi)yDDFqTW zpjG*@1((Dhw$R`%hfUI^aCcu^l$efR7cx>R>f@2YQ}`dwGn)q>4fC#r7qY7Ki-c^Qa%2n!tlErKYAvJLJ4^aB;?8KkHp%4chpl;R$4( znHXS3o?A}tO!`ApMHuQk^KXS`C;V|aH8xqQ+gxoQ^J<}A_$sedZrIPSv7_uyY|o1K z>-P^qP{42A=6s4>^Eqe2kWL5FP-y8dc^T?pfUd^QJSi?Nu8Lz+?UY7r4rb`n!?wbG rjfq7X;LIX4Jp>Bwlk&GZb<2jD(lFJs;q3}=rh;Hlo^F*cSML4|N%1^5 From 218f166374bb479f243776906b6330c769777a88 Mon Sep 17 00:00:00 2001 From: spawn-bot Date: Fri, 3 Apr 2026 00:11:41 +0000 Subject: [PATCH 5/6] feat: wire Reddit growth agent to Slack approval via SPA Growth agent scans Reddit daily, extracts structured JSON from output, and POSTs candidates to SPA's new HTTP endpoint. SPA posts Block Kit cards to #proj-spawn with Approve/Edit/Skip buttons. Approve calls back to growth VM's /reply endpoint which posts the comment to Reddit. - growth-prompt.md: add json:candidate output format - growth.sh: extract JSON + POST to SPA_TRIGGER_URL - reply.sh: new script for Reddit comment posting via OAuth - trigger-server.ts: add POST /reply endpoint - SPA helpers.ts: add candidates table + CRUD - SPA main.ts: HTTP server, button handlers, edit modal - spa.test.ts: candidate DB operation tests Co-Authored-By: Claude Opus 4.6 (1M context) --- .../skills/setup-agent-team/growth-prompt.md | 31 ++ .claude/skills/setup-agent-team/growth.sh | 29 ++ .claude/skills/setup-agent-team/reply.sh | 96 ++++ .../skills/setup-agent-team/trigger-server.ts | 89 +++- .claude/skills/setup-spa/helpers.ts | 133 +++++ .claude/skills/setup-spa/main.ts | 474 ++++++++++++++++++ .claude/skills/setup-spa/spa.test.ts | 97 +++- 7 files changed, 942 insertions(+), 7 deletions(-) create mode 100755 .claude/skills/setup-agent-team/reply.sh diff --git a/.claude/skills/setup-agent-team/growth-prompt.md b/.claude/skills/setup-agent-team/growth-prompt.md index c02cb29eb..004f35343 100644 --- a/.claude/skills/setup-agent-team/growth-prompt.md +++ b/.claude/skills/setup-agent-team/growth-prompt.md @@ -158,6 +158,29 @@ Draft reply: === END CANDIDATE === ``` +**IMPORTANT: After the human-readable summary above, you MUST also print a machine-readable JSON block.** This is how the automation pipeline picks up your findings. Print it exactly like this (with the `json:candidate` marker): + +```` +```json:candidate +{ + "found": true, + "title": "{post_title}", + "url": "https://reddit.com{permalink}", + "permalink": "{permalink}", + "subreddit": "{subreddit}", + "postId": "{thing fullname, e.g. t3_abc123}", + "upvotes": {score}, + "numComments": {num_comments}, + "postedAgo": "{time_ago}", + "whatTheyAsked": "{brief summary}", + "whySpawnFits": "{1-2 sentences}", + "posterQualification": "{signals found}", + "relevanceScore": {score_out_of_10}, + "draftReply": "{the draft reply text}" +} +``` +```` + **If no candidates found:** ``` @@ -168,6 +191,14 @@ No candidates this cycle. === END SCAN === ``` +And the machine-readable JSON: + +```` +```json:candidate +{"found": false, "postsScanned": {total}} +``` +```` + ## Safety rules 1. **Pick exactly 1 candidate per cycle.** No more. diff --git a/.claude/skills/setup-agent-team/growth.sh b/.claude/skills/setup-agent-team/growth.sh index fa6de1a6a..1f19f6a32 100644 --- a/.claude/skills/setup-agent-team/growth.sh +++ b/.claude/skills/setup-agent-team/growth.sh @@ -136,3 +136,32 @@ if [[ "${CLAUDE_EXIT}" -eq 0 ]]; then else log "Cycle failed (exit_code=${CLAUDE_EXIT})" fi + +# --- Extract candidate JSON and POST to SPA --- +CANDIDATE_JSON="" + +# Extract the json:candidate block from the log (between ```json:candidate and ```) +if [[ -f "${LOG_FILE}" ]]; then + CANDIDATE_JSON=$(sed -n '/^```json:candidate$/,/^```$/{/^```/d;p;}' "${LOG_FILE}" | tail -1) +fi + +if [[ -z "${CANDIDATE_JSON}" ]]; then + log "No json:candidate block found in output" + CANDIDATE_JSON='{"found":false}' +fi + +log "Candidate JSON: ${CANDIDATE_JSON}" + +# POST to SPA if SPA_TRIGGER_URL is configured +if [[ -n "${SPA_TRIGGER_URL:-}" && -n "${SPA_TRIGGER_SECRET:-}" ]]; then + log "Posting candidate to SPA at ${SPA_TRIGGER_URL}/candidate" + HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ + -X POST "${SPA_TRIGGER_URL}/candidate" \ + -H "Authorization: Bearer ${SPA_TRIGGER_SECRET}" \ + -H "Content-Type: application/json" \ + -d "${CANDIDATE_JSON}" \ + --max-time 30) || HTTP_STATUS="000" + log "SPA response: HTTP ${HTTP_STATUS}" +else + log "SPA_TRIGGER_URL or SPA_TRIGGER_SECRET not set, skipping Slack notification" +fi diff --git a/.claude/skills/setup-agent-team/reply.sh b/.claude/skills/setup-agent-team/reply.sh new file mode 100755 index 000000000..c0556c5e2 --- /dev/null +++ b/.claude/skills/setup-agent-team/reply.sh @@ -0,0 +1,96 @@ +#!/bin/bash +set -eo pipefail + +# Reddit Reply — Posts a comment to a Reddit thread. +# Called by trigger-server.ts via POST /reply. +# +# Required env vars: +# POST_ID — Reddit fullname of parent (e.g. t3_abc123) +# REPLY_TEXT — Comment text to post +# REDDIT_CLIENT_ID — Reddit OAuth app client ID +# REDDIT_CLIENT_SECRET — Reddit OAuth app client secret +# REDDIT_USERNAME — Reddit account username +# REDDIT_PASSWORD — Reddit account password + +if [[ -z "${POST_ID:-}" ]]; then + echo '{"ok":false,"error":"POST_ID env var is required"}' >&2 + exit 1 +fi + +if [[ -z "${REPLY_TEXT:-}" ]]; then + echo '{"ok":false,"error":"REPLY_TEXT env var is required"}' >&2 + exit 1 +fi + +if [[ -z "${REDDIT_CLIENT_ID:-}" || -z "${REDDIT_CLIENT_SECRET:-}" || -z "${REDDIT_USERNAME:-}" || -z "${REDDIT_PASSWORD:-}" ]]; then + echo '{"ok":false,"error":"REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET, REDDIT_USERNAME, and REDDIT_PASSWORD are all required"}' >&2 + exit 1 +fi + +# Use bun to authenticate + post comment (avoids shell escaping issues with reply text) +exec bun -e " +const clientId = process.env.REDDIT_CLIENT_ID; +const clientSecret = process.env.REDDIT_CLIENT_SECRET; +const username = process.env.REDDIT_USERNAME; +const password = process.env.REDDIT_PASSWORD; +const postId = process.env.POST_ID; +const replyText = process.env.REPLY_TEXT; + +const auth = Buffer.from(clientId + ':' + clientSecret).toString('base64'); +const userAgent = 'spawn-growth:v1.0.0 (by /u/' + username + ')'; + +// Step 1: Get OAuth token +const tokenRes = await fetch('https://www.reddit.com/api/v1/access_token', { + method: 'POST', + headers: { + 'Authorization': 'Basic ' + auth, + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': userAgent, + }, + body: 'grant_type=password&username=' + encodeURIComponent(username) + '&password=' + encodeURIComponent(password), +}); + +if (!tokenRes.ok) { + console.log(JSON.stringify({ ok: false, error: 'Reddit auth failed: ' + tokenRes.status })); + process.exit(1); +} + +const tokenData = await tokenRes.json(); +const token = tokenData.access_token; +if (!token) { + console.log(JSON.stringify({ ok: false, error: 'No access_token in Reddit auth response' })); + process.exit(1); +} + +// Step 2: Post comment +const commentRes = await fetch('https://oauth.reddit.com/api/comment', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': userAgent, + }, + body: 'thing_id=' + encodeURIComponent(postId) + '&text=' + encodeURIComponent(replyText), +}); + +if (!commentRes.ok) { + const body = await commentRes.text(); + console.log(JSON.stringify({ ok: false, error: 'Reddit comment failed: ' + commentRes.status, body })); + process.exit(1); +} + +const commentData = await commentRes.json(); + +// Extract the comment URL from Reddit's response +const things = commentData?.jquery?.flat?.() ?? []; +const commentThing = commentData?.json?.data?.things?.[0]?.data; +const commentId = commentThing?.id ?? commentThing?.name ?? ''; +const commentPermalink = commentThing?.permalink ?? ''; +const commentUrl = commentPermalink ? 'https://reddit.com' + commentPermalink : ''; + +console.log(JSON.stringify({ + ok: true, + commentId, + commentUrl, +})); +" diff --git a/.claude/skills/setup-agent-team/trigger-server.ts b/.claude/skills/setup-agent-team/trigger-server.ts index 244fd6853..71604d927 100644 --- a/.claude/skills/setup-agent-team/trigger-server.ts +++ b/.claude/skills/setup-agent-team/trigger-server.ts @@ -80,12 +80,7 @@ let nextRunId = 1; /** Timing-safe auth check — prevents timing side-channel attacks on TRIGGER_SECRET */ function isAuthed(req: Request): boolean { - const given = req.headers.get("Authorization") ?? ""; - const expected = `Bearer ${TRIGGER_SECRET}`; - if (given.length !== expected.length) { - return false; - } - return timingSafeEqual(Buffer.from(given), Buffer.from(expected)); + return isAuthedWith(req, TRIGGER_SECRET); } /** Allowed values for the reason query parameter */ @@ -186,6 +181,81 @@ function gracefulShutdown(signal: string) { process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); process.on("SIGINT", () => gracefulShutdown("SIGINT")); +const REPLY_SCRIPT = resolve(SKILL_DIR, "reply.sh"); +const REPLY_SECRET = process.env.REPLY_SECRET ?? TRIGGER_SECRET; + +/** Check auth against a given secret (timing-safe). */ +function isAuthedWith(req: Request, secret: string): boolean { + const given = req.headers.get("Authorization") ?? ""; + const expected = `Bearer ${secret}`; + if (given.length !== expected.length) { + return false; + } + return timingSafeEqual(Buffer.from(given), Buffer.from(expected)); +} + +/** + * Handle POST /reply — post a comment to Reddit via reply.sh. + * This is synchronous: it waits for reply.sh to finish and returns the result. + */ +async function handleReply(req: Request): Promise { + if (!isAuthedWith(req, REPLY_SECRET)) { + return Response.json({ error: "unauthorized" }, { status: 401 }); + } + + let body: unknown; + try { + body = await req.json(); + } catch { + return Response.json({ error: "invalid JSON body" }, { status: 400 }); + } + + const obj = typeof body === "object" && body !== null ? (body as Record) : null; + const postId = obj && typeof obj.postId === "string" ? obj.postId : ""; + const replyText = obj && typeof obj.replyText === "string" ? obj.replyText : ""; + + if (!postId || !replyText) { + return Response.json({ error: "postId and replyText are required" }, { status: 400 }); + } + + // Validate postId format (Reddit fullname: t1_, t3_, etc.) + if (!/^t[1-6]_[a-z0-9]+$/i.test(postId)) { + return Response.json({ error: "invalid postId format" }, { status: 400 }); + } + + console.log(`[trigger] Reply request: postId=${postId}, replyText=${replyText.slice(0, 80)}...`); + + const proc = Bun.spawn(["bash", REPLY_SCRIPT], { + stdout: "pipe", + stderr: "pipe", + env: { + ...process.env, + POST_ID: postId, + REPLY_TEXT: replyText, + }, + }); + + const [stdout, stderr] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]); + const exitCode = await proc.exited; + + if (exitCode !== 0) { + console.error(`[trigger] reply.sh failed (exit=${exitCode}): ${stderr}`); + return Response.json({ error: "reply failed", stderr: stderr.slice(0, 500) }, { status: 502 }); + } + + // Parse reply.sh JSON output + try { + const result = JSON.parse(stdout.trim()); + console.log(`[trigger] Reply posted: ${JSON.stringify(result)}`); + return Response.json(result); + } catch { + return Response.json({ ok: true, raw: stdout.trim() }); + } +} + /** * Spawn the target script and return immediately with a JSON response. * Script stdout/stderr are piped to the server console (journalctl). @@ -278,6 +348,13 @@ const server = Bun.serve({ }); } + if (req.method === "POST" && url.pathname === "/reply") { + if (shuttingDown) { + return Response.json({ error: "server is shutting down" }, { status: 503 }); + } + return handleReply(req); + } + if (req.method === "POST" && url.pathname === "/trigger") { if (shuttingDown) { return Response.json( diff --git a/.claude/skills/setup-spa/helpers.ts b/.claude/skills/setup-spa/helpers.ts index 78ef04fe8..1e5668105 100644 --- a/.claude/skills/setup-spa/helpers.ts +++ b/.claude/skills/setup-spa/helpers.ts @@ -149,6 +149,23 @@ export function openDb(path?: string): Database { PRIMARY KEY (channel, thread_ts) ) `); + db.run(` + CREATE TABLE IF NOT EXISTS candidates ( + post_id TEXT PRIMARY KEY, + permalink TEXT NOT NULL, + title TEXT NOT NULL, + subreddit TEXT NOT NULL, + draft_reply TEXT NOT NULL, + slack_channel TEXT, + slack_ts TEXT, + status TEXT NOT NULL DEFAULT 'pending', + actioned_by TEXT, + actioned_at TEXT, + posted_reply TEXT, + reddit_comment_url TEXT, + created_at TEXT NOT NULL + ) + `); if (!path) { migrateFromJson(db); } @@ -237,6 +254,122 @@ export function updateThread( ); } +// #region Candidates — Reddit growth pipeline + +/** A Reddit growth candidate tracked for approval. */ +export interface CandidateRow { + postId: string; + permalink: string; + title: string; + subreddit: string; + draftReply: string; + slackChannel?: string; + slackTs?: string; + status: "pending" | "approved" | "posted" | "skipped" | "error"; + actionedBy?: string; + actionedAt?: string; + postedReply?: string; + redditCommentUrl?: string; + createdAt: string; +} + +/** Raw SQLite row shape for candidates. */ +interface RawCandidate { + post_id: string; + permalink: string; + title: string; + subreddit: string; + draft_reply: string; + slack_channel: string | null; + slack_ts: string | null; + status: string; + actioned_by: string | null; + actioned_at: string | null; + posted_reply: string | null; + reddit_comment_url: string | null; + created_at: string; +} + +function rowToCandidate(r: RawCandidate): CandidateRow { + return { + postId: r.post_id, + permalink: r.permalink, + title: r.title, + subreddit: r.subreddit, + draftReply: r.draft_reply, + slackChannel: r.slack_channel ?? undefined, + slackTs: r.slack_ts ?? undefined, + status: r.status === "approved" || r.status === "posted" || r.status === "skipped" || r.status === "error" + ? r.status + : "pending", + actionedBy: r.actioned_by ?? undefined, + actionedAt: r.actioned_at ?? undefined, + postedReply: r.posted_reply ?? undefined, + redditCommentUrl: r.reddit_comment_url ?? undefined, + createdAt: r.created_at, + }; +} + +/** Insert or update a candidate. On conflict (same post_id), updates Slack coordinates. */ +export function upsertCandidate(db: Database, candidate: CandidateRow): void { + db.run( + `INSERT INTO candidates (post_id, permalink, title, subreddit, draft_reply, slack_channel, slack_ts, status, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (post_id) DO UPDATE SET + slack_channel = excluded.slack_channel, + slack_ts = excluded.slack_ts`, + [ + candidate.postId, + candidate.permalink, + candidate.title, + candidate.subreddit, + candidate.draftReply, + candidate.slackChannel ?? null, + candidate.slackTs ?? null, + candidate.status, + candidate.createdAt, + ], + ); +} + +/** Look up a candidate by Reddit post ID. */ +export function findCandidate(db: Database, postId: string): CandidateRow | undefined { + const row = db + .query("SELECT * FROM candidates WHERE post_id = ?") + .get(postId); + return row ? rowToCandidate(row) : undefined; +} + +/** Update a candidate's status and related fields after an action. */ +export function updateCandidateStatus( + db: Database, + postId: string, + update: { + status: CandidateRow["status"]; + actionedBy?: string; + postedReply?: string; + redditCommentUrl?: string; + }, +): void { + db.run( + `UPDATE candidates SET + status = ?, + actioned_by = ?, + actioned_at = ?, + posted_reply = ?, + reddit_comment_url = ? + WHERE post_id = ?`, + [ + update.status, + update.actionedBy ?? null, + new Date().toISOString(), + update.postedReply ?? null, + update.redditCommentUrl ?? null, + postId, + ], + ); +} + // #endregion // #region Claude Code stream parsing diff --git a/.claude/skills/setup-spa/main.ts b/.claude/skills/setup-spa/main.ts index d90601047..c2b9a0b7e 100644 --- a/.claude/skills/setup-spa/main.ts +++ b/.claude/skills/setup-spa/main.ts @@ -5,11 +5,13 @@ import type { ActionsBlock, ContextBlock, KnownBlock, SectionBlock } from "@slac import type { Block } from "@slack/types"; import type { ToolCall } from "./helpers"; +import { timingSafeEqual } from "node:crypto"; import { isString, toRecord } from "@openrouter/spawn-shared"; import { App } from "@slack/bolt"; import * as v from "valibot"; import { downloadSlackFile, + findCandidate, findThread, formatToolStats, markdownToRichTextBlocks, @@ -20,7 +22,9 @@ import { ResultSchema, runCleanupIfDue, stripMention, + updateCandidateStatus, updateThread, + upsertCandidate, upsertThread, } from "./helpers"; @@ -31,6 +35,11 @@ type SlackClient = InstanceType["client"]; const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN ?? ""; const SLACK_APP_TOKEN = process.env.SLACK_APP_TOKEN ?? ""; const GITHUB_REPO = process.env.GITHUB_REPO ?? "OpenRouterTeam/spawn"; +const TRIGGER_SECRET = process.env.TRIGGER_SECRET ?? ""; +const GROWTH_TRIGGER_URL = process.env.GROWTH_TRIGGER_URL ?? ""; +const GROWTH_REPLY_SECRET = process.env.GROWTH_REPLY_SECRET ?? ""; +const SLACK_CHANNEL_ID = process.env.SLACK_CHANNEL_ID ?? ""; +const HTTP_PORT = Number.parseInt(process.env.HTTP_PORT ?? "3100", 10); for (const [name, value] of Object.entries({ SLACK_BOT_TOKEN, @@ -967,6 +976,470 @@ app.action("cancel_run", async ({ ack, payload }) => { } }); +// --- growth_approve: post draft reply to Reddit --- +app.action("growth_approve", async ({ ack, body, client }) => { + await ack(); + const payload = toRecord( + "actions" in body && Array.isArray(body.actions) ? body.actions[0] : null, + ); + const postId = payload && isString(payload.value) ? payload.value : ""; + if (!postId) return; + + const userId = "user" in body && toRecord(body.user) ? String((toRecord(body.user) ?? {}).id ?? "") : ""; + const candidate = findCandidate(db, postId); + if (!candidate) return; + + if (candidate.status !== "pending") { + await client.chat.postMessage({ + channel: candidate.slackChannel ?? "", + thread_ts: candidate.slackTs ?? undefined, + text: `:warning: Already handled (${candidate.status}${candidate.actionedBy ? ` by <@${candidate.actionedBy}>` : ""})`, + }).catch(() => {}); + return; + } + + updateCandidateStatus(db, postId, { status: "approved", actionedBy: userId }); + + // POST to growth VM to send the Reddit reply + if (!GROWTH_TRIGGER_URL) { + await client.chat.postMessage({ + channel: candidate.slackChannel ?? "", + thread_ts: candidate.slackTs ?? undefined, + text: ":x: GROWTH_TRIGGER_URL not configured — cannot post to Reddit", + }).catch(() => {}); + return; + } + + try { + const res = await fetch(`${GROWTH_TRIGGER_URL}/reply`, { + method: "POST", + headers: { + Authorization: `Bearer ${GROWTH_REPLY_SECRET}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ postId: candidate.postId, replyText: candidate.draftReply }), + }); + + const result = toRecord(await res.json().catch(() => null)); + if (res.ok && result && result.ok) { + const commentUrl = isString(result.commentUrl) ? result.commentUrl : ""; + updateCandidateStatus(db, postId, { + status: "posted", + actionedBy: userId, + postedReply: candidate.draftReply, + redditCommentUrl: commentUrl, + }); + // Update the Slack message — replace buttons with confirmation + if (candidate.slackChannel && candidate.slackTs) { + await replaceButtonsWithStatus( + client, + candidate.slackChannel, + candidate.slackTs, + `:white_check_mark: Posted by <@${userId}>${commentUrl ? ` — <${commentUrl}|view comment>` : ""}`, + ); + } + } else { + const errMsg = isString(result?.error) ? result.error : `HTTP ${res.status}`; + updateCandidateStatus(db, postId, { status: "error", actionedBy: userId }); + await client.chat.postMessage({ + channel: candidate.slackChannel ?? "", + thread_ts: candidate.slackTs ?? undefined, + text: `:x: Reddit reply failed: ${errMsg}`, + }).catch(() => {}); + } + } catch (err) { + updateCandidateStatus(db, postId, { status: "error", actionedBy: userId }); + await client.chat.postMessage({ + channel: candidate.slackChannel ?? "", + thread_ts: candidate.slackTs ?? undefined, + text: `:x: Reddit reply failed: ${err instanceof Error ? err.message : String(err)}`, + }).catch(() => {}); + } +}); + +// --- growth_edit: open modal with draft reply for editing --- +app.action("growth_edit", async ({ ack, body, client }) => { + await ack(); + const payload = toRecord( + "actions" in body && Array.isArray(body.actions) ? body.actions[0] : null, + ); + const postId = payload && isString(payload.value) ? payload.value : ""; + if (!postId) return; + + const triggerId = "trigger_id" in body && isString(body.trigger_id) ? body.trigger_id : ""; + if (!triggerId) return; + + const candidate = findCandidate(db, postId); + if (!candidate) return; + + if (candidate.status !== "pending") { + return; // already handled + } + + await client.views.open({ + trigger_id: triggerId, + view: { + type: "modal", + callback_id: "growth_edit_submit", + private_metadata: postId, + title: { type: "plain_text", text: "Edit Reply" }, + submit: { type: "plain_text", text: "Post to Reddit" }, + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: `*<${candidate.permalink.startsWith("http") ? candidate.permalink : `https://reddit.com${candidate.permalink}`}|${candidate.title}>*\nr/${candidate.subreddit}`, + }, + }, + { + type: "input", + block_id: "reply_block", + label: { type: "plain_text", text: "Reply text" }, + element: { + type: "plain_text_input", + action_id: "reply_text", + multiline: true, + initial_value: candidate.draftReply, + }, + }, + ], + }, + }).catch(() => {}); +}); + +// --- growth_edit_submit: modal submitted with edited reply --- +app.view("growth_edit_submit", async ({ ack, view, body, client }) => { + await ack(); + const postId = view.private_metadata; + if (!postId) return; + + const candidate = findCandidate(db, postId); + if (!candidate || candidate.status !== "pending") return; + + const replyBlock = toRecord(view.state?.values?.reply_block?.reply_text); + const editedReply = replyBlock && isString(replyBlock.value) ? replyBlock.value : ""; + if (!editedReply) return; + + const userId = toRecord(body.user) ? String((toRecord(body.user) ?? {}).id ?? "") : ""; + + updateCandidateStatus(db, postId, { status: "approved", actionedBy: userId }); + + if (!GROWTH_TRIGGER_URL) return; + + try { + const res = await fetch(`${GROWTH_TRIGGER_URL}/reply`, { + method: "POST", + headers: { + Authorization: `Bearer ${GROWTH_REPLY_SECRET}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ postId: candidate.postId, replyText: editedReply }), + }); + + const result = toRecord(await res.json().catch(() => null)); + if (res.ok && result && result.ok) { + const commentUrl = isString(result.commentUrl) ? result.commentUrl : ""; + updateCandidateStatus(db, postId, { + status: "posted", + actionedBy: userId, + postedReply: editedReply, + redditCommentUrl: commentUrl, + }); + if (candidate.slackChannel && candidate.slackTs) { + await replaceButtonsWithStatus( + client, + candidate.slackChannel, + candidate.slackTs, + `:white_check_mark: Posted (edited) by <@${userId}>${commentUrl ? ` — <${commentUrl}|view comment>` : ""}`, + ); + } + } else { + updateCandidateStatus(db, postId, { status: "error", actionedBy: userId }); + if (candidate.slackChannel && candidate.slackTs) { + await client.chat.postMessage({ + channel: candidate.slackChannel, + thread_ts: candidate.slackTs, + text: `:x: Reddit reply failed: ${isString(result?.error) ? result.error : `HTTP ${res.status}`}`, + }).catch(() => {}); + } + } + } catch { + updateCandidateStatus(db, postId, { status: "error", actionedBy: userId }); + } +}); + +// --- growth_skip: skip this candidate --- +app.action("growth_skip", async ({ ack, body, client }) => { + await ack(); + const payload = toRecord( + "actions" in body && Array.isArray(body.actions) ? body.actions[0] : null, + ); + const postId = payload && isString(payload.value) ? payload.value : ""; + if (!postId) return; + + const userId = "user" in body && toRecord(body.user) ? String((toRecord(body.user) ?? {}).id ?? "") : ""; + const candidate = findCandidate(db, postId); + if (!candidate || candidate.status !== "pending") return; + + updateCandidateStatus(db, postId, { status: "skipped", actionedBy: userId }); + + if (candidate.slackChannel && candidate.slackTs) { + await replaceButtonsWithStatus( + client, + candidate.slackChannel, + candidate.slackTs, + `:no_entry_sign: Skipped by <@${userId}>`, + ); + } +}); + +/** Replace the actions block in a candidate card with a status context line. */ +async function replaceButtonsWithStatus( + client: SlackClient, + channel: string, + ts: string, + statusText: string, +): Promise { + try { + // Fetch the current message to get its blocks + const result = await client.conversations.history({ + channel, + latest: ts, + inclusive: true, + limit: 1, + }); + const msg = result.messages?.[0]; + if (!msg) return; + + const blocks = Array.isArray(msg.blocks) ? msg.blocks : []; + // Replace the actions block with a context block showing the status + const updatedBlocks = blocks + .filter((b: Record) => b.type !== "actions") + .concat({ + type: "context", + elements: [{ type: "mrkdwn", text: statusText }], + }); + + await client.chat.update({ + channel, + ts, + text: statusText, + blocks: updatedBlocks, + }); + } catch { + // non-fatal + } +} + +// #endregion + +// #region Growth candidate HTTP server + +/** Valibot schema for incoming candidate JSON from growth agent. */ +const CandidatePayloadSchema = v.object({ + found: v.boolean(), + title: v.optional(v.string()), + url: v.optional(v.string()), + permalink: v.optional(v.string()), + subreddit: v.optional(v.string()), + postId: v.optional(v.string()), + upvotes: v.optional(v.number()), + numComments: v.optional(v.number()), + postedAgo: v.optional(v.string()), + whatTheyAsked: v.optional(v.string()), + whySpawnFits: v.optional(v.string()), + posterQualification: v.optional(v.string()), + relevanceScore: v.optional(v.number()), + draftReply: v.optional(v.string()), + postsScanned: v.optional(v.number()), +}); + +/** Timing-safe auth for the HTTP trigger endpoint. */ +function isHttpAuthed(req: Request): boolean { + if (!TRIGGER_SECRET) return false; + const given = req.headers.get("Authorization") ?? ""; + const expected = `Bearer ${TRIGGER_SECRET}`; + if (given.length !== expected.length) return false; + return timingSafeEqual(Buffer.from(given), Buffer.from(expected)); +} + +/** Post a Block Kit candidate card to Slack and store in DB. */ +async function postCandidateCard( + client: SlackClient, + candidate: v.InferOutput, +): Promise { + const channel = SLACK_CHANNEL_ID; + if (!channel) { + return Response.json({ error: "SLACK_CHANNEL_ID not configured" }, { status: 500 }); + } + + if (!candidate.found) { + // No candidate — post brief summary + const scanText = candidate.postsScanned + ? `Growth scan complete — scanned ${candidate.postsScanned} posts, no candidates today.` + : "Growth scan complete — no candidates today."; + await client.chat.postMessage({ + channel, + text: scanText, + blocks: [ + { type: "context", elements: [{ type: "mrkdwn", text: scanText }] }, + ], + }).catch(() => {}); + return Response.json({ ok: true, action: "no_candidate" }); + } + + // Candidate found — build Block Kit card + const title = candidate.title ?? "Untitled"; + const url = candidate.url ?? `https://reddit.com${candidate.permalink ?? ""}`; + const postId = candidate.postId ?? ""; + const subreddit = candidate.subreddit ?? ""; + const upvotes = candidate.upvotes ?? 0; + const numComments = candidate.numComments ?? 0; + const postedAgo = candidate.postedAgo ?? ""; + const draftReply = candidate.draftReply ?? ""; + + const blocks: (KnownBlock | Block)[] = [ + { + type: "header", + text: { type: "plain_text", text: "Reddit Growth — Candidate Found", emoji: true }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `*<${url}|${title}>*\nr/${subreddit} | ${upvotes} upvotes | ${numComments} comments | ${postedAgo}`, + }, + }, + ]; + + if (candidate.whatTheyAsked) { + blocks.push({ + type: "section", + text: { type: "mrkdwn", text: `*What they asked:*\n${candidate.whatTheyAsked}` }, + }); + } + + if (candidate.whySpawnFits) { + blocks.push({ + type: "section", + text: { type: "mrkdwn", text: `*Why Spawn fits:*\n${candidate.whySpawnFits}` }, + }); + } + + if (candidate.posterQualification) { + blocks.push({ + type: "section", + text: { type: "mrkdwn", text: `*Poster signals:*\n${candidate.posterQualification}` }, + }); + } + + if (draftReply) { + blocks.push({ + type: "section", + text: { type: "mrkdwn", text: `*Draft reply:*\n>${draftReply.replace(/\n/g, "\n>")}` }, + }); + } + + if (candidate.relevanceScore !== undefined) { + blocks.push({ + type: "context", + elements: [{ type: "mrkdwn", text: `Relevance: ${candidate.relevanceScore}/10` }], + }); + } + + // Action buttons + blocks.push({ + type: "actions", + elements: [ + { + type: "button", + text: { type: "plain_text", text: "Approve", emoji: true }, + style: "primary", + action_id: "growth_approve", + value: postId, + }, + { + type: "button", + text: { type: "plain_text", text: "Edit", emoji: true }, + action_id: "growth_edit", + value: postId, + }, + { + type: "button", + text: { type: "plain_text", text: "Skip", emoji: true }, + style: "danger", + action_id: "growth_skip", + value: postId, + }, + ], + }); + + const msg = await client.chat.postMessage({ + channel, + text: `Reddit Growth — ${title}`, + blocks, + }); + + // Store candidate in DB + upsertCandidate(db, { + postId, + permalink: candidate.permalink ?? "", + title, + subreddit, + draftReply, + slackChannel: channel, + slackTs: msg.ts ?? undefined, + status: "pending", + createdAt: new Date().toISOString(), + }); + + return Response.json({ ok: true, action: "posted", ts: msg.ts }); +} + +/** Start the HTTP server for growth candidate ingestion. */ +function startHttpServer(client: SlackClient): void { + if (!TRIGGER_SECRET) { + console.log("[spa] TRIGGER_SECRET not set — HTTP server disabled"); + return; + } + + Bun.serve({ + port: HTTP_PORT, + async fetch(req) { + const url = new URL(req.url); + + if (req.method === "GET" && url.pathname === "/health") { + return Response.json({ status: "ok" }); + } + + if (req.method === "POST" && url.pathname === "/candidate") { + if (!isHttpAuthed(req)) { + return Response.json({ error: "unauthorized" }, { status: 401 }); + } + + let body: unknown; + try { + body = await req.json(); + } catch { + return Response.json({ error: "invalid JSON" }, { status: 400 }); + } + + const parsed = v.safeParse(CandidatePayloadSchema, body); + if (!parsed.success) { + return Response.json({ error: "invalid payload", issues: parsed.issues }, { status: 400 }); + } + + return postCandidateCard(client, parsed.output); + } + + return Response.json({ error: "not found" }, { status: 404 }); + }, + }); + + console.log(`[spa] HTTP server listening on port ${HTTP_PORT}`); +} + // #endregion // #region Graceful shutdown @@ -1002,6 +1475,7 @@ process.on("SIGINT", () => shutdown("SIGINT")); } await app.start(); + startHttpServer(app.client); console.log(`[spa] Running (any channel + DMs, repo=${GITHUB_REPO})`); })(); diff --git a/.claude/skills/setup-spa/spa.test.ts b/.claude/skills/setup-spa/spa.test.ts index 0802e067d..5d68e5de4 100644 --- a/.claude/skills/setup-spa/spa.test.ts +++ b/.claude/skills/setup-spa/spa.test.ts @@ -1,4 +1,4 @@ -import type { ToolCall } from "./helpers"; +import type { CandidateRow, ToolCall } from "./helpers"; import { afterEach, describe, expect, it, mock } from "bun:test"; import { toRecord } from "@openrouter/spawn-shared"; @@ -7,6 +7,7 @@ import { downloadSlackFile, extractMarkdownTables, extractToolHint, + findCandidate, findThread, formatToolHistory, formatToolStats, @@ -21,6 +22,8 @@ import { parseStreamEvent, plainTextFallback, stripMention, + updateCandidateStatus, + upsertCandidate, upsertThread, } from "./helpers"; @@ -1046,3 +1049,95 @@ describe("markdownTableToSlackBlock", () => { expect(block?.rows[1][2].text).toBe(""); }); }); + +// #region Candidate DB tests + +function makeCandidate(overrides: Partial = {}): CandidateRow { + return { + postId: "t3_abc123", + permalink: "/r/SelfHosted/comments/abc123/test", + title: "How to run coding agents on cloud?", + subreddit: "SelfHosted", + draftReply: "check out spawn, it does exactly this. disclosure: i help build this", + status: "pending", + createdAt: new Date().toISOString(), + ...overrides, + }; +} + +describe("candidates table", () => { + it("upsertCandidate and findCandidate round-trip", () => { + const db = openDb(":memory:"); + const candidate = makeCandidate(); + upsertCandidate(db, candidate); + const found = findCandidate(db, "t3_abc123"); + expect(found).toBeTruthy(); + expect(found?.postId).toBe("t3_abc123"); + expect(found?.title).toBe("How to run coding agents on cloud?"); + expect(found?.subreddit).toBe("SelfHosted"); + expect(found?.draftReply).toContain("spawn"); + expect(found?.status).toBe("pending"); + db.close(); + }); + + it("findCandidate returns undefined for missing post", () => { + const db = openDb(":memory:"); + expect(findCandidate(db, "t3_nonexistent")).toBeUndefined(); + db.close(); + }); + + it("upsertCandidate updates Slack coordinates on conflict", () => { + const db = openDb(":memory:"); + upsertCandidate(db, makeCandidate()); + upsertCandidate(db, makeCandidate({ slackChannel: "C123", slackTs: "1234.5678" })); + const found = findCandidate(db, "t3_abc123"); + expect(found?.slackChannel).toBe("C123"); + expect(found?.slackTs).toBe("1234.5678"); + db.close(); + }); + + it("updateCandidateStatus changes status and sets actioned fields", () => { + const db = openDb(":memory:"); + upsertCandidate(db, makeCandidate()); + updateCandidateStatus(db, "t3_abc123", { + status: "posted", + actionedBy: "U789", + postedReply: "the actual reply text", + redditCommentUrl: "https://reddit.com/r/SelfHosted/comments/abc123/test/def456", + }); + const found = findCandidate(db, "t3_abc123"); + expect(found?.status).toBe("posted"); + expect(found?.actionedBy).toBe("U789"); + expect(found?.actionedAt).toBeTruthy(); + expect(found?.postedReply).toBe("the actual reply text"); + expect(found?.redditCommentUrl).toContain("def456"); + db.close(); + }); + + it("updateCandidateStatus to skipped", () => { + const db = openDb(":memory:"); + upsertCandidate(db, makeCandidate()); + updateCandidateStatus(db, "t3_abc123", { + status: "skipped", + actionedBy: "U111", + }); + const found = findCandidate(db, "t3_abc123"); + expect(found?.status).toBe("skipped"); + expect(found?.actionedBy).toBe("U111"); + db.close(); + }); + + it("updateCandidateStatus to error", () => { + const db = openDb(":memory:"); + upsertCandidate(db, makeCandidate()); + updateCandidateStatus(db, "t3_abc123", { + status: "error", + actionedBy: "U222", + }); + const found = findCandidate(db, "t3_abc123"); + expect(found?.status).toBe("error"); + db.close(); + }); +}); + +// #endregion From 9be80a150cf5652ac95a0486d1495e1c1dc7d7ee Mon Sep 17 00:00:00 2001 From: Ahmed Abushagur Date: Thu, 2 Apr 2026 22:12:51 -0700 Subject: [PATCH 6/6] fix: address security review findings on growth agent - chmod 0600 temp prompt file to prevent credential exposure - Use stdin redirect instead of $(cat) for claude -p to avoid shell expansion - Use curl --data-binary @- heredoc instead of -d to prevent command injection - Move reply.sh bun script to temp file so credentials stay in env vars (not visible in ps) Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/setup-agent-team/growth.sh | 5 +++-- .claude/skills/setup-agent-team/reply.sh | 24 ++++++++++++++--------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/.claude/skills/setup-agent-team/growth.sh b/.claude/skills/setup-agent-team/growth.sh index 1f19f6a32..b37845d29 100644 --- a/.claude/skills/setup-agent-team/growth.sh +++ b/.claude/skills/setup-agent-team/growth.sh @@ -80,6 +80,7 @@ claude update --yes 2>&1 | tee -a "${LOG_FILE}" || log "WARNING: Claude Code upd log "Launching growth cycle..." PROMPT_FILE=$(mktemp /tmp/growth-prompt-XXXXXX.md) +chmod 0600 "${PROMPT_FILE}" PROMPT_TEMPLATE="${SCRIPT_DIR}/growth-prompt.md" if [[ ! -f "$PROMPT_TEMPLATE" ]]; then @@ -98,7 +99,7 @@ safe_substitute "REDDIT_PASSWORD_PLACEHOLDER" "${REDDIT_PASSWORD:-}" "${PROMPT_F log "Hard timeout: ${HARD_TIMEOUT}s" # Run claude in background -claude -p "$(cat "${PROMPT_FILE}")" --dangerously-skip-permissions --model sonnet >> "${LOG_FILE}" 2>&1 & +claude -p - --dangerously-skip-permissions --model sonnet < "${PROMPT_FILE}" >> "${LOG_FILE}" 2>&1 & CLAUDE_PID=$! log "Claude started (pid=${CLAUDE_PID})" @@ -159,7 +160,7 @@ if [[ -n "${SPA_TRIGGER_URL:-}" && -n "${SPA_TRIGGER_SECRET:-}" ]]; then -X POST "${SPA_TRIGGER_URL}/candidate" \ -H "Authorization: Bearer ${SPA_TRIGGER_SECRET}" \ -H "Content-Type: application/json" \ - -d "${CANDIDATE_JSON}" \ + --data-binary @- <<< "${CANDIDATE_JSON}" \ --max-time 30) || HTTP_STATUS="000" log "SPA response: HTTP ${HTTP_STATUS}" else diff --git a/.claude/skills/setup-agent-team/reply.sh b/.claude/skills/setup-agent-team/reply.sh index c0556c5e2..1127824bc 100755 --- a/.claude/skills/setup-agent-team/reply.sh +++ b/.claude/skills/setup-agent-team/reply.sh @@ -28,13 +28,16 @@ if [[ -z "${REDDIT_CLIENT_ID:-}" || -z "${REDDIT_CLIENT_SECRET:-}" || -z "${REDD fi # Use bun to authenticate + post comment (avoids shell escaping issues with reply text) -exec bun -e " -const clientId = process.env.REDDIT_CLIENT_ID; -const clientSecret = process.env.REDDIT_CLIENT_SECRET; -const username = process.env.REDDIT_USERNAME; -const password = process.env.REDDIT_PASSWORD; -const postId = process.env.POST_ID; -const replyText = process.env.REPLY_TEXT; +# Write script to temp file so credentials stay in env vars, not visible in ps output +REPLY_SCRIPT=$(mktemp /tmp/reply-XXXXXX.ts) +chmod 0600 "${REPLY_SCRIPT}" +cat > "${REPLY_SCRIPT}" <<'EOSCRIPT' +const clientId = process.env.REDDIT_CLIENT_ID!; +const clientSecret = process.env.REDDIT_CLIENT_SECRET!; +const username = process.env.REDDIT_USERNAME!; +const password = process.env.REDDIT_PASSWORD!; +const postId = process.env.POST_ID!; +const replyText = process.env.REPLY_TEXT!; const auth = Buffer.from(clientId + ':' + clientSecret).toString('base64'); const userAgent = 'spawn-growth:v1.0.0 (by /u/' + username + ')'; @@ -82,7 +85,6 @@ if (!commentRes.ok) { const commentData = await commentRes.json(); // Extract the comment URL from Reddit's response -const things = commentData?.jquery?.flat?.() ?? []; const commentThing = commentData?.json?.data?.things?.[0]?.data; const commentId = commentThing?.id ?? commentThing?.name ?? ''; const commentPermalink = commentThing?.permalink ?? ''; @@ -93,4 +95,8 @@ console.log(JSON.stringify({ commentId, commentUrl, })); -" +EOSCRIPT + +cleanup_reply() { rm -f "${REPLY_SCRIPT}" 2>/dev/null || true; } +trap cleanup_reply EXIT +exec bun run "${REPLY_SCRIPT}"