Files
platform/plugins/EXTRACT-ATELIER-PROMPT.md
T
pastilhas adc45c4343 atelier brief: port the ui, do not frame it
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.
2026-08-20 03:29:05 +00:00

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

  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.