agentkit · product proposal · 003 · stage: accepted

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.

status accepted decided explicit files, not a learned model decided enforcement is configuration estimate 18–25 sessions, 3 phases origin release-tier incident, 2026-08-05
revised 2026-08-05 · after owner review
  • 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-police reads 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.
01 · The whole loop

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.

exists in agentkit today this proposal builds it the outcome that proves it works refuses — enforce: block
01 · correction
The owner says no
“patch unless I agree a minor”
a session, 2026-08-05
happens today, lands nowhere
writes
02 · written
A taste file
.agentkit/tastes/
release-tier.md
“Cut patch releases by default. A minor or major tier needs the owner's explicit agreement…” match:
…v[0-9]+\.[0-9]+\.0…
committed
full file ↓
loads
03 · loaded
Every harness reads it
Claude Code · OpenCode
Codex · Grok
the taste skill, at session start
applies
04 · honored
The next release is a patch
proposes v0.7.4, not v0.8.0
in a harness that never saw the original correction
for the
tail
05 · blocked
One generic hook refuses
taste-police · reads this file
set enforce: block — no new hook code, ever
stage 02 · where a taste comes from — highest wins
project
.agentkit/tastes/committed — the whole team gets it
wins
external
.agentkit/tastes-vendor/a repo you subscribe to — committed
user
~/.agentkit/tastes/personal, every project you touch
kit
rules/ · 5 filesagentkit's own discipline, unchanged
stages 03–05 · enforce: — a field the owner sets, not a rank a taste earns
enforce: advise
Agents read it
The default, and where most tastes stay. In the instruction stream at session start.
enforce: check
The skill re-reads it
Immediately before a matching action, so an hour of tool calls cannot bury it.
enforce: block
taste-police refuses
One generic hook reads the rule in this file. Local data — agentkit ships no code for it.
most tastes never leave advise · a block rule is a pattern and a remedy string, never code
02 · Anatomy

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.

.agentkit/tastes/ one file per taste — never a monolith
├── 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
naming · why none of those filenames has a number

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.

The name is the key, not the position. A correction updates the file whose 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.
Order would advertise a precedence that does not exist. What wins is decided by scope and by position in sources: — never by filename. Numbering the files would imply a ranking the resolver never reads.
The same 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.
what one file per taste buys
One MR changes one convention. The review is about that convention and nothing else, instead of a diff against a thousand-line instruction file nobody rereads.
Dedupe has something to key on. A correction updates the file whose name: already covers it; there is no scanning of prose for a paragraph that might be the same rule.
Re-scoping is a file move. A preference that turns out to be personal, or global, moves between directories — it does not need extracting from a paragraph first.
The enforce rule travels inside its taste. What is checked and what is refused live in the same file as the reason, so they cannot drift apart.

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:

.agentkit/tastes/release-tier.md project scope committed
---
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.

FieldValuesWhat it changes
namekebab-caseThe identity used for dedupe and for scope resolution
scopeproject
user
Which directory it belongs in, and who it binds
categoryfree, optionalLets a skill load only the tastes an action can touch
strengthprefer
require
prefer is a default an agent may argue with; require is not
enforceadvise
check
block
How hard it binds. The owner's setting, not a rank the taste earns — advise is the default
rulekind
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
provenancea date and whereWhen the preference was stated, so a stale one is visible
“Releasing: bump the PATCH version by default. A minor or major tier requires the owner's explicit agreement for that specific release, recorded in the release PR — ‘publish this’ authorizes a release, never the tier.” CHANGELOG.md, lines 6–8 — the same rule as prose, reachable only by an agent that happens to open the changelog
hygiene · what keeps a folder from becoming the thing it replaced
A taste is a topic, not a sentence. The frontmatter is the taste's identity — one 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.
The test that decides a split. Would these clauses ever change independently? If yes they are separate tastes and the file should become two. If no they are one taste with several clauses, however long it reads. Length is a symptom worth noticing — a taste past a screen usually fails that test — but the question is the rule, not the line count.
Subfolders once a set is large. Optional 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.
Context is the real budget. Small files load selectively: a skill can read frontmatter and pull only the tastes an action can touch. For a large set, one index line per taste is what enters the session, and the body is read when it matters.
03 · Scopes

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.

LayerPathIn git?How it shipsContents 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.

04 · External sources

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.

sources · ordered, later wins
1
agentkit-tastes Public and generic — tastes any stranger could adopt, and the living showcase of the format. Opt-in, unlike rules/, which stays the always-on kit layer.
2
business-tastes Private, and the central repository of this topology. Being second, it wins any conflict with the generic set.
vendored
SHA-pinned
every repository in the business
repo A .agentkit/tastes-vendor/ + 1 project taste — a real deviation, visible in this repo's diff
repo B .agentkit/tastes-vendor/ no deviations — the central set is the whole policy
repo C .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.

TodayWith 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.

.agentkit/config.yaml proposed keys
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
~/.config/agentkit/config.yaml user scope
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.lock proposed committed — a bump is an MR you read
agentkit-tastes  4f1c2be9a7d0e3b8c5a19f7264e0d3b1c8a5f2e9  v2026.08.1
reference mode only · a vendored source has nothing to fetch at read time
on_unreachable

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.

the same call agentkit's branch WIP cap already makes when no forge answers
max_cache_age

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.

atomic swap

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.

