Files
platform/plugins/CLAUDE.md
T
pastilhasandClaude Opus 5 d8c97b6814 affordances: the file format, where they live, and what is still open
Design only — nothing reads any of this yet.

Markdown with YAML frontmatter, like every other item in capabilities/.
Frontmatter is for the PLATFORM, the body is for the agent: `name` becomes the
heading in the compiled block, `description` says when to reach for it and is
not sent, `version` is an INTEGER to match capabilities/agents/ rather than the
semver plugins use. A plugin-contributed affordance sits near both numbers and
they mean different things — the integer is the revision of that text, the
semver is the plugin's.

They live at capabilities/affordances/<publisher>/<plugin>/, with `core` in the
plugin slot for the platform's own. A plugin ships an affordances/ directory
and install copies it in — the same shape as assets/ and publishAssets.

No `surface` field. It was drafted and dropped: for image rendering it would
mean what the CLIENT renders, for gitea what TOOLING exists on the machine.
Two axes wearing one name. Everything loads unconditionally until that is
worked out, which is fine at two files and will not be at twenty — and this
codebase has already been bitten by a field that validated itself and was never
consumed (unauthenticatedPaths).

TODO.md carries the build list and the two deferred questions, including the
honest one: "declared where implemented" is the rule, and a hand-authored file
in a repository is not that. Accepted because prose reaches every harness and a
registry reaches one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 07:19:40 +00:00

7.8 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

DESIGN, NOT BUILT. Agreed 2026-08-19. There is no registry, no API and nothing to call yet. It is written down now because the name and the boundary are the parts that are expensive to change later, and because a plugin author who invents a different word for this will be hard to merge with afterwards.

What one is

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.

Why it needs to exist

The platform hosts the agent and never introduces itself. An agent gets a shell, a filesystem and a prompt; everything about the surface it is speaking through has to be smuggled in as prose somebody maintains by hand.

The worked example is images. Officer's chat renders markdown images — ChatImage is wired into react-markdown — but an <img> carries no bearer token, so it can only load what the browser can fetch unauthenticated. A fresh agent knows none of that. It writes ![](/home/me/chart.png), nothing renders, and nothing tells it why. The knowledge currently lives in a hand-written CLAUDE.md on one machine, which says "use a public URL" — a workaround, one step removed from the actual constraint, with nothing keeping the two in sync.

Three properties make prose the wrong container for this:

  • It drifts. The file describes a workaround; the code has a reason. Neither knows about the other.
  • It is per-machine. It lives in one repository, copied by hand.
  • A plugin cannot contribute to it. Install a plugin that adds a surface and no agent learns it exists — which is the case that matters here, because a plugin is exactly a new surface arriving at runtime.

The boundary

Three neighbours, easily confused:

answers
tool what the agent can do — it acts, something happens
permission what this account may reach
skill instructions the agent follows
affordance what the client will render, accept or ignore

Two tests settle most cases:

  1. Would it change if the same agent talked to a different client? A phone app, a terminal, a webhook, a cron run with no human at all. If yes, it is an affordance. Rendering an image is; reading a file is not.
  2. Does the agent have to guess it today? If the honest answer is "it writes the markdown and hopes", that is a missing affordance.

The rule that makes it worth having

An affordance is declared where it is implemented. ChatImage declares that images render, because ChatImage is what makes them render. The moment it becomes a list maintained somewhere else, it is the hand-written CLAUDE.md problem again with a better name — and this time with a build step.

Same posture as permissions/registry.ts, and for the same reason: one enumeration, owned by the code it describes, which cannot quietly disagree with reality.

The file

Markdown with YAML frontmatter, like every other item in capabilities/. Frontmatter is for the PLATFORM; the body is for the agent.

---
name: Image rendering
description: Use when you have an image to show the user in chat.
version: 1
---

`![alt](https://…)` renders as an image. **The user's device fetches the URL**, so it must be
reachable from there — a local path, `file://`, or anything behind authentication renders nothing,
and fails silently.
field
name becomes the heading in the compiled block, so the agent can index it
description when to reach for it. Costs the agent nothing — it is not sent
version an INTEGER, not semver, matching capabilities/agents/ (agent-files.ts parses it that way). A plugin-contributed affordance sits near two version numbers and they mean different things: this integer is the revision of this text, the plugin's semver is the plugin's. Do not reconcile them

Keep the body short. Every affordance is loaded into every conversation, so it is charged to the context budget of every turn. State the constraint, name the silent failures, say what to do instead.

There is deliberately no surface field yet. It was drafted and dropped: for the image affordance it would mean what the client renders, and for the gitea one below what tooling exists on the machine — two different axes wearing one name. Everything is loaded unconditionally until that is worked out, which is fine at two files and will not be at twenty. See TODO.md.

Where they live

capabilities/affordances/
  officer/core/          ← core, installed by the setup script
    image-rendering.md
  officer/gitea/         ← contributed by the gitea plugin on install
    gitea.md

<publisher>/<plugin>, with core occupying the plugin slot for the platform's own. A plugin ships an affordances/ directory in its repository; installing copies it in, uninstalling removes it — the same shape as assets/ and publishAssets.

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.