Files
platform/plugins/EXTRACT-A-PLUGIN-PROMPT.md
pastilhasandClaude Opus 5 f977d26fef a ratchet on hardcoded colour, and the docs to match
The three previous attempts at this job died of endlessness, not difficulty. Nothing
stopped the next component arriving with its own `text-emerald-400` while an old one
was being fixed, so the number never fell for long.

`bun run check:theme` counts palette classes per directory against a committed
baseline. It does not care what the number is, only which way it moves. 942 across 21
directories today. Every directory can be paid down independently and none can quietly
regrow — which turns an infinite job into a finite one.

A baseline rather than a ban, deliberately: a rule forbidding palette classes outright
fails on its first run, and a check nobody can make pass is a check everybody turns
off.

Proven by adding one `text-emerald-400` and watching it fail with the directory, the
delta, and what to use instead.

── Documentation, which is part of the work rather than after it ──

`src/workspaces/theme/README.md` is new and is the contract: how to write a theme, all
84 declarations by family, why colours are unwrapped HSL triplets while surfaces are
complete CSS values, why assets are quoted paths, and the rules for components. Start
from Plain, not from Duck — every value you change is then one you decided about.

`plugins/EXTRACTING-A-PLUGIN.md` gains a rule beside the others: a plugin never
hardcodes a colour, and pays its debt down DURING the extraction. The counts are named
there — Soulseek 313, Invoices 95, Wallet 68 — because "some colours" does not convey
that it is two thirds of the platform's total. The kickoff prompt says the same thing
in the form a fresh agent will meet it.

That is what makes the plugin exemption safe rather than a way of losing the work: the
ratchet stops covering a directory the moment it leaves, so the rule has to travel with
it.

`THEMES-NOTES.md` rewritten to match reality — it still listed done things as open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 23:42:56 +00:00

99 lines
5.8 KiB
Markdown

# Kickoff prompt: extract a feature into a plugin
Paste everything below the line into a fresh agent, replacing `<FEATURE>` with the feature's name
(`photos`, `jellyfin`, `email`, `soulseek`, `wallet`, …). Nothing else needs to be said first.
---
You are extracting **`<FEATURE>`** out of the Officer platform and into a standalone plugin, the way
`offscale` and `music` were extracted before it. Work in `/home/pastilhas/officer-dev/platform`.
## Read these first, in this order, before touching anything
1. **`plugins/EXTRACTING-A-PLUGIN.md`** — the runbook. It is the authority: the rules, the order that
worked, the verification table, and a list of traps that each cost real time once. Read the section
"What changed on 2026-08-16" carefully; it contradicts an earlier instruction in the same file, and the
contradiction is deliberate.
2. **`plugins/example/README.md`** — what a plugin IS, stated as conventions, each one verified against
`src/servers/plugins/discover.ts` rather than remembered.
3. **`src/servers/plugins/`** — the system itself. `discover.ts` (what a directory must contain),
`manifest.ts` (the contract and `mountPrefix`), `mount.ts`, `install.ts`, `schema.ts`, `ecosystem.ts`.
When this prompt and that code disagree, the code is right.
4. **`CLAUDE.md`** — house rules for the whole repository. Style, the permission model, and why
`assertPermissionTotality` refuses to boot the server.
5. If `<FEATURE>` holds credentials: **`docs/secret-store.md`**.
Then read the two plugins that already did this. Both carry a `PLUGIN.md` recording their own extraction:
`gitea.officer.dev/plugins/music` and `gitea.officer.dev/plugins/offscale`. Music is the harder case and
the more useful one, because its boundary was dictated by imports pointing the wrong way.
## What "done" means
`<FEATURE>` is a public repository that a stranger's Officer can install from the marketplace, and the
platform contains no trace of it beyond dated history. Specifically:
- its own repo on `gitea.officer.dev/plugins/<FEATURE>`, public, cloning anonymously over https
- a `manifest.ts`, and `api/`, `db/`, `sidecar/`, `web/` as it needs them
- a README that works as both documentation and shop window
- a row in the marketplace catalogue, and a matching entry in `BUNDLED` in
`src/servers/plugins/marketplace.ts`
- installs, enables, disables, uninstalls and reinstalls on a live server, verified by you, not reasoned
about
- `bunx tsgo` clean, `bun test` with no new failures against the baseline you recorded before starting
## How to work
**Map before you move.** The first task is not a file move — it is finding every line of platform code
that names `<FEATURE>`, and every import pointing INTO it from `src/`. A plugin may import platform code
(`@@/…`); the reverse cannot happen. Anything in `src/` that reaches into `<FEATURE>` and cannot move
decides where the boundary goes. Music's boundary was chosen by two such imports, found by reading the
graph rather than by reasoning about what music "is". Report what you find before you start moving.
**Ask before deciding anything that is not mechanical.** The owner would rather answer a question than
review a wrong assumption. Say what you would do and why, and wait, when: a piece cannot cleanly move; a
permission's shape is not obvious; the split would change behaviour for a member; or the feature wants
something the plugin contract does not offer (a global overlay, a websocket, a component export — the
answers are no, not yet, and no, and the runbook says why).
**Verify on the live server, and purge between attempts.** Uninstall deliberately keeps the directory and
the tables, so a second install is not a fresh one. Use the Marketplace screen's "Delete … and all its
data", then install from the catalogue. That is the only run that resembles a stranger's machine.
**Do not restart the platform yourself.** Hand the owner the command. `pm2 restart officer` breaks their
chat session, and they will run it when they are ready.
**Report honestly.** Say what you verified and what you did not. "I did not test the member path" is
useful; a claim you did not check is not. If you assert a behaviour, name the file and line, or the
command whose output you read.
## When you get stuck
Blockers go in writing, not into a workaround. Write them down, say plainly that you are blocked and on
what, and move to the next piece. The runbook's "Still open, platform-wide" section lists problems that
are already known — check there before treating one as new.
## Finishing
Commit and push both repositories — the plugin's and the platform's — including if it went badly. A
reverted commit with an explanatory message is worth more than a clean history that hides an abandoned
approach. Commit messages: simple lowercase, no prefixes, and say what moved, what it found, and what is
still open.
Then update `plugins/EXTRACTING-A-PLUGIN.md`: add what this extraction taught, and correct anything it
told you that turned out to be wrong. The runbook is the thing that makes the next one cheaper, and it is
only worth that if each extraction leaves it more accurate than it found it.
## Colour is part of the job
The feature you are extracting almost certainly hardcodes Tailwind palette classes — `text-emerald-400`,
`bg-zinc-800`, `border-red-500/30`. Replace them with semantic tokens as you go: `text-success`,
`bg-muted`, `border-destructive`, `bg-busy/15`. They follow the theme; a palette number does not.
Read `src/workspaces/theme/README.md` for the contract and `src/workspaces/components/Data/tone.ts` for the
closed set of state tones. `bun run check:theme` counts what is left.
Do it during the extraction rather than after. You are already rewriting every import in these files, and
once the directory leaves the platform the ratchet stops covering it.