Swedish, plugin capabilities read where they live, dev tools as a property of a role, a commander as a directory, and atomic uploads. TWO schema changes and bun db:push can apply neither. Both drop a column while adding others, which is the shape drizzle-kit cannot tell from a rename — it asks, bails with no terminal to answer, and the wrapper still exits 0. The plan carries both blocks, guarded and idempotent, tested against a scratch database: they apply, re-apply cleanly once already applied, and the roles guard still refuses a host where anything wrote a provisioning_script. The command_hq block DELETES every commander rather than migrating one. That is the owner's call and the plan says to tell people what was removed and put it in the report — a commander is somebody's standing job, not a row. Also carries a cleanup no plan format covers: three hosts hold a copy of gitea's affordance published by the mechanism this release removes, and affordances are deliberately not de-duplicated, so without the removal the agent reads the same guidance twice. The verification step is a conversation rather than a query. The capability refactor's failure mode is an agent that quietly knows less, which no schema check can see. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
7.8 KiB
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()wasrole === '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/<email>/commanders/<id>/with aCLAUDE.mdwritten at creation.commander-block.tsis 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.
-- ── 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:
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.
cd <the capabilities directory beside the checkout>
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
git fetch origin --tagsthengit merge --ff-only deploy/2026.08.23.06- The SQL above, both blocks, then the two verification queries. Stop on the
RAISE EXCEPTION. bun db:push— expect no changes and no prompt. If it asks anything, the SQL did not take.bun run plugins:update— this pulls the capabilities repository too, which is how the new.gitignorearrives.- The orphan removal, on the three hosts that have one.
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/<email>/commanders/<n>/ 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, perdocs/servers.md.officer-dev— skip it. Both schema changes are already applied and verified by query there, its capabilities repository is clean, and restartingofficeron the host running the rollout ends the rollout. Name it as skipped and say it is current.