Repository navigation
refactor(skill): a small core with a dispatch table, flows in reference files, hooks for the hard rules - #41
Merged
Conversation
pasrom
force-pushed
the
refactor/skill-core-and-references
branch
from
November 7, 2026 02:38
c950f45 to
8de2632
Compare
…iles SKILL.md keeps the core (running km, sync, GitHub access, routing, citation, consuming vs. authoring, search, writing content, rules) and names the seven reference files under skills/km/reference/ that now hold the flows: setup, brains, github-reads, validate-and-gate, review-and-pr, queue, pins-and-cites. The text moved verbatim; only cross-file pointers changed, each listed in tests/skill_inventory.py, which checks against the frozen pre-split SKILL.md (tests/fixtures/skill-baseline.md) that every paragraph lands in exactly one file and that every heading, code span and rule sentence survives. tests/skill_smoke.sh runs it in CI.
The core now opens with the rules, then a table that maps every /km verb and request shape (help included) to the reference files its flow needs, with the note that after a compaction only SKILL.md is re-attached, so a flow's files are read again before it goes on. The long 'Running km' and 'Keeping the copy current' paragraphs moved verbatim to setup.md; the core keeps the sync rule in four lines. The description is shorter, one trigger per activity, qualified where a bare word would fire on unrelated work (a brain doc or brain PR), and the rules moved to the top of the file. Measured with a one-turn haiku call: the core is 4.6k tokens (the whole old SKILL.md was 21.5k, of which a compaction keeps 5k); the inventory test gains a check that every /km verb of the README and the baseline has a row in the table.
…rain's main Two of the skill's hard rules become hooks, so they hold after a compaction and for a Claude that never read the flow: - hooks/guard-brain-edit.sh, a PreToolUse hook on Edit, Write, MultiEdit and NotebookEdit: the file's directory lies in a git submodule whose checkout and superproject are both brains (CONVENTIONS.md plus a schema file), found through git rev-parse --show-superproject-working-tree, not a path pattern; a mount that is not checked out yet (an empty gitlink directory, where a file would block the checkout) is refused too. It exits 2 with the fix flows named; everything else exits 0 at once. Bash builtins plus one git call (a second one next to an empty mount), no jq: the path comes out of the JSON with a bash regex, and a Windows path arrives as C:\\... and is read as C:/..., so it runs in Git Bash too. - a pre-push hook in a team brain's pre-commit config (templates/team/ no-push-hook.yaml, a local hook on PRE_COMMIT_REMOTE_BRANCH) that refuses a push to refs/heads/main, next to the no-commit-to-branch hook; the config now sets default_install_hook_types so pre-commit install writes both git hooks, and default_stages so the hooks without stages keep running at commit only. pre-commit inspects the first ref of a push only and skips a push whose commits the remote already has, so git push origin main is refused while a push of several refs is not; the template and the README say so. km init --team writes it, km upgrade adds whichever of the hooks, the install types (appended to a one-line list) and default_stages a team brain lacks and says to run pre-commit install again, km doctor checks one git hook per installed type. A personal brain gets none of it; a throwaway clone has no hooks, there the prose rule holds by itself. Force pushes without a lease stay a prose rule. tests/hook_smoke.sh pipes the hook JSON of each tool in and out of a real mount, an empty mount, a shared resource, a standalone clone and a plain repository, and pushes through pre-commit where it is installed (CI installs it). It runs on the Windows job too, where the C:\\... case runs.
pasrom
force-pushed
the
refactor/skill-core-and-references
branch
from
November 7, 2026 05:29
8de2632 to
601b38e
Compare
The prose part is deliberately small, since the point of this change is
that nothing gets lost: the two wall-of-text sections (cited files, index
completeness) are split into paragraphs at their sentence boundaries with
the wording untouched, brains.md, github-reads.md and pins-and-cites.md
get the top-level headings they lacked, and the core drops a rule bullet
that repeated the last sentence of 'GitHub access'. The inventory check
stays strict for every other paragraph; the two re-wrapped ones are
listed in REWRAPPED and must still appear once with whitespace
normalised, so a changed word fails the test (checked).
This commit also carries what the three review passes changed in the
core and the pointers, folded here rather than into the reorder commit:
full ${CLAUDE_SKILL_DIR}/reference/ paths in every dispatch row and
merged rows that read the same files; a row for @<name>/@ALL naming
brains.md and pins-and-cites.md on the review row; the sync caveat
(never a reset, a force or a moved mount) back in the core; the confirm
bullet of the reorder commit dropped again, the baseline rule stays under Writing content; the hook rule worded as covering
the file-edit tools; the description keeps Meeting-Notiz, ADR and
freigeben; setup.md names ${CLAUDE_SKILL_DIR}/bin/km and the team init
says the first push of a fresh brain takes --no-verify once; two
reference pointers name their file. Each pointer change is an entry of
REWRITES. Core: 4.6k tokens.
The skill's core and reference files, the plugin's edit guard and the team brain's pre-push hook ship with this version; without the bump no brain or plugin install would see them.
Thirteen cases for claude plugin eval, each scaffolding a brain with km init from this checkout (evals/lib/brain.sh reuses tests/lib.sh and its fake km remote, since km init pins a release by its commit): a lookup question, "fix this in @peer" on a mounted brain, /km queue, a C review that must not fire the skill, and one case per remaining row of the dispatch table (save content, help, doctor, sync, lint, review a doc, pr, brain list + remove, repin). A case whose row names a reference file has a reads-reference grader; every case has a grader on the flow's key step (the km command it runs, the file it writes, the line it answers), which the pre-split skill passes too. Graders are tool_used and regex, plus one llm grader. Run from the plugin root with --scaffold --no-publish --ablation none --allow-tools Bash Write Edit (evals/README.md); evals/results/ is ignored.
pasrom
force-pushed
the
refactor/skill-core-and-references
branch
from
November 7, 2026 06:08
601b38e to
99e67d5
Compare
pasrom
added a commit
that referenced
this pull request
Nov 7, 2026
The check that every paragraph of the frozen pre-split SKILL.md still appears in the split skill guarded one refactor (#41). Since then it makes every ordinary edit of the skill register a rewrite entry. It goes with its fixture; the structural checks stay: 500 lines, CORE_BYTES, the description length, reference paths that resolve, contents lists, and a dispatch row for every /km verb. The verb check now reads the skill's own files instead of the baseline, so approve, doctor, lint, pr and review, which the README does not name, stay covered. A row counts only where the verb opens a code span or a quoted phrase in the table, not where it is a word in another row's text, and skill_smoke shows the check fails without the review/approve row.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
skills/km/SKILL.mdhad grown to 774 lines (57 KB, 21.5k tokens as the API counts them). Three things followed from that:## Rulesat line 769, the hard-rule blocks ofbrain fixandcontributein the middle), the last place a truncated skill keeps.Design
SKILL.mdkeeps what every flow shares (rules, routing, citation, consuming vs. authoring, search, writing content, the sync rule, GitHub access) and a dispatch table that maps every/kmverb and request shape to the files its flow needs, all of them (brain fix->brains.md+review-and-pr.md+validate-and-gate.md;queue->queue.md+github-reads.md+review-and-pr.md). The flows live inskills/km/reference/*.md, one level deep, each with a contents list.helpis defined in the table. The core tells Claude that after a compaction onlySKILL.mdis re-attached, so a flow's files are read again.## Ruleslist moved to the top and gained the hard rules that were buried in the flows (no push to a brain's default branch, no force push without a lease, no edit of a mounted brain,km approveas its own commit, a doc withcontribution:is edited on its PR head, no parent-repo private content in a brain PR).when_to_use.hooks/guard-brain-edit.sh, a PreToolUse hook on Edit, Write, MultiEdit and NotebookEdit: refuses (exit 2, reason on stderr) an edit of a file whose directory lies in a git submodule when both the submodule and its superproject are brains (CONVENTIONS.mdplus a schema file). Detection throughgit rev-parse --show-superproject-working-tree, not a path pattern; a mount that is not checked out yet (an empty gitlink directory, where a file would block the checkout) is refused too. Bash builtins plus one git call, no jq: the path comes out of the JSON with a bash regex; a Windows path arrives asC:\\Users\\...and is read asC:/Users/..., so it runs in Git Bash (the Windows CI job runs that case). Everything else exits 0 at once (no path in the input, no git, a plain repository, a shared resource, a brain mounted in a repository that is no brain, a standalone clone, the brain's ownbrains/folder).templates/team/no-push-hook.yaml, arepo: localhook onPRE_COMMIT_REMOTE_BRANCH) that refuses a push torefs/heads/main, next to the existingno-commit-to-branch; the config now setsdefault_install_hook_types: [pre-commit, pre-push]sopre-commit installwrites both git hooks, anddefault_stages: [pre-commit]so the hooks withoutstages:(no-commit-to-branch,km-validate) keep running at commit only.km init --teamwrites it;km upgradeadds whichever of the two hooks and the install types a team brain lacks and says to runpre-commit installagain;km doctorfails when the config names a pre-push hook git does not have. A personal brain gets none of it.--no-verifystill pushes (a guard against mistakes, not a boundary, likeno-commit-to-branch); force pushes without a lease stay a prose rule; a brain mounted in a non-brain repository is not guarded.plugin.json,__init__.py, README together), since without it no brain or plugin install would see the change.Evidence
tests/skill_inventory.pyruns in CI (tests/skill_smoke.sh) against the frozen pre-splitSKILL.md(tests/fixtures/skill-baseline.md): every baseline paragraph (blank-line separated, a code fence as one) must be a substring of exactly one new file after the 20 enumerated pointer rewrites inREWRITES; every heading, inline code span and sentence with a hard-rule word must appear in the union. 116 paragraphs, 753 atoms, 0 problems. Mutation-checked: a dropped paragraph, a duplicated one, a stale rewrite, a lost code span and a changed word each fail it.git diff --color-moved=zebra: 1032 moved lines, 121 plain, all of them pointer rewrites, contents lists and the reference list.SKILL.md21,502 tokens; new core 4,593 (151 lines, 11,524 bytes; it was 4,194 before the two review passes added rows and a clause, see open questions); reference files 1,273 to 5,457 each. CI guards the core with a byte limit of 12,000 intests/skill_inventory.py(CORE_BYTES), a proxy for the 5,000-token compaction budget calibrated from that measurement (2.5 bytes per token, so about 4,780 tokens at the limit; padding the core past it fails the test, checked)./km <verb>in the README and the baseline has a row in the table and thathelpis defined.REWRAPPEDand requires them once with whitespace normalised; a changed word still fails (checked). Every other paragraph stays under the strict check, so the relaxation is exactly two entries the reviewer can read.tests/hook_smoke.shruns on the Windows job too (CI installs pre-commit for the push cases).exit 0instead ofexit 2fails five cases;refs/heads/masterinstead ofmainin the template fails the push test; droppingdefault_stagesmakes a hook withoutstages:run at push (a test case asserts exactly that). The hook costs 19 ms per call on the common path (2 execs: cat and git).claude plugin eval, Claude Code 2.1.319, fixture brain viakm initin the scaffold; the first four cases with read-only tools, the nine dispatch cases with--allow-tools Bash Write Editin the sandbox). Every row of the dispatch table has a case; each dispatch case has areads-referencegrader (the flow's reference file was read) and a behaviour grader on the flow's key step (thekmcommand run, the file written, the line answered) that the old plugin can pass too. Scores are the mean over runs;n/amarks a grader the old plugin cannot pass because it has no reference files./km queuequeue.mdinbox/<slug>.mdwithstatus: draftandtype: decisionhelpqueueandbrain fixdoctor("prüfe meine Einrichtung")km doctorsynckm synclintkm validatereview <doc>km validateon the doc (step 1)prgit log/diff/merge-base) or checksgh auth statusbrain list+brain removepeer, thengit submodule deinit/git rmrepinkm repin. Cause: the core's confirm bullet added in the review pass, which the pre-split skill never had at that level; it is dropped again, the baseline's "Always confirm before writing" stays under Writing content and the flows keep their own OK steps. Re-run 3/3 with the bullet scoped to writes the user did not ask for; the drop is the simpler form of the same fix and was not re-run for budget)km repinCost: first four cases $3.22 new (24 runs, both arms) + $2.66 old (12 runs) + $2.04 final re-run; the nine dispatch cases $7.90 new (27 runs) + $3.52 old (9 runs) + $0.85 for the repin re-run (3 runs), plus $0.82 lost to an attempt that the account's usage limit killed before any run was graded: $13.09 for this follow-up against a $12 cap (the overrun is the lost attempt plus the re-run). The old-plugin runs of the nine dispatch cases were cut to one run per case for that budget; three would have cost about $7.90 more. The suite is in
evals/(seeevals/README.md);evals/results/is ignored.Review passes
tests/lib.shinstead of re-implementingkmand the fake km remote; one git-options array intests/lib.sh; the edit guard uses bash builtins and one git call (26 ms to 19 ms per call on the common path, 6 execs to 2) and lost its test seam;init.pykeeps one table of the team hooks, parses the config once and tests the install types by meaning;upgradederives its messages from that table;doctorshares one "installed" test and checks one git hook per type indefault_install_hook_types; the inventory checker dropped three empty exception tables; the dispatch table uses full paths throughout and merged rows that read the same files. Left out: retiring the containment test after merge (the brief asked for it to stay; see open questions); a sharedhooks/lib.shfor the brain predicate (two standalone scripts); a filesystem pre-check before calling git in the guard (changes the contract from "ask git"); per-step file loading for the three-file rows (the plan wants every file a flow needs named in its row).git push origin mainin the template and README;default_stages: [pre-commit]is written with the install types so hooks withoutstages:(no-commit-to-branch, km-validate) do not run at push; a doctor regression that took another tool's executable hook for a non-executable one of pre-commit's;default_install_hook_typesparsed in block and quoted form; the edit guard refuses a Write into a mount that is not checked out yet (an empty gitlink directory, where the file would block the checkout);with_team_hooksappendspre-pushto a one-line install types list instead of giving up; the core says the hook covers the file-edit tools (a shell edit is not caught) and points at the heading that exists insetup.md; the eval fixture's fake km remote lives undermktemp, not next to the workspace.evals/lib/brain.sh; the save-content grader became two plain ones; the README shrank; the core's confirm bullet went (see the repin row). Left out, since each would need paid re-runs: lowermax_turns, empty brains for the cases that need no doc, narrower graders forpr(git log/diffonly) andbrains-list-remove(the list grader'speeralso appears in the prompt), prompt wording that tellsdoctor/sync/prto run the command once and stop.reads-referencegrader (helpandsave contentname only the core).git ls-files --stage -- <dir>is a prefix pathspec, so the empty-mount check refused a write into the brain's ownbrains/folder (now the entry's path must equal the folder; four cases);default_stagesis now written whenever a hook is added and the config sets none, not only with the install types (a config that already installs pre-push would otherwise runno-commit-to-branchat push); the question row namesbrains.mdfor@<name>/@all(shared resources,@allsync live there) and the review rowpins-and-cites.md; the sync caveat ("never a reset, a force or a moved mount") is back in the core; the "confirm before every write" bullet, which contradicted the orderedkm syncand later made an explicit/km repinstop for an OK, is dropped again (the baseline rule stays under Writing content); the README carries both pre-push gaps and the pre-commit version it needs; the/km queuecase drops its unpassable skill-fired grader;setup.mdnames${CLAUDE_SKILL_DIR}/bin/km; the description keeps Meeting-Notiz, ADR and freigeben;github-reads.mdandpins-and-cites.mdget the headings they lacked; team init says the first push of a fresh brain takes--no-verifyonce. Checked and found right by that reviewer: every baseline rule survives (hard rules of brain fix and contribute included), every cross-file pointer names an existing heading, every verb has a row, the hook uses only builtins, git, sed and cut and exits 0 on every input outside a mount, no non-public name anywhere in the diff.Housekeeping for the maintainer
During the code-review pass, a reviewer agent's experiment ran in the wrong directory and pushed two stray refs to this repository:
refs/heads/featureandrefs/tags/v1, both at an interim commit of this branch (ba9f75e). No workflow ran on them (CI triggers onmainand pull requests only). Deleting refs on the remote is outside what the agent may do, so please rungit push origin --delete feature v1(andgit tag -d v1in the local worktree).Open questions
${CLAUDE_SKILL_DIR}paths in every row) and under the 5,000 a compaction keeps. If you would rather have the margin, the rows for@<name>/@allandreviewcould name their second file in the flow's own text instead.hooks/session-start.sh). A brain mounted in a project that is no brain is therefore not guarded; the alternative (guard every mounted brain) would also block an unrelated repository that happens to be a submodule.repo: localbash hook rather than a hook id in km's own.pre-commit-hooks.yaml, so a brain pinned to an older km keeps a valid config andkm init --pin <older>still works; the cost is that it needsbashon the PATH where git runs hooks (Git for Windows provides it).tests/fixtures/skill-baseline.mdfreezes the pre-split text (57 KB) so the inventory test runs on a shallow CI checkout. Two reviewers asked whether the paragraph check should stay a permanent gate: every later edit of the reference prose needs aREWRITESorREWRAPPEDentry. It stays because the brief asked for it; it can be retired after merge, keeping only the structural checks (limits, pointers, dispatch rows), once the frozen copy has done its job.