`plugins/EXTRACT-A-PLUGIN-PROMPT.md` is meant to be pasted into a fresh agent with one word changed. It deliberately does not restate the runbook — it orders the reading, says what "done" means, and sets the three behaviours that matter: map the import graph before moving anything, ask rather than assume, and verify on a live server after a purge. ── The runbook needed correcting before it could be handed to anyone ── Its verification table told the reader to run `bun db:push` while the plugin was uninstalled and expect "No changes detected". Following that today would hang on drizzle-kit's interactive rename prompt, or — with `--force` — drop the tables of every plugin not currently on disk. Struck out, with the reasoning, and the property it was checking is now checked by reinstalling and finding the rows still there. Also folded in: installs create tables through `pushPluginSchema` and verify them afterwards; `purge` is the only honest way to test an install twice, because uninstall keeps the directory and the tables on purpose; orphan tables are now reported on screen; and the marketplace is part of the job — a public repo that clones anonymously, a README that renders in both the store and the install pane, a catalogue row, and a `BUNDLED` entry for machines with no internet. Two rules from today that generalise beyond extraction: the platform must not carry the plugin's VOCABULARY, not just its code — ten comments explained what Headscale was months after it left, each correct and in the wrong repository — and nothing in the install path may trust `$PATH`, which cost an install with `Executable not found in $PATH: "bunx"`. The stale "757 pass, 10 failures" is now "record the baseline before you start", which does not go out of date. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5.1 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.