Skip to content
Guide contents

Documentation Workflow

← Guide index

This is the main page of the guide. It walks through the four layers, front matter, freshness checks, daily operation, AI integration, and CI.

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.

  1. which before you start — let the machine pick the relevant docs, and read only those. Not everything
  2. verify when 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.

LayerRoleLifetimeRewritten?Freshness check
adr/Decision records. Why this approach, what was rejectedPermanentNo (append only)Only if it has a scope
spec/Current specification. How it is supposed to behave nowSynced with codeYesYes
arch/Architecture. Structure, dependency direction, data flowSynced with codeYesYes
plan/Temporary work plan. Goal and stepsDisposableYesNever

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).

Layerid formatFilenameExample
adr/ADR- + 4 digitsNNNN-<slug>.mdADR-0003adr/0003-sha-based-staleness.md
spec/SPEC-<slug><slug>.mdSPEC-clispec/cli.md
arch/ARCH-<slug><slug>.mdARCH-overviewarch/overview.md
plan/PLAN-<slug><slug>.mdPLAN-phase1plan/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)
---
FieldTypeRequiredNotes
idstringFormat per the table above. Unique
titlestringMust not be empty
statusenumExactly four values: current / draft / stale / superseded
scopestring[]Code this doc is responsible for. Repo-root-relative, /-separated globs
depends_onstring[]Ids this doc builds on. Must reference existing ids
verified_atstringFull 40-char hex commit SHA. Written by docs-kit verify
superseded_bystringwhen supersededId of the successor document
x-*anyProject-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 which and 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

MistakeSymptomFix
Too broad (src/**)Everything makes it stale; eventually nobody looksNarrow to what the doc actually describes. One doc per module is a good target
Too narrow (one file)A change next door goes unnoticedWiden to “the range where a change could make this doc a lie”
Includes testsStale on every test editAdd **/*_test.go etc. to ignore in docs.config.yaml
Points at a nonexistent pathLint warning W002; the verdict is broken (dead-scope). A draft is exempt — see belowFix the typo
Points outside codeRootsLint warning W003Correct 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

statusMeaningHow the AI treats itWho moves it
draftNot reconciled with the implementationReference only; trust the codeSet by new / adopt
currentReconciled. Valid as specificationTrusted as specPromoted by verify
staleKnown to be out of dateReference only; trust the codeDeclared by a human
supersededReplaced by a successorNot readDeclared by a human (superseded_by required)

Points to remember:

  • draft → current goes through verify for docs with a scope. Setting current by hand fails lint E008 without a verified_at, so verify is effectively mandatory
  • Docs without a scope (most ADRs) may be set to current by hand. They carry no freshness guarantee, so verify would mean nothing
  • stale is a human declaration. The verdict from docs-kit stale is never written back into front matter. The command’s verdict (a transient fact) and the document’s status (a statement of intent) are kept separate
  • superseded is 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 plan layer — always excluded
  • A current doc 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 codeWhat happenedFix
unknown-shaThe verified_at SHA is not in the repositoryIf it is a shallow clone, use fetch-depth: 0. If the history is gone, re-verify
not-ancestorA rebase / force push moved the verification point off the current historyReconcile again and re-verify
dead-scopeThe scope globs match no file (code deleted or moved)Correct the scope. If the feature is gone, mark the doc superseded
missing-dependencyA depends_on target id does not existFix the typo, or drop the reference

stale: causes

Reason codeWhat happened
code-changedCode inside scope changed since verified_at (the common case)
premise-supersededA document listed in depends_on became superseded
scope-driftA 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 ruleWhat it absorbs
The ignore globs in docs.config.yamlTests, snapshots — anything that cannot affect the spec
Excluding verify commitsThe self-referential diff when implementation + doc update + verify land in one commit
Intersection with the tree diffChanges 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 scope globs 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

statusHow to treat it
currentTrust it as specification
draft / staleReference material. Trust the code instead
supersededDo 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 plan layer
  • 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.

ToolCLI equivalentPurpose
docs_whichwhichReverse lookup: path → documents worth reading
docs_stalestaleFreshness check
docs_verifyverifyDeclare reconciliation (front matter only)
docs_newnewCreate 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_stale omits skipped documents from results by default (the count still appears in summary). Since non-judged documents are usually the majority, this alone cuts the payload substantially
  • docs_verify takes 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:

  1. Before starting, run which to identify the relevant documents and read only those
  2. Trust current as specification; treat draft / stale as reference and trust the code
  3. If a current document contradicts the implementation, report it rather than silently picking a side
  4. When finished, update the spec, append new decisions as ADRs (never rewrite an existing one — supersede it), verify every document reconciled, and confirm lint / stale / index --check all pass

Notes:

  • The v1 in the marker is the block spec version. Re-running init replaces 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/.

CommandPurpose
/docs-verifyThe closing routine (reflect spec changes → verify → check lint/stale)
/docs-catchupResolve 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_which call 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.

CommandWhat it checksgit history
lintFront matter schema violationsNot needed
staleDrift against the implementationRequired
index --checkWhether 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:

FlagDescription
-C <dir>Change the working directory (like git)
--format text|jsonOutput format for lint / stale / which. Default is text
--versionPrint the version
--forceinit only. Overwrite the generated CI and .claude/commands

Exit codes:

codeMeaning
0Success (warnings included)
1Findings (lint errors / stale)
2broken detected (stale only)
3Execution 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__/**"
KeyPurpose
schemaVersionCompatibility. An unknown value exits 3
rootParent of the layer directories (adr/ etc.)
codeRootsBasis for lint W003 (scope points outside the code roots)
ignoreGlobs 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):

CodeMeaning
E001No front matter, or it does not parse as YAML
E002Missing required field (id / title / status)
E003Malformed id (layer prefix, character set)
E004id and filename do not correspond
E005Duplicate id
E006status outside the enum
E007Unknown top-level field (other than x-)
E008status: current with a scope but no verified_at
E009verified_at is not 40 hex characters
E010status: superseded without superseded_by
E011depends_on / superseded_by references a nonexistent id
E012Malformed scope glob (including absolute paths and ..)

Warnings (exit 0):

CodeMeaning
W001A plan layer document has verified_at (meaningless)
W002A scope glob matches no git-tracked file. Not reported while status is draft
W003A 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