Project Router: surf-agent-skill
Project Identity
surf-agent-skill v8.0.0 — an npm package providing an autonomous web research CLI and library for AI coding agents, built on Brave Search and nothing else.
- Type: npm package (
surf-agent-skill)
- License: MIT
- Runtime: Node.js >= 18
- Module system: ESM (
.mjs extension)
- Dependencies: Zero npm runtime dependencies
- Description: Brave-only web research CLI + library. The
surf-ai orchestrator runs the entire research loop inside the CLI — an LLM plans the queries, up to --sub-agents of them execute at once against Brave (paced to the plan's real rate limit, with key rotation), the LLM analyzes remaining gaps, and follow-ups enter a priority frontier as tree nodes so later waves descend into thin branches rather than re-searching wide.
- Non-negotiable invariant: there is exactly one search adapter. No valid Brave key means exit 78 before any work runs. Do not add a second search provider without changing that contract deliberately — the guarantee is structural, and the whole v8 design rests on it.
Repository Map
| Directory |
Purpose |
bin/ |
5 CLI entry points: surf, surf-research-skill, surf-search-normal, surf-search-unlimit, surf-plan-skill |
src/lib/ai/ |
surf-ai orchestrator: plan -> wave -> analyze -> deepen -> synthesize. Contains orchestrator.mjs, frontier.mjs (the deepening tree), openrouter.mjs (LLM), prompts.mjs, cli.mjs, render.mjs, heuristics.mjs, ledger.mjs, setup.mjs |
src/lib/providers/ |
ONE search adapter: brave.mjs, plus index.mjs (registry, capabilityMap.search = ['brave']) |
src/lib/api/ |
Library wrappers: search.mjs only |
src/lib/ |
Core modules: dispatch.mjs (key rotation + the gate), preflight.mjs (the Brave key gate, exit 78), ratelimit.mjs (cross-process token bucket), html.mjs, state.mjs, cost.mjs, cache.mjs, audit.mjs, flags.mjs, pool.mjs, progress.mjs, setup.mjs, format.mjs, keys-cmd.mjs, project-config.mjs, harness-install.mjs, check-surf-skill.mjs |
src/install/ |
postinstall.mjs (cross-OS symlink creation) + preuninstall.mjs |
src/plan/ |
plan-file.mjs, plans-dir.mjs, slug.mjs — research plan file utilities |
src/validators/ |
index.mjs — per-provider API key validators |
skills/ |
Sub-skills shipped with the package: surf-plan-agent-skill |
references/ |
Documentation: surf-ai-cli.md (CLI reference for delegation prompts), brave-api.md (what Brave returns and every gotcha), burst-templates.md, failure-modes.md, COSTS.md, plan-workflow.md |
test/ |
smoke.mjs (orchestrator, offline) + brave.mjs (adapter, flags, frontier, gate) |
Skills
Skills that agents working on this project should know about:
| Skill |
File |
Purpose |
surf-research-agent-skill |
SKILL.md (root) |
Main orchestrator — multi-agent research using bursts of doubt. v8. Two modes: single-burst and continuous-burst. Ceiling of 10 simultaneous sub-agents, tunable with sub-agents=N |
surf-plan-agent-skill |
skills/surf-plan-agent-skill/SKILL.md |
Research-grounded execution planning — writes execution plans from research findings |
Key Files
Every agent working on this project should read these files for context:
| File |
Why |
package.json |
Version, bin commands, exports map, scripts, npm metadata |
SKILL.md |
Main skill definition — the surf-research-agent-skill orchestrator v8 |
references/brave-api.md |
Before touching providers/brave.mjs or debugging why a flag had no effect |
src/lib/preflight.mjs |
The key gate. Every entry point routes through it |
README.md |
Comprehensive project documentation, usage, setup |
src/index.mjs |
Library entry point — public API surface |
references/surf-ai-cli.md |
CLI reference used in delegation prompts to sub-agents |
Conventions
- Language: JavaScript, ESM modules only (
.mjs extension throughout)
- No TypeScript — plain Node.js, no compilation step
- Zero npm runtime dependencies — the package ships no
dependencies, only devDependencies
- Version: Declared as
const VERSION in each source file that needs it, plus in package.json
- Provider adapter pattern: the adapter in
src/lib/providers/ exports search(), validate(), mapError(), and capability declarations. Registered in index.mjs. The pattern is retained for clarity, not because a second provider is expected
- Error classification: Brave answers a bad key and a bad parameter with the same HTTP 422. Always branch on
error.code, never on the status — and never on meta.component, which reads "authentication" for a plan gate on a perfectly good key
- Exit codes: 0 ok · 1 the operation ran and failed · 2 usage · 78 configuration (no valid Brave key) · 143 killed by the harness
- CLI flag parsing: CLI bins use
parseFlags() from src/lib/flags.mjs
- Output discipline: Progress/logs go to stderr; stdout is clean JSON or Markdown
- Key storage: API keys stored in
~/.config/surf/keys.json (chmod 600). Not stored in environment variables (CLI mode). LLM key via OPENROUTER_API_KEY env var or surf-research-skill ai-setup
- Commit convention: Conventional Commits —
feat:, fix:, docs:, chore:, etc.
- License: MIT — every file carries the MIT header
Architecture Notes
surf-ai Research Loop
orchestrator.mjs --> openrouter.mjs (LLM, via OpenRouter API)
--> frontier.mjs (the deepening tree)
--> dispatch.mjs (gate -> key rotation -> rate limiter)
--> brave.mjs
The loop: gate -> plan queries -> run a wave of up to --sub-agents -> analyze -> admit follow-ups into the frontier as tree nodes -> repeat until resolved / saturated / every branch closed -> synthesize.
There is no fallback chain
search -> brave. That is the entire chain.
The gate (preflight.mjs) runs before any search. Without a usable, validated Brave key the process exits 78. This is deliberate: v7's keyless tier meant a research run could answer from Wikipedia and exit 0, and nothing in the output revealed it.
Rate limiting
Brave enforces a 1-second sliding window counted on arrival, with no Retry-After. The per-second allowance is read from x-ratelimit-policy (and from the 429 body, which carries the plan inline) and enforced by a token bucket in ~/.cache/surf/ratelimit.json — on disk, because sub-agents are separate processes. Each key has its own budget, so a second key doubles real parallelism.
Model Fallback Chain
deepseek-v4-pro -> deepseek-v4-flash-0731 -> deepseek-v4-flash -> deepseek-v3.2 -> deepseek-chat-v3.1
Via OpenRouter. Falls back through models on failure or rate limiting.
Key Rotation
- Auth failure (401/403): Key is burned (removed from pool)
- Rate limit (429): Key enters cooldown period
- Auto-reset: Key pools reset monthly
- Pool management:
src/lib/pool.mjs — multi-key pools per provider
1---2name: project-router3description: Project knowledge map for surf-agent-skill — an autonomous web research tool for AI coding agents. Maps the repo structure, skills, conventions, and key files.4---56# Project Router: surf-agent-skill78## Project Identity910**surf-agent-skill v8.0.0** — an npm package providing an autonomous web research CLI and library for AI coding agents, built on **Brave Search and nothing else**.1112- **Type**: npm package (`surf-agent-skill`)13- **License**: MIT14- **Runtime**: Node.js >= 1815- **Module system**: ESM (`.mjs` extension)16- **Dependencies**: Zero npm runtime dependencies17- **Description**: Brave-only web research CLI + library. The `surf-ai` orchestrator runs the entire research loop inside the CLI — an LLM plans the queries, up to `--sub-agents` of them execute at once against Brave (paced to the plan's real rate limit, with key rotation), the LLM analyzes remaining gaps, and follow-ups enter a priority frontier as tree nodes so later waves descend into thin branches rather than re-searching wide.18- **Non-negotiable invariant**: there is exactly one search adapter. No valid Brave key means **exit 78** before any work runs. Do not add a second search provider without changing that contract deliberately — the guarantee is structural, and the whole v8 design rests on it.1920## Repository Map2122| Directory | Purpose |23|---|---|24| `bin/` | 5 CLI entry points: `surf`, `surf-research-skill`, `surf-search-normal`, `surf-search-unlimit`, `surf-plan-skill` |25| `src/lib/ai/` | `surf-ai` orchestrator: plan -> wave -> analyze -> deepen -> synthesize. Contains `orchestrator.mjs`, `frontier.mjs` (the deepening tree), `openrouter.mjs` (LLM), `prompts.mjs`, `cli.mjs`, `render.mjs`, `heuristics.mjs`, `ledger.mjs`, `setup.mjs` |26| `src/lib/providers/` | ONE search adapter: `brave.mjs`, plus `index.mjs` (registry, `capabilityMap.search = ['brave']`) |27| `src/lib/api/` | Library wrappers: `search.mjs` only |28| `src/lib/` | Core modules: `dispatch.mjs` (key rotation + the gate), `preflight.mjs` (the Brave key gate, exit 78), `ratelimit.mjs` (cross-process token bucket), `html.mjs`, `state.mjs`, `cost.mjs`, `cache.mjs`, `audit.mjs`, `flags.mjs`, `pool.mjs`, `progress.mjs`, `setup.mjs`, `format.mjs`, `keys-cmd.mjs`, `project-config.mjs`, `harness-install.mjs`, `check-surf-skill.mjs` |29| `src/install/` | `postinstall.mjs` (cross-OS symlink creation) + `preuninstall.mjs` |30| `src/plan/` | `plan-file.mjs`, `plans-dir.mjs`, `slug.mjs` — research plan file utilities |31| `src/validators/` | `index.mjs` — per-provider API key validators |32| `skills/` | Sub-skills shipped with the package: `surf-plan-agent-skill` |33| `references/` | Documentation: `surf-ai-cli.md` (CLI reference for delegation prompts), `brave-api.md` (what Brave returns and every gotcha), `burst-templates.md`, `failure-modes.md`, `COSTS.md`, `plan-workflow.md` |34| `test/` | `smoke.mjs` (orchestrator, offline) + `brave.mjs` (adapter, flags, frontier, gate) |3536## Skills3738Skills that agents working **on this project** should know about:3940| Skill | File | Purpose |41|---|---|---|42| `surf-research-agent-skill` | `SKILL.md` (root) | Main orchestrator — multi-agent research using bursts of doubt. v8. Two modes: single-burst and continuous-burst. Ceiling of 10 simultaneous sub-agents, tunable with `sub-agents=N` |43| `surf-plan-agent-skill` | `skills/surf-plan-agent-skill/SKILL.md` | Research-grounded execution planning — writes execution plans from research findings |4445## Key Files4647Every agent working on this project should read these files for context:4849| File | Why |50|---|---|51| `package.json` | Version, bin commands, exports map, scripts, npm metadata |52| `SKILL.md` | Main skill definition — the surf-research-agent-skill orchestrator v8 |53| `references/brave-api.md` | Before touching `providers/brave.mjs` or debugging why a flag had no effect |54| `src/lib/preflight.mjs` | The key gate. Every entry point routes through it |55| `README.md` | Comprehensive project documentation, usage, setup |56| `src/index.mjs` | Library entry point — public API surface |57| `references/surf-ai-cli.md` | CLI reference used in delegation prompts to sub-agents |5859## Conventions6061- **Language**: JavaScript, ESM modules only (`.mjs` extension throughout)62- **No TypeScript** — plain Node.js, no compilation step63- **Zero npm runtime dependencies** — the package ships no `dependencies`, only `devDependencies`64- **Version**: Declared as `const VERSION` in each source file that needs it, plus in `package.json`65- **Provider adapter pattern**: the adapter in `src/lib/providers/` exports `search()`, `validate()`, `mapError()`, and capability declarations. Registered in `index.mjs`. The pattern is retained for clarity, not because a second provider is expected66- **Error classification**: Brave answers a bad key and a bad parameter with the same HTTP 422. Always branch on `error.code`, never on the status — and never on `meta.component`, which reads `"authentication"` for a plan gate on a perfectly good key67- **Exit codes**: 0 ok · 1 the operation ran and failed · 2 usage · **78 configuration (no valid Brave key)** · 143 killed by the harness68- **CLI flag parsing**: CLI bins use `parseFlags()` from `src/lib/flags.mjs`69- **Output discipline**: Progress/logs go to stderr; stdout is clean JSON or Markdown70- **Key storage**: API keys stored in `~/.config/surf/keys.json` (chmod 600). Not stored in environment variables (CLI mode). LLM key via `OPENROUTER_API_KEY` env var or `surf-research-skill ai-setup`71- **Commit convention**: [Conventional Commits](https://www.conventionalcommits.org/) — `feat:`, `fix:`, `docs:`, `chore:`, etc.72- **License**: MIT — every file carries the MIT header7374## Architecture Notes7576### surf-ai Research Loop7778```79orchestrator.mjs --> openrouter.mjs (LLM, via OpenRouter API)80 --> frontier.mjs (the deepening tree)81 --> dispatch.mjs (gate -> key rotation -> rate limiter)82 --> brave.mjs83```8485The loop: gate -> plan queries -> run a wave of up to `--sub-agents` -> analyze -> admit follow-ups into the frontier as tree nodes -> repeat until resolved / saturated / every branch closed -> synthesize.8687### There is no fallback chain8889```90search -> brave. That is the entire chain.91```9293The gate (`preflight.mjs`) runs before any search. Without a usable, validated Brave key the process exits 78. This is deliberate: v7's keyless tier meant a research run could answer from Wikipedia and exit 0, and nothing in the output revealed it.9495### Rate limiting9697Brave enforces a 1-second sliding window counted on arrival, with no `Retry-After`. The per-second allowance is read from `x-ratelimit-policy` (and from the 429 body, which carries the plan inline) and enforced by a token bucket in `~/.cache/surf/ratelimit.json` — **on disk, because sub-agents are separate processes**. Each key has its own budget, so a second key doubles real parallelism.9899### Model Fallback Chain100101```102deepseek-v4-pro -> deepseek-v4-flash-0731 -> deepseek-v4-flash -> deepseek-v3.2 -> deepseek-chat-v3.1103```104105Via OpenRouter. Falls back through models on failure or rate limiting.106107### Key Rotation108109- **Auth failure (401/403)**: Key is burned (removed from pool)110- **Rate limit (429)**: Key enters cooldown period111- **Auto-reset**: Key pools reset monthly112- **Pool management**: `src/lib/pool.mjs` — multi-key pools per provider