Skip to main content
Glama

Valkama

A local, modular control surface for agent work and its supporting tools.

Valkama connects Claude Code, Codex, and local services through one web and desktop workspace. Planning, Sessions, Analytics, Improvements, Skills, Memory, and Settings are modules over the same Kernel contracts. Kanban is one Planning view, not the definition of the product.

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. Durable state stays in local SQLite stores in your home directory, and the MCP server only exists while a client is talking to it.

What it does

  • Planning with Kanban, list, and graph views. Its MCP tools create, move, claim, comment, link, and summarise work; claim_work_item refuses a competing claim on the current Planning wire.

  • Sessions and execution tracking. Launches carry an explicit delivery contract, and outcomes are typed results rather than exit codes.

  • A visible integration registry. Settings distinguishes Modules, Adapters, Services, Connections, and Assignments, including health and ownership.

  • Analytics and Improvements. Missing history stays unknown, and recurring failures retain bounded evidence instead of raw private transcripts.

  • Memory that points rather than copies. Each project's knowledge is searched through whichever provider answers for it, and what is attached to a work item is a pointer with a stored label — so it still reads when the source behind it cannot be reached.

  • A live local UI at http://127.0.0.1:8642/, in English or Russian, updated over Server-Sent Events rather than polling.

  • A desktop window and tray icon on Windows, so Valkama is an application rather than a browser tab and active agent work remains visible.

Related MCP server: Pigeon MCP Server

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 and launcher import nothing outside the Python standard library — no pip install step, no virtualenv. That is deliberate: your agent client launches Valkama with the selected system Python, and a missing dependency would otherwise appear only as a missing control surface.

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

Platforms. Developed and used on Windows 11. The service launcher is tested on Windows and Linux. The server's other platform-specific branches should run on macOS and Linux, but the tray icon, desktop installer and session-root lookup are Windows-only by design — see Limitations.

Install

1. Get the source

Clone the repository or download valkama-<version>-source.zip from a release and extract it. The source archive includes the Python service and built web UI; it does not need Node.js to run. From the resulting directory:

python valkama.py serve

For a Git clone, first run git clone https://github.com/Muratovnik/valkama.git and cd valkama.

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

2. Install the stable launcher

python valkama.py launcher install
python valkama.py launcher status

The status output is JSON. Its command field is the path agent clients should register:

  • Windows: %LOCALAPPDATA%\Valkama\bin\valkama.cmd

  • macOS/Linux: $XDG_BIN_HOME/valkama or ~/.local/bin/valkama

The user-facing command contains no source-checkout path. A managed Python shim beside it is the durable process identity; its manifest records the current valkama.py, interpreter and managed hashes. Agent clients, the tray and the desktop all use that verified pair. After moving the checkout, run launcher install once from the new location; client configuration and listener ownership remain unchanged. See SERVICE.md for ownership, safe listener reconciliation, drift handling, uninstall and relocation details.

3. Register it with your agent

Claude Code — PowerShell example, user scope so every project gets it:

$launcher = (python .\valkama.py launcher status | ConvertFrom-Json).command
claude mcp add valkama --scope user --env VALKAMA_AUTHOR=claude --env PYTHONUTF8=1 --env PYTHONIOENCODING=utf-8 -- $launcher mcp

Codex — use the exact command returned by launcher status in ~/.codex/config.toml:

[mcp_servers.valkama]
command = "C:/Users/<user>/AppData/Local/Valkama/bin/valkama.cmd"
args = ["mcp"]
env = { VALKAMA_AUTHOR = "codex", PYTHONUTF8 = "1", PYTHONIOENCODING = "utf-8" }

This absolute path belongs to the installed service launcher, not to the source checkout.

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

4. Check that it worked

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

> use the valkama mcp server to list planning spaces

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.

For local diagnostics without opening or migrating the database:

python valkama.py status

5. Register a project for project views

Valkama keeps a local project inventory. Register an existing directory, using your own lowercase project id, display name, and absolute path. Replace the example values, including the path, with your project's values:

python valkama.py projects add my-project --name "My Project" --root "C:\path\to\my-project"
python valkama.py projects check

The check reports "ok": true when Valkama's inventory and the projection used by its modules agree. To connect the project to Planning, use the MCP create_planning_space tool with project_id set to my-project, then bind the returned space key:

python valkama.py projects bind my-project --space MY

Replace MY with the key returned for your Planning space. A binding must point to a space whose project id matches the registered project. Run python valkama.py projects list to inspect the result.

6. Optional: the desktop app (Windows)

