release 2026.08.23.06

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
This commit is contained in:
2026-08-24 02:15:40 +00:00
co-authored by Claude Opus 5
parent b6d6de7e53
commit 2646842c32
2 changed files with 77 additions and 62 deletions
+75 -60
View File
@@ -1,10 +1,10 @@
# 2026.08.23.06
A plugin's capabilities are read where they live instead of being copied into `capabilities/`, dev tools
become a property of a role rather than a hardcoded name, and Whisper learns 23 more languages.
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 IS a schema change, and `bun db:push` cannot apply it.** Read "The schema change" below before
running anything — it will stop and ask a question no terminal is there to answer, and then exit 0.
**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".
@@ -13,40 +13,37 @@ twice. See "The orphan copies".
## What is in it
- **A plugin's capabilities are LOADED, not copied.** Installing used to publish a plugin's affordances,
notifications and skills into `capabilities/` — a repository meant to be forked, so a fork accumulated
files it did not author. They are now read from each installed and enabled plugin's own directory. A
copy also went stale on `plugins:update` and survived `plugins:disable`; neither can happen now.
- **Notification codes moved from the path to the manifest.** They were derived from the copy's location,
`<publisher>/<appName>/file.yaml`. Read in place there is no such path, so every plugin code would have
vanished silently. The identity comes from the manifest instead, which is validated at discovery.
- **Roles phase 3.** `rolePermitsDevTools()` was `role === 'Developer'` — one line gating a Postgres login
role with CREATEDB and a rootless Docker daemon. Two boolean columns carry it now, so a role created at
runtime can hold dev tools. `provisioning_script` is dropped rather than left dead.
- **Whisper priming prompts for 23 more languages**, Swedish included. The table had `en` and `pt`; every
other language got no prompt, which is why non-English transcription had nothing steering its
punctuation or diacritics. Inert on every host until an STT server is configured, which none is.
- **The mailbox picker** in the email plugin, reaching the multi-account backend that shipped without a UI.
- **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/<email>/commanders/<id>/` 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 change
## The schema changes
Two columns added to `roles`, one dropped.
Two tables. **Run this SQL first**, then `bun db:push` to confirm it took.
**`bun db:push` will not do this.** Dropping a column while adding another makes drizzle-kit ask whether
it is a rename:
**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`.
```
Is grants_postgres column in roles table created or renamed from another column?
```
With no terminal it bails, the wrapper still exits 0, and the `postdb:push` seed then fails against an
unchanged schema — reporting a missing column several steps from the thing that actually stopped. Verified
on `officer-dev` on 2026-08-23. See `src/databases/CLAUDE.md`.
Run this **first**. It is idempotent and safe to run twice.
Both blocks are idempotent and safe to run twice.
```sql
-- Nothing ever wrote this column. Prove it rather than assume, before dropping it.
-- ── roles: dev tools become a property of the role ──────────────────────────────────────────────
DO $$
DECLARE used int;
BEGIN
@@ -61,71 +58,89 @@ 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 '{}';
```
Then verify **by query**:
**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;
```
Four rows. **`Developer` must have both flags true and the other three both false** — that is exactly the
behaviour `rolePermitsDevTools` had, preserved. If Developer comes back false, the seed did not run: fix
it before restarting, or Developer accounts silently lose their Postgres role and Docker daemon on their
next provisioning.
**`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.
`provisioning_script` must be absent from the column list.
`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/`
— a copy published by the old mechanism.** 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.
Remove the copy on those three hosts. `deadpool-rezio-bridge` does not have one; check anyway.
**`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 "$(dirname "$OFFICER_PLATFORM_DIR")/capabilities" # or the capabilities dir beside the checkout
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/` and that is what now gets read.
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.** It is written by the platform's own bootstrap every boot, not published by a
plugin. This release gitignores it, and the ignore arrives with the capabilities pull in step 4.
**`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**, then the verification query. Stop on the `RAISE EXCEPTION` and report it.
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 above**, on the three hosts that have one.
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 now
has `psql` at `/opt/homebrew/opt/libpq/bin/psql` and a `.zshenv` that puts it on a non-interactive PATH,
so the SQL above can be run there directly rather than through Bun.
**`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 confirm the agent still has its skills.** The preamble is built from a new code path —
**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. Ask it what skills it has; it should name the commander, deploy and platform
ones, plus anything a plugin ships on that host.
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**.
On a host with the gitea plugin, confirm the gitea affordance appears **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, per `docs/servers.md`.
- **`officer-dev`** — skip it. The schema is already applied there and verified by query, its capabilities
repository is already clean, and restarting `officer` on the host running the rollout ends the rollout.
Name it as skipped and say it is current.
- **`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.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "officer",
"version": "2026.08.23.05",
"version": "2026.08.23.06",
"private": true,
"type": "module",
"workspaces": [