Files
platform/plugins/EXTRACT-A-PLUGIN-PROMPT.md
T
pastilhasandClaude Opus 5 83cd7af156 a kickoff prompt for the next extraction, and a runbook that is true again
`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>
2026-08-16 12:14:20 +00:00

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

  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.