Skip to content
Merged
212 changes: 212 additions & 0 deletions .claude/skills/setup-agent-team/growth-prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
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 output a summary. You do NOT post replies. You only find and report.

## 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`

## 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: Score for relevance

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
- 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 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:

```
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 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. Say "no candidates this cycle" and stop.

## Step 6: Output summary

Print a structured summary of what you found. This goes to the log file.

**If a candidate was found:**

```
=== GROWTH CANDIDATE FOUND ===
Thread: {post_title}
URL: https://reddit.com{permalink}
Subreddit: r/{subreddit}
Upvotes: {score} | Comments: {num_comments}
Posted: {time_ago}

What they asked:
{brief summary of their question}

Why Spawn fits:
{1-2 sentences}

Poster qualification:
{signals found in their history}

Relevance score: {score}/10

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 ===
```

**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:**

```
=== GROWTH SCAN COMPLETE ===
Posts scanned: {total}
Scored 7+: 0
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.
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

Complete within 25 minutes. If still searching at 20 minutes, stop and report what you have.
168 changes: 168 additions & 0 deletions .claude/skills/setup-agent-team/growth.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
#!/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 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)"
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)
chmod 0600 "${PROMPT_FILE}"
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}"
Comment thread
AhmedTMM marked this conversation as resolved.

log "Hard timeout: ${HARD_TIMEOUT}s"

# Run claude in background
claude -p - --dangerously-skip-permissions --model sonnet < "${PROMPT_FILE}" >> "${LOG_FILE}" 2>&1 &
Comment thread
AhmedTMM marked this conversation as resolved.
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

# --- 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" \
--data-binary @- <<< "${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
Loading
Loading