Doctor
persist doctor is the part that makes Persist OS more than a template. It is read-only, deterministic, and offline, and it returns an exit code you can gate on.
The doctor does not call a model and does not judge whether your architecture is good. It checks things with a definite answer: required files, required sections, evidence behind a completion claim, references to decisions and code, and a few signals of memory going stale. Same input, same report, every run. That is what lets it sit in a pre-commit hook and in CI without anyone second-guessing it.
Running it
$ persist doctor # human-readable report
$ persist doctor --json # machine-readable, schemaVersion persist.doctor.v1
The report lists findings grouped by severity, then the checks that could not run and
why, then the result. This is a repository right after init, one ADR, and
one module:
$ persist doctor
Doctor Report
WARNING
- Security model authentication and authorization section is still an unfilled template. (docs/20-security/SECURITY_MODEL.md)
- Product purpose is still an unfilled template. (docs/00-product/PRODUCT.md)
- Product users section is still an unfilled template. (docs/00-product/PRODUCT.md)
- Module memory purpose is still an unfilled template. (docs/30-modules/billing/MODULE.md)
- Module memory owns section is still an unfilled template. (docs/30-modules/billing/MODULE.md)
- Conventions canonical-primitives section is still an unfilled template. (docs/60-engineering/CONVENTIONS.md)
INFO
- Persist OS config validates. (.persist/config.json)
- 1 feature folders detected.
- 1 module folders detected.
- 2 ADRs detected.
Result: WARNINGS
Exit codes
- 0 Healthy No errors and no warnings. Result: PASSED.
- 1 Warnings only Something to look at. Result: WARNINGS.
- 2 Errors Memory is broken or a claim lacks evidence. Result: FAILED.
Info findings never change the exit code. Exit 1 is still non-zero, and a CI step
fails on any non-zero exit — but the generated pre-commit hook reads the exit code
and only fails on errors, so a warning is visible without blocking the commit. This
is a recorded commit attempt right after persist feature create, before
any template was filled:
$ git commit -m "Plan the checkout feature"
Doctor Report
WARNING
- Security model authentication and authorization section is still an unfilled template. (docs/20-security/SECURITY_MODEL.md)
- Product purpose is still an unfilled template. (docs/00-product/PRODUCT.md)
- Product users section is still an unfilled template. (docs/00-product/PRODUCT.md)
- Conventions canonical-primitives section is still an unfilled template. (docs/60-engineering/CONVENTIONS.md)
INFO
- Persist OS config validates. (.persist/config.json)
- 1 feature folders detected.
- 0 module folders detected.
- 0 ADRs detected.
Result: WARNINGS
[main 305fdb2] Plan the checkout feature
2 files changed, 50 insertions(+)
[exit 0]
$ git log --oneline
305fdb2 Plan the checkout feature
8c93abe Add repository memory
Every check
Checks run in this order. The first two run always; the rest need a valid
.persist/config.json, because they depend on the configured paths.
| Check | What it looks for | Severity |
|---|---|---|
config |
.persist/config.json exists, parses, and validates. Paths must stay inside the repository; a field from an earlier release is reported with the line to delete. |
error |
required-files |
AGENTS.md, CLAUDE.md when claude is selected, the six required documents, and docs/adrs/README.md exist. A missing Cursor rule when cursor is selected is a warning. |
error / warning |
memory-integrity |
A feature folder has PLAN.md and TASKS.md (and TEST_PLAN.md when a test command is set), a module folder has its docs, and every ADR has its required sections. Doc paths linked from the six required documents resolve, and ADRs they cite exist (error) and are accepted (warning). Also reports the counts as info. |
error / warning |
standards |
An ADR has substance in its Consequences and Alternatives Considered sections (error once accepted, otherwise warning). A decision written in security-sensitive terms links a security document under Related Documents (warning). For feature folders in the earlier nine-file layout, a feature marked complete has a review that is not pending and test and result evidence (error). | error / warning |
drift |
Feature or module memory cites an ADR that does not exist (error) or is not yet accepted (warning). | error / warning |
content |
A module doc, or, once the repository has real work, the product file and the security model still contain the template prompts. Older feature PRDs and threat models are checked when present. | warning |
conventions |
CONVENTIONS.md is still a stub once the repository has real work. |
warning |
code-references |
Current-state memory (ADRs, CONVENTIONS.md, and in-progress feature or module docs) cites a code path that does not exist. A code path is any backticked file path under a top-level code folder (src/, app/, packages/, cmd/, and so on); dependencies, build output, and the memory folder are skipped. A feature with a completion report counts as history and is skipped. |
warning |
superseded |
Memory — feature and module docs, context cards, and the required documents — still cites an ADR that has been superseded by a newer decision. | warning |
context-budget |
CLAUDE.md, AGENTS.md, and the Cursor rule together exceed 24 KiB — or the fence index injected by the SessionStart hook no longer fits its share of it. The always-loaded memory should stay a map. |
warning |
staleness |
Current-state memory cites code, in any top-level code folder, whose last commit is more than 90 days after the memory's last commit. Needs git history; otherwise not evaluated. | warning |
ignored-files |
A memory file exists but git ignores it, so the team never receives it. Outside a git repository this check reports not evaluated. | warning |
hook-drift |
The tracked hooks in .persist/hooks/ differ from what the current config would generate. Hand edits are allowed, but should be visible. |
warning |
governing-adrs |
A change touches files an accepted ADR governs (its Applies To list), so the ADR and its decision are named for the agent and the reviewer to check against. It reports and never judges. Not evaluated when no ADR lists the paths it governs. |
info |
hooks-active |
The hooks are written but not switched on in this clone, so doctor and the test gate never run on commit and push. Info, not a warning, when another hooks tool owns core.hooksPath. Not evaluated outside git and in CI, where hooks never apply. |
warning |
retired-skills |
A skill directory holds one of the four skills still retired after 1.4.0 (the message names its replacement and gives the removal command). Custom skills you wrote yourself stay silent — skill create is a supported workflow. |
warning |
duplicate-titles |
Two accepted decisions share one title (error), or a proposal shares a live title (warning). Superseded records are history and never collide. | error / warning |
fence |
A staged source change, or an unpushed one when nothing is staged, rewrites existing code with no record in FENCES.md and no Accepted ADR reference (warning). The warning names the rewritten lines, so the question is answerable. Brand-new files never count as crossings — only edits to existing logic do. A change named only by a Proposed ADR is reported for review as info, a second crossing surfaces the recorded reason as info, and a file a human cleared with --no-constraint stays quiet. |
warning / info |
context-cards |
A context card lists a Start Here path that no longer exists (warning), the files under its Applies To changed long after the card did (warning, the staleness check's 90-day gap), or its Answers list is empty so no task can find it (info). Not evaluated when no cards exist — cards are optional. | warning / info |
The fence loop
A staged change to source with no recorded reason and no ADR reference is a fence crossing. Brand-new files never count — a new file has no existing logic to misunderstand — so creating a migration is quiet while editing an old one still asks why. Generated and cache folders are out of scope too. Doctor names the rewritten lines with their hunk context, so the question is answerable as asked, and warns rather than blocks. A change that only adds lines still warns, naming its insertion points. When nothing is staged, for example because a commit was made with the hooks off, doctor checks the commits the branch hasn't pushed yet, so the question still comes up. Trimmed to the finding:
$ persist doctor
Doctor Report
WARNING
- Change rewrites existing code in src/billing.js with no recorded reason: line 2 (in `export function writeOrder(order) {`). Ask the person who knows: was that behaviour deliberate? If yes: persist fence add src/billing.js --why "<reason>" --by <name>. If no: persist fence add src/billing.js --no-constraint --by <name>. (src/billing.js)
…
Result: WARNINGS
The answer comes from a person, because the constraint lives in someone’s memory
rather than in the code. persist fence add writes it in the exact shape
doctor and the SessionStart hook read, and refuses a path that does not exist:
$ persist fence add src/billing.js --why "Writes four collections in one transaction so the ledger and the audit trail cannot diverge." --by Priya
Persist OS fence add complete.
Fenced: src/billing.js
Created:
- docs/60-engineering/FENCES.md
From then on, a change to that path hands the reason back instead of asking again:
$ persist doctor
Doctor Report
INFO
…
- Change touches a recorded fence: Writes four collections in one transaction so the ledger and the audit trail cannot diverge. (see docs/60-engineering/FENCES.md). Confirm the reason still holds before changing the logic. (src/billing.js)
…
Result: PASSED
The honest answer is sometimes that nothing in the file is deliberate — and that is
recorded too. persist fence add src/billing.js --no-constraint --by Priya
writes a No constraint entry naming who confirmed it, and the file stays
quiet from then on. A later --why for the same path replaces the standing
line and keeps the history; --no-constraint over a recorded reason is
refused. Cleared files are not injected into sessions — they answer nothing an agent
needs before editing.
The file starts empty and grows only through fence add; nothing generates
it. Rename or delete a fenced file and doctor names the entry that now points at
nothing.
Not evaluated is not a pass
When a check has nothing to look at or cannot run, the report says so instead of
silently passing. In a fresh repository with no ADRs, features, or modules,
standards and content are listed as not evaluated with the
reason. The config-dependent checks are all not evaluated when the config is missing
or invalid. staleness and fence are not evaluated
outside a git repository (staleness also in a shallow clone), and fence
also when nothing is staged and the branch has no upstream; hooks-active
outside git and in CI; hook-drift
is not evaluated when a hook file is missing; retired-skills when the
skills directories do not exist; duplicate-titles when the ADR directory
does not exist. Each one carries a reason, in both the text report and the JSON.
$ persist doctor
Doctor Report
INFO
- Persist OS config validates. (.persist/config.json)
- 0 feature folders detected.
- 0 module folders detected.
- 0 ADRs detected.
NOT EVALUATED
- standards: no feature folders or ADRs exist, so there are no completion claims or decisions to check
- content: no feature folders, module folders, or ADRs exist, so there is no memory content to check
- fence: nothing is staged and the branch has no upstream, so there is no change to check — the pre-commit hook checks each commit as it is made
- governing-adrs: no accepted ADR lists the paths it governs (an Applies To section), so no change can be matched to a decision
Result: PASSED
The same fresh memory in a folder that is not a git repository: every check that reads git says so.
$ persist doctor
Doctor Report
INFO
- Persist OS config validates. (.persist/config.json)
- 0 feature folders detected.
- 0 module folders detected.
- 0 ADRs detected.
NOT EVALUATED
- standards: no feature folders or ADRs exist, so there are no completion claims or decisions to check
- content: no feature folders, module folders, or ADRs exist, so there is no memory content to check
- staleness: not a git repository, so commit history is unavailable
- ignored-files: not a git repository, so ignore rules cannot be evaluated
- hooks-active: not a git repository, so there is no clone to switch the hooks on in
- fence: not inside a git work tree, so the staged set is unknown
- governing-adrs: no accepted ADR lists the paths it governs (an Applies To section), so no change can be matched to a decision
Result: PASSED
The JSON report
--json is for CI artifacts, hooks, and agent handoffs. The schema is
versioned so tooling can depend on it. Abridged from a real run:
{
"schemaVersion": "persist.doctor.v1",
"status": "warnings",
"exitCode": 1,
"summary": {
"errors": 0,
"warnings": 6,
"info": 4
},
"findings": [
{
"severity": "info",
"check": "config",
"message": "Persist OS config validates.",
"path": ".persist/config.json"
},
{
"severity": "warning",
"check": "content-product",
"message": "Product purpose is still an unfilled template.",
"path": "docs/00-product/PRODUCT.md"
}
],
"checks": [
{ "id": "config", "status": "evaluated" },
{ "id": "staleness", "status": "not-evaluated", "reason": "…" }
]
}
findings[].check is a fine-grained rule id such as
content-product or standards-adr-consequences;
checks[].id is the check module from the table above. They are different
namespaces on purpose.
Where it runs
- Pre-commit hook
-
.persist/hooks/pre-commitrunspersist doctorfirst, then yourpreCommitGates. Init switches it on when you agree; other clones rungit config core.hooksPath .persist/hooksonce. - CI
-
.github/workflows/persist.ymlrunsnpx --yes persist-os@latest doctoron pull requests and pushes tomain, with full history so the staleness check can run. - The completion loop
-
The generated agent rules end every piece of work with
persist doctor. For a human the same loop ispnpm test:run && pnpm typecheck && persist doctor.
What it will not catch
Contradictions in meaning. An accepted ADR that says one thing while the code does another passes every structural check. That judgment stays with the people and agents doing the work, because the moment the gate needs a model to decide whether two documents agree, it stops being something you can trust blindly.