Metadata-Version: 2.4
Name: hey-dave
Version: 0.1.0a2
Summary: A terminal coding-agent harness that runs D.A.V.E. natively
Keywords: agent,coding-agent,llm,tui,terminal,assistant
Author: thesawdawg
Author-email: thesawdawg <sawyerksu@gmail.com>
License-Expression: PolyForm-Noncommercial-1.0.0 AND MIT AND ISC AND BSD-3-Clause AND OFL-1.1
License-File: LICENSE.md
License-File: src/hey_dave/dave/NOTICE
License-File: src/hey_dave/plugins/pi/shim/THIRD_PARTY_NOTICES.md
License-File: src/hey_dave/skills/bundled/archify/LICENSE
License-File: src/hey_dave/skills/bundled/archify/THIRD_PARTY_NOTICES.md
License-File: src/hey_dave/skills/bundled/archify/assets/JetBrainsMono-OFL.txt
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development
Requires-Dist: httpx>=0.28.1,<0.29
Requires-Dist: mcp>=2,<3
Requires-Dist: openai>=3.17.0,<4
Requires-Dist: platformdirs>=4.11.12,<5
Requires-Dist: pydantic>=2.13.5,<3
Requires-Dist: pyyaml>=6.0.3,<7
Requires-Dist: textual>=8.2.8,<9
Requires-Dist: trafilatura>=2.2.0,<3
Requires-Dist: typer>=0.27.2,<0.28
Requires-Python: >=3.14
Project-URL: Homepage, https://github.com/thesawdawg/hey-dave
Project-URL: Repository, https://github.com/thesawdawg/hey-dave
Project-URL: Issues, https://github.com/thesawdawg/hey-dave/issues
Description-Content-Type: text/markdown

# hey-dave

A terminal coding-agent harness that runs **D.A.V.E.** (Digital Assistant for
Various Endeavors) natively — not as a plugin inside someone else's harness.
D.A.V.E. is an independent implementation derived from the legacy skill bundle
in `thesawdawg/agent-skills` at commit `ca9118c`; see
`src/hey_dave/dave/NOTICE` for attribution.

D.A.V.E. is a personal assistant persona — opinionated, occasionally witty,
not a productivity product. The banner shown at startup says so on purpose:
*unstable, opinionated, and sometimes unhelpful. Not a productivity tool.*

An animated, watch-only tour lives in
[docs/walkthrough/](https://github.com/thesawdawg/hey-dave/tree/master/docs/walkthrough)
(open `index.html` from a checkout in a browser). Nine goal paths, from setup
to delegation, each with scripted terminal scenes and a strip showing what the
harness does underneath.

## Install

Python 3.14+ required. hey-dave is in alpha: releases are pre-releases,
so installers need to be told to take one.

```bash
# uv tool (recommended)
uv tool install --prerelease=allow hey-dave

# or pip into any environment
pip install --pre hey-dave

# or from a checkout
uv tool install .

# Docker (includes SearXNG and Ollama profiles)
docker compose -f docker/compose.yaml run --rm dave --version
```

**Upgrading from dave-harness.** This project used to be called
`dave-harness`. Remove the old tool (`uv tool uninstall dave-harness`) and
install `hey-dave`; the command is still `dave`. On its first start, `dave`
moves your folders to the new name: `~/.config/dave-harness`,
`~/.local/share/dave-harness` (sessions and the vault) and
`~/.local/state/dave-harness` become `…/hey-dave`, and a project's
`.dave-harness/` becomes `.hey-dave/` when you open it. Each move is
printed. Folders not yet moved keep working, and `DAVE_HARNESS_CONFIG` is
still read when `HEY_DAVE_CONFIG` is not set. Hook commands now see
`HEY_DAVE=1` instead of `DAVE_HARNESS=1`.

## First run

```bash
dave init      # nine-step guided setup: models, permissions, tracker, vault
dave init --vibe   # a model, then D.A.V.E. from three plain-language answers
dave doctor    # environment + config health checks (--json for machines)
dave           # the TUI
dave --resume  # the TUI, starting with a list of sessions to pick up
dave run "summarise this repo"   # headless
```

The wizard guides you through setup. Each step opens with a progress rail
(`── ●●◉○○○  3/6 vault ───`) and a short card that says what the
step is for and what to pick. Every answer says what it leads to, and typing
`?` at any question explains it in more detail.

The first step asks where D.A.V.E.'s model will run. It looks for a Codex
sign-in (`~/.codex/auth.json`), and for Ollama and LM Studio running on this
machine, then lists what it found next to Ollama on another computer and any
OpenAI-compatible API (OpenAI, OpenRouter, vLLM…). Found servers show how many
models they have, and Ollama models without tool support are marked. For an
API key it asks for the name of the environment variable, not the key, and
says whether that variable is set. Then you pick a permissions preset
(`cautious` or `developer`), a tracker backend and a search backend, and the
wizard initialises the D.A.V.E. vault. Every question is a numbered list:
Enter takes the default, a number picks, and open questions end with "type
your own…". Everything is shown as a diff and written only after
confirmation.

`dave init --vibe` (or `/init vibe` in the TUI, once a profile works) sets up
D.A.V.E. from three answers instead of the permission, tracker, search and
persona steps. Its first four steps (models, profiles, vault, theme) need no
model, and it will not move past the first until one is set up:

1. What is this D.A.V.E.'s purpose?
2. What is D.A.V.E. explicitly denied?
3. What should I know about you? (optional)

The model may ask up to two short follow-ups per answer, then proposes
settings. Examples:

- "ask before any git commit" becomes the rule `permissions.ask:
  bash(git commit*)`.
- "keep everything in this directory" becomes `permissions.confine:
  "project"`. File writes outside the project are denied, and shell commands
  that name outside paths ask.
- Anything no rule can express is saved as an *advisory* restriction. The
  model is told about it, but nothing enforces it.

`dave init --vibe` also asks for a theme. The same theme colours the TUI and
the interview itself, and telling the interview about contrast or colour
needs can propose one. Both wizards are in colour: step headings, questions,
option numbers and the default each have their own. `NO_COLOR` turns terminal
colour off.

You tick what to keep. Suggested allow rules start unticked, and the harness
refuses broad allow rules and any change to providers, secrets or existing
deny rules. Nothing is written until you approve, and the interview is not
saved.

## Using the TUI

| Key | Does |
|---|---|
| Enter / Shift+Enter | send / new line; during a turn Enter **queues** the message (up to 3, `tui.limits.queue`) and it is sent when the turn ends |
| Up (first line) / Down (last line) | step through sent-message history; with messages queued and the input empty, Up pulls the last one back to edit and Esc drops it |
| `/` | open the command list — Up/Down/PageUp/PageDown move, Tab completes, Enter runs, Esc closes |
| `@` | open the file list — Tab or Enter completes the path (a folder opens its entries), Esc closes |
| `!` | run a shell command yourself: `! git status` shows its output in the chat; `!! make test` also sends the output with your next message |
| PageUp / PageDown | scroll the transcript (the input box always keeps focus) |
| shift+tab | toggle plan mode (between turns) |
| ctrl+p | command palette |
| ctrl+b | show or hide the sidebar; under 110 columns, open or close it as a drawer (Esc closes it too) |
| ctrl+t | pending tracker proposals |
| ctrl+o | the activity list: every tool call, injected command text and event in full (on a `[pasted …]` placeholder: expand it) |
| ctrl+c | with text selected: copy it. Otherwise cancel the turn (queued messages pause: Enter on an empty input sends the next); while idle, clears a paused queue first, and twice quits |
| right-click | with text selected: copy it; otherwise paste |
| ctrl+v / alt+v | paste text / paste an image from the clipboard |

The indicator beside the input shows what is running (thinking, the current
tool, `waiting for you`, subagents) and for how long.

**Shell commands.** Start a message with `!` to run it in your shell
instead of sending it: `! echo "Hello"` shows `Hello` and `[exit 0 · 0.0s]`
in the chat. It runs in the session's folder with your environment, and
D.A.V.E. never sees it. Use `!!` to share the output: `!! pytest -x` runs
it and attaches the output to your next message ("why does this fail?").
Permission rules don't apply, since you typed it, and the model can never
run a `!` command. Each command starts fresh (`cd` does not carry over),
ctrl+c stops it, and during a turn it waits in the queue. Over Telegram `!`
is refused.

**Screen sizes.** The TUI is fully usable from 80×24 and fits the space it
has:
- **From 110 columns:** the sidebar sits beside the chat: 32 columns, or
  about a quarter of a wide screen (up to 56).
- **Below 110 columns:** the chat gets the whole width. `ctrl+b` opens the
  sidebar as a drawer over the chat, and Esc closes it. The header shows
  what the sidebar would, on one line: the D.A.V.E. focus and its minutes
  (amber or red when you drift), your tasks, running subagents. The
  activity indicator moves to one row under the input.
- **The input box** is one line tall until you type more; it grows up to 8
  lines (4 on a short screen).
- **The header and status bars** drop their least important fields rather
  than being cut off; `ctx %` and the error count always show.
- **Every picker and card** (`/model`, `/theme`, `/resume`, `/music config`,
  `/vault config`, permission and plan cards…) fits the screen, with its
  keys in view.
- **Permission cards show the change.** An `edit` or `write` card shows a
  colored diff against the file on disk (`+`/`−` counts, line numbers), and
  says so when the edit would fail. Long diffs show their first lines; `e`
  expands the rest.

**Make it yours.** The top-level `tui` section of the config changes the
layout and keys, and applies as soon as it is saved:

```jsonc
"tui": {
  "sidebar": { "sections": ["now", "tasks", "activity"], "position": "left", "shown": true },
  "header": ["name", "mode", "ctx", "usage", "profile"], // fields, in order
  "status": ["tokens", "music"],                    // the error count always shows
  "keys": { "sidebar": "ctrl+s", "permission.allow_once": "y" },
  "layout": { "sidebar_inline_from": 120, "composer_lines": 12 },
  "skin": { "placeholder": "Ask D.A.V.E.", "sections": { "now": "Today" } },
  "skins": { "matrix": { "rain": false } },
  "banner": false
}
```

- **`sidebar`:** which sections show, in what order, on which side, and
  whether it starts open. D.A.V.E.'s six (`dave`, `now`, `goals`,
  `today`, `proposals`, `subagents`) scroll together as one panel.
