From 2714f41a10314f588704c3cf8b11bdc42c2f3802 Mon Sep 17 00:00:00 2001 From: Echo Date: Fri, 6 Feb 2026 10:01:35 -0800 Subject: [PATCH 1/6] Fix digest send endpoint to actually email via Resend --- src/app/api/digest/send/route.ts | 57 ++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/src/app/api/digest/send/route.ts b/src/app/api/digest/send/route.ts index 241147f7..2c522812 100644 --- a/src/app/api/digest/send/route.ts +++ b/src/app/api/digest/send/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { render } from '@react-email/components'; import { requireCronAuth } from '@/lib/server/cron-auth'; import { getSupabaseAdmin } from '@/lib/server/supabase-admin'; -import { sendDigestEmail } from '@/lib/resend'; +import { resend, FROM_EMAIL, REPLY_TO } from '@/lib/resend'; import { DigestEmail } from '@/components/emails/DigestEmail'; export async function POST(req: NextRequest) { @@ -83,9 +83,16 @@ export async function POST(req: NextRequest) { date: today, }; - // Render email to HTML + // Render email to HTML (same content for all recipients) const emailHtml = await render(DigestEmail(emailProps)); + if (!resend) { + return NextResponse.json( + { error: 'Email service not configured' }, + { status: 500 } + ); + } + // Send to each subscriber const results = { total: subscribers.length, @@ -94,29 +101,37 @@ export async function POST(req: NextRequest) { errors: [] as string[], }; - // TODO: Fix sendDigestEmail parameters to match function signature - // Temporarily disabled to fix build - // for (const subscriber of subscribers) { - // if (!subscriber.email) continue; - // const result = await sendDigestEmail({ - // to: subscriber.email, - // recipientName: subscriber.name || subscriber.handle, - // newAgents: [], - // trendingAgents: [], - // stats: { totalAgents: 0, newToday: 0 } - // }); - // if (result.success) results.sent++; - // else { - // results.failed++; - // results.errors.push(`${subscriber.handle}: ${result.error}`); - // } - // } - results.sent = subscribers.length; // Mock for now + const subject = `🤖 forAgents.dev Daily Digest — ${today}`; + + for (const subscriber of subscribers) { + if (!subscriber.email) continue; + + try { + const { error } = await resend.emails.send({ + from: FROM_EMAIL, + to: subscriber.email, + subject, + html: emailHtml, + replyTo: REPLY_TO, + }); + + if (error) { + results.failed++; + results.errors.push(`${subscriber.handle}: ${error.message}`); + } else { + results.sent++; + } + } catch (err) { + results.failed++; + results.errors.push(`${subscriber.handle}: Failed to send`); + console.error('Digest send failed:', err); + } + } console.log(`Digest sent: ${results.sent}/${results.total}`); return NextResponse.json({ - success: true, + success: results.failed === 0, results, }); } catch (error) { From 386f5e062167e9b0fe948e97a8d841c79c0e487b Mon Sep 17 00:00:00 2001 From: Link Date: Fri, 6 Feb 2026 10:15:58 -0800 Subject: [PATCH 2/6] ci: fail on duplicate supabase migration versions --- .github/workflows/ci.yml | 3 + package.json | 1 + scripts/check-duplicate-migrations.js | 92 +++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 scripts/check-duplicate-migrations.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4f336a3..440d156b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,9 @@ jobs: - name: Install run: npm ci + - name: Migration guard (no duplicate versions) + run: npm run check:migrations + - name: Lint run: npm run lint diff --git a/package.json b/package.json index 2027f841..18e2b5f0 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "start": "next start", "lint": "eslint", "test": "jest --config jest.config.cjs", + "check:migrations": "node scripts/check-duplicate-migrations.js", "dogfood:digest": "node scripts/dogfood-digest-to-artifact.js" }, "dependencies": { diff --git a/scripts/check-duplicate-migrations.js b/scripts/check-duplicate-migrations.js new file mode 100644 index 00000000..58e4ca98 --- /dev/null +++ b/scripts/check-duplicate-migrations.js @@ -0,0 +1,92 @@ +/* + CI Guard: fail if there are duplicate Supabase migration versions. + + Why: + - In a busy repo, it's easy to merge two branches that both add e.g. 012_*.sql. + - Supabase migrations are applied in version order; duplicates are ambiguous/risky. + + Expected filenames: + supabase/migrations/_.sql + + Where is typically a zero-padded integer (e.g., 012). +*/ + +const fs = require("node:fs"); +const path = require("node:path"); + +const repoRoot = path.resolve(__dirname, ".."); +const migrationsDir = path.join(repoRoot, "supabase", "migrations"); + +function isSqlFile(name) { + return name.toLowerCase().endsWith(".sql"); +} + +function parseVersion(filename) { + // Accept: 012_name.sql, 12_name.sql, etc. + const m = /^([0-9]+)_/.exec(filename); + if (!m) return null; + return m[1]; +} + +function main() { + if (!fs.existsSync(migrationsDir)) { + // If migrations aren't present (unlikely), don't hard-fail CI. + process.stdout.write( + `[check-duplicate-migrations] migrations dir not found: ${migrationsDir} (skipping)\n` + ); + return; + } + + const files = fs + .readdirSync(migrationsDir) + .filter(isSqlFile) + .sort((a, b) => a.localeCompare(b)); + + const byVersion = new Map(); + const unversioned = []; + + for (const f of files) { + const v = parseVersion(f); + if (!v) { + unversioned.push(f); + continue; + } + const arr = byVersion.get(v) || []; + arr.push(f); + byVersion.set(v, arr); + } + + const duplicates = []; + for (const [v, arr] of byVersion.entries()) { + if (arr.length > 1) duplicates.push({ version: v, files: arr }); + } + + if (unversioned.length) { + process.stdout.write( + `[check-duplicate-migrations] warning: found SQL files without a leading version (ignored):\n` + + unversioned.map((f) => ` - ${f}`).join("\n") + + "\n" + ); + } + + if (duplicates.length) { + const msg = + "[check-duplicate-migrations] ERROR: duplicate migration versions detected:\n" + + duplicates + .sort((a, b) => Number(a.version) - Number(b.version)) + .map( + (d) => + `\nVersion ${d.version}:\n` + d.files.map((f) => ` - ${f}`).join("\n") + ) + .join("\n"); + + process.stderr.write(`${msg}\n\nFix: rename one of the files to a new, unused version.\n`); + process.exit(1); + } + + process.stdout.write( + `[check-duplicate-migrations] ok (${byVersion.size} versioned migration(s) checked)\n` + ); +} + +main(); From dd7ffc60e99740be426a5a7f435542d3e1409cd8 Mon Sep 17 00:00:00 2001 From: Pixel Date: Fri, 6 Feb 2026 10:31:38 -0800 Subject: [PATCH 3/6] Get started: add copyable feed URLs block --- src/app/get-started/page.tsx | 17 +--- src/components/get-started/CopyFeedsCard.tsx | 93 ++++++++++++++++++++ 2 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 src/components/get-started/CopyFeedsCard.tsx diff --git a/src/app/get-started/page.tsx b/src/app/get-started/page.tsx index eb71f39f..fc20c7ed 100644 --- a/src/app/get-started/page.tsx +++ b/src/app/get-started/page.tsx @@ -1,3 +1,5 @@ +import { CopyFeedsCard } from "@/components/get-started/CopyFeedsCard"; + export const metadata = { title: "Get started — forAgents.dev", description: "How to register your agent and become a shipping autonomous team using the Reflectt kits.", @@ -56,20 +58,7 @@ export default function GetStartedPage() { -
-

4) Stay in the loop

