Support herdr as a terminal workspace manager beside tmux #422

Open
opened 2026-08-19 19:59:00 +00:00 by mph · 0 comments
Owner

Problem

iw start, iw open and iw rm can only manage a terminal workspace with tmux. We now use herdr as our terminal workspace manager for agent work. In herdr, iw start creates a tmux session inside a herdr pane, which gives a nested, unusable workspace.

The tool must detect the terminal workspace manager and use it.

Scope

In scope: the worktree session lifecycle (create, exists, attach, focus, close, send command) behind one backend-neutral port, with a tmux adapter and a herdr adapter, selected automatically.

Out of scope (follow-up issues):

  • The herdr agent API (herdr agent prompt --wait --until idle) as a replacement for blind keystrokes. --prompt keeps send-keys semantics for now.
  • Worktree creation by herdr (herdr worktree create). iw keeps creating its worktrees with git.
  • Dashboard integration with the herdr socket API.

Design

1. Close the port leak first

SessionHooks.scala:52 and SessionHooks.scala:81 call TmuxAdapter.sendKeys directly and bypass the TmuxOps port. No backend swap is possible until the send-command operation is part of the port.

2. Generalise the port

TmuxOps (core/commands/CommandEnv.scala:373) becomes a backend-neutral port for "one named terminal workspace per worktree". The operation names must not name tmux.

3. Add a herdr adapter

The adapter speaks the herdr socket API through the herdr CLI and parses the JSON replies with upickle (already a dependency, core/project.scala:8).

Verified mapping:

port operation herdr command
sessionExists(name) herdr workspace list, match on label
createSession(name, dir) herdr workspace create --cwd DIR --label NAME --no-focus
sendKeys(name, cmd) herdr pane run <pane_id> <cmd> (sends the text and Enter)
killSession(name) herdr workspace close <id>
isInsideTmux env HERDR_ENV=1
currentSessionName env HERDR_WORKSPACE_ID, then herdr workspace get <id> for the label
switchSession(name) herdr workspace focus <id>
attachSession(name) herdr workspace focus <id>, then attach the session (NOT YET VERIFIED)

Herdr addresses every object by ID, not by label. The adapter must resolve label to workspace_id on each call. workspace create returns the workspace_id and the root_pane.pane_id together, so no second lookup is necessary on the create path.

4. Select the adapter

HERDR_ENV=1 selects herdr. If it is absent, tmux stays the backend. An explicit override must stay possible for the tests.

5. Doctor check

commands/start.hook-doctor.scala must check that a terminal workspace manager is available, not that tmux is available.

Findings from the live probe

Two probe workspaces were created and closed in a running herdr server.

  • The environment filter becomes dead weight. Tmux.scala:24-30 removes IW_* variables to prevent leakage into the session. A herdr pane inherits the environment of the herdr server, not of the calling process. The probe pane showed no IW_* variable except the globally set IW_HOME.
  • direnv already runs. The user shell hook loads .envrc in each new pane. The DirenvSetup hook stays necessary, but only for its direnv allow on a new worktree.
  • herdr worktree open needs a parent workspace. Without --workspace <parent repo workspace id> it fails with linked_worktree_source. With it, it succeeds and reports already_open. It gives the workspace real worktree metadata, which workspace create does not. Proposal: use worktree open when the parent repo workspace exists, and fall back to workspace create.

Acceptance

  • In a herdr session, iw start <KEY> creates the worktree and a herdr workspace with the session name as its label, runs the setup hooks in it, and focuses it.
  • In a herdr session, iw open <KEY> focuses the existing workspace, or creates it if it is absent.
  • In a herdr session, iw rm <KEY> closes the workspace and refuses to run from inside that same workspace.
  • With no herdr present, all three commands behave exactly as they do today with tmux.
  • iw doctor reports the workspace manager it found.

Tests

  • Unit tests for label-to-ID resolution over recorded JSON replies (pure functions).
  • A herdr contract test beside test/contract/tmux_adapter_contract.bats, skipped when herdr is not in PATH.
  • The existing FakeCommandEnv harness tests (StartHarnessTest, OpenHarnessTest, RmHarnessTest) carry over after the port rename.
