From 1523de043b26c34c6cfd5990b228e0aacd4f29b4 Mon Sep 17 00:00:00 2001 From: Roman PASSLER Date: Sat, 7 Nov 2026 23:05:44 +0100 Subject: [PATCH 1/3] docs(skill): the team brain's bump-mounted-pins workflow and its setup setup.md listed the ci and staleness workflows as what km init --team writes, and said nothing of the workflow that moves the pins of mounted brains. It now names main-guard and bump-mounted-pins with its script, and a new section says what bump-mounted-pins does and what the maintainer sets up before the first mount, taken from the workflow's header: the GitHub App (Contents and Actions: read), the environment mounted-pins restricted to main and created before the first run, its secrets CI_APP_CLIENT_ID and CI_APP_PRIVATE_KEY, allowing Actions to create pull requests, "Require approval of the most recent reviewable push", and the optional variable MOUNTED_PINS_RUNNER. The upgrade step says what km upgrade adds to a team brain and to pass on its warning about the environment; the doctor step says the environment note is the maintainer's to fix. Dependabot is named for the km pin and the action pins only; no sentence in the skill or the README said it moves the mounted pins. --- skills/km/reference/setup.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/skills/km/reference/setup.md b/skills/km/reference/setup.md index ffa5b85..499a32a 100644 --- a/skills/km/reference/setup.md +++ b/skills/km/reference/setup.md @@ -6,13 +6,14 @@ - Setup check (`/km doctor`, "check my setup") - Init (`/km init`) - Team brain init (`/km init --team`) +- Mounted pins on a team brain (`bump-mounted-pins`) - Upgrade (`/km upgrade`) ## Running km and keeping the copy current **Running km.** `${CLAUDE_SKILL_DIR}/bin/km` (next to SKILL.md) runs km from this installed plugin, written `km` below; `km --help` lists the commands, and all take `--root DIR`. Brains carry no km code: CI and the pre-commit hook install the km release the brain pins, by its commit (`pasrom/km@ # vX.Y.Z`, so a moved tag cannot change what runs), and so does `km` here: in a brain that pins another version it runs that commit through `uvx`, so results match CI (`KM_LOCAL=1` keeps the plugin's; `init`, `upgrade`, `doctor`, `list` and `sync` always run the plugin's km, so `km upgrade` needs no `KM_LOCAL`). Without uv, offline, for a pin to a movable tag, or when the workflows disagree, it runs the plugin's km and warns; `/km upgrade` the brain then. -**Keeping the copy current.** A copy of the brain goes stale: answers then come from old knowledge and new work starts on an old base. `km sync` fetches the upstream brain (the remote `upstream`, else a fork's parent, else origin) and only ever fast-forwards: the checked-out default branch on a clean tree, a fork's default branch on the host, and the mounted brains to the commit the brain pins (a pin moves only through the host's automation and a reviewed PR). Everything else it only reports, with how far behind. The plugin runs it when a session starts in a brain, and its lines are then in your context. Before answering a question from the brain, run `km sync --quiet --max-age 15m` (nothing happens within 15 minutes of the last run; it prints only what changed or needs attention). Before creating a page or a branch, run `km sync`, so the work starts from the current upstream. When it reports `branch is commits behind upstream ; rebase it when you are ready`, tell the user and offer the rebase; run `git rebase upstream/` (`origin/` without a fork) only on their OK and on a clean tree (`git status --porcelain --ignore-submodules=all` prints nothing), never a reset or a force. A default branch with commits of its own, a dirty tree, a mount with changes of its own or a diverged fork are the user's call too: pass the note on and do nothing else. +**Keeping the copy current.** A copy of the brain goes stale: answers then come from old knowledge and new work starts on an old base. `km sync` fetches the upstream brain (the remote `upstream`, else a fork's parent, else origin) and only ever fast-forwards: the checked-out default branch on a clean tree, a fork's default branch on the host, and the mounted brains to the commit the brain pins (a pin moves only through a reviewed PR, on a team brain usually the one its bump-mounted-pins workflow opens). Everything else it only reports, with how far behind. The plugin runs it when a session starts in a brain, and its lines are then in your context. Before answering a question from the brain, run `km sync --quiet --max-age 15m` (nothing happens within 15 minutes of the last run; it prints only what changed or needs attention). Before creating a page or a branch, run `km sync`, so the work starts from the current upstream. When it reports `branch is commits behind upstream ; rebase it when you are ready`, tell the user and offer the rebase; run `git rebase upstream/` (`origin/` without a fork) only on their OK and on a clean tree (`git status --porcelain --ignore-submodules=all` prints nothing), never a reset or a force. A default branch with commits of its own, a dirty tree, a mount with changes of its own or a diverged fork are the user's call too: pass the note on and do nothing else. ## Setup check (`/km doctor`, "check my setup") @@ -20,7 +21,7 @@ For someone who just set up a computer for the brain, often not a developer. Ans 1. Before km: `git --version`. Without git, nothing else works: say so and stop; the brain's own setup guide (README or CLAUDE.md) says how to install it. 2. `km doctor` in the brain (`--offline` when they say they have no network). Where `km` itself cannot start, its message names uv and the one-line command that installs it (no administrator rights needed): offer to run that command for their system, then run `km doctor` again. km finds uv where the installer puts it, so the app need not be restarted first. -3. Each `FAIL` comes with a fix. Offer to run the ones that change only this computer and are safe (`git submodule update --init`, the `km sync` or `git pull --rebase` it names on a clean tree, `git config --global user.name` after asking for the value, `pre-commit install`); hand over the ones they must do themselves (signing in, `gh auth login`, installing a plugin or adding a marketplace with `/plugin`, asking for access). The re-checkout that fixes CRLF files discards uncommitted changes: run it only on a clean tree and after they agree. A `note` needs nothing; mention it only if it explains something they asked about. +3. Each `FAIL` comes with a fix. Offer to run the ones that change only this computer and are safe (`git submodule update --init`, the `km sync` or `git pull --rebase` it names on a clean tree, `git config --global user.name` after asking for the value, `pre-commit install`); hand over the ones they must do themselves (signing in, `gh auth login`, installing a plugin or adding a marketplace with `/plugin`, asking for access). The re-checkout that fixes CRLF files discards uncommitted changes: run it only on a clean tree and after they agree. A `note` needs nothing; mention it only if it explains something they asked about. A note whose fix is in the repository's settings (such as the environment `mounted-pins`) is the maintainer's, nothing to do on this computer. 4. Run `km doctor` again after fixes and end with what still fails, or that the setup is complete. ## Init (`/km init`) @@ -36,16 +37,20 @@ When the user runs `/km init` (or confirms after "CONVENTIONS.md missing" prompt A **team brain** is a shared, served knowledge base: one maintainer merges, the team reads the repo, everyone else contributes by pull request or promotes from a personal brain. On top of a personal brain it adds CI (validation, a generated `_index.md` Documents list, a served bundle, weekly staleness demotion), domain folders instead of `inbox/`, and the one-merger rules. 1. Ask for: the brain's name (e.g. `QA-Brain`), a one-line description of the team (e.g. `the QA team`), the maintainer's initials, and the top-level folders with a one-line purpose each (suggest `process/`, `projects/`, `reference/` plus the team's own domains). Optionally one or two placement rules that decide which folder a doc goes to; without them each folder's purpose becomes its rule. -2. In the empty repo (or an empty clone of a fresh remote): `km init . --team --name --desc "" --initials --folder process="" --folder ... [--rule "" ...]`. It writes the docs, indexes, `schema.local.yaml`, the ci and staleness workflows, the pre-commit hook, a Dependabot config and a `.gitattributes` for LF, all pinned by commit to this km's release (or `--pin `), and refuses to run if any file it would write already exists, such as a README the host created with the repo: delete that first. +2. In the empty repo (or an empty clone of a fresh remote): `km init . --team --name --desc "" --initials --folder process="" --folder ... [--rule "" ...]`. It writes the docs, indexes, `schema.local.yaml`, the ci, staleness, main-guard and bump-mounted-pins workflows (the last with its script `.github/scripts/bump-mounted-pins.py`), the pre-commit hook, a Dependabot config and a `.gitattributes` for LF, all pinned by commit to this km's release (or `--pin `), and refuses to run if any file it would write already exists, such as a README the host created with the repo: delete that first. 3. `git add -A`, then `km gen-index` (it reads tracked files only, so add first; it fills the root and folder lists), `git add -A` and `km validate`: expect **0 errors / 0 warnings**. 4. Commit: `chore: initialize (team served knowledge base)`. -5. Tell the user what km does not do: create the repo (private), push `main` (after `pre-commit install` the pre-push hook refuses a push to `main`: the first push of the fresh brain takes `--no-verify`, once), give the team **read** access and the maintainer write, and ask members to add the brain to their personal brain with `/km brain add `. Branch protection on a private repo needs a paid GitHub plan; without it the one-merger rule is held by the permission model alone. Members with read access contribute by fork PR, and GitHub does not run Actions on fork PRs of a private repo until "Run workflows from fork pull requests" is enabled (repository or organization settings, Actions): without it those PRs get no CI. Dependabot then proposes a PR whenever a newer km is released. +5. Tell the user what km does not do: create the repo (private), push `main` (after `pre-commit install` the pre-push hook refuses a push to `main`: the first push of the fresh brain takes `--no-verify`, once), give the team **read** access and the maintainer write, and ask members to add the brain to their personal brain with `/km brain add `. Branch protection on a private repo needs a paid GitHub plan; without it the one-merger rule is held by the permission model alone. Members with read access contribute by fork PR, and GitHub does not run Actions on fork PRs of a private repo until "Run workflows from fork pull requests" is enabled (repository or organization settings, Actions): without it those PRs get no CI. Dependabot then proposes a PR whenever a newer km or another action is released; mounted brains move through bump-mounted-pins ("Mounted pins on a team brain"). + +## Mounted pins on a team brain (`bump-mounted-pins`) + +The workflow moves the pins of mounted brains (submodules) by pull request: on weekday mornings it opens or updates `chore/bump-mounted-pins` with each mount at the newest commit of its tracked branch whose CI did not fail. Without a mount it stops after reading `.gitmodules`; with one it runs, but moves nothing (and warns) until the maintainer has set it up once, as the header of `.github/workflows/bump-mounted-pins.yml` describes (read it there for the details): a GitHub App with Contents: read and Actions: read on the mounted repositories, the environment `mounted-pins` restricted to `main` with the secrets `CI_APP_CLIENT_ID` and `CI_APP_PRIVATE_KEY`, Actions allowed to create pull requests, and "Require approval of the most recent reviewable push" on `main`; optionally the repository variable `MOUNTED_PINS_RUNNER` for another runner. Create the environment BEFORE the workflow first runs on a brain with mounts: GitHub otherwise creates it on that run, open to every branch. ## Upgrade (`/km upgrade`) Move a brain to the km you run, without touching repo-specific files: -1. `km upgrade` moves the brain's km pins to this km's release commit (`--pin ` for another), turns a pin to a movable tag into a commit pin, and on a brain from the copy-in days drops the copied `scripts/*.py` and `schema.base.yaml` and rewrites its hook and km workflows. It refuses to move a pin to a newer km back (`--force` overrides). +1. `km upgrade` moves the brain's km pins to this km's release commit (`--pin ` for another), turns a pin to a movable tag into a commit pin, and on a brain from the copy-in days drops the copied `scripts/*.py` and `schema.base.yaml` and rewrites its hook and km workflows. On a team brain it adds what is missing of the main-guard and bump-mounted-pins workflows (with the script) and `workflow_dispatch` in the km ci workflow, and replaces a bump-mounted-pins copy that is still a version km shipped; one the brain edited stays, with a note. Pass on its warning when it adds bump-mounted-pins to a brain that already has mounts ("Mounted pins on a team brain"). It refuses to move a pin to a newer km back (`--force` overrides). 2. Show `git diff`. `CONVENTIONS.md`, `CLAUDE.md`, `README.md` and every `_index.md` are repo-owned and stay (in `schema.local.yaml` only an obsolete `team_brain:` line goes); point out prose that still mentions `scripts/`. 3. Run `km validate` (expect **0 errors**); on a team brain also `km gen-index` (a newer km may list more, e.g. subfolder indexes) and stage what it changed. 4. Commit: `chore(km): upgrade to km ` From 9ee69656717624db52008fc06c2ca18417bebc54 Mon Sep 17 00:00:00 2001 From: Roman PASSLER Date: Sat, 7 Nov 2026 23:06:31 +0100 Subject: [PATCH 2/3] test(skill): size limits in bytes and characters, not lines With one paragraph per line, a line count measures paragraphs, not size. Measured on the hard-wrapped files (4777eb7), the reference files averaged 69 bytes a line and SKILL.md 76: - The core's 500-line limit was about 38,000 bytes, three times CORE_BYTES (12,000), so it never bound; it goes, CORE_BYTES stays. - The contents-list rule for reference files over 100 lines split them at 3,433 to 3,702 bytes (under) against 8,064 to 13,599 (over). CONTENTS_BYTES = 6,000 keeps that split with room on both sides. New: no line in a Markdown file under skills/ may exceed 1,900 characters, since the Read tool cuts a line after 2,000 and the rest of the paragraph is never seen. Characters, as the tool counts them. The longest line today is 1,875 (queue.md, Open PRs step). skill_smoke shows each check bites: a core padded past CORE_BYTES, brains.md without its contents list, and a 1,901-character line fail, while a 1,000-character line of two-byte characters passes. Disabling each check, or counting the line in bytes, fails the smoke test. --- README.md | 2 +- tests/skill_inventory.py | 34 ++++++++++++++++++++++++++-------- tests/skill_smoke.sh | 16 ++++++++++++++-- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d9ace9b..27bb2f6 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/tests/skill_inventory.py b/tests/skill_inventory.py index b6b2821..2f6b598 100644 --- a/tests/skill_inventory.py +++ b/tests/skill_inventory.py @@ -1,9 +1,10 @@ """The skill's core keeps its limits, and its dispatch table and reference files stay consistent. -The core SKILL.md: 500 lines and CORE_BYTES, a description of at most 1024 characters, every -reference file named by its full `${CLAUDE_SKILL_DIR}/reference/` path and every such path -resolving, a contents list in a reference file over 100 lines, and a row in the dispatch table (the verb -opening a code span or a quoted phrase) for every `/km ` the README or the skill's own files mention, `help` included. +The core SKILL.md: CORE_BYTES, a description of at most 1024 characters, every reference file +named by its full `${CLAUDE_SKILL_DIR}/reference/` path and every such path resolving, a contents +list in a reference file over CONTENTS_BYTES, no line over MAX_LINE characters in any Markdown file +under skills/, and a row in the dispatch table (the verb opening a code span or a quoted phrase) for +every `/km ` the README or the skill's own files mention, `help` included. python3 tests/skill_inventory.py [--skill skills/km] """ @@ -21,6 +22,19 @@ # table, so 12,000 bytes is about 4,780 tokens, 4 percent under the budget. Re-measure the ratio if # the file's character changes a lot. CORE_BYTES = 12000 +# The limits below were line counts (500 for the core, a contents list over 100 lines), which meant +# something while the Markdown was hard-wrapped near 100 columns; with one paragraph per line a file +# has as many lines as paragraphs. Measured on the hard-wrapped files (4777eb7), the reference files +# averaged 69 bytes a line and SKILL.md 76, so 500 lines were about 38,000 bytes, three times +# CORE_BYTES: the core's line limit never bound and goes without a replacement. The 100-line rule +# split the reference files into github-reads, pins-and-cites and queue (3,433 to 3,702 bytes, +# under) and setup, validate-and-gate, review-and-pr and brains (8,064 to 13,599 bytes, over); 6,000 +# bytes keeps that split with room on both sides. +CONTENTS_BYTES = 6000 +# Claude's Read tool cuts a line after 2,000 characters, so the rest of a longer paragraph is never +# seen. Characters, not bytes, since that is what it counts; 100 under it, so an edit that nears the +# cut fails here first. All of skills/, not only this skill's files. +MAX_LINE = 1900 def main() -> int: @@ -32,8 +46,6 @@ def main() -> int: files = {p.name if p.parent == skill else f"reference/{p.name}": p.read_text(encoding="utf-8") for p in [skill / "SKILL.md", *refs]} core = files["SKILL.md"] problems = [] - if len(core.splitlines()) > 500: - problems.append(f"SKILL.md has {len(core.splitlines())} lines, limit 500") if (n := len(core.encode("utf-8"))) > CORE_BYTES: problems.append(f"SKILL.md is {n} bytes, limit {CORE_BYTES}: a byte proxy for the 5,000 tokens a compaction keeps of a " f"skill (calibrated 2026-11-07 with a one-turn haiku differential, see CORE_BYTES); move text to a reference file") @@ -42,10 +54,16 @@ def main() -> int: if not desc or len(desc) > 1024: problems.append(f"description is {len(desc)} chars (limit 1024)") for ref in refs: + text = files[f"reference/{ref.name}"] if f"`{REF}/{ref.name}`" not in core: problems.append(f"SKILL.md never names `{REF}/{ref.name}`") - if len(files[f"reference/{ref.name}"].splitlines()) > 100 and "## Contents" not in files[f"reference/{ref.name}"]: - problems.append(f"{ref.name} is over 100 lines and has no '## Contents' list") + if (n := len(text.encode("utf-8"))) > CONTENTS_BYTES and not re.search(r"(?m)^## Contents$", text): + problems.append(f"{ref.name} is {n} bytes, over {CONTENTS_BYTES}, and has no '## Contents' list") + for md in sorted(skill.parent.rglob("*.md")): + for no, line in enumerate(md.read_bytes().decode("utf-8").split("\n"), 1): # at \n only, as the Read tool splits + if len(line) > MAX_LINE: + problems.append(f"{md.relative_to(skill.parent.parent)}:{no} is {len(line)} characters, limit {MAX_LINE}: " + f"the Read tool cuts lines at 2000; split the paragraph") for name, text in files.items(): for hit in re.findall(r"\$\{CLAUDE_SKILL_DIR\}/reference/([\w-]+\.md)", text): if not (skill / "reference" / hit).is_file(): diff --git a/tests/skill_smoke.sh b/tests/skill_smoke.sh index ff896ac..468a58e 100644 --- a/tests/skill_smoke.sh +++ b/tests/skill_smoke.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Smoke test for the skill: the core keeps its limits and its pointers to the reference files resolve -# (tests/skill_inventory.py). +# Smoke test for the skill: the core keeps its limits, no Markdown line is cut by the Read tool, and +# its pointers to the reference files resolve (tests/skill_inventory.py). set -u source "$(dirname "$0")/lib.sh" cd "$REPO" @@ -13,4 +13,16 @@ grep -v '^| `review ' skills/km/SKILL.md > "$C/skills/km/SKILL.md" out="$(kmpy tests/skill_inventory.py --skill "$C/skills/km" 2>&1)" { grep -q "no row for /km approve" <<< "$out" && grep -q "no row for /km review" <<< "$out"; } \ && ok "a verb whose row is gone fails, also one the README never names" || no "missing dispatch row ($out)" +# the size checks bite: a core over CORE_BYTES, a reference file over CONTENTS_BYTES without its +# contents list, and a line over MAX_LINE characters (a multi-byte one, so characters are counted) +cp skills/km/SKILL.md "$C/skills/km/SKILL.md" && printf 'x%.0s' $(seq 12001) >> "$C/skills/km/SKILL.md" +grep -v '^## Contents' skills/km/reference/brains.md > "$C/skills/km/reference/brains.md" +{ printf '\n'; printf 'x%.0s' $(seq 1901); printf '\n'; } >> "$C/skills/km/reference/queue.md" +{ printf '\n'; printf 'ΓΌ%.0s' $(seq 1000); printf '\n'; } >> "$C/skills/km/reference/github-reads.md" +out="$(kmpy tests/skill_inventory.py --skill "$C/skills/km" 2>&1)" +grep -q "SKILL.md is [0-9]* bytes, limit" <<< "$out" && ok "a core over CORE_BYTES fails" || no "core size ($out)" +grep -q "brains.md is [0-9]* bytes, over [0-9]*, and has no '## Contents' list" <<< "$out" \ + && ok "a large reference file without a contents list fails" || no "contents list ($out)" +{ grep -q "queue.md:[0-9]* is 1901 characters" <<< "$out" && ! grep -q "github-reads.md:[0-9]* is" <<< "$out"; } \ + && ok "a line over the limit fails, counted in characters" || no "line length ($out)" summary skill_smoke From 023dc5a7a97c0832f46ea9b4500ba3ad1abdca2a Mon Sep 17 00:00:00 2001 From: Roman PASSLER Date: Sat, 7 Nov 2026 23:06:44 +0100 Subject: [PATCH 3/3] chore(release): km 1.10.0 A team brain gets bump-mounted-pins, a workflow with its script that moves the pins of the mounted brains by pull request in place of Dependabot's submodule updates, and km doctor notes its environment; km upgrade adds both and keeps a shipped copy current. The team ci workflow also runs on workflow_dispatch, so a pull request a workflow opens gets its check, and km upgrade adds the trigger. The skill's setup reference describes the new workflow and its maintainer setup, km's Markdown is one paragraph per line, and the skill inventory measures size in bytes and line length in characters instead of lines, without the retired paragraph check. Without the bump no brain or plugin install would see these. --- .claude-plugin/plugin.json | 2 +- README.md | 6 +++--- src/km/__init__.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index f31306e..c238874 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -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" diff --git a/README.md b/README.md index 27bb2f6..39329ed 100644 --- a/README.md +++ b/README.md @@ -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@ # v1.9.0`, or run it without installing: `uvx --from git+https://github.com/pasrom/km@ 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@ # v1.10.0`, or run it without installing: `uvx --from git+https://github.com/pasrom/km@ 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@ # v1.9.0, installs the km CLI at that commit +- uses: pasrom/km@ # v1.10.0, installs the km CLI at that commit - run: km validate - run: km gen-index --check ``` @@ -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: # 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: # 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. diff --git a/src/km/__init__.py b/src/km/__init__.py index d5a7da6..95afa36 100644 --- a/src/km/__init__.py +++ b/src/km/__init__.py @@ -1,4 +1,4 @@ """km: knowledge management for markdown knowledge bases.""" -__version__ = "1.9.0" +__version__ = "1.10.0" KM_REF = f"v{__version__}" # this km's release tag; brains pin the commit it points at