The release's Valkama-Setup-<version>.exe installs the desktop window. It requires the source service, Python 3.11+, and the managed launcher from step 2; the installer does not bundle a Python backend. For a fresh installation, run the installer after installing the launcher. If an older Agent Kanban or Agent Hub desktop app is installed, use the migration script below from the source directory to remove those registered copies before installing the new one.

To build the desktop installer from source:

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 migration script is needed across past renames because each 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

These commands cover the current CLI. Planning MCP tools use spaces and work items; a Kanban card is one view of a work item.

python valkama.py <command> [options]

Command

What it does

mcp

speak MCP over stdio — what the installed launcher runs for agent clients

serve [--port 8642]

serve the JSON API and Valkama UI

capabilities

report public interfaces, MCP revisions and tool count as JSON without reading user data

status [--launcher-directory DIR]

report source, database, runtime and launcher state without opening the database

launcher install [--directory DIR] [--force]

atomically install or repair the stable client command

launcher status [--directory DIR]

verify launcher ownership and hashes

launcher listener status|stop [--port 8642]

inspect or stop only the Windows listener proven to run through the managed shim

launcher uninstall [--directory DIR] [--force]

remove only the managed launcher files

summary [--space KEY]

a short Planning-space report, meant for session hooks

runtime

print this checkout's backend and static identity

projects list|add|update|remove|bind|check|apply|doctor|rollback|import

manage the local project inventory and its projection; see Project registry

config [--json]

every setting, the layer it came from, and the value in use

setup [--json] [--apply]

what this installation needs, and the exact command for each gap

adapter check --at URL | --command ARGV | --mcp ARGV | --record FILE

ask an adapter for its manifest, then probe it for the calls it should refuse

adapter install --at URL | --command ARGV | --mcp ARGV | --record FILE [--force]

check it, then declare it for this installation

adapter remove ADAPTER_ID

stop declaring an installed adapter

adapter list

the adapters this installation declares

doctor [--json] [--no-probe]

check this installation in four levels — Installation, Projects, Capabilities, Connections — and say what to do about each problem

export [--section planning|relations|settings|improvements]

write the selected sections as one export; repeat --section, or omit it for all sections

migrate planning-model [--dry-run]

explicitly convert a Board-era store to Planning once; dry run converts a copy

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 Planning spaces

purge-stream [--session ID] [--include-active] [--retention]

delete purgeable stream events; analytics rows stay

External adapters

An adapter is a service written to be reached by Valkama. It answers three calls, over HTTP under a namespaced prefix so it stays free to serve anything else on the same port:

GET  {base}/valkama/v1/manifest   its own manifest, which is how discovery finds it
GET  {base}/valkama/v1/health     {"status": "ready" | "degraded" | "unavailable"}
POST {base}/valkama/v1/invoke     {"capability_id": ..., "payload": {...}}

or as a process, one JSON request on stdin and one answer on stdout:

{"call": "manifest"}
{"call": "health"}
{"call": "invoke", "capability_id": ..., "payload": {...}}

or, if it is already an MCP server, as tools — valkama.manifest, valkama.health, and one named for each capability it declares. There is no second protocol to implement: a tool is how an MCP server offers anything, and a tool whose name is not a Valkama capability is simply not one, so a general-purpose server can serve Valkama without becoming only that. Valkama opens a session per call and owns no process between them.

Nothing goes on the command line, because argv is readable by every process on the machine. An answer carrying {"error": "..."} is a refusal on either transport: stdin and stdout have no status line, and a refusal only one transport could make is one the conformance tool would miss.

A manifest carries no destination. It is published to the browser and the contract refuses a path or an argv anywhere in one, so where an adapter lives is something you supply — which is also the only order that works, since the destination is how the manifest gets fetched. Installing pairs the two in a record under ~/.valkama/adapters/.

This is not what a backend does. Phoenix, Langfuse and Jaeger are other people's products with their own APIs; Valkama reaches one through a provider written on this side that knows that product's shape. Asking a backend to answer the three calls above would be asking it to become a Valkama plugin.

adapter check needs a destination: --at, --command, --mcp, or an installed record supplied with --record. It fetches and validates the manifest, then probes the running adapter for calls it should refuse, such as an undeclared capability or a payload that is not an object. It also checks that the manifest still identifies the adapter at that destination. A record describing one adapter while its destination runs another cannot establish a valid connection.

Check a maintained adapter at the destination its owner documents, then install that same destination only after the check passes:

python valkama.py adapter check --at http://127.0.0.1:8770
python valkama.py adapter install --at http://127.0.0.1:8770

Process and MCP adapters use their owner-supplied command directly:

python valkama.py adapter check --command ADAPTER_COMMAND ARG...
python valkama.py adapter install --command ADAPTER_COMMAND ARG...
python valkama.py adapter check --mcp MCP_COMMAND ARG...
python valkama.py adapter install --mcp MCP_COMMAND ARG...

