Skip to content

Commit 303c3ee

Browse files
khaliqgantclaude
andcommitted
feat(agent): public /agent page + 5 agent skeletons
The build-in-public page. Reads content/agent-log.json (the receipts) and renders a timeline grouped by day, with trigger badges and links per entry. Header shows weekly stats: total actions broken down by trigger type — proves all three primitives are firing. /agents/ holds the typed handlers, one per agent, with comments where external integrations plug in: notion-to-blog/ change Notion → MDX → PR weekly-digest/ time Web + Reddit search → one rolling issue sunday-ping/ time Reads digest, drafts outline, Slack DM pr-reviewer/ change Deploy preview, dead-link, copy-edit manual-chatbot/ message RAG over essays, answers DMs shared/log.ts is the only side effect every handler shares: append to the public log so the /agent page stays current. Nav now: Essays · Triggers · Agent · About. Skeletons fire the log but stub the integrations — wiring follows in PRs once tokens/IDs are wired. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 23e2dec commit 303c3ee

13 files changed

Lines changed: 885 additions & 3 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
{
2+
"id": "education-seo-geo-blog-operator",
3+
"intent": "education-content-operations",
4+
"tags": ["planning", "implementation", "documentation", "analytics"],
5+
"description": "Plans, writes, and operates education-first blog and site content with strong SEO and generative engine optimization discipline. Uses Notion as the operational source of truth for briefs, editorial pipeline, and publishing checklists.",
6+
"defaultTier": "best",
7+
"skills": [
8+
{
9+
"id": "anthropics/skills@frontend-design",
10+
"source": "https://github.com/anthropics/skills#frontend-design",
11+
"description": "Creates distinctive, high-quality frontend design systems and UI implementations that avoid generic patterns while staying production-ready."
12+
},
13+
{
14+
"id": "coreyhaines31/marketingskills@seo-audit",
15+
"source": "https://github.com/coreyhaines31/marketingskills#seo-audit",
16+
"description": "Applies a structured SEO audit workflow across crawlability, technical health, on-page signals, internal links, and prioritized remediation."
17+
},
18+
{
19+
"id": "coreyhaines31/marketingskills@ai-seo",
20+
"source": "https://github.com/coreyhaines31/marketingskills#ai-seo",
21+
"description": "Optimizes content for AI discovery and citation with GEO patterns such as answer-first structure, llms.txt readiness, citation depth, and entity clarity."
22+
},
23+
{
24+
"id": "coreyhaines31/marketingskills@schema-markup",
25+
"source": "https://github.com/coreyhaines31/marketingskills#schema-markup",
26+
"description": "Implements and validates JSON-LD schema markup for content, product, and organizational entities to improve rich results and AI comprehension."
27+
},
28+
{
29+
"id": "resciencelab/opc-skills@seo-geo",
30+
"source": "https://github.com/resciencelab/opc-skills#seo-geo",
31+
"description": "Adds a combined SEO and GEO optimization workflow with platform-specific guidance, metadata/schema checks, and structured reporting for AI and traditional search visibility."
32+
},
33+
{
34+
"id": "luka2chat/geo-skills@geo-best-practices",
35+
"source": "https://github.com/luka2chat/geo-skills#geo-best-practices",
36+
"description": "Adds focused GEO best-practice guidance for AI answer engines, including content structure, retrieval visibility, and citation-friendly writing patterns."
37+
}
38+
],
39+
"inputs": {
40+
"TASK_DESCRIPTION": {
41+
"description": "Kickoff task from the caller. If none is supplied, begin by asking for the specific content or site operation goal.",
42+
"default": "No kickoff task was provided. Ask the user for the exact blog or site operation task before taking action."
43+
}
44+
},
45+
"mcpServers": {
46+
"notion": {
47+
"type": "http",
48+
"url": "https://mcp.notion.com/mcp"
49+
}
50+
},
51+
"permissions": {
52+
"allow": ["mcp__notion"]
53+
},
54+
"claudeMdContent": "# Education SEO/GEO Blog Operator\n\nYou are responsible for creating and operating an education-first content site that wins in both traditional search and AI-mediated discovery. You should prioritize clarity, accuracy, and durable instructional value over hype.\n\n## Inputs\n\nYou may receive a task prompt, existing repository content, analytics signals, and Notion workspace context.\n\nYour output must always include production-ready content or implementation artifacts plus a concise validation report.\n\n## Process\n\n1. Define the educational objective, target learner profile, and user intent before writing or editing anything.\n2. Build or update the content brief in Notion first, then use Notion as the live operating source for status, decisions, and publishing checkpoints.\n3. Produce content that teaches: answer core questions directly, explain why, include examples, and make next actions explicit.\n4. Apply SEO fundamentals on every deliverable: search intent alignment, strong titles/meta descriptions, heading structure, internal linking, canonical assumptions, indexability checks, and performance-aware markup.\n5. Apply GEO fundamentals on every deliverable: answer-first structure, high factual density, citation-friendly claims, entity clarity, llms.txt-aware information architecture, and structured data that helps machine comprehension.\n6. Use schema markup where appropriate and validate implementation using proper rendered-page tooling; do not assume static fetches reveal all JSON-LD.\n7. For design or UX tasks, deliver visually intentional interfaces with strong typography, coherent color systems, and polished interactions that support readability and comprehension.\n8. Where product mentions are needed, keep them subtle and educational: frame capabilities as practical patterns and outcomes, not brand-heavy sales copy. Avoid naming internal product brands unless the user explicitly asks for that language.\n9. End each task by documenting what changed, why it helps learners, what SEO/GEO checks were completed, and what remains unresolved.\n\n## Quality Bar\n\nCorrectness and usefulness are mandatory. Every output must be accurate, specific, implementation-aware, and safe to publish. Do not invent facts, analytics, or citations. Do not ship generic filler content, thin AI-style prose, or unverified technical claims.\n\n## Anti-Goals\n\nDo not prioritize keyword stuffing over comprehension.\nDo not produce content that is promotional-first instead of educational-first.\nDo not treat GEO as separate from SEO; both must be handled together.\nDo not bypass Notion when the task requires planning, tracking, or operational handoff.\n\n## Output Contract\n\nReturn:\n- What was produced (content, code, or both).\n- Exact files or Notion artifacts changed.\n- SEO checks completed and issues found.\n- GEO checks completed and issues found.\n- Risks, assumptions, and the next highest-leverage action.",
55+
"tiers": {
56+
"best": {
57+
"harness": "claude",
58+
"model": "claude-opus-4-6",
59+
"systemPrompt": "$TASK_DESCRIPTION",
60+
"harnessSettings": {
61+
"reasoning": "high",
62+
"timeoutSeconds": 1200
63+
}
64+
},
65+
"best-value": {
66+
"harness": "claude",
67+
"model": "claude-sonnet-4-6",
68+
"systemPrompt": "$TASK_DESCRIPTION",
69+
"harnessSettings": {
70+
"reasoning": "medium",
71+
"timeoutSeconds": 900
72+
}
73+
},
74+
"minimum": {
75+
"harness": "claude",
76+
"model": "claude-haiku-4-5-20251001",
77+
"systemPrompt": "$TASK_DESCRIPTION",
78+
"harnessSettings": {
79+
"reasoning": "low",
80+
"timeoutSeconds": 600
81+
}
82+
}
83+
}
84+
}

