pulse/AGENTS.md

206 lines
6.3 KiB
Markdown

# 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<FetchResult>
fetchMany(feedUrls: string[]): Promise<FetchResult>
}
```
### Parser
```typescript
// interfaces/parser.interface.ts
export interface IParser {
parse(rawXml: string, source: string): Promise<FeedItem[]>
supports(contentType: string): boolean // rss, atom, json feed
}
```
### Deduplication
```typescript
// interfaces/dedup.interface.ts
export interface IDedup {
filter(items: FeedItem[]): Promise<FeedItem[]> // returns only unseen items
markSeen(items: FeedItem[]): Promise<void>
}
```
### Storage
```typescript
// 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
```typescript
// 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:
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.