@mmainguy/vrek (0.19.0)
Installation
@mmainguy:registry=npm install @mmainguy/vrek@0.19.0"@mmainguy/vrek": "0.19.0"About this package
Vrek
An API-first system of record for humans and agents doing work together — planning it, deciding it, building it and evidencing it. Software is the first domain, not the only one: requirements gathering and analysis run the same loop.
Jira and Confluence are two systems of record-keeping bolted together. Vrek is one graph: the work, the knowledge, the decisions, the evidence and the principals — human or agent — who touched them are the same objects, reachable through one API, with provenance and freshness as properties of every node.
The reasoning behind it is in RESEARCH_BRIEF.md. The shape of the implementation is in docs/ARCHITECTURE.md.
Status
v0.1. Vrek tracks its own development: .vrek/ in this repository is
the live workspace, and every claim below is a verified issue in it.
630 events, 41 issues shipped with evidence, 22 ready to pick up, 7 questions waiting on a human. 161 tests; 64% of statements covered, which understates by about 850 lines because the CLI is tested as a subprocess and V8 cannot see it.
Working: the event log with causal parents and content hashes, the graph, the ready set with footprints, blocking questions, typed verification, principles with recorded violations, documents as views over facts with exact drift, and one core behind five surfaces.
Not built: real identity beyond static bearer tokens, Postgres for a shared server, three-way merge on import, erasure mechanisms, and any UI. The git projection is deferred to 2027.
Known limitations, since a guarantee should say where it stops holding:
importLog is not atomic; attrs cannot be written or queried from any surface,
which is a mechanism one principle depends on; docDrift is blind to evidence
and to path footprints; ready() does a full scan and is the first latency cliff.
All are recorded — vrek ready lists them with the rest.
Migrating a workspace named before the rename
The database does not change. Nothing in the schema, the event types or the event payloads carries the product name — only the directory does. So a workspace built under the old name migrates by moving a directory:
cd ~/your-project
mv .flowstate .vrek # or: git mv .flowstate .vrek
vrek fsck # the chain verifies unchanged
If only the committed log survived and the database is gone, rebuild it:
vrek init
vrek import .vrek/log.ndjson
Then repoint the agent config. .mcp.json needs three things changed: the
command becomes vrek-mcp, every FLOWSTATE_* environment variable becomes
VREK_*, and any path ending /.flowstate becomes /.vrek. Running
vrek register --force rewrites it for you.
Two things change that a shell will not warn you about, because they are
protocol-visible. Every MCP tool is now vrek_* rather than flowstate_*, and
document resources are vrek://doc/<id>. An agent holding an old connection
keeps calling the old names until it reconnects.
The event log is deliberately not rewritten. Every event carries a content
hash covering its payload, so editing historical text to say vrek would break
the chain on every event and vrek fsck would report the whole log as tampered.
The log keeps saying flowstate wherever it did, because that is what happened.
Install
Nothing is required to run it. Node 24 executes the TypeScript directly and
the runtime depends on nothing outside Node's standard library, so a fresh
clone works with no npm install and no build:
node src/cli/main.ts version # or: npm run cli -- version
That is always the current source, which is what you want while working on Vrek itself. Anything that reads the graph needs a workspace first — see below.
To get vrek, vrek-mcp and vrek-api onto your PATH instead:
npm install
npm run build # emits dist/ — the bin entries point INTO it
npm link # symlinks the three commands at this checkout
npm unlink -g vrek removes them.
Both steps matter and in that order: npm link publishes the bin entries
from package.json, which name dist/cli/bin.js, so linking without
building leaves you a command that dies on a missing file. dist/ is also a
snapshot rather than a live view — after pulling or editing, npm run build
again or the linked vrek runs the previous version. Running from source has
neither problem.
vrek: command not found after cloning is expected. npm install links
the bins of your dependencies, never of the package itself, and a link is
machine-local state that does not travel with the repository — so a clone on
a second machine has no vrek until you link it there too. Use
node src/cli/main.ts or run the two commands above.
Start a workspace
cd ~/some-project
vrek init # creates .vrek/
vrek name "My Project" # records the name in the log
vrek register --model claude-opus-5 # writes .mcp.json so an agent connects
vrek add "the first thing" --touches src/
vrek ready
vrek help prints this. register merges into an existing .mcp.json
rather than replacing it, refuses to overwrite an entry without --force, and
points at the checkout it was run from — so working on Vrek itself needs no
rebuild between edits.
To open this repository's own workspace after a clone:
node src/cli/main.ts init # or `vrek init`, if linked
node src/cli/main.ts import .vrek/log.ndjson
node src/cli/main.ts report
This is the step a clone always needs and the one that is easy to miss:
.vrek/workspace.db is gitignored, so a fresh checkout has the log and
nothing to read it into, and every command answers no Vrek workspace found
until you import. init takes no name: the name travels in the log and the
import supplies it.
.vrek/log.ndjson is the committed event log; .vrek/workspace.db is
a projection of it and is not committed. Export after any session that changed
the workspace: vrek export --out .vrek/log.ndjson. Export and import
also reach MCP (vrek_export, vrek_import) and HTTP
(GET /export, POST /import), so an agent can get the workspace out of the
machine without a shell — commit it beside the code and it travels.
The loop
vrek ready # what can be picked up, with each item's footprint
vrek status <id> in_progress
vrek ask "Postgres or SQLite for the shared server?" \
--option "postgres|a service to run on day one" \
--option "sqlite|single binary, one writer" \
--blocks <id> --owner mike --revisit 2026-09-19
vrek status <id> done # a claim
vrek verify <id> "integration suite passes" \
--result pass --evidence "npm test" # the evidence behind it
vrek report # shipped / blocked on you / claimed / ready
vrek status <id> done records that somebody said it was done. Until a
verification passes, vrek report and vrek unverified both show the
claim as a claim. That distinction is the point of the product.
A check can be registered before it is performed — --result pending is the
honest state for that — and resolved afterwards with
vrek resolve <ver-id> --result pass --evidence "…". Correcting the result
appends a new event naming who corrected it; the original stays in the log.
Work put off on purpose is vrek defer <id> --until DATE --because "why",
not blocked — blocked asserts something is in the way, and a deferred item
returns to the inbox on its wake date.
Documents are views over facts
A document is not a page of prose that rots. It is an ordered list of blocks:
vrek doc "Phase 1 scope"
vrek block doc-abc --text "Phase 1 covers EMEA only."
vrek block doc-abc --node dec-5kg0g6n # renders current state
vrek block doc-abc --query kind=question,status=open
vrek render doc-abc
vrek ack doc-abc # one action: you vouch for it as of now
vrek drift # documents something has moved under
node and query blocks resolve when the document is read, so they cannot go
out of date. Only what you write as text can later be wrong, which is the
point: the part that rots is reduced to the part a human wrote.
Acknowledging stamps the document with the current event seq. Drift is then any event on a covered node with a higher seq — exact rather than heuristic, and it names which node moved and when. A render reports the seq it resolved at, because a query block changes a document without any event on the document.
Every block records the principal who wrote it, and every renderer attributes it. A doc block is text one principal wrote that another principal's agent reads, so authored text reaches a model as a quoted claim by a named person rather than as an instruction from the system.
The log is checkable
Every event carries the events it was appended after and a hash over its canonical form, so the hash covers the history reaching it and altering any ancestor changes every descendant.
vrek fsck # recompute every hash, resolve every parent and actor
vrek fsck --fields # also replay, to name payload keys this build does not read
It reports three different things and does not confuse them: a hash mismatch is tampering, a missing parent or orphaned actor is a broken reference, and unchained means an event predates hashing and cannot be checked at all. Unverifiable is not a failure; corruption is.
Surfaces
| Surface | Entry point | For |
|---|---|---|
| CLI | vrek |
humans and shell-driven agents; --json on any command |
| MCP over stdio | vrek-mcp |
one trusted writer; see Choosing a transport |
| HTTP | vrek-api |
services and future UIs; SSE at /events/stream |
| MCP over HTTP | POST /mcp on that server |
agents with no checkout and no terminal |
| MCP resources | vrek://doc/<id> |
documents as agent-readable context |
All of them call the same Workspace class. A capability reachable from one and
not the others is a bug, and gets filed as one.
Choosing a transport
Over stdio the actor comes from VREK_ACTOR, so it is asserted rather
than authenticated: anyone who can start the process can claim any identity.
That is fine for one person who is the sole writer of their own workspace, and it
fails silently the moment that stops being true — a shared machine, a CI runner,
or a workspace several people write to.
So stdio is for a single trusted writer, and MCP over HTTP with bearer tokens is the transport for anything else:
VREK_PORT=7373 \
VREK_TOKENS=tok-mike:mike:human,tok-agent:claude:agent \
vrek-api
vrek register --http https://your-host:7373/mcp
export VREK_HTTP_TOKEN=tok-agent # in the agent's environment, never in the file
MCP is then at /mcp, and a call carries Authorization: Bearer tok-agent. The
.mcp.json entry holds ${VREK_HTTP_TOKEN} rather than the token, so the file
can be committed; the client expands it. An unauthenticated call is refused
with 401 rather than silently attributed to somebody.
The console on the same server signs in with any of the same tokens: paste one
into the form and it becomes an HttpOnly session cookie, gone when the browser
closes. Writes from the console are attributed to that token's handle.
Every entry needs all three parts, and the kind is exactly human or agent.
The server will not start on an entry that is missing a part, misspells a kind
or repeats a token, or on one whose kind disagrees with the principal its
handle already names. See Identifying an agent for
how that disagreement is resolved.
The server binds loopback unauthenticated by default and refuses a non-loopback
bind without VREK_TOKENS. VREK_ALLOWED_ORIGINS=https://app.example.com,...
lists the origins a browser may call /mcp from; a request carrying any other
Origin is refused, and one carrying none (curl, an MCP client, a service) passes.
vrek-api --help lists every variable the server reads.
TLS
vrek-api speaks plain HTTP and expects TLS, when there is any, to be
terminated in front of it. Caddy does that in one line, including obtaining and
renewing a Let's Encrypt certificate:
caddy reverse-proxy --from vrek.example.com --to 127.0.0.1:7373
nginx with certbot works the same way. Whatever the proxy is, it must pass
X-Forwarded-Proto: https (Caddy does by default; nginx with
proxy_set_header X-Forwarded-Proto $scheme;), which is how the server knows to
mark the console's session cookie Secure.
For a private network where a proxy is one more thing to run, vrek-api can
serve https itself from a certificate it is handed:
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout key.pem -out cert.pem -subj "/CN=vrek.internal"
VREK_TLS_CERT=cert.pem VREK_TLS_KEY=key.pem VREK_TOKENS=... vrek-api
A self-signed certificate has to be trusted by every browser that opens the
console and by every agent that connects (NODE_EXTRA_CA_CERTS=cert.pem for a
Node-based MCP client), or the connection is refused. vrek-api never obtains or
renews a certificate: replace the files and restart. dec-23gfv9w records why.
Without a repository
Nothing in the codebase touches git. .vrek/ is a directory, not a checkout.
The CLI once fell back to git config user.email for an identity, which put
real email addresses into the log. It now reads a handle you choose with
vrek whoami <handle>, kept in .vrek/.actor and never in the log. A hosted
workspace is a data directory and a server:
VREK_DIR=/srv/vrek VREK_HOST=0.0.0.0 \
VREK_TOKENS=tok-priya:priya:human,tok-agent:claude:agent \
vrek-api
An agent then connects to https://your-host/mcp — no clone, no Node, no
.mcp.json. Its calls are attributed to whichever token it presented, so "who
decided this" stays a query rather than an inference.
Instructions
What an agent is told on connect belongs to the workspace, not to a file in a repository that a non-git user does not have:
vrek instructions # what agents are told today
vrek instructions "This workspace is the Q4 requirements phase. \
Requirements are issues; a sign-off is a verification with the meeting \
that produced it as evidence."
The default names no codebase. The loop — ready, ask when you would guess, claim, then evidence the claim — is the same for requirements gathering, analysis and planning as it is for software. Instructions and the workspace name are both event-sourced, so both survive an export into a fresh workspace.
Identifying an agent
VREK_ACTOR=claude VREK_ACTOR_KIND=agent \
VREK_MODEL=claude-opus-5 VREK_SESSION=$SESSION_ID \
vrek ready
Every event records the principal, whether it was a human or an agent, and for
agents the model and session. "Who did this and why" is a query, and
vrek_principals answers "who else works here" from the agent side.
A principal is recorded by its first write, in the same transaction as that
write. A caller that only reads (vrek ready, vrek_ready, GET /ready) is
never recorded, and takes no write lock doing it. Until the first write,
vrek whoami and GET /session report the caller as not recorded yet.
A principal's kind is recorded along with it, and how it connects later
does not change it. A caller that states a different kind (VREK_ACTOR_KIND, a
VREK_TOKENS entry, or an X-Vrek-Actor-Kind header) can still read, but its
writes are refused with a message saying which side disagrees. A kind that was
never stated is only a default, and the record wins. When the record is the
side that is wrong, correct it:
vrek principal-kind mike human --because "a person; the console token named no kind"
This works the same through vrek_set_principal_kind and
PUT /principals/:id/kind. The correction is an event, so it records who made
it and why. Events written before it keep the kind they were written under.
Dependencies
Development Dependencies
| ID | Version |
|---|---|
| @awesome.me/webawesome | ^3.12.0 |
| @sveltejs/vite-plugin-svelte | ^7.3.0 |
| @types/node | ^26.2.0 |
| @vitest/coverage-v8 | ^4.1.11 |
| mermaid | ^11.17.2 |
| svelte | ^5.57.0 |
| svelte-check | ^4.7.6 |
| typescript | ^5.9.2 |
| vite | ^8.2.2 |
| vitest | ^4.1.10 |