.gitignore

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@ yarn-error.log*
4040
.wrangler/
4141
.dev.vars
4242

43-
# AgentWorkforce internal tooling (not site content)
44-
.agentworkforce/
45-
4643
# typescript
4744
*.tsbuildinfo
4845
next-env.d.ts

agents/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# /agents
2+
3+
The proactive agents that run this site. Each subdirectory is one agent. Each
4+
agent has a single entry point — `handler.ts` — that takes a typed event and
5+
returns nothing (side effects only).
6+
7+
## Why these live in this repo
8+
9+
This site argues that a proactive agent is small code wrapped around the right
10+
runtime. Co-locating the agents with the site they operate keeps the demo
11+
honest and the deploy simple.
12+
13+
## The roster
14+
15+
| Directory | Trigger | What it does |
16+
|--------------------|---------|------------------------------------------------------------------------------|
17+
| `notion-to-blog` | change | Watches a Notion database. When a page flips to `ready`, converts to MDX and opens a PR. |
18+
| `weekly-digest` | time | Weekly. Searches the web + Reddit for "proactive agents" mentions. Files one rolling GitHub issue, deduped, grouped by topic. |
19+
| `sunday-ping` | time | Sundays at 09:00 local. Reads the latest weekly digest and pings me on Slack with a draft outline for the next post. |
20+
| `pr-reviewer` | change | Comments on PRs to this repo with deploy preview, dead-link check, copy-edit notes. |
21+
| `manual-chatbot` | message | Answers Slack DMs / emails grounded in the published essays. |
22+
23+
## Shape
24+
25+
```
26+
agents/
27+
shared/
28+
log.ts # writeLogEntry({ agent, action, summary, ... }) → appends to content/agent-log.json
29+
types.ts # AgentEntry, Trigger, Outcome (re-exports from lib/agent-log.ts)
30+
notion-to-blog/
31+
handler.ts # export async function handler(event: NotionPageEvent)
32+
weekly-digest/
33+
handler.ts # export async function handler() — invoked by relaycron
34+
sunday-ping/
35+
handler.ts # export async function handler()
36+
pr-reviewer/
37+
handler.ts # export async function handler(event: PullRequestEvent)
38+
manual-chatbot/
39+
handler.ts # export async function handler(event: InboxMessage)
40+
```
41+
42+
## Wiring
43+
44+
Each handler is invoked by the proactive runtime (`relaycron` for time,
45+
`relayfile` for change, `relaycast` for message). The wiring lives in the
46+
runtime config, not in the handler — so you can read each handler in isolation
47+
and see the full behaviour.
48+
49+
Every handler ends by calling `writeLogEntry(...)` so the public `/agent` page
50+
on the site stays current. That page is the receipts.
51+
52+
## Status
53+
54+
Skeletons. Each handler currently logs to the activity feed and returns;
55+
external integrations (Notion, GitHub, Slack, Reddit, web search) are stubbed.
56+
Wire them in PRs &mdash; the PR reviewer agent will check your work.

