pulse/AGENTS.md

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

  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.

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:

  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.