Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 5 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
# km

The km knowledge-management skill for Claude Code, plus the scripts and workflows it copies into
brains. See README.md for the layout.
The km knowledge-management skill for Claude Code, plus the scripts and workflows it copies into brains. See README.md for the layout.

## Conventions

- Language: English for code, comments, documentation and commit messages.
- Public repository: no employer, customer, host or ticket names in code, examples, tests or history.
- Conventional Commits `type(scope): description`; one logical change per commit.
- Python 3.11+, only PyYAML as a dependency. The package lives in `src/km/`; each command is a module
with a `main()` that `km.cli` calls.
- Python 3.11+, only PyYAML as a dependency. The package lives in `src/km/`; each command is a module with a `main()` that `km.cli` calls.
- Shell scripts: `bash`, portable (macOS + Linux).
- Brains pin a km version and never carry km code, but their `schema.local.yaml`, docs and templates
from earlier versions stay: keep new versions working on them, and keep `km upgrade` able to move
an older brain forward.
- Brains pin a km version and never carry km code, but their `schema.local.yaml`, docs and templates from earlier versions stay: keep new versions working on them, and keep `km upgrade` able to move an older brain forward.
- The one exception is the team template `bump-mounted-pins.py` (with its workflow), copied into a team brain because the job holding the app token must not install third-party code, the km package included. `km upgrade` replaces a copy whose hash is in `SHIPPED` (src/km/upgrade.py): a change to either template adds its new hash there and keeps the old ones (a test checks the current one is listed).
- A release bumps `src/km/__init__.py`, `.claude-plugin/plugin.json` and the README examples together
(a test checks it). Merging it to main is the release: CI tags `vX.Y.Z` once main is green. The
templates pin the tag of the km that wrote them.
- Run all the smoke tests before pushing; add a case for every behaviour change and check that it
fails without the change.
- A release bumps `src/km/__init__.py`, `.claude-plugin/plugin.json` and the README examples together (a test checks it). Merging it to main is the release: CI tags `vX.Y.Z` once main is green. The templates pin the tag of the km that wrote them.
- Run all the smoke tests before pushing; add a case for every behaviour change and check that it fails without the change.
- A change under `skills/km/` runs the plugin evals before merge (CI does not run them, they cost money); `evals/README.md` says which cases and how to report them.
84 changes: 15 additions & 69 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
# km

Knowledge management for markdown knowledge bases ("brains"): a Claude Code skill, and the `km`
command and GitHub Action that keep a brain consistent.
Knowledge management for markdown knowledge bases ("brains"): a Claude Code skill, and the `km` command and GitHub Action that keep a brain consistent.

- Answer a question from the brain, with sources, also from its open pull requests. GitHub reads
use `gh`, or GitHub MCP tools where `gh` is not signed in.
- Answer a question from the brain, with sources, also from its open pull requests. GitHub reads use `gh`, or GitHub MCP tools where `gh` is not signed in.
- Save a note, decision or meeting transcript with validated frontmatter, in the right folder.
- Update or supersede a page, lint the brain, mount peer brains as submodules.
- Contribute to a brain you only read, as a pull request.
- Set up a shared **team brain**: one maintainer merges, the team reads the repo, CI validates
every PR, keeps each folder's `_index.md` generated and reachable, builds a per-audience bundle
for AI tools, and demotes accepted docs whose review date has lapsed.
- Set up a shared **team brain**: one maintainer merges, the team reads the repo, CI validates every PR, keeps each folder's `_index.md` generated and reachable, builds a per-audience bundle for AI tools, and demotes accepted docs whose review date has lapsed.

## Install

Expand Down Expand Up @@ -38,9 +34,7 @@ As a Claude Code plugin:
/km help # all commands
```

**Cross-repo brains:** other knowledge repos are mounted as git submodules under `brains/`. Each
brain keeps its own access permissions, is read at the commit the brain pins (`km sync` keeps the
checkout there; a pin moves only through a pull request), and every answer cites `[brain@commit]`.
**Cross-repo brains:** other knowledge repos are mounted as git submodules under `brains/`. Each brain keeps its own access permissions, is read at the commit the brain pins (`km sync` keeps the checkout there; a pin moves only through a pull request), and every answer cites `[brain@commit]`.

## The km command

Expand All @@ -66,39 +60,15 @@ km sync [--quiet] [--offline] [--max-age 15m]
km list [PATH...] [filters] docs from their frontmatter: --status, --signoff, --due DATE, --json
```

