From 735bbc9b7726f37404a9daf42df5e2a7a9926cad Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 3 Nov 2026 15:45:57 +0100 Subject: [PATCH] feat(kvb-coach): run the short coach at the first prompt of every full hour 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 --- README.md | 4 +++- scripts/enable-plugins.js | 2 +- skills/kvb-coach/SKILL.md | 5 +++++ skills/kvb-coach/hooks/reminder.sh | 15 +++++++++++++++ tests/test_coach.py | 28 ++++++++++++++++++++++++++++ tests/test_dream.py | 1 + 6 files changed, 53 insertions(+), 2 deletions(-) create mode 100755 skills/kvb-coach/hooks/reminder.sh diff --git a/README.md b/README.md index 76ab9ea..83f2eb0 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ flowchart LR stage_req([staging or fix ready]) edit_hook([edit in a git worktree]) reminders([12 h agent hint, 17:00 GNOME popup]) + full_hour([full hour]) code_task([code task]) spawn([subagent spawn]) end @@ -172,6 +173,7 @@ flowchart LR stage_req -.-> confidence edit_hook -. council gate .-> democracy reminders -.-> dream + full_hour -.-> coach code_task -.-> no_defaults code_task -.-> scope_guard spawn -.-> subagent_model @@ -295,7 +297,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 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. +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. diff --git a/scripts/enable-plugins.js b/scripts/enable-plugins.js index 11b31ec..5b50e7d 100644 --- a/scripts/enable-plugins.js +++ b/scripts/enable-plugins.js @@ -15,7 +15,7 @@ const MARKETPLACES = { ponytail: { source: { source: "github", repo: "DietrichGebert/ponytail" } }, }; const PLUGINS = { "caveman@caveman": true, "ponytail@ponytail": true }; -const REMINDERS = ["kvb-dream"]; +const REMINDERS = ["kvb-dream", "kvb-coach"]; const RETIRED = [ "autotune/hooks/reminder.sh", "kvb-skills-tune/hooks/reminder.sh", diff --git a/skills/kvb-coach/SKILL.md b/skills/kvb-coach/SKILL.md index 81181ee..e7f0066 100644 --- a/skills/kvb-coach/SKILL.md +++ b/skills/kvb-coach/SKILL.md @@ -21,6 +21,11 @@ coach.py --profile show [--json] Areas: `psyche`, `body`, `sleep`, `food`, `tasks`, `delegate`. Every coaching skill beyond this one is named `kvb-coach-`. +`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 diff --git a/skills/kvb-coach/hooks/reminder.sh b/skills/kvb-coach/hooks/reminder.sh new file mode 100755 index 0000000..180e012 --- /dev/null +++ b/skills/kvb-coach/hooks/reminder.sh @@ -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 diff --git a/tests/test_coach.py b/tests/test_coach.py index a8a7dbe..7a95634 100644 --- a/tests/test_coach.py +++ b/tests/test_coach.py @@ -11,6 +11,7 @@ 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): @@ -62,5 +63,32 @@ class TestCoach(unittest.TestCase): 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() diff --git a/tests/test_dream.py b/tests/test_dream.py index 895474e..900c24f 100644 --- a/tests/test_dream.py +++ b/tests/test_dream.py @@ -148,6 +148,7 @@ class TestSettingsRegistration(unittest.TestCase): data = self._patch() entries = json.dumps(data["hooks"]["UserPromptSubmit"]) self.assertEqual(entries.count("kvb-dream/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"]) self.assertEqual(gate.count("kvb-org-organize/scripts/council.py"), 1)