Concepts

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-commit runs persist doctor first, then your preCommitGates. Init switches it on when you agree; other clones run git config core.hooksPath .persist/hooks once.
CI
.github/workflows/persist.yml runs npx --yes persist-os@latest doctor on pull requests and pushes to main, 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 is pnpm 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.