6.3 KiB
6.3 KiB
Pulse — Agent Guide
RSS feed aggregator hosted at pulse.eazz.io. Built with a strict module-per-agent architecture. Each agent owns one module and communicates with others only through documented interfaces.
Golden Rules
- Ask before you build. If anything is unclear, ask. Do not assume. Do not start coding until you fully understand the task.
- Plan before you execute. Always produce a written plan first. Wait for approval before writing any code.
- Stay in your module. Do not read or write files outside your module folder unless you are the Orchestrator.
- Respect the interfaces. Never call internal functions of another module. Only use what is exported in
interfaces/. - Small commits, clear messages. One logical change per commit.
- No silent assumptions. If a requirement is ambiguous, surface it explicitly before proceeding.
Project Structure
pulse/
├── AGENTS.md # This file
├── interfaces/ # Shared contracts — source of truth for all modules
│ ├── feed.types.ts # Core data types
│ ├── fetcher.interface.ts # Fetcher module contract
│ ├── parser.interface.ts # Parser module contract
│ ├── storage.interface.ts # Storage module contract
│ ├── dedup.interface.ts # Deduplication module contract
│ └── formatter.interface.ts # Formatter module contract
├── modules/
│ ├── fetcher/ # HTTP fetching of RSS/Atom feeds
│ ├── parser/ # XML parsing into FeedItem structs
│ ├── dedup/ # Deduplication of feed items
│ ├── storage/ # Persistence (SQLite)
│ └── formatter/ # Output rendering (terminal / HTML)
├── orchestrator/ # Coordinates modules, owns no business logic
└── opencode.json # OpenCode agent config
Core Data Types
These are defined in interfaces/feed.types.ts and are the lingua franca between all modules.
export interface FeedItem {
id: string // Deterministic hash of url + publishedAt
source: string // Feed origin URL
title: string
url: string
publishedAt: Date
content?: string // Optional full content
summary?: string // Optional short summary
}
export interface FetchError {
source: string
reason: string
code: "NETWORK" | "TIMEOUT" | "PARSE" | "UNKNOWN"
}
export interface FetchResult {
items: FeedItem[]
errors: FetchError[]
fetchedAt: Date
}
Module Interfaces
Fetcher
// interfaces/fetcher.interface.ts
export interface IFetcher {
fetch(feedUrl: string): Promise<FetchResult>
fetchMany(feedUrls: string[]): Promise<FetchResult>
}
Parser
// interfaces/parser.interface.ts
export interface IParser {
parse(rawXml: string, source: string): Promise<FeedItem[]>
supports(contentType: string): boolean // rss, atom, json feed
}
Deduplication
// interfaces/dedup.interface.ts
export interface IDedup {
filter(items: FeedItem[]): Promise<FeedItem[]> // returns only unseen items
markSeen(items: FeedItem[]): Promise<void>
}
Storage
// interfaces/storage.interface.ts
export interface IStorage {
save(items: FeedItem[]): Promise<void>
getRecent(limit: number): Promise<FeedItem[]>
getBySource(source: string, limit: number): Promise<FeedItem[]>
search(query: string): Promise<FeedItem[]>
}
Formatter
// interfaces/formatter.interface.ts
export type OutputFormat = "terminal" | "html" | "json"
export interface IFormatter {
format(items: FeedItem[], format: OutputFormat): Promise<string>
}
Agent Responsibilities
| Agent | Owns | Can read interfaces | Cannot touch |
|---|---|---|---|
orchestrator |
orchestrator/ |
All | modules/ internals |
fetcher-agent |
modules/fetcher/ |
feed.types, fetcher.interface |
All other modules |
parser-agent |
modules/parser/ |
feed.types, parser.interface |
All other modules |
dedup-agent |
modules/dedup/ |
feed.types, dedup.interface |
All other modules |
storage-agent |
modules/storage/ |
feed.types, storage.interface |
All other modules |
formatter-agent |
modules/formatter/ |
feed.types, formatter.interface |
All other modules |
Workflow Every Agent Must Follow
Step 1 — Clarify
Before doing anything, re-read your task. If any of these are unclear, ask:
- What exact input will I receive?
- What exact output is expected?
- Are there edge cases not mentioned?
- Does this touch an interface that other modules depend on?
Do not proceed until you have answers.
Step 2 — Plan
Write a short implementation plan in plain text:
- What files will you create or modify?
- What are the key decisions and why?
- What could go wrong?
Post the plan and wait for approval before writing any code.
Step 3 — Implement
Only after plan approval:
- Stay inside your module folder
- Implement against the interface, not against other module internals
- Write tests alongside the code
Step 4 — Verify
- Run tests for your module only
- Confirm the exported interface still matches
interfaces/ - Do not break existing interface contracts without flagging it first
Interface Change Protocol
Interfaces in interfaces/ are shared contracts. Changing them affects all modules.
If you need to change an interface:
- Stop. Do not change it unilaterally.
- Post a proposed change with reasoning to the orchestrator.
- Wait for explicit approval.
- Only then update the interface file AND all affected modules in the same commit.
Tech Stack
- Runtime: Node.js (TypeScript)
- Storage: SQLite via
better-sqlite3 - HTTP:
undici - XML parsing:
fast-xml-parser - Testing:
vitest - Linting:
eslint+prettier - Web: React + Tailwindcss + shadcn/ui
- Server: Koa
Questions Agents Should Ask Themselves
Before starting any task:
- Do I fully understand what "done" looks like?
- Have I read the relevant interface file?
- Is my plan written and approved?
- Am I staying inside my module?
- Will my changes break any existing interface contract?
If any answer is "no" or "unsure" — ask before proceeding.