agents/manual-chatbot/handler.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { writeLogEntry } from "../shared/log";
2+
3+
/**
4+
* Wired to relaycast: any message addressed to @manual in our Slack workspace,
5+
* or sent to manual@proactiveagents.dev.
6+
*
7+
* Answers questions about proactive agents grounded in the published essays
8+
* (RAG over content/posts/*.mdx). Always cites the post(s) it's drawing from.
9+
*
10+
* Refuses (politely, with a log entry) when:
11+
* - The question isn't about proactive agents / the runtime
12+
* - The corpus has no matching answer (rather than hallucinate)
13+
*
14+
* "Calibrated restraint" is the hard part — see the home page argument.
15+
* False answers cost trust faster than honest "don't know"s.
16+
*/
17+
type InboxMessage = {
18+
channel: "slack" | "email";
19+
from: string;
20+
text: string;
21+
threadId?: string;
22+
};
23+
24+
export async function handler(event: InboxMessage) {
25+
// TODO: cheap intent check — is this a question about proactive agents?
26+
// const onTopic = await isOnTopic(event.text);
27+
// if (!onTopic) { ...refuse politely, log skipped, return }
28+
29+
// TODO: retrieve relevant chunks from content/posts/*.mdx
30+
// const chunks = await retrieveCorpusChunks(event.text, { topK: 4 });
31+
32+
// TODO: if no chunk above threshold, refuse rather than hallucinate
33+
// if (chunks.length === 0 || chunks[0].score < THRESHOLD) { ...refuse, log skipped }
34+
35+
// TODO: draft an answer with inline citations to /posts/<slug>
36+
// const answer = await draftAnswer(event.text, chunks);
37+
38+
// TODO: reply via the source channel
39+
// await cast.reply({ channel: event.channel, threadId: event.threadId, text: answer });
40+
41+
const cited: { slug: string; title: string }[] = []; // chunks.map(...)
42+
43+
await writeLogEntry({
44+
agent: "manual-chatbot",
45+
trigger: "message",
46+
action: `Answered ${event.channel} message`,
47+
summary: `Question from ${event.from}: "${event.text.slice(0, 80)}${event.text.length > 80 ? "…" : ""}". Answered with ${cited.length} citation(s).`,
48+
outcome: "success",
49+
links: cited.map((c) => ({ label: c.title, url: `/posts/${c.slug}` })),
50+
});
51+
}

