# 2026.08.23.06 Swedish, a plugin's capabilities read where they live instead of being copied, dev tools as a property of a role, a commander as a directory rather than a session, and uploads that land atomically. **There are TWO schema changes and `bun db:push` can apply NEITHER of them.** Both stop and ask a question no terminal is there to answer, and then exit 0. Read "The schema changes" before running anything. **There is also a one-off cleanup on three hosts**, and skipping it means the agent reads one affordance twice. See "The orphan copies". **No dependency change.** `bun.lock` is untouched. **Do not run `bun install`.** ## What is in it - **Swedish.** `sv-SE`, complete across all three catalogues — six shipped locales now. - **Parity checks itself.** The three parity tests listed their locales by hand, so adding a sixth added a file none of them looked at; they read the directory now. They also check both directions — the old check only asked whether every English key was present, so a rename reported half of itself. - **A plugin's capabilities are LOADED, not copied.** Affordances, notifications and skills are read from each installed and enabled plugin's own directory. `capabilities/` is meant to be forked and was accumulating files nobody authored. A copy also went stale on update and survived a disable. - **Roles phase 3.** `rolePermitsDevTools()` was `role === 'Developer'` gating a Postgres role and a rootless Docker daemon. Two boolean columns carry it now, so a role created at runtime can hold dev tools. - **Installing a plugin grants it to every role.** Deny-by-default meant every install needed a second decision on another screen, which mostly got forgotten. The owner revokes instead. Rows, not a rule change — absence still denies. - **A commander is a directory, not a session.** Its own `$DATA_PATH//commanders//` with a `CLAUDE.md` written at creation. `commander-block.ts` is gone. - **Uploads land atomically** — written aside and renamed, so a watcher never sees a half-written file. Same for SendOff. - **Whisper priming prompts for 23 more languages.** Inert until an STT server is configured; none is. ## The schema changes Two tables. **Run this SQL first**, then `bun db:push` to confirm it took. **Why not `db:push`:** `roles` drops a column while adding two, and `command_hq_sessions` does the same shape — drizzle-kit cannot tell a drop-plus-add from a rename, so it asks. With no terminal it bails, the wrapper still exits 0, and the `postdb:push` seed then fails against an unchanged schema. Verified on `officer-dev` twice. See `src/databases/CLAUDE.md`. Both blocks are idempotent and safe to run twice. ```sql -- ── roles: dev tools become a property of the role ────────────────────────────────────────────── DO $$ DECLARE used int; BEGIN SELECT count(*) INTO used FROM roles WHERE provisioning_script IS NOT NULL; IF used > 0 THEN RAISE EXCEPTION 'STOP: % role(s) have a provisioning_script. Do not continue; report this.', used; END IF; EXCEPTION WHEN undefined_column THEN NULL; -- already dropped by a previous run END $$; ALTER TABLE roles ADD COLUMN IF NOT EXISTS grants_postgres boolean NOT NULL DEFAULT false; ALTER TABLE roles ADD COLUMN IF NOT EXISTS grants_docker boolean NOT NULL DEFAULT false; ALTER TABLE roles DROP COLUMN IF EXISTS provisioning_script; -- ── command_hq_sessions: the row owns its identity, the transcript id becomes a pointer ────────── -- Every existing commander is DELETED rather than migrated. This is deliberate and the owner's call: -- a commander's identity, directory and CLAUDE.md are all new, so migrating means writing a path that -- runs once and never again. Re-arm afterwards; it also exercises the flow this release exists to fix. DELETE FROM command_hq_sessions; ALTER TABLE command_hq_sessions ALTER COLUMN session_id DROP NOT NULL; ALTER TABLE command_hq_sessions ADD COLUMN IF NOT EXISTS dir_name text NOT NULL DEFAULT ''; ALTER TABLE command_hq_sessions ALTER COLUMN dir_name DROP DEFAULT; ALTER TABLE command_hq_sessions ADD COLUMN IF NOT EXISTS reference_paths text[] NOT NULL DEFAULT '{}'; ``` **Tell the owner what you deleted** before running it, and put it in the report — a commander is somebody's standing job, and "we removed yours" is not a footnote. On `officer-dev` it was one, `Platform Deployer`, already deleted there. Then verify **by query**, not by an exit code: ```sql SELECT name, is_system, grants_postgres, grants_docker FROM roles ORDER BY name; SELECT column_name, is_nullable FROM information_schema.columns WHERE table_name = 'command_hq_sessions' ORDER BY column_name; ``` **`Developer` must have both flags true and the other three both false.** If Developer comes back false the seed did not run — fix it before restarting, or Developer accounts lose their Postgres role and Docker daemon at their next provisioning. `provisioning_script` must be absent. `command_hq_sessions` must show `dir_name` and `reference_paths`, with `session_id` nullable. ## The orphan copies **`alpha`, `fofos` and `production-pertento-bridge` each have `capabilities/affordances/officerdev/gitea/`** — published by the mechanism this release removes. The gitea plugin still ships that affordance in its own directory, and affordances are deliberately NOT de-duplicated, so after this release both load and the agent reads the same guidance twice. ```bash cd git status --porcelain # expect only ?? extensions/ afterwards rm -rf affordances/officerdev ``` Safe because it is a copy: the source is `plugins/gitea/affordances/`, which is what now gets read. `deadpool-rezio-bridge` has none; check anyway. **`extensions/` stays.** The platform's own bootstrap writes it every boot. This release gitignores it, and the ignore arrives with the capabilities pull in step 4. ## Every host 1. `git fetch origin --tags` then `git merge --ff-only deploy/2026.08.23.06` 2. **The SQL above**, both blocks, then the two verification queries. Stop on the `RAISE EXCEPTION`. 3. `bun db:push` — expect **no changes and no prompt**. If it asks anything, the SQL did not take. 4. `bun run plugins:update` — this pulls the capabilities repository too, which is how the new `.gitignore` arrives. 5. **The orphan removal**, on the three hosts that have one. 6. `pm2 restart officer` `officer` only at step 6. Step 4 restarts each plugin's own sidecar, which belongs in the report. **`macbookpro`:** append `/opt/homebrew/bin` to PATH for the `bun` commands, run `pm2` separately. It has `psql` now and a `.zshenv` that puts it on a non-interactive PATH, so the SQL can be run there directly. ## What to check, this release **Open a chat and ask the agent what skills it has.** The preamble is built from a new code path — `capability-sources.ts` resolving roots per session — and the failure mode is an agent that quietly knows less rather than an error. It should name the commander, deploy and platform skills, plus anything a plugin ships on that host. On a host with the gitea plugin, its affordance should appear **once**. **The language picker should offer six.** Switch to Swedish and look at a *panel*, not just the shell. **Creating a commander should produce a directory.** `$DATA_PATH//commanders//` holding `CLAUDE.md` and `state/`. The setup pane asks for a *reference path* now, not a working directory. No direct SQL beyond the block above is needed, and none is authorised. ## Hosts - **`edge-pertento-bridge`** — skipped. Down, per `docs/servers.md`. - **`officer-dev`** — skip it. Both schema changes are already applied and verified by query there, its capabilities repository is clean, and restarting `officer` on the host running the rollout ends the rollout. Name it as skipped and say it is current.