Compare commits

...
10 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 3f873fe4c2 regenerate: /todos left for the kanban plugin
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 03:29:59 +01:00
pastilhasandClaude Opus 5 79acaa5eb9 regenerate: /user joins the owner-only broadcast routes
real drift, and the only thing left in the diff once the generator stopped
rewriting `name` back to a label. the platform gained an owner-only `/user`
broadcast route and this had not caught up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 01:35:30 +01:00
pastilhasandClaude Opus 5 a57cb1b19d a skill for bots, which had none
the bot's OWN brief is thorough and lives in the bots directory, rewritten every
turn. what was missing is the doc for everyone else: what a bot is, where it
lives, why it has not fired, and how it differs from a commander.

the line is not size. one thing watched and one action is a bot; anything that
has to remember what it already handled is a commander. the bot brief tells bots
not to read the commander skill for exactly that reason.

carries the thing that is easy to lose: <sessionId>.bot.json is the ONLY link
between a conversation and a detached shell pid. a bot deleted on 2026-08-26 was
still polling a website eleven minutes later because that file did not exist yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 16:43:03 +01:00
pastilhasandClaude Opus 5 c24859c925 skill names are identifiers, not labels
lowercase-hyphenated and equal to the directory, per both harnesses' rules. the
human name moves to `title`. a skill named "Deploying Officer" was listed to an
agent and could not be loaded by it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 13:11:20 +00:00
pastilhasandClaude Opus 5 91596b7965 db:push can offer data loss at a prompt, and it waits
catching edge-pertento-bridge up by 215 commits, drizzle-kit stopped and asked
whether dav_app_passwords had been RENAMED to command_hq_spawned. both answers
destroy something: rename turns the DAV app passwords into the spawned-commander
table, create drops them.

the table had not gone anywhere — it moved to a plugin the same day. installing
the plugin first declares it and the question disappears.

records the whole sequence, including `< /dev/null` so an unattended push fails
rather than hangs on a prompt, the expected one-failure-then-seed-roles dance on
a host predating the roles table, and that the env var is OFFICERDEV_POSTGRES_URL
on several hosts rather than POSTGRES_URL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 08:49:20 +00:00
pastilhasandClaude Opus 5 750ed9ecc2 regenerate: /bots
picked up by docs:check rather than remembered — the generated platform skill
lists the routes, and a new one makes it stale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
2026-08-26 07:13:50 +00:00
pastilhasandClaude Opus 5 7f1d3c130a mock data to build a dashboard against
templates/sample-data, beside the app template. two shapes: daily orders with
seasonality worth charting, and subscriptions where "active" is a calculation
over dates rather than a column.

the point of the entry is the last line — use this rather than inventing rows.
an agent asked for a dashboard makes numbers up, and flat noise produces a
chart that proves nothing, which is exactly what a demo cannot afford.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
2026-08-25 20:17:11 +00:00
pastilhasandClaude Opus 5 b018aa52d2 cloning into the folder you are already in, and what it destroys
`git clone <url> .` works only into a genuinely empty directory. verified: one
file makes it fail, and so does one HIDDEN file.

which is not an edge case on this machine — a commander's directory holds
CLAUDE.md and state/, and a plugin's chat directory holds a CLAUDE.md the
platform wrote. cloning into either fails outright.

the fetch-into-place workaround is written down because it is the answer, and
so is the thing it does quietly: `git checkout FETCH_HEAD -- .` OVERWRITES a
colliding file without asking. verified by doing it in a directory that had a
CLAUDE.md and watching it become the template's. in a commander directory that
is the brief gone, and the commander would come back next turn not knowing what
its job is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
2026-08-25 20:13:28 +00:00
pastilhasandClaude Opus 5 8a1bcb03f5 a new app starts from the template, and its dev server runs in the background
two things an agent cannot discover and gets wrong by default.

FIRST: asked to create an app, it scaffolds one. there is a template —
templates/template-react on gitea — and it is the agreed starting point. it
documents itself, so this points at its README, CLAUDE.md and CONVENTIONS.md
rather than restating them; the one thing worth lifting out is README step 1,
the rename, because it costs thirty seconds at the start and is a chore
forever after.

SSH not HTTPS, verified both ways: the https remote prompts for a username and
dies in a non-interactive shell. the tailnet address is noted as the fallback
for when the public host's ssh is being rate-limited, which the deploy skill
already records as a recurring thing.