## Problem `iw start`, `iw open` and `iw rm` can only manage a terminal workspace with tmux. We now use [herdr](https://herdr.dev) as our terminal workspace manager for agent work. In herdr, `iw start` creates a tmux session inside a herdr pane, which gives a nested, unusable workspace. The tool must detect the terminal workspace manager and use it. ## Scope In scope: the worktree session lifecycle (create, exists, attach, focus, close, send command) behind one backend-neutral port, with a tmux adapter and a herdr adapter, selected automatically. Out of scope (follow-up issues): - The herdr agent API (`herdr agent prompt --wait --until idle`) as a replacement for blind keystrokes. `--prompt` keeps send-keys semantics for now. - Worktree creation by herdr (`herdr worktree create`). `iw` keeps creating its worktrees with git. - Dashboard integration with the herdr socket API. ## Design ### 1. Close the port leak first `SessionHooks.scala:52` and `SessionHooks.scala:81` call `TmuxAdapter.sendKeys` directly and bypass the `TmuxOps` port. No backend swap is possible until the send-command operation is part of the port. ### 2. Generalise the port `TmuxOps` (`core/commands/CommandEnv.scala:373`) becomes a backend-neutral port for "one named terminal workspace per worktree". The operation names must not name tmux. ### 3. Add a herdr adapter The adapter speaks the herdr socket API through the `herdr` CLI and parses the JSON replies with upickle (already a dependency, `core/project.scala:8`). Verified mapping: | port operation | herdr command | |---|---| | `sessionExists(name)` | `herdr workspace list`, match on `label` | | `createSession(name, dir)` | `herdr workspace create --cwd DIR --label NAME --no-focus` | | `sendKeys(name, cmd)` | `herdr pane run <pane_id> <cmd>` (sends the text and Enter) | | `killSession(name)` | `herdr workspace close <id>` | | `isInsideTmux` | env `HERDR_ENV=1` | | `currentSessionName` | env `HERDR_WORKSPACE_ID`, then `herdr workspace get <id>` for the label | | `switchSession(name)` | `herdr workspace focus <id>` | | `attachSession(name)` | `herdr workspace focus <id>`, then attach the session (NOT YET VERIFIED) | Herdr addresses every object by ID, not by label. The adapter must resolve label to `workspace_id` on each call. `workspace create` returns the `workspace_id` and the `root_pane.pane_id` together, so no second lookup is necessary on the create path. ### 4. Select the adapter `HERDR_ENV=1` selects herdr. If it is absent, tmux stays the backend. An explicit override must stay possible for the tests. ### 5. Doctor check `commands/start.hook-doctor.scala` must check that a terminal workspace manager is available, not that tmux is available. ## Findings from the live probe Two probe workspaces were created and closed in a running herdr server. - **The environment filter becomes dead weight.** `Tmux.scala:24-30` removes `IW_*` variables to prevent leakage into the session. A herdr pane inherits the environment of the herdr server, not of the calling process. The probe pane showed no `IW_*` variable except the globally set `IW_HOME`. - **direnv already runs.** The user shell hook loads `.envrc` in each new pane. The `DirenvSetup` hook stays necessary, but only for its `direnv allow` on a new worktree. - **`herdr worktree open` needs a parent workspace.** Without `--workspace <parent repo workspace id>` it fails with `linked_worktree_source`. With it, it succeeds and reports `already_open`. It gives the workspace real worktree metadata, which `workspace create` does not. Proposal: use `worktree open` when the parent repo workspace exists, and fall back to `workspace create`. ## Acceptance - In a herdr session, `iw start <KEY>` creates the worktree and a herdr workspace with the session name as its label, runs the setup hooks in it, and focuses it. - In a herdr session, `iw open <KEY>` focuses the existing workspace, or creates it if it is absent. - In a herdr session, `iw rm <KEY>` closes the workspace and refuses to run from inside that same workspace. - With no herdr present, all three commands behave exactly as they do today with tmux. - `iw doctor` reports the workspace manager it found. ## Tests - Unit tests for label-to-ID resolution over recorded JSON replies (pure functions). - A herdr contract test beside `test/contract/tmux_adapter_contract.bats`, skipped when `herdr` is not in PATH. - The existing `FakeCommandEnv` harness tests (`StartHarnessTest`, `OpenHarnessTest`, `RmHarnessTest`) carry over after the port rename.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
iterative-works/iw-cli#422
No description provided.