From 18f71369596d0052e67c3bf4430d083a2ecf6656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Thu, 13 Aug 2026 01:33:50 +0000 Subject: [PATCH] add user and officer.dev claude.md files --- officer.dev/CLAUDE.md | 241 ++++++++++++++++++++++++++++++++++++++++++ user/CLAUDE.md | 43 ++++++++ 2 files changed, 284 insertions(+) create mode 100644 officer.dev/CLAUDE.md create mode 100644 user/CLAUDE.md diff --git a/officer.dev/CLAUDE.md b/officer.dev/CLAUDE.md new file mode 100644 index 0000000..c62759f --- /dev/null +++ b/officer.dev/CLAUDE.md @@ -0,0 +1,241 @@ +# CLAUDE.md — officer.dev workspace root + +This is the working root for **developing Officer**: authoring **capabilities** and making +**platform** changes that support them. Sessions started in this cwd are for exactly that loop — +write/fix a capability, and when it needs platform support, fix the platform too. + +**You (Claude) are a standing collaborator here.** The owner spins up new chats from this cwd to build +capabilities and correct the platform. Assume that recurring intent. Be proactive about the runtime +facts below — most wasted time in past sessions came from forgetting them. + +## Layout (root is NOT a git repo; the two subdirs are) + +``` +officer.dev/ +├── platform/ # the Officer app — one Bun process. Git repo. See platform/CLAUDE.md +├── capabilities/ # OFFICER_ITEMS_DIR — file-based store of items. Git repo. See capabilities/CLAUDE.md +├── data/ # DATA_PATH — managed homes, attachments, job logs (data/jobs/.log) +└── (marketplace/, mobile-apps/, Lyrics/, DOCS/, … — not usually in scope) +``` + +Read `platform/CLAUDE.md` and `capabilities/CLAUDE.md` — they are authoritative for their subtrees. +This file captures the **cross-cutting operational reality** that neither of those spells out. + +## Runtime facts that WILL bite you (learned the hard way) + +- **The server runs via PM2 as `bun start` — production, NO `--watch`.** Process `officer` + (`NODE_ENV=production bun src/server.tsx`). So **platform TypeScript changes do NOT hot-reload.** + After editing anything under `platform/src` (backend *or* frontend), it needs a + **`pm2 restart officer`** to take effect. Forgetting this looks exactly like "my fix didn't + work." Confirm the reload before diagnosing further. + - **NEVER run pm2 yourself — tell the owner the exact command to run.** After a platform change, + print the precise `pm2 restart ` command that reloads ONLY the process(es) the change + needs (for platform TS edits that is `pm2 restart officer` alone — never the whole + `ecosystem.config.cjs`, which bounces every sidecar). The owner runs it. +- **Capabilities ARE hot — no restart.** Files under `capabilities/` are re-read from disk on each + run / request (Pi per agent session, the app per request). Edit `TASK.md` / `index.ts` / + `categories.yaml` and it's live immediately. This asymmetry is the #1 source of confusion: + the capability change shows up, the platform change doesn't, and it looks like a partial failure. +- **Frontend is served bundled from `index.gen.html`** (`platform/src/apps/officer-web/index.gen.html`, + imported by `src/server.tsx`, regenerated by `platform/scripts/gen-index.ts` at boot). After a + platform restart the browser can still hold a **stale JS bundle** → do a hard refresh + (Ctrl/Cmd-Shift-R). "Backend correct but UI unchanged" is almost always this. +- **React Query caches** `tasks` / `task-categories` with `staleTime: 60_000`. Menu/category changes + lag up to a minute or until the view remounts. Hard refresh forces it. +- **`pm2 restart officer` no longer kills the agent session.** Fixed and merged on 2026-07-30; verified + running. The agent is its own PM2 peer (`officer-agent`) and the Anthropic proxy is honestly named + (`officer-anthropic-proxy`) — they were one misnamed entry, `officer-claude`, which is how the *old* + claim in this file ("restarting officer doesn't disturb the agent") was both wrong and believed: the + process that runs `claude` was spawned by the main server, so PM2's tree-kill took it down every time. + A restart now costs the agent a socket reconnect, not the session, and output produced while officer is + down survives — the sidecar writes to `chat_session_events` itself. There is no `officer-claude`. +- **Officer is twenty PM2 entries** (verified 2026-08-07 with `pm2 jlist`): `officer`, + `officer-anthropic-proxy`, `officer-agent`, `officer-opencode`, `officer-email`, `officer-pty`, + `officer-vnc`, `officer-music`, `officer-vault`, `officer-slskd`, `officer-headscale`, + `officer-transmission`, `officer-invoiceshelf`, `officer-wallet`, `officer-photos`, `officer-notify`, + `officer-caldav`, `officer-memos`, `officer-jellyfin`, `officer-gitea`. + **`pm2 list` shows twenty-two** — `mybiblepal-api` and `mybiblepal-audio` are a different project sharing + this PM2 daemon, and nothing here should touch them. + Never `pm2 restart ecosystem.config.cjs` — it bounces every + sidecar including the vault. Use `--only `, or restart the single process the change needs. + This list goes stale every time a sidecar lands; `pm2 jlist` is the source of truth. + +## Environment (this machine) + +- `OFFICER_ITEMS_DIR=/home/pastilhas/dockers/officer.dev/capabilities` ← items live here +- `DATA_PATH=/home/pastilhas/dockers/officer.dev/data` +- `HOME_DIR=/home/pastilhas` and `HOME=/home/pastilhas` — **equal here** (matters for `~` expansion). + `getOwnerHomeDir(email) = process.env.HOME_DIR ?? DATA_PATH//home`. Where task runs actually + execute = `getOwnerHomeDir` (the real login home), NOT the managed `getHomeDir`. +- `POSTGRES_URL=postgresql://postgres:postgres@127.0.0.1:5432/officer_dev` (single DB, Drizzle). + Schema changes: edit schema code → `bun db:push` (no migrations, no `__drizzle_migrations`). + Multi-column uniqueness is declared with **`uniqueIndex`, never `unique().on(...)`** — drizzle-kit + mis-diffs named composite unique *constraints* and re-creates them on every push, which used to stop + `db:push` on an unanswerable truncate prompt. Rules in `platform/src/databases/CLAUDE.md` → + "Composite keys". +- Typecheck with **`bunx tsgo`** (not tsc). Keep it clean. +- **Installs are frozen.** `bunfig.toml` sets `[install] frozenLockfile = true` in `platform/` and + `monorepo-mobile/`, so `bun install` resolves from `bun.lock` and fails rather than picking up a + version nobody chose. Changing a dependency means `bun install --no-frozen-lockfile` **deliberately**, + reading the lockfile diff, and committing it. Never add that flag to a script or CI to silence the + error. Full reasoning — and the 2026-08-04 npm compromise that prompted it — in `platform/CLAUDE.md` + → "Installs are frozen". + +## Task-runner data flow (script mode) — the mental model + +File browser right-click → `TaskRunnerDialog` → `TaskRunnerModal`: +- `cwd = { root:'home', path: currentPath-without-leading-slash }` — the folder the browser is + **showing** (parent of the clicked entry). +- `entryName` = the clicked entry; `entry_path` autofill = `~//` (**tilde** form). +- For a **directory trigger**, the `ScriptRunner` must descend into the entry: + `cwd = cwd.path ? `${cwd.path}/${entryName}` : entryName` (mirrors Pipeline/Agentic runners). +- `POST /jobs {taskDirName, inputs, cwd, action}` → job row in `pipeline_jobs`. +- `execute-script.ts`: cwd resolved as `isAbsolute(cwd) ? cwd : join(getOwnerHomeDir, cwd)` — so **cwd + must be home-relative WITHOUT a leading slash**. Inputs passed BOTH as positional `args` (in + `task.args` order) and as `INPUT_` env vars (uppercased). Runs `bun run index.ts` in a temp + dir — **only `index.ts` is materialized**, so script tasks must be fully self-contained (no sibling + imports). +- `inline` tri-state on the task: `false`/absent = background job; `true` = ephemeral modal; + `'ask'` = modal offers the choice. + +## Jobs view architecture + +- `pipeline_jobs.mode` is a TS-only enum (`'pipeline' | 'script' | 'agentic'`) — no DB CHECK. +- `JobDetailPanel` fetches `GET /jobs/:id`, reads `mode`, dispatches: + `script → ScriptJobDetail`, `download → DownloadJobDetail` (legacy history), else `PipelineJobDetail`. +- `GET /jobs/:id` returns the whole row (`db.select().from(pipelineJobs)` → includes `progress`). +- **Progress protocol**: a script prints `@@officer:progress@@ {json}` lines. `execute-script`'s + `pumpStdout` line-buffers stdout, plucks those lines out (→ `progress` events, NOT written to the + `.log`), and passes everything else through to `data/jobs/.log`. The job manager persists the + latest progress to the `progress` column (periodic flush, `PROGRESS_FLUSH_MS = 3000`). + `ScriptJobDetail` renders `PhaseBar`s when `progress` has `{meta, dl}` shape (`isDownloadProgress`). +- The persisted `.log` is the human narration; the bars come from the sentinel. Keep those two + channels separate — never let sentinels leak into the log. +- killTree: Stop → SIGTERM the process tree, SIGKILL after ~2s grace → scripts can cooperatively + abort on SIGTERM/SIGINT. + +## Frontend navigation — the URL is the source of truth + +Before building or changing any screen that *selects* something, read +**`platform/docs/navigation-audit.md`**. It is the authority, and the rules it sets are easy to violate +by accident because most of the existing app predates it: + +- Selection state lives in the URL (`useParams` / `useSearchParams`), **not** in a `usePanelChannel` + or `useGlobal` channel. Channels are for refresh/signal buses only. +- Rows are real ``s; nav chrome uses **react-router's ``** so active state comes from + the router instead of hand-rolled JS. `src/workspaces/components/NavLink.tsx` is a *different* thing + — a `` wrapper that carries the global query string, with no active state. +- Screens come in route pairs (`/x` + `/x/:param`) with a single `` guard. + A master list with a live preview uses `?selected=`, because the detail route is the full page. + +Full rationale and the per-case detail are in `platform/CLAUDE.md` → "Frontend route conventions". + +## Reference capability: `download-media` (the worked example) + +Script task (`mode: script`, `language: typescript`, `inline: ask`, directory trigger, `category: +Media`). Talks directly to self-hosted **ReClip** (its own yt-dlp) at `RECLIP_URL` +(default `http://localhost:8899`): `POST /api/playlist`, `/api/info`, `/api/download {url,format,title} +→{job_id}`, `GET /api/status/{job_id}`, `/api/file/{job_id}`. **Titles are mandatory** — ReClip names +the output file from the `title` sent (empty title → hash filename), which is why Phase 1 fetches +metadata before downloading. Two phases (metadata → download), per-item failures skipped not fatal, +description sidecars next to each file. Mix/radio playlists (`list=RD…`) return a different set each +call, so the download panel pre-expands to individual video URLs and the task passes those through +untouched. +- `resolveDestDir()`: honours an absolute `INPUT_PATH`, **including the `~/…` autofill form** (expanded + against `$HOME`); otherwise falls back to `process.cwd()` (which the platform sets to the target + folder). Relative paths are ignored (would double-resolve). + +## Category ordering + +`capabilities/categories.yaml` is a plain ordered list; a task joins a category via its `TASK.md` +`category:` field. Add a category = one line here + the tasks that use it, no code change. Unlisted +categories sort alphabetically after listed ones; no category → "Other" (always last). + +## State of play (2026-08-07) + +**Multi-user is the current direction, and it is half-built.** On 2026-08-07 a capability permission +model landed in `platform/src/servers/capabilities/`: `registry.ts` enumerates every capability in four +kinds (`core` / `app` / `execution` / `admin`), `authorize.ts` resolves them per request, and +`totality.ts` makes the server **refuse to boot** if a mounted router has no registry entry. Grants are +keyed on **role**, never on a user, and live in `role_capabilities`; absence denies. `execution` +capabilities (terminal, chat, files, tasks, items, desktop, browser) run as the owner's OS user in the +owner's home and are **never grantable at any level** — that is structural, not a policy default. + +What is NOT built: **there is no way to create a second account.** `createUser` has one call site, +`api/auth/bootstrap.ts`, gated on an empty `users` table. No signup route, no invites, no admin +create-user handler — the six accounts in Postgres were inserted by hand. Details and the open defect +list are in `platform/TODO.md` → "Multi-user"; the design write-up is `COMMS/capability-permissions-2026-08-07.md`. + +The single-user claims that used to be all over this repo's docs were corrected on 2026-08-07. If you +find another one, it is stale — `platform/src/servers/capabilities/registry.ts` is the authority. + +**Both repos are clean and pushed** — platform to `gitea` and `origin`, capabilities to `gitea` only +(deliberately; never `rezio`). The sidecar isolation work and the navigation refactor are merged and +runtime-tested (41-check pass; the checklist was spent and deleted). Every `sidecars-*` branch +and `navigation-refactor` is deleted — merged, nothing stranded. + +**Jellyfin is the newest capability** (`officer-jellyfin`, `/jellyfin`): server registry with the access +token write-only across the boundary, browse/detail/search, and a player with three transports — direct +file, HLS over a **vendored** hls.js, and a progressive fallback. Two things there are worth knowing before +touching it. `src/workspaces/officerdev/src/apps/Jellyfin/vendor/` holds hls.js as a committed, unminified +file with its provenance hashes in a README beside it: that is the frozen-install rule applied to a library +we genuinely need, and it is reformat-exempt in `.prettierignore` because the diff against upstream is the +only thing that makes it reviewable. And `useClient()` **rebuilds its verbs on every render** — anything +that puts one of them in a `useCallback` dependency list gets a new identity every render. That silently +disabled every playback report in the Jellyfin player for days (an unmount cleanup re-running mid-playback); +the fix is refs, and the same trap applies to any React Query mutation object. + +**Projects is gone.** The feature was removed end to end on 2026-07-30 — screens, panel apps, routes, +dock entry, the `projects` table and its `proj-*` keys in the dashboards endpoint. `getUserProjectsDir` +in `data-path.ts` stays: the apps and dev-server routers resolve user apps under the same on-disk +`Projects/` directory and are unrelated. Nothing on disk was touched. + +**Sidecar work still outstanding**, in rough order of size: + +- **claude stages 3–5** — invert the transport so the agent listens on its own port instead of dialling + out, move the 361-line transcript reader in behind it, and survive a restart of the *agent sidecar* + itself. Stages 0–2 (PM2 peer, durable turn output) are done and running. This is the last sidecar still + driven by a typed command vocabulary over the registration socket. +- **the terminal session-orphan leak** — closing a panel for good leaves its shell running. Re-attach on + reopen and a Running Shells panel landed, so an orphan is now visible and killable; what is missing is + the panel system being able to tell a real close from an incidental unmount, which is what would let + anything send `pty:close` automatically. +- **slskd** is largely compliant. **The vault is off-limits.** + +**Done since this list was written:** email (store, routes, accounts, resync and both syncs now live in +the sidecar, which schedules its own work and serves its own HTTP — the platform side is 22 lines), +pty stage 4 (the bridge is a byte relay onto the sidecar's own listener; the pty command vocabulary is +gone from the wire protocol), every HTTP sidecar moved onto the shared `createSidecarProxy` factory, and +five new sidecars built to that shape from the start: photos, notify, caldav, memos, jellyfin. + +**Known and deliberately unfixed:** on mobile, tapping edit on a dashboard opens the form in a panel you +cannot see. The dashboard edit form is rendered into a panel that mobile layout never shows; mobile web +is being retired in favour of a native app, so it is not worth the fix. + +## Test checklists live here, not in the repo + +Working checklists for a migration in progress — what has been clicked through and what has not — belong +in this directory, deliberately untracked. That is state about *this machine at this moment*, not +something a clone of the repo should carry. **When a migration is finished, delete its checklist**; on +2026-08-08 ten spent ones went (~750 KB of nav, email, desktop and agent-status notes), and the only +reason they had survived was that nobody was sure whether anything still pointed at them. + +`REVOKE-TOMORROW.md` is the exception and must stay until the Discord and Telegram tokens it names are +actually reset at the providers — it is the only record of which ones. + +## Working agreement + +- **Fix what you find.** If a defect is diagnosed and you know the fix, apply it — don't stop to ask. + The owner retests immediately and restarts cost nothing. Still ask when it's a genuine product + decision rather than a defect, when it widens scope, or when it's hard to reverse. +- Explore related files before editing. Keep it simple, explicit, focused. Note unrelated problems, + don't fix them uninvited. +- **Report honestly** — distinguish what you verified (filesystem, DB, logs) from what you assumed. +- When something "doesn't work," check the restart/cache asymmetry above BEFORE re-diagnosing code. +- **A background timer must leave evidence, but only interrupt on action.** Any recurring/detached job + set up on the owner's behalf — a cron entry, a watcher, a poll loop — splits its reporting in two: + - **To the log, every tick, including no-ops.** A fresh timestamp is the only thing that distinguishes + "nothing to do" from "the timer died", and that question comes up. Cheap, silent, always on. + - **To the owner, only when the tick actually acted on something.** Say what changed and what was done + about it, unprompted. Never narrate a quiet tick at him — a heartbeat he has to read is noise. + Current instance: `auto-pull.sh` + the `* * * * *` crontab entry, log at `officer.dev/auto-pull.log`. diff --git a/user/CLAUDE.md b/user/CLAUDE.md new file mode 100644 index 0000000..90db087 --- /dev/null +++ b/user/CLAUDE.md @@ -0,0 +1,43 @@ +# User preferences + +## Input method: voice transcription + +The user dictates via speech-to-text, so prompts may contain transcription +errors — wrong homophones, mangled technical terms, missing punctuation. + +**Interpret intent, don't parse literally.** When a word is off but the meaning +is obvious from context, silently read it as the intended word and carry on. +Only ask when the ambiguity actually changes what you would do. + +Known substitutions seen so far: + +| Transcribed | Means | +| ----------- | ----- | +| cloud | claude | + +Add new ones to this table as they come up. + +## Ambiguity: interpret, don't ask + +**Default to acting on your best interpretation.** Work out what was meant from +context, say which reading you took, and proceed. The user would rather correct +a wrong guess afterwards than answer a question up front — a clarifying question +costs a full round trip, and dictated prompts are ambiguous often enough that +asking every time would stall everything. + +State the interpretation in passing, not as a request for approval. "Taking this +as X" and then doing it — not "did you mean X?" and then waiting. + +**Ask only when getting it wrong would be expensive**, which in practice means: + +- the action is destructive or hard to reverse (deleting data, force-pushing, + rotating a credential, restarting something in use) +- it commits a secret, a public URL, or anything else to a place that is awkward + to retract +- the readings differ so much that the wrong one wastes real work +- it is a genuine product or design decision rather than a defect with a + known fix + +Everything else: pick the likeliest reading and go. Verifying a guess against the +filesystem, the process table or the database is almost always faster than +asking, and it is usually conclusive — prefer that over both asking and assuming.