dpm
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@dpmpublish the current planning artefacts to docs/"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
DPM — Data-Modelled Planning Method
SQLite-backed persistence for planning artefacts. Every artefact is a row with typed
columns; every cross-artefact reference is a foreign key. Markdown under docs/ is a
generated, one-way projection of the database rather than the place the data lives.
Skills write exclusively through typed MCP tools — no skill contains SQL, and nothing in DPM parses prose. The host is OpenCode v1; there is no other host DPM runs under.
Installation
You need OpenCode 1.18.25 or later, run as opencode, and Node 24 or later —
specifically the node OpenCode itself resolves. See About the host and the
runtime if either is in doubt.
Clone the repository somewhere you are happy to keep it:
git clone https://github.com/ninthspace/opencode-dpm.git ~/src/opencode-dpmName the entry file and the skills directory in your opencode.json, both as absolute
paths:
{
"plugin": [
"/absolute/path/to/opencode-dpm/src/plugin/index.ts"
],
"skills": [
"/absolute/path/to/opencode-dpm/skills"
]
}Absolute, and ~ is not a shortcut for it here. The plugin key takes the string as
written: a path beginning ~/ names no file, the entry never loads, and nothing is
logged — at any level, with no error and no warning. What makes it worth its own
paragraph is that the other key does not behave this way. skills expands ~ quite
happily, so the two are written side by side, they look alike, and only one of them
silently does nothing. The clone command above puts the repository at ~/src/opencode-dpm;
what belongs in the file is that path spelled out in full.
Restart opencode. That is the install — there is nothing to compile, and upgrading is
git pull in the clone.
Where that file goes, and DPM works from either. OpenCode reads configuration from two scopes:
Scope | Path |
One project |
|
Every project |
|
The project file is found by walking up from the working directory to the worktree root, so
it does not have to be the directory you started opencode in. The global path follows
XDG_CONFIG_HOME if you have set one. Put DPM in the global file if you plan to use it in
more than one repository, and in a project file if you are trying it out in one.
The two scopes merge key by key, and the project one wins a key outright. A project
file that says nothing about skills leaves the global entry in place; a project file that
does declare skills replaces the global array rather than adding to it.
To check it worked, from any project:
opencode mcp list
opencode debug skillThe first should print ✓ dpm connected; the second should list twenty-three dpm-
skills. Then do the two things under First run in each repository DPM will
keep planning artefacts in.
Each skill is a slash command. /dpm-spec, /dpm-do, /dpm-status — twenty-three of
them, named as the skills are, and whatever you type after one is the skill's input. The
host makes commands out of skills on its own; what DPM's plugin registers is a one-line
command for each, replacing the generated one whose whole prompt is the skill's own body.
That is why /dpm-epics does not paste twenty-four thousand characters into your turn. The
instructions still reach the model — through the skill tool, on demand — which is also
why the skill entry under Permissions is the one that matters.
Run both, because each answers for one key and a half-install is one of them passing.
opencode mcp list reports what plugin did and opencode debug skill reports what
skills did, and neither knows anything about the other's half. Twenty-three skills and no
server is the ~ above — every skill in the menu, nothing behind any of them. A connected
server and no dpm- skill is the replacement hazard below. Both look like a working
install from inside a session until the moment you need the half that is missing, and this
pair is what separates them.
Why two keys, and only one of them a plugin. plugin loads DPM's entry file, whose
config hook is the only handle v1 offers on the MCP registry. The skills do not come
through a plugin at all: v1's plugin API has no skill hook, and the object protocol that
does — a default { id, setup } — is fed by a plugins key that v1 strips before any
loader sees it. So the skills go in skills, a first-class key that points the host at a
directory and lets it read each SKILL.md itself, taking every skill's name from its own
front matter.
skills is last-one-wins, silently. If a later entry in that array holds a skill
declaring a name DPM also declares, the later one replaces DPM's and nothing is
logged — no warning, no diagnostic, and the session looks normal. Every DPM skill
declares a dpm-prefixed name precisely so this is unlikely rather than merely unlucky,
but the ordering is yours: keep DPM's entry last if you list several skill directories,
or check opencode debug skill if a dpm-* skill starts behaving like something else.
A globally installed DPM disappears from any project that declares its own skills.
This follows from the replacement above and is worth stating on its own, because of what it
looks like from the inside: the plugin key is untouched, so the MCP server still connects
and every tool is still there — and none of the twenty-three skills are, with nothing
logged. Tools and no skills is the same silent half-install as skills and no tools, arrived
at from the other end. The fix is to name DPM's skills path in that project's array as
well; opencode debug skill is what tells you, and it is why the check above is worth
running in a repository where DPM has stopped appearing.
opencode plugin opencode-dpm is not the install, and the failure it produces is
quiet. That is the packaged route — opencode plugin <module> takes an npm module name,
and there is no add subcommand — and it unpacks the package under
$XDG_CACHE_HOME/opencode/packages/<specifier>/node_modules/opencode-dpm/, where Node
refuses to strip types from any .ts file — on 22 and on 24, with no flag that lifts it.
The other runtime on hand is the bun compiled into the host, which reads TypeScript from
anywhere but carries no node:sqlite, which is the one thing DPM's server needs. So from
a packaged install there is no runtime that can start the server, and what you get is
twenty-three skills with no tools behind them — inert, with nothing in the interface
saying so. A checkout is outside node_modules, which is the whole of the difference.
Related MCP server: operations-pulse
First run
Two steps, in each repository DPM is going to keep planning artefacts in.
1. Install the pre-commit hook. It regenerates the projection and refuses a commit that disagrees with the database. From the repository root — one command to install it, two to check:
ln -s ~/src/opencode-dpm/hooks/pre-commit .git/hooks/pre-commit
ls -l .git/hooks/pre-commit
git config core.hooksPathThe target is the clone you named in Installation, so the hook and the tools are never
two different ages. The path must be absolute: a symlink's target resolves from the
directory holding the link — .git/hooks/ — not from wherever you ran ln, so a path
written relative to the repository root lands two levels too deep. ~ expands before ln
sees it, which is why the line above is safe as written.
The other two lines are there because every way this goes wrong goes wrong quietly:
lnsaysFile exists. Something already owns the hook, and DPM's ends inexecso it would replace rather than run it. Do not reach for-f— see When something else owns the hook, which tells the four cases apart.ls -lshows no link, or one whose target does not exist. Git skips a hook it cannot resolve without a warning and without failing the commit, so a broken link and no link at all look identical from the outside.git config core.hooksPathprints a path. Git then looks only there, and the link you just made is inert however correctls -lmakes it look. Same section.
2. Publish before committing. The markdown under docs/ is generated, and nothing
generates it as a side effect of writing. After a skill run that changed anything, ask for
the dpm-publish skill — "publish", or the skill tool with that id — then commit, with
the projection and .dpm/dpm.sql going in together.
Skip it and the hook refuses the commit; nothing is lost and nothing is written behind
you. The hook does not publish for you, and that is deliberate — one that regenerated
and staged the result would silently overwrite a hand-edit, which is the failure the guard
exists to catch. The refusal names node <dpm clone>/bin/dpm-publish.ts, where
<dpm clone> is the checkout you named in opencode.json and stands for that same path
everywhere below; the refusal prints it in full, because it may arrive at a terminal with
no session open. That binary is the same publish without the gate — the dpm-publish
skill shows you every file it would remove and asks first, and the binary alone does not.
Reach for the skill whenever you are in a session, which is nearly always.
Neither the database nor its .gitignore is a step. On a fresh clone the first tool call
finds no database, finds .dpm/dpm.sql beside it, builds one from it and says so on
stderr; and it writes .dpm/.gitignore before creating the database, so there is no
window in which an unignored dpm.db can be staged. A checkout that already has a
database keeps it untouched, whatever the dump holds — replacing one is a merge, and a
merge is something you ask for.
Permissions
None of this is a step. Under the stock build agent DPM works with nothing added:
nothing it does reads outside your project, and its tools are ordinary tools the agent
already allows. What follows is for the case where you have set a restrictive baseline and
DPM has to be let back through it.
Nothing DPM does reads outside the project. Every skill body asks for the shared
conventions through dpm_read_shared_document, an ordinary DPM tool that reads the
document in the server's own process — so there is no external_directory rule to set,
and if you carried one over from an earlier version you can drop it.
The key is permission, singular, and it holds an object. Each entry is keyed by what
is being done — skill, bash, edit, or a tool's own name — and its value is either a
bare action ("deny", which is shorthand for { "*": "deny" }) or an object of
pattern: action. Within one of those objects insertion order matters: OpenCode
evaluates the last matching pattern, so broad rules go first and narrow ones last. A
per-agent permission overrides the top-level one rather than being appended to it.
If you have set a restrictive baseline, these two entries are the minimum that lets DPM work:
{
"permission": {
"skill": { "dpm-*": "allow" },
"dpm_*": "allow"
}
}One dpm-* covers all twenty-three because the id a skill rule matches against is the
name in that skill's front matter — not its directory, and not anything DPM composes
while registering it. Each skill declares its own dpm- prefix at the top of
skills/dpm-<skill>/SKILL.md, so the string the host registers is the string you can read
out of the file.
If you want a confirmation before anything is removed, put it on the tool and not on the skill. Publish is the only DPM operation that deletes a file:
{
"permission": {
"dpm_publish": "ask"
}
}dpm_publish is what writes the projection and unlinks the generated files no document
produces any more, so it is the line the removal actually passes through. The skill
rule — "skill": { "dpm-publish": "ask" } — governs whether the procedure is loaded
into the conversation, which gets you a confirmation for reading a set of instructions and
none at all for the deletion: the wrong half of the pair, and it reads like the right one.
A /dpm- command narrows the session's tools to the ones that skill names, and it does
that after your rules rather than instead of them. DPM registers 184 tools and their
schemas are 138 KB on every request; no skill uses more than a quarter of them, and on a
small local model the difference is measurable — /dpm-publish sends 34 KB where an
unrestricted turn sends 138 KB. So when one of these commands runs, DPM writes a rule to
the session denying dpm_* and allowing back the tools that skill's body calls.
Session rules are evaluated after configuration ones, and the last match wins, so this
narrowing overrides a top-level "dpm_*": "allow" — which is the point, and is worth
knowing if you are looking at a tool that was available a moment ago and is not now. It
lasts until the next command replaces it. Nothing outside dpm_* is touched: read,
edit, bash and question are exactly as you configured them, and a command that is
not DPM's is left alone entirely.
permissions — plural, an array of { action, resource, effect } — is the shape the
host's next major version takes, and v1 does not merely ignore it. It refuses the whole
configuration, and the session does not start:
Error: Configuration is invalid at /path/to/opencode.json
↳ V2 permissions are not supported by OpenCode V1. Use V1 "permission" rules or run opencode2.That is the one failure in this file that is loud, and it is worth knowing which shape you are looking at before you copy anything: the two are close enough to read as variants of each other and only one of them starts.
See What the two permission keys do for how skill
and tool rules differ, and what a deny on each actually stops.
Status
Beta, and young — but not new. This package is a standalone fork of DPM 0.7.0. The method, the schema, the 183 tools and the 23 skills are DPM's and have been in use for some time; what is new is the host binding, and that is the part still settling.
Two specifications are built out here, and the second changed the first one's answer.
The fork was written against the OpenCode beta, because that was the release on hand. The
second specification moved it onto the 1.x line, and the host turned out to differ in ways
that reached well past the registrar: the skills registered through a configuration key
rather than a plugin, a clone rather than a packaged install, and the shared conventions
served by a tool rather than rewritten into skill bodies as they were registered.
The epics are in docs/epics/:
Epic | What it delivered |
| The standalone repository and the JavaScript-to-TypeScript conversion, checked byte-for-byte against a dump v0.7.0 wrote |
| The plugin entry, one MCP server and 183 |
| All twenty-three skill bodies off Claude Code's tool prefix and slash commands, with the prohibition enforced in CI |
| The pre-commit guard at OpenCode's hook path, this README, and permission behaviour |
| What the package ships, and the restrictions it holds to in production |
| The move to the supported host: the plugin entry for its MCP registry, and the clone install that replaced the packaged one |
| The |
| The shared conventions served by |
| This README and the permission guidance, brought onto the one host DPM supports |
| The end-to-end trip through a real 1.18.25: the host survey, both plugin entries loading, and a README configuration the host refuses to start on |
What is not settled at 0.1.0. Three things are on the record as unfinished rather than
unknown. No skill has been watched end to end inside a real session — from starting one
through to a commit the guard accepts. Epic 02-05 walked the host itself and found three
stacked defects doing it, so the registration, the runtime and the configuration are
verified against a running 1.18.25; what is assumed is the part after that, where a model
follows a skill body and the tools do the writing. ralph is registered like the other
twenty-two, but the loop it describes rests on a Claude Code stop hook that OpenCode has no
equivalent for, so that loop does not run. And DPM's database path is relative, which means
the working directory OpenCode hands a spawned MCP server is what decides which repository
.dpm/ lands in.
How it works
docs/is output. DPM generates it from.dpm/dpm.db. Editing a file under it is writing into something with a generator behind it; the edit survives until the next publish and no longer.The database is not committed.
.dpm/dpm.sqlis its text form and is what a fresh clone rebuilds from, automatically, on the first tool call.The pre-commit hook is a symlink into the clone, so it follows whatever you have checked out. A stale link refuses and tells you; a missing one is silent, and
.git/hooks/is not tracked — sols -l .git/hooks/pre-commitis worth running when you come back to a repository.A refused commit is telling you which of four things happened, and each has a different fix — publishing when you should have imported destroys what you pulled. The refusal names the command; When the guard refuses explains the choice.
About the host and the runtime
DPM is an OpenCode plugin: it registers one MCP server and twenty-three skills, and there is no other host it runs under.
Node 24 or later, and it must be the node OpenCode resolves. DPM uses node:sqlite
from the standard library and runs its own TypeScript by Node's native type-stripping, so
there is no native module, no node-gyp, no loader and no build step — but both of those
need 24. Below the floor, each of DPM's five executables refuses with a message naming the
version you have and the version it needs, rather than failing on an unknown builtin or a
syntax error in a type annotation.
The server is registered as the bare command node, which the host looks up on its own
PATH — not yours, and not a version manager's shell hook. If that node is older than 24
the server refuses to start, and what you see is one line in the log:
WARN server unavailable key=dpm type=local status=failedfollowed by a session with the skills present and no tools behind them. opencode started
from a shell where node --version says 24 or later is the whole of the fix.
The plugin API is one the host is still free to change. Entrypoints may move under it: the two protocols, the shape of
opencode.json, where a package is cached, and the names of the commands below. When something here stops matching whatopencodedoes, the host has moved and this file is behind — checkopencode --helpand the release notes before assuming DPM is broken.
What the two permission keys do
The external_directory rule Permissions tells you to drop used to be the one entry
every user had to add. The bodies named
shared/skill-conventions.md as a path in the clone, the host classified that read as
leaving the project, and the stock rules end with "external_directory": { "*": "ask" } —
so interactively it prompted on the first skill of every session, and non-interactively it
was rejected and the skill carried on without the conventions it had been told to read.
That last outcome is the one that mattered: nothing announced it. A tool call has no such
failure mode, because a refused or failed call is one the session sees.
DPM occupies two keys, and telling them apart is the whole of this section:
What happens | Key | Pattern |
A skill is loaded into the conversation |
| the skill's id — |
A DPM tool runs | the tool's own name — |
|
The two fail differently, and the tool half fails quietly. A denied skill refuses at
the point of use and says so, quoting the rule that stopped it. A denied tool is removed
from the model's tool list before the session starts — there is no refusal to read,
because there is nothing left to call. "dpm_*": "deny" produces a session that reports
having no DPM tools at all rather than one that is told it may not use them, which is why a
tool rule is worth getting right the first time rather than debugging from the inside.
That the registered id comes from the front matter is worth knowing rather than taking on trust: a prefix applied at registration would leave the file saying one thing and the permission engine matching another, which is what a rule that silently matches nothing looks like from the outside.
None of it is about restricting DPM's skills. The twenty-three are the product — they are meant to be used, they are how the method is followed at all, and a repository that denies one has a hole in the method rather than a tightened setup.
If you do meet a deny on a skill — an inherited config, a restricted agent — it is
honest about what it stops. The host checks before it reads the file, so the instructions
never enter the conversation, and there is no second route to them: each skill registers
under exactly one id, no tool returns skill text, and the package puts no executables on
your PATH. A skill that cross-references a denied one sends the model back through the
same tool, which refuses again. What it does not stop is DPM's tools, which are
separate keys — so a denied skill is a method nobody can follow, not a repository
nothing can write to. ask is a question rather than a slower deny: answer it and the
skill loads normally.
The refusal quotes the ruleset that produced it, which is what tells you a dpm-* pattern
matched rather than something broader:
The user has specified a rule which prevents you from using this specific tool call.
Here are some of the relevant rules [{"permission":"skill","pattern":"dpm-*","action":"deny"}]Keeping the hook healthy
Upgrading DPM does not disturb the link, and there is no re-linking step. The symlink
names a path rather than a version, so git pull in the clone rewrites the target in place
and every repository linked against it runs the new guard at its next commit. Nothing here
selects a version — a clone install has none to select — so the guard and the MCP server
are the same directory and cannot be of different ages. Restart opencode after pulling,
which is the one thing git pull does not do for you: a running session holds the server
it started with.
Two clones is the only thing that undoes that, and it is deliberate rather than accidental. Developing DPM itself is where a second checkout has a reason to exist. A link into the one you did not pull is a current database checked by an older guard, and When the guard is out of date is what that looks like from the commit that meets it.
A stale link announces itself; a missing one never does. A link into an older release
produces a refusal that names the release it ran from, so you find out at the next commit.
A link that is gone — .git/hooks/ is not tracked, so it does not survive a re-clone, a
fresh git init, or anything that rewrites the directory — produces nothing at all. Git
skips a hook it cannot find without a warning and without failing the commit, so the
working state and the unguarded one are indistinguishable from the outside, and every
commit after it goes in unchecked. Run ls -l .git/hooks/pre-commit when you arrive in a
repository you have not committed to for a while; it is the only thing that tells you.
dpm says so itself, on the one case it can be sure of. The first tool call of a session
looks for .git/hooks/pre-commit on its way to the database, and writes a line to stderr when
there is nothing there — the same channel, and the same terms, as the restore report: unusual,
actionable, silent otherwise. It warns on absence and on nothing else. A hook that exists and
is not dpm's may well be dispatching to dpm, and a warning that fired every session on a
correctly configured repository is one you would learn to skip. It also stays quiet outside a
repository, in a linked worktree, and where core.hooksPath has moved the hooks directory —
three states where .git/hooks/ is not the question.
A shell function for your .bashrc or .zshrc, if the rest is a check you would rather
not remember. It is bash and zsh, not POSIX sh — a hyphen is not allowed in a
function name there, so sh rejects dpm-link before it runs anything:
DPM_CLONE=~/src/opencode-dpm
dpm-link() {
ln -s "$DPM_CLONE/hooks/pre-commit" .git/hooks/pre-commit
ls -l .git/hooks/pre-commit
git config core.hooksPath
}DPM_CLONE is the same path you named in opencode.json, written once so the two cannot
drift apart. dpm-link is step 1 with both checks attached, so File exists still stops
you and sends you to When something else owns the
hook rather than being forced past. Run it from the
repository root, once in each repository DPM keeps artefacts in — .git/hooks/ is not
tracked, so a re-clone or a fresh git init is a repository that needs it again.
There is deliberately no companion that adds -f. Overwriting a hook is not part of
installing one: it belongs to the cases in the next section, each of which is a different
thing to have found at that path, and only one of which overwriting is right for.
When something else owns the hook
DPM's hook ends in exec — it hands the process to the guard and never returns, so it
runs instead of whatever was at .git/hooks/pre-commit, not before it. Git has no
notion of a second hook at the same path. Four cases, and the check in step 1 tells them
apart.
ls showed a symlink into an older DPM. The stale-link case, and the only one where
overwriting is correct — the install command from step 1, with -f:
ln -sf ~/src/opencode-dpm/hooks/pre-commit .git/hooks/pre-commit-f deletes what it replaces, without asking and without a copy. That is what you want
when it is DPM's own stale link and never what you want otherwise, which is why it lives
here among the four cases rather than in a command kept to hand: ls -l first, and confirm
the target really is an older DPM.
git config core.hooksPath printed a path. Something — husky, lefthook, or the
pre-commit framework — has moved the hooks directory, and git now looks only there.
This is the case worth knowing about, because installing DPM's link anyway works
perfectly and does nothing: the file is created, ls -l shows it correct, and git never
invokes it. Put DPM's hook inside whatever owns that directory instead, using that tool's
own mechanism, or unset the setting if you no longer use it:
git config --unset core.hooksPathYou use the pre-commit framework (the Python one — the name collision is
unfortunate). It owns .git/hooks/pre-commit and dispatches from
.pre-commit-config.yaml, so overwriting it disables every other check in the
repository. Register DPM as a local hook instead:
repos:
- repo: local
hooks:
- id: dpm-guard
name: DPM projection guard
entry: <dpm clone>/hooks/pre-commit
language: system
pass_filenames: falsels showed a hook of your own, or another tool's. Keep it and run both, in a wrapper
you own. Move the incumbent aside, then write a hook that calls each in turn:
mv .git/hooks/pre-commit .git/hooks/pre-commit.local
cat > .git/hooks/pre-commit <<'SH'
#!/bin/sh
set -e
.git/hooks/pre-commit.local
exec <dpm clone>/hooks/pre-commit
SH
chmod +x .git/hooks/pre-commitOrder matters, and DPM's goes last. set -e stops at the first failure, and DPM's
guard is the one whose refusal has a specific fix attached — reaching it after your own
checks have passed means the message you are reading is about the thing you still have
to do. It stays exec so the guard's exit status is the hook's.
In both, <dpm clone> is what it is everywhere else in this file: the checkout you named in
opencode.json, which is the same path step 1's ln uses.
The wrapper is a real file rather than a symlink, so nothing re-points it — but nothing re-points a symlink either, and here the path is spelled out rather than resolved. It goes stale in the one case step 1's does: you moved the clone, or started linking against a different one. Re-edit the hook, in a place the check in step 1 will not find for you.
When the guard refuses
The database and .dpm/dpm.sql are two forms of the same thing, and they can fall out of
step in three different ways. The guard says which one happened and names the fix — but
each of these is a real command you can run at any time, not only when a commit is
refused, and each discards whatever is only on the side it overwrites. Knowing which is
which before you are standing in front of a refusal is the point of this section.
There is a fourth refusal that is not about the two artefacts at all — the guard reporting that it is itself out of date. It has its own section below, because its fix is not one of these commands.
The database moved. You changed something and did not publish. Regenerate both artefacts:
node <dpm clone>/bin/dpm-publish.tsor invoke the skill tool with id dpm-publish if you are already in a session — which is
the wording the refusal itself uses. This is step 2 of First run, and it is the common
case.
The dump moved. You pulled. .dpm/dpm.sql arrived rewritten and your database is
behind it. Rebuild the database from the dump:
node <dpm clone>/bin/dpm-import.tsPublishing here would do the opposite of what you want — it regenerates the dump from a database that is behind it, and everything the pull brought would be gone.
Both moved. You pulled onto work you had not published. Neither can be regenerated from the other without losing whatever is only on the side being overwritten, so the two have to be reconciled:
node <dpm clone>/bin/dpm-merge.tsRun it during the conflicted git merge, from the repository root. It reads git's three
stages of .dpm/dpm.sql, merges them row by row, and rebuilds the database from the
result. Where it cannot decide, it stops and says which rows are in question rather than
picking one. Git does not invoke it for you; registering it as a merge driver needs
per-clone configuration and is not something DPM does on your behalf.
When the guard is out of date
Nothing is out of step here; the hook is. DPM upgraded, the database is at a schema
version this guard has never heard of, and .git/hooks/pre-commit is still symlinked into
an older clone. git pull in the clone you linked against rewrites it in place and the
link keeps working — what leaves two DPMs of different ages is a second clone, with the
link pointing into the one you did not pull.
The fix is to re-make the link against the clone you are actually running. There is a link
there already, so this is step 1's command with -f — the same one the stale-link case
under When something else owns the hook gives, and
the warning attached to it there applies here too:
ln -sf ~/src/opencode-dpm/hooks/pre-commit .git/hooks/pre-commitThe refusal names the directory it ran from, which is the old one's, so the path you are replacing is in the message.
It refuses rather than carrying on for the reason the migrator leaves a newer database alone. This guard's picture of the schema is missing whatever the release added, so what it would produce is a comparison against part of a database — and the outcome of that is very often a pass, which is the one verdict nobody investigates. Until the link is re-made, no commit in that repository has been checked by anything.
Developing DPM itself
In a repository that develops DPM, the guard is the code you are editing, and there is
no longer an installed release to stand apart from it. That separation was what a
packaged install bought: the release that wrote .dpm/dpm.db was the release that checked
the projection, and a half-written change could not block its own commit. With a checkout
install the two are one tree, so a bug in the guard blocks committing the fix for that
bug — a real cost, paid because no runtime can start the server from a packaged copy.
The way out is to unlink for the length of that one commit — rm .git/hooks/pre-commit,
commit, dpm-link — rather than to --no-verify, which skips the guard silently and
leaves nothing saying it was skipped. Keeping a second, stable clone and linking against
that one restores the old separation if you want it back.
Point the link at a different clone only while the schema is what you are editing, and
only for as long as that lasts. Then the working tree's guard and the working tree's
publisher agree with each other and neither agrees with the stable clone, which is what you
want while a migration is half-written and want nothing to do with afterwards. Verify a
re-point either way by running .git/hooks/pre-commit the way git does — from the
repository root, with no arguments — rather than by invoking bin/ directly.
Coming from CPM
Migrate under Claude Code first, then come here. The CPM migration guide belongs to
DPM's Claude Code release and is maintained there; this package does not carry a copy,
because a second copy is a second thing to keep current and the move it describes happens
while CPM is still installed — which is not this host. Install DPM under Claude Code, work
through its MIGRATION.md, and install this once your repository is a DPM repository.
What follows is the one part of that guide you should not put off even if you do nothing else, because it is what DPM will delete if you skip it.
There is no importer, and that is a decision rather than a gap (AD8). DPM never reads a
CPM docs/ tree. New and existing projects alike begin with a blank database, so a project
adopting DPM carries none of its history across — the artefacts stay exactly where they are,
as CPM's files, and DPM neither converts nor repairs them.
But DPM will offer to delete some of them, so move them out of the way first. The
projection reclaims a file it did not write when the name carries one of DPM's own kind names
in the position the renderer puts it — -spec-, -epic-, and so on — inside the directory that
kind is mapped to. Whether that catches a given CPM directory comes down to whether the two
systems happen to use the same word for the same kind of document: spec and spec collide,
plan and problem_brief do not.
Move all twelve regardless. The ones that are safe are safe by coincidence of vocabulary, and renaming a single kind in a later version moves a directory from one column to the other with nothing to announce it. Sorting them is work that has to be redone every release, and being wrong costs files.
Only those twelve are walked, and only one level deep, so docs/cpm/ is permanently out of
reach and stays readable:
mkdir -p docs/cpm
git mv docs/plans docs/briefs docs/specifications docs/epics docs/retros docs/quick \
docs/discussions docs/communications docs/reviews docs/audits docs/runbooks \
docs/library docs/cpm/ # drop any you do not havedocs/architecture/ is not on that list because it is never walked at all: DPM renders an ADR
inside the document that raised it and has no directory for the kind. Leave your ADRs where
they are.
A walked directory is still safe for files the rule cannot mistake for its own: a hand-kept
docs/epics/README.md is never a candidate.
Preview before the first publish either way. The dpm-publish skill lists every removal
and asks before it removes anything. The non-interactive form does not — it is the command the
pre-commit guard names when the refusal is a database that has moved ahead of its dump. The
other two refusals name other fixes; see When the guard
refuses. In a repository with a CPM corpus still in place, reach for
the skill.
Once the corpus is out of reach, the other half — which of it, if any, is worth carrying over, and which is finished work no DPM skill will ever read — is the conversation the Claude Code guide walks you through.
Licence
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
Minimal issue tracker: projects, objectives and trackable plans, written by agents over MCP.
System-of-record notebook for AI coding agents: pages, datastores, tasks, skills over MCP.
Agent-native notes, tasks, dev-docs, vaults, sync & handoffs. MCP + OpenAPI dual surface.
Build and run grounded business agents over MCP: agents, knowledge bases, skills, Storylines.
Related MCP Servers
- FlicenseNot gradedqualityBmaintenanceMCP server that turns planning documents into structured SQLite data and exposes plan metadata to AI agents, with tools to list and delete saved plans.-

operations-pulseofficial
AlicenseNot gradedqualityAmaintenanceEnables AI assistants to run local-first operations checks, review evidence, and manage durable tickets in a SQLite-backed ledger through self-describing MCP tools.MIT- FlicenseAqualityBmaintenanceEnables coding agents to track and coordinate project work through a shared SQLite ledger, including task plans, session ancestry, claims, work locations, blockers, and commits via MCP tools.9-
- AlicenseNot gradedqualityAmaintenanceEnables AI agents to manage projects, roadmaps, periods, and tasks through MCP tools, backed by a local SQLite database with schema validation.MIT