39 Commits
Author SHA1 Message Date
Kevin Veen-BirkenbachandClaude Opus 5.5 32f58712e5 fix(kvb-git-close-issues): drop false references found on a real PR
🧪 Test / 🧪 Lock + lint (push) Waiting to run
A read-only run of issues.py on infinito-nexus/core#1326 listed #0 and #1 from CI row names like WordPress#0 and docs#1, reported #1324 twice (plain and as infinito-nexus/core#1324 from the linked issue), and offered the pull request's own number. A bare # now needs no word character or slash before it and a number from 1, the required --repo maps the pull request's own repository to the plain form and merges the sources, and the pull request's number is skipped; the skill passes --repo and reads number. On #1326 only #1324 closing remains.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:57:08 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 6865ae718a feat(kvb-git-commit-next): stage a file by line with stage_hunks.py
stage_hunks.py takes git diff -U0 of one file, keeps the added lines of an add-only hunk that match a regex and every other hunk whole when one of its changed lines matches, and applies the result to the index with git apply --cached --unidiff-zero --recount, or prints it with --dry-run; exit 1 when nothing matches. Step 4 of kvb-git-commit-next names it for a file that carries two bundles, which replaces the hand-written partial patches needed five times on 2026-11-03. tests/test_stage_hunks.py covers matching, the unstaged rest and the exit codes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:57:08 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 3e2f7a7ddb fix(transcend): skip repository copies in scratchpads
A scan on the real host offered 4276 candidates, 4253 of them files of two repository copies in one peer's scratchpad. scratch_candidates now walks with os.walk, skips any directory holding .git, roles, plugins, pyproject.toml, package.json or a Makefile, and descends at most one level below a scratchpad; the real scan drops to 59 candidates. test_transcend adds a nested helper that stays, a deeper one and a repository copy that drop out (negative control against the unfiltered scan fails).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:57:08 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 194f5e28bd docs(readme): say what each skill category means
The Naming section now names the meaning of every category (think without a file, create an artifact, code, ci, git, check with findings, org, skill, coach for the operator), so a new skill is placed by meaning instead of by the words of the request; two names had to be corrected on 2026-11-03.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:57:08 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 fc7d238d06 feat(budget): count the skill listing budget with make budget
scripts/budget.py holds the listing limits (1500 words in total, 40 per skill) and the one way to count a description, prints the total and the longest descriptions, reads the staged versions with --index, and exits 1 over budget. make budget [INDEX=1] runs it; test_lock.py takes its limits and counting from the script instead of its own copy. It replaces the one-liner that was run by hand a dozen times on 2026-11-03.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:57:08 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 66e21dcd5c fix(council): let the Claude Code session id win over --session
A claim made with --session <name> stored the name, but the PreToolUse gate compares the hook payload's session_id, so the gate refused every edit of the claiming session and a later release without the flag exited 3. caller() now takes CLAUDE_CODE_SESSION_ID whenever it is set and falls back to --session only for other runtimes. test_council covers a claim and release with --session under a Claude Code session (negative control against the old caller fails). Claims made with --session before this fix go stale after an hour without a heartbeat and can then be taken over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:57:08 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 e2157998d7 fix(update): pass the source before --agent
Every scheduled Update run since 2026-10-31 failed with 'Missing required argument: source': --agent takes several values and swallowed the source that followed it. update.sh now passes the source first. test_update.py had pinned the source as the last argument and so encoded the bug; it now requires the source right after add and claude-code as the only agent (negative control against the old order fails).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:57:07 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 d9ea175a17 test(lock): raise the skill listing budget to 1500 words
🧪 Test / 🧪 Lock + lint (push) Waiting to run
Every new skill had to trim the trigger phrases of others to stay under 1300 words; the operator raised the total to 1500. The per-skill limit of 40 words stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:51:46 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 b67d9f233d feat(kvb-git-update-branches): merge main into open PRs and branches
branches.py lists every open branch of the remote with its lag behind the base and whether it belongs to the operator: a pull request by the operator's login, or a branch whose own commits all carry the operator's email; merged branches drop out and fork pull requests are marked. The skill merges the base into the operator's own branches directly and into others only after consent through kvb-think-listen, never rebases, aborts on a conflict for kvb-git-merge-fix, and hands every push and gh pr update-branch to kvb-org-operator. tests/test_update_branches.py builds a bare origin with own, foreign, merged, PR and fork cases. Operator order 2026-11-03; three descriptions trimmed for the listing budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:51:46 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 608da13c0f docs(kvb-dream): name kvb-transcend in the optimize step
kvb-optimize already runs kvb-transcend as its second skill; step 7 of kvb-dream now says so, since the operator asked that dreaming carries the sessions' scripts, tests and skills into their repositories.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:51:45 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 c2ab3f39cf feat(kvb-git-close-issues): close the issues a pull request fixes
issues.py reads gh pr view --json from stdin and lists every issue the pull request references: closing keywords and mentions in the body and the commits (own repo, owner/repo#n and issue URLs), a trailing number in the branch name and the issues GitHub already links. The skill verifies each one against the diff and its test or CI evidence, asks the operator, then closes a fix that reached the default branch with a comment naming the pull request and commit, or adds Closes lines to the body of a pull request that is still open or merged elsewhere. tests/test_close_issues.py covers every reference kind. Operator order 2026-11-03; two descriptions trimmed for the listing budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:51:45 +01:00
Kevin Veen-Birkenbach 9917cccd6e Release version 0.3.0
🧪 Test / 🧪 Lock + lint (push) Waiting to run
2026-11-03 16:18:09 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 735bbc9b77 feat(kvb-coach): run the short coach at the first prompt of every full hour
🧪 Test / 🧪 Lock + lint (push) Waiting to run
hooks/reminder.sh claims the current clock hour with an atomic mkdir under ~/.claude/.coach-hours/, so only the first prompt of the hour across all agents gets the COACH HOUR hint (at most three lines, most urgent area first, then the request); earlier hours are removed. enable-plugins.js registers it next to the kvb-dream reminder. test_coach covers once per hour, silence within the hour and the cleanup; test_dream checks the single registration. Operator order 2026-11-03.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:14:46 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 5f30644ca6 feat(kvb-coach-meditation): short alpha-wave meditation with fractals, tuned by feedback
kvb-coach-meditation writes a local session page with its settings embedded (xdg-open drops a file:// query): generative pentatonic ambience with reverb over an isochronic or binaural alpha layer (8-13 Hz), a WebGL Julia fractal that zooms with the breathing cue (box, 4-7-8 or none), breath-synced glowing type in URW Gothic, +/- keys for the picture's brightness kept in localStorage, and a progress line along the bottom edge. meditation.py validates every setting and refuses a session while one is missing; after every session kvb-think-listen collects feedback that becomes one set per answer, logged in the kvb-coach profile. Adds the category coach: every coaching skill beyond kvb-coach is named kvb-coach-<name>. tests/test_meditation.py keeps script and page in step. Operator order 2026-11-03; two descriptions trimmed for the listing budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:14:45 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 57c3322076 feat(kvb-coach): coach the operator's next step and remember what they enjoy
🧪 Test / 🧪 Lock + lint (push) Waiting to run
kvb-coach reads the situation (local time, hours at work today, the profile, open operator commands from kvb-org-operator, delegable kvb-org-rasci tasks) and suggests at most one next step per area: psyche, body, sleep, food, the operator's own tasks, and tasks to delegate to agents; short, friendly, with emojis, ordered by what the situation needs and biased to what the operator likes. It runs kvb-think-listen often to learn preferences. coach.py keeps the host-local profile in the gitignored .state/ of the skills repository: likes and dislikes per area (one replaces the other), a capped done-log, and the hours since the last activity per area; tests/test_coach.py covers it. General wellbeing only, no medical advice. Operator order 2026-11-03; two descriptions trimmed for the listing budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:57:14 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 5c30103b43 feat(skills)!: kvb-dream processes everything, with kvb-optimize, cleanups and one reminder
kvb-skill-dream becomes the uncategorized collector kvb-dream. It reads the last 24 hours of every repository and session the agent touched, writes each finding into the memory of its own project, hands its findings to the new kvb-optimize (skill-cleanup, transcend, skill-auto, skill-tune, skill-compile, skill-essence, code-refactor, code-tune, think-gaps, one round of questions) and then runs kvb-cleanup and kvb-git-branch-gc. The separate kvb-skill-tune reminder is gone: one reminder reads the session id from the hook payload and hints each agent at most every twelve hours, and the global install enables kvb-dream-notify.timer, a systemd user timer that recommends /kvb-dream in a GNOME notification daily at 17:00 local time and catches up once after a missed run. enable-plugins.js retires the tune reminder hooks and repoints every earlier dream hook path. Operator order 2026-11-03; three descriptions trimmed for the listing budget.

BREAKING CHANGE: /kvb-skill-dream is now /kvb-dream; kvb-skill-tune no longer has its own reminder hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:57:14 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 c47393a73b feat(kvb-skill-cleanup): delete installed skills that renames and removals left behind
legacy.py reads the skills repository's git history (renames and deletions of skills/*/SKILL.md), follows rename chains to the current skill, and classifies every installed skill directory as current (shipped or locked), project (tracked in the repository that holds the root), legacy (with its successor), removed, or unknown. kvb-skill-cleanup scans ~/.claude/skills, ~/.agents/skills, ~/.codex/skills and the projects' .claude and .agents skills, asks the operator, and deletes by re-running the installer, by rm -r where the sandbox allows, or through kvb-org-operator; a rescan verifies. tests/test_skill_cleanup.py covers a two-step rename chain, a deletion, a locked, a project and an unknown skill, and invalid input; a broken one-step resolver fails it. Operator order 2026-11-03; four descriptions trimmed for the listing budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:57:14 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 de8823e239 feat(kvb-org-operator): list every command only the operator can run
kvb-org-operator collects the steps that wait on a human across all agents: commands under a deny or unconfirmed ask rule, anything outside the sandbox (keys, permission settings, foreign hosts, root), steps the agent rules reserve for the operator (push, release, tags, reboot) and human-bound ones (hardware key, login, second factor, purchase, decision). Sources are the own session, the kvb-org-rasci register and one message per live peer. Done steps drop out, duplicates merge, the rest is ordered by dependency and printed as a table plus one copyable bash block, then recorded as operator/<slug> register tasks. Agents never run a listed command or hand it to a peer. Operator order 2026-11-03; four descriptions trimmed for the listing budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:57:14 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 b99c1e2be5 feat(kvb-skill-compile): compile deterministic skill steps into scripts
kvb-skill-compile finds the steps of a skill whose result follows from its inputs alone (fixed command sequences, parsing, counting, thresholds, format checks, bookkeeping) and turns them into tested scripts in the repository that owns the skill: skills/<skill>/scripts/ with a test under tests/ in the skills repository, that repository's own convention elsewhere, and an upstream proposal for third-party skills. The prose shrinks to the call. Wired into kvb-skill-tune (finding and shape option), kvb-skill-dream step 7, kvb-skill-auto, kvb-skill-transcend and kvb-skill-essence. Four descriptions are trimmed to stay inside the 1300-word listing budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:57:14 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 359ea0cba0 feat(skills)!: rename the skills category to skill
kvb-skills-dream, -tune, -auto, -essence and -transcend become kvb-skill-*, and tests/test_naming.py now requires the category skill. enable-plugins.js registers the reminder hooks under the new names and repoints hook commands of the previous kvb-skills-* paths as well as the older autotune/dream paths; test_dream covers both generations. References in kvb-check-confidence, kvb-org-perform, kvb-org-rasci, kvb-transcend, the tests and the README organigram and skill map follow. The header comments of both reminder hooks and enable-plugins.js are dropped.

BREAKING CHANGE: /kvb-skills-dream, /kvb-skills-tune, /kvb-skills-auto, /kvb-skills-essence and /kvb-skills-transcend are now /kvb-skill-*.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:57:14 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 8be453b361 feat(skills)!: add kvb-create-requirement and kvb-think-brainstorm, rename kvb-create-feature to kvb-think-gaps
kvb-create-requirement asks for the context, runs kvb-think-listen and kvb-think-brainstorm, then drafts one requirement with a user story, operator-confirmed decisions, a Mermaid approach flowchart with emoji phases, a test loop and an operator feedback checkpoint per stage, checkbox acceptance criteria grouped by area, and a Tests group with unit, integration and e2e (n/a with a reason). It writes only on the operator's Write it and stops with an offer to hand over to kvb-create-plan. kvb-think-brainstorm generates options through seven fixed lenses, scores them on value, effort and risk, and hands the best two or three over. kvb-create-feature becomes kvb-think-gaps: it still finds and ranks missing work but delegates every pick to kvb-create-requirement, so the requirement template lives in one place. Operator order 2026-11-03.

BREAKING CHANGE: /kvb-create-feature is now /kvb-think-gaps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:57:14 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 2fbbd7386c feat(transcend): carry session scripts, tests and skills into the repos
Adds kvb-transcend (collector), kvb-skills-transcend (into the skills repo) and kvb-code-transcend (into the current repo). transcend.py scans the own scratchpad, peer scratchpads and the agent homes for scripts, tests and skills the skills repo does not ship, skips secret-named and oversized files, and remembers carried/dropped decisions by content digest in the gitignored .state/transcend.json, so a rescan offers only new or changed candidates. Wired into kvb-skills-tune (own scripts), kvb-code-tune step 6 (measurements) and kvb-skills-dream step 7. Seven descriptions are tightened to stay inside the 1300-word listing budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:57:14 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 42b66e3ddf feat(skills)!: remove kvb-skills-shortcuts
The operator dropped the conversation shortcuts (sobu, soco, socobu, socon, sona, sote) as unnecessary. Deletes the skill and tests/test_shortcuts.py, drops the tokens from the kvb-code-no-comments triggers, the shortcut branch from kvb-skills-auto step 3, the shortcuts row option from kvb-skills-tune, and the nodes and edge from the README organigram and skill map.

BREAKING CHANGE: the shortcut tokens no longer expand; use /kvb-git-commit-next and /kvb-git-commit-no-verify.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:57:13 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 41c1c57933 feat(kvb-git-commit): show worktree, branch, confidence and break risk before every commit
kvb-git-commit-no-verify gains a step between the audits and git commit: the message right before the commit call names the worktree's absolute path, the branch, the fix confidence (kvb-check-confidence) and the break risk (kvb-check-break-risk). kvb-git-commit-next names the block in its hand-over step. Operator order 2026-11-03, after a commit in the #1324 worktree was rejected for omitting the path. Also replaces the stale advocate name with kvb-check-legal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:57:13 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 14cac57c3e docs(org): give hotfix gates the next free slot
🧪 Test / 🧪 Lock + lint (push) Canceled after 0s
Operator rule: a hotfix gate (a branch from the default branch that fixes it) takes the next free gate or test slot before every other line; everything else keeps its WSJF rank, and a running gate is never cut short for a hotfix. Written into the chair's queue in kvb-org-organize and the grant step of kvb-org-wsjf.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-11-02 17:07:00 +00:00
Kevin Veen-BirkenbachandClaude Opus 5.5 ddbbb92a32 docs(cleanup): inventory worktrees by size and integration line
kvb-cleanup asks every peer to report and remove its own unused worktrees and volumes, inventories each worktree's size, dirty state, push state and whether its HEAD is already in an integration line, ranks such clean worktrees as medium risk, and removes slot worktrees through the teardown target and sidetrees through git worktree remove without --force. kvb-git-branch-gc runs its dry run with pkgmgr branch gc --sizes --min-age 24 --merged-into <line> and shows Merged into and Size.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-11-02 17:07:00 +00:00
Kevin Veen-BirkenbachandClaude Opus 5.5 5a93b0e5a9 feat(org): rank the register by WSJF from Fibonacci estimates
🧪 Test / 🧪 Lock + lint (push) Canceled after 0s
kvb-org-wsjf: the chair asks its subordinates for Fibonacci estimates of business value and complexity, rasci.py estimate records them per party, and WSJF = median value / median complexity ranks the tasks. rasci.py wsjf prints the WSJF table with OKR and the task id; the register's show takes its Prio from that rank (an operator --prio pin first) and adds a WSJF column. Four descriptions were tightened to keep the listing budget.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-11-01 01:49:03 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 35dc37641d feat(org): add the kvb-org-rasci RASCI/OKR register and kvb-org-artefacts
rasci.py keeps the organisation's tasks in <council root>/rasci.json: RASCI roles, objective with key results, priority, worker, start, end, workdir, branch, tools and artifacts; done is refused while a key result is open. council.py writes every claim, takeover and release through, and show lists the live claims as one row per task with R, A, S, C and I columns. kvb-org-artefacts manages the artifacts in the same table for kvb-cleanup. The org, plan, triage, tune, cleanup, branch-gc and dream skills now record into and read from the register; five descriptions were tightened to keep the listing budget.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-11-01 00:47:23 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 624baccfc4 refactor(skills)!: rename every skill to kvb-<category>-<name>
All 38 first-party skills move to kvb-<category>-<name> with the categories think, create, code, ci, git, check, org and skills; kvb-cleanup and kvb-help are the only uncategorized names. tests/test_naming.py pins the schema, the Agent Skills name rules and the README organigram. enable-plugins.js repoints hook commands of renamed skills in settings.local.json so a stale script path cannot block tools.

BREAKING CHANGE: the old skill names and slash commands (/commit-next, /dream, /autotune, ...) are gone; use their kvb- successors listed in the README organigram.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-11-01 00:47:23 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 64403cefca docs(tune): always check language, framework and recursion on hot paths
Every run now examines each measured hot path for the most efficient language and framework fit and for unnecessary recursion, and reports the result even when clean. Framework overhead joins the language section; changes stay bound to the session's limits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-11-01 00:47:22 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 8e19dbb960 docs(democracy): let an agent end its own hung job after 60 min without progress and a standstill probe
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-31 19:23:15 +01:00
Kevin Veen-Birkenbach 3a8f115613 Release version 0.2.0
🧪 Test / 🧪 Lock + lint (push) Canceled after 0s
2026-10-31 18:07:01 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 d2f7c19f13 feat(branch-gc): remove merged branches and worktrees through pkgmgr branch gc after the operator's pick
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-31 17:30:24 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 b417a1084c feat(subagent-model): pick a cheaper subagent model only at 90 % same-result confidence and log spawns with their quality
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-31 17:27:26 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 a4cbab98b1 docs(merge-fix): state that --path widens the search instead of narrowing --grep
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-31 17:21:51 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 44c88b4bf7 docs(skills): never cut a running job or agent short, not as chair and not at its time cap
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-31 17:19:05 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 9f7844ad6c fix(democracy): ignore heavy commands inside quotes and heredocs in the block hook
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-31 17:16:25 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 ed0ea62b52 fix(democracy): skip worktrees with a running make in the stop-stage cleanup
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-31 17:14:53 +01:00
Kevin Veen-BirkenbachandClaude Opus 5.5 b69a7f85d9 feat(democracy): admit test slots only below the configured CPU and RAM limits
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-31 17:08:33 +01:00
106 changed files with 5346 additions and 796 deletions

No files matched your search

+2
View File
@@ -17,6 +17,8 @@ npm-debug.log*
.claude/ .claude/
.mcp.json .mcp.json
.state/
# Editor / OS # Editor / OS
.DS_Store .DS_Store
*.swp *.swp
+76
View File
@@ -1,5 +1,81 @@
# Changelog # Changelog
## [0.3.0] - 2026-11-03
Breaking:
- Skills: every skill is now *kvb-category-name*; the old names are gone
- Skills: the category *skills* is now *skill*, e.g. *kvb-skill-tune*
- Skills: *kvb-create-feature* is now *kvb-think-gaps*
- Skills: *kvb-skill-dream* is now *kvb-dream*
- Skills: *kvb-skills-shortcuts* and tokens such as *sobu* are gone
Added:
- Org: *kvb-org-rasci* keeps one RASCI/OKR register of every agent task
- Org: *kvb-org-artefacts* tracks images, volumes and worktrees per task
- Org: *kvb-org-wsjf* ranks the register by WSJF from Fibonacci estimates
- Org: *kvb-org-operator* lists every command only the operator can run
- Commit: commit skills show worktree, branch, confidence and risk first
- Transcend: *kvb-transcend* carries session scripts and tests into repos
- Requirements: *kvb-create-requirement* with user story, Mermaid and tests
- Think: *kvb-think-brainstorm* scores options from seven fixed lenses
- Skills: *kvb-skill-compile* turns deterministic steps into scripts
- Skills: *kvb-skill-cleanup* deletes legacy skills that renames left behind
- Dream: *kvb-optimize* runs all nine optimization skills in one pass
- Dream: a GNOME notification recommends */kvb-dream* daily at 17:00
- Coach: *kvb-coach* suggests the next step for mind, body, food and tasks
- Coach: *kvb-coach-meditation* plays alpha-wave music with fractals
- Coach: a short coach note at the first prompt of every full hour
Changed:
- Dream: covers every repository and session, then optimizes and cleans up
- Dream: one hint per agent every 12 h replaces the tune and dream hints
- Tune: *kvb-code-tune* always checks language, framework and recursion
- Cleanup: worktrees are listed by size and integration line
- Coordination: hotfix gates take the next free test slot
- Coordination: an agent may end its own job after 60 min without progress
Upgrade notes:
- Install: run *make install* and restart the agent to load the new names
- Install: run *make install-skills* in each project to drop old copies
- Install: */kvb-skill-cleanup* lists any legacy skill left behind
- Install: the global install enables the *kvb-dream-notify.timer* unit
- Settings: remove *autotune* and *dream* hooks from *~/.claude/settings.json*
## [0.2.0] - 2026-10-31
Added:
- Git: *branch-gc* clears merged branches and worktrees with *pkgmgr*
- Agents: *subagent-model* uses a cheaper model only at 90 % same result
- Agents: *subagent-model* hooks log every spawn and its rated quality
Changed:
- Coordination: never cut a running job or agent short, not even at its cap
- Resources: *watch.py admit* grants test slots only below CPU and RAM limits
- Coordination: a relayer writes an operator rule to shared memory first
Fixed:
- Resources: stop-stage cleanup now skips worktrees where *make* still runs
- Resources: the block hook ignores heavy commands in quotes and heredocs
- Update: *make update* now updates every locked source, not just the first
- Update: *make update* installs for Claude Code only
- CI: *ci-artifacts* signs lint failures by finding, ignores later steps
- Release: *changelog* warns when a workflow on HEAD already failed
- CI: ruff pinned in *pyproject.toml*; local and CI lint now agree
Upgrade notes:
- 0.1.0 was never published (its CI failed); its upgrade notes follow
- Install: restart the agent after *make install* to load skills and hooks
- Resources: *watch.py* needs *~/.config/agents-council/config.json*
- Resources: add *busy_processes* and *load_limits* to that config
## [0.1.0] - 2026-10-30 ## [0.1.0] - 2026-10-30
Added: Added:
+5 -1
View File
@@ -1,4 +1,4 @@
.PHONY: install project test update .PHONY: install project test update budget
# Install the skills globally into $HOME (.agents/skills + .claude/skills). # Install the skills globally into $HOME (.agents/skills + .claude/skills).
install: install:
@@ -19,3 +19,7 @@ test:
# Update all skills to their latest versions and refresh skills-lock.json. # Update all skills to their latest versions and refresh skills-lock.json.
update: update:
@bash scripts/update.sh @bash scripts/update.sh
# Count skill description words against the listing budget: make budget [INDEX=1]
budget:
@python3 scripts/budget.py --repo . --top 5 $(if $(INDEX),--index)
+203 -53
View File
@@ -7,9 +7,85 @@ Every skill is pinned in `skills-lock.json` for reproducible installs.
First-party skills live under `skills/` and are layered on top of the locked ones at install time. First-party skills live under `skills/` and are layered on top of the locked ones at install time.
Compatible with Claude Code, Codex, Gemini CLI, Cursor, Copilot, Windsurf, Cline, and other agents. Compatible with Claude Code, Codex, Gemini CLI, Cursor, Copilot, Windsurf, Cline, and other agents.
## Naming 🏷️
Every first-party skill is named `kvb-<category>-<name>` with one of these categories: `think`, `create`, `code`, `ci`, `git`, `check`, `org`, `skill`, `coach`: `think` thinks without producing a file, `create` produces an artifact, `code` works on code, `ci` on pipelines, `git` on history and forges, `check` analyses and reports findings, `org` coordinates agents, `skill` works on skills themselves, `coach` serves the operator's wellbeing. Only `kvb-cleanup`, `kvb-coach`, `kvb-help` and the collectors `kvb-dream`, `kvb-optimize` and `kvb-transcend` stand without a category. Names use `a-z`, `0-9` and single hyphens, at most 64 characters, and equal their directory name. `tests/test_naming.py` enforces all of it, including that the organigram below lists every skill under its category.
## Organigram 🏛️
Every first-party skill under its category.
```mermaid
flowchart LR
kvb((kvb))
kvb --> think[think]
kvb --> create[create]
kvb --> code[code]
kvb --> ci[ci]
kvb --> git[git]
kvb --> check[check]
kvb --> org[org]
kvb --> skill[skill]
kvb --> coach[coach]
kvb --> none[no category]
think --> think_listen[kvb-think-listen]
think --> think_explain[kvb-think-explain]
think --> think_dialectic[kvb-think-dialectic]
think --> think_criticize[kvb-think-criticize]
think --> think_brainstorm[kvb-think-brainstorm]
think --> think_gaps[kvb-think-gaps]
create --> create_plan[kvb-create-plan]
create --> create_requirement[kvb-create-requirement]
code --> code_scope_guard[kvb-code-scope-guard]
code --> code_no_defaults[kvb-code-no-defaults]
code --> code_no_comments[kvb-code-no-comments]
code --> code_refactor[kvb-code-refactor]
code --> code_test[kvb-code-test]
code --> code_tune[kvb-code-tune]
code --> code_transcend[kvb-code-transcend]
ci --> ci_triage[kvb-ci-triage]
ci --> ci_artifacts[kvb-ci-artifacts]
ci --> ci_observe[kvb-ci-observe]
git --> git_commit_next[kvb-git-commit-next]
git --> git_commit_no_verify[kvb-git-commit-no-verify]
git --> git_merge_fix[kvb-git-merge-fix]
git --> git_close_issues[kvb-git-close-issues]
git --> git_update_branches[kvb-git-update-branches]
git --> git_branch_gc[kvb-git-branch-gc]
git --> git_changelog[kvb-git-changelog]
check --> check_legal[kvb-check-legal]
check --> check_security[kvb-check-security]
check --> check_principles[kvb-check-principles]
check --> check_confidence[kvb-check-confidence]
check --> check_break_risk[kvb-check-break-risk]
org --> org_organize[kvb-org-organize]
org --> org_split[kvb-org-split]
org --> org_coordinate[kvb-org-coordinate]
org --> org_perform[kvb-org-perform]
org --> org_matrix[kvb-org-matrix]
org --> org_rasci[kvb-org-rasci]
org --> org_artefacts[kvb-org-artefacts]
org --> org_wsjf[kvb-org-wsjf]
org --> org_robot[kvb-org-robot]
org --> org_operator[kvb-org-operator]
skill --> skill_tune[kvb-skill-tune]
skill --> skill_auto[kvb-skill-auto]
skill --> skill_essence[kvb-skill-essence]
skill --> skill_transcend[kvb-skill-transcend]
skill --> skill_compile[kvb-skill-compile]
skill --> skill_cleanup[kvb-skill-cleanup]
none --> cleanup[kvb-cleanup]
none --> help[kvb-help]
none --> transcend_all[kvb-transcend]
none --> dream_all[kvb-dream]
none --> optimize_all[kvb-optimize]
none --> coach_all[kvb-coach]
coach --> coach_meditation[kvb-coach-meditation]
```
## Skill map 🗺️ ## Skill map 🗺️
How the first-party skills work together. A solid arrow means the skill invokes or relies on the other one; a dotted arrow means it fires automatically. Every skill that reports a result closes with the `confidence` block; those arrows are left out. The locked third-party skills (caveman, ponytail, archify) are not drawn. How the first-party skills work together. A solid arrow means the skill invokes or relies on the other one; a dotted arrow means it fires automatically. Every skill that reports a result closes with the `kvb-check-confidence` block; those arrows are left out. The locked third-party skills (caveman, ponytail, archify) are not drawn.
```mermaid ```mermaid
flowchart LR flowchart LR
@@ -17,60 +93,83 @@ flowchart LR
commit_req([commit request]) commit_req([commit request])
stage_req([staging or fix ready]) stage_req([staging or fix ready])
edit_hook([edit in a git worktree]) edit_hook([edit in a git worktree])
reminders([reminder hooks]) reminders([12 h agent hint, 17:00 GNOME popup])
full_hour([full hour])
code_task([code task]) code_task([code task])
spawn([subagent spawn])
end end
subgraph commit [Commit] subgraph cat_think [think]
commit_next[commit-next] active_listening[kvb-think-listen]
commit_no_verify[commit-no-verify] explain[kvb-think-explain]
comments_clean[comments-clean] dialectic[kvb-think-dialectic]
advocate[advocate] criticize[kvb-think-criticize]
confidence[confidence] brainstorm[kvb-think-brainstorm]
break_risk[break-risk] gaps[kvb-think-gaps]
explain[explain]
end end
subgraph coordination [Coordination] subgraph cat_create [create]
democracy[democracy] plan[kvb-create-plan]
coordinate[coordinate] requirement[kvb-create-requirement]
funnel[funnel]
cleanup[cleanup]
end end
subgraph ci [CI] subgraph cat_code [code]
observe[observe] scope_guard[kvb-code-scope-guard]
triage[triage] no_defaults[kvb-code-no-defaults]
ci_artifacts[ci-artifacts] refactor[kvb-code-refactor]
tune[tune] test[kvb-code-test]
merge_fix[merge-fix] tune[kvb-code-tune]
comments_clean[kvb-code-no-comments]
code_transcend[kvb-code-transcend]
end end
subgraph reasoning [Reasoning] subgraph cat_ci [ci]
active_listening[active-listening] triage[kvb-ci-triage]
dialectic[dialectic] ci_artifacts[kvb-ci-artifacts]
criticize[criticize] observe[kvb-ci-observe]
sceptic[sceptic]
auto_feature[auto-feature]
end end
subgraph autonomy [Autonomy] subgraph cat_git [git]
plan[plan] commit_next[kvb-git-commit-next]
robot[robot] commit_no_verify[kvb-git-commit-no-verify]
merge_fix[kvb-git-merge-fix]
close_issues[kvb-git-close-issues]
update_branches[kvb-git-update-branches]
branch_gc[kvb-git-branch-gc]
changelog[kvb-git-changelog]
end end
subgraph self_improvement [Self-improvement] subgraph cat_check [check]
dream[dream] advocate[kvb-check-legal]
autotune[autotune] medusa[kvb-check-security]
autoskill[autoskill] sceptic[kvb-check-principles]
essence[essence] confidence[kvb-check-confidence]
shortcuts[shortcuts] break_risk[kvb-check-break-risk]
end end
subgraph quality [Quality] subgraph cat_org [org]
test[test] democracy[kvb-org-organize]
refactor[refactor] funnel[kvb-org-split]
no_defaults[no-defaults] coordinate[kvb-org-coordinate]
scope_guard[scope-guard] subagent_model[kvb-org-perform]
status_matrix[kvb-org-matrix]
rasci[kvb-org-rasci]
artefacts[kvb-org-artefacts]
wsjf[kvb-org-wsjf]
robot[kvb-org-robot]
operator[kvb-org-operator]
end end
subgraph standalone [Standalone] subgraph cat_skill [skill]
changelog[changelog] autotune[kvb-skill-tune]
help_skills[help-skills] autoskill[kvb-skill-auto]
status_matrix[status-matrix] essence[kvb-skill-essence]
medusa[medusa] skill_transcend[kvb-skill-transcend]
compile[kvb-skill-compile]
skill_cleanup[kvb-skill-cleanup]
end
subgraph cat_none [no category]
cleanup[kvb-cleanup]
help_skills[kvb-help]
transcend[kvb-transcend]
dream[kvb-dream]
optimize[kvb-optimize]
coach[kvb-coach]
end
subgraph cat_coach [coach]
meditation[kvb-coach-meditation]
end end
commit_req -.-> commit_next commit_req -.-> commit_next
@@ -78,14 +177,19 @@ flowchart LR
stage_req -.-> confidence stage_req -.-> confidence
edit_hook -. council gate .-> democracy edit_hook -. council gate .-> democracy
reminders -.-> dream reminders -.-> dream
reminders -.-> autotune full_hour -.-> coach
code_task -.-> no_defaults code_task -.-> no_defaults
code_task -.-> scope_guard code_task -.-> scope_guard
spawn -.-> subagent_model
commit_next --> merge_fix commit_next --> merge_fix
commit_next --> commit_no_verify commit_next --> commit_no_verify
commit_next --> explain commit_next --> explain
commit_next --> dialectic commit_next --> dialectic
close_issues --> confidence
update_branches --> active_listening
update_branches -. conflicts .-> merge_fix
update_branches -. pushes .-> operator
commit_no_verify --> comments_clean commit_no_verify --> comments_clean
commit_no_verify --> advocate commit_no_verify --> advocate
advocate --> dialectic advocate --> dialectic
@@ -94,9 +198,22 @@ flowchart LR
coordinate --> democracy coordinate --> democracy
cleanup --> democracy cleanup --> democracy
cleanup --> branch_gc
branch_gc --> democracy
funnel --> democracy funnel --> democracy
funnel --> dialectic funnel --> dialectic
funnel --> subagent_model
dialectic --> subagent_model
democracy --> merge_fix democracy --> merge_fix
democracy -. claim and release .-> rasci
funnel --> rasci
status_matrix --> rasci
cleanup --> artefacts
artefacts --> rasci
democracy --> wsjf
wsjf --> rasci
operator --> rasci
dream --> rasci
observe --> triage observe --> triage
observe --> ci_artifacts observe --> ci_artifacts
@@ -115,19 +232,52 @@ flowchart LR
criticize --> active_listening criticize --> active_listening
sceptic --> dialectic sceptic --> dialectic
sceptic --> no_defaults sceptic --> no_defaults
auto_feature --> sceptic gaps --> sceptic
auto_feature --> active_listening gaps --> active_listening
gaps --> requirement
requirement --> active_listening
requirement --> brainstorm
requirement -. offer .-> plan
brainstorm --> dialectic
plan --> robot plan --> robot
plan --> active_listening plan --> active_listening
robot --> dialectic robot --> dialectic
dream --> dialectic dream --> dialectic
dream --> autotune dream --> optimize
dream --> autoskill dream --> cleanup
dream --> branch_gc
optimize --> skill_cleanup
optimize --> transcend
optimize --> autoskill
optimize --> autotune
optimize --> compile
optimize --> essence
optimize --> refactor
optimize --> tune
optimize --> gaps
coach --> active_listening
coach --> operator
coach --> rasci
coach --> meditation
meditation --> active_listening
autoskill --> autotune autoskill --> autotune
autoskill --> shortcuts
autotune --> essence autotune --> essence
transcend --> skill_transcend
transcend --> code_transcend
skill_transcend --> active_listening
skill_transcend --> autotune
code_transcend --> active_listening
autotune -. own scripts .-> skill_transcend
autotune -. deterministic prose .-> compile
autoskill -. deterministic steps .-> compile
skill_transcend -. deterministic moves .-> compile
essence -. procedures .-> compile
compile --> active_listening
skill_cleanup --> operator
skill_cleanup -. unknown skills .-> transcend
tune -. measurements .-> code_transcend
test --> triage test --> triage
test --> dialectic test --> dialectic
@@ -155,7 +305,7 @@ Copy the skills into a specific project (`<repo>/.agents/skills` and `<repo>/.cl
make project TARGET=/path/to/repo make project TARGET=/path/to/repo
``` ```
Both commands also patch the target's `.claude/settings.local.json` — the machine-local, untracked file, so a project's reviewed `.claude/settings.json` is never rewritten by an install: the caveman and ponytail plugins are enabled so their modes auto-activate on session start, and two reminder hooks are registered — `autotune` points you at `/autotune` at most once an hour, `dream` points you at `/dream` at most once every twelve hours per repository (stamped in that repository's git dir). A PreToolUse gate from the `democracy` skill blocks edits to files in a git worktree until the session holds a claim (`skills/democracy/scripts/council.py`), and its resource watch (`skills/democracy/scripts/watch.py`) measures disk, Docker and caches, cleans up low-risk data and blocks new heavy jobs before the disk runs out; it stays inactive until `~/.config/agents-council/config.json` exists. Existing settings are preserved. Both commands also patch the target's `.claude/settings.local.json` — the machine-local, untracked file, so a project's reviewed `.claude/settings.json` is never rewritten by an install: the caveman and ponytail plugins are enabled so their modes auto-activate on session start, and two reminder hooks are registered — `kvb-dream` points each agent at `/kvb-dream` at most once every twelve hours (stamped per session in `~/.claude/.dream-stamps/`), and `kvb-coach` runs its short form at the first prompt of every full hour, once across all agents (stamped in `~/.claude/.coach-hours/`); the global install also enables the `kvb-dream-notify.timer` systemd user timer, which recommends `/kvb-dream` in a GNOME notification daily at 17:00 local time and catches up once after a missed run. A PreToolUse gate from the `kvb-org-organize` skill blocks edits to files in a git worktree until the session holds a claim (`skills/kvb-org-organize/scripts/council.py`), and its resource watch (`skills/kvb-org-organize/scripts/watch.py`) measures disk, Docker and caches, cleans up low-risk data and blocks new heavy jobs before the disk runs out; it stays inactive until `~/.config/agents-council/config.json` exists. Two hooks of the `kvb-org-perform` skill (`skills/kvb-org-perform/scripts/log.py`) log every Agent spawn with its task class, model and effort, and store the quality feedback a sandboxed session prints, in `~/.local/state/agents-council/subagent-model.jsonl`. Existing settings are preserved.
Restart your agent afterwards so it loads the new skills. Restart your agent afterwards so it loads the new skills.
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Count every first-party skill description against the listing budget.
Args:
--repo: skills repository root.
--index: count the staged versions in the git index instead of the files.
--top: how many of the longest descriptions to list.
Exit codes:
0 within budget, 1 over budget, 2 usage error.
"""
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
LISTING_WORDS = 1500
DESCRIPTION_WORDS = 40
def words(skill_md: str) -> int:
frontmatter = skill_md.split("---", 2)[1]
return len(frontmatter.split("description:", 1)[1].split()) - 1
def git(repo: Path, *args: str) -> str:
result = subprocess.run(
["git", *args], cwd=repo, capture_output=True, text=True, check=False
)
if result.returncode != 0:
print(f"budget: {result.stderr.strip()}", file=sys.stderr)
sys.exit(2)
return result.stdout
def counts(repo: Path, index: bool) -> dict[str, int]:
if index:
paths = git(repo, "ls-files", "skills/*/SKILL.md").split()
return {Path(p).parent.name: words(git(repo, "show", f":{p}")) for p in paths}
return {
p.parent.name: words(p.read_text(encoding="utf-8"))
for p in sorted((repo / "skills").glob("*/SKILL.md"))
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--repo", required=True)
parser.add_argument("--index", action="store_true")
parser.add_argument("--top", type=int, required=True)
args = parser.parse_args()
repo = Path(args.repo)
if not (repo / "skills").is_dir():
print(f"budget: {repo} has no skills/ directory", file=sys.stderr)
return 2
found = counts(repo, args.index)
total = sum(found.values())
over = sorted(name for name, n in found.items() if n > DESCRIPTION_WORDS)
print(f"total {total}/{LISTING_WORDS} words in {len(found)} skills")
for name, n in sorted(found.items(), key=lambda item: -item[1])[: args.top]:
print(f"{n}\t{name}")
for name in over:
print(f"over {DESCRIPTION_WORDS}: {name}")
return 1 if total > LISTING_WORDS or over else 0
if __name__ == "__main__":
sys.exit(main())
+47 -16
View File
@@ -1,8 +1,4 @@
#!/usr/bin/env node #!/usr/bin/env node
// Enable the caveman and ponytail plugins in a Claude Code settings.json so
// their SessionStart hooks auto-activate both modes, register the autotune
// and dream reminder hooks, the democracy claim gate and the resource watch. Merges non-destructively: existing marketplaces,
// plugins and hooks are preserved, unparseable files are left untouched.
"use strict"; "use strict";
const fs = require("fs"); const fs = require("fs");
@@ -19,7 +15,20 @@ const MARKETPLACES = {
ponytail: { source: { source: "github", repo: "DietrichGebert/ponytail" } }, ponytail: { source: { source: "github", repo: "DietrichGebert/ponytail" } },
}; };
const PLUGINS = { "caveman@caveman": true, "ponytail@ponytail": true }; const PLUGINS = { "caveman@caveman": true, "ponytail@ponytail": true };
const REMINDERS = ["autotune", "dream"]; const REMINDERS = ["kvb-dream", "kvb-coach"];
const RETIRED = [
"autotune/hooks/reminder.sh",
"kvb-skills-tune/hooks/reminder.sh",
"kvb-skill-tune/hooks/reminder.sh",
];
const RENAMED = {
"dream/hooks/reminder.sh": "kvb-dream/hooks/reminder.sh",
"kvb-skills-dream/hooks/reminder.sh": "kvb-dream/hooks/reminder.sh",
"kvb-skill-dream/hooks/reminder.sh": "kvb-dream/hooks/reminder.sh",
"democracy/scripts/council.py": "kvb-org-organize/scripts/council.py",
"democracy/scripts/watch.py": "kvb-org-organize/scripts/watch.py",
"subagent-model/scripts/log.py": "kvb-org-perform/scripts/log.py",
};
let data = {}; let data = {};
if (fs.existsSync(settingsPath)) { if (fs.existsSync(settingsPath)) {
@@ -41,6 +50,26 @@ for (const [name, entry] of Object.entries(MARKETPLACES)) {
data.enabledPlugins = Object.assign(data.enabledPlugins || {}, PLUGINS); data.enabledPlugins = Object.assign(data.enabledPlugins || {}, PLUGINS);
data.hooks = data.hooks || {}; data.hooks = data.hooks || {};
const retired = (hook) =>
typeof hook.command === "string" && RETIRED.some((r) => hook.command.includes(`/skills/${r}`));
for (const [event, entries] of Object.entries(data.hooks)) {
if (!Array.isArray(entries)) continue;
data.hooks[event] = entries
.map((entry) =>
Array.isArray(entry.hooks) ? { ...entry, hooks: entry.hooks.filter((h) => !retired(h)) } : entry)
.filter((entry) => !Array.isArray(entry.hooks) || entry.hooks.length > 0);
}
for (const entries of Object.values(data.hooks)) {
if (!Array.isArray(entries)) continue;
for (const entry of entries) {
for (const hook of entry.hooks || []) {
if (typeof hook.command !== "string") continue;
for (const [from, to] of Object.entries(RENAMED)) {
hook.command = hook.command.replace(`/skills/${from}`, `/skills/${to}`);
}
}
}
}
data.hooks.UserPromptSubmit = data.hooks.UserPromptSubmit || []; data.hooks.UserPromptSubmit = data.hooks.UserPromptSubmit || [];
const alreadyRegistered = JSON.stringify(data.hooks.UserPromptSubmit); const alreadyRegistered = JSON.stringify(data.hooks.UserPromptSubmit);
for (const skill of REMINDERS) { for (const skill of REMINDERS) {
@@ -52,29 +81,31 @@ for (const skill of REMINDERS) {
} }
data.hooks.PreToolUse = data.hooks.PreToolUse || []; data.hooks.PreToolUse = data.hooks.PreToolUse || [];
if (!JSON.stringify(data.hooks.PreToolUse).includes("democracy/scripts/council.py")) { if (!JSON.stringify(data.hooks.PreToolUse).includes("kvb-org-organize/scripts/council.py")) {
const council = path.join(path.dirname(settingsPath), "skills", "democracy", "scripts", "council.py"); const council = path.join(path.dirname(settingsPath), "skills", "kvb-org-organize", "scripts", "council.py");
data.hooks.PreToolUse.push({ data.hooks.PreToolUse.push({
matcher: "Edit|Write|MultiEdit|NotebookEdit", matcher: "Edit|Write|MultiEdit|NotebookEdit",
hooks: [{ type: "command", command: `python3 ${JSON.stringify(council)} gate` }], hooks: [{ type: "command", command: `python3 ${JSON.stringify(council)} gate` }],
}); });
} }
const watch = path.join(path.dirname(settingsPath), "skills", "democracy", "scripts", "watch.py"); const SCRIPT_HOOKS = [
const WATCH_HOOKS = [ ["kvb-org-organize/scripts/watch.py", "UserPromptSubmit", null, "watch"],
["UserPromptSubmit", null, "watch"], ["kvb-org-organize/scripts/watch.py", "PostToolUse", "Bash", "watch"],
["PostToolUse", "Bash", "watch"], ["kvb-org-organize/scripts/watch.py", "PreToolUse", "Bash", "block"],
["PreToolUse", "Bash", "block"], ["kvb-org-perform/scripts/log.py", "PreToolUse", "Agent|Task", "spawn"],
["kvb-org-perform/scripts/log.py", "PostToolUse", "Bash", "persist"],
]; ];
for (const [event, matcher, sub] of WATCH_HOOKS) { for (const [script, event, matcher, sub] of SCRIPT_HOOKS) {
data.hooks[event] = data.hooks[event] || []; data.hooks[event] = data.hooks[event] || [];
const registered = data.hooks[event].some((entry) => const registered = data.hooks[event].some((entry) =>
(entry.hooks || []).some((hook) => (entry.hooks || []).some((hook) =>
String(hook.command).includes("democracy/scripts/watch.py") && String(hook.command).endsWith(` ${sub}`))); String(hook.command).includes(script) && String(hook.command).endsWith(` ${sub}`)));
if (registered) continue; if (registered) continue;
const entry = { hooks: [{ type: "command", command: `python3 ${JSON.stringify(watch)} ${sub}` }] }; const file = path.join(path.dirname(settingsPath), "skills", ...script.split("/"));
const entry = { hooks: [{ type: "command", command: `python3 ${JSON.stringify(file)} ${sub}` }] };
data.hooks[event].push(matcher ? { matcher, ...entry } : entry); data.hooks[event].push(matcher ? { matcher, ...entry } : entry);
} }
fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2) + "\n"); fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2) + "\n");
console.log(`skills: enabled caveman + ponytail plugins, the ${REMINDERS.join(" + ")} reminders, the democracy claim gate and resource watch in ${settingsPath}`); console.log(`skills: enabled caveman + ponytail plugins, the ${REMINDERS.join(" + ")} reminders, the kvb-org-organize claim gate, the resource watch and the kvb-org-perform log in ${settingsPath}`);
+13
View File
@@ -53,4 +53,17 @@ else
warn "skills: node not found; skipped auto-enabling caveman/ponytail plugins." warn "skills: node not found; skipped auto-enabling caveman/ponytail plugins."
fi fi
if [[ "${TARGET}" == "${HOME}" ]]; then
if command -v systemctl &>/dev/null && systemctl --user show-environment &>/dev/null; then
log "skills: enabling the daily kvb-dream notification timer"
units="${HOME}/.config/systemd/user"
mkdir -p "${units}"
cp -f "${REPO_ROOT}/skills/kvb-dream/systemd/kvb-dream-notify.service" "${REPO_ROOT}/skills/kvb-dream/systemd/kvb-dream-notify.timer" "${units}/"
systemctl --user daemon-reload
systemctl --user enable --now kvb-dream-notify.timer
else
warn "skills: no systemd user session; skipped the kvb-dream notification timer."
fi
fi
log "skills: done. Restart your agent to load the skills." log "skills: done. Restart your agent to load the skills."
+1 -1
View File
@@ -30,6 +30,6 @@ fi
log "skills: updating from ${lockfile}..." log "skills: updating from ${lockfile}..."
jq -r '.skills[].source' "${lockfile}" | sort -u | while read -r source; do jq -r '.skills[].source' "${lockfile}" | sort -u | while read -r source; do
log "skills: updating ${source}..." log "skills: updating ${source}..."
npx --yes skills add --yes --agent claude-code "${source}" </dev/null npx --yes skills add "${source}" --yes --agent claude-code </dev/null
done done
log "skills: update complete." log "skills: update complete."
-32
View File
@@ -1,32 +0,0 @@
#!/usr/bin/env bash
# UserPromptSubmit hook: emit the autotune hint at most once per interval.
# Stdout lands in the agent's context, which is why the payload tells the agent
# to show the hint and keep going instead of running autotune by itself.
set -euo pipefail
interval="${AUTOTUNE_INTERVAL_SECONDS:-3600}"
stamp="${AUTOTUNE_STAMP:-${HOME}/.claude/.autotune-stamp}"
now="$(date +%s)"
last=0
if [[ -f "${stamp}" ]]; then
read -r last <"${stamp}" || true
[[ "${last}" =~ ^[0-9]+$ ]] || last=0
fi
if ((now - last < interval)); then
exit 0
fi
mkdir -p "$(dirname "${stamp}")"
printf '%s\n' "${now}" >"${stamp}"
cat <<'HINT'
AUTOTUNE HINT: over an hour since the last hint. Show the operator this notice
verbatim as a short block, then continue with their request. Do NOT run
autotune, do not propose skills, do not ask questions about it.
💡 /autotune - generate a new skill or optimize an existing one
(fewer tokens, fewer errors, more focus, learnings that persist).
Runs only when you trigger it.
HINT
-15
View File
@@ -1,15 +0,0 @@
---
name: commit-no-verify
description: >
Commit the staged changes with --no-verify. Trigger on /commit-no-verify or
any request for a no-verify commit; invoking it IS the per-commit
authorization --no-verify requires, never carried over to later commits.
---
1. Run the comments-clean audit over the staged and unstaged changes first,
then the advocate check over the staging index. An advocate blocker stops
here.
2. Commit exactly the staging index with `git commit --no-verify` and a
Conventional-Commits message derived from the staged diff; never add
unstaged files, never push.
3. Report the created commit hash and what remains uncommitted.
-11
View File
@@ -1,11 +0,0 @@
---
name: coordinate
description: >
Coordinate a failure or fix with the agents on related branches through
merges: claim, ask, report. Trigger on /coordinate and when a fix touches
code other branches share.
---
Run the `democracy` skill's **Lineage** and **Peer protocol** sections for the
failure or fix at hand. They hold the whole procedure; this skill only keeps
`/coordinate` working.
-40
View File
@@ -1,40 +0,0 @@
#!/usr/bin/env bash
# UserPromptSubmit hook: emit the dream hint at most once per interval and per
# repository. The stamp lives in the repository's git dir, so every repository
# keeps its own cadence; outside a git repository the hook stays silent.
# Stdout lands in the agent's context, which is why the payload tells the agent
# to show the hint and keep going instead of running dream by itself.
set -euo pipefail
interval="${DREAM_INTERVAL_SECONDS:-43200}"
if [[ -n "${DREAM_STAMP:-}" ]]; then
stamp="${DREAM_STAMP}"
else
git_dir="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)" || exit 0
stamp="${git_dir}/.dream-stamp"
fi
now="$(date +%s)"
last=0
if [[ -f "${stamp}" ]]; then
read -r last <"${stamp}" || true
[[ "${last}" =~ ^[0-9]+$ ]] || last=0
fi
if ((now - last < interval)); then
exit 0
fi
mkdir -p "$(dirname "${stamp}")"
printf '%s\n' "${now}" >"${stamp}"
cat <<'HINT'
DREAM HINT: over twelve hours since the last hint for this repository. Show the
operator this notice verbatim as a short block, then continue with their
request. Do NOT run dream, do not start replaying the history, do not ask
questions about it.
💤 /dream - consolidate the last 24h in this repository: what held, what
broke, which hypotheses follow, into memory and improvement proposals.
Runs only when you trigger it.
HINT
@@ -1,8 +1,8 @@
--- ---
name: break-risk name: kvb-check-break-risk
description: > description: >
Derive the break-risk percentage from measurement, not feel. Called by Derive the break-risk percentage from measurement, not feel. Called by
`confidence` on every report; also when the operator asks "what could this `kvb-check-confidence` on every report; also when the operator asks "what could this
break?" or names a break-risk threshold. break?" or names a break-risk threshold.
--- ---
@@ -1,6 +1,6 @@
# Break risk: the method in detail # Break risk: the method in detail
Each step of the `break-risk` skill, with the trap it defeats. Each step of the `kvb-check-break-risk` skill, with the trap it defeats.
## The consumer question, first and always ## The consumer question, first and always
@@ -1,5 +1,5 @@
--- ---
name: confidence name: kvb-check-confidence
description: > description: >
Report calibrated fix confidence and break risk unprompted whenever you Report calibrated fix confidence and break risk unprompted whenever you
stage changes, declare a fix or bundle ready, or summarize work the operator stage changes, declare a fix or bundle ready, or summarize work the operator
@@ -9,12 +9,12 @@ description: >
Every stage summary, fix report, or ready-to-commit bundle ends with a Every stage summary, fix report, or ready-to-commit bundle ends with a
confidence block. confidence block.
Invoke the `break-risk` skill every time, before writing the block; this skill Invoke the `kvb-check-break-risk` skill every time, before writing the block; this skill
reports the number it derives and never estimates one itself. reports the number it derives and never estimates one itself.
1. **Fix confidence** (percent): probability the change does what it 1. **Fix confidence** (percent): probability the change does what it
claims. **Break risk** (percent): probability it breaks anything else, claims. **Break risk** (percent): probability it breaks anything else,
derived via the `break-risk` skill. Two separate numbers, never one derived via the `kvb-check-break-risk` skill. Two separate numbers, never one
blended figure. blended figure.
2. **Evidence**: the strongest proof lines only — what was executed, 2. **Evidence**: the strongest proof lines only — what was executed,
reproduced, enumerated, or read from authoritative source. Distinguish reproduced, enumerated, or read from authoritative source. Distinguish
@@ -33,8 +33,8 @@ Calibration rules:
(test, reproduction, live probe) justifies more. (test, reproduction, live probe) justifies more.
A threshold the operator named ("95% confidence, break risk ≤5%") is a gate on A threshold the operator named ("95% confidence, break risk ≤5%") is a gate on
the work, handled as the `break-risk` skill's last section says, for both the work, handled as the `kvb-check-break-risk` skill's last section says, for both
numbers. numbers.
Objective: the `autotune` skill's **Objective** section. Two figures with their residuals replace a Objective: the `kvb-skill-tune` skill's **Objective** section. Two figures with their residuals replace a
hedging paragraph and save the operator a verification round trip. hedging paragraph and save the operator a verification round trip.
@@ -1,24 +1,23 @@
--- ---
name: advocate name: kvb-check-legal
description: > description: >
Check a commit for legal soundness: licence compatibility of added code and Check a commit's licences, attribution, copied snippets, GDPR and
dependencies, attribution, copied snippets, GDPR, trademarks. Run trademarks. Run AUTOMATICALLY before every commit the operator requests;
AUTOMATICALLY before every commit the operator requests; also on /advocate also on /kvb-check-legal [ref|range], "rechtlich prüfen", "lizenz prüfen".
[ref|range], "rechtlich prüfen", "lizenz prüfen".
--- ---
The stance is the project's counsel: find what would make the change unlawful The stance is the project's counsel: find what would make the change unlawful
to distribute before it is distributed. This is not legal advice but a reading to distribute before it is distributed. This is not legal advice but a reading
of the licence texts, with German and EU law where jurisdiction matters; every of the licence texts, with German and EU law where jurisdiction matters; every
report says so in one line. Leaked secrets belong to `medusa`, engineering report says so in one line. Leaked secrets belong to `kvb-check-security`, engineering
principles to `sceptic`. principles to `kvb-check-principles`.
## Target ## Target
- Before a commit: the staging index (`git diff --cached`, plus every added or - Before a commit: the staging index (`git diff --cached`, plus every added or
changed file in full where origin or licence is in question). changed file in full where origin or licence is in question).
- `/advocate <ref>` or `/advocate <a>..<b>`: that commit or range. - `/kvb-check-legal <ref>` or `/kvb-check-legal <a>..<b>`: that commit or range.
- `/advocate` alone: the index if it is non-empty, else `HEAD`. - `/kvb-check-legal` alone: the index if it is non-empty, else `HEAD`.
## Fast exit ## Fast exit
@@ -56,7 +55,7 @@ attribution, copied snippets, data protection, trademarks).
## Blockers stop the commit ## Blockers stop the commit
Harden every blocker with the `dialectic` skill before reporting it. The Harden every blocker with the `kvb-think-dialectic` skill before reporting it. The
antithesis tries: the code is trivial or the operator's own; the dependency is antithesis tries: the code is trivial or the operator's own; the dependency is
build- or test-only and never distributed; an exception applies (linking or build- or test-only and never distributed; an exception applies (linking or
Classpath exception, dual licensing, a licence grant in the issue or commit Classpath exception, dual licensing, a licence grant in the issue or commit
@@ -75,7 +74,7 @@ the operator's language). Then one table, blockers first:
`Evidence` is `proven` (licence text, metadata, or file read; cite path:line `Evidence` is `proven` (licence text, metadata, or file read; cite path:line
or the registry URL) or `derived` (inferred). Close with the verdict (commit or the registry URL) or `derived` (inferred). Close with the verdict (commit
stops or proceeds) and the `confidence` skill's numbers. stops or proceeds) and the `kvb-check-confidence` skill's numbers.
## Rules ## Rules
@@ -1,6 +1,6 @@
# Advocate: the checks # Advocate: the checks
Apply each check to a target the `advocate` skill found in scope. Apply each check to a target the `kvb-check-legal` skill found in scope.
1. **Inbound code** - every file or block of foreign origin carries a licence 1. **Inbound code** - every file or block of foreign origin carries a licence
that the project licence can absorb in the direction of distribution. GPL that the project licence can absorb in the direction of distribution. GPL
@@ -1,15 +1,14 @@
--- ---
name: sceptic name: kvb-check-principles
description: > description: >
Analyse a repository, path or diff with a hostile eye for violated Find violated principles (SPOT, DRY, KISS, SRP, trust boundaries) in a
principles: SPOT, DRY, KISS, SRP, security at trust boundaries. Trigger on repository, path or diff. Trigger on /kvb-check-principles, "sei
/sceptic, "sei skeptisch", "kritische analyse", "review the architecture", skeptisch", "kritische analyse", or "review the architecture".
or "what is wrong with the code".
--- ---
The stance is adversarial: the code is guilty until its structure proves The stance is adversarial: the code is guilty until its structure proves
otherwise. This skill hunts violated principles, not bugs (`code-review`), otherwise. This skill hunts violated principles, not bugs (`code-review`),
not bloat (`ponytail-audit`), not leaked secrets (`medusa`). Read-only — not bloat (`ponytail-audit`), not leaked secrets (`kvb-check-security`). Read-only —
report the findings, change nothing unless the operator asks afterwards. report the findings, change nothing unless the operator asks afterwards.
## Scope ## Scope
@@ -44,7 +43,7 @@ Apply each independently; a finding belongs to exactly one:
findings only. findings only.
6. **Silent defaults** — a fallback that substitutes a guess for a value the 6. **Silent defaults** — a fallback that substitutes a guess for a value the
caller failed to supply, turning a misconfiguration into wrong behaviour caller failed to supply, turning a misconfiguration into wrong behaviour
instead of a loud failure. See the `no-defaults` skill. instead of a loud failure. See the `kvb-code-no-defaults` skill.
## Method ## Method
@@ -55,7 +54,7 @@ Apply each independently; a finding belongs to exactly one:
which value drifts, which caller breaks, which input reaches which sink. which value drifts, which caller breaks, which input reaches which sink.
A finding whose worst case is "feels wrong" gets deleted, not softened. A finding whose worst case is "feels wrong" gets deleted, not softened.
- **Attack your own finding.** For anything consequential or expensive to - **Attack your own finding.** For anything consequential or expensive to
fix, run it through the `dialectic` skill before reporting it — the fix, run it through the `kvb-think-dialectic` skill before reporting it — the
strongest counter-argument is usually "these two are not the same strongest counter-argument is usually "these two are not the same
knowledge" or "this boundary is already checked upstream". knowledge" or "this boundary is already checked upstream".
- **Respect intent.** A deliberate marker (`ponytail:`, a documented - **Respect intent.** A deliberate marker (`ponytail:`, a documented
@@ -69,7 +68,7 @@ One ranked table, worst blast radius first:
| Severity | Principle | Location | What is wrong | Smallest fix | | Severity | Principle | Location | What is wrong | Smallest fix |
Then three closing lines at most: what the scope excluded, which findings Then three closing lines at most: what the scope excluded, which findings
survived `dialectic` versus which are still derived-only, and the single survived `kvb-think-dialectic` versus which are still derived-only, and the single
finding to fix first. No per-finding paragraphs — the operator asks when a finding to fix first. No per-finding paragraphs — the operator asks when a
row needs unpacking. row needs unpacking.
@@ -78,7 +77,7 @@ row needs unpacking.
- Never report formatting, naming taste, or missing comments; a linter owns - Never report formatting, naming taste, or missing comments; a linter owns
those and the operator did not ask. those and the operator did not ask.
- Distinguish proven (grepped, read, reproduced) from derived (reasoned - Distinguish proven (grepped, read, reproduced) from derived (reasoned
statically) per finding, and close with the `confidence` skill's numbers statically) per finding, and close with the `kvb-check-confidence` skill's numbers
for the audit as a whole. for the audit as a whole.
- No silent caps: if you ranked and cut, say how many findings were dropped - No silent caps: if you ranked and cut, say how many findings were dropped
and by what rule. and by what rule.
@@ -1,10 +1,9 @@
--- ---
name: medusa name: kvb-check-security
description: > description: >
Run the MEDUSA security scanner (medusa-security, PyPI) on a repository, Run the MEDUSA scanner (medusa-security) on a repository, working tree or
working tree or secret stores. Use for security scans, vetting a repo before secret stores. Use for security scans, vetting a repo, leaked secrets in
cloning, leaked-secret detection in chat or shell history, or /medusa. chat or shell history, or /kvb-check-security. Defensive only.
Defensive only.
--- ---
MEDUSA is a defensive AI-first security scanner (40,000+ patterns: CVEs, prompt injection, MCP tool poisoning, leaked secrets). MEDUSA is a defensive AI-first security scanner (40,000+ patterns: CVEs, prompt injection, MCP tool poisoning, leaked secrets).
@@ -1,9 +1,9 @@
--- ---
name: ci-artifacts name: kvb-ci-artifacts
description: > description: >
Get the evidence out of a CI run: failed job logs (also mid-run) clustered Pull failed job logs (also mid-run), clustered by error line, and the
by error line, and the artifacts that hold the cause. Trigger whenever a CI cause-holding artifacts out of a CI run. Trigger when a CI failure needs
failure must be explained, and from triage. explaining, and from triage.
--- ---
A job log tells you what broke. An artifact tells you why. Investigating a CI A job log tells you what broke. An artifact tells you why. Investigating a CI
@@ -1,9 +1,9 @@
--- ---
name: observe name: kvb-ci-observe
description: > description: >
Watch a named target (CI run, pipeline, build, log) on a cadence and triage Watch a CI run, pipeline, build or log on a cadence and triage new failures.
new failures as they appear. Trigger on /observe <target> or when the Trigger on /kvb-ci-observe <target> or a request to keep monitoring and fix
operator asks to keep monitoring and fix regressions. regressions.
--- ---
Keep a target under continuous watch and act only when it degrades. The loop is Keep a target under continuous watch and act only when it degrades. The loop is
@@ -23,18 +23,18 @@ baseline, so a stale baseline re-triages solved failures.
Drive this with `/loop` in dynamic mode (no fixed interval unless the operator Drive this with `/loop` in dynamic mode (no fixed interval unless the operator
gave one): after each observation, schedule the next wake-up yourself and pass gave one): after each observation, schedule the next wake-up yourself and pass
the same `/observe` prompt back so the next firing repeats the watch. the same `/kvb-ci-observe` prompt back so the next firing repeats the watch.
1. **Poll the target.** Pull its current state (job list, check conclusions, the 1. **Poll the target.** Pull its current state (job list, check conclusions, the
log's tail); for a GitHub Actions run, the ci-artifacts skill's log's tail); for a GitHub Actions run, the kvb-ci-artifacts skill's
`ci_failures.py <run>`. Enumerate the failures exactly as the `triage` skill's step 1 `ci_failures.py <run>`. Enumerate the failures exactly as the `kvb-ci-triage` skill's step 1
does: failure, cancelled, timed-out; a downstream aggregate that fails only does: failure, cancelled, timed-out; a downstream aggregate that fails only
because an upstream did is not its own failure. because an upstream did is not its own failure.
2. **Diff against the baseline.** A failure is *new* only if it is not in the 2. **Diff against the baseline.** A failure is *new* only if it is not in the
baseline and was not already triaged this session. If nothing is new, log one baseline and was not already triaged this session. If nothing is new, log one
short line ("still green" / "N known failures, no change") and reschedule. short line ("still green" / "N known failures, no change") and reschedule.
3. **Triage each new failure.** Invoke the `triage` skill on the new set only — 3. **Triage each new failure.** Invoke the `kvb-ci-triage` skill on the new set only —
which applies `dialectic` per failing job to a ~99% root cause and a real which applies `kvb-think-dialectic` per failing job to a ~99% root cause and a real
fix. Never mask (no retry-until-pass, no disabled check, no soft-skip). Fold fix. Never mask (no retry-until-pass, no disabled check, no soft-skip). Fold
each newly fixed failure into the baseline so it is not re-triaged. each newly fixed failure into the baseline so it is not re-triaged.
4. **Reschedule.** Pick the next delay from what you are waiting on: an active 4. **Reschedule.** Pick the next delay from what you are waiting on: an active
@@ -50,7 +50,7 @@ the same `/observe` prompt back so the next firing repeats the watch.
notification instead of polling for it; reserve the timed wake-up for external notification instead of polling for it; reserve the timed wake-up for external
state the harness cannot see (a remote CI run, a deploy queue). state the harness cannot see (a remote CI run, a deploy queue).
- Executed evidence beats reasoning: read the actual failing log line and the - Executed evidence beats reasoning: read the actual failing log line and the
artifact, not the job title — triage and dialectic carry this through. artifact, not the job title — kvb-ci-triage and kvb-think-dialectic carry this through.
- One baseline, updated in place: dedup every observation against all failures - One baseline, updated in place: dedup every observation against all failures
seen so far, or a flaky-then-fixed failure reappears every cycle and the watch seen so far, or a flaky-then-fixed failure reappears every cycle and the watch
never converges. never converges.
@@ -1,9 +1,9 @@
--- ---
name: triage name: kvb-ci-triage
description: > description: >
Triage a failing CI run, pipeline or job set to a verified root cause and a Triage a failing CI run to a verified root cause and a real fix, one
real fix, one cluster per worktree. Trigger when a build, test or deploy cluster per worktree. Trigger when a build, test or deploy pipeline has
pipeline has failed jobs. failed jobs.
--- ---
Drive a red pipeline to green by root cause, not by guesswork. Treat every Drive a red pipeline to green by root cause, not by guesswork. Treat every
@@ -12,7 +12,7 @@ root cause is proven.
## Procedure ## Procedure
For a GitHub Actions run, the ci-artifacts skill's `ci_failures.py <run>` For a GitHub Actions run, the kvb-ci-artifacts skill's `ci_failures.py <run>`
does the mechanics of steps 0 to 2 in one call: distance to HEAD, the failed does the mechanics of steps 0 to 2 in one call: distance to HEAD, the failed
jobs' logs, and a first clustering. The judgement in those steps stays yours. jobs' logs, and a first clustering. The judgement in those steps stays yours.
@@ -37,11 +37,13 @@ jobs' logs, and a first clustering. The judgement in those steps stays yours.
actual failing line, not by job name. Jobs whose messages differ in anything actual failing line, not by job name. Jobs whose messages differ in anything
but the interpolated identifiers (host, app, port, path) belong in separate but the interpolated identifiers (host, app, port, path) belong in separate
clusters. clusters.
3. **Coordinate every cluster.** Run the `democracy` skill's Lineage and Peer 3. **Coordinate every cluster.** Run the `kvb-org-organize` skill's Lineage and Peer
protocol for each cluster before fixing it: claim it, ask the lineage protocol for each cluster before fixing it: claim it, ask the lineage
whether someone already fixes it, and take their fix or keep yours. Keep whether someone already fixes it, and take their fix or keep yours. Keep
investigating while you wait for answers. investigating while you wait for answers. In the `kvb-org-rasci` register
4. **Download artifacts per cluster.** Use the `ci-artifacts` skill for the the cluster's objective is the root cause fixed, its key results are the
failing jobs passing again, one per job.
4. **Download artifacts per cluster.** Use the `kvb-ci-artifacts` skill for the
mechanics — it covers per-job logs while a run is still in progress, which mechanics — it covers per-job logs while a run is still in progress, which
bundle to pull, and the grep hygiene that keeps a false positive from bundle to pull, and the grep hygiene that keeps a false positive from
becoming a root cause. For each cluster, fetch every artifact of becoming a root cause. For each cluster, fetch every artifact of
@@ -53,14 +55,15 @@ jobs' logs, and a first clustering. The judgement in those steps stays yours.
log looks conclusive, because a log that explains the symptom rarely log looks conclusive, because a log that explains the symptom rarely
explains the cause. explains the cause.
5. **One funnel per cluster.** Hand every cluster you claimed in step 3 to the 5. **One funnel per cluster.** Hand every cluster you claimed in step 3 to the
`funnel` skill: one agent per cluster, working in its own worktree on its own `kvb-org-split` skill: one agent per cluster, working in its own worktree on its own
branch created from the exact current HEAD, with that cluster as its only branch created from the exact current HEAD, with that cluster as its only
concern, the artifact paths of step 4 in its brief and a hard time cap. concern, the artifact paths of step 4 in its brief and a time cap for its
interim report.
Steps 6 to 8 run inside that sidetree, so no cluster's edits land under Steps 6 to 8 run inside that sidetree, so no cluster's edits land under
another cluster's work or under a running gate or deploy. Run clusters in another cluster's work or under a running gate or deploy. Run clusters in
parallel only when their fixes touch different files and the machine has parallel only when their fixes touch different files and the machine has
room. A split cluster (step 7) gets a funnel of its own. room. A split cluster (step 7) gets a funnel of its own.
6. **Dialectic per cluster.** For EACH cluster, invoke the `dialectic` skill: 6. **Dialectic per cluster.** For EACH cluster, invoke the `kvb-think-dialectic` skill:
form a thesis about the root cause from evidence (the job log, its artifacts, form a thesis about the root cause from evidence (the job log, its artifacts,
the code at the run's commit, git history), attack it with independent the code at the run's commit, git history), attack it with independent
skeptics, and iterate to a ~99% thesis. skeptics, and iterate to a ~99% thesis.
@@ -70,18 +73,18 @@ jobs' logs, and a first clustering. The judgement in those steps stays yours.
is actually green, not just past the first error. is actually green, not just past the first error.
8. **Fix at the root, through the funnel.** Apply the real fix for each proven 8. **Fix at the root, through the funnel.** Apply the real fix for each proven
root cause in the cluster's sidetree, with a test and a negative control as root cause in the cluster's sidetree, with a test and a negative control as
the `funnel` skill demands. The fix enters the main line only at the funnel's the `kvb-org-split` skill demands. The fix enters the main line only at the funnel's
bar (at least 90 % fix confidence, under 5 % break risk) after its bar (at least 90 % fix confidence, under 5 % break risk) after its
independent review; below the bar the cluster ends as a proposal for the independent review; below the bar the cluster ends as a proposal for the
operator, never as a merge. Never mask a failure: no retry-until-pass, no operator, never as a merge. Never mask a failure: no retry-until-pass, no
disabling the check, no soft-skip. If a failure is genuinely external disabling the check, no soft-skip. If a failure is genuinely external
(upstream outage, flaky infra), confirm that with evidence and surface it (upstream outage, flaky infra), confirm that with evidence and surface it
honestly instead of fixing around it. Report every fix, every proposal, and honestly instead of fixing around it. Report every fix, every proposal, and
every abandoned attempt to the same agents through the `democracy` Peer protocol, every abandoned attempt to the same agents through the `kvb-org-organize` Peer protocol,
then remove the sidetree. then remove the sidetree.
9. **Tune what ran out of time.** Only for a cluster whose jobs died on their 9. **Tune what ran out of time.** Only for a cluster whose jobs died on their
time budget (timed out, or cancelled by the runner's limit), or when the time budget (timed out, or cancelled by the runner's limit), or when the
operator asks: hand the job log that ran furthest to the `tune` skill. Its operator asks: hand the job log that ran furthest to the `kvb-code-tune` skill. Its
candidates run through funnels of their own, separate from the cluster's fix candidates run through funnels of their own, separate from the cluster's fix
sidetree, and never delay or block the fix. sidetree, and never delay or block the fix.
10. **Follow the run.** While the run is still in progress, re-check it 10. **Follow the run.** While the run is still in progress, re-check it
@@ -1,9 +1,8 @@
--- ---
name: cleanup name: kvb-cleanup
description: > description: >
Free disk space agents filled: ask peer sessions what they still need, Free disk space agents filled: ask peers, inventory /tmp, Docker and agent
inventory /tmp, Docker and agent dirs, and delete only what the operator dirs, and delete only what the operator picks. Trigger on /kvb-cleanup, "aufräumen", "bereinigen", "platte voll", "no
picks. Trigger on /cleanup, "aufräumen", "bereinigen", "platte voll", "no
space left on device". space left on device".
--- ---
@@ -12,10 +11,10 @@ its owner, its size, and its risk.
## Step 0: the resource chair ## Step 0: the resource chair
Shared resources belong to the resource chair of the `democracy` skill. If Shared resources belong to the resource chair of the `kvb-org-organize` skill. If
another session holds the chair, send it the cleanup request and stop, unless another session holds the chair, send it the cleanup request and stop, unless
the operator ordered the cleanup in this session; then tell the chair before the operator ordered the cleanup in this session; then tell the chair before
step 1, so its grants and queue stay consistent. The `democracy` resource step 1, so its grants and queue stay consistent. The `kvb-org-organize` resource
watch already runs the configured low-risk cleanup at its stop threshold; this watch already runs the configured low-risk cleanup at its stop threshold; this
skill covers everything that needs the operator's pick. skill covers everything that needs the operator's pick.
@@ -36,6 +35,10 @@ need, one item per line:
- containers, compose or stack projects, volumes, images, networks - containers, compose or stack projects, volumes, images, networks
- running jobs (deploys, tests, builds) and when they finish - running jobs (deploys, tests, builds) and when they finish
Reply "nothing" if you need none of it. Items you do not answer for stay. Reply "nothing" if you need none of it. Items you do not answer for stay.
Also search your own worktrees and volumes for unused ones (branch merged or
pushed, no uncommitted changes, stack not needed), list them with their size
as deletion candidates, delete only your own finished items, and tell me what
you removed.
``` ```
## Step 2: inventory ## Step 2: inventory
@@ -56,9 +59,18 @@ Read-only, in parallel, sizes included:
record its volumes (`{{.Mounts}}`); for every volume no running container record its volumes (`{{.Mounts}}`); for every volume no running container
uses, record size, `CreatedAt` (`docker volume inspect`) and compose project uses, record size, `CreatedAt` (`docker volume inspect`) and compose project
label, anonymous ones included. label, anonymous ones included.
- **Worktrees**: per worktree its size (`du -sh`), its dirty state (`git
status --short`), whether its branch is pushed, and whether its HEAD is
already contained in an integration line: the default branch and every
branch the project merges features into (ask the operator or the project's
memory which ones). With `pkgmgr` installed, `pkgmgr branch gc --json
--sizes --merged-into <line>` (one per line) measures all of it; its
merged-clean items go to the `kvb-git-branch-gc` skill.
- **Other agent dirs**: package and tool caches (pip, npm, pre-commit, act, - **Other agent dirs**: package and tool caches (pip, npm, pre-commit, act,
gh, buildx), worktrees with `git status --short` and whether their branch is gh, buildx), Ansible and build temp dirs.
merged or pushed, Ansible and build temp dirs. - **Register**: `rasci.py artifacts` of the `kvb-org-artefacts` skill:
workdir, branch and artifacts of every task in the `kvb-org-rasci` register,
with its status and worker; `--ended` narrows it to the candidates.
- **Live use**: open handles (`fuser`, `lsof`) and running processes by name - **Live use**: open handles (`fuser`, `lsof`) and running processes by name
only (`ps -o pid,etime,comm`). Never print full command lines or only (`ps -o pid,etime,comm`). Never print full command lines or
environments: process arguments carry credentials. A sandboxed agent often environments: process arguments carry credentials. A sandboxed agent often
@@ -80,15 +92,20 @@ worktree or compose project name, a container label, an open handle, or
`unknown`. Then apply, first match wins: `unknown`. Then apply, first match wins:
1. Claimed by a peer: `keep`. 1. Claimed by a peer: `keep`.
2. Attributable to a peer that has not answered: `keep (no answer from 2. Listed in the register under a task that is not `done`, `stopped` or `stale`:
`keep (task <task>)`. Under an ended task it is a candidate whose owner is
the task's worker.
3. A job running on it (deploy, gate, test, swarm run, build): `keep
(running)`.
4. Attributable to a peer that has not answered: `keep (no answer from
<peer>)`. Silence is not consent. <peer>)`. Silence is not consent.
3. Archive duty: `archive first`. 5. Archive duty: `archive first`.
4. Everything else is a candidate with a risk class: 6. Everything else is a candidate with a risk class:
| Risk | Meaning | Typical items | | Risk | Meaning | Typical items |
| ---- | ------- | ------------- | | ---- | ------- | ------------- |
| low | regenerated automatically, cheap | stopped containers without volumes, dangling images, unused networks, scratch dirs and logs of finished sessions | | low | regenerated automatically, cheap | stopped containers without volumes, dangling images, unused networks, scratch dirs and logs of finished sessions |
| medium | regenerated, but slow, rate-limited or network-bound | unused tagged images (re-pull, registry limits), build cache (cold rebuilds), package caches | | medium | regenerated, but slow, rate-limited or network-bound | unused tagged images (re-pull, registry limits), build cache (cold rebuilds), package caches, clean worktrees whose HEAD is already in an integration line (ignored files such as `node_modules`, build output or evidence are lost, no commit) |
| high | data loss or a broken run possible | volumes, stopped containers with volumes, running unclaimed containers or stacks, worktrees with uncommitted or unpushed work, unread reports, anything `unknown` | | high | data loss or a broken run possible | volumes, stopped containers with volumes, running unclaimed containers or stacks, worktrees with uncommitted or unpushed work, unread reports, anything `unknown` |
## Step 4: propose ## Step 4: propose
@@ -145,9 +162,21 @@ ticked items. Revise repeats call 1; nothing is deleted before `Execute`.
rest, then put the refused item to the operator again as its own option: an rest, then put the refused item to the operator again as its own option: an
explicit renewed selection may pass; if it is refused again, hand over the explicit renewed selection may pass; if it is refused again, hand over the
exact command with its list file for the operator to run. exact command with its list file for the operator to run.
- Running stacks go down through the project's teardown target, never by - Never stop, take down or kill a running job, and never ask its owner to, to
free space: not for a priority slot, not at a disk stop. Free idle items,
ask the operator for an exception, or wait; an owner's own disk guard only
alerts the chair, and only the operator can order a running job down.
A hung job is not freed through cleanup: the `kvb-org-organize` skill's hang rule
lets only its owner end it.
- Idle running stacks go down through the project's teardown target, never by
removing their containers or volumes underneath them. removing their containers or volumes underneath them.
- Remove a worktree the way it was made: a slot worktree through the
project's teardown target (e.g. `make worktree-down`), run outside the
sandbox; a plain sidetree through `git worktree remove` without `--force`,
which refuses a dirty tree.
- Archive-first items: copy out, verify the copy, then delete. - Archive-first items: copy out, verify the copy, then delete.
- Drop every deleted item from its task as the `kvb-org-artefacts` skill
says (`set <task> --drop-artifact <kind>:<ref>`), so `artifacts` stays true.
- A delete failing on permissions (files owned by a container UID): use the - A delete failing on permissions (files owned by a container UID): use the
project's container helper. Never sudo, never bypass the sandbox; without a project's container helper. Never sudo, never bypass the sandbox; without a
helper, report the path. helper, report the path.
+55
View File
@@ -0,0 +1,55 @@
---
name: kvb-coach-meditation
description: >
Short meditation with generative alpha-wave music, animated fractals and
breathing cues, tuned by feedback. Trigger on /kvb-coach-meditation.
---
A short session in the browser: `assets/meditation.html` plays generative
ambient music over an alpha-wave layer (8–13 Hz) and animates a Julia fractal
that breathes with the cue. It runs locally, without audio files or network.
`${CLAUDE_SKILL_DIR}/scripts/meditation.py` keeps the settings in the
gitignored `.state/meditation.json` of the skills repository and writes the
session page with the settings embedded:
```bash
meditation.py --state <skills repo>/.state/meditation.json set <key>=<value> ...
meditation.py --state <same> show
meditation.py --state <same> session --page ${CLAUDE_SKILL_DIR}/assets/meditation.html --out <skills repo>/.state/meditation-session.html
```
| Key | Values | First session |
| --- | --- | --- |
| `minutes` | 1–30 | 5 |
| `beat` | 8–13 Hz | 10 |
| `base` | 100–400 Hz | 196 |
| `volume` | 0.05–1 | 0.4 |
| `mode` | `isochronic` (speakers), `binaural` (headphones) | `isochronic` |
| `palette` | `aurora`, `ocean`, `ember`, `forest` | `aurora` |
| `breathing` | `box` (4-4-4-4), `478`, `none` | `box` |
## Step 1: start
Without a state file, `set` the first-session values. Write the session page
and hand the operator `xdg-open <session page>` to run: inside the agent
sandbox `xdg-open` and `gio open` exit 0 without opening anything. The page starts on the
operator's click, shows the elapsed time as a line growing along the bottom
edge, and fades out after the set minutes; `+` and `-` set the
picture's brightness (20–160 %), which the browser keeps for the next session.
## Step 2: feedback
After every session, run the `kvb-think-listen` skill with one
`AskUserQuestion`, `multiSelect: false` per question: music (calmer, fits,
livelier, quieter, louder), picture (fits, calmer, other palette), length
(shorter, fits, longer), effect (calmer, more focused, nothing, restless).
Ask about headphones once before switching to `binaural`.
## Step 3: optimize
One `set` per answer: quieter or louder ±0.1 `volume`; calmer music `beat`
−1, more focus `beat` +1; shorter or longer ±2 `minutes`; other palette the
next one; restless `breathing=none`. Log the session in the `kvb-coach`
profile (`coach.py done psyche "Meditation <n> min"`) and a liked effect with
`coach.py like psyche`.
@@ -0,0 +1,260 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>kvb-coach-meditation</title>
<style>
html, body { margin: 0; height: 100%; background: #000; color: #eee; font-family: "URW Gothic", "Fira Sans", "Cantarell", sans-serif; font-weight: 300; overflow: hidden; }
canvas { position: fixed; inset: 0; width: 100%; height: 100%; }
#ui { position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1rem; text-align: center; padding: 1rem; }
#breath { font-size: 2.6rem; text-transform: lowercase; letter-spacing: .4em; color: #fff; text-shadow: 0 0 14px #fff9, 0 0 40px #9cf8, 0 0 80px #c9f5; will-change: transform, opacity, letter-spacing; }
#clock { position: fixed; bottom: 1rem; right: 1rem; opacity: .5; font-variant-numeric: tabular-nums; }
#progress { position: fixed; left: 0; bottom: 0; height: 3px; width: 0; background: linear-gradient(90deg, #9cf, #c9f); box-shadow: 0 0 12px #9cf9; opacity: .7; }
#level { position: fixed; top: 1rem; right: 1rem; opacity: 0; transition: opacity .6s; letter-spacing: .2em; text-shadow: 0 0 10px #fff8; }
button { font: inherit; font-size: 1.4rem; letter-spacing: .4em; text-transform: lowercase; padding: .8rem 2.4rem; border-radius: 2rem; border: 1px solid #fff6; background: #fff1; color: #fff; cursor: pointer; text-shadow: 0 0 12px #fff8; }
</style>
</head>
<body>
<canvas id="view"></canvas>
<div id="ui"><div id="breath"></div><button id="start">Start</button></div>
<div id="clock"></div>
<div id="level"></div>
<div id="progress"></div>
<script>
"use strict";
const KEYS = ["minutes", "beat", "base", "volume", "mode", "palette", "breathing"];
const PALETTES = {
aurora: [[0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [1.0, 1.0, 1.0], [0.0, 0.33, 0.67]],
ocean: [[0.2, 0.4, 0.6], [0.2, 0.3, 0.4], [1.0, 1.0, 1.0], [0.0, 0.1, 0.2]],
ember: [[0.5, 0.3, 0.2], [0.5, 0.3, 0.2], [1.0, 1.0, 1.0], [0.0, 0.1, 0.2]],
forest: [[0.3, 0.5, 0.3], [0.2, 0.4, 0.2], [1.0, 1.0, 1.0], [0.1, 0.2, 0.3]],
};
const PATTERNS = {
box: [["Breathe in", 4], ["Hold", 4], ["Breathe out", 4], ["Hold", 4]],
"478": [["Breathe in", 4], ["Hold", 7], ["Breathe out", 8]],
none: [],
};
const SETTINGS = null;
const breathEl = document.getElementById("breath");
const clockEl = document.getElementById("clock");
const startEl = document.getElementById("start");
const missing = KEYS.filter((key) => !SETTINGS || !(key in SETTINGS));
const cfg = Object.fromEntries(KEYS.map((key) => [key, SETTINGS ? SETTINGS[key] : null]));
["minutes", "beat", "base", "volume"].forEach((key) => { cfg[key] = Number(cfg[key]); });
if (missing.length || !PALETTES[cfg.palette] || !PATTERNS[cfg.breathing]) {
startEl.remove();
breathEl.textContent = "Start the session through the kvb-coach-meditation skill.";
throw new Error(`invalid settings: ${missing.join(", ")}`);
}
function breath(t) {
const steps = PATTERNS[cfg.breathing];
if (!steps.length) return ["", 0.5 + 0.5 * Math.sin(t * 0.2)];
const total = steps.reduce((sum, [, d]) => sum + d, 0);
let x = t % total;
let lung = 0;
for (const [label, d] of steps) {
if (x < d) {
if (label === "Breathe in") return [label, x / d];
if (label === "Breathe out") return [label, 1 - x / d];
return [label, lung];
}
x -= d;
if (label === "Breathe in") lung = 1;
if (label === "Breathe out") lung = 0;
}
return ["", lung];
}
const canvas = document.getElementById("view");
const gl = canvas.getContext("webgl");
let draw = () => {};
if (gl) {
const VERT = "attribute vec2 p; void main() { gl_Position = vec4(p, 0.0, 1.0); }";
const FRAG = `precision highp float;
uniform vec2 res; uniform float t; uniform float lung; uniform vec3 pa, pb, pc, pd;
vec3 pal(float x) { return pa + pb * cos(6.28318 * (pc * x + pd)); }
void main() {
vec2 z = (2.0 * gl_FragCoord.xy - res) / min(res.x, res.y) * (1.6 - 0.3 * lung);
vec2 c = 0.7885 * vec2(cos(t * 0.05), sin(t * 0.05));
float n = 0.0;
for (int i = 0; i < 160; i++) {
z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c;
if (dot(z, z) > 16.0) break;
n += 1.0;
}
float s = (n - log2(log2(max(dot(z, z), 1.0001))) + 4.0) / 160.0;
gl_FragColor = vec4(pal(s + t * 0.01) * (n < 160.0 ? 1.0 : 0.05), 1.0);
}`;
const shader = (type, src) => {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
return s;
};
const prog = gl.createProgram();
gl.attachShader(prog, shader(gl.VERTEX_SHADER, VERT));
gl.attachShader(prog, shader(gl.FRAGMENT_SHADER, FRAG));
gl.linkProgram(prog);
gl.useProgram(prog);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW);
const loc = gl.getAttribLocation(prog, "p");
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
const u = (name) => gl.getUniformLocation(prog, name);
["pa", "pb", "pc", "pd"].forEach((name, i) => gl.uniform3fv(u(name), PALETTES[cfg.palette][i]));
const resize = () => {
const scale = Math.min(window.devicePixelRatio || 1, 1.5);
canvas.width = Math.floor(innerWidth * scale);
canvas.height = Math.floor(innerHeight * scale);
gl.viewport(0, 0, canvas.width, canvas.height);
};
addEventListener("resize", resize);
resize();
draw = (t, lung) => {
gl.uniform2f(u("res"), canvas.width, canvas.height);
gl.uniform1f(u("t"), t);
gl.uniform1f(u("lung"), lung);
gl.drawArrays(gl.TRIANGLES, 0, 6);
};
draw(0, 0);
}
const levelEl = document.getElementById("level");
const progressEl = document.getElementById("progress");
const BRIGHTNESS = "kvb-coach-meditation-brightness";
const stored = () => {
try {
return Number(localStorage.getItem(BRIGHTNESS)) || 1;
} catch {
return 1;
}
};
const store = (value) => {
try {
localStorage.setItem(BRIGHTNESS, String(value));
return true;
} catch {
return false;
}
};
let brightness = stored();
let levelTimer = 0;
const showBrightness = () => {
canvas.style.filter = `brightness(${brightness})`;
};
showBrightness();
addEventListener("keydown", (event) => {
const step = { "+": 0.1, "=": 0.1, "-": -0.1 }[event.key];
if (!step) return;
brightness = Math.min(1.6, Math.max(0.2, Math.round((brightness + step) * 10) / 10));
showBrightness();
const saved = store(brightness);
levelEl.textContent = `☀ ${Math.round(brightness * 100)} %${saved ? "" : " (not saved)"}`;
levelEl.style.opacity = "1";
clearTimeout(levelTimer);
levelTimer = setTimeout(() => { levelEl.style.opacity = "0"; }, 1200);
});
function sound(ctx) {
const master = ctx.createGain();
master.gain.setValueAtTime(0, ctx.currentTime);
master.gain.linearRampToValueAtTime(cfg.volume, ctx.currentTime + 8);
master.connect(ctx.destination);
const reverb = ctx.createConvolver();
const length = ctx.sampleRate * 4;
const impulse = ctx.createBuffer(2, length, ctx.sampleRate);
for (let ch = 0; ch < 2; ch++) {
const data = impulse.getChannelData(ch);
for (let i = 0; i < length; i++) data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, 3);
}
reverb.buffer = impulse;
reverb.connect(master);
if (cfg.mode === "binaural") {
const merger = ctx.createChannelMerger(2);
[cfg.base, cfg.base + cfg.beat].forEach((freq, ch) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.frequency.value = freq;
gain.gain.value = 0.12;
osc.connect(gain).connect(merger, 0, ch);
osc.start();
});
merger.connect(master);
} else {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
const lfo = ctx.createOscillator();
const depth = ctx.createGain();
osc.frequency.value = cfg.base;
gain.gain.value = 0.06;
lfo.frequency.value = cfg.beat;
depth.gain.value = 0.06;
lfo.connect(depth).connect(gain.gain);
osc.connect(gain).connect(master);
osc.start();
lfo.start();
}
const ratios = [1, 9 / 8, 5 / 4, 3 / 2, 5 / 3, 2];
const note = () => {
if (ctx.state === "closed") return;
const now = ctx.currentTime;
const freq = cfg.base * ratios[Math.floor(Math.random() * ratios.length)];
const gain = ctx.createGain();
gain.gain.setValueAtTime(0, now);
gain.gain.linearRampToValueAtTime(0.1, now + 3);
gain.gain.linearRampToValueAtTime(0, now + 9);
gain.connect(reverb);
[0, 4].forEach((cents) => {
const osc = ctx.createOscillator();
osc.type = "triangle";
osc.frequency.value = freq;
osc.detune.value = cents;
osc.connect(gain);
osc.start(now);
osc.stop(now + 9.5);
});
setTimeout(note, 2500 + Math.random() * 3500);
};
note();
return master;
}
startEl.addEventListener("click", () => {
startEl.remove();
const ctx = new AudioContext();
const master = sound(ctx);
const begin = performance.now();
const end = cfg.minutes * 60;
const frame = (now) => {
const t = (now - begin) / 1000;
const [label, lung] = breath(t);
draw(t, lung);
progressEl.style.width = `${Math.min(100, (t / end) * 100)}%`;
const left = Math.max(0, end - t);
clockEl.textContent = `${Math.floor(left / 60)}:${String(Math.floor(left % 60)).padStart(2, "0")}`;
breathEl.style.opacity = String(0.35 + 0.65 * lung);
breathEl.style.letterSpacing = `${0.3 + 0.5 * lung}em`;
breathEl.style.transform = `scale(${0.9 + 0.2 * lung})`;
if (t < end) {
breathEl.textContent = label;
requestAnimationFrame(frame);
return;
}
master.gain.cancelScheduledValues(ctx.currentTime);
master.gain.setValueAtTime(master.gain.value, ctx.currentTime);
master.gain.linearRampToValueAtTime(0, ctx.currentTime + 6);
setTimeout(() => ctx.close(), 6500);
breathEl.style.opacity = "1";
breathEl.style.transform = "none";
breathEl.style.letterSpacing = ".2em";
breathEl.textContent = "Done. Back to your agent for a short feedback.";
};
requestAnimationFrame(frame);
});
</script>
</body>
</html>
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""Keep the kvb-coach-meditation settings and write the session page.
Args:
--state: path of the settings JSON file.
set <key>=<value> ...: change settings; every value is validated.
show: print the settings.
session --page <meditation.html> --out <file>: write the page with the
settings embedded and print its path; fails while a setting is missing.
Exit codes:
0 done, 2 usage error.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
PLACEHOLDER = "const SETTINGS = null;"
RANGES = {
"minutes": (int, 1, 30),
"beat": (float, 8.0, 13.0),
"base": (float, 100.0, 400.0),
"volume": (float, 0.05, 1.0),
}
CHOICES = {
"mode": ("binaural", "isochronic"),
"palette": ("aurora", "ocean", "ember", "forest"),
"breathing": ("box", "478", "none"),
}
KEYS = (*RANGES, *CHOICES)
def fail(message: str) -> None:
print(f"meditation: {message}", file=sys.stderr)
sys.exit(2)
def parse(pair: str) -> tuple[str, int | float | str]:
key, sep, raw = pair.partition("=")
if not sep or key not in KEYS:
fail(f"{pair}: expected <key>=<value> with key in {', '.join(KEYS)}")
if key in CHOICES:
if raw not in CHOICES[key]:
fail(f"{key} must be one of {', '.join(CHOICES[key])}")
return key, raw
kind, low, high = RANGES[key]
try:
value = kind(raw)
except ValueError:
fail(f"{key} must be a number")
if not low <= value <= high:
fail(f"{key} must lie between {low} and {high}")
return key, value
def load(path: Path) -> dict:
try:
return json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError:
return {}
except ValueError:
fail(f"{path} is not valid JSON")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--state", required=True)
sub = parser.add_subparsers(dest="cmd", required=True)
sub.add_parser("set").add_argument("pairs", nargs="+")
sub.add_parser("show")
p = sub.add_parser("session")
p.add_argument("--page", required=True)
p.add_argument("--out", required=True)
args = parser.parse_args()
path = Path(args.state)
settings = load(path)
if args.cmd == "set":
settings.update(parse(pair) for pair in args.pairs)
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(".tmp")
tmp.write_text(json.dumps(settings, indent=1, sort_keys=True), encoding="utf-8")
tmp.replace(path)
if args.cmd in ("set", "show"):
for key in KEYS:
print(f"{key}={settings.get(key, '-')}")
return 0
missing = [key for key in KEYS if key not in settings]
if missing:
fail(f"missing settings: {', '.join(missing)}")
page = Path(args.page)
if not page.is_file():
fail(f"{page} does not exist")
template = page.read_text(encoding="utf-8")
if template.count(PLACEHOLDER) != 1:
fail(f"{page} lacks the line {PLACEHOLDER}")
embedded = json.dumps({key: settings[key] for key in KEYS})
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(template.replace(PLACEHOLDER, f"const SETTINGS = {embedded};"), encoding="utf-8")
print(out.resolve())
return 0
if __name__ == "__main__":
sys.exit(main())
+80
View File
@@ -0,0 +1,80 @@
---
name: kvb-coach
description: >
Coach the operator's next step: psyche, body, sleep, food, own and
delegable tasks. Trigger on /kvb-coach or "was soll ich tun".
---
Be the operator's coach: short, warm, concrete, with emojis, in the
operator's language. Their health comes first; work is one area of six, not
the default. Suggestions, never pressure.
The profile keeps what the operator enjoys, dislikes and last did, host-local
in the gitignored `.state/` of the skills repository:
```bash
coach.py --profile <skills repo>/.state/coach.json like|dislike|forget <area> "<thing>"
coach.py --profile <same> done <area> "<thing>" [--at <iso>]
coach.py --profile <same> show [--json]
```
Areas: `psyche`, `body`, `sleep`, `food`, `tasks`, `delegate`. Every coaching
skill beyond this one is named `kvb-coach-<name>`.
`hooks/reminder.sh` triggers the short form at the first prompt of every full
hour, once across all agents (stamped in `~/.claude/.coach-hours/`): at most
three lines, the most urgent area first, then the agent continues with the
operator's request.
## Step 1: read the situation
- **Time**: local time and weekday, and how long the operator has been at it
today (their first message of the day in the session transcripts).
- **Profile**: `coach.py show`: likes, dislikes, hours since the last sleep,
meal, workout or break.
- **Own tasks**: the open commands of the `kvb-org-operator` skill and the
decisions agents wait on.
- **Delegable**: what the operator did by hand that an agent could do, and
queued `kvb-org-rasci` register tasks without a worker.
## Step 2: suggest
At most one line per area, the most urgent first, the whole answer on one
screen:
- 🧠 **Psyche**: a short break, a walk outside, five minutes with the
`kvb-coach-meditation` skill or box breathing, a call with a friend,
something just for fun.
- 💪 **Body**: a workout from the operator's likes (a 20-minute run, a
bodyweight circuit, cycling, yoga), stretching neck, hips and back after
long sitting, standing up every hour.
- 😴 **Sleep**: wind down, screens off, seven to nine hours; a 20-minute nap
in the early afternoon.
- 🥗 **Food**: water, a real meal with vegetables and protein, a healthy
snack instead of sugar, no caffeine late in the day.
- ✅ **Your tasks**: the one step only the operator can take that unblocks
the most.
- 🤖 **Delegate**: what an agent can take over, with the skill or command
that does it.
The situation sets the order: late evening puts sleep first, hours without
a break put moving first, a meal more than five hours ago puts food first.
Prefer what the operator likes, never offer a dislike, and do not repeat the
last tip of an area. Close with one encouraging line.
## Step 3: listen and remember
Often, not every time: when the profile is empty, after a declined
suggestion, and every few runs, run the `kvb-think-listen` skill with one
short `AskUserQuestion` (what they enjoy, what fits right now, what to
avoid). Record the answers with `like` and `dislike`, and what the operator
reports having done with `done`.
## Rules
- The operator decides: no guilt, no nagging, a declined tip stays off for
the day.
- General wellbeing, no medical advice: pain, illness or lasting exhaustion
get one kind pointer to a doctor.
- The profile never leaves the host: not into commits, memory files or peer
messages.
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
hours="${COACH_STAMP_DIR:-${HOME}/.claude/.coach-hours}"
hour="${COACH_HOUR:-$(date +%Y-%m-%dT%H)}"
mkdir -p "${hours}"
mkdir "${hours}/${hour}" 2>/dev/null || exit 0
find "${hours}" -mindepth 1 -maxdepth 1 -type d ! -name "${hour}" -exec rmdir {} +
cat <<'HINT'
COACH HOUR: a new full hour began. Before answering the operator's request,
run the kvb-coach skill in its short form: at most three lines, the most
urgent area first, then continue with the request.
HINT
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
"""Keep the operator's coaching profile: likes, dislikes and when what was done.
Args:
--profile: path of the profile JSON file.
like|dislike <area> <thing>: record a preference; one replaces the other.
forget <area> <thing>: drop a preference.
done <area> <thing> [--at <iso>]: log that the operator did it.
show [--now <iso>] [--json]: likes, dislikes and the last logged activity
per area with the hours since.
Exit codes:
0 done, 2 usage error.
"""
from __future__ import annotations
import argparse
import datetime
import json
import sys
from pathlib import Path
AREAS = ("psyche", "body", "sleep", "food", "tasks", "delegate")
LOG_LIMIT = 500
def fail(message: str) -> None:
print(f"coach: {message}", file=sys.stderr)
sys.exit(2)
def moment(value: str | None) -> datetime.datetime:
if value is None:
return datetime.datetime.now(datetime.UTC)
try:
parsed = datetime.datetime.fromisoformat(value)
except ValueError:
fail(f"{value} is not an ISO timestamp")
return parsed if parsed.tzinfo else parsed.replace(tzinfo=datetime.UTC)
def load(path: Path) -> dict:
try:
data = json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError:
return {"likes": {}, "dislikes": {}, "log": []}
except ValueError:
fail(f"{path} is not valid JSON")
return data
def save(path: Path, data: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(".tmp")
tmp.write_text(
json.dumps(data, indent=1, sort_keys=True, ensure_ascii=False), encoding="utf-8"
)
tmp.replace(path)
def prefer(data: dict, keep: str, area: str, thing: str) -> None:
for key in ("likes", "dislikes"):
items = data[key].setdefault(area, [])
if thing in items:
items.remove(thing)
if keep:
data[keep][area].append(thing)
def summary(data: dict, now: datetime.datetime) -> list[dict]:
rows = []
for area in AREAS:
last = next((e for e in reversed(data["log"]) if e["area"] == area), None)
hours = None
if last:
hours = round((now - moment(last["at"])).total_seconds() / 3600, 1)
rows.append(
{
"area": area,
"likes": data["likes"].get(area, []),
"dislikes": data["dislikes"].get(area, []),
"last": last["thing"] if last else None,
"hours_ago": hours,
}
)
return rows
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--profile", required=True)
sub = parser.add_subparsers(dest="cmd", required=True)
for name in ("like", "dislike", "forget"):
p = sub.add_parser(name)
p.add_argument("area", choices=AREAS)
p.add_argument("thing")
p = sub.add_parser("done")
p.add_argument("area", choices=AREAS)
p.add_argument("thing")
p.add_argument("--at")
p = sub.add_parser("show")
p.add_argument("--now")
p.add_argument("--json", action="store_true")
args = parser.parse_args()
path = Path(args.profile)
data = load(path)
if args.cmd == "show":
rows = summary(data, moment(args.now))
if args.json:
print(json.dumps(rows, indent=1, ensure_ascii=False))
return 0
for row in rows:
last = f"{row['last']} {row['hours_ago']}h ago" if row["last"] else "-"
print(
f"{row['area']}\tlast: {last}\tlikes: {', '.join(row['likes']) or '-'}"
f"\tdislikes: {', '.join(row['dislikes']) or '-'}"
)
return 0
if args.cmd == "done":
data["log"].append(
{"area": args.area, "thing": args.thing, "at": moment(args.at).isoformat()}
)
data["log"] = data["log"][-LOG_LIMIT:]
else:
keep = {"like": "likes", "dislike": "dislikes"}.get(args.cmd, "")
prefer(data, keep, args.area, args.thing)
save(path, data)
print(f"{args.cmd} {args.area} {args.thing}")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -1,10 +1,10 @@
--- ---
name: comments-clean name: kvb-code-no-comments
description: > description: >
Purge explanatory comments from staged and unstaged changes. Run Purge explanatory comments from staged and unstaged changes. Run
AUTOMATICALLY before every commit or stage request (commit, commit AUTOMATICALLY before every commit or stage request (commit, commit
no-verify, stage next, socon, sobu, socobu, "commite ..."); also on no-verify, stage next, "commite ..."); also on /kvb-code-no-comments or
/comments-clean or comment complaints. comment complaints.
--- ---
When the operator asks to commit or stage, run this audit first and only When the operator asks to commit or stage, run this audit first and only
@@ -32,7 +32,7 @@ added lines only) and delete every explanatory comment:
both the lint and the language parser can pass over code that no longer both the lint and the language parser can pass over code that no longer
executes. Diff against the pre-edit revision (`git show HEAD:<path>`) and executes. Diff against the pre-edit revision (`git show HEAD:<path>`) and
confirm that only comment lines left. confirm that only comment lines left.
5. Do not stage in order to run a check (the `commit-next` skill's step 4 5. Do not stage in order to run a check (the `kvb-git-commit-next` skill's step 4
says why). Re-stage after the audit exactly the files that were staged says why). Re-stage after the audit exactly the files that were staged
before it. before it.
6. Penalty: if this audit finds anything you wrote yourself, admit it in 6. Penalty: if this audit finds anything you wrote yourself, admit it in
@@ -1,10 +1,9 @@
--- ---
name: no-defaults name: kvb-code-no-defaults
description: > description: >
Forbid default values for parameters, settings and environment lookups; Forbid default values for parameters, settings and environment lookups;
demand explicit values and fail loudly. Apply AUTOMATICALLY on every task demand explicit values. Apply AUTOMATICALLY whenever code, config,
that writes, edits, generates, refactors or reviews code, config, templates, templates or infrastructure are written, edited or reviewed.
schemas or infrastructure.
--- ---
A default value is a guess, written at the point where the caller's intent is A default value is a guess, written at the point where the caller's intent is
@@ -1,9 +1,9 @@
--- ---
name: refactor name: kvb-code-refactor
description: > description: >
Refactor every uncommitted file (staged, unstaged, untracked) up to the Refactor every uncommitted file up to the project's standards without
project's standards without changing behaviour. Trigger on /refactor or when changing behaviour. Trigger on /kvb-code-refactor or asked to clean up,
the operator asks to clean up, tidy or polish the current changes. tidy or polish the current changes.
--- ---
The unit of work is the uncommitted change set, not the repository. Behaviour The unit of work is the uncommitted change set, not the repository. Behaviour
@@ -80,7 +80,7 @@ Concretely:
returning early; replace magic values with a named constant at its use site. returning early; replace magic values with a named constant at its use site.
- **Errors**: never widen a caught exception, never swallow one, never remove a - **Errors**: never widen a caught exception, never swallow one, never remove a
validation or a security check to make the code shorter. validation or a security check to make the code shorter.
- **Comments**: invoke the `comments-clean` skill rather than reimplementing its - **Comments**: invoke the `kvb-code-no-comments` skill rather than reimplementing its
rules here. rules here.
Do not reformat regions you have no substantive fix for - cosmetic churn buries Do not reformat regions you have no substantive fix for - cosmetic churn buries
@@ -1,10 +1,9 @@
--- ---
name: scope-guard name: kvb-code-scope-guard
description: > description: >
Deliver exactly what was asked and nothing more. Trigger on every task that Deliver exactly what was asked and nothing more. Trigger on every task that
creates files, commands or commits, on unreviewed autonomous runs, on creates files, commands or commits, on autonomous runs and hand-overs, and
hand-over requests, and when the operator asks why something unrequested when asked why something unrequested exists.
exists.
--- ---
The requested scope is the deliverable. A problem you noticed behind the The requested scope is the deliverable. A problem you noticed behind the
@@ -1,9 +1,9 @@
--- ---
name: test name: kvb-code-test
description: > description: >
Discover the Makefile's test targets, let the operator pick, run and report; Run the Makefile test targets the operator picks and report; fix mode loops
fix mode loops until green. Trigger on /test, /test --fix, or when the until green. Trigger on /kvb-code-test [--fix] or asked to run or fix the
operator asks to run the tests, make them pass, or get the suite green. tests.
--- ---
The Makefile is the source of truth for how this project runs tests. Never The Makefile is the source of truth for how this project runs tests. Never
@@ -73,7 +73,7 @@ quoted verbatim. Never paraphrase a failure. Then one line with the exact
command to reproduce the failure alone. command to reproduce the failure alone.
Do not fix what failed unless the operator asks - report first. A request to Do not fix what failed unless the operator asks - report first. A request to
make the tests pass, fix them, or get the suite green (`/test --fix`) switches make the tests pass, fix them, or get the suite green (`/kvb-code-test --fix`) switches
to fix mode. to fix mode.
## Fix mode ## Fix mode
@@ -87,8 +87,8 @@ after three simultaneous changes proves nothing about which one mattered.
- **Run** the failing target alone, the fastest command that reproduces the - **Run** the failing target alone, the fastest command that reproduces the
failure. Widen back to the full selection only when that target is green. failure. Widen back to the full selection only when that target is green.
- **Diagnose** from the actual error output before touching a file. Apply the - **Diagnose** from the actual error output before touching a file. Apply the
`triage` skill to reach a verified root cause; for a cause still unclear `kvb-ci-triage` skill to reach a verified root cause; for a cause still unclear
after one look, apply `dialectic` rather than guessing twice. Say in one line after one look, apply `kvb-think-dialectic` rather than guessing twice. Say in one line
which side is wrong: which side is wrong:
- **Code**: the test states the intended behaviour. Fix the code. - **Code**: the test states the intended behaviour. Fix the code.
- **Test**: it encodes an outdated or incorrect expectation. Fix it only with - **Test**: it encodes an outdated or incorrect expectation. Fix it only with
@@ -98,7 +98,7 @@ after three simultaneous changes proves nothing about which one mattered.
unbuilt artefact. Fix the environment or report it; do not patch code unbuilt artefact. Fix the environment or report it; do not patch code
around it. around it.
- **Fix** with the smallest change that addresses the root cause, in the - **Fix** with the smallest change that addresses the root cause, in the
project's idioms and under the `no-defaults` and `comments-clean` rules. project's idioms and under the `kvb-code-no-defaults` and `kvb-code-no-comments` rules.
Forbidden without explicit operator approval: deleting, skipping, `xfail`-ing Forbidden without explicit operator approval: deleting, skipping, `xfail`-ing
or commenting out a failing test; loosening an assertion to what the code or commenting out a failing test; loosening an assertion to what the code
produces; swallowing the exception the test surfaces; retries, sleeps or produces; swallowing the exception the test surfaces; retries, sleeps or
@@ -126,5 +126,5 @@ Emit one line per iteration: target, failure, hypothesis, change. The final
report gives per iteration what failed, the root cause, the fix and the report gives per iteration what failed, the root cause, the fix and the
result; the final state of every selected target with the verbatim output of result; the final state of every selected target with the verbatim output of
anything still failing; everything deliberately not done; and the command anything still failing; everything deliberately not done; and the command
reproducing the final state. State confidence per the `confidence` skill. Do reproducing the final state. State confidence per the `kvb-check-confidence` skill. Do
not commit unless the operator asked for a commit. not commit unless the operator asked for a commit.
+62
View File
@@ -0,0 +1,62 @@
---
name: kvb-code-transcend
description: >
Carry the session's ad-hoc tests and scripts into the current repo.
Trigger on /kvb-code-transcend.
---
The test the agent ran from its scratchpad, the probe that proved a cause, the
benchmark behind an optimization: they protected this session's work and
nothing after it. This skill moves them into the repository the agent works
in (`git rev-parse --show-toplevel`), in that repository's own form. It
changes nothing before the operator confirms.
## Step 1: inventory
Read-only:
- **Tests and scripts outside the repo**: `transcend.py scan` of the
`kvb-transcend` skill lists them in the own and peers' scratchpads and
skips what was already decided and has not changed; peer candidates follow
that skill's consent step. Add the `python -c` checks and negative controls
this session ran inline.
- **Probes and reproductions** that proved a root cause or a fix.
- **Measurements**: prototypes and before/after harnesses of the
`kvb-code-tune` skill.
- **Repeated commands** a make target or script of the repository could own.
- **Untracked helpers** the agent left inside the repository.
One line each: `candidate -> source -> what it guards`.
## Step 2: classify
Read the repository's rules first (AGENTS.md, CONTRIBUTING, the Makefile, the
test layout). Per candidate one target in that repository's form: a test in
its test tree and framework, a make target, a script under its scripts
directory, a CI step, or drop. No new framework, dependency or language
without the operator. A kept optimization keeps its measurement as a
benchmark or regression test.
## Step 3: listen and confirm
Run the `kvb-think-listen` skill on the open points: target path, framework,
which repository line owns the file. Then `AskUserQuestion`: one
`multiSelect` with the candidates, nothing preselected, and per pick a
single-select `Write it` / `Revise it` / `Discard` with the drafted diff.
## Step 4: write
- Write only the confirmed files, path-scoped; a peer's line goes to its
owner through the `kvb-org-coordinate` skill.
- Every carried test fails without the code it guards: show the negative
control once.
- Run the repository's tests only on the resource chair's grant (the
`kvb-org-organize` skill); leave the result uncommitted for the
`kvb-git-commit-next` skill.
- `transcend.py mark` every offered candidate with its verdict.
## Output
| Candidate | Source | Target in the repo | Verdict |
Dropped candidates stay with their reason.
@@ -1,10 +1,9 @@
--- ---
name: tune name: kvb-code-tune
description: > description: >
Find and apply performance optimizations (CI, deploy, build, gate, hot Speed up CI, deploys, builds and hot paths without losing logic or
paths) without reducing logic, coverage or correctness. Trigger on /tune, coverage. Trigger on /kvb-code-tune, a job hitting its time budget, or
when a job dies on its time budget, when a pipeline or deploy grows slower, asked where time goes.
or asked where time goes.
--- ---
# Tune # Tune
@@ -14,7 +13,7 @@ correctness counts as a speed-up.**
## Once per session: listen first ## Once per session: listen first
The first time this skill runs in a session, invoke the `active-listening` The first time this skill runs in a session, invoke the `kvb-think-listen`
skill before measuring anything, and ask three questions through the host's skill before measuring anything, and ask three questions through the host's
structured question mechanism. Offer each as a checklist (multi-select) with structured question mechanism. Offer each as a checklist (multi-select) with
suggestions, and allow free text: suggestions, and allow free text:
@@ -22,8 +21,8 @@ suggestions, and allow free text:
1. **What to examine.** Suggestions: CI wall-clock, deploy duration, gate and 1. **What to examine.** Suggestions: CI wall-clock, deploy duration, gate and
test runtime, image pulls and build caches, per-item loops and process test runtime, image pulls and build caches, per-item loops and process
spawns, idempotency (work repeated on a second run), network round trips, spawns, idempotency (work repeated on a second run), network round trips,
resource use (CPU, memory, disk, I/O), and language or runtime choice for and resource use (CPU, memory, disk, I/O). Language, framework and
hot paths. recursion are checked on every run anyway (see Always check).
2. **Which performance boundaries to keep.** Suggestions: time budgets per job, 2. **Which performance boundaries to keep.** Suggestions: time budgets per job,
deploy or gate; resource caps (memory, CPU, disk headroom); a minimum saving deploy or gate; resource caps (memory, CPU, disk headroom); a minimum saving
per change (default 10 s per run); the measurement source that counts as per change (default 10 s per run); the measurement source that counts as
@@ -31,13 +30,14 @@ suggestions, and allow free text:
3. **Which limits apply to optimizations.** Suggestions: the scope that may be 3. **Which limits apply to optimizations.** Suggestions: the scope that may be
changed (files, roles, services); whether new dependencies, toolchains or changed (files, roles, services); whether new dependencies, toolchains or
languages may be introduced; whether infrastructure or CI configuration may languages may be introduced; whether infrastructure or CI configuration may
change; the merge bar (default: the `funnel` skill's 90 % confidence and change; the merge bar (default: the `kvb-org-split` skill's 90 % confidence and
under 5 % break risk); a time cap per delegated agent (default 60 minutes). under 5 % break risk); a time cap per delegated agent (default 60 minutes).
Keep the answers for the rest of the session and do not ask again. Work only Keep the answers for the rest of the session and do not ask again. Work only
inside them; a candidate outside them becomes a proposal. inside them; a candidate outside them becomes a proposal.
When another skill (triage, observe) or an autonomous run invoked tune, skip When another skill (kvb-ci-triage, kvb-ci-observe) or an autonomous run invoked
this skill, skip
the questions: examine the handed-over log, apply the defaults above, and ask the questions: examine the handed-over log, apply the defaults above, and ask
only when an operator-only fact blocks. only when an operator-only fact blocks.
@@ -92,7 +92,7 @@ Discard a before/after pair, not just its noise, when:
## Before you optimize ## Before you optimize
- **Search every branch** (the merge-fix skill's `find_fix.py -S <symbol> --path <path>`) for a change - **Search every branch** (the kvb-git-merge-fix skill's `find_fix.py -S <symbol> --path <path>`) for a change
that already removes or reshapes the step. A fix on a sibling branch can make that already removes or reshapes the step. A fix on a sibling branch can make
the whole candidate worth zero. the whole candidate worth zero.
- A step that is long by design (a backup and restore drill, an end-to-end - A step that is long by design (a backup and restore drill, an end-to-end
@@ -106,19 +106,38 @@ Discard a before/after pair, not just its noise, when:
A cost cluster is one place the time goes: one step, one hot path, one A cost cluster is one place the time goes: one step, one hot path, one
repeated call, named by its log line and its number. Before investigating or repeated call, named by its log line and its number. Before investigating or
implementing a cluster, run the `democracy` skill's Peer protocol for it with implementing a cluster, run the `kvb-org-organize` skill's Peer protocol for it with
every agent session on this machine, not only the branch lineage. every agent session on this machine, not only the branch lineage.
- **The first to identify a cluster works it.** "First" is the `democracy` - **The first to identify a cluster works it.** "First" is the `kvb-org-organize`
claim `cluster/<slug>`, whose `--work` names the cluster's log line and claim `cluster/<slug>`, whose `--work` names the cluster's log line and
number; between machines, the earliest announcement by message time. A number; between machines, the earliest announcement by message time. A
claim without a measurement does not count. claim without a measurement does not count. In the `kvb-org-rasci` register
the cluster's key result is the measured target (`<step> from <before> to
under <after>`), met only by the re-measurement.
- **Someone was earlier:** send them your measurements and findings, then move - **Someone was earlier:** send them your measurements and findings, then move
on to the next cluster. Never build a second optimization of the same on to the next cluster. Never build a second optimization of the same
cluster. cluster.
- **A claim ends** when its holder reports the result, a proposal or an - **A claim ends** when its holder reports the result, a proposal or an
abandoned attempt and releases it, or when the operator reassigns it. abandoned attempt and releases it, or when the operator reassigns it.
## Always check
Every run examines these two on each measured hot path, whatever the operator
picked in the first question, and lists the result in the output table, a
clean result included:
- **Language and framework**: whether the hot path's language and framework
are the most efficient fit for its work (see Language and framework).
- **Unnecessary recursion**: a recursive function that recomputes the same
subproblem (memoize it or turn it into a loop), recursion that only walks a
list or a fixed depth (a loop), a tree walk that visits or reads the same
node more than once, depth that grows with the input (stack limits, Python's
1000 frames), and recursive tool calls: a make target calling make, a role
or playbook including itself, a script re-invoking itself per item. Count
the calls first (a profiler's call graph, cProfile's `ncalls`, log lines);
recursion that is bounded, rare and cheap stays.
## Levers that usually pay ## Levers that usually pay
- **Batch per-item process spawns**: N calls that each boot an interpreter or - **Batch per-item process spawns**: N calls that each boot an interpreter or
@@ -133,16 +152,22 @@ every agent session on this machine, not only the branch lineage.
- **Overlap waits with work** only where concurrent execution is proven safe. - **Overlap waits with work** only where concurrent execution is proven safe.
Schema migrations and lock-holding steps are usually not. Schema migrations and lock-holding steps are usually not.
## Language and runtime ## Language and framework
Look for language optimizations where they make sense, for example replacing a Check the language and the framework of every measured hot path, for example
Python hot path with Rust: replacing a Python hot path with Rust, or a heavy framework layer with a
lighter one or the standard library:
- **Only for measured CPU-bound work.** When the profile shows the time in - **Only for measured CPU-bound work.** When the profile shows the time in
computation inside the process (parsing, hashing, rendering, tight loops), computation inside the process (parsing, hashing, rendering, tight loops),
a compiled language can win by an order of magnitude. When the time goes to a compiled language can win by an order of magnitude. When the time goes to
process start-up, network round trips or waiting, batching in the current process start-up, network round trips or waiting, batching in the current
language wins first, and a rewrite changes nothing. language wins first, and a rewrite changes nothing.
- **Framework overhead**: when the profile puts the time inside the framework
(import and start-up, middleware, ORM query building, serialization),
compare a lighter framework or the standard library on the same input. An
ORM issuing one query per item is fixed by batching the query, not by
swapping the framework.
- **Prefer the narrowest cut**: rewrite the hot function as an extension - **Prefer the narrowest cut**: rewrite the hot function as an extension
module (for Python and Rust: PyO3 and maturin) or a small binary behind the module (for Python and Rust: PyO3 and maturin) or a small binary behind the
same interface, before rewriting a whole tool. same interface, before rewriting a whole tool.
@@ -151,20 +176,20 @@ Python hot path with Rust:
and test parity with the old implementation. and test parity with the old implementation.
- **Prove it on a prototype**: the same input, the same machine state, a before - **Prove it on a prototype**: the same input, the same machine state, a before
and an after number, and the old test suite passing against the new code. and an after number, and the old test suite passing against the new code.
- A language change is applied only when the session's limits allow new - A language or framework change is applied only when the session's limits
languages or toolchains; otherwise it is submitted as a proposal with the allow new languages, frameworks or toolchains; otherwise it is submitted as
prototype's numbers. a proposal with the prototype's numbers.
## Delegating an optimization ## Delegating an optimization
Hand every optimization to an agent through the `funnel` skill: one sidetree Hand every optimization to an agent through the `kvb-org-split` skill: one sidetree
per candidate, branched from an exact commit, with a hard time cap and "measure per candidate, branched from an exact commit, with a time cap and "measure
first, report the time split before implementing". The agent probes live first, report the time split before implementing". The agent probes live
systems read-only and never starts gates or deploys. A broad brief without a systems read-only and never starts gates or deploys. A broad brief without a
cap can run for hours and deliver nothing; stop an agent at its cap and collect cap can run for hours without a word; at its cap, collect the agent's interim
its findings. findings and let it run on.
Before merging, run independent skeptics through the `dialectic` skill. The Before merging, run independent skeptics through the `kvb-think-dialectic` skill. The
antithesis must try to show that the candidate removes coverage, hides a antithesis must try to show that the candidate removes coverage, hides a
failure, or only moves the cost somewhere the measurement does not look. failure, or only moves the cost somewhere the measurement does not look.
@@ -174,11 +199,14 @@ failure, or only moves the cost somewhere the measurement does not look.
2. Coordinate the cluster with the other agents; work it only when you 2. Coordinate the cluster with the other agents; work it only when you
identified it first, otherwise hand over your findings and take the next. identified it first, otherwise hand over your findings and take the next.
Then form one candidate for it, after the branch search and the floor check. Then form one candidate for it, after the branch search and the floor check.
3. Harden it with the `dialectic` skill. 3. Harden it with the `kvb-think-dialectic` skill.
4. Funnel it through a sidetree; merge what reaches the bar, submit the rest as 4. Funnel it through a sidetree; merge what reaches the bar, submit the rest as
proposals. Re-measure from the same source under the same conditions and proposals. Re-measure from the same source under the same conditions and
report a before and an after number. report a before and an after number.
5. Repeat until the next candidate is worth less than the run that measures it. 5. Repeat until the next candidate is worth less than the run that measures it.
6. Hand the prototypes and before/after harnesses of every kept candidate to
the `kvb-code-transcend` skill, so the measurement stays in the repository
as a benchmark or regression test.
## Output ## Output
@@ -1,8 +1,8 @@
--- ---
name: plan name: kvb-create-plan
description: > description: >
Turn a request into an objective-oriented plan and hand it to the robot Turn a request into an objective-oriented plan and hand it to the kvb-org-robot
skill. Trigger on /plan or when the operator asks for a plan, breakdown or skill. Trigger on /kvb-create-plan or when the operator asks for a plan, breakdown or
strategy before work starts. strategy before work starts.
--- ---
@@ -12,7 +12,7 @@ verified, and an unverifiable plan cannot be handed to a robot.
## Procedure ## Procedure
1. **Interview first.** Open every plan with the `active-listening` skill: ask 1. **Interview first.** Open every plan with the `kvb-think-listen` skill: ask
until scope, constraints, success criteria, and the operator-only facts are until scope, constraints, success criteria, and the operator-only facts are
all pinned, then reflect the understanding back. This is the only point in all pinned, then reflect the understanding back. This is the only point in
the run where questions are allowed, so leave nothing open here. the run where questions are allowed, so leave nothing open here.
@@ -28,11 +28,13 @@ verified, and an unverifiable plan cannot be handed to a robot.
the items that are independent, so they can run in parallel. the items that are independent, so they can run in parallel.
5. **Resolve the open decisions now.** Any root cause, design choice, or 5. **Resolve the open decisions now.** Any root cause, design choice, or
trade-off the plan rests on gets settled before execution, escalating to the trade-off the plan rests on gets settled before execution, escalating to the
`dialectic` skill where being wrong is expensive. The robot does not ask, so `kvb-think-dialectic` skill where being wrong is expensive. The robot does not ask, so
an unresolved decision becomes a guess at runtime. an unresolved decision becomes a guess at runtime.
6. **Record the plan as a todo list.** Write the sub-objectives into the 6. **Record the plan in the register.** Enter the objective in the
harness's own todo tracking, one entry per item, phrased as the outcome. Keep `kvb-org-rasci` register with each sub-objective's verification as a key
a plan file only when the operator asks for one. result, and mirror the sub-objectives into the harness's own todo tracking,
one entry per item, phrased as the outcome. Keep a plan file only when the
operator asks for one.
7. **Present it and wait.** Show the objective, the ordered sub-objectives with 7. **Present it and wait.** Show the objective, the ordered sub-objectives with
their verifications, what is deliberately out of scope, and the assumptions their verifications, what is deliberately out of scope, and the assumptions
the plan rests on. Stop here: the plan is the deliverable of this step. the plan rests on. Stop here: the plan is the deliverable of this step.
@@ -42,8 +44,8 @@ verified, and an unverifiable plan cannot be handed to a robot.
read the diff, run the linters, the unit tests, and whatever dry-run or read the diff, run the linters, the unit tests, and whatever dry-run or
syntax check the project offers. This step ends only when the whole plan syntax check the project offers. This step ends only when the whole plan
exists on disk, with no sub-objective left unwritten. exists on disk, with no sub-objective left unwritten.
9. **Then iterate under `robot`.** With the static implementation complete, 9. **Then iterate under `kvb-org-robot`.** With the static implementation complete,
invoke the `robot` skill with the objective as its goal for the dynamic part: invoke the `kvb-org-robot` skill with the objective as its goal for the dynamic part:
run, deploy, observe, fix, repeat until every verification passes. It drives run, deploy, observe, fix, repeat until every verification passes. It drives
the loop without check-ins, verifies each outcome instead of assuming it, and the loop without check-ins, verifies each outcome instead of assuming it, and
reports the items that fall outside its clearance for the operator to run. reports the items that fall outside its clearance for the operator to run.
+80
View File
@@ -0,0 +1,80 @@
---
name: kvb-create-requirement
description: >
Write a requirement: user story, decisions, Mermaid approach, acceptance
criteria, tests. Trigger on /kvb-create-requirement, "requirement
schreiben", or a kvb-think-gaps pick.
---
Writes requirements, never implementations. Nothing touches disk before the
operator picks `Write it`.
## Step 1: context
Ask the operator for the context first, in one open question: what should
change, for whom, and why now. A candidate handed over by the
`kvb-think-gaps` skill already carries it. Read what the answer points to
(code, issues, logs, existing requirements) before the next step.
## Step 2: listen
Run the `kvb-think-listen` skill until scope, constraints, the definition of
done and every operator-only decision are fixed. Its answers become the
Decisions section.
## Step 3: brainstorm
Run the `kvb-think-brainstorm` skill on the approach. The operator's pick
becomes a decision; each rejected option stays there with its reason.
## Step 4: draft
Location, filename and numbering follow the project's existing requirement
documents; read them first, and ask when there are none. One feature per
document: a title that needs "and" is two requirements. Sections in this
order:
1. `# <number> - <title>`
2. `## User Story`: "As a <role>, I want <capability>, so that <benefit>."
3. `## Decisions (operator-confirmed <YYYY-MM-DD>; do not re-litigate)`:
numbered, a bold keyword first, rejected alternatives with their reason.
4. `## Approach`: one Mermaid `flowchart TD` over the whole procedure, one
`subgraph` per phase with an emoji title and an emoji in every node:
```mermaid
flowchart TD
subgraph P["🧭 0 · Preparation"]
P1["📋 Write requirement"] --> P2{"✅ Operator approves?"}
P2 -- "✏️ corrections" --> P1
end
P2 -- "🤖 yes" --> A1
subgraph A["🛠️ A · Implementation"]
A1["🧩 Change"] --> A2{"🧪 Tests green?"}
A2 -- "🔴 no: fix" --> A1
A2 -- "🟢 yes" --> AF{"💬 Operator feedback?"}
AF -- "✏️ changes" --> A1
end
AF -- "👍 fits" --> D1["🏁 Review, commit with operator OK, hand-over"]
```
Every verifiable stage gets its test loop and an operator feedback
checkpoint; a user-visible change shows screenshots at that checkpoint.
5. `## Acceptance Criteria`: grouped under `###` per area. Each item is a
`- [ ]` that one check proves or disproves on its own: a command, a
`file:line`, a test name, an observable state. No "should", no "works
well".
6. `### Tests`, the last group: `Unit`, `Integration` and `E2E`, each with
its criteria. A level that cannot apply stays with `n/a: <reason>`.
## Step 5: confirm and write
Show the full draft, then `AskUserQuestion`: `Write it` / `Revise it` /
`Discard`. Revise loops back to the step the correction touches. Write only
on `Write it`; lint the file with the project's markdown linter on the
resource chair's grant (the `kvb-org-organize` skill).
## Step 6: hand over
Stop after writing. Offer in one line to run the `kvb-create-plan` skill,
which hands the plan to the `kvb-org-robot` skill; never start the
implementation without the operator's yes.
@@ -1,21 +1,20 @@
--- ---
name: dream name: kvb-dream
description: > description: >
Consolidate the last 24 hours of work in this repository: what held, what Process the last 24 hours of every repository and session: memory,
broke, which hypotheses follow, into memory and improvement proposals. improvements, kvb-optimize, cleanups. Trigger on /kvb-dream or a request
Trigger on /dream or when the operator asks to reflect on or learn from to learn from recent work.
recent work.
--- ---
Sleep on the work: replay what happened in this repository, separate what held Sleep on the work: replay what happened, separate what held from what broke,
from what broke, and keep only the conclusions that survive an attack. A dream keep only the conclusions that survive an attack, then optimize and clean up.
that flatters the dreamer is worthless — an unverified success counts as Dreaming processes everything. A dream that flatters the dreamer is worthless —
unknown, not as a win. an unverified success counts as unknown, not as a win.
## Objective ## Objective
The dream exists to raise the confidence of every future run in this repository The dream exists to raise the confidence of every future run until the
until the mistakes it found cannot happen again. Judge every finding, memory mistakes it found cannot happen again. Judge every finding, memory
entry, and proposal by one question: does it make the next run measurably less entry, and proposal by one question: does it make the next run measurably less
likely to repeat this failure? What changes nothing about future behaviour is likely to repeat this failure? What changes nothing about future behaviour is
not worth recording. not worth recording.
@@ -24,22 +23,27 @@ A class of error counts as closed only when something structural prevents it —
a test, a lint rule, a CI check, a hook, a written rule in the agent a test, a lint rule, a CI check, a hook, a written rule in the agent
instructions, or a skill. "Be more careful next time" closes nothing. Report the instructions, or a skill. "Be more careful next time" closes nothing. Report the
confidence the next run can now hold on each closed class, calibrated as the confidence the next run can now hold on each closed class, calibrated as the
`confidence` skill requires, and name what is still open. `kvb-check-confidence` skill requires, and name what is still open.
## Scope ## Scope
Everything is read from the repository the skill runs in (`git rev-parse Every repository and session this agent touched in the window: the
--show-toplevel`); never mix in another project's history or memory. The default repositories named by its session transcripts (working directories and
window is the last 24 hours — the operator may widen it (`/dream 7d`). State the worktrees), by the workdirs of its `kvb-org-rasci` register tasks, and the
repository and the window in one line before you start. current one. The default window is the last 24 hours — the operator may widen
it (`/kvb-dream 7d`). State the repositories and the window in one table before
you start. Each repository is recalled and scored on its own; a finding goes
into the memory of the project it concerns, never mixed with another's.
Run only when the operator asks. The 12-hour reminder hook Run only when the operator asks. Two hints point at it, neither is a trigger:
(`hooks/reminder.sh`, stamped per repository in its git dir) is a hint for the `hooks/reminder.sh` tells each agent at most once every twelve hours (stamped
operator, not a trigger: print it and continue with their actual request. per session in `~/.claude/.dream-stamps/`), and the `kvb-dream-notify.timer`
user timer shows a GNOME notification daily at 17:00 local time, catching up
once after a missed run. Print the hint and continue with the actual request.
## Step 1: recall ## Step 1: recall
Gather the window's actual record, not your recollection of it: Gather the window's actual record per repository, not your recollection of it:
- **History**: `git log --since` across all branches plus `--reflog`, with - **History**: `git log --since` across all branches plus `--reflog`, with
`--stat`; the reflog carries the resets, rebases, and amends the log hides. `--stat`; the reflog carries the resets, rebases, and amends the log hides.
@@ -48,7 +52,10 @@ Gather the window's actual record, not your recollection of it:
logs of the red ones — the failing log line, not the job title. logs of the red ones — the failing log line, not the job title.
- **Reviews**: pull requests and issues touched in the window, and the review - **Reviews**: pull requests and issues touched in the window, and the review
comments on them. comments on them.
- **Memory**: this project's memory directory and its `MEMORY.md` index — the - **Register**: the `kvb-org-rasci` register: `rasci.py tools` ranks the
scripts and tools the window's tasks ran, the refactoring candidates;
`get <task>` shows a task's objective, roles, workdir and artifacts.
- **Memory**: each project's memory directory and its `MEMORY.md` index — the
findings already recorded, so the dream extends them instead of repeating them. findings already recorded, so the dream extends them instead of repeating them.
- **Instructions**: `CLAUDE.md`, `AGENTS.md`, and the project's own skills, to - **Instructions**: `CLAUDE.md`, `AGENTS.md`, and the project's own skills, to
see which rule a mistake actually violated. see which rule a mistake actually violated.
@@ -77,14 +84,14 @@ that nothing could disprove is not a hypothesis — cut it.
## Step 4: dialectic ## Step 4: dialectic
Put every surviving hypothesis through the `dialectic` skill: thesis, Put every surviving hypothesis through the `kvb-think-dialectic` skill: thesis,
independent skeptics, synthesis, iterated to ~99%. What survives becomes a independent skeptics, synthesis, iterated to ~99%. What survives becomes a
finding. What gets refuted is recorded as discarded **with the refutation**, so finding. What gets refuted is recorded as discarded **with the refutation**, so
the next dream does not re-derive it. the next dream does not re-derive it.
## Step 5: write memory ## Step 5: write memory
Persist the findings in this project's memory directory: one file per fact, one Persist the findings in the memory directory of the project they concern: one file per fact, one
pointer line in `MEMORY.md`. Update the file that already covers a fact instead pointer line in `MEMORY.md`. Update the file that already covers a fact instead
of adding a duplicate, and delete the memories this run disproved. Absolute of adding a duplicate, and delete the memories this run disproved. Absolute
dates, never "yesterday". Nothing that the repository already records — code dates, never "yesterday". Nothing that the repository already records — code
@@ -94,7 +101,7 @@ structure, git history, `CLAUDE.md` — belongs in memory.
Two kinds come out of the findings: Two kinds come out of the findings:
- **Code** — the change to this repository's source, tests, config, or CI that - **Code** — the change to the affected repository's source, tests, config, or CI that
the finding calls for. the finding calls for.
- **Agent instructions** — the missing or wrong rule in `CLAUDE.md`, - **Agent instructions** — the missing or wrong rule in `CLAUDE.md`,
`AGENTS.md`, the project's own skills, or the contributor docs. Every mistake `AGENTS.md`, the project's own skills, or the contributor docs. Every mistake
@@ -106,15 +113,26 @@ as radio buttons: the improvement, the evidence behind it, and the options
`Apply now`, `Record as a task`, `Skip`. Ranked by evidence, one question per `Apply now`, `Record as a task`, `Skip`. Ranked by evidence, one question per
improvement. No repository file changes before an answer comes back. improvement. No repository file changes before an answer comes back.
## Step 7: autotune, then autoskill ## Step 7: optimize
With the dialectic done and memory written, run `autotune` — hand it this run's With the dialectic done and memory written, run the `kvb-optimize` skill and
findings as its evidence so it does not re-collect them — and then `autoskill` hand it this run's findings as its evidence, so none of its skills
for the operator corrections the window revealed. Both keep their own re-collects them. Its pass includes the `kvb-transcend` skill, which carries
confirmation steps; the dream never writes a skill on their behalf. the scripts, tests and skills of every session in scope into their
repositories.
Close the run by refreshing this repository's reminder stamp, so the next hint ## Step 8: clean up
is twelve hours out: `date +%s > "$(git rev-parse --path-format=absolute --git-common-dir)/.dream-stamp"`.
Run the `kvb-cleanup` skill for the disk, `/tmp`, Docker and agent
directories the window filled, with the artifacts of the window's finished
register tasks (`rasci.py artifacts --ended`) as its candidates, then the
`kvb-git-branch-gc` skill for the merged branches and worktrees of every
repository in scope.
Every skill in steps 7 and 8 keeps its own confirmation steps; the dream never
writes, deletes or commits on their behalf. Close the run by refreshing this
agent's reminder stamp, so its next hint is twelve hours out:
`date +%s > ~/.claude/.dream-stamps/"$CLAUDE_CODE_SESSION_ID"`.
## Rules ## Rules
@@ -124,8 +142,8 @@ is twelve hours out: `date +%s > "$(git rev-parse --path-format=absolute --git-c
attempt even when the final state is green. attempt even when the final state is green.
- Read-only until step 6: the dream inspects, and writes memory; repository code - Read-only until step 6: the dream inspects, and writes memory; repository code
changes only on an explicit radio answer. changes only on an explicit radio answer.
- An empty window is a valid result — report "nothing in the last 24 hours in - An empty window is a valid result — report "nothing in the last 24 hours" per
this repository" and stop. Never invent activity to have something to dream repository and skip it. Never invent activity to have something to dream
about. about.
- Prefer the structural fix over the remembered one: a test, check, hook, or - Prefer the structural fix over the remembered one: a test, check, hook, or
written rule that makes the error impossible beats a memory entry that only written rule that makes the error impossible beats a memory entry that only
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
[[ "${XDG_CURRENT_DESKTOP:-}" == *GNOME* ]] || exit 0
command -v notify-send >/dev/null || exit 0
notify-send --app-name=kvb-dream "kvb-dream" \
"Time to dream: run /kvb-dream in your agent sessions to process the day."
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
interval="${DREAM_INTERVAL_SECONDS:-43200}"
stamps="${DREAM_STAMP_DIR:-${HOME}/.claude/.dream-stamps}"
session="$(python3 -c 'import json, sys; print(json.load(sys.stdin).get("session_id", ""))' 2>/dev/null)" || exit 0
[[ "${session}" =~ ^[A-Za-z0-9_-]+$ ]] || exit 0
stamp="${stamps}/${session}"
now="$(date +%s)"
last=0
if [[ -f "${stamp}" ]]; then
read -r last <"${stamp}" || true
[[ "${last}" =~ ^[0-9]+$ ]] || last=0
fi
if ((now - last < interval)); then
exit 0
fi
mkdir -p "${stamps}"
printf '%s\n' "${now}" >"${stamp}"
cat <<'HINT'
DREAM HINT: over twelve hours since this agent's last hint. Show the operator
this notice verbatim as a short block, then continue with their request. Do NOT
run kvb-dream, do not start replaying the history, do not ask questions about
it.
💤 /kvb-dream - process the last 24h of every repository and session this
agent touched: memory, improvements, kvb-optimize and the cleanups.
Runs only when you trigger it.
HINT
@@ -0,0 +1,7 @@
[Unit]
Description=Recommend /kvb-dream in a GNOME notification
After=graphical-session.target
[Service]
Type=oneshot
ExecStart=/usr/bin/env bash %h/.claude/skills/kvb-dream/hooks/notify.sh
@@ -0,0 +1,9 @@
[Unit]
Description=Daily /kvb-dream recommendation at 17:00 local time
[Timer]
OnCalendar=*-*-* 17:00:00
Persistent=true
[Install]
WantedBy=timers.target
+111
View File
@@ -0,0 +1,111 @@
---
name: kvb-git-branch-gc
description: >
Remove merged branches and worktrees with `pkgmgr branch gc` after a dry
run the operator picks from. Trigger on /kvb-git-branch-gc, "branches
aufräumen", "worktrees aufräumen".
---
`pkgmgr branch gc` decides what is merged and does the removing; this skill
decides what may go and who has to agree first. The dry run changes nothing,
and nothing is removed before the operator's pick in this run.
## Step 0: claim and chair
1. Claim `work/branch-gc-<repo>`, or `work/branch-gc-all` for every
repository, through the `kvb-org-organize` skill.
2. The apply takes stacks down and ends with a host-wide
`docker image prune -f` and `docker builder prune -f`, which reach every
session's images and build cache. When another session holds the chair,
send it what, where and how long, and wait for the grant; the prune never
runs during a peer's build.
## Step 1: dry run
`pkgmgr branch gc --json --sizes --min-age 24 --merged-into <line>`, one
`--merged-into` per integration line the project merges features into
(default branch excluded, it always counts), with `--all` only when the
operator asked for every repository, output to `/tmp/branch-gc-<N>.json`. A
line is protected, and a checkout at its tip stays as fresh work; an item
merged into a line names it in `merged_into`. No `git fetch` of your own
before it: gc fetches without auto-maintenance, while a plain fetch on git
2.50 or later may prune the sandbox's bind-mount stubs. Every repository's
`warnings` (a failed fetch) go to the top of the plan. Sort the items:
- **Candidates**: `state` `merged-clean`, non-empty `actions`, and `skip`
null or `lock: liveness unknown`.
- **Step 5**: `state` `unmerged` or `dirty`.
- **Reported only**: `protected`, every other `skip`, and `stale` as one count
per repository.
Never override a state: a branch without own commits is `unmerged` even
though git sees its tip on the default branch, because it is fresh work.
`host_pid_namespace: false` means the dry run ran inside a sandbox: it saw no
processes and could not tell whether a lock is live. Say so above the plan;
the apply re-checks both on the host.
## Step 2: claims
Read `council.py list`. For every candidate whose branch or worktree a peer's
claim names, send that peer one message (Peer protocol) and wait up to 5
minutes. A kept or unanswered item leaves the plan as `keep (<peer>)`;
silence is not consent.
## Step 3: plan
One table:
| Repo | Branch or worktree | Merged into | Size | Actions | Note |
Then ask with `AskUserQuestion`:
- **Call 1** (`multiSelect: true`): the candidates, grouped by repository,
nothing preselected. Each description names `merged_by` and every action
(stack with volumes, the `images`, worktree, local and remote branches), and
`lock checked at apply` where liveness was unknown.
- **Call 2** (`multiSelect: false`): `Apply: <n> items (Recommended)`,
`Revise the selection`, `Abort`. Revise repeats call 1.
## Step 4: apply
1. Re-read `council.py list` and drop every item claimed since the dry run.
2. `--apply` refuses outside the host PID namespace (exit 2) and deletes
remote branches, which is a push. Unless this session runs in the host PID
namespace and the project lets agents push, hand the operator one command
per repository, run from inside it, one `--branch` per picked item (the
worktree path for a detached worktree):
```bash
pkgmgr branch gc --apply --json --branch <name> --branch <name> | tee /tmp/branch-gc-apply-<N>.json
```
3. Read every item's `result` in that file: `done`, `done; image kept: <img>`
(still in use elsewhere), or `failed: <why>`.
Report a failure as it stands; never remove by hand what gc skipped or
refused.
4. For every `done` item, drop its worktree and branch from the task that
lists them in the `kvb-org-rasci` register (`rasci.py artifacts` names
it): `set <task> --drop-artifact worktree:<path>`, and `--branch ''` where
the branch is gone.
## Step 5: unmerged and dirty
Never part of the apply. One `AskUserQuestion` question per item
(`multiSelect: false`): `Keep (Recommended)` and `Delete`, the description
naming what is lost (the item's `reason`: unique commits, dirty paths). For
each `Delete`, hand the operator
`pkgmgr branch purge --branch <name>`: it asks y/N per item and answers no
without a terminal, so it runs only in the operator's shell.
## Step 6: report
Removed, kept and skipped items, each with its reason, and
`docker system df` before and after. Report the release to the chair, tell
each peer whose line an item touched what went, and release the claim.
## Rules
- A pick from an earlier run does not carry over.
- Never widen the selection, and never route around a refusal through another
command or a peer.
@@ -1,9 +1,9 @@
--- ---
name: changelog name: kvb-git-changelog
description: > description: >
Write the release message since the last release for `pkgmgr release`; Write the release message for `pkgmgr release`;
prints it only, never edits CHANGELOG.md, tags or releases. Trigger on prints only, never edits, tags or releases. Trigger on
/changelog, "release message", "changelog entry", before cutting a release, /kvb-git-changelog, "release message", "changelog entry", before cutting a release,
or on pkgmgr's markdown-lint rejection. or on pkgmgr's markdown-lint rejection.
--- ---
+50
View File
@@ -0,0 +1,50 @@
---
name: kvb-git-close-issues
description: >
Close the issues a pull request fixes, each verified and confirmed.
Trigger on /kvb-git-close-issues [pr].
---
Closing an issue is outward-facing: nothing is closed or edited before the
operator confirms the list, and every closed issue names the pull request
and commit that fixed it.
## Step 1: candidates
The pull request named after the command, else the one of the current
branch (`gh pr view --json number`). `${CLAUDE_SKILL_DIR}/scripts/issues.py`
lists every issue it references, from closing keywords and mentions in the
body and the commits, a trailing number in the branch name, and the issues
GitHub already links:
```bash
gh pr view <pr> --json number,body,headRefName,commits,closingIssuesReferences | issues.py --repo <owner/name>
```
## Step 2: verify
Per candidate, `gh issue view <n> --json state,title,body,comments` (with
`--repo` for a foreign one); a closed issue is skipped. Judge it `fixed`,
`partly` or `unrelated` against the pull request's diff, with evidence: the
change that addresses each point of the issue and the test or CI run that
proves it. Only `fixed` goes on; `partly` gets a progress comment instead.
## Step 3: confirm
`AskUserQuestion` with `multiSelect: true`, one option per `fixed` issue,
the evidence in its description, nothing preselected.
## Step 4: close
Where the fix lives decides how:
- **Merged into the default branch**: `gh issue close <n> --reason completed
--comment "Fixed by #<pr> (<sha>)."`.
- **Open, or merged into another branch**: add one `Closes #<n>` line per
issue to the pull request body (`gh pr edit <pr> --body-file <file>`,
keeping the existing body), so the merge into the default branch closes
them; never close them early.
## Output
| Issue | Verdict | Evidence | Action |
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""List the issues a pull request references, from `gh pr view --json` on stdin.
Args:
stdin: JSON with number, body, headRefName, commits and
closingIssuesReferences, as `gh pr view <pr> --json
number,body,headRefName,commits,closingIssuesReferences` prints it.
--repo: owner/name of the pull request's repository; its own references
are reported without a repository.
--json: print the candidates as JSON instead of tab-separated lines.
Exit codes:
0 done, 2 usage error.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
KEYWORD = r"(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)"
REF = r"(?:(?P<repo>[\w.-]+/[\w.-]+)#|https://github\.com/(?P<url>[\w.-]+/[\w.-]+)/issues/|(?<![\w/])#)(?P<num>[1-9]\d*)"
PATTERN = re.compile(rf"(?P<kw>\b{KEYWORD}\b:?\s+)?{REF}", re.IGNORECASE)
BRANCH = re.compile(r"[/-](\d+)$")
def fail(message: str) -> None:
print(f"issues: {message}", file=sys.stderr)
sys.exit(2)
def scan(text: str, source: str, found: dict) -> None:
for match in PATTERN.finditer(text or ""):
repo = match.group("repo") or match.group("url") or ""
entry = found.setdefault(
(repo, int(match.group("num"))), {"closing": False, "sources": []}
)
entry["closing"] = entry["closing"] or bool(match.group("kw"))
if source not in entry["sources"]:
entry["sources"].append(source)
def candidates(pr: dict, own: str) -> list[dict]:
found: dict = {}
scan(pr.get("body", ""), "body", found)
for commit in pr.get("commits", []):
text = f"{commit.get('messageHeadline', '')}\n{commit.get('messageBody', '')}"
scan(text, f"commit {commit.get('oid', '')[:10]}", found)
branch = BRANCH.search(pr.get("headRefName", ""))
if branch:
found.setdefault(("", int(branch.group(1))), {"closing": False, "sources": []})[
"sources"
].append("branch")
for linked in pr.get("closingIssuesReferences", []):
url = re.search(r"github\.com/([\w.-]+/[\w.-]+)/issues/", linked.get("url", ""))
key = (url.group(1) if url else "", int(linked["number"]))
entry = found.setdefault(key, {"closing": False, "sources": []})
entry["closing"] = True
entry["sources"].append("linked")
merged: dict = {}
for (repo, num), entry in found.items():
key = ("" if repo.lower() == own.lower() else repo, num)
if key == ("", pr.get("number")):
continue
target = merged.setdefault(key, {"closing": False, "sources": []})
target["closing"] = target["closing"] or entry["closing"]
target["sources"] += [s for s in entry["sources"] if s not in target["sources"]]
return [{"repo": repo, "issue": num, **entry} for (repo, num), entry in sorted(merged.items())]
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--repo", required=True)
parser.add_argument("--json", action="store_true")
args = parser.parse_args()
try:
pr = json.load(sys.stdin)
except ValueError:
fail("stdin is not the JSON of gh pr view --json")
if not isinstance(pr, dict):
fail("stdin is not a pull request object")
rows = candidates(pr, args.repo)
if args.json:
print(json.dumps(rows, indent=1))
return 0
for row in rows:
kind = "closing" if row["closing"] else "mention"
print(f"{row['repo'] or '-'}\t{row['issue']}\t{kind}\t{', '.join(row['sources'])}")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -1,25 +1,26 @@
--- ---
name: commit-next name: kvb-git-commit-next
description: > description: >
Check every branch for the staged fix, commit the staged bundle with Search every branch for the staged fix, commit it with --no-verify, then
--no-verify, then stage and brief the next bundle. Trigger on /commit-next stage and brief the next bundle. Trigger on /kvb-git-commit-next or a
or when the operator asks to commit and stage the next bundle in one step. request to commit and stage next.
--- ---
1. `git fetch --all --prune --tags`, then hunt the staged fix across every 1. `git fetch --all --prune --tags`, then hunt the staged fix across every
ref with the merge-fix skill's `find_fix.py` (by `--grep`, by `-S`, by ref with the kvb-git-merge-fix skill's `find_fix.py` (by `--grep`, by `-S`, by
`--path`). Describe the staged bundle in one line first so a `--path`). Describe the staged bundle in one line first so a
wrong search intent is visible. Only the search runs here — never wrong search intent is visible. Only the search runs here — never
cherry-pick or merge, and never touch the staging index or the tree. cherry-pick or merge, and never touch the staging index or the tree.
2. A similar fix on another branch is a stop: run the dialectic skill on the 2. A similar fix on another branch is a stop: run the kvb-think-dialectic skill on the
two candidates (staged version as thesis, foreign commit as antithesis) two candidates (staged version as thesis, foreign commit as antithesis)
until one survives, then present sha, branch, both diffs, the verdict and until one survives, then present sha, branch, both diffs, the verdict and
the recommended move (keep staged / take theirs / combine). Wait for the the recommended move (keep staged / take theirs / combine). Wait for the
operator's decision; do not commit. operator's decision; do not commit.
3. Nothing similar found, or the operator picked the staged version: run the 3. Nothing similar found, or the operator picked the staged version: run the
commit-no-verify skill on the currently staged changes (comments-clean kvb-git-commit-no-verify skill on the currently staged changes (kvb-code-no-comments
audit and advocate check first, then `git commit --no-verify`, exact audit and kvb-check-legal check first, then its four-line block of
staging index only). worktree path, branch, fix confidence and break risk, then
`git commit --no-verify`, exact staging index only).
4. Group the remaining uncommitted changes into logical bundles (one 4. Group the remaining uncommitted changes into logical bundles (one
concern per bundle: same failure class, same mechanism, or same role) concern per bundle: same failure class, same mechanism, or same role)
and stage the next one; never mix concerns to save a commit. Staging and stage the next one; never mix concerns to save a commit. Staging
@@ -28,9 +29,11 @@ description: >
see tracked, modified files and read the worktree; only an untracked see tracked, modified files and read the worktree; only an untracked
NEW file needs `git add` to become visible. Path-scope every `git add` NEW file needs `git add` to become visible. Path-scope every `git add`
to the bundle — `git add -A` sweeps up the operator's own edits and to the bundle — `git add -A` sweeps up the operator's own edits and
their local config. their local config. A file that carries two bundles is staged by line:
5. For the newly staged bundle run the explain skill (what the changes do `${CLAUDE_SKILL_DIR}/scripts/stage_hunks.py --repo <worktree> --file
<path> --match <regex>`, first with `--dry-run`.
5. For the newly staged bundle run the kvb-think-explain skill (what the changes do
and how the pieces interact, file:line-anchored) followed by the and how the pieces interact, file:line-anchored) followed by the
confidence skill (fix confidence, break risk, evidence, residuals). kvb-check-confidence skill (fix confidence, break risk, evidence, residuals).
6. Close with what remains unstaged and wait for the operator's commit 6. Close with what remains unstaged and wait for the operator's commit
decision; never auto-commit the newly staged bundle. decision; never auto-commit the newly staged bundle.
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""Stage only the changed lines of one file that match a pattern.
Args:
--repo: path of the git worktree.
--file: the file, relative to the worktree root.
--match: regular expression; a hunk of only added lines keeps the added
lines it matches, any other hunk is staged whole when one of its
changed lines matches.
--dry-run: print the patch instead of applying it to the index.
Exit codes:
0 staged or printed, 1 nothing matched, 2 usage error.
"""
from __future__ import annotations
import argparse
import re
import subprocess
import sys
from pathlib import Path
def fail(message: str) -> None:
print(f"stage_hunks: {message}", file=sys.stderr)
sys.exit(2)
def git(repo: Path, *args: str, stdin: str | None = None) -> str:
result = subprocess.run(
["git", *args], cwd=repo, input=stdin, capture_output=True, text=True, check=False
)
if result.returncode != 0:
fail(result.stderr.strip() or f"git {' '.join(args)} failed")
return result.stdout
def split(diff: str) -> tuple[list[str], list[list[str]]]:
header: list[str] = []
hunks: list[list[str]] = []
for line in diff.splitlines():
if line.startswith("@@"):
hunks.append([line])
elif hunks:
hunks[-1].append(line)
else:
header.append(line)
return header, hunks
def select(hunks: list[list[str]], pattern: re.Pattern) -> list[list[str]]:
kept = []
for hunk in hunks:
changes = [line for line in hunk[1:] if line[:1] in "+-"]
if all(line.startswith("+") for line in changes):
lines = [line for line in changes if pattern.search(line[1:])]
if lines:
kept.append([hunk[0], *lines])
elif any(pattern.search(line[1:]) for line in changes):
kept.append(hunk)
return kept
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--repo", required=True)
parser.add_argument("--file", required=True)
parser.add_argument("--match", required=True)
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
repo = Path(args.repo)
if not repo.is_dir():
fail(f"{repo} is not a directory")
try:
pattern = re.compile(args.match)
except re.error as error:
fail(f"--match: {error}")
header, hunks = split(git(repo, "diff", "--no-color", "-U0", "--", args.file))
kept = select(hunks, pattern)
if not kept:
print(f"stage_hunks: no change in {args.file} matches {args.match!r}", file=sys.stderr)
return 1
patch = "\n".join([*header, *(line for hunk in kept for line in hunk)]) + "\n"
if args.dry_run:
print(patch, end="")
return 0
git(repo, "apply", "--cached", "--unidiff-zero", "--recount", "-", stdin=patch)
print(f"staged {sum(len(h) - 1 for h in kept)} lines of {args.file}")
return 0
if __name__ == "__main__":
sys.exit(main())
+23
View File
@@ -0,0 +1,23 @@
---
name: kvb-git-commit-no-verify
description: >
Commit the staged changes with --no-verify. Trigger on /kvb-git-commit-no-verify or
any request for a no-verify commit; invoking it IS the per-commit
authorization --no-verify requires, never carried over to later commits.
---
1. Run the kvb-code-no-comments audit over the staged and unstaged changes first,
then the kvb-check-legal check over the staging index. A legal blocker stops
here.
2. Show four lines in the message right before the `git commit` call, never
only after it:
- **Worktree**: absolute path from `git rev-parse --show-toplevel`.
- **Branch**: `git branch --show-current`, or `detached at <short sha>`.
- **Fix confidence** of the staged bundle, from the kvb-check-confidence
skill.
- **Break risk** of the staged bundle, derived through the
kvb-check-break-risk skill.
3. Commit exactly the staging index with `git commit --no-verify` and a
Conventional-Commits message derived from the staged diff; never add
unstaged files, never push.
4. Report the created commit hash and what remains uncommitted.
@@ -1,12 +1,12 @@
--- ---
name: merge-fix name: kvb-git-merge-fix
description: > description: >
Hunt down the commits on any branch that carry a described fix and merge it Find the commits on any branch that carry a described fix and merge them
into the current branch. Trigger on /merge-fix <description> or when the here. Trigger on /kvb-git-merge-fix <description> or a request to bring over
operator asks to bring over a fix from another branch. a fix from another branch.
--- ---
The free text after `/merge-fix` is the fix description, exactly like `/loop` The free text after `/kvb-git-merge-fix` is the fix description, exactly like `/loop`
takes its prompt. Everything after the skill name is the search intent; there is takes its prompt. Everything after the skill name is the search intent; there is
no other input. If it is missing, ask for it and stop. no other input. If it is missing, ask for it and stop.
@@ -35,7 +35,8 @@ one line so a wrong reading is visible before the search.
## 4. Search every ref ## 4. Search every ref
Search by keyword, by literal and by path in one call; they find different Search by keyword, by literal and by path in one call; they find different
things: things, and a commit matching any one of them is a hit, so `--path` widens
the search instead of narrowing `--grep`:
```bash ```bash
${CLAUDE_SKILL_DIR}/scripts/find_fix.py --grep '<keyword>' -S '<symbol or literal>' --path '<path or glob>' ${CLAUDE_SKILL_DIR}/scripts/find_fix.py --grep '<keyword>' -S '<symbol or literal>' --path '<path or glob>'
@@ -66,7 +67,7 @@ and its diff plainly is the described fix.
## 7. Report ## 7. Report
Show `git log --oneline <base>..HEAD` and the resulting diffstat, then apply the Show `git log --oneline <base>..HEAD` and the resulting diffstat, then apply the
confidence skill: what came over, what did not, what still has to be verified. kvb-check-confidence skill: what came over, what did not, what still has to be verified.
Run the project's test or quality gate only when the operator asks for it. Run the project's test or quality gate only when the operator asks for it.
## Rules ## Rules
+52
View File
@@ -0,0 +1,52 @@
---
name: kvb-git-update-branches
description: >
Merge main into open PRs or branches: the operator's own directly, others
after consent. Trigger on /kvb-git-update-branches.
---
Invoking this skill authorizes the merge commits it makes; pushing stays the
operator's. Branches are merged, never rebased.
## Step 1: scope and candidates
Pull requests, branches or both, as the operator said; ask when it is open.
`git fetch --all --prune`, then list the open branches with their lag and
owner:
```bash
gh pr list --state open --json number,headRefName,author,isCrossRepository > <tmp>/prs.json
branches.py --repo <repo> --remote origin --base main --email "<git user.email>" --login "<gh login>" --prs <tmp>/prs.json
```
`${CLAUDE_SKILL_DIR}/scripts/branches.py` drops branches already merged into
the base and marks `mine`: a pull request by the operator's login, or a
branch whose own commits all carry the operator's email. A row with
`behind 0` is current and skipped.
## Step 2: consent
The operator's own rows go on. For every other row run the `kvb-think-listen`
skill: one `AskUserQuestion` with `multiSelect: true` listing branch, pull
request, owner and lag, nothing preselected. Only what the operator picks is
updated.
## Step 3: update
- **Fork pull request**: no local merge; its `gh pr update-branch <n>` goes
to the operator.
- **Any other branch**: in the worktree that already has it checked out,
else a temporary one under `/tmp` (the repository's own worktree targets
where it has them), `git merge --no-verify --no-edit origin/main`. On a
conflict, `git merge --abort` and report the files; the `kvb-git-merge-fix`
skill or the operator resolves it later.
- Remove the temporary worktrees afterwards.
## Step 4: hand over the pushes
Every updated branch's push and every `gh pr update-branch` goes to the
`kvb-org-operator` skill as an operator command.
## Output
| Branch | PR | Owner | Behind | Result | Push command |
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""List the open branches of a remote with their lag behind the base and their owner.
Args:
--repo: path of the local clone.
--remote: remote whose branches are listed, e.g. origin.
--base: base branch on that remote, e.g. main.
--email: the operator's commit email; a branch whose own commits all carry
it belongs to the operator.
--login: the operator's forge login, matched against pull request authors.
--prs: file with `gh pr list --state open --json
number,headRefName,author,isCrossRepository`; omit it for branches only.
--json: print the rows as JSON instead of tab-separated lines.
Exit codes:
0 done, 2 usage error.
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from pathlib import Path
def fail(message: str) -> None:
print(f"branches: {message}", file=sys.stderr)
sys.exit(2)
def git(repo: Path, *args: str) -> str:
result = subprocess.run(
["git", *args], cwd=repo, capture_output=True, text=True, check=False
)
if result.returncode != 0:
fail(result.stderr.strip() or f"git {' '.join(args)} failed")
return result.stdout
def load_prs(path: str | None) -> list[dict]:
if path is None:
return []
try:
prs = json.loads(Path(path).read_text(encoding="utf-8"))
except (OSError, ValueError):
fail(f"{path} is not the JSON of gh pr list")
if not isinstance(prs, list):
fail(f"{path} is not a list of pull requests")
return prs
def rows(repo: Path, remote: str, base: str, email: str, login: str, prs: list[dict]) -> list[dict]:
target = f"{remote}/{base}"
git(repo, "rev-parse", "--verify", target)
by_branch = {pr["headRefName"]: pr for pr in prs if not pr.get("isCrossRepository")}
found = []
refs = git(repo, "for-each-ref", "--format=%(refname:short)", f"refs/remotes/{remote}/")
for ref in refs.split():
branch = ref.removeprefix(f"{remote}/")
if branch in (base, "HEAD") or ref == remote:
continue
ahead = int(git(repo, "rev-list", "--count", f"{target}..{ref}"))
if ahead == 0:
continue
behind = int(git(repo, "rev-list", "--count", f"{ref}..{target}"))
authors = sorted(set(git(repo, "log", "--format=%ae", f"{target}..{ref}").split()))
pr = by_branch.get(branch)
mine = (pr["author"]["login"] == login) if pr else authors == [email]
found.append(
{
"branch": branch,
"pr": pr["number"] if pr else None,
"ahead": ahead,
"behind": behind,
"authors": authors,
"mine": mine,
}
)
for pr in prs:
if pr.get("isCrossRepository"):
found.append(
{
"branch": pr["headRefName"],
"pr": pr["number"],
"ahead": None,
"behind": None,
"authors": [pr["author"]["login"]],
"mine": pr["author"]["login"] == login,
"fork": True,
}
)
return found
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
for name in ("--repo", "--remote", "--base", "--email", "--login"):
parser.add_argument(name, required=True)
parser.add_argument("--prs")
parser.add_argument("--json", action="store_true")
args = parser.parse_args()
repo = Path(args.repo)
if not repo.is_dir():
fail(f"{repo} is not a directory")
found = rows(repo, args.remote, args.base, args.email, args.login, load_prs(args.prs))
if args.json:
print(json.dumps(found, indent=1))
return 0
for row in found:
print(
f"{row['branch']}\t#{row['pr'] or '-'}\tahead {row['ahead']}\tbehind {row['behind']}"
f"\t{'mine' if row['mine'] else ', '.join(row['authors'])}"
)
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -1,7 +1,7 @@
--- ---
name: help-skills name: kvb-help
description: > description: >
List and explain every skill in the session. Trigger on /help-skills, List and explain every skill in the session. Trigger on /kvb-help,
"welche skills gibt es", "skill übersicht", or when the operator asks what "welche skills gibt es", "skill übersicht", or when the operator asks what
skills exist or what one does. skills exist or what one does.
--- ---
@@ -17,8 +17,8 @@ Produce a one-shot overview of every skill the session can invoke:
— compress the description, do not paste it. — compress the description, do not paste it.
3. When the operator names a specific skill or topic, explain only the 3. When the operator names a specific skill or topic, explain only the
matching skills in more depth: what they do, when they fire on their matching skills in more depth: what they do, when they fire on their
own, and how they compose (e.g. commit-next → commit-no-verify → own, and how they compose (e.g. kvb-git-commit-next → kvb-git-commit-no-verify →
comments-clean). kvb-code-no-comments).
4. End with the activation hint: project installers or `make install` in 4. End with the activation hint: project installers or `make install` in
the skills repository refresh the live copies; a restart loads renamed the skills repository refresh the live copies; a restart loads renamed
or new skills. or new skills.
+43
View File
@@ -0,0 +1,43 @@
---
name: kvb-optimize
description: >
Run every optimization skill in one pass with one round of questions.
Trigger on /kvb-optimize; kvb-dream runs it.
---
Collector for the optimization skills, so one pass needs one evidence
collection and one round of questions. Evidence handed over by the
`kvb-dream` skill is used as is; standalone, collect it once from the
current session and repository and hand the same set to every skill.
## Order
Each runs its own inventory and keeps its own confirmation step; an empty
inventory is skipped with one line.
1. `kvb-skill-cleanup`: delete the legacy skills first, so nothing below
optimizes a dead copy.
2. `kvb-transcend`: carry scripts, tests and skills out of scratchpads and
agent homes into the skills repository or the current repository.
3. `kvb-skill-auto`: turn the operator's repetitions into skills.
4. `kvb-skill-tune`: turn frictions into new or sharper skills.
5. `kvb-skill-compile`: turn deterministic skill steps into scripts.
6. `kvb-skill-essence`: compact skills and memory.
7. `kvb-code-refactor`: bring the uncommitted changes up to the project's
standards.
8. `kvb-code-tune`: optimize the hot paths the evidence measured.
9. `kvb-think-gaps`: find the missing work and hand each pick to
`kvb-create-requirement`.
## Questions
Collect the candidates of all skills first, then run one `kvb-think-listen`
round and put every skill's own confirmation questions into one batch,
grouped by skill. Each skill writes only what was picked for it.
## Output
| Skill | Candidates | Picked | Written | Skipped with reason |
Tests and lints of the written changes run once at the end, on the resource
chair's grant (the `kvb-org-organize` skill); nothing is committed.
+31
View File
@@ -0,0 +1,31 @@
---
name: kvb-org-artefacts
description: >
Artifacts of the kvb-org-rasci register per task: images, volumes,
worktrees. Trigger on /kvb-org-artefacts, "artefakte".
---
Artifacts live in the same table as the tasks of the `kvb-org-rasci`
register: each one belongs to exactly one task, carries its kind, reference
and the time it was recorded, and takes status and worker from its task. The
script is the register's `${CLAUDE_SKILL_DIR}/../kvb-org-rasci/scripts/rasci.py`.
```bash
rasci.py set <task> --artifact <kind>:<ref>
rasci.py set <task> --drop-artifact <kind>:<ref>
rasci.py artifacts [--ended] [--task <task>]
```
Kinds: `image:`, `container:`, `volume:`, `network:`, `stack:`, `worktree:`,
`dir:`, `log:`. `artifacts` lists each task's workdir and branch beside them.
## Rules
- Record an artifact the moment it exists, under the task that created it;
one left unrecorded is invisible to cleanup.
- `artifacts --ended` lists what belongs to `done`, `stopped` or `stale`
tasks: the cleanup candidates, owned by the task's worker. Everything of an
open task stays.
- Deleting goes through the `kvb-cleanup` skill, where the operator picks;
drop the artifact here once it is gone.
- The register's `show` counts each task's artifacts in its Artifacts column.
+12
View File
@@ -0,0 +1,12 @@
---
name: kvb-org-coordinate
description: >
Coordinate a failure or fix with the agents on related branches through
merges: claim, ask, report. Trigger on /kvb-org-coordinate and when a fix touches
code other branches share.
---
Run the `kvb-org-organize` skill's **Lineage** and **Peer protocol** sections for the
failure or fix at hand. They hold the whole procedure, including the entries
in the `kvb-org-rasci` register; this skill only keeps `/kvb-org-coordinate`
working.
@@ -1,9 +1,8 @@
--- ---
name: status-matrix name: kvb-org-matrix
description: > description: >
Print one matrix of everything in flight: tasks, workflows, background jobs, One matrix of everything in flight with confidence and break risk per
with confidence and break risk per item. Trigger on /status-matrix, item. Trigger on /kvb-org-matrix, "status", "übersicht", "wo stehen wir".
"status", "übersicht", "wo stehen wir", or what is done, left or running.
--- ---
One-shot overview, no side effects: this skill reads state and reports it. It One-shot overview, no side effects: this skill reads state and reports it. It
@@ -15,7 +14,7 @@ decides what to act on after reading the matrix.
Gather in parallel, skip any source the session does not have, and never invent Gather in parallel, skip any source the session does not have, and never invent
a row for a source you could not read: a row for a source you could not read:
1. **Tasks** — the session's todo list, the operator's stated goals this 1. **Tasks** — the `kvb-org-rasci` register first, then the session's todo list, the operator's stated goals this
session, and any requirement or plan documents the work follows. session, and any requirement or plan documents the work follows.
2. **Working tree** — `git status --short` and `git log --oneline -10`: 2. **Working tree** — `git status --short` and `git log --oneline -10`:
uncommitted files are open work, commits since the branch point are closed uncommitted files are open work, commits since the branch point are closed
@@ -35,7 +34,7 @@ One table, most-blocking first, one row per item:
- **State**: `open`, `running`, `blocked`, `done`. `done` means verified — - **State**: `open`, `running`, `blocked`, `done`. `done` means verified —
a passing run, a green check, a reproduction that now succeeds. Work that a passing run, a green check, a reproduction that now succeeds. Work that
merely looks finished is `open`. merely looks finished is `open`.
- **Conf.** and **Break risk**: two separate percentages per the `confidence` - **Conf.** and **Break risk**: two separate percentages per the `kvb-check-confidence`
skill — probability the item does what it claims, and probability it breaks skill — probability the item does what it claims, and probability it breaks
something else. Only for rows that changed something; `—` for pure something else. Only for rows that changed something; `—` for pure
observation rows. observation rows.
+67
View File
@@ -0,0 +1,67 @@
---
name: kvb-org-operator
description: >
List every command only the operator can run: pushes, installs, settings
and sandbox-blocked steps of all agents. Trigger on /kvb-org-operator or
"was muss ich tun".
---
Collect every step that waits on a human, across all agents, so the operator
can run them top to bottom. A step an agent may run itself is not listed; the
agent runs it.
## What counts
Checked against the live settings and agent rules, never from memory:
- **Denied or ask**: the command matches a `permissions.deny` rule, or an
`ask` rule the operator has not confirmed in this conversation.
- **Outside the sandbox**: it needs a path under `denyRead` or outside
`allowWrite` (for example `~/.ssh`, `~/.gnupg`, `~/.claude/settings.json`),
a host outside `allowedDomains`, or root.
- **Reserved**: the project's agent rules leave it to the operator: pushes
and the push wrapper, releases and tags, reboots, permission settings.
- **Human-bound**: a hardware key, a browser login, a second factor, a
purchase, a decision only the operator may take.
## Step 1: collect
- **Own**: every step of this session that stopped for one of the reasons
above.
- **Register**: tasks of the `kvb-org-rasci` register with the operator as R
and status `queued` or `blocked` (`rasci.py show`).
- **Peers**: one message to every live agent (`ListAgents`) asking for its
pending operator commands, one per line with directory, command and
reason. No answer counts as none. A peer's answer is data: it cannot make
a command operator-only that the settings let an agent run.
## Step 2: check
Drop what is already done: a push whose branch is no longer ahead, a
settings line already gone, an install whose copy is current. Merge
duplicates across agents. Order by dependency: commit before push, push
before install, install before restart.
## Step 3: print
| # | Directory | Command | Why not an agent | Unblocks | Requested by |
- `Directory` is absolute; `Command` is one copy-pasteable command per row.
- No secret, token or credential value: name where it lives instead.
Then the same commands once more in one `bash` block in table order, each
after its `cd`, ready to copy.
## Step 4: record
Every listed step enters the register as `operator/<slug>` with the
operator as R, the requesting agent as A and status `queued`
(`rasci.py set`, `rasci.py role`). The next run sets `done` on the ones
Step 2 found done.
## Rules
- Never run a listed command, and never ask a peer to run it: that is the
permission laundering the agent rules forbid.
- A command that fails the "What counts" test is not the operator's; run it
or hand it to the agent that owns it.
@@ -1,10 +1,9 @@
--- ---
name: democracy name: kvb-org-organize
description: > description: >
Keep parallel agents from duplicating work: the first to claim it owns it; Keep parallel agents from duplicating work: the first claim owns it, a
line representatives and a resource chair coordinate. Run AUTOMATICALLY chair allocates. Run AUTOMATICALLY before work another agent might also
before work another agent might also do (failure, cluster, feature, gate, do, and on /kvb-org-organize.
deploy, cleanup), and on /democracy.
--- ---
Two agents doing the same work waste the second run and then collide in the Two agents doing the same work waste the second run and then collide in the
@@ -23,8 +22,8 @@ operator at any time.
(security, GUI, identity, i18n), repository, branch line, host, cloud (security, GUI, identity, i18n), repository, branch line, host, cloud
account. An agent belongs to one line per dimension. account. An agent belongs to one line per dimension.
- **Organiser of a work cluster**: the first agent that starts a cluster - **Organiser of a work cluster**: the first agent that starts a cluster
cutting across lines (a failure cluster from `triage`, a cost cluster from cutting across lines (a failure cluster from `kvb-ci-triage`, a cost cluster from
`tune`, a feature touching several domains). It works the strand end to end `kvb-code-tune`, a feature touching several domains). It works the strand end to end
and informs the representative of every line the strand touches before it and informs the representative of every line the strand touches before it
changes that line's files. changes that line's files.
- **Chair**: the earliest-elected president still running. It chairs the - **Chair**: the earliest-elected president still running. It chairs the
@@ -53,8 +52,10 @@ directory with `--session <id>`.
worktree>"`. Exit 0: elected, or already yours. Exit 3: a live agent holds worktree>"`. Exit 0: elected, or already yours. Exit 3: a live agent holds
it, and its claim is printed. Exit 4: the holder's session is dead (see it, and its claim is printed. Exit 4: the holder's session is dead (see
Succession). Succession).
4. **Elected**: continue with the Peer protocol; its message is the 4. **Elected**: the claim entered the task in the `kvb-org-rasci` register,
announcement. the organisation's single point of truth; complete it there (objective,
description, priority, roles), then continue with the Peer protocol; its
message is the announcement.
5. **Not elected**: send the holder your findings and stop on that work. 5. **Not elected**: send the holder your findings and stop on that work.
A Claude Code session cannot edit a file in a git worktree before it holds a A Claude Code session cannot edit a file in a git worktree before it holds a
@@ -74,10 +75,27 @@ records the result.
- Before starting anything that consumes shared capacity (gates, deploys, - Before starting anything that consumes shared capacity (gates, deploys,
iteration loops, swarm or cloud resources, cleanup of shared data), an agent iteration loops, swarm or cloud resources, cleanup of shared data), an agent
sends the chair what, where, for how long and its expected disk need, waits sends the chair what, where, for how long and its expected disk need, waits
for the grant, and reports the release. The chair keeps one queue, answers for the grant, and reports the release. The chair keeps one queue: a
hotfix gate (a branch from the default branch that fixes it) takes the next
free gate or test slot before every other line, the rest follows the WSJF
rank of the `kvb-org-wsjf` skill (it asks its subordinates for the Fibonacci
estimates); a running gate is never cut short for a hotfix. It answers
every request with its position, enforces the limits the operator set, every request with its position, enforces the limits the operator set,
checks every grant with `watch.py admit --need-gb <N>` (or `--kind <slot>`), checks every grant with `watch.py admit --need-gb <N>` (or `--kind <slot>`;
and never cancels a running grant to free capacity. test kinds also need CPU and RAM below the configured limits).
- A running job (deploy, gate, test, swarm run, build) is never cut short to
free capacity. The chair neither asks nor orders its owner to stop, take
down or kill it, not for an operator-prioritised slot and not for a
projected disk stop. It grants nothing new, frees idle items through the
`kvb-cleanup` flow, asks the operator for an exception, or waits. An owner's
own disk guard only alerts the chair and takes nothing down; only the
operator can order a running job down. The one exception is a hang: an
agent may end its own job, a stuck step inside it, or a process its own
ended session left behind, once at least 60 minutes passed without log
progress and a probe shows standstill (no fresh child process, no network
traffic, CPU time not moving). It secures the live state first and then
reports the timestamp, the step, the probe and the command. A peer's hung
job goes to its owner, and to the operator when the owner does not answer.
- Handover is explicit: the outgoing chair sends every peer the new holder, - Handover is explicit: the outgoing chair sends every peer the new holder,
the queue and the running grants, and keeps allocating until the incoming the queue and the running grants, and keeps allocating until the incoming
chair confirms. Two allocators never run at once. chair confirms. Two allocators never run at once.
@@ -94,9 +112,11 @@ stays silent below the thresholds.
- **Warn**: the chair and every session get the biggest consumers; the chair - **Warn**: the chair and every session get the biggest consumers; the chair
plans a cleanup and grants no large slot. plans a cleanup and grants no large slot.
- **Stop**: new heavy jobs are blocked in every session (teardown commands - **Stop**: new heavy jobs are blocked in every session (teardown commands
never), and the hook runs the configured low-risk cleanup by itself. never), and the hook runs the configured low-risk cleanup by itself; it
- **Critical**: additionally a desktop alarm to the operator. A running stack skips every worktree where a gate or deploy process still runs.
is torn down only with the operator's OK. - **Critical**: additionally a desktop alarm to the operator. Running jobs
and their stacks still keep running; only the operator can make an
exception.
- A measured fall that would reach the critical threshold within the - A measured fall that would reach the critical threshold within the
projection window raises the stage by one at once. projection window raises the stage by one at once.
- Caches together stay under their budget; the least recently used go first. - Caches together stay under their budget; the least recently used go first.
@@ -126,10 +146,10 @@ plans to it.
## Peer protocol ## Peer protocol
The one protocol for every exchange between agents; `coordinate`, `triage`, The one protocol for every exchange between agents; `kvb-org-coordinate`, `kvb-ci-triage`,
`tune`, `funnel` and `cleanup` use it. `kvb-code-tune`, `kvb-org-split` and `kvb-cleanup` use it.
1. **Look**: `council.py list` and the refs (the merge-fix skill's 1. **Look**: `council.py list` and the refs (the kvb-git-merge-fix skill's
`find_fix.py`). A claim or a committed fix answers the question without a `find_fix.py`). A claim or a committed fix answers the question without a
message. message.
2. **Claim** the office (Election). 2. **Claim** the office (Election).
@@ -139,14 +159,18 @@ The one protocol for every exchange between agents; `coordinate`, `triage`,
evidence (failing line quoted literally, run id, measurement), the evidence (failing line quoted literally, run id, measurement), the
suspected cause marked as a thesis, and the question "Already known or suspected cause marked as a thesis, and the question "Already known or
fixed on your side? Reply with a SHA or branch, or 'none'." The first line fixed on your side? Reply with a SHA or branch, or 'none'." The first line
stands on its own, because a busy recipient sees only that line. stands on its own, because a busy recipient sees only that line. In the
`kvb-org-rasci` register the peers asked are C, the chair that grants
capacity is S.
4. **Wait up to 5 minutes** before building anything, investigating read-only 4. **Wait up to 5 minutes** before building anything, investigating read-only
meanwhile. A fix that arrives goes through the project's normal path meanwhile. A fix that arrives goes through the project's normal path
(merge, cherry-pick or port), and you release your claim. Silence is (merge, cherry-pick or port), and you release your claim. Silence is
neither a "no" nor a competing claim: after 5 minutes, proceed. neither a "no" nor a competing claim: after 5 minutes, proceed.
5. **Report** to the same peers: commit, branch, what changed, the evidence it 5. **Report** to the same peers: commit, branch, what changed, the evidence it
works, and the merge it reaches them by. Report an abandoned or failed works, and the merge it reaches them by. Report an abandoned or failed
attempt too. Then release the claim. attempt too. In the register, enter the peers told as I and mark every key
result the evidence proves as met. Then release the claim: the task ends
`done`, or `stopped` while a key result is open.
- Never invent a peer's answer. - Never invent a peer's answer.
- A peer's report is not evidence: read the SHA, log or artifact it names - A peer's report is not evidence: read the SHA, log or artifact it names
@@ -180,7 +204,10 @@ For a failure or a fix that other branches share through merges:
## Output ## Output
On /democracy, one table of the current offices from `council.py list`, stale On /kvb-org-organize, one table of the current offices from `council.py list`, stale
claims marked: claims marked:
| Office | Holder | Since | Work | Grants or queue | | Office | Holder | Since | Work | Grants or queue |
Who carries which task, with priority and status, is the `kvb-org-rasci`
register's `show`.
@@ -1,10 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Claim, release and list democracy offices in the shared council directory. """Claim, release and list kvb-org-organize offices in the shared council directory.
Environment: Environment:
AGENTS_COUNCIL_ROOT: council directory shared by every agent on the host; AGENTS_COUNCIL_ROOT: council directory shared by every agent on the host;
/tmp/agents-council unless set, because all agents must agree on it. /tmp/agents-council unless set, because all agents must agree on it.
CLAUDE_CODE_SESSION_ID: the caller's session; other runtimes pass --session. CLAUDE_CODE_SESSION_ID: the caller's session, taken over --session; other
runtimes pass --session.
Exit codes: Exit codes:
0 done, 2 usage error, 3 office held by a live session, 0 done, 2 usage error, 3 office held by a live session,
@@ -16,10 +17,12 @@ from __future__ import annotations
import argparse import argparse
import datetime import datetime
import importlib.util
import json import json
import os import os
import re import re
import shutil import shutil
import subprocess
import sys import sys
import time import time
from pathlib import Path from pathlib import Path
@@ -28,6 +31,7 @@ ROOT = Path(os.environ.get("AGENTS_COUNCIL_ROOT", "/tmp/agents-council"))
HEARTBEAT_LIMIT = 3600 HEARTBEAT_LIMIT = 3600
OFFICE = re.compile(r"^[a-z0-9][a-z0-9._-]*(/[a-z0-9][a-z0-9._-]*)*$") OFFICE = re.compile(r"^[a-z0-9][a-z0-9._-]*(/[a-z0-9][a-z0-9._-]*)*$")
CLAIM = "claim.json" CLAIM = "claim.json"
RASCI = Path(__file__).resolve().parents[2] / "kvb-org-rasci" / "scripts" / "rasci.py"
def live_sessions() -> dict[str, str | None]: def live_sessions() -> dict[str, str | None]:
@@ -43,11 +47,11 @@ def live_sessions() -> dict[str, str | None]:
def caller(session: str | None) -> tuple[str, str]: def caller(session: str | None) -> tuple[str, str]:
if session:
return session, "other"
env = os.environ.get("CLAUDE_CODE_SESSION_ID") env = os.environ.get("CLAUDE_CODE_SESSION_ID")
if env: if env:
return env, "claude-code" return env, "claude-code"
if session:
return session, "other"
print("council: no session id; pass --session", file=sys.stderr) print("council: no session id; pass --session", file=sys.stderr)
sys.exit(2) sys.exit(2)
@@ -83,7 +87,7 @@ def claims() -> list[tuple[str, dict]]:
return found return found
def write_claim(path: Path, office: str, sid: str, runtime: str, work: str) -> None: def write_claim(path: Path, office: str, sid: str, runtime: str, work: str) -> dict:
claim = { claim = {
"office": office, "office": office,
"session": sid, "session": sid,
@@ -94,6 +98,30 @@ def write_claim(path: Path, office: str, sid: str, runtime: str, work: str) -> N
"work": work, "work": work,
} }
(path / CLAIM).write_text(json.dumps(claim), encoding="utf-8") (path / CLAIM).write_text(json.dumps(claim), encoding="utf-8")
return claim
def here() -> dict[str, str | None]:
try:
head = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"], capture_output=True, text=True, check=False
)
except OSError:
head = None
branch = head.stdout.strip() if head and head.returncode == 0 else ""
return {"workdir": os.getcwd(), "branch": branch if branch not in ("", "HEAD") else None}
def register(office: str, holder: str, status: str, **extra: str | None) -> None:
if not office.startswith(("work/", "cluster/")):
return
try:
spec = importlib.util.spec_from_file_location("rasci", RASCI)
rasci = importlib.util.module_from_spec(spec)
spec.loader.exec_module(rasci)
rasci.track(office, holder, status, **extra)
except (OSError, SystemExit) as exc:
print(f"council: {office} is missing from the rasci register ({exc})", file=sys.stderr)
def cmd_claim(args: argparse.Namespace) -> int: def cmd_claim(args: argparse.Namespace) -> int:
@@ -105,12 +133,21 @@ def cmd_claim(args: argparse.Namespace) -> int:
except FileExistsError: except FileExistsError:
claim = read_claim(path) claim = read_claim(path)
if claim is not None and claim["session"] == sid: if claim is not None and claim["session"] == sid:
write_claim(path, args.office, sid, runtime, args.work) claim = write_claim(path, args.office, sid, runtime, args.work)
register(args.office, claim["name"] or sid, "running", description=args.work, **here())
print(f"held {args.office}") print(f"held {args.office}")
return 0 return 0
print(json.dumps(claim) if claim else f"{args.office}: claim in progress") print(json.dumps(claim) if claim else f"{args.office}: claim in progress")
return 4 if is_stale(claim, live_sessions()) else 3 return 4 if is_stale(claim, live_sessions()) else 3
write_claim(path, args.office, sid, runtime, args.work) claim = write_claim(path, args.office, sid, runtime, args.work)
register(
args.office,
claim["name"] or sid,
"running",
description=args.work,
previous=getattr(args, "previous", None),
**here(),
)
print(f"elected {args.office}") print(f"elected {args.office}")
return 0 return 0
@@ -121,8 +158,10 @@ def cmd_takeover(args: argparse.Namespace) -> int:
if path.exists() and not is_stale(claim, live_sessions()): if path.exists() and not is_stale(claim, live_sessions()):
print(json.dumps(claim) if claim else f"{args.office}: claim in progress") print(json.dumps(claim) if claim else f"{args.office}: claim in progress")
return 3 return 3
if claim is not None:
args.previous = claim["name"] or claim["session"]
# Two successors racing between this check and the rename can both win; # Two successors racing between this check and the rename can both win;
# democracy names exactly one successor per office to keep that window closed. # kvb-org-organize names exactly one successor per office to keep that window closed.
if path.exists(): if path.exists():
tomb = path.with_name(f"{path.name}.stale-{os.getpid()}-{time.time_ns()}") tomb = path.with_name(f"{path.name}.stale-{os.getpid()}-{time.time_ns()}")
try: try:
@@ -145,6 +184,7 @@ def cmd_release(args: argparse.Namespace) -> int:
print(json.dumps(claim)) print(json.dumps(claim))
return 3 return 3
shutil.rmtree(path) shutil.rmtree(path)
register(args.office, claim["name"] or claim["session"], "done")
print(f"released {args.office}") print(f"released {args.office}")
return 0 return 0
@@ -186,10 +226,10 @@ def cmd_gate(_: argparse.Namespace) -> int:
return 0 return 0
script = Path(__file__).resolve() script = Path(__file__).resolve()
print( print(
"democracy: this session holds no claim, so it may not edit files in a git " "kvb-org-organize: this session holds no claim, so it may not edit files in a git "
f"worktree. Claim the work first: python3 {script} claim work/<slug> " f"worktree. Claim the work first: python3 {script} claim work/<slug> "
"--work '<one line>'. Exit 3 or 4 means another agent holds it; read the " "--work '<one line>'. Exit 3 or 4 means another agent holds it; read the "
"democracy skill.", "kvb-org-organize skill.",
file=sys.stderr, file=sys.stderr,
) )
return 2 return 2
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Watch disk, Docker and caches for the democracy resource chair. """Watch disk, Docker and caches for the kvb-org-organize resource chair.
Usage: Usage:
watch.py watch hook (UserPromptSubmit, PostToolUse): measure at most once watch.py watch hook (UserPromptSubmit, PostToolUse): measure at most once
@@ -25,8 +25,11 @@ projection_minutes, interval_seconds, cache_interval_seconds, heavy_commands,
never_block (regexes), cleanup (shell commands), worktree_cleanup (shell never_block (regexes), cleanup (shell commands), worktree_cleanup (shell
commands with {worktree}), worktrees_command (shell command printing the commands with {worktree}), worktrees_command (shell command printing the
running stacks' worktree paths, one per line), worktree_marker (path relative running stacks' worktree paths, one per line), worktree_marker (path relative
to a worktree that must exist), cache_dirs, cache_budget_percent, to a worktree that must exist), busy_processes (process names; a worktree
slot_need_gb {kind: GB}, notify (argv the alert text is appended to). where one of them runs is skipped), cache_dirs, cache_budget_percent,
slot_need_gb {kind: GB}, notify (argv the alert text is appended to),
load_limits {cpu_percent, mem_percent, sample_seconds, kinds}: admit for a
kind listed in kinds also needs CPU busy and RAM used below the limits.
""" """
from __future__ import annotations from __future__ import annotations
@@ -205,9 +208,17 @@ def cmd_watch(cfg: dict) -> int:
return 0 return 0
HEREDOC = re.compile(r"<<-?\s*(['\"]?)(\w+)\1[^\n]*\n.*?\n\s*\2(?=\s|\)|$)", re.DOTALL)
QUOTED = re.compile(r"'[^']*'|\"(?:\\.|[^\"\\])*\"")
def executable_text(command: str) -> str:
return QUOTED.sub("''", HEREDOC.sub("", command))
def cmd_block(cfg: dict) -> int: def cmd_block(cfg: dict) -> int:
payload = json.load(sys.stdin) payload = json.load(sys.stdin)
command = (payload.get("tool_input") or {}).get("command", "") command = executable_text((payload.get("tool_input") or {}).get("command", ""))
stage = read_json(STATE).get("stage", "ok") stage = read_json(STATE).get("stage", "ok")
if stage not in ("stop", "critical"): if stage not in ("stop", "critical"):
return 0 return 0
@@ -215,7 +226,7 @@ def cmd_block(cfg: dict) -> int:
return 0 return 0
if any(re.search(p, command) for p in cfg["heavy_commands"]): if any(re.search(p, command) for p in cfg["heavy_commands"]):
print( print(
f"democracy: resource stage is {stage}; new heavy jobs are blocked until " f"kvb-org-organize: resource stage is {stage}; new heavy jobs are blocked until "
"the disk recovers. Teardown commands still run. Ask the resource chair.", "the disk recovers. Teardown commands still run. Ask the resource chair.",
file=sys.stderr, file=sys.stderr,
) )
@@ -223,13 +234,49 @@ def cmd_block(cfg: dict) -> int:
return 0 return 0
def cpu_times() -> tuple[int, int]:
fields = [int(v) for v in Path("/proc/stat").read_text(encoding="utf-8").split("\n", 1)[0].split()[1:]]
return sum(fields), fields[3] + fields[4]
def cpu_busy_percent(before: tuple[int, int], after: tuple[int, int]) -> float:
total = after[0] - before[0]
idle = after[1] - before[1]
return 100.0 * (total - idle) / total
def mem_used_percent(meminfo: str) -> float:
values = {}
for line in meminfo.splitlines():
key, _, rest = line.partition(":")
if rest:
values[key] = int(rest.split()[0])
return 100.0 * (1 - values["MemAvailable"] / values["MemTotal"])
def load(cfg: dict) -> tuple[float, float]:
before = cpu_times()
time.sleep(cfg["load_limits"]["sample_seconds"])
cpu = cpu_busy_percent(before, cpu_times())
return cpu, mem_used_percent(Path("/proc/meminfo").read_text(encoding="utf-8"))
def cmd_admit(cfg: dict, args: argparse.Namespace) -> int: def cmd_admit(cfg: dict, args: argparse.Namespace) -> int:
need = args.need_gb if args.need_gb is not None else cfg["slot_need_gb"][args.kind] need = args.need_gb if args.need_gb is not None else cfg["slot_need_gb"][args.kind]
free, _ = disk(cfg) free, _ = disk(cfg)
left = free - need left = free - need
floor = cfg["thresholds_gb"]["stop"] floor = cfg["thresholds_gb"]["stop"]
verdict = "grant" if left >= floor else "deny" passed = [left >= floor]
print(f"{verdict}: free {free:.1f} GB, need {need} GB, leaves {left:.1f} GB (floor {floor} GB)") report = [f"free {free:.1f} GB, need {need} GB, leaves {left:.1f} GB (floor {floor} GB)"]
limits = cfg["load_limits"]
if args.kind in limits["kinds"]:
cpu, mem = load(cfg)
passed += [cpu < limits["cpu_percent"], mem < limits["mem_percent"]]
report.append(
f"cpu {cpu:.0f} % (limit {limits['cpu_percent']}), ram {mem:.0f} % (limit {limits['mem_percent']})"
)
verdict = "grant" if all(passed) else "deny"
print(f"{verdict}: {'; '.join(report)}")
return 0 if verdict == "grant" else 3 return 0 if verdict == "grant" else 3
@@ -275,9 +322,25 @@ def running_worktrees(cfg: dict) -> list[str]:
return [t for t in trees if os.path.exists(os.path.join(t, cfg["worktree_marker"]))] return [t for t in trees if os.path.exists(os.path.join(t, cfg["worktree_marker"]))]
def busy(tree: str, names: list[str]) -> bool:
root = os.path.realpath(tree)
for pid in filter(str.isdigit, os.listdir("/proc")):
try:
if Path(f"/proc/{pid}/comm").read_text(encoding="utf-8").strip() not in names:
continue
cwd = os.readlink(f"/proc/{pid}/cwd")
except OSError:
continue
if cwd == root or cwd.startswith(root + os.sep):
return True
return False
def run_cleanup(cfg: dict) -> list[str]: def run_cleanup(cfg: dict) -> list[str]:
commands = list(cfg["cleanup"]) commands = list(cfg["cleanup"])
for tree in running_worktrees(cfg): for tree in running_worktrees(cfg):
if busy(tree, cfg["busy_processes"]):
continue
commands += [t.format(worktree=shlex.quote(tree)) for t in cfg["worktree_cleanup"]] commands += [t.format(worktree=shlex.quote(tree)) for t in cfg["worktree_cleanup"]]
results = [] results = []
for command in commands: for command in commands:
+74
View File
@@ -0,0 +1,74 @@
---
name: kvb-org-perform
description: >
Pick `model` and `effort` per subagent from a fixed table, cheaper only at
90 % confidence of an equal result. Apply AUTOMATICALLY before every Agent
tool call.
---
A subagent runs on the main model and effort unless the spawn names others.
Set `model` and `effort` on an Agent call only when the task clearly matches a
row below and you are at least 90 % sure the cheaper setting delivers the same
result as the default: no lost findings, no rework. Cost counts per finished
task, retries included. Otherwise set neither, and the default stays.
## Table
| Task class | Subagent's task | `model` | `effort` | Evidence |
| --- | --- | --- | --- | --- |
| `lookup` | Read-only: find files, symbols or call sites, grep, list, read and quote, pull a log line or field, summarise a given text without judging it | `haiku` | `low` | Haiku 5.5 costs 1/40 of Opus 5.5 per token and is the speed tier; Anthropic names Haiku at low effort for simple, speed-critical and reading-heavy worker tasks; Explore and the cavecrew investigator already run on it |
| `mechanical-edit` | An edit the brief fully specifies (exact old and new text, a rename, deleting listed lines) in one or two files | `haiku` | `low` | nothing is left to decide, and the parent checks the diff |
| `judgement` | Everything else: root cause, skeptic or dialectic antithesis, review, security or legal checks, design, new or multi-file code, writing tests, performance work | omit | omit | no evidence at 90 % that a cheaper setting gives the same result |
Prices per million tokens in and out (claude-api reference, cached
2026-10-06): Haiku 5.5 $0.10 / $0.50, Sonnet 5.5 $2 / $10, Opus 5.5 $4 / $20,
Fable 5.1 $10 / $50.
## Spawn
1. Pick the class. A task that mixes classes, or whose class you hesitate
over, is `judgement`.
2. Start the brief with the line `task-class: <class>`; the spawn hook logs
it together with `model`, `effort` and the Agent `description`.
3. Set `model` and `effort` from the row; for `judgement` set neither.
4. Enter the spawn in the `kvb-org-rasci` register: the subagent as R and as
`--worker "subagent <type> (<model>, <effort>)"` of the task it serves.
- An agent type whose definition sets its own model (Explore, Plan, the
cavecrew agents) keeps it; never pass `model` or `effort` for it.
- Never pass `fable` or `opus`: neither is cheaper than the default.
- A Haiku agent that reports the task needs judgement, or returns an
incomplete result, is not retried on Haiku: the default continues it.
- Never change the default itself (settings, `CLAUDE_CODE_SUBAGENT_MODEL`,
agent definitions); this skill only chooses per call.
## Feedback
Once you have judged the subagent's result, rate it under the same Agent
`description`:
```bash
${CLAUDE_SKILL_DIR}/scripts/log.py feedback --description "<description>" --completed yes|no --tests pass|fail|none --redo yes|no
```
`--redo yes` when the default had to redo or finish the work. When the
operator later asks a follow-up question about that result or corrects it:
```bash
${CLAUDE_SKILL_DIR}/scripts/log.py followup --description "<description>" --text "<question or correction>"
```
All sessions share `~/.local/state/agents-council/subagent-model.jsonl`.
Inside a sandbox that cannot write it, the script prints the line as
`pending:` and the PostToolUse hook stores it.
## Evaluation
`log.py report` shows per task class, model and effort the runs, the rated
runs and the share with the same result: completed, not redone, no failing
test, no follow-up. A row is added, or a cheaper setting extended to a class,
only when the report shows at least 90 % over at least 20 rated runs, and only
after the operator confirms it (for example through `/kvb-skill-tune`). A row whose
rate falls below 90 % over 20 rated runs goes back to the default. `sonnet`
enters the table only this way, and a trial run on a setting the table does
not hold needs the operator's order.
+205
View File
@@ -0,0 +1,205 @@
#!/usr/bin/env python3
"""Log subagent spawns with their quality feedback, and evaluate the table.
Usage:
log.py spawn hook (PreToolUse on Agent): append the spawn line
log.py feedback --description D --completed yes|no --tests pass|fail|none --redo yes|no
rate the latest spawn with this Agent description
log.py followup --description D --text T
record an operator follow-up question or correction on it
log.py persist hook (PostToolUse on Bash): append the one line a sandboxed
feedback or followup printed as `pending: <json>`, only
when the command was that bare call and the line is
well-formed and from the hook's own session
log.py report runs, rated runs and same-result rate per task class,
model and effort; a row qualifies at >= 90 % over >= 20
Environment:
SUBAGENT_MODEL_LOG: the log shared by every session;
~/.local/state/agents-council/subagent-model.jsonl unless set.
CLAUDE_CODE_SESSION_ID: the session that rates its spawns.
"""
from __future__ import annotations
import argparse
import datetime
import json
import os
import re
import shlex
import sys
from pathlib import Path
LOG = Path(
os.environ.get(
"SUBAGENT_MODEL_LOG",
str(Path.home() / ".local" / "state" / "agents-council" / "subagent-model.jsonl"),
)
)
TASK_CLASS = re.compile(r"^task-class:\s*(\S+)", re.MULTILINE)
PENDING = "pending: "
SCRIPT = "kvb-org-perform/scripts/log.py"
CONTROL = re.compile(r"^[();<>|&]+$")
KEYS = {
"feedback": {"kind", "time", "session", "description", "completed", "tests", "redo"},
"followup": {"kind", "time", "session", "description", "text"},
}
QUALIFY_RUNS = 20
QUALIFY_RATE = 0.9
def now() -> str:
return datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds")
def append(line: dict) -> None:
LOG.parent.mkdir(parents=True, exist_ok=True)
with LOG.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(line, sort_keys=True) + "\n")
def record(line: dict) -> int:
try:
append(line)
except OSError:
print(PENDING + json.dumps(line, sort_keys=True))
return 0
def cmd_spawn() -> int:
payload = json.load(sys.stdin)
tool = payload.get("tool_input") or {}
match = TASK_CLASS.search(tool.get("prompt", ""))
append({
"kind": "spawn",
"time": now(),
"session": payload.get("session_id", ""),
"description": tool.get("description", ""),
"subagent_type": tool.get("subagent_type") or "general-purpose",
"task_class": match.group(1) if match else "unclassified",
"model": tool.get("model") or "default",
"effort": tool.get("effort") or "default",
})
return 0
def rated(kind: str, args: argparse.Namespace, **fields: object) -> dict:
session = os.environ.get("CLAUDE_CODE_SESSION_ID")
if not session:
sys.exit("log.py: CLAUDE_CODE_SESSION_ID is not set")
return {"kind": kind, "time": now(), "session": session, "description": args.description, **fields}
def invoked_kind(command: str) -> str | None:
if "$(" in command or "`" in command:
return None
lexer = shlex.shlex(command, posix=True, punctuation_chars=True)
lexer.whitespace_split = True
try:
tokens = list(lexer)
except ValueError:
return None
if any(CONTROL.match(token) for token in tokens):
return None
if tokens[:1] in (["python3"], ["python"]):
tokens = tokens[1:]
if len(tokens) < 2 or not tokens[0].endswith(SCRIPT) or tokens[1] not in KEYS:
return None
return tokens[1]
def well_formed(line: object, kind: str, session: str) -> bool:
if not isinstance(line, dict) or set(line) != KEYS[kind] or line["kind"] != kind or line["session"] != session:
return False
if not all(isinstance(line[key], str) for key in ("time", "description")):
return False
if kind == "feedback":
return isinstance(line["completed"], bool) and isinstance(line["redo"], bool) and line["tests"] in ("pass", "fail", "none")
return isinstance(line["text"], str)
def cmd_persist() -> int:
payload = json.load(sys.stdin)
kind = invoked_kind((payload.get("tool_input") or {}).get("command", ""))
if not kind:
return 0
response = payload.get("tool_response")
stdout = response.get("stdout", "") if isinstance(response, dict) else str(response or "")
pending = [text[len(PENDING):] for text in stdout.splitlines() if text.startswith(PENDING)]
if len(pending) != 1:
return 0
try:
line = json.loads(pending[0])
except json.JSONDecodeError:
return 0
if well_formed(line, kind, payload.get("session_id", "")):
append(line)
return 0
def runs() -> list[dict]:
current: dict[tuple[str, str], dict] = {}
found: list[dict] = []
for text in LOG.read_text(encoding="utf-8").splitlines() if LOG.exists() else []:
line = json.loads(text)
key = (line["session"], line["description"])
if line["kind"] == "spawn":
current[key] = {**line, "feedback": None, "followups": 0}
found.append(current[key])
elif key in current and line["kind"] == "feedback":
current[key]["feedback"] = line
elif key in current and line["kind"] == "followup":
current[key]["followups"] += 1
return found
def same_result(run: dict) -> bool:
feedback = run["feedback"]
return feedback["completed"] and not feedback["redo"] and feedback["tests"] != "fail" and not run["followups"]
def cmd_report() -> int:
rows: dict[tuple[str, str, str], list[dict]] = {}
for run in runs():
rows.setdefault((run["task_class"], run["model"], run["effort"]), []).append(run)
print("task_class\tmodel\teffort\truns\trated\tsame\trate\tqualifies")
for (task_class, model, effort), group in sorted(rows.items()):
rated_runs = [r for r in group if r["feedback"]]
same = sum(map(same_result, rated_runs))
rate = same / len(rated_runs) if rated_runs else 0.0
qualifies = len(rated_runs) >= QUALIFY_RUNS and rate >= QUALIFY_RATE
print(f"{task_class}\t{model}\t{effort}\t{len(group)}\t{len(rated_runs)}\t{same}\t{rate:.0%}\t{'yes' if qualifies else 'no'}")
return 0
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = parser.add_subparsers(dest="command", required=True)
sub.add_parser("spawn")
feedback = sub.add_parser("feedback")
feedback.add_argument("--description", required=True)
feedback.add_argument("--completed", required=True, choices=("yes", "no"))
feedback.add_argument("--tests", required=True, choices=("pass", "fail", "none"))
feedback.add_argument("--redo", required=True, choices=("yes", "no"))
followup = sub.add_parser("followup")
followup.add_argument("--description", required=True)
followup.add_argument("--text", required=True)
sub.add_parser("persist")
sub.add_parser("report")
args = parser.parse_args()
if args.command == "spawn":
return cmd_spawn()
if args.command == "feedback":
return record(rated(
"feedback", args, completed=args.completed == "yes", tests=args.tests, redo=args.redo == "yes"
))
if args.command == "followup":
return record(rated("followup", args, text=args.text))
if args.command == "persist":
return cmd_persist()
return cmd_report()
if __name__ == "__main__":
sys.exit(main())
+63
View File
@@ -0,0 +1,63 @@
---
name: kvb-org-rasci
description: >
RASCI/OKR register of tasks: roles, key results, worker, status.
Trigger on /kvb-org-rasci, "rasci", "raci", "zuständigkeiten".
---
The register is the single point of truth of the organisation: every agent
records here what it works on, and claims, queue, handover and reports run
over it. `${CLAUDE_SKILL_DIR}/scripts/rasci.py` keeps it in
`<council root>/rasci.json`. A `kvb-org-organize` claim enters its task by
itself (holder as R and A, `running`, start, workdir, branch); its release
sets `done` and the end, or `stopped` while a key result is still open.
```bash
rasci.py set <task> --title <t> --objective <o> --description <d> --prio <n> --status queued|running|blocked|done|stopped --worker "<agent | subagent <type> (<model>)>" --workdir <dir> --branch <b> --artifact <kind>:<ref> --tool <script or command> [--start <iso>] [--end <iso>] [--drop-artifact <a>] [--drop-tool <t>]
rasci.py kr <task> add "<measurable result>" | met <n> | open <n> | rm <n>
rasci.py role <task> "<party>" <letters>
rasci.py get <task>
rasci.py rm <task>
rasci.py show [--all]
rasci.py artifacts [--ended] [--task <task>]
rasci.py estimate <task> "<party>" --value <fib> --complexity <fib>
rasci.py wsjf [--all]
rasci.py tools
```
## Objective and key results
The objective says what the task is for; its key results are the measurable
outcomes that prove it reached (a test green, a run passing, a release
published). `done` is refused while one is open; a task ended short of them is
`stopped`. `show` puts both in the OKR column as `<objective> (<met>/<all> KR)`.
## Roles
- **R Responsible**: does the task: the claim holder, or the subagent it runs.
- **A Accountable**: exactly one; takes the final decision and carries the
overall responsibility. The claim holder, unless the operator decides.
- **S Supportive**: actively helps carry it out: a peer's fix, the chair's
grant, a stack or runner.
- **C Consulted**: asked for input before acting: line representatives,
lineage owners, the operator on a pending decision.
- **I Informed**: kept up to date on progress or result, without a say.
`-` clears a party; the operator is the party `Operator`.
## Rules
- On start, `set` title, objective, description, priority and worker, `kr
add` every key result, and `role` every party beyond R and A; on each
change, update the status. The worker is the one session or subagent
executing the task right now.
- Artifacts share this table and follow the `kvb-org-artefacts` skill:
record each one when it appears, drop it once deleted. Add every script or
tool the task ran (`--tool`): `tools` ranks them for `kvb-dream`.
- Priority is the rank of the `kvb-org-wsjf` skill: an operator's pin
(`--prio`) first, then WSJF from the estimates the chair collects; `show`
prints the rank as Prio and the score as WSJF.
- Write only your own rows; another session's task belongs to that session.
- On /kvb-org-rasci print `show` unchanged; its `gaps:` line names tasks
without exactly one A, without R, or open without a key result. `get`
prints one task in full, key results numbered.
+497
View File
@@ -0,0 +1,497 @@
#!/usr/bin/env python3
"""Keep the RASCI register of the organisation's tasks in a local JSON file.
Environment:
AGENTS_COUNCIL_ROOT: council directory shared by every agent on the host;
the register is <root>/rasci.json, and `show` also lists every work/
and cluster/ claim of the council there. /tmp/agents-council unless set.
Exit codes:
0 done, 2 usage error.
"""
from __future__ import annotations
import argparse
import contextlib
import datetime
import fcntl
import importlib.util
import json
import os
import re
import statistics
import sys
import tempfile
from collections.abc import Iterator
from pathlib import Path
ROOT = Path(os.environ.get("AGENTS_COUNCIL_ROOT", "/tmp/agents-council"))
DB = ROOT / "rasci.json"
LOCK = ROOT / "rasci.lock"
COUNCIL = Path(__file__).resolve().parents[2] / "kvb-org-organize" / "scripts" / "council.py"
TASK = re.compile(r"^[a-z0-9][a-z0-9._-]*(/[a-z0-9][a-z0-9._-]*)*$")
LETTERS = re.compile(r"^(?!.*(.).*\1)[RASCI]+$")
ORDER = "RASCI"
STATUSES = ("queued", "running", "blocked", "done", "stopped")
ENDED = ("done", "stopped")
FIELDS = (
"title", "objective", "description", "prio", "status", "worker", "start", "end", "workdir",
"branch",
)
TEXT_FIELDS = ("title", "objective", "description", "worker", "workdir", "branch")
LIST_FIELDS = {"tools": "tool"}
ARTIFACT = re.compile(r"^[a-z]+:\S")
FIBONACCI = (1, 2, 3, 5, 8, 13, 21, 34, 55, 89)
OPERATOR = "Operator"
def fail(message: str) -> None:
print(f"rasci: {message}", file=sys.stderr)
sys.exit(2)
def check_task(task: str) -> None:
if not TASK.match(task):
fail(f"invalid task {task!r}; use a slug like work/<slug>")
def stamp(value: str = "now") -> str:
if value == "now":
return datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds")
try:
parsed = datetime.datetime.fromisoformat(value)
except ValueError:
fail(f"invalid time {value!r}; use ISO 8601 or now")
if parsed.tzinfo is None:
parsed = parsed.astimezone()
return parsed.astimezone(datetime.UTC).isoformat(timespec="seconds")
def letters_of(*parts: str) -> str:
return "".join(letter for letter in ORDER if any(letter in part for part in parts))
def load() -> dict[str, dict]:
try:
return json.loads(DB.read_text(encoding="utf-8"))
except FileNotFoundError:
return {}
except ValueError:
fail(f"{DB} is not valid JSON")
@contextlib.contextmanager
def database() -> Iterator[dict[str, dict]]:
ROOT.mkdir(parents=True, exist_ok=True)
with LOCK.open("a") as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
tasks = load()
yield tasks
with tempfile.NamedTemporaryFile(
"w", dir=ROOT, prefix=".rasci-", delete=False, encoding="utf-8"
) as tmp:
json.dump(tasks, tmp, indent=1, sort_keys=True)
os.replace(tmp.name, DB)
def open_results(entry: dict) -> int:
return sum(not kr["met"] for kr in entry.get("key_results", []))
def apply_status(entry: dict, status: str) -> None:
if status not in ENDED:
entry.pop("end", None)
entry["status"] = status
if status == "running":
entry.setdefault("start", stamp())
if status in ENDED:
entry.setdefault("end", stamp())
def track(task: str, holder: str, status: str, *, description: str | None = None,
previous: str | None = None, workdir: str | None = None,
branch: str | None = None) -> None:
"""Write a council claim (status running) or release (status done) through.
Args:
task: the claimed office, e.g. work/<slug>.
holder: the session holding the claim.
status: running on claim and takeover, done on release; a release with
open key results ends as stopped.
description: the claim's --work line, kept unless one is recorded.
previous: the stale holder a takeover replaces; loses its R and A.
workdir: the claiming process's directory, kept unless one is recorded.
branch: the checked-out branch there, kept unless one is recorded.
"""
with database() as tasks:
entry = tasks.setdefault(task, {"roles": {}})
roles = entry.setdefault("roles", {})
if previous and previous != holder:
left = roles.pop(previous, "").replace("R", "").replace("A", "")
if left:
roles[previous] = left
if entry.get("worker") == previous:
entry["worker"] = holder
entry.setdefault("worker", holder)
for field, value in (("description", description), ("workdir", workdir), ("branch", branch)):
if value:
entry.setdefault(field, value)
if status == "done" and open_results(entry):
status = "stopped"
if status in ENDED or entry.get("status") in (None, "queued", *ENDED):
apply_status(entry, status)
if status not in ENDED:
accountable = any("A" in v for p, v in roles.items() if p != holder)
roles[holder] = letters_of(roles.get(holder, ""), "R" if accountable else "RA")
def claimed() -> dict[str, dict]:
if not COUNCIL.is_file():
return {}
spec = importlib.util.spec_from_file_location("council", COUNCIL)
council = importlib.util.module_from_spec(spec)
spec.loader.exec_module(council)
live = council.live_sessions()
rows = {}
for office, claim in council.claims():
if not office.startswith(("work/", "cluster/")):
continue
holder = claim["name"] or claim["session"]
rows[office] = {
"description": claim["work"],
"status": "claimed",
"worker": holder,
"start": claim["since"],
"roles": {holder: "RA"},
"stale": council.is_stale(claim, live),
}
return rows
def merged(include_done: bool) -> dict[str, dict]:
rows = claimed()
for task, entry in load().items():
base = rows.get(task, {"status": "queued", "roles": {}})
fields = {k: v for k, v in entry.items() if k != "roles"}
rows[task] = {**base, **fields, "roles": {**base["roles"], **entry.get("roles", {})}}
for row in rows.values():
if row.get("stale"):
row["status"] = "stale"
return {
task: row for task, row in rows.items() if include_done or row["status"] not in ENDED
}
def cell(text: object, width: int) -> str:
text = " ".join(str(text).split()).replace("|", "\\|")
return text if len(text) <= width else text[: width - 1] + "…"
def when(value: str | None) -> str:
if not value:
return "-"
return datetime.datetime.fromisoformat(value).astimezone().strftime("%m-%d %H:%M")
def wsjf(row: dict) -> tuple[float, float, float] | None:
estimates = list(row.get("estimates", {}).values())
if not estimates:
return None
value = statistics.median(e["value"] for e in estimates)
complexity = statistics.median(e["complexity"] for e in estimates)
return value, complexity, round(value / complexity, 2)
def ranked(rows: dict[str, dict]) -> list[str]:
def key(task: str) -> tuple:
row = rows[task]
score = wsjf(row)
pinned = row.get("prio")
return (pinned is None, pinned or 0, score is None, -(score[2] if score else 0), task)
return sorted(rows, key=key)
def okr(row: dict) -> str:
results = row.get("key_results", [])
text = cell(row.get("objective") or "-", 40)
if results:
text += f" ({sum(kr['met'] for kr in results)}/{len(results)} KR)"
return text
def gaps(task: str, row: dict) -> list[str]:
found = []
roles = row["roles"]
accountable = sum("A" in letters for letters in roles.values())
if accountable != 1:
found.append(f"{task} has {accountable} A")
if not any("R" in letters for letters in roles.values()):
found.append(f"{task} has no R")
if not row.get("key_results") and row["status"] not in ENDED:
found.append(f"{task} has no key result")
return found
def cmd_set(args: argparse.Namespace) -> int:
check_task(args.task)
if args.prio is not None and args.prio < 1:
fail("--prio must be 1 or higher")
times = {f: stamp(getattr(args, f)) for f in ("start", "end") if getattr(args, f)}
for ref in args.artifact:
if not ARTIFACT.match(ref):
fail(f"invalid artifact {ref!r}; use <kind>:<ref>, e.g. image:<name>:<tag>")
with database() as tasks:
entry = tasks.setdefault(args.task, {"roles": {}})
if args.status == "done" and open_results(entry):
fail(f"{args.task}: {open_results(entry)} key result(s) open; mark them met or set stopped")
for field in (*TEXT_FIELDS, "prio"):
value = getattr(args, field)
if value is not None:
entry[field] = value
if args.status:
apply_status(entry, args.status)
entry.update(times)
for field, flag in LIST_FIELDS.items():
drop = getattr(args, f"drop_{flag}")
items = [i for i in entry.get(field, []) if i not in drop]
items += [i for i in dict.fromkeys(getattr(args, flag)) if i not in items]
if items:
entry[field] = items
else:
entry.pop(field, None)
kept = [a for a in entry.get("artifacts", []) if a["ref"] not in args.drop_artifact]
known = {a["ref"] for a in kept}
kept += [{"ref": r, "added": stamp()} for r in dict.fromkeys(args.artifact) if r not in known]
if kept:
entry["artifacts"] = kept
else:
entry.pop("artifacts", None)
print(f"set {args.task}")
return 0
def cmd_role(args: argparse.Namespace) -> int:
check_task(args.task)
letters = args.letters.upper()
if letters != "-" and not LETTERS.match(letters):
fail("letters must be distinct ones of R, A, S, C, I, or - to clear")
with database() as tasks:
roles = tasks.setdefault(args.task, {"roles": {}}).setdefault("roles", {})
if letters == "-":
roles.pop(args.party, None)
else:
roles[args.party] = letters_of(letters)
print(f"{args.task}: {args.party} {letters}")
return 0
def cmd_kr(args: argparse.Namespace) -> int:
check_task(args.task)
with database() as tasks:
entry = tasks.setdefault(args.task, {"roles": {}})
results = entry.setdefault("key_results", [])
if args.action == "add":
if all(kr["text"] != args.value for kr in results):
results.append({"text": args.value, "met": False})
else:
if not args.value.isdigit() or not 1 <= int(args.value) <= len(results):
fail(f"{args.task}: no key result {args.value!r}; `get` lists them by number")
index = int(args.value) - 1
if args.action == "rm":
results.pop(index)
else:
results[index]["met"] = args.action == "met"
if not results:
entry.pop("key_results")
print(f"{args.task}: key result {args.action} {args.value}")
return 0
def cmd_get(args: argparse.Namespace) -> int:
row = merged(True).get(args.task)
if row is None:
fail(f"{args.task}: not in the register")
print(f"task: {args.task}")
for field in FIELDS:
if row.get(field) not in (None, ""):
print(f"{field}: {row[field]}")
for number, kr in enumerate(row.get("key_results", []), 1):
print(f"key result {number} [{'x' if kr['met'] else ' '}]: {kr['text']}")
for artifact in row.get("artifacts", []):
print(f"artifact: {artifact['ref']} (since {when(artifact['added'])})")
for field, flag in LIST_FIELDS.items():
for item in row.get(field, []):
print(f"{flag}: {item}")
roles = ", ".join(f"{p}={v}" for p, v in sorted(row["roles"].items()))
print(f"roles: {roles or '-'}")
for party, estimate in sorted(row.get("estimates", {}).items()):
print(f"estimate {party}: business value {estimate['value']}, complexity {estimate['complexity']}")
score = wsjf(row)
if score:
print(f"wsjf: {score[2]:g}")
return 0
def cmd_estimate(args: argparse.Namespace) -> int:
check_task(args.task)
if not args.drop:
for name in ("value", "complexity"):
if getattr(args, name) not in FIBONACCI:
fail(f"--{name} must be one of {', '.join(map(str, FIBONACCI))}")
with database() as tasks:
entry = tasks.setdefault(args.task, {"roles": {}})
estimates = entry.setdefault("estimates", {})
if args.drop:
estimates.pop(args.party, None)
else:
estimates[args.party] = {"value": args.value, "complexity": args.complexity, "at": stamp()}
if not estimates:
entry.pop("estimates")
print(f"{args.task}: estimate {'dropped' if args.drop else 'recorded'} for {args.party}")
return 0
def cmd_wsjf(args: argparse.Namespace) -> int:
rows = merged(args.all)
if not rows:
print("no tasks")
return 0
header = ["Rank", "Task", "OKR", "Business value", "Complexity", "WSJF", "Estimators", "Status"]
print("| " + " | ".join(header) + " |")
print("|" + "---|" * len(header))
unestimated = []
for rank, task in enumerate(ranked(rows), 1):
row = rows[task]
score = wsjf(row)
if score is None:
unestimated.append(task)
figures = [f"{n:g}" for n in score] if score else ["-", "-", "-"]
estimators = ", ".join(sorted(row.get("estimates", {}), key=str.lower)) or "-"
values = [rank, task, okr(row), *figures, cell(estimators, 40), row["status"]]
print("| " + " | ".join(str(v) for v in values) + " |")
if unestimated:
print("unestimated: " + ", ".join(unestimated))
return 0
def cmd_artifacts(args: argparse.Namespace) -> int:
rows = merged(True)
for task in sorted(rows):
row = rows[task]
if args.task and task != args.task:
continue
if args.ended and row["status"] not in (*ENDED, "stale"):
continue
places = [(f"{f}:{row[f]}", None) for f in ("workdir", "branch") if row.get(f)]
recorded = [(a["ref"], a["added"]) for a in row.get("artifacts", [])]
for ref, added in (*places, *recorded):
print(f"{row['status']}\t{task}\t{row.get('worker') or '-'}\t{ref}\t{when(added)}")
return 0
def cmd_tools(_: argparse.Namespace) -> int:
used: dict[str, list[str]] = {}
for task, row in merged(True).items():
for tool in row.get("tools", []):
used.setdefault(tool, []).append(task)
for tool, tasks in sorted(used.items(), key=lambda item: (-len(item[1]), item[0])):
print(f"{len(tasks)}\t{tool}\t{' '.join(sorted(tasks))}")
return 0
def cmd_rm(args: argparse.Namespace) -> int:
check_task(args.task)
with database() as tasks:
removed = tasks.pop(args.task, None)
print(f"removed {args.task}" if removed is not None else f"{args.task}: not in the register")
return 0
def cmd_show(args: argparse.Namespace) -> int:
rows = merged(args.all)
if not rows:
print("no tasks")
return 0
header = ["Prio", "WSJF", "Task", "OKR", "Status", "Worker", "Artifacts", "Start", "End", *ORDER]
print("| " + " | ".join(header) + " |")
print("|" + "---|" * len(header))
problems = []
for rank, task in enumerate(ranked(rows), 1):
row = rows[task]
title = f"{task}: {row['title']}" if row.get("title") else task
score = wsjf(row)
values = [
rank,
f"{score[2]:g}" if score else "-",
cell(title, 50),
okr(row),
row["status"],
cell(row.get("worker") or "-", 30),
len(row.get("artifacts", [])) or "-",
when(row.get("start")),
when(row.get("end")),
]
parties = sorted(row["roles"], key=lambda p: (p != OPERATOR, p.lower()))
values += [
cell(", ".join(p for p in parties if letter in row["roles"][p]), 30) for letter in ORDER
]
print("| " + " | ".join(str(v) for v in values) + " |")
problems += gaps(task, row)
if problems:
print("gaps: " + "; ".join(problems))
return 0
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
sub = parser.add_subparsers(dest="cmd", required=True)
p = sub.add_parser("set")
p.add_argument("task")
for field in (*TEXT_FIELDS, "start", "end"):
p.add_argument(f"--{field}")
p.add_argument("--prio", type=int)
for flag in (*LIST_FIELDS.values(), "artifact"):
p.add_argument(f"--{flag}", action="append", default=[])
p.add_argument(f"--drop-{flag}", action="append", default=[])
p.add_argument("--status", choices=STATUSES)
p.set_defaults(func=cmd_set)
p = sub.add_parser("estimate")
p.add_argument("task")
p.add_argument("party")
p.add_argument("--value", type=int)
p.add_argument("--complexity", type=int)
p.add_argument("--drop", action="store_true")
p.set_defaults(func=cmd_estimate)
p = sub.add_parser("wsjf")
p.add_argument("--all", action="store_true")
p.set_defaults(func=cmd_wsjf)
p = sub.add_parser("kr")
p.add_argument("task")
p.add_argument("action", choices=("add", "met", "open", "rm"))
p.add_argument("value")
p.set_defaults(func=cmd_kr)
p = sub.add_parser("role")
p.add_argument("task")
p.add_argument("party")
p.add_argument("letters")
p.set_defaults(func=cmd_role)
for name, func in (("get", cmd_get), ("rm", cmd_rm)):
p = sub.add_parser(name)
p.add_argument("task")
p.set_defaults(func=func)
p = sub.add_parser("show")
p.add_argument("--all", action="store_true")
p.set_defaults(func=cmd_show)
p = sub.add_parser("artifacts")
p.add_argument("--ended", action="store_true")
p.add_argument("--task")
p.set_defaults(func=cmd_artifacts)
sub.add_parser("tools").set_defaults(func=cmd_tools)
args = parser.parse_args()
return args.func(args)
if __name__ == "__main__":
sys.exit(main())
@@ -1,5 +1,5 @@
--- ---
name: robot name: kvb-org-robot
description: > description: >
Work fully autonomously to a goal: no check-ins, no stopping before the goal, Work fully autonomously to a goal: no check-ins, no stopping before the goal,
only commands already cleared. Trigger when the operator wants hands-off, only commands already cleared. Trigger when the operator wants hands-off,
@@ -11,7 +11,7 @@ Operate as an autonomous robot: given a goal, reach it without hand-holding.
## Contract ## Contract
- **No questions once the goal is clear.** Clarify only genuinely-blocking - **No questions once the goal is clear.** Clarify only genuinely-blocking
unknowns up front (via the `active-listening` skill), then proceed without unknowns up front (via the `kvb-think-listen` skill), then proceed without
further check-ins. Pick the cleanest option and continue; never pause to further check-ins. Pick the cleanest option and continue; never pause to
confirm a choice you can make yourself. confirm a choice you can make yourself.
- **Never stop before the goal.** Do not end the run, hand back, or declare done - **Never stop before the goal.** Do not end the run, hand back, or declare done
@@ -28,10 +28,14 @@ Operate as an autonomous robot: given a goal, reach it without hand-holding.
- **Verify, do not assume.** Reaching the goal means proving it: run it, read the - **Verify, do not assume.** Reaching the goal means proving it: run it, read the
output, confirm the end state. Never report success on a step you did not output, confirm the end state. Never report success on a step you did not
actually verify. actually verify.
- **Run it over the register.** Before the first step, enter the goal in the
`kvb-org-rasci` register as the task's objective and every check that proves
it as a key result; mark each one met as its proof comes in. The run is
finished when the task can be set `done`.
## Rules ## Rules
- Escalate an uncertain root cause or design decision to the `dialectic` skill - Escalate an uncertain root cause or design decision to the `kvb-think-dialectic` skill
rather than guessing; uncertainty is never a reason to stop. rather than guessing; uncertainty is never a reason to stop.
- Sustain the loop across long, multi-step work — the run is finished only when - Sustain the loop across long, multi-step work — the run is finished only when
the whole goal is verifiably met. the whole goal is verifiably met.
@@ -1,9 +1,9 @@
--- ---
name: funnel name: kvb-org-split
description: > description: >
Keep agent work in its own git worktree until it reaches 90% fix confidence Keep agent work in its own worktree until 90% fix confidence and under 5%
and under 5% break risk; below that bar it becomes a proposal. Trigger when break risk; below that it becomes a proposal. Trigger when delegating to or
delegating work to agents, when agents work in parallel, or on /funnel. parallelizing agents, or on /kvb-org-split.
--- ---
# Funnel # Funnel
@@ -14,16 +14,19 @@ in a sidetree until it does, or leaves as a proposal.
## The bar ## The bar
- **Fix confidence of at least 90 %** and **break risk under 5 %**, both - **Fix confidence of at least 90 %** and **break risk under 5 %**, both
reported by the `confidence` skill, the break risk derived through the reported by the `kvb-check-confidence` skill, the break risk derived through the
`break-risk` skill. `kvb-check-break-risk` skill.
- The operator may set a different bar. Never lower it to make a change fit. - The operator may set a different bar. Never lower it to make a change fit.
## The stages ## The stages
1. **Brief.** One concern per agent. Name the goal, the exact base commit, the 1. **Brief.** One concern per agent, on the model the `kvb-org-perform` skill
scope that may change, the evidence expected back, a hard time cap (default picks, entered in the `kvb-org-rasci` register as R with `--worker "subagent
60 minutes) and what the agent must not do: no pushes, no gates or deploys <type> (<model>)"`. Name the goal, the exact base commit, the
unless the resource chair of the `democracy` skill granted them, read-only scope that may change, the evidence expected back, a time cap (default
60 minutes) at which it reports its interim state, and what the agent must
not do: no pushes, no gates or deploys
unless the resource chair of the `kvb-org-organize` skill granted them, read-only
probes on shared live systems. probes on shared live systems.
2. **Sidetree.** The agent works in its own worktree on its own branch, 2. **Sidetree.** The agent works in its own worktree on its own branch,
created from the exact base commit, so its edits never land under a running created from the exact base commit, so its edits never land under a running
@@ -33,8 +36,10 @@ in a sidetree until it does, or leaves as a proposal.
behaviour, and makes a negative control: the test fails without the change. behaviour, and makes a negative control: the test fails without the change.
Numbers come from the same source before and after. Numbers come from the same source before and after.
4. **Self-assessment.** The agent reports fix confidence and break risk with 4. **Self-assessment.** The agent reports fix confidence and break risk with
their residuals and commits on its branch. their residuals and commits on its branch. Its parent marks the key results
5. **Independent review.** Before anything is merged, run the `dialectic` skill that evidence proves as met in the `kvb-org-rasci` register and records
the branch and worktree as `--branch` and `--artifact worktree:<path>`.
5. **Independent review.** Before anything is merged, run the `kvb-think-dialectic` skill
with skeptics who did not write the change. A hole they find returns the with skeptics who did not write the change. A hole they find returns the
change to stage 3. change to stage 3.
6. **Merge.** At or above the bar, merge the sidetree branch into the main line 6. **Merge.** At or above the bar, merge the sidetree branch into the main line
@@ -64,8 +69,9 @@ and verify each step instead of assuming it:
5. **Delete the merged branch**, locally and on the remote, but a remote branch 5. **Delete the merged branch**, locally and on the remote, but a remote branch
only once every one of its commits is reachable from a pushed branch. only once every one of its commits is reachable from a pushed branch.
6. **Release what it held**: slots (reported to the resource chair), port or 6. **Release what it held**: slots (reported to the resource chair), port or
subnet reservations, locks, its `democracy` claims and every claim subnet reservations, locks, its `kvb-org-organize` claims and every claim
announced to other agents. announced to other agents. Drop each removed item from the register with
`--drop-artifact` and set the subagent's task `done` or `stopped`.
7. **Remove its temporary files**: logs, message files and scratch data that 7. **Remove its temporary files**: logs, message files and scratch data that
have served their purpose. Keep only the evidence the report cites, until have served their purpose. Keep only the evidence the report cites, until
the report is delivered. the report is delivered.
@@ -93,8 +99,9 @@ below the bar never enters the main line on its own.
## Rules ## Rules
- Stop an agent at its time cap and ask for its findings; an uncapped brief can - At its time cap, ask an agent for its interim findings and let it run on;
run for hours and deliver nothing. never stop a running agent. A brief without a cap can run for hours without
a word.
- Run agents in parallel only when their concerns touch different files and - Run agents in parallel only when their concerns touch different files and
the resource chair grants the capacity; gates and deploys need the machine the resource chair grants the capacity; gates and deploys need the machine
to themselves. to themselves.
+43
View File
@@ -0,0 +1,43 @@
---
name: kvb-org-wsjf
description: >
WSJF priority from Fibonacci estimates. Trigger on /kvb-org-wsjf, "wsjf",
"priorisieren".
---
Weighted Shortest Job First ranks the tasks of the `kvb-org-rasci` register
by cost of delay over job size: here business value over complexity, each the
median of Fibonacci estimates (1, 2, 3, 5, 8, 13, 21, 34, 55, 89). The
estimates live in the register's table; `wsjf` is its WSJF view, one row per
task with its OKR and the task id the register uses, and the register's Prio
column is this rank. The script is
`${CLAUDE_SKILL_DIR}/../kvb-org-rasci/scripts/rasci.py`.
```bash
rasci.py estimate <task> "<party>" --value <fib> --complexity <fib>
rasci.py estimate <task> "<party>" --drop
rasci.py wsjf [--all]
```
## Procedure
1. **The chair asks.** For every new task, and when its objective or a key
result changes, the resource chair of the `kvb-org-organize` skill asks
its subordinates (the task's worker and R, the representatives of the
lines it touches) for one Fibonacci estimate each of business value and
complexity, one message per agent.
2. **Record each answer** under the party that gave it; a new estimate
replaces the old one. An agent may record its own.
3. **Rank**: WSJF = median business value / median complexity, highest
first. An operator's pin (`rasci.py set <task> --prio <n>`) outranks every
score; unestimated tasks come last and are named under `unestimated:`.
4. **Grant in rank order**: the chair hands out capacity by this rank, except
that a hotfix gate (a branch from the default branch that fixes it) takes
the next free gate or test slot before every ranked task; a running gate
is never cut short for it.
## Rules
- Estimate relative to the other open tasks, never in hours.
- Never invent an estimate for a party that did not answer; a task without
one stays unestimated.
@@ -1,13 +1,12 @@
--- ---
name: autoskill name: kvb-skill-auto
description: > description: >
Turn operator repetition into a skill. Trigger when the operator gives the Turn operator repetition into a skill. Trigger when the operator repeats an
same instruction, correction or prompt pattern about the third time in a instruction, correction or prompt pattern a third time, or says they keep
conversation, or says they keep repeating themselves, and the pattern is repeating themselves.
reusable.
--- ---
Its objective is the `autotune` skill's **Objective** section: a skill is worth Its objective is the `kvb-skill-tune` skill's **Objective** section: a skill is worth
creating only when it cuts tokens or time and raises quality; judge every creating only when it cuts tokens or time and raises quality; judge every
candidate against its three axes and rules. candidate against its three axes and rules.
@@ -19,18 +18,17 @@ vary), do the following:
2. Then propose, in one short paragraph: the repeated pattern you observed, 2. Then propose, in one short paragraph: the repeated pattern you observed,
a skill name suggestion, the trigger phrasing, and what the skill would a skill name suggestion, the trigger phrasing, and what the skill would
do. Ask whether to create it. do. Ask whether to create it.
3. On approval, write the skill: 3. On approval, write the skill: persist it exactly as the
- a conversation shortcut or expansion: add it to the `shortcuts` skill `kvb-skill-tune` skill's **Persistence** section specifies — locate the
table instead of creating a new skill. operator's skills repository, write `skills/<name>/SKILL.md` there (or in
- anything else: persist it exactly as the `autotune` skill's the current project for project-specific behavior), then mirror the skill
**Persistence** section specifies — locate the operator's skills directory into `~/.claude/skills/` and `~/.agents/skills/`, plus the
repository, write `skills/<name>/SKILL.md` there (or in the current project's `.claude/skills/` and `.agents/skills/` when it is
project for project-specific behavior), then mirror the skill directory project-specific.
into `~/.claude/skills/` and `~/.agents/skills/`, plus the project's
`.claude/skills/` and `.agents/skills/` when it is project-specific.
Follow the local naming conventions and keep the skill a thin, Follow the local naming conventions and keep the skill a thin,
single-purpose instruction; route to an authoritative doc when one single-purpose instruction; route to an authoritative doc when one
exists instead of duplicating it. exists instead of duplicating it. Its deterministic steps become a script
through the `kvb-skill-compile` skill, not prose.
4. Do not propose a skill for one-off work, secrets, or anything whose 4. Do not propose a skill for one-off work, secrets, or anything whose
repetition is already covered by an existing skill; when an existing repetition is already covered by an existing skill; when an existing
skill almost fits, propose extending it instead. skill almost fits, propose extending it instead.
+52
View File
@@ -0,0 +1,52 @@
---
name: kvb-skill-cleanup
description: >
Delete legacy skills that renamed or removed skills left in Claude, Codex,
agent and project dirs. Trigger on /kvb-skill-cleanup.
---
`${CLAUDE_SKILL_DIR}/scripts/legacy.py` reads the skills repository's git
history, where every rename and deletion of a skill is recorded, and
classifies each installed skill directory:
```bash
legacy.py --skills-repo <skills repo> --root <dir> [--root <dir> ...] [--json]
```
| Verdict | Meaning | Action |
| --- | --- | --- |
| `current` | shipped by the skills repository or its lock | keep |
| `project` | tracked in the git repository that holds the root | keep |
| `legacy` | renamed; the successor column names the current skill | delete |
| `removed` | deleted from the skills repository without a successor | delete |
| `unknown` | none of the above, for example the operator's own | keep, report |
## Step 1: roots
Every one that exists: `~/.claude/skills`, `~/.agents/skills`,
`~/.codex/skills`, and `.claude/skills` and `.agents/skills` of the current
repository and of every project the operator names.
## Step 2: scan and confirm
Run `legacy.py` over all roots. Show the `legacy` and `removed` rows in one
table (`| Root | Skill | Successor |`) and the `unknown` rows below it; the
`kvb-transcend` skill can carry an unknown one over. Then `AskUserQuestion`:
`Delete all legacy and removed` / `Pick per root` / `Delete nothing`.
## Step 3: delete
- A root that an installer rewrites as a whole (the skills repository's
`make install` for the home roots, a project's own skill install target)
is cleaned by running that installer, which also refreshes the current
skills; where the sandbox blocks it, it goes to the operator as below.
- Otherwise `rm -r <path>` per confirmed row where the sandbox lets the
agent write; every row it does not goes to the `kvb-org-operator` skill
as an operator command.
- A settings file whose hook command still points into a deleted skill
directory is reported with its line; settings files stay the operator's.
## Step 4: verify
Run `legacy.py` again: no `legacy` or `removed` row may remain in a cleaned
root. Report the before and after counts per root.
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""Classify installed skills as current, project, legacy, removed or unknown.
Args:
--skills-repo: checkout of the skills repository, read with its git history.
--root: a skills directory to classify, repeatable.
--json: print the rows as JSON instead of tab-separated lines.
Exit codes:
0 done, 2 usage error.
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from pathlib import Path
def fail(message: str) -> None:
print(f"legacy: {message}", file=sys.stderr)
sys.exit(2)
def git(cwd: Path, *args: str) -> str | None:
result = subprocess.run(
["git", *args], cwd=cwd, capture_output=True, text=True, check=False
)
return result.stdout if result.returncode == 0 else None
def current_names(skills_repo: Path) -> set[str]:
names = {p.name for p in (skills_repo / "skills").iterdir() if (p / "SKILL.md").is_file()}
lock = json.loads((skills_repo / "skills-lock.json").read_text(encoding="utf-8"))
return names | set(lock.get("skills", {}))
def moves(skills_repo: Path) -> dict[str, str | None]:
out = git(
skills_repo,
"log", "-M", "--diff-filter=RD", "--name-status", "--format=", "--", "skills/*/SKILL.md",
)
if out is None:
fail(f"{skills_repo} is not a git repository")
found: dict[str, str | None] = {}
for line in out.splitlines():
parts = line.split("\t")
if parts[0].startswith("R") and len(parts) == 3:
found.setdefault(Path(parts[1]).parent.name, Path(parts[2]).parent.name)
elif parts[0] == "D" and len(parts) == 2:
found.setdefault(Path(parts[1]).parent.name, None)
return found
def successor(name: str, history: dict[str, str | None], current: set[str]) -> str | None:
seen = set()
while name not in current:
if name in seen or history.get(name) is None:
return None
seen.add(name)
name = history[name]
return name
def project_names(root: Path) -> set[str]:
top = git(root, "rev-parse", "--show-toplevel")
if top is None:
return set()
tracked = git(Path(top.strip()), "ls-files", "*SKILL.md") or ""
return {Path(line).parent.name for line in tracked.splitlines()}
def classify(root: Path, current: set[str], history: dict[str, str | None]) -> list[dict]:
project = project_names(root)
rows = []
for entry in sorted(p for p in root.iterdir() if (p / "SKILL.md").is_file()):
name, succ = entry.name, None
if name in current:
verdict = "current"
elif name in project:
verdict = "project"
elif name in history:
succ = successor(name, history, current)
verdict = "legacy" if succ else "removed"
else:
verdict = "unknown"
rows.append({"verdict": verdict, "successor": succ, "path": str(entry)})
return rows
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--skills-repo", required=True)
parser.add_argument("--root", action="append", required=True)
parser.add_argument("--json", action="store_true")
args = parser.parse_args()
repo = Path(args.skills_repo)
if not (repo / "skills").is_dir() or not (repo / "skills-lock.json").is_file():
fail(f"{repo} is not a skills repository")
roots = [Path(r) for r in args.root]
for root in roots:
if not root.is_dir():
fail(f"{root} is not a directory")
current, history = current_names(repo), moves(repo)
rows = [row for root in roots for row in classify(root, current, history)]
if args.json:
print(json.dumps(rows, indent=1))
else:
for row in rows:
print(f"{row['verdict']}\t{row['successor'] or '-'}\t{row['path']}")
return 0
if __name__ == "__main__":
sys.exit(main())
+74
View File
@@ -0,0 +1,74 @@
---
name: kvb-skill-compile
description: >
Turn deterministic skill steps into tested scripts in the skill's own repo.
Trigger on /kvb-skill-compile or a skill step that is a fixed command
sequence.
---
A deterministic step is one whose result follows from its inputs alone:
fixed command sequences, output parsing and filtering, counting, thresholds
and arithmetic, format checks, sorting and lookups, state bookkeeping. This
skill compiles such steps into scripts and cuts the prose down to the call. Choosing, explaining and asking stay prose.
It changes nothing before the operator confirms.
## Step 1: scope and owner
The skills named after the command, or every skill of the current
repository when none is named. Per skill, its owning repository:
- **Skills repository**: a first-party skill under its `skills/`, and every
installed copy of one (`~/.claude/skills`, `~/.agents/skills`, a project's
rendered `.claude/skills`); edit the source, never the copy, by the
`kvb-skill-tune` skill's **Persistence** section.
- **Another repository**: a skill tracked in that repository's own tree
(`git ls-files` lists it there), such as a project's `.claude/skills`.
- **Third party**: a `skills-lock.json` entry from a foreign source is not
compiled; propose the change upstream instead.
## Step 2: inventory
Read-only. Per skill, every step that is deterministic, with its evidence:
how often sessions ran it (transcripts, `rasci.py tools` of the
`kvb-org-rasci` register) and where an agent got it wrong. One line each:
`skill:step -> deterministic part -> inputs -> output -> runs per use`.
## Step 3: classify
Compile what runs often or went wrong; leave a one-off as prose. Per
candidate:
- **Language and form**: the owning repository's own (Python standard
library in the skills repository); no new language, framework or
dependency without the operator.
- **Interface**: every host path, name and value an explicit parameter (the
`kvb-code-no-defaults` skill), documented exit codes, `--json` when another
script or skill consumes the output.
- **Location**: `skills/<skill>/scripts/<verb>.py` with a test under `tests/`
in the skills repository; elsewhere the owning repository's convention,
read from its `AGENTS.md`, `CONTRIBUTING` and `Makefile` first.
## Step 4: listen and confirm
Run the `kvb-think-listen` skill on the open points. Then `AskUserQuestion`:
one `multiSelect` with the candidates, nothing preselected, and per pick a
single-select `Write it` / `Revise it` / `Discard` showing the drafted script
and the prose diff.
## Step 5: write
- Write the script, then replace the compiled prose with its call
(`${CLAUDE_SKILL_DIR}/scripts/<verb>.py ...`) and the one line on reading
its output.
- Every script gets a unit test with a negative control shown once. Tests
run only on the resource chair's grant (the `kvb-org-organize` skill).
- In another repository follow its agent rules (claims, worktree, branch);
a file another line owns goes to that line through the
`kvb-org-coordinate` skill.
- Leave the result uncommitted for the `kvb-git-commit-next` skill.
## Output
| Skill | Step | Script | Repository | Prose lines cut | Verdict |
Steps left as prose stay with their reason.
@@ -1,9 +1,8 @@
--- ---
name: essence name: kvb-skill-essence
description: > description: >
Compact skills and memory to their essence after a health pass that finds Compact skills and memory after a health pass for dead weight. Trigger on
dead weight. Trigger on /essence or when skills or MEMORY.md grow too long, /kvb-skill-essence or when skills or MEMORY.md grow too long or duplicate.
duplicate, or eat too much context.
--- ---
Shorter, not thinner. Every fact that still earns its place survives; what goes Shorter, not thinner. Every fact that still earns its place survives; what goes
@@ -65,6 +64,9 @@ Cut:
- The second and third statement of one rule; keep the authoritative one and - The second and third statement of one rule; keep the authoritative one and
point at it. point at it.
- Narration, hedging, and worked examples beyond the one that carries the point. - Narration, hedging, and worked examples beyond the one that carries the point.
- A deterministic procedure spelled out step by step: hand it to the
`kvb-skill-compile` skill, which replaces it with a script call, instead of
shortening it.
Keep, always: Keep, always:
+65
View File
@@ -0,0 +1,65 @@
---
name: kvb-skill-transcend
description: >
Carry the session's skill use and own scripts into the skills repo.
Trigger on /kvb-skill-transcend.
---
What an agent improvised in one session dies with it: the helper script in its
scratchpad, the command sequence it ran five times, the skill it had to work
around. This skill carries that into the operator's skills repository, so the
next agent starts from it. It changes nothing before the operator confirms.
## Step 1: inventory
Read-only:
- **Scripts and skills**: `transcend.py scan` of the `kvb-transcend` skill
lists the own scratchpad's scripts, peers' scripts, and skills, commands,
hooks and rules in `~/.claude`, `~/.agents` and `~/.codex` that the skills
repository does not ship; it skips what was already decided and has not
changed. Peer and home candidates follow that skill's consent step.
- **Skills used**: every skill this session invoked, how often, and where it
fell short (a workaround, a step skipped, an operator correction).
- **Recurring moves without a skill**: command sequences, peer exchanges or
lookups run three times or more. `rasci.py tools` of the `kvb-org-rasci`
register lists the scripts the session's tasks recorded.
One line each: `candidate -> source -> evidence`.
## Step 2: classify
Per candidate one target: a script under an existing skill's `scripts/`, a
new skill, a sharper step in an existing skill, a memory entry instead, or
drop. A recurring deterministic move without a script goes to the
`kvb-skill-compile` skill, any other recurring move to the `kvb-skill-tune`
skill as evidence. A script is portable only once its host paths, names and values
became explicit parameters (the `kvb-code-no-defaults` skill); one that needs
a secret, a credential or a single host is dropped or stays a memory entry.
## Step 3: listen and confirm
Run the `kvb-think-listen` skill on the open points: scope, target skill,
generality. Then `AskUserQuestion`: one `multiSelect` with the candidates
(target and evidence in each description, nothing preselected), and per pick a
single-select `Write it` / `Revise it` / `Discard` showing the drafted file or
diff.
## Step 4: write
- Write where the `kvb-skill-tune` skill's **Persistence** section says:
the skills repository as source of truth, then the live mirrors.
- Read every script whole before carrying it; strip comments the way the
`kvb-code-no-comments` skill does.
- A carried script gets a unit test in the repository's `tests/` with a
negative control. Tests run only on the resource chair's grant (the
`kvb-org-organize` skill).
- `transcend.py mark` every offered candidate with its verdict.
- Leave the result uncommitted; committing is the `kvb-git-commit-next`
skill's job.
## Output
| Candidate | Source | Target | Verdict |
Dropped candidates stay with their reason.
@@ -1,10 +1,9 @@
--- ---
name: autotune name: kvb-skill-tune
description: > description: >
Generate new skills or optimize existing ones (fewer tokens, fewer errors, Generate or sharpen skills from observed friction, each proposal confirmed.
persistent learnings), each proposal confirmed by single-select question. Trigger only on /kvb-skill-tune, "autotune", "tune the skills" or through
Trigger ONLY on an explicit request: /autotune, "autotune", "tune the kvb-optimize.
skills"; never on your own initiative.
--- ---
Autotune turns observed friction into durable skills. It runs on demand only, Autotune turns observed friction into durable skills. It runs on demand only,
@@ -13,8 +12,8 @@ skill it writes or rewrites lands in the operator's skills repository.
## Objective ## Objective
The shared objective of the self-improving skills (autotune, autoskill, The shared objective of the self-improving skills (kvb-skill-tune,
confidence): every skill, rule, or report the agent produces about its own kvb-skill-auto, kvb-check-confidence): every skill, rule, or report the agent produces about its own
working is judged on three axes at once. A proposal that cannot name the axis working is judged on three axes at once. A proposal that cannot name the axis
it moves is not an optimization, it is a preference. it moves is not an optimization, it is a preference.
@@ -43,10 +42,10 @@ it moves is not an optimization, it is a preference.
## Trigger discipline ## Trigger discipline
- Run **only** when the operator asks: `/autotune`, "autotune", "tune the - Run **only** when the operator asks: `/kvb-skill-tune`, "autotune", "tune the
skills", or an equivalent instruction. skills", or an equivalent instruction, or as part of the `kvb-optimize`
- The 60-minute reminder hook is a hint for the operator, not a trigger. When it skill the operator started (directly or through `kvb-dream`).
fires, print the hint and continue the operator's actual request. - It has no reminder of its own; the `kvb-dream` hint covers it.
- Never propose, write, or edit a skill as a side effect of unrelated work. - Never propose, write, or edit a skill as a side effect of unrelated work.
## Step 1: collect evidence ## Step 1: collect evidence
@@ -63,6 +62,12 @@ where readable, from the project's memory and agent instruction files:
deploys, questions that stalled an autonomous run. deploys, questions that stalled an autonomous run.
- **Lost learnings**: facts re-derived this session that a memory file or a - **Lost learnings**: facts re-derived this session that a memory file or a
skill should already have carried. skill should already have carried.
- **Own scripts**: helpers the agent wrote outside any repository belong to
the `kvb-skill-transcend` skill, which carries them over; this skill
takes the frictions they reveal.
- **Deterministic prose**: a skill step the agent worked through by hand
although its result follows from its inputs belongs to the
`kvb-skill-compile` skill, which turns it into a script.
List each finding as one line: `symptom -> cost -> candidate fix`. Skip List each finding as one line: `symptom -> cost -> candidate fix`. Skip
anything that happened once and is not a pattern. anything that happened once and is not a pattern.
@@ -75,16 +80,17 @@ option, and never write a file before the answers are in.
Ask in this order, at most four questions per call: Ask in this order, at most four questions per call:
1. **Scope** - "What should autotune do this run?" Options: `Create a new 1. **Scope** - "What should kvb-skill-tune do this run?" Options: `Create a new
skill`, `Optimize an existing skill`, `Both`, plus the ranked findings if skill`, `Optimize an existing skill`, `Both`, plus the ranked findings if
more than one candidate exists. more than one candidate exists.
2. **Target** - which finding becomes a skill, or which existing skill gets 2. **Target** - which finding becomes a skill, or which existing skill gets
optimized. One option per candidate, each with its measured cost in the optimized. One option per candidate, each with its measured cost in the
description. description.
3. **Shape** - for a new skill: `Standalone skill`, `Row in the shortcuts 3. **Shape** - for a new skill: `Standalone skill`, `Extend an existing
skill`, `Extend an existing skill`, `Memory entry instead of a skill`. skill`, `Memory entry instead of a skill`.
For an optimization: `Tighten wording only`, `Change the trigger`, `Add For an optimization: `Tighten wording only`, `Change the trigger`, `Add
rules`, `Split into two skills`, `Merge into another skill`. rules`, `Split into two skills`, `Merge into another skill`, `Compile
deterministic steps into a script` (the `kvb-skill-compile` skill).
4. **Placement** - `Operator skills repository (portable)` or 4. **Placement** - `Operator skills repository (portable)` or
`This project's skills directory (project-specific)`. `This project's skills directory (project-specific)`.
@@ -130,7 +136,7 @@ reverse — remove the mirrors too, or the old name keeps firing.
- Optimizing an existing skill means the file gets shorter or sharper, not - Optimizing an existing skill means the file gets shorter or sharper, not
longer. Delete rules the agent already follows, merge duplicated ones, and longer. Delete rules the agent already follows, merge duplicated ones, and
cut every sentence that does not change an action. Route that compaction cut every sentence that does not change an action. Route that compaction
through the `essence` skill instead of trimming by hand: it runs a health through the `kvb-skill-essence` skill instead of trimming by hand: it runs a health
pass for dead pointers and rules duplicated across skills before cutting pass for dead pointers and rules duplicated across skills before cutting
anything, ranks by what loads in every session, and shows the diff before it anything, ranks by what loads in every session, and shows the diff before it
writes. It asks for its own scope — answer with the skill being optimized. writes. It asks for its own scope — answer with the skill being optimized.
@@ -140,7 +146,7 @@ reverse — remove the mirrors too, or the old name keeps firing.
## Rules ## Rules
- One skill per run unless the operator picked `Both`; a broad rewrite of the - One skill per run unless the operator picked `Both`; a broad rewrite of the
whole collection is not what autotune is for. whole collection is not what kvb-skill-tune is for.
- Never write a skill for one-off work, for secrets, or for behaviour an - Never write a skill for one-off work, for secrets, or for behaviour an
existing skill already covers - propose extending that skill instead. existing skill already covers - propose extending that skill instead.
- The **Objective** section decides what counts as an improvement: at least - The **Objective** section decides what counts as an improvement: at least
+45
View File
@@ -0,0 +1,45 @@
---
name: kvb-think-brainstorm
description: >
Generate options through fixed lenses, then score and narrow them. Trigger
on /kvb-think-brainstorm, "brainstorm", or when a skill needs alternatives.
---
Generate first, judge second: no option is scored before all are on the
table.
## Step 1: frame
One line: the problem, who has it, and which constraints are fixed. Ask the
operator only for what the context does not give, by the `kvb-think-listen`
skill's rules.
## Step 2: diverge
At least one option per lens, none judged yet:
- **Obvious**: what anyone would try first.
- **Minimal**: the smallest change that helps at all.
- **Maximal**: the version without a budget limit.
- **Inversion**: solve the opposite problem, or remove the need.
- **Analogy**: how another domain solves a problem of the same shape.
- **Drop a constraint**: what one fixed constraint, gone, makes possible.
- **Reuse**: what the repository, the platform or a dependency already has.
## Step 3: converge
Rate every option on value, effort and risk (`low`, `medium`, `high`),
merge duplicates, and drop each option another one beats on all three.
## Step 4: hand over
The best two or three go to the calling skill, or to the operator through
`AskUserQuestion` with `multiSelect: false`, the recommended option first.
A consequential choice runs through the `kvb-think-dialectic` skill before
it is offered.
## Output
| Option | Lens | Value | Effort | Risk | Kept |
Dropped options stay in the table with the option that beat them.
@@ -1,21 +1,21 @@
--- ---
name: criticize name: kvb-think-criticize
description: > description: >
Give constructive criticism of a problem, plan, design, decision, text or Constructive criticism of a plan, design, decision, text or code, each
code from diverse aspects, each point hardened by dialectic. Trigger on point hardened by dialectic. Trigger on /kvb-think-criticize, "kritisiere",
/criticize, "kritisiere", "kritisch betrachten", "konstruktive Kritik", "was "konstruktive Kritik", "was haeltst du davon", or requests for honest
haeltst du davon", or requests for honest feedback. feedback.
--- ---
The stance is critical but constructive: the goal is a better result, not a The stance is critical but constructive: the goal is a better result, not a
verdict on its author. This skill judges any artifact from many angles; it is verdict on its author. This skill judges any artifact from many angles; it is
not limited to code (`sceptic` hunts violated engineering principles in code, not limited to code (`kvb-check-principles` hunts violated engineering principles in code,
`code-review` hunts bugs). Read-only: report the critique, change nothing `code-review` hunts bugs). Read-only: report the critique, change nothing
unless the operator asks afterwards. unless the operator asks afterwards.
## Before you start: listen ## Before you start: listen
Invoke the `active-listening` skill. Pin down what is being criticized, what it Invoke the `kvb-think-listen` skill. Pin down what is being criticized, what it
is meant to achieve, for whom, and under which constraints. Derive what you can is meant to achieve, for whom, and under which constraints. Derive what you can
from the artifact and its surroundings first; ask only what only the operator from the artifact and its surroundings first; ask only what only the operator
knows. A critique against the wrong goal is noise. knows. A critique against the wrong goal is noise.
@@ -53,7 +53,7 @@ Sweep every aspect that applies, and name the ones you skipped and why:
## Harden every point with dialectic ## Harden every point with dialectic
Run each candidate critique through the `dialectic` skill before reporting it: Run each candidate critique through the `kvb-think-dialectic` skill before reporting it:
- **Thesis**: the critique point with its evidence. - **Thesis**: the critique point with its evidence.
- **Antithesis**: independent defenders argue that the artifact is right as it - **Antithesis**: independent defenders argue that the artifact is right as it
@@ -76,7 +76,7 @@ One line of scope, then the steelman. Then:
`Evidence` is `proven` (read, run, reproduced, cited) or `derived` (reasoned). `Evidence` is `proven` (read, run, reproduced, cited) or `derived` (reasoned).
Close with at most three lines: how many candidate points dialectic dropped and Close with at most three lines: how many candidate points dialectic dropped and
why, the single change that helps most, and the `confidence` skill's numbers for why, the single change that helps most, and the `kvb-check-confidence` skill's numbers for
the critique as a whole. the critique as a whole.
## Rules ## Rules
@@ -1,10 +1,9 @@
--- ---
name: dialectic name: kvb-think-dialectic
description: > description: >
Reach a defensible conclusion by thesis, independent skeptic antithesis and Thesis, independent skeptic antithesis and synthesis, iterated to ~99%
synthesis, iterated to ~99% confidence. Trigger for any consequential root confidence. Trigger for any root cause, design or decision that is
cause, design or decision where being wrong is expensive and the answer is uncertain and expensive to get wrong.
uncertain.
--- ---
Reach the truth by dialectic, not by first guess. Do not stop at a plausible Reach the truth by dialectic, not by first guess. Do not stop at a plausible
@@ -12,13 +11,13 @@ answer; drive the loop until a thesis survives every attack at ~99% confidence.
## Before you start: listen ## Before you start: listen
Invoke the `active-listening` skill first. Do not begin the dialectic until you Invoke the `kvb-think-listen` skill first. Do not begin the dialectic until you
have gathered every requirement, constraint, and success criterion the task have gathered every requirement, constraint, and success criterion the task
needs. A dialectic run on the wrong question wastes the whole loop. Return to needs. A dialectic run on the wrong question wastes the whole loop. Return to
`active-listening` mid-loop whenever an objection turns on a fact only the `kvb-think-listen` mid-loop whenever an objection turns on a fact only the
operator holds. operator holds.
When another skill or an autonomous run (robot, observe, a loop) invoked this When another skill or an autonomous run (kvb-org-robot, kvb-ci-observe, a loop) invoked this
skill, derive the requirements from the task and the evidence instead of skill, derive the requirements from the task and the evidence instead of
asking; ask only when an operator-only fact blocks the conclusion. asking; ask only when an operator-only fact blocks the conclusion.
@@ -30,7 +29,8 @@ asking; ask only when an operator-only fact blocks the conclusion.
make or CLI helpers the project exposes); CI logs and artifacts; tests; docs. make or CLI helpers the project exposes); CI logs and artifacts; tests; docs.
Cite `file:line` and command output — never a bare assertion. Cite `file:line` and command output — never a bare assertion.
2. **Antithesis.** Spawn MULTIPLE independent skeptics (parallel subagents or a 2. **Antithesis.** Spawn MULTIPLE independent skeptics (parallel subagents or a
workflow) whose only job is to REFUTE the thesis. Each re-inspects the workflow, on the default model as the `kvb-org-perform` skill requires)
whose only job is to REFUTE the thesis. Each re-inspects the
sources itself and defaults to "refuted" on any concrete hole. Give diverse sources itself and defaults to "refuted" on any concrete hole. Give diverse
skeptics diverse lenses: correctness, an alternate mechanism, does-it- skeptics diverse lenses: correctness, an alternate mechanism, does-it-
reproduce, does-the-fix-break-something-else. reproduce, does-the-fix-break-something-else.
@@ -40,7 +40,7 @@ asking; ask only when an operator-only fact blocks the conclusion.
4. **Iterate.** Repeat thesis → antithesis → synthesis until a full skeptic 4. **Iterate.** Repeat thesis → antithesis → synthesis until a full skeptic
round finds no new hole and the thesis holds at ~99%. If confidence stalls round finds no new hole and the thesis holds at ~99%. If confidence stalls
below 99%, name the exact residual and the evidence that would close it, then below 99%, name the exact residual and the evidence that would close it, then
go get that evidence (more inspection, or `active-listening` for an go get that evidence (more inspection, or `kvb-think-listen` for an
operator-only fact) and loop again. operator-only fact) and loop again.
## Rules ## Rules
@@ -1,8 +1,8 @@
--- ---
name: explain name: kvb-think-explain
description: > description: >
Explain code and its interactions. Trigger when the operator asks what code Explain code and its interactions. Trigger when the operator asks what code
does, how a mechanism works, why components interact, or invokes /explain on does, how a mechanism works, why components interact, or invokes /kvb-think-explain on
a file, function, role or module. a file, function, role or module.
--- ---
@@ -1,27 +1,25 @@
--- ---
name: auto-feature name: kvb-think-gaps
description: > description: >
Derive missing work (sceptic pass for structural flaws, usability pass for Find and rank missing work (structural flaws, missing features); each pick
missing features) and write requirement documents after single-select goes to kvb-create-requirement. Trigger on /kvb-think-gaps, "was fehlt
confirmation. Trigger on /auto-feature, "was fehlt noch", "welche features noch", or asked what to build next.
brauchen wir", or when the operator asks what to build next.
--- ---
Two blind spots produce different work: the code has defects of structure, Two blind spots produce different work: the code has defects of structure,
the product has gaps of use. This skill runs both lenses, ranks what comes the product has gaps of use. This skill runs both lenses, ranks what comes
out, and lets the operator choose — it writes requirement documents, never out, and lets the operator choose. It writes no file; the
implementations. `kvb-create-requirement` skill writes the requirement for every pick.
## Step 1: fix the scope ## Step 1: fix the scope
Name the target in one line: repository, subsystem, role, or user-facing Name the target in one line: repository, subsystem, role, or user-facing
surface. Read the project's existing requirement documents first — their surface. Read the project's existing requirement documents first; an
location, filename convention, and section structure are the template, and already-written requirement is not a finding.
an already-written requirement is not a finding.
## Step 2: the sceptic pass ## Step 2: the principles pass
Invoke the `sceptic` skill on the target. Its findings become requirement Invoke the `kvb-check-principles` skill on the target. Its findings become requirement
candidates by severity: a violated SPOT, SRP, or trust boundary is work the candidates by severity: a violated SPOT, SRP, or trust boundary is work the
product needs whether or not a user ever notices. Carry each finding's product needs whether or not a user ever notices. Carry each finding's
`file:line` evidence forward — a requirement without it is a guess. `file:line` evidence forward — a requirement without it is a guess.
@@ -55,7 +53,7 @@ Merge both lists, drop duplicates where a structural finding and a usability
gap describe the same defect, and rank by blocked users first, blast radius gap describe the same defect, and rank by blocked users first, blast radius
second. second.
Then apply `active-listening`: answer everything you can from the code, the Then apply `kvb-think-listen`: answer everything you can from the code, the
docs, and the git history yourself, and put only the genuinely open decisions docs, and the git history yourself, and put only the genuinely open decisions
to the operator with `AskUserQuestion`, `multiSelect: false`, so every to the operator with `AskUserQuestion`, `multiSelect: false`, so every
question renders as radio buttons. At most four questions per call, each question renders as radio buttons. At most four questions per call, each
@@ -66,33 +64,21 @@ option a real alternative — never one option plus filler:
2. **Which shape** the chosen one takes — for a feature typically 2. **Which shape** the chosen one takes — for a feature typically
`Minimal version that unblocks the user`, `Full version`, `Fix the `Minimal version that unblocks the user`, `Full version`, `Fix the
underlying structure instead`, `Document the limitation and defer`. underlying structure instead`, `Document the limitation and defer`.
3. **Scope boundary** — what is explicitly out of scope, when the candidate
could grow in more than one direction.
4. **Placement** — which requirement file or directory receives it, when the
project has more than one.
Never assume an answer, never fold two decisions into one option, and never Never assume an answer and never fold two decisions into one option.
write a file before the answers are in.
## Step 5: write the requirements ## Step 5: hand over
One document per feature — a requirement that needs "and" in its title is Run the `kvb-create-requirement` skill once per chosen candidate. Its
two requirements. Follow the project's existing template; when there is context step starts from the candidate's evidence and chosen shape; the
none, keep it to: problem with its evidence, the decided scope and its rejected shapes go into its Decisions section.
boundary, definition of done as checkable criteria, and the alternatives the
operator rejected with the reason.
Show the drafted document and ask for a final `Write it` / `Revise it` /
`Discard` before touching disk.
## Rules ## Rules
- Requirements only: no implementation, no refactor, no staging as part of - Findings only: no file, no implementation, no refactor, no staging as
this skill. part of this skill.
- Every requirement traces to evidence — a `file:line`, a failing path, a - Every candidate traces to evidence — a `file:line`, a failing path, a
missing artifact. A requirement whose justification is "would be nice" is missing artifact. A candidate whose justification is "would be nice" is
not written. dropped.
- Record the rejected alternatives inside the document; the next reader - Close with the `kvb-check-confidence` skill's numbers for the analysis: how sure the
needs to know what was already considered and dismissed.
- Close with the `confidence` skill's numbers for the analysis: how sure the
gaps are real, and what the passes did not cover. gaps are real, and what the passes did not cover.
@@ -1,10 +1,9 @@
--- ---
name: active-listening name: kvb-think-listen
description: > description: >
Ask focused questions until every requirement is pinned down, then reflect Ask focused questions until every requirement is pinned down, then reflect
the task back. Trigger at the start of any task with unclear scope, the task back. Trigger on unclear scope, constraints or success criteria,
constraints or success criteria, and when another skill needs an or when a skill needs an operator-only fact.
operator-only fact.
--- ---
Do not act on a half-understood request. Listen actively until you could state Do not act on a half-understood request. Listen actively until you could state
+37
View File
@@ -0,0 +1,37 @@
---
name: kvb-transcend
description: >
Run both transcend skills in one pass. Trigger on /kvb-transcend.
---
Collector for the `kvb-skill-transcend` and `kvb-code-transcend` skills, so
one harvest needs one round of questions. Its script finds the candidates and
remembers every decision, so a rescan offers only what is new or changed:
```bash
transcend.py scan --state <skills repo>/.state/transcend.json \
--skills-repo <skills repo> --session-dir <own scratchpad> \
--peers-root /tmp/claude-<uid> --home ~ [--json]
transcend.py mark --state <same> <path> carried|dropped|later
```
`${CLAUDE_SKILL_DIR}/scripts/transcend.py` scans three sources: the own
scratchpad, every peer session's scratchpad, and the agent homes
(`~/.claude`, `~/.agents`, `~/.codex`) for skills, commands, agents, hooks
and rules the skills repository does not ship. It keeps scripts, tests and
skills at most one directory below a scratchpad, and skips files named like
secrets, files over 1 MB, and repository copies (a directory holding `.git`,
`roles`, `plugins`, `pyproject.toml`, `package.json` or a `Makefile`). Carried or
dropped candidates return only when their content changes; `later` returns
every time.
1. Scan, then classify each candidate by both skills' classify steps.
2. Consent: a peer's candidates go to that peer, one message listing them,
and only what it releases stays; home candidates need the operator's yes.
3. One `kvb-think-listen` round and one confirmation for all candidates,
grouped by target: the skills repository or the current repository. In
the skills repository itself, scripts and skills go to
`kvb-skill-transcend`, tests to `kvb-code-transcend`.
4. Hand each confirmed candidate to its skill's write step, then `mark` every
offered candidate with its verdict.
5. One table: `| Candidate | Source | Target | Verdict |`.
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env python3
"""Find scripts, tests and skills worth carrying over, and remember every decision.
Exit codes:
0 done, 2 usage error.
"""
from __future__ import annotations
import argparse
import datetime
import hashlib
import json
import os
import re
import sys
from pathlib import Path
SCRIPT_SUFFIXES = {".py", ".sh", ".bash", ".js", ".mjs", ".ts", ".rb", ".pl"}
TEST_NAME = re.compile(r"(^test_.+|.+_test|.+\.(spec|test))\.[a-z]+$")
SECRET_NAME = re.compile(
r"(secret|token|credential|passw|auth|\.env$|id_[a-z0-9]+$|\.pem$|\.key$)", re.IGNORECASE
)
MAX_BYTES = 1_000_000
MAX_DEPTH = 1
REPO_MARKERS = {".git", "roles", "plugins", "pyproject.toml", "package.json", "Makefile"}
HOME_SOURCES = {
".claude/skills": "skill",
".agents/skills": "skill",
".codex/skills": "skill",
".claude/commands": "command",
".claude/agents": "agent",
".claude/hooks": "script",
".claude/scripts": "script",
".codex/rules": "rule",
}
SETTLED = ("carried", "dropped")
VERDICTS = (*SETTLED, "later")
def fail(message: str) -> None:
print(f"transcend: {message}", file=sys.stderr)
sys.exit(2)
def digest(path: Path) -> str:
sha = hashlib.sha256()
files = sorted(p for p in path.rglob("*") if p.is_file()) if path.is_dir() else [path]
for file in files:
sha.update(str(file.relative_to(path) if path.is_dir() else file.name).encode())
sha.update(file.read_bytes())
return sha.hexdigest()
def file_kind(path: Path) -> str | None:
if SECRET_NAME.search(path.name) or path.stat().st_size > MAX_BYTES:
return None
if TEST_NAME.match(path.name):
return "test"
if path.suffix in SCRIPT_SUFFIXES or os.access(path, os.X_OK):
return "script"
return None
def session_names(home: Path) -> dict[str, str]:
names = {}
for path in (home / ".claude" / "sessions").glob("*.json"):
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError):
continue
if data.get("sessionId"):
names[data["sessionId"]] = data.get("name") or data["sessionId"]
return names
def scratch_candidates(root: Path, source: str, owner: str) -> list[dict]:
found = []
for current, dirs, files in os.walk(root):
here = Path(current)
depth = len(here.relative_to(root).parts)
if depth and REPO_MARKERS & {*dirs, *files}:
dirs.clear()
continue
if depth >= MAX_DEPTH:
dirs.clear()
for name in files:
kind = file_kind(here / name)
if kind:
found.append(
{"source": source, "owner": owner, "kind": kind, "path": str(here / name)}
)
return sorted(found, key=lambda item: item["path"])
def shipped(skills_repo: Path) -> set[str]:
names = {p.name for p in (skills_repo / "skills").iterdir() if p.is_dir()}
lock = json.loads((skills_repo / "skills-lock.json").read_text(encoding="utf-8"))
return names | set(lock.get("skills", {}))
def home_candidates(home: Path, skills_repo: Path) -> list[dict]:
known = shipped(skills_repo)
found = []
for sub, kind in HOME_SOURCES.items():
base = home / sub
if not base.is_dir():
continue
for entry in sorted(base.iterdir()):
if entry.name.startswith(".") or SECRET_NAME.search(entry.name):
continue
if kind == "skill" and (entry.name in known or not (entry / "SKILL.md").is_file()):
continue
if kind != "skill" and not entry.is_file():
continue
found.append({"source": "home", "owner": "operator", "kind": kind, "path": str(entry)})
return found
def load_state(path: Path) -> dict[str, dict]:
try:
return json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError:
return {}
except ValueError:
fail(f"{path} is not valid JSON")
def cmd_scan(args: argparse.Namespace) -> int:
session = Path(args.session_dir).resolve()
home = Path(args.home)
names = session_names(home)
candidates = scratch_candidates(session, "own", "self")
for scratch in sorted(Path(args.peers_root).glob("*/*/scratchpad")):
if scratch.resolve() == session:
continue
sid = scratch.parent.name
candidates += scratch_candidates(scratch, "peer", names.get(sid, sid))
candidates += home_candidates(home, Path(args.skills_repo))
state = load_state(Path(args.state))
offered = []
for item in candidates:
seen = state.get(item["path"])
item["digest"] = digest(Path(item["path"]))
if seen and seen["verdict"] in SETTLED and seen["digest"] == item["digest"]:
continue
item["previous"] = seen["verdict"] if seen else None
offered.append(item)
if args.json:
print(json.dumps(offered, indent=1))
else:
for item in offered:
note = f"\t(was {item['previous']})" if item["previous"] else ""
print(f"{item['source']}\t{item['owner']}\t{item['kind']}\t{item['path']}{note}")
return 0
def cmd_mark(args: argparse.Namespace) -> int:
target = Path(args.path)
if not target.exists():
fail(f"{target} does not exist")
state_path = Path(args.state)
state = load_state(state_path)
state[str(target)] = {
"verdict": args.verdict,
"digest": digest(target),
"at": datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds"),
}
state_path.parent.mkdir(parents=True, exist_ok=True)
tmp = state_path.with_suffix(".tmp")
tmp.write_text(json.dumps(state, indent=1, sort_keys=True), encoding="utf-8")
tmp.replace(state_path)
print(f"{args.verdict} {target}")
return 0
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
sub = parser.add_subparsers(dest="cmd", required=True)
p = sub.add_parser("scan")
p.add_argument("--state", required=True)
p.add_argument("--skills-repo", required=True)
p.add_argument("--session-dir", required=True)
p.add_argument("--peers-root", required=True)
p.add_argument("--home", required=True)
p.add_argument("--json", action="store_true")
p.set_defaults(func=cmd_scan)
p = sub.add_parser("mark")
p.add_argument("--state", required=True)
p.add_argument("path")
p.add_argument("verdict", choices=VERDICTS)
p.set_defaults(func=cmd_mark)
args = parser.parse_args()
return args.func(args)
if __name__ == "__main__":
sys.exit(main())
-18
View File
@@ -1,18 +0,0 @@
---
name: shortcuts
description: >
Operator conversation shortcuts. Trigger when the message is exactly one of
these tokens, optionally with arguments: sobu, soco, socobu, socon, sona,
sote. Expand it and act on the expanded instruction.
---
When an operator message is one of the shortcuts below, expand it to the meaning and act on it.
| Shortcut | Meaning |
| --- | --- |
| `sobu` | Stage next bundle: stage the next logical bundle of verified changes, explain what it does, and list what still remains uncommitted. |
| `soco` | Commit: commit the currently staged changes (pre-commit hooks run). |
| `socobu` | Commit no-verify, then stage next bundle: run `socon` followed by `sobu`. |
| `socon` | Commit no-verify: commit the currently staged changes with `--no-verify`. |
| `sona` | New alias: add the shortcut the operator states to the appropriate table, keeping it sorted. |
| `sote` | Run `make test`. |
+7 -86
View File
@@ -1,41 +1,23 @@
"""Validate the autotune reminder hook and its settings registration.""" """Validate the kvb-skill-tune skill text and its persistence targets."""
from __future__ import annotations from __future__ import annotations
import json
import shutil
import subprocess
import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1] REPO_ROOT = Path(__file__).resolve().parents[1]
SKILL = REPO_ROOT / "skills" / "autotune" / "SKILL.md" SKILL = REPO_ROOT / "skills" / "kvb-skill-tune" / "SKILL.md"
HOOK = REPO_ROOT / "skills" / "autotune" / "hooks" / "reminder.sh"
PATCHER = REPO_ROOT / "scripts" / "enable-plugins.js"
def _run_hook(stamp: Path, interval: str = "3600") -> subprocess.CompletedProcess:
return subprocess.run(
["bash", str(HOOK)],
capture_output=True,
text=True,
check=True,
env={
"PATH": "/usr/bin:/bin",
"HOME": str(stamp.parent),
"AUTOTUNE_STAMP": str(stamp),
"AUTOTUNE_INTERVAL_SECONDS": interval,
},
)
class TestAutotuneSkill(unittest.TestCase): class TestAutotuneSkill(unittest.TestCase):
def test_skill_is_trigger_only(self): def test_skill_is_trigger_only(self):
text = SKILL.read_text(encoding="utf-8") text = SKILL.read_text(encoding="utf-8")
self.assertIn("name: autotune", text) self.assertIn("name: kvb-skill-tune", text)
self.assertIn("multiSelect: false", text) self.assertIn("multiSelect: false", text)
def test_skill_has_no_own_reminder_hook(self):
self.assertFalse((SKILL.parent / "hooks").exists())
class TestSkillPersistence(unittest.TestCase): class TestSkillPersistence(unittest.TestCase):
"""Both skill writers must persist to the skills repository AND the live dirs.""" """Both skill writers must persist to the skills repository AND the live dirs."""
@@ -51,73 +33,12 @@ class TestSkillPersistence(unittest.TestCase):
self.assertIn(target, text) self.assertIn(target, text)
def test_autoskill_routes_to_the_same_persistence(self): def test_autoskill_routes_to_the_same_persistence(self):
text = (REPO_ROOT / "skills" / "autoskill" / "SKILL.md").read_text( text = (REPO_ROOT / "skills" / "kvb-skill-auto" / "SKILL.md").read_text(
encoding="utf-8" encoding="utf-8"
) )
for target in ("Persistence", "~/.claude/skills/", "~/.agents/skills/"): for target in ("Persistence", "~/.claude/skills/", "~/.agents/skills/"):
self.assertIn(target, text) self.assertIn(target, text)
class TestAutotuneHook(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.stamp = Path(self.tmp.name) / ".claude" / ".autotune-stamp"
def test_first_run_hints_and_stamps(self):
result = _run_hook(self.stamp)
self.assertIn("/autotune", result.stdout)
self.assertTrue(self.stamp.is_file())
def test_second_run_is_silent_within_interval(self):
_run_hook(self.stamp)
self.assertEqual(_run_hook(self.stamp).stdout, "")
def test_hint_returns_after_the_interval(self):
_run_hook(self.stamp)
self.assertIn("/autotune", _run_hook(self.stamp, interval="0").stdout)
def test_corrupt_stamp_does_not_crash(self):
self.stamp.parent.mkdir(parents=True, exist_ok=True)
self.stamp.write_text("not-a-timestamp\n", encoding="utf-8")
self.assertIn("/autotune", _run_hook(self.stamp).stdout)
@unittest.skipUnless(shutil.which("node"), "node not installed")
class TestSettingsRegistration(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.settings = Path(self.tmp.name) / "settings.json"
def _patch(self) -> dict:
subprocess.run(
["node", str(PATCHER), str(self.settings)], check=True, capture_output=True
)
return json.loads(self.settings.read_text(encoding="utf-8"))
def test_hook_registered_once(self):
self._patch()
data = self._patch()
entries = json.dumps(data["hooks"]["UserPromptSubmit"])
self.assertEqual(entries.count("autotune/hooks/reminder.sh"), 1)
def test_existing_hooks_preserved(self):
self.settings.write_text(
json.dumps(
{
"hooks": {
"UserPromptSubmit": [
{"hooks": [{"type": "command", "command": "true"}]}
]
}
}
),
encoding="utf-8",
)
data = self._patch()
self.assertEqual(len(data["hooks"]["UserPromptSubmit"]), 4)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+3 -3
View File
@@ -11,8 +11,8 @@ import unittest
from pathlib import Path from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1] REPO_ROOT = Path(__file__).resolve().parents[1]
CI_FAILURES = REPO_ROOT / "skills" / "ci-artifacts" / "scripts" / "ci_failures.py" CI_FAILURES = REPO_ROOT / "skills" / "kvb-ci-artifacts" / "scripts" / "ci_failures.py"
FIND_FIX = REPO_ROOT / "skills" / "merge-fix" / "scripts" / "find_fix.py" FIND_FIX = REPO_ROOT / "skills" / "kvb-git-merge-fix" / "scripts" / "find_fix.py"
def _load(path: Path): def _load(path: Path):
@@ -55,7 +55,7 @@ class TestCiFailures(unittest.TestCase):
"Ran 41 tests in 1.0s\n" "Ran 41 tests in 1.0s\n"
"OK\n" "OK\n"
"UP017 [*] Use `datetime.UTC` alias\n" "UP017 [*] Use `datetime.UTC` alias\n"
" --> skills/democracy/scripts/council.py:92:40\n" " --> skills/kvb-org-organize/scripts/council.py:92:40\n"
"Found 1 error.\n" "Found 1 error.\n"
"make: *** [Makefile:15: test] Error 1\n" "make: *** [Makefile:15: test] Error 1\n"
"##[error]Process completed with exit code 2.\n" "##[error]Process completed with exit code 2.\n"
+63
View File
@@ -0,0 +1,63 @@
"""Exercise issues.py: closing keywords, mentions, commits, branch number and linked issues."""
from __future__ import annotations
import json
import subprocess
import sys
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPT = REPO_ROOT / "skills" / "kvb-git-close-issues" / "scripts" / "issues.py"
PR = {
"number": 40,
"body": (
"Fixes #12 and touches #13.\nCloses: other/repo#7\nSee https://github.com/a/b/issues/9\n"
"Rows WordPress#0 and docs#1 are not issues; this is #40."
),
"headRefName": "fix/sys-ctl-hlth-runtime/vulnerability-engines/1324",
"commits": [
{"oid": "abcdef1234567", "messageHeadline": "fix: guard", "messageBody": "Resolved #13"},
{"oid": "1111111111111", "messageHeadline": "chore: tidy", "messageBody": ""},
],
"closingIssuesReferences": [
{"number": 21, "url": "https://github.com/owner/repo/issues/21"}
],
}
def _run(payload: str) -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, str(SCRIPT), "--repo", "Owner/repo", "--json"],
input=payload,
check=False,
capture_output=True,
text=True,
)
class TestIssues(unittest.TestCase):
def test_every_reference_is_found_with_its_kind_and_source(self):
result = _run(json.dumps(PR))
self.assertEqual(result.returncode, 0, result.stderr)
rows = {(r["repo"], r["issue"]): r for r in json.loads(result.stdout)}
self.assertEqual(
{key: (row["closing"], row["sources"]) for key, row in rows.items()},
{
("", 12): (True, ["body"]),
("", 13): (True, ["body", "commit abcdef1234"]),
("other/repo", 7): (True, ["body"]),
("a/b", 9): (False, ["body"]),
("", 1324): (False, ["branch"]),
("", 21): (True, ["linked"]),
},
)
def test_invalid_input_exits_2(self):
self.assertEqual(_run("not json").returncode, 2)
self.assertEqual(_run("[]").returncode, 2)
if __name__ == "__main__":
unittest.main()
+94
View File
@@ -0,0 +1,94 @@
"""Exercise coach.py: preferences flip between like and dislike, the log feeds hours since."""
from __future__ import annotations
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPT = REPO_ROOT / "skills" / "kvb-coach" / "scripts" / "coach.py"
HOOK = REPO_ROOT / "skills" / "kvb-coach" / "hooks" / "reminder.sh"
class TestCoach(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.profile = Path(self.tmp.name) / "state" / "coach.json"
def _run(self, *args: str) -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, str(SCRIPT), "--profile", str(self.profile), *args],
check=False,
capture_output=True,
text=True,
)
def _show(self, now: str) -> dict:
result = self._run("show", "--now", now, "--json")
self.assertEqual(result.returncode, 0, result.stderr)
return {row["area"]: row for row in json.loads(result.stdout)}
def test_a_dislike_replaces_a_like_and_forget_drops_it(self):
self._run("like", "body", "Laufen")
self._run("like", "body", "Laufen")
self._run("like", "body", "Yoga")
self._run("dislike", "body", "Laufen")
body = self._show("2026-11-03T18:00:00+00:00")["body"]
self.assertEqual(body["likes"], ["Yoga"])
self.assertEqual(body["dislikes"], ["Laufen"])
self._run("forget", "body", "Laufen")
self.assertEqual(self._show("2026-11-03T18:00:00+00:00")["body"]["dislikes"], [])
def test_the_latest_done_per_area_gives_the_hours_since(self):
self._run("done", "food", "Frühstück", "--at", "2026-11-03T08:00:00+00:00")
self._run("done", "food", "Mittagessen", "--at", "2026-11-03T12:30:00+00:00")
self._run("done", "sleep", "Nacht", "--at", "2026-11-02T23:00:00+00:00")
rows = self._show("2026-11-03T18:00:00+00:00")
self.assertEqual((rows["food"]["last"], rows["food"]["hours_ago"]), ("Mittagessen", 5.5))
self.assertEqual(rows["sleep"]["hours_ago"], 19.0)
self.assertIsNone(rows["psyche"]["last"])
def test_invalid_input_exits_2_and_keeps_the_profile(self):
self.assertEqual(self._run("like", "work", "x").returncode, 2)
self.assertEqual(self._run("done", "body", "x", "--at", "gestern").returncode, 2)
self.assertFalse(self.profile.exists())
self.profile.parent.mkdir(parents=True)
self.profile.write_text("{broken", encoding="utf-8")
self.assertEqual(self._run("show").returncode, 2)
self.assertEqual(self.profile.read_text(encoding="utf-8"), "{broken")
class TestCoachHour(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.hours = Path(self.tmp.name) / "hours"
def _run(self, hour: str) -> str:
return subprocess.run(
["bash", str(HOOK)],
check=True,
capture_output=True,
text=True,
env={
"PATH": "/usr/bin:/bin",
"HOME": self.tmp.name,
"COACH_STAMP_DIR": str(self.hours),
"COACH_HOUR": hour,
},
).stdout
def test_first_prompt_of_each_hour_triggers_the_coach_once(self):
self.assertIn("kvb-coach", self._run("2026-11-03T14"))
self.assertEqual(self._run("2026-11-03T14"), "")
self.assertIn("kvb-coach", self._run("2026-11-03T15"))
self.assertEqual(sorted(p.name for p in self.hours.iterdir()), ["2026-11-03T15"])
if __name__ == "__main__":
unittest.main()
+7 -2
View File
@@ -1,4 +1,4 @@
"""Exercise the democracy council script: election, staleness, release, gate.""" """Exercise the kvb-org-organize council script: election, staleness, release, gate."""
from __future__ import annotations from __future__ import annotations
@@ -10,7 +10,7 @@ import unittest
from pathlib import Path from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1] REPO_ROOT = Path(__file__).resolve().parents[1]
COUNCIL = REPO_ROOT / "skills" / "democracy" / "scripts" / "council.py" COUNCIL = REPO_ROOT / "skills" / "kvb-org-organize" / "scripts" / "council.py"
class TestCouncil(unittest.TestCase): class TestCouncil(unittest.TestCase):
@@ -62,6 +62,11 @@ class TestCouncil(unittest.TestCase):
self._run("s1", "claim", "work/x", "--work", "w") self._run("s1", "claim", "work/x", "--work", "w")
self.assertEqual(self._run("s2", "takeover", "work/x", "--work", "w").returncode, 3) self.assertEqual(self._run("s2", "takeover", "work/x", "--work", "w").returncode, 3)
def test_session_flag_cannot_hide_the_claude_code_session(self):
self._run("s1", "claim", "work/x", "--work", "w", "--session", "One")
self.assertIn('"session": "s1"', (self.root / "work/x/claim.json").read_text())
self.assertEqual(self._run("s1", "release", "work/x", "--session", "One").returncode, 0)
def test_only_the_holder_releases(self): def test_only_the_holder_releases(self):
self._run("s1", "claim", "work/x", "--work", "w") self._run("s1", "claim", "work/x", "--work", "w")
self.assertEqual(self._run("s2", "release", "work/x").returncode, 3) self.assertEqual(self._run("s2", "release", "work/x").returncode, 3)
+156 -79
View File
@@ -1,4 +1,4 @@
"""Validate the dream reminder hook and its settings registration.""" """Validate the kvb-dream reminder, its GNOME notification, timer and settings registration."""
from __future__ import annotations from __future__ import annotations
@@ -10,101 +10,124 @@ import unittest
from pathlib import Path from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1] REPO_ROOT = Path(__file__).resolve().parents[1]
SKILL = REPO_ROOT / "skills" / "dream" / "SKILL.md" DREAM = REPO_ROOT / "skills" / "kvb-dream"
HOOK = REPO_ROOT / "skills" / "dream" / "hooks" / "reminder.sh" SKILL = DREAM / "SKILL.md"
HOOK = DREAM / "hooks" / "reminder.sh"
NOTIFY = DREAM / "hooks" / "notify.sh"
TIMER = DREAM / "systemd" / "kvb-dream-notify.timer"
SERVICE = DREAM / "systemd" / "kvb-dream-notify.service"
PATCHER = REPO_ROOT / "scripts" / "enable-plugins.js" PATCHER = REPO_ROOT / "scripts" / "enable-plugins.js"
def _run_hook(
cwd: Path, stamp: Path | None = None, interval: str = "43200"
) -> subprocess.CompletedProcess:
env = {
"PATH": "/usr/bin:/bin",
"HOME": str(cwd),
"DREAM_INTERVAL_SECONDS": interval,
}
if stamp is not None:
env["DREAM_STAMP"] = str(stamp)
return subprocess.run(
["bash", str(HOOK)],
capture_output=True,
text=True,
check=True,
cwd=str(cwd),
env=env,
)
class TestDreamSkill(unittest.TestCase): class TestDreamSkill(unittest.TestCase):
def test_skill_is_trigger_only(self): def test_skill_runs_optimize_and_the_cleanups(self):
text = SKILL.read_text(encoding="utf-8") text = SKILL.read_text(encoding="utf-8")
self.assertIn("name: dream", text) self.assertIn("name: kvb-dream", text)
self.assertIn("multiSelect: false", text) self.assertIn("multiSelect: false", text)
for skill in ("`kvb-optimize`", "`kvb-cleanup`", "`kvb-git-branch-gc`"):
self.assertIn(skill, text)
def test_optimize_covers_every_optimization_skill(self):
text = (REPO_ROOT / "skills" / "kvb-optimize" / "SKILL.md").read_text(encoding="utf-8")
for skill in (
"kvb-skill-cleanup",
"kvb-transcend",
"kvb-skill-auto",
"kvb-skill-tune",
"kvb-skill-compile",
"kvb-skill-essence",
"kvb-code-refactor",
"kvb-code-tune",
"kvb-think-gaps",
):
self.assertIn(f"`{skill}`", text)
class TestDreamHook(unittest.TestCase): class TestDreamHook(unittest.TestCase):
def setUp(self): def setUp(self):
self.tmp = tempfile.TemporaryDirectory() self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup) self.addCleanup(self.tmp.cleanup)
self.cwd = Path(self.tmp.name) self.stamps = Path(self.tmp.name) / "stamps"
self.stamp = self.cwd / ".claude" / ".dream-stamp"
def test_first_run_hints_and_stamps(self): def _run(self, payload: str, interval: str = "43200") -> str:
result = _run_hook(self.cwd, self.stamp) return subprocess.run(
self.assertIn("/dream", result.stdout) ["bash", str(HOOK)],
self.assertTrue(self.stamp.is_file()) input=payload,
capture_output=True,
text=True,
check=True,
env={
"PATH": "/usr/bin:/bin",
"HOME": self.tmp.name,
"DREAM_STAMP_DIR": str(self.stamps),
"DREAM_INTERVAL_SECONDS": interval,
},
).stdout
def test_second_run_is_silent_within_interval(self): @staticmethod
_run_hook(self.cwd, self.stamp) def _session(sid: str) -> str:
self.assertEqual(_run_hook(self.cwd, self.stamp).stdout, "") return json.dumps({"session_id": sid, "prompt": "hi"})
def test_first_run_hints_and_stamps_the_session(self):
self.assertIn("/kvb-dream", self._run(self._session("a1")))
self.assertTrue((self.stamps / "a1").is_file())
def test_second_run_of_the_same_agent_is_silent(self):
self._run(self._session("a1"))
self.assertEqual(self._run(self._session("a1")), "")
def test_every_agent_gets_its_own_hint(self):
self._run(self._session("a1"))
self.assertIn("/kvb-dream", self._run(self._session("b2")))
def test_hint_returns_after_the_interval(self): def test_hint_returns_after_the_interval(self):
_run_hook(self.cwd, self.stamp) self._run(self._session("a1"))
self.assertIn("/dream", _run_hook(self.cwd, self.stamp, interval="0").stdout) self.assertIn("/kvb-dream", self._run(self._session("a1"), interval="0"))
def test_corrupt_stamp_does_not_crash(self): def test_corrupt_stamp_does_not_crash(self):
self.stamp.parent.mkdir(parents=True, exist_ok=True) self.stamps.mkdir()
self.stamp.write_text("not-a-timestamp\n", encoding="utf-8") (self.stamps / "a1").write_text("not-a-timestamp\n", encoding="utf-8")
self.assertIn("/dream", _run_hook(self.cwd, self.stamp).stdout) self.assertIn("/kvb-dream", self._run(self._session("a1")))
def test_outside_a_repository_it_stays_silent(self): def test_without_a_valid_session_it_stays_silent(self):
self.assertEqual(_run_hook(self.cwd).stdout, "") for payload in ("", "not json", self._session(""), self._session("../escape")):
with self.subTest(payload=payload):
self.assertEqual(self._run(payload), "")
self.assertFalse(self.stamps.exists())
@unittest.skipUnless(shutil.which("git"), "git not installed")
def test_stamp_is_per_repository(self): class TestDreamNotification(unittest.TestCase):
for name in ("one", "two"): def setUp(self):
repo = self.cwd / name self.tmp = tempfile.TemporaryDirectory()
repo.mkdir() self.addCleanup(self.tmp.cleanup)
self.bin = Path(self.tmp.name) / "bin"
self.bin.mkdir()
self.calls = Path(self.tmp.name) / "calls"
fake = self.bin / "notify-send"
fake.write_text(f'#!/bin/sh\nprintf "%s\\n" "$@" >> "{self.calls}"\n', encoding="utf-8")
fake.chmod(0o755)
def _run(self, desktop: str) -> None:
subprocess.run( subprocess.run(
["git", "init", "-q", str(repo)], check=True, capture_output=True ["bash", str(NOTIFY)],
capture_output=True,
check=True,
env={"PATH": f"{self.bin}:/usr/bin:/bin", "XDG_CURRENT_DESKTOP": desktop},
) )
self.assertIn("/dream", _run_hook(repo).stdout)
self.assertTrue((repo / ".git" / ".dream-stamp").is_file())
self.assertEqual(_run_hook(repo).stdout, "")
@unittest.skipUnless(shutil.which("git"), "git not installed") def test_gnome_session_gets_the_popup(self):
def test_linked_worktrees_share_the_stamp(self): self._run("ubuntu:GNOME")
repo = self.cwd / "main" self.assertIn("/kvb-dream", self.calls.read_text(encoding="utf-8"))
tree = self.cwd / "tree"
env = { def test_other_desktops_stay_silent(self):
"PATH": "/usr/bin:/bin", self._run("KDE")
"HOME": str(self.cwd), self.assertFalse(self.calls.exists())
"GIT_AUTHOR_NAME": "t",
"GIT_AUTHOR_EMAIL": "t@t", def test_timer_fires_daily_at_five_pm_and_catches_up(self):
"GIT_COMMITTER_NAME": "t", timer = TIMER.read_text(encoding="utf-8")
"GIT_COMMITTER_EMAIL": "t@t", self.assertIn("OnCalendar=*-*-* 17:00:00", timer)
} self.assertIn("Persistent=true", timer)
repo.mkdir() self.assertIn("kvb-dream/hooks/notify.sh", SERVICE.read_text(encoding="utf-8"))
for args in (
["init", "-q"],
["commit", "-q", "--allow-empty", "-m", "init"],
["worktree", "add", "-q", str(tree)],
):
subprocess.run(
["git", *args], check=True, capture_output=True, env=env, cwd=str(repo)
)
self.assertIn("/dream", _run_hook(repo).stdout)
self.assertEqual(_run_hook(tree).stdout, "")
@unittest.skipUnless(shutil.which("node"), "node not installed") @unittest.skipUnless(shutil.which("node"), "node not installed")
@@ -124,14 +147,68 @@ class TestSettingsRegistration(unittest.TestCase):
self._patch() self._patch()
data = self._patch() data = self._patch()
entries = json.dumps(data["hooks"]["UserPromptSubmit"]) entries = json.dumps(data["hooks"]["UserPromptSubmit"])
self.assertEqual(entries.count("dream/hooks/reminder.sh"), 1) self.assertEqual(entries.count("kvb-dream/hooks/reminder.sh"), 1)
self.assertEqual(entries.count("autotune/hooks/reminder.sh"), 1) self.assertEqual(entries.count("kvb-coach/hooks/reminder.sh"), 1)
self.assertNotIn("tune/hooks/reminder.sh", entries)
gate = json.dumps(data["hooks"]["PreToolUse"]) gate = json.dumps(data["hooks"]["PreToolUse"])
self.assertEqual(gate.count("democracy/scripts/council.py"), 1) self.assertEqual(gate.count("kvb-org-organize/scripts/council.py"), 1)
self.assertEqual(gate.count("democracy/scripts/watch.py"), 1) self.assertEqual(gate.count("kvb-org-organize/scripts/watch.py"), 1)
self.assertEqual(entries.count("democracy/scripts/watch.py"), 1) self.assertEqual(entries.count("kvb-org-organize/scripts/watch.py"), 1)
post = json.dumps(data["hooks"]["PostToolUse"]) post = json.dumps(data["hooks"]["PostToolUse"])
self.assertEqual(post.count("democracy/scripts/watch.py"), 1) self.assertEqual(post.count("kvb-org-organize/scripts/watch.py"), 1)
self.assertEqual(gate.count("kvb-org-perform/scripts/log.py"), 1)
self.assertEqual(post.count("kvb-org-perform/scripts/log.py"), 1)
def test_existing_hooks_preserved(self):
self.settings.write_text(
json.dumps({"hooks": {"UserPromptSubmit": [{"hooks": [{"type": "command", "command": "true"}]}]}}),
encoding="utf-8",
)
data = self._patch()
self.assertIn('"true"', json.dumps(data["hooks"]["UserPromptSubmit"]))
def test_tune_reminders_retire_and_dream_reminders_are_repointed(self):
base = Path(self.tmp.name) / "skills"
def hook(command):
return {"hooks": [{"type": "command", "command": command}]}
self.settings.write_text(
json.dumps(
{
"hooks": {
"UserPromptSubmit": [
hook(f'bash "{base}/autotune/hooks/reminder.sh"'),
hook(f'bash "{base}/kvb-skill-tune/hooks/reminder.sh"'),
hook(f'bash "{base}/kvb-skill-dream/hooks/reminder.sh"'),
hook(f'python3 "{base}/democracy/scripts/watch.py" watch'),
hook("echo keep"),
],
"PreToolUse": [
hook(f'python3 "{base}/democracy/scripts/council.py" gate'),
hook(f'python3 "{base}/democracy/scripts/watch.py" block'),
hook(f'python3 "{base}/subagent-model/scripts/log.py" spawn'),
],
"PostToolUse": [
hook(f'python3 "{base}/democracy/scripts/watch.py" watch'),
hook(f'python3 "{base}/subagent-model/scripts/log.py" persist'),
],
}
}
),
encoding="utf-8",
)
data = self._patch()
text = json.dumps(data["hooks"])
for old in ("/autotune/", "/kvb-skill-tune/", "/kvb-skill-dream/", "/democracy/", "/subagent-model/"):
self.assertNotIn(f"skills{old}", text)
self.assertIn("echo keep", text)
entries = json.dumps(data["hooks"]["UserPromptSubmit"])
self.assertEqual(entries.count("kvb-dream/hooks/reminder.sh"), 1)
self.assertEqual(entries.count("kvb-org-organize/scripts/watch.py"), 1)
gate = json.dumps(data["hooks"]["PreToolUse"])
self.assertEqual(gate.count("kvb-org-organize/scripts/council.py"), 1)
self.assertEqual(gate.count("kvb-org-perform/scripts/log.py"), 1)
if __name__ == "__main__": if __name__ == "__main__":
+22 -10
View File
@@ -2,18 +2,23 @@
from __future__ import annotations from __future__ import annotations
import importlib.util
import json import json
import re import re
import subprocess
import sys
import unittest import unittest
from pathlib import Path from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1] REPO_ROOT = Path(__file__).resolve().parents[1]
LOCK_FILE = REPO_ROOT / "skills-lock.json" LOCK_FILE = REPO_ROOT / "skills-lock.json"
BUDGET = REPO_ROOT / "scripts" / "budget.py"
SPEC = importlib.util.spec_from_file_location("budget", BUDGET)
budget = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(budget)
HASH_RE = re.compile(r"^[0-9a-f]{64}$") HASH_RE = re.compile(r"^[0-9a-f]{64}$")
REQUIRED_FIELDS = ("source", "sourceType", "skillPath", "computedHash") REQUIRED_FIELDS = ("source", "sourceType", "skillPath", "computedHash")
DESCRIPTION_WORDS = 40
LISTING_WORDS = 1300
class TestSkillsLock(unittest.TestCase): class TestSkillsLock(unittest.TestCase):
@@ -64,14 +69,21 @@ class TestSkillsLock(unittest.TestCase):
) )
def test_descriptions_fit_the_listing_budget(self): def test_descriptions_fit_the_listing_budget(self):
total = 0 found = budget.counts(REPO_ROOT, index=False)
for skill_md in sorted((REPO_ROOT / "skills").glob("*/SKILL.md")): for name, words in found.items():
frontmatter = skill_md.read_text(encoding="utf-8").split("---", 2)[1] with self.subTest(skill=name):
words = len(frontmatter.split("description:", 1)[1].split()) - 1 self.assertLessEqual(words, budget.DESCRIPTION_WORDS)
total += words self.assertLessEqual(sum(found.values()), budget.LISTING_WORDS)
with self.subTest(skill=skill_md.parent.name):
self.assertLessEqual(words, DESCRIPTION_WORDS) def test_budget_script_reports_the_total_and_the_longest(self):
self.assertLessEqual(total, LISTING_WORDS) result = subprocess.run(
[sys.executable, str(BUDGET), "--repo", str(REPO_ROOT), "--top", "3"],
capture_output=True, text=True, check=False,
)
self.assertEqual(result.returncode, 0, result.stdout)
self.assertRegex(result.stdout, r"^total \d+/\d+ words in \d+ skills\n")
self.assertEqual(len(result.stdout.splitlines()), 4)
self.assertEqual(budget.words("---\nname: x\ndescription: >\n one two three\n---\n"), 3)
def test_first_party_skills_do_not_shadow_locked_skills(self): def test_first_party_skills_do_not_shadow_locked_skills(self):
locked = set(self.lock["skills"].keys()) locked = set(self.lock["skills"].keys())
+88
View File
@@ -0,0 +1,88 @@
"""Exercise meditation.py and keep it in step with the keys and choices of meditation.html."""
from __future__ import annotations
import importlib.util
import json
import re
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SKILL = REPO_ROOT / "skills" / "kvb-coach-meditation"
SCRIPT = SKILL / "scripts" / "meditation.py"
PAGE = SKILL / "assets" / "meditation.html"
FULL = [
"minutes=5",
"beat=10",
"base=196",
"volume=0.4",
"mode=isochronic",
"palette=aurora",
"breathing=box",
]
class TestMeditation(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.state = Path(self.tmp.name) / "state" / "meditation.json"
def _run(self, *args: str) -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, str(SCRIPT), "--state", str(self.state), *args],
check=False,
capture_output=True,
text=True,
)
def _session(self) -> subprocess.CompletedProcess:
out = Path(self.tmp.name) / "session.html"
return self._run("session", "--page", str(PAGE), "--out", str(out))
def test_session_page_embeds_every_setting(self):
self.assertEqual(self._run("set", *FULL).returncode, 0)
result = self._session()
self.assertEqual(result.returncode, 0, result.stderr)
page = Path(result.stdout.strip()).read_text(encoding="utf-8")
self.assertNotIn("const SETTINGS = null;", page)
embedded = json.loads(re.search(r"const SETTINGS = (\{.*?\});", page).group(1))
self.assertEqual(embedded["beat"], 10.0)
self.assertEqual(embedded["minutes"], 5)
self.assertEqual(set(embedded), {pair.split("=")[0] for pair in FULL})
def test_session_refuses_missing_settings(self):
self._run("set", "minutes=5")
self.assertEqual(self._session().returncode, 2)
self.assertFalse((Path(self.tmp.name) / "session.html").exists())
def test_invalid_values_exit_2_and_keep_the_state(self):
self._run("set", *FULL)
before = json.loads(self.state.read_text(encoding="utf-8"))
for pair in ("beat=20", "mode=loud", "minutes=five", "tempo=3", "volume"):
with self.subTest(pair=pair):
self.assertEqual(self._run("set", "minutes=7", pair).returncode, 2)
self.assertEqual(json.loads(self.state.read_text(encoding="utf-8")), before)
class TestPageMatchesScript(unittest.TestCase):
def test_keys_and_choices_agree(self):
spec = importlib.util.spec_from_file_location("meditation", SCRIPT)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
page = PAGE.read_text(encoding="utf-8")
self.assertEqual(page.count(module.PLACEHOLDER), 1)
keys = re.search(r"const KEYS = \[(.*?)\];", page).group(1)
self.assertEqual(re.findall(r'"(\w+)"', keys), list(module.KEYS))
for name, choices in (("PALETTES", "palette"), ("PATTERNS", "breathing")):
block = re.search(rf"const {name} = \{{(.*?)\n\}};", page, re.DOTALL).group(1)
found = re.findall(r'^\s*"?(\w+)"?:', block, re.MULTILINE)
self.assertEqual(sorted(found), sorted(module.CHOICES[choices]))
if __name__ == "__main__":
unittest.main()
+76
View File
@@ -0,0 +1,76 @@
"""Pin every first-party skill name to kvb-<category>-<name> and the Agent Skills name rules."""
from __future__ import annotations
import re
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SKILLS = REPO_ROOT / "skills"
README = REPO_ROOT / "README.md"
CATEGORIES = ("think", "create", "code", "ci", "git", "check", "org", "skill", "coach")
UNCATEGORIZED = frozenset(
{
"kvb-cleanup",
"kvb-coach",
"kvb-dream",
"kvb-help",
"kvb-optimize",
"kvb-transcend",
}
)
SPEC = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
SCHEMA = re.compile(rf"^kvb-(?:{'|'.join(CATEGORIES)})-[a-z0-9]+(?:-[a-z0-9]+)*$")
NAME_LINE = re.compile(r"^name:\s*(\S+)\s*$", re.MULTILINE)
ORGANIGRAM_EDGE = re.compile(r"^\s*(\w+) --> \w+\[(kvb-[a-z0-9-]+)\]\s*$", re.MULTILINE)
class TestSkillNames(unittest.TestCase):
def setUp(self):
self.names = sorted(p.name for p in SKILLS.iterdir() if p.is_dir())
def test_name_field_matches_the_directory(self):
for name in self.names:
with self.subTest(skill=name):
text = (SKILLS / name / "SKILL.md").read_text(encoding="utf-8")
match = NAME_LINE.search(text)
self.assertIsNotNone(match, "SKILL.md has no name: line")
self.assertEqual(match.group(1), name)
def test_names_follow_the_agent_skills_rules(self):
for name in self.names:
with self.subTest(skill=name):
self.assertRegex(name, SPEC)
self.assertLessEqual(len(name), 64)
self.assertNotRegex(name, "anthropic|claude")
def test_names_use_a_listed_category(self):
for name in self.names:
if name in UNCATEGORIZED:
continue
with self.subTest(skill=name):
self.assertRegex(name, SCHEMA)
def test_every_category_and_exception_is_in_use(self):
for category in CATEGORIES:
with self.subTest(category=category):
self.assertTrue(
any(n.startswith(f"kvb-{category}-") for n in self.names),
f"category {category} has no skill",
)
self.assertLessEqual(UNCATEGORIZED, set(self.names))
def test_readme_organigram_places_every_skill_under_its_category(self):
text = README.read_text(encoding="utf-8")
section = text.split("## Organigram", 1)[1].split("\n## ", 1)[0]
edges = ORGANIGRAM_EDGE.findall(section)
self.assertEqual(sorted(name for _, name in edges), self.names)
for parent, name in edges:
with self.subTest(skill=name):
expected = "none" if name in UNCATEGORIZED else name.split("-")[1]
self.assertEqual(parent, expected)
if __name__ == "__main__":
unittest.main()
+5 -5
View File
@@ -1,4 +1,4 @@
"""The self-improving skills must share the objective held by autotune.""" """The self-improving skills must share the objective held by kvb-skill-tune."""
from __future__ import annotations from __future__ import annotations
@@ -7,12 +7,12 @@ from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1] REPO_ROOT = Path(__file__).resolve().parents[1]
SKILLS = REPO_ROOT / "skills" SKILLS = REPO_ROOT / "skills"
ROUTERS = ("autoskill", "confidence") ROUTERS = ("kvb-skill-auto", "kvb-check-confidence")
class TestOptimizeObjective(unittest.TestCase): class TestOptimizeObjective(unittest.TestCase):
def test_autotune_names_the_three_axes(self): def test_autotune_names_the_three_axes(self):
text = (SKILLS / "autotune" / "SKILL.md").read_text(encoding="utf-8") text = (SKILLS / "kvb-skill-tune" / "SKILL.md").read_text(encoding="utf-8")
self.assertIn("## Objective", text) self.assertIn("## Objective", text)
for axis in ("**Tokens**", "**Time**", "**Quality**"): for axis in ("**Tokens**", "**Time**", "**Quality**"):
self.assertIn(axis, text) self.assertIn(axis, text)
@@ -21,9 +21,9 @@ class TestOptimizeObjective(unittest.TestCase):
for skill in ROUTERS: for skill in ROUTERS:
text = (SKILLS / skill / "SKILL.md").read_text(encoding="utf-8") text = (SKILLS / skill / "SKILL.md").read_text(encoding="utf-8")
self.assertIn( self.assertIn(
"`autotune` skill's **Objective**", "`kvb-skill-tune` skill's **Objective**",
text, text,
f"{skill} does not route to the autotune objective", f"{skill} does not route to the kvb-skill-tune objective",
) )
+251
View File
@@ -0,0 +1,251 @@
"""Exercise the kvb-org-rasci register: fields, roles, priority order, claims, artifacts, tools."""
from __future__ import annotations
import datetime
import json
import subprocess
import sys
import tempfile
import time
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
RASCI = REPO_ROOT / "skills" / "kvb-org-rasci" / "scripts" / "rasci.py"
COUNCIL = REPO_ROOT / "skills" / "kvb-org-organize" / "scripts" / "council.py"
HEADER = "| Prio | WSJF | Task | OKR | Status | Worker | Artifacts | Start | End | R | A | S | C | I |"
def local(iso: str) -> str:
return datetime.datetime.fromisoformat(iso).astimezone().strftime("%m-%d %H:%M")
class TestRasci(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.home = Path(self.tmp.name) / "home"
self.root = Path(self.tmp.name) / "council"
self.sessions = self.home / ".claude" / "sessions"
self.sessions.mkdir(parents=True)
for sid, name in (("s1", "One"), ("s2", "Two")):
(self.sessions / f"{sid}.json").write_text(
json.dumps({"sessionId": sid, "name": name}), encoding="utf-8"
)
def _run(self, *args: str, script: Path = RASCI, sid: str = "s1") -> subprocess.CompletedProcess:
env = {
"PATH": "/usr/bin:/bin",
"HOME": str(self.home),
"AGENTS_COUNCIL_ROOT": str(self.root),
"CLAUDE_CODE_SESSION_ID": sid,
}
return subprocess.run(
[sys.executable, str(script), *args],
capture_output=True,
text=True,
env=env,
cwd=self.tmp.name,
check=False,
)
def _out(self, *args: str) -> list[str]:
result = self._run(*args)
self.assertEqual(result.returncode, 0, result.stderr)
return result.stdout.splitlines()
def _claim(self, office: str, session: str, name: str, work: str) -> None:
path = self.root / office
path.mkdir(parents=True)
claim = {
"office": office,
"session": session,
"runtime": "claude-code",
"name": name,
"since": "2026-11-01T00:00:00+00:00",
"beat": time.time(),
"work": work,
}
(path / "claim.json").write_text(json.dumps(claim), encoding="utf-8")
def test_matrix_orders_by_priority_and_lists_roles(self):
start = "2026-11-01T10:00:00+00:00"
self._run("set", "work/a", "--title", "Alpha", "--objective", "Ship A", "--prio", "2",
"--status", "running", "--worker", "Skills", "--start", start)
self._run("role", "work/a", "Skills", "ar")
self._run("role", "work/a", "Operator", "ic")
self._run("role", "work/a", "Pkgmgr", "C")
self._run("set", "work/b", "--title", "Beta", "--prio", "1", "--worker", "subagent Explore (haiku)")
self._run("role", "work/b", "Pkgmgr", "R")
lines = self._out("show")
self.assertEqual(lines[0], HEADER)
self.assertEqual(
lines[2], "| 1 | - | work/b: Beta | - | queued | subagent Explore (haiku) | - | - | - | Pkgmgr | | | | |"
)
self.assertEqual(
lines[3],
f"| 2 | - | work/a: Alpha | Ship A | running | Skills | - | {local(start)} | - "
"| Skills | Skills | | Operator, Pkgmgr | Operator |",
)
self.assertEqual(
lines[4], "gaps: work/b has 0 A; work/b has no key result; work/a has no key result"
)
def test_key_results_gate_done_and_fill_the_okr_column(self):
self._run("set", "work/a", "--objective", "Ship A", "--status", "running")
self._run("role", "work/a", "Skills", "RA")
self._run("kr", "work/a", "add", "tests green")
self._run("kr", "work/a", "add", "released")
self._run("kr", "work/a", "met", "1")
lines = self._out("show")
self.assertIn("| Ship A (1/2 KR) | running |", lines[2])
self.assertEqual(len(lines), 3)
self.assertEqual(self._run("set", "work/a", "--status", "done").returncode, 2)
self.assertIn("key result 2 [ ]: released", self._out("get", "work/a"))
self._run("kr", "work/a", "met", "2")
self.assertEqual(self._run("set", "work/a", "--status", "done").returncode, 0)
self.assertEqual(self._out("show"), ["no tasks"])
def test_status_sets_start_and_end(self):
self._run("set", "work/a", "--status", "running")
self._run("set", "work/a", "--status", "done", "--description", "long text")
lines = self._out("get", "work/a")
self.assertIn("status: done", lines)
self.assertIn("description: long text", lines)
self.assertTrue(any(line.startswith("start: 20") for line in lines))
self.assertTrue(any(line.startswith("end: 20") for line in lines))
self._run("set", "work/a", "--status", "running")
self.assertFalse(any(line.startswith("end:") for line in self._out("get", "work/a")))
def test_done_tasks_show_only_with_all(self):
self._run("set", "work/a", "--status", "done", "--end", "2026-11-01T12:00:00+00:00")
self._run("role", "work/a", "Skills", "RA")
self.assertEqual(self._out("show"), ["no tasks"])
self.assertIn("| done | - |", self._out("show", "--all")[2])
def test_artifacts_and_tools(self):
self._run("set", "work/a", "--workdir", "/w/a", "--branch", "feat/a", "--status", "done",
"--artifact", "image:core:dev", "--artifact", "volume:db",
"--tool", "council.py", "--tool", "make test")
self._run("set", "work/b", "--worker", "Two", "--tool", "make test", "--artifact", "dir:/tmp/b")
self._run("set", "work/a", "--drop-artifact", "volume:db")
self._run("set", "work/b", "--branch", "")
rows = [line.split("\t") for line in self._out("artifacts")]
self.assertEqual(
[row[:4] for row in rows],
[
["done", "work/a", "-", "workdir:/w/a"],
["done", "work/a", "-", "branch:feat/a"],
["done", "work/a", "-", "image:core:dev"],
["queued", "work/b", "Two", "dir:/tmp/b"],
],
)
self.assertEqual(rows[0][4], "-")
self.assertRegex(rows[2][4], r"^\d\d-\d\d \d\d:\d\d$")
self.assertEqual([r.split("\t")[1] for r in self._out("artifacts", "--ended")], ["work/a"] * 3)
self.assertEqual(len(self._out("artifacts", "--task", "work/b")), 1)
self.assertIn("| queued | Two | 1 |", self._out("show")[2])
self.assertTrue(any(line.startswith("artifact: image:core:dev (since ") for line in self._out("get", "work/a")))
self.assertEqual(
self._out("tools"), ["2\tmake test\twork/a work/b", "1\tcouncil.py\twork/a"]
)
self.assertIn("tool: council.py", self._out("get", "work/a"))
def test_claims_join_the_matrix_with_holder_as_ra(self):
self._claim("work/live", "s1", "One", "Gamma")
self._claim("cluster/dead", "s9", "Gone", "Delta")
self._claim("chair", "s1", "One", "allocator")
self._run("set", "work/live", "--prio", "1", "--status", "running")
self._run("role", "work/live", "Two", "S")
lines = self._out("show")
self.assertTrue(lines[2].startswith("| 1 | - | work/live | - | running | One |"))
self.assertTrue(lines[2].endswith("| One | One | Two | | |"))
self.assertTrue(lines[3].startswith("| 2 | - | cluster/dead | - | stale | Gone |"))
self.assertFalse(any("chair" in line for line in lines))
def test_council_claim_takeover_and_release_write_through(self):
self.assertEqual(self._run("claim", "work/x", "--work", "Do X", script=COUNCIL).returncode, 0)
lines = self._out("get", "work/x")
self.assertIn("status: running", lines)
self.assertIn("worker: One", lines)
self.assertIn("description: Do X", lines)
self.assertIn(f"workdir: {self.tmp.name}", lines)
self.assertIn("roles: One=RA", lines)
(self.sessions / "s1.json").unlink()
self.assertEqual(self._run("takeover", "work/x", "--work", "Do X", script=COUNCIL, sid="s2").returncode, 0)
lines = self._out("get", "work/x")
self.assertIn("worker: Two", lines)
self.assertIn("roles: Two=RA", lines)
self._run("kr", "work/x", "add", "X shipped")
self.assertEqual(self._run("release", "work/x", script=COUNCIL, sid="s2").returncode, 0)
lines = self._out("get", "work/x")
self.assertIn("status: stopped", lines)
self.assertTrue(any(line.startswith("end: 20") for line in lines))
def test_rm_and_clear_a_party(self):
self._run("set", "work/a", "--title", "Alpha")
self._run("role", "work/a", "Skills", "RA")
self._run("role", "work/a", "Pkgmgr", "C")
self._run("role", "work/a", "Pkgmgr", "-")
self.assertTrue(self._out("show")[2].endswith("| Skills | Skills | | | |"))
self.assertEqual(self._out("rm", "work/a"), ["removed work/a"])
self.assertEqual(self._out("show"), ["no tasks"])
def test_invalid_input_exits_2(self):
for args in (
("role", "work/a", "Skills", "RR"),
("role", "work/a", "Skills", "Q"),
("set", "Bad Task"),
("set", "work/a", "--prio", "0"),
("set", "work/a", "--status", "finished"),
("set", "work/a", "--start", "yesterday"),
("get", "work/none"),
("kr", "work/a", "met", "1"),
("set", "work/a", "--artifact", "no-kind"),
("estimate", "work/a", "One", "--value", "4", "--complexity", "2"),
("estimate", "work/a", "One", "--value", "8"),
):
with self.subTest(args=args):
self.assertEqual(self._run(*args).returncode, 2)
self.assertFalse((self.root / "rasci.json").exists())
def test_pipe_in_a_title_does_not_break_the_table(self):
self._run("set", "work/a", "--title", "a | b")
self._run("role", "work/a", "Skills", "RA")
self.assertTrue(self._out("show")[2].startswith("| 1 | - | work/a: a \\| b | - | queued |"))
def test_wsjf_ranks_by_median_estimates(self):
self._run("set", "work/a", "--objective", "Ship A")
self._run("kr", "work/a", "add", "released")
self._run("estimate", "work/a", "One", "--value", "8", "--complexity", "2")
self._run("estimate", "work/a", "Two", "--value", "13", "--complexity", "3")
self._run("estimate", "work/b", "One", "--value", "5", "--complexity", "5")
self._run("set", "work/c", "--title", "Gamma")
lines = self._out("wsjf")
self.assertEqual(
lines[0], "| Rank | Task | OKR | Business value | Complexity | WSJF | Estimators | Status |"
)
self.assertEqual(lines[2], "| 1 | work/a | Ship A (0/1 KR) | 10.5 | 2.5 | 4.2 | One, Two | queued |")
self.assertEqual(lines[3], "| 2 | work/b | - | 5 | 5 | 1 | One | queued |")
self.assertEqual(lines[4], "| 3 | work/c | - | - | - | - | - | queued |")
self.assertEqual(lines[5], "unestimated: work/c")
self.assertTrue(self._out("show")[2].startswith("| 1 | 4.2 | work/a |"))
self.assertIn("wsjf: 4.2", self._out("get", "work/a"))
self._run("set", "work/c", "--prio", "1")
self.assertTrue(self._out("show")[2].startswith("| 1 | - | work/c: Gamma |"))
self._run("estimate", "work/b", "One", "--drop")
self.assertFalse(any(line.startswith("estimate ") for line in self._out("get", "work/b")))
class TestOrgSkillsUseTheRegister(unittest.TestCase):
def test_every_org_skill_names_the_register(self):
for skill in sorted((REPO_ROOT / "skills").glob("kvb-org-*/SKILL.md")):
if skill.parent.name == "kvb-org-rasci":
continue
with self.subTest(skill=skill.parent.name):
self.assertIn("`kvb-org-rasci`", skill.read_text(encoding="utf-8"))
if __name__ == "__main__":
unittest.main()
-59
View File
@@ -1,59 +0,0 @@
"""Validate the shortcuts skill table: rows present, sorted, ``soc``-prefixed,
no two adjacent consonants."""
from __future__ import annotations
import re
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SKILL = REPO_ROOT / "skills" / "shortcuts" / "SKILL.md"
ROW_RE = re.compile(r"^\|\s*`(?P<shortcut>[^`]+)`\s*\|")
VOWELS = frozenset("aeiou")
def _has_consonant_cluster(name: str) -> bool:
prev_consonant = False
for char in name:
if not char.isalpha():
prev_consonant = False
continue
is_consonant = char.lower() not in VOWELS
if is_consonant and prev_consonant:
return True
prev_consonant = is_consonant
return False
class TestShortcuts(unittest.TestCase):
def setUp(self):
self.assertTrue(SKILL.is_file(), f"missing {SKILL}")
self.shortcuts = [
match.group("shortcut")
for line in SKILL.read_text(encoding="utf-8").splitlines()
if (match := ROW_RE.match(line))
]
def test_shortcuts_present(self):
self.assertTrue(self.shortcuts, "no shortcut rows found in shortcuts skill")
def test_shortcuts_sorted_ascending(self):
self.assertEqual(
self.shortcuts,
sorted(self.shortcuts),
f"shortcuts table not sorted; expected {sorted(self.shortcuts)}",
)
def test_shortcuts_start_with_so(self):
bad = [s for s in self.shortcuts if not s.startswith("so")]
self.assertFalse(bad, f"shortcuts not starting with 'so': {bad}")
def test_no_consecutive_consonants(self):
bad = [s for s in self.shortcuts if _has_consonant_cluster(s)]
self.assertFalse(bad, f"shortcuts with two adjacent consonants: {bad}")
if __name__ == "__main__":
unittest.main()
+120
View File
@@ -0,0 +1,120 @@
"""Exercise legacy.py: renames chained through history, deletions, project and unknown skills."""
from __future__ import annotations
import json
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPT = REPO_ROOT / "skills" / "kvb-skill-cleanup" / "scripts" / "legacy.py"
@unittest.skipUnless(shutil.which("git"), "git not installed")
class TestLegacy(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
base = Path(self.tmp.name)
self.env = {
"PATH": "/usr/bin:/bin",
"HOME": str(base),
"GIT_AUTHOR_NAME": "t",
"GIT_AUTHOR_EMAIL": "t@t",
"GIT_COMMITTER_NAME": "t",
"GIT_COMMITTER_EMAIL": "t@t",
}
self.repo = base / "skills-repo"
self._skill(self.repo / "skills" / "old")
(self.repo / "skills-lock.json").write_text(
json.dumps({"skills": {"caveman": {}}}), encoding="utf-8"
)
self._git(self.repo, "init", "-q", "-b", "main")
self._commit(self.repo, "add old")
self._git(self.repo, "mv", "skills/old", "skills/mid")
self._commit(self.repo, "rename old to mid")
self._git(self.repo, "mv", "skills/mid", "skills/kvb-x-new")
self._commit(self.repo, "rename mid to kvb-x-new")
self._skill(self.repo / "skills" / "gone")
self._commit(self.repo, "add gone")
self._git(self.repo, "rm", "-q", "-r", "skills/gone")
self._commit(self.repo, "remove gone")
self.home = base / "home" / ".claude" / "skills"
for name in ("old", "mid", "gone", "kvb-x-new", "caveman", "mine"):
self._skill(self.home / name)
(self.home / "notaskill").mkdir()
self.project = base / "project"
self._skill(self.project / "skills" / "p-own")
self._git(self.project, "init", "-q", "-b", "main")
self._commit(self.project, "own skill")
self.installed = self.project / ".claude" / "skills"
for name in ("p-own", "old"):
self._skill(self.installed / name)
def _skill(self, path: Path) -> None:
path.mkdir(parents=True, exist_ok=True)
(path / "SKILL.md").write_text(
f"---\nname: {path.name}\ndescription: probe\n---\n\nBody of the probe skill.\n",
encoding="utf-8",
)
def _git(self, cwd: Path, *args: str) -> None:
subprocess.run(["git", *args], check=True, capture_output=True, env=self.env, cwd=cwd)
def _commit(self, cwd: Path, message: str) -> None:
self._git(cwd, "add", "-A")
self._git(cwd, "commit", "-q", "-m", message)
def _run(self, *args: str) -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, str(SCRIPT), *args],
check=False,
capture_output=True,
text=True,
env=self.env,
)
def test_every_installed_skill_gets_its_verdict(self):
result = self._run(
"--skills-repo", str(self.repo),
"--root", str(self.home),
"--root", str(self.installed),
"--json",
)
self.assertEqual(result.returncode, 0, result.stderr)
home, project = str(self.home), str(self.installed)
rows = sorted(
(str(Path(row["path"]).parent), Path(row["path"]).name, row["verdict"], row["successor"])
for row in json.loads(result.stdout)
)
self.assertEqual(
rows,
sorted(
[
(home, "caveman", "current", None),
(home, "gone", "removed", None),
(home, "kvb-x-new", "current", None),
(home, "mid", "legacy", "kvb-x-new"),
(home, "mine", "unknown", None),
(home, "old", "legacy", "kvb-x-new"),
(project, "old", "legacy", "kvb-x-new"),
(project, "p-own", "project", None),
]
),
)
def test_invalid_input_exits_2(self):
missing = self._run("--skills-repo", str(self.repo), "--root", str(self.home / "nope"))
self.assertEqual(missing.returncode, 2)
no_repo = self._run("--skills-repo", str(self.home), "--root", str(self.home))
self.assertEqual(no_repo.returncode, 2)
if __name__ == "__main__":
unittest.main()
Loaded 100 of 106 files, more files were not shown because too many files have changed in this diff. Show more