`km sync` keeps a copy from going stale. Its upstream is the remote named `upstream` where one
exists, else the parent of a fork (asked of `gh`), else origin. It fetches, and then only ever
fast-forwards: the default branch when it is checked out and the tree has no uncommitted changes;
a fork's default branch on the host (`gh repo sync`; a plain push, which git refuses unless it is
a fast-forward, only where gh is not installed); the mounted brains, to the commit the brain pins
when they are behind it (a pin never moves here, that stays with the host's automation and a
reviewed pull request). A work branch, a dirty tree, a branch that diverged or a mount moved past
its pin is reported with how far behind it is, never rebased, reset or moved back. `--quiet`
prints only what changed or needs attention, `--max-age 15m` makes a repeated run free, and an
unreachable remote is a note, not a failure. The plugin runs `km sync --quiet --max-age 15m` when a
Claude Code session starts in a brain (`hooks/hooks.json`), so what it reports is in Claude's
context from the first prompt; outside a brain, without network or without km the hook is silent.

`km doctor` reads what to expect from the brain alone: its origin and upstream (the same upstream
`km sync` follows: on a fork, both the checked-out branch and the fork's default branch on the
host must have everything the upstream brain has), the brains it mounts (`.gitmodules`), the km
version it pins, and the plugins its `.claude/settings.json` enables (`enabledPlugins`, with the
marketplaces in `extraKnownMarketplaces`). A brain lists its plugins there for Claude Code anyway,
which offers to add the marketplace when the folder is trusted. It also checks that the pre-commit
hook is installed, that text files keep LF line endings, and, for a brain on GitHub, that `gh` is
signed in and whether you push to the brain or through a fork. Each check that fails names its
fix; it needs no PyYAML, so it also runs on a computer that is half set up.

Every command takes `--root DIR`; the default is the git work tree around the current directory.
Install a release by its commit (`git ls-remote https://github.com/pasrom/km refs/tags/v1.9.0` shows
it), not by the tag: `pip install git+https://github.com/pasrom/km@<commit> # v1.9.0`, or run it
without installing: `uvx --from git+https://github.com/pasrom/km@<commit> km validate # v1.9.0`.
`km sync` keeps a copy from going stale. Its upstream is the remote named `upstream` where one exists, else the parent of a fork (asked of `gh`), else origin. It fetches, and then only ever fast-forwards: the default branch when it is checked out and the tree has no uncommitted changes; a fork's default branch on the host (`gh repo sync`; a plain push, which git refuses unless it is a fast-forward, only where gh is not installed); the mounted brains, to the commit the brain pins when they are behind it (a pin never moves here, that stays with the host's automation and a reviewed pull request). A work branch, a dirty tree, a branch that diverged or a mount moved past its pin is reported with how far behind it is, never rebased, reset or moved back. `--quiet` prints only what changed or needs attention, `--max-age 15m` makes a repeated run free, and an unreachable remote is a note, not a failure. The plugin runs `km sync --quiet --max-age 15m` when a Claude Code session starts in a brain (`hooks/hooks.json`), so what it reports is in Claude's context from the first prompt; outside a brain, without network or without km the hook is silent.

