Web API Reference
Reference: packages/web/
Server (Hono on Bun)
Binding
Bun.serve({ hostname: "127.0.0.1", port: config.port, fetch: app.fetch });NEVER bind to 0.0.0.0.
LoopManager (singleton)
The server maintains a LoopManager singleton (packages/web/src/server/loop-manager.ts) that:
- Tracks active loops by project path (max one loop per project)
- Creates
LoopRunnerinstances from@rauf/loop, subscribes to all 20 event types - Fans events out to SSE clients via
subscribe(projectPath, listener) - Recovers stale loops on startup by scanning discovered projects and resetting stalled
in_progressitems topending - Handles graceful shutdown (
shutdownAll()): cancels all active loops and waits for completion
CSRF Middleware
All POST/PUT/DELETE routes require header: X-Rauf-Request: true.
Requests without it get 403 Forbidden.
No Access-Control-Allow-Origin headers set (blocks cross-origin).
Static Serving
Built React frontend served from / as static files.
API routes under /api/.
API Routes
Health
GET /api/health → { version, uptime, rootDirectory, projectCount }
Projects
GET /api/projects → { data: DiscoveredProject[] }GET /api/projects/:id → { data: ProjectDetail }POST /api/projects/:id/install → { data: InstallationReport }POST /api/projects/init → { data: InstallationReport }POST /api/projects/:id/update → { data: InstallationReport }POST /api/projects/:id/uninstall → { data: void }:id = directory name (URL-encoded). Resolved to ROOT_DIRECTORY/<id>.
Backlog
GET /api/projects/:id/backlog → { data: BacklogItem[] } Query params: ?status=pending&type=bug&sort=priorityPOST /api/projects/:id/backlog → { data: BacklogItem } Body: CreateItemInputGET /api/projects/:id/backlog/:itemId → { data: BacklogItem }PUT /api/projects/:id/backlog/:itemId → { data: BacklogItem } Body: UpdateItemInputDELETE /api/projects/:id/backlog/:itemId → { data: void }POST /api/projects/:id/backlog/restore → { data: void }POST /api/projects/:id/backlog/sweep → { data: SweepResult } Body: { minAgeDays?: number } Note: registered BEFORE /:itemId routes to prevent "sweep" matching as an itemIdGET /api/projects/:id/archive → { data: { months: { month: string, count: number }[] } }GET /api/projects/:id/archive/:month → { data: ArchiveMonth }DELETE /api/projects/:id/archive/:month → { data: { purgedCount: number, purgedMonths: string[] } }Status
GET /api/projects/:id/status → { data: DerivedStatus }GET /api/projects/:id/log?tail=50 → { data: string[] }GET /api/projects/:id/log/stream → SSE streamGET /api/projects/:id/progress → { data: string } (raw markdown)Profile
GET /api/projects/:id/profile → { data: ProjectProfile }PUT /api/projects/:id/profile → { data: ProjectProfile }POST /api/projects/:id/profile/detect → { data: ProjectProfile }Config
GET /api/config → { data: ToolConfig }PUT /api/config → { data: ToolConfig }Loop Management
POST /api/projects/:id/loop/start → { data: { started: true, projectPath } } Body (optional): { maxIterations?, maxRetries?, model?, sessionTimeoutMinutes?, review?, reviewOnly?, provider? } Defaults: maxIterations=20, maxRetries=3, sessionTimeoutMinutes=60 409 Conflict: Loop already running for this project Note (v0.5.0): This route is the backend for `rauf loop run --detached`. URL and contract unchanged.
POST /api/projects/:id/loop/stop → { data: { stopped: true, projectPath } } 404 Not Found: No active loop for this project
GET /api/projects/:id/loop/events → SSE stream of LoopEvent Event type: "loop_event" (data: JSON-encoded LoopEvent) Heartbeat: "heartbeat" every 30s (data: ISO timestamp) Streams until client disconnectsRecovery
All mutation routes require X-Rauf-Request: true header (403 otherwise). An invalid /:id returns 400 INVALID_ID; a path-sandbox violation returns 400 PATH_VIOLATION.
POST /api/projects/:id/reset Body: { backlogRoot?: string, clearBacklog?: boolean, keepProgress?: boolean, keepLog?: boolean } Guard: acquires recovery lock (409 LOCK_CONFLICT if a live loop holds the lock) 200: { data: { resetCount, resetIds, treeClean, stalledReset } } 404 if project not installed
POST /api/projects/:id/resume Body: { backlogRoot?: string, retryBlocked?: boolean, answers?: { itemId: string, text: string }[] } Guard: acquires recovery lock (409 LOCK_CONFLICT if a live loop holds the lock) Injects each answer as humanAnswer on the item, optionally unblocks blocked items, runs recoverInterruptedLoop, then relaunches the loop if there are eligible items. 200: { data: { reconciled: ReconcileSummary, relaunched: boolean, reason?: string } } A failed relaunch is reported as relaunched:false + reason in a 200 (not an HTTP error). 404 if project not installed
POST /api/projects/:id/loop/review Body: { model?: string, sessionTimeoutMinutes?: number, backlogRoot?: string } Guard: loop-start dedupe (409 CONFLICT if a loop/review is already running for this backlog root) Starts a review-only pass (maxIterations:1, reviewOnly:true). 200: { data: { started: true } } Note: registered in loop.ts alongside loop/start and loop/stop
POST /api/projects/:id/backlog/unblock Body: { itemId?: string } — omit itemId to unblock all blocked items Guard: assertNoLiveLoop (409 LOCK_CONFLICT if a live loop is running; fail-open on lock I/O error) 200: { data: { unblockedCount: number, unblockedIds: string[] } } Note: registered BEFORE /:itemId routes
GET /api/projects/:id/backlog/validate Query: ?backlogRoot=<path> (optional; defaults to project's default backlog root) Guard: none — read-only; no X-Rauf-Request required; safe to call during a live run 200: { data: { valid: boolean, findings: ValidationFinding[] } } valid:false returns 200 (not an HTTP error); findings list structural issues (e.g. DUPLICATE_ID) Note: registered BEFORE /:itemId routesError Response Format
{ "error": { "code": "VALIDATION_ERROR", "message": "Priority must be between 1 and 4", "details": { "field": "priority", "value": 5 } }}SSE Log Stream
GET /api/projects/:id/log/stream
Event types:
log: new log line(s) (data: JSON array of strings)status: loop state change detected (data: DerivedStatus JSON)loop_event: LoopEvent from the loop runner (data: JSON-encoded LoopEvent, see SCHEMAS.md)heartbeat: sent every 30s (data: ISO timestamp)
Implementation:
- Watch
.rauf/rauf.logwith fs.watch - Poll for new lines every 1s
- Watch
.rauf/state.jsonfor state changes - Client auto-reconnects (standard SSE behavior)
Frontend (React SPA)
Router Structure (TanStack Router)
/ → Redirect to /projects/projects → Projects Dashboard/projects/:id → Redirect to /projects/:id/backlog/projects/:id/backlog → Backlog View/projects/:id/status → Status View/projects/:id/settings → Project Settings/install → Installation Wizard/init → Greenfield Wizard/settings → Global SettingsShared Fetch Wrapper
async function raufFetch(url: string, options?: RequestInit) { return fetch(url, { ...options, headers: { ...options?.headers, "X-Rauf-Request": "true", "Content-Type": "application/json", }, });}All API calls go through this wrapper.
TanStack Query Keys
['projects'] → project list['projects', id] → project detail['projects', id, 'backlog'] → backlog items['projects', id, 'backlog', itemId] → single item['projects', id, 'status'] → derived status['projects', id, 'profile'] → project profile['config'] → tool config['projects', id, 'loop', 'start'] → start loop mutation['projects', id, 'loop', 'stop'] → stop loop mutationUI Views
Projects Dashboard
- Card grid of discovered projects
- Each card: name, stack badge, loop state badge, backlog summary, last activity
- “Install Rauf” and “Initialize New Project” buttons
- Auto-refresh every 30s + manual refresh button
Backlog View
- Filter bar: type, status, priority
- Sort: priority (default), id, status
- Summary counts row
- Item cards: ID, type badge, priority, title, status badge, criteria count, dependencies
- “Add Item” → side panel/modal
- Click item → edit side panel
- Active loop warning banner if running
Status View
- Two-column layout (wide screens)
- Left: loop state badge, iteration info, backlog summary, current/blocked/recent items
- Right: live log tail panel (monospaced, SSE-fed, auto-scroll)
- Loop control buttons: Start Loop (visible when state is IDLE, PAUSED, COMPLETE, or ERROR) / Stop Loop (visible when RUNNING or SLEEPING_LIMIT)
- Below: progress.md rendered as markdown
Installation Wizard (6 steps)
- Select Target (path input, validation)
- Preflight (checklist of checks)
- Tech Stack & Profile (auto-detected, editable)
- Configure (project name, gitignore toggle, options preview)
- Review (file list preview, RAUF.md verification section)
- Result (installation report, quick links)
Greenfield Wizard (5 steps)
- Project Info (name, path, description)
- Tech Stack (preset selection, command fields)
- Initial Backlog (empty / import file / enter inline)
- Review (file preview, CLAUDE.md preview, backlog preview)
- Result (creation report, next steps)
Settings
- ROOT_DIRECTORY path input (triggers re-discovery on change)
- Server port
- Theme toggle (light/dark/system)
- Project visibility toggles
Styling
- Tailwind CSS (utility-first)
- No component library dependency; custom components
- Responsive: works at 1024px+ width
- Light/dark theme support
Key UX Requirements
- All destructive actions require confirmation dialog
- Toast notifications for async results
- Error boundaries: malformed project files show recovery UI
- No external network requests (all assets served locally)
- Markdown rendering via react-markdown + remark-gfm