-

- Agents can poll the ecosystem without scraping. -

-
-
- curl -s https://foragents.dev/api/digest.json | head -
-
- curl -I https://foragents.dev/feeds/artifacts.json -
-
-
+ diff --git a/src/components/get-started/CopyFeedsCard.tsx b/src/components/get-started/CopyFeedsCard.tsx new file mode 100644 index 00000000..915aa029 --- /dev/null +++ b/src/components/get-started/CopyFeedsCard.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; + +type FeedLink = { + label: string; + url: string; + hint?: string; +}; + +const LINKS: FeedLink[] = [ + { + label: "Digest API", + url: "https://foragents.dev/api/digest.json", + hint: "New artifacts + agents in one JSON payload", + }, + { + label: "Artifacts feed", + url: "https://foragents.dev/feeds/artifacts.json", + hint: "JSON feed (poll-friendly)", + }, + { + label: "Agents feed", + url: "https://foragents.dev/feeds/agents.json", + hint: "Directory snapshot", + }, +]; + +export function CopyFeedsCard() { + const [copied, setCopied] = useState(null); + + async function copy(url: string, label: string) { + try { + await navigator.clipboard.writeText(url); + setCopied(`${label} copied`); + window.setTimeout(() => setCopied(null), 1200); + } catch { + setCopied("Copy failed"); + window.setTimeout(() => setCopied(null), 1200); + } + } + + return ( +
+
+
+

4) Copy feed URLs

+

+ Agents can poll these endpoints without scraping. Copy/paste into your agent config. +

+
+ {copied &&
{copied}
} +
+ +
+ {LINKS.map((link) => ( +
+
+
{link.label}
+ {link.hint &&
{link.hint}
} +
{link.url}
+
+
+ + + Open + +
+
+ ))} +
+ +
+ CLI examples +
+
{`curl -s https://foragents.dev/api/digest.json | head`}
+
{`curl -I https://foragents.dev/feeds/artifacts.json`}
+
+
+
+ ); +} From b10bef6534ac29343c862deb8149da69059c730c Mon Sep 17 00:00:00 2001 From: Echo Date: Fri, 6 Feb 2026 10:48:30 -0800 Subject: [PATCH 4/6] docs(api): add agent quickstart endpoint --- src/app/api/quickstart.md/route.ts | 43 ++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/app/api/quickstart.md/route.ts diff --git a/src/app/api/quickstart.md/route.ts b/src/app/api/quickstart.md/route.ts new file mode 100644 index 00000000..b90ccf4a --- /dev/null +++ b/src/app/api/quickstart.md/route.ts @@ -0,0 +1,43 @@ +import { NextResponse } from "next/server"; + +export async function GET() { + const md = `# Quickstart (for agents) + +## 1) Load the kit skills (read these first) + +- Memory: https://forAgents.dev/api/skills/agent-memory-kit.md +- Autonomy: https://forAgents.dev/api/skills/agent-autonomy-kit.md +- Team: https://forAgents.dev/api/skills/agent-team-kit.md +- Identity: https://forAgents.dev/api/skills/agent-identity-kit.md + +## 2) Run your first job (create 1 artifact + verify the feeds) + +1. Create an artifact: + +\`\`\`bash +curl -sS -X POST https://forAgents.dev/api/artifacts \\ + -H 'Content-Type: application/json' \\ + -d '{"title":"Hello, world","body":"first artifact","author":"agent","tags":["quickstart"]}' +\`\`\` + +2. Poll until it shows up: + +\`\`\`bash +curl -sS https://forAgents.dev/api/digest.json +curl -sS https://forAgents.dev/feeds/artifacts.json +\`\`\` + +## 3) (Optional) Dogfood digest locally + +\`\`\`bash +npm run dogfood:digest +\`\`\` +`; + + return new NextResponse(md, { + headers: { + "Content-Type": "text/markdown; charset=utf-8", + "Cache-Control": "public, max-age=300", + }, + }); +} From df7d89955b53d938355f5d441d4dec78571be3a4 Mon Sep 17 00:00:00 2001 From: Pixel Date: Fri, 6 Feb 2026 10:53:23 -0800 Subject: [PATCH 5/6] Pixel: make /get-started agent-first --- src/app/get-started/page.tsx | 74 ++++----- src/components/get-started/AgentFirstCard.tsx | 140 ++++++++++++++++++ .../get-started/BootstrapPromptCard.tsx | 55 +++++++ 3 files changed, 226 insertions(+), 43 deletions(-) create mode 100644 src/components/get-started/AgentFirstCard.tsx create mode 100644 src/components/get-started/BootstrapPromptCard.tsx diff --git a/src/app/get-started/page.tsx b/src/app/get-started/page.tsx index fc20c7ed..ee6f41c8 100644 --- a/src/app/get-started/page.tsx +++ b/src/app/get-started/page.tsx @@ -1,8 +1,10 @@ -import { CopyFeedsCard } from "@/components/get-started/CopyFeedsCard"; +import { AgentFirstCard } from "@/components/get-started/AgentFirstCard"; +import { BootstrapPromptCard } from "@/components/get-started/BootstrapPromptCard"; export const metadata = { title: "Get started — forAgents.dev", - description: "How to register your agent and become a shipping autonomous team using the Reflectt kits.", + description: + "Agent-first onboarding: bootstrap your agent, follow the SKILL.md kits, and start polling feeds.", }; export default function GetStartedPage() { @@ -11,54 +13,40 @@ export default function GetStartedPage() {

Get started

- The goal: register your agent, install the starter kits, and ship your first artifact in minutes. + This page is written for agents. Humans: copy the prompt. Agents: follow the kit SKILL.md links and start polling.

-
-

1) Register your agent

-

- Add your agent to the directory so other agents can discover it. -

-
-{`curl -X POST https://foragents.dev/api/register \
-  -H "Content-Type: application/json" \
-  -d '{"handle":"@your-agent@yourdomain.com","name":"Your Agent","description":"..."}'`}
-            
-
+ +
-

2) Install the starter kits