agents/notion-to-blog/handler.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { writeLogEntry } from "../shared/log";
2+
3+
/**
4+
* Wired to a relayfile watch on the Notion "Drafts" database. Fires on every
5+
* page change, but only acts when the page's `Status` tag is `ready`.
6+
*
7+
* Side effects on success:
8+
* 1. Convert Notion blocks → MDX (Scene, Sidenote, Callout where used)
9+
* 2. Write `content/posts/<slug>.mdx` with frontmatter
10+
* 3. Open a PR against `main` with the new file
11+
* 4. Append an entry to the agent log
12+
*/
13+
type NotionPageEvent = {
14+
pageId: string;
15+
pageUrl: string;
16+
title: string;
17+
slug: string;
18+
status: "draft" | "editing" | "ready" | "published";
19+
previousStatus?: NotionPageEvent["status"];
20+
};
21+
22+
export async function handler(event: NotionPageEvent) {
23+
if (event.status !== "ready") {
24+
await writeLogEntry({
25+
agent: "notion-to-blog",
26+
trigger: "change",
27+
action: `Skipped — page still ${event.status}`,
28+
summary: `Page "${event.title}" changed in Notion but tag is "${event.status}", not "ready". Nothing published.`,
29+
outcome: "skipped",
30+
skippedReason: `tag != ready`,
31+
links: [{ label: "Notion page", url: event.pageUrl }],
32+
});
33+
return;
34+
}
35+
36+
// TODO: Notion API call → fetch page blocks
37+
// const blocks = await notion.blocks.children.list({ block_id: event.pageId });
38+
39+
// TODO: blocks → MDX conversion (handle our Scene/Sidenote/Callout components)
40+
// const mdx = blocksToMdx(blocks, frontmatterFromPage(event));
41+
42+
// TODO: write file + open PR via GitHub API
43+
// const prUrl = await openPr({ path: `content/posts/${event.slug}.mdx`, body: mdx });
44+
45+
await writeLogEntry({
46+
agent: "notion-to-blog",
47+
trigger: "change",
48+
action: "Published essay",
49+
summary: `New page "${event.title}" tagged ready. Converted to MDX, opened PR with the post.`,
50+
outcome: "success",
51+
links: [
52+
{ label: "Notion source", url: event.pageUrl },
53+
// { label: "Source PR", url: prUrl },
54+
{ label: "Will live at", url: `/posts/${event.slug}` },
55+
],
56+
});
57+
}

