Follow this skill: --- name: cli-engineer description: Plan, map and build the full command line of a project or a project idea - its ` help` screens, its commands and aliases, and their wiring into the user's shell (~/.zsh.d, ~/.bashrc) - to CLI best practice and the user's own conventions (`opencode help`, `workstation help`). Use when asked to "map out the CLI", "make a help command", "add help", "wire this into my zshrc", "give this project commands", "what commands should this have", or when a project's commands, scripts and files have no single place to find them. metadata: usage-tier: mid-level --- # CLI engineer You design and build the command line a person uses to reach everything a project has: one facade command, ` help` as the map, aliases, and thin commands. Started as the `cli-engineer` persona (`~/projects/my-skills/agents/cli-engineer/claude.md`), or loaded by any agent that needs this. The bar: every CLI you make is at least as good as `opencode help` and `workstation help` are today. Read both before you plan anything (paths in `references/house-conventions.md`); they are the floor, not a template. ## How to think about it Every convention here is a first bet, not a rule. What decides is what this project's CLI helps its user do, and what keeps everything easy to reach. A convention that does not serve that project gets changed, and the plan says why. The user, 2026-10-26: "the 3 columns are just a suggestion. each case need to be considered based on the project and what the cli helps to facilitate." The help is a map, not an index. Every command has the form ` … `, each noun one screen deeper, and `help` may follow the last one: ` … help`. `help` always means "what can follow the noun just before me", so it can only come last, and it works after every noun at every depth: `codebot guests guest1 functions help` lists what can follow `functions`. `codebot help guests` is not a command: it asks two things at once, and exits 1 with the right form (`codebot guests help`). The user, 2026-10-26: "the pattern is \" help\" where \"help\" always refers to the n-th noun." `help` is a menu, not a screen and not a place, so it is never a part of a breadcrumb. ` help` is the case with no noun: the table of contents, naming the few "things" the project has so that it does not grow into an unwieldy 50-item list however large the project gets. It has no path to show, so it opens with one plain-English sentence and then the table. Every deeper `… help` opens with the breadcrumb of its nouns, then one sentence, then the menu, so that deep in the tree you still know where you are: ``` codebot › guests › guest1 › functions The functions of codebot's guest1. ``` The user, 2026-10-26: "In this situation I might want to be presented with a content of menu, and also a plain english explanation what i'm looking at". A bare noun (`codebot guests`, no `help`) prints that thing's screen. Each row of a help menu usually has three columns: | Column | What goes in it | | --- | --- | | Name | the thing, in the user's words | | Its technical form | what it is on the machine, clickable: a file or folder (OSC 8 link that opens it), a URL, the file and line of a function, or the command that opens the thing's screen (`codebot guests`, plain text: a link cannot run a command) | | What it's for | one plain-English line that starts with the verb for what running or clicking it does, then the noun it acts on. `codebot qa start`: "Starts the throwaway CodeBot on port 3506, where every change is tried before it reaches you." `codebot qa`, which only prints its screen: "Prints the QA screen: the throwaway CodeBot on port 3506 and what you can do with it." | The verb tells the user what happens before they type: does it start a server, print something, open a file? It is the verb of that exact command, so a group's own row says what its bare noun does (usually prints its screen), never what one of its actions does. Take it from the verbs the software industry already uses: opens, prints, lists, starts, stops, restarts, tails, runs, deploys, edits. When none fits, use the plainest one that does, and the plan says why. Take the noun from the project's own words (its README, its code), never a word you invented. The user, 2026-10-26: "i'm missing the verb, I need to know if this command launches a server? prints? and so on". The sentence under a breadcrumb names a place, so it is a noun phrase and needs no verb. Add, drop or rename columns when the project calls for it (opencode's profile table has a column per profile). Every screen (every bare noun command that prints) opens the same way: where you are and what you are looking at, a breadcrumb line, then one plain-English sentence, then its table. Screens can nest, so the breadcrumb shows the whole path, and each part is the command that opens that screen (`help` is never a part of it): ``` codebot › qa The throwaway CodeBot on port 3506, where every change is tried before it reaches you. ``` The user, 2026-10-26: "to every screen a plain english sentence about what screen I'm seeing. Uses a breadcrumbs pattern because some screens can be nested." Piped, the same two lines lead the markdown. When a list gets reasonably too long, put it behind an earlier screen with fewer options. Deciding where to cut is abstract, high-stakes reasoning, so avoid it unless you really have to, and when you do, ask the user before you split. The user, 2026-10-26: "it's about how to know to decompose a huge list into more manageable one". ## Steps 1. **Orient.** Run `rules` in the project folder. Read the project end to end: README, scripts, `package.json`, `Makefile`, `pyproject.toml`, docs, its `.idd/` and `.bdd/` if present, and every `~/.zsh.d/*.zsh` that already touches it. Read `~/.zsh.d/workstation.zsh`, `~/.zsh.d/opencode-profiles.zsh` and the scripts they call. 2. **Write the real-world scenario first** (`conceptual-modeling`): who reaches for this CLI, at what moment, to do what (deploy, find a log, open a config, see state). Label it reasoning from experience. 3. **Inventory every thing** a user would want to reach: commands and scripts, files and folders, URLs and dashboards, services and ports, live state (current profile, running jobs). Each with its technical form and where it is defined. Commands that already exist count; never write a second one for the same job (the duplicate-function rule: grep `~/.zsh.d`, the project, and `whence -a ` in zsh and bash). 4. **Group and name.** Group by what the user is trying to do, not by file type. Pick the facade name, a short alias, and `-help` forms (`ws`, `ws-help`, `workstation-help`; the user likes many aliases). Check every name for clashes: `zsh -ic 'whence -a '`, `bash -ic 'type -a '`, every `~/.zsh.d` file, the PATH. 5. **Write the plan** in the project's `docs/plans/-cli/plan.md` and open it with `review-open`: the scenario, the inventory, the help screens drawn as markdown tables exactly as they will print, the command tree with aliases, the wiring, what you build yourself and what goes to a worker, the clashes found, and every judgment call you made with its reason. 6. **Short grill.** Ask only what is the user's to decide, one question at a time, each with your recommendation: the names they will type, any split of a list behind a new screen, any clash with an existing command. Usually 1 to 3 questions. Every other call you make yourself and it is already in the plan. Then ask "build this?". Nothing is written before the yes. 7. **Build the CLI layer** (after the yes): - the script (Python, run with `python3 -I`, a docstring listing its commands, `fail()` writing `: ` to stderr and exiting 1 with the valid choices); - `help` drawn on a terminal, plain markdown when piped, reusing `~/projects/workstation/workstation-cli/terminal_links.py` (`draw_table`, `path_text`, `markdown_cell`) rather than new drawing code; - thin commands that only reach what exists (run a script, open a file with `review-open`, tail a log); - `zsh.d/.zsh` in the project repo: a header comment (what it is, that zsh and bash source it, where the help is), the facade intercepting `help` before anything else, the aliases; - the wiring: copy it to `~/.zsh.d/` (back up any file there first with `cp`), add one `[ -f "$HOME/.zsh.d/.zsh" ] && . "$HOME/.zsh.d/.zsh"` line to `~/.bashrc` (backup first); never edit `~/.zshrc`, its loop already sources `~/.zsh.d/*.zsh`. A command that needs new logic is not yours to write: write it up as a task for a worker (what, why, the intent row) and list it in your report. Write the script by `coding-guidelines`. 8. **Prove it** (`tank--bulletproof`): an intent at `.idd/modules/-cli/INTENT.md` with constraints and a Given / When / Then table naming each test; tests written first and seen failing, then passing. At least: every alias prints the table of contents, opening with its sentence and no breadcrumb; `help` after every noun, at every depth, prints that noun's menu, opening with the breadcrumb of its nouns and a sentence; `help` anywhere but last exits 1 and names the right form; every screen starts with its breadcrumb and sentence; every "what it's for" cell starts with a verb; piped output is plain markdown; on a terminal every path is a link to something that exists; an unknown command or category exits 1 and lists the valid ones; every thing in the inventory has a row (a new script, folder or command fails the test until it gets one). Then run the real thing in `zsh -ic` and `bash -ic` and paste what it printed. 9. **Record and ship.** A dated entry in `~/projects/workstation/README.md`, "System-wide changes" (what was wired, the backups, red and green). Commit and push as the git policy in the rules says, and give the hashes. For a project that is only an idea: steps 1 to 6 only. The plan is the map and the help as they will be; it is built once the project exists. When the project grows later, re-run steps 1 to 9 against the existing CLI: new things go into an existing group where they fit. A new group, or a split behind a new screen, is the user's call (step 6). ## Before you say it is done - ` help` on a terminal and piped, side by side with `workstation help`: is it at least as clear? - Each name you added: `whence -a` shows only yours. - Each function you added: grep its noun again across `~/.zsh.d` and the project (the duplicate check, second run). - Every path on screen opens; every row says what it is for in plain English, starting with the verb for what it does. - Every screen starts with its breadcrumb and its one-sentence "what you are looking at". - `help` after every noun prints its menu; `help` is never a part of a breadcrumb; ` help` opens with only its sentence; `help` before a noun is an error that names the right form. ## References | File | Read it when | | --- | --- | | `references/house-conventions.md` | before step 5: what the two examples do, where, and why | | `references/cli-best-practices.md` | step 4 and step 7: naming, help, errors, output, exit codes | You are given a project (below, as a dump of its files; you cannot read anything else or run commands). Task: organize everything this project's user would want to reach into screens, the map a command line's help screens will be built from. Do the planning only: build nothing, and don't wait for answers. Where a decision is the user's to make, put it in "questions" with your recommendation. End your answer with the map as ONE fenced ```json block, in exactly this shape: {"screens": [{"path": ["noun", "..."], "sentence": "...", "rows": [{"name": "...", "form": "...", "what": "..."}]}], "questions": ["..."]} - "path": the nouns that lead to the screen, [] for the top screen. - "sentence": what the screen shows, in plain words. - "rows": what can be reached from that screen: "name" in plain words, "form" its technical form (a path, URL, port, command, or the screen it opens), "what" what it is for. # Project: codebot ## File tree (git ls-files) ``` ./: .env.example, .gitignore, .nvmrc, AGENTS.md, ARCHITECTURE.md, CLAUDE.md, ... (17 files) .bdd/features/attachments/: attachment-actions.feature .bdd/features/bot-questions/: answer.feature .bdd/features/chat-current-session/: current-session-control.feature .bdd/features/main-navigation/: top-buttons.feature .bdd/features/update-notice/: update-notice.feature .bdd/qa/findings/: bot-questions.md .bdd/qa/resolutions/: attachments.md, bot-questions.md, main-navigation.md .bdd/steps/: main-navigation.test.mjs, update-notice.test.mjs .claude/: settings.json .github/workflows/: check.yml, supabase-keepalive.yml .idd/modules/attachments/: INTENT.md .idd/modules/bot-questions/: INTENT.md .idd/modules/chat-current-session/: INTENT.md .idd/modules/main-navigation/: INTENT.md .idd/modules/terminal-tabs/: INTENT.md .idd/modules/update-notice/: INTENT.md .impeccable/critique/: 2026-10-03T21-20-32Z__apps-web-src-widgets-gmailwidget-tsx.md .plans/: answer-bot-questions.md apps/control/src/: account-profile.ts, gmail-relay.ts, host-access.ts apps/service/assets/avatar-parts/: README.md apps/service/assets/avatar-parts/svg/accessories/front/: antenna.svg, bandana.svg, baseball_cap.svg, beanie.svg, gear.svg, glasses.svg, halo.svg, headphones.svg, heart.svg, hood.svg, horn.svg, sprout.svg apps/service/assets/avatar-parts/svg/accessories/rear/: bandana.svg, frog_eyes.svg, headphones.svg apps/service/assets/avatar-parts/svg/accessories/surface/: brown_patch.svg, iridescent_coat.svg apps/service/assets/avatar-parts/svg/accessories/under_eyes/: robot_visor.svg apps/service/assets/avatar-parts/svg/bodies/colors/: beige.svg, black.svg, blue.svg, cyan.svg, gray.svg, green.svg, ... (15 files) apps/service/assets/avatar-parts/svg/bodies/shapes/: bubble_top.svg, bunny_ears.svg, cat_ears.svg, cloud.svg, fish_tail.svg, flame.svg, plain_blob.svg, rounded_cube.svg, side_bump.svg, small_nub.svg, soft_tuft.svg, spiky.svg apps/service/assets/avatar-parts/svg/eyes/: angry.svg, black.svg, cyan.svg, dizzy.svg, focused.svg, happy.svg, ... (16 files) apps/service/assets/avatar-parts/svg/imported/accessories/: analytics.svg, backpack.svg, blonde_bun.svg, blonde_fringe.svg, blunt_bob.svg, book.svg, ... (61 files) apps/service/assets/avatar-parts/svg/imported/colors/: dark_brown.svg, deep_brown.svg, tan.svg, warm_brown.svg apps/service/assets/avatar-parts/svg/imported/eyes/: relaxed.svg, spiral.svg apps/service/assets/avatar-parts/svg/imported/shapes/: bear_ears.svg, diamond.svg, egg.svg apps/service/assets/avatar-parts/svg/professional/accessory/: ai_chip.svg, banjo.svg, black_cape.svg, bug.svg, calendar.svg, camera.svg, ... (55 files) apps/service/assets/avatar-parts/svg/professional/color/: bronze.svg, copper.svg, electric_violet.svg, emerald.svg, galaxy.svg, gold.svg, ... (16 files) apps/service/assets/avatar-parts/svg/professional/face/: angular_beard.svg, bandage.svg, black_mask.svg, blue_bandana.svg, broken_glasses.svg, cat_whiskers.svg, ... (27 files) apps/service/assets/avatar-parts/svg/professional/head/: aws_cap.svg, blue_tousled_hair.svg, blue_updo.svg, bowler_hat.svg, building_cap.svg, chef_hat.svg, ... (29 files) apps/service/assets/avatar-parts/svg/professional/shape/: antlers.svg, bell.svg, crowned_bell.svg, forked_antennae.svg, pawn.svg, rounded_triangle.svg, split_triangle.svg apps/service/src/: auth.ts, avatar-art.ts, avatar-generation.ts, avatar-routes.ts, avatar-store.ts, avatar.ts, ... (99 files) apps/service/src/providers/: claude-session.ts, claude-text.ts, claude-work-context.ts, claude.ts, codex-session.ts, codex.ts, ... (21 files) apps/service/src/terminal/: broker.ts, claude-sessions.ts, linux.ts, routes.ts, session.ts, transport.ts, windows.ts apps/service/test/: approved-files.test.ts, chat-store.test.ts, deploy-guard.test.ts, design-taste.test.ts, dump-gmail-cards.ts, fake-google.ts, ... (26 files) apps/web/: index.html apps/web/playground/: gmail-real.json, gmail.html, gmail.tsx, playground.css, variants.tsx apps/web/public/: icon.svg, manifest.webmanifest, offline.html, startup.css apps/web/public/avatars/: blob_01.png, blob_02.png, blob_03.png, blob_04.png, blob_05.png, blob_06.png, ... (42 files) apps/web/public/icons/: apple-touch-icon.png, codebot-192.png, codebot-512.png, codebot-maskable-512.png apps/web/public/licenses/: inter-OFL.txt apps/web/public/tool-icons/: LICENSE.md, README.md, canva.svg, figma.svg, github.svg, google.svg, ... (14 files) apps/web/src/: App.tsx, TerminalPage.tsx, TerminalTab.tsx, TerminalViewport.tsx, api.ts, avatar-thumbnails.d.ts, build.d.ts, main.tsx, storage.ts, theme.ts, useHostSnapshot.ts, whatsNew.ts apps/web/src/contracts/: ComponentRelationships.tsx, ContractCatalog.tsx, ContractConflict.tsx, ContractDiff.tsx, ContractFiles.tsx, ContractHistory.tsx, ... (17 files) apps/web/src/gmail/: GmailAccess.tsx, GmailActivity.tsx, GmailApprovals.tsx, GmailCards.tsx, GmailDaily.tsx, GmailGlance.tsx, ... (13 files) apps/web/src/onboarding/: DeviceArrival.tsx, DeviceConnection.tsx, FamilySetup.tsx, FirstBot.tsx, FirstBotCreation.tsx, FirstBotIntroduction.tsx, ... (17 files) apps/web/src/pwa/: DeviceNotifications.tsx, UpdateNotice.tsx, notifications.css, register.ts, service-worker.js, usePushNotifications.ts apps/web/src/resources/: ApproveFileLink.tsx, AttachmentPicker.tsx, FilePreview.tsx, PinnedFiles.tsx, PinnedResources.tsx, RecordingPlayer.tsx, ResourceRow.tsx, ResourceViewer.tsx, markdownLines.ts, resourceDrag.ts, useAttachments.ts, useResourceCopy.ts apps/web/src/styles/: automation.css, avatars.css, base.css, bot-setup.css, contracts.css, git.css, ... (16 files) apps/web/src/ui/: Collapsible.tsx, DateTime.tsx, Modal.tsx, SelectionIndicator.tsx, Settings.tsx, StartupScreen.tsx, TabRow.tsx, motion.ts, sheet-swipe.ts, useSwipe.ts apps/web/src/widgets/: ContractWidget.tsx, GmailWidget.tsx, SurfaceWidgets.tsx, WidgetArrangement.tsx, WidgetBrowser.tsx, WidgetCard.tsx, ... (14 files) apps/web/src/workspace/: ActionCard.tsx, AvatarPartThumbnail.tsx, AvatarPicker.tsx, AvatarStudio.tsx, BannerClose.tsx, BotForm.tsx, ... (49 files) design/: README.md, content-previews.png, create-new-bot.png, main.png, mobile__chat.png design/annotated/: content-previews-annotated.png, create-new-bot-annotated.png, main-annotated.png, mobile-chat-annotated.png design/fixtures/: draft-recovery.json design/onboarding/grokbot-reference/: 00-00.100-landing.png, 00-12.000-welcome.png, 00-28.000-welcome-detail.png, 00-30.000-own-computer.png, 00-32.000-bot-jobs.png, 00-34.000-everyday-tools.png, ... (13 files) design/reference/: remote-terminal-desktop.png, remote-terminal-feature-contract-desktop.png, remote-terminal-feature-contract-mobile.png, remote-terminal-feature-contract-review-current.png, remote-terminal-mobile.png design/streamables/feature-contract/: backend.png, endpoints.png, files.png, frontend_components.png, frontend_functions.png, frontend_state-machines.png, mobile-viewport.png, types.png docs/: agentic-development.md, ai-readiness-verification.md, design-method.md, design-taste.md, host-account-exchange.md, onboarding-implementation.md, onboarding-recorded-proof-2026-09-14.md, onboarding-review-gate.md, self-hosting-practices.md docs/automation/: gmail-reference.md, gmail.md docs/images/: claude-code-chat-footer-reference.png docs/objectives/: README.md, chat-current-session.md, gmail-widget.md, schedules.md, terminal-tabs.md docs/plans/: MASER_PLAN.md, automatic-provider-memory-collection.md, background-work-survives-chats-design.md, bot-config-export-plan.md, bot-config-import-plan.md, chat-rich-content-and-replies-design.md, ... (20 files) docs/superpowers/plans/: 2026-09-23-feature-contract-context-refactor.md docs/tasks/: 2026-09-22-P0-complete-shared-storage-backup-restore.md docs/tasks/2026-09-22-self-hosted-session-evidence/: codebot-multihost-enrollment-audit.md, codebot-storage-audit.md, self-hosted-execution-ownership.md, self-hosted-native-sessions.md, self-hosted-terminal-and-storage.md, task-5-stage1-report.md, task-5-stage1-rereview-2.md, task-8-stage1-report.md, task-8-stage1-review.md, worktree-status-at-handoff.txt docs/verification/: bot-questions.md, self-hosted-terminal-and-storage.md ops/: OPERATIONS.md, agent-start.sh, avatar-thumbnails.ts, build-integration.mjs, build.mjs, codebot-family-lobby.service, ... (26 files) ops/global/: CLAUDE.md ops/review/: gmail-review.mjs, redirect-google.mjs, schedule-drill.mjs, schedules-review.mjs, seed-gmail.ts, terminal-tabs-stack.sh packages/contracts/src/: account-profile.ts, automation.ts, avatar-catalog.json, avatar.ts, bot-model.ts, bot-setup.ts, ... (30 files) skills/codebot-housekeeping/: SKILL.md supabase/: config.toml supabase/functions/gmail-relay/: deno.json, index.ts supabase/functions/host-access/: deno.json, deno.lock, index.ts supabase/migrations/: 20260913231209_account_onboarding.sql, 20260914001658_host_account_binding.sql, 20260914010339_host_progress_reports.sql, 20260914083358_account_device_handoff.sql, 20260914083841_host_account_setup.sql, 20260914143000_drop_host_setup_flags.sql ``` ## File: README.md ``` **Codebot is a bot-first workspace for getting work done.** Its bots can code, research, plan, operate tools, and handle other kinds of work. Instead of forcing you to recreate your working context every time you change device, application, or location, Codebot centralizes that context into one clean interface. The ideal use case is continuity: start something at your desk, continue it from your phone, and return to the same bots, tasks, decisions, artifacts, and history later. **Less activation friction. Less context switching. More continuity.** Concretely, Codebot runs on a computer you control and gives you several enduring coding teammates: a Frontend Bot, an Infrastructure Bot, a Research Bot. Each has its own role, a provider (Codex, Claude, or OpenCode), a home folder, and one lasting conversation. You watch and steer them from desktop, phone, or tablet, and can always tell which Bot needs you, what it is doing, what it produced, and whether it is safe to step in. The first beta is paused part-way; `IMPLEMENTATION-STATUS.md` and `DEFERRED-WORK.md` say where it stopped. `PRODUCT-SPEC.md` is the behavior contract and `ARCHITECTURE.md` explains how it is built. To run the local service (Node 24 or newer): ```bash npm install cp .env.example .env npm run dev ``` the data and the docs (`cli/codebot.py`). ## Run the experimental apps V2, V3 and V4 These are separate apps in their own private repos (not part of CodeBot, and CodeBot's port 3506 rules do not apply to them). Each one reuses CodeBot's server and has its own front end. Run them on this computer only. Opening them from the same computer signs you in by itself (`CODEBOT_OPEN_LOCAL_ACCESS=1` is set by the run script), so there is no key to type. | App | Folder | Port | What it is | |---|---|---|---| | V2 Terminal chat | `~/projects/codebot-v2-terminal-chat` | 3512 | CodeBot's look and Bot list; a Bot's chat is the Claude Code terminal | | V3 Claude Code mod | `~/projects/codebot-v3-claude-mod` | 3513 | Claude Code first; a mod draws the Bot list band above the prompt | | V4 Launcher menu | `~/projects/codebot-v4-launcher-menu` | 3514 | A simple main menu whose buttons set up the Claude Code sessions | Start one (use a separate terminal for each; Node 24 is set by the script, and `npm install` is already done in each folder): ```bash cd ~/projects/codebot-v2-terminal-chat # or codebot-v3-claude-mod, or codebot-v4-launcher-menu sh ops/app-run.sh dev ``` Then open it in Chrome: `http://127.0.0.1:3512` (V2), `http://127.0.0.1:3513` (V3), `http://127.0.0.1:3514` (V4). Stop it with Ctrl+C in its terminal. - Each app keeps its data in its own `.data/app` folder, so nothing is shared with CodeBot (3505, 3506) or with the other two. - The run script starts the app's own terminal broker and turns the terminal on. If you see "The terminal is not turned on for this computer", the app was started some other way; start it with the script above. - Stop an app by the process that holds its port, never by a name pattern, or you will stop the other apps too (this happened once): `ss -ltnp | grep :3513`, then stop that one process id. - After an app restarts, reload its Chrome tab once. - V3 runs Claude Code sessions (Bots) in the background. If its Bot list shows old test Bots, stop them with `claude stop ` (`claude agents` lists the ids). - Not tested: phone, Tailscale. These apps are local experiments. - Each repo has `docs/experiment/` with the owner's vision word for word, a `LOG.md` of decisions and a `REPORT.md` of what works and what does not. ## How the pieces fit ``` apps/service/ the local HTTP/WebSocket service — Bots, transcripts, git, avatars, host access, provider adapters. SQLite in WAL mode owns the state. apps/web/ the interface you watch and steer Bots from apps/control/ the trusted control service that issues signed host proofs packages/contracts/ schemas shared by service and client, so a rejected payload never reaches the store supabase/ the central account service: migrations, functions, config ops/ build, packaging, the systemd unit, and setup-account skills/ codebot-housekeeping docs/automation/ how Bots use external services (Gmail first): supervisor guide and generated reference ``` The service runs on a machine you control and holds the authority; the account service exists only for sign-in and the host handshake. `.env.example` is explicit that a service-role or secret key must never appear in a `VITE_` variable, and that the host accepts one-use account arrivals only from the configured HTTPS origin. `CODEBOT_PROJECT_ROOTS` bounds what Bots can reach on disk — widening it grants real filesystem authority, so treat it as a security boundary rather than a setting. ## Status and verification The first beta is paused part-way at the user's request (2026-09-10). `IMPLEMENTATION-STATUS.md` records what was built and `DEFERRED-WORK.md` holds the remaining checklist and the safe restart state. `MASTER-PLAN.md` remains the release scope — an internal milestone is not a finished product, and nothing here should imply one. ```bash npm run typecheck # tsc --noEmit npm run build # typecheck, then ops/build.mjs npm test # Gmail automation tests against a fake Google server npm run docs:gmail # regenerate docs/automation/gmail-reference.md ``` `npm run typecheck` was clean on 2026-09-20 under Node 24.19.0. There is no lint script, and the only tests cover Gmail automation (against a fake Google, not the real one), so a passing typecheck means the code compiles and says little about Bot behavior. `docs/` holds the recorded verification evidence, including the onboarding proof and the review gate. ``` ## File: package.json ``` { "name": "codebot", "description": "Bot-first workspace: a local service and web app for supervising enduring coding Bots (Codex, Claude, OpenCode) from desktop, phone, or tablet.", "version": "0.1.0", "private": true, "type": "module", "engines": { "node": ">=24.0.0" }, "scripts": { "dev": "tsx watch apps/service/src/main.ts", "build": "tsc --noEmit && node ops/build.mjs", "start": "cross-env NODE_ENV=production tsx apps/service/src/main.ts", "typecheck": "tsc --noEmit", "setup-account": "tsx ops/setup-account.ts", "test": "node --import tsx --test apps/service/test/*.test.ts", "docs:gmail": "tsx ops/gmail-reference.ts", "review:gmail": "node ops/review/gmail-review.mjs", "playground": "vite --host 127.0.0.1 --open /playground/gmail.html", "check": "tsc --noEmit && node --import tsx --test apps/service/test/*.test.ts", "sync:agents": "tsx ops/sync-agents.ts", "review:schedules": "node ops/review/schedules-review.mjs", "build:integration": "tsc --noEmit && node ops/build-integration.mjs" }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "0.3.265", "@fontsource-variable/inter": "5.3.0", "@lydell/node-pty": "1.2.0-beta.15", "@modelcontextprotocol/sdk": "1.30.0", "@opencode-ai/sdk": "1.14.39", "@radix-ui/react-dialog": "1.1.23", "@supabase/supabase-js": "2.116.0", "@tanstack/react-virtual": "3.14.11", "@xterm/addon-fit": "0.11.0", "@xterm/xterm": "6.0.0", "express": "5.2.1", "file-type": "22.0.2", "highlight.js": "11.12.0", "lucide-react": "1.43.0", "node-notifier": "^10.0.1", "playwright-core": "1.63.0", "qrcode-generator": "2.0.4", "react": "19.2.8", "react-dom": "19.2.8", "react-markdown": "10.1.0", "rehype-highlight": "7.0.2", "sharp": "0.35.4", "web-push": "3.6.7", "ws": "8.21.3", "zod": "4.5.4" }, "devDependencies": { "@types/express": "5.0.6", "@types/node": "26.5.0", "@types/node-notifier": "^8.0.5", "@types/react": "19.2.18", "@types/react-dom": "19.2.7", "@types/web-push": "3.6.4", "@types/ws": "8.18.1", "@vitejs/plugin-react": "6.1.1", "cross-env": "10.1.0", "tsx": "4.23.13", "typescript": "7.0.2", "vite": "8.2.2" } } ``` ## File: AGENTS.md ``` # CLAUDE.md Fundamentals about CodeBot that every task assumes. Read before designing anything. ## How CodeBot is used - **The server does everything.** Bots, providers, tools, OAuth, storage and scheduling all run on the CodeBot server. Phones, tablets and desktops are thin clients that only connect to that server and show its state. Never design a flow that needs the phone to reach anything itself (for example `127.0.0.1` on the phone, or a client-side token exchange). - **Most users are on a phone.** Design for phone first: one tap, no copying addresses, no desktop-only steps for the person using the app. - **The server runs in one of two places:** cloud (hosted by the CodeBot project) or self-hosted (a machine the owner controls, reached through their own domain, a Tailscale name or similar). A feature must work for both. - **One server per owner, in cloud too.** Each cloud user gets their own CodeBot server. A server never has to share accounts, tokens or data between different people, so per-server settings (for example one connected Google account) are correct for both cloud and self-hosted. - **No single CodeBot domain for servers.** Every cloud user's server has its own domain. The only fixed, shared address in the system is the central account service (`CODEBOT_ACCOUNT_ENTRY_URL`, the common sign-in entry). Anything that needs one fixed address registered with a third party (such as a Google OAuth redirect) must live there, never on a server's own domain. - **Addresses vary.** Self-hosters use custom domains, Tailscale names and other addresses, and one server can be opened from several. Nothing may assume a fixed domain or port. Anything that needs an address registered elsewhere (OAuth redirects, push, webhooks) has to be reasoned about per deployment kind. - **Who does what.** The *operator* sets up the server once (often on a desktop). The *owner* uses the app, usually from a phone. Do not put operator work in the owner's path. ## Decisions already made - **No paid Google verification.** CodeBot does not pay for Google's Gmail app verification or security assessment. Self-hosters bring their own Google credentials (client ID and secret). Cloud can only offer Gmail to a small early group through CodeBot's unverified Google app until that changes. - **Google sign-in returns through a shared relay.** Google redirects need registered addresses, so the account service hosts a stateless `gmail-relay` function (one fixed address) that forwards the browser to the user's own server. Servers connected to the account service use it; others use their own address. Details: `docs/automation/gmail.md` section 3. ## Working with the owner - Lead with the decision and its consequences in a short comparison (options, pros, cons, costs). Details only after the owner asks. - Do not ask the owner to explain how the app is used; it is recorded here. If something fundamental is missing from this file, ask once and **add the answer to this file straight away**. The owner must never have to repeat an answer about the core product. - Talk in costs (money, effort, risk, ongoing maintenance) when options are compared. - Put the answer first in every reply. Context and detail come after it, and only if they help. - Designing or redesigning any screen follows the rules in "Designing screens" below. ## Designing screens Every screen change follows these. The full method and the reasons are in `docs/design-method.md`. - **Reuse the app's own parts.** Before writing any CSS, find what the app already uses for each element (look at the sibling widgets, the Bot form, `base.css`) and use it unchanged: `.icon-button`, `.button`, `.text-button`, `.streamable` with `

