Metadata-Version: 2.4
Name: hey-dave
Version: 0.2.0a3
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: markdown-it-py>=4.2,<5
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.

## Core redesign in progress

D.A.V.E. keeps your work as **Tasks**: persistent checklists with one level
of children, audited memory (where you stopped, the next action, decisions,
blockers), counted time, Projects, Goals and Promises
([spec 11](docs/spec/11-core-redesign.md), [spec 12](docs/spec/12-context-controls.md)).
You can be interrupted by an outage, capture it as a sidequest, switch, and
come back to the same checklist and memory — in the previous conversation or
a fresh one backed by Task memory.

`dave init` (or `dave vault init`) creates a fresh vault. An older schema-1
vault is never converted or changed: hey-dave refuses it and asks you to choose
an empty folder with `DAVE_VAULT=<dir>` or `dave config set dave.vault_path
<dir>`. In the TUI, `/task new <title>` opens editable attributes; `/task child
<id> <title>` adds a step; `/task review <id>` accepts the initial checklist.
`/task start <id>` offers a previous conversation or a new one with Task
memory. `/return` lists interrupted work. A busy switch offers wait or cancel
and keeps queued input with the old session. Headless use is
`dave run --task <id> [--subtask <id>] "prompt"` or `--general` (the default).
`--no-dave` remains pure coding and cannot be combined with those selections.

## Install

Python 3.14+ required, on Linux, macOS or WSL: the vault uses file locks, so on
native Windows `dave` says so and stops. 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 (on a fresh machine: setup first, see below)
dave --resume  # the TUI, starting with a list of sessions to pick up
dave run "summarise this repo"   # headless
```

**Bare `dave` on a fresh machine.** On a terminal with no global config file
and no vault, `dave` prints "no D.A.V.E. configuration found: starting setup",
runs `dave init --vibe`, and opens the TUI with the result. A setup you stop
(`stop here`, ctrl+c, exit 1) does not open the TUI, and the next `dave` asks
again. With either a config or a vault, with no terminal (Docker, CI,
scripts), with a subcommand, or with `dave --skip-setup`, the TUI opens as
before. `dave --resume` and `dave --continue` count as bare `dave`.

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 or Ctrl+J | send / new line (many terminals cannot tell Shift+Enter from Enter and send it; Ctrl+J always works); 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 |
| `/command ` | after a command and a space, the list shows what it takes next (`/config ` lists `theme`, `music`, `vault`…; `<placeholders>` show as hints) — Tab completes the word, Enter completes a word you typed or picked and otherwise runs the line |
| `@` | 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 selected Task and its minutes,
  checklist progress, 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": ["today", "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": { "goals": "Aims" } },
  "skins": { "matrix": { "rain": false } },
  "banner": false
}
```

- **`sidebar`:** which sections show, in what order, on which side, and
  whether it starts open. The D.A.V.E. panel sections (`dave`, `goals`,
  `today`, `coming_due`, `long_term`, `promises`, `general`, `proposals`,
  `subagents`) scroll together as one panel. The old `now` still loads and
  shows nothing. `collapsed` lists sections folded to their headings: click a
  heading, or run `/sidebar collapse tasks`, `/sidebar expand all` and so on. A
  heading shows `▾` open and `▸` folded, with a count where it has one
  (`Tasks ▸ 3/5`, `Activity ▸ 41`), and the choice is saved.
- **`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.
- **`chat`:** how the transcript draws your messages, the model's replies
  and command output: a preset (`classic`, `bubbles`, `boxed`, `compact`,
  `spacious`, `minimal`) and options over it per role (frame, colours,
  alignment, width, padding, gap, a name label). `/config chat bubbles`
  switches and saves it; `/config chat user align left` changes one option.
  Colours follow the theme and text stays readable.
- **`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 the common commands by category; **`/help all`** lists every
  one, and **`/help <word>`** searches (`/help clock` finds `/time`).
  **`/<command> --help`** (or `/help <command>`) shows a command's details,
  subcommands and examples without running it. Typing `/` offers the common
  commands first.
- A command missing a required argument shows its usage instead of running.
- **`/clear`** starts a new, empty session and keeps the old one in
  `/resume`. It keeps your Task or General work, the model and the mode, and
  drops session-only permission grants and the queue. `/clear delete` also
  deletes the old session after you confirm; a conversation your vault
  archives is never erased, so that form refuses it. Not while a turn runs.
- **`/session refocus [title]`** (or `/refocus`) branches into a new session
  that starts from a short brief of this one, for a tangent that should not
  pollute the current work. The model writes the brief and adds the files
  touched and the Task checklist; you edit it in a card (`ctrl+s` starts,
  `esc` cancels with nothing changed). The current session stays saved, so
  `/resume` goes back to it. Not while a turn runs.
- **`/docs`** opens the documentation in your browser: the specs and
  architecture notes as searchable pages, the interactive diagrams and the
  animated walkthrough. It runs a small server on `127.0.0.1` only (the
  address is also written to the chat, for a terminal that cannot open a
  browser). `/docs diagrams` and `/docs walkthrough` open those, `/docs
  <words>` opens a search, and `/docs stop` ends the server, which also stops
  when you quit. The stage plans and reviews are not included. An install
  from PyPI carries the docs; a checkout serves its own `docs/`.
- **`/demo`** opens a recorded tour of every feature in your browser, one
  page per feature with each example's steps and the screen it produced. Every
  example ran for real in a throwaway sandbox, so nothing touches your vault,
  config or provider. Every command, usage form, key and theme has an example;
  the few steps that need a real account (Google, Telegram, npm) are marked
  *simulated* beside the real offline output, and three full-screen apps are
  listed as not shown. `/demo list` prints the pages in the chat, `/demo
  <feature>` opens one, `/demo <words>` searches the demos, and `/demo stop`
  ends the server. It shares the `/docs` server, and works even when an install
  carries no docs.