- **`header` and `status`:** which fields the bars show.
- **`keys`:** any key in the list above, and every card's letters. `/keys`
  lists what can be changed. Esc, Enter, ctrl+c and the arrows stay fixed,
  and a key that would type into the input or clash on a screen is refused.
  The hints on each card show the key you chose.
- **`layout`:** the sizes above: where the sidebar goes inline, how wide it
  is, how tall the input grows.
- **`skin`** (every theme) and **`skins.<theme>`** (one theme): the words and
  glyphs a theme uses: the header name, sidebar headings, activity tags,
  spinner, placeholder, rain, and `tune`, the built-in music it plays.
- **`themes`:** your own themes ([below](#your-own-themes)).
- **`banner`:** `false` turns the startup banner off.

The chat holds only the conversation. Tool calls, the text a slash command
injects for the model, and harness events (compaction, warnings, subagent
reports) are listed under **Activity** at the bottom of the sidebar, in time
order, each tagged with its type — `TOOL`, `CTX` (injected context) or `EVT`
— in its own colour. Click a line, or press ctrl+o, to see it in full.
There, `c` copies the entry to your clipboard, and `s` saves it to a file
under `~/.local/state/hey-dave/exports/`.

**Copy and paste.**

- **Copy.** Drag over any text (the chat, Activity, the input box, an open
  card) to select it, then press **ctrl+c** or **right-click**. A toast
  says what was copied and how. With nothing selected, ctrl+c still
  cancels or quits. shift+drag makes your terminal's own selection instead,
  for its own ctrl+shift+c.
- **`/copy`** copies without the mouse, word for word as it was said:
  - `/copy`: D.A.V.E.'s last reply, as markdown;
  - `/copy 2`: the reply before it;
  - `/copy code`: the last code block of the last reply, without the
    fences (`/copy code 2` is the one before);
  - `/copy all`: the whole conversation.
- **How it reaches the clipboard.** Copying uses `clip.exe` under WSL,
  `pbcopy` on macOS, `wl-copy` or `xclip`/`xsel` on Linux, and otherwise
  the terminal (OSC 52, which some terminals ignore).
  `harness.clipboard: native` or `osc52` forces one way.
- **Paste** with ctrl+v, or right-click with nothing selected. A long
  paste (over 20 lines or 2,000 characters, `harness.paste`) shows as
  `[pasted #1 · 240 lines]`:
  - the model still gets all of it, after your words;
  - Backspace deletes the placeholder whole;
  - ctrl+o on it puts the text back in the box.
- **Images.** **alt+v** pastes the image on your clipboard (a screenshot,
  say) as `[image #1]`. It is sent as an image with your message and saved
  under `~/.local/state/hey-dave/pastes/`. That file is deleted once no
  saved session uses it.

**`@` puts files and folders into the context.** Type `@src/app.py` (or
`@"notes/big plan.md"` for a path with spaces) anywhere in a message:

- A file is sent to the model with its contents, after your words.
- A folder is sent as a list of the files in it. In a git repository,
  files your `.gitignore` excludes are left out.
- An image (`.png`, `.jpg`, `.gif`, `.webp`, up to 5 MB, 4 per message) is
  sent as an image, and so is one D.A.V.E. opens with `read`.
- Typing `@name` lists matches from the folder you're in and from anywhere
  in the project, skipping what `.gitignore` excludes. You can still type
  the full path to an ignored file to attach it.

The chat keeps what you typed. Each attachment is a `CTX » @path` row in
Activity. Some things are refused, with a warning in Activity:

- a path your `permissions.deny` rules block;
- other binary files, and `.bmp` images;
- files over 4 MB.

A file longer than 100,000 characters is cut off, and the model is told
where, so it can read the rest. `dave run "explain @src/app.py"` works the
same way. It also works in the arguments of commands from files (D.A.V.E.,
skill and your own commands): `/mycheck @src/app.py` attaches the file
after the command's text. A command can turn that off with `mentions:
false` in its frontmatter. Built-in commands like `/title` take `@` as
plain text.

Slash commands:

- `/help` lists them; **`/<command> --help`** (or `/help <command>`) shows a
  command's details, subcommands and examples without running it.
- A command missing a required argument shows its usage instead of running.
- **`/resume`** picks up a saved session. It lists this project's
  sessions, most recently active first. Each row has the title, when you
  last used it and how many messages it holds, plus the first line of your
  last message and of the last reply. Under the list, a preview shows the
  highlighted session's last few messages. Type to filter by title, text
  or id; Tab switches to every project; Enter resumes; Esc stays where you
  are. `dave --resume` starts the TUI with the same list, `/resume <id>`
  and `dave --resume <id>` go straight to one, and `dave --continue` takes
  the latest. `/sessions` prints the same details for the last ten.
- `/model` opens a picker of profiles and every provider's models;
  `/model <profile> [model]`, `/model <model>` and `/model <provider>/<model>`
  switch directly. The choice is kept with the session.
- **`/rewind`** undoes D.A.V.E.'s file changes, turn by turn, even without
  git. Pick a turn to put the files it (and later turns) changed back as
  they were before it:
  - `a` also cuts the conversation back to that message, and puts the
    message back in the input box;
  - `c` cuts only the conversation.

  Files you changed since D.A.V.E. wrote them are shown as conflicts and
  need a second yes. Changes made by shell commands are **not** recorded,
  so turns where bash ran say so. A rewind can itself be rewound. Settings
  live under `harness.checkpoints`.
- `/init vibe` configures D.A.V.E. from three answers (see
  [First run](#first-run)).
- `/theme` opens a theme picker that previews as you move; `/theme <name>`
  switches directly. Both save `harness.theme`. Each theme shows its WCAG
  contrast rating (AAA or AA). Every theme is tuned for readability: all
  text, including placeholders and muted text, reaches at least AA on every
  background it sits on, with muted text at 6:1 or better. The dark classics
  are adjusted the same way and keep their colours wherever they already
  passed. The themes come in five groups, plus your own:
  - D.A.V.E.: `dave-dark` (the default) and `dave-light`.
  - Accessible: `dave-high-contrast` and `dave-high-contrast-light` (AAA),
    and `dave-colorblind` and `dave-colorblind-light` (Okabe–Ito
    colour-blind-safe colours, AA).
  - Dark classics: Dracula, Nord, Gruvbox, Tokyo Night, Catppuccin Mocha,
    One Dark, Monokai, Solarized Dark and Rosé Pine.
  - Immersive: `matrix`, `dnd` and `lotr`. These restyle the whole TUI,
    not just its colours:
    - `matrix` is The Matrix: green phosphor on black, digital rain down
      the left edge, square frames, `// OPERATOR`-style headings,
      `EXEC`/`LOAD`/`SYS` activity tags and a katakana spinner.
    - `dnd` is Dungeons & Dragons: torchlit parchment and gold, double
      frames, headings such as "The Party" and "Adventure Log" in Fraktur,
      `CAST`/`LORE`/`OMEN` tags and a dice spinner.
    - `lotr` is The Lord of the Rings: gold on the green-black of a Shire
      night, round frames like hobbit doors, slow glowing runes down the
      left edge, headings such as "The Fellowship" and "The Red Book",
      `DEED`/`TOME`/`SIGN` tags, a rune spinner, and **background music**.

  - Avengers: `iron-man`, `captain-america`, `thor` and `hulk`. Immersive
    like the three above, and each plays its own music:
    - `iron-man`: the armour's cherry red and gold, frames
      with a lit title bar like a heads-up display, hex telemetry down the
      left edge, headings such as "J.A.R.V.I.S." and "Iron Legion",
      `FIRE`/`SCAN`/`PING` tags, and a hard-rock riff.
    - `captain-america`: the shield's navy, white and red, heavy-rimmed
      frames, star headings such as "The Mission" and "Field Log",
      `OPS`/`INTL`/`COMM` tags, and a brass march.
    - `thor`: storm blue, gold and red, heavy iron frames, lightning and
      runes down the left edge, headings such as "The All-Father",
      `BOLT`/`RUNE`/`HORN` tags, and a thunder saga with horns and a choir.
    - `hulk`: gamma green and purple, solid slab frames, headings that
      shout ("SMASH LIST", "RAGE LOG"), `HIT`/`LOOK`/`ROAR` tags, a
      rage-meter spinner, and a rampage that breaks into a lonely piano.

    A terminal app can't change your font. These themes use Unicode
    letterforms instead (Fraktur, half-width katakana, runes), which need a
    font that has them or your terminal's fallback. Windows Terminal and
    most Linux terminals have one. With `TEXTUAL_ANIMATIONS=none` the rain
    holds still.
  - <a id="your-own-themes"></a>Yours: themes you define under
    `tui.themes` in the config. Each starts from a built-in theme (`base`)
    and takes its frames, words, music and banner quotes, with any colours
    and skin parts you set on top:

    ```jsonc
    "tui": { "themes": {
      "ocean": {
        "base": "matrix",                    // unset: dave-dark
        "description": "deep sea blues",     // shown by /theme
        "colours": { "primary": "#4aa3ff", "background": "#001018", "muted": "#7fa0b8" },
        "skin": { "app_name": "~ dave ~", "tune": "thor" }
      },
      "nord": { "colours": { "accent": "#ffcc00" } }   // your version of Nord
    } }
    ```

    The colour roles are `primary`, `secondary`, `accent`, `success`,
    `warning`, `error`, `foreground`, `background`, `surface`, `panel`,
    `muted` and `border`, as `#rrggbb`. Your themes are made readable like
    the rest: a colour that falls under AA is lightened or darkened just
    enough, and `/theme` shows the rating. They are listed under **Yours**,
    and a theme named after a built-in replaces it. A change applies at
    once, even to the theme on screen; `/dave` can make it for you.
- **`/music`** plays music while you work.
  - **`/music config`** is the easiest way to set it up. It opens a screen
    where you pick a source and fill in its settings. It checks what works
    as you type: the player, the folder's tracks, and the Spotify sign-in
    and open devices. Nothing plays while you set up. Enter saves, and
    ctrl+l signs in to Spotify.
  - Or use `/music source …` to pick what plays. The choice is saved:
  - **`tune`** (the default): the theme's own music, for `lotr` and the
    four Avengers. It starts with the theme, changes when you switch to
    another of them, and stops when you switch to a theme without music.
    Each piece is original (none is a film's score), synthesised once and
    cached: a folk air for `lotr`, a rock riff, a march, a thunder saga and
    a rampage for the Avengers. Set `harness.music.file` to play your own
    audio file instead, in any of those themes.
  - **`local <folder>`**: your own `.mp3` (or `.flac`, `.ogg`, `.m4a`, …)
    files, in any theme, as a playlist. For example,
    `/music source local ~/Music/focus`.
  - **`spotify`**: your Spotify account. The harness controls Spotify
    wherever it's open (the desktop app, your phone, a speaker), and the
    music plays there. You need Premium and your own client ID:
    1. Create an app at <https://developer.spotify.com/dashboard>. Tick
       *Web API*, and add the redirect URI `http://127.0.0.1:8898/callback`.
    2. Run `/music login <client-id>` and sign in in the browser.
    3. If the page after sign-in doesn't load, paste its address:
       `/music login <address>`.

    The tokens stay in the state directory (0600), and `/music logout`
    deletes them. `/music play <link>` plays a Spotify playlist, album or
    track link.
  - **`youtube`** (saved as `media`): the system media keys. They control
    YouTube in a browser or the YouTube Music app, or any other player that
    has the media session: Windows' under WSL, or `playerctl` on Linux. You
    start what plays in that player; the harness only plays, pauses and
    skips.
- **Controls:**
  - `/music` says what's playing, where, and at what volume.
  - `on`, `off`, `play`, `pause`, `resume`, `next`, `prev`, `volume <0-100>`,
    `shuffle on|off` and `repeat off|all|one` work where the source allows.
  - **F7/F8/F9** are previous, play/pause and next.
  - The status bar shows the current track, for example `♪ 02 Second (2/12)`.
- **The harness's own player** (for `tune` and `local`) is the first one
  it finds: `ffplay` (from ffmpeg), `mpv`, `pw-play`, `paplay`, `afplay`
  or `aplay`.
  - Choose one with `harness.music.player`, and set the volume with
    `/music volume` (`harness.music.volume`, default 40).
  - On WSL, `ffplay` works through WSLg's sound server.
  - It never starts loud. Before starting on its own, it reads your system
    volume (Windows' volume on WSL, PipeWire, PulseAudio or ALSA on Linux,
    or macOS's). If system × music volume would be over 25%, it holds and
    asks: lower the volume, or `/music on` to play anyway. That confirms it
    for the rest of the session.
- `DAVE_NO_AUDIO=1` keeps the harness silent: no player, no media keys, no
  Spotify calls. `harness.music.player: "none"` silences the harness's
  player only.
- `/copy` copies a reply, a code block or the conversation (see *Copy
  and paste* above).
- `/errors` lists what went wrong this session (see *When something goes
  wrong* below).
- `/status` summarises the session: profile, context (and images in it),
  focus, subagents, waiting reports and proposals, and remote state.
- `/remote` connects the open session to a chat app (see [Remote](#remote)).
- `/vault` shows vault sync; `/vault config` sets it up; `/vault sync`,
  `/vault login [client_secret….json|client-id]`, `/vault logout [--forget-client]`
  (see *Sync across devices* below).
- D.A.V.E. commands: `/brief`, `/focus`, `/item`, `/sidequest`, `/park`, `/check`, `/delegate`,
  `/mission`, `/project`, `/promise`, `/intake`, `/review`, `/standup`,
  `/supervise`.

`dave run "/<command> --help"` prints the same help headless.

### Permission cards

When D.A.V.E. needs your yes, the card shows the call and how long an
answer can last:

| Key | Does |
|---|---|
| `a` | allow this call |
| `s` / `p` / `g` | allow this session / for this project / **everywhere**: every project, every open and future session |
| `d` | deny: `d` again denies this call; `s` / `p` / `g` deny for that long; Esc goes back |
| `n` | deny and tell the model why |
| `b` | widen what `s`/`p`/`g` save: the exact call, its prefix (`bash(git status*)`, a folder, `mcp(redmine:*)`), or the whole tool (`bash(*)`). The card shows the rule |
| Esc | deny this call |

Project answers go to `.hey-dave/config.json`, everywhere answers to
`~/.config/hey-dave/config.json`, as `permissions.allow` or
`permissions.deny`. A file path saved everywhere is absolute. A project's
rules add to the global ones and never hide them. Every open session picks
up a saved rule on its next call. `/permissions` lists the rules by scope
and `/permissions remove <rule>` takes one out; `/permissions
reset-session` drops this session's. Over `/remote` answers are for this
call only.

### Plan mode

**Plan first, then change things.** `/plan add a --json flag to dave
doctor` (or shift+tab, then type) puts the session in plan mode:

- D.A.V.E. can read, search and run look-only commands: `git status`,
  `git log`, `git diff`, `git show`, `ls`, `cat`, `head`, `tail`, `wc`,
  `rg`, `grep` and `find`. Change the list with `permissions.plan_bash`.
  Every other command, every write and edit, and redirection are denied,
  whatever your allow rules say. So are `find -delete`/`-exec` and D.A.V.E.
  state writes. A listed command still needs your usual allow rule or a
  yes on its card.
- The header says `mode: plan` and the input box takes the theme's warning
  colour.
- It ends with a **plan card**:
  - `a` approves: plan mode ends and D.A.V.E. implements the plan in a new
    turn, so `/rewind` can undo it as one step;
  - `m` also saves the numbered steps as the open mission's plan, ready for
    `/supervise`. It asks first;
  - `k` keeps planning, with your feedback;
  - `d` or Esc discards the plan and stays in plan mode.

`/plan` or shift+tab toggles plan mode, and `/plan on|off` sets it. The mode
is saved with the session. Over `/remote` the card has Approve and Discard
buttons, and any message is feedback. `dave run --plan "<task>"` prints the
plan and stops (`--json` emits `plan_proposed`).

### Unfocused sessions

**General work, with your projects held back.** `/unfocused` is for work
that belongs to no task: a quick script, a question, tidying notes.

- D.A.V.E. works normally, but files inside your registered projects are
  read-only. Writes and edits there are refused. Shell commands that run in
  a project or name a path in one ask first, unless they only look (the
  `permissions.plan_bash` list). Subagents inherit the hold.
- Your current focus is paused under a general detour, so the time shows in
  the ledger and the weekly review as general time, not drift. After the
  drift threshold D.A.V.E. mentions it once per period, then stays quiet.
- Setting a focus ends unfocused mode and lifts the hold: picking a task is
  how you get back into a project. `/unfocused off` ends it and returns to
  the paused focus.
- Each session's unfocused periods are recorded in the vault under
  `sessions/general/<device>/<yyyy-mm>/<session-id>.md`: when, why each
  ended, the files changed and how many calls were held back. Transcripts
  stay in the normal session store.
- The header says `mode: unfocused`. The mode is saved with the session, and
  a resume starts a new period. With no focus at session start, D.A.V.E.
  offers it once. It needs a vault.

### Task list

On work of three or more steps D.A.V.E. keeps a short checklist with
`todo_write`. It shows in the sidebar's **Tasks** section, between the
D.A.V.E. panel and Activity: `☐` to do, `◐` in progress, `☑` done, and a
count in the heading (`Tasks 2/5`). The list belongs to this session only.
It is saved with the session, and it is not your Now list or a mission's
plan. It hides while empty, works in plan mode, and is switched off under
`/supervise`, where the mission's steps are the plan.

### Notifications

When you're in another window, the terminal tells you that D.A.V.E.:
- finished a turn that took 15 s or more (`harness.notify_after`);
- is waiting on a permission card or a plan card;
- has a subagent report ready.

Nothing fires while the dave window has focus, unless you set
`harness.notify_when_focused: true`.

- `harness.notify: "bell"` (the default) rings the terminal bell.
  Windows Terminal flashes the tab or plays its bell sound, depending on
  your profile's bell setting.
- `"desktop"` shows an OS notification with a short text such as `turn
  finished (42s)`. It never includes tool output. `dave doctor` shows the
  route it picked for your terminal:

  | Terminal | Route |
  |---|---|
  | Windows Terminal under WSL | a Windows toast (via `powershell.exe`) |
  | Windows Terminal with `compatibility.allowOSC777` on | set `harness.notify_via: "osc777"` |
  | iTerm2, Apple Terminal, ConEmu | OSC 9 |
  | WezTerm, Ghostty, Warp, GNOME Terminal and other VTE terminals | OSC 777 |
  | kitty, foot | OSC 99 |
  | anything else | the bell |

  Inside tmux, add `set -g allow-passthrough on`.
- `"off"` turns notifications off. `/remote` has its own list
  (`remote.notify`).

### When something goes wrong

A failure shows as a card in the chat, never as a silent freeze or a crash:

```
✗ Internal error while showing a tool result — KeyError: 'path'
  kept: the turn is still running; nothing lost
  next: /errors copy 1 to report it
```

- The status bar counts errors, and `/errors` lists them. `/errors 1`
  shows the newest in full, and `/errors copy 1` copies a bug report with
  secrets removed.
- The full traceback is in the log. `/errors log` prints where the log,
  crash reports and stall logs are.
- Nothing typed is lost. A reply that was streaming when the app closed
  comes back on resume, marked as interrupted. Your unsent message and
  queued messages come back too, with the queue paused.
- If the app ever does close on an error, it says so in the terminal. It
  prints the command to resume your session (`dave --resume <id>`) and
  the path of a crash report with secrets removed. The next start also
  notes that the last run did not close cleanly.
- When quitting seems stuck, press ctrl+c again. When the whole app
  seems stuck, run `dave debug stacks` in another terminal. It prints what
  every part of the app is doing, even while it is frozen, and doesn't
  stop it.
- `dave doctor` also checks where sessions are saved: whether the folder
  is writable, whether there is disk space, and whether it is on a slow
  filesystem (WSL's `/mnt/c`, network drives). It also lists runs that
  did not close cleanly and recent crash reports.
- A subagent that has been silent for 5 minutes shows `idle` on its
  sidebar card. If the disk fills up, the session is kept in memory and
  saved once there is room again.
- A frozen screen is noticed. If the app stops responding for 2 s, what
  it was doing is written to a file. After 30 s, the bell rings once and
  the terminal title says `not responding`. When it recovers, a card says
  how long it was frozen and where. When the model is silent, the
  indicator says `waiting for <model> · 35s`, then `no response for 60s ·
  ctrl+c cancels`.
- ctrl+c always stops a turn, even when the model has gone silent. After
  about 1.5 s the turn is cut off, and the text so far is kept.
- When a provider fails and the request is retried, the indicator says so
  (`retrying 2/3 after network error`). Set `harness.turn_stall_seconds`
  to stop a model that has gone silent and retry once.

## Remote

`/remote` connects the **open TUI session** to Telegram, so you can follow it,
approve single actions and talk to D.A.V.E. from your phone. Nothing polls or
leaves the machine until you configure a channel and run `/remote on`. The
bridge stops when the TUI closes.

Setup:

1. In Telegram, message **@BotFather**, send `/newbot`, and copy the token.
2. Export it: `export DAVE_TELEGRAM_TOKEN=<token>` (the config only names the
   variable, `remote.telegram.bot_token_env`).
3. `dave config set remote.channel telegram`, then start `dave`.
4. In the TUI, `/remote pair` shows a 6-digit code. Send `/pair <code>` to your
   bot in a private chat within 5 minutes. Your Telegram user ID is stored in
   `remote.telegram.allowed_user_id`.
5. `/remote test` sends a test message. `dave doctor` checks the token and
   the pairing.

In the chat:

- Plain text is a prompt, and `/command args` runs a slash command. During
  a turn the phone is told `busy`: the terminal's message queue is terminal
  only.
- `/cancel` stops the running turn. `/help` lists the commands allowed
  remotely.
- Permission cards arrive with *Allow once* / *Deny* / *Deny + reason*.
  Whichever of the terminal or the chat answers first wins.

Remote can never grant a session or project rule, or *Allow writes* on a
delegate card. `/quit`, `/resume`, `/export` and a few other commands reply
"terminal only". Only the paired user in a private chat is heard; everything
else is dropped silently.

Telegram bot chats are not end-to-end encrypted. With `remote.share:
"summary"` (the default), only these are sent:

- replies;
- the commands you are asked to approve;
- paths, diffstats and tool outcomes.

`"full"` also sends tool output and diffs as files. `remote.notify` picks the
notifications: `turn_finished`, `permission`, `report_ready`, `error` and
`drift`. `remote.autostart: true` starts the bridge with the TUI. Details are
in [docs/spec/10-remote-channels.md](https://github.com/thesawdawg/hey-dave/blob/master/docs/spec/10-remote-channels.md).

## Configuration

One JSON file: `~/.config/hey-dave/config.json` (project overrides in
`.hey-dave/config.json`; `${ENV_VAR}` references for secrets — never
literal keys). Manage it with `dave config get|set|validate`. Details:
[docs/spec/01-config.md](https://github.com/thesawdawg/hey-dave/blob/master/docs/spec/01-config.md).

```jsonc
{
  "providers": { "codex": { "type": "codex" } },
  "profiles":  { "main":  { "provider": "codex", "model": "gpt-5-codex" } },
  "harness":   { "default_profile": "main" },
  "roles":     { "scout": "main" },          // roster role → profile routing
  "permissions": { "allow": ["bash(git status*)"] },
  "mcpServers": { "redmine": { "command": "…" } },
  "tracker":   { "backend": "local" }
}
```

Provider kinds: `codex` (Codex CLI auth), `openai_compat`, `ollama`,
`scripted` (tests). Every kind lists its models from the server: `codex`
asks the ChatGPT backend for your account's models, as the Codex CLI does.

### Limits and timeouts

The sizes, counts and waits the harness used to fix are settings, each
next to the feature it tunes, with the old value as the default:

- `harness.tools`: the longest `bash` timeout, the `read` size cap, `glob`
  and search caps, the task list's length, a hook's timeout;
- `harness.shell` (`!` and `!!`), `harness.mentions` (`@`),
  `harness.context` (compaction, `AGENTS.md`), `harness.housekeeping`
  (backups, crash reports, `dave doctor`'s disk and crash checks);
- `tui.limits`: the queue, the activity log, the busy line's "waiting",
  the notification gap, the sidebar refresh, `/resume`'s preview;
- `remote.limits`: messages a minute, the pairing window and attempts;
- per provider `max_attempts` (and Ollama's `capability_ttl_hours`, Codex's
  `refresh_early_seconds`), `plugins.web_fetch.max_redirects`.

Each has a hard range, so no value switches a guard off, and raising a
remote rate or pairing limit through `/dave` is flagged as widening
access. `/dave` reads them all (`harness_guide limits` lists every one).

### Set up from the chat: `/dave`

`/dave <request>` asks D.A.V.E. to work on the harness itself, without
leaving the TUI. For example:

- `/dave help me add my Ollama box as a provider`;
- `/dave route the critic role to the local model`;
- `/dave why is web search failing?`

It enters *harness mode* (`mode: harness` in the header), and your replies
continue it until `/dave off`.

- **What it can do.** In this mode D.A.V.E. can only read the config (with
  secrets hidden), read built-in setup guides, run read-only checks, and
  propose changes. It has no file, shell or web tools.
- **What it can see.** Every section of the config and what each holds,
  whether set or not, with each key's type; every key id you can remap;
  each helper role as packaged; every command you can turn off. It can
  also read D.A.V.E.'s own settings in the vault (your name, hours,
  review day, which helpers are on…) and which of D.A.V.E.'s texts are
  your versions, but it can't change those here: it tells you how
  (`/dave off` and ask, `/init vibe`, or `dave prompts copy`).
- **The card.** Every change comes as a card: tick the items you want and
  press Enter; `f` sends feedback and Esc rejects.
  - Anything that widens access starts unticked and needs you to type
    `allow`: an allow rule, a removed deny rule, turning `confine` off.
  - New MCP servers and skill folders (which run commands) and new
    provider hosts are flagged with a ⚠.
- **API keys** are never written into the config. D.A.V.E. uses an
  `api_key_env` name, and the card lets you paste the key for this session
  only; export it in your shell profile to keep it.
- **After you apply,** the files are backed up to
  `~/.local/state/hey-dave/config-backups/`, and the change reloads at
  once. Plugins, MCP servers and skills load at the next start. Checks then
  run (the new provider's model list, for instance), and D.A.V.E. fixes
  what failed.
- **`/dave undo`** puts the files back. See [docs/spec/02-providers.md](https://github.com/thesawdawg/hey-dave/blob/master/docs/spec/02-providers.md).

Images go to every provider. Ollama is asked whether the model has vision;
for the others, set `"images": false` on a profile whose model can't take
them. The model then gets a short note instead of the image. Sessions keep
the image's path and hash, not its bytes, so an image moved or edited later
is sent as a note, not a different picture.

## External agents (Claude, Devin)

D.A.V.E. can hand a charge to an **external coding agent** over
[ACP](https://agentclientprotocol.com) — Claude Code through the
`claude-agent-acp` adapter, or Devin through `devin acp` — instead of a
harness subagent. They are *workers*, not model connections: they never
answer D.A.V.E.'s own turns, and they run with **your own sign-in**. There
is no credential field; the harness never logs in for them, and the agent
process gets your environment unchanged (a key the harness resolved for a
profile is not forwarded — if your shell exports `ANTHROPIC_API_KEY`,
Claude's worker bills that key, not your plan; the card and `dave doctor`
say which).

Setup:

- **Claude Code**: `claude` installed and signed in, then the adapter —
  `dave plugins install @agentclientprotocol/claude-agent-acp --latest-stable`
  (into the harness's npm prefix; needs Node ≥ 22).
- **Devin**: the `devin` CLI installed and signed in (Pro, Max or Teams);
  `devin acp` is Cognition's own ACP entry point.
  `agents.providers.devin.cloud: true` runs `devin acp --cloud`.
- **Check**: `dave doctor` gives each enabled provider an `agent:<id>`
  line — the binary (PATH, then the npm prefix), version, Node floor, an
  `initialize` handshake and the sign-in *method*. It never opens a
  session, signs in, or installs anything. Bare `/agent` in the TUI lists
  the same, plus the external workers running.

Use:

- `/agent claude fix the login redirect` dispatches a charge (the
  `implementer` role, or `--role critic`). The **dispatch card** always
  comes first — provider, role, model, local or cloud, the billing method,
  and a warning when the provider can act outside the harness — because it
  spends your agent's own quota; no allow rule skips it. One charge per
  provider at a time (`agents.providers.<id>.max_concurrent`); more queue.
- D.A.V.E. can dispatch too: `delegate(agent="claude")` for one charge, or
  `agents.routes` to route a role (`"agents": {"routes": {"implementer":
  "claude"}}`). `agent="local"` forces the harness worker.
  `quartermaster` and `scribe` always stay local — their work is the
  vault's own tools.
- Reports come back like any subagent's and list the files changed.
  `/agent stop [id]` cancels a running worker; `delegate(resume="<id>")`
  sends a follow-up charge (a live worker is reused; after a restart the
  stored ACP session is reloaded — nothing respawns on its own).
- Headless: `dave run "/agent claude <charge>"`. `--approve agent`
  pre-approves only the dispatch card — the agent's own file and terminal
  asks still deny, so unattended runs stay read-only unless you also pass
  `--approve allow`.

What the harness enforces for an external charge:

- Its file writes, reads and terminals go through your permission rules
  and cards. "Allow always" stays a harness grant — the agent keeps
  asking. ACP-mediated writes are checkpointed, so `/rewind` undoes them.
  `git push`, the vault, `.hey-dave/` and the agents' credential
  stores (`~/.claude/.credentials.json`, `~/.config/devin/`) are
  hard-denied, reads included.
- Only safe modes are ever selected (`default` for Claude, `ask` for
  Devin; `plan` for read-only roles) — bypass modes are refused, and an
  agent-initiated switch to one is put back.
- **Claude is a bypass provider**: its SDK edits files and runs commands
  itself, so only its permission requests reach the harness — and allow
  rules in your own `~/.claude/settings.json` can approve its actions
  without a card. The dispatch card warns, and the report lists what
  changed outside the harness's sight (a git diff), marked as not covered
  by `/rewind`. `plan` mode is the stricter lever.

Policy links (checked 2026-09-26 — they change, so `dave doctor` prints
the date too):

- Claude Code: [legal and compliance](https://code.claude.com/docs/en/legal-and-compliance),
  [Agent SDK overview](https://code.claude.com/docs/en/agent-sdk/overview),
  [use the Agent SDK with your Claude plan](https://support.claude.com/en/articles/15036540).
- Devin: [ACP docs](https://docs.devin.ai/desktop/acp),
  [Platform Terms of Service](https://cognition.com/legal/platform-terms-of-service),
  [Acceptable Use Policy](https://cognition.com/legal/acceptable-use-policy).

Other ACP agents can be added by config alone
(`agents.providers.<id>.command`). Details: spec 03 *External agents*.

## Extending

- **Skills** — markdown `SKILL.md` under `skills.roots` (default
  `~/.config/hey-dave/skills`, `~/.agents/skills`,
  `./.hey-dave/skills`); slash commands and hooks too.
  `dave skills list`. Command files can declare `usage`, `help` and
  `examples` frontmatter so `--help` and argument checks work for them.
  [docs/spec/04](https://github.com/thesawdawg/hey-dave/blob/master/docs/spec/04-skills-commands-hooks.md).
- **Bundled skill: archify** — ask D.A.V.E. for an architecture, workflow,
  sequence, dataflow or state diagram and it writes a validated, interactive
  HTML page (pan, zoom, search, light and dark, exports). It ships with the
  harness and needs Node.js 18+ on `PATH`; without it the skill is hidden and
  `dave doctor` says so. Your own `archify` in a skills root replaces it;
  `skills.disabled: ["archify"]` turns it off. MIT, from
  [tt-a1i/archify](https://github.com/tt-a1i/archify), pinned to the harness
  release with no update check.
- **Plugins** — Python entry points (`hey_dave.plugins`) or built-ins;
  `dave plugins list`. [docs/spec/05-plugins.md](https://github.com/thesawdawg/hey-dave/blob/master/docs/spec/05-plugins.md).
- **MCP** — paste a Claude-Desktop `mcpServers` block unchanged; tools appear
  as `mcp__<server>__<tool>`. [docs/spec/05](https://github.com/thesawdawg/hey-dave/blob/master/docs/spec/05-plugins.md).
- **Trackers** — `local` (file-backed) or `redmine_mcp` (Redmine over MCP);
  outward writes are always proposed and approved individually
  (`/tracker` panel). `dave tracker list`. [docs/spec/05](https://github.com/thesawdawg/hey-dave/blob/master/docs/spec/05-plugins.md).
- **Web search** — `searxng` or `brave` backends; `dave search "query"`.
- **Web pages** — `web_fetch` reads a page as markdown: the main content,
  with headings, code and links, minus menus, footers and scripts.
  - Each new host asks first (`GET <url>` on the card). Allow a host for
    good with `network(web_fetch:docs.python.org)`, or every host with
    `network(web_fetch:*)`.
  - Internal addresses (localhost, `10.x`, `192.168.x`, cloud metadata)
    are refused, even after a redirect. Set
    `plugins.web_fetch.allow_private: true` for intranet docs.
  - Page text is marked untrusted, and D.A.V.E. is told never to follow
    instructions in it.
  - Only HTML, text, markdown, CSV and JSON are read; PDFs and images are
    refused. Limits are under `plugins.web_fetch` (20 s, 2 MB, 20,000
    characters). It works in plan mode. Turn it off with
    `plugins.disabled: ["web_fetch"]`.

### Using your Claude Code plugins

hey-dave can read Claude Code plugin directories — nothing runs until you
enable one, and foreign code never runs inside the harness's own process.

- `dave plugins list` shows what was detected: `plugins.sources` entries
  always, plus `~/.claude/plugins/` marketplace installs and this project's
  own `.claude-plugin/` (set `plugins.detect: false` to scan only named
  entries). Each source starts as `available`.
- `dave plugins enable claude:<name>` enables it (`disable` takes it back),
  or add `{"format": "claude", "path": "…", "enabled": true}` to
  `plugins.sources` yourself. `/plugins` shows the same list in the TUI.
- What then works: the plugin's skills, slash commands (`/<plugin>:<name>`),
  hooks (`SessionStart`, `PreToolUse`, `PostToolUse`, `Stop`) and MCP servers
  (`mcp__<plugin>-<server>__<tool>`). Anything the format has that the
  harness cannot run — unknown hook events, LSP servers, output styles,
  themes — is listed as `not run`/`unsupported`, never an error.
- Hooks can only tighten: `deny` refuses the call, `ask` forces the
  approval card, and `allow` is ignored — the permission gate decides.
- Agents become `delegate` roles named `<plugin>:<name>` that start
  read-only — no shell, no writes — until you bound them with
  `dave.roster.<plugin>:<name>`.
- `dave plugins install <pkg>@<ver>` (or `--latest-stable`, which takes the
  newest release at least 7 days old) installs an MCP server's npm package
  into the harness's own prefix, with install scripts off by default —
  nothing is ever installed automatically.

### Using your Pi extensions

[Pi](https://pi.dev) extensions are TypeScript — hey-dave hosts them in
a small vendored Node shim, one process per extension, so nothing foreign
runs inside the harness itself. Needs Node 22+ on `PATH` (or installed into
the npm prefix).

- `dave plugins list` finds `~/.pi/agent/extensions/`, this project's
  `.pi/extensions/`, `~/.pi/agent/skills/`, `.pi/skills/`, and `mcpServers`
  in `~/.pi/agent/settings.json`. Without Node 22+ an extension shows
  `unavailable: needs node ≥22`.
- `dave plugins enable pi:<name>` loads the extension once to check it —
  that's the moment its code first runs — and refuses extensions that
  register no tool.
- What then works: the extension's **tools**, arriving as
  `mcp__pi_<ext>__<tool>` under the normal MCP permission kind (plan mode
  blocks them); skills roots and `settings.json` MCP servers import as
  `pi:skills`/`pi:project-skills`/`pi:mcp`.
- What is listed but never run: everything else the extension registered —
  events (`on:<event>`, including `tool_call`), commands, shortcuts,
  flags, providers, renderers, messages, session entries and `ctx.ui`. The
  shim records them as `unsupported` rather than emulating them.
- A missing dependency shows `cannot find module '<x>'`; when the
  extension ships a `package.json`, `dave plugins install --from pi:<name>`
  runs `npm install` in its directory (scripts off by default).

## D.A.V.E. state

The vault lives at `$DAVE_VAULT`, else `dave.vault_path`, else
`~/.local/share/hey-dave/vault/` — an append-only JSONL journal plus
derived views, written only by `hey_dave.dave.state`. Inspect it without
starting a session:

```bash
dave brief       dave today       dave focus show
dave mission list   dave review   dave vault doctor
```

Goals and tasks are durable vault items. `/focus new annual|deadline|daily
<title>` creates and focuses one. `/sidequest <title>` captures an unplanned
Daily Task for today without changing focus or Now; it accepts `--priority`
and `--due`. Use `/item` to list, add, inspect, edit, link, complete, reopen,
reschedule, backlog or skip items, `/item now <ref>` to propose one for Now,
and `/item plan` to see goals Now does not serve yet. The CLI equivalents are
`dave focus new`, `dave sidequest`, and `dave item`. Annual Goals have a
calendar year, Deadline Goals have a due date, and Daily Tasks have a work
day.

Now stays the one commitment list. A task is on plan when it or a goal it
supports is in the ranked list; anything else, a Daily Task included, counts
as drift. Priority expresses importance and never promotes anything: an
Urgent item outside Now makes D.A.V.E. ask whether it belongs there, through
the usual diff. The sidebar's **Goals** section shows each open Annual Goal's
progress and how long since it last moved; **Today** carries unfinished tasks
forward, marks which goal each serves or that it was a sidequest, and holds
`priorities.max_today` before D.A.V.E. asks what to move. The weekly review
sets planned work against sidequests, and on `review.plan_day` D.A.V.E. offers
to pick the week's goals for Now. Dates never complete items automatically,
and classifying an old focus ref is offered once, never required.

D.A.V.E.'s own settings live in the vault's `config.json` and are set with
`dave config set dave.vault.<key> <value>` (or `/init vibe`, or by asking
D.A.V.E.). Each one takes effect:

- `user.timezone` (an IANA name such as `Europe/Berlin`) decides what "today"
  is for the journal, promises, the brief and the review; empty uses the
  machine's zone. `user.work_hours` (`09:00-17:00`) makes the brief say when
  you are working outside them.
- `priorities.max_now` caps Now: D.A.V.E. cannot grow it past the cap, and
  the sidebar shows that many. `priorities.ranking_factors` is what it ranks
  by, in order.
- `review.day` is the weekday the brief calls the weekly review due.
- `priorities.max_today` caps Today (5): a task over it is still saved, and
  D.A.V.E. asks what to reschedule. `review.plan_day` is the weekday it asks
  which goals this week's Now serves; `review.deadline_horizon_days` is how
  far ahead Deadline Goals show in the brief.
- `kanban.boards` names your intake boards; once the last intake is
  `kanban.stale_after_days` old, the brief says the boards may be stale.

### Make D.A.V.E. your own

**Its words.** Every prompt D.A.V.E. runs on (`dave.md`, `persona.md`,
`first-run.md`, `supervisor.md`, the six references), the ten role
contracts and its slash commands can be replaced or added to from the
vault, so your versions sync with it:

```bash
dave prompts list                    # every file, and whether yours replaces or adds to it
dave prompts copy persona            # puts persona.md in <vault>/harness/prompts/ to edit
dave prompts copy roles/critic --append   # starts roles/critic.append.md: added after the packaged text
dave prompts show roles/critic       # the text in use
```

`<name>.md` replaces the packaged file; `<name>.append.md` is added after it,
so upgrades to the packaged text still reach you. A new file in
`harness/commands/` adds a command. D.A.V.E. can't edit these files: the
vault is off limits to its write tools.

**Its helpers' limits.** Each role's tools, shell commands, write paths and
`dave_*` actions can be replaced in the config:

```jsonc
"dave": { "roster": {
  "critic": { "bash_allow": ["git diff*", "uv run pytest*"] },
  "scout":  { "tools": ["read", "glob", "grep"] }
} }
```

A role never gets `delegate`, or writes to priorities, promises, focus or
the config, whatever this says. When D.A.V.E. proposes one of these changes,
it shows as *widens access*, unticked.

**Its commands.** `"commands": {"disabled": ["standup", "music"]}` turns
slash commands off: they disappear from the list and `/help`, and typing
one says it is off. `/help`, `/quit`, `/keys`, `/dave`, `/errors` and
`/init` always stay.

### Sync across devices (Google Drive)

One D.A.V.E. on every machine: the vault stays a local folder that works
offline, and the harness mirrors it to a folder in your Google Drive. Each
device adds only to its own journal, so what you log on the laptop shows up
on the desktop. A file edited on two devices keeps both versions: Drive's in
place, yours beside it as `<name>.conflict-<device>-<time>.md`, and D.A.V.E.
mentions it until you merge it.

Setup, once per Google account, then `/vault login` on each device:

1. In [Google Cloud](https://console.cloud.google.com/apis/credentials),
   enable the **Google Drive API**. Set up the OAuth consent screen
   (External, add yourself) and **publish** it: while it says *Testing*,
   Google signs you out every 7 days.
2. Create an **OAuth client ID** of type **Desktop app**, then **Download
   JSON** (`client_secret_….json`).
3. In the TUI, `/vault login ~/Downloads/client_secret_….json`: it keeps the
   client on this device, turns sync on and opens the browser. Or `/vault
   config`: a setup screen with these steps (ctrl+o opens the console), a
   field for the file, and checks that change nothing; ctrl+l signs in with
   the screen open and shows how it goes, with a box for the redirect
   address if its page doesn't load. From a shell: `dave vault login
   --client-file ~/Downloads/client_secret_….json`.
4. `/vault sync` (it also syncs on its own from then on).

The client ID and secret stay on the device (the state directory, 0600,
never the config), so signing in again later, after a logout or when the
sign-in expires, is just `/vault login`: nothing to retype. `/vault` and the
setup screen say when a sign-in expired, or when Google says it will; `/vault
logout --forget-client` also deletes the saved client.

Use the same client on every device. The harness asks only for `drive.file`:
it sees the files it created, nothing else in your Drive. Tokens are kept in
the state directory (0600), never in the config or the vault. In the TUI it
syncs at start, shortly after D.A.V.E. writes, every 5 minutes and at exit
(`dave.sync.auto`, `interval_minutes`, `push_delay_seconds`). `/vault` shows
where it stands, and `dave vault sync --dry-run` shows what would move.
Two machines with the same hostname need `DAVE_DEVICE` set on one of them.
Deletions don't sync: remove a file on every device. See
[docs/spec/06-dave-state.md](https://github.com/thesawdawg/hey-dave/blob/master/docs/spec/06-dave-state.md) *Sync*.

## Docker

```bash
docker compose -f docker/compose.yaml run --rm dave            # TUI
docker compose -f docker/compose.yaml --profile search up -d   # SearXNG
docker compose -f docker/compose.yaml --profile ollama up -d   # Ollama
```

Volumes: `~/.config/hey-dave` → `/config` (create it first — Docker makes
missing bind sources root-owned), named `dave-data` → `/data` (the vault lives
here), `dave-state` → `/state`, the directory you run compose from (or
`$PROJECT_DIR`) → `/work`, and `~/.codex` read-write (Codex token refresh
writes back to the mounted file).

SearXNG needs a secret in the environment and refuses to start without one:

```bash
export SEARXNG_SECRET=$(python3 -c "import secrets; print(secrets.token_hex(32))")
docker compose -f docker/compose.yaml --profile search up -d
dave config set plugins.web_search.searxng.base_url http://searxng:8080   # inside compose
```

From the `dave` container SearXNG is `http://searxng:8080` — `localhost`
there is the harness container itself.
A host Ollama is reachable as `http://host.docker.internal:11434`; `dave
discover` inside Docker only scans the compose network. Non-interactive
provisioning: `dave init --non-interactive --from config.json`.
See [docs/spec/08-packaging-deploy.md](https://github.com/thesawdawg/hey-dave/blob/master/docs/spec/08-packaging-deploy.md).

## Development

```bash
bash scripts/validate.sh  # frozen sync, quality gate, clean installed-wheel smoke
```

Releases publish themselves: bump the version in `pyproject.toml` and
`src/hey_dave/__init__.py`, commit, then push a tag such as `v0.1.0a2`.
GitHub Actions runs the full gate, checks the tag matches the version, and
publishes to PyPI through trusted publishing. See
[spec 08](https://github.com/thesawdawg/hey-dave/blob/master/docs/spec/08-packaging-deploy.md)
for the one-time PyPI and GitHub setup.

Tests use temporary personal-state paths and block external sockets by default.
MCP and Spotify callback integrations are explicitly marked for loopback TCP.
See [the dependency policy](https://github.com/thesawdawg/hey-dave/blob/master/docs/dependency-policy.md) for update boundaries
and [CI validation](https://github.com/thesawdawg/hey-dave/blob/master/.github/workflows/validate.yml) for the shared gate.

Normative specs and the staged plan live under `docs/` — see
[docs/README.md](https://github.com/thesawdawg/hey-dave/blob/master/docs/README.md).
Interactive diagrams of the design (the system map, a turn, the permission
gate, delegation, persistence and the stage roadmap) are in
[docs/diagrams/](https://github.com/thesawdawg/hey-dave/blob/master/docs/diagrams/README.md); `bash scripts/diagrams.sh`
re-renders them from their JSON sources.

To try features by hand, `user-test-docs/setup.sh` builds a throwaway demo
project in `~/dave-demo` (a small calculator with a planted bug, test
images, and files that exercise `@` limits).
[user-test-docs/CHECKLIST.md](https://github.com/thesawdawg/hey-dave/blob/master/user-test-docs/CHECKLIST.md) lists the manual
checks, each with what you should see.

Resuming a session from another project asks before loading that project's
settings, skills and plugins. Headless resume requires starting in the saved
project. Changes to `dave.vault_path` take effect after restarting D.A.V.E.

`--continue` resumes this project's most recently active saved session.
Session listing reflects renamed titles and profile/model changes. An interrupted
final session write can be recovered; its damaged bytes are preserved in a
private `.corrupt` sidecar before the next write. Corruption in the middle
produces a file-and-line error while other sessions remain discoverable.

Streaming replies use batched Markdown rendering. Recursive searches skip
what your `.gitignore` ignores (or, without ripgrep and git, folders like
`node_modules` and `.venv`). They are cancellable and bounded: at most
10,000 files are searched, output stops at 1 MiB, and a content search has
60 seconds. A search that reaches a limit still returns what it found, with
a note to narrow the path or pattern. Deny rules hold for every file a
search opens. Allowing a search once covers the files it finds; files an
`ask` rule covers share one extra prompt. The session picker searches the newest
50 sessions per scope and reuses unchanged summaries between openings.

## License

hey-dave is free for noncommercial use under the
[PolyForm Noncommercial License 1.0.0](https://github.com/thesawdawg/hey-dave/blob/master/LICENSE.md): personal projects,
study, research, hobby use, and use by charities, schools, public research
and government bodies. Using it for a commercial purpose, including work for
a company, needs a separate license from the author. The bundled Archify
skill and the Pi shim's inlined dependencies keep their own licenses (MIT,
ISC, BSD-3-Clause, OFL-1.1); their texts ship with the package.