TITLE

…` and one icon button, `.streamable-empty`, `.approval-card`, `.form-field` with `.policy-options`, `.error`. Same sizes, 5 to 6px radii, 12px text, existing colour tokens. Never draw a custom version of something that exists; a duplicated, slightly different component is a regression. New CSS only for what has no equivalent. - **The words and values on screen are part of the design.** Check what each line actually says, not just the layout. Never show IDs, codes, JSON or internal names. Anything that changed shows what it was and what it is now (before → after). - **Test with real output.** Render screens from what the real code produces, not fixtures you wrote. Fix the producer, not the screen. - **Start from the owner's moments.** What question are they asking when they open it, how often, and what does a wrong answer cost? Say the answer in a plain sentence first; put controls and raw records below. - **Settings live behind one cog button** at the top right of a widget, not scattered through it. - **A list of facts is not insight.** Say what it means for the owner: what is risky, what stays in effect, what needs a decision, how to undo it. Lead with the verdict, put exceptions before routine work, fold routine work into counts. - **Process for any new or reworked screen** (small tweaks may skip 3 and 4): 1. Read `docs/design-taste.md` (the owner's rulings and what they rejected) and the plan docs for earlier work on this screen. 2. Check which skills and plugins could help (`ListSkills`, `SearchSkills`, `SearchPlugins`) and say what you found. 3. Research how others solved the same job, by what they show first and how they flag risk, and record the sources in the plan doc. 4. Build two or three variants side by side in `apps/web/playground/` with real output and run `npm run playground`. Recommend one; the owner judges. 5. Check the winner in the real app with `npm run review:` (real service, real build, real browser), at phone and desktop size, light and dark. 6. Add the owner's reaction and any new ruling to `docs/design-taste.md`. - **Make every visible change noticeable.** Add an entry to `apps/web/src/whatsNew.ts` in the owner's words, saying what changed and where to find it; the app shows it once after an update. A change the owner cannot find, or is never told about, did not ship. Check it in the real running app (service plus browser), not only in a harness. - **Push and restart after code changes and pulls.** In this checkout, after every code change and every `git pull`, run `npm run build`, commit and push the resulting work to the project's designated remote/branch (confirm `git push` reports up to date when there is no new commit), wait for active turns and recordings to finish, restart `codebot.service` using `ops/OPERATIONS.md`, then verify `/api/session` and a new `ExecMainStartTimestamp` before reporting the update as live. This applies to frontend-only changes too. `sh ops/deploy.sh` does all of it and waits its turn: it refuses while a Bot is working or the server code has someone else's uncommitted edits. A Stop hook (`ops/deploy-guard.sh`) blocks finishing a turn while code is unpushed, newer than the running service, or newer than the served build. Say when a merge or pull still needs this deployment step. ## Where things are `README.md` (layout and commands), `PRODUCT-SPEC.md` (behaviour), `ARCHITECTURE.md` (how it is built), `docs/automation/gmail.md` (Gmail for Bots, supervisor guide), `DEFERRED-WORK.md` (open work). ``` ## File: ops/OPERATIONS.md ``` # Running and recovering CodeBot CodeBot runs on Node 24, binds `127.0.0.1:3505`, and is served privately at `https://liel.tail18e69e.ts.net/`. The application data is separate from this checkout, at `~/.local/share/codebot`. The installed production service is `codebot.service`; `codebot-integration.service` is a disposable QA instance on port 3506. The earlier `codebot-preview.service` was transient. ## Install or update the host Prerequisites: Node 24, `flock` (util-linux), the selected native provider programs, Bubblewrap, Chromium, FFmpeg/FFprobe, poppler-utils (`pdftoppm`, for PDF thumbnails) and Tailscale. This workstation's Node executable is `~/.nvm/versions/node/v24.19.0/bin/node`. Update `ExecStart` in the service file if that path or the checkout location changes. Provider executable locations are declared in `apps/service/src/providers/sandbox.ts`. From the checkout, install dependencies and build before restarting: ```bash export PATH="$HOME/.nvm/versions/node/v24.19.0/bin:$PATH" npm ci npm run build ``` The build completes in a new `dist/releases/` directory, then atomically swaps the `dist/web` symlink. A failed build leaves the served release intact; a concurrent build exits without publishing. Keep using `npm run build` so the type check and publishing lock apply. The current release and two predecessors are retained, including their hashed assets for older open documents. Reload a page left open across more releases. After every code change and every `git pull`, including frontend-only updates, run `git push` for the configured branch after committing any local changes. Then wait for active turns and recordings to finish, restart `codebot.service`, and verify `/api/session` and a new `ExecMainStartTimestamp`. This does not create an atomic source/database rollback snapshot. Create the private configuration once. Preserve an existing `service.env`. Copy any provider credential already used by the working service into that private file, never into this checkout. Claude on this workstation uses `CLAUDE_CODE_OAUTH_TOKEN`; omitting it selects a different native login and was the cause of the earlier expired-session failure. Do not print this file in diagnostics or paste it into an issue. ```bash install -d -m 700 "$HOME/.config/codebot" "$HOME/.config/systemd/user" test -e "$HOME/.config/codebot/service.env" || install -m 600 ops/environment.example "$HOME/.config/codebot/service.env" install -m 644 ops/codebot.service "$HOME/.config/systemd/user/codebot.service" systemctl --user daemon-reload ``` Use only one service for port 3505. Before the first switch, stop the old preview after finishing or stopping its active work. For later updates, restart `codebot.service` after the new build succeeds. ```bash # First switch only, if that old unit is still installed: systemctl --user stop codebot-preview.service # Reload after stopping a transient unit when replacing one with a disk unit: systemctl --user daemon-reload systemctl --user enable --now codebot.service curl --fail --silent --show-error --retry 12 --retry-connrefused --retry-delay 1 http://127.0.0.1:3505/api/session ``` For subsequent updates use `systemctl --user restart codebot.service` after active turns and recordings finish. Confirm `systemctl --user show codebot.service -p FragmentPath` points to the installed unit under `~/.config/systemd/user`, not the old `/run/user/.../transient` path. ### Persistent terminal broker Terminal access is a separate, optional self-hosted capability. The web service does not create a shell unless the configured per-user broker answers its private readiness probe. Cloud deployments, unknown deployments, disabled brokers, root installations and an unavailable broker remain disabled. The broker runs as the installation owner and refuses to start as root; it never receives the CodeBot service environment file. Provider login files and explicitly supplied provider variables can be available to the owner shell, while `CODEBOT_*`, database, service-role and browser credentials are excluded from the broker environment. The repository unit is a template for a deliberate operator installation. It is not installed or enabled by `npm run build`: ```bash install -m 644 ops/codebot-terminal-broker.service "$HOME/.config/systemd/user/codebot-terminal-broker.service" systemctl --user daemon-reload systemctl --user enable --now codebot-terminal-broker.service systemctl --user status codebot-terminal-broker.service --no-pager ``` Keep `codebot-terminal-broker.service` separate from `codebot.service`. A web-service restart leaves this unit and its owner-only tmux session running; stopping or losing this broker unit tears down its tmux process group. The unit's `KillMode=control-group` enforces that broker-loss boundary. The broker uses a private socket under `%t`, an owner-only tmux socket under `~/.local/state/codebot-terminal`, and a 10,000-line tmux history limit. Do not expose either socket through Tailscale Serve or Funnel. A terminal is pinned to this host and is never forwarded to another host when the host is offline. Before enabling this capability on an installation, set the operator configuration to `deploymentMode=self-hosted`, `terminalBroker=systemd-user` and the exact non-root `terminalUser`. The web service and broker must run as that same user. A broker that is merely running does not override the service-side owner, host, ticket, control-epoch, input-size, resize, or backpressure checks. Windows remains disabled until a packaged per-user ConPTY broker passes its OS-backed token and restart qualification. Its named-pipe commands also require the same operator-provided `CODEBOT_TERMINAL_IPC_TOKEN` in the web service and broker environments; a missing or malformed token refuses broker startup. Linux verification does not qualify Windows. An unauthenticated session response is a successful readiness check. Service `active` alone does not prove the listener is ready. Check `loginctl show-user "$USER" -p Linger`: `Linger=yes` keeps the user service available without a desktop login. If disabled, enable lingering through the machine's normal administrator setup. The current workstation already has it enabled. Read the entrance key locally when connecting a new device: ```bash cat "$HOME/.local/share/codebot/owner-key" ``` ## Private access and PWA installation ### Bot model selection Leave a Bot's model empty to use CodeBot's automatic policy: | Provider | Ordinary message | Message beginning with `/goal` | | --- | --- | --- | | Claude | `claude-sonnet-5` (`xhigh`) | `claude-opus-5` (`high`) | | Codex | `gpt-6-luna` (`xhigh`) | `gpt-6.1-sol` (`xhigh`) | | OpenCode | Sonnet 5 (`xhigh`) or Luna 6 (`xhigh`) | Opus 5 (`high`) or Sol 6.1 (`xhigh`), respectively | Existing Bots with empty model settings use this policy on their next turn. Existing `gpt-6-sol` Codex and `*/gpt-6-sol` OpenCode selections request `gpt-6.1-sol` from the same provider on their next turn. Other explicit Bot models (including `CODEBOT_DEFAULT_MODEL` for the first Bot) stay pinned on both ordinary and `/goal` messages. OpenCode chooses a connected account offering both tiers with high effort, preferring direct Anthropic, then GitHub Copilot's Claude models, then OpenAI. If none qualifies, it asks for an account connection or explicit model instead of choosing an arbitrary fallback. Explicit OpenCode models retain their provider prefix and use the high variant when that model offers one. Selection is made for each submitted message, so the next ordinary message uses the cheaper model even when resuming a provider session. Merely mentioning `/goal` in prose or using another command such as `/goals` does not promote it. This does not implement native `/goal` dispatch or track a goal lifecycle; that integration remains in `DEFERRED-WORK.md`, section 9. ### Connect a device Inspect the current mapping first: ```bash tailscale serve status --json ``` The expected root handler is `http://localhost:3505` or `http://127.0.0.1:3505` on private HTTPS port 443. On a new machine, with an unused root handler, configure `tailscale serve --bg http://127.0.0.1:3505`. Do not reset an existing mapping or enable Funnel. See the official [Tailscale Serve reference](https://tailscale.com/docs/reference/tailscale-cli/serve). On the phone, connect Tailscale and open the private HTTPS URL. In a supporting browser choose its Install app / Add to Home Screen command. The manifest sets CodeBot's name, icon and standalone display. The app stays useful in a normal browser too. See [PWA installation](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable). Only public application assets are cached. If navigation cannot reach the host, the offline page offers Try again and retries when the connection returns. Conversations, attachments, recordings, session responses and actions are not service-worker cached. No offline command queue is created. An update does not reload an open conversation; reopen or reload to use the new UI. ## Host a guest on this machine A CodeBot host serves one Google account. To let another person (a family member) have their own account and Bots on this machine, powered by the owner's AI subscriptions, a second CodeBot runs as its own Linux user, `codebot-guest`. | | Owner | Guest | | --- | --- | --- | | Program | this checkout | `/opt/codebot-guest` (packaged copy, owner-owned) | | Service | `codebot.service` (user unit) | `codebot-guest.service` (system unit, `User=codebot-guest`) | | Data and settings | `~/.local/share/codebot`, `~/.config/codebot` | same paths under `/home/codebot-guest` | | Project folders | `~/projects` | `/home/codebot-guest/projects` | | Address | `https://liel.tail18e69e.ts.net` | same host, port `10000` | The separate user is the isolation. The owner's home is `drwxr-x---`, so the guest's Bots cannot read it even with a Bot's sandbox switched off. Git commits and pushes use the guest's own identity instead of the owner's. The guest can read the owner's Claude token and Codex login copies, which is what lets it use the subscriptions. Revoke them from the providers to cut it off. The service is capped at 6 GB of memory and four CPU cores. Set up once, then update whenever the guest should get the current version: ```bash sudo ops/guest-install.sh # user, /opt dir, service, settings, sudo rules, Tailscale :10000 ops/guest-update.sh # build, replace /opt/codebot-guest, restart; no password needed sudo -u codebot-guest /opt/codebot-guest/invite # one-use Google link, valid ten minutes ``` Share this machine with the guest's Tailscale account from the Tailscale admin console (Machines → this machine → Share) instead of adding their device to the tailnet. They open the invite link and continue with their Google account. Afterwards they sign in at the owner's entry (`https://liel.tail18e69e.ts.net/onboarding`) and are sent to their own host. Check on it with `systemctl status codebot-guest`, `journalctl -u codebot-guest`, `ps -u codebot-guest` and `sudo du -sh /home/codebot-guest`. To remove it: `sudo systemctl disable --now codebot-guest`, `sudo tailscale serve --https=10000 off`, `sudo rm /etc/systemd/system/codebot-guest.service /etc/sudoers.d/codebot-guest`, `sudo userdel -r codebot-guest` and `sudo rm -r /opt/codebot-guest`. Letting the guest work on files on their own computer instead is deferred; see `DEFERRED-WORK.md`, section 12. ## Home Wi-Fi address Anyone on the home Wi-Fi opens `https://family.codebot.lielbenor.com`, signs in with their own Google account, and gets their own CodeBot: fresh onboarding, their own Bots, the owner's Claude subscription. Nothing is done per person. Plan and reasons: `docs/plans/family-wifi-accounts.md`. | Part | Where | | --- | --- | | Address | GoDaddy A record → `192.168.1.137` (keep it reserved on the router). At home it connects directly. Away from home it works for people **invited** to the owner's tailnet (Tailscale admin → Users → Invite; sharing the machine is not enough, shared machines do not carry subnet routes) with Tailscale on: the laptop advertises `192.168.1.137/32` (`tailscale set --advertise-routes`), approved once in the admin console (Machines → this laptop → Edit route settings), and phones pick the route up by themselves. Same address, same account, same workspace | | Certificate | Let's Encrypt through acme.sh in the owner's account (`~/.local/share/acme.sh`, settings `~/.config/acme.sh`), proven through GoDaddy DNS with the key in `~/.config/godaddy-api`; its cron job renews it and reloads nginx | | Front door | `codebot-family-lobby.service`, user `codebot-lobby`, port 3530, nginx `:443` (and `:80` → https) in `/etc/nginx/sites-available/codebot-family`. An empty CodeBot with no AI token: it signs people in and, for a new Google account, runs `codebot-family add N` (route `/api/family/provision`, numbers in `family-slots.json` in its data, at most 6) | | Family CodeBot N | `codebot-family@N.service`, user `codebot-fN` (its own home, so it cannot read the owner's or anyone else's), port `3540+N`, address `https://family.codebot.lielbenor.com:10100+N` (nginx `codebot-family-N`), 6 GB and 4 cores at most. From the owner's settings it gets only the sign-in service address, the Claude token and the Google app ID and secret (so its person can connect their own Gmail), rewritten on every `add N` and restarted when they change | | Program | `/opt/codebot-family`, one build for the front door and every family CodeBot | | Root helper | `/usr/local/sbin/codebot-family` (`ops/family-helper.sh`): `add N`, `remove N`, `stop-all`, `start-all`, `status` (running, and whether Gmail can be connected); settings `/etc/codebot-family.conf`; sudo rules `/etc/sudoers.d/codebot-family` (the front door may only `add`) | Set up once, then update whenever family CodeBots should get the current version: ```bash sudo ops/family-install.sh # front door user, helper, services, sudo rules, nginx ops/family-update.sh # build, replace /opt/codebot-family, restart all; no password ``` The owner's own Google account is linked to the owner's CodeBot, so signing in with it at the family address tries to open that one and fails; the owner keeps using the Tailscale address. A Google account stays linked to its family CodeBot for good: `sudo codebot-family remove N` deletes that CodeBot and frees its number, but the same Google account cannot be linked again without editing the account database. Check on it with `systemctl status codebot-family-lobby 'codebot-family@*'`, `journalctl -u codebot-family-lobby -u 'codebot-family@*'` and `ls /home | grep codebot-f`. To remove everything: `sudo codebot-family remove N` for each, `sudo systemctl disable --now codebot-family-lobby`, `sudo rm /etc/nginx/sites-enabled/codebot-family /etc/nginx/sites-available/codebot-family /etc/sudoers.d/codebot-family /etc/systemd/system/codebot-family-lobby.service '/etc/systemd/system/codebot-family@.service' /usr/local/sbin/codebot-family /etc/codebot-family.conf`, `sudo systemctl reload nginx`, `sudo userdel -r codebot-lobby`, `sudo rm -r /opt/codebot-family`, `~/.local/share/acme.sh/acme.sh --uninstall`, and delete the GoDaddy record. ## Background notifications Open a Bot's settings, choose **Enable this device**, and allow the browser's notification prompt. The Bot's own Notifications switch must also be saved on for its replies, approval requests and failures. **Send test** verifies the selected device independently of that Bot switch. A queued test or push-service acceptance is not proof that the device displayed it. On iPhone/iPad, open the installed Home Screen app; see [Apple's Web Push guidance](https://developer.apple.com/documentation/usernotifications/sending-web-push-notifications-in-web-apps-and-browsers). Use **Reconnect this device** after an expired/rejected subscription. **Other devices** lets the owner remove old subscriptions; up to 16 devices are retained. Permission blocked in site settings must be changed in the browser. Isolated automation browsers can reject registration even with permission granted. The host needs outbound HTTPS to the supported browser push services. The notification contains the Bot name and outcome, without the conversation body. Opening the Bot still requires access to the private Tailscale host. Temporary delivery failures retry up to eight attempts with a ten-second request deadline; queued Bot alerts expire after one hour. A full 256-row queue retains the newest notices. A newer state can replace or discard an obsolete alert. Database migration 11 stores the signing identity, subscriptions, event cursor and delivery queue. Include them in the private whole-data backup below. Restoring only public signing information invalidates existing subscriptions. `CODEBOT_RECOVERY_HOLD=1` pauses automatic sends and rejects test dispatch; normal restart resumes durable unexpired work. `/api/notifications/diagnostics` reports the queue, timer and active-send count to an authenticated owner. ## Change this machine's address The machine's Tailscale name (now `liel.tail18e69e.ts.net`) doubles as CodeBot's address, so renaming the machine, moving to another tailnet or replacing the laptop breaks sign-in until every place below is updated. This is the complete list as of 2026-09-25, found by searching configs, both databases, Supabase and the domain. | # | Place | Holds | Why it must change | | --- | --- | --- | --- | | 1 | Domain forward for `codebot.lielbenor.com` (registrar, not in any repo) | 301 to `https://OLD/` | People start at this address | | 2 | Supabase → Authentication → URL Configuration | redirect allow list `https://OLD/onboarding` (Site URL is still `http://localhost:3000`) | Google sign-in only returns to allowed addresses | | 3 | `apps/web/.env.local` → `VITE_CODEBOT_ACCOUNT_ENTRY_URL` | `https://OLD/onboarding` | Compiled into the web app; needs a rebuild | | 4 | `~/.config/codebot/service.env` → `CODEBOT_ACCOUNT_ENTRY_URL` | `https://OLD/onboarding` | The host accepts arrivals only from this origin | | 5 | `/home/codebot-guest/.config/codebot/service.env` → `CODEBOT_ACCOUNT_ENTRY_URL`, `CODEBOT_GUEST_ADDRESS` | `https://OLD/onboarding`, `https://OLD:10000` | Same arrival check; the invite link | | 6 | Supabase `public.codebot_account_progress.server_url` (one row per account) | `https://OLD`, `https://OLD:10000` | Sign-in sends each person to this address | | 7 | Tailscale serve (443 → 3505, 10000 → 3525) and the share with the guest | per machine | Only on a new machine; a rename keeps them | | 8 | Installed phone apps and their notifications | per address | A web app belongs to the address it was installed from | | 9 | Docs: this file, workstation `README.md` (2026-09-25 entry) | the address | Keep the runbook true | Unaffected: conversation text that mentions the address, host identities and Supabase host bindings (keyed by server ID and key, not address), and HTTPS certificates (Tailscale issues them for the new name). Flow, with `OLD=liel.tail18e69e.ts.net` and `NEW` the new name from `tailscale status --json` (`Self.DNSName` without the final dot): 1. Supabase: add `https://NEW/onboarding` to the redirect allow list (keep `OLD` until the end) and set Site URL to `https://NEW`. 2. Owner host: edit `NEW` into places 3 and 4, then `npm run build` and `systemctl --user restart codebot`. 3. Guest host: `sudoedit` place 5, then `ops/guest-update.sh`, which rebuilds its web app from place 3 and restarts it. 4. Accounts: in the Supabase SQL editor run `update public.codebot_account_progress set server_url = replace(server_url, 'https://OLD', 'https://NEW') where server_url like 'https://OLD%';` 5. New machine only: restore data first (see backup and restore below), rerun `sudo ops/guest-install.sh`, recreate both `tailscale serve` mappings, and re-share the machine with the guest's Tailscale account. 6. Point the domain forward at `https://NEW/`. 7. Check: `codebot.lielbenor.com` → sign in as each account → each lands on its own host. Then remove `OLD` from the allow list, reinstall phone apps from the new address, re-enable notifications, and update place 9. Making this self-healing (a fixed sign-in site at `codebot.lielbenor.com`, and hosts that report their own address) is planned in `docs/plans/guest-hosting-and-global-instructions.md`. ## Reset someone's onboarding There is no button for this; it is an admin operation. Onboarding state lives in two places, and both must be reset together: - Supabase `public.codebot_account_progress` (one row per account): the first-use draft (`first_use`), `first_bot_id`, `first_use_complete`, and the computer connection (`hosting`, `server_url`, `server_id`, `setup_complete`). - The person's host database, `first_use_receipts` (one row per account): which Bot the first-use tour created. While it exists the host hands back that Bot instead of starting over. Host databases: the owner's is `~/.local/share/codebot/codebot.sqlite`; the guest's is `/home/codebot-guest/.local/share/codebot/codebot.sqlite`. ### Replay only the first-use tour Works only when the person's host has **no Bots**. The tour never creates a second Bot: it reuses the host's first existing Bot as "preexisting", and its last step then fails with "This Bot already has a workspace" (`apps/service/src/first-use.ts`, `create` and `acceptTask`). On a host that serves one person (the guest), use the full reset below instead. The account and its computer connection stay. 1. Stop the person's host (`sudo systemctl stop codebot-guest`, or `systemctl --user stop codebot` for the owner). 2. Delete their receipt, running as the host's user with a Node 24 runtime (for the guest): ```bash sudo -u codebot-guest /opt/codebot-guest/runtime/bin/node -e \ "new (require('node:sqlite').DatabaseSync)('/home/codebot-guest/.local/share/codebot/codebot.sqlite').prepare('DELETE FROM first_use_receipts WHERE account_id = ?').run(process.argv[1])" \ ACCOUNT_ID ``` 3. In the Supabase SQL editor, restart the draft at `meet` with fresh idempotency keys (reusing the old keys makes the host treat it as a repeat): ```sql update public.codebot_account_progress set first_use_complete = false, first_bot_id = null, first_use = first_use || jsonb_build_object('step', 'meet', 'name', '', 'tools', '[]'::jsonb, 'message', '', 'creationKey', gen_random_uuid(), 'messageKey', gen_random_uuid()) where account_id = (select id from auth.users where email = 'PERSON@EXAMPLE.COM'); ``` The same statement with `returning account_id` gives the `ACCOUNT_ID` for step 2. 4. Start the host again. At next sign-in the person sees the tour from the beginning. ### Restart onboarding with a fresh CodeBot, keeping the account The way to show someone the onboarding again on a host that serves only them (the guest). Their Bots and conversations go; their Google account, its link to the host and their project folders stay. CodeBot has no "delete Bot" feature, and deleting Bot rows by hand is unsafe, so the host's data is wiped except its three identity files. Done for the guest on 2026-09-25. 1. `sudo systemctl stop codebot-guest` 2. Wipe the data but keep the identity (password needed; run in a real terminal): ```bash sudo find /home/codebot-guest/.local/share/codebot -mindepth 1 -maxdepth 1 ! -name server-id ! -name server-signing-key ! -name owner-key -exec rm -rf {} + ``` 3. Run the Supabase statement from step 3 of the tour replay above. 4. `sudo systemctl start codebot-guest`, then check that `https://OLD:10000/api/onboarding/identity` shows the same `serverId`. After signing in, the app sees `first_use_complete = false` and shows the onboarding from the first step (`apps/web/src/onboarding/Onboarding.tsx`), and its last step creates a new Bot. Old sessions were in the wiped database, so the person signs in again. ### Full reset: the person starts as new Everything goes: account, Bots, conversations, attachments and recordings. Project folders are separate and stay unless deleted too (guest: `/home/codebot-guest/projects`). The owner's own full reset destroys the owner's data; back up first (below). 1. Stop the person's host and delete its data folder (guest: `sudo rm -rf /home/codebot-guest/.local/share/codebot`). The host gets a new identity on next start. 2. Delete the person in Supabase (Authentication → Users, or `delete from auth.users where email = '…'`). Their progress row and host binding are removed with it (`on delete cascade`). 3. Start the host and send a fresh invite (`sudo -u codebot-guest /opt/codebot-guest/invite`). Neither procedure has been run yet (written 2026-09-25). The reset draft was checked against `firstUseDraftSchema`; the first real run should confirm the tour restarts and the new Bot is created. ## Account service (Supabase) The central account service is Supabase project `wbgrjvqbupgdclaztuou`. It belongs to the Supabase login in the Chrome profile "mobile-coder" (the product's earlier name), not the `Lebnor` login. The `SUPABASE_ACCESS_TOKEN` exported in the shell is Lebnor's and gets 403 here. For CLI work, create a token in the mobile-coder account with Edge Functions, Migrations and Project Settings set to read-write, and keep it in `~/.config/supabase-codebot-token` (mode 600): ```bash SUPABASE_ACCESS_TOKEN="$(cat ~/.config/supabase-codebot-token)" \ npx supabase functions deploy host-access --project-ref wbgrjvqbupgdclaztuou ``` Redeploy `host-access` whenever a contract it parses changes, including `packages/contracts/src/avatar.ts`. It validates strictly and rejects fields it does not know, which surfaces as "Invalid account access request." ## Diagnose without exposing credentials ```bash systemctl --user status codebot.service --no-pager journalctl --user -u codebot.service -n 80 --no-pager ss -ltnp 'sport = :3505' ``` From an authenticated app browser, inspect `/api/diagnostics/host`, `/api/diagnostics/scheduler` and `/api/diagnostics/vision`. These contain counters and lifecycle state, not provider tokens. The host snapshot reports migration, event cursor, queued/running turns and recovery hold. Avoid dumping environment variables, cookies or provider credential files. Check the failed Bot's visible error before retrying; provider authentication, model access and rate limits are different failures. Existing configured credentials must still be valid. ## Back up a consistent installation Stop the service so database state and resource files describe the same point in time. Finish recordings and active work first if their unfinished tail matters. Pause checkout edits during the copy. A live copy of only `codebot.sqlite` can miss its WAL; SQLite's online backup API alone does not make the adjacent resource directory atomic. This procedure copies the whole stopped data directory. See [SQLite backup guidance](https://www.sqlite.org/backup.html). The archive contains owner sessions, the entrance key and private provider profiles. Keep it in a mode-0700 directory outside approved project roots. Back up project repositories separately; CodeBot's data archive does not contain their working trees. Browser-local unsaved drafts are also separate. Run in Bash, from the checkout, and stop on the first error: ```bash set -e umask 077 CODEBOT_BACKUP_DIR="$HOME/.local/share/codebot-backups/$(date -u +%Y%m%dT%H%M%SZ)" install -d -m 700 "$CODEBOT_BACKUP_DIR" systemctl --user stop codebot.service test "$(systemctl --user is-active codebot.service || true)" = inactive tar -C "$HOME/.local/share/codebot" -cpf "$CODEBOT_BACKUP_DIR/state.tar" . tar --exclude='./node_modules' --exclude='./dist' --exclude='./.git' --exclude='./.data' --exclude='./.playwright-mcp' -cpf "$CODEBOT_BACKUP_DIR/source.tar" . install -m 600 "$HOME/.config/codebot/service.env" "$CODEBOT_BACKUP_DIR/service.env" install -m 600 "$HOME/.config/systemd/user/codebot.service" "$CODEBOT_BACKUP_DIR/codebot.service" (cd "$CODEBOT_BACKUP_DIR" && sha256sum state.tar source.tar service.env codebot.service > SHA256SUMS) systemctl --user start codebot.service ``` If copying fails, retain the original data, inspect the error and restart the original service when safe. Do not promote a partial archive. A backup is only verified after an extraction and application smoke test as described below. ## Restore into a separate directory first Keep the original installation intact. Verify `sha256sum -c SHA256SUMS` inside the chosen backup directory. Extract only your own trusted archive into a new, empty, private directory; never extract over a running installation. ```bash umask 077 CODEBOT_RESTORE_DIR="$HOME/.local/share/codebot-restore-staging" mkdir -m 700 "$CODEBOT_RESTORE_DIR" tar -C "$CODEBOT_RESTORE_DIR" -xpf "$CODEBOT_BACKUP_DIR/state.tar" ``` Use the matching source archive and Node/dependency versions. Confirm database integrity before startup (Node 24, read-only): ```bash node --input-type=module - "$CODEBOT_RESTORE_DIR/codebot.sqlite" <<'JS' import { DatabaseSync } from 'node:sqlite'; const db = new DatabaseSync(process.argv[2], { readOnly: true }); const check = db.prepare('PRAGMA integrity_check').all(); const references = db.prepare('PRAGMA foreign_key_check').all(); if (check.length !== 1 || Object.values(check[0])[0] !== 'ok' || references.length) throw new Error('The restored database needs investigation'); console.log('Database integrity and references passed'); db.close(); JS ``` Start the restored copy on an unused loopback port with recovery hold. This prevents schedules and queued messages from starting automatically; explicit Send / Continue / Retry remains available. Do not make those actions part of a read-only inspection. Provider credentials must be configured separately if you deliberately run a model against this copy. ```bash NODE_ENV=production CODEBOT_DATA_DIR="$CODEBOT_RESTORE_DIR" CODEBOT_PORT=3508 CODEBOT_RECOVERY_HOLD=1 node --import tsx apps/service/src/main.ts ``` Connect to `http://127.0.0.1:3508/` with the restored entrance key. Confirm `recoveryHold: true` in the host diagnostics, inspect a known Bot's history and contract revisions, open a known screenshot/attachment, and play a recording. Confirm zero running turns and scheduler timers. Stop this inspection process before promotion. Recovered records and media must be reviewed if the backup followed a crash; an unfinished recording tail may be absent. For promotion, stop the original service, retain its data directory as a rollback copy, point `CODEBOT_DATA_DIR` in the private environment file at the verified restore and start in recovery hold. Confirm project roots and credentials, disable any schedules that should not resume, then set `CODEBOT_RECOVERY_HOLD=0` and restart when automatic work is intended. Never run two normal hosts from copies of the same queued/scheduled state. Do not run older service code against a newer database migration. ## Emergency stop and rollback Normal `systemctl --user stop codebot.service` stops scheduling, interrupts Bot turns, finalizes/reconciles recordings, closes browser work and terminates the service cgroup. Browser disconnection alone does not stop host work. If immediate termination is necessary, request a stop first so systemd does not restart the killed service: ```bash systemctl --user stop --no-block codebot.service systemctl --user kill --kill-whom=all --signal=SIGKILL codebot.service systemctl --user is-active codebot.service ss -ltnp 'sport = :3505' ``` An already stopped service may report no process to kill. Verify that it is inactive and the listener is gone. Force-killing can lose uncommitted media; the next start marks active turns interrupted and exposes recoverable footage. Use [... cut at 32000 characters] ``` ## File: ops/environment.example ``` # Store the real copy at ~/.config/codebot/service.env with mode 0600. # Paths below describe this workstation; change them for another installation. CODEBOT_PORT=3505 CODEBOT_DATA_DIR=/home/liel/.local/share/codebot CODEBOT_PROJECT_ROOTS=/home/liel/projects # Deployment and storage authority. Leave terminal access disabled until the # separate per-user terminal broker is installed and supervised. CODEBOT_DEPLOYMENT_MODE=self-hosted CODEBOT_TERMINAL_BROKER=disabled CODEBOT_TERMINAL_USER= # Add an already configured provider credential here only when needed. # CLAUDE_CODE_OAUTH_TOKEN=... # ANTHROPIC_API_KEY=... # Recovery inspection only: disables automatic queue pumping and schedules. # Explicit Send / Continue / Retry actions remain available. CODEBOT_RECOVERY_HOLD=0 # Optional: Jev classifies owner messages (emoji reactions, new-chat detection, kind of work). # JEV_URL defaults to TypeSafe's API; on the owner's workstation it points at the local jev-relay. # Without JEV_TOKEN those features are off and everything else works. # JEV_URL=http://localhost:8787 # JEV_TOKEN=... # Optional: Gmail for Bots. Create a Google OAuth client (type "Web application"), enable the Gmail API, # and register the redirect URI below. Without both values Gmail stays off. Host configuration only: # never put the secret in a VITE_ variable. See docs/automation/gmail.md. # CODEBOT_GOOGLE_CLIENT_ID= # CODEBOT_GOOGLE_CLIENT_SECRET= # Optional. Normally leave empty: a server connected to the account service returns through its fixed gmail-relay # address; otherwise through https:///api/gmail/oauth/callback. Set this only to force one address. # CODEBOT_GOOGLE_REDIRECT_URI= ``` ## File: ops/codebot.service ``` [Unit] Description=CodeBot private host After=network.target StartLimitIntervalSec=30 StartLimitBurst=3 [Service] Type=simple WorkingDirectory=%h/projects/codebot ExecStart=%h/.nvm/versions/node/v24.19.0/bin/node --import tsx apps/service/src/main.ts Environment=NODE_ENV=production Environment="PATH=%h/.nvm/versions/node/v24.19.0/bin:%h/.local/bin:/usr/local/bin:/usr/bin:/bin" EnvironmentFile=%h/.config/codebot/service.env Restart=on-failure RestartSec=2 TimeoutStopSec=45 KillMode=control-group UMask=0077 [Install] WantedBy=default.target ``` ## File: ops/codebot-guest.service ``` [Unit] Description=CodeBot guest host (a second account on this machine; see ops/OPERATIONS.md) After=network.target StartLimitIntervalSec=30 StartLimitBurst=3 [Service] Type=simple User=codebot-guest Group=codebot-guest WorkingDirectory=/home/codebot-guest ExecStart=/opt/codebot-guest/codebot EnvironmentFile=/home/codebot-guest/.config/codebot/service.env Environment="PATH=/opt/codebot-guest/runtime/bin:/home/codebot-guest/.local/bin:/usr/local/bin:/usr/bin:/bin" Restart=on-failure RestartSec=2 TimeoutStopSec=45 KillMode=control-group UMask=0077 # The guest's builds and Bots share this machine with the owner's; these caps keep them from starving it. MemoryMax=6G CPUQuota=400% [Install] WantedBy=multi-user.target ``` ## File: ops/codebot-family-lobby.service ``` [Unit] Description=CodeBot family front door: Google sign-in and a CodeBot per family member (see ops/OPERATIONS.md, "Home Wi-Fi address") After=network.target StartLimitIntervalSec=30 StartLimitBurst=3 [Service] Type=simple User=codebot-lobby Group=codebot-lobby WorkingDirectory=/home/codebot-lobby ExecStart=/opt/codebot-family/codebot EnvironmentFile=/home/codebot-lobby/.config/codebot/service.env Environment="PATH=/opt/codebot-family/runtime/bin:/usr/local/bin:/usr/bin:/bin" Restart=on-failure RestartSec=2 TimeoutStopSec=45 KillMode=control-group UMask=0077 MemoryMax=1G [Install] WantedBy=multi-user.target ``` ## File: ops/codebot-family@.service ``` [Unit] Description=CodeBot for family member %i (see ops/OPERATIONS.md, "Home Wi-Fi address") After=network.target StartLimitIntervalSec=30 StartLimitBurst=3 [Service] Type=simple User=codebot-f%i Group=codebot-f%i WorkingDirectory=/home/codebot-f%i ExecStart=/opt/codebot-family/codebot EnvironmentFile=/home/codebot-f%i/.config/codebot/service.env Environment="PATH=/opt/codebot-family/runtime/bin:/home/codebot-f%i/.local/bin:/usr/local/bin:/usr/bin:/bin" Restart=on-failure RestartSec=2 TimeoutStopSec=45 KillMode=control-group UMask=0077 # Family CodeBots share this machine with the owner's; these caps keep one from starving it. MemoryMax=6G CPUQuota=400% [Install] WantedBy=multi-user.target ``` ## File: ops/codebot-terminal-broker.service ``` [Unit] Description=CodeBot per-user terminal broker After=default.target StartLimitIntervalSec=30 StartLimitBurst=3 [Service] Type=simple WorkingDirectory=%h/projects/codebot ExecStart=%h/.nvm/versions/node/v24.19.0/bin/node --import tsx ops/terminal-broker.ts Environment="PATH=%h/.nvm/versions/node/v24.19.0/bin:%h/.local/bin:/usr/local/bin:/usr/bin:/bin" Environment=CODEBOT_TERMINAL_BROKER_SOCKET=%t/codebot-terminal/broker.sock Environment=CODEBOT_TERMINAL_TMUX_SOCKET=%h/.local/state/codebot-terminal/tmux.sock Restart=on-failure RestartSec=2 TimeoutStopSec=15 # The broker owns the managed tmux server. A web-service restart leaves this unit # untouched, while stopping or losing this unit tears down its tmux process group. KillMode=control-group UMask=0077 RuntimeDirectory=codebot-terminal RuntimeDirectoryMode=0700 [Install] WantedBy=default.target ``` ## File: ARCHITECTURE.md ``` # CodeBot Technical Architecture This document defines the technical boundaries that keep CodeBot modular, secure, responsive, and recoverable. It favors boundaries that change for different reasons rather than grouping unrelated behavior by screen or endpoint. ## Architectural invariants 1. A Bot is durable; provider processes and context epochs are replaceable. 2. The canonical transcript is lossless for user-visible conversation history and independent of provider storage. 3. Every mutation is authorized and validated at the local service before reaching files, providers, browsers, or recordings. 4. Every long-lived resource has one owner, one lifecycle, a bound, and an idempotent disposer. 5. UI state is a typed projection of domain state; impossible combinations are rejected at protocol boundaries. 6. A failure in one Bot, provider, viewer, or Streamable cannot corrupt unrelated Bots. 7. Realtime delivery may reconnect or replay; commands may not execute twice accidentally. 8. Generated resources never inherit application authority merely because they are rendered by CodeBot. 9. Every Streamable has exactly one immutable `ownerBotId`; no Streamable state, command, subscription, or context crosses that Bot boundary. ## Runtime topology CodeBot is local-first. The primary deployment is one service bound to loopback and published privately through Tailscale HTTPS. The installable PWA is the primary desktop and mobile client. ```mermaid flowchart TB subgraph ClientDevice[Desktop / phone / tablet] Web[React TypeScript PWA] Viewers[Resource and Streamable viewers] end subgraph Host[User-controlled CodeBot host] API[HTTP command/query gateway] RT[Resumable realtime gateway] Core[Bot application core] DB[(SQLite WAL)] Blob[(Artifact and recording store)] Scheduler[Generic wakeup scheduler] Providers[Provider supervisors] Chromium[Managed Chromium] Screenshots[Screenshot artifact index] FC[FeatureContract adapter] Git[Git projection and action gateway] end Web -->|HTTPS + WSS| Tailnet[Tailscale private routing] Viewers --> Web Tailnet --> API Tailnet --> RT API --> Core RT --> Core Core --> DB Core --> Blob Core --> Scheduler Core --> Providers Core --> Chromium Core --> Screenshots Core --> FC Core --> Git ``` Tailscale is transport and network membership, not the only application authorization boundary. CodeBot still issues scoped authenticated sessions and validates each capability. ## Volatility map Modules are separated when they are expected to change for different reasons. | Boundary | Reason it changes | Stable contract | | ----------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------- | | Bot domain | Product semantics change | Commands, events, state transitions | | Provider driver | Provider CLI/protocol changes | Start/resume/interrupt/observe/capabilities | | Persistence | Storage/retention strategy changes | Transcript, Bot, epoch, artifact repositories | | Realtime transport | Network/replay strategy changes | Versioned event envelopes and cursors | | Client platform | Browser/native shell changes | Application API and view models | | Resource renderer | New media types or security policy | Renderer capabilities and safe resource descriptors | | Vision source | Chromium/CDP behavior changes | Page lifecycle, frames, input, snapshots | | Vision transport | Delivery technology changes | Subscribe/unsubscribe/quality/backpressure | | Recorder | Codec/storage policy changes | Start/pause/finalize/recover | | Screenshot index | Capture tools and gallery behavior change | Declared capture, latest pointer, ordered artifact feed | | FeatureContract adapter | Contract schema/store and synchronization rules change | Identity, current-context binding, revisions, validation, and revision stream | | Git gateway | Git projection/action policy changes | Current repository, status revision, typed commands, outcomes | | Authentication | Identity/lease policy changes | Principal and capability checks | | Automation | Trigger sources change | Durable deduplicated Bot wakeup | This separation allows, for example, Vision delivery to move from binary WebSocket frames to WebRTC after measurement without changing browser ownership, recording, or UI semantics. ## Proposed repository shape The repository begins lean and grows only when a real boundary exists: ```text codebot/ apps/ service/ # composition root and local process web/ # responsive React/PWA client packages/ contracts/ # versioned HTTP/realtime schemas and generated types bot-domain/ # Bot, turn, context epoch, wakeup policies provider-runtime/ # supervisor plus Codex/Claude/OpenCode drivers persistence/ # SQLite repositories and artifact metadata realtime/ # cursors, replay, liveness, backpressure resources/ # safe descriptors and renderer registry vision/ # Chromium source, control, transport, recording screenshots/ # screenshot intake, indexing, thumbnails feature-contract/ # latest/revision projection git-surface/ # current-repository projection and quick actions access/ # principals, sessions, permissions, leases MASTER-PLAN.md PRODUCT-SPEC.md ARCHITECTURE.md IMPLEMENTATION-ROADMAP.md ``` This is a target shape, not permission to create empty packages. A package is extracted when it owns a contract and can be tested or operated independently. UI components remain close to the user concept they represent; generic helper directories are not a dumping ground. ## Domain ownership ### Bot The Bot aggregate owns: - Stable identity, name, role, provider configuration, and Primary Folder - Automatic-action policy and attached skills - Current lifecycle and attention projection - Current provider context epoch - Current FeatureContract identity, when present - Current canonical working location and project/repository projection - The one-at-a-time active turn invariant - Queued user messages and wakeups - Owned Streamable identities, not Streamable implementation state The Bot does not own provider process internals, browser page internals, or artifact bytes. Each initial Streamable kind has at most one logical instance per Bot. The shared Streamable identity contract is: ```ts interface StreamableIdentity { id: string; ownerBotId: string; kind: "vision" | "screenshot_gallery" | "feature_contract" | "git_quick_info"; } ``` `ownerBotId` is immutable. A unique `(ownerBotId, kind)` constraint prevents duplicate logical surfaces. Deleting or copying data never mutates a Streamable into another Bot's scope. ### Allowed project roots Machine configuration defines one or more allowed project roots. Every filesystem-bearing command uses the same path authority: 1. Require an absolute or Primary-Folder-relative candidate. 2. Resolve the nearest existing parent and final real path. 3. Resolve symlinks before authorization. 4. Confirm the result is equal to or a descendant of an allowed canonical root. 5. Reject missing/ambiguous authorization before spawning a provider or opening a resource. Provider child processes receive an allowed starting directory. Where a provider can invoke arbitrary shell commands, CodeBot clearly treats the Bot as host-authorized within the configured scope; the UI must not imply stronger sandboxing than the runtime provides. Stronger OS-level isolation can be added behind the execution boundary later. ### Turn and queued intent One Bot has at most one active provider turn. New user input is either: - Accepted as the next turn - Added to a durable queue - Held in the queue while a reply is running, shown grey. When a round of tool calls finishes (Claude's `PostToolBatch`, Codex's last tool item completing) the queue is handed to the same reply, oldest first. If the reply ends first, each message becomes its own turn, in order - On touch screens a queue bar above the composer shows how many are queued, with 44px Edit and Send now buttons; Stop stays beside Queue message while a draft is typed - Taken back: `POST /bots/:id/queue/take` removes the queued owner messages and returns them to the composer, one per line ahead of the draft, to be edited and sent again. Messages already on their way to the provider stay - Sent now: `POST /bots/:id/queue/send-now` hands the queue over at once. Claude moves running shell commands and sub-agents to the background and reads the messages in the same turn; otherwise, and always for Codex, the reply is interrupted and the messages follow as its next turns. Stop (Esc) interrupts the same way and never discards the queue - Rejected with an actionable state reason Every command carries an idempotency key. A reconnect may repeat delivery of a command request, but the domain executes it once. ## Provider runtime Each provider driver implements the same semantic surface: - Declare capabilities - Start a context epoch - Resume when safe and supported - Submit a user turn - Stream normalized user-visible events - Report lifecycle/attention changes - Interrupt the active turn - Suspend/resume browser automation for Vision control - Read provider session metadata needed for recovery - Dispose the process and observers idempotently Provider-specific events are translated once at the adapter boundary. The domain and UI never branch on raw Codex, Claude, or OpenCode message shapes. A provider supervisor owns the child process, stdout/stderr readers, parser, observer, exit handling, and restart policy for one Bot epoch. Process output queues are bounded; backpressure or truncation produces an explicit diagnostic event rather than unbounded memory growth. ### Provider capabilities Capabilities are data, not scattered conditionals. Examples include: - Can interrupt current turn - Can accept queued follow-up input - Can resume a native session - Can report tool/activity events - Can pause at a browser-action boundary - Can estimate context usage The UI and orchestration policy derive available actions from capabilities plus canonical state. ## Canonical transcript and context compaction ### Canonical history SQLite stores an append-only ordered event ledger for the user-visible conversation. It includes: - Original user and assistant messages as displayed - Message delivery and terminal outcomes - Artifact/resource references - Screenshot capture events - Concise tool/activity summaries intended for the user - Control handoff and recording events - Context epoch boundaries - Wakeup instructions and outcomes Large bytes live in the artifact store and are referenced by stable IDs. Provider chain-of-thought and unbounded internal logs are not canonical conversation content. The client reads transcript pages by stable ordinal/cursor. A bounded in-memory replay window accelerates reconnect but is never the sole copy. ### Context epochs A context epoch — stored as a **chat** (`chats` table) — maps the durable Bot to one provider context/session. The transcript stays one conversation; each turn records its chat (`turns.chat_id`). A chat records the provider session identity, start and end, end reason (`topic` or `cleared` for new behavior; older `settings` and `size` records remain readable), a label (the first line of its first message) and a kind of work chosen by Jev. A new chat starts, checked before a turn is claimed, only when Jev says with confidence ≥ 0.85, in the adaptive topic checks, that the message starts a new topic. Anything ambiguous stays. Provider processes may refresh for their own runtime settings, but that never creates or prevents a visible chat boundary. A Claude Bot keeps one process across chats: a new chat starts a new session inside it (`/clear`), so its background work keeps running (`docs/plans/background-work-survives-chats-design.md`). Continuity comes from the transcript itself rather than a summary: when a chat ends, CodeBot writes its turns as a text file to `chats/` in the Bot's home, and the system prompt names that folder. The Bot reads the newest file when the owner refers to earlier work. The message that opens a new chat carries a `chat` marker, shown as a divider. Original messages remain unchanged and queryable. Design and QA record: `docs/plans/message-classification-and-live-sessions.md`. Classification sends only owner-typed messages, after a local pass strips secrets and personal data (`message-redaction.ts`), to Jev through `jev-client.ts`. Without `JEV_TOKEN`, reactions and topic splits are off and the other rules still apply. ## Persistence SQLite in WAL mode is the default because Cod [... cut at 14000 characters] ```