feat(autotune): add skill for generating and tuning skills
The skill generates new skills and optimizes existing ones for token usage, error rate, focus and persisted learnings. Every decision is confirmed through single-select (radio button) questions, and the skill only runs on an explicit operator trigger. A UserPromptSubmit hook reminds the operator at most once an hour that /autotune exists; the reminder is a hint for the operator, never a trigger for the agent. The install-time settings patch registers it alongside the caveman and ponytail plugins. 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 enable the caveman and ponytail plugins in the target's `.claude/settings.json` so their modes auto-activate on session start; 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 the `autotune` reminder hook is registered so the agent points you at `/autotune` at most once an hour. Existing settings are preserved.
|
||||
|
||||
Restart your agent afterwards so it loads the new skills.
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
// Enable the caveman and ponytail plugins in a Claude Code settings.json so
|
||||
// their SessionStart hooks auto-activate both modes. Merges non-destructively:
|
||||
// existing marketplaces and enabled plugins are preserved, unparseable files
|
||||
// are left untouched.
|
||||
// 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.
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const settingsPath = process.argv[2];
|
||||
if (!settingsPath) {
|
||||
@@ -38,5 +39,15 @@ 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) {
|
||||
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 in ${settingsPath}`);
|
||||
console.log(`skills: enabled caveman + ponytail plugins and the autotune reminder in ${settingsPath}`);
|
||||
|
||||
92
skills/autotune/SKILL.md
Normal file
92
skills/autotune/SKILL.md
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
name: autotune
|
||||
description: >
|
||||
Generate new agent skills and optimize existing ones so the agent works more
|
||||
efficiently: fewer input and output tokens, fewer errors, more focus, and
|
||||
learnings that persist across sessions. Every proposal is confirmed with the
|
||||
operator through single-select (radio button) questions. Trigger ONLY on an
|
||||
explicit operator request (/autotune, "autotune", "tune the skills"); never
|
||||
run it on your own initiative. Portable across projects.
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||
## Trigger discipline
|
||||
|
||||
- Run **only** when the operator asks: `/autotune`, "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.
|
||||
- Never propose, write, or edit a skill as a side effect of unrelated work.
|
||||
|
||||
## Step 1: collect evidence
|
||||
|
||||
Before asking anything, gather concrete friction from the current session and,
|
||||
where readable, from the project's memory and agent instruction files:
|
||||
|
||||
- **Token waste**: repeated file re-reads, whole-file reads where a range was
|
||||
enough, long tool output pasted back, re-running a command instead of
|
||||
grepping a saved log, verbose report prose.
|
||||
- **Errors**: rejected tool calls, permission prompts, retried commands,
|
||||
corrections the operator had to give twice.
|
||||
- **Focus loss**: work that drifted beyond the request, unrequested tests or
|
||||
deploys, questions that stalled an autonomous run.
|
||||
- **Lost learnings**: facts re-derived this session that a memory file or a
|
||||
skill should already have carried.
|
||||
|
||||
List each finding as one line: `symptom -> cost -> candidate fix`. Skip
|
||||
anything that happened once and is not a pattern.
|
||||
|
||||
## Step 2: ask, do not decide
|
||||
|
||||
Use `AskUserQuestion` with `multiSelect: false` so every question renders as
|
||||
radio buttons. Never assume an answer, never batch several decisions into one
|
||||
option, and never write a file before the answers are in.
|
||||
|
||||
Ask in this order, at most four questions per call:
|
||||
|
||||
1. **Scope** - "What should autotune do this run?" Options: `Create a new
|
||||
skill`, `Optimize an existing skill`, `Both`, plus the ranked findings if
|
||||
more than one candidate exists.
|
||||
2. **Target** - which finding becomes a skill, or which existing skill gets
|
||||
optimized. One option per candidate, each with its measured cost in the
|
||||
description.
|
||||
3. **Shape** - for a new skill: `Standalone skill`, `Row in the shortcuts
|
||||
skill`, `Extend an existing skill`, `Memory entry instead of a skill`.
|
||||
For an optimization: `Tighten wording only`, `Change the trigger`, `Add
|
||||
rules`, `Split into two skills`, `Merge into another skill`.
|
||||
4. **Placement** - `Operator skills repository (portable)` or
|
||||
`This project's skills directory (project-specific)`.
|
||||
|
||||
Present the drafted skill text (name, trigger phrasing, body outline) and ask
|
||||
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.
|
||||
|
||||
## 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.
|
||||
- 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.
|
||||
- Body is thin and single-purpose: the smallest instruction that changes
|
||||
behaviour. Route to an authoritative doc instead of duplicating it.
|
||||
- Optimizing an existing skill means the file gets shorter or sharper, not
|
||||
longer. Delete rules the agent already follows, merge duplicated ones, and
|
||||
cut every sentence that does not change an action.
|
||||
- Report what changed in at most three lines, then refresh the reminder stamp
|
||||
so the next hint is an hour out: `date +%s > ~/.claude/.autotune-stamp`.
|
||||
|
||||
## Rules
|
||||
|
||||
- One skill per run unless the operator picked `Both`; a broad rewrite of the
|
||||
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.
|
||||
32
skills/autotune/hooks/reminder.sh
Executable file
32
skills/autotune/hooks/reminder.sh
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# UserPromptSubmit hook: emit the autotune hint at most once per interval.
|
||||
# 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 autotune by itself.
|
||||
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
|
||||
autotune, do not propose skills, do not ask questions about it.
|
||||
|
||||
💡 /autotune - 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
|
||||
92
tests/test_autotune.py
Normal file
92
tests/test_autotune.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""Validate the autotune 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" / "autotune" / "SKILL.md"
|
||||
HOOK = REPO_ROOT / "skills" / "autotune" / "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):
|
||||
def test_skill_is_trigger_only(self):
|
||||
text = SKILL.read_text(encoding="utf-8")
|
||||
self.assertIn("name: autotune", text)
|
||||
self.assertIn("multiSelect: false", 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("/autotune", 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("/autotune", _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("/autotune", _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("autotune/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"]), 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user