Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
015fa8a
herdr-setup: install the whole Herdr kit from devlaunch alone
blooop Sep 29, 2026
e6d6742
test: judge dl --herdr-setup from outside on a new machine
blooop Sep 29, 2026
e23ce28
docs: the Herdr kit dl --herdr-setup installs
blooop Sep 29, 2026
0e50c58
Make status.sh executable and name the pytest fixture for pylint
blooop Sep 29, 2026
1544700
fix: herdr hook dedupe kept the stale entry and dropped the current one
blooop Sep 30, 2026
146b988
fix: config could name a pane shell the same run failed to install
blooop Sep 30, 2026
5977405
herdr setup: do not run integration install on a chezmoi-managed sett…
blooop Sep 30, 2026
2369940
Stop herdr setup giving a default action a key the user already bound
blooop Sep 30, 2026
fa257db
Keep the agent-queue toggle off a prefix+a the user bound to a plain …
blooop Sep 30, 2026
90d27b5
test: pin that a foreign hook group with no hooks survives the merge
blooop Sep 30, 2026
559fb89
test: pin that a dry run writes no settings backup
blooop Sep 30, 2026
4146370
test: pin that a herdr file already on ~/.local/bin is left alone
blooop Sep 30, 2026
ae19975
test: pin that duplicate herdr SessionStart hooks are removed once, w…
blooop Sep 30, 2026
62341b9
test: pin that a second run over a user's own files changes nothing
blooop Sep 30, 2026
25f5862
ci: say what herdr_environment spawns and why it takes the plain form
blooop Sep 30, 2026
7539843
test: pin the kitty include appended to a kitty.conf with no final ne…
blooop Sep 30, 2026
3cafe65
herdr-setup: an unreadable settings.json no longer installs an unregi…
blooop Sep 30, 2026
8580956
herdr-setup: a chezmoi-managed settings.json that needs hooks removed…
blooop Sep 30, 2026
ce8de6c
herdr-setup: chezmoi instructions no longer print a ~ path that a lat…
blooop Sep 30, 2026
8fa6736
Merge remote-tracking branch 'origin/main' into feat/herdr-setup-kit
blooop Sep 30, 2026
2a602bd
fix(herdr-setup): a new-machine config did not resume agents on restore
blooop Sep 30, 2026
b2acc85
test(herdr_environment): the spawned dl keeps its coverage counters
blooop Sep 30, 2026
c7c2c6c
ci(rust-coverage): collect the herdr_environment suite
blooop Sep 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,9 @@ jobs:
# fence as the two above. `|| ec=$?` rather than `; ec=$?` because the
# `run:` block is `bash -e` and a bare `;` exits at the failing line, so
# the `cat` never runs and a timeout reports with an empty log -- see the
# backstop's comment below. `herdr_environment` spawns nothing and takes
# the plain form.
# backstop's comment below. `herdr_environment` runs a stub herdr and the
# agent-queue view.sh, but each exits before the test ends and nothing
# detaches, so it takes the plain form.
- name: Test dl herdr_editor
working-directory: rust
run: |
Expand Down Expand Up @@ -445,6 +446,7 @@ jobs:
"-p dl --test lifecycle" \
"-p dl --test launch" \
"-p dl --test migration" \
"-p dl --test herdr_environment" \
; do
echo "::group::cargo llvm-cov $suite"
timeout 900 cargo llvm-cov --no-report --locked $suite -- --test-threads=1
Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **`dl --herdr-setup` sets up a whole machine for Herdr, with no dotfiles.** Besides the
pane shell, it now installs a CPU, memory and disk segment for the tab bar and the
agent-queue plugin (linked, with its startup hook run once), merges devlaunch's keymap,
theme and sidebar into Herdr's config, installs herdr's Claude Code integration when it
is missing or outdated, adds a Stop hook that names a one-pane tab after Claude's
session title, writes herdr's skill, writes kitty's F-key fix, and links
`~/.local/bin/herdr` to `~/.pixi/bin/herdr`. Every file is compiled into `dl`, so the
conda package and the wheel carry it. Each step prints `changed`, `current`, `skipped`
or `failed`, and a second run writes nothing. `--dry-run` prints the plan and writes
nothing; `--no-claude` and `--no-kitty` leave those two alone.