`km doctor` reads what to expect from the brain alone: its origin and upstream (the same upstream `km sync` follows: on a fork, both the checked-out branch and the fork's default branch on the host must have everything the upstream brain has), the brains it mounts (`.gitmodules`), the km version it pins, and the plugins its `.claude/settings.json` enables (`enabledPlugins`, with the marketplaces in `extraKnownMarketplaces`). A brain lists its plugins there for Claude Code anyway, which offers to add the marketplace when the folder is trusted. It also checks that the pre-commit hook is installed, that text files keep LF line endings, and, for a brain on GitHub, that `gh` is signed in and whether you push to the brain or through a fork. Each check that fails names its fix; it needs no PyYAML, so it also runs on a computer that is half set up.

Every command takes `--root DIR`; the default is the git work tree around the current directory. Install a release by its commit (`git ls-remote https://github.com/pasrom/km refs/tags/v1.9.0` shows it), not by the tag: `pip install git+https://github.com/pasrom/km@<commit> # v1.9.0`, or run it without installing: `uvx --from git+https://github.com/pasrom/km@<commit> km validate # v1.9.0`.

## What a brain gets

Brains carry no km code (the one exception, the script of the mounted-pins workflow, is below). `km init` writes `CONVENTIONS.md`, `CLAUDE.md`, `schema.local.yaml`,
`inbox/`, a `.gitattributes` that keeps text files LF, and a pre-commit hook pinned to a km version. `km init --team` adds domain folders with
generated indexes, a README, and CI that installs km through this repository's GitHub Action:
Brains carry no km code (the one exception, the script of the mounted-pins workflow, is below). `km init` writes `CONVENTIONS.md`, `CLAUDE.md`, `schema.local.yaml`, `inbox/`, a `.gitattributes` that keeps text files LF, and a pre-commit hook pinned to a km version. `km init --team` adds domain folders with generated indexes, a README, and CI that installs km through this repository's GitHub Action:

```yaml
- uses: pasrom/km@<commit> # v1.9.0, installs the km CLI at that commit
Expand All @@ -110,26 +80,9 @@ The team brain's ci workflow also runs on `workflow_dispatch`: a pull request th

A team brain also gets `bump-mounted-pins`, a workflow that moves the pins of the mounted brains (submodules) by pull request, in place of Dependabot's submodule updates, which need an organisation-wide grant for private submodules and stop every submodule of a group on one refusal. On weekday mornings it walks each mount's tracked branch (its `branch =`, else the repository's default branch) back from the tip to the newest commit whose CI on that branch did not fail (runs of a side branch or a tag do not count; a commit without a run of its own, such as a bot's, inherits the state of the nearest older commit that has runs; a pin never moves backwards), sets the gitlinks without cloning, force-pushes `chore/bump-mounted-pins` with a lease and opens or updates the pull request, which lists short shas only, and dispatches ci on it. A brain without mounts stops after reading `.gitmodules`. It reads the mounts with a GitHub App (Contents and Actions: read) whose secrets `CI_APP_CLIENT_ID` and `CI_APP_PRIVATE_KEY` live in an environment `mounted-pins` restricted to `main`; the workflow's header says how to set it up, and `km doctor` notes a missing or unrestricted environment. Its script, `.github/scripts/bump-mounted-pins.py`, is the one piece of km code a brain carries: the job that holds the app token installs nothing, so it cannot run the km package. `km upgrade` adds both files when missing and replaces a copy that is still a version km shipped, unchanged but for the action pins its Dependabot moves (a pin moved past the template's version is kept), so later fixes reach the brain; a copy the brain edited is its own, left alone with a note.

The pre-commit hook works the same way (`repo: https://github.com/pasrom/km`,
`rev: <commit> # frozen: v1.9.0`, `id: km-validate`). Every pin names a release by its commit, so a
tag moved later cannot change what a brain runs; the version rides along as a comment, and
`km upgrade` checks that the commit is that release's. km's own dependencies are pinned to exact
versions. `km init` and `km upgrade` look a release's commit up by its tag once, so a release tag here
must never be moved or deleted; a repository ruleset on `v*` makes sure of it. Dependabot
and `pre-commit autoupdate --freeze` propose newer versions; `/km upgrade` moves the pins and turns
a brain from the copy-in days into one that pins km.

