docs: add the working-on-officer guide
The orientation layer above platform/CLAUDE.md and capabilities/CLAUDE.md: which of the three directories a change belongs in, the two-repo git rules, how to run and verify without disturbing the running server, and the task system's conventions — including the ones capabilities/CLAUDE.md omits, like INPUT_INCLUDE and inline: ask. Also records failure modes found by running things rather than reading them: the vision model inventing text for images that have none, Whisper's translate being English-only, and the Host header that protected routes require. Lives here rather than at the deployment root so it is versioned and reaches every install; the root CLAUDE.md points at it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6224f6be51
commit
1192aa23fc
@@ -0,0 +1,188 @@
|
||||
# Working on Officer
|
||||
|
||||
The guide for anyone — human or agent — changing this deployment. It assumes you are working from the
|
||||
root of the install (the directory holding `platform/`, `capabilities/` and `data/`), which is where
|
||||
agent sessions start.
|
||||
|
||||
Three directories sit there, and knowing which one a change belongs in is most of the job:
|
||||
|
||||
```
|
||||
officer/
|
||||
├── platform/ the application — a git repo
|
||||
├── capabilities/ what the agent can do — a separate git repo
|
||||
└── data/ runtime state — NOT version controlled
|
||||
```
|
||||
|
||||
Officer is a self-hosted personal platform: an AI agent, a terminal, a file browser, a code editor,
|
||||
email, chat channels, a remote desktop and dashboards, behind one web app. **It serves exactly one
|
||||
person — the owner of this server.** There is no tenancy, no roles, no other users. If a question
|
||||
turns on "which user", the answer is the owner.
|
||||
|
||||
`platform/` and `capabilities/` each have their own `CLAUDE.md` with detail. This file is the layer
|
||||
above them: where things live, how to change them safely, and the things that are true of the running
|
||||
system but written down nowhere else.
|
||||
|
||||
---
|
||||
|
||||
## Which directory does this change belong in?
|
||||
|
||||
**`capabilities/` — almost always start here.** Tasks, tools, skills, processes. It is *data*: plain
|
||||
directories of Markdown and scripts, read fresh on every request. Adding a task, changing what a task
|
||||
does, renaming a category — none of that needs a code change or a restart.
|
||||
|
||||
**`platform/` — only when the mechanism itself is missing.** If a task needs a form control that
|
||||
doesn't exist, or an endpoint that isn't there, that's platform work. Adding a *capability* is not.
|
||||
|
||||
**`data/` — never edit by hand.** `DATA_PATH`. Holds the owner's managed home, per-account email
|
||||
SQLite stores, job logs, the queue, sidecar state. It is not backed up by git; deleting things here
|
||||
destroys the only copy.
|
||||
|
||||
A useful test: **would this differ between two Officer installs?** Domain, paths, credentials → `.env`.
|
||||
Which tasks exist and what they're called → `capabilities/`. Everything else → `platform/`.
|
||||
|
||||
## Git
|
||||
|
||||
Both repos are on **`master`**, both push to `gitea.pastilhas.dev`. There is one branch; per-instance
|
||||
differences live in `.env`, never in tracked files.
|
||||
|
||||
**Commit and push both repos** when a change spans them — a task usually pairs with the platform
|
||||
support it needs, and a half-pushed pair leaves the deployment inconsistent.
|
||||
|
||||
Keep history linear: `git pull --rebase`, not `git merge`. The remote moves — the owner develops on
|
||||
this box too — so expect to rebase before pushing. Say so before force-pushing anything.
|
||||
|
||||
Commit messages: simple lowercase, no prefixes, explaining *why*.
|
||||
|
||||
---
|
||||
|
||||
## Running and checking your work
|
||||
|
||||
The server runs under pm2 as `officer`, plus sidecars (`officer-claude`, `officer-opencode`,
|
||||
`officer-email`, `officer-pty`, `officer-vnc`). `pm2 list` shows them; `pm2 logs officer` follows.
|
||||
|
||||
**Don't restart the owner's server to test.** Boot your own on a spare port instead — the running
|
||||
instance holds `PORT` from `.env` (9010):
|
||||
|
||||
```bash
|
||||
cd platform && PORT=59999 bun --env-file=.env src/server.tsx
|
||||
```
|
||||
|
||||
Before claiming anything works:
|
||||
|
||||
```bash
|
||||
cd platform
|
||||
bunx tsgo --noEmit # must be 0 errors — the tree is clean, keep it that way
|
||||
bun test # must stay green
|
||||
bun format # prettier, changed files only
|
||||
```
|
||||
|
||||
### Calling the API by hand
|
||||
|
||||
Protected routes validate the `Host` header against `PUBLIC_URL`, so a plain localhost request gets
|
||||
**403 Invalid origin**. Send the real host explicitly:
|
||||
|
||||
```bash
|
||||
TOKEN=$(bun --env-file=.env -e \
|
||||
'import { sign } from "./src/servers/jwt.ts"; console.log(await sign({ id: 1, email: "<owner>" }, "10m"))')
|
||||
curl -H "Authorization: Bearer $TOKEN" -H "Host: rezio.pastilhas.dev" http://127.0.0.1:9010/api/tasks
|
||||
```
|
||||
|
||||
This trips people up repeatedly. It is also why script tasks are handed `OFFICER_API_HOST`.
|
||||
|
||||
### Services this box depends on
|
||||
|
||||
| port | what | used by |
|
||||
|------|------|---------|
|
||||
| 9010 | Officer itself | — |
|
||||
| 9002 | Kokoro TTS | text-to-speech |
|
||||
| 8178 | whisper.cpp | transcription |
|
||||
| 1234 | vision model (Qwen2.5-VL) | OCR |
|
||||
| 5432 | Postgres | the database |
|
||||
|
||||
URLs live in Settings → System, stored in the `server_config` table — **not** in `.env`. If
|
||||
transcription or OCR fails, check the service is up before reading any code.
|
||||
|
||||
---
|
||||
|
||||
## The task system
|
||||
|
||||
This is what most requests will be about. Tasks appear in the file browser's right-click menu under
|
||||
**Run Task**, grouped into submenus by category.
|
||||
|
||||
A task is a directory under `capabilities/tasks/<slug>/` with a `TASK.md` — frontmatter plus a body —
|
||||
and, for script mode, a sibling `run.sh` / `run.py` / `index.ts`. **The directory name is the task's
|
||||
identity**; renaming it breaks every reference to it.
|
||||
|
||||
`capabilities/CLAUDE.md` documents the format. It is accurate but **incomplete** — the following are
|
||||
used heavily by real tasks and appear nowhere in it:
|
||||
|
||||
| convention | what it does |
|
||||
|---|---|
|
||||
| `category: Video` | which submenu the task appears in. Order comes from `capabilities/categories.yaml`; an unlisted category still works, sorting after the listed ones. A category with no tasks never renders. |
|
||||
| `inline: true` | runs ephemerally in the modal instead of becoming a job |
|
||||
| `inline: ask` | offers both — **Run here** and **Run as job** |
|
||||
| `INPUT_INCLUDE` | newline-separated paths, injected by the modal on a multi-selection. **The single most used input in the library** — a task that ignores it silently processes the whole folder instead of the selection. |
|
||||
| `INPUT_GROUP_CONFIG` | per-track-layout config for folder-wide video runs |
|
||||
| input types `audio_tracks`, `subtitle_tracks`, `subtitle_edit` | render real track pickers, driven by probing the file |
|
||||
| `config.perGroupTracks`, `config.folderKeepAll`, `config.perGroupAllFiles` | opt into the folder track-picker behaviour — on **script** tasks, though the docs describe `config` as pipeline-only |
|
||||
|
||||
Inputs reach a script twice: positionally in `args` order, and as `INPUT_<NAME>` environment
|
||||
variables.
|
||||
|
||||
### Writing a task: copy an existing one
|
||||
|
||||
`convert-audio` is the plain template. `transcribe-audio` shows calling Officer's own API.
|
||||
`extract-audio` shows handling multiple streams. `optimize-size` shows the `INPUT_INCLUDE` helper.
|
||||
|
||||
Two ways a task does work:
|
||||
|
||||
1. **Run a tool directly** (`ffmpeg`, `jq`, …). Right when the work is just a command.
|
||||
2. **Call Officer's API**, when the logic already exists server-side — transcription and OCR do this,
|
||||
so language detection, model config and output conventions stay in one place. Script tasks are
|
||||
given `OFFICER_API_URL`, `OFFICER_API_HOST` and `OFFICER_AUTH_TOKEN` for exactly this.
|
||||
|
||||
House style for file-processing tasks, worth keeping consistent:
|
||||
|
||||
- Never delete or modify the source; write output beside it.
|
||||
- Handle a single file *and* a directory, recursively.
|
||||
- Honour `INPUT_INCLUDE`.
|
||||
- No caching. Re-running redoes the work and overwrites — and say so in the body, because it also
|
||||
overwrites edits.
|
||||
- End with a summary line and a non-zero exit if anything failed.
|
||||
|
||||
### Debugging "my task didn't run"
|
||||
|
||||
1. Does `TASK.md` parse? `bun -e` with `getTaskByDirName` from `platform/src/servers/api/tasks/task-files.ts`.
|
||||
2. Does the trigger match? Extension matching is on the **lowercased final extension only** — no
|
||||
globs, no MIME.
|
||||
3. Is the script executable, and does `bash -n run.sh` pass?
|
||||
4. Run it directly with `INPUT_*` set, before blaming the platform.
|
||||
|
||||
---
|
||||
|
||||
## Things that will bite you
|
||||
|
||||
- **The vision model hallucinates on textless images.** A blank white PNG came back as "The quick
|
||||
brown fox jumps over the lazy dog." OCR output on an image with no text is invented, confidently.
|
||||
Never point OCR at a photo library.
|
||||
- **Whisper's translate mode only outputs English.** It cannot translate into any other language.
|
||||
Transcription is therefore always in the source language; translating would mean a second pass
|
||||
through an LLM.
|
||||
- **Agents run unsandboxed as the owner, with `--dangerously-skip-permissions`.** Deliberate — it is
|
||||
the owner's own machine. Don't add a jail without being asked. The old bwrap sandbox was removed.
|
||||
- **Schema changes use `bun db:push`, not migrations.** `drizzle-kit migrate` has never run here;
|
||||
there is no `__drizzle_migrations` table. The schema code is the source of truth.
|
||||
- **`PUBLIC_BUILD_ENV` fails closed.** Unset means hardened — origin checks, rate limits and password
|
||||
rules all on. Only an explicit `dev`/`development` relaxes them.
|
||||
- **`index.html` is a template.** It carries `__PUBLIC_URL__` placeholders; `scripts/gen-index.ts`
|
||||
writes the real `index.gen.html` at predev/prestart. Edit the template, then re-run
|
||||
`bun gen:index` — `--watch` will not do it for you.
|
||||
|
||||
## Verify, don't assume
|
||||
|
||||
Much of what is written down about this codebase has drifted from what it does. Several bugs found
|
||||
recently were invisible in the code and obvious the moment something was actually run — a type
|
||||
assertion that lied about a third-party response, a language detector whose result was computed and
|
||||
then discarded, a menu entry pointing at a deleted screen.
|
||||
|
||||
Read the code, then run it. When reporting, say what you actually checked and what you didn't.
|
||||
Reference in New Issue
Block a user