Tools
The five MCP tools Whetstone exposes, plus the out-of-band maintenance & sharing CLI.
Whetstone is a stdio MCP server. A skill (or your agent) calls these tools around each task — you rarely call them by hand. All state is a git-tracked, per-skill store, organized into learnings (soft preferences) and issues (mandatory rules) grouped by scope (a short label for what a preference is about, e.g. currency-formatting) — see Concepts for the full vocabulary this page assumes.
attach
Register a skill so Whetstone tracks its learned layer.
recall
At task start, retrieve the learnings and issues relevant to your intent.
capture
Turn a correction into a scoped, deduped entry.
revise
Reinforce, weaken, remove, promote, or demote an entry by id.
metrics
Reporting only — per-skill KPIs from the event log.
attach
attach(skill: str, path: str | None = None) -> dictRegister a skill so Whetstone tracks its learned layer. Scaffolds a git-tracked store for skill, organized by scope, and records it in the registry. Idempotent — attaching an already-attached skill is a no-op that reports already_attached. recall / capture create a store lazily if you skip this, so attach is optional setup. path is an optional reference to the target skill’s location, recorded as provenance.
Returns a summary: skill, slug, path (store dir), and created.
recall
recall(skill: str, intent: str, learnings_k: int | None = None) -> dictCall at the start of any task that might use an attached skill — call it blindly; an empty result is fine. Pass intent as a concrete, elaborated description of what you’re about to produce, expanding vague requests into their specific dimensions (e.g. “styling a table: color palette, number formatting, column alignment, row banding, density”) — not the user’s raw words. Retrieval quality lives and dies on this: “make a table styled well” embeds nowhere near a scope named “currency formatting”.
Returns the weighted learnings (soft preferences) and issues (mandatory constraints) that match the intent, ready for the skill to apply. learnings_k optionally caps how many learnings come back. Each entry is tagged with its origin — "skill" (this skill’s own store) or "global" (a preference promoted to the cross-skill learned global layer); prefer the more specific "skill" entry when the two disagree.
The payload also carries conflicts — an always-present list ([] when clean) flagging pairs, within the entries just returned, where a learning affirms what a co-returned issue forbids. It’s a read-only annotation over the finalized set (never a retrieval decision): the issue always wins. Each item tags an origin alongside each id (a_origin/b_origin), since the skill store and the global store mint ids independently and can collide, plus an a_skill/b_skill — the literal skill to pass to revise for that side, since a "global"-origin entry must be revised through the reserved global slug, not the current skill.
capture
capture(skill, polarity, body, scope, provenance=None, run_id=None, confirm=False) -> dictCall the moment you act on user feedback about output from an attached skill, when it’s something new. Cues: a fix (“right-align that”), a preference (“I like muted palettes”), a rejection (“no, not like that”), or approval of a specific choice.
Classify it: taste / preference → polarity:"learning"; a mistake to never repeat or an explicit “always/never” rule → polarity:"issue" (word the body objectively). Generalize the feedback into a scoped rule of a few short sentences that capture the user’s why. capture dedupes and conflict-checks against existing entries before writing.
possible_contradiction (§M7c, experimental — gated by same_polarity_contradiction_check, off by default, opt-in): a near-duplicate learning whose wording actually flips against the existing one — a small, hand-picked antonym/negation lexicon catches the clearest cases (“left” vs. “right”, a negation word on only one side) — returns this status (the existing entry’s id and its current wording as existing_body, the new candidate_body, and a short note) instead of silently reinforcing. Nothing is written. This check is deterministic, so re-calling capture with the same body flags again rather than resolving anything — resolve it via revise on the flagged entry_id instead: action:"reinforce" with no body if it genuinely was a duplicate (bumps recurrence without re-running this check). If it truly was a contradiction, don’t "reinforce" with a reworded body — that keeps the old recurrence and could push a single opposite observation straight to promotion; "remove" the entry instead (not "weaken", which only lowers recurrence and typically leaves it indexed, so a fresh capture would flag again) and THEN capture the corrected preference fresh, at its own honest recurrence. Confirm with the user before calling "remove" — this is a heuristic signal, not a user decision, and removing a learning is dial-governed (it deletes immediately outside supervised mode, unlike an issue contradiction, which always confirms). This narrows, but does not remove, the general limitation that embeddings alone can’t separate a same-polarity contradiction from a paraphrase — see Concepts and LEARNING_SKILLS_DESIGN.md §7.
On a committed change capture (and revise) return a short human-facing confirmation string — “Captured: currency columns — …. Re-applies on future great-tables runs.” — so the agent can tell you what it recorded and that it will stick. The learned layer is otherwise invisible; the tool descriptions instruct the model to relay it.
revise
revise(skill, entry_id, action, body=None, scope=None, run_id=None, confirm=False) -> dictCall when feedback concerns a learning or issue that recall already showed you (use its id). action is one of:
| action | when |
|---|---|
reinforce |
the user repeated a preference (raises its weight) |
weaken |
the user contradicted one (lowers its weight) |
promote |
a preference should become an always/never rule (reword required) |
demote |
a hard rule should soften back to a preference |
remove |
the user rejected it outright |
Confirmation-gated: destructive actions require confirm=True. Optional body / scope supply reworded prose (required for promote).
metrics
metrics(skill: str | None = None) -> dictReporting only — never call during normal work. Computes per-skill KPIs from each store’s events.jsonl and current state: runs, average learnings-applied-per-run, capture counts by status, the reinforcement rate (a repeat-correction proxy — the “money metric”), and % survived. KPIs that need a labeled/calibration set — capture-rate, regressions-prevented, retrieval-precision — are returned as {"value": null, "note": …} rather than guessed. See the Metrics page for the live numbers and Methodology for how they’re produced.
The out-of-band CLI (not tools)
Some operations are deliberate, human- or schedule-initiated maintenance — they rewrite the whole store, commit, and can change what the next recall returns, so they are command-line subcommands, never MCP tools an agent might fire mid-task. They are all safe to run at a session boundary.
whetstone compact <skill> # tidy one store + report advisory behavioral findings
whetstone compact --all # tidy every store, then *report* cross-skill candidates
whetstone promote <skill> <id> # lift one learning/issue into the learned global layer
whetstone promote <skill> <id> --cluster # enact one reported cross-skill candidate, by hand
whetstone export <skill> # write a shareable preference pack (.tar.gz)
whetstone import <skill> <pack> # import a pack (dedup-aware: --merge default | --replace)
whetstone doctor <skill> # read-only health check for the learned loopcompact — tidy, and mine for health
compact runs two passes. The structural pass is auto-applied and always has been: it dedupes near-duplicate entries, merges overlapping scopes, and retires learnings whose weight has decayed below threshold (issues are never auto-retired). On top of that, a behavioral mining pass reads the store’s events.jsonl and reports — advisory only, never auto-applied — four health signals:
| finding | meaning |
|---|---|
harden |
a learning you keep reinforcing and never contradict → consider promoting it to an issue |
bad_capture |
a scope with repeated captures and corrections → the original capture was likely too broad |
stale |
a learning recall keeps surfacing but you never reinforce, or never surfaces at all → a retire nudge |
conflict_residue |
a flagged conflict you never resolved |
Findings are printed and written to a git-ignored compact-report.md; enacting one is a manual revise. Only the safe structural pass ever mutates automatically — the same conservatism compact has always had. See Concepts → Maintenance.
compact --all — report cross-skill promotion candidates
With --all, compact runs over every attached skill and then looks for a preference you have taught in several skills. When the same learning recurs across enough distinct skills, it comes back as a fifth, advisory global_candidate finding — never auto-applied. Promotion always asks a human, regardless of supervision mode, and --all is no exception: it never writes to the global store itself. The finding names a representative entry and the exact command to enact it: whetstone promote <skill> <id> --cluster.
promote — lift a preference by hand
whetstone promote <skill> <id> moves a single learning or issue into the learned global layer deliberately, re-minting its id there. Use it when you already know a preference is cross-cutting and don’t want to wait for --all to notice. Add --cluster to instead enact a whole cross-skill cluster reported by compact --all as a global_candidate finding — <skill> <id> then name that cluster’s representative entry, and the command writes the representative into the global store and retires every per-skill copy in the cluster, across all its skills.
export / import — preference packs
Sharing your taste is a deliberate human act (never something an agent should do on its own), so it lives on the CLI. export writes a preference pack — a .tar.gz of your learnings/ + issues/ markdown plus a pack.toml manifest (telemetry, the derived index, and git history are excluded). import folds a pack into a store: --merge (default) reuses the same dedup + conflict checks capture does — near-duplicates fold in, opposite-polarity clashes are surfaced rather than silently applied — while --replace swaps the store’s contents wholesale. Either way ids are re-minted, so a teammate’s pack can never collide with yours. See Concepts → Preference packs.
doctor — is the loop actually wired?
The one failure mode Whetstone can hit quietly is a silent no-op: a store is attached but the host never calls recall/capture, so nothing is learned and nothing looks wrong. whetstone doctor <skill> is a read-only diagnostic — it reports whether the store exists, whether recall/capture events are landing (counts + last timestamp), the backend in use, and the entry counts. If the loop looks dead (attached but zero recalls) it prints a host-agnostic snippet of operating instructions to paste into your host. It never edits anything.