- **`/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.
- **`/config`** gathers the setup commands: `/config theme`, `chat`, `music`,
  `vault`, `remote`, `plugins`, `mcp`, `permissions` and `skills`, each with
  the arguments it always had (for example `/config vault sync`; `chat` is
  new, see `tui.chat` above). The old
  names still work but are no longer listed; `/dave`, `/init`, `/keys` and
  `/errors` stay as they are. Bare `/config` lists the areas, and what a
  remote chat may run is unchanged.
- **`/session`** gathers the session commands: `/session list`, `title`,
  `compact`, `export`, `copy` and `rewind` (for example `/session copy code`).
  `/sessions`, `/title`, `/compact`, `/export`, `/copy` and `/rewind` still
  work but are no longer listed. `list` and `copy` run during a turn, as
  before; `export`, `copy` and `rewind` stay in the terminal.
- `/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 "Command" 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),
  the selected Task, 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: `/task`, `/return`, `/general`, `/sidequest`, `/goal`,
  `/promise`, `/time`, `/memory`, `/notice`, `/brief`, `/check`, `/delegate`,
  `/project`, `/intake`, `/review`, `/standup`, `/supervise`. `/focus` and
  `/park` are aliases of `/task start` and `/sidequest`, and `/unfocused` of
  `/general`; aliases still work but are not listed in `/help` or completion
  (turning a command off in `commands.disabled` turns its aliases off too, and a
  grouped form such as `/session export` follows its old name `export`). The retired
  `/mission`, `/item` and `/priorities` say what replaced them.

`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, unless you raise `remote.permission_ceiling` (see [Remote](#remote)).

### 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 selected Task's checklist,
    ready for `/supervise`, after an exact review (shown only when a work
    Task is selected);
  - `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. (`/mode`
groups `/plan`, `/chat` and `/general` as `/mode plan`, `/mode chat` and
`/mode general`; bare `/mode` shows the plan and chat-only state, and the old
names still work.) 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`).

### General work and sidequests

`/general` offers previous General conversations or a new one. Each new
conversation has separate memory and time; restoring one reuses its subject.
General uses normal configured repository permissions. The repository startup
picker offers a Task, a new Task, or continuing General once per conversation.
`/unfocused` is a discoverability alias for General; `/unfocused off` opens return.

`/general promote [id]` reviews title, description, classification, dates and
Project links before turning the conversation into a standalone Task. Its ID,
memory, context settings, conversation history and interval IDs are retained;
its prior time moves out of General and counts once on the promoted Task.

`/sidequest [title]` offers **Save** and **Save and switch**. Project links suggest
standalone work; an explicit root suggests a child or a selected child's sibling.
The modal lets you review every shared Task attribute, multiple Project links,
and the one-level parent. `--project <id>`, `--parent <id>` and `--switch` supply
editable suggestions. Save keeps the current conversation and timer. A failed
switch retains the capture and restores the old selection and manual pause.
`/sidequest <title> --switch --carry` also writes a brief of the conversation
you leave, lets you edit it, and starts the new Task's session from it (cancel
the brief and the Task is still captured, and you stay put).

`/return` uses the standard checklist, memory and time recap, with previous
conversation or a fresh conversation backed by memory. Missing checkouts offer
an explicit local attachment or memory-only work. Memory-only conversations
exclude filesystem, shell and delegation tools regardless of context edits or
session grants. Switching to another checkout asks before loading its settings
and discards previous project-relative conversation grants.

Headless callers can use `dave general history`, `dave general show <id>`,
`dave general promote <id> --yes`, `dave sidequest`, and
`dave run --task <id> --memory-only <prompt>`. Promotion previews without `--yes`.

### 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`). With a work Task selected it is that
Task's persistent checklist (stable child IDs, kept across sessions);
without one it belongs to this session only. It hides while empty, works in
plan mode, and is switched off under `/supervise`, where the reviewed
checklist is 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.
- The screen paints before the vault opens. A checklist in the transcript shows
  what is loading (`opening the vault — checking history 412 of 1,564`,
  `preparing the session`, `starting plugins`, `syncing the vault`) and folds away
  when it is done; the composer unlocks once the session is ready, and the
  sidebar never makes the screen wait for a vault that something else is busy
  with.
