From 168a307e4fbe7d66f0d7a6570a02ffa1553962e9 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Sun, 2 Aug 2026 19:34:05 +0200 Subject: [PATCH] feat(dream): add reflection skill and 12h reminder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replay the last 24h of a repository — git log and reflog, CI, reviews, memory, session transcripts — score every action as held, broke or unknown against its evidence, harden the resulting hypotheses with dialectic, and persist what survives. The objective is a measurably more confident next run: a class of error counts as closed only once a test, check, hook or written rule prevents it. Derived improvements to the code and to the agent instructions are offered as single-select questions, and the run ends in autotune and autoskill. The reminder hook stamps into each repository's git dir, so every repo carries its own twelve-hour cadence and the hook stays silent outside a git repository. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 2 +- scripts/enable-plugins.js | 14 ++-- skills/dream/SKILL.md | 141 +++++++++++++++++++++++++++++++++ skills/dream/hooks/reminder.sh | 40 ++++++++++ tests/test_autotune.py | 2 +- tests/test_dream.py | 102 ++++++++++++++++++++++++ 6 files changed, 293 insertions(+), 8 deletions(-) create mode 100644 skills/dream/SKILL.md create mode 100755 skills/dream/hooks/reminder.sh create mode 100644 tests/test_dream.py diff --git a/README.md b/README.md index f1d9a57..85e9bfb 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,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.json`: the caveman and ponytail plugins are enabled so their modes auto-activate on session start, and the `autotune` reminder hook is registered so the agent points you at `/autotune` at most once an hour. Existing settings are preserved. +Both commands also patch the target's `.claude/settings.json`: 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). 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 2c06153..abe4a81 100644 --- a/scripts/enable-plugins.js +++ b/scripts/enable-plugins.js @@ -1,8 +1,8 @@ #!/usr/bin/env node // Enable the caveman and ponytail plugins in a Claude Code settings.json so // their SessionStart hooks auto-activate both modes, and register the autotune -// reminder hook. Merges non-destructively: existing marketplaces, plugins and -// hooks are preserved, unparseable files are left untouched. +// and dream reminder hooks. Merges non-destructively: existing marketplaces, +// plugins and hooks are preserved, unparseable files are left untouched. "use strict"; const fs = require("fs"); @@ -19,6 +19,7 @@ const MARKETPLACES = { ponytail: { source: { source: "github", repo: "DietrichGebert/ponytail" } }, }; const PLUGINS = { "caveman@caveman": true, "ponytail@ponytail": true }; +const REMINDERS = ["autotune", "dream"]; let data = {}; if (fs.existsSync(settingsPath)) { @@ -39,15 +40,16 @@ for (const [name, entry] of Object.entries(MARKETPLACES)) { } data.enabledPlugins = Object.assign(data.enabledPlugins || {}, PLUGINS); -const reminder = path.join(path.dirname(settingsPath), "skills", "autotune", "hooks", "reminder.sh"); data.hooks = data.hooks || {}; data.hooks.UserPromptSubmit = data.hooks.UserPromptSubmit || []; -const registered = JSON.stringify(data.hooks.UserPromptSubmit).includes("autotune/hooks/reminder.sh"); -if (!registered) { +const alreadyRegistered = JSON.stringify(data.hooks.UserPromptSubmit); +for (const skill of REMINDERS) { + if (alreadyRegistered.includes(`${skill}/hooks/reminder.sh`)) continue; + const reminder = path.join(path.dirname(settingsPath), "skills", skill, "hooks", "reminder.sh"); data.hooks.UserPromptSubmit.push({ hooks: [{ type: "command", command: `bash ${JSON.stringify(reminder)}` }], }); } fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2) + "\n"); -console.log(`skills: enabled caveman + ponytail plugins and the autotune reminder in ${settingsPath}`); +console.log(`skills: enabled caveman + ponytail plugins and the ${REMINDERS.join(" + ")} reminders in ${settingsPath}`); diff --git a/skills/dream/SKILL.md b/skills/dream/SKILL.md new file mode 100644 index 0000000..d281717 --- /dev/null +++ b/skills/dream/SKILL.md @@ -0,0 +1,141 @@ +--- +name: dream +description: > + Consolidate the last 24 hours of work in the current repository: what the + agent did, where it was right, where it was wrong, and which hypotheses + follow. Every hypothesis is hardened with the dialectic skill, the survivors + are written to memory, the derived improvements to the repository's code and + to its agent instructions (CLAUDE.md, AGENTS.md) are offered as single-select + questions, and the run ends in autotune and autoskill. The objective is to + raise the confidence of future runs until those mistakes stop recurring. + Trigger on /dream, or + when the operator asks to reflect on, consolidate, or learn from recent work. + Portable across projects. +--- + +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. + +## 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 +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. + +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 +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` skill requires, and name what is still open. + +## 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 (`/dream 7d`). State the +repository and the window in one line before you start. + +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. + +## Step 1: recall + +Gather the window's actual record, 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. +- **Working state**: `git status`, the staged and unstaged diff, stashes. +- **CI**: the runs created in the window, their conclusions, and the failing + 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 + comments on them. +- **Memory**: this 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. +- **Sessions**: the readable session transcripts touched in the window, for the + operator corrections that never reached a commit. + +## Step 2: score + +Classify every action in the window, each with its evidence (`sha`, `file:line`, +run id): + +- **Held** — shipped and still proven: green CI, a passing test that covers it, + no revert, no follow-up fix. +- **Broke** — reverted, amended, hot-fixed by a later commit, red CI, or + corrected by the operator. A failure that ended green is still a failure; count + the detour, not only the destination. +- **Unknown** — no verification exists either way. Unknown is a finding in its + own right, never a quiet pass. + +## Step 3: hypothesize + +Turn the scored record into falsifiable hypotheses, each written as *cause → +predicted consequence → what would disprove it*. Take a pattern (two or more +occurrences) or a single expensive failure; drop the one-off noise. A hypothesis +that nothing could disprove is not a hypothesis — cut it. + +## Step 4: dialectic + +Put every surviving hypothesis through the `dialectic` skill: thesis, +independent skeptics, synthesis, iterated to ~99%. What survives becomes a +finding. What gets refuted is recorded as discarded **with the refutation**, so +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 +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 +structure, git history, `CLAUDE.md` — belongs in memory. + +## Step 6: offer the improvements + +Two kinds come out of the findings: + +- **Code** — the change to this 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 + the agent repeated in the window is a candidate: quote the line that failed to + prevent it, or name the rule that is absent, and propose the exact wording. + +Ask for each one with `AskUserQuestion` and `multiSelect: false` so it renders +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: autotune, then autoskill + +With the dialectic done and memory written, run `autotune` — hand it this run's +findings as its evidence so it does not re-collect them — and then `autoskill` +for the operator corrections the window revealed. Both keep their own +confirmation steps; the dream never writes a skill on their behalf. + +Close the run by refreshing this repository's reminder stamp, so the next hint +is twelve hours out: `date +%s > "$(git rev-parse --absolute-git-dir)/.dream-stamp"`. + +## Rules + +- Evidence or unknown: a claim about what held needs a green signal, and its + absence is reported as unknown rather than assumed. +- Honest scoring: reverts, amends, and follow-up fixes are failures of the first + 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 + 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 + reminds you of it. +- Report the whole ledger: held, broke, unknown, the surviving findings, the + refuted hypotheses with why they fell, and the confidence each closed error + class now carries into the next run. diff --git a/skills/dream/hooks/reminder.sh b/skills/dream/hooks/reminder.sh new file mode 100755 index 0000000..eedaca5 --- /dev/null +++ b/skills/dream/hooks/reminder.sh @@ -0,0 +1,40 @@ +#!/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 --absolute-git-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 diff --git a/tests/test_autotune.py b/tests/test_autotune.py index dd11059..65652f8 100644 --- a/tests/test_autotune.py +++ b/tests/test_autotune.py @@ -85,7 +85,7 @@ class TestSettingsRegistration(unittest.TestCase): encoding="utf-8", ) data = self._patch() - self.assertEqual(len(data["hooks"]["UserPromptSubmit"]), 2) + self.assertEqual(len(data["hooks"]["UserPromptSubmit"]), 3) if __name__ == "__main__": diff --git a/tests/test_dream.py b/tests/test_dream.py new file mode 100644 index 0000000..35e7e58 --- /dev/null +++ b/tests/test_dream.py @@ -0,0 +1,102 @@ +"""Validate the dream reminder hook and its settings registration.""" + +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" / "dream" / "SKILL.md" +HOOK = REPO_ROOT / "skills" / "dream" / "hooks" / "reminder.sh" +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): + text = SKILL.read_text(encoding="utf-8") + self.assertIn("name: dream", text) + self.assertIn("multiSelect: false", 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" + + def test_first_run_hints_and_stamps(self): + result = _run_hook(self.cwd, self.stamp) + self.assertIn("/dream", result.stdout) + self.assertTrue(self.stamp.is_file()) + + def test_second_run_is_silent_within_interval(self): + _run_hook(self.cwd, self.stamp) + self.assertEqual(_run_hook(self.cwd, self.stamp).stdout, "") + + def test_hint_returns_after_the_interval(self): + _run_hook(self.cwd, self.stamp) + self.assertIn("/dream", _run_hook(self.cwd, 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("/dream", _run_hook(self.cwd, self.stamp).stdout) + + def test_outside_a_repository_it_stays_silent(self): + self.assertEqual(_run_hook(self.cwd).stdout, "") + + @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("/dream", _run_hook(repo).stdout) + self.assertTrue((repo / ".git" / ".dream-stamp").is_file()) + self.assertEqual(_run_hook(repo).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("dream/hooks/reminder.sh"), 1) + self.assertEqual(entries.count("autotune/hooks/reminder.sh"), 1) + + +if __name__ == "__main__": + unittest.main()