Compare commits

...

5 Commits

Author SHA1 Message Date
478ef1ac9f deleted file
Some checks failed
🧪 Test / 🧪 Lock + lint (push) Has been cancelled
🔄 Update / 🧠 Update skills-lock.json (push) Has been cancelled
2026-08-10 17:14:39 +02:00
1e90ac1737 updated lock
Some checks failed
🧪 Test / 🧪 Lock + lint (push) Has been cancelled
2026-08-10 17:13:29 +02:00
30bdc7c516 feat(optimize): share the efficiency objective
autotune, autoskill and confidence each carried their own idea of what an
improvement is, and autotune's two efficiency rules were the same sentence
twice. They now route to one skill that names three axes to move at once —
fewer tokens, less time, higher quality — and the rules that decide when a
proposal counts: at least one axis better, none damaged, quality never the
currency, observed friction instead of a guessed percentage.

Applying its own rule, autotune gets shorter rather than longer. The test
holds the coupling, so a later rewrite cannot silently drop a route.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 19:41:52 +02:00
89e088b6f4 feat(autotune): persist skills to repo and mirrors
A skill written only under .claude/skills is lost on the next install, and
one written only into the skills repository is not live until the next
restart, so both writers now do both: locate the operator's skills
repository through an ordered first-hit lookup, write the source of truth
there, then mirror the whole skill directory into the live locations so
hooks and references come along.

The lookup ends in a question to the operator rather than a silent skip
when no repository matches. autoskill routes to the same section instead
of carrying its own copy of the paths.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 19:41:51 +02:00
168a307e4f 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>
2026-08-02 19:41:51 +02:00
12 changed files with 455 additions and 24 deletions

View File

@@ -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.

View File