### Changed

- **The Herdr config is merged, not only pointed at the pane shell.** A key you set is
never changed. Four keys that point at files setup installs are kept current:
`terminal.default_shell`, the `prefix+a` agent-queue binding, the `status.sh` tab-bar
segment, and the ` · herdr` end of `ui.window_title`. A missing or empty config gets
devlaunch's packaged config.
- **A Herdr config that chezmoi manages no longer stops `dl --herdr-setup`.** The config
step is skipped with the source file named, and the other steps run. A chezmoi that
cannot answer still fails the command.

## [0.58.0] - 2026-09-30

### Added
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,8 @@ instead. [docs/cli.md](docs/cli.md) has the full `--rm` contract, including whic
| `dl --reconcile` | Re-point workspaces whose recorded source folder went missing. Deletes nothing |
| `dl --purge` | Remove devlaunch's own workspaces and caches |
| `dl --install` | Install shell completions, and the `dl-herdr-shell` name a herdr pane opens through |
| `dl --herdr-setup` | Install and configure the Herdr pane shell |
| `dl --herdr-setup` | Set up Herdr for devlaunch on this machine: the pane shell, a merged Herdr config, the agent-queue plugin, the status segment, Claude Code's tab-title hook and herdr skill, and kitty's F-key fix. Safe to re-run |
| `dl --herdr-setup --dry-run` | Print every change setup would make, and write nothing. `--no-claude` and `--no-kitty` leave those two alone |
| `dl --herdr-env set KEY=VALUE` | Save a variable for new host shells in this Herdr workspace |
| `dl --herdr-env profile NAME` | Choose an existing Claude login for new panes |
| `dl --herdr-env show` | Show saved overrides; `unset KEY` removes an inherited variable and `clear` resets overrides |
Expand All @@ -329,9 +330,10 @@ instead. [docs/cli.md](docs/cli.md) has the full `--rm` contract, including whic

`--herdr-shell` is not a command to type. It is what herdr's `[terminal] default_shell` points at,
so that splitting a pane in an `aid` tab opens a terminal in that workspace's container instead of
on this host. `dl --herdr-setup` writes the script and updates Herdr config;
`dl --install` writes the script and prints the config line for a manual setup.
[docs/workspace-tools.md](docs/workspace-tools.md) has the rest.
on this host. `dl --herdr-setup` writes the script and points Herdr's config at it, along with the
rest of the Herdr kit. `dl --install` writes the script and prints the config line for a manual
setup. [docs/workspace-tools.md](docs/workspace-tools.md) has the rest, including what setup
changes and what it never touches.

`--prune`, `--reconcile` and `--purge` print their plan and ask first. `-y` skips the question,
and for `--prune` and `rm`, `--force` goes ahead despite work that is nowhere else.
Expand Down
98 changes: 94 additions & 4 deletions docs/workspace-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -1515,6 +1515,95 @@ as some reboot orders do, leaves nothing to restore, and `aid resume` is still t
back. A `dl <ws> -- claude ...` typed by hand tells herdr nothing, because only `aid`
knows which session its line opens.

## Setting up Herdr on a new machine

```bash
dl --herdr-setup --dry-run # see every change first
dl --herdr-setup
herdr server reload-config # only if a Herdr session is already running
```

One command gives a machine the whole Herdr setup devlaunch is built around, with
no dotfiles. devlaunch ships as a conda package and a wheel that hold its binaries
and nothing else, so every file below is compiled into `dl` and written out by
setup. Each step reports `changed`, `current`, `skipped` or `failed`, with the
reason. A step writes a file only when its content differs, so a second run
reports every step `current` and writes nothing. A failed step makes the command
exit non-zero; the other steps still run.

