{"id":"47344de5-484f-4a9f-b800-debea4544624","slug":"ihavespoons-tau","kind":"skill","name":"tau","summary":"Twice the pi","owner":"ihavespoons","repoUrl":"https://github.com/ihavespoons/tau","license":"MIT","licenseTier":"permissive","trust":"community","stars":1,"match":null,"fewStars":true,"format":"skill","createdAt":"2026-07-16T05:06:18.000Z","addedAt":"2026-10-21T21:32:56.859Z","logoUrl":"https://avatars.githubusercontent.com/u/5601410?u=a15ab8137780c3a5710ac2b70298ad5a2fd4f8f6&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["writing"],"tags":[],"runtime":"go","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-22T11:49:12.748Z","sample":false,"url":"https://conduits.dev/tools/ihavespoons-tau","language":"Go","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# tau\n\nA coding agent for your terminal. tau is a Go reimplementation of\n[Pi](https://github.com/earendil-works/pi) — same architecture, same feature surface,\nsame minimalist philosophy — shipped as a single static binary with no runtime\ndependencies.\n\n> **Status: early development.** tau is being built in phases toward parity with\n> Pi v0.82.1. The interactive agent, all ten wire APIs, session trees, Pi\n> import, the extension system, themes, keybindings, skills, prompt templates,\n> the package manager, HTML export/share and the full slash-command set are in;\n> the TUI polish pass is still landing. `/settings` is a typed command here\n> rather than Pi's toggle menu — that picker is still to come.\n>\n> **tau does not ask before it acts.** It edits files and runs shell commands\n> without a confirmation prompt. Run it on a clean git tree or in a scratch\n> directory until approval gating lands.\n\n## Goals\n\n- **Single binary.** No Node, no npm. `brew install` or download one file.\n- **Pi parity.** The agent loop, session trees, providers, extensions, themes,\n  skills, and modes you know from Pi, in idiomatic Go.\n- **Easy migration.** tau reads your existing `~/.pi` state (sessions, settings,\n  auth, models) and imports it. Existing Pi TypeScript extensions run against tau\n  via a host shim.\n- **Libraries first.** `ai`, `agent`, `session`, `tools`, and `extension` are\n  importable Go packages, not just internals of the CLI.\n\n## Install\n\n```sh\nbrew install ihavespoons/tap/tau\n# or\ngo install github.com/ihavespoons/tau/cmd/tau@latest\n```\n\n## Use\n\n```sh\ntau login                  # Claude Pro/Max OAuth, or an API key with -k\ntau                        # interactive session in the current directory\ntau -c                     # …continuing the most recent one\ntau -p \"fix the failing test\"    # one-shot, non-interactive\ntau -p --mode json \"…\"     # JSONL events, for scripts and CI\n```\n\nTools: `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`. Search runs on\nripgrep and fd, found on your PATH or downloaded into `~/.tau/agent/bin` on\nfirst use — set `TAU_OFFLINE=1` to refuse the download instead.\n\nInside a session: `Enter` sends, `Alt+Enter` adds a newline, `Esc` stops the\nagent, `Ctrl+P` cycles models, `Ctrl+T` cycles thinking level, and `/help` lists\nthe commands. Typing while the agent is working *steers* it — the message is\ndelivered at the next turn boundary instead of starting a competing run.\n\n`Tab` completes three things: a `/command` name, that command's argument — so\n`/model ant` offers the models it matches — and a file path after `@`, anywhere\nin a line. Path completion goes through fd when it is there, so it skips what\n`.gitignore` skips, and falls back to reading the one directory you named when\nit is not. Accepting a directory offers what is inside it.\n\n`Ctrl+V` attaches an image from the clipboard to your next message — screenshot\nthe thing you are asking about, paste, type the question, send. Images in the\ntranscript are drawn inline in kitty, Ghostty, WezTerm and iTerm2, and described\nas `[png 1024×768, 240 kB]` everywhere else, including under tmux. Set\n`TAU_NO_IMAGES=1` to force the description form.\n\nThe transcript is printed into your terminal's scrollback rather than an\nalternate screen, so scrolling, selection, and search keep working, and session\nlength costs nothing to render.\n\n## Other providers\n\n**37 providers and 1,153 models ship compiled in** — Anthropic, OpenAI,\nOpenRouter, Google, Groq, Cerebras, xAI, DeepSeek, Mistral, Together, Fireworks,\nMoonshot, Z.ai, MiniMax, NVIDIA, Bedrock, Vertex, Azure, Copilot, the Vercel and\nCloudflare gateways, and the rest. Set the provider's API key environment\nvariable and its models appear in `tau models`, ready to select.\n\nThe catalog is generated from models.dev and diffed field-by-field against\nPi's own, so the per-model quirks come along with it: which thinking levels a\nmodel accepts, which token field it wants, where its pricing tier changes,\nwhether it needs cache markers, which wire it actually speaks.\n\nAll ten wire APIs are live: Anthropic messages, OpenAI chat-completions, OpenAI\nresponses, Azure OpenAI, Gemini, Vertex AI, Mistral, Bedrock ConverseStream, the\nChatGPT Codex backend, and pi-messages — Pi's own protocol. Every provider in\nthe catalog is reachable.\n\nSeven providers have a login rather than a key, so a subscription you already\npay for works without one:\n\n```sh\ntau login                    # Anthropic — Claude Pro/Max\ntau login github-copilot     # device flow\ntau login openai-codex       # ChatGPT subscription\ntau login openrouter         # browser; yields a permanent API key\ntau login xai                # device flow — SuperGrok or X Premium\ntau login kimi-coding        # device flow\ntau login radius             # browser or device code\n```\n\nRadius is a gateway rather than a vendor, and it publishes no fixed model list:\nwhich models you can reach is a property of your account. Logging in fetches the\ncatalog and caches it in `~/.tau/agent/models-store.json`, so it is there on the\nnext start without a network round trip; logging in again refreshes it. Point\ntau at your own deployment with `TAU_RADIUS_GATEWAY`.\n\nTwo authenticate from the ambient cloud environment instead. Vertex uses\nApplication Default Credentials, so `gcloud auth application-default login` is\nenough; set `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION`. Bedrock uses the\nstandard AWS chain — `AWS_PROFILE`, environment keys, SSO, or an instance role —\nand honours `AWS_REGION`, inference-profile ARNs, and `AWS_BEARER_TOKEN_BEDROCK`\nfor Bedrock API keys. Everything else takes an API key from the environment.\n\nAnything not listed that speaks `/v1/chat/completions` — vLLM, llama.cpp,\nLiteLLM, Ollama, a private gateway — can be declared in\n`~/.tau/agent/models.json` and used immediately:\n\n```jsonc\n{\n  \"providers\": {\n    \"openrouter\": {\n      \"name\": \"OpenRouter\",\n      \"baseUrl\": \"https://openrouter.ai/api/v1\",\n      // A \"$VAR\" reference is read from the environment, so the key\n      // never sits in a file.\n      \"apiKey\": \"$OPENROUTER_API_KEY\",\n      \"models\": [\n        {\n          \"id\": \"anthropic/claude-sonnet-4.5\",\n          \"reasoning\": true,\n          \"input\": [\"text\", \"image\"],\n          \"contextWindow\": 1000000,\n          \"maxTokens\": 64000,\n          \"cost\": { \"input\": 3.0, \"output\": 15.0, \"cacheRead\": 0.3, \"cacheWrite\": 3.75 }\n        }\n      ]\n    }\n  }\n}\n```\n\nThen `tau --model openrouter/anthropic/claude-sonnet-4.5`, or set\n`defaultModel` in settings. Omitting `api` assumes `openai-completions`, which\nis what nearly every endpoint speaks; the per-provider quirks are detected from\nthe provider id and base URL. Any of the ten wires can be named instead —\n`\"api\": \"pi-messages\"` reaches a backend speaking Pi's own protocol.\n\nQualify a model with its provider — `anthropic/claude-sonnet-5`, not\n`claude-sonnet-5`. A bare id is matched loosely across the whole catalog, and a\ndozen providers resell the same models.\n\nTo refresh the compiled catalogs after an upstream change:\n`go run ./cmd/tau-genmodels -refresh`.\n\n## Long sessions\n\nA conversation that outgrows the model's context window is compacted: the older\npart is replaced by a structured summary, and recent work is kept verbatim. It\nhappens on its own before a turn that would not fit, and again if the provider\nrejects one for length anyway — which is the case no estimate can predict, and\nthe one where the alternative is a failed turn. `/compact` forces it, and takes\nan optional focus (`/compact keep the migration details`).\n\nFiles are tracked separately from the prose. A summary may paraphrase what was\ndecided; it must not paraphrase which files were read and which were changed, so\nthose come out as an exact list.\n\n## Session trees\n\nA session is an append-only tree, not a list. Nothing is ever deleted:\n\n```\n/tree            go back to an earlier point; later branches stay in the file\n/fork            copy the session up to a chosen message and continue there\n/clone           copy the whole session\n/label <text>    bookmark where you are, so it is findable later\n```\n\nGoing back offers to summarize the branch being left, so the next turn knows the\nexploration happened instead of silently losing it.\n\nIn the interface `/tree` opens a picker over the whole tree rather than a list\nof your own messages, because a tool result is where a session went wrong often\nenough that it has to be reachable. Five views decide what is on screen —\n`ctrl+o` cycles them, or go straight to one:\n\n```\nctrl+d    default: everything except bookkeeping entries\nctrl+t    hide tool results as well\nctrl+u    your messages only\nctrl+l    labelled entries only\nctrl+a    everything, including model switches and leaf moves\n```\n\nEach of those keys toggles, so pressing it again returns to the default view.\n`ctrl+left` folds the branch under the cursor and `ctrl+right` unfolds it; with\nnothing to fold they move a row instead. `shift+l` labels the highlighted entry\n— an emptied prompt clears the label — and `shift+t` shows when each label was\nset. Typing searches the summaries and the labels, and the first `escape` takes\nthe search back rather than closing the picker.\n\nNarrowing a view usually hides the row under the cursor. It lands on that row's\nnearest visible ancestor instead of jumping to the top, so you keep your place.\n\n## Coming from Pi\n\n`tau import` reads `~/.pi/agent` and reports what is there. Nothing is copied\nuntil you say what to bring:\n\n```sh\ntau import                      # what does this Pi installation contain?\ntau import --sessions           # history only\ntau import --all --dry-run      # everything — but show me first\ntau import --all                # everything, including stored credentials\n```\n\nSessions land where tau looks for them, because the working-directory encoding\nis byte-identical to Pi's: an imported conversation is found by `tau -c` in the\ndirectory it came from. Older session formats are migrated on the way in.\n\nYour Pi installation is never modified — not the sessions, not the credentials.\nThe two can coexist while you decide, and an import that damages what it\nimported is not one you would run twice. Files that already exist under `~/.tau`\nare left alone unless you pass `--overwrite`.\n\n## Skills and prompt templates\n\nA **skill** is a folder with a `SKILL.md` in it — instructions the agent reads\nwhen a task matches:\n\n```markdown\n---\nname: deploy\ndescription: How to release and roll back the service\n---\n\nRun `make release`. If the smoke test fails, `make rollback` and stop.\n```\n\ntau reads `~/.tau/agent/skills/` and `.tau/skills/` in the project. Only the\nname and description go into the system prompt; the body is read on demand, so\na long skill costs nothing until it is used. Relative paths inside a skill\nresolve against its own directory, which makes a skill folder with scripts\nbeside the markdown work the way you would expect.\n\nEach skill is also a command — `/skill:deploy` — for when you want it applied\nnow rather than when the model decides. Set `\"enableSkillCommands\": false` in\nsettings to keep skills prompt-only, or put `disable-model-invocation: true` in\na skill's frontmatter to invert it: reachable by command, invisible to the\nmodel.\n\nA **prompt template** is a markdown file in `~/.tau/agent/prompts/` or\n`.tau/prompts/` that becomes a slash command expanding to its own body:\n\n```markdown\n---\ndescription: Review a file for bugs\nargument-hint: <path>\n---\n\nReview $1 for correctness bugs. Ignore style.\n```\n\nThat is `/review api.go`. Arguments substitute as `$1`, `$2`, `$@` or\n`$ARGUMENTS` for the lot, `${1:-default}` when one is missing, and `${@:2}` to\ndrop the first.\n\nScanning honours `.gitignore`, `.ignore`, and `.fdignore`, so a skills folder\nliving next to build output does not drag it in, and a skill reached twice\nthrough a symlink is one skill rather than a name collision. `/reload` picks up\nedits to both without restarting the session.\n\nProject-local skills and prompts are behind the trust gate: a directory you\nhave not approved contributes neither. Both put text in front of the model, and\ncloning a repository should not be enough to steer the agent.\n\n## Themes\n\nSet one in `~/.tau/settings.json`:\n\n```json\n{ \"theme\": \"dark\" }\n```\n\n`dark` and `light` are built in. A custom theme is a JSON file in\n`~/.tau/agent/themes/`, named by its `name` field rather than its filename, and\n`themes` in settings adds further files or directories to search.\n\nGive the setting two names separated by a slash and tau picks between them by\nwhat your terminal is:\n\n```json\n{ \"theme\": \"solarized-light/solarized-dark\" }\n```\n\nThe background is read from `COLORFGBG` when the terminal sets it, and asked for\nover OSC 11 when it does not.\n\nThe file format is Pi's, unchanged — a `colors` table of 52 tokens, each a\n`#rrggbb` string, a 256-colour palette index, the name of an entry in `vars`, or\n`\"\"` for no colour:\n\n```json\n{\n  \"name\": \"solarized-dark\",\n  \"vars\": { \"base0\": \"#839496\", \"cyan\": \"#2aa198\" },\n  \"colors\": { \"text\": \"base0\", \"accent\": \"cyan\", \"error\": \"#dc322f\" }\n}\n```\n\nA theme written for Pi loads in tau and vice versa; copying one out of\n`~/.pi/agent/themes` is all the migration there is. A theme that fails to\nload — a missing token, a variable that refers to nothing — is named at startup\nwith the reason, and the rest still load.\n\n## Keybindings\n\nThe keys are in `~/.tau/agent/keybindings.json`, mapping a binding id to a key\nor a list of keys:\n\n```json\n{\n  \"tui.app.model.cycle\": [\"ctrl+p\", \"f2\"],\n  \"tui.input.submit\": \"ctrl+s\",\n  \"tui.app.suspend\": []\n}\n```\n\nAn empty list unbinds an action entirely. Identifiers are written\nmodifier-first — `ctrl+p`, `shift+ctrl+p`, `alt+left` — and modifier order\ncarries no meaning, so `shift+ctrl+p` and `ctrl+shift+p` are the same key.\n\nThis file is global only. There is no project-level version and no setting that\nintroduces one: cloning a repository should not change what Ctrl+C does in your\nterminal.\n\nThe ids are Pi's, so a `keybindings.json` copied out of `~/.pi/agent` works\nunchanged. A file that still uses Pi's older flat names is migrated in place the\nfirst time tau reads it, and anything tau does not recognise is preserved\nverbatim rather than dropped on the rewrite. A binding tau cannot parse — an\nunknown id, a key that names nothing — falls back to its default and is reported\nat startup, along with any two actions that now answer to the same key.\n\nThe defaults worth knowing:\n\n| Key | Action |\n|---|---|\n| `enter` | send |\n| `shift+enter`, `ctrl+j` | newline |\n| `alt+enter` | queue a follow-up — sends immediately when nothing is running |\n| `esc` | stop the agent |\n| `ctrl+c` | clear the prompt; twice on an empty one quits |\n| `ctrl+d` | quit, when the prompt is empty and nothing is running |\n| `ctrl+p`, `shift+ctrl+p` | next / previous model |\n| `shift+tab` | cycle the thinking level |\n| `ctrl+t` | show or hide thinking blocks |\n| `ctrl+z` | suspend |\n\nThe prompt is a readline: `ctrl+a`/`ctrl+e`, `ctrl+u`/`ctrl+k`, `ctrl+w`,\n`alt+b`/`alt+f`, `alt+d`, and the arrows, all rebindable under `tui.editor.*`.\nTyping always wins — binding an action to a bare letter shadows nothing, because\na config that made `a` untypeable would leave you unable to type the command\nthat undoes it.\n\n`ctrl+-` undoes, a word or a run of typing at a time rather than a character.\n`ctrl+w`, `alt+d`, `ctrl+u` and `ctrl+k` put what they remove on a kill ring;\n`ctrl+y` yanks the last kill back and `alt+y` walks further back through the\nring. The ring outlives a submission, so text cut from one prompt can be yanked\ninto the next. `ctrl+g` opens the prompt in `$VISUAL` or `$EDITOR` and adopts\nwhatever comes back — as one undoable edit, so `ctrl+-` takes it back.\n\nThe session picker (`/resume`) acts on the highlighted row: `ctrl+s` flips the\norder, `ctrl+p` shows full paths, `ctrl+r` renames, and `ctrl+d` deletes after\na confirmation. Every one of them closes the picker, does the thing, and opens\nit again on a fresh listing. `ctrl+backspace` also deletes, but only while the\nfilter is empty — with nothing typed there is no text for backspace to remove,\nwhich is what makes the key safe to reuse there.\n\nThe session-tree picker (`/tree`) has its own set — views, folding and labels —\ndescribed under [Session trees](#session-trees). `shift+l` and `shift+t` work\nthere because a capital letter and `shift+<letter>` are now understood as one\nkey; a terminal cannot spell the second, it just sends the character shift\nproduced. `shift+ctrl+o`, the backward cycle, is the exception: terminals send\nthe same byte for `ctrl+o` with and without shift, so it only fires on terminals\nthat speak a protocol tau does not yet read. Cycling forward wraps, so nothing\nis unreachable.\n\nThree caveats. `shift+enter` needs a terminal that distinguishes it from\n`enter`, which most do not report to tau yet; `ctrl+j` is the one that always\nworks. `ctrl+backspace` and `ctrl+h` are the same key: a terminal sends one\nbyte for both, so binding them to different actions binds one key twice.\nAnd `pageUp`, `pageDown` and `copy` are deliberately left alone: tau prints into\nthe terminal's own scrollback rather than taking over the screen, so scrolling\nand selection belong to the terminal, and claiming those keys would take them\naway from it.\n\n## Running shell commands\n\nA prompt starting with `!` runs in your shell instead of going to the model:\n\n```\n!git status\n!go test ./...\n!!npm install          # runs, but the model never sees it\n```\n\nThe prompt gutter changes colour as soon as the `!` is typed, so there is never\na question about where Enter will send the line. What the command printed is\nrecorded in the session and shown to the model on the next turn — which is the\npoint: you run the test, and the model can see why it failed without you pasting\nanything.\n\nDoubling the prefix keeps the run out of the model's context. Use it for the\ncommand you are about to run twenty times while iterating, where each run would\ncost tokens and tell the model nothing new. It is still recorded in the\ntranscript and in an export; it is just never sent.\n\nThis is your shell, not the model's tool: there is no approval step, no timeout,\nand `$EDITOR`-style interactive programs will not work, because the command runs\nwithout a terminal attached. Output beyond 64 KiB is captured to a file the\ntranscript points at.\n\n## Settings\n\n`/settings` on its own opens a menu of the settings worth changing while tau is\nrunning: Enter flips a toggle, or opens a list or a field for the rest. It shows\nwhat is in effect, defaults included, and reads the file back after each change\nrather than assuming the write landed. Emptying a field unsets the key, which is\ndifferent from setting it to an empty string.\n\nThe menu is curated, not generated. Nested configuration — `compaction`,\n`retry`, `thinkingBudgets` — is left to the file and the typed form below, since\na menu that could only edit them as pasted JSON would be worse than typing.\n\nEverything is also reachable as arguments, which is the only form that works\nheadless:\n\n```\n/settings                          # (headless) what is set, and its scope\n/settings theme                    # one key\n/settings theme gruvbox-dark       # write it to ~/.tau/settings.json\n/settings compaction.enabled false # one level of nesting\n/settings unset theme              # remove it\n```\n\nA value is read as JSON when it parses as JSON and as a string when it does\nnot, so `true` is a boolean, `2` is a number, `[\"pnpm\",\"add\"]` is a list, and\n`gruvbox-dark` is a string. Writes go to the global file; project settings are\nlisted with their scope but edited in `.tau/settings.json` directly, since a\ndirectory tau has not trusted must not be written to on a one-line command.\nKeys tau does not model are kept in the file and reported as unread rather than\nrejected — an extension may own them.\n\n`/scoped-models` narrows the set `ctrl+p` cycles through. On its own it opens a\nchecklist: space ticks a model, `ctrl+p` ticks a whole provider, `ctrl+a` and\n`ctrl+x` tick everything and nothing, `alt+↑`/`alt+↓` reorder — the order is the\norder cycling walks — and Enter saves. Ticking everything and ticking nothing\nboth mean \"no scope\", so both put the whole catalog back.\n\nPatterns still work, and are the only form that works headless:\n\n```\n/scoped-models anthropic/* openai/gpt-5.2      # patterns, saved to settings\n/scoped-models all                             # back to the whole catalog\n```\n\nPatterns are matched against both `provider/id` and the bare id, so\n`anthropic/*` also picks up the same models offered through a reseller. A\npattern matching nothing is refused rather than saved, because an empty set\nsilently falls back to every model.\n\n`/changelog` prints what changed in each release, oldest first, from the copy\ncompiled into the binary. `/import <path>.jsonl` is the other half of\n`/export`: it copies a session file into this directory's history and continues\nit, leaving the file you pointed at untouched.\n\n## Extensions\n\nAn extension observes and shapes what the agent does: gate a tool call, rewrite\nthe context, register tools and commands, ask the user something. There are two\nways to write one, and they are the same feature.\n\n**In-process, in Go** — `import \"github.com/ihavespoons/tau/extension\"`, write a\nfactory, compile it in. This is what the bundled MCP client is.\n\n**Out of process, in anything** — a program that reads and writes JSON lines on\nstdin and stdout. tau spawns it, hands it the events it subscribed to, and\ntreats it exactly like a compiled-in one: the same composition rules, the same\nerror handling, the same tool registry.\n\n```sh\ntau -e ./my-extension.ts        # repeatable\n```\n\nDiscovery, nearest first: `-e` flags, then `extensions` in your settings, then\n`~/.tau/agent/extensions/`, then `.tau/extensions/` — the last only in a\ndirectory you have trusted, because cloning a repository must not be enough to\nmake tau launch what is in it.\n\nA `.ts` or `.js` file runs under the host shim; an executable runs directly.\ntau never installs anything on an extension's behalf.\n\n### Keys and renderers\n\nAn extension can bind a key and draw a message, and both work the same whether\nit is compiled in or spawned:\n\n- **Shortcuts** — `Ctrl+C` and `Esc` always reach tau, because interrupt and\n  abort are the two ways out of a wedged turn. Every other key is claimable,\n  including tau's own bindings, so an extension can take over `Ctrl+P`; a\n  shortcut bound to a bare letter will shadow typing. When two extensions bind\n  the same key the first loaded wins.\n- **Message renderers** — a renderer draws a transcript message in place of\n  tau's own rendering. Returning no lines means *no opinion* and the built-in\n  rendering runs, so hiding a message takes a blank line rather than an empty\n  list. A renderer is called on the draw path with a 100 ms deadline; one that\n  fails or overruns it falls back to the built-in rendering instead of stalling\n  the transcript.\n\nEntry renderers can be registered and reach a subprocess extension, but nothing\ncalls them yet: tau's transcript is built from messages, not session entries.\n\n### Pi extensions\n\nPi's TypeScript extensions run on tau unmodified, through a shim you install\nonce:\n\n```sh\nnpm i -g @ihavespoons/tau-pi-host\ntau -e ~/.pi/agent/extensions/my-extension.ts\n```\n\nThe shim aliases Pi's packages to an implementation backed by the protocol, so\n`import { defineTool } from \"@earendil-works/pi-coding-agent\"` resolves and\nworks. Node 22.18+ strips TypeScript types natively, so there is no build step.\n\nWhat does not cross a pipe is anything that needs to draw: custom editors,\noverlays, and provider registration throw a named error and are reported once at\nload, rather than appearing to work and silently doing nothing. Those need a Go\nport.\n\n## Packages\n\nA package is a bundle of skills, prompt templates, themes and extensions that\nsomeone else wrote. Install one and its resources join yours:\n\n```sh\ntau install npm:some-pack               # from npm\ntau install git:github.com/you/pack     # from a git host\ntau install ./pack                      # a directory already on disk\ntau packages                            # what is installed, and from where\ntau update                              # refresh everything unpinned\ntau remove npm:some-pack\n```\n\nnpm sources take a version (`npm:some-pack@1.2.3`), git sources a ref\n(`git:github.com/you/pack#v2`). Either one, spelled exactly, is *pinned* and\n`tau update` leaves it alone: a version you wrote down is a version you meant.\nnpm and git packages land under `~/.tau/agent/npm` and `~/.tau/agent/git`. A\nlocal package is used where it lies, so `tau remove` only stops reading it —\ntau does not delete your directory.\n\nInside a package, resources are found by convention — `skills/`, `prompts/`,\n`themes/`, `extensions/` — or declared in `package.json`:\n\n```json\n{ \"name\": \"some-pack\", \"tau\": { \"skills\": [\"skills/*\"], \"themes\": [\"midnight.json\"] } }\n```\n\nThe key is `tau`, and `pi` is accepted too, so a package written for Pi works\nunchanged.\n\nInstalling records the source in `~/.tau/settings.json`. Expand that entry to\nswitch individual resources off without uninstalling:\n\n```json\n{\n  \"packages\": [\n    \"npm:some-pack\",\n    { \"source\": \"git:github.com/you/pack\", \"skills\": [\"!deploy\"], \"themes\": [] }\n  ]\n}\n```\n\nPatterns filter what loads: a bare glob includes, `!` excludes, and `+`/`-`\nforce a single file in or out regardless of the rest. `\"autoload\": false` flips\nthe default, so nothing from the package loads except what a pattern names.\n\n`tau install -l` writes `.tau/settings.json` instead, so the package travels\nwith the checkout. That path is behind the trust gate — a package is code, and\ncloning a repository should not be enough to make tau fetch and run it — so an\nunapproved project refuses the install and tells you to pass `-approve`.\n\nWhen two packages ship a resource under the same name, the first one registered\nwins: paths you wrote by hand in settings beat package paths, and a project\npackage beats a user one. `/reload` picks up a package installed mid-session.\n\n## Exporting and sharing\n\n`/export` writes the conversation to a single HTML file that opens in a browser\nwith no server and no network — the transcript, the tool calls, the diffs and\nthe system prompt are all inside it:\n\n```\n/export                      # tau-session-<name>.html, in the working directory\n/export ~/notes/bug.html     # somewhere else\n/export ~/notes/bug.jsonl    # a session file instead of a page\n```\n\nThe suffix picks the format. A `.jsonl` path writes a session file tau can open\nagain with `tau --session <file>`, flattened to the current branch; anything\nelse renders the page. Both keep the whole history, including what a compaction\nsummarized away — an export is the transcript, not the model's context.\n\nThe same renderer runs on a session file you already have:\n\n```sh\ntau export <file>.jsonl                              # tau-session-<name>.html, here\ntau export <file>.jsonl out.html -theme gruvbox-dark\n```\n\n`/share` exports the page and uploads it as a secret GitHub gist through the\n`gh` CLI, which must be installed and logged in. **A secret gist is unlisted,\nnot private:** anyone holding the link can read the whole transcript, including\nfile contents and command output that ended up in it. Read what you are sending\nbefore you send it.\n\ntau prints the gist URL and stops there — there is no tau-operated viewer, and\ntau will not hand your transcript to a third-party site by default. `gh gist\nview --web` opens it, and downloading the file opens the page as exported. If\nyou run a viewer that takes a gist id in its fragment, point `TAU_SHARE_VIEWER_URL`\nat it and tau will print `<url>#<gistId>` alongside the gist link.\n\nTool output that tau renders in the terminal but the viewer does not know about\nyet — `grep`, `find`, and extension tools — falls back to a generic view in the\npage. `bash`, `read`, `write`, `edit` and `ls` render fully.\n\n## Driving tau from another program\n\n`--mode rpc` turns tau into a JSONL server on stdin and stdout — the mode for an\neditor or a supervisor rather than a person:\n\n```sh\ntau --mode rpc\n```\n\nCommands go in, responses and events come out, one JSON value per line. Unlike\n`-p`, the connection stays usable while a turn runs: you can steer it, abort it,\nnavigate the session tree, or answer a dialog an extension opened. The command\nand event shapes are Pi's, so a client written against Pi drives tau unchanged.\n\nSessions are JSONL files by default, one per session. A program embedding tau\ncan put them somewhere else instead:\n\n```go\nrepo, err := sqlite.Open(\"/var/lib/tau/sessions.db\")\ncs, err := coding.New(ctx, coding.Options{Repo: repo})\n```\n\n`storage/sqlite` is the backend that ships. It exists for what the file layout\nis bad at — thousands of sessions, a listing that would otherwise mean opening\nevery file to read its first line, and querying from outside tau — and it is a\nsatellite: the binary does not import it, so nothing is linked into `tau` unless\nyou ask for it. The driver is pure Go, so `CGO_ENABLED=0` still holds.\n\nEntries are stored as the bytes they were written as and replayed through the\nsame decoder and the same `session.Index` the file backend uses, so the two can\nnever disagree about what a session means.\n\n## Editors: the Agent Client Protocol\n\n`tau --mode acp` speaks the [Agent Client Protocol](https://agentclientprotocol.com),\nso an editor that already drives ACP agents can drive tau. The editor launches\nit as a subprocess and talks JSON-RPC 2.0 over stdin and stdout, one message per\nline.\n\n```sh\ntau --mode acp\n```\n\n`initialize`, `session/new`, `session/prompt` and `session/cancel` are\nimplemented, against schema v1. A turn streams back as `session/update`\nnotifications — `agent_message_chunk` for prose, `agent_thought_chunk` for\nthinking, `tool_call` and `tool_call_update` as tools run — and ends with the\nstop reason the protocol asks for. Images in a prompt are accepted; that is what\nthe `image` prompt capability advertises.\n\nThere is no session up front, unlike `--mode rpc`: the editor names a working\ndirectory per `session/new` and may open several in the one process.\n\nTwo things are deliberately not claimed. `loadSession` is advertised as false,\nbecause an ACP session id is not a tau session file and promising a resume that\ncannot happen is worse than not offering it. And tau never sends\n`session/request_permission`, because tau does not gate tools at all — see\nbelow.\n\n## The server (experimental)\n\n`tau server` supervises several agents at once and hands them out over HTTP:\n\n```sh\ntau server                        # a Unix socket under ~/.tau/agent\ntau server -listen 127.0.0.1:7777 # or a port\n```\n\n```\nGET    /instances              list them\nPOST   /instances              {\"cwd\": \"...\", \"label\": \"...\"} starts one\nGET    /instances/{id}         one record\nDELETE /instances/{id}         stop it\nPOST   /instances/{id}/rpc     one command, one response\nGET    /instances/{id}/events  server-sent events for the whole stream\n```\n\nEach instance is a `tau --mode rpc` subprocess. That mode on its own is one\nagent on one pair of pipes for as long as you hold them; the server is many,\neach addressable by id and outliving any single connection — the shape an editor\nplugin or a deployment wants. Agents go down with the server, because leaving\nthem writing to session files with nothing left to address them is worse.\n\n**The socket is full control of every agent it owns.** tau does not ask before\nediting a file or running a shell command, so anything that can reach the socket\ncan run commands as you. It defaults to a Unix socket at mode 0600 for that\nreason. Think hard before `-listen` puts it on a port.\n\n## Evals\n\n`evals` runs a table of tasks against an agent and scores what it did, which is\nthe question no unit test answers: whether a change to the prompt, the tools, or\nthe model made tau better or worse at actual work.\n\n```go\nr := &evals.Runner{NewAgent: newAgent, Parallel: 4}\nresults := r.Run(ctx, []evals.Task{{\n    Name:   \"fixes the failing test\",\n    Prompt: \"make the test pass\",\n    Files:  map[string]string{\"main_test.go\": \"...\"},\n    Check:  evals.All(evals.FileContains(\"main.go\", \"return 42\"), evals.ToolCalled(\"edit\")),\n}})\nfmt.Print(evals.Report(results))\n```\n\nEach task runs in its own seeded directory, so one task's mess cannot score\nanother. The agent is supplied by the caller, which is what lets the same table\nrun offline against `ai/faux` and against a real provider when it matters.\n\nChecks read what the agent *did* — the files it wrote, the tools it called — not\nonly what it said, because the changes on disk are the work and the prose about\nthem is commentary. A failed check and a failed run are reported separately: \"the\nagent did the wrong thing\" and \"the agent broke\" are different problems.\n\n## MCP\n\ntau speaks the Model Context Protocol through a bundled extension. Configure\nservers in `~/.tau/agent/mcp.json`, using the same `mcpServers` shape other MCP\nhosts read:\n\n```json\n{\n  \"mcpServers\": {\n    \"filesystem\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@modelcontextprotocol/server-filesystem\", \"/tmp\"]\n    },\n    \"example\": {\n      \"url\": \"https://example.com/mcp\",\n      \"headers\": { \"Authorization\": \"Bearer …\" }\n    }\n  }\n}\n```\n\nA project may add servers in `.tau/mcp.json`, but only in a directory you have\ntrusted — otherwise cloning a repository would be enough to make tau launch a\nprocess. Run `/mcp` to see what connected.\n\nEverything MCP does goes through the public `extension` package, which is the\npoint: it is the acceptance test for that API, not a privileged core feature.\n\n## License\n\nMIT. tau is derived from Pi, MIT © 2025 Mario Zechner — see\n[THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md).\n"}