From 83cd7af156641ada009a8e24adb38d0b6378ad06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 16 Aug 2026 12:14:20 +0000 Subject: [PATCH] a kickoff prompt for the next extraction, and a runbook that is true again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 --- plugins/EXTRACT-A-PLUGIN-PROMPT.md | 84 ++++++++++++++++++++++++++++++ plugins/EXTRACTING-A-PLUGIN.md | 81 +++++++++++++++++++++++++++- 2 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 plugins/EXTRACT-A-PLUGIN-PROMPT.md diff --git a/plugins/EXTRACT-A-PLUGIN-PROMPT.md b/plugins/EXTRACT-A-PLUGIN-PROMPT.md new file mode 100644 index 00000000..bf26ebe9 --- /dev/null +++ b/plugins/EXTRACT-A-PLUGIN-PROMPT.md @@ -0,0 +1,84 @@ +# Kickoff prompt: extract a feature into a plugin + +Paste everything below the line into a fresh agent, replacing `` with the feature's name +(`photos`, `jellyfin`, `email`, `soulseek`, `wallet`, …). Nothing else needs to be said first. + +--- + +You are extracting **``** 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 `` 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 + +`` 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/`, 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 ``, and every import pointing INTO it from `src/`. A plugin may import platform code +(`@@/…`); the reverse cannot happen. Anything in `src/` that reaches into `` 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. diff --git a/plugins/EXTRACTING-A-PLUGIN.md b/plugins/EXTRACTING-A-PLUGIN.md index cc133a60..db80489c 100644 --- a/plugins/EXTRACTING-A-PLUGIN.md +++ b/plugins/EXTRACTING-A-PLUGIN.md @@ -1,5 +1,8 @@ # Extracting a feature into a plugin +> **Handing this to a fresh agent?** `plugins/EXTRACT-A-PLUGIN-PROMPT.md` is the kickoff prompt — paste it, +> replace the feature name, and it points back here. This file is the authority; that one is the way in. + The runbook, written the day offscale became the first one. Follow it for music, then for the rest. **This directory holds documentation, not plugins.** Every plugin is its own repository as of @@ -91,7 +94,7 @@ on uninstall. Only deleting a plugin's source can lose its data. ## Verification — run all of it ``` -bun test # 757 pass, 10 pre-existing failures. Any 11th is yours +bun test # record the baseline BEFORE you start. Any new failure is yours pm2 restart officer ``` @@ -107,7 +110,7 @@ Then through `/plugins`, watching PM2 and the browser at each step: | disable | route 404s, sidecar stops, **tables and rows survive** | | enable | comes back | | uninstall | route gone, `pm2 list` loses it, **data still there** | -| `bun db:push` while uninstalled | `No changes detected` — data survives | +| `bun db:push` while uninstalled | **do not run this — see "What changed on 2026-08-16"** | | install again | identical to the first install | A normal refresh is enough; the shell is `no-store`. When the log's last line appears, the bundle exists. @@ -173,6 +176,80 @@ Reopen this only for a feature where the overlay is the whole product, and expec --- +## What changed on 2026-08-16. Read this before following the steps above + +A day of installing plugins onto a machine that had never had one found four things. The runbook above is +still right about the shape of an extraction; these change how you verify it. + +**The install no longer runs `bun db:push`, and you should not either.** A plugin's tables are created by +`pushPluginSchema`: `drizzle-kit generate` against the plugin's schema alone, into an empty directory, +then the statements applied one at a time with "already exists" skipped. It cannot emit a DROP of +anything. + +The global push is a trap on any machine holding tables the current schema does not declare — a plugin +that is not on disk, anything left from a rename. drizzle-kit sees a new table appearing while others are +due to be dropped and asks, interactively, whether it is a rename. **That prompt needs a TTY.** `--force` +does not answer it, `< /dev/null` does not answer it, piping newlines does not either — all measured. With +no terminal it applies NOTHING and exits 0, so an install reports success and the plugin fails its first +query with 42P01. `--force` would be worse if it worked: the destructive statements are `drop table` for +every plugin whose directory is absent. + +So: the row in the verification table that says to run `bun db:push` while uninstalled is struck out. The +property it was checking — uninstall does not destroy data — is now checked by reinstalling and finding +your rows still there. + +**Use `purge` to test the install twice.** Uninstall deliberately keeps the directory and the tables, so a +second install is not a fresh one: the marketplace hides a plugin whose directory exists, and the tables +are already there. The Marketplace screen has "Delete and all its data" — it drops the tables, +deletes the directory and regenerates the barrel, and afterwards the plugin installs from the catalogue +exactly as a stranger's machine would. **That is the only honest test of an install.** + +**The install now verifies its own tables.** After the push it asks Postgres whether each declared table +exists and fails the install naming what is missing. If your extraction reports "schema push reported +success but did not create …", the push is the problem, not your schema. + +**Tables with no owner are now reported.** The Marketplace screen lists any table no code on the machine +claims, with a row estimate. During an extraction this is how you notice you renamed a table and left the +old one behind — it is exactly the state that produced the trap above. + +--- + +## The marketplace is part of the job now + +An extracted plugin is not finished when it installs from disk. It has to be installable from +`marketplace.officer.dev`, which is three things: + +1. **A public repository that clones anonymously over https.** Check it with + `GIT_TERMINAL_PROMPT=0 git clone --depth 1 /tmp/probe` — an SSH remote only works on the machine + that published it, and the catalogue rewrites SSH to HTTPS precisely so this cannot be got wrong. +2. **A README.** It renders on the plugin's page in the store AND in the platform's install pane, so it is + the shop window as much as the documentation. `plugins/example/README.md` is the model, and the house + voice applies: state what is missing as plainly as what works. +3. **A catalogue row.** `bun --env-file=.env scripts/marketplace-sync.ts` reads every plugin directory's + `manifest.ts` and README and upserts them. Add a matching entry to `BUNDLED` in + `src/servers/plugins/marketplace.ts` as well — that is the offline fallback, and a machine with no + internet shows it. + +`docs/marketplace.md` is the design; `docs/marketplace-todo.md` is what it does not do yet. + +**The screen is `/marketplace` now, not `/plugins`.** The API kept its name. + +--- + +## Two more rules, learned the same day + +**The platform must not know what your plugin is about.** Not just its code — its VOCABULARY. Ten +comments in `src/` still explained what Headscale was, months after the code left, and each one was +correct and in the wrong repository. When you finish, grep the platform for your feature's domain words, +not only its identifiers, and keep only dated history: "X was here until " is a record; explaining +what X does is a leak. + +**Do not add a spawn that trusts `$PATH`.** An install died on `Executable not found in $PATH: "bunx"`, +because PM2 starts the platform with a PATH that has `bun` and not `bunx`. Resolve a path +(`node_modules/.bin/…`, `process.execPath`) as `pushPluginSchema` now does. + +--- + ## Which one next No decision has been made. What the tree says, for whoever picks it: