Architecture
The core design principle: dumb server, smart agents.
The server has zero AI. It makes no LLM calls, holds no API keys, and forms no opinions about your codebase. It’s a pure CRUD coordination layer with atomic task pickup. All planning, decomposition, and reasoning happens in the agents.
System layers
┌─────────────────────────────────────────────────────┐
│ Agents (any machine, any tool) │
│ Claude Code · Codex · Cursor · Copilot · scripts │
└────────────────────┬────────────────────────────────┘
│ cpk CLI (~250 tokens/call)
▼
┌─────────────────────────────────────────────────────┐
│ codepakt server (Hono + SQLite) │
│ ├── REST API /api/* (port 41920) │
│ └── Dashboard / (same port) │
└────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ <project>/.codepakt/data.db (SQLite, WAL mode) │
└─────────────────────────────────────────────────────┘
Technology stack
| Layer | Choice | Reason |
|---|---|---|
| HTTP framework | Hono | Lightweight, fast, TypeScript-native |
| Database | better-sqlite3 | Synchronous API, WAL mode, zero config, embeds in npm package |
| CLI | Commander.js | Mature, minimal, well-typed |
| Validation | Zod | Runtime type safety at API boundaries |
| Query layer | Typed functions, no ORM | SQL is readable; ORMs add abstraction without benefit here |
Single npm package
Everything ships in one package: codepakt.
- CLI binary (
cpk) - HTTP server (Hono)
- SQLite driver (better-sqlite3)
- Dashboard HTML/JS (served from the same process)
No docker. No separate database service. No config file beyond what cpk init creates. Install and run.
Atomic task pickup
The central concurrency problem: two agents calling cpk task pickup simultaneously must not receive the same task.
Solution: BEGIN IMMEDIATE transaction in SQLite.
BEGIN IMMEDIATE;
SELECT id FROM tasks
WHERE status = 'open'
AND deps_met = 1
ORDER BY CASE priority WHEN 'P0' THEN 0 WHEN 'P1' THEN 1 ELSE 2 END, created_at ASC
LIMIT 1;
UPDATE tasks SET status = 'in-progress', assignee = :agent WHERE id = :task_id;
COMMIT;
BEGIN IMMEDIATE acquires a write lock at transaction start, before the SELECT. Any concurrent transaction attempting to start waits. The first writer wins; others retry. This is SQLite’s built-in serialization — no application-level locking needed.
Audit trail
Every mutation (task creation, status change, pickup, completion) writes to an events table:
events
id TEXT PRIMARY KEY
type TEXT -- 'task.created', 'task.pickup', 'task.done', etc.
entity_id TEXT -- task ID, agent name, doc ID
actor TEXT -- agent name or 'system'
payload TEXT -- JSON snapshot
created_at TEXT
This is an append-only log. Nothing is ever deleted from events. It’s the source of truth for “what happened and when.”
Dependency resolution
When a task reaches review (agent calls cpk task done), the server runs a dependency check:
- Find all tasks whose
depends_onincludes the completed task’s ID - For each, check whether all other dependencies are in
reviewordone - If yes, set
deps_met = trueand transition frombacklogtoopen
Dependencies resolve on review, not on done. This means the pipeline keeps moving without waiting for human approval. The check runs synchronously inside the same transaction. No background jobs, no eventual consistency.
Dashboard delivery
The dashboard is a static HTML/JS application bundled into the npm package. The server serves it from the root path (/). The JS makes fetch calls to /api/* — the same origin, no CORS needed.
In v0.1 the dashboard is embedded in the server binary at build time. There is no separate deployment step.
What’s not in the server
- No authentication or authorization (v0.1, single-user)
- No LLM calls
- No file watching or filesystem access
- No webhooks or external integrations
- No background job queue
- No Postgres (planned for v0.2)
Keeping the server simple is a deliberate choice. Complexity lives in the agents, where it can be reasoned about, tested, and replaced independently.
Related
- Agents & Coordination — how the “smart” part works