| Step | What it writes |
|---|---|
| pane shell | `~/.local/bin/dl-herdr-shell`, [below](#opening-a-new-pane-in-the-workspace-its-tab-already-holds) |
| status segment | `$XDG_DATA_HOME/devlaunch/herdr/status.sh`: CPU, memory and disk for the tab bar |
| agent-queue plugin | `$XDG_DATA_HOME/devlaunch/herdr/plugins/agent-queue/`, then `herdr plugin link` and the plugin's startup hook, which linking does not fire |
| herdr config | a merge into Herdr's config, described next |
| herdr on `~/.local/bin` | a link to `~/.pixi/bin/herdr`, only when that exists and `~/.local/bin/herdr` does not, so `herdr machine add` does not push a stale copy |
| claude integration | `herdr integration install claude`, only when `herdr integration status` says it is missing or outdated |
| claude settings | a Stop hook entry in Claude Code's `settings.json` |
| claude tab-title hook | `hooks/devlaunch-herdr-tab-title.sh`, which names a one-pane tab after Claude's session title and stops once a person renames the tab |
| claude herdr skill | `skills/herdr/SKILL.md`, from `herdr --skill` |
| kitty F-key fix | `devlaunch-herdr.conf` in kitty's config directory, and an `include` line for it in `kitty.conf` |

`$XDG_DATA_HOME` is `~/.local/share` when it is unset. The Claude steps use
`$CLAUDE_CONFIG_DIR` when it is set, otherwise `~/.claude`, which is the directory
herdr's own installer uses. They are skipped when that directory does not exist,
or with `--no-claude`. The kitty steps use `$KITTY_CONFIG_DIRECTORY`, otherwise
`$XDG_CONFIG_HOME/kitty`, and are skipped when it does not exist, or with
`--no-kitty`. Setup never starts, stops or reloads a Herdr server.

### What the config merge changes, and what it never does

A missing or empty config gets devlaunch's packaged config: a keymap with bare
F-keys and Ctrl chords beside Herdr's prefix layer, `ctrl+space` as the prefix, a
white text theme, the agent queue on F7 and `prefix+a`, desktop toasts, and
[agents resumed on restore](#coming-back-after-herdr-restarts).

An existing config is merged, with comments and order kept. Two kinds of key:

- **Managed keys** point at files setup installs, so they are set on every run:
`terminal.default_shell`, the `prefix+a` binding to `local.agent-queue.toggle`,
the `status.sh` segment in `ui.tab_bar_right`, and the ` · herdr` end of
`ui.window_title`. Each stops at a value that is plainly yours. A custom
`default_shell` fails the step. A `status.sh` of your own keeps its segment. A
`prefix+a` bound to another action or command stays bound, and the toggle gets no key.
- **Default keys** are everything else in the packaged config. Each is added only
when the key is absent. A value you set is never changed, even one that matches
Herdr's own default.

Tables that setup adds go after your own. The file is written by a rename from a
temporary file in the same directory, and keeps its mode.

### Claude Code's settings.json

Claude Code rewrites `settings.json` itself, and other tools add hooks to it, so
the merge owns two things. It keeps exactly one Stop hook that runs
`devlaunch-herdr-tab-title.sh`. It also keeps one copy of herdr's SessionStart hook
(`herdr-agent-state.sh`), because older herdr releases appended another copy on
every install. Every other key, event and hook is left as it is, in its order. A
file that needs no change is not rewritten. Before its first change, setup copies
the file to `settings.json.devlaunch-backup`, and it never replaces that copy.

When a `herdr-tab-title.sh` Stop hook is already registered, for example from
dotfiles, setup adds neither its entry nor its script, so two hooks do not rename
the same tab.

The skill is written only when `herdr --skill` succeeds and prints something, so a
herdr that dropped the flag cannot replace a working skill with an error message.
A `skills/herdr` that is a symlink stays one, and the file is written through it.

### Files chezmoi manages

Before it edits Herdr's config, `settings.json` or `kitty.conf`, setup asks chezmoi
whether it manages the file. A managed file is left alone, and the step prints the
line to add to its chezmoi source instead. Files that only devlaunch writes, such as
the plugin and `devlaunch-herdr.conf`, are written either way.

### DEVLAUNCH_HERDR stays off

Setup ends by naming [`DEVLAUNCH_HERDR`](#reporting-an-agent-started-inside-the-workspace),
and does not set it. It lends herdr into every container and opens a second ssh
connection per launch, so it stays a choice you make.

## Opening a new pane in the workspace its tab already holds

The two sections above are about *watching* a pane. This one is about working in
Expand Down Expand Up @@ -1611,8 +1700,9 @@ existence rather than its container.
`dl --install` writes the script and leaves the config alone. `dl --herdr-setup`
uses `$HERDR_CONFIG_PATH` when set, otherwise `$XDG_CONFIG_HOME/herdr/config.toml`, or
`~/.config/herdr/config.toml` when XDG_CONFIG_HOME is unset. If chezmoi manages a
regular config file, setup refuses the edit and names the source file. Change
`terminal.default_shell` in that source, apply it, and run `dl --install` to install
regular config file, setup leaves it alone, reports the config step as skipped and
names the source file, and goes on with the other steps. Change
`terminal.default_shell` in that source and apply it; setup has already installed
the launcher. A symlink is different: setup preserves the link and updates its
target.

Expand All @@ -1627,8 +1717,8 @@ itself report every file as not managed, and nothing downstream can tell that ap
from the truth.

Re-running setup preserves an already current file. A custom `default_shell`
is refused with the manual replacement instruction, rather than silently replacing
the user's launcher.
fails the config step with the manual replacement instruction, rather than silently
replacing the user's launcher, and the config is left as it was.

### Workspace environments and Claude logins

Expand Down
2 changes: 1 addition & 1 deletion rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion rust/devlaunch-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ license.workspace = true
repository.workspace = true

[dependencies]
toml_edit = { workspace = true }
devlaunch-runner = { workspace = true }
libc = { workspace = true }
serde = { workspace = true }
Expand Down
2 changes: 1 addition & 1 deletion rust/devlaunch-core/completions/dl.bash
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ _dl_completion() {
# The retired spellings (--stop, --autorm) are absent by rule rather than by
# hand: the grammar marks them `hide = true`, and the test drops every hidden
# flag, so a spelling this build only still answers for is never offered.
local global_opts="--ls --install --refresh --prune --reconcile --purge --herdr-shell --herdr-setup --herdr-env --herdr-workspace --rm --devcontainer --claude-profile --claude-profiles --help -h --version"
local global_opts="--ls --install --refresh --prune --reconcile --purge --herdr-shell --herdr-setup --dry-run --no-claude --no-kitty --herdr-env --herdr-workspace --rm --devcontainer --claude-profile --claude-profiles --help -h --version"
if [[ "$cmd" == aid ]]; then
global_opts="--claude --codex --gemini --devcontainer --claude-profile --help -h --version"
fi
Expand Down
1 change: 0 additions & 1 deletion rust/devlaunch-core/public-api.rest.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1487,7 +1487,6 @@ pub fn devlaunch_core::flows::herdr_environment::Store::clear(&self) -> core::io
pub fn devlaunch_core::flows::herdr_environment::Store::new(&std::path::Path, &std::path::Path, &str) -> core::io::error::Result<Self>
pub fn devlaunch_core::flows::herdr_environment::Store::read(&self) -> core::io::error::Result<devlaunch_core::flows::herdr_environment::Environment>
pub fn devlaunch_core::flows::herdr_environment::Store::update(&self, impl core::ops::function::FnOnce(&mut devlaunch_core::flows::herdr_environment::Environment) -> core::io::error::Result<()>) -> core::io::error::Result<()>
pub fn devlaunch_core::flows::herdr_environment::configure(&std::path::Path, &std::path::Path, &std::path::Path) -> core::io::error::Result<bool>
pub fn devlaunch_core::flows::herdr_environment::profile_directory(&devlaunch_core::clients::ProfileName, &std::path::Path) -> core::io::error::Result<std::path::PathBuf>
pub mod devlaunch_core::flows::kept_copies
pub mod devlaunch_core::flows::kill
Expand Down
Loading
Loading