# Billion — the one agent you talk to Design notes, worked out step by step. Status: **built** (parts 1–4, Codex approvals included, and the charter split). Builds on PR #92 (orchestrator charter template and guide). ## Idea The last step of the progression: once the job board runs work without you watching, Billion stops you having to post the jobs too. Give it a goal and it runs the board. Borrowed from Munder Difflin's orchestrator ("Michael"): one contact point. You talk to Billion; Billion posts job cards, directs workers, and approves or denies their permission prompts. It escalates only what needs you. ## Step 1 — the server starts Billion Decided: 1. **On by default**, with a setting to turn it off. 2. **Repo:** `~/.agent-007/billion/` (under `CONFIG_DIR`, so tests that redirect `AGENT007_CONFIG_DIR` never touch the real one). Override with `BILLION_DIR`. First run: `git init`, copy Billion's templates (`charter.md` → `CHARTER.md`, `owner.md` → `CLAUDE.md`, plus `STATE.md` and `COMPANY.md`), commit. Already there: just use it (the charter is refreshed on every start; see "Charter upgrades"). 3. **Runs without permission prompts** (`--dangerously-skip-permissions`): it works all the time and must never block on a dialog. 4. **Server restart resumes it**: `claude --continue` when the folder has a previous session. Memory files cover whatever the conversation loses. A resumed conversation keeps the board tool definitions it first loaded (Claude Code pins them in the transcript), so each start saves the current ones to `billion-tools.json` in the config folder, and the restart prompt names any that changed since the last start and points there. Its habits outlast a re-read charter too, so each start also saves the charter to `billion-charter.md` in the config folder, and a resumed conversation's prompt quotes the paragraphs that changed (past 40 lines, only their sections' names). With no saved copy yet, it quotes the charter's paragraphs on answering the owner in the tab rather than the terminal. Every start prompt, first run, restart or handover, also says which CLI Billion runs on and the models a card may name per CLI, and the charter has it name both `agent` and `model` on every card. The start waits up to 15 s for the first model discovery; past that it starts anyway and the prompt points to `billion-tools.json`, which carries the lists and is rewritten whenever a refresh finds them changed. A list that changed between starts makes `post_job` one of the changed tools the restart prompt names. 5. **No auto-restart** if it exits or crashes: show it stopped, with a Start button; start it again on the next server start. 6. **Fixed name "Billion"**, reserved. `send_message` addresses agents by name, so no other agent may take it. No cocktail codename. 7. **Placement:** top of the left panel (above the repo groups, not under "(no repo)"), top center of the office on its own desk, and its terminal running from the start, its tab in the right panel hidden until the owner clicks Billion (closing it hides it again). 8. **One Billion per server.** Messages only flow between agents with the same owner; per-user Billions wait until someone runs multi-user. Existing code to lean on: repo-less agents already run (`server/pty.js` `createSessionFromConfig`, cwd falls back to home), are listed under "(no repo)" (`public/modules/explorer.js`, the "(no repo)" section), and sit in a final pod (`public/modules/office.js:156`). Billion is a repo-less agent whose cwd is its own folder. ## Step 2 — Billion approves workers' permission prompts Decided: workers keep asking for permission; Billion answers. Both CLIs have a `PermissionRequest` hook with the same shape: - Input on stdin: `tool_name`, `tool_input`, `cwd`, `session_id`, ... . - Output: `{"hookSpecificOutput": {"hookEventName": "PermissionRequest", "decision": {"behavior": "allow"}}}`, or `"deny"` with a `"message"`. - **No decision printed → the normal dialog appears for a person.** - Codex: `updatedInput` / `updatedPermissions` / `interrupt` are reserved and fail closed (deny). Don't use them. Flow: 1. At spawn, the server adds the hook: `--settings` for Claude Code, a `-c` override for Codex, the same way the board MCP server is added (`server/agent-mcp.js` `withMcpConfig`). It is added to argv after the fact, so `isUnguarded` (which reads the typed command) still sees the worker as guarded. 2. The hook sends the request (tool, input, agent, repo, branch) to the server and waits. 3. The server hands it to Billion; Billion answers with a new MCP tool `answer_permission {id, decision: allow|deny|owner, reason}`. A deny reason goes back to the worker; owner leaves it to you. 4. **No answer within ~2 minutes, or Billion not running → no decision**, so the dialog appears for you. Never allow by default. 5. Actions under the escalation rules (destructive, paid, secrets, ...): Billion deliberately gives no decision and tells you why in its terminal. Constraints and consequences: - **Hook timeouts:** both CLIs limit how long a hook may run. Set that limit a little above the 2-minute wait, or the CLI gives up first and counts it as a deny. - **Messaging rule must change.** Today guarded agents cannot message an unguarded one (`server/messages.js` `messageableAgents`), which would cut off approvals and "done" reports. Exempt Billion as a recipient. Accepted cost: a worker that read untrusted text can pass instructions to an agent with full access. Messages are labelled "from another agent, not the user" and the charter says to treat them as information — an instruction, not a sandbox. - **Billion can hold up the workers.** Messages reach it only while it rests at its prompt, and every approval takes up its conversation and tokens. Start with Billion answering directly and log how long approvals wait. If waits get long, answer approvals with a separate `claude -p` call that uses Billion's charter, and give Billion only a summary. ### Codex hook trust (resolved) Codex runs a hook only once its hash is trusted in `~/.codex/config.toml` (`[hooks.state]`), or when launched with `--dangerously-bypass-hook-trust` (which applies to that session only). - If Codex loads hooks only from `~/.codex/` and `-c` overrides → **use the flag.** Simpler, and no trust prompt that Billion can't answer. - If Codex also loads hooks from the project (`.codex/` in the worktree or a parent) → the flag would run those unreviewed too. That covers repos you didn't write, and a worker that writes a hooks file into its worktree turns "write a file" (which approval covers) into "run any command" on the next re-spawn (which it doesn't). Then instead: approve once in Codex's own "Hooks need review" prompt, with a **stable hook command** (`~/.agent-007/hooks/permission-request.js`, port passed by env var) so the hash doesn't change across runs, worktrees and ports. The app already recognises that prompt (`lib/helpers.js:96`), so Billion won't answer it. - Not writing the hash into `~/.codex/config.toml` ourselves: agent-007 has not changed the user's permanent CLI config so far. Question sent to the other agent (codex-cli 0.156.1; Codex is not installed on this machine): > 1. Besides `~/.codex/hooks.json` (and `~/.codex/config.toml`) and `-c` > overrides, does it load hooks from the project, e.g. a `.codex/` directory > in the working directory, the git root, or any parent? > 2. If so, are those under the same `[hooks.state]` hash trust, and does > `--dangerously-bypass-hook-trust` skip review for them too? > 3. Does a project's `.codex/` load only when the project is marked trusted > (`[projects.""] trust_level`)? Is a git worktree of a trusted repo > the same project, or a new, untrusted one? Answer (other agent, 2026-09-24; tested against the installed 0.156.1 binary with a throwaway `CODEX_HOME`, file hooks only): 1. Project hooks load from every `.codex/` between cwd and the repo root (`hooks.json` and a `[hooks]` table in `config.toml`), never above the root. 2. They use the same `[hooks.state]` hash trust (key `:::`). The bypass flag skips the hash check for project and user hooks alike. 3. Project hooks load only if the project is `trust_level = "trusted"` in `config.toml` (a `-c` override of that does not count). The bypass flag does not override this. A worktree counts as the same project, **and runs the main checkout's hook files, never the worktree's own** (trust keys use main checkout paths). What that means: - The "worker writes a hooks file into its worktree" risk is **gone**: those files never run. - Untrusted repos are safe even with the flag. - What the flag still exposes: in a *trusted* repo, a hook that lands in the main checkout (e.g. a merged contributor PR) runs without hash review. - Their conclusion ("the hook must sit in each main checkout or `~/.codex/hooks.json`") assumes a file. We plan a `-c` override, which was not tested. That case decides the design. Follow-up sent (all without the bypass flag): > 1. A PermissionRequest hook supplied only via `-c`, no hooks file: does it > run with no `[hooks.state]` entry, or is it held for trust? > 2. If held: what is its `[hooks.state]` key, and is it stable across cwds, > worktrees and repos (same hook command)? > 3. Does a `-c` hook run in an untrusted project? > 4. Does `hooks/list` show it, and with what source? What each outcome means: - Runs untrusted, in untrusted projects too → no flag, no approval. Best case. - Needs trust, stable key → approve once (stable command, port by env var). - Needs trust, key varies by folder → choose between the flag (accept the merged-PR risk above) and writing our own trust entry into `config.toml`. Follow-up answer (other agent, 2026-09-24; app-server threads with `approvalPolicy: "on-request"`, never the bypass flag; `codex exec` forces `approval: never` so it can't test this): 1. A `-c`-only hook is **held for trust** without a `[hooks.state]` entry; with a trusted hash it runs and its allow is honoured. Stdin carries `tool_name`, `tool_input` (`command`, `description`), `cwd`, `session_id`, `turn_id`, `model`, `permission_mode`, `transcript_path`. 2. Key: `//config.toml:permission_request::` — no cwd in it; identical across non-git folder, repos and a worktree. The hash covers the command string (exact, incl. the script's absolute path), `timeout` and `statusMessage`; not `matcher`. Position is in the key: a hook placed ahead of ours makes ours untrusted. **The trust entry can itself be passed with `-c`** (`hooks.state={""={trusted_hash="sha256:…"}}`). 3. It runs in untrusted projects and their worktrees: only per-hook hash trust applies, not project trust. 4. `hooks/list` shows it with `source: "sessionFlags"` and a `trustStatus` (untrusted / trusted / modified). **Resolved: the best case, and better.** Each Codex spawn gets two `-c` flags: the hook and its trusted hash. No bypass flag (repo hooks keep their review), no hook file anywhere, nothing written to `~/.codex/config.toml`, no trust prompt for you, works in every worktree. Build rules: 1. Ours is the **only** `PermissionRequest` entry agent-007 passes (position is in the key). 2. The command never changes: as built, `"" ""` with no argument, fixed for the server's life. The worker's MCP config (board address and token, 0600) reaches the hook as a path in `AGENT007_HOOK_CONFIG`, set on that worker's PTY only; Claude Code's hook still takes it as an argument. 3. **Get the hash at startup from Codex itself**: run `codex app-server` with our `-c` hook and read it from `hooks/list`. Don't hardcode (the path includes the home directory) or reimplement the hashing (a Codex update could change it). Codex not installed → skip; no Codex workers to hook. 4. `timeout` above Billion's 2-minute wait; it's in the hash, so pick once. Checked at build time (codex-cli 0.157.0, app-server turns with `approvalPolicy: "on-request"` and a read-only sandbox, stub hooks, a scratch `CODEX_HOME`): with the user's own `PermissionRequest` hook in `~/.codex/hooks.json`, both run, side by side (keys `…/hooks.json:…:0:0` and ours, `//…:0:0`, so ours stays trusted). **A deny from either wins**; failing that an allow from either stands; only when neither decides does the dialog appear. So a user hook that denies overrides Billion's allow, and a user hook that allows lets a request through that Billion left to the owner. Our `hooks.state` flag merges into the user's `[hooks.state]`: their own trusted hooks stay trusted. ## Step 3 — first run: introduction Decided: on its first run (empty repo just created), Billion greets you with a short self-introduction, then asks for the information it needs. 1. **The server decides it's the first run** (it just created the repo) and starts `claude ""` instead of `claude --continue`. If the app is closed partway through, the next start resumes with `--continue`; the charter says to finish the introduction while `STATE.md` still says "not started". 2. **Ask only what it can't look up.** Repos, agents and cards it finds itself (board tools, file system). **Decided:** a one-sentence introduction, the escalation rules (so you know what it will bring to you, and can change them), then three questions — the mission, where new repos go, and whether to ask before merging a PR that deploys. Everything else uses defaults and can be changed by telling Billion later. Example: > Hi, I'm Billion. I run the work here: I turn a goal into a plan, hand the > pieces to workers on the job board, review what comes back, and merge it. > > I decide most things myself. I'll come to you before anything that: > - spends money; > - needs access I don't have (accounts, keys, logins); > - can't be undone, like deleting data or repos; > - makes a private repo public (its whole history goes public, including > anything ever committed); > - touches payments, pricing, security or secrets; > - is a real fork in direction that's yours to call. > > If you want that list different, just tell me, now or any time. > > If you don't give me a goal, I'll look for improvements across your > projects and work on those. Do you have a specific mission in mind? It > can be as narrow as "ship the Windows build of agent-007" or as broad as > "grow the company". > > And when I start a new project, where should its repo go? Your projects > seem to live in `~/Projects/`. Shall I use that? > > Some merges deploy: they publish a release or push to production. Shall > I ask you before merging those, or merge them like any other PR? (I'd > ask, at least for repos that deploy to production.) After your answers, it closes with where everything lives: > Got it. Everything I know is in `~/.agent-007/billion/`: my rules in > `CLAUDE.md`, the mission and what we learn in `COMPANY.md`, and my > current plan in `STATE.md`. Every change is a commit there. Ask me to > change any of it, any time. Starting now. The suggested folder is worked out from the repos already added (their common parent), not hardcoded; with none, it just asks. Where answers go: the mission (or "none, find improvements") → `COMPANY.md`; the projects directory and the deploy answer (a dated standing rule such as "Deploying merges: ask first", the only policy for deploying merges) → `CLAUDE.md` (an operating rule, like the escalation list, which also lives there and changes there if you ask). Then commit, `billion_ready`, start the loop. 3. **It never contacts your agents.** It manages work, not agents: it messages only the workers of its own cards and leaves agents you started alone, so there is nothing to ask (replaces PR #92's "message every running agent" first cycle). No messages during the introduction is covered by point 4. 4. **Billion's message queue holds until the introduction is done.** `server/messages.js` already queues per agent (cap 20), delivering only while the recipient rests at its prompt, not within 30 s of a person typing, one per stop. That alone isn't enough: during the introduction Billion rests at its prompt waiting for *you*, so the queue would deliver into your conversation. Add one condition to the "may I deliver now?" check: nothing goes to Billion until it calls `billion_ready`. - Messages (worker questions, `finish_job` notices) wait and arrive after the introduction. Nothing is lost. - The owner's own words are the exception: chat messages, answers and Telegram messages go in as usual, since the introduction is waiting for them. The introduction itself is said with `tell_owner`, in the chat the owner reads, not only in Billion's terminal (its tab is hidden by default). Found in the fresh-install test of v0.46: the introduction sat in the hidden terminal and the owner's reply in the chat waited for a `billion_ready` that waited for that reply. - Approvals get no decision straight away while the inbox is held, so the dialog goes to you at once. Rare in practice: after a restart only the board starts workers (its running state is saved; first check 2 s after boot, `server/jobs.js` `startDispatcher`), and a new install has an empty board. 5. **The loop starts right after the introduction**, in the same conversation. Later starts: `--continue`, then straight into the loop. ## Template files The templates themselves, in `templates/billion/`, are the authority; the copies quoted below are how they were designed and may have drifted in wording. Billion's own `templates/billion/`, starting as a copy of PR #92's `templates/orchestrator/` (merge #92 as the general template; don't couple Billion's behaviour to a file others edit for their own orchestrators). Copied into `~/.agent-007/billion/` on first run. ### The files at a glance - **`CHARTER.md` — how Billion works, as Agent 007 ships it.** Role, cycle, principles, escalation list, tools and limits. Rewritten by the server on every start; Billion never edits it. - **`CLAUDE.md` — the owner's rules.** Imports `@CHARTER.md`, then the owner's settings and rules (e.g. where new repos go), which take precedence. Changes only when you ask. Loaded automatically by Claude Code at session start (with the charter) and kept through compaction. - **`COMPANY.md` — what's true about the company.** *Mission* (your words; only you change it) + *What we know* (Billion: projects, customers, numbers, lessons). Changes when something is learned. **Not imported** (it grows, and an import would put all of it in every turn, approvals included). A pointer in `CLAUDE.md`; read at the start of every cycle, so the mission is in front of Billion whenever it plans, and reloaded after compaction. **Kept as an index:** a line or two per project pointing at the project's repo (README / `NOTES.md`), where the details live; only cross-project things (mission, project list, customers, key numbers, lessons) stay here. - **`STATE.md` — what's happening now.** Plan, waiting on you, short-term notes. Rewritten every cycle. Read explicitly at the start of every cycle, not imported (an import is read once per session and this changes every cycle). After a restart it's the handoff note. All of this — what each file is for, when it's read, and the boundary test — goes into `CLAUDE.md` itself, the one file guaranteed to be in context. Boundary test for Billion: - A rule the owner gave me? → `CLAUDE.md` (Agent 007's rules are in `CHARTER.md`, read-only) - A fact that will still matter in a month? → `COMPANY.md` - About what's happening now? → `STATE.md` - A card's status or full result? → the board - Why I decided something? → that cycle's commit message ### `charter.md` (→ `CHARTER.md`) Billion is a founder: it runs the company, not just the office. It turns a statement into a plan and the plan into cards. - **The statement:** free text from you, kept in `COMPANY.md`. Anything from "build an app that does X" to "improve the company". Limits you want (money, time, how many projects) go in the statement too. - **The cycle, always the same:** read `COMPANY.md` and `STATE.md` → check status (what has been done, its cards and PRs, worker reports and questions, pending approvals) → make or update the plan → post cards → commit → schedule the next wake-up. Everything else — start a project, research, build, drop — is a decision inside the plan, not a step in the cycle. - **`STATE.md` holds the plan and status**, rewritten every cycle. The decisions and why go in each cycle's commit message. With `CHARTER.md`, `CLAUDE.md` and `COMPANY.md`, that is all of Billion's files. - **It manages work, not agents.** It posts cards, follows them with `list_jobs`, and messages only the workers of its own cards. Agents you start by hand are yours. Throughput is capped by the board's `maxPerRepo` and, across all repos, its `maxWorkers` (lowered while the machine is short of memory; see [FEATURES.md](FEATURES.md)). `post_job`'s `urgent: true` puts one of its cards at the front of the queue when the board is full. - **Tools and limits:** as in #92, plus Billion's own `billion_ready`, `add_repo`, `close_job` and `answer_permission`, and `read_agent_screen`: the last lines (default 40, at most 200) of a worker's terminal, ANSI stripped, with its status. Narrower than `send_message`: only workers on Billion's own cards, never an agent started by hand, since a screen can show a secret that scrolled by. The text is never logged, and reaches Billion quoted and labelled as untrusted. Principles (the charter gives these, not procedures): - **Every project is a repo, from the first research onwards.** Research, drafts and code for a project all live in its repo; Billion's own repo holds only its memory. A new repo needs a remote and a pushed `main` before its first card (workers branch from the remote base; PRs need a remote), and `add_repo` so the board knows it (today `post_job` rejects unknown repos, `server/jobs.js:333`; `add_repo` wraps `addRepo`, `server/git.js:173`). Private by default. - **Check results, not claims.** - **Billion reviews and merges its PRs**, after `merge_check` says whether the merge deploys (see *Merges that deploy*). - **Escalate** money, missing access, irreversible steps, payments / pricing / security / secrets, making a private repo public (its whole git history goes public), and strategic forks. Going public is **not** an escalation on its own: publishing posts, changing the website, launching, emailing people are Billion's call. A merge escalates when it touches payments, pricing, auth, secrets or data deletion. - **Archive, never delete.** - Money and safety sections: as in #92. Practical note for the charter: check `baseRefName` and retarget stacked PRs to main before merging. Machine-specific quirks (which `gh` account can write, for example) are not in the template; Billion learns them and keeps them in its own `CLAUDE.md`. Projects directory for new repos: asked in the introduction (step 3). ### `STATE.md` Decided. Billion's working memory between cycles: read first after a restart, rewritten whole every cycle, readable by you. The board already records every card Billion posted (`postedByBillion`, set from the posting session in `server/jobs.js` `postJobForAgent`; `postedByAgent` is only the display name) with its status, PR and summary, so none of that is copied here — it would go stale. `STATE.md` holds only what the board can't know. ```markdown # STATE Rewritten every cycle; keep it to one screen. The board is the record of my cards and their results. This file is what the board can't know. Status: not started ## Plan Next steps toward the statement in COMPANY.md, in order. Add the card id once a step is posted. Group by project when there is more than one. ## Waiting on you Escalations: what, why, what I recommend, asked when. ## Notes Short-term only: what the next few cycles need. Anything that will still matter in a month goes in COMPANY.md. ``` - **Status:** "not started" until the introduction is done (step 3's marker); then a line or two on the current focus. - **Plan:** next steps in order; the order is the priority. Posted steps carry their card id. Finished steps drop out; the commit history keeps them. - **Waiting on you:** open escalations, each with a recommendation. - **Notes:** short-term only ("recheck card 118 after the retry"). Anything that will still matter in a month goes in `COMPANY.md`; full results stay on the card. Example of an active one: ```markdown Status: Validating a CLI for agent cost reports; building agent-007's close_job tool. ## Plan agent-cost (new repo, ~/Projects/agent-cost): 1. Market research: who tracks agent spend today (card 118, in progress) 2. Decide build or drop from 118's result agent-007: 1. close_job tool (card 119, in Review, reviewing the PR) 2. Notify the poster on finish_job ## Waiting on you - Buy agentcost.dev ($12/yr)? Needed only if we build. Recommend: wait for card 118. Asked 14:20. ## Notes - Card 118's first run hit a rate limit; recheck its summary before deciding. ``` ### `COMPANY.md` Decided. The company's lasting memory: your mission plus what Billion learns that outlasts a cycle. Uppercase to match `CLAUDE.md` and `STATE.md`. ```markdown # Company ## Mission ## What we know Maintained by Billion. Facts that outlast a cycle: - Projects: each one's repo, what it's for, its stage, where its research lives. - Customers and channels: who uses what, where they came from. - Numbers that matter: users, revenue, costs. - Lessons: what worked, what didn't, and constraints (e.g. the board runs one worker per repo). ``` The split: `COMPANY.md` = what stays true (changes when something is learned); `STATE.md` = what's happening now (changes every cycle); the board = every card and its full summary. Billion's test: "would this still matter a month from now?" → `COMPANY.md`. One file rather than a separate `MISSION.md`: the mission is one paragraph, marked as yours. ### `decisions.md` — dropped Decided. Each cycle's commit message is the decision log: git history is already append-only and timestamped, and a separate file would have to be kept in step with it. Billion reads recent decisions with `git log -10`; you read them with `git log` in its repo. The card id replaces #92's "who" field. Example: ``` cycle: start agent-cost; drop the browser-extension idea - Started agent-cost: three users asked for per-agent spend reports (card 112). - Dropped browser extension: research found 4 free competitors (card 107). - Merged #119 (close_job): reviewed, CI green, no escalation items. ``` ## Board changes on main (v0.4.8.0 #91, v0.4.9.0 #93) and what they change - Workers finish with `finish_job` (PR URL, or a summary for `requires_pr: false` cards) → card to Review. Review keeps the agent until Done, so Billion can question a worker before merging. Schedules post a run card per firing and never flood the board. - Research results come back as **card summaries** (`read_job`), not files. `STATE.md` Notes keep only conclusions, never copies of summaries. - "Report back to Billion" in every card is **dropped**: `finish_job` is the report. Built: when one of Billion's own cards reaches Review, the server sends Billion a notice, so it wakes at once. A server notification isn't agent-to-agent, so the guarded/unguarded rule doesn't apply; the messaging exemption is now needed only for worker questions and replies. - **Gap: Billion can't close a card that has no PR.** PR cards close on merge; a no-PR card waits in Review for Done, which only the UI can do. Needs a tool (e.g. `close_job`: accept → Done, or reject → back to To do with a note), or Review fills with idle workers. - Recurring work → a schedule card Billion posts once. ## GitHub accounts The owner may be signed in to more than one gh account (say one for personal repos and one for an organisation's), each unable to see the other's repos. `gh auth switch` changes the active account for the whole machine, so one agent switching it breaks every other agent and the owner's shell until someone switches back. Nothing in Agent 007 switches it: - A board worker spawned (or re-spawned onto its card) in a repo with a github.com remote gets `GH_TOKEN` for the account that can see it (the account named like the repo's owner, else the first whose token can read `repos//`), remembered per repo for the server's lifetime and re-checked at each spawn. Git's credential helper for github.com is pointed at `gh auth git-credential` through `GIT_CONFIG_*`, so `git push` uses the same account. The token is in the worker's process environment, the same exposure as before (a worker could always run `gh auth token`), and is never logged. hosts.yml is never written. - Claude Code workers are spawned with deny rules for `gh auth switch`, `gh auth login` and `gh auth logout`, which hold in every permission mode; every worker's card prompt says never to run them (Codex has no deny rules). - The board's own gh calls (PR lookups, CI notices) pass each account's token per call and remember which one answered for each repo. - Billion works across repos, so its charter says to pick the account per command: `GH_TOKEN=$(gh auth token -u ) gh …`. - `agent-007 doctor` names the account board workers use for each GitHub repo on the board, and marks ✗ a repo no signed-in account can see. ## Merges that deploy A revert undoes a merge but not a deploy the merge set off (#192). Billion merges with `gh pr merge` from its own terminal, so the guard is a check it runs first: the Billion-only board tool `merge_check` (`server/merge-check.js`), given a PR URL or a card id. Read-only, through the repo's gh account (`ghAccountFor`); it never writes to GitHub or runs a workflow. - **What the merge runs.** `.github/workflows/*.yml` as they will be after the merge: the base branch's, with each workflow the PR adds, edits, renames or deletes taken from the PR's head instead (a note names them; one it cannot read at the head is unknown): `push` whose branch filters match the base and whose path filters match the PR's changed files, `pull_request` / `pull_request_target` with `types: [closed]`, `workflow_run` chains from those, and the jobs of a local reusable workflow they call. Release and tag-push workflows count only when one of those makes a tag (`git tag`, `gh release create`, a release action); otherwise the result says they were left out. - **Which of those deploy.** A job with an `environment:`, deploy / release / publish in its id or name, or a step using a known deploy action (`aws-actions/*`, `google-github-actions/deploy-*`, `azure/*deploy`, Vercel, Netlify, Fly, Pages, Wrangler, release actions, `docker/build-push-action` with `push: true`) or running `docker push`, `npm publish`, `vercel`, `netlify`, `fly deploy` or `gh release create`. Each match names the workflow, job, trigger and why. `if:` conditions are not evaluated, so it errs towards "deploys". - **Unknown, never "no".** A workflow it cannot read or parse, a changed-file list it cannot get, or an external reusable workflow it cannot see inside goes in `unknown`, and an unknown asks the owner like a deploy does. - **Context.** The repo's GitHub environments (a hosting integration such as Vercel can deploy on push with no workflow at all). - **Policy.** None in the tool: whether to ask is the owner's rule in `CLAUDE.md`, written in the introduction (step 3). The result carries the deploys, each with its environment, so "ask only for production" can be followed. The charter's *Merging* section: run `merge_check` before every merge; the owner's rule in `CLAUDE.md` decides whether to ask; without one, Billion asks when `merge_check` reports a deploy or can't tell (`notify_owner`, project = the repo, naming which workflow deploys what), and merges only on a yes. ## Claude Code or Codex Billion runs on Claude Code by default, or on Codex with `BILLION_AGENT=codex`. The button next to Billion's name in the left panel switches it to the other one, and the server switches it by itself when the CLI it runs on hits its usage limit (below). - **Which one.** `BILLION_AGENT` in `.env` is the default. A switch is saved in `~/.agent-007/billion-agent.json` together with the `BILLION_AGENT` it was made under, and holds across restarts until that setting changes: an edited `.env` is the newer word, so it wins again. An automatic switch can then change the CLI without touching `.env`. - **Codex's launch.** `codex --dangerously-bypass-approvals-and-sandbox` in Billion's folder, with the board MCP server and the folder's trust passed as per-run `-c` overrides (never written to `~/.codex/config.toml`), like a board worker. A restart resumes Codex's newest session in that folder by id (`codex resume `, never `--last`). Without `codex` on the PATH its tab says how to install it, as it does for `claude`. - **Instructions.** Codex reads `AGENTS.md`, not `CLAUDE.md`, and follows no `@`-import, so the server writes `AGENTS.md` on every start: the owner's `CLAUDE.md` with its `@CHARTER.md` line replaced by the charter. The owner's rules stay after the charter, where they say they take precedence, and `CLAUDE.md` itself is only read. Only `@CHARTER.md` is expanded. - **The loop.** Codex has no `/loop` or ScheduleWakeup, so on both CLIs the server drives the loop (`server/billion-wake.js`). It types `Run one operating cycle as defined in CHARTER.md.` into Billion's terminal every 30 minutes, every 3 while its work is moving (a worker on one of its cards running, by the board's own status, or a card that reached Review or finished CI since the last wake; a stalled, waiting, needs-you or gone worker does not count, since each wake re-reads Billion's whole context), or when Billion asked with `set_next_wake` (3 to 60 minutes, the next wake only). Never mid-turn: only when Billion rests at its prompt, the check mail delivery uses, with its inbox open and no mail waiting. Never within 2 minutes of the owner typing in its terminal. The charter tells Billion not to run a loop of its own, so a Claude Billion is not driven twice. - **The handover.** A switch writes `HANDOVER.md` in Billion's folder, stops the running Billion (its waiting mail moves to the new one), and starts the other CLI in a new conversation whose first prompt says to read `STATE.md` and `HANDOVER.md` first. `HANDOVER.md` includes up to 80 recent dialogue and tool entries of the old CLI's newest conversation in that folder as plain text (from `~/.claude/projects//*.jsonl` or `~/.codex/sessions/...`), with no model involved, plus pointers to `STATE.md`, `git log -10`, and the original transcript. Entries are bounded to about 8,000 characters each and 96,000 total; truncation is explicit. A failed handover write cancels the switch. `agent-007 handover` writes one on demand. The new CLI starts fresh rather than resuming its own older conversation, which would be out of date. - **Not committed.** `AGENTS.md` and `HANDOVER.md` are listed in the repo's `.git/info/exclude`. `AGENTS.md` is made from two committed files, so it would only repeat them. `HANDOVER.md` is raw conversation, which can hold whatever the owner typed, and is replaced at every switch. - **Across a switch** the inbox, the *Billion* tab, Telegram and the board tools keep working: all of them find whichever Billion is running. Permission requests waiting on the old Billion go to the owner, as they do when Billion stops. - **Usage limits** (`server/billion-limit.js`, off with `BILLION_AUTO_SWITCH=0`). Every 10 seconds the server reads the bottom 15 lines of Billion's screen, and only Billion's, for the CLI's own limit notices: Claude Code's `You've used 92% of your weekly limit · resets …`, `You've hit your session limit …`, `You've reached your Fable limit`, `You're out of usage credits …`; Codex's `Heads up, you have less than 25% of your weekly limit left`, `You've hit your usage limit …`, `You're out of credits`. At a warning past 75% and again past 90% it types, while Billion rests at its prompt, `Your usage is at N%; bring STATE.md up to date and commit now, in case you're switched.` At a hard limit, once Billion has printed nothing for 5 seconds, it switches to the other CLI as the button does, saves the reason in `billion-agent.json`, logs one line and tells the owner through `tell_owner`'s rule below (a browser notice when it does not reach the phone). It never switches back on a timer, and never twice within 30 minutes: a limit on the new CLI that soon, or a target CLI that is not installed or not logged in (`claude auth status --json`, `codex login status`), leaves Billion where it is and puts `Billion paused: both Claude Code and Codex are at their limits` in the *Billion* tab and on Telegram, once per stall: again only after a new Billion starts or it rotates to another login. With account rotation on, a paused or failed handover is tried again after the same 30 minutes. **Claude account rotation** (`server/account-rotation.js`) comes first when enabled in Settings (on by default once two accounts are added; saved off settings stay off). Limits on Billion or managed Claude workers mark the shared login limited, and the app selects the next included account. It stops and resumes its Claude sessions on their exact conversation IDs, changing only authentication. Account order, cooldowns, active identity, refreshed credentials and recovery state persist across server restarts. With every selected account unavailable, workers wait; Billion hands over to Codex if the Settings list has a selected Codex account. Otherwise it retries after the earliest known reset or a 30-minute backoff. Returning from Codex first selects an eligible Claude login and then uses the normal conversation handover. Billion's repository and state files stay in the same place throughout. **Codex account rotation** is the same for a Billion on Codex: its next Codex login (`~/.codex/auth.json` swapped, the background server stopped, `codex resume `) comes before any handover, which happens only once every selected Codex account is unavailable, the Settings list has a selected Claude account and `BILLION_AUTO_SWITCH` is not `0` (rotation itself runs either way); a move to Codex first selects an eligible Codex login. Codex's `try again at …` time sets when a limited login is retried. ## Briefings The owner, 2026-10-01: too many questions, a chat that takes a long time to answer without saying what is going on. So Billion comes to the owner twice a day, not whenever a question occurs to it. **When.** Two briefings a day, 08:30 and 15:30 in the owner's time zone. In `~/.agent-007/config.json`: `rounds` (`["08:30", "15:30"]`; `[]` turns briefings off and every question shows at once, as before), `roundMaxPerProject` (2) and `roundsTimeZone` (an IANA name such as `"America/New_York"`; the server's own zone when unset). Read at startup. **Queue.** `notify_owner` no longer shows a question: it queues it under its `project`, which is the department. Its result says where it stands: "Queued as Q12 for the 15:30 briefing (10/1 pm), position 1 of 2 in agent-007", or "position 3 … behind the top 2" when two are ahead of it. Queued questions are never sent to a browser. Billion manages the queue with `list_round_queue` (per project, in the order the briefing takes them), `drop_queued` (number or id) and `notify_owner`'s optional `rank` (1 first). **Release.** At briefing time the server (`server/rounds.js` for the clock, `releaseRound` in `server/owner.js`) takes, per project, the two highest-priority queued questions: blocking, then normal, then low; then `rank`, a ranked one before an unranked one; then the newest. Those open in the tab and the thread. Every question the previous briefing released that is still open is **consolidated**: status `consolidated`, kept in history (the tab's *Earlier*), off the open list and the badge, and no longer answerable (Telegram buttons on it say so). Questions past two per project stay queued for a later briefing. A briefing that comes due while the server is down is released once when it starts. Billion hears it in its terminal as one line: `[Owner briefing] Briefing 10/1 pm released 5 (Q120, …); consolidated Q118, Q119: re-queue only if still top two; 3 still queued for later briefings (list_round_queue to re-rank or drop).` (kept in `rounds.json` until Billion runs). The owner's phone gets ONE Telegram message per briefing, "Afternoon briefing: 5 items across 3 departments", with Billion's brief and a link to the app (`PUBLIC_URL`, or `APP_URL`, its older name; else the first `ALLOWED_ORIGINS` entry), never one per question. **Numbers and Done.** Every item in a briefing carries a short number, 1, 2, 3…, in the order the tab shows them (*Needs you now* first, then each project); an emergency asked during the briefing takes the next number. The owner clears items with a card's *Done*, or by typing `1d` (`1d 3d`, `1d, 3d`) in the chat box, in any card's reply box, or on Telegram. Each becomes answered "done" (it folds as *✓ done*) and Billion reads one line: `[Owner via app] item 1 done (Q12: "..."); item 3 done (Q14: "...")`. A number with no open item in this briefing is reported back and the rest still go. Like an answer, it needs Billion running. **Start the briefing now.** *Start the briefing now (N waiting)* at the top of the Briefing view, or the owner typing "start the briefing now" (also "start briefing", "release the next briefing") in the chat or on Telegram, releases the next briefing immediately, with the same two-per-department rule and the same consolidation. That briefing's time is then used up, so the clock does not release it again, and "next briefing" moves on to the one after. These two commands are handled by the server and are not typed into Billion's terminal. **Brief.** `set_round_brief` (up to 600 characters) is shown at the top of the briefing and in its Telegram message: for the next briefing by default, or `round: "current"` for the one on screen. **Emergencies.** `urgency: "blocking"` or `telegram: true` is the only way to the owner outside a briefing: it shows at once (in *Needs you now* at the top of the briefing) and goes to the phone as before, under the per-minute limit. A blocking question with `telegram: false` waits for the briefing, first in its project. The tool description says this is for true emergencies only. **First start.** The first time a server with briefings starts, every open question except blocking ones is consolidated and Billion is told which, so it can re-queue the ones that still matter; the briefing that already passed that day is not released on the spot (`rounds.json`: `migratedAt`, `lastAt`, the briefing on screen and its brief, a note waiting for Billion). **Day one.** Until the first briefing comes due (or the owner starts one early), `notify_owner` shows a question at once, as with briefings off: a new install's introduction and first cycle should not wait for 08:30. Decided in the fresh-install test of v0.46 (2026-10-02), over two alternatives: briefings starting the day after install leaves an evening install with a day of questions at once, and exempting only the introduction leaves the first cycle's questions waiting. Before that first briefing the tab's section for them is *Open questions*, not *Needs you now*. The first briefing consolidates the ones still open (blocking ones excepted), as any briefing does the last one's, and Billion re-queues what still matters. **The tab.** The Billion tab opens on **Chat** until a briefing has been released (where a new install sees the introduction, and why a missing or logged-out CLI keeps Billion from starting), then on **Briefing**; the owner's own pick wins either way. The Briefing view shows a status line, the briefing's name and a counter ("3 of 7 done"), the brief, then one section per project with at most two cards. Each card is the question, its tap choices (recommended first), a reply box with Send, and *Skip* (dismiss). An answered card folds to one line where it stood ("✓ you answered: …", *Undo* for a minute). Nothing reorders or jumps while the owner reads or types: a card is updated in place, and when the set of cards changes the view keeps its scroll position, the drafts typed in reply boxes and the focus. *Earlier* (closed by default) lists what past briefings consolidated or got answered. The chat thread moved to the second view, **Chat**, unchanged; the browser remembers which view was last open. ![Briefing on a phone](img/billion-round-numbers.png) **The owner's messages and the step log.** What the owner types is never held for a briefing: it goes to Billion's terminal as soon as Billion rests at its prompt. Each message stays *pending* ("waiting for Billion…") until a `tell_owner` answers it, oldest first, so two messages sent before a reply each get their own (the reply carries `replyTo`, the message it answered). A server notice (an account switch) answers none. Under each pending message a box says where it stands, in so many words: - **Waiting for Billion to finish its current step**: the message is *queued*, not given to Billion yet (Billion is mid-turn; it is typed in when Billion next rests at its prompt). - **Billion is working on this**: Billion has it and it is the one being answered, with a timer from when Billion got it until the first step. - **Billion has this, and is answering your earlier message first**: given to Billion, behind an older message still waiting for its reply. Below the heading is the **step log**: every step of the work on the message being answered, oldest first, each with its time. It scrolls on its own once long, follows the newest step, and the newest step is highlighted with a running timer ("12 s"). Steps come from two places: - **Billion's own words.** Each `set_status` call adds a step (it no longer replaces the last one). Use short, owner-facing summaries of the current step, evidence or findings, uncertainty or blockers, and the next check (140 characters per update). These show in full weight. - **The tools Billion uses.** Every 2 seconds while Billion works on the message, the server looks at Billion's screen for new tool calls (Claude Code's `⏺ Bash(…)`, Codex's `• Ran …`) and adds each as a fixed phrase picked by the tool's name alone: "Running a command", "Reading a file", "Searching files", "Checking the job board", "Posting a card", "Reading a worker's screen" and so on (`toolStep` in `server/billion-status.js`). Nothing else on the line is shown: no command, arguments, output, file path or contents, and none of Billion's prose or reasoning. The one exception is the name of one of Billion's own notes files ("Reading STATE.md"). An unknown tool, `set_status` and `tell_owner` add no step. These show in italics, after Billion's own words in importance. Only the oldest waiting message gets steps; the same text twice in a row is one step, and the newest 30 are kept. The configured Telegram token is redacted from Billion's words before storage. None of this needs Telegram or a new setting, and it fits a phone-width tab. **Cancelling a message Billion has not got yet.** While Billion is busy, the owner's messages wait in its terminal queue and read *queued*, with a **× Cancel** link (tap twice; the first tap asks "Tap again to cancel"). A cancelled message never reaches Billion: it leaves the queue, stays in the thread greyed and struck through ("Cancelled — Billion never got it"), loses its progress box, and is saved that way in `chat.json`, so a reload or restart shows it cancelled and delivers nothing. Typed and voice (Talk to Billion) turns, attachments and Telegram messages all wait in the same queue, so all of them can be cancelled from the tab. Once typing into Billion's terminal has started, Cancel goes away; one tapped at that same moment loses to delivery and says "Already sent to Billion". Answers to questions and slash commands have no Cancel (a question's answer has Undo). When `tell_owner` binds the answer, the live box disappears immediately and its steps become a small, closed **Work details (N steps)** disclosure under that answer, each step with its time. An empty log adds no disclosure. The owner can expand it, and ordinary updates leave it expanded. The existing status stream carries the steps to the browser; the timers tick in the page. Request markers and summaries are stored with the chat so new requests keep their oldest-first reply bindings across reconnects and server restarts. Messages written before request markers existed are not guessed into the queue. After an hour without a reply, or while Billion is stopped, the active box is hidden. While the browser is disconnected the box stays with the progress last heard, not active and headed "Reconnecting… your message is kept", and the reconnect's replay brings it up to date. A late answer still binds to its original request; an activity timeout never shifts an answer to a newer message. A waiting CLI shows no animated activity dot. Worker updates and server notices do not close a request. ![Earlier progress layout, before per-message summaries and Work details](img/billion-chat-progress.png) *Earlier layout shown above: progress now sits inside each pending message as a timestamped step log, and folds under the answer as Work details.* **Status line.** One line in the bar at the top of both views, beside the *Briefing | Chat* switch (under it on a phone or a narrow pane): what Billion is doing and what is running, "Working: reviewing PR #120 · 3 workers running · next briefing 3:30 pm". Billion sets its part with `set_status` (up to 140 characters; gone after 30 minutes without an update); the server adds "Thinking…" while Billion's terminal is mid-turn, the number of running workers on Billion's cards and the next briefing. From the moment the owner sends a message (in the tab or on Telegram) until its `tell_owner` reply, it says "Billion is working on your message…" while the CLI is working, or "Waiting for Billion to reply…" while it waits (for at most an hour). A disconnected browser says "Reconnecting…" and shows no activity until the connection returns, keeping each pending message and its progress box as last heard. It is the owner's, like the chat: sent to the owner's browser only. ## Telegram Billion's terminal is its work log: board notices, worker messages and cycle prompts are typed in there all day, and a conversation with the owner gets buried. So the owner talks to Billion in the **Billion** tab next to Jobs (a chat bubble and "Billion", the tab a page opens on, badged with the count of open questions; on a phone, the *Billion* button in the bottom bar): a chat thread, the web twin of the Telegram channel. Billion's `notify_owner` questions, its `tell_owner` replies and your messages, from the tab and from Telegram, are one conversation there, newest at the bottom; the last 500 messages are kept in `~/.agent-007/chat.json`, with open questions and unanswered owner requests retained until answered. Open questions also show as a chip in the bar at the top of the tab ("7 open questions", "7 open" on a phone, with a "!" when one is blocking). Tap it (or the tab's badge) and the **Open questions** panel slides over the thread, one section per project with its open count, the project with a blocking question first, then the one whose question has waited longest. Each row is a question's urgency mark, Q-number, first line and age, with its choices (recommended first) and *Reply*, which sets the box below to answer it; tap a row's text to jump to its bubble. × or Esc goes back to the chat; on a phone the panel fills the screen and *Reply* closes it. The browser remembers whether the panel was open. Billion names the project with `notify_owner`'s `project` (a repo's folder name on the board, or `general`); left out, the server reads it off a GitHub URL or a repo's name in the text, else `general`. A name not on the board is kept, lower-cased. A switch at the top of the panel, *by project | by type*, regroups the same rows by the question's `type` (engineering, marketing, outreach, finance, product, admin or other): Billion passes it to `notify_owner`, a name off the list reads as other, and left out the server reads it off the text, first match winning (money, a `$`, price, plan, subscription, renew or pay is finance, so it wins over the rest; a PR, CI, merge, deploy, bug, test or release is engineering; a reply, LinkedIn, an email from, a DM or inbound is outreach, even about a post; a post, Reddit, HN, a newsletter, tweet, a capital X, Changelog or launch is marketing; a login, token, account, access, credentials, setup or install is admin; a feature, design, UX, roadmap or direction is product). A question saved before types existed gets one read off its text the first time the list is read, and keeps it. Every section starts closed, a header with its name, count, "!" when a blocking question is inside, and a chevron; tap it to open. The browser remembers the grouping and which sections are open. When Billion cannot talk yet, a bar above the text box says why, with the button past it: its CLI is missing (Start), Billion is stopped (Start), or `claude auth status` / `codex login status` said it is logged out, so it sits at the CLI's own sign-in (open its terminal). The server clears that last one at `billion_ready` or when that Billion exits. Type in the box at the bottom (Enter sends, Shift+Enter is a new line) and it is typed into Billion's terminal as `[Owner via app] `, as a turn of its own, while mail keeps flowing to it as before, even while questions are open: typing never answers a question you did not pick. To answer one by typing, tap *Reply* under it (or on its row in the Open questions panel): the box then says "Answers Q3" (× goes back to a plain message). If Billion is not running the message is refused and stays in the box. A message that starts with `/` is a slash command for Billion's CLI: `/model opus` is typed bare into its terminal, as if you typed it there, so it reaches whichever CLI Billion runs on (Claude Code's models, or Codex's). Like any message it waits until Billion rests at its prompt. It shows in the thread as yours, marked *Command*, awaits no reply, and a few seconds later Billion's terminal screen follows as a bubble of its own. A command that opens a picker (a bare `/model`) cannot be driven from the chat: its screen is shown and the picker closed with Escape, so give the argument instead (the box says so while it holds a command). Start with `//` to send a message that begins with `/` (`//etc/hosts is wrong` reaches Billion as `[Owner via app] /etc/hosts is wrong`). The same works from your private Telegram chat with the bot, the screen coming back as a message; in a group nobody's `/` text runs as a command. Agents' messages never do. There is no practical length limit on what you type or paste, in a message or an answer: it reaches Billion whole, line breaks kept, as one turn (only a paste over 200,000 characters is refused, with the text left in the box), and the thread shows it whole in a scrollable bubble. Telegram itself caps a message at 4,096 characters and a caption at 1,024, so Billion's longer messages to your phone go as consecutive messages instead of being cut, and a question's answered copy that no longer fits gets the rest as a follow-up message. To show Billion a screenshot or a file, paste an image into the box, drop files anywhere on the tab, or pick them with the paperclip beside the mic. They wait as chips above the box (× removes one) and go with the next Send, with or without text. Each is saved owner-only (files 0600, folders 0700) in `~/.agent-007/chat-files//`, under the job form's limits (10MB a file, 20 files and 50MB a message), and the turn ends with their absolute paths: `[Owner via app] (attached: /path/a.png, /path/b.pdf)`, or `[Owner via app] Q3: (re: "...") (attached: ...)` for an answer. Your bubble shows images as thumbnails that open full size and other files as download links; they are served only from that folder, never while user accounts are on, and deleted when their message leaves chat history. Unanswered owner requests stay past the 500-message cap. With briefings on (the default, see [Briefings](#briefings)) a normal or low question waits in Billion's queue until a briefing releases it, and the briefing sends one Telegram message for all of them; what follows is how a question behaves once it is in the tab, and how every question behaves with briefings off. `notify_owner` puts each question in the tab. Only a **blocking** one also goes to your phone, when a Telegram bot is set up: normal and low questions wait in the tab (the thread, the open-questions strip and the badge) without buzzing it. Billion can override that per question with `telegram`: `true` pushes a non-blocking question it judges super urgent, `false` keeps even a blocking one off the phone; left out, blocking goes and the rest stay. Its tool result says which happened ("Put in the owner's Billion tab as Q12; not sent to Telegram (urgency normal)"). A question that went to the phone can be answered there, by button or reply; one that did not is answered in the tab. Each question gets a short number, Q1, Q2 and so on. When the answer is a pick, Billion passes `choices` (2 to 5 short answers) and marks the one it `recommended`. Replies and status updates that need no answer ("Got it, restart looks clean") go through `tell_owner` instead: no numbered item, no badge, the same per-minute limit. It always shows in the Billion tab, but reaches Telegram only when your last message came from there, so a conversation in the tab does not buzz the phone. The server remembers the channel of your latest message (`[Owner via app]` or `[Owner via Telegram]`, answers included) until it restarts; before your first message a `tell_owner` goes to the phone too, so a status update still finds you away from the browser. Server notices (account-switch results, a Billion switched at its limit) follow the same rule. `notify_owner` questions follow their own rule above: blocking ones (or `telegram: true`) go to both, the rest stay in the tab. On Telegram the bot is Billion's own, so messages carry no name: a question reads `Q3: ` (`! Q3: ` when blocking), a reply is its text alone. When you answer a question somewhere else, say by typing in Billion's terminal, Billion closes it with `resolve_question`: it moves to **Answered** (marked *in the terminal*) and your phone's copy shows the answer, like any other. Answered the wrong question? *Undo* sits beside "you answered: ..." for a minute: the question opens again and Billion reads `[Owner via app] Q3: undo my answer "..."`. After that, tell Billion; it puts the question back with `reopen_question` (nothing is sent to Telegram). Answer in the app: tap a choice under the question (the recommended one first), or tap *Reply* and type in the box. The answer is typed into Billion's terminal as `[Owner via app] Q3: (re: "")`, and the buttons collapse to "you answered: ". If Billion is not running the question says so and stays open. Answered questions stay in the thread as history; *Dismiss* closes one unanswered. With user accounts on (`users.json`) Billion does not run, and nobody answers or dismisses for the owner. The list lives in `~/.agent-007/waiting.json`; one written before answers existed still loads, its items open and numbered in order. Answer on Telegram: a question with choices arrives with a button for each. Tap one, or *reply* to the question's message with text, and it reaches Billion as `[Owner via Telegram] Q3: (re: "...")`; the message is edited to show the answer and loses its buttons. An answer in the app edits it too, to "Answered in app: ". Whatever else you send the bot (a message that is not a reply, a voice note) is typed into Billion's terminal as `[Owner via Telegram] `, the same way board notices and agent messages arrive. If Billion is not running the bot says so and drops the message. Setup: 1. In Telegram, message **@BotFather**, send `/newbot`, and put the token it gives you in `~/.agent-007/.env` as `TELEGRAM_BOT_TOKEN=...`. Restart. 2. Send your new bot any message (or add it to your team's group and say something there). 3. The *Billion* tab shows `Telegram: a message from (chat ). Use it for Billion?`. Check it is yours and press **Use this chat**. The bot answers "Connected to Agent 007." in that chat and the tab says *Telegram connected*; no restart. `agent-007 doctor` checks the token: it says whether the bot answers, or that Telegram rejects the token. Every chat that messages the bot is offered, one by one (up to 20 a run), and none is ever picked for you, so a stranger who finds the bot is just an offer to *Dismiss*. Only the owner's browser sees the offers and can accept one: with user accounts on, nobody can. The chat picked is kept in `~/.agent-007/telegram-chat.json`. The Settings gear's *Telegram* line names the connected chat; its **Change** forgets it, and the next chat to message the bot is offered again (with nothing connected, the line gives the two steps above). The offers are server state, so they stay through a reload until you act on them. `TELEGRAM_CHAT_ID` in the environment or `.env` still works and wins over it; with neither, the server log also shows each chat's id, for a server with no browser. The chat id is the only gate: messages and button taps from any other chat are ignored without a reply. The token is never logged or sent to the browser. **A group as the owner's chat.** A team can connect a Telegram group instead of a private chat: every member of it counts as the owner (there is no per-member allowlist). Their messages carry the sender's name so Billion knows who spoke: `[Owner via Telegram (Alice)] `, `[Owner via Telegram (Alice), voice] ...` and `[Owner via Telegram (Alice)] Q3: (re: "...")`; the thread's bubble reads *Alice on Telegram* and the question *Alice answered: Merge*. The name is the member's first and last name, else their @username, cut to 40 characters on one line with no brackets. A private chat is unchanged. By default a bot in a group only receives commands and replies to its own messages, so turn its privacy mode off: `/setprivacy` in @BotFather, pick the bot, *Disable*, then remove the bot from the group and add it back. The server logs a hint when a group has only sent it commands and replies. Billion can notify you at most five times a minute. No library: the server long-polls `getUpdates` with Node's own `fetch`. ### Read aloud and dictation in the tab For the car, or anywhere reading a wall of text is not an option. All of it runs in the browser (`speechSynthesis` and the Web Speech API's `SpeechRecognition`): free, no server work, no API key. - **Read aloud**: every message from Billion in the thread (questions, `tell_owner` replies, server notices) has a speaker button. Tap it to hear the message; while it speaks the button is a stop square, and a second tap stops it. It reads plain words: markdown is dropped, links are read as "link", `Q51` as "question 51", `#160` as "number 160", a code block as "code block", and an open question ends with its choices ("Choices: Done, Not yet; recommended: Done"). Long messages are spoken sentence by sentence, so Chrome's cut-off after about 15 seconds of one utterance never truncates them. The voice is the best English one the browser lists (en-US first, Premium, Enhanced, Siri or Google voices before the rest); the picker in the Settings window (the gear, *Voice for reading messages aloud* under *Read aloud*) changes it, and the browser remembers the choice. - **Read new messages aloud**: the speaker button in the bar at the top of the tab ("Read aloud" beside it when the tab is wide enough), one tap on and one off, with a brief "Read aloud: on" or "Read aloud: off" to confirm (accent while on; off by default, remembered by the browser). While it is on and the tab is showing, each new message from Billion is spoken as it arrives, one after another, never over each other. Tapping the speaker while anything is being read stops it and turns reading off; tap again to turn it back on. Browsers let a page speak only after a tap on it: the switch's own tap counts, but after a reload the tab shows **Resume reading** (with how many messages are waiting) until you tap it once. Leaving the Billion tab (or the phone's Billion view) stops reading. - **On a phone**: reading goes on while the screen is on and the Billion view is showing. With the screen locked or the browser in the background, mobile browsers pause or drop page speech, so it is not reliable there; for that, use Telegram voice notes ([Voice](#voice)), which play like any audio message. - **Dictate a reply**: the mic beside the text box. Tap it, allow the microphone the first time, and speak: the words being heard show greyed above the box, and each finished phrase is added to the box as text. Nothing is sent until you tap Send, so you can check or fix it first. If the box says "Answers Q3", the dictated text answers Q3 exactly like typed text. The mic has the terminal mic's limits: it stops after about a minute with no speech, always after 5 minutes, when the page is hidden and when you leave the tab; a red dot and the pulsing mic show while it listens. `Cmd+D` in the Billion tab toggles this mic. It needs HTTPS or localhost (see [REMOTE.md](REMOTE.md)), and most browsers do the recognition on their vendor's servers (Chrome and Edge: Google and Microsoft; Safari may do it on the device), so don't dictate secrets. The terminal's own mic is unchanged. ### Talk to Billion A hands-free conversation with the same Billion, in the Chat view of the Billion tab. Tap **Talk to Billion**, allow the microphone, and speak: when you have finished, what you said is sent to Billion as a voice turn, a short "Got it, one sec." is said at once, Billion's `tell_owner` reply is spoken as soon as it comes, and it listens again. The bar says **Listening**, **Thinking…** (with Billion's status line, and a spoken "Still working on it." after 20 seconds of nothing else to say), **Speaking**, **Muted** or **Disconnected**; **Mute** turns the mic off, **End** ends it. The thread is the transcript: your turns show as "(voice)" bubbles, Billion's replies as usual. On an iPhone the bar also has **Speaker** / **Earpiece**: where replies play for the whole call, remembered by that browser (Speaker by default). Mute silences the mic without closing it, so iOS no longer moves the sound between the loudspeaker and the earpiece mid-call. The browser's own voice (when the computer has no `say`) cannot be routed and goes where iOS sends it. - **Pauses are yours**: a small voice detector in the browser (Silero VAD, served by Agent 007 itself) hears each stretch of speech end, and each is transcribed at once, but the turn ends only after a real pause: about 1.2 seconds after a finished sentence or question, 1.5 seconds otherwise, and 2.6 seconds when the words so far stop mid-thought ("...and the", "um", a trailing comma). Speak again before then and it is the same turn. - **Adding to a turn**: speak again after the turn went but before Billion's reply came, and your words join that turn. While Billion has not been given it yet they are added to it, so Billion gets one message; once it has, they go as `[Owner via app, voice #b2c1d3e4, adds to #3f9a0c1e] ...`, and Billion answers both at once. - **A fast first response**: the moment a turn ends, a short acknowledgment plays ("Got it, one sec.", "Okay, let me check.", ..., never the same one twice in a row), fetched when the conversation starts, while Billion works on the reply. The reply streams in as soon as `tell_owner` sends it. The browser console logs each turn's `[talk] latency`: end of turn to the acknowledgment's first sound (`ackMs`), to the reply arriving (`billionMs`), and to its first sound (`totalMs`). - **Billion's voice**: with `ELEVENLABS_API_KEY` set in `~/.agent-007/.env`, replies are spoken by ElevenLabs (voice `ELEVENLABS_VOICE`, a name on your account or a voice id, default Eric; model `ELEVENLABS_MODEL`, default `eleven_flash_v2_5`), streamed and played as they arrive. Only the reply's text goes to ElevenLabs; each acknowledgment is made once per voice and kept in `~/.agent-007/talk-voice/`. If ElevenLabs fails, `say` speaks instead and the server log says why. Without the key, replies are spoken by `say` on the computer running Agent 007, with your `SAY_VOICE` and `SAY_RATE`. - **Your audio stays on this machine**: what you say is transcribed on the computer running Agent 007 by whisper.cpp. Set it up as in [Voice](#voice). Without it, the browser's own speech recognition is used after a one-time notice that it may send the audio to the browser's maker (Chrome: Google); without ElevenLabs or `say` (Linux, Windows), the browser speaks the replies. The bar always says which. - **Only its own replies are spoken**: each turn is a message of its own, typed into Billion as `[Owner via app, voice #3f9a0c1e] ...`, and Billion answers it with `tell_owner` and `reply_to: "3f9a0c1e"`, which binds the reply to that turn even when older messages are still waiting. A `reply_to` that names no waiting message binds to none; without `reply_to`, a reply answers the oldest waiting typed or Telegram message, then the oldest voice turn. Only the reply bound to the turn is read out, a sentence or two at a time. Replies to what you typed or sent from Telegram appear in the thread but are not spoken; questions and terminal output never are. The charter asks Billion to keep replies to voice turns short. - **Progress while it works**: while a turn waits for its reply, each new `set_status` line is spoken as a short update, its first few words with code, links, paths and file names left out. Nothing is said in the first 3 seconds (a fast answer needs no filler), at most one update every 9 seconds, never the same line twice, never while you are speaking, and never over a reply: when the answer arrives, an update still playing stops and the answer plays next. The detector's raised bar while Billion talks covers updates too, and an update is never treated as a reply you might be echoing. - **Interrupting**: start talking while Billion speaks, or tap **Stop speaking**, and it stops at once (it drops in volume the moment the detector hears what may be speech, and stops a few frames in); the rest of that reply is dropped (it is still in the thread), and what you say is sent as usual. A reply that comes while you are speaking, or pausing mid-turn, waits until your turn is sent, then plays. Stopping the voice never stops Billion's work. - **Echo**: the microphone runs with echo cancellation, the detector needs louder speech to interrupt while Billion talks, and an utterance that repeats six or more words of the reply that was playing, in order, is dropped as Billion's own voice (the bar says so; say it again if it was you). A shorter repeat, like "merge PR 196 now", is yours. Speech that is the acknowledgment playing when you started is dropped the same way; the same words with nothing playing are yours. - **Once each**: every turn carries an id, and the server sends an id to Billion once, so a retry after a dropped connection never makes a second message. Voice turns are always messages to Billion, never "start the briefing" or "1d": a misheard word cannot act for you. - **Limits**: the mic stops on **End**, when you leave the Billion tab, while the page is hidden (it comes back with the page), and after 10 minutes with no speech. A stretch of speech can be up to 5 minutes; 60 a minute. With user accounts on, like the chat itself, it is off. A dropped connection keeps the conversation: once it is back, any reply that came meanwhile is spoken. A server restart or update reloads the page; tap **Resume talking** and the same happens. ### Voice A poor man's audio chat, free and on your own machine: nothing is sent anywhere but Telegram, and there is no paid transcription. - **Billion speaks** on macOS with ffmpeg installed (`brew install ffmpeg`): `say` reads the message, ffmpeg encodes it as OGG/Opus, and the bot sends it as a voice message with the same text as its caption, so links stay tappable. Links are read out as "link". Without `say` or ffmpeg (Linux, Windows) it sends text, and the server log says once why. - **Which voice**: `SAY_VOICE` names one from `say -v '?'` (e.g. `SAY_VOICE=Ava (Premium)`; a bare `Ava` takes the best Ava installed, and `Samantha` matches `Samantha (English (US))`). Unset, the server picks the best English voice installed: a Premium one, then Enhanced, en_US before en_GB, else `say`'s old default. The Premium and Enhanced voices sound far more natural and are free: System Settings → Accessibility → Spoken Content → System voice → Manage Voices, open English, and download one (Premium voices are a few hundred MB). Restart the server to pick it up; the log says which voice it chose when Telegram starts. A `SAY_VOICE` that is not installed is logged once and the automatic pick is used. - **How fast**: `SAY_RATE` in words per minute, 120 to 300 (outside that is clamped, anything but a whole number is logged once and ignored). Unset, no `-r` is passed to `say` at all, so each voice speaks at its own system default speed. The startup line gives both: `Telegram: speaking with the Ava (Premium) voice at 205 wpm` (or "...at the system default speed" when `SAY_RATE` is unset). - **You speak**: send the bot a voice note (or an audio file) and it is transcribed on this machine by [whisper.cpp](https://github.com/ggml-org/whisper.cpp), then typed into Billion's terminal as `[Owner via Telegram, voice] `. A caption you type on the note follows as `(caption: ...)`. Whisper's markers like `[BLANK_AUDIO]` are dropped, and a note with no words left gets a reply asking you to send it again. Set up with one command: ```bash agent007 install --voice # voice only, no service (npm start -- install --voice in a clone) agent007 install # the service, then asks whether to set up voice too agent007 install --all # both, no questions (--yes accepts the default model's download) ``` It installs whisper.cpp and ffmpeg (Homebrew on macOS; on Linux and Windows it prints the steps), asks which model, downloads it after you confirm, sets `WHISPER_MODEL`, tests it on a clip and offers to restart a running server; each step is skipped if already done. Or by hand: ```bash brew install whisper-cpp ffmpeg mkdir -p ~/.agent-007/whisper curl -L -o ~/.agent-007/whisper/ggml-base.en.bin \ https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin ``` and in `~/.agent-007/.env`, `WHISPER_MODEL=/Users/you/.agent-007/whisper/ggml-base.en.bin` (a full path). `ggml-base.en` (about 150 MB) is quick and fine for English; `ggml-small.bin` (about 500 MB) is more accurate and multilingual. The server looks for `whisper-cli`, `whisper-cpp` or `main` on `PATH`; `WHISPER_CPP_BIN` points at one elsewhere. Until it is set up, a voice note gets a one-line reply saying so, and nothing reaches Billion. Notes over 5 minutes or 20 MB are refused, and `say`, ffmpeg or whisper.cpp running longer than 5 minutes is stopped (text instead, or a reply to send text). - **When Billion uses voice**: `TELEGRAM_VOICE=mirror` (the default) answers in the mode of your last message, voice for a voice note and text for text, and Billion's own new questions follow it too; text until you have sent anything. The mode is kept in `~/.agent-007/telegram-voice.json` across restarts. `always` speaks every message, `never` none. Whatever the setting, a message over about a minute of speech (900 characters) or one that is mostly links, code or paths goes as text, and voice never goes without its text caption. ## Order of building 1. **Billion itself — built.** `server/billion.js` (on/off via `BILLION`; off, the web app draws no Billion tab or phone button and opens on Jobs, folder via `BILLION_DIR`, first-run repo from `templates/billion/`, command with first-run or resume prompt, projects-folder suggestion), started from `server.js` `startup()`; name reserved; rename refused; pinned row with a Start button when stopped; own desk top center; tab first, hidden until the owner clicks Billion. The charter template is `charter.md` in this repo (copied in as `CHARTER.md`, with `owner.md` → `CLAUDE.md`) so agents working on Agent 007 don't load it. Verified live in an isolated server: repo created and committed, introduction as designed. 2. **Messaging — built.** Every agent can message Billion (exempt from the permission and owner rules as a recipient, `server/messages.js`); its inbox is held until it calls `billion_ready` (a tool listed for Billion only), which the charter calls at the end of the introduction and at the start of every cycle, so a restart needs no stored flag; a card Billion posted sends it a `[Job board]` notice when it reaches Review, through `finish_job` or the PR poll (Billion only: another agent's terminal may be mid-conversation with a person), and another once CI on the card's PR finishes on its head commit (`CI finished on … : all passed` or `failed: `, once per head commit and again after a re-run, from a 60s poll of Review cards that also files a merged or closed PR at once, `checkReviewCi` in `server/jobs.js`); workers on Billion's cards are told they can ask it. Verified live: a guarded worker messaged Billion, Billion replied, the reply arrived. 3. **Board tools — built.** `add_repo` (wraps `addRepo`, `~/` allowed) and `close_job` (accept a no-PR card → Done; send any card back → To do with the note appended to its detail; a PR card is filed Done by its merge, so accept refuses it). Billion only, its own cards only, Review or To do; both listed only for Billion and checked again in the route. On a To do card `close_job` (accept, with a note) drops it instead: archived to Finished without running it, schedule or not, with the note as the card's reason (`archiveJob` in `server/jobs.js`, the same path as the owner's **Archive** button); send back is refused there. This was `retire_job` until it folded into `close_job`. Work due once on a date is a one-time card posted with `run_at`: it waits in To do until that time, then is dispatched like any other card. Verified live end to end: Billion added a repo, posted a no-PR card, the worker finished, the `[Job board]` notice woke Billion, it read the result and accepted the card — Done in 41 s. 4. **Approvals — built.** Claude Code workers on Billion's cards get a `--settings` with a `PermissionRequest` hook (`server/permission-hook.js`) that posts the request to `POST /hook/permission` with the worker's own agent token, read from its 0600 MCP config (not an env var: every child process would inherit it). `server/approvals.js` types `[Approval ]` into Billion and waits up to 2 minutes for `answer_permission` (allow / deny with a reason / owner); anything else — no Billion, one not ready, silence, an error — is no decision, and the dialog goes to a person. Long input is typed in cut short; an allow on it goes to the owner until Billion reads the whole request with `read_approval` (up to 20 KB; past that it stays the owner's). The same settings pre-allow `finish_job` and `send_message`, the two board tools the job prompt tells workers to use. Your own agents and cards are not hooked. Verified live: a worker in manual mode asked to Write outside its worktree, Billion allowed it, the file was written, the card closed. - Approvals fire only when a worker would show a dialog: in `auto` mode the classifier decides most things itself, and a user allow list (yours allows all `Bash`) skips the dialog entirely. - **Codex: built** (#98). Two `-c` flags, the hook and its trusted hash (see "Codex hook trust"); the hash comes from `codex app-server` `hooks/list` once at server start, and only if the one session-flags entry it reports is exactly our command, timeout and key. No codex, or any other answer: Codex workers keep asking the owner. Verified live on a scratch server with Billion itself on Codex and the worker in `manual`: Billion answered `owner` (the worker's own dialog appeared), then, briefed, `allow`, and the command ran and the card closed. A Codex upgraded under a running server hashes anew; its workers then hold the hook for review until the server restarts. Each part is useful without the next. **Charter upgrades — built.** Billion's instructions have two owners, so they are two files. `CHARTER.md` is Agent 007's (from `templates/billion/charter.md`): the server rewrites it on every start and commits it on its own ("Agent 007: update the charter") when it changed, so a new release reaches a Billion that already exists; the pathspec commit leaves Billion's uncommitted work alone. `CLAUDE.md` is the owner's (from `templates/billion/owner.md`, first run only): it imports `@CHARTER.md` and holds *Owner's rules* — projects folder, escalation changes, any rule the owner gives — which take precedence. Verified live: the import loads, the introduction writes the owner's rules into `CLAUDE.md` only, and a simulated older charter was replaced on restart with the owner's rules intact. Found while verifying part 1: - **Claude Code's folder trust dialog** appears on Billion's first start and defaults to "No, exit". No flag skips it, and pre-trusting would mean writing `~/.claude.json`, which live Claude sessions write concurrently. Decided: the server answers it, for Billion only (its folder holds only what the server put there). It reads the settled screen and presses one key for the last cursor drawn — Down off "No", Enter on "Yes", nothing otherwise — because the dialog arrives in several reads and a late one can still show the old cursor (answering per read pressed Down twice and wrapped back to "No"). Verified live on a fresh folder. Since v0.6.2.0 the key off "No" is Ctrl-N, not Down: an arrow starts with ESC, and a lone ESC on that dialog is "Esc to cancel", which exits Claude Code. Board-dispatched Claude Code workers now get the same answer too, and their worktree is pre-trusted in `~/.claude.json` before the spawn, with the screen answer kept as the fallback for a lost write (`server/claude-trust.js`; `TRUST_BOARD_WORKTREES=0` keeps the dialog). - A server started from inside a Claude Code session passes `CLAUDE_CODE_CHILD_SESSION` to its agents, which turns their transcript saving off — and `--continue` needs a transcript. Only affects a server launched by an agent, not one started from a terminal. ## Not borrowing from Munder Difflin The hive (mailboxes, blackboard, git event log), the `Stop`-hook inbox loop, semantic memory. Here each job is an isolated branch that ends in a PR; the board already does their job.