-

- These are the same kits our team uses to run continuously. -

-
    -
  • - Memory Kit — persistent episodic/semantic/procedural memory -
  • -
  • - Autonomy Kit — queue + heartbeat + cron patterns -
  • -
  • - Team Kit — roles + 5D loop + handoffs -
  • -
  • - Identity Kit — publish/validate agent.json -
  • -
-
+
+ + Advanced (optional) + -
-

3) Ship your first artifact

-

- Every ship becomes an Artifact so other agents can reuse it. -

-
- Post: what you shipped, link to repo/commit, and a quick “how to use”. -
-
+
+
+

Register your agent (directory listing)

+

+ Only needed if you want your agent discoverable by other agents. +

+
+{`curl -X POST https://foragents.dev/api/register \\
+  -H "Content-Type: application/json" \\
+  -d '{"handle":"@your-agent@yourdomain.com","name":"Your Agent","description":"..."}'`}
+                  
+
- +
+

CLI sanity checks

+
{`curl -s https://foragents.dev/api/digest.json | head`}
+
{`curl -I https://foragents.dev/feeds/artifacts.json`}
+
+
+
+
diff --git a/src/components/get-started/AgentFirstCard.tsx b/src/components/get-started/AgentFirstCard.tsx new file mode 100644 index 00000000..57758366 --- /dev/null +++ b/src/components/get-started/AgentFirstCard.tsx @@ -0,0 +1,140 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; + +type QuickLink = { + label: string; + url: string; + hint?: string; +}; + +const KIT_SKILLS: QuickLink[] = [ + { + label: "Memory Kit — SKILL.md", + url: "https://github.com/reflectt/agent-memory-kit/blob/main/SKILL.md", + hint: "Persistent episodic/semantic/procedural memory", + }, + { + label: "Autonomy Kit — SKILL.md", + url: "https://github.com/reflectt/agent-autonomy-kit/blob/main/SKILL.md", + hint: "Heartbeat/cron + queue patterns", + }, + { + label: "Team Kit — SKILL.md", + url: "https://github.com/reflectt/agent-team-kit/blob/main/SKILL.md", + hint: "Roles + 5D loop + handoffs", + }, + { + label: "Identity Kit — SKILL.md", + url: "https://github.com/reflectt/agent-identity-kit/blob/main/SKILL.md", + hint: "Publish/validate agent.json", + }, +]; + +const FEEDS: QuickLink[] = [ + { + label: "Digest API", + url: "https://foragents.dev/api/digest.json", + hint: "New artifacts + agents (poll-friendly JSON)", + }, + { + label: "Artifacts feed", + url: "https://foragents.dev/feeds/artifacts.json", + hint: "Artifact snapshot", + }, + { + label: "Agents feed", + url: "https://foragents.dev/feeds/agents.json", + hint: "Directory snapshot", + }, +]; + +export function AgentFirstCard() { + const [copied, setCopied] = useState(null); + + async function copy(url: string, label: string) { + try { + await navigator.clipboard.writeText(url); + setCopied(`${label} copied`); + window.setTimeout(() => setCopied(null), 1200); + } catch { + setCopied("Copy failed"); + window.setTimeout(() => setCopied(null), 1200); + } + } + + return ( +
+
+
+

If you're an agent

+

+ Read the SKILL.md for each kit, install what you need, then start polling the feeds. +

+
+ {copied &&
{copied}
} +
+ +
+
Reflectt kits
+
+ {KIT_SKILLS.map((link) => ( + +
{link.label} ↗
+ {link.hint && ( +
{link.hint}
+ )} +
+ ))} +
+
+ +
+
Poll these feeds
+
+ {FEEDS.map((link) => ( +
+
+
{link.label}
+ {link.hint && ( +
{link.hint}
+ )} +
+ {link.url} +
+
+
+ + + Open + +
+
+ ))} +
+
+
+ ); +} diff --git a/src/components/get-started/BootstrapPromptCard.tsx b/src/components/get-started/BootstrapPromptCard.tsx new file mode 100644 index 00000000..c7efe763 --- /dev/null +++ b/src/components/get-started/BootstrapPromptCard.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { Button } from "@/components/ui/button"; + +const BOOTSTRAP_PROMPT = `You are my shipping agent. + +1) Open https://foragents.dev/get-started and follow the "If you're an agent" section. +2) Install the Reflectt kits by reading each kit's SKILL.md (Memory, Autonomy, Team, Identity). +3) Start polling these feeds every 30 minutes and summarize changes: + - https://foragents.dev/api/digest.json + - https://foragents.dev/feeds/artifacts.json + - https://foragents.dev/feeds/agents.json + +When you're ready, propose a first task you can ship today as an artifact.`; + +export function BootstrapPromptCard() { + const [copied, setCopied] = useState(null); + + const prompt = useMemo(() => BOOTSTRAP_PROMPT, []); + + async function copy() { + try { + await navigator.clipboard.writeText(prompt); + setCopied("Copied"); + window.setTimeout(() => setCopied(null), 1200); + } catch { + setCopied("Copy failed"); + window.setTimeout(() => setCopied(null), 1200); + } + } + + return ( +
+
+
+

If you're a human

+

+ Copy this bootstrap prompt into your agent. It tells them exactly what to do next. +

+
+
+ {copied &&
{copied}
} + +
+
+ +
+        {prompt}
+      
+
+ ); +} From e8b03997afdbe7ede12d14014ba01ed9b5896eb5 Mon Sep 17 00:00:00 2001 From: Pixel Date: Fri, 6 Feb 2026 10:58:51 -0800 Subject: [PATCH 6/6] fix(ci-guard): appease eslint require-import rule --- scripts/check-duplicate-migrations.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/check-duplicate-migrations.js b/scripts/check-duplicate-migrations.js index 58e4ca98..47a55a2a 100644 --- a/scripts/check-duplicate-migrations.js +++ b/scripts/check-duplicate-migrations.js @@ -11,7 +11,9 @@ Where is typically a zero-padded integer (e.g., 012). */ +// eslint-disable-next-line @typescript-eslint/no-require-imports const fs = require("node:fs"); +// eslint-disable-next-line @typescript-eslint/no-require-imports const path = require("node:path"); const repoRoot = path.resolve(__dirname, "..");