Skip to main content
Glama
Muratovnik

Valkama

by Muratovnik
README.md
# Valkama

**A kanban board your coding agents write to, on your own machine.**

Valkama is a local [MCP](https://modelcontextprotocol.io) server with a web
board attached. Register it once in Claude Code or Codex, and your agents claim
cards, tick checklists and attach commits as they work. You watch it happen in a
browser window or a desktop app, on the same board where you plan.

It is built for one person: whoever runs a local development loop and is tired
of the work living in three places at once. There is no account, no cloud and no
service to keep alive. Everything is one SQLite file in your home directory, and
the server only exists while a client is talking to it.

## What it does

- **30 MCP tools** for the board — create, move, claim, comment, link, check off,
  summarise. `claim_card` is atomic, so two agents cannot start the same card.
- **A live board UI** at `http://127.0.0.1:8642/`, in English or Russian. It
  never polls: updates arrive over Server-Sent Events.
- **A record of who did what.** Every move, claim, takeover and link change is
  an event on the card, next to commit hashes and session ids the agents attach.
- **Session tracking.** Launch an agent from a card with an explicit delivery
  contract, and the outcome comes back as a typed result instead of an exit code.
- **A dashboard** over the same data — flow, throughput, cycle time — that
  reports "unknown" instead of inventing a zero when history is missing.
- **A desktop window and a tray icon** on Windows, so the board is an app rather
  than a browser tab, and you can see at a glance whether an agent is working.

## Requirements

| | Needed for | Version |
| --- | --- | --- |
| Python | the server, the MCP tools, the CLI | 3.11 or newer |
| Node.js | rebuilding the web UI or the desktop app | 20.19+ or 22.12+ — Vite 7's floor (only if you build) |
| Git | cloning | any |

The server imports **nothing outside the Python standard library** — no `pip
install` step, no virtualenv. That is deliberate: your agent client launches it
with whatever Python it finds, and a missing dependency would be a failure you
only see as a disappeared board.

The built UI is committed to the repository, so a fresh clone serves the board
without Node installed at all.

**Platforms.** Developed and used on Windows 11. The server's platform-specific
branches are guarded and it should run on macOS and Linux, but that is not
exercised. The tray icon, the installer and session-root lookup are
Windows-only by design — see [Limitations](#limitations).

## Install

### 1. Clone it

```bash
git clone https://github.com/Muratovnik/valkama.git
cd valkama
python valkama.py serve
```

Open `http://127.0.0.1:8642/`. You should see an empty board directory. Stop the
server with `Ctrl+C` — agents spawn their own copy and do not need this one
running.

### 2. Register it with your agent

**Claude Code** — one command, user scope so every project gets it:

```bash
claude mcp add valkama --scope user --env VALKAMA_AUTHOR=claude -- python /absolute/path/to/valkama/valkama.py mcp
```

**Codex** — add this to `~/.codex/config.toml`:

```toml
[mcp_servers.valkama]
command = "python"
args = ["/absolute/path/to/valkama/valkama.py", "mcp"]
env = { VALKAMA_AUTHOR = "codex", PYTHONUTF8 = "1", PYTHONIOENCODING = "utf-8" }
```

Use an absolute path in both. The client launches the server from its own
working directory, which is rarely yours.

`VALKAMA_AUTHOR` is what claims and comments are signed with. Give each client a
different one, or every card will say `agent`.

### 3. Check that it worked

Restart the client, then ask it to list boards. In Claude Code:

```
> use the valkama mcp server to list boards
```

You should get an empty list rather than an error. If the tools are missing,
`/mcp` shows the server's status; a server that failed to start needs a new
conversation, not a reconnect.

### 4. Optional: the desktop app (Windows)

```powershell
cd desktop
npm ci
npm run dist                            # builds release/Valkama Setup <version>.exe
cd ..
.\windows\install-desktop.ps1 -WhatIf   # show what would change
.\windows\install-desktop.ps1 -Build    # rebuild, remove older copies, install
```

The installer is not a double-click because each past rename changed the NSIS
`appId`, so a new installer lands *beside* the old copy instead of over it.
`install-desktop.ps1` reads what is actually registered under `HKCU`, stops
anything running from those directories, runs each old copy's own uninstaller,
and installs the current one. It only ever touches something registered under
one of this product's names *and* installed under `%LOCALAPPDATA%\Programs`;
anything else is reported and left alone.

## Commands

```
python valkama.py <command> [options]
```

| Command | What it does |
| --- | --- |
| `mcp` | speak MCP over stdio — what the agent registrations run |
| `serve [--port 8642]` | serve the JSON API and the board UI |
| `summary [--board NAME]` | a short text report, meant for session hooks |
| `runtime` | print this checkout's backend and static identity |
| `dump [--out FILE]` | export the whole database as JSONL |
| `graph [--board NAME] [--format mermaid\|json]` | the dependency graph of one board |
| `merge-boards --target NAME --source OLD [--source ...]` | consolidate boards, keeping every id |
| `attach NAME PATH [--label TEXT]` | attach another database file as a scope |
| `detach NAME` | drop a scope from every view; the file is untouched |
| `scopes [--json]` | list attached scopes and their boards |
| `purge-stream [--session ID] [--include-active] [--retention]` | delete purgeable stream events; analytics rows stay |
| `import-routa FILE` | import a Routa export |

## Configuration

Everything is an environment variable; there is no configuration file.

| Variable | Default | What it changes |
| --- | --- | --- |
| `VALKAMA_DB` | `~/.valkama/valkama.sqlite3` | where the database lives |
| `VALKAMA_AUTHOR` | `agent` | the name written into claims and comments |
| `VALKAMA_NO_TRAY` | unset | set to anything to suppress the tray icon |
| `VALKAMA_SOURCE_ROOT` | the checkout's grandparent | the workspace root the desktop app may open files from |
| `VALKAMA_PORT` | `8642` | the port the desktop app expects the server on |
| `VALKAMA_SCRIPT` | derived from the install record | absolute path to `valkama.py`, for the desktop app and tray |
| `VALKAMA_PYTHON` | `python` from `PATH` | which interpreter the desktop app and tray launch |
| `VALKAMA_TRAY_MUTEX` | `Local\ValkamaTrayIcon` | the mutex tray sessions coordinate through |
| `CODEX_WORKFLOW_REGISTRY` | `%LOCALAPPDATA%\CodexWorkflow\projects.json` | the registry that maps a session to its project root |

`--port` on `serve` overrides the port for that process. `VALKAMA_PORT` tells the
*desktop app* where to look; set both if you move the port.

## The board

Six fixed columns: Backlog, Todo, Dev, Review, Done, Blocked.

A card becomes an **epic** when other cards point `parent_id` at it. The board
keeps a persistent index of epics and renders one selected six-column view;
inside each lane, children are grouped under their epic instead of repeating the
epic id on every card.

Three things make a card honest about its own progress:

- **A checklist with stable ids.** Agents claim an item (`claim_checklist_item`)
  and complete it by id (`tick_item`). No invented percentages, and two agents
  splitting a card each hold their own item.
- **Typed refs** — commit hashes, session ids, memory entry ids — attached with
  `attach_ref`, so the next agent reads pointers instead of doing archaeology.
- **An activity trace.** Creations, moves, claims, takeovers, releases,
  completions and link changes are all recorded with an author. `get_card`
  returns it, so nobody has to guess who did what. Reordering within a lane is
  deliberately *not* history.

Cards relate to each other the way [beads](https://github.com/steveyegge/beads)
proved useful for agents. `blocked_by` / `blocks` feed `list_cards(ready=true)`
— the take-next queue of cards that are queued, unclaimed and unblocked.
`discovered_from` records where work found en route came from. Blockers that
aren't cards stay comments.

Guards refuse a move the board could not honestly report: `dev` needs an
executor, `done` needs every checklist item closed and a summary, `blocked`
needs a linked blocker or a stated reason. A refusal names its guard, and
`force=true` carries the move through as a recorded override.

**Concurrent moves are resolved, not raced.** A drag sends the column the card
was in when you grabbed it. If it has since moved, the server answers `409` with
who moved it where, and the drop is refused rather than silently undoing an
agent's work.

### Rolling back a merge

`merge-boards` writes a clean SQLite backup before it touches anything. To undo
one: stop every client, delete any `-wal` / `-shm` file beside
`~/.valkama/valkama.sqlite3`, then copy the `kanban-premerge-*.sqlite3` snapshot
named in that merge's own result over the database.
`test_a_merge_can_be_rolled_back_from_the_snapshot_it_takes` runs exactly that
sequence, so the path is tested rather than assumed.

Schema upgrades snapshot the database into `~/.valkama/backups/` before any
`ALTER` runs, keeping the last five.

## Launching agents from a card

The launch dialog records a delivery contract, not just a command: an
`expected_effect` (`change_required`, `no_change_acceptable` or
`read_only_finding`), an optional review mode, and an interface version.

Both clients get an enforced JSON result schema. The persisted outcome is one of
`launch_failed`, `refused`, `expected_no_change`, `unexpected_no_change`,
`partial` or `complete` — **exit code zero is not delivery.** Only `complete`, or
an `expected_no_change` the contract allows, moves Dev to Review. A spawn failure
restores the exact pre-launch lane and claim and stays visible on the card.

Resume works only with the client's own stored session identity: a generated
UUID for fresh Claude launches, the thread id from Codex's JSON event stream.
The runner never guesses which task to continue from a title, a directory, a
timestamp or `--last`.

Worktree launches run a preflight before mutating anything: the path must be a
repository top level and not a submodule, and the sibling target must not
escape, traverse a symlink or junction, or already exist without a real worktree
registration.

## The dashboard

`?board=<name>&view=dashboard` is a read-only projection over the same cards and
events: status visits, flow, throughput, cycle/reopen/blocked KPIs, and an
`as_of` timestamp.

Its rule is that missing history stays missing. Coverage is reported as
`confirmed`, `inferred`, `partial` or `unknown`; a gap is `null` or a lower
bound, never a made-up zero. A contradictory move invalidates the preceding
segment at the gap and resumes at the observed destination, so no duration is
fabricated across a hole.

The optional usage panel reads local Codex rollout or Claude JSONL journals,
if you configure the roots. It is deliberately conservative: exact card-linked
session refs only, no newest-file heuristics, data marked
`source=local_journal`. Cost appears only when a journal states it. Sessions
linked to several cards are labelled non-exclusive and counted once.

## The desktop app and the tray

`desktop/` is a thin Electron shell. It checks that the Python server is
listening, then shows the board in a real window with its own process, taskbar
entry and icon. It runs the server **from this repository**, not a bundled copy,
so the window and your agents can never be on different versions.

```bash
cd desktop
npm ci
npm start        # run the window from source
npm run dist     # build the installer into release/
```

`release/` is git-ignored — a 90 MB installer does not belong in a repository.
Rebuild and reinstall after changing `desktop/main.js`; UI-only changes just
need `npm run build` in `web/`.

The window can open a Markdown source at an exact `#kb:` anchor through VS Code's
URL handler, but only inside the approved workspace root. Plain browser mode has
no local-file authority and copies the pointer instead.

**The tray icon** appears while an agent holds an MCP session and disappears when
the last one ends, so it answers "is anybody working here right now?". Clicking
it always does something visible: it opens the app, opens a browser, starts the
server, offers a restart, or explains the refusal in a dialog. When the running
server is this checkout's but older than your working tree, the dialog says which
half drifted and offers to restart it. A listener that does not identify itself
as this checkout is refused — holding the port is not proof of whose process it
is.

Set `VALKAMA_NO_TRAY=1` to turn it off.

> Running `electron` from a VS Code terminal fails with `app is undefined`,
> because VS Code exports `ELECTRON_RUN_AS_NODE=1`. Clear that variable first.
> The installed app is unaffected.

## Building the UI

```bash
cd web
npm ci
npm run dev      # Vite on :5173, proxying /api to the Python server on :8642
npm run build    # refresh web/dist
```

Vue 3 + Vite + TypeScript. `web/dist` is committed on purpose, so a clone serves
the built board with no build step — **rebuild and commit it whenever `web/src`
changes.**

The Python side serves `web/dist` plus the JSON API: `/api/boards`,
`/api/board`, `/api/card`, `/api/activity`, `/api/move`, `/api/events`.
`GET /api/integrations` is a read-only inventory of observed session adapters and
local knowledge services — identity, capabilities, health and who owns their
configuration. It loads no third-party code and invents no enable switches.

## How the repository is laid out

| Path | Owns |
| --- | --- |
| `valkama.py` | the entry point, and nothing else — it prepares `sys.path` and hands off to `server/cli.py` |
| `server/` | the Python server: domain modules, and the `cli`, `http_surface` and `mcp_surface` above them |
| `web/` | the Vue application, its build and its tests |
| `desktop/` | the Electron window — its own npm package, tests and release output |
| `windows/` | what exists only because the host is Windows: the tray script, the icon, its generator, the installer |
| `tests/` | the Python suite, mirroring `server/` |
| `docs/` | the product's written contracts |

`windows/` is separate from `desktop/` because they answer different questions.
`desktop/` is an application — JavaScript, npm, an asar. `windows/` is
PowerShell, an `.ico` and an NSIS installer, and the Electron app is one of its
consumers rather than its owner.

Five documents state what code cannot:

- [`DESIGN.md`](DESIGN.md) — the design system, and the authority for every
  visual decision.
- [`docs/platform-contract.md`](docs/platform-contract.md) — the two operating
  levels and the object kinds a module may not confuse.
- [`docs/connection-contract.md`](docs/connection-contract.md) — what a client
  learns on connect, and how the server refuses to answer from a stale build.
- [`docs/improvements-contract.md`](docs/improvements-contract.md) and
  [`docs/session-event-contract.md`](docs/session-event-contract.md) — the
  improvements payload and the session event.

## Limitations

Worth knowing before you adopt it:

- **Single user, single machine.** No authentication, no multi-tenancy, no
  remote access. The server binds `127.0.0.1` and assumes everyone who can reach
  it is you.
- **Windows is the exercised platform.** The tray, the installer and the
  session-root registry lookup are Windows-only. The server should run on macOS
  and Linux; nobody has proven it.
- **No MCP prompts or resources.** The board is a task queue. Project context
  belongs somewhere else.
- **No hosted anything.** No sync between machines, no shared board, no backup
  other than the snapshots it takes locally.
- **The MCP server is spawned by your client**, so it lives as long as the
  session and does not update itself. See
  [`docs/connection-contract.md`](docs/connection-contract.md) for how version
  skew is made visible instead of silent.

## Development

Every change runs all of these, from the repository root. The Python toolchain
is pinned in `requirements-dev.txt` and configured in `pyproject.toml`; none of
it is a runtime dependency.

```powershell
python -m ruff check .
python -m ruff format --check .
typos
vulture server tests valkama.py vulture_whitelist.py --min-confidence 60
mypy
lint-imports --cache-dir .cache/import-linter
semgrep scan --config p/python --config p/security-audit --metrics off --error server valkama.py
python -m coverage run -m unittest discover -s . -p "test_*.py"
python -m coverage report
cd web
npm test
npm run typecheck
npm run build
cd ../desktop
npm test
```

`npm run build` is not optional and not last. The server serves the snapshot of
`web/dist` it read at startup, so a browser check against a listener that
predates the build inspects the bundle that build replaced. Rebuild, restart the
listener, *then* verify anything live.

Issues and pull requests are welcome — [`CONTRIBUTING.md`](CONTRIBUTING.md) has
the setup, the commit convention, and the handful of architectural rules that
would otherwise send a pull request back. Released changes are recorded in
[`CHANGELOG.md`](CHANGELOG.md).

## License

[MIT](LICENSE).