A team brain also keeps direct pushes off `main`, which the host may not enforce for a private
repository on a free plan: its pre-commit config adds `no-commit-to-branch` for `main` from
`pre-commit/pre-commit-hooks` and a pre-push hook that refuses `git push origin main` (`pre-commit install`
installs both, the config names both hook types and needs pre-commit 3.2 or newer; pre-commit checks
the first ref of a push only and skips a push whose commits the remote already has on another ref, so
a push of several refs at once or a first push of `main` gets past it), and a `main-guard` workflow fails and opens an
issue when a commit reaches `main` without a merged pull request. `km upgrade` adds whichever a team
brain lacks. The plugin itself refuses the file-edit tools (Edit, Write, MultiEdit, NotebookEdit) inside
a mounted brain (a brain checked out as a submodule of a brain, found through git, not by path) and
in a mount that is not checked out yet: a correction goes through a pull request against that
brain, in a throwaway clone, as the skill describes; a shell edit is not caught.
The pre-commit hook works the same way (`repo: https://github.com/pasrom/km`, `rev: <commit> # frozen: v1.9.0`, `id: km-validate`). Every pin names a release by its commit, so a tag moved later cannot change what a brain runs; the version rides along as a comment, and `km upgrade` checks that the commit is that release's. km's own dependencies are pinned to exact versions. `km init` and `km upgrade` look a release's commit up by its tag once, so a release tag here must never be moved or deleted; a repository ruleset on `v*` makes sure of it. Dependabot and `pre-commit autoupdate --freeze` propose newer versions; `/km upgrade` moves the pins and turns a brain from the copy-in days into one that pins km.

A team brain also keeps direct pushes off `main`, which the host may not enforce for a private repository on a free plan: its pre-commit config adds `no-commit-to-branch` for `main` from `pre-commit/pre-commit-hooks` and a pre-push hook that refuses `git push origin main` (`pre-commit install` installs both, the config names both hook types and needs pre-commit 3.2 or newer; pre-commit checks the first ref of a push only and skips a push whose commits the remote already has on another ref, so a push of several refs at once or a first push of `main` gets past it), and a `main-guard` workflow fails and opens an issue when a commit reaches `main` without a merged pull request. `km upgrade` adds whichever a team brain lacks. The plugin itself refuses the file-edit tools (Edit, Write, MultiEdit, NotebookEdit) inside a mounted brain (a brain checked out as a submodule of a brain, found through git, not by path) and in a mount that is not checked out yet: a correction goes through a pull request against that brain, in a throwaway clone, as the skill describes; a shell edit is not caught.

## Layout

Expand All @@ -147,16 +100,9 @@ tests/ smoke tests

## Development

The smoke tests need Python 3.11+, PyYAML and git: `bash tests/gate_smoke.sh`, `index_smoke.sh`,
`team_smoke.sh`, `doctor_smoke.sh`, `sync_smoke.sh`, `list_smoke.sh`, `mounted_pins_smoke.sh` (the mounted-pins workflow and script a team brain gets, against a stand-in GitHub API), `skill_smoke.sh` (the skill's core
keeps its size limits, its reference paths resolve, every `/km` verb has a dispatch row) and `hook_smoke.sh` (the plugin's edit guard;
the pre-push hook too where pre-commit is installed). CI runs them plus the GitHub Action on every pull request, and
`encoding_smoke.sh`, `doctor_smoke.sh`, `sync_smoke.sh` and `hook_smoke.sh` on Windows (team_smoke runs the encoding test on macOS under non-UTF-8 locales). A release bumps
`__version__` in `src/km/__init__.py`, `version` in `.claude-plugin/plugin.json` and the version in
this README together; merging that to main is the release, CI tags `vX.Y.Z` once main is green.
The smoke tests need Python 3.11+, PyYAML and git: `bash tests/gate_smoke.sh`, `index_smoke.sh`, `team_smoke.sh`, `doctor_smoke.sh`, `sync_smoke.sh`, `list_smoke.sh`, `mounted_pins_smoke.sh` (the mounted-pins workflow and script a team brain gets, against a stand-in GitHub API), `skill_smoke.sh` (the skill's core keeps its size limits, its reference paths resolve, every `/km` verb has a dispatch row) and `hook_smoke.sh` (the plugin's edit guard; the pre-push hook too where pre-commit is installed). CI runs them plus the GitHub Action on every pull request, and `encoding_smoke.sh`, `doctor_smoke.sh`, `sync_smoke.sh` and `hook_smoke.sh` on Windows (team_smoke runs the encoding test on macOS under non-UTF-8 locales). A release bumps `__version__` in `src/km/__init__.py`, `version` in `.claude-plugin/plugin.json` and the version in this README together; merging that to main is the release, CI tags `vX.Y.Z` once main is green.

km started inside [dotclaude](https://github.com/pasrom/dotclaude) and was moved here with its
history.
km started inside [dotclaude](https://github.com/pasrom/dotclaude) and was moved here with its history.

## License

Expand Down
Loading
Loading