Files
platform/plugins/EXTRACT-ATELIER-PROMPT.md

182 lines
9.9 KiB
Markdown

# 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 is watching
`/home/pastilhas/officer-dev/platform` and pulling into it as you push. Two processes in one working tree
is how work gets lost — on 2026-08-20 it nearly did.
```
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`.
**Push every commit straight to master**, from the worktree:
```
git push origin HEAD:master
```
The branch is only there to keep your files out of the other checkout. Master is what is being reviewed.
Push after every task — a reviewer reading five small commits can tell you what went wrong on the second
one; nobody can do that with one commit at the end.
**Your final commit message must contain `ATELIER-COMPLETE` on its own line**, and no earlier commit may
contain that string. It is the signal that the run is finished and the review can start. Do not use it to
mean "this part is done".
## 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 <paths>`, 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 `<img>` 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 `<img>` 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.