This is Linear Brain — a project management tool for a small team (2 devs, 2-3 designers, no PM). It reads from Linear freely and provides a human-approval queue for write operations, managed through a web dashboard. The intelligence layer is Claude Code (CC), operated interactively — there is no automated AI in the app.
This product learns per-team. Whenever the user requests a change to general CC behaviour, update the appropriate local file immediately:
- Board conventions (ticket naming, labels, descriptions, workflows) →
BOARD_RULES.md - Product behaviour (coding conventions, safety rules, architecture) →
CLAUDE.mdorARCHITECTURE.md
This ensures the repo is self-contained — anyone who clones it gets the same behaviour without needing private memory or context.
Do NOT update these files for one-off requests (e.g. "fix this specific ticket", "create a proposal for X"). Only persist rules that should apply to all future interactions.
Every write operation to Linear MUST go through the approval queue. The AI must NEVER directly mutate Linear data. All mutations flow through src/queue/executor.ts which requires an approved proposal. No exceptions.
The only module that imports Linear SDK write methods is src/linear/writer.ts. No other file should ever call linearClient.createIssue(), linearClient.updateIssue(), or any mutation method directly.
- Runtime: Bun (latest stable)
- Language: TypeScript (strict mode)
- Linear:
@linear/sdk— their official SDK wraps the GraphQL API - API Server: Hono (lightweight, runs on Bun natively) — JSON API only
- Frontend: React SPA with Ant Design, built by Vite, served as static files by Hono in production
- Database: SQLite via
bun:sqlite— used for the approval queue, audit log, dashboard snapshots, and insights - AI: Claude Code (CC) — interactive + headless (
claude -pvia Opus) for automated actions - Validation: Zod for external data boundaries (webhook payloads, user input)
linear-brain/
├── src/
│ ├── index.ts # entry: starts Hono server
│ ├── config.ts # env loading, typed config
│ ├── linear/
│ │ ├── client.ts # singleton Linear SDK client
│ │ ├── reader.ts # ALL read operations (free to call anytime)
│ │ └── writer.ts # ALL write operations (ONLY via approved proposals)
│ ├── queue/
│ │ ├── db.ts # SQLite schema + migrations
│ │ ├── proposals.ts # proposal CRUD
│ │ └── executor.ts # executes APPROVED proposals only
│ ├── dashboard/
│ │ ├── types.ts # DashboardSnapshot, MemberStats, etc.
│ │ ├── snapshot.ts # Fetches Linear data and computes dashboard stats
│ │ └── store.ts # SQLite CRUD for dashboard_snapshots table
│ ├── actions/
│ │ ├── gather-board.ts # Shared: gathers full board state from Linear
│ │ ├── clean-drafts.ts # Headless CC action: clean up DRAFT tickets
│ │ ├── audit-board.ts # Headless CC action: audit board for issues
│ │ └── generate-insight.ts # Headless CC action: generate PM-style insight
│ └── server/
│ ├── app.ts # Hono app setup + static file serving
│ └── routes/
│ ├── api.ts # JSON API: proposals, dashboard, actions, insights
│ └── webhooks.ts # Linear webhook receiver (future)
├── web/ # React SPA (built by Vite)
│ ├── index.html # Vite entry
│ ├── vite.config.ts # Vite config (proxy, build output)
│ ├── tsconfig.json # Separate TS config for React (DOM libs, JSX)
│ └── src/
│ ├── main.tsx # React mount point
│ ├── App.tsx # Root component with router + Ant Design
│ ├── api.ts # Fetch wrapper for /api/* endpoints
│ ├── types.ts # Shared types (Proposal, AuditEntry, DashboardSnapshot, Insight)
│ └── pages/
│ ├── Dashboard.tsx # Project overview with stats, members, blockers
│ ├── ProposalList.tsx # Proposal queue + Tidy Drafts / Audit Board buttons
│ ├── ProposalDetail.tsx # Single proposal with structured payload view
│ ├── Insights.tsx # AI-generated board insights (PM briefing)
│ └── AuditLog.tsx # Dev-only audit log
├── dist/web/ # Built frontend (gitignored)
├── tests/
│ ├── queue.test.ts
│ ├── reader.test.ts
│ └── proposals.test.ts
├── CLAUDE.md # this file
├── ARCHITECTURE.md
├── PROGRESS.md
├── BOARD_RULES.md # team-specific board conventions (gitignored)
├── BOARD_RULES.example.md # template for new installations
├── package.json
├── tsconfig.json
├── bunfig.toml
└── .env
- No classes unless wrapping an SDK. Use plain functions and modules.
- Explicit imports. No barrel files (
index.tsre-exports). Import from the actual file. - Zod at boundaries. Validate all: webhook payloads and user input from the dashboard.
- Error handling: Use try/catch at the top of each route handler. Never let errors crash the process silently. Log them clearly.
- Naming: Files are
kebab-case.ts. Functions arecamelCase. Types/interfaces arePascalCase. Database columns aresnake_case. - No
anytype. Ever. Useunknownand narrow. - Logging: Plain
console.log/console.errorwith prefixes like[linear-reader],[proposal-queue],[executor]. No logging library needed yet.
- We use the
@linear/sdkpackage which provides a typed GraphQL client. - If your organisation has MCP disabled (common on enterprise plans), this project is a good alternative — everything goes through the SDK/API.
- The SDK uses a personal API key (stored in
LINEAR_API_KEYenv var). - Rate limits: Linear allows 1,500 requests per hour. The reader should cache aggressively and avoid polling more than once per minute for any given resource.
- Pagination: Linear uses cursor-based pagination. Always handle it — don't assume a single page returns everything.
- No Anthropic API SDK.
@anthropic-ai/sdkis NOT used. NoANTHROPIC_API_KEYin config. - The primary AI workflow is: CC reads Linear data via reader.ts → CC reasons about it → CC creates proposals in the queue via the API → human reviews proposals on the dashboard → executor runs approved ones.
- Headless actions (
src/actions/): The dashboard triggers headless Claude Code CLI (claude -p --model opus) for automated tasks. The server gathers Linear data (issues, labels, states, comments), sends it to CC with a structured prompt, and processes the output. Three actions exist:- Tidy Drafts — finds DRAFT-labeled tickets, cleans up titles/descriptions/labels per BOARD_RULES, suggests estimates. Results go through the approval queue.
- Audit Board — reviews the entire board for convention violations, missing info, related tickets, and suggests fixes. Results go through the approval queue.
- Generate Insight — produces a structured PM-style daily briefing (summary, cycle progress, focus areas, team performance, risks, recommendations). Stored in the
insightstable, not proposals — it's a read-only report.
- All headless actions include issue comments in their context so CC can see clarifications and discussions.
- Requires
claudeCLI to be installed and on PATH for headless actions. Bun.serveusesidleTimeout: 255to accommodate long-running headless CC calls.
Team-specific board conventions (ticket naming, label groups, description format, DRAFT workflow) live in BOARD_RULES.md. This file is gitignored — each installation maintains their own.
- New users:
cp BOARD_RULES.example.md BOARD_RULES.mdand edit to match your board. - CC reads
BOARD_RULES.mdat the start of every interaction and follows those rules when creating, updating, or auditing tickets. - CC updates
BOARD_RULES.mdwhen it learns new general conventions from the user (see Self-Improvement Rule above). One-off requests are not persisted. - If
BOARD_RULES.mdis missing, prompt the user to create one from the example.
- React SPA with Ant Design (dark theme, shadcn-inspired palette), built by Vite, served as static files by Hono in production.
- Frontend source lives in
web/, built output goes todist/web/(gitignored). web/tsconfig.jsonis a separate TS config (DOM libs, react-jsx). Root tsconfig excludesweb/anddist/.- In dev: Vite runs on port 5173 with HMR, proxies
/apiand/webhooksto Hono on port 3000. - In prod:
bun run startbuilds the frontend then starts Hono which serves both the API and static files on port 3000. - Custom styling uses inline styles (no Tailwind, no CSS files). Ant Design handles component styling.
- Dashboard (
/) — project overview with summary stats, cycle progress, issue breakdown, team member table, blockers, and stale issues. Data comes from dashboard snapshots (refreshable via button orPOST /api/dashboard/snapshot). - Proposals (
/proposals) — approval queue for Linear write operations. Pending proposals at top, history in a collapsible accordion with pagination. Includes "Tidy Drafts" and "Audit Board" buttons that trigger headless CC actions. Approve All / Reject All for batch operations. - Proposal Detail (
/proposals/:id) — structured view of a single proposal with human-readable payload ("Changes" section), reasoning, and approve/reject buttons. - Insights (
/insights) — AI-generated PM briefings. Latest insight displayed in full, previous insights in collapsible accordions. "Generate Insight" button triggers Opus analysis. - Audit Log (
/audit) — dev-only page, icon-only button in the header far right. Shows raw audit trail.
- Proposal/AuditEntry/DashboardSnapshot/Insight types are duplicated between
src/andweb/src/types.ts— keep in sync manually.
- Use Bun's built-in test runner (
bun test). - Test the approval queue logic thoroughly — this is the safety layer.
- Mock the Linear SDK client in tests. Never hit the real API in tests.
- Test that
writer.tsrefuses to execute unapproved proposals.
LINEAR_API_KEY=lin_api_xxxxx
LINEAR_WEBHOOK_SECRET=whsec_xxxxx # optional; if set, webhook signatures are verified
PORT=3000
DATABASE_PATH=./data/brain.db
- Conventional commits:
feat:,fix:,chore:,docs: - Branch from
main, PR back tomain - No force pushes to
main
- Self-verify after every task. Run all checks yourself (type-check, tests, server start, etc.). Never ask the user to verify — fix any failures before marking a task done.
- Linear SDK ships a
@linear/sdk/webhookssub-package withLinearWebhookClient. UsewebhookClient.verify(Buffer.from(rawBody), signature, timestamp)— note the first arg must beBuffer, notstring. LINEAR_WEBHOOK_SECRETis optional in config (null when absent). When absent, accept without verification but log a warning (dev convenience). When present, reject missing/invalid signatures.webhookClient.verify()can throw (e.g. on malformed signatures) — wrap in try/catch and treat exceptions as invalid.
- Bun 1.3.10 does NOT support
[test.env]in bunfig.toml. Use.env.testinstead — Bun automatically loads it when runningbun test. mock.module()must be called before any import that transitively loads the module being mocked. Place it at the top of the test file before other imports.- Use
DATABASE_PATH=:memory:in.env.testto keep tests fully in-memory — no files created, no cleanup needed. - Use
beforeEach(() => { db.run("DELETE FROM ...") })for test isolation within a test file that shares the singleton DB.
listProposals()takesProposalStatus, notProposalType— easy to mix up when casting query params. Always import the right type explicitly.- Hono's
c.redirect()with status 303 is correct for post-then-redirect pattern (form submits). - For routes used by both forms (HTML) and CC (JSON), check
Acceptheader to decide response format on errors.
- Linear SDK mutation input types (
IssueCreateInput,IssueUpdateInput,CommentCreateInput) are also not exported. UseParameters<typeof linearClient.createIssue>[0]etc. to extract them. linearClient.createIssue()returns a payload with asuccessflag and an.issuegetter that itself returns aLinearFetch(another async call needed to resolve the issue object).
- On macOS, chaining
bun run src/index.ts & sleep 3 && curl ...doesn't work — the shell treats extra tokens as sleep arguments. Run server in background separately (run_in_background: true), then issue curl in a follow-up command. /debug/linearroute confirmed real Linear workspace data returns correctly with the API key.
IssuesQueryVariablesis not exported from@linear/sdk. UseParameters<typeof linearClient.issues>[0]to get the type from the SDK directly.bunx tsc --noEmitmust be run without individual file args to pick up tsconfig.json (which enablesallowImportingTsExtensions). Running it with file args ignores tsconfig and gives false errors.- The Linear SDK
LinearFetch<T>resolves toT | undefined, so callers ofgetIssue()should handleundefined. - Pagination pattern: loop with
collectAll()helper, passaftercursor, stop whenpageInfo.hasNextPageis false.
- User prefers plain TS over Zod for config/env validation. Use
requireEnv()helper pattern instead of Zod schemas for env vars. bun --checkexecutes the file rather than just type-checking; usebunx tsc --noEmitfor type-checking individual files.
bun initauto-creates a.cursor/folder with IDE rules — delete it immediately, we use Claude Code only.bun initplacesindex.tsat the project root; move it tosrc/index.tsto match the project structure.- Package name should be
linear-brain(matches the project name in the docs), notlinear-bot(the directory name). - Installed versions:
@linear/sdk@77.0.0,hono@4.12.6,zod@4.3.6. (@anthropic-ai/sdkwas removed — CC-driven, no in-app AI.) - Note: zod 4.x is installed (not 3.x). Be aware of any API differences if tasks assume zod v3 patterns.