@@ -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}`);

View File

@@ -11,7 +11,7 @@
"source": "JuliusBrussee/caveman",
"sourceType": "github",
"skillPath": "skills/caveman/SKILL.md",
"computedHash": "d18cdf73a5f5c496d681a43a6846336b110223bdffa6817b8992529c57f1a815"
"computedHash": "59e1fe0d3eeb4189ee5c467efde567672e5cacb41f157c477a6152ca907d44ea"
},
"caveman-commit": {
"source": "JuliusBrussee/caveman",
@@ -23,13 +23,13 @@
"source": "JuliusBrussee/caveman",
"sourceType": "github",
"skillPath": "skills/caveman-compress/SKILL.md",
"computedHash": "84517cd4cf7a49d8d2bc1baf00f61bc359306c6ad9389756b6937eff958bd374"
"computedHash": "52f2301832b376a765b0ed02445c8bf05874b624052bf9a9c3861d9c3dfcee4b"
},
"caveman-help": {
"source": "JuliusBrussee/caveman",
"sourceType": "github",
"skillPath": "skills/caveman-help/SKILL.md",
"computedHash": "dd85267e76baad76995157e7b9f762dfa557cd58951ee92af0c283f48aa26537"
"computedHash": "c76fd4aa86ad557eee62aacbd4b9dd46499fe3913910e7d596d20d094296b984"
},
"caveman-review": {
"source": "JuliusBrussee/caveman",
@@ -41,7 +41,7 @@
"source": "JuliusBrussee/caveman",
"sourceType": "github",
"skillPath": "skills/caveman-stats/SKILL.md",
"computedHash": "47ce2de3d6cb39a75047b5c962e4eb3da15594e7397c94103e9a104d42626553"
"computedHash": "57c7db449641379e2afd1389fdb17cec8f33d9c8e25b1d28093937b4431895e8"
},
"ponytail": {
"source": "DietrichGebert/ponytail",

View File

@@ -7,6 +7,10 @@ description: >
and the pattern is general enough to reuse. Portable across projects.
---
Its objective is the `optimize` skill's: a skill is worth creating only when it
cuts tokens or time and raises quality — judge every candidate against that
skill's three axes and rules.
When you notice the operator repeating the same kind of instruction,
correction, or prompt pattern (about three occurrences, exact wording may
vary), do the following:
@@ -18,10 +22,12 @@ vary), do the following:
3. On approval, write the skill:
- a conversation shortcut or expansion: add it to the `shortcuts` skill
table instead of creating a new skill.
- project-specific behavior: create it in the project's own skills
directory (e.g. `skills/<name>/SKILL.md` in the repository).
- portable behavior: create `skills/<name>/SKILL.md` in the operator's
skills repository.
- anything else: persist it exactly as the `autotune` skill's
**Persistence** section specifies — locate the operator's skills
repository, write `skills/<name>/SKILL.md` there (or in the current
project for project-specific behavior), then mirror the skill directory
into `~/.claude/skills/` and `~/.agents/skills/`, plus the project's
`.claude/skills/` and `.agents/skills/` when it is project-specific.
Follow the local naming conventions and keep the skill a thin,
single-purpose instruction; route to an authoritative doc when one
exists instead of duplicating it.

View File

@@ -13,6 +13,10 @@ Autotune turns observed friction into durable skills. It runs on demand only,
it changes nothing without an explicit answer from the operator, and every
skill it writes or rewrites lands in the operator's skills repository.
Its objective is the `optimize` skill's: every proposal must cut tokens or time
and raise quality — follow that skill's three axes and rules, and drop any
candidate that moves none of them.
## Trigger discipline
- Run **only** when the operator asks: `/autotune`, "autotune", "tune the
@@ -65,10 +69,35 @@ for a final `Write it` / `Revise it` / `Discard` confirmation before touching
disk. Rewrites of an existing skill additionally show a before/after diff of
the changed lines in that confirmation.
## Persistence
A skill that exists only under `.claude/skills` is lost on the next install, and
one that exists only in the repository is not live until the next restart — so
every skill is written to **both**, in this order:
1. **Locate the operator's skills repository.** Take the first hit: the
`SKILLS_REPO` environment variable; the current repository, when it holds
`skills-lock.json` and a `skills/` directory; `pkgmgr path skills`; a git
checkout named `skills` under the usual roots
(`~/Repositories/*/*/skills`, `~/git`, `~/src`, `~/Projects`) carrying
`skills-lock.json`. If none matches, ask the operator for the path with a
single-select question — never skip the repository and write only the live
copies.
2. **Write the source of truth**: `skills/<name>/SKILL.md` in that repository
(portable skills), or `skills/<name>/SKILL.md` in the current project
(project-specific ones). This is the copy that gets committed.
3. **Mirror it live**, so it works without a reinstall: copy the skill's whole
directory to `~/.claude/skills/<name>/` and `~/.agents/skills/<name>/`, and
for a project-specific skill additionally to that project's
`.claude/skills/<name>/` and `.agents/skills/<name>/`. Copy the directory,
not just `SKILL.md`, so hooks and references come along.
Report all written paths. Deleting or renaming a skill follows the same list in
reverse — remove the mirrors too, or the old name keeps firing.
## Step 3: write
- Portable skills go to `skills/<name>/SKILL.md` in the operator's skills
repository; project-specific ones to that project's skills directory.
- Write the skill to every location the **Persistence** section lists.
- Frontmatter carries `name` and a `description` that states what the skill
does **and** when to trigger it - the description is the only part loaded into
every session, so it decides whether the skill ever fires.
@@ -86,7 +115,6 @@ the changed lines in that confirmation.
whole collection is not what autotune is for.
- Never write a skill for one-off work, for secrets, or for behaviour an
existing skill already covers - propose extending that skill instead.
- A skill that saves tokens but loses correctness is a regression; validation,
error handling, and security steps are never the thing that gets trimmed.
- Efficiency claims stay honest: name the observed friction the skill removes,
never a guessed percentage.
- The `optimize` skill's rules decide what counts as an improvement: at least
one axis better, none damaged, and the claim named as observed friction rather
than a guessed percentage.

View File

@@ -27,3 +27,9 @@ Calibration rules:
hidden layers may follow, and give both numbers.
- Static analysis alone caps at the low nineties; only executed evidence
(test, reproduction, live probe) justifies more.
Objective: the `optimize` skill's. The number exists to save the operator a
verification round trip (time), to stop a rework cycle before it starts
(quality), and to replace a long hedging paragraph with two figures and their
residuals (tokens). A confidence block that costs more than it saves — padding,
repeated caveats, a number without residuals — misses its own objective.

141
skills/dream/SKILL.md Normal file
View 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
View 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

40
skills/optimize/SKILL.md Normal file
View File

@@ -0,0 +1,40 @@
---
name: optimize
description: >
The shared objective of the self-improving skills: raise efficiency — fewer
tokens, less time — and raise quality at the same time. Trigger from autotune,
autoskill and confidence, which route their objective here, and on /optimize
when the operator asks how to make the agent's own work cheaper, faster or
better. Portable across projects.
---
Every skill, rule, or report the agent produces about its own working is judged
on three axes at once. A proposal that cannot name the axis it moves is not an
optimization, it is a preference.
## The three axes
- **Tokens** — fewer input and output tokens for the same result: read the range
instead of the whole file, grep the saved log instead of re-running the
command, route to an authoritative doc instead of duplicating it, report in
lines instead of paragraphs.
- **Time** — fewer round trips and less waiting: independent calls in parallel,
one command that answers the question instead of three that circle it, no
polling for work the harness will report on its own.
- **Quality** — fewer errors and less rework: verification instead of
assumption, and learnings that persist so the same mistake is not paid for
twice.
## Rules
- A change must improve at least one axis and damage none. Output that got
cheaper but lost correctness is a regression, not an optimization.
- Quality is never the currency: validation, error handling, tests, and security
steps are not what gets cut to save tokens or time.
- Measure, do not guess: name the observed friction — the re-read, the retry,
the correction the operator had to give twice — and what it cost. A guessed
percentage is not a measurement.
- The cheapest step is the one that does not run: drop work before optimizing
it.
- Persist it or pay again: an optimization that lives only in this session is
re-derived in the next one, so it belongs in a skill or a memory entry.

View File

@@ -37,6 +37,27 @@ class TestAutotuneSkill(unittest.TestCase):
self.assertIn("multiSelect: false", text)
class TestSkillPersistence(unittest.TestCase):
"""Both skill writers must persist to the skills repository AND the live dirs."""
def test_autotune_names_every_target(self):
text = SKILL.read_text(encoding="utf-8")
for target in (
"SKILLS_REPO",
"skills/<name>/SKILL.md",
"~/.claude/skills/",
"~/.agents/skills/",
):
self.assertIn(target, text)
def test_autoskill_routes_to_the_same_persistence(self):
text = (REPO_ROOT / "skills" / "autoskill" / "SKILL.md").read_text(
encoding="utf-8"
)
for target in ("Persistence", "~/.claude/skills/", "~/.agents/skills/"):
self.assertIn(target, text)
class TestAutotuneHook(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
@@ -70,7 +91,9 @@ class TestSettingsRegistration(unittest.TestCase):
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)
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):
@@ -81,11 +104,19 @@ class TestSettingsRegistration(unittest.TestCase):
def test_existing_hooks_preserved(self):
self.settings.write_text(
json.dumps({"hooks": {"UserPromptSubmit": [{"hooks": [{"type": "command", "command": "true"}]}]}}),
json.dumps(
{
"hooks": {
"UserPromptSubmit": [
{"hooks": [{"type": "command", "command": "true"}]}
]
}
}
),
encoding="utf-8",
)
data = self._patch()
self.assertEqual(len(data["hooks"]["UserPromptSubmit"]), 2)
self.assertEqual(len(data["hooks"]["UserPromptSubmit"]), 3)
if __name__ == "__main__":

108
tests/test_dream.py Normal file
View File

@@ -0,0 +1,108 @@
"""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()

29
tests/test_optimize.py Normal file
View File

@@ -0,0 +1,29 @@
"""The self-improving skills must share the optimize objective."""
from __future__ import annotations
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SKILLS = REPO_ROOT / "skills"
ROUTERS = ("autotune", "autoskill", "confidence")
class TestOptimizeObjective(unittest.TestCase):
def test_skill_names_the_three_axes(self):
text = (SKILLS / "optimize" / "SKILL.md").read_text(encoding="utf-8")
self.assertIn("name: optimize", text)
for axis in ("**Tokens**", "**Time**", "**Quality**"):
self.assertIn(axis, text)
def test_routers_point_at_it(self):
for skill in ROUTERS:
text = (SKILLS / skill / "SKILL.md").read_text(encoding="utf-8")
self.assertIn(
"`optimize` skill", text, f"{skill} does not route to optimize"
)
if __name__ == "__main__":
unittest.main()