feat(dream): add reflection skill and 12h reminder
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) <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,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.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.
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
141
skills/dream/SKILL.md
Normal file
141
skills/dream/SKILL.md
Normal file
@@ -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.
|
||||
40
skills/dream/hooks/reminder.sh
Executable file
40
skills/dream/hooks/reminder.sh
Executable file
@@ -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
|
||||
@@ -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__":
|
||||
|
||||
102
tests/test_dream.py
Normal file
102
tests/test_dream.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user