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
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "km",
"version": "1.9.0",
"version": "1.10.0",
"description": "Knowledge management for markdown knowledge bases: answer from it with sources, save notes, decisions and transcripts with validated frontmatter, lint, mount peer brains, contribute to a team brain by PR, and set up a shared team brain with CI (/km init --team).",
"author": {
"name": "Roman Passler"
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,14 @@ km list [PATH...] [filters] docs from their frontmatter: --status, --signoff, --

`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`.
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.10.0` shows it), not by the tag: `pip install git+https://github.com/pasrom/km@<commit> # v1.10.0`, or run it without installing: `uvx --from git+https://github.com/pasrom/km@<commit> km validate # v1.10.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:

```yaml
- uses: pasrom/km@<commit> # v1.9.0, installs the km CLI at that commit
- uses: pasrom/km@<commit> # v1.10.0, installs the km CLI at that commit
- run: km validate
- run: km gen-index --check
```
Expand All @@ -80,7 +80,7 @@ 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.
The pre-commit hook works the same way (`repo: https://github.com/pasrom/km`, `rev: <commit> # frozen: v1.10.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.

Expand All @@ -100,7 +100,7 @@ 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 limit, no Markdown line under `skills/` is long enough for the Read tool to cut, 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.

Expand Down
Loading
Loading