SECOND: asked to run it, it runs bun dev in the foreground, which blocks until
the tool times out and reports a working server as a failure. backgrounding is
both the only thing that works AND what the owner wants — a background job gets
officer's live panel, the same one they would see running it themselves, so
they can read the output and stop it without asking.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
2026-08-25 20:10:44 +00:00
pastilhasandClaude Opus 5 d963bd0a4c the one process an agent must never restart is its own
pm2 restart officer-claude-code tree-kills the sidecar, which takes claude and
the agent's shell with it. the tool call never returns, the conversation stops
dead, and the harness records it as an INTERRUPTED or REJECTED call — so the
transcript blames the user for something they did not do. that misattribution
is the expensive part: it sends everyone looking in the wrong place. the owner
had seen it before and could not reproduce it.

pm2 restart officer is safe and always was — sidecars are peers of the main
server, not its children. only the agent's own host is special.

and the trap underneath it, which is quieter: a change under
src/servers/sidecar/claude/ does not take effect when officer restarts,
because it lives in the process the agent is running inside. on 2026-08-25 a
credential scope was widened, officer was restarted four times, and the sidecar
carried the previous build all day while every report said otherwise. check the
process uptime before claiming a change is live.

the way out is to ask the owner, or to detach with setsid and say first that
the session is about to end.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
2026-08-25 18:45:02 +00:00
7 changed files with 300 additions and 10 deletions
@@ -0,0 +1,67 @@
---
name: Restarting your own host
description: Use before restarting, stopping or reloading any PM2 process, and whenever a change you made lives in a sidecar rather than in the main server. Names the one process that kills you, and what to do instead.
version: 1
---
**You are a child of `officer-claude-code`.** Your process tree is:
```
zsh ← every Bash command you run
claude --output-format stream-json …
bun run src/servers/sidecar/claude/user-instance.ts ← officer-claude-code, a PM2 process
PM2 God Daemon
```
So **`pm2 restart officer-claude-code` kills you, mid-command.** PM2 tree-kills the sidecar, which takes
`claude` and your shell with it. The tool call never returns. What the user sees is the conversation
stopping dead — and what the harness records is an *interrupted* or *rejected* tool call, so the transcript
blames them for something they did not do. That misattribution is the worst part: it sends everyone
looking in the wrong place.
The same applies to `pm2 stop`, `pm2 delete`, `pm2 reload` and `pm2 restart all` or
`ecosystem.config.cjs`, which include you.
## What is safe
**`pm2 restart officer` is safe.** The sidecars are PM2 PEERS of the main server, not its children, so
restarting `officer` costs you a socket reconnect and nothing else. Every other sidecar —
`officer-pty`, `officer-email`, `officer-notify` and the rest — is equally safe for the same reason.
Only your own host is special.
## The trap this actually sets
A change under `src/servers/sidecar/claude/` — the session credential, the manager, the transcript
reader — **does not take effect when you restart `officer`.** It lives in the process you are running
inside. So the honest failure mode is not a crash, it is this:
> You edit the file, restart `officer`, report the change as live, and it is not. The old code keeps
> running for hours while everything you say about it is wrong.
That happened on 2026-08-25: a credential scope was widened, `officer` was restarted four times, and the
sidecar carried the previous build all day. The symptom was a legitimate refusal that read as a bug.
**So check which process owns your change before you claim it is live.** `pm2 jlist` gives each process's
uptime; if the sidecar's start time predates your edit, your edit is not running.
## What to do instead
**Ask the user to run it.** One line, and they are already there:
```
pm2 restart officer-claude-code
```
Say why it is theirs to run rather than yours — that it ends this session — so it does not read as
helplessness.
If it genuinely must happen without them, detaching it from your process group is the only form that
survives, and **it still ends this conversation** — the restart completes, you do not:
```
setsid nohup sh -c 'sleep 2; pm2 restart officer-claude-code' >/dev/null 2>&1 &
```
Never do that silently. Tell the user first that the session is about to end, or they will see exactly
what this affordance exists to explain.
+101
View File
@@ -0,0 +1,101 @@
---
name: Starting an app
description: Use when asked to create, scaffold or start a new app or project, and when asked to run one. There is a template to begin from, and a specific way the owner expects a dev server to be started.
version: 1
---
**Do not scaffold a new app from scratch.** There is a template, it is the agreed starting point, and it
carries decisions that took a while to settle.
```bash
git clone git@gitea.officer.dev:templates/template-react.git <name>
```
Bun · Hono · React 19 · Tailwind v4 · shadcn/ui · Postgres with Drizzle. A JSON API at `/api` and a React
app at `/`, over one query layer.
**SSH, not HTTPS.** `https://gitea.officer.dev/…` prompts for a username and fails in a non-interactive
shell; the SSH remote is already authenticated. If SSH is being refused for a stretch — the public host is
behind a proxy that rate-limits it — `ssh://git@100.64.0.13:10023/templates/template-react.git` is the same
repository over the tailnet.
### Cloning into the folder you are already in
`git clone <url> .` works, **and only into a genuinely empty directory**. Verified: a single file makes it
fail, and so does a single HIDDEN one —
```
fatal: destination path '.' already exists and is not an empty directory.
```
That is not an edge case here. A commander's directory holds `CLAUDE.md` and `state/`; a plugin's chat
directory holds a `CLAUDE.md` the platform wrote. Cloning into either fails.
When the directory is not empty, fetch instead of cloning:
```bash
git init -q .
git remote add origin git@gitea.officer.dev:templates/template-react.git
git fetch -q --depth 1 origin HEAD
git checkout -q FETCH_HEAD -- .
git reset -q
```
**This overwrites a file that collides, without asking.** Also verified: doing it in a directory that
already had a `CLAUDE.md` replaced it with the template's. So look at what is there first, and if one of
those files is the platform's — a commander's brief, a plugin's chat brief — move it aside and put it back,
or clone somewhere else and copy in what you need.
## Then read its own documentation, and do step 1
The template documents itself. **`README.md` step 1 is a rename** — everything project-specific is called
`app`, and a case-sensitive search for `app`, `App` and `APP` finds all of it. Thirty seconds at the start,
a chore forever after. Do it before the first commit.
Its `CLAUDE.md` points at `CONVENTIONS.md` (React patterns, feature folders, manager hooks),
`docs/DATABASE.md` and `docs/SURFACES.md`. Read those rather than inferring house style from the code —
they exist so the same argument is not had twice.
Drop the template's git history and start your own unless told otherwise: it is a starting point, not an
upstream to track.
## Data to build against
There is mock data, so a dashboard has something real to show:
```bash
git clone git@gitea.officer.dev:templates/sample-data.git
```
`sales` — 6,402 daily orders over two years, with a Q4 lift, a summer dip, quiet weekends and one region
pulling ahead in year two. `subscriptions` — 480 accounts where "active" is a calculation over start and
cancel dates rather than a column, and small plans churn far more than large ones.
Both come as `.xlsx` and `.csv`, identical data. **The CSV needs no parser**, so prefer it unless reading
the spreadsheet is the point. Its `README.md` has every column.
Use this rather than inventing rows. Generated data with no shape in it produces a chart that proves
nothing, and that is what gets built when an agent makes numbers up.
## Running it — always as a background job
```bash
bun dev
```
**Start it in the BACKGROUND, never in the foreground.** Two reasons, and either one is sufficient:
- **A dev server does not exit.** Run in the foreground it blocks the turn until the tool times out, and
the timeout is reported as a failure of a thing that was working perfectly.
- **The owner wants to watch it.** A background job gets Officer's live panel — the same one they would
see running it themselves — so they can read the output, see the port, and stop it. Run any other way it
is invisible, and stopping it means asking you.
`bun dev` prints steadily, so the panel fills immediately. `predev` regenerates the index first; that is
normal and takes a moment.
The general rule about background output still applies — see the background-jobs affordance. It matters
less here only because this particular command is already talkative.
**Say the URL when it comes up.** The port is in `.env` and the template's README, and the first thing
anybody wants after "it's running" is where to look.
+90
View File
@@ -0,0 +1,90 @@
---
name: bots
title: Bots
description: >-
Use when the owner asks about a bot — what one is, how to start one, why one has not fired, how to stop
one, or why it is a bot rather than a commander. Also read it before setting one up FOR them. Carries
where a bot lives, the record that is the only thing able to stop it, and the two ways one dies quietly.
version: 1
---
# A bot is one thing watched and one thing done
"Notify me when x.com is down." "Tell the todo guy when NYT has a new headline." That sentence is the
whole specification, and **the first message arms it** — a bot that interviews the owner first has already
failed at the thing that makes it a bot.
## Bot or commander — the line, and it is not size
- **A bot watches ONE thing and reacts.** A shell loop that exits when a condition is true. No ledger, no
procedures, no dispatch policy. Arming is `run_in_background: true` on a script and nothing else.
- **A commander runs a JOB.** It classifies, it decides, it spawns agents, it keeps `JOB.md` and a record
of what it has already handled, and it is set up in a conversation before it is armed.
If the answer to "what does it do when it fires" is one action, it is a bot. If the answer needs a
paragraph, or the thing has to remember what it already did, it is a commander. Reach for
[[commander]] there — and note that the bot brief tells bots explicitly NOT to read it, because a bot that
starts building a dispatch loop has stopped being a bot.
## Where everything is
```
$DATA_PATH/<owner-email>/bots/
├── CLAUDE.md the brief every bot runs under — PLATFORM-OWNED, rewritten every turn
├── <name>.sh the watch itself, one per bot
├── <name>.state its baseline, so a restart resumes instead of re-firing
└── <sessionId>.bot.json the record: script, state, pids, what it watches
```
Every bot shares that one directory — `getBotsDir(email)` — which is why the record is named for the
session id rather than filed under it.
The chat list for them is `GET /api/chat/sessions?context=bots`. The context IS the address: no `cwd`
needed, because there is only one bots directory per owner.
## `<sessionId>.bot.json` is the only thing that can stop a bot
A watch is a detached shell process. Nothing else on the machine knows it belongs to a bot — not the
transcript, not the session list, and not the process table, which sees an anonymous `bash`. The record is
the only link between the conversation and the pid.
That cost was paid before the file existed: a bot deleted on 2026-08-26 was still polling a website eleven
minutes later, with nothing in the product able to find it. So when a bot re-arms, it rewrites the record
with the new pid, in the same turn. A record that is stale is worse than absent — it names a pid that may
now belong to something else entirely.
## Why a bot has not fired — check in this order
1. **Is the process alive?** Read the record, then `ps -p <pid>`. A watch dies with a reboot and nothing
re-arms it automatically; the bot re-arms itself on its next turn, which needs a message.
2. **Has it fired already and not re-armed?** The exit IS the signal. A bot that fired, reported, and
never re-armed looks identical to one that is watching quietly.
3. **Is the baseline wrong?** A "has it changed" watch that wrote its state before exiting will not fire
on the same change twice. That is correct, and it is also the usual reason a test does not reproduce.
4. **Is it watching the wrong thing?** `watches` in the record is the bot's own description of its job.
Compare it to what the owner actually asked for before debugging any further down.
## The two ways a bot dies quietly
- **A reboot.** The loop is a plain background process; nothing supervises it. The record survives, which
is what lets the next turn re-arm from it — so a bot that has been silent since a restart is not broken,
it is unstarted.
- **A fired-and-forgotten watch.** It exited, said its line, and the turn ended without re-arming. Act,
re-arm, record — in one turn — is the rule in the brief precisely because forgetting the third step is
invisible until the thing never fires again.
## Setting one up for the owner
Say the sentence and let the bot arm itself. The brief in that directory is thorough and current — it is
rewritten on every turn from `src/servers/api/bots/brief.ts`, so it cannot drift from the platform the way
a copy here would.
**Do not paste the mechanism into the conversation.** If you find yourself explaining the script shape, the
record, or the naming rules to a bot, it has not read its own brief and the fix is to point at it.
## Naming
A bot's automatic title is the owner's first sentence, truncated, typos included. It is a prompt, not a
name. Two to four words, the subject and what is watched about it — `newweb uptime`, `NYT front page`,
`gitea disk space`. Not "Website Monitor Bot": every bot is a monitor, so the word earns nothing in a list
of them.
+2 -1
View File
@@ -1,5 +1,6 @@
---
name: Commanders
name: commander
title: Commanders
description: >-
Use when the user asks about a commander — what one is, how to start one, why one is not firing, how much
one costs — or when they describe work that should keep happening while they are away ("watch X and do Y",
+2 -1
View File
@@ -1,5 +1,6 @@
---
name: Delegating the writing
name: delegating
title: Delegating the writing
description: >-
Use when a feature has been agreed and somebody has to type it — how to hand the writing to a cheaper
agent, what a brief has to contain, and how to review what comes back. Also read it before deciding NOT
+34 -1
View File
@@ -1,5 +1,6 @@
---
name: Deploying Officer
name: deploy
title: Deploying Officer
description: >-
Use when asked to deploy, release or roll out Officer, to watch for a release and deploy it, or when a
deploy has gone wrong — a host behind, a tag not noticed, a rollout that stopped halfway. Points at the
@@ -66,6 +67,38 @@ money. That is not hypothetical.
- **A dirty tree means TRACKED changes.** `git status --porcelain` counts untracked files; a stray script a
fast-forward could never touch is not a reason to skip a host. Use `--untracked-files=no`.
# `db:push` can offer data loss at a prompt, and it waits for an answer
A schema push on a host that is many commits behind can stop and ask whether a table was RENAMED. Both
answers destroy something, so neither is safe to guess and neither is safe to automate.
Seen 2026-08-27 catching `edge-pertento-bridge` up by 215 commits:
```
Is command_hq_spawned table created or renamed from another table?
❯ + command_hq_spawned create table
~ dav_app_passwords › command_hq_spawned rename table
```
drizzle-kit saw one table leaving the platform schema and another arriving, and could not tell a rename
from a coincidence. `rename` would have turned the DAV app passwords into the spawned-commander table;
`create` would have dropped them.
**The table had not gone anywhere — it had moved to a PLUGIN.** Caldav was extracted that day, so
`dav_app_passwords` belongs to `plugins/caldav/db/schema.ts` and reaches the barrel through
`gen-plugin-schemas`. Installing the plugin FIRST declares it, and the question disappears.
So, on a host far enough behind that a plugin extraction is in the gap:
1. Install the plugins whose tables are involved, **before** pushing.
2. Then `bun db:push < /dev/null` — stdin closed, so it can never hang waiting on a prompt in an
unattended run. A push that needs an answer fails instead, which is the outcome you want.
3. Expect ONE failure on a `role_permissions` foreign key, then `seed-roles.ts`, then push again. That
sequence is normal on a host predating the roles table.
**Back up the database first.** `pg_dump` against the URL in `.env` — and read the variable name rather
than assuming: several hosts use `OFFICERDEV_POSTGRES_URL`, not `POSTGRES_URL`.
# The report is an artefact, the lock is the record
Write the local record FIRST, then commit and push the report. If the push fails, say so and stop: the
+4 -7
View File
@@ -1,5 +1,6 @@
---
name: Officer platform
name: platform
title: Officer platform
description: >-
Use when the user asks what Officer can do, how to use a feature, what an endpoint is, why something is
refused, or how permissions and accounts work — "how do I start a commander", "can a member see my files",
@@ -41,7 +42,7 @@ diligence.
- API `/bug-report`
- **Notifications** (`notify`) — Push notifications to your devices
- API `/notify`
- owner only, even when granted: `/broadcast`
- owner only, even when granted: `/broadcast`, `/user`
- **Screen layouts and dashboards** (`dashboards`) — Where your own screen layouts and dashboards are saved
- API `/dashboards` · screen `/dashboards`
- **Command HQ** (`command-hq`) — Long-running commanders you started, organised into categories
@@ -51,8 +52,6 @@ diligence.
- **Memos** (`memos`) — Notes
- screen `/memos`
- **Calendar and contacts** (`calendar`) — Calendars, contacts, and the app passwords that sync them to a phone
- API `/caldav`, `/dav` · screen `/calendar`, `/contacts`
- **SendOff** (`sendoff`) — Send files to your own machines, and to accounts that have allowed you
- API `/sendoff` · screen `/sendoff`
@@ -61,7 +60,7 @@ diligence.
- **Terminal** (`terminal`) — A shell on this machine, as your own user
- API `/terminal` · socket `terminal` · screen `/terminal`
- **Chat** (`chat`) — The agent
- API `/chat`, `/agent-status`, `/claude-auth` · socket `chat`, `chat-notify` · screen `/chat`
- API `/chat`, `/agent-status`, `/claude-auth` · socket `chat`, `chat-notify` · screen `/chat`, `/bots`
- **Files** (`files`) — Your own home directory on this machine, and the code editor over it
- API `/file-browser`, `/upload` · screen `/files`, `/code-editor`
- a POST that is really a read, allowed at read level: `/download`
@@ -83,8 +82,6 @@ diligence.
- API `/app-store` · screen `/app-store`
- **Plugins** (`plugins`) — Install, enable and remove the plugins this server runs
- API `/plugins` · screen `/marketplace`, `/plugins`
- **Todo board** (`todos`) — The platform's own backlog — todo/ in the repo, browsed and edited at /todos
- API `/todos` · screen `/todos`
- **Server settings** (`server-admin`) — Server configuration, integrations and the activity log
- API `/server-settings`, `/integrations`, `/activity`, `/system-monitor` · screen `/settings/server`, `/settings/system`, `/activity`, `/system-monitor`
- **User management** (`user-admin`) — Accounts, roles and what each role may reach