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>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-03 13:57:14 +01:00
1 parent c47393a73b
commit 5c30103b43
18 files changed
+327 -293

No files matched your search

+19 -10
View File
@@ -9,7 +9,7 @@ Compatible with Claude Code, Codex, Gemini CLI, Cursor, Copilot, Windsurf, Cline
## Naming 🏷️
Every first-party skill is named `kvb-<category>-<name>` 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-<category>-<name>` 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 (`<repo>/.agents/skills` and `<repo>/.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.
+18 -5
View File
@@ -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) {
+13
View File
@@ -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."
+3 -4
View File
@@ -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
@@ -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 <task>` 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
+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
+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.
+1 -1
View File
@@ -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.
-36
View File
@@ -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
+2 -3
View File
@@ -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
+6 -6
View File
@@ -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
-29
View File
@@ -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
+4 -83
View File
@@ -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()
+113 -82
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
@@ -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)
+3 -1
View File
@@ -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)