agents/pr-reviewer/handler.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { writeLogEntry } from "../shared/log";
2+
3+
/**
4+
* Wired to a relayfile watch on github:AgentWorkforce/proactive-agents/pulls.
5+
* Fires on `opened` and `synchronize` events.
6+
*
7+
* Posts ONE consolidated review comment per push (idempotent — edits the same
8+
* comment on subsequent pushes). The comment includes:
9+
* - Cloudflare Pages preview URL for the PR branch
10+
* - Dead-link check (any new external links in changed MDX files)
11+
* - Copy-edit notes (overlong sentences, weasel words, Markdown-quirk fixes)
12+
* - Screenshot diff vs production for any /app/* changes (optional, expensive)
13+
*/
14+
type PullRequestEvent = {
15+
number: number;
16+
url: string;
17+
branch: string;
18+
changedFiles: string[];
19+
action: "opened" | "synchronize" | "reopened";
20+
};
21+
22+
export async function handler(event: PullRequestEvent) {
23+
const mdxChanged = event.changedFiles.filter((f) => f.endsWith(".mdx"));
24+
const codeChanged = event.changedFiles.filter((f) => f.startsWith("app/") || f.startsWith("components/"));
25+
26+
// TODO: deploy preview URL — Cloudflare Pages exposes branch deployments at:
27+
// https://<branch>.proactive-agents.pages.dev
28+
// const previewUrl = `https://${slugifyBranch(event.branch)}.proactive-agents.pages.dev`;
29+
30+
// TODO: dead-link check on changed MDX
31+
// const deadLinks = mdxChanged.length ? await checkLinks(mdxChanged) : [];
32+
33+
// TODO: copy edit pass via LLM
34+
// const copyNotes = mdxChanged.length ? await copyEdit(mdxChanged) : [];
35+
36+
// TODO: screenshot diff (optional, only if codeChanged)
37+
// const screenshots = codeChanged.length ? await renderDiff(previewUrl) : null;
38+
39+
// TODO: upsert comment on the PR (find existing review-comment by signature, edit; else create)
40+
// await upsertPrComment(event.number, render({ previewUrl, deadLinks, copyNotes, screenshots }));
41+
42+
const findings = 0; // deadLinks.length + copyNotes.length
43+
44+
if (mdxChanged.length === 0 && codeChanged.length === 0) {
45+
await writeLogEntry({
46+
agent: "pr-reviewer",
47+
trigger: "change",
48+
action: `Skipped PR #${event.number}`,
49+
summary: `Nothing reviewable changed (no MDX, no app/ or components/).`,
50+
outcome: "skipped",
51+
skippedReason: "no reviewable files",
52+
links: [{ label: `PR #${event.number}`, url: event.url }],
53+
});
54+
return;
55+
}
56+
57+
await writeLogEntry({
58+
agent: "pr-reviewer",
59+
trigger: "change",
60+
action: `Reviewed PR #${event.number}`,
61+
summary: `${mdxChanged.length} MDX file(s), ${codeChanged.length} code file(s). ${findings} finding(s). Deploy preview attached.`,
62+
outcome: "success",
63+
links: [{ label: `PR #${event.number}`, url: event.url }],
64+
});
65+
}

agents/shared/log.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import fs from "node:fs/promises";
2+
import path from "node:path";
3+
import type { AgentEntry } from "@/lib/agent-log";
4+
5+
const LOG_PATH = path.join(process.cwd(), "content", "agent-log.json");
6+
7+
/**
8+
* Appends an entry to the public agent log. Idempotent on `id` — if an entry
9+
* with the same id is already present, it's replaced rather than duplicated.
10+
*
11+
* Designed to run from a Node-side handler (Cloudflare Worker, scheduled job,
12+
* webhook receiver). At deploy time, the static export bakes the log into the
13+
* /agent page; for live updates we'd swap this for an edge KV write.
14+
*/
15+
export async function writeLogEntry(
16+
entry: Omit<AgentEntry, "id" | "timestamp"> & {
17+
id?: string;
18+
timestamp?: string;
19+
}
20+
): Promise<AgentEntry> {
21+
const full: AgentEntry = {
22+
id: entry.id ?? `${Date.now()}-${entry.agent}-${Math.random().toString(36).slice(2, 8)}`,
23+
timestamp: entry.timestamp ?? new Date().toISOString(),
24+
agent: entry.agent,
25+
trigger: entry.trigger,
26+
action: entry.action,
27+
summary: entry.summary,
28+
outcome: entry.outcome,
29+
links: entry.links,
30+
skippedReason: entry.skippedReason,
31+
};
32+
33+
const existing = await readLog();
34+
const without = existing.filter((e) => e.id !== full.id);
35+
const next = [full, ...without];
36+
await fs.writeFile(LOG_PATH, JSON.stringify(next, null, 2) + "\n", "utf8");
37+
return full;
38+
}
39+
40+
async function readLog(): Promise<AgentEntry[]> {
41+
try {
42+
const raw = await fs.readFile(LOG_PATH, "utf8");
43+
return JSON.parse(raw) as AgentEntry[];
44+
} catch {
45+
return [];
46+
}
47+
}

0 commit comments

Comments
 (0)