# 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 1. **Ask before you build.** If anything is unclear, ask. Do not assume. Do not start coding until you fully understand the task. 2. **Plan before you execute.** Always produce a written plan first. Wait for approval before writing any code. 3. **Stay in your module.** Do not read or write files outside your module folder unless you are the Orchestrator. 4. **Respect the interfaces.** Never call internal functions of another module. Only use what is exported in `interfaces/`. 5. **Small commits, clear messages.** One logical change per commit. 6. **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. ```typescript 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 ```typescript // interfaces/fetcher.interface.ts export interface IFetcher { fetch(feedUrl: string): Promise fetchMany(feedUrls: string[]): Promise } ``` ### Parser ```typescript // interfaces/parser.interface.ts export interface IParser { parse(rawXml: string, source: string): Promise supports(contentType: string): boolean // rss, atom, json feed } ``` ### Deduplication ```typescript // interfaces/dedup.interface.ts export interface IDedup { filter(items: FeedItem[]): Promise // returns only unseen items markSeen(items: FeedItem[]): Promise } ``` ### Storage ```typescript // interfaces/storage.interface.ts export interface IStorage { save(items: FeedItem[]): Promise getRecent(limit: number): Promise getBySource(source: string, limit: number): Promise search(query: string): Promise } ``` ### Formatter ```typescript // interfaces/formatter.interface.ts export type OutputFormat = "terminal" | "html" | "json" export interface IFormatter { format(items: FeedItem[], format: OutputFormat): Promise } ``` --- ## 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: 1. Stop. Do not change it unilaterally. 2. Post a proposed change with reasoning to the orchestrator. 3. Wait for explicit approval. 4. 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.