# Kickoff prompt: extract Atelier into a plugin Written 2026-08-20 for an **unattended overnight run**. That changes two things from `EXTRACT-A-PLUGIN-PROMPT.md`, which assumes the owner is awake: - **It does not say "ask and wait".** Nobody is there. Decisions that would normally be questions are pre-made below, with reasoning, so the run does not stall on them. - **It assumes you may be restarted.** Commit after every task, so whoever picks this up — including you, an hour later, with no memory of it — finds a clean tree and a readable history. Paste everything below the line into a fresh agent. --- You are extracting **Atelier** out of `~/officer-dev/dockers/officer-atelier` and into an Officer plugin, the way `music`, `offscale` and `gitea` were extracted before it. **Work in a git worktree, not the main checkout.** Another agent may be working in `/home/pastilhas/officer-dev/platform` tonight. On 2026-08-20 two agents with bypassed permissions ended up in one working tree and only avoided losing work because one of them noticed and stopped. Do not arrange that again: ``` git -C /home/pastilhas/officer-dev/platform worktree add /home/pastilhas/officer-dev/atelier-work -b atelier-plugin ``` Everything below happens in `~/officer-dev/atelier-work`. ## Read first, in this order 1. **`plugins/EXTRACTING-A-PLUGIN.md`** — the runbook and the authority. Read "What changed on 2026-08-16" carefully; it contradicts an earlier instruction in the same file, deliberately. 2. **`plugins/example/README.md`** — what a plugin is, as conventions. 3. **`src/servers/plugins/`** — `discover.ts`, `manifest.ts`, `mount.ts`, `install.ts`, `ecosystem.ts`. Where this prompt and that code disagree, **the code is right**. 4. **`CLAUDE.md`** — house rules, the permission model, and why `assertPermissionTotality` refuses to boot. 5. **`plugins/gitea/`** — because of the UI decision below. It solves the same problem. 6. **`docs/secret-store.md`** — Atelier holds an OpenAI key. ## What Atelier is Image generation as a service: a provider-shaped API over OpenAI's image endpoints, storing what it makes so every image has a permanent URL. Bun · Hono · Postgres/Drizzle · htmx with Hono JSX · a React SPA. ``` 84 ts/tsx files, ~6,400 lines tables collections, provider_settings (its own `atelier` database) routes /generations, /generations/:id, /edits, /adapters, /:id/items, /:id/items/:generationId, mask env BUN_PORT, PUBLIC_URL, POSTGRES_URL, ATELIER_DATA_DIR, OPENAI_API_KEY, ATELIER_MAX_CONCURRENCY runs as pm2 `officer-atelier`, cwd ~/officer-dev/dockers/officer-atelier ``` It is a **sidecar** plugin, not an API-only one, for the two reasons that always decide this: it does long-running work with a concurrency cap, and it holds a credential the platform must not. ## Decisions already made — do not re-open these Each of these would normally be a question. They are answered so you can keep moving. If you find evidence that one is wrong, **write it in `BLOCKERS.md`, follow the decision anyway, and carry on** — a half-applied decision is worse than a wrong one consistently applied. **1. The UI: follow gitea, do not port it.** Atelier ships htmx _and_ a React SPA with its own build (`build-web.ts`, `gen-index.ts`). Rewriting 84 files into panels is not an overnight job and is not the point of this extraction. Instead: the sidecar keeps serving its own UI, the plugin proxies it same-origin with `createSidecarProxy`, and a single panel renders it in an iframe. `plugins/gitea` does exactly this and its `sidecar/rewrite.ts` records the traps. **Read that file before writing any of it.** **2. The database: check the rows, then decide.** Atelier uses a separate `atelier` Postgres database, deliberately — `src/databases/atelier_db/drizzle.config.ts` says why. Plugin tables normally live in `officer_db` with a prefix. - Count the rows in `collections` and `provider_settings` first. - **If both are empty**, fold them into `officer_db` as `atelier_collections` and `atelier_provider_settings`, following `plugins/*/db/schema.ts`. Two tables do not earn a second database. - **If either has rows**, keep the separate database for now and record the migration as a follow-up in `BLOCKERS.md`. Losing someone's generated images to a schema decision at 4am is not a trade worth making. **3. Artefacts: `$OFFICER_ROOT/data/atelier/`.** Generated images are files, and `ATELIER_DATA_DIR` currently points wherever its `.env` says. A plugin directory is a git clone that an update replaces, so artefacts kept there vanish on upgrade with nothing logging it. `plugins/kiosk/artefacts.ts` has the same problem solved and the comment explaining it. **4. `OPENAI_API_KEY` goes in the secret store**, not `.env`. `putSecret`/`getSecret` landed 2026-08-19; `bun run secret:set atelier-openai-key` puts it there. Read with `getSecret`, **never `getKey`** — `getKey` mints a random value for an absent purpose, which for a foreign credential means silently authenticating with a fake. Fall back to env when the store has nothing, so an install that predates this keeps working. **5. One permission, `atelier`, `kind: 'app'`.** Image generation is not `execution` — it does not run as the owner's OS user in the owner's home. ## How to work **Map before you move.** The first task is not a file move. Find every line in the platform that names atelier, and every import pointing INTO it. A plugin may import platform code (`@@/…`); the reverse cannot happen. Report what you find before moving anything. **Commit after every task, and push.** You may be restarted at any point by a scheduler that has no idea what you were doing. A clean tree and a commit message are how the next attempt — possibly you — knows where it is. Commit messages: simple lowercase, no prefixes. **Do not restart the platform.** `pm2 restart officer` breaks the owner's chat session. Write the command down instead; they will run it. **Do not `git add -A`.** Add the paths you touched. This has caused two commits to swallow another agent's work in one day. **Format only what you touched** — `bunx prettier --write `, never `bun format`. **`bunx tsgo` must be clean** before every commit. Not `tsc`. ## When you get stuck Write it in `BLOCKERS.md` at the worktree root — what you were doing, what stopped you, what you did instead, and whether it is reversible. Then **move to the next task**. Do not wait, do not invent a workaround that changes behaviour without saying so, and do not stop the run. If invented content ends up anywhere durable — copy, defaults, seed data — mark it `[placeholder]` so it can be found later with a grep. ## Done means - `plugins/atelier/` with `manifest.ts`, `api/router.ts`, `sidecar/index.ts`, `web/panels.ts`, `web/layout.ts`, `db/`, `assets/icon.png`, `assets/splash.png`, and its own git repository. - It installs from the Marketplace onto a machine that has never had it, and the panel renders. - `bunx tsgo` clean, `bun test` passing. - A `PLUGIN.md` in the plugin recording what moved, what did not, and why — the way `music` and `offscale` did. This is the part that is worth most in three months. - `BLOCKERS.md` listing every decision you made without being able to ask. ## What you may NOT do - No unauthenticated routes. A plugin cannot serve one; `UNPROTECTED_API_PREFIXES` is hardcoded in the platform. If Atelier needs one, that is a blocker, not something to work around. (`plugins/kiosk` is stuck on exactly this — read its README before concluding you have found a way.) - No changes to `src/servers/plugins/` to make Atelier fit. If the contract does not accommodate it, that is a finding worth more than the workaround. - No second agent, no second worktree, no background job that writes to this tree.