Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion docs/running.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ convoy --prompt-file prd.md --include-dirty

In interactive terminals, Convoy shows a full-screen OpenTUI dashboard headed by a compact run summary (clock, elapsed, cost, tokens). The `pipeline` panel on the left is a tab selector: every step — done, running, or still scheduled — is a row you move through with `↑`/`↓` (or `j`/`k`), or by clicking, with `▸` marking the focused one. Focusing a step drives the whole right side to it: a detail panel (name; whether it's ongoing, done, failed, or scheduled; model; cost; tokens; attempt; files changed) over that step's todo list and a three-tab content panel — switched with `←`/`→`, `Tab`, the number keys `1`/`2`/`3`, or by clicking the tab strip. The tabs are `logs` (the step's color-coded activity feed), `reports` (the markdown report that step wrote, if any, scrollable with `PgUp`/`PgDn` — available live the moment a step finishes, not only at the end), and `session` (a read-only "follow along" view of that step's OpenCode session: its live state — reasoning, running a command, editing, applying a diff — model, attempt, cost, diff summary, and a scrolling transcript of what the model is doing, newest at the bottom). A not-yet-started step reads as `scheduled` with its planned model and zeroed usage, so you can inspect what's coming; focus auto-follows the active step until you navigate, and `Esc` hands it back to auto-follow. The dashboard never paints backgrounds: the canvas is your terminal's own background and panels are delineated by borders alone, derived as subtle elevations of the terminal's reported background color, with dark or light accents picked by its brightness (and a neutral fallback when the terminal doesn't answer); floating modals repaint the reported color exactly to mask the content beneath them. It follows live theme changes. For full interactivity, press `o` (or click the detail panel) to open the focused step's OpenCode session in a new terminal window attached to Convoy's running OpenCode server; clicking a pipeline row only focuses that step — it no longer opens the session. Inside Herdr or Zellij that session opens in a sibling pane instead (see below); otherwise Ghostty is preferred when installed and Terminal.app is the fallback (`CONVOY_TERMINAL=herdr|zellij|ghostty|terminal` forces a backend). Press `Shift+Tab` to cycle auto-accept modes — off, auto-accept, smart (see the permission gate below). Press `Ctrl+C` once to abort the active OpenCode session and shut down Convoy cleanly; press it again to force-stop the owned OpenCode server (SIGKILL) and exit within a bounded second instead of leaving it behind. Human gates stay inside the dashboard (`c` continue · `o` open OpenCode · `a` abort); without a TTY dashboard they fall back to plain terminal prompts. A step that fails now waits for you instead of retrying: the dashboard shows a `step failed` gate with `r` retry clean (restore the baseline and run again), `o` open the OpenCode session and fix it by hand, `a` abort — no auto-retry, no lost work. Once you open the session (`o`), the gate becomes the interactive one and `c` unlocks; `c` delivers the step's report (including one written in the reopened session), and without any valid report it re-opens the gate instead of advancing to the next step. Use `--no-tui` to fall back to plain logs.

When Convoy runs inside Herdr or Zellij (including over SSH), `o` and `i` open OpenCode in a focused sibling pane rather than a macOS window, named for what it holds (`opencode session`, `opencode iterate`, `claude session`). Inside Herdr the pane splits the current one to the right; inside Zellij it is a new pane. The multiplexer's normal focus shortcut returns to Convoy without closing the pane. When OpenCode exits the pane deliberately stays, showing the exit code — so a session that failed to start is readable instead of vanishing; press `Ctrl+C` there to close the pane, or `Enter` to run it again. Set `CONVOY_TERMINAL=herdr`, `zellij`, `ghostty`, or `terminal` to override automatic backend selection — any other value is rejected with an error rather than silently ignored. When both multiplexers are detected, Herdr wins because the session runs inside it — and a failed Herdr open never falls through to Zellij, which would talk to the outer session and hang or open a pane you cannot see. If Convoy is inside a multiplexer but can't find its binary on its own `PATH`, it falls back to a macOS window rather than losing session opening altogether.
When Convoy runs inside Herdr or Zellij (including over SSH), `o` and `i` open OpenCode in a focused sibling pane rather than a macOS window, named for what it holds (`opencode session`, `opencode iterate`, `claude session`). Inside Herdr the pane splits the current one to the right; inside Zellij it is a new pane. The pane opens in your own login shell — with your prompt and configuration, so the client resolves the same `opencode` your shell would — and Convoy waits for that shell to settle before typing the command. The multiplexer's normal focus shortcut returns to Convoy without closing the pane. When OpenCode exits the pane deliberately stays, now back at your normal interactive shell, so a session that failed to start is readable instead of vanishing; press `Ctrl+C` there to close the pane, or `Enter` to run it again. Set `CONVOY_TERMINAL=herdr`, `zellij`, `ghostty`, or `terminal` to override automatic backend selection — any other value is rejected with an error rather than silently ignored. When both multiplexers are detected, Herdr wins because the session runs inside it — and a failed Herdr open never falls through to Zellij, which would talk to the outer session and hang or open a pane you cannot see. If Convoy is inside a multiplexer but can't find its binary on its own `PATH`, it falls back to a macOS window rather than losing session opening altogether.

Inside Herdr the sidebar agent is **Convoy** — the live pipeline name, the `N/M` step counter, and the current step label — not the underlying OpenCode session. A Herdr config can render those with the sidebar agents block:

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-17
56 changes: 56 additions & 0 deletions openspec/changes/archive/2026-09-17-fix-herdr-user-shell/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
## Context

See proposal.md for motivation and specs/work-conversations/spec.md for the target behavior. The relevant current state:

- All external clients (run-session `[o]`, iterate, claude-code session, authoring conversation) funnel through `openSessionCommand` → `openInHerdr` in `src/terminal-host.ts`.
- `herdr pane split` always launches an interactive login shell; it has no exec-a-command flag (verified against Herdr 0.9.0 `pane split --help`), so Convoy splits, waits for output, then types the command with `herdr pane run`.
- `openInHerdr` passes `--env PATH=<Convoy PATH>` and `--env ZDOTDIR=/var/empty`. The latter suppresses the operator's shell startup so the prompt appears immediately, shrinking the window before `pane run`.
- On macOS, the pane's login shell runs `/etc/zprofile` → `path_helper`, which can place `/opt/homebrew/bin` ahead of the operator's own PATH entries. The operator's startup files are what re-prepend their real toolchain (e.g. `~/.opencode/bin`); suppressing them lets a stale Homebrew `opencode` shadow the operator's working binary, which Herdr reports as `zsh: killed` (SIGKILL 137). This was reproduced: with `ZDOTDIR=/var/empty` the pane resolved `/opt/homebrew/bin/opencode` and died; without it, it resolved the operator's binary and succeeded.

## Goals / Non-Goals

**Goals:**

- The pane loads the operator's normal shell startup, so the launched client resolves as it does in the operator's shell and the pane returns to a normal interactive shell.
- The command is still submitted reliably once the shell is ready, despite startup output.

**Non-Goals:**

- Changing the Zellij, Ghostty, or Terminal.app backends: they already run the command through a non-interactive login shell or a normal terminal window and do not exhibit this problem.
- Adding a user-facing configuration toggle for pane shell behavior.
- Repairing a broken `opencode` binary on any operator's machine; Convoy only stops selecting the wrong one.
- Changing session references, persisted state, or harness protocol.

## Decisions

### D1: Remove the `ZDOTDIR=/var/empty` override

Drop the env pair from the Herdr split so the login/interactive shell loads the operator's configuration. This is the smallest change that restores both the correct executable resolution and the normal post-exit shell.

Alternatives considered:

- **Keep the fast shell and hand off afterwards** (`command; unset ZDOTDIR; exec "$SHELL" -l`): preserves startup speed but keeps a typed, shell-specific escape hatch, and it is visible in the pane. Rejected: clever for the wrong reason; the real cost of the override is correctness, not just the leftover shell.
- **Exec the command so the pane closes on exit**: removes the leftover shell but also removes the readable failure state that the pane intentionally keeps (a failed launch would vanish), and still does not fix PATH resolution under the suppressed shell.
- **Configuration toggle** (`minimal` vs `user` shell): lets the operator opt in, but ships a known-wrong default and adds a second code path to maintain.
- **Upstream Herdr `pane split -- <cmd>` exec support**: the structurally clean answer, but not available in 0.9.0; revisit if Herdr adds it.

### D2: Keep injecting working directory, PATH, and extra environment

Continue to pass `--cwd`, `--env PATH=…`, and per-open env (e.g. `OPENCODE_CONFIG_CONTENT`). The operator's startup may override PATH afterwards, which is exactly what we want; the injection remains the fallback when their startup does not set it.

### D3: Make pane readiness tolerant of real shell startup

