README
¶
worklog
A per-project, SQLite-backed task and decision log for AI coding agents,
exposed over MCP so work is remembered across sessions. One static binary, no
runtime, no server to run, no cloud — the database is a single file in your
project's .worklog/ directory.
It tracks not just what needs doing but what was done and why: every task carries its blocking dependencies, the GitHub issues/PRs it touches, the decisions made while executing it, and a running journal — so a fresh session can answer "where was I, and why did we do it this way?"
Why it exists
In-session task lists die when the session ends; prose notes aren't queryable. worklog fills the gap between them: structured, durable, cross-session work-state the agent reads on start and writes as it goes.
Install
go install github.com/MatLomax/worklog/cmd/worklog@latest # needs Go 1.27+
Or download a prebuilt binary from the releases,
or build from source (make build). Put worklog (or worklog.exe) on your
PATH. It's a single static, dependency-free binary for Linux, Windows, and
macOS — SQLite is compiled in (pure-Go modernc.org/sqlite, no cgo).
Per-project setup
cd your-project
worklog init # creates ./.worklog/tasks.db (idempotent)
The database is resolved as --db / $WORKLOG_DB / $CLAUDE_PROJECT_DIR /
current directory — walking up to the nearest ancestor .worklog/, else
./.worklog/tasks.db. It's local, gitignored working state — not committed.
The MCP server is attach-only: it opens an existing database but never
creates one. So a globally-installed worklog stays inert in any project until
worklog init (or /worklog:init) has been run there — it won't seed a
database into every project you open.
Configuring statuses
With no config, a task's status is one of pending (the default),
in_progress, blocked, done, or dropped. To change that list, add an
optional .worklog/config.jsonc beside tasks.db. worklog writes no ignore
rules for it, so whether it is committed is the project's choice. The format is
JSON with // and /* */ comments and trailing commas allowed (LF, CRLF and
CR line endings all work). statuses replaces the whole list, in the order
given; this example adds a review status to the defaults:
{
"statuses": [
{ "name": "pending", "kind": "open", "default": true }, // new tasks start here
{ "name": "in_progress", "kind": "active" },
{ "name": "review", "kind": "active" }, // awaiting review; still actionable
{ "name": "blocked", "kind": "blocked" },
{ "name": "done", "kind": "closed" },
{ "name": "dropped", "kind": "closed" },
],
}
A status's kind decides how the rest of worklog treats a task holding it:
| Kind | Meaning |
|---|---|
open |
actionable, not started |
active |
actionable, underway |
blocked |
a manual hold; not actionable |
closed |
terminal: stamps closed_at (on create and update), unblocks dependents, hidden from default lists |
The config must satisfy these rules. An error names the file and points at the
offending value as path:line:col (columns count Unicode code points); a rule
about a missing field points at its status entry, and a list-wide rule at the
statuses array:
- each name matches
^[a-z][a-z0-9]*(_[a-z0-9]+)*$(lowercase letters and digits, words joined by single underscores, starting with a letter) and is unique; - exactly one status has
"default": true, and it is of kindopenoractive; - at least one status is
openoractive, and at least one isclosed; - an
openoractivestatus may not be named so its briefing heading reads "Blocked" or "Recent activity" (blockedandclosedstatuses get no section of their own, so any name is fine for them); - keys are exact-case (
statuses,name,kind,default), unknown or duplicate keys are rejected, and no value may benull. A missing file, or{}, means the defaults; a file that exists but holds no JSON value (empty, or only comments or a byte-order mark) is an error. A file that lists the default statuses explicitly counts as a loaded config, not as the defaults.
The session briefing (worklog session-start / worklog context) lists open
work in one section per active status, then one per open status (in config
order), then Blocked. A heading is the status name with _ as a space and the
first letter capitalised (in_progress → "In progress").
The MCP server re-reads the config before each tools/list and tool call, and
switches statuses only when it reads a complete, valid file — so an edit
applies to the next call without a restart, and a file caught mid-save never
changes the statuses in force. While the file is invalid, empty or unreadable,
the server keeps the statuses in force (the last valid config, or the defaults
if none was loaded) and adds a note to each tool result naming the error. If a
file whose statuses list was loaded disappears, those statuses stay in force,
with a note saying so, until the file is recreated or the server restarts;
while the defaults are in force (no file, or {}) a missing file simply means
the defaults. A read error never counts as
the file having been there. Only a server that has never had usable statuses
(the file was invalid or unreadable when it started) refuses tool calls, until
the file can be read and is valid. If a read has ever found the file (even
invalid) and it is then deleted, calls stay refused until it is recreated or
the server restarts; if no read ever found it (it was only unreadable), a
missing file just means the defaults. The tool schemas (status enums, and the
descriptions naming which statuses are closed and actionable) are built from
the statuses in force; clients usually cache the tool list, so a changed enum
shows up from the next session, while the checks apply at once.
An invalid config never stops session bookkeeping: worklog session-start
shows the error in place of the briefing, worklog session-end and
worklog init carry on (init warns), and worklog context fails with it.
Removing a status leaves tasks that already hold it untouched: they keep
it and show it as saved. Such a status counts as not closed (the task still
blocks its dependents and still appears in lists) and not actionable, and it
can't be set on create or update. The task-list / work-find status filters
accept any well-formed name, so these tasks stay reachable, and the briefing
gives each leftover status its own section after Blocked, sorted by name and
headed with a (not in config) suffix (e.g. "Review (not in config)"). In
ordering, a leftover-status task sorts with the blocked ones.
Wiring it to an agent
Claude Code (plugin)
worklog ships as a Claude Code plugin — distributed through the
MatLomax/claude-plugins
marketplace — that wires up the MCP server, the warm-load/session hooks, and a
/worklog:init command in one install. The plugin is pure glue: it expects the
worklog binary on your PATH (see Install).
/plugin marketplace add MatLomax/claude-plugins
/plugin install worklog@matlomax --scope project
Then, in each project you want tracked:
/worklog:init # creates .worklog/tasks.db (idempotent)
Because the server is attach-only, the globally-installed plugin does nothing in
a project until /worklog:init runs there. After it does, the SessionStart hook
warm-loads open work (into the model's context) and shows the next task as a
visible line to you, the Stop hook closes the session, and the worklog MCP tools
are live.
Manual setup (without the plugin)
claude mcp add worklog -- worklog serve
Hooks in your settings:
{
"hooks": {
"SessionStart": [{ "hooks": [{ "type": "command", "command": "worklog session-start" }] }],
"Stop": [{ "hooks": [{ "type": "command", "command": "worklog session-end" }] }]
}
}
worklog session-start emits one SessionStart payload over both of the hook's
channels: additionalContext feeds the full briefing into the model's context
(invisible to you), and systemMessage shows the next task as a visible line to
you. It is safe to wire globally — it prints nothing until worklog init has run
in a project. Ready-made hook scripts (bash and PowerShell) for both events are
in hooks/.
Codex CLI
Codex is an MCP client too. Add to ~/.codex/config.toml:
[mcp_servers.worklog]
command = "worklog"
args = ["serve"]
All tools work identically. Session boundaries are handled automatically — the
server closes its session when Codex disconnects, so no Stop hook is needed.
Codex has no SessionStart-style context hook, so to warm-load either call
task-next yourself at the start or add to your AGENTS.md:
At the start of a task, run
worklog context(or call thetask-nexttool) to see open work.
Tools
Status enums and descriptions in the tool schemas follow the project's status config.
| Tool | Purpose |
|---|---|
task-list |
tasks in actionable order (unblocked first, then priority); filter by status, parent, closed |
task-get |
full detail: body, blockers, subtasks, links, decisions, journal |
task-next |
the single highest-priority actionable task |
task-tree |
the task forest / a subtree, nested |
task-create |
create a task, optionally under a parent, with blockers; status defaults to the configured default |
task-update |
change slug/status/priority/parent/body; status must be a configured one, and a closed status unblocks dependents |
task-add-blocker / task-remove-blocker |
add or remove blocking deps on an existing task |
task-toc / task-section-get / task-section-set |
read/edit one ## section of a body |
task-section-append |
append a block to the end of a section (after its subsections) |
task-section-insert |
insert a new section relative to a heading path (or the document) |
task-section-delete |
delete a section and its subsections |
task-section-move |
move a section (with subsections) before/after another heading |
task-body-replace |
replace an exact, unique substring anywhere in a body |
task-link |
attach a GitHub issue/PR (full URL), commit, file, or URL |
task-decide |
record a decision + rationale made during execution |
task-journal |
append a freeform note to the running log |
record-edit |
correct a decision/journal field in place, silently |
record-delete |
delete a decision or journal entry by id |
work-find |
search tasks, decisions, and journal — find what was done |
session-summary |
close the session with a summary for the next one |
Referencing tasks in a body
Refer to another task from within a task body by its slug, in either a wikilink
[[some-slug]] or a code span `some-slug`. Renaming a task with
task-update's new_slug rewrites both forms of the old slug across every task
body in the same operation, so cross-references never dangle. The exact
delimited token is matched, so renaming build-it never touches [[build-it-2]]
or the bare word build-it in prose.
CLI
worklog serve [--db PATH] run the MCP server over stdio
worklog init [--dir DIR] create .worklog/tasks.db
worklog context [--db PATH] [-n N] print the "where was I" briefing (plain text; Codex / debugging)
worklog session-start [--db PATH] [-n N] SessionStart hook output: warm-load the model + show the next task
worklog session-end [--summary S] close the current session
worklog update [--check|--auto|--force] update the binary in place from the latest release
worklog version
worklog update self-updates the on-PATH binary from the latest GitHub release
(SHA-256 verified, atomic in-place swap). It follows the shared self-update
contract in MatLomax/claude-plugins CONVENTIONS.md;
the worklog specifics are the worklog-<goos>-<goarch>[.exe] release asset and a
version taken from the build stamp, falling back to the Go build-info module
version.
The commands that read the project read its status config
too; an invalid config makes context fail, while session-start, session-end
and init carry on (see that section).
See design.md for the data model and the reasoning behind it.
License
MIT © Mathieu Lomax
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
worklog
command
Command worklog is a per-project, SQLite-backed task and decision log for AI coding agents, exposed to them over MCP so work is remembered across sessions.
|
Command worklog is a per-project, SQLite-backed task and decision log for AI coding agents, exposed to them over MCP so work is remembered across sessions. |
|
internal
|
|
|
mcp
Package mcp implements worklog's Model Context Protocol server: a newline-delimited JSON-RPC 2.0 loop over stdin/stdout exposing the task tree and its decision/journal record as tools any agent can call mid-session.
|
Package mcp implements worklog's Model Context Protocol server: a newline-delimited JSON-RPC 2.0 loop over stdin/stdout exposing the task tree and its decision/journal record as tools any agent can call mid-session. |
|
store
Package store is worklog's persistence layer: a per-project SQLite database (`.worklog/tasks.db`) holding the task tree, its blocking edges, and the durable record of what was decided and done across agent sessions.
|
Package store is worklog's persistence layer: a per-project SQLite database (`.worklog/tasks.db`) holding the task tree, its blocking edges, and the durable record of what was decided and done across agent sessions. |