- 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, unless you hand the session to the
background with [`/background`](#background-sessions).

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, grouped; `/help all` and `/help <word>` work too.
- Permission cards arrive with *Allow once* / *Deny* / *Deny + reason*.
  Whichever of the terminal or the chat answers first wins.

By default remote answers last one call: *Allow once* and *Deny*. Set
`remote.permission_ceiling` to `session`, `project` or `global` and a card adds
**More…** (how long, which rule, Allow or Deny) up to that ceiling; a deny at any scope
is always allowed, a rule for the whole tool needs `project` or wider, and *Allow
writes* on a delegate card needs `session` or wider. `remote.step_up: pin` makes each
such allow wait for a four-digit PIN shown in your terminal (or by `dave daemon pin`).
Raising the ceiling lets whoever controls the paired account add rules that last,
including a `bash` rule, which is code execution on this machine; the default stays
`once`, `dave doctor` warns when it is not, and a rule added this way is marked
`(remote)` in `/permissions`, noted when you next open the session, and removable only
in the terminal. `/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).

## Background sessions

**`/background`** hands the open session to a process of its own, so the agent
keeps working and keeps listening on [remote](#remote) after you close the
terminal. Come back later with **`dave attach`**.

```text
/background            hand over now (a turn in flight is stopped, as with ctrl+c,
                       and the background session carries on with a note about it)
/background soon       hand over when the step in hand has finished: nothing is cut off,
                       nothing new starts, then the background session carries on
/background wait       hand over when the turn, its subagents and the queue are done
dave background list   what is running, what finished, and what it last said
dave background logs -f   follow one without interrupting it
dave background watch [id]   look at it and talk to it without ending it (ctrl+q leaves)
dave attach [id]       stop it and resume the session here (--wait lets it finish first)
dave background stop <id>
```

**`dave background watch`** opens a small screen on a running background session:
its events as they happen, a box to send it a message or a `/command`, and the
approvals it is waiting on as cards you answer. The session keeps running when you
leave, and the daemon and the chat keep reaching it. Answers from here are
bounded by `remote.permission_ceiling` like the chat's; `dave attach` is how you
take the session back into the full terminal.

`/background soon` is the one in between: the model reply or tool calls running
finish, no further step starts, and the session carries on in the background with a
note that it was paused, not cut off. ctrl+c or `/background cancel` while it waits
keeps the session here; after `background.drain_seconds` (10 minutes) it hands over
as the bare form does.

What goes with the session: the model, mode and permissions it had, the
messages you had queued, and the remote bridge if it was on. Nobody is at the
keyboard there, so an approval card goes to your phone when remote is paired
and waits `background.ask_seconds` (15 minutes) before it is denied; with
remote off it is denied at once and the model is told why. The session ends
when its work is done (remote off), or after `background.idle_hours` of just
listening, or when you stop it. With nothing to run and remote off,
`/background` says so instead of starting an idle process. While a session is
in the background, `/resume` and `dave --resume` refuse it and point to
`dave attach`. Linux, macOS and WSL. Details:
[docs/spec/07-sessions-tui.md](https://github.com/thesawdawg/hey-dave/blob/master/docs/spec/07-sessions-tui.md#background-sessions-stage-80).

### The daemon

A background session that crashes stays crashed, and nothing starts after a
reboot. **`dave daemon`** is one optional process that watches them:

```text
dave daemon start        run it as a process of its own (no terminal)
dave daemon status       its sessions, restarts and what it did lately (--json)
dave daemon stop         stop it; the sessions keep running (--hosts also stops them)
dave daemon restart      stop and start; --hosts also renews each session onto this version
dave daemon logs -f      follow its log
dave daemon retry <id>   start a session it gave up on again
dave daemon run          run in the foreground (what a service unit calls)
dave daemon install      write a systemd user unit or launchd agent (--enable also turns it on)
dave daemon uninstall    turn it off and remove the file install wrote
```

A session whose process dies is started again with a note saying what was lost,
after 2 s, 4 s, 8 s…; more than `daemon.restart_max` (3) restarts in
`daemon.restart_window_minutes` (10) and it stops trying (`gave_up`). When the
daemon starts it takes over every running background session and, with
`daemon.autoresume: live`, starts the ones that were live a reboot ago (updated
within `background.keep_days`). With a daemon running `/background` hands the
session to it; `daemon.autostart` starts the daemon for you. A daemon-run
session does not expire when idle unless `daemon.idle_hours` says so. Nothing
starts the daemon but you.

With a daemon running it owns the Telegram bot: one poller for every session, so
several background sessions can be reached at once. `dave daemon pair` pairs your chat
(or `/remote pair` in a terminal), and from the chat `/sessions` lists the sessions,
`/switch <n>` chooses who hears you (the others arrive prefixed `[title]`, and their
approval cards stay answerable), `/new <task-id|folder> [prompt]` starts one,
`/stop` stops one and `/daemon` shows the daemon. `/new` is limited to Projects the
vault knows and the `daemon.roots` you list. Start the daemon before turning remote on in
a terminal (a terminal already polling the bot stops the daemon's channel; `dave daemon
channel restart` tries again). Linux, macOS and WSL ([spec 14](https://github.com/thesawdawg/hey-dave/blob/master/docs/spec/14-background-daemon.md)).

Telegram updates are acknowledged after durable acceptance and deduplicated by update
id. A queued prompt survives a host restart; unfinished deliveries retain their original
session even if you switch sessions in the chat. `remote.limits.delivery_attempts`
(default 3) bounds unanswered attempts. Rejected or interrupted deliveries get an honest
chat report; a failed report leaves the update pending, and held input retries.
Telegram retains updates for at most 24 hours. If a resumed poll no longer returns an unfinished update,
the saved destination gets an uncertainty report naming the session.

To have the daemon come back after a reboot or a logout, install it as a user service:

```bash
dave daemon install --enable         # systemd on Linux, launchd on macOS
loginctl enable-linger $USER         # Linux: start at boot, not only at login
```

`install` writes the unit (or plist) and, without `--enable`, only prints the command that
turns it on. The file carries `# managed by hey-dave`; a file without that line is never
overwritten or removed, and `dave daemon uninstall` removes only one that has it. The service
runs the same Python that installed it, restarts on failure, and gets `DAVE_VAULT`, your
config path override, the XDG folders and `PATH` from the shell you ran `install` in. The bot
token is **not** written: the unit names its variable (`remote.telegram.bot_token_env`) and reads
`~/.config/hey-dave/daemon.env` (mode 0600) if you create it, or you can run `systemctl --user
import-environment DAVE_TELEGRAM_TOKEN` (`launchctl setenv` on macOS). Sessions outlive the
daemon (stopping or restarting it leaves them running), and when the machine comes back the
daemon starts again every session that was live (`daemon.autoresume`). Under WSL without
systemd, or in a container, `install` says so; use `dave daemon start` there. `dave doctor`
reports whether the service is installed and enabled.

### The API

Scripts, dashboards and other front ends can reach the daemon's sessions over HTTP. It is
**off by default** (`daemon.api.enabled`), listens on `127.0.0.1:7421` only (plain HTTP, no
TLS, nothing leaves the machine unless you tunnel it) and needs a bearer token. Tokens are
made in the terminal, never over the API: the secret is printed once and only its hash is
kept.

```bash
dave daemon token new dashboard --scope read,prompt,approve   # prints the secret once
dave daemon token list                                        # ids, scopes, ceilings, last use
dave daemon token revoke dashboard                            # works on its next request
```

A token carries scopes: `read` (list, snapshot, events, cards), `prompt` (messages, commands,
cancel), `approve` (answer a card), `manage` (start and stop sessions) and `full` (ask for
whole events, tool output and diffs, instead of the summary chat shows). Card answers are
bound by the token's `--ceiling` (`once` unless you say more) and by `remote.permission_ceiling`,
whichever is lower, so the API can do no more than the paired chat can. Every call that
changes anything is written into the session as an `api_input` event. Requests with an
`Origin` header, a `Host` other than `127.0.0.1:<port>` or `localhost:<port>`, no token or a
revoked one are refused; bodies over `daemon.api.max_body_kib` (256) get 413, more than
`daemon.api.requests_per_second` (30) from one token 429, and at most `daemon.api.max_streams`
(8) event streams are open at once. Any process running as you can use it, as it can the
control sockets; treat a token like a key.

```bash
export TOKEN=dvt_...   # from `dave daemon token new`; $SESSION is an id from the list below
curl -s http://127.0.0.1:7421/v1/health
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:7421/v1/daemon
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:7421/v1/sessions
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:7421/v1/sessions/$SESSION
curl -s -H "Authorization: Bearer $TOKEN" -d '{"text": "run the tests"}' http://127.0.0.1:7421/v1/sessions/$SESSION/messages
curl -s -H "Authorization: Bearer $TOKEN" -d '{"name": "status"}' http://127.0.0.1:7421/v1/sessions/$SESSION/commands
curl -s -H "Authorization: Bearer $TOKEN" -X POST http://127.0.0.1:7421/v1/sessions/$SESSION/cancel
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:7421/v1/sessions/$SESSION/cards
curl -s -H "Authorization: Bearer $TOKEN" -d '{"action": "allow", "scope": "once"}' http://127.0.0.1:7421/v1/sessions/$SESSION/cards/$NONCE
curl -N -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:7421/v1/sessions/$SESSION/events?after=0"
curl -s -H "Authorization: Bearer $TOKEN" -d '{"folder": "~/code/hey-dave", "prompt": "fix the build"}' http://127.0.0.1:7421/v1/sessions
curl -s -H "Authorization: Bearer $TOKEN" -X DELETE http://127.0.0.1:7421/v1/sessions/$SESSION
```

Events are server-sent events: `id:` is the sequence number, so a client that reconnects
with `Last-Event-ID` (or `?after=N`) is sent exactly what it missed, or one `resync` event
with a snapshot when the session's ring (`daemon.api.replay_events`, 500) no longer reaches
back. Errors are `{"error": {"code", "message"}}`. The routes and what each needs are in
[spec 14](https://github.com/thesawdawg/hey-dave/blob/master/docs/spec/14-background-daemon.md#api).

## 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. The wizard
offers that live list in the backend's order, defaults to its first model (or your saved
one) and never names a model of its own; when the account cannot be asked it says the
list is the Codex CLI's cached one (with its age) or a built-in one that may be out of
date, and `dave doctor` and `/model` show the same source.

### 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/`: readable JSON/Markdown records for Tasks,
Projects, Goals, Promises, memory and portable conversations, changed only
through audited transactions by `hey_dave.dave.state`. Inspect it without
starting a session:

```bash
dave task list     dave brief      dave review     dave goal list
dave promise list  dave memory audit   dave time      dave vault check
```

Tasks are classified **Day** (scheduled for a date), **Coming Due** or **Long
Term**, each in an order you set (`/task reorder`, `dave queue`); D.A.V.E.
suggests but never reorders, reclassifies or reschedules on its own. Goals
count completed linked work once; Promises record who, what and when, and
completing a Task only suggests marking one Kept. Natural-pause notices
(`/notice`, `/check`) compare what you are doing with your queues.

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 Day scheduling, overdue Tasks, reviews and Promise dates; empty uses
  the machine's zone.
- `user.name`, `user.role`, `user.address_as`, `user.work_hours`
  (`09:00-17:00`) and `kanban.boards` (your intake board names) are in
  D.A.V.E.'s prompt; `personality.wit`/`pushback` set its voice.
- `time.idle_timeout_minutes` (5) ends a timed interval at your last activity;
  `memory.portable_conversations` decides whether new transcripts sync, and
  `transcript.segment_kib` (256) / `transcript.segment_records` (500) decide when
  a transcript's open segment is sealed and a new one starts.
- `memory.digest` (`auto`/`off`), `memory.digest_model`, `memory.digest_input_tokens`
  (8000), `memory.digest_output_tokens` (1000) and `memory.digest_timeout_seconds`
  (30) control the memory digest; `context.recap_tokens` (2000) bounds a Task recap.
- `projects.source_max_paths`/`source_max_bytes` bound Project source snapshots.

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

**Its words.** Every prompt D.A.V.E. runs on (`persona.md` and 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`, makes commitments (Promises) or changes 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. `"common": ["task", "goal"]` replaces the short list
bare `/help` (and completion's first rows) shows; it never changes what
runs.

### Sync across devices (Google Drive or a synced folder)

One D.A.V.E. on every machine: the vault stays a local folder that works
offline, and the harness shares it as complete, checksummed transactions,
either through a folder in your Google Drive or through any folder your own
tool already syncs. Edits to different fields merge; the same field changed
on two devices keeps your local version and both originals for review
(`/vault conflict`, `dave vault resolve`). `dave vault recover` (or `/vault recover`)
groups every open conflict by Task; a text file can be kept both ways (`--take both`) or
merged by hand through a draft with conflict markers (`dave vault merge`), a whole Task
settled at once (`dave vault resolve-all`), and a device that cannot sync cleanly (a vault
copied without its packs, restored from an older backup, or pointing at a different vault)
is diagnosed, fixed with `dave vault recover --fix` where nothing can be lost, or restarted
from the remote with its own copy kept aside (`--from-remote`). A repeated or interrupted sync
never duplicates Tasks or time, and timers, checkout paths and sign-ins stay
on each device. A new device joins from an empty folder.

**Google Drive.** On each device, `/vault login` (or `dave vault login`):
the harness ships its own Google client, so there is nothing to register. It
turns sync on and opens the browser; then `/vault sync` (it also syncs on its
own from then on). The harness asks only for `drive.file`: it sees the files
it created, nothing else in your Drive. Tokens stay in the state directory
(0600), never in the config or the vault. `/vault` and the setup screen
(`/vault config`) say when a sign-in expired, or when Google says it will;
renewing is another `/vault login`.

**Your own Google client** (a fork, a policy that blocks the shipped client,
or your own quota): in [Google Cloud](https://console.cloud.google.com/apis/credentials)
enable the **Google Drive API**, set up the OAuth consent screen and
**publish** it (while it says *Testing*, Google signs you out every 7 days),
create an **OAuth client ID** of type **Desktop app** and **Download JSON**.
Then `/vault login ~/Downloads/client_secret_….json` (or `dave vault login
--client-file …`, or `/vault config`, whose screen has these steps, ctrl+o to
open the console, and ctrl+l to sign in). The client stays on the device
(0600, never the config); `/vault logout --forget-client` deletes it. Use the
same client on every device.

**A synced folder, no sign-in.** Point the harness at a directory that
Google Drive for desktop, Syncthing, Dropbox or a share keeps in step:

```bash
dave config set dave.sync.backend folder
dave config set dave.sync.path "~/Google Drive/dave-vault"
dave vault sync
```

or fill the folder field in `/vault config`. Writes are atomic, so the tool
never uploads half a file; the path must not be the vault itself.

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.

`bash scripts/bundle-docs.sh` copies `docs/` (without `plan/`, `reviews/` and
`ideas/`) into the package for `/docs`; `scripts/check-wheel.sh` and the
publish workflow run it before building, and the copy is git-ignored.

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. `python3 user-test-docs/walkthrough/build.py` turns it into
a single-page walkthrough: a route through the sections, Pass/Fail/Skip with win and pain-point
flags and comments, section ratings, and a feedback report built from the answers.

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.

Schema-2 Task sessions use one timer owner per device. `/time` shows the total,
direct and child time, unresolved device overlaps, and provisional active time.
`/time pause` lasts until `/time resume`; five minutes without meaningful activity
pauses at the last activity and excludes the idle gap. Model/tool execution keeps
counting, while permission waits can idle. Crashes close at durable evidence
(heartbeats at most ten seconds apart). `/time adjust <interval-id>` records a
reasoned correction on the same interval; exported time creates a compensation
proposal. `/time resolve <task-id> <slice-id> <node-id>` reviews ambiguous
allocation before export. These controls require a fresh schema-2 vault.

**Agent time.** Your time is the one clock you hold on a device. What the agents did
without it (a `/background` session, a daemon-started one, a subagent) is shown beside it,
labelled and never added: `/status`, `/time show`, `/return`, `dave time show`, `/review`
and `/standup` print for a Task the agents' model and tool minutes (waits for your
approval are left out) and, when agents overlapped, how long they covered. It is recorded
in the conversation transcripts, so it follows a portable conversation and disappears with
an erased one; a conversation kept local-only shows none.

**When you last reviewed.** `/review` records that a weekly sweep was done, and `/review` and
`/brief` say when the last one was: `last review: Fri 19 Sep (9 days ago)`, or `no review yet`.
Any sweep counts, whatever its window. It is kept the same way as agent time, so a
conversation kept local-only leaves no mark. There is no "review due" any more, since the
old `review.day` setting went with the schema-1 vault.

### Portable Task memory and conversations

Schema-2 Task/child memory captures observed requests and labels model-reported
progress as unverified, kept as worklog records (`/memory` shows only the five
sections and the five newest entries, and a Task recap is cut to
`context.recap_tokens`, 2,000 by default). At a switch, a timer pause, the end of a
session and before a compaction D.A.V.E. also makes one visible call (`updating
memory…`) that updates *Where stopped*, *Next action*, *Decisions* and *Blockers*
from the work since the last one: every entry cites messages, tool results or
worklog entries that must exist, is labelled `observed` or `reported`, and never
rewrites a line you wrote or edited (a disagreement is added beside it as
`suggested`). A call that fails or hangs never holds up the switch, pause or exit;
it is retried at the next pause within the same complete request budget and model
context cap. Oversized fixed input is `pending: too large` and waits for memory,
checklist/recap or settings to change. Only a successful cited tool result is
`observed`; failed or unfinished calls remain `reported`. Each entry's stable sources
and tool outcomes are saved on its revision and shown in `/memory audit` and
`/memory show`, including an explicit reason when a source is unavailable. Turn it off with `memory.digest` = `off`, pick the
model with `memory.digest_model` (`provider/model`), and skip it for one headless
run with `dave run --no-digest`. Captures carry stable entry IDs and source evidence;
failed saves retain the transcript and last good memory, report pending status,
and retry without duplicate entries. `/memory [subject-id]` opens literal
Markdown; `/memory edit <subject-id>`, `/memory audit [subject-id]`, and
`/memory show <revision-id>` expose authored text and exact before/after changes.
`/memory revert <revision-id>` previews a memory-only correction and asks before
saving. (`/context memory …` is the same command, for example
`/context memory audit user`; `/memory` still works but is no longer listed.) Unrelated later entries survive; overlapping changes require review.
CLI equivalents are `dave memory view/edit/audit/show/revert --help`; edit accepts
`--file`, while revert previews until explicitly repeated with `--yes`.

If you edit a memory file (`memory.md`) in an editor, the next time D.A.V.E. opens
it keeps your edit as a revision you can see in `/memory audit` and revert. A
structured file (`task.json`, `project.json`, …) edited by hand is held back
instead, on its own: everything else carries on, the vault serves the last saved
version, and `dave vault status` names the file with the two ways out, `dave
vault adopt <path>` (keep your version, after checking it is valid) and `dave
vault restore <path>` (put the saved one back). Editing those files by hand is
still not a supported way to work.

The vault never deletes history, but it keeps the working set small. Past
`history.checkpoint_transactions` (2000) loose transactions or
`history.checkpoint_mib` (64) MiB of them, and only while you are idle, the
transactions are copied byte for byte into a compressed pack under
`history/packs/`, read back and compared, and a checkpoint records what every
file hashed to. Opening the vault then replays only what came after.
`dave vault checkpoint` does it now, `dave vault status` shows loose
transactions, packs and the checkpoint in use, and `dave vault check` reads every
pack back. Sync uploads the packs and checkpoints once; a copy that lost
`history/packs/` fetches them back on its next sync, and without them it still
opens but cannot show or restore the packed history.

To remove something for good, `dave vault erase task|conversation|attachment|memory-revision
<id>` (add `--dry-run` to see counts first). It takes the item out of the vault **and rewrites the
history that held its bytes**, so it cannot be read back or restored; a Task keeps its time
records as numbers with the text emptied. You type a confirmation, so it works only at a
terminal. With sync on, the next `dave vault sync` hands the erasure to the remote and to
every other device: each applies it before it reads anything else, an offline device
does so on its first sync, and the remote's copies of the erased history are rewritten or
deleted (Drive files are deleted for good, not moved to the trash). A backend that cannot
delete says so and the erasure stays local to your devices. Backups, a copy of the folder you
made yourself and Drive's own version history are not reached
([spec 15](https://github.com/thesawdawg/hey-dave/blob/master/docs/spec/15-erasure.md)).
`dave vault erasures` lists them, and the devices that have acknowledged each, without
showing what was erased.

Portable conversations default on (`memory.portable_conversations` in vault
config). The choice is fixed for each new conversation; switching it off keeps
existing archives and always retains Task memory. Copies of actual allowed image
inputs use relative content hashes. Missing, changed, denied, and oversized
attachments are labeled. Copied vaults restore text, tool results, images and
compaction summaries without executing old tools or copying permission grants.
Transcripts are stored as append-only segments, so a long conversation adds only
what you said to the vault's history and to sync, not the whole transcript each
time; a vault that holds segments names them in `vault.json`, and an older build
refuses it rather than misreading it. If two devices continue the same
conversation offline, both tails are kept and opening it says so.
`dave sessions import <session-id> --task <task-id>` explicitly imports a local
conversation; import counts no historical active time.

Register Git Projects with `dave project add`, including worktrees. Use `dave
project attach <project-id> <directory>` for an explicitly moved checkout,
`dave project sources <project-id>` to inspect provenance, `dave project refresh
<project-id>` to refresh, and `dave project source-register <project-id> <path>`
for additional documentation. README/AGENTS snapshots remain versioned; authored
Project memory stays separate. Source scans default to 5,000 eligible paths and
2 MiB inspected content (`projects.source_max_paths` / `source_max_bytes`),
respect exclusions/read rules, and label missing or truncated sources.

### Effective context and scoped references

Schema-2 requests and previews share a source catalog: exact system text, actual
tool schemas, hashes, controlling policy scope, unavailable sources and budget
truncation. Global → selected Task/child → session overrides apply independently
to source inclusion and prose. Memory and documents default to summary plus
reference with a 2,000-token summary budget; explicit full selections that cannot
fit produce actionable guidance. Excluded tools cannot dispatch, and child roles
retain their own limits.

`dave_ref` retrieves included subject memory, linked Project sources or packaged
guidance by scoped reference ID. It returns bounded content with provenance and
freshness; denied cached documents stay denied. Activation reloads actual allowed
bodies after resume or compaction. Editing injected text is separate from editing
authored memory. Setup and interactive source review are the next redesign stage.

### Review and edit context

Both `dave init` and `dave init --vibe` offer a source checklist before saving.
Vibe first interprets your answers; Back lets you revise the proposed settings
and refresh the preview. Portable conversations are on by default and can be
turned off while keeping portable memory. Cancel leaves installed settings alone.

With a schema-2 vault, `/context` opens the same checklist during work.
(`/status errors` shows the session's faults, like `/errors`.) Space
toggles a source; Include chooses its mode, Details shows all text and provenance,
and Edit text changes added prose. Apply reviews the diff. Scope defaults to
session; choose Task or global to keep broader defaults. Memory saves the
authored source separately. Permissions remain enforced independently.

```bash
dave context show global --text
dave context set global prompt.dave off
dave context edit global prompt.dave --file ./instructions.md
dave context reset global prompt.dave
```

Use `--task T…` for Task policy and `--session <id>` for an existing session.
A new conversation starts without old chat text; disabling a source does not
erase earlier messages.

### Linked outcomes and reviewed work

Schema-2 vaults keep Goals and Promises separate from Tasks. `/goal` and
`/promise` list records or open reviewed attribute/link forms. Goal progress
counts linked work once when both a Task and its child are linked. A completed
Task leaves its Goal and Promise statuses unchanged. Promise due-date changes
require a reason; model-proposed outcome writes always show their exact review.

```bash
dave goal new "Launch app" --task T-<uuid>
dave goal show G-<uuid>
dave promise new Alex 2026-10-10 "Send release notes" --task T-<uuid>
dave queue show day --on 2026-10-01
dave review --days 7 --json
dave standup 1
```

Replace the placeholder IDs with actual record IDs. `/task reorder` reviews a
complete classification order. Date views do not reschedule or reclassify work.
The sidebar shows Today, Coming Due, Long Term, Goals, Promises and General
history alongside worker reports and the selected Task's time/checklist.

Work notices appear at a pause above the composer. Choose Continue, Switch,
Snooze or Dismiss with the buttons or `/notice`. `/notice snooze 45` edits the
default thirty-minute delay. Dismiss applies to that category in the current
conversation, including resume on this device. Notices stay hidden while a turn
streams; due flags remain visible in queues.

Saving an approved plan to a selected Task reviews its description and steps
and preserves existing child IDs and statuses. `/supervise <task-id>` uses the
reviewed checklist and Task-scoped report verdicts. Background workers retain
their assigned Task and have no separate human timer. Explicitly waiting for
delegates counts on the selected session's clock; permission waits allow idle
pause, and manual pause remains in force.

For tracker time, pause the Task timer, then propose with `task_id` and an
explicit tracker `ref`; omit `hours` to derive durable unexported counted time.
Every outward apply still requires approval. Denied or expired drafts release
their reservation. An uncertain response keeps time reserved for reconciliation
so a retry cannot log the same interval again.

### Task controls without the TUI

`dave task new` accepts `--class`, `--on`, `--due`, repeatable `--project`,
and `--parent` for a one-level child. Omitted child choices copy the parent's
defaults; later parent edits leave child data intact. Each command has `--help`.

```bash
dave task new "Add push notifications" --class coming_due --due 2026-11-01
dave task new "Test delivery" --parent T-<uuid>
dave task review T-<uuid> --yes
dave task status T-<uuid> completed --yes
dave task move T-<child-uuid> T-<parent-uuid> --yes
dave time show T-<uuid>
dave time exports
```

Use actual IDs from `dave task list`. Parent completion requires review and
finished children. Completed work suggests reviewing its linked open Promises;
marking one Kept remains a separate decision. `dave task plan <id> --file plan.json`
previews JSON with `description` and `items` (`text`, `status`, optional existing
child `id` and `blocked_reason`); `--yes` saves prose and checklist together.
`archive` retains data and warns about active children; `restore` retains status.
`move` and `promote` retain node identity, memory, sessions and time history.

`/focus` selects work through standard resume choices, `/park` opens
sidequest review, `/brief` reads current facts, and `/check` inspects the
work notice. Mission, item and priorities commands give replacement guidance.
`/standup` uses Task review facts and deliberate follow-ups.

If a tracker wrote time but its response was uncertain, verify the entry ID in
the tracker, then use `dave time reconcile <export-id> <entry-id>` to preview and
repeat with `--yes` to save it. `/time reconcile` offers the same review in the
TUI. Reconciliation records evidence without sending another tracker write.
`dave time adjust <interval-id> <minutes> <reason> --yes` saves an unexported
correction; already-exported time retains a compensating proposal visible in
`time exports`. `tracker(action="ledger")` also reads these records. Draft each
replacement with `tracker(action="propose_correction", correction_id="TC-…",
export_id="E-…")`; the ordinary proposal panel shows the exact entry and old/new
hours for approval. Local time changes only after every affected entry has a
confirmed replacement. Denial/expiry releases an unstarted draft; uncertain
writes stay reserved across restart.

For an uncertain replacement, verify its amount in the tracker and use
`dave time reconcile TC-<uuid>/E-<uuid> <entry-id>` (preview, then `--yes`), or
`/time reconcile` in the TUI. `dave time cancel TC-<uuid> --yes` or `/time cancel`
can cancel an intent before any replacement begins. Added time beyond the
original exported ranges remains unexported for a separate reviewed proposal.
Local tracker replacements append an audit event referencing the original entry.
Redmine requires an explicit `tracker.redmine_mcp.tools.time_replace` mapping;
it receives `time_entry_id` and `hours`, and a structured entry identity confirms
success. Backends without replacement support leave the intent available for
review.

### Board intake in Task mode

`/intake sprint-board` opens a source editor. Capture preserves the original
text without creating work. `/intake propose <id>` lets you add/edit/remove
proposed Tasks through the standard forms, including classification, dates,
Projects and references. Save keeps the proposal; `/intake apply <id>` reviews
it before creating the Tasks together. New Tasks append to your manual queues.
An existing Task association leaves its fields intact; duplicate references
require an explicit match.

For headless intake, capture with `dave intake sprint-board < board.txt`, inspect
with `dave intake list` or `show <id>`, then save a JSON item array with
`dave intake propose <id> --file items.json`. For example:

```json
[{"title": "Add push notifications", "classification": "coming_due", "due_on": "2026-11-01"}]
```

`dave intake apply <id>` previews; repeat with `--yes` after review. Use actual
capture IDs from `intake list`. Capture and accepted associations stay in the
vault with their original input and audit history.

Task-mode Project tools now support assigned Project knowledge for delegated
workers. `dave project show` accepts an ID or slug; `link`/`unlink` associate a
Project with an existing Task ID or unique tracker ref. `of` returns all linked
Projects, and `resolve` respects nested repositories. `status active|archived`
controls visibility while Task status tracks work. Project links never choose
cwd or expand permissions. Model registration and source refresh respect the
current read rules; every model mutation requires review.

### Headless supervision and remote Task controls

`dave task sessions <id>` prints the standard recap and valid new/resume
arguments. Run `dave run --task <id> --supervise "Continue the reviewed plan"`
to supervise an eligible root with an accepted initial checklist. Supervision
only grants report verdict bookkeeping for that root; parent completion,
commitments and outward writes keep their own approvals. `dave run --resume
<session-id> "/supervise off"` leaves that mode. Use `dave task assignments
[task-id]` to inspect charges/reports and `dave task verdict <assignment-id>
trust|partial|rerun|discard --summary "Evidence"` to record your judgment.
Host slash controls give headless CLI guidance and are never sent as model work.

From a paired remote chat, `/task list`, `/task show <id>`, `/task sessions <id>`
and `/return` inspect work. `/task start <id>` displays the recap and both
conversation choices. Explicit `new`, `memory`, or `resume [session-id]`
returns display an exact review with `/task confirm <token>`. Tokens expire
in two minutes, apply once, and refuse stale work or a changed conversation.
Same-workspace returns keep the bridge connected and preserve existing session
denials. Cross-workspace returns, restored broader grants, supervision setup
and attribute/checklist/grant modals require the terminal. `/supervise off`
remains remote. `/time show [id]` and `/time exports` inspect time; pause/resume
can control only the open conversation's timer. Goal/Promise list/show work remotely.

Remote board intake uses `/intake capture-text <source> <text>` (a one-word
source name), `/intake propose-json <id> <items-json>`, `/intake show <id>`, and
`/intake apply <id>`. Acceptance displays original input, standard Task fields
and queue effects; `/intake confirm <token>` accepts that exact review.
Capture/propose create no Tasks. Task state cards include their exact change
under summary sharing; tracker and delegate cards retain Allow once and
restricted worker choices. No remote approval adds a lasting grant.

Task-mode `dave dossier set/get [project-id-or-slug]` edits/reads audited Project
memory; an omitted identity resolves the current checkout. `dave project touch`
refreshes bounded source facts, preserving authored memory. `dave scan` reports
local Git/Task associations without fetching (`--all` includes archived Projects).
`dave project decline <path>` suppresses repository offers on this device only.
Project add supports active/archive; legacy slug/cadence/goal choices are refused
with Task/Goal guidance rather than silently ignored.

The [Stage 73 review guide](docs/plan/stage-73-review.md) provides a fresh-vault
walkthrough. Task-vault sync, fresh schema-2 defaults, scenario coverage and
package validation are implemented.

Remote reviews freeze all nonvolatile graph evidence. Ordinary elapsed/heartbeat
updates on an open interval do not invalidate them; interval closure, allocation,
correction, Task/memory/proposal edits and other graph changes still do. Acceptance
checks that evidence under the writer lock and uses the current CAS generation,
so clock progress does not weaken the atomic board acceptance guard.

### Task-vault sync and conflict review

Vaults use immutable transaction bundles with optional Google Drive
sync. Existing OAuth setup and `dave.sync` settings are retained. `dave vault sync`
uploads content objects and history before publishing a checksummed commit;
receivers verify complete causal bundles and apply through the shared writer.
Interrupted transfers retry, and durable receipts prevent duplicate import.
Credentials, checkout mappings, caches and timing ownership stay on the device.
Local-only use still needs no sign-in or sync call.

Use `dave vault sync --dry-run` to preview without changing authority. An empty
local path can join an existing schema-2 Drive vault; choose it with `DAVE_VAULT`
and run sync before initializing another vault there. Independently initialized
vault identities are refused rather than combined. Full-vault copies retain
history and import receipts; checkout/source mappings must be set for the device.

Disjoint structured fields and nonoverlapping Markdown edits merge after graph
validation. Competing fields, prose, tree changes and unsupported deletions leave
local records intact and preserve both versions for review. Promotion relocates
owned data; an old child path cannot silently return.

```bash
dave vault conflicts
dave vault conflict TX-<uuid>
dave vault resolve TX-<uuid> --take incoming
dave vault resolve TX-<uuid> --take incoming --yes
```

Resolution previews first; `--yes` accepts the displayed current replacement.
`--take local` retains current work and acknowledges the conflict. Original
evidence stays in history. In the terminal, `/vault conflicts`, `/vault conflict
<id>` and `/vault resolve <id> local|incoming` share the service; resolution opens
a literal confirmation modal and refuses changed evidence. Task recaps identify
related unresolved conflicts. Sync commands retain terminal-only origin bounds.
`dave vault recover` groups them by Task. `dave vault resolve <id> --take both` keeps both
sides of a text file one after the other; `dave vault merge <id>` writes a draft with
conflict markers to edit (`--apply`, then `--yes`, commits it; a draft with a marker left is
refused); `dave vault resolve-all --task <id> --take local|incoming|both` settles a whole Task
(preview first, `--yes` applies). `dave vault recover --fix` runs the one sync that settles
what loses nothing (changes not yet applied or published, packs the remote holds), and
`--from-remote --yes` moves this vault aside as `<vault>.aside-<time>` and joins the remote.