Installing writes a record to ~/.valkama/adapters/, one file per adapter. That is the whole mechanism: a built-in adapter is declared in code, an installed one is declared by a file, and both then travel the identical path that turns a declaration into registry rows and a Connection. Installing an adapter whose check failed is refused; --force overrides and says so.

adapter remove deletes the file. The registry rows written on an earlier start stay, so a ref pointing at that adapter still resolves and its Connection reads unavailable — a ref whose target evaporated reads as data loss, and an unavailable Connection reads as what it is.

An installed adapter claims no core ref kind. Claiming one binds every ref of that kind to it and only one provider may, so a manifest able to claim memory would displace the provider you already have, at install time, without you deciding anything.

Configuration

Four settings have a file. Everything else is an environment variable, and the environment overrides the file for every setting, so automation and a development shell never have to edit anything to change one run.

The file is ~/.valkama/config.json, and it may not exist. This example puts the store on another drive and points at Claude Code journals; it is not the default configuration:

{
  "store": "D:/valkama/valkama.sqlite3",
  "claude_session_root": "~/.claude/projects"
}

A value may be a reference instead of a literal: ${VAR} takes the environment variable, ${VAR:-fallback} takes it or the fallback. This is the same syntax .mcp.json uses, including the same behaviour when a reference cannot be resolved — the text stays as written and the failure is reported, rather than the value quietly becoming empty. Use a reference for anything you would not want written down.

python valkama.py config prints every setting, the layer its value came from, and the value the product actually uses. A malformed file stops the product rather than falling back to a default, and python valkama.py doctor says why.

Setting

Variable

Default

What it changes

store

VALKAMA_DB

~/.valkama/valkama.sqlite3

where the database lives

document_roots

VALKAMA_DOC_ROOTS

none

document roots to search, separated by the path separator

claude_session_root

VALKAMA_CLAUDE_SESSION_ROOT

none

where Claude Code keeps its session journals

codex_rollout_root

VALKAMA_CODEX_ROLLOUT_ROOT

none

where Codex keeps its rollout journals

Project registry

The project registry is managed by Valkama's projects command, separate from config.json. On Windows, Valkama keeps the authoritative project inventory at %LOCALAPPDATA%\Valkama\project-registry.toml and publishes a schema-v3 projection at %LOCALAPPDATA%\Valkama\projects.json. Planning, Memory, Skills, and the desktop read that complete projection. A missing or malformed projection exposes no partial project list.

Use projects add to register an existing directory and projects bind to connect it to an existing Planning space. projects update ID --name NAME changes its display name; --root ABS moves its root to an existing directory, even if the old root is gone. projects remove ID removes only the registration, leaving the Planning store untouched. projects check compares the inventory and projection; projects doctor reports the same health result. projects apply republishes the current inventory, and projects rollback restores the exact inventory and projection bytes saved before the preceding change. For a one-time transfer from an existing schema-1 TOML inventory, use projects import --source <absolute-path-to-inventory>. Import refuses to overwrite an inventory or projection with different project data. There is no project-registry path override.

The rest are environment-only, because each is either a one-shot switch or belongs to a process rather than to an installation:

Variable

Default

What it changes

VALKAMA_AUTHOR

agent

the name written into claims and comments

VALKAMA_NO_TRAY

unset

set to anything to suppress the tray icon

VALKAMA_PORT

8642

the port the desktop app expects the server on

VALKAMA_TRAY_MUTEX

Local\ValkamaTrayIcon

the mutex tray sessions coordinate through

--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.

Setting it up again, or checking that it is right

python valkama.py setup

It reports what this installation has — Python, Git, a verified stable launcher, and whether each agent client runs that launcher with its required author and UTF-8 environment — and prints the exact command sequence for anything missing or stale. It changes nothing until you add --apply.

It never edits ~/.claude.json or ~/.codex/config.toml. Both clients own their registration through their own CLI, so this reads through claude mcp get and codex mcp list --json and writes through claude mcp add and codex mcp add, which know their own file's format and scopes.

The check worth having is not "is a server registered" but "does it use the verified service launcher": a registration left pointing at a checkout that moved keeps working until it does not, and says nothing in between. A stale registration is removed before its launcher-backed replacement is added.

Planning: Kanban view

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 a step with claim_work_item_checklist_item and complete it by id with tick_work_item_checklist_item. No invented percentages, and two agents splitting a work item each hold their own step.

  • Typed refs — commit hashes, session ids, memory entry ids — attached with attach_work_item_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_work_item returns it, so nobody has to guess who did what. Reordering within a lane is deliberately not history.

