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.
93 lines
4.1 KiB
Markdown
93 lines
4.1 KiB
Markdown
# 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
|
|
```
|
|
|
|
```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.
|
|
|
|
```markdown
|
|
---
|
|
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.
|