Skip to content

Linting (CI checks)

Nothing in Keep the Why is enforced the way a compiler enforces correctness — that's stated plainly in "What this skill is not." Part of that gap is mechanically closable, though: whether every entry carries its required fields, whether the values are from the documented sets, whether index.md is complete and sorted, whether .keep-the-why is internally consistent. That part has a linter:

keep-the-why-linton PyPI, developed in this repository under lint/. Python 3.10+, no dependencies beyond the standard library.

What it deliberately does not check: content. Whether recorded rationale is true, complete, or honest isn't mechanically checkable, and the linter doesn't pretend otherwise — Evidence classification stays a judgment call; the linter only guarantees the field is there and holds a legal value.

Schema-version-aware

The linter reads your project's context-schema from .keep-the-why (or the legacy AGENTS.md block, for projects not yet migrated) and enforces only what that skill version defines — the same "next time touched" philosophy as migrations. An unmigrated project doesn't fail on structure its schema version never had. Only the location named by your config's context: field gets linted, plus the config file itself.

Enforced from context-schema What
0.3.0 Status/Evidence mandatory per entry, Verification values
0.7.0 Type field (missing → warning, per the skill's "next time touched" rule)
0.8.0 undefined — <reason> Type value, exclusive
0.9.0 Multiple Type lines per entry
0.10.0 Dedicated .keep-the-why (id required), sorted index.md, guard files

Version scheme

The linter is versioned as <schema>.<revision> — e.g. 0.10.1.0. The first three segments are the newest skill schema this release knows every structural gate of; the fourth is the linter's own revision, bumped for linter-only changes (0.10.1.1). A skill release without structural changes doesn't require a linter release — the gating handles newer context-schema values, and the linter warns (W003) when a project's schema is newer than the newest one it knows, so you can check migrations.md for whether an update matters.

PEP 440, not strict SemVer — PyPI rejects the build-metadata spelling SemVer would use for this. The skill itself keeps SemVer tags (v0.10.1); the linter's PyPI releases are tagged lint-v<version> in this repository, created by the publish workflow itself, never by hand.

Setup snippets

The project init wizard offers to write these for you (GitHub Actions or GitLab CI, detected from the repository; the pre-commit hook only if the project already uses pre-commit) — see CI linting setup. By hand, they're the same files:

GitHub Actions.github/workflows/ktw-lint.yml:

name: ktw-lint

on:
  push:
    branches: [main]
  pull_request:

jobs:
  ktw-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: oliver-zehentleitner/keep-the-why@latest
        with:
          path: "."
          strict: "false"    # "true" turns warnings (e.g. missing Type on old entries) into failures
          # version: "0.10.1.0"   # optional: pin the linter instead of tracking latest

GitLab CI — job for .gitlab-ci.yml:

ktw-lint:
  image: python:3.12
  script:
    - pip install keep-the-why-lint
    - ktw-lint .

pre-commit — hook for .pre-commit-config.yaml:

repos:
  - repo: local
    hooks:
      - id: ktw-lint
        name: keep-the-why-lint
        entry: ktw-lint .
        language: python
        additional_dependencies: ["keep-the-why-lint"]
        pass_filenames: false

Any other CI, or locally:

pip install keep-the-why-lint
ktw-lint .            # exit 0 clean, 1 findings, 2 usage error
ktw-lint . --strict   # warnings fail too

Inside GitHub Actions, findings show up as file/line annotations on the PR. The action always installs the latest linter from PyPI — nothing to pin on your side unless you want to.

What it checks

.keep-the-why / legacy config block — block present and parseable; required fields (context, init, context-schema, capture-confirmation, source-reference, plus id for dedicated files since 0.10.0); values from the documented sets; filtered source-reference carries its criteria; no field recorded twice (conflicting duplicates are exactly the state the skill refuses to guess about); no unknown fields; context-schema is plain semver; pinned-version/pinned-path only as a pair, with the path existing; the configured context location exists; personal-defaults blocks carry no last: timestamps.

Entries (level-2 headings in topic files; fenced code blocks are skipped, so example entries in documentation never get linted as real ones) — Status and Evidence present, single, and valid; Type values valid, undefined carries a reason and combines with nothing; no duplicate Type values; Verification starts with a valid value, and contradicted must say what contradicts it; a heading with no schema fields at all is a warning, not an error — it may be a legitimate prose section.

index.md — exists; every link resolves; every topic file is listed; sorted alphabetically by filename (an error since 0.10.0 — the convention's merge-conflict benefit only exists when the whole list is sorted).

Guard filesREADME.md, AGENTS.md, CLAUDE.md inside the context directory (warnings; an equivalent doing the same job is fine).

Hidden content — invisible/directional Unicode (zero-width characters, bidi overrides) is an error; base64-looking blobs are a warning. This is the one mechanically checkable slice of the trust model: if it needs decoding to be read, it doesn't belong in an entry meant to be read. This is not a secret scanner — pair it with one (e.g. gitleaks) if you need that.

Finding codes

Code Severity Meaning
E001 error no config block found
E002 error required config field missing
E003 error invalid config value
E004 error config field recorded more than once
E005 error unknown config field
E006 error pinned-version/pinned-path pair violation, or pinned path missing
E007 error configured context location doesn't exist
E008 error last: timestamp inside personal-defaults
E101/E102 error entry missing Status / Evidence
E103/E104 error invalid Status / Evidence value
E105 error invalid Type value
E106 error invalid Verification value
E107 error Type: undefined without a reason
E108 error undefined combined with another Type value
E109 error duplicate Type value
E110 error multiple Type lines below schema 0.9.0
E111 error Verification: contradicted without explanation
E112 error more than one Status/Evidence line
E201–E204 error index.md missing / broken link / unlisted topic file / not sorted
E301 error invisible or directional Unicode character
W001 warning context-schema missing (assumed 0.2.0)
W002 warning unrecognized check-interval shape in personal-defaults
W003 warning project context-schema newer than the newest schema this linter knows
W101 warning entry has no Type field
W102 warning level-2 heading without any schema fields
W103 warning Type placed after Status
W104 warning Type present but schema predates 0.7.0
W105 warning empty Revisit when condition
W201 warning guard file missing
W301 warning base64-looking blob

This repository runs the linter on its own context/ in CI, from source and in --strict mode.