Work items relate to each other the way beads proved useful for agents. A blocks link keeps unfinished dependencies out of claim_ready_work_item, which chooses and claims a ready item in one step. discovered-from records where work found en route came from. Blockers that are not work items 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.

Migrating an older store

A store that still contains the Board-era model refuses to open until you authorize its one-way conversion. Inspect the conversion first, then apply it:

python valkama.py migrate planning-model --dry-run
python valkama.py migrate planning-model

The dry run converts a temporary copy. Before changing the actual store, the migration takes a clean SQLite snapshot in the backups/ directory beside the configured database. The generated filename begins valkama- and includes a migration label such as preproductmodel or preupgrade. To check recovery without replacing your working store, copy the snapshot to a separate path, set VALKAMA_DB to the copy, and run the same dry run and migration there. The database path may differ from the default when store or VALKAMA_DB is configured. Generated snapshots retain the last five files per migration label.

Planning: 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 pre-launch lane and claim while they still belong to that launch. If someone changed the item meanwhile, their state is preserved and the failed attempt records why compensation was skipped.

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.

Analytics dashboard

Open Analytics from the module navigation to see a read-only projection over the project's work items 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 verifies the managed launcher, checks that its Python server is listening, then shows Valkama in a real window with its own process, taskbar entry and icon. It runs runtime and serve through the stable shim, whose manifest selects this repository; there is no second desktop source record or checkout-path fallback.

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. Each click carries only {source, resource_ref}; Electron re-reads the fixed project registry and accepts only the exact existing canonical_root mapped to that Planning-space resource. An absent, malformed, unavailable or ambiguous mapping is refused, and renderer paths, process directories, launcher paths, titles and environment overrides never choose another root. Plain browser mode has no local-file authority and copies the source 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 a listener owned by the managed shim is older than the launcher's current source, the dialog says which half drifted and offers to restart it. A listener whose managed process ownership cannot be verified is refused and left untouched — holding the port or returning Valkama-shaped JSON is not stop authority.

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

python valkama.py serve --development-origin http://127.0.0.1:5173
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 UI with no build step — rebuild and commit it whenever web/src changes.

The Python side serves web/dist plus the module and Planning APIs. GET /api/modules is the canonical persisted module registry; GET /api/platform/registry returns normalized integration definitions, Connections, Assignments, health, and ownership without loading third-party browser code.

Local HTTP security

The listener accepts only the exact authority 127.0.0.1:<bound-port>. Browser writes bootstrap a fresh process-scoped session credential and send it in X-Valkama-Session; the UI keeps it only in memory and retries one write after a server restart. Originless /api/ingest adapters instead read the persistent installation bearer from ~/.valkama/installation-token. These credentials are not interchangeable, never belong in URLs, cookies, browser storage, argv, or environment variables, and every write must be an explicitly framed UTF-8 JSON object. Vite development is the only extra browser origin and must be enabled by the exact --development-origin http://127.0.0.1:5173 flag shown above.

The token file is atomically created with the restrictive file mode supported by the Python standard library. This boundary protects the loopback web surface from hostile sites and DNS rebinding; it does not isolate malicious native code already running as the same OS user, which can read the same user-owned runtime files and process memory.

How the repository is laid out

Path

Owns

valkama.py

the source entry point — it prepares sys.path and hands off to server/cli.py

server/

the Python server, managed launcher, domain modules, and CLI/HTTP/MCP surfaces

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.

Seven documents state what code cannot:

Limitations

Worth knowing before you adopt it:

  • Single user, single machine. There is no account system, multi-tenancy, or remote access. Loopback HTTP uses local browser and installation credentials, but malicious native code running as the same OS user remains trusted.

  • Windows is the exercised product platform. The service launcher is tested on Windows and Linux; the tray, installer and session-root registry lookup remain Windows-only. The rest of the server should run on macOS and Linux, but nobody has proven the full product there.

  • No MCP prompts or resources. Planning tools manage work; project context belongs somewhere else.

  • No hosted anything. No sync between machines, no shared installation, 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 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.

python -m ruff check .
python -m ruff format --check .
python .github/relkit.pyz audit --history
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

An owner checkout additionally runs python .github/relkit.pyz protect install once and python .github/relkit.pyz audit --history --owner before publication.

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 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.

License

MIT.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    B
    quality
    F
    maintenance
    An MCP server that provides a database-backed kanban board with 40+ tools for AI agents to track issues, features, todos, epics, and diary entries across projects, including status workflows, relationships, and semantic search.
    45
    18 PyPI
    83
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    A kanban board MCP server that enables AI agents to read and write project boards through session loops, with built-in cost tracking and local-first storage.
    10 npm
    1
    MIT