Compare commits
3 Commits
a42d6b76f4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd32841847 | ||
|
|
1bc7711f38 | ||
|
|
27aaabf305 |
@@ -26,7 +26,7 @@ Copy the skills into a specific project (`<repo>/.agents/skills` and `<repo>/.cl
|
|||||||
make project TARGET=/path/to/repo
|
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.
|
Restart your agent afterwards so it loads the new skills.
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// Enable the caveman and ponytail plugins in a Claude Code settings.json so
|
// Enable the caveman and ponytail plugins in a Claude Code settings.json so
|
||||||
// their SessionStart hooks auto-activate both modes. Merges non-destructively:
|
// their SessionStart hooks auto-activate both modes, and register the autotune
|
||||||
// existing marketplaces and enabled plugins are preserved, unparseable files
|
// reminder hook. Merges non-destructively: existing marketplaces, plugins and
|
||||||
// are left untouched.
|
// hooks are preserved, unparseable files are left untouched.
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
const settingsPath = process.argv[2];
|
const settingsPath = process.argv[2];
|
||||||
if (!settingsPath) {
|
if (!settingsPath) {
|
||||||
@@ -38,5 +39,15 @@ for (const [name, entry] of Object.entries(MARKETPLACES)) {
|
|||||||
}
|
}
|
||||||
data.enabledPlugins = Object.assign(data.enabledPlugins || {}, PLUGINS);
|
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");
|
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
|
||||||
108
skills/refactor/SKILL.md
Normal file
108
skills/refactor/SKILL.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
---
|
||||||
|
name: refactor
|
||||||
|
description: >
|
||||||
|
Refactor every file that is currently uncommitted in the git working tree -
|
||||||
|
staged, unstaged and untracked - up to the project's coding standards,
|
||||||
|
without changing behaviour. Trigger on /refactor or when the operator asks to
|
||||||
|
clean up, tidy, or polish the current changes before committing. Portable
|
||||||
|
across projects.
|
||||||
|
---
|
||||||
|
|
||||||
|
The unit of work is the uncommitted change set, not the repository. Behaviour
|
||||||
|
stays identical: a refactor that changes what the code does is a bug, not a
|
||||||
|
cleanup.
|
||||||
|
|
||||||
|
## Step 1: fix the file set
|
||||||
|
|
||||||
|
Read the set from git, never from memory of what was edited:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status --porcelain
|
||||||
|
```
|
||||||
|
|
||||||
|
Take modified, added, renamed and untracked files. Drop deleted files, binaries,
|
||||||
|
lockfiles, vendored trees and generated output. Restate the resulting list in
|
||||||
|
one line before touching anything; if it is empty, say so and stop.
|
||||||
|
|
||||||
|
Never `git stash` and never revert a working-tree file to compare - other agents
|
||||||
|
and the operator share this tree. Read the committed version with
|
||||||
|
`git show HEAD:<path>` into a temporary file instead.
|
||||||
|
|
||||||
|
## Step 2: get a safety net first
|
||||||
|
|
||||||
|
A refactor is only provably behaviour-preserving if something failed before it
|
||||||
|
and passes after it. Before editing, check what covers the change set:
|
||||||
|
|
||||||
|
- **Coverage exists**: run it now and record the green baseline. A suite that
|
||||||
|
was already red gets reported, not refactored around.
|
||||||
|
- **No coverage and the code is testable logic**: write the characterization
|
||||||
|
tests first, against the current behaviour - including the behaviour you
|
||||||
|
consider wrong, so the refactor cannot silently change it. Keep them small
|
||||||
|
and framework-free unless the project already has a framework. Fix the
|
||||||
|
captured wrongness in a separate change, never in this one.
|
||||||
|
- **Not sensibly testable**: pure glue, declarative config, a thin wrapper over
|
||||||
|
a service the test would have to mock into meaninglessness, or a project with
|
||||||
|
no test setup at all. Say so in one line and refactor without a net; do not
|
||||||
|
invent a test harness the project never asked for.
|
||||||
|
|
||||||
|
The new tests are part of the deliverable and must still pass unchanged after
|
||||||
|
the refactor. A test that had to be edited to stay green means the behaviour
|
||||||
|
moved: revert the refactor, not the test.
|
||||||
|
|
||||||
|
## Step 3: refactor each file
|
||||||
|
|
||||||
|
Read the whole file first, then apply the standards that the surrounding code
|
||||||
|
and the project's own config already establish (formatter config, linter rules,
|
||||||
|
existing idioms) over any generic preference of your own.
|
||||||
|
|
||||||
|
Four principles govern the result:
|
||||||
|
|
||||||
|
- **SRP**: one file, one responsibility; one function, one reason to change. A
|
||||||
|
unit that needs an "and" to describe it is two units.
|
||||||
|
- **SPOT**: every fact - a constant, a path, a schema, a rule - lives in exactly
|
||||||
|
one place and everything else references it.
|
||||||
|
- **KISS**: the simplest construct that works. No abstraction without a second
|
||||||
|
caller today, no configuration for a value that never varies.
|
||||||
|
- **DRY**: the same logic is written once. A literal repeated three times is a
|
||||||
|
missing constant.
|
||||||
|
|
||||||
|
Concretely:
|
||||||
|
|
||||||
|
- **File size**: no file containing program code exceeds 250 lines. Split the
|
||||||
|
overflow along the SRP seam - it is the symptom, not the cause. Description
|
||||||
|
and markup languages are exempt (Markdown, YAML, JSON, HTML, translation and
|
||||||
|
data files), as is the rare file the language or framework genuinely forbids
|
||||||
|
splitting: name that reason in the report, "it was easier" does not qualify.
|
||||||
|
- **Duplication**: fold repeated logic into the helper that already exists;
|
||||||
|
create a new one only when two or more call sites need it now.
|
||||||
|
- **Dead weight**: unused imports, variables, parameters, branches that cannot
|
||||||
|
be reached, flags with a single value, abstractions with one implementation.
|
||||||
|
- **Naming**: names that state what a thing is, matching the file's convention.
|
||||||
|
- **Shape**: split a function that needs a scroll to read; collapse nesting by
|
||||||
|
returning early; replace magic values with a named constant at its use site.
|
||||||
|
- **Errors**: never widen a caught exception, never swallow one, never remove a
|
||||||
|
validation or a security check to make the code shorter.
|
||||||
|
- **Comments**: invoke the `comments-clean` skill rather than reimplementing its
|
||||||
|
rules here.
|
||||||
|
|
||||||
|
Do not reformat regions you have no substantive fix for - cosmetic churn buries
|
||||||
|
the real change in the diff. If a file's problem is architectural and cannot be
|
||||||
|
fixed inside the change set, report it instead of half-doing it.
|
||||||
|
|
||||||
|
## Step 4: keep behaviour provable
|
||||||
|
|
||||||
|
- Re-run the step 2 net and require the same green result as the baseline.
|
||||||
|
- Never fold a bug fix, a feature, or a dependency change into a refactor.
|
||||||
|
Found a real bug? Name it in the report and leave it for a separate change.
|
||||||
|
- Run the cheap checks the project already has for the touched files (formatter,
|
||||||
|
linter, the module's own unit tests). Do not start a full suite, a build, or a
|
||||||
|
deploy on your own initiative - propose it and let the operator trigger it.
|
||||||
|
- State plainly what was verified and what was not. An unverified refactor is
|
||||||
|
reported as unverified.
|
||||||
|
|
||||||
|
## Step 5: report
|
||||||
|
|
||||||
|
One line per file: what changed and why it is safe. Then one line naming what
|
||||||
|
was deliberately left alone (architectural findings, bugs, files skipped) and
|
||||||
|
the exact command the operator can run to validate. Do not commit unless the
|
||||||
|
operator asked for a commit.
|
||||||
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