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>
189 lines
9.1 KiB
Markdown
189 lines
9.1 KiB
Markdown
# 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.
|