the substance was in plugins/CLAUDE.md marked "DESIGN, NOT BUILT", which stopped being true the same day it was written — load.ts reads them, publish.ts copies a plugin's in on install, and claude-manager compiles them into every session. moved to docs/affordances.md and corrected; plugins/CLAUDE.md keeps the plugin-specific part and points here. the new part is a third test, earned today by nearly failing it twice: CAN IT BE FIXED INSTEAD? an affordance is for something irreducible about the surface, and a defect is not irreducible. both candidates drafted today failed it. "links open in the same tab" was one missing line in MD_COMPONENTS. "AskUserQuestion never returns an answer" was true when the code comment was written and already fixed by the time i read it — that one would have shipped a permanent instruction to work around a bug that no longer existed, in a file whose whole purpose is to be believed without checking. names the failure mode: an affordance describing a defect calcifies it. once there is a document explaining the workaround, the defect has a rationale, and the rationale outlives anyone's memory that it was a bug. also records two things that are not built and read as if they were: nothing is scoped, so every affordance is in every conversation, and affordanceBlock() has exactly one caller — an opencode turn gets none at all.
4.1 KiB
Plugins
EXTRACTING-A-PLUGIN.md is the runbook for turning a platform feature into a plugin, and
EXTRACT-A-PLUGIN-PROMPT.md is the way to hand that to a fresh agent. This file is for the concepts a
plugin author needs that are not procedure.
This directory holds documentation, not plugins. Every plugin is its own repository; plugins/<name>/
appears by git clone when somebody installs one, and is gitignored here.
Affordances
docs/affordances.md is the authority — what one is, the three tests for whether something qualifies,
the file format and how they reach the agent. Read it before writing one. What follows is only the part
that is specific to shipping one from a plugin.
An affordance is something the surface on the other end of the conversation offers the agent — declared by the code that implements it, and read by the agent at runtime. Not what the agent can do; what the client will honour.
A plugin ships an affordances/ directory of markdown-with-frontmatter in its repository. Installing
copies it to capabilities/affordances/<publisher>/<appName>/, uninstalling removes it — the same shape
as assets/ and publishAssets, and the namespace comes from where it lands rather than from anything
inside the file.
The test that catches most bad ones: can it be fixed instead? If the answer is yes, fix it. An
affordance describing a defect gives that defect a rationale, and the rationale outlives everyone's memory
that it was a bug. See the two worked examples in docs/affordances.md.
Keep it short — every affordance is loaded into every conversation on this machine, including turns that have nothing to do with your plugin.
A plugin-contributed one, for reference## Notifications — the same shape, for push
A plugin can also declare what its push notifications MEAN. Same mechanism, different store:
capabilities/notifications/
officerdev/music/
catalogue.yaml
nowPlaying:
title: 'Now playing'
route: 'officer://music/{id}'
That becomes the code officerdev/music:nowPlaying. A push carries the code and integers — never content —
and the device renders the title and follows the route. The point is that adding a notification is a
server change and never an app build, because the mapping is synced to the device rather than compiled
into it.
The namespace comes from where the file lands, not from what it says. Writing officer/core:test as a
key in your own catalogue produces <you>/<yourplugin>:officer/core:test, not the core code — the same
rule as mountPrefix, and pinned by test in src/servers/notifications/catalogue.test.ts.
An entry needs a title; body, target, route and thread are optional, thread defaults to your
namespace, and unknown keys are dropped rather than forwarded to the device. Broken YAML in one plugin's
catalogue is skipped without affecting anyone else's.
See docs/push-notifications.md.
A plugin-contributed one, for reference
This is the gitea plugin's, and it shows what these are for: the agent cannot discover from the
filesystem that tea is authenticated, or that it should prefer git for the things git already does.
---
name: Gitea
description: Use when working with git repositories, or with Gitea issues and pull requests.
version: 1
---
For anything git already does — clone, fetch, pull, push, branches, tags — use plain `git`. It talks
to the server directly.
For anything git does not — pull requests, issues, comments, releases, creating a repository — use
`tea`. It is already logged in as the current user, with that user's own permissions. Do not
configure it, and do not ask the user for a token.
On the word
capability was taken — CAPABILITIES is the permission registry. skill was taken twice: by
capabilities/skills/ here and by the wider AI ecosystem.
affordance is the exact term for what an environment offers an actor, and the precision earns the
slight awkwardness: rendering an image is not a skill the agent has, it is something the client on the
other end permits. Keeping that distinction in the name saves explaining it every time.