# 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. **`docs/secret-store.md`** — Atelier holds an OpenAI key. 6. **`src/workspaces/officerdev/APP_CONVENTIONS.md`** and `HOOK_CONVENTIONS.md` — you are writing panels. ## 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 for one reason that survives the UI moving into Officer: generation is long-running, capped by `ATELIER_MAX_CONCURRENCY`, and holds a credential the platform must not. The UI is Officer's; the work is the sidecar's. ## 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 is PORTED into Officer. No proxy, no iframe, no sidecar-served UI.** Atelier was a separate server because that was the quick answer at the time; this extraction is what undoes that. It becomes part of Officer, not something Officer frames. It is smaller than the repository suggests: ``` src/apps/frontend 1,736 lines / 25 files 6 of those components/ui/* — shadcn Officer ALREADY HAS. Delete, import from @/components/ui. real work Generate · Gallery (+ Lightbox) · Collections · Providers ≈1,200 lines src/apps/web the htmx half — 8 files, DELETED, not ported ``` So: three panel apps — **Generate**, **Gallery**, **Collections** — under `plugins/atelier/web/`, each exporting `appRegistryMetas`, with a `layout.ts` composing them. Follow the frontend route conventions in `CLAUDE.md`: selection lives in the URL, rows are real links, no `usePanelChannel` for "which thing is open". The fourth feature, **Providers**, is not a panel — see the next decision. **2. Providers become Officer settings, with more than one.** `features/Providers/` is a settings screen, and Officer has a settings surface already. It moves there rather than becoming a panel. `src/servers/providers/` is already adapter-shaped — `types.ts`, `openai.ts`, `credentials.ts`, an `index.ts` that dispatches. Keep that shape and keep the seam: today OpenAI is the only adapter, and the point of moving the settings is that a second one is a new file rather than a new screen. **Do not add a second provider in this run** — leave the shape able to take one and say so in `PLUGIN.md`. **3. 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. **4. 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. **5. `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. **6. 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.) - **Generated images are served through an authenticated route**, like every other plugin artefact. Every image having "a permanent URL" was true of a standalone server with its own `PUBLIC_URL`; inside Officer the URL is behind the session. If something needs an image without one — an `` in an email, a share link — that is a blocker to write down, not a route to open. See the image-rendering affordance in `capabilities/affordances/officer/core/` for why an `` cannot carry a token. - 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.