Documentation Workflow
This is the main page of the guide. It walks through the four layers, front matter, freshness checks, daily operation, AI integration, and CI.
- 1. The big picture
- 2. The four layers
- 3. Front matter
- 4. Writing scope
- 5. The status lifecycle
- 6. How the freshness check works
- 7. Day-to-day operation
- 8. Using it from an AI
- 9. Enforcing it in CI
- 10. Command reference
- 11. Lint code reference
The CLI’s messages are currently Japanese only. Output shown below is the real output.
1. The big picture
Operating docs-kit means running a small loop once per code change.
Only two things matter.
whichbefore you start — let the machine pick the relevant docs, and read only those. Not everythingverifywhen you finish — stamp the docs you reconciled with “reconciled as of now”
Hold those two and CI catches the rest.
2. The four layers — what goes where
Every document lives in one of four layers. The set cannot be extended.
| Layer | Role | Lifetime | Rewritten? | Freshness check |
|---|---|---|---|---|
adr/ | Decision records. Why this approach, what was rejected | Permanent | No (append only) | Only if it has a scope |
spec/ | Current specification. How it is supposed to behave now | Synced with code | Yes | Yes |
arch/ | Architecture. Structure, dependency direction, data flow | Synced with code | Yes | Yes |
plan/ | Temporary work plan. Goal and steps | Disposable | Yes | Never |
Which layer?
Guidance per layer
adr/ — decision records
One decision per file, written as Situation → Decision → Consequences. Always record the options you rejected and why. Without that, you will restart the same argument in six months.
When the direction changes, do not rewrite the ADR: add a new one and mark the old with status: superseded and superseded_by: ADR-00NN.
spec/ — current specification
Write only what is true now. Link to the ADR for the history. Always fill in scope — a spec without one is excluded from the freshness check, which removes most of the point of using docs-kit.
arch/ — architecture
Package structure, dependency direction, data flow. For many projects a single arch/overview.md is enough.
plan/ — work plans
Temporary scaffolding, deleted at merge. It cannot carry verified_at and is never freshness-checked. Do not write progress state (done/doing checkboxes) — that belongs in the issue tracker.
3. Front matter — document metadata
Layout and ids
Only *.md files directly inside <root>/<layer>/ are managed. Subdirectories are not scanned, so they are free for assets such as images. <root>/INDEX.md is generated output.
docs/
├── INDEX.md ← generated. never hand-edit
├── adr/
│ ├── 0001-markdown-as-source-of-truth.md → ADR-0001
│ └── 0002-four-layer-structure.md → ADR-0002
├── spec/
│ ├── cli.md → SPEC-cli
│ └── staleness.md → SPEC-staleness
├── arch/
│ └── overview.md → ARCH-overview
├── plan/
│ └── phase1-core-cli.md → PLAN-phase1-core-cli
└── assets/ ← not scanned. images can live here
└── diagram.png
The id and the filename must correspond one-to-one. A mismatch is a lint error (E004).
| Layer | id format | Filename | Example |
|---|---|---|---|
adr/ | ADR- + 4 digits | NNNN-<slug>.md | ADR-0003 ↔ adr/0003-sha-based-staleness.md |
spec/ | SPEC-<slug> | <slug>.md | SPEC-cli ↔ spec/cli.md |
arch/ | ARCH-<slug> | <slug>.md | ARCH-overview ↔ arch/overview.md |
plan/ | PLAN-<slug> | <slug>.md | PLAN-phase1 ↔ plan/phase1.md |
Slugs match [a-z0-9]+(-[a-z0-9]+)* (lowercase alphanumerics and hyphens). Ids are unique within the repository.
Fields
---
id: SPEC-editing-model # required
title: Editing model # required
status: current # required
scope: # optional (recommended for spec/arch)
- src/core/editing/**
depends_on: [ADR-0003] # optional
verified_at: 8f3a1c2e0b7d4a91c3f6e2d8b5a704c1e9f3d602 # optional (40 chars)
x-owner: platform-team # optional (free-form)
---
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | ✔ | Format per the table above. Unique |
title | string | ✔ | Must not be empty |
status | enum | ✔ | Exactly four values: current / draft / stale / superseded |
scope | string[] | — | Code this doc is responsible for. Repo-root-relative, /-separated globs |
depends_on | string[] | — | Ids this doc builds on. Must reference existing ids |
verified_at | string | — | Full 40-char hex commit SHA. Written by docs-kit verify |
superseded_by | string | when superseded | Id of the successor document |
x-* | any | — | Project-specific extensions. Preserved verbatim, never interpreted |
Any unknown top-level field other than x- is a lint error (E007). To add team-specific metadata (owner, Jira key, …), prefix it with x-.
Body conventions
- A heading of the form
# <id>: <title>right after the front matter is recommended (lint does not enforce it) - There is no summary field. The 200-character summary returned by
whichand MCP is derived from the first non-heading, non-empty paragraph of the body. A separate summary field would drift from the body - Therefore, make the first paragraph say what this document is. It directly improves findability
4. Writing scope
scope is the one parameter that determines docs-kit’s precision. It is worth some care.
scope:
- src/core/editing/** # everything under a directory
- src/api/editing.ts # a single file
- "src/**/*.editing.ts" # a name pattern
- Syntax is doublestar (
**supported) - Repo-root-relative,
/-separated (even on Windows) - Leading
/,./, or..is invalid (E012) - Only git-tracked files are considered
Common mistakes
| Mistake | Symptom | Fix |
|---|---|---|
Too broad (src/**) | Everything makes it stale; eventually nobody looks | Narrow to what the doc actually describes. One doc per module is a good target |
| Too narrow (one file) | A change next door goes unnoticed | Widen to “the range where a change could make this doc a lie” |
| Includes tests | Stale on every test edit | Add **/*_test.go etc. to ignore in docs.config.yaml |
| Points at a nonexistent path | Lint warning W002; the verdict is broken (dead-scope). A draft is exempt — see below | Fix the typo |
Points outside codeRoots | Lint warning W003 | Correct codeRoots, or correct the scope |
The test is: “if this file changes, do I need to reread this document?” If yes, include it.
Writing the doc before the code
While a document is draft, its scope may name files that do not exist yet. W002 stays quiet, and that is deliberate: a draft’s scope declares the code the document is about to own.
status: draft
scope:
- Sources/Ingest/** # not written yet — this is fine
This is what makes the protocol work in a greenfield project. docs-kit which Sources/Ingest/Pipeline.swift finds the spec that will own that file before the file exists, so an AI told to “read what which returns” has something to read.
The moment verify promotes the document to current, W002 and the dead-scope verdict both fire — so a real typo surfaces exactly when it starts to matter. W003 keeps watching for a wrong root the whole time.
If nothing declares a scope yet, which returns empty with a hint pointing at docs/INDEX.md, rather than silently looking like there is nothing to read.
5. The status lifecycle
| status | Meaning | How the AI treats it | Who moves it |
|---|---|---|---|
draft | Not reconciled with the implementation | Reference only; trust the code | Set by new / adopt |
current | Reconciled. Valid as specification | Trusted as spec | Promoted by verify |
stale | Known to be out of date | Reference only; trust the code | Declared by a human |
superseded | Replaced by a successor | Not read | Declared by a human (superseded_by required) |
Points to remember:
draft → currentgoes throughverifyfor docs with ascope. Settingcurrentby hand fails lint E008 without averified_at, so verify is effectively mandatory- Docs without a
scope(most ADRs) may be set tocurrentby hand. They carry no freshness guarantee, so verify would mean nothing staleis a human declaration. The verdict fromdocs-kit staleis never written back into front matter. The command’s verdict (a transient fact) and the document’s status (a statement of intent) are kept separatesupersededis terminal. Running verify on it is an error
6. How the freshness check works
What gets judged
Only documents that are status: current, have a non-empty scope, and carry a verified_at. Everything else is skipped (not judged, no effect on the exit code).
draft/stale/superseded— they never claimed freshness- The
planlayer — always excluded - A
currentdoc missing scope or verified_at — caught by lint as E008
The decision path
After all documents are judged, the exit code is the worst verdict (broken > stale > ok).
broken: causes and fixes
broken means the premise of the check is broken — a problem before the content even matters.
| Reason code | What happened | Fix |
|---|---|---|
unknown-sha | The verified_at SHA is not in the repository | If it is a shallow clone, use fetch-depth: 0. If the history is gone, re-verify |
not-ancestor | A rebase / force push moved the verification point off the current history | Reconcile again and re-verify |
dead-scope | The scope globs match no file (code deleted or moved) | Correct the scope. If the feature is gone, mark the doc superseded |
missing-dependency | A depends_on target id does not exist | Fix the typo, or drop the reference |
stale: causes
| Reason code | What happened |
|---|---|
code-changed | Code inside scope changed since verified_at (the common case) |
premise-superseded | A document listed in depends_on became superseded |
scope-drift | A rename quietly moved a file out of scope |
premise-superseded earns its keep quietly. It catches the case where the code did not change but the decision it rested on was overturned — the kind of rot humans miss most often.
Diffs that get absorbed automatically
“Any change inside scope means stale” would not be workable, so diffs that clearly cannot affect the spec are excluded.
| Exclusion rule | What it absorbs |
|---|---|
The ignore globs in docs.config.yaml | Tests, snapshots — anything that cannot affect the spec |
| Excluding verify commits | The self-referential diff when implementation + doc update + verify land in one commit |
| Intersection with the tree diff | Changes made and undone within the range (net-zero) |
| Pure renames (R100) | File moves with no content change |
“Excluding verify commits” is decided by whether that commit modified the verified_at line — not by blanket-excluding the last commit that touched the doc. So a commit that edited the doc without verifying still shows up as a diff.
Uncommitted changes (dirty)
Judgement is always against HEAD. If there are uncommitted changes inside scope, the result carries a dirty warning (the verdict itself is unaffected). verify warns the same way — changes not in HEAD cannot be declared verified.
Output
docs-kit stale
SPEC-editing-model stale code-changed: 3 files changed since 8f3a1c2
- src/core/editing/model.ts (2 commits)
- src/core/editing/undo.ts
ARCH-overview ok (verified at 8f3a1c2)
SPEC-mcp-tools skipped (draft)
ok 1 / stale 1 / broken 0 / skipped 1
A verdict line may end with [uncommitted changes inside scope] — that is the dirty warning (“uncommitted changes inside scope”).
JSON is available for CI and AI consumers.
docs-kit stale --format json
{
"results": [
{
"id": "SPEC-editing-model",
"path": "docs/spec/editing-model.md",
"level": "stale",
"verified_at_short": "8f3a1c2",
"reasons": [{ "code": "code-changed", "detail": "3 files changed" }],
"evidence": {
"files": ["src/core/editing/model.ts", "src/core/editing/undo.ts"],
"commits": ["...", "..."],
"absorbed": [{ "kind": "rename", "from": "src/a.ts", "to": "src/b.ts" }]
},
"dirty": false
}
],
"summary": { "ok": 1, "stale": 1, "broken": 0, "skipped": 1 },
"exit_code": 1
}
evidence carries the reasoning (changed files and commits), so an AI can go read those diffs directly.
7. Day-to-day operation
7.1 Before you start — narrow the reading list with which
Pass the paths you are about to touch and get back the relevant documents.
docs-kit which src/core/editing/model.ts
primary:
SPEC-editing-model current 8f3a1c2 docs/spec/editing-model.md
related:
ADR-0003 current - docs/adr/0003-undo-stack.md
- primary — documents whose
scopeglobs matched one of the argument paths - related — a one-hop expansion of primary’s
depends_on
Multiple paths and directories work too (a directory expands to <dir>/**).
docs-kit which src/core/editing src/api/editing.ts
No match produces empty output and exit 0 — “no related documents” is normal information, not an error.
Read only what came back. This is the findability half of docs-kit’s value, and the mechanism that keeps docs/ from being read wholesale.
7.2 While working — what to trust
| status | How to treat it |
|---|---|
current | Trust it as specification |
draft / stale | Reference material. Trust the code instead |
superseded | Do not read it (follow superseded_by) |
If a current document contradicts the implementation, do not silently pick a side. Report the contradiction and get a decision. docs-kit can detect that something inside scope changed; deciding which of the two is right is a human call.
7.3 When you finish — update, then verify
verify updates verified_at to HEAD and promotes draft / stale to current.
docs-kit verify SPEC-editing-model ARCH-overview
SPEC-editing-model: verified_at → 8f3a1c2
ARCH-overview: verified_at → 8f3a1c2 (promoted to current)
verify rewrites only the relevant front matter lines. It does not re-serialize the YAML, so x- fields, comments, and the body survive byte for byte.
It fails (exit 3) unless the preconditions hold:
- The id exists
- It has a
scope - It is not in the
planlayer - Its status is not
superseded
7.4 Commit granularity
Because of how the check works, bundling unrelated implementation changes into a verify commit hides them (verify commits are excluded from the diff calculation).
The recommended shape:
commit 1: the implementation change
commit 2: doc update + verify ← no other implementation changes here
If you do combine them, keep the implementation changes in that commit limited to what the updated document describes.
Changes that arrive only through a merge commit also drop out of the calculation, so squash merges are recommended.
8. Using it from an AI (Claude Code)
The MCP server
claude mcp add docs-kit -- docs-kit mcp
The registration points at docs-kit on your PATH, so replacing the binary requires no re-registration — start a new session and the updated tool definitions are picked up.
Four tools are provided.
| Tool | CLI equivalent | Purpose |
|---|---|---|
docs_which | which | Reverse lookup: path → documents worth reading |
docs_stale | stale | Freshness check |
docs_verify | verify | Declare reconciliation (front matter only) |
docs_new | new | Create a document from a template |
The server never returns or writes document bodies. It returns absolute paths, metadata, and a 200-character summary. Reading and editing the body is the AI’s own Read / Edit tools’ job — which is why diff display, permission prompts, and partial edits all keep working.
Two differences from the CLI matter in practice:
docs_staleomitsskippeddocuments fromresultsby default (the count still appears insummary). Since non-judged documents are usually the majority, this alone cuts the payload substantiallydocs_verifytakes multiple ids and returns per-item results. Ids that fail preconditions do not stop the rest, so the AI can self-repair just the failures
The CLAUDE.md managed block
docs-kit init injects this block into CLAUDE.md. It is byte-for-byte identical in every repository.
<!-- docs-kit:begin v1 -->
## docs-kit protocol
Design docs live under docs/ (adr = decision records, spec = current specification, arch = architecture, plan = temporary work plans).
Before starting work:
1. Run `docs-kit which <path you are changing>` to identify the relevant documents, and read only those
2. Trust documents with `status: current` as specification. Treat draft / stale as reference material and trust the implementation code instead
3. If a current document contradicts the implementation, do not quietly pick a side — report the contradiction and ask
When finishing work:
1. If the specification changed, update spec/. Record new design decisions as an ADR with `docs-kit new adr <slug>` (never rewrite an existing ADR — supersede it instead)
2. Run `docs-kit verify <id>` for every document you reconciled against the implementation
3. Confirm `docs-kit lint && docs-kit stale && docs-kit index --check` all pass before calling the work done
Conventions:
- Do not record progress state (done/doing) in docs. That belongs in an issue tracker
- docs/INDEX.md is generated. Never edit it by hand (regenerate with `docs-kit index`)
<!-- docs-kit:end -->
In English, the block pins this protocol:
- Before starting, run
whichto identify the relevant documents and read only those - Trust
currentas specification; treatdraft/staleas reference and trust the code - If a
currentdocument contradicts the implementation, report it rather than silently picking a side - When finished, update the spec, append new decisions as ADRs (never rewrite an existing one — supersede it),
verifyevery document reconciled, and confirmlint/stale/index --checkall pass
Notes:
- The
v1in the marker is the block spec version. Re-runninginitreplaces just this block with the current version - Nothing outside the block is touched, so it coexists with your existing CLAUDE.md content
Slash commands
init also writes two commands into .claude/commands/.
| Command | Purpose |
|---|---|
/docs-verify | The closing routine (reflect spec changes → verify → check lint/stale) |
/docs-catchup | Resolve stale / broken documents by reconciling them with the implementation |
What selective reading actually looks like
Measured during docs-kit’s own development sessions:
- One
docs_whichcall returned primary 3 + related 12 = 15 documents; only 4 bodies were read. The other 11 were ruled out from title and summary alone - However, all 4 that were opened needed the full text. A 200-character summary is enough to decide whether to read, not enough to tell you what it says
- Paths that do not exist yet are silently ignored. Being able to speculatively pass “paths I am about to create” before starting turns out to be useful
9. Enforcing it in CI
The workflow generated by docs-kit init:
name: docs-kit
on:
pull_request:
push:
branches: [main]
jobs:
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # stale needs the full history
- name: Install docs-kit
run: |
curl -fsSL "https://downloads.noop.co.jp/docs-kit/${DOCS_KIT_VERSION}/docs-kit_linux_amd64.tar.gz" | tar -xz
sudo install -m 755 docs-kit /usr/local/bin/docs-kit
env:
DOCS_KIT_VERSION: v1.0.0
- run: docs-kit lint
- run: docs-kit stale
- run: docs-kit index --check
fetch-depth: 0 is required. With a shallow clone the verified_at SHAs are not in history, and every document comes back broken (unknown-sha).
The three commands have distinct jobs.
| Command | What it checks | git history |
|---|---|---|
lint | Front matter schema violations | Not needed |
stale | Drift against the implementation | Required |
index --check | Whether INDEX.md is current (gofmt style) | Not needed |
If stale fires on a lot of documents right after adoption, tighten CI in stages — see Adopting in an existing project.
10. Command reference
docs-kit init Generate scaffolding + config + CLAUDE.md block + CI
docs-kit adopt Add front matter stubs to existing docs (status: draft)
docs-kit lint Validate front matter against the schema
docs-kit stale [id...] Freshness check
docs-kit index [--check] Generate INDEX.md (--check verifies sync, for CI)
docs-kit which <path...> Reverse lookup: path -> related documents
docs-kit new <layer> <slug> Create a document from a template (--title)
docs-kit verify <id...> Update verified_at to HEAD and promote to current
docs-kit mcp Start the MCP server over stdio
Common flags:
| Flag | Description |
|---|---|
-C <dir> | Change the working directory (like git) |
--format text|json | Output format for lint / stale / which. Default is text |
--version | Print the version |
--force | init only. Overwrite the generated CI and .claude/commands |
Exit codes:
| code | Meaning |
|---|---|
| 0 | Success (warnings included) |
| 1 | Findings (lint errors / stale) |
| 2 | broken detected (stale only) |
| 3 | Execution error (bad config, git missing, bad arguments, …) |
All output paths are repo-root-relative and /-separated.
Configuration file
docs.config.yaml sits at the repository root, exactly one of them. This is the entire configuration surface.
schemaVersion: 1 # required. currently only 1
root: docs # optional. parent of the layer dirs. default "docs"
codeRoots: [src] # optional. code roots. default ["src"]
ignore: # optional. globs ignored by the freshness check. default []
- "**/*.test.ts"
- "**/__snapshots__/**"
| Key | Purpose |
|---|---|
schemaVersion | Compatibility. An unknown value exits 3 |
root | Parent of the layer directories (adr/ etc.) |
codeRoots | Basis for lint W003 (scope points outside the code roots) |
ignore | Globs removed from the stale change set (applied after the scope match) |
Unknown top-level keys are an error; there is no x- namespace in the config. A broken config exits 3 immediately rather than reporting lint/stale results, so that a false green is impossible.
11. Lint code reference
Errors (exit 1):
| Code | Meaning |
|---|---|
| E001 | No front matter, or it does not parse as YAML |
| E002 | Missing required field (id / title / status) |
| E003 | Malformed id (layer prefix, character set) |
| E004 | id and filename do not correspond |
| E005 | Duplicate id |
| E006 | status outside the enum |
| E007 | Unknown top-level field (other than x-) |
| E008 | status: current with a scope but no verified_at |
| E009 | verified_at is not 40 hex characters |
| E010 | status: superseded without superseded_by |
| E011 | depends_on / superseded_by references a nonexistent id |
| E012 | Malformed scope glob (including absolute paths and ..) |
Warnings (exit 0):
| Code | Meaning |
|---|---|
| W001 | A plan layer document has verified_at (meaningless) |
| W002 | A scope glob matches no git-tracked file. Not reported while status is draft |
| W003 | A scope points outside codeRoots (likely a typo) |
lint performs only static validation with no dependency on git history. History-dependent checks such as SHA resolvability belong to stale.
Next
- The reasoning behind the design → Concept and design principles
- Set up a new project → Project setup
- Bring in existing design docs → Adopting in an existing project