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>
5.8 KiB
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
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.plugins/example/README.md— what a plugin IS, stated as conventions, each one verified againstsrc/servers/plugins/discover.tsrather than remembered.src/servers/plugins/— the system itself.discover.ts(what a directory must contain),manifest.ts(the contract andmountPrefix),mount.ts,install.ts,schema.ts,ecosystem.ts. When this prompt and that code disagree, the code is right.CLAUDE.md— house rules for the whole repository. Style, the permission model, and whyassertPermissionTotalityrefuses to boot the server.- 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, andapi/,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
BUNDLEDinsrc/servers/plugins/marketplace.ts - installs, enables, disables, uninstalls and reinstalls on a live server, verified by you, not reasoned about
bunx tsgoclean,bun testwith 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.