Compare commits

..
8 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
6 changed files with 177 additions and 10 deletions
@@ -19,6 +19,33 @@ shell; the SSH remote is already authenticated. If SSH is being refused for a st
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
@@ -32,6 +59,24 @@ 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
+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