From 5c30103b4344723c9451b0948178697bd7774dbd Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 3 Nov 2026 13:43:15 +0100 Subject: [PATCH] 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 --- README.md | 29 ++- scripts/enable-plugins.js | 23 ++- scripts/install.sh | 13 ++ skills/kvb-check-legal/SKILL.md | 7 +- .../{kvb-skill-dream => kvb-dream}/SKILL.md | 77 ++++--- skills/kvb-dream/hooks/notify.sh | 8 + skills/kvb-dream/hooks/reminder.sh | 34 +++ .../systemd/kvb-dream-notify.service | 7 + .../kvb-dream/systemd/kvb-dream-notify.timer | 9 + skills/kvb-optimize/SKILL.md | 43 ++++ skills/kvb-org-rasci/SKILL.md | 2 +- skills/kvb-skill-dream/hooks/reminder.sh | 36 ---- skills/kvb-skill-essence/SKILL.md | 5 +- skills/kvb-skill-tune/SKILL.md | 12 +- skills/kvb-skill-tune/hooks/reminder.sh | 29 --- tests/test_autotune.py | 87 +------- tests/test_dream.py | 195 ++++++++++-------- tests/test_naming.py | 4 +- 18 files changed, 327 insertions(+), 293 deletions(-) rename skills/{kvb-skill-dream => kvb-dream}/SKILL.md (63%) create mode 100755 skills/kvb-dream/hooks/notify.sh create mode 100755 skills/kvb-dream/hooks/reminder.sh create mode 100644 skills/kvb-dream/systemd/kvb-dream-notify.service create mode 100644 skills/kvb-dream/systemd/kvb-dream-notify.timer create mode 100644 skills/kvb-optimize/SKILL.md delete mode 100755 skills/kvb-skill-dream/hooks/reminder.sh delete mode 100755 skills/kvb-skill-tune/hooks/reminder.sh diff --git a/README.md b/README.md index f3e3d70..bf24b74 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Compatible with Claude Code, Codex, Gemini CLI, Cursor, Copilot, Windsurf, Cline ## Naming 🏷️ -Every first-party skill is named `kvb--` with one of these categories: `think`, `create`, `code`, `ci`, `git`, `check`, `org`, `skill`. Only `kvb-cleanup`, `kvb-help` and the collector `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. +Every first-party skill is named `kvb--` with one of these categories: `think`, `create`, `code`, `ci`, `git`, `check`, `org`, `skill`. Only `kvb-cleanup`, `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 🏛️ @@ -65,7 +65,6 @@ flowchart LR org --> org_wsjf[kvb-org-wsjf] org --> org_robot[kvb-org-robot] org --> org_operator[kvb-org-operator] - skill --> skill_dream[kvb-skill-dream] skill --> skill_tune[kvb-skill-tune] skill --> skill_auto[kvb-skill-auto] skill --> skill_essence[kvb-skill-essence] @@ -75,6 +74,8 @@ flowchart LR none --> cleanup[kvb-cleanup] none --> help[kvb-help] none --> transcend_all[kvb-transcend] + none --> dream_all[kvb-dream] + none --> optimize_all[kvb-optimize] ``` ## Skill map 🗺️ @@ -87,7 +88,7 @@ flowchart LR commit_req([commit request]) stage_req([staging or fix ready]) edit_hook([edit in a git worktree]) - reminders([reminder hooks]) + reminders([12 h agent hint, 17:00 GNOME popup]) code_task([code task]) spawn([subagent spawn]) end @@ -144,7 +145,6 @@ flowchart LR operator[kvb-org-operator] end subgraph cat_skill [skill] - dream[kvb-skill-dream] autotune[kvb-skill-tune] autoskill[kvb-skill-auto] essence[kvb-skill-essence] @@ -156,6 +156,8 @@ flowchart LR cleanup[kvb-cleanup] help_skills[kvb-help] transcend[kvb-transcend] + dream[kvb-dream] + optimize[kvb-optimize] end commit_req -.-> commit_next @@ -163,7 +165,6 @@ flowchart LR stage_req -.-> confidence edit_hook -. council gate .-> democracy reminders -.-> dream - reminders -.-> autotune code_task -.-> no_defaults code_task -.-> scope_guard spawn -.-> subagent_model @@ -227,18 +228,26 @@ flowchart LR robot --> dialectic dream --> dialectic - dream --> autotune - dream --> autoskill + dream --> optimize + dream --> cleanup + dream --> branch_gc + optimize --> skill_cleanup + optimize --> transcend + optimize --> autoskill + optimize --> autotune + optimize --> compile + optimize --> essence + optimize --> refactor + optimize --> tune + optimize --> gaps autoskill --> autotune autotune --> essence - dream --> transcend transcend --> skill_transcend transcend --> code_transcend skill_transcend --> active_listening skill_transcend --> autotune code_transcend --> active_listening autotune -. own scripts .-> skill_transcend - dream --> compile autotune -. deterministic prose .-> compile autoskill -. deterministic steps .-> compile skill_transcend -. deterministic moves .-> compile @@ -274,7 +283,7 @@ Copy the skills into a specific project (`/.agents/skills` and `/.cl 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 — `kvb-skill-tune` points you at `/kvb-skill-tune` at most once an hour, `kvb-skill-dream` points you at `/kvb-skill-dream` at most once every twelve hours per repository (stamped in that repository's git dir). 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. +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 one reminder hook is registered — `kvb-dream` points each agent at `/kvb-dream` at most once every twelve hours (stamped per session in `~/.claude/.dream-stamps/`); 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. diff --git a/scripts/enable-plugins.js b/scripts/enable-plugins.js index 021f62a..11b31ec 100644 --- a/scripts/enable-plugins.js +++ b/scripts/enable-plugins.js @@ -15,12 +15,16 @@ const MARKETPLACES = { ponytail: { source: { source: "github", repo: "DietrichGebert/ponytail" } }, }; const PLUGINS = { "caveman@caveman": true, "ponytail@ponytail": true }; -const REMINDERS = ["kvb-skill-tune", "kvb-skill-dream"]; +const REMINDERS = ["kvb-dream"]; +const RETIRED = [ + "autotune/hooks/reminder.sh", + "kvb-skills-tune/hooks/reminder.sh", + "kvb-skill-tune/hooks/reminder.sh", +]; const RENAMED = { - "autotune/hooks/reminder.sh": "kvb-skill-tune/hooks/reminder.sh", - "dream/hooks/reminder.sh": "kvb-skill-dream/hooks/reminder.sh", - "kvb-skills-tune/hooks/reminder.sh": "kvb-skill-tune/hooks/reminder.sh", - "kvb-skills-dream/hooks/reminder.sh": "kvb-skill-dream/hooks/reminder.sh", + "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", @@ -46,6 +50,15 @@ for (const [name, entry] of Object.entries(MARKETPLACES)) { data.enabledPlugins = Object.assign(data.enabledPlugins || {}, PLUGINS); 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) { diff --git a/scripts/install.sh b/scripts/install.sh index ccc5860..8ba0a6a 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -53,4 +53,17 @@ else warn "skills: node not found; skipped auto-enabling caveman/ponytail plugins." 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." diff --git a/skills/kvb-check-legal/SKILL.md b/skills/kvb-check-legal/SKILL.md index b4ddf05..b19232b 100644 --- a/skills/kvb-check-legal/SKILL.md +++ b/skills/kvb-check-legal/SKILL.md @@ -1,10 +1,9 @@ --- name: kvb-check-legal description: > - Check a commit for legal soundness: licence compatibility of added code and - dependencies, attribution, copied snippets, GDPR, trademarks. Run - AUTOMATICALLY before every commit the operator requests; also on /kvb-check-legal - [ref|range], "rechtlich prüfen", "lizenz prüfen". + Check a commit's licences, attribution, copied snippets, GDPR and + trademarks. Run AUTOMATICALLY before every commit the operator requests; + also on /kvb-check-legal [ref|range], "rechtlich prüfen", "lizenz prüfen". --- The stance is the project's counsel: find what would make the change unlawful diff --git a/skills/kvb-skill-dream/SKILL.md b/skills/kvb-dream/SKILL.md similarity index 63% rename from skills/kvb-skill-dream/SKILL.md rename to skills/kvb-dream/SKILL.md index a6d6a68..bba4f06 100644 --- a/skills/kvb-skill-dream/SKILL.md +++ b/skills/kvb-dream/SKILL.md @@ -1,20 +1,20 @@ --- -name: kvb-skill-dream +name: kvb-dream description: > - Consolidate the last 24 hours of this repository (what held, what broke) - into memory and proposals. Trigger on /kvb-skill-dream or a request to - learn from recent work. + Process the last 24 hours of every repository and session: memory, + improvements, kvb-optimize, cleanups. Trigger on /kvb-dream or a request + to learn from recent work. --- -Sleep on the work: replay what happened in this repository, separate what held -from what broke, and keep only the conclusions that survive an attack. A dream -that flatters the dreamer is worthless — an unverified success counts as -unknown, not as a win. +Sleep on the work: replay what happened, separate what held from what broke, +keep only the conclusions that survive an attack, then optimize and clean up. +Dreaming processes everything. A dream that flatters the dreamer is worthless — +an unverified success counts as unknown, not as a win. ## Objective -The dream exists to raise the confidence of every future run in this repository -until the mistakes it found cannot happen again. Judge every finding, memory +The dream exists to raise the confidence of every future run until the +mistakes it found cannot happen again. Judge every finding, memory 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 not worth recording. @@ -27,18 +27,23 @@ confidence the next run can now hold on each closed class, calibrated as the ## Scope -Everything is read from the repository the skill runs in (`git rev-parse ---show-toplevel`); never mix in another project's history or memory. The default -window is the last 24 hours — the operator may widen it (`/kvb-skill-dream 7d`). State the -repository and the window in one line before you start. +Every repository and session this agent touched in the window: the +repositories named by its session transcripts (working directories and +worktrees), by the workdirs of its `kvb-org-rasci` register tasks, and the +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 -(`hooks/reminder.sh`, stamped per repository in its git dir) is a hint for the -operator, not a trigger: print it and continue with their actual request. +Run only when the operator asks. Two hints point at it, neither is a trigger: +`hooks/reminder.sh` tells each agent at most once every twelve hours (stamped +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 -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 `--stat`; the reflog carries the resets, rebases, and amends the log hides. @@ -50,7 +55,7 @@ Gather the window's actual record, not your recollection of it: - **Register**: the `kvb-org-rasci` register: `rasci.py tools` ranks the scripts and tools the window's tasks ran, the refactoring candidates; `get ` shows a task's objective, roles, workdir and artifacts. -- **Memory**: this project's memory directory and its `MEMORY.md` index — the +- **Memory**: each project's memory directory and its `MEMORY.md` index — the findings already recorded, so the dream extends them instead of repeating them. - **Instructions**: `CLAUDE.md`, `AGENTS.md`, and the project's own skills, to see which rule a mistake actually violated. @@ -86,7 +91,7 @@ the next dream does not re-derive it. ## 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 of adding a duplicate, and delete the memories this run disproved. Absolute dates, never "yesterday". Nothing that the repository already records — code @@ -96,7 +101,7 @@ structure, git history, `CLAUDE.md` — belongs in memory. 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. - **Agent instructions** — the missing or wrong rule in `CLAUDE.md`, `AGENTS.md`, the project's own skills, or the contributor docs. Every mistake @@ -108,18 +113,24 @@ 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 improvement. No repository file changes before an answer comes back. -## Step 7: kvb-skill-tune, kvb-skill-auto, kvb-transcend, kvb-skill-compile +## Step 7: optimize -With the dialectic done and memory written, run `kvb-skill-tune` — hand it this run's -findings as its evidence so it does not re-collect them — then `kvb-skill-auto` -for the operator corrections the window revealed, `kvb-transcend` for the -scripts and tests the window's sessions wrote outside a repository, and -`kvb-skill-compile` for the skill steps the window's sessions worked through -by hand although they are deterministic. All keep -their own confirmation steps; the dream never writes a skill on their behalf. +With the dialectic done and memory written, run the `kvb-optimize` skill and +hand it this run's findings as its evidence, so none of its skills +re-collects them. -Close the run by refreshing this repository's reminder stamp, so the next hint -is twelve hours out: `date +%s > "$(git rev-parse --path-format=absolute --git-common-dir)/.dream-stamp"`. +## Step 8: clean up + +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 @@ -129,8 +140,8 @@ is twelve hours out: `date +%s > "$(git rev-parse --path-format=absolute --git-c attempt even when the final state is green. - Read-only until step 6: the dream inspects, and writes memory; repository code changes only on an explicit radio answer. -- An empty window is a valid result — report "nothing in the last 24 hours in - this repository" and stop. Never invent activity to have something to dream +- An empty window is a valid result — report "nothing in the last 24 hours" per + repository and skip it. Never invent activity to have something to dream about. - 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 diff --git a/skills/kvb-dream/hooks/notify.sh b/skills/kvb-dream/hooks/notify.sh new file mode 100755 index 0000000..13fecd9 --- /dev/null +++ b/skills/kvb-dream/hooks/notify.sh @@ -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." diff --git a/skills/kvb-dream/hooks/reminder.sh b/skills/kvb-dream/hooks/reminder.sh new file mode 100755 index 0000000..c361790 --- /dev/null +++ b/skills/kvb-dream/hooks/reminder.sh @@ -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 diff --git a/skills/kvb-dream/systemd/kvb-dream-notify.service b/skills/kvb-dream/systemd/kvb-dream-notify.service new file mode 100644 index 0000000..aba5069 --- /dev/null +++ b/skills/kvb-dream/systemd/kvb-dream-notify.service @@ -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 diff --git a/skills/kvb-dream/systemd/kvb-dream-notify.timer b/skills/kvb-dream/systemd/kvb-dream-notify.timer new file mode 100644 index 0000000..b0109a5 --- /dev/null +++ b/skills/kvb-dream/systemd/kvb-dream-notify.timer @@ -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 diff --git a/skills/kvb-optimize/SKILL.md b/skills/kvb-optimize/SKILL.md new file mode 100644 index 0000000..db61b29 --- /dev/null +++ b/skills/kvb-optimize/SKILL.md @@ -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. diff --git a/skills/kvb-org-rasci/SKILL.md b/skills/kvb-org-rasci/SKILL.md index 2afe8fa..3a25b17 100644 --- a/skills/kvb-org-rasci/SKILL.md +++ b/skills/kvb-org-rasci/SKILL.md @@ -53,7 +53,7 @@ published). `done` is refused while one is open; a task ended short of them is 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-skill-dream`. + 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. diff --git a/skills/kvb-skill-dream/hooks/reminder.sh b/skills/kvb-skill-dream/hooks/reminder.sh deleted file mode 100755 index 8952ffd..0000000 --- a/skills/kvb-skill-dream/hooks/reminder.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -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 kvb-skill-dream, do not start replaying the history, do -not ask questions about it. - - 💤 /kvb-skill-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 diff --git a/skills/kvb-skill-essence/SKILL.md b/skills/kvb-skill-essence/SKILL.md index 2558976..5acfe27 100644 --- a/skills/kvb-skill-essence/SKILL.md +++ b/skills/kvb-skill-essence/SKILL.md @@ -1,9 +1,8 @@ --- name: kvb-skill-essence description: > - Compact skills and memory to their essence after a health pass that finds - dead weight. Trigger on /kvb-skill-essence or when skills or MEMORY.md grow too long, - duplicate, or eat too much context. + Compact skills and memory after a health pass for dead weight. Trigger on + /kvb-skill-essence or when skills or MEMORY.md grow too long or duplicate. --- Shorter, not thinner. Every fact that still earns its place survives; what goes diff --git a/skills/kvb-skill-tune/SKILL.md b/skills/kvb-skill-tune/SKILL.md index 432fefc..5f76902 100644 --- a/skills/kvb-skill-tune/SKILL.md +++ b/skills/kvb-skill-tune/SKILL.md @@ -1,9 +1,9 @@ --- name: kvb-skill-tune description: > - Generate new skills or optimize existing ones (fewer tokens and errors, - persistent learnings), each proposal confirmed. Trigger ONLY on an explicit - request: /kvb-skill-tune, "autotune", "tune the skills". + Generate or sharpen skills from observed friction, each proposal confirmed. + Trigger only on /kvb-skill-tune, "autotune", "tune the skills" or through + kvb-optimize. --- Autotune turns observed friction into durable skills. It runs on demand only, @@ -43,9 +43,9 @@ it moves is not an optimization, it is a preference. ## Trigger discipline - Run **only** when the operator asks: `/kvb-skill-tune`, "autotune", "tune the - skills", or an equivalent instruction. -- The 60-minute reminder hook is a hint for the operator, not a trigger. When it - fires, print the hint and continue the operator's actual request. + skills", or an equivalent instruction, or as part of the `kvb-optimize` + skill the operator started (directly or through `kvb-dream`). +- 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. ## Step 1: collect evidence diff --git a/skills/kvb-skill-tune/hooks/reminder.sh b/skills/kvb-skill-tune/hooks/reminder.sh deleted file mode 100755 index 817a2a8..0000000 --- a/skills/kvb-skill-tune/hooks/reminder.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -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 -kvb-skill-tune, do not propose skills, do not ask questions about it. - - 💡 /kvb-skill-tune - 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 diff --git a/tests/test_autotune.py b/tests/test_autotune.py index c4acc6e..62d7649 100644 --- a/tests/test_autotune.py +++ b/tests/test_autotune.py @@ -1,33 +1,12 @@ -"""Validate the kvb-skill-tune reminder hook and its settings registration.""" +"""Validate the kvb-skill-tune skill text and its persistence targets.""" from __future__ import annotations -import json -import shutil -import subprocess -import tempfile import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] SKILL = REPO_ROOT / "skills" / "kvb-skill-tune" / "SKILL.md" -HOOK = REPO_ROOT / "skills" / "kvb-skill-tune" / "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): @@ -36,6 +15,9 @@ class TestAutotuneSkill(unittest.TestCase): self.assertIn("name: kvb-skill-tune", 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): """Both skill writers must persist to the skills repository AND the live dirs.""" @@ -58,66 +40,5 @@ class TestSkillPersistence(unittest.TestCase): 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("/kvb-skill-tune", 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("/kvb-skill-tune", _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("/kvb-skill-tune", _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("kvb-skill-tune/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__": unittest.main() diff --git a/tests/test_dream.py b/tests/test_dream.py index 0475762..895474e 100644 --- a/tests/test_dream.py +++ b/tests/test_dream.py @@ -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 @@ -10,101 +10,124 @@ import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] -SKILL = REPO_ROOT / "skills" / "kvb-skill-dream" / "SKILL.md" -HOOK = REPO_ROOT / "skills" / "kvb-skill-dream" / "hooks" / "reminder.sh" +DREAM = REPO_ROOT / "skills" / "kvb-dream" +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" -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): - def test_skill_is_trigger_only(self): + def test_skill_runs_optimize_and_the_cleanups(self): text = SKILL.read_text(encoding="utf-8") - self.assertIn("name: kvb-skill-dream", text) + self.assertIn("name: kvb-dream", 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): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.addCleanup(self.tmp.cleanup) - self.cwd = Path(self.tmp.name) - self.stamp = self.cwd / ".claude" / ".dream-stamp" + self.stamps = Path(self.tmp.name) / "stamps" - def test_first_run_hints_and_stamps(self): - result = _run_hook(self.cwd, self.stamp) - self.assertIn("/kvb-skill-dream", result.stdout) - self.assertTrue(self.stamp.is_file()) + def _run(self, payload: str, interval: str = "43200") -> str: + return subprocess.run( + ["bash", str(HOOK)], + 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): - _run_hook(self.cwd, self.stamp) - self.assertEqual(_run_hook(self.cwd, self.stamp).stdout, "") + @staticmethod + def _session(sid: str) -> str: + 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): - _run_hook(self.cwd, self.stamp) - self.assertIn("/kvb-skill-dream", _run_hook(self.cwd, self.stamp, interval="0").stdout) + self._run(self._session("a1")) + self.assertIn("/kvb-dream", self._run(self._session("a1"), interval="0")) 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("/kvb-skill-dream", _run_hook(self.cwd, self.stamp).stdout) + self.stamps.mkdir() + (self.stamps / "a1").write_text("not-a-timestamp\n", encoding="utf-8") + self.assertIn("/kvb-dream", self._run(self._session("a1"))) - def test_outside_a_repository_it_stays_silent(self): - self.assertEqual(_run_hook(self.cwd).stdout, "") + def test_without_a_valid_session_it_stays_silent(self): + 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): - for name in ("one", "two"): - repo = self.cwd / name - repo.mkdir() - subprocess.run( - ["git", "init", "-q", str(repo)], check=True, capture_output=True - ) - self.assertIn("/kvb-skill-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_linked_worktrees_share_the_stamp(self): - repo = self.cwd / "main" - tree = self.cwd / "tree" - env = { - "PATH": "/usr/bin:/bin", - "HOME": str(self.cwd), - "GIT_AUTHOR_NAME": "t", - "GIT_AUTHOR_EMAIL": "t@t", - "GIT_COMMITTER_NAME": "t", - "GIT_COMMITTER_EMAIL": "t@t", - } - repo.mkdir() - 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("/kvb-skill-dream", _run_hook(repo).stdout) - self.assertEqual(_run_hook(tree).stdout, "") +class TestDreamNotification(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + 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( + ["bash", str(NOTIFY)], + capture_output=True, + check=True, + env={"PATH": f"{self.bin}:/usr/bin:/bin", "XDG_CURRENT_DESKTOP": desktop}, + ) + + def test_gnome_session_gets_the_popup(self): + self._run("ubuntu:GNOME") + self.assertIn("/kvb-dream", self.calls.read_text(encoding="utf-8")) + + def test_other_desktops_stay_silent(self): + self._run("KDE") + self.assertFalse(self.calls.exists()) + + def test_timer_fires_daily_at_five_pm_and_catches_up(self): + timer = TIMER.read_text(encoding="utf-8") + self.assertIn("OnCalendar=*-*-* 17:00:00", timer) + self.assertIn("Persistent=true", timer) + self.assertIn("kvb-dream/hooks/notify.sh", SERVICE.read_text(encoding="utf-8")) @unittest.skipUnless(shutil.which("node"), "node not installed") @@ -124,8 +147,8 @@ class TestSettingsRegistration(unittest.TestCase): self._patch() data = self._patch() entries = json.dumps(data["hooks"]["UserPromptSubmit"]) - self.assertEqual(entries.count("kvb-skill-dream/hooks/reminder.sh"), 1) - self.assertEqual(entries.count("kvb-skill-tune/hooks/reminder.sh"), 1) + self.assertEqual(entries.count("kvb-dream/hooks/reminder.sh"), 1) + self.assertNotIn("tune/hooks/reminder.sh", entries) gate = json.dumps(data["hooks"]["PreToolUse"]) self.assertEqual(gate.count("kvb-org-organize/scripts/council.py"), 1) self.assertEqual(gate.count("kvb-org-organize/scripts/watch.py"), 1) @@ -135,7 +158,15 @@ class TestSettingsRegistration(unittest.TestCase): self.assertEqual(gate.count("kvb-org-perform/scripts/log.py"), 1) self.assertEqual(post.count("kvb-org-perform/scripts/log.py"), 1) - def test_hooks_of_renamed_skills_are_repointed(self): + 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): @@ -147,7 +178,8 @@ class TestSettingsRegistration(unittest.TestCase): "hooks": { "UserPromptSubmit": [ hook(f'bash "{base}/autotune/hooks/reminder.sh"'), - hook(f'bash "{base}/kvb-skills-dream/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"), ], @@ -167,12 +199,11 @@ class TestSettingsRegistration(unittest.TestCase): ) data = self._patch() text = json.dumps(data["hooks"]) - for old in ("/autotune/", "/kvb-skills-dream/", "/democracy/", "/subagent-model/"): + 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-skill-dream/hooks/reminder.sh"), 1) - self.assertEqual(entries.count("kvb-skill-tune/hooks/reminder.sh"), 1) + 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) diff --git a/tests/test_naming.py b/tests/test_naming.py index 1884fca..4741ca0 100644 --- a/tests/test_naming.py +++ b/tests/test_naming.py @@ -10,7 +10,9 @@ 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") -UNCATEGORIZED = frozenset({"kvb-cleanup", "kvb-help", "kvb-transcend"}) +UNCATEGORIZED = frozenset( + {"kvb-cleanup", "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)