“Three outcomes stay distinguishable: silence when the repository is clean, a refusal naming the branches when it is not, and an UNCHECKED reminder — never a refusal — when no forge could be reached, because blocking a developer on a network hiccup is worse than the sprawl.” CHANGELOG.md, v0.7.0 — the philosophy on_unreachable: use-cache inherits
trust boundary · an external taste is an instruction someone else writes into your agent's context
opt-in
Subscribing is a reviewed change. A source is added by editing committed config; at project scope that is a merge request someone approves. Nothing arrives because a tool discovered it.
pinning
The lockfile pins an exact commit. A vendored source sits at the SHA recorded in .agentkit/tastes.lock and moves only when a bump is merged. A reference source moves only at a sync you can see.
visibility
You review the words, not a version number. A lock bump carries the vendored content diff, so what your agents will now be told is exactly the text in front of you. A reference sync reports its diff against the cache.
no code
A block rule is data, never code. It is a match pattern, a remedy string and the name of an override — nothing executable. The worst a hostile source can do is over-block you, which is immediate and obvious. It cannot run anything.
04b · Publishing

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.

publish

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 own

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 limit
05 · Learning

What 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.

A correction lands. You tell an agent it did the wrong thing for a reason that will hold next time — a preference, not a bug.any harness · the taste skill is on
Look before writing. Search existing tastes by name and category. An update to the file that already covers it beats a second file that half does — and a contradiction never gets one..agentkit/tastes/ then ~/.agentkit/tastes/
State why and how to apply. A taste without both is a slogan; the agent that reads it next has only the words on the page.the file body
Route to the scope that owns it. A global preference becomes an MR against the central source. A project taste is the exception path, for what is true here and nowhere else.never a direct commit
The diff is the audit trail. Every change to how your agents behave is a reviewable commit with a date and an author.git log .agentkit/tastes/
the fork step 02 opens · when the instruction contradicts a taste you already have

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:

one-off

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.

durable

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.

the rule step 04 depends on

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.

06 · Enforcement

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:

“Instructions alone are demonstrably routed around: a working one-MR cap was bypassed eleven times by simply never opening an MR.” CHANGELOG.md, v0.7.0 — the measurement that justified issue-police and the branch WIP cap

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.

enforce: advise

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 them
enforce: check

The 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.

the setting for a preference no pattern can capture; it is also where a taste stays if its rule cannot be written declaratively
enforce: block

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.

blocking a new preference is a local file edit — no agentkit release, no upstream change, no hook code written for you

Which is the whole architectural point. The refusal below is produced by a hook that has never heard of releases, semver, or this repository:

taste-police · PreToolUse · .agentkit/tastes/release-tier.md proposal — the hook is generic, the text is data
BLOCKED by taste release-tier (enforce: block): 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.

07 · Inspiration

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.

AxisTaste — 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
08 · Neighbours

What tastes are not replacing

PieceHoldsWhoseRelationship
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
.agentkit/config.yaml proposed keys or ~/.config/agentkit/config.yaml
taste:
  # load applicable tastes at session start
  enabled: true
  # write or update a taste when a correction lands
  learning: true
09 · Phases

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
ship: tastes are honored on every harness agentkit installs to

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
ship: a taste can refuse a command, and blocking the next one is a file edit

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
ship: a taste repo anyone can publish, vendored into every clone that subscribes
10 · Non-goals

What this deliberately is not

No neural model, and no passive observation. Learning fires at explicit correction moments only, and its entire output is a visible diff. Nothing watches your keystrokes to infer what you meant.
No cloud taste registry. Git is the transport and any repository of taste files is already a package. Publishing means granting read access to a repo you own — never uploading your conventions to someone's service, and never asking anyone's permission to list them.
No per-harness divergence. One format, one directory, one copy in the shared root. A taste that behaves differently in Codex than in Claude Code is a bug, not a feature.
Not a way to ship hook code. A preference that cannot be written as a declarative rule stays at 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.
No enforcement a taste earns on its own. 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.
Not a CLAUDE.md replacement on day one. Migration is extraction: pull preferences out of instruction files one at a time as they come up, and leave the rest working.
11 · Acceptance

What has to be true to call it done

A preference corrected in one harness lands as a taste file, and the next session in a different harness honors it without being told again.
A project taste and a user taste with the same name: resolve to the project one, in a test that fails if precedence inverts.
Setting taste.enabled: false stops both pickup and writes — a session with it off neither reads a taste nor creates one.
The release-tier incident replays end to end: correction → taste file → honored in a fresh session → the owner sets enforce: block → the generic taste-police refuses git tag v0.8.0 using that file's own remedy text.
Blocking a second, unrelated preference requires no change to agentkit — a test adds a taste with a rule and asserts the refusal, with the hook's own source byte-identical before and after.
A taste's named override lets the command through when it is used deliberately, and a mistyped override value fails closed rather than silently disabling the rule.
A fresh clone of a project carrying a vendored source honors the external policy with the network unreachable — nothing is fetched at read time — and a project taste of the same name still wins.
A reference source past its 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.
A durable correction that contradicts an existing taste ends as exactly one modified file whose diff shows the old preference becoming the new one — never a second taste on the same topic.
A one-off exception leaves the taste byte-identical: the override is used in the command and nothing is written.
A global correction made in one repository ends as an MR against the central source plus lock bumps elsewhere — not as a project taste in the repository where it happened.
A correction whose scope is genuinely unclear lands in the private set, and promoting any taste to the public set is only ever an owner-approved MR — never automatic.
Two external sources listed in order resolve a same-name: conflict to the later entry, in a test that fails if the order stops mattering.
An interrupted or corrupt refresh leaves the previous copy byte-identical — a test kills a fetch mid-flight and asserts the served policy is unchanged.