The current wait matches the *first* output (`wait-output --regex .`), which was only a proxy for "the prompt appeared" while the startup was suppressed. With a real startup, a banner can match before the prompt and `pane run` could type too early. Replace it with a quiet-settled wait: poll the pane until its output revision stops changing for a short interval, bounded by a timeout, then submit the command; keep the existing retry loop as a safety net. If the timeout elapses the command is still attempted so a slow or silent startup cannot strand the pane.

Alternative considered: match a prompt pattern. Rejected: operator prompts are arbitrary and theme-dependent, so no pattern is reliable.

### D4: Scope the fix to the Herdr backend

Only `openInHerdr` changes. The window backends and Zellij keep their current command construction.

## Risks / Trade-offs

- **Slower pane open** because the operator's startup runs → the readiness wait is bounded and the value is correctness; accepted.
- **Operator startup prints banners or blocks** → bounded timeout still submits the command, and the retry loop covers a missed ready signal.
- **Behavior relied on for speed changes for everyone inside Herdr** → tests and `docs/running.md` are updated to describe the normal shell.
- **A stale/duplicate harness binary on PATH** is a machine condition Convoy cannot fix → the fix makes the pane match the operator's shell, which is the correct contract; operators with a broken Homebrew `opencode` should still reinstall or remove it.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Why

Convoy's Herdr pane backend forces `ZDOTDIR=/var/empty` on `herdr pane split` so the pane shell skips the operator's shell startup (`~/.zprofile`, `~/.zshrc`) and its prompt appears immediately, shrinking the window in which `herdr pane run` types the command before the shell is ready. That shortcut is observably wrong twice over. After the client exits, the pane is left in a bare, unconfigured shell instead of the operator's normal one. And because the skipped startup files are also what re-establish the operator's PATH ordering, the pane can resolve `opencode` to a *different* binary than the operator's own shell does — on Herdr 0.9.0 this surfaces as `zsh: killed opencode` (SIGKILL 137) whenever a stale Homebrew `opencode` shadows the operator's `~/.opencode/bin/opencode`.

## What Changes

- Remove the `ZDOTDIR=/var/empty` override from the Herdr pane backend so the pane shell loads the operator's normal login/interactive startup.
- Keep injecting the working directory, PATH, and extra environment variables on the split; only the user-startup suppression is removed.
- Make the pane-ready wait robust to a real shell startup that can emit output (banners, prompt) before the command is safe to type.
- Update the Herdr tests that assert the removed env pair and the documentation describing the pane's post-exit shell.

## Capabilities

### New Capabilities

<!-- None: this corrects behavior of an existing capability's external presentation. -->

### Modified Capabilities

- `work-conversations`: external pane presentation must host the harness client inside the operator's normal shell environment instead of a suppressed shell, so the launched client resolves as it does in the operator's shell and the pane returns to a normal interactive shell once the client exits.

## Impact

- `src/terminal-host.ts` (`openInHerdr` split arguments; `waitForHerdrPanePrompt` readiness).
- `test/opencode.test.ts` (Herdr split / wait-output / pane-run expectations).
- `docs/running.md` (pane shell wording).
- No CLI surface, persisted state, or harness protocol change.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## ADDED Requirements

### Requirement: External pane presentation hosts the client in the operator's shell

When Convoy opens a harness client in a multiplexer pane, it SHALL host that client inside the operator's normal interactive shell, loading the shell's login and interactive startup configuration rather than suppressing it. The pane's requested working directory and any Convoy-supplied environment SHALL still be applied, but the operator's own startup configuration SHALL remain authoritative for environment resolution such as `PATH`, so a harness command resolves to the same executable it would in the operator's own shell. After the harness client exits, the pane SHALL present a normal interactive shell rather than an unconfigured one. Convoy SHALL submit the client command only once the shell is ready to receive it, so shell startup output cannot cause the command to be lost, duplicated, or run before the interactive shell is usable.

#### Scenario: Multiplexer pane loads the operator's shell startup

- **WHEN** Convoy opens a harness client in a supported multiplexer pane
- **THEN** the pane shell runs the operator's login and interactive startup configuration, with the requested working directory and Convoy-supplied environment applied

#### Scenario: Launched client resolves as in the operator's shell

- **WHEN** the operator's shell resolves the harness executable to a specific path
- **THEN** the pane resolves and runs the same executable instead of a different one that the operator's environment would not have selected

#### Scenario: Exiting the client leaves a normal interactive shell

- **WHEN** the harness client exits in the pane
- **THEN** the pane presents the operator's normal interactive shell, with the operator's prompt and configuration, rather than an unconfigured shell

#### Scenario: Command submission tolerates shell startup output

- **WHEN** the pane shell emits startup output before it becomes interactive
- **THEN** Convoy still submits the client command exactly once, after the shell is ready, without losing or duplicating it
17 changes: 17 additions & 0 deletions openspec/changes/archive/2026-09-17-fix-herdr-user-shell/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## 1. Herdr pane backend

- [x] 1.1 Remove the `--env ZDOTDIR=/var/empty` pair from the split arguments in `openInHerdr` and update its explanatory comment to say the pane loads the operator's shell (`src/terminal-host.ts`). Verify with `bun test test/opencode.test.ts` that the Herdr split assertion no longer expects `ZDOTDIR`.
- [x] 1.2 Replace the first-output wait (`waitForHerdrPanePrompt`) with a quiet-settled readiness wait bounded by a timeout, keeping the existing `pane run` retry loop (`src/terminal-host.ts`). Verify a unit test injects a spawn whose pane output settles after startup lines and asserts the command is submitted once.
- [x] 1.3 Confirm the split still injects working directory, `PATH`, and per-open env (e.g. `OPENCODE_CONFIG_CONTENT`) alongside the removed override. Verify the existing PATH and extra-env Herdr tests pass unchanged.

## 2. Tests

- [x] 2.1 Update the Herdr tests in `test/opencode.test.ts` that assert `ZDOTDIR=/var/empty` and the `--regex .` / `--timeout 1500` wait to match the new split arguments and readiness behavior. Verify `bun test test/opencode.test.ts` is green.
- [x] 2.2 Add a test that a command is not submitted before the pane settles and is submitted exactly once, covering the startup-output case. Verify the new test passes and fails against the old first-output behavior.
- [x] 2.3 Run `bun run typecheck` and the full `bun test` suite. Verify both pass.

## 3. Documentation and end-to-end verification

- [x] 3.1 Update `docs/running.md` (the pane sentence near the `o`/`i` behavior) so it describes the pane opening in the operator's normal shell rather than a suppressed one. Verify the wording matches the implemented behavior.
- [x] 3.2 Manually verify inside Herdr: open a client from Convoy (`[o]` / `[i]` / authoring conversation) and confirm the pane resolves the same `opencode` as the operator's shell, the client starts, and exiting it leaves the operator's normal interactive shell. Verify with `herdr pane read` that `which -a opencode` in the pane matches the operator's shell order.
- [x] 3.3 Run `openspec validate fix-herdr-user-shell --strict` and confirm the change validates. Verify the command reports the change as valid.
24 changes: 24 additions & 0 deletions openspec/specs/work-conversations/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,27 @@ When Convoy hands the terminal to a foreground harness client and later reclaims

- **WHEN** the foreground client cannot start, exits non-zero, or is interrupted
- **THEN** the handoff still clears the primary screen and restores input, echo, and rendering

### Requirement: External pane presentation hosts the client in the operator's shell

When Convoy opens a harness client in a multiplexer pane, it SHALL host that client inside the operator's normal interactive shell, loading the shell's login and interactive startup configuration rather than suppressing it. The pane's requested working directory and any Convoy-supplied environment SHALL still be applied, but the operator's own startup configuration SHALL remain authoritative for environment resolution such as `PATH`, so a harness command resolves to the same executable it would in the operator's own shell. After the harness client exits, the pane SHALL present a normal interactive shell rather than an unconfigured one. Convoy SHALL submit the client command only once the shell is ready to receive it, so shell startup output cannot cause the command to be lost, duplicated, or run before the interactive shell is usable.

#### Scenario: Multiplexer pane loads the operator's shell startup

- **WHEN** Convoy opens a harness client in a supported multiplexer pane
- **THEN** the pane shell runs the operator's login and interactive startup configuration, with the requested working directory and Convoy-supplied environment applied

#### Scenario: Launched client resolves as in the operator's shell

- **WHEN** the operator's shell resolves the harness executable to a specific path
- **THEN** the pane resolves and runs the same executable instead of a different one that the operator's environment would not have selected

#### Scenario: Exiting the client leaves a normal interactive shell

- **WHEN** the harness client exits in the pane
- **THEN** the pane presents the operator's normal interactive shell, with the operator's prompt and configuration, rather than an unconfigured shell

#### Scenario: Command submission tolerates shell startup output

- **WHEN** the pane shell emits startup output before it becomes interactive
- **THEN** Convoy still submits the client command exactly once, after the shell is ready, without losing or duplicating it
Loading