the first draft said follow gitea — sidecar serves its own ui, plugin proxies it, one panel renders an iframe. wrong for this one. atelier was a separate server because that was the quick answer at the time, and this extraction is what undoes that: it becomes part of officer, not something officer frames. and it is smaller than the repository suggests. 1,736 lines of react, but six of the twenty-five files are shadcn components officer already has, and the htmx half is eight files that get deleted rather than ported. what is left is generate, gallery, collections and providers — about 1,200 lines. providers is a settings screen, not a panel, so it moves to officer's settings surface. src/servers/providers/ is already adapter-shaped, so the point of moving it is that a SECOND provider becomes a new file rather than a new screen. the brief says explicitly not to add one in this run — leave the seam, say so in PLUGIN.md. still a sidecar, for the one reason that survives the ui moving: 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. added the trap that follows from all of this: "every image has a permanent URL" was true of a standalone server with its own PUBLIC_URL, and inside officer that url is behind the session. an <img> cannot carry a token — the core affordance says why — so anything needing an unauthenticated image is a blocker to record, not a route to open.
9.4 KiB
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
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.plugins/example/README.md— what a plugin is, as conventions.src/servers/plugins/—discover.ts,manifest.ts,mount.ts,install.ts,ecosystem.ts. Where this prompt and that code disagree, the code is right.CLAUDE.md— house rules, the permission model, and whyassertPermissionTotalityrefuses to boot.docs/secret-store.md— Atelier holds an OpenAI key.src/workspaces/officerdev/APP_CONVENTIONS.mdandHOOK_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
collectionsandprovider_settingsfirst. - If both are empty, fold them into
officer_dbasatelier_collectionsandatelier_provider_settings, followingplugins/*/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/withmanifest.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 tsgoclean,bun testpassing.- A
PLUGIN.mdin the plugin recording what moved, what did not, and why — the waymusicandoffscaledid. This is the part that is worth most in three months. BLOCKERS.mdlisting every decision you made without being able to ask.
What you may NOT do
- No unauthenticated routes. A plugin cannot serve one;
UNPROTECTED_API_PREFIXESis hardcoded in the platform. If Atelier needs one, that is a blocker, not something to work around. (plugins/kioskis 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 incapabilities/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.