Skip to content
Open
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
64 changes: 64 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# DECISIONS — herdr-annotate multi-machine

Started: 2026-09-18T08:17:43Z
Updated: 2026-09-18T20:58:00Z
Worktree: /Users/emo/.herdr/worktrees/herdr-annotate/annotate-clipboard
Branch: annotate-clipboard
Workspace: annotate-clipboard

## Constraints
- No merge tonight. Morning review by human owner.
- Own this worktree only (`/Users/emo/.herdr/worktrees/herdr-annotate/annotate-clipboard`).
- No focus stealing in Herdr.
- Do not invent a second clipboard transport.

## Decisions

### D1: Route Global Copy via Herdr Client Clipboard API
In remote or multi-machine sessions (over SSH, `herdr --remote`, or federated machines), the plugin runs on the remote server host where no local display or GUI clipboard manager is available (e.g., headless Linux boxes without `xclip`/`wl-clipboard`). Furthermore, even if a local clipboard tool existed on the remote host, it would only write to the remote server's clipboard, not the viewing developer's client clipboard.
- Plugin path (exclusive): `write_client_clipboard(text: &str)` in `rust/src/herdr.rs` calls `herdr clipboard set --stdin`. That is the only global-copy transport. Do not add OSC 52, VPN sinks, or local `pbcopy`/`xclip` fallbacks for `copy-context` / `copy-archive`.
- Herdr forwards the payload to the connected foreground viewing client via `client.clipboard.set` (`{ delivered: true }` means the client connection accepted it, not that the host OS clipboard acknowledged it).
- `copy-context` and `copy-archive` in `rust/src/cli.rs` invoke `write_client_clipboard` to route Markdown annotations to that client clipboard.

### D1a: Minimum Herdr version for `clipboard set`
- Tagged public Herdr **0.9.0** (2026-09-07) does **not** include `herdr clipboard set`. The command and `client.clipboard.set` landed in herdr-core **Unreleased** after that tag (commit `0a4f3e5f`, merged in `2c854568`).
- Practical gate: Herdr **0.9.0 with `herdr clipboard set --stdin`** (command-center Mac build has this). Stock 0.9.0 without the annotation patches is insufficient.
- There is no later public semver that ships the command yet. Probe: `herdr clipboard set --stdin` must exist on the **server** host where the plugin runs.

### D2: Selection Retention across Mouse-up and Prefix in Herdr 0.9.0
In Herdr core (commit `0a4f3e5f` merged into `2c854568` and deployed across fleet machines `studio`, `mbp-16-m4`, `spark0`, `spark1`, `emo-win`), selection clearing was updated so mouse-up and prefix key (`Ctrl+B`) do not prematurely wipe terminal selection. The client context JSON (`HERDR_PLUGIN_CONTEXT_JSON`) provides `selected_text` directly to `capture`.
- We update plugin documentation and remote session guidance in `README.md` to reflect that Herdr 0.9.0 natively supports remote selection retention.

### D3: Test Harness Updates for stdin-consuming Fake Herdr
The existing integration test harness in `rust/tests/commands.rs` used a shell script mock for `herdr` that did not consume standard input. Because `herdr clipboard set --stdin` pipes clipboard text into stdin of the child process, a non-consuming mock can cause pipe write errors (EPIPE) if closed early.
- We update `fake_herdr` in `rust/tests/commands.rs` to consume stdin (now captured to `HERDR_TEST_STDIN`) before logging arguments and exiting.
- We add dedicated regression tests for `copy-archive` verifying active annotation retention on failure and archiving on success, plus `copy_context_routes_markdown_through_herdr_clipboard_set` proving the `clipboard set --stdin` argv and Markdown payload.

### D4: No second clipboard transport
Global copies stay on `herdr clipboard set --stdin` only. OSC 52 remains the pane-manager path (it needs a PTY). Do not add VPN clipboard sinks, extra RPC, or silent local-OS fallbacks for `copy-context` / `copy-archive`. Wait for fleet Herdr to grow `clipboard set`.

### D5: Platform-Aware Binary Resolution and Cross-Machine Distribution (EMO-404)
In a multi-machine environment, local staging (`scripts/stage-local.sh`) or git/rsync syncs the `bin/` directory from a macOS development host to remote fleet targets (`spark0`/`spark1` on Linux aarch64, `emo-win` on Linux x86_64 / Windows). Previously, `fetch-herdr-annotate.sh` and `fetch-herdr-annotate.ps1` checked only whether `bin/herdr-annotate.exe` was executable and its version stamp matched `herdr-annotate.version`. Because permissions and version files were preserved during sync, the fetcher short-circuited with "already installed", leaving incompatible Mach-O arm64 binaries on Linux and Windows.
- Platform detection: detect OS and architecture via `uname -s`/`uname -m` (supporting Darwin arm64/x86_64, Linux aarch64/x86_64, Windows Git Bash / MSYS / CYGWIN, and PowerShell `$architecture`), with fallback to `process.platform` / `process.arch` via Node/Bun.
- Binary verification: verify that any existing binary actually executes on the host OS via `--version` and matches the host target stamp in `bin/herdr-annotate.target` (and `bin/plannotator-tui.target`) before allowing an idempotent skip.
- Native fallback build: if prebuilt download is unavailable or fails (e.g. offline fleet boxes), automatically invoke `cargo build --manifest-path rust/Cargo.toml --release` to compile and stage the native binary.

## Tradeoffs

- **Client clipboard vs OSC 52:** OSC 52 can only be emitted from an active terminal pane with direct PTY output. Global plugin actions (`annotate.copy-context`, `annotate.copy-archive`) run out-of-band without an attached PTY window. Herdr's RPC endpoint (`herdr clipboard set --stdin`) is therefore the only reliable route for global copy actions.
- **Fail-open vs strict error reporting:** On global copy failure, reporting an error notification and retaining unarchived records prevents data loss rather than silently writing to a disconnected or headless server's OS clipboard.

## Verification Evidence

- `cargo test --test commands`:
7 passed, including `copy_context_routes_markdown_through_herdr_clipboard_set` and copy-archive stdin routing through `clipboard set --stdin`.
- `cargo test --lib`:
80 passed covering formatting, store, archive workflow, handoff, and CLI routing.
- `cargo test` after the clipboard-path tests:
87 passed across 4 suites.

## Remaining Gaps / Next Steps
- Plugin path is landed and locally proven. Fleet Herdr on spark0/spark1/mbp-16-24 now exposes `herdr clipboard set --stdin`.
- Local viewing-client round-trip succeeded 2026-09-18T20:58Z (`copy-context` → Mac clipboard `# Annotated context`).
- Remote plugin-host round-trip still unmet: spark0 CLI invoke returns `no_foreground_client` unless a TUI client is viewing that machine. Command-center Herdr has no `--machine` flag, so saved-machine plugin actions cannot be targeted from this Mac's CLI. Do not steal focus to attach a viewer.
- `emo-win` SSH timed out. Do not invent a second clipboard transport.
80 changes: 80 additions & 0 deletions GOAL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# GOAL — multi-machine herdr-annotate

Linear parent: [EMO-404](https://linear.app/emo-eth/issue/EMO-404/herdr-fix-multi-machine-herdr-annotate-platform-binary-distribution)
Herdr: annotate-clipboard (w3R)

Written: 2026-09-18T08:17:43Z
Updated: 2026-09-18T20:58:00Z
Worktree: /Users/emo/.herdr/worktrees/herdr-annotate/annotate-clipboard
Repo: /Users/emo/dev/herdr-remote-annotation/herdr-annotate
Branch: annotate-clipboard
Status: **Working contract via `omp --yolo` (2026-09-18). Lists below are the Done / Acceptance / Validation criteria. Not permission to merge or close.**
Owner: this tree is the single annotate chair. `herdr-annotate-broken` (w2V) is sitting.

## Objective
Global copy actions (`copy-context`, `copy-archive`) on a remote/multi-machine Herdr server deliver formatted Markdown to the **viewing client's** clipboard via `herdr clipboard set --stdin`. No second clipboard transport.

## Done criteria (proposal)

Specific, this worktree. Operator must approve before any of these count as finished.

1. `write_client_clipboard` in `rust/src/herdr.rs` is the only global-copy writer and invokes `herdr clipboard set --stdin` (stdin = Markdown body).
2. `copy-context` and `copy-archive` in `rust/src/cli.rs` call `write_client_clipboard`. They do not call `write_clipboard` / `pbcopy` / `xclip` / OSC 52.
3. `rust/tests/commands.rs` asserts both actions emit argv `clipboard set --stdin` and pipe Markdown containing `# Annotated context`.
4. Copy-archive failure (`herdr clipboard set` nonzero) leaves `annotations.jsonl` unchanged and does not write `archives.jsonl`.
5. `DECISIONS.md` records: exclusive path, min Herdr gate, no second transport, fleet-version blocker.
6. `README.md` documents that global copies use Herdr's client clipboard API, not the server OS clipboard.
7. No merge, no Linear close, no second transport invented while waiting on fleet Herdr.

## Acceptance criteria (proposal)

Observable product behavior once Herdr on the **plugin host** has `herdr clipboard set --stdin`.

1. On a remote or saved-machine workspace, `copy-context` puts the formatted annotation Markdown on the **viewing client's** clipboard. The server host clipboard is not the destination.
2. `copy-archive` does the same, then archives and clears active annotations **only** after clipboard set succeeds.
3. Empty store: `copy-context` / `copy-archive` notify "No annotations" and succeed without calling clipboard set.
4. Clipboard set failure: user-visible error notification; active annotations retained.
5. Manager pane copies (`y` / `c` / `Shift+C`) stay on the existing OSC 52 + native pane path (unchanged by this work).
6. Capture still reads `selected_text` from `HERDR_PLUGIN_CONTEXT_JSON` (Herdr selection retention). Headless `herdr clipboard get` is **out of scope**.
7. Tagged public Herdr 0.9.0 without `clipboard set` is insufficient. Gate: Herdr 0.9.0 **with** `herdr clipboard set --stdin` on the server. Stock 0.9.0 is not enough.

## Validation criteria (proposal)

How we prove the lists above. Operator must approve these checks.

1. `cargo test` in `rust/` passes (currently 87 tests / 4 suites), including:
- `copy_context_routes_markdown_through_herdr_clipboard_set`
- `copy_archive_success_archives_and_clears_active` (argv + stdin Markdown)
- `copy_archive_failure_preserves_active_annotations_and_does_not_archive`
- empty-store copy-context notify/success
2. Source grep: `copy_context` / `copy_archive` have no `write_clipboard(` call.
3. Command-center Mac: `herdr clipboard set --help` exists. This is the **client** probe, not a fleet-server probe.
4. Live round-trip (blocked until fleet Herdr is upgraded; not claimed met):
- Plugin host is a fleet box (`spark0` or `spark1`) whose `herdr clipboard --help` lists `set`.
- From a viewing Mac, invoke `copy-context` on that machine's workspace.
- Viewing Mac system clipboard contains `# Annotated context` and the annotation body.
5. Until (4) is run, this worktree is **not finished**, even if (1)–(3) pass.

## Non-goals
- No merge to master or release deployment without operator-approved criteria **and** those criteria met.
- Do not close user Herdr panes or steal Herdr focus.
- Do not invent a second clipboard transport (VPN sink, OSC 52 for global actions, silent local-OS fallback).
- Do not upgrade/deploy fleet `~/.local/bin/herdr` from this plugin worktree unless the operator assigns that.
- Do not close Linear tickets.

## Remaining work (this tree only)

1. **Live remote round-trip (validation 4, unmet):** operator must be *viewing* a spark0/spark1 workspace in the Herdr TUI, then invoke `copy-context`. SSH/CLI on spark0 reaches `herdr clipboard set` and returns `no_foreground_client` when no TUI viewer is attached. Command-center `herdr` has no `--machine` flag (`unknown option: --machine`), so this Mac cannot target a saved-machine plugin action from CLI. Do not steal focus to select that machine.
2. `emo-win` SSH is accessible. Platform-aware binary resolution (EMO-404) verified across spark0 (aarch64) and emo-win (x86_64).
3. Parked-chair artifacts stay in `/Users/emo/.herdr/worktrees/scratch/herdr-annotate-broken`. Do not continue work there. Extra chair sitting, not hidden, not removed.

## Current evidence (not a finish)
- Plugin path landed: `5866499`, tests `ca9e446` (87 tests, including clipboard-set routing). `copy_context` / `copy_archive` have no `write_clipboard(` calls.
- Fleet Herdr **now** has `herdr clipboard set --stdin` on command-center Mac, `spark0`, `spark1`, `mbp-16-24` (`herdr 0.9.0`).
- Linked `herdr-annotate.exe` on spark0/spark1 already contains `clipboard set --stdin`. mbp-16-24 restaged 2026-09-18T20:57Z from this tree's darwin binary.
- Local live round-trip **met**: `herdr plugin action invoke copy-context --plugin annotate` on this Mac wrote `# Annotated context` plus probe body to the viewing Mac clipboard (plugin-log-239, exit 0). Probe records cleared from both local and spark0 `annotations.jsonl`.
- spark0 AppleDouble `.git/objects/pack/._pack-*` removed. git is a usable work tree again.
- spark0 CLI `copy-context` (plugin-log-41) failed `no_foreground_client` — plugin RPC works; delivery needs a TUI viewer.
- Command-center `herdr plugin action invoke` exists; `herdr --machine` does not (`unknown option`). Remote invoke from this Mac is SSH-to-host only.
- `standup: … done` is a status ping only. It is not permission to merge or close.
- EMO-404 platform-aware binary resolution verified on Darwin arm64, Linux aarch64 (spark0), and Linux x86_64 (emo-win). Unrunnable Mach-O binaries previously synced to Linux are detected and automatically replaced with native binaries; local fallback builds via `cargo build --release` are supported.
51 changes: 9 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ Annotate inside [Herdr](https://github.com/herdrdev/herdr): comment on any termi

## Requirements

- Herdr 0.8.0 or later
- Herdr 0.9.0 with client clipboard support (or compatible patched build)
- macOS, Linux, or Windows

There is no runtime to install. Both installs download a small prebuilt `herdr-annotate` binary and verify its SHA-256 checksum.

On Linux, install `wl-clipboard`, `xclip`, or `xsel` for clipboard access.
Global copy actions (`copy-context`, `copy-archive`) forward directly to the connected viewing client via Herdr's clipboard API. For the manager popup's local copy fallback, install `wl-clipboard`, `xclip`, or `xsel` on Linux when running on a local desktop session alongside terminal OSC 52.

On Windows, native Herdr plugin support is preview/best-effort. Clipboard access uses PowerShell; no extra clipboard package is required. The install, keybinding, configuration check, reload, and use instructions below also apply on Windows.
On Windows, native Herdr plugin support is preview/best-effort. Local clipboard fallback uses PowerShell; no extra clipboard package is required. The install, keybinding, configuration check, reload, and use instructions below also apply on Windows.

## Install

Expand Down Expand Up @@ -146,10 +146,7 @@ herdr server reload-config
| `Ctrl+B Ctrl+A` | copy all annotations as Markdown, then archive them |
| `Ctrl+B M` | manage · `y` copy one · `c` copy all · `Shift+C` copy and archive · `Tab` archives (`y` copy · `u` restore · `d d` delete) |

Copies made inside the manager pane also emit OSC 52, so on Herdr 0.9.0 they reach the clipboard of
the machine you are viewing from even when the plugin runs on a remote server with no clipboard tool
installed; `Ctrl+B Shift+A` and `Ctrl+B Ctrl+A` do not, because those actions run outside a pane and
have no terminal to write to.
Global copy actions (`Ctrl+B Shift+A` and `Ctrl+B Ctrl+A`) forward annotations directly to the viewing client's clipboard via Herdr's clipboard API. Copies made inside the manager pane also emit OSC 52, reaching the viewing client on local or remote sessions.

### Review documents and agent replies

Expand All @@ -171,44 +168,14 @@ Full install. Works with Claude Code, Codex, pi, Copilot CLI, Droid, Oh My Pi, H

### Remote sessions

Over SSH or `herdr --remote`, the plugin runs on the **server**, and two things get in the way:
Herdr's default copy-on-select clears the selection on mouse-up, and the prefix keypress
clears whatever selection remains before a bound action runs
([herdrdev/herdr#3380](https://github.com/herdrdev/herdr/issues/3380)). A headless server also
has no clipboard for the plugin to fall back to.

What works today:

1. On the server, keep the selection after mouse-up:

```toml
# remote server: ~/.config/herdr/config.toml
[ui]
copy_on_select = false # the selection stays; copy explicitly with Ctrl+C
```

2. Trigger the action **without a keypress in Herdr**, while the selection is still
highlighted. From your laptop, bound to any key in your terminal or OS:

```sh
ssh <host> herdr plugin action invoke annotate.capture
# named session on the server: ssh <host> HERDR_SESSION=<name> herdr plugin action invoke annotate.capture
```

The action reads the focused pane's selection through Herdr's API, which never touches the
keyboard path, so the text arrives. Verified: the same selection gives `selected_text` this
way and nothing through `prefix+a`.

3. In Neovim, use the mapping below; it hands the selection over in a file.

Server-side key bindings and `herdr --remote <host> --remote-keybindings server` are still
needed for the manager (`prefix+m`) and other plugin keys; without the flag, `herdr --remote`
uses your local keys and drops plugin bindings. `prefix+a` itself will work once
herdrdev/herdr#3380 is fixed.
Over SSH or saved machine federation, terminal annotations work directly from the viewing client:
- Mouse selections stay highlighted across mouse-up (with automatic copying preserved) and across prefix keys (`prefix+a`), capturing the remote selection cleanly into the annotation dialog.
- Global copies (`prefix+shift+a` and `prefix+ctrl+a`) route through Herdr's clipboard API, delivering formatted Markdown directly to your viewing client.
- For standalone `herdr --remote <host>` connections, connect with `--remote-keybindings server` (or configure remote machine profiles via `herdr machine add`) so the remote server's plugin actions are published to your client.

## Selection limits

Herdr Annotate reads text that Herdr copies to the system clipboard. The plugin cannot read selection state from Neovim or another terminal application.
Herdr Annotate receives the active terminal selection directly from Herdr's client shell invocation context. When triggered without an active Herdr terminal selection, it falls back to handoff text or clipboard reads. The plugin cannot read selection state from Neovim or another internal terminal application; use the Neovim mapping below to hand selections over directly.

## Development

Expand Down
8 changes: 4 additions & 4 deletions rust/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ use uuid::Uuid;
use crate::archive_workflow::{
CopyAndArchiveDependencies, CopyAndArchiveOutcome, copy_and_archive_annotations,
};
use crate::clipboard::{read_clipboard, write_clipboard};
use crate::clipboard::read_clipboard;
use crate::format::format_annotations;
use crate::handoff::take_default_handoff;
use crate::herdr::{notify, run_herdr};
use crate::herdr::{notify, run_herdr, write_client_clipboard};
use crate::paths::{normalize_windows_path, plugin_root, state_dir};
use crate::store::{
append_archived_set, load_annotations, newest_first_annotations, remove_annotations_by_id,
Expand Down Expand Up @@ -128,7 +128,7 @@ fn copy_context() -> Result<(), String> {
notify("No annotations", Some("There is nothing to copy yet."));
return Ok(());
}
write_clipboard(&format_annotations(&annotations))?;
write_client_clipboard(&format_annotations(&annotations))?;
notify(
"Annotations copied",
Some(&format!(
Expand Down Expand Up @@ -196,7 +196,7 @@ fn copy_archive() -> Result<(), String> {
}
loaded
},
write_clipboard: |text: String| write_clipboard(&text),
write_clipboard: |text: String| write_client_clipboard(&text),
save_archive: |archive: ArchivedAnnotationSet| append_archived_set(&dir, &archive),
remove_active: |ids: Vec<String>| remove_annotations_by_id(&dir, &ids),
create_archive_id: || Uuid::new_v4().to_string(),
Expand Down
Loading