diff --git a/.claude/skills/setup-agent-team/growth-prompt.md b/.claude/skills/setup-agent-team/growth-prompt.md index effa7c3d..43222cc5 100644 --- a/.claude/skills/setup-agent-team/growth-prompt.md +++ b/.claude/skills/setup-agent-team/growth-prompt.md @@ -6,6 +6,14 @@ Your job: from the pre-fetched Reddit posts below, find the ONE best thread wher **IMPORTANT: Do NOT use any tools.** All data is provided below. Your entire response should be plain text output — no bash commands, no file reads, no tool calls. Just analyze the data and respond with your findings. +## Past decisions + +The team has reviewed previous candidates. Learn from these patterns — what got approved, what got skipped, and how replies were edited. Prefer posts similar to approved ones and avoid patterns seen in skipped ones. + +``` +DECISIONS_PLACEHOLDER +``` + ## Pre-fetched Reddit data The following posts were fetched automatically. Each post includes the title, selftext, subreddit, engagement stats, and the poster's recent comment history. diff --git a/.claude/skills/setup-agent-team/growth.sh b/.claude/skills/setup-agent-team/growth.sh index a8eb5cae..0bf1148a 100644 --- a/.claude/skills/setup-agent-team/growth.sh +++ b/.claude/skills/setup-agent-team/growth.sh @@ -82,10 +82,16 @@ fi # Inject Reddit data into prompt template REDDIT_JSON=$(cat "${REDDIT_DATA_FILE}") # Use bun for safe substitution to avoid sed escaping issues with JSON +DECISIONS_FILE="${HOME}/.config/spawn/growth-decisions.md" bun -e " +import { existsSync } from 'node:fs'; const template = await Bun.file('${PROMPT_TEMPLATE}').text(); const data = await Bun.file('${REDDIT_DATA_FILE}').text(); -const result = template.replace('REDDIT_DATA_PLACEHOLDER', data.trim()); +const decisionsPath = '${DECISIONS_FILE}'; +const decisions = existsSync(decisionsPath) ? await Bun.file(decisionsPath).text() : 'No past decisions yet.'; +const result = template + .replace('REDDIT_DATA_PLACEHOLDER', data.trim()) + .replace('DECISIONS_PLACEHOLDER', decisions.trim()); await Bun.write('${PROMPT_FILE}', result); " diff --git a/.claude/skills/setup-spa/helpers.ts b/.claude/skills/setup-spa/helpers.ts index 1e566810..a2cf6fae 100644 --- a/.claude/skills/setup-spa/helpers.ts +++ b/.claude/skills/setup-spa/helpers.ts @@ -5,7 +5,16 @@ import type { Result } from "@openrouter/spawn-shared"; import type { Block } from "@slack/bolt"; import { Database } from "bun:sqlite"; -import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; +import { + appendFileSync, + existsSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; import { dirname } from "node:path"; import { Err, isString, Ok, toRecord } from "@openrouter/spawn-shared"; import { slackifyMarkdown } from "slackify-markdown"; @@ -299,9 +308,10 @@ function rowToCandidate(r: RawCandidate): CandidateRow { 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", + 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, @@ -335,7 +345,12 @@ export function upsertCandidate(db: Database, candidate: CandidateRow): void { /** 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 = ?") + .query< + RawCandidate, + [ + string, + ] + >("SELECT * FROM candidates WHERE post_id = ?") .get(postId); return row ? rowToCandidate(row) : undefined; } @@ -370,6 +385,43 @@ export function updateCandidateStatus( ); } +const DECISIONS_PATH = `${process.env.HOME ?? "/tmp"}/.config/spawn/growth-decisions.md`; + +/** Append a decision entry to the growth decisions log. */ +export function logDecision( + candidate: CandidateRow, + decision: "approved" | "edited" | "skipped", + editedReply?: string, +): void { + const dir = dirname(DECISIONS_PATH); + if (!existsSync(dir)) + mkdirSync(dir, { + recursive: true, + }); + + const date = new Date().toISOString().split("T")[0]; + const reply = editedReply ?? candidate.draftReply; + const entry = ` +## ${decision.toUpperCase()} — ${date} + +- **Post**: [${candidate.title}](https://reddit.com${candidate.permalink}) +- **Subreddit**: r/${candidate.subreddit} +- **Decision**: ${decision} +${editedReply ? "- **Edited**: yes (original draft was modified)\n" : ""}\ +- **Reply**: ${reply.replace(/\n/g, " ")} + +--- +`; + + appendFileSync(DECISIONS_PATH, entry); +} + +/** Read the decisions log (returns empty string if no file). */ +export function readDecisions(): string { + if (!existsSync(DECISIONS_PATH)) return ""; + return readFileSync(DECISIONS_PATH, "utf-8"); +} + // #endregion // #region Claude Code stream parsing diff --git a/.claude/skills/setup-spa/main.ts b/.claude/skills/setup-spa/main.ts index 02ecb4a3..c0a6f53d 100644 --- a/.claude/skills/setup-spa/main.ts +++ b/.claude/skills/setup-spa/main.ts @@ -14,12 +14,14 @@ import { findCandidate, findThread, formatToolStats, + logDecision, markdownToRichTextBlocks, openDb, PR_URL_REGEX, parseStreamEvent, plainTextFallback, ResultSchema, + readDecisions, runCleanupIfDue, stripMention, updateCandidateStatus, @@ -1042,6 +1044,7 @@ app.action("growth_approve", async ({ ack, body, client }) => { postedReply: candidate.draftReply, redditCommentUrl: commentUrl, }); + logDecision(candidate, "approved"); // Update the Slack message — replace buttons with confirmation if (candidate.slackChannel && candidate.slackTs) { await replaceButtonsWithStatus( @@ -1184,6 +1187,7 @@ app.view("growth_edit_submit", async ({ ack, view, body, client }) => { postedReply: editedReply, redditCommentUrl: commentUrl, }); + logDecision(candidate, "edited", editedReply); if (candidate.slackChannel && candidate.slackTs) { await replaceButtonsWithStatus( client, @@ -1230,6 +1234,7 @@ app.action("growth_skip", async ({ ack, body, client }) => { status: "skipped", actionedBy: userId, }); + logDecision(candidate, "skipped"); if (candidate.slackChannel && candidate.slackTs) { await replaceButtonsWithStatus(