Tastes: your conventions as versioned, enforceable files
Each taste is one small markdown file saying how you want work done — committed to the repository it governs, kept in your home directory, or subscribed to from a taste repo someone published, and read by every harness agentkit installs to. The decision that separates it from the obvious alternative: nothing is learned invisibly, and how hard a taste binds is a field you set, carried out by one generic hook that reads your files rather than hook code written for your preference.
- Enforcement is configuration, not escalation.
enforce:is a field the owner sets or approves — never a rank a taste earns by being violated. - One generic hook, shipped once.
taste-policereads a small declarative rule out of your local taste file. Blocking something new is a file edit, not a release. - No upstream coupling. The earlier draft had a violated taste generate hook code for agentkit itself. Nothing about your preferences belongs in agentkit's source.
- A folder of small files, and sources that stack. Every preference is its own file;
sources:is ordered, so a public generic set and a private central set compose, later winning. A global correction routes to the source that owns it, not to a local copy. - External tastes come from a git repo you subscribe to. A
sources:list pulls a shared taste set in at a new scope between project and user — vendored into the repository by default, so it travels with every clone and needs no network to read.
One correction, every harness, and a refusal when you ask for one
On 2026-08-05 an agent tagged v0.8.0 on semver reflex. The owner's rule — patch
unless a minor is agreed for that release — lived in no file, so no agent could have read it
and nothing could have refused it. Fixing it took three hand-built layers. Tastes make it one
file with a path, and let the owner decide how hard that file binds.
enforce: block
happens today, lands nowhere
release-tier.md
…v[0-9]+\.[0-9]+\.0…
full file ↓
Codex · Grok
tail
A folder of small files, one preference each
A scope is a directory, and every preference in it is its own file. That is the whole structural idea, and it is the opposite of what conventions do today: a single instruction file that every new rule gets appended to until nobody can say what is in it, which is the failure mode tastes exist to replace.
├── branch-naming.md 9 lines require · advise ├── commit-identity.md 13 lines require · check ├── mr-style.md 11 lines prefer · advise └── release-tier.md 18 lines require · block
Kebab-case, and deliberately unnumbered. Numbering belongs to append-only records, where
position is the history: agentkit's own designs/ runs 001-,
002-, 003-, and each number is permanent because the record is. A
taste folder is not a record. It is a living dictionary keyed by name:, whose
files get added, rewritten and deleted — and rules/, five files and no numbers,
already works exactly that way.
name: already covers the preference. Dedupe, update and lookup all key on that field; a number in front of it is one more thing to keep in sync with nothing.sources: — never by filename. Numbering the files would imply a ranking the resolver never reads.name: across sources is the feature. Two sources both defining release-tier is precisely how the later one overrides the earlier. Prefix them 003- and 007- and that deliberate collision becomes two unrelated files that happen to share a number.name: already covers it; there is no scanning of prose for a paragraph that might be the same rule.One of those files in full. Frontmatter an agent can filter on, a body a human wrote — and, because this owner wants the rule refused rather than merely read, the declarative rule the generic hook enforces:
--- name: release-tier scope: project category: release strength: require enforce: block rule: kind: command match: 'git tag .*\bv[0-9]+\.[0-9]+\.0\b' remedy: Cut a patch tag, or record the owner's agreement in the release PR first. override: AGENTKIT_RELEASE_TIER provenance: 2026-08-05 · session correction ---
Cut patch releases by default. A minor or major tier needs the owner's explicit agreement for that specific release.
Why: “publish this” authorizes a release, never the tier. An agent reasoning from semver alone will tag a minor for any feature-shaped diff.
How to apply: propose the patch version in the release PR. If the diff looks minor-worthy, say so and ask — do not tag it.
| Field | Values | What it changes |
|---|---|---|
| name | kebab-case | The identity used for dedupe and for scope resolution |
| scope | project user | Which directory it belongs in, and who it binds |
| category | free, optional | Lets a skill load only the tastes an action can touch |
| strength | prefer require | prefer is a default an agent may argue with; require is not |
| enforce | advise check block | How hard it binds. The owner's setting, not a rank the taste earns — advise is the default |
| rule | kind match remedy override | With enforce: check or block — at check the match names which actions to re-read before. The declarative rule the generic hook reads: what to match, what to tell the agent, and the one named override |
| provenance | a date and where | When the preference was stated, so a stale one is visible |
name:, one strength, one enforce rule — so bundling two preferences into one file costs per-preference diffs, dedupe, re-scoping by file move and per-name conflict resolution, all at once. But a body may carry several clauses that always travel together: mr-style is terse and point-form and assigns the owner, and none of the three would ever be changed alone.category subdirectories — release/, git/, writing/ — for collections big enough that one flat listing stops being readable. Grouping belongs here, not in cramming related preferences into a single file.Four layers, one winner
Precedence is project > external > user > kit. When the same name:
appears at two scopes the higher one replaces the lower outright — agentkit never merges two
tastes into a third that nobody wrote and nobody can review.
That ordering puts a project above the organisation policy it subscribes to, which is deliberate. A repository with a good reason to differ writes its own taste and the override shows up in the repository's diff, where a human approves it. Governance by review, not by a tool refusing to let you work.
| Layer | Path | In git? | How it ships | Contents today | |
|---|---|---|---|---|---|
| Project | .agentkit/tastes/ | Yesin every clone | Committed; changed on a branch through an MR, never written directly | Sits beside .agentkit/config.yaml | new |
| External | .agentkit/ tastes-vendor/ |
Yesvendored snapshot; the cache mode is not | Declared as a git repo in committed config; vendored into the repository by default, cached per machine when referenced | Nothing yet — the shape a published taste set would take | new |
| User | ~/.agentkit/tastes/ | Nomachine-local — but publishable as an external source | One copy in the shared root, linked into each harness like skills and rules | Root already holds skills/ rules/ instructions/ | new |
| Kit default | rules/ | Shipswith agentkit itself | Installed for everyone; unchanged by this proposal | coding-standards · comment-discipline · consent-protocol · credential-bootstrap · issue-tracking | exists |
To say the load-bearing part plainly, because it is the thing people assume works the other way: a project taste is an ordinary committed file. It lands on a branch, gets reviewed in a merge request, and arrives in every clone of the repository. There is no separate store, no database, and nothing that lives only on the machine that happened to write it. A vendored external source is committed too. Only the user layer is machine-local — and even that can be published as a repo you subscribe to from everywhere.
One copy, four harnesses: the installer already links ~/.claude/skills,
~/.agents/skills, ~/.codex and ~/.grok/skills back to
~/.agentkit. Tastes ride that same wiring, so a preference stated in one harness
is present in the next without a second copy or a per-harness format.
Tastes that outlive one repository
The topology this is designed against is a single owner's. One central repository holds the conventions; every repository in the business subscribes to it as a vendored external source; and because one person sets them, the project scopes hold only deviations that are genuinely repo-specific and the user scope stays close to empty. Today those same conventions live as copy-pasted paragraphs in each repo's instruction file, where copies drift and nobody can say which one is stale.
sources: is an ordered list, and several sources compose at the external
layer — a later entry wins a conflict with an earlier one. That is what makes two sets
useful rather than one.
rules/, which stays the always-on kit layer.
SHA-pinned
.agentkit/tastes-vendor/
+ 1 project taste — a real deviation, visible in this repo's diff
.agentkit/tastes-vendor/
no deviations — the central set is the whole policy
.agentkit/tastes-vendor/
no deviations
An outsider subscribes to the public set only, and gets a working taste collection with none of the business's specifics in it.
The first version of that central set is not written from scratch. It is extracted from the convention corpus already sitting in per-repo instruction files — the same paragraphs, moved somewhere an agent can find them and a diff can review them.
| Today | With one external source, vendored | |
|---|---|---|
| Where the rule lives | A paragraph in every repository's instruction file | One upstream file, vendored into every project that subscribes |
| When it changes | Every repository edited by hand — or quietly left behind | One commit; every project picks it up at its next session |
| What an agent reads | Whatever that repository's copy happens to say | The org policy at the exact commit that project pinned |
| A fresh clone, offline | Works — the paragraphs are committed, current or not | Works — the vendored snapshot is committed here too, pinned to a reviewed SHA |
That last row is the one that decides the design. A policy fetched at read time is a policy that is missing on a fresh clone, on a plane, and on a CI runner with no credentials for the org's git host. So a source has a mode, and the default is to vendor it: the snapshot is committed alongside the code it governs and travels with every clone, exactly like a dependency you check in.
| mode: vendored — the default | mode: reference | |
|---|---|---|
| Where the taste lives | .agentkit/tastes-vendor/<source>/committed with the code | ~/.agentkit/tastes-cache/<source>/this machine only, never committed |
| Pinned by | .agentkit/tastes.lock — the upstream commit SHA | The ref: you named, resolved at each sync |
| Network at read time | None, ever. The files are already in the working tree | None. A session reads only the cache; nothing fetches mid-run |
| A fresh clone | Already has the policy — it arrived with the repository | Fetches at the first session. No network and no cache means nothing to read |
| CI | Byte-identical to a developer's checkout, by construction | Depends on whether that runner has a warm cache |
| How it updates | An agent proposes a lock bump; the MR diff is the new policy text | Sync refreshes the cache at a session boundary |
| Use it for | An organisation's policy, where every repository must agree | Personal conventions you carry between projects |
A source is declared in committed config, so subscribing is a reviewed change like any other. Project scope defaults to vendoring; a personal source on your own machine is usually a reference.
taste: sources: # ordered — a later entry wins - repo: git@github.com:developerinlondon/agentkit-tastes.git mode: vendored # the default — committed to this repo ref: v2026.08.1 path: tastes/ # optional subdirectory - repo: git@github.com:developerinlondon/business-tastes.git mode: vendored ref: v2026.08.4 # wins conflicts with the set above
taste:
sources:
- repo: git@github.com:developerinlondon/my-tastes.git
mode: reference # cached on this machine only
ref: main
on_unreachable: warn # use-cache | warn | fail
max_cache_age: 14d # quiet inside it, loud past
agentkit-tastes 4f1c2be9a7d0e3b8c5a19f7264e0d3b1c8a5f2e9 v2026.08.1
What a failed refresh is allowed to do
use-cache, the default, serves the last good copy and carries on.
warn says so every session. fail refuses to start, for shops
that would rather stop than run on a policy they cannot confirm. It is set per source, so
an org policy can be strict while a personal one is not.
How long silence stays acceptable
Inside the window a served cache is quiet. Past it the same cache is loud, whatever
on_unreachable says — a copy old enough to be wrong should never be quiet.
Vendored sources have no cache age at all; the policy is in the repository.
A half-fetch never replaces a good copy
A refresh assembles into a temporary directory and moves into place only once it is complete. An interrupted or corrupt fetch leaves the previous copy exactly as it was, so the failure mode is staleness — which is reportable — rather than a policy with holes in it.
.agentkit/tastes.lock and moves only when a bump is merged. A reference source moves only at a sync you can see.The same mechanism, pointed outward
Nothing above is specific to consuming. A source is just a git repository whose files are
tastes, which means any repository of taste files is already a publishable taste set —
there is no registry to list it in, no package format to conform to, and no publish step
beyond git push.
Grant read access; that is the whole distribution story
Whoever can clone the repository can subscribe to it with one line of config. An organisation publishing its conventions and a person publishing the way they like commit messages written are the same act, and the trust rules above are what governs anyone consuming either.
Your personal tastes, current on every machine
Your user scope is already a directory of markdown files. Push it to a repository and subscribe to that repository from each machine, and the user layer becomes an external source you own — one place to edit, every machine current, and the same SHA pin and content-diff review you would want from anyone else's.
the layer table's one No under “in git?” is a default, not a limitWhat happens the moment you correct an agent
Learning is an event, not a background process. It fires when a correction arrives, and its whole output is a diff you can read, reject, or amend.
Day 0 you say patch increments. Day 2 you say you want minor increments. Nothing should let both of those be true at once, and the first thing capture does is make sure they cannot be: before writing anything it checks the existing tastes by name and topic, so an instruction that contradicts a covered preference never produces a second file. Two tastes that disagree can never both be loaded, because the second one is never written.
What the instruction means, though, is genuinely ambiguous, and the two readings want opposite things:
An exception, this time only
“Make this one a minor” is a request about a single release. It is served by the taste's own named override — used deliberately, visible in the command — and the file is not touched. The release after it is a patch again, because nothing changed.
The policy changed
“From now on, minor” supersedes the taste in place: a rewritten body, a rule
adjusted or removed, and provenance bumped with the date and what changed.
In the central-repo topology that is a merge request against the owning source, followed
by lock bumps everywhere else.
Which reading applies is not something to infer from tone, so strength decides how much
ceremony the fork gets. Contradicting a require taste — more so one at
enforce: block — asks outright: this contradicts release-tier — one-off, or
change the taste? A policy that took a deliberate decision to set should not flip because
of one ambiguous sentence at the end of a long session. A prefer taste updates
directly and says that it did.
Either way the outcome is one file. A superseded taste is rewritten, never shadowed by
a release-tier-v2.md sitting next to it, because the archive already exists: git
history holds every previous version of that file. Reading the current preference should cost
one file, not a chain of them — which is the same reason a decision record gets revised in
place rather than superseded by a new document.
Learning routes to the owning scope. A correction lands in whichever repository you happened to be working in, but the preference usually is not about that repository. When it is global, the skill proposes a merge request against the central source — the repo that owns that taste — and the update reaches everywhere else the ordinary way, as lock-bump MRs. Writing it locally instead is the fast path and the wrong one: it fixes the repository you were in and leaves every other one wrong, which is exactly the drift that made copy-pasted conventions unusable.
Which set a given correction belongs to is judgment, and there is no honest way to present it as a lookup. What the skill can do is capture the signals at the moment the correction lands, when the context is still there:
| When the correction… | It goes to | |
|---|---|---|
| names business repositories, hosts, or identities | the private central set — an MR there | external |
| is a stance any stranger could adopt | a candidate for the public set — owner-approved, never auto-published | external |
| is true in this repository and no other | a project taste, committed here, override visible in this repo's diff | project |
| is personal ergonomics rather than policy | your own published set — the user layer you subscribe to everywhere | user |
| is genuinely unclear | the private central set, as the safe default | external |
Judgment is enough here because the stakes are bounded, and it is worth saying why rather than hoping the heuristic is good. A misfiled taste is one small file. Moving it is two merge requests — out of one set, into another — with no data migration and nothing to rewrite. The periodic audit reads placement as well as staleness, so the wrong call surfaces as a proposal rather than sitting there: nothing in this taste is business-specific — propose promoting it to the public set. Unsure defaults inward for the same reason: a private taste that should have been public costs a promotion MR, while the reverse costs a leak.
Zoom: enforcement is a setting, not a rank
The shortest description of the mechanism is git hooks for agents, and it is a fair
one: taste-police is mechanically a hook, refusing a command before it runs. Two
differences are worth keeping in view. Enforcement is opt-in per taste rather than per
repository, so most of your tastes never enforce anything. And the rule it applies is learned
from a correction and arrives as a diff you approve, rather than being hand-written into a
script nobody revisits.
Agents follow a loaded taste the large majority of the time, and most tastes will never need
more than that. check and block exist for the tail: a long session
where the instruction has been buried under hours of tool calls, a reflex that overrides what
was read — semver beat the changelog in the incident above — and sessions where the taste was
never loaded at all. agentkit has measured its own version of this:
So enforce: is a dial the owner sets on the file, in advance, for the reason they
already know. advise is the default and where most tastes stay.
Agents read it
The taste text enters the instruction stream at session start. Nothing checks anything, and nothing needs to — for a preference about tone, structure, or what counts as done, being read is the whole mechanism.
the release-tier rule as it exists today: a memory note and a CHANGELOG line, both advise-grade, and nothing connecting themThe skill re-reads it before acting
The taste skill re-reads the matching tastes immediately before an action that could
break one — reading category: release before proposing a tag — instead of
trusting that a session-start instruction survived the intervening hour of work.
One generic hook refuses the command
The taste carries a small declarative rule. A single taste-police hook,
shipped once in the core kit, reads every local taste at enforce: block and
refuses a matching command using that taste's own remedy text and its own named override.
The hook holds the mechanism; your file holds the policy.
Which is the whole architectural point. The refusal below is produced by a hook that has never heard of releases, semver, or this repository:
git tag v0.8.0 matches rule.match in that file. Cut a patch tag, or
record the owner's agreement in the release PR first.
Override, when the agreement exists: prefix the command with
AGENTKIT_RELEASE_TIER=minor.
Every bold fragment there is a field of the taste file in section 02 — the taste's name, its
match, its remedy, its override. The hook contributes
the word BLOCKED and the machinery to intercept a command. Adding a second blocking taste
tomorrow changes no code anywhere.
The override is deliberate, and modelled on the branch WIP cap in
hooks/claude/git-police.sh: a guard with no way through gets disabled wholesale,
and a guard you can switch off by mistyping it is worse than no guard. One named escape
hatch, used on purpose, visible in the command.
Two things deliberately do not happen here. A preference that cannot be expressed as a
declarative rule stays at check rather than growing bespoke code — and if the
gap it exposes is genuinely general, that is an agentkit feature request, which is outside
what a taste is for. And an agent that observes repeated violations may propose raising
enforce:, but the proposal arrives the same way every other change does: as a
diff the owner reads and merges.
Where this comes from, and where it deliberately diverges
The idea is commandcode.ai's Taste, which deserves the credit for naming the problem: your preferences are real, they are learnable, and no tool was holding them. It describes itself as “an invisible architecture of your choices”. That invisibility is exactly the part agentkit inverts.
| Axis | Taste — commandcode.ai (inspiration) | Tastes — this proposal |
|---|---|---|
| Representation | A neural model of your choices; invisible by design | One markdown file per preference, with frontmatter you can grep |
| How it learns | Passively in the background, from what you accept and reject | Only at an explicit correction moment, and always as a diff you approve |
| Scopes | .commandcode/taste/ · ~/.commandcode/taste/ · remote local > project > user |
.agentkit/tastes/ · sources: · ~/.agentkit/tastes/ · rules/ project > external > user > kit |
| Transport | npx taste push / pull, “intelligently merged” on pull, through their registry | Git. Any repo of taste files is a package — commit it, review it in an MR, vendor someone else's into your own at a pinned SHA. Higher scope replaces, never blends |
| Enforcement | The model shapes what gets suggested | A per-taste setting the owner picks: advise · check · blockblock carried out by one generic hook reading your file |
| Turning it off | taste disable | taste.enabled: false — stops pickup and writes together |
What tastes are not replacing
| Piece | Holds | Whose | Relationship | |
|---|---|---|---|---|
| rules/ | Universal engineering discipline — comment discipline, coding standards, consent | The kit's | Unchanged. Tastes sit above it and can override it for your project | exists |
| Memory kit | Episodic project knowledge — what broke, what a system does, what you learned | The project's | A reflect finding that turns out to be a preference routes to a taste instead of a vault note | exists |
| CLAUDE.md & friends | Whatever a single harness happens to read at startup | One harness's | Not replaced on day one. Tastes get extracted out of it gradually, one preference at a time | exists |
| The taste skill | Loading, precedence resolution, the write path, and the check re-read | Yours, everywhere | Ships in the core kit, so it installs with every agentkit install | new |
| taste-police | One generic hook. Reads every local taste at enforce: block and refuses what its rule matches | The kit's, run on your data | Built once, alongside the other police hooks. It never changes when you block something new — that is a file edit | phase 2 |
taste: # load applicable tastes at session start enabled: true # write or update a taste when a correction lands learning: true
Shippable slices
1 · Convention and skill
~5–7 sessions- The file format — frontmatter fields including enforce, body shape, one worked example per field
- Scopes and precedence, resolved by name, higher scope replacing lower — four layers from the start, with the external slot empty until phase 3 fills it
- The taste skill in the core kit: load at session start, re-read on check, update on correction
- A lint in the test suite so a malformed taste fails CI, not a future session
2 · The generic hook, and the CLI
~4–6 sessions- taste-police — one hook in the core kit that reads every local enforce: block rule and refuses what it matches, with that taste's remedy and override
- agentkit taste list | add | lint — read them, write one by hand, check them (lint covers the rule block)
- Dedupe and merge so a year of corrections is a shelf, not a pile
- meditate integration: the vault audit also audits tastes for staleness
- Installer wiring for the tastes directory in the shared root
3 · External sources and upkeep
~9–12 sessions- sources: in config — an ordered list, so several sets stack at the external layer and a later entry wins
- Vendored mode and the lockfile: a committed snapshot under .agentkit/tastes-vendor/ pinned by SHA, so a fresh clone needs no network
- Lock-bump proposals — an agent proposes the update and the MR diff is the new policy text
- Reference mode for personal sources: per-machine cache, on_unreachable, max_cache_age, atomic swap
- An enforce-bump proposal: after observed violations an agent may propose raising enforce:, as a diff the owner merges
- A tastes browser page showing which layer each effective taste came from
What this deliberately is not
check. If the gap it exposes is genuinely general, that is an agentkit feature request — outside what a taste is for, and never something your local file causes upstream.enforce: only ever changes because the owner set it or merged a diff proposing it. Repeated violation is evidence for a proposal, never a promotion.What has to be true to call it done
name: resolve to the project one, in a test that fails if precedence inverts.taste.enabled: false stops both pickup and writes — a session with it off neither reads a taste nor creates one.enforce: block → the generic taste-police refuses git tag v0.8.0 using that file's own remedy text.max_cache_age warns loudly every session while the same cache inside the window stays silent, and on_unreachable: fail stops rather than proceeding on a policy it could not confirm.name: conflict to the later entry, in a test that fails if the order stops mattering.