From 1192aa23fcd0dd51f7cb4856fa1a87c8ea8bd239 Mon Sep 17 00:00:00 2001 From: brunorezio Date: Sun, 26 Jul 2026 04:19:28 +0100 Subject: [PATCH] docs: add the working-on-officer guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/working-on-officer.md | 188 +++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 docs/working-on-officer.md diff --git a/docs/working-on-officer.md b/docs/working-on-officer.md new file mode 100644 index 00000000..c510355a --- /dev/null +++ b/docs/working-on-officer.md @@ -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: "" }, "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//` 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_` 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.