| description | Use Bun instead of Node.js, npm, pnpm, or vite. |
|---|---|
| globs | *.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json |
| alwaysApply | false |
Default to using Bun instead of Node.js.
- Use
bun <file>instead ofnode <file>orts-node <file> - Use
bun testinstead ofjestorvitest - Use
bun build <file.html|file.ts|file.css>instead ofwebpackoresbuild - Use
bun installinstead ofnpm installoryarn installorpnpm install - Use
bun run <script>instead ofnpm run <script>oryarn run <script>orpnpm run <script> - Use
bunx <package> <command>instead ofnpx <package> <command> - Bun automatically loads .env, so don't use dotenv.
Bun.serve()supports WebSockets, HTTPS, and routes. Don't useexpress.bun:sqlitefor SQLite. Don't usebetter-sqlite3.Bun.redisfor Redis. Don't useioredis.Bun.sqlfor Postgres. Don't usepgorpostgres.js.WebSocketis built-in. Don't usews.- Prefer
Bun.fileovernode:fs's readFile/writeFile - Bun.$
lsinstead of execa.
Use bun test to run tests. Tests are in tests/.
import { test, expect } from "bun:test";
test("hello world", () => {
expect(1).toBe(1);
});When making any change, update docs before committing. Map changes to affected sections:
| Change type | Update in README | Update in CLAUDE.md |
|---|---|---|
| New/removed CLI command | ## CLI Commands |
— |
| New/changed config field | ### 2. Configure workspaces table |
— |
| New/removed source adapter | File structure, test coverage, ## Extending Patchwork |
Modules table |
| New/removed module or util | ## File Structure |
Modules table |
| Issue status flow change | ### Issue Status Flow |
### Issue Status Flow |
| New notification channel | ## Extending Patchwork |
Modules table |
| Test count change | ### Test Coverage |
— |
| Critical impl detail | — | ### Critical Implementation Details |
If a change doesn't fit the table, ask: "Would a new developer need to know this to use or extend the project?" → README. "Would Claude need this to implement correctly?" → CLAUDE.md.
Autonomous SWE agent that automates developer work. Pulls tasks from project management tools (Asana, Linear, Jira) and error trackers (Sentry), triages them with Claude Code, implements solutions in isolated git worktrees, and provides an interactive React web dashboard for approval. Telegram notifications are optional. Bug fixing via Sentry is one feature — the core value is automating any development task from any source.
Projects are grouped into workspaces (one per client/org). Each workspace optionally has a Telegram bot and a list of projects. Auth tokens live inside each source config in SQLite. This allows managing multiple clients with separate credentials from a single daemon instance. All config is managed through the web dashboard at http://localhost:7842/settings.
| Module | Purpose |
|---|---|
src/cli.ts |
CLI entrypoint — patchwork start, patchwork stop, patchwork doctor, patchwork update, patchwork version, patchwork help. Flags: --config, --log-level. Version read from package.json at runtime. doctor checks Bun/claude/gh/config/repo paths. update checks npm registry and self-updates via bun install -g |
src/constants.ts |
Shared constants: FAILURE_HINTS, CallbackAction, CallbackData — used by telegram, dashboard, and daemon |
src/dashboard.ts |
DashboardServer — Hono + Bun.serve() at localhost:7842, serves React dashboard from dist/, WebSocket /ws for live updates, SPA fallback |
src/api/router.ts |
Hono app combining all API routes |
src/api/issues.ts |
Issue CRUD + action endpoints (GET /api/issues, POST /api/issues/:id/action, GET /api/status) |
src/api/config.ts |
Config CRUD, setup wizard, connection testing endpoints |
src/api/middleware.ts |
CORS middleware |
src/daemon.ts |
Main orchestrator — per-workspace runtimes (optional Telegram + adapters), poll loop, queue, executeAction(), config from SQLite with live reload |
src/config.ts |
Config types, JSON loading (legacy migration only), validation helpers: findProjectConfig(), allProjects() |
src/db.ts |
bun:sqlite WAL mode. IssueDB for issue tracking, ConfigDB for config storage (settings, workspaces, projects, sources, telegram) |
src/queue.ts |
Severity-based priority queue, concurrency control, dedup, pause/resume |
src/sources/types.ts |
NormalizedIssue, SourceAdapter interface, Severity type |
src/sources/sentry.ts |
Sentry source: poll errors (excludes CSP + noise), fetch stacktraces, resolve on accept |
src/sources/asana.ts |
Asana source: poll tasks, fetch comments, severity from tags/custom fields |
src/sources/linear.ts |
Linear source: GraphQL API, poll issues with team/project/status/assignee/label/priority filters, transition to Done on accept |
src/sources/jira.ts |
Jira source: REST API v3, JQL builder for simple mode, raw JQL for advanced mode, transition on accept |
src/worker/claude.ts |
runClaudeText / runClaudeJson — spawn claude -p with --output-format stream-json, model selection, session resume. Streams stdout line-by-line with onEvent callback for real-time events. parseStreamJson() extracts text + session ID from NDJSON events |
src/worker/git.ts |
Worktree create/remove, push, PR via gh, diff helpers |
src/worker/triage.ts |
Claude triage with read-only tools → { fixable, confidence, plan, failed, failureReason, sessionId }. Uses sonnet model. reviseTriage() resumes existing session with user feedback → revised plan, no codebase re-exploration |
src/worker/fix.ts |
Claude implements task in worktree, auto-commits tests first then code, then runs lintCommand (if set) and commits formatting fixups, validation. Returns failureReason on failure. Uses opus model. Resumes triage session when available |
src/notifications/telegram.ts |
Optional. HTML messages with forum topics (each issue = own topic), inline keyboards, long-poll for callbacks |
src/utils/html.ts |
esc() HTML escape helper for Telegram HTML parse mode |
src/utils/logger.ts |
ANSI colored stderr logger, optional file output |
dashboard/src/main.tsx |
React entry point with QueryClient, BrowserRouter |
dashboard/src/router.tsx |
React Router config with setup wizard guard (redirects to /setup if no config) |
dashboard/src/api.ts |
Typed API client for all endpoints |
dashboard/src/pages/IssuesPage.tsx |
Two-panel issue list + detail with filters, WebSocket real-time updates |
dashboard/src/pages/SettingsPage.tsx |
General settings + workspace/project/integration management |
dashboard/src/pages/SetupPage.tsx |
First-run setup wizard (5 steps) |
dashboard/src/hooks/useWebSocket.ts |
WebSocket hook with auto-reconnect |
dashboard/vite.config.ts |
Vite config with API proxy for development |
new → triaging → pending_confirmation [user clicks "Start"] → triaged_actionable → working → pending_approval → accepted/discarded/skipped
→ triaged_not_actionable → [user clicks "Retry"] → new (re-triages from scratch)
→ failed (on triage error, work error, or daemon restart recovery) → [user clicks "Retry"] → new or pending_confirmation
When triage: false in config, issues skip triage and go straight to pending_confirmation.
Confirmation shows the triage result (or a simple "ready to start" card) with Start / Skip buttons. Work does not start until the user confirms. After the work completes, the approval flow (Accept / Discard) works as before.
Queue pauses when an issue reaches pending_confirmation (waiting for "Start") or pending_approval (waiting for "Accept"/"Discard"), and resumes after any callback action.
-
Claude nesting:
worker/claude.tsdeletes theCLAUDECODEenv var before spawningclaudeCLI to avoid "cannot nest" errors. -
Claude stream-json output: Both
runClaudeTextandrunClaudeJsonuse--output-format stream-jsonwith--verbose(required when combined with-p/--print). Stdout is streamed line-by-line; optionalonEventcallback firestool_use/text/resultevents in real-time.parseStreamJson()parses collected lines to extract session ID, result text, token counts (message_start.message.usage.input_tokens,message_delta.usage.output_tokens), andcost_usdfrom theresultevent. Token/cost stats are shown in Telegram messages as⏱ 45s · 🪙 1.2K in / 456 out · 💰 $0.023. -
Worktree paths:
{repoParent}/.patchwork-worktrees/fix-{source}-{sourceId}— sibling dir to the repo, not inside it. -
Branch naming:
patchwork/fix-{source}-{sourceId} -
Auto-commit strategy:
fix.tsauto-commits tests first (separate commit), then code changes. Falls back to single commit if no test files changed. IflintCommandis set, it runs after Claude's commits: formatting fixups are committed asstyle: apply lint/format fixes, and non-zero exit fails the task withfix:lint_failed. -
Telegram is optional:
WorkspaceConfig.telegramis optional. When absent,WorkspaceRuntime.telegramisnulland all Telegram calls are guarded withws.telegram?.. The web dashboard provides full interactive control (action buttons, streaming progress) regardless of Telegram. Workspaces with the same bot token still share one polling connection to avoid Telegram 409 conflicts. -
Telegram callback data: Format is
action:source:sourceId— colons in sourceId are handled by splitting on first two colons only. Actions:fix(start fix),accept(accept fix),discard(discard fix),skip(skip issue). -
Telegram forum topics: Each issue gets its own forum topic via
createForumTopic(telegram_thread_idstores themessage_thread_id). All pipeline updates are sent within the topic viamessage_thread_id. Topics are closed viacloseForumTopicon completion. Severity-based icon colors: critical=red, high=orange, medium=blue, low=green.telegram_message_idtracks the latest message with action buttons. Streaming progress edits are throttled to 3s. The Telegram group must have forum/topics mode enabled. -
Rate limiting: Both Sentry and Asana adapters retry on 429 with backoff. Sentry tracks remaining quota from response headers.
-
Sentry noise filter: CSP (
logger:csp), timeout, network, and CORS errors are filtered both at the API query level and client-side before entering the pipeline. -
Triage error handling: Triage timeouts (exit 143) and errors are marked as
failed, nottriaged_not_actionable. Can be retried. -
Config from SQLite: Config is stored in SQLite (settings, workspaces, projects, source_configs, telegram_configs tables). On first run,
config.jsonis auto-imported into SQLite. All config changes are managed via the web dashboard. API config mutations triggerdaemon.reloadFromDB()for live reload without restart. -
Dashboard-only mode: If no config exists in DB and no
config.json, daemon starts the web dashboard only and auto-opens browser to/setup. After wizard completion, daemon loads config and starts polling. -
Queue pause/resume: Queue pauses on
pending_approval, resumes after accept/discard/skip callback. -
Daemon shutdown:
process.exit(0)after stop, 10s drain timeout. SIGINT/SIGTERM handled cleanly. -
PR descriptions: Structured markdown with Summary, Context, Approach, Changes, and Test Plan sections. The test plan is generated by Claude at the end of the fix prompt — a
## Test Plansection with bullet steps. Parsed from the fix result and stored in thetest_planDB column, then included in the PR body. -
Model selection: Triage uses
--model sonnet(fast, cheap), fix uses--model opus(thorough). Hardcoded indaemon.ts. -
Triage feedback loop: When an issue is at
pending_confirmation, users can submit feedback via theFeedbackBoxcomponent (⌘↵ to submit).POST /api/issues/:id/feedbackcallsreviseTriage()insrc/worker/triage.ts, which resumes the existing triage session (--resume <session_id>) and sends the feedback as a new message. Claude can re-read files but skips full codebase exploration. Returns a revised{ fixable, confidence, reason, plan }. Stream progress is broadcast via WebSocket. User feedback stored inuser_feedbackDB column. -
Session continuity: Triage captures
session_idfrom Claude's stream-json output, stored in thesession_idDB column. Fix stage passes--resume <session-id>so Claude resumes with all triage context (explored files, analysis) already loaded. When resuming, the fix prompt is shorter since context is preserved. -
Failure reason codes: Every failure stores a machine-readable
failure_reasonin the DB. Codes:triage:timeout,triage:claude_failed,fix:timeout,fix:no_changes,fix:tests_failed,fix:commit_failed,fix:claude_failed,worktree:create_failed,accept:push_failed,accept:pr_failed,pipeline:unexpected. Shown in dashboard failure cards with human-readable hints fromFAILURE_HINTS. -
Issue logs: Triage writes raw Claude NDJSON to
/tmp/patchwork-{id}-triage.ndjson; fix writes to/tmp/patchwork-{id}-fix.ndjson. -
Streaming progress:
createStreamReporterin daemon.ts trackstool_use+resultevents from the Claude stream. Broadcasts{ type: "stream_progress", issueId, stage, tool, elapsed, toolCallCount, costUsd }to web dashboard via WebSocket. Also updates Telegram if available (throttled to 3s viaeditMessage). -
React web dashboard: Built with React + Tailwind CSS + Vite. Served by Hono from
dist/atlocalhost:7842. Features: issue list with filters (workspace/project/source/status), issue detail with triage/diff/actions, streaming progress, settings management, setup wizard. API routes via Hono (src/api/). WebSocket/wsfor real-timeissue_updateandstream_progressevents. Dev mode:bun run devruns Vite on:5173with proxy to API on:7842. -
Shared action execution:
Daemon.executeAction(data: CallbackData)is the single entry point for all actions (from both Telegram callbacks and dashboard REST). Returns{ ok, error?, issue? }.handleCallbackis now a thin Telegram wrapper callingexecuteAction. -
Build pipeline:
bun run buildruns Vite to build the React dashboard intodist/.bun run devstarts the daemon with--watchand Vite dev server concurrently. Production:bun run startbuilds then starts.
Auth tokens are stored in SQLite (in source_configs.config JSON and telegram_configs columns). On first run, tokens from config.json are auto-imported.
All runtime data lives in ~/.patchwork/:
config.json— legacy config (auto-imported into SQLite on first run)sqlite.db/sqlite.db-wal/sqlite.db-shm— databasepatchwork.pid— daemon PID file
.patchwork-worktrees/— temporary worktreespatchwork.stdout.log/patchwork.stderr.log— service logsdist/— built dashboard output (generated bybun run build)