Valkama
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
Valkama
A local workspace for planning, executing, and reviewing agent work.
Valkama connects projects, work items, agent executions, and results in one web and desktop workspace. Work brings board, list, and graph views over one Planning read model; Settings holds preferences and the installation's health checks. Memory and session references stay with the work item they support.
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_itemrefuses a competing claim on the current Planning wire. A claim is a lease held by the agent session that took it, so it lapses once that session is gone, 12 hours after an ordinary end so that a resumed session keeps its work.Execution tracking on work items. Launches carry an explicit delivery contract, and outcomes are typed results rather than exit codes. The card's Execution tab is off by default; turn it on under Settings → General → Interface.
Task finder with Ctrl/Cmd+K across mapped projects. Results retain their exact store and space; unavailable sources and truncated results stay visible.
Colours for task types. Epics are purple by default, bugs rose, improvements turquoise and tasks grey-blue, on a type's icon and word only: state colours stay as they are. Change them under Settings → General → Task type colors; a change applies at once. The choice is kept in
localStorage, on the device, so a browser and the desktop app each remember their own.Questions and decisions in the task's discussion and a shared inbox. Agents create typed requests; the owner answers, in their own words if they prefer, or declines the exact request, and can change an answer while the task is open. Expired requests stop accepting answers, and sending an answer does not automatically resume an execution.
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. The question inbox also refreshes every 30 seconds while visible and when the window regains focus.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 serveFor 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 Work, Analytics and Settings 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 statusThe status output is JSON. Its command field is the path agent clients should
register:
Windows:
%LOCALAPPDATA%\Valkama\bin\valkama.cmdmacOS/Linux:
$XDG_BIN_HOME/valkamaor~/.local/bin/valkama
The user-facing command contains no source-checkout path. It runs a pinned
release: the checkout's current commit, copied out with git archive and never
edited, so uncommitted work in the checkout never reaches a client session or
the store it opens. Run launcher install again to publish newer commits, or
with --revision <commit> to go back to an earlier release (a release from
before valkama hook is answered by the shim with silence and exit 0 when a
client hook calls it; see SERVICE.md). A managed Python
shim beside the command is the durable process identity; its manifest records
the release's valkama.py, the pinned commit, the 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 releases, 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 mcpCodex — 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 the name Planning claims and comments are signed with when an
MCP tool call names no author of its own. Give each client a different one in
its registration. Without the variable, the author in config.json applies; with
neither, the name is agent.
Then give the agents the procedure that goes with the tools — finding and claiming an item, keeping its checklist, merging duplicates, closing it:
python valkama.py skill installIt writes the Valkama skill for each client this user has, where that client
finds its user's skills (~/.claude/skills/valkama, ~/.agents/skills/valkama),
from the release the launcher pins, and launcher install keeps it in step. It
edits no client configuration; see
SERVICE.md.
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 spacesYou 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 status5. 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 checkThe 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 MYReplace 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, installThe 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 |
| speak MCP over stdio — what the installed launcher runs for agent clients |
| serve the JSON API and Valkama UI; with |
| report public interfaces, MCP revisions and tool count as JSON without reading user data |
| report source, database, runtime and launcher state without opening the database |
| pin a commit as a release and atomically install or repair the stable client command that runs it |
| verify launcher ownership and hashes |
| inspect or stop only the Windows listener proven to run through the managed shim |
| on Windows, keep an on-demand listener on the port from login; off until enabled, see |
| remove only the managed launcher files and this launcher's autostart value |
| a short Planning-space report, meant for session hooks |
| list the open work items whose commit refs went into a release range, and how each was found: |
| record one client hook event from stdin without a running server; see Client hooks |
| print this checkout's backend and static identity |
| manage the local project inventory and its projection; see Project registry |
| every setting, the layer it came from, and the value in use |
| what this installation needs, and the exact command for each gap |
| ask an adapter for its manifest, then probe it for the calls it should refuse |
| check it, then declare it for this installation |
| stop declaring an installed adapter |
| the adapters this installation declares |
| check this installation in four levels — Installation, Projects, Capabilities, Connections — and say what to do about each problem |
| write every Planning space, every one of its work items and their questions as one JSON export; |
| delete archived transcripts now, on request, whatever retention says; prints counts only |
| read a session's usage run again from its transcript, the way the background reader reads it, and replace the run's segments in one transaction; |
| explicitly convert a Board-era store to Planning once; dry run converts a copy |
| attach another database file as a scope |
| drop a scope from every view; the file is untouched |
| list attached scopes and their Planning spaces |
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 names the adapter and nothing else. It has exactly four fields, all required:
{
"contract_version": "valkama-adapter",
"adapter_id": "example-notes",
"version": "1.0.0",
"capabilities": ["notes.search", "notes.open"]
}version is a semantic version, and each capability is a dotted lowercase id
the adapter chooses for an operation it answers. A manifest carrying any other
field is refused with that field's name rather than read in part, and so is a
path or an argv anywhere in it. Where an adapter lives is therefore 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:8770Process 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,
holding the manifest and the transport and nothing else. That file is the whole
declaration: no table holds an installed adapter, so adapter remove deletes
the file and nothing else. Installing an adapter whose check failed is refused;
--force overrides and says so.
adapter list reads the records back and names every file it refuses, with the
reason, and doctor asks each installed adapter for its health under
Connections. A record written for the earlier, longer manifest is refused with
the names of the fields this contract does not have; check and install that
adapter again. No module calls an installed adapter yet: today the contract is
what an adapter is checked, declared and health-reported against.
Configuration
Five 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 |
|
|
| where the database lives |
|
| none | document roots to search, separated by the path separator |
|
| none | where Claude Code keeps its session journals |
|
| none | where Codex keeps its rollout journals |
|
|
| the name written into Planning claims and comments when an MCP tool call names no |
|
|
|
|
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,
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 |
| unset | set to anything to suppress the tray icon |
|
| the port the desktop app expects the server on, and the one a work item's |
|
| the mutex tray sessions coordinate through |
--port on serve overrides the port for that process. VALKAMA_PORT tells the
desktop app where to look and an MCP session which address to hand out; set both
if you move the port.
Setting it up again, or checking that it is right
python valkama.py setupIt 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
One column per state of the space's workflow. A new space gets seven: Backlog, Todo, Dev, Review, Done, Blocked and Cancelled. Done is for finished work; Cancelled is for work closed as not needed, duplicated or superseded, and needs a closing summary that says why but no ticked checklist.
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 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_itemand complete it by id withtick_work_item_checklist_item. No invented percentages, and two agents splitting a work item each hold their own step.comment_work_itemaccepts an optionalchecklist_item_id: the saved comment keeps a quote of that step even after it is renamed or removed.Typed refs — commit hashes, memory entry ids, urls — attached with
attach_work_item_ref, so the next agent reads pointers instead of doing archaeology. An agent's own session needs no ref: Valkama records the confirmed session behind every write, andget_work_itemlists those sessions.An activity trace. Creations, moves, claims, takeovers, releases, completions and link changes are all recorded with an author.
get_work_itemreturns the newest of it, and all of it withfull: true, so nobody has to guess who did what. Reordering within a lane is deliberately not history.
An agent that finishes a step usually makes several writes to one card: tick it,
attach the commit, comment, record the summary, move the card.
apply_work_item_changes does them in one call, in a fixed order and all or
nothing, so a closing move sees the ticks and the summary sent with it. The
exact shapes are in
the connection contract.
Two items filed for one change — the same finding twice, or one cause found in
two places — are folded together with merge_work_items. The surviving item
takes their steps, discussion, refs and links, and each merged item closes as
its duplicate with its own history intact
(details).
A claim also says whether anyone is still behind it. When the client proves its
session, the claim is a lease on that session: live while the session is
active and has shown a sign of life — a hook event or a Valkama write — in the
last 14 days, stale once it ended or went quiet, and unconfirmed when no
session was proved, which is also how every older claim and every claim made in
the browser reads. Another session needs force to take a live claim, even when
it signs with the same author. A stale claim is released by whoever meets it
next, and taking it over needs no force; the history shows the expiry as a
released event by valkama. The board marks stale claims, and the inspector
shows the lease and when it expires. The exact rules are in
the connection contract.
The inspector keeps editable descriptions and a shared discussion. A comment can
target a checklist step, and a reply stands under the comment, report or question
it answers, one level deep. A thread can be marked resolved with a one-line
decision, by a person in the card or by an agent with resolve_work_item_comment,
and get_work_item with unresolved: true reads the threads still open
(details). Work
reports are what agents publish with report_work_item; they show in the
discussion with “What was done / Why / What
comes next”, and any of them can be selected as the task outcome. Closing a task
that has no outcome asks a person for one: what was done and what comes next. System
and execution history have filters, date groups and expandable details. Their
search covers the records loaded for that task.
Use create_work_item_request for a question or decision that needs an explicit
answer. Each request has 1–12 named fields: text, one choice, multiple choices or
a boolean. Optional execution and session bindings must belong to the task.
get_work_item_request and list_work_item_requests expose the current status;
answer_work_item_request, decline_work_item_request and
expire_work_item_request require that request's expected_revision. A terminal
task, ended execution or elapsed deadline closes the pending request. An option
can explain itself in a description and be recommended. A field with options
is answered with an option or the person's own variant, and may carry a comment;
a note comments on the whole question and does not answer a required field. While
the task is open, a given answer can be changed, in the card or with
answer_work_item_request and change: true, and the answers it replaced are
kept (details). Answers are
stored separately from comments and do not change the task's workflow.
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, and create_work_item takes that link and
the claim in the call that files the item. 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 or a keyboard move sends
the revision of the card as the board last read it. If an agent has moved or
edited the card since, the server answers 409 with the revision it is at, the
board says so, and the card stays where the next read of the space puts it
rather than silently undoing the agent's work.
The board's order is the order work is taken in. Dragging a card up or down
its column, or pressing Alt+↑ or Alt+↓ on a focused card, places it among the
cards of its state; the move is announced to a screen reader. Dropping it in
another column moves it there at the place it was dropped. Agents see the same
order: claim_ready_work_item takes the first ready item of it,
list_work_items lists each state in it, and update_work_item and
transition_work_item take a rank (first, last, before:<item> or
after:<item>) to place an item themselves. Priority only breaks a tie. The
order is the item's position, which every store already holds, so it needs
no new schema; the exact rules are in
the connection contract.
The board reads by epic. Grouped by epic, each epic is a lane with its reference, title, open and closed counts and a fold; a lane with nothing open starts folded, and the board remembers the lanes you fold or unfold, per space, in the browser. Work under no epic comes last. A lane's Chain action narrows the board to that epic's chain, and the chain stays when you switch to List or Graph; its chip leaves it in every view.
Migrating an older store
A store written by an older schema is upgraded the first time a newer build
opens it, after a verified snapshot (see below). From then on a build of the
older schema refuses the store rather than read it, so restart every agent
client after pinning the newer release with launcher install: each one
starts its own server, and the first to open the store upgrades it. Schema 9
dropped the extension registry's tables, the module rows and the integration
switches; its snapshot is labelled preschema9. Schema 10 lets a comment be a
reply under another one or under a question and carry a resolve mark, numbers a
question's answer and keeps the answers it replaced, records where a session
was last opened, and adds the owner's Windows notification settings
(owner_alert_settings, empty until they are saved); its snapshot is labelled
preschema10. A store older than 9 takes both steps in one open, under the
preschema9 (or preproductmodel) snapshot. Schema 14 records the pictures
pasted into an item's text (work_item_images); the files themselves stand in
an images folder beside the database, one per content hash. Its snapshot is
labelled preschema14. Schema 15 admits the history event of a changed label
set (labels_changed) and rebuilds the event table for it, keeping every event;
its snapshot is labelled preschema15, and going back to a build at schema 14
means restoring that snapshot, its pictures included, as the next paragraph
describes. Schema 16 counts each usage request once in the whole
store and records which reading wrote each usage run, so that the background
reader reads every older run again, and it adds the counts of how often each
Analytics surface was opened (analytics_surface_uses, empty until one is). Its
snapshot is labelled preschema16, and
beside it stands <snapshot>.liveness, a verified copy of the liveness file
the store had then. Going back to a build at schema 15 is the restore described
below with the preschema16 snapshot, plus that copy put beside the store as
<store>.liveness. A store at 14 takes both steps in one open, under the one
preschema15 snapshot, and that copy then stands beside it instead.
Every snapshot taken from schema 14 on carries the pictures as well: a folder
with the snapshot's name and .images after it stands beside the snapshot file,
and a snapshot counts as verified only when that folder holds every picture the
store held, each file's SHA-256 equal to its name. To restore one, stop every
process that holds the store, delete the -wal and -shm files beside it, copy
the snapshot file in place of the database, delete the images folder beside it
and put a copy of the snapshot's .images folder there under the name images;
a snapshot with no .images folder held no pictures, and the store then has
none. The folder's .store file names the store it belongs to, and a snapshot's
.images carries it, so the restored folder is still its store's. Every open of
the store also removes a picture file that no item holds, so a picture written
after the snapshot does not outlive the restore even when the folder was not
replaced; a store neither clears nor writes into a folder whose .store names
another store, as one opened from a second database file in the same directory
would find it. Going back to a build at schema 13 is the same with
the preschema14 snapshot, which has no .images folder, before pinning the
earlier release as described below for schema 9: everything written since is
lost, pictures included.
The order of work took no migration of its own. A placement is written to the
item's position, a column every schema-9 store already has, so no snapshot was
taken for it, and an earlier schema-9 build opens a schema-9 store that holds
placements and lists its states in the placed order. Such a build still puts a
moved item last in its new state, and a later placement works from there.
Notes on answers and option descriptions took no migration either. A schema-9
build from before them opens a schema-9 store that holds them, and its tools read
and answer those questions, with the note among the answers as _note. Its web
interface is strict about what it reads, so it reports that it could not update
the questions: on the card of a question with a note or a described option, and for
the whole "waiting for my answer" list while a pending question has a described
option. Everything else in that build works.
To go back to a build at schema 8, first stop every process that holds the
store: the listener and every agent client, since each one runs its own server.
Pin the earlier release with launcher install --revision <commit>. Then
either stamp the store back with PRAGMA user_version=8, which keeps
every row including those written since the upgrade (a schema-8 build creates
the dropped tables again, empty), or put the preschema9 snapshot in its place,
which loses everything written since. Before replacing the file, delete the
-wal and -shm files beside it: a write-ahead log left behind would be
applied to the snapshot on the next open.
Going back to a build at schema 9 has one way, and its order matters. Stop every
process that holds the store, delete the -wal and -shm files, put the
preschema10 snapshot in place of the store, and only then pin the earlier
release with launcher install --revision <commit>. Pinned first, the release
reaches every running session through its stdio front within seconds and
refuses the store that is still at 10, so the board disappears from all of
them. Everything written since the upgrade is lost. Stamping the store back to
9 is not a way back here: a schema-9 build would show a reply as a comment of
its own and misread the new keys in an answer.
The hook spool changed in the same release. A session start that has to wait in
the spool is written as spool version 2, which carries where the session is open;
every other event keeps version 1. After going back, the earlier build delivers a
version-1 file left in the spool and sets a version-2 one aside as *.rejected,
which doctor counts until the file is deleted. That session start belonged to
the upgraded store going back discards, so nothing more is lost.
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-modelThe 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 preschema9, 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. A snapshot that a retry would reuse but that cannot be read --
half a file left by a process killed while writing it -- is renamed with the
suffix .unverified, kept, and replaced by a fresh snapshot; remove it by hand
once you no longer need it.
Planning: launching agents from a card
The launch dialog opens from the card's Execution tab, which is off by default. Turn it on under Settings → General → Interface, on each device you use; it takes effect at once. With the tab off, a card does not offer launching and does not read attempts, and launching itself keeps working as described here.
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.
Session context on a work item
Execution history, launch, stop, resume, and native session opening belong to work-item context. A session reference retains the client's exact stored identity; resume never chooses a session by title, directory, time, or recency.
GET /api/session?id=<id> resolves compact session context by exact identity
or a unique stored prefix. A successful
response has resolved: true and includes session, client_family,
work_item, space_root, status, detail, and at. Missing or ambiguous
identity returns resolved: false with the corresponding status. This endpoint
does not return an event feed.
Planning owns POST /api/ingest, which accepts only session_start,
session_end, attention, and turn_end lifecycle events. The standalone
session fleet, inbox, tool stream, /api/sessions, /api/session-seen, and
Sessions SSE view are retired. The flow-metrics Analytics dashboards and
exports, Improvements, and the Skills catalogue and activation API are also
retired; the Analytics module that stands in the navigation now is for the usage
of agent work. This does not
change installed skill files or agent-client settings.
Project documents
A path to a project's file or folder in a work item's text, or a Markdown link to
one, can be pressed. A Markdown file opens in Valkama's document viewer, read-only and
at the #kb:… anchor, heading or :42 line the path names: beside the card or the
panel of open questions where the window has room for both, over them where it does
not. Beside them, F6 moves between the two and the first Escape closes the document.
A link in it to another Markdown file opens in the same place, and Back returns. The
viewer's head opens the file in your editor at the line you are reading, and moves the
document to a window of its own at /doc?project=<id>&path=<relative>, a browser tab
or a second window of the desktop app, where a reload keeps your place. Any other file
opens in the editor Settings names, at its line, and a folder in that editor, through
the open plan described below. A path that cannot be opened is copied, and the notice
says why. A work item's source, its plan anchor, opens in
the viewer at the anchor, from the mark in the card's head and from its row in
Relations, in a browser and in the desktop app alike.
A path in a work item's text is a candidate on the page, and the server says
what it is. web/tests/fixtures/document-path-samples.json is the table of what
a text becomes: strong, weak, copy-only or nothing, and what a click copies when
the path cannot be opened. Three shapes stay text: a run holding a character no
Windows name can hold (tmp/<task>/, docs/*.md) is a template, neither it nor
any part of it a path, though a tag the path does not go on through (docs/a.md<br>)
only ends it; a drive's root alone (C:\) names nothing; and two
backslashes are a network path only before a server and a share
(\\server\share), never in \\n. A path is opened only when it lies inside the root of
a registered project, after .., symbolic links and junctions are followed,
and the path that comes out is compared with the root as the disk spells both: in
a folder Windows keeps case-sensitive, proj beside Proj is a sibling and
outside. Anything else the page only copies, and a network path never reaches the server.
The three routes below are GET, read files and write nothing. Unlike the other reads they
require the request to come from the page itself: an Origin, when there is one,
must be allowed (origin_refused) and Sec-Fetch-Site must be same-origin
(fetch_metadata_refused), both 403. Every answer carries
Cache-Control: no-store and X-Content-Type-Options: nosniff. A refusal is
{"error": {"code": ...}} from the closed list below and never carries the text
of an operating-system error.
GET /api/documents/resolve?space=<id>&path=<p>&path=<p>... takes up to 64 paths
and only asks the file system whether each exists; it opens no file. space is
the planning space's exact identity, the same value GET /api/ref takes, and is
only the base of a relative path. Inside a document the page shows, the base is
that document's project instead: project=<id> in place of space, the id
resolve answered with. Its root is the one the registry gives that id, a
project registered inside it is asked after it, as from a space, and the boundary
stays every registered root. The page has already read a link in the document
from the document's folder into a path from the project's root, so no folder is
sent, and it adds exact=1 for those links: a project registered inside the root
is not asked for them, since their author wrote them from that folder. A project
the registry does not know is project_unknown for the whole
request, and naming both space and project is invalid_request. Each path is sent as written, without its
:42 or #... suffix, and comes back unchanged in path, so an answer is matched
without parsing it. A request line may not pass 64 KiB (the listener answers 414),
so a batch of long paths is split sooner than 64:
{"paths": [
{"path": "docs/plan.md", "status": "file", "project": "example-project",
"relative": "docs/plan.md", "markdown": true},
{"path": "src/a.ts", "status": "file", "project": "example-project",
"relative": "src/a.ts", "markdown": false},
{"path": "./docs/../docs/plan.md", "status": "file", "project": "example-project",
"relative": "docs/plan.md", "markdown": true},
{"path": "docs/plans/", "status": "directory", "project": "example-project",
"relative": "docs/plans"},
{"path": "C:/projects/example/services/example-service/web", "status": "directory",
"project": "example-service", "relative": "web"},
{"path": "D:/outside/.valkama", "status": "refused", "code": "outside_root"},
{"path": "../outside.md", "status": "refused", "code": "outside_root"},
{"path": "docs/gone.md", "status": "missing"},
{"path": "src/index.ts", "status": "ambiguous"},
{"path": "docs/x.md", "status": "space_unbound"}
]}
| Meaning |
| It exists. |
| Nothing there, under the space's root or under a project registered inside that root |
| A relative path found in two or more of the projects registered inside the space's root, and not under the root itself. The projects are not named |
| A relative path in a space the registry does not bind to one existing root. An absolute path in the same request still resolves |
|
|
A relative path is taken from the space's root first. Only when nothing is there
is it looked for in the projects registered inside that root, so a nested project
never overrides the root. The server does not send back an absolute path it
worked out; an absolute path the page sent comes back as sent. A registry that
cannot be read leaves no roots, so a relative path is space_unbound and an
absolute one outside_root.
GET /api/documents/read?project=<id>&path=<relative> reads one Markdown file,
taking project and relative from resolve. The path is checked again, inside
that project and no other, and once more on the file it opened, before a byte is
read: a folder on the way swapped for a junction out in between is outside_root.
{"project": "example-project", "path": "docs/plan.md", "markdown": "...", "size": 65145,
"modified_at": "2026-11-03T15:12:09Z", "revision": "9f2c41d0a7b3e655"}revision is the first 16 hexadecimal digits of the SHA-256 of the bytes read.
The text is strict UTF-8 with a leading byte order mark removed and its line
endings untouched.
GET /api/documents/search?space=<id>&q=<text> offers the Markdown files of the
project a planning space is bound to, for the @ list of a text being written. It
is the document search of GET /api/search over that project's root alone, which
also takes a file whose path from the root holds the text, case aside, at line 0
and ahead of the files found by a line. Each path is what resolve takes from the
same space; the root is never told. At most 20 files, one hit each; nothing typed,
or a space the registry does not bind to one existing root, offers none. A file
reached through a link or a junction out of the root is left out, and so is one the
project's Git ignores, such as scratch or private evidence: where a root lies in a
Git work tree that does not ignore it, Git lists its files, and only a root outside
any work tree is walked, past node_modules, dist and the like. No space, one
that is not a planning space's identity, or a text over 200 characters is
invalid_request.
{"documents": [
{"path": "docs/plan.md", "line": 0, "text": ""},
{"path": "docs/notes.md", "line": 3, "text": "See the plan here."}
]}Code | HTTP | When |
| 400 | Any route: no |
| 400 | The path's form, as in |
| 404 |
|
| 403 | Outside every registered root |
| 404 | Nothing there |
| 422 | Not a regular file |
| 415 | Not |
| 413 | Over 512 KiB; the answer also carries |
| 415 | A NUL in the bytes, or bytes that are not UTF-8 |
| 503 | The operating system refused the read; its text goes to the server's log |
Targets the opener takes
Opening a file or a folder in the editor goes through the same plan as opening a
session (docs/connection-contract.md, "What a click opens"), and the page names
the target only by what resolve answered:
GET /api/open-plan?target=file&project=<id>&path=<relative>[&line=<n>]&surface=desktop|browser[&fallback=<editor>]
GET /api/open-plan?target=folder&project=<id>&path=<relative>&surface=desktop|browser[&fallback=<editor>]fallback names the editor: vscode, vscode-insiders, cursor or windsurf,
VS Code when it is absent or names no editor. When that editor is not installed
the plan opens VS Code instead and says so in opens.missed; with VS Code missing
too it only explains, explain {reason: "not-installed", host}. The steps depend
on the surface:
Target | In the desktop app | In a browser |
|
|
|
|
|
|
The desktop app starts the editor on the folder and then on the file, so both
steps run on one click. A browser follows link, vscode://file/<path>:<line>:1
for a file and vscode://file/<path> for a folder, with the editor's own scheme in
place of vscode and the path encoded as Python's Path.as_uri encodes it.
The planner checks both targets with the same path check as read, inside the
named project, and the answer carries the absolute path it found, because a link
needs one. A folder's rule for a session (an existing absolute local folder, not
a network path, without ..) stays as it is, and this target adds that it lies
inside a registered root.
A target that opens nothing is not an HTTP error. The plan answers 200 with one
step, explain {reason: "path-refused", code}, where code is one of read's
codes or one the opener adds:
| When |
|
|
| As in |
| A |
| A |
Only a request outside the closed values, such as no project, is refused with
HTTP 400 invalid_request.
Client hooks
valkama hook <client> <event> is the command Claude Code and Codex hooks run.
It reads the hook input from stdin and records the session event straight into
the store, so sessions stay visible while neither serve nor an MCP session is
running. When the store is busy, missing or at another schema version, the
event goes to a spool beside the store and lands later. The command always
exits 0, and a lifecycle event prints nothing of its own, so it never blocks a
client session.
valkama hook claude session-start < hook-input.json
valkama hook codex stop < hook-input.json
valkama hook claude pre-tool-use < hook-input.jsonThe events are session-start, session-end, subagent-start,
subagent-stop, stop, user-prompt-submit and pre-compact for both
clients, plus notification and post-tool-use for Claude Code. Each one updates
the session row that GET /api/session and work-item context read.
claude post-tool-use names the agent an Agent tool call started. claude pre-tool-use adds the session identity to a Valkama tool call. session-start
also records where the session is open — an IDE extension, its terminal, a
terminal, Claude Desktop or the Codex app — from a few named environment
variables and, on Windows, the names of the programs above the hook, which is
where a click on the session opens it. What is kept is the kind of place, the
product, the folder the session started in and Claude Desktop's own id for it,
never a variable's value
(details). Only an
allowlisted set of fields is kept: never the prompt, tool arguments other than an
Agent call's short description, or assistant text.
The same hooks tell a working agent what a person did in the interface on the
work its session holds. post-tool-use, user-prompt-submit and stop answer
with a short notice when there is news: a comment, an answer, a status change, a
cancel. The orchestrator hears about everything its session holds, a subagent
about the items where it holds the claim or a step and about replies in its own
threads and answers to its own questions, each event once. claude watch,
registered with Claude Code's asyncRewake, wakes an orchestrator that is idle,
for example while it waits for a background subagent. Codex is told at its
next prompt or tool call. notify switches this off (see
Configuration), and
the hook contract
says who is told, what a notice holds and what it costs.
When a session ends, the hook also copies the session's transcript, and a
subagent's own transcript when the client reports one, into transcripts/
beside the store, and records the copy's sha256. The copy survives the client
deleting its original. A transcript too large to copy within the hook's time
budget is left for the transcript sweep to copy later. Copies stay on this
machine and never appear in an export. They are kept while an open work item
refers to the session, and for at least 90 days after the later of the
session's last activity and the closing of the last item that refers to it.
Transcripts can contain secrets and personal data, so treat the store's
directory accordingly. To delete copies when someone asks, without waiting for
retention:
valkama transcripts purge --session <id> [--session <id> ...] [--dry-run]
valkama transcripts purge --before 2026-01-01 [--dry-run]--session names sessions by exact id; --before takes the sessions whose
last activity is earlier than that date (UTC) or time. Given both, a session
must match both. The copies go even while an open work item refers to the
session, and the command says how many such items there were. A copy still
owed for those sessions is never made. The store keeps only the fact of each
deletion and the copy's hash, never its content, and the command prints counts
of sessions, files and bytes, never a session id or a path. --dry-run
prints the same counts and changes nothing.
doctor reports spool files still waiting, rejected spool files, the last
hook event from each client, and the transcript archive: copies kept, copies
still owed and files no record names. For the usage read from transcripts it
reports the models that spent tokens with no price, the requests met in more
than one run, how many runs are current, wait to be read again, were read
again, or were refused by reason, and what share of the usage segments, and of
their seconds, names the organization they ran for. The fields, the spool format, the session
rules and the archive's retention are in
the client hook contract.
The desktop app and the tray
desktop/ is a thin Electron shell. Its window opens at once, on a starting
page in the workspace's palette and language; behind it the app verifies the
managed launcher, reuses the managed server or starts one, and then shows
Valkama in that window with its own process, taskbar entry and icon. When it
cannot, the same window says why and offers the remedy: a stale managed server
is offered a restart there, and only a listener the launcher proves its own is
ever stopped. It runs runtime and serve through the stable shim, whose
manifest selects the release pinned from this repository; there is no second
desktop source record or checkout-path fallback. Each step of opening the
window is logged, with its time, to %TEMP%\valkama-desktop.log.
With the app installed on Windows, a link to a work item followed from outside a browser page opens the card in this window, at first start or on top of a running window, and the browser tab closes itself where the browser allows it. The window comes up above what is in use; while you are working elsewhere, Windows keeps the keyboard there until you click it. The page in the tab offers to open the item in the browser instead; the address of a work item states when this happens.
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 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. With the desktop app installed it starts the
app and nothing else, and the app decides the rest in its own window. Without
the app it opens a browser, starts the server, offers a restart, or explains the
refusal in a dialog, deciding off the icon's thread. 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. Clicks, the steps they
took and how they ended are logged to %TEMP%\valkama-tray.log.
launcher install moves the icon along with the session that holds it: its script
restarts from the new release, so two icons can stand side by side for up to two
seconds. An icon hidden from its menu stays hidden.
Set VALKAMA_NO_TRAY=1 to turn it off.
Running
electronfrom a VS Code terminal fails withapp is undefined, because VS Code exportsELECTRON_RUN_AS_NODE=1. Clear that variable first. The installed app is unaffected.
Windows notifications
When an agent asks you a question or asks for a decision, or moves work to review, Valkama shows a Windows notification, so you need not watch the board or the chats. It works with Valkama's window closed, because the process that wrote the event shows it. A launch from Valkama that fails can notify you too; that one is off until you turn it on.
What it needs:
The desktop app. Its installer puts the Valkama shortcut in the Start menu, whose identity the notifications are shown under, and registers the
valkama:scheme a click opens. Without the shortcut Windows drops a notification and reports no error, so without the app there are no notifications..NET Framework 4, which Windows includes. A notification is shown by a small C# program,
windows/toast.cs, compiled on first use into%LOCALAPPDATA%\Valkama\toast. The repository holds no binary and you need no SDK.Notifications on in Windows, under Settings → System → Notifications, for the system and for Valkama. Do Not Disturb is respected: there is no banner and the notification waits in the notification centre, unless you make Valkama a priority app.
A click starts the app if it is closed, and the app starts the server, so nothing has to
hold the port and launcher autostart is not needed for this. With autostart on, the
listener answers a notification's one question without loading the server.
What it does:
A click opens the item; a click on a notification about a question goes on to that question, with the keyboard on its first field. Several items within a few seconds become one notification, and a click on that one opens the panel «Needs my answer» when any of them is a question.
A question already on your screen, in a focused Valkama page, shows nothing. Another screen in the same window does not count as showing it.
A newer notification about an item replaces the older one. It is taken away when the item has no open question left or is closed.
A notification is shown at most once. A process killed before showing one loses it, and the question still waits in the panel.
Settings → Windows notifications chooses the events and how much the text reveals, and
sends a trial notification. The lock screen shows the same text unless you turn on
Windows' "Hide content when notifications are on lock screen" for Valkama. doctor
reports the shortcut, the helper and autostart in one line, without compiling anything or
showing a notification; it cannot see whether Windows has blocked Valkama, which the
settings screen asks Windows directly. VALKAMA_NO_ALERTS=1 turns notifications off for
a process, for tests and for machines with no desktop.
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/distVue 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 module list this build declares in code, and
GET /api/platform/context the store's identity and the project directory with
each project's binding state, which the shell reads before any module renders.
No endpoint serves integration definitions or loads 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. The bootstrap and every write with that credential must carry
Sec-Fetch-Site: same-origin, which a browser adds to its own same-origin
requests and a plain HTTP client does not, so a local script that just asks is
refused (fetch_metadata_refused); the trust boundary is stated in
the connection contract. 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.
A picture is sent with POST /api/images?item=<reference> as the bytes of the
file, under the same browser credential as any write, and read back with
GET /api/images/<sha256>, which, like the bootstrap, answers only a request with
Sec-Fetch-Site: same-origin. The server decides by the first bytes whether it is
a PNG, JPEG, WebP or GIF, refuses anything else (415) and anything over 10 MB
(413), and answers a read with that type, X-Content-Type-Options: nosniff and
Content-Security-Policy: default-src 'none'; sandbox. The server neither logs
the bytes nor sends them anywhere by itself; an agent that reads a picture with
read_work_item_image hands it to its client and so to its model provider (see
the connection contract).
Every page the listener answers, the app's own and the document page alike, carries
Content-Security-Policy: frame-ancestors 'none' and X-Frame-Options: DENY: no
site, Valkama included, may show one in a frame, where another page could be laid
over a click.
POST /api/hooks/message-display, Claude Code's display hook, writes nothing and
takes neither credential: it answers a local process, and refuses any request with
Origin or Sec-Fetch-Site, which a browser cannot leave out. See
the hook contract.
A Planning write that passes the browser credential is the owner's: its call is
recorded as confirmed with source browser and no agent session, and when the
request names no author it is signed user, so a claim, a checklist tick or a
comment made by hand reads as the owner's. /api/ingest is not such a write.
How agent calls are identified is in
the connection contract.
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 |
| the source entry point — it prepares |
| the Python server, managed launcher, domain modules, and CLI/HTTP/MCP surfaces |
| the Vue application, its build and its tests |
| the Electron window — its own npm package, tests and release output |
| what exists only because the host is Windows: the tray script, the icon, its generator, the installer |
| the Python suite, mirroring |
| 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.
The maintained product contracts are:
SERVICE.md— launcher ownership, client registration and relocation.DESIGN.md— the design system, and the authority for every visual decision.docs/product-model.md— the product vocabulary and the distinctions between modules, views, adapters, and runtime records.docs/platform-contract.md— the two operating levels, what each module means on them, and what an adapter may do.docs/connection-contract.md— what a client learns on connect, and how the server refuses to answer from a stale build.docs/hook-contract.md— whatvalkama hookkeeps from a client hook, the spool, what each event does to a session, and the transcript archive.
The superseded Improvements and session-event contracts remain in docs/ as
historical records; they do not describe supported endpoints.
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.mdfor 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 testAn 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.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
This server cannot be deployed
Maintenance
Related MCP Connectors
Kanban board for teams and coding agents: manage tasks, subtasks, sprints and wiki pages via MCP.
A task list shared by you and your coding agents (Claude Code, Cursor, Codex and any MCP client). Agents plan each task into steps, report live progress to a board, and block on questions you answer from your phone.
Task & board management for AI agents + humans. Kanban, comments, digests via MCP.
Project board for ChatGPT, Claude and coding agents, with scoped MCP access to projects and tasks.
Related MCP Servers
- AlicenseBqualityDmaintenanceAn 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.4539 PyPI86MIT
- AlicenseNot gradedqualityAmaintenanceA 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 npm1MIT
- AlicenseNot gradedqualityCmaintenanceAn MCP server that provides shared memory, kanban board, and agent registry for AI agents to collaborate as a team, with a live dashboard for human oversight.10 npmMIT
- AlicenseAqualityCmaintenanceA kanban board system served over MCP that lets coding agents manage shared work pipelines with enforced state transitions, work reports, and audit trails.37AGPL 3.0