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

attach(skill: str, path: str | None = None) -> dict

Register 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) -> dict

Call 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) -> dict

Call 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) -> dict

Call 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) -> dict

Reporting 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 loop

compact — 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.