ace-herdr 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/herdr/config.yml +26 -0
  3. data/.ace-defaults/herdr/tabs/agent.yml +11 -0
  4. data/.ace-defaults/herdr/workspaces/development.yml +23 -0
  5. data/CHANGELOG.md +22 -0
  6. data/LICENSE +21 -0
  7. data/README.md +54 -0
  8. data/Rakefile +12 -0
  9. data/docs/usage.md +306 -0
  10. data/exe/ace-herdr +17 -0
  11. data/lib/ace/herdr/atoms/answer_digest.rb +18 -0
  12. data/lib/ace/herdr/cli/commands/capture.rb +55 -0
  13. data/lib/ace/herdr/cli/commands/close.rb +53 -0
  14. data/lib/ace/herdr/cli/commands/deliver.rb +67 -0
  15. data/lib/ace/herdr/cli/commands/dispatch.rb +62 -0
  16. data/lib/ace/herdr/cli/commands/list.rb +71 -0
  17. data/lib/ace/herdr/cli/commands/list_presets.rb +47 -0
  18. data/lib/ace/herdr/cli/commands/send.rb +103 -0
  19. data/lib/ace/herdr/cli/commands/support.rb +68 -0
  20. data/lib/ace/herdr/cli/commands/tab.rb +54 -0
  21. data/lib/ace/herdr/cli/commands/tidy.rb +65 -0
  22. data/lib/ace/herdr/cli/commands/wait.rb +88 -0
  23. data/lib/ace/herdr/cli/commands/workspace.rb +52 -0
  24. data/lib/ace/herdr/cli.rb +98 -0
  25. data/lib/ace/herdr/errors.rb +51 -0
  26. data/lib/ace/herdr/models/delivery_record.rb +109 -0
  27. data/lib/ace/herdr/models/dispatch_outcome.rb +29 -0
  28. data/lib/ace/herdr/molecules/delivery_record_store.rb +150 -0
  29. data/lib/ace/herdr/molecules/herdr_executor.rb +264 -0
  30. data/lib/ace/herdr/molecules/pane_tidy_probe.rb +80 -0
  31. data/lib/ace/herdr/molecules/preset_loader.rb +70 -0
  32. data/lib/ace/herdr/molecules/preset_resolver.rb +88 -0
  33. data/lib/ace/herdr/organisms/control_surface.rb +575 -0
  34. data/lib/ace/herdr/organisms/deliverer.rb +358 -0
  35. data/lib/ace/herdr/organisms/dispatcher.rb +130 -0
  36. data/lib/ace/herdr/organisms/tidy.rb +188 -0
  37. data/lib/ace/herdr/version.rb +7 -0
  38. data/lib/ace/herdr.rb +103 -0
  39. metadata +224 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 11805892b3ae5bdf74d4bdd555f8e508e7fe3693476d07fa537a41b9f891857e
4
+ data.tar.gz: 2aa907e23b109838153215ac004f344045220d2c77cf0f295a99c6ff55a75cb2
5
+ SHA512:
6
+ metadata.gz: 53e2cbd3f3c97db17b5e14609e54deb210e0d7474886dc4f6fe7d78a33154df1cb7fe680a565aa500d534c0ce06235dd20445f9ae6bb867d5b989732c5a5fa70
7
+ data.tar.gz: 9397dfe3b7fb8ac0d9b42f2a43ad7ac56204c7b70197e5f1a6095a00f62f38ecb8d7a0967ca0ab5d68c91864656c86db25a9cc9bc0274fa65c63d74216771e09
@@ -0,0 +1,26 @@
1
+ # ace-herdr default configuration
2
+ # Override in ~/.ace/herdr/config.yml or .ace/herdr/config.yml
3
+
4
+ # Agent kind used when bootstrapping a pane without an agent
5
+ # (herdr agent start --kind)
6
+ default_agent_kind: pi
7
+
8
+ # Push delivery retries; deterministic fixed backoff, no jitter
9
+ delivery:
10
+ max_attempts: 3
11
+ backoff_seconds: [1, 2, 4]
12
+
13
+ # Timeouts in seconds
14
+ timeouts:
15
+ agent_start: 60
16
+ prompt: 30
17
+ wait: 30
18
+
19
+ # Delivery record root (ADR-029 short-name layout), relative to project root
20
+ deliveries_dir: .ace-local/herdr/deliveries
21
+
22
+ # Tidy (spec 8wq.t.1w0): archive delivered records strictly older than this
23
+ # many days (age = record updated_at); failed/retryable/pending are never
24
+ # touched
25
+ tidy:
26
+ delivered_retention_days: 7
@@ -0,0 +1,11 @@
1
+ # Default tab preset: one pane hosting a pi agent (no initial prompt;
2
+ # add `prompt:` under agent to submit one at creation).
3
+ #
4
+ # Override in ~/.ace/herdr/tabs/ or .ace/herdr/tabs/
5
+ # (project wins; see ace-herdr docs/usage.md for the full schema).
6
+ label: agent
7
+ panes:
8
+ - label: agent
9
+ agent:
10
+ kind: pi
11
+ name: task-agent
@@ -0,0 +1,23 @@
1
+ # Default workspace preset: a work tab with a shell and a pi agent side by
2
+ # side, plus a scratch shell tab.
3
+ #
4
+ # Override in ~/.ace/herdr/workspaces/ or .ace/herdr/workspaces/
5
+ # (project wins; see ace-herdr docs/usage.md for the full schema).
6
+ label: development
7
+ focus: true
8
+ tabs:
9
+ - label: work
10
+ panes:
11
+ - label: shell
12
+ - label: agent
13
+ agent:
14
+ kind: pi
15
+ name: development-agent
16
+ prompt: Ready to work on the current task.
17
+ splits:
18
+ - direction: right
19
+ target: shell
20
+ pane: agent
21
+ - label: scratch
22
+ panes:
23
+ - label: shell
data/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to `ace-herdr` will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-09-27
9
+
10
+ ### Added
11
+ - `ace-herdr tidy` (spec 8wq.t.1w0): dry-run-by-default cleanup of finished agent panes and delivery records. Pane closure requires positive completion evidence only (observed `done` agent state, or a pane whose native `pane process-info` shows no live foreground process — herdr omits `foreground_processes` when empty) re-confirmed by a fresh probe immediately before the rename-to-`done`-then-close mutation; revived or uncertain candidates are excluded and never mutated. Delivered records strictly older than the new `tidy.delivered_retention_days` config key (default 7) are atomically archived to `deliveries_dir/archive/` after a lock-guarded reload re-proves eligibility; `pending`/`retryable`/`failed` and unreadable records are never touched, an unreadable record encountered during apply is preserved and reported instead of aborting. Archival never breaks delivery idempotency (`deliver`/`--resume` consult the archive copy, so identical re-delivery still short-circuits and conflicts still fail closed). Deterministic one-line JSON report with explicit empty states; an unreachable herdr runtime fails with an explicit error and no partial report.
12
+ - Terminal-control surface matching the ace-tmux intent vocabulary (spec 8wq.t.k84), keeping one-line JSON output: `ace-herdr list` (panes/tabs/workspaces, `--workspace` scoping, explicit empty arrays), `ace-herdr send` (declaration-ordered `--cmd`/`--msg`/`--key`; agent-aware routing sends one self-submitting `agent prompt` on agent panes with `agent_blocked`/`agent_prompt_stalled` gating and drop-and-report of a single trailing `--key Enter`; invalid shapes fail closed before any transport call), `ace-herdr capture` (raw `pane read` text, `--source visible|recent`), `ace-herdr wait --for output --pattern` (literal match, immediate check then poll, seconds→ms conversion, timeout CLI error) alongside the unchanged agent wait, and `--list-presets`.
13
+ - Preset-driven creation: `ace-herdr workspace <preset>` / `ace-herdr tab <preset>` compose YAML layouts through the ADR-022 cascade (`.ace/herdr/` > `~/.ace/herdr/` > gem `.ace-defaults/herdr/`, ships `workspaces/development.yml` + `tabs/agent.yml`), with recursive `preset:` inheritance (deep merge, array concat, cycle detection, fail-closed missing references), deterministic creation order (workspace → tabs → root pane + splits → renames → pane commands → readiness-gated agent starts; herdr's seeded initial tab is closed once the declared tabs exist), CLI `--cwd` overrides (CLI > tab > root > pane), and pre-creation layout validation (unplaced panes, duplicate placements, unknown split targets/directions fail closed and create nothing). Includes a tmux-intent ↔ herdr-command parity table in `docs/usage.md`.
14
+ - `Ace::Herdr::Organisms::ControlSurface`: owner of the terminal-control behavior — native JSON normalization into public payloads, send validation/routing, and preset instantiation.
15
+ - `Ace::Herdr::Molecules::PresetLoader` (ADR-022 cascade discovery) and `Ace::Herdr::Molecules::PresetResolver` (`preset:` reference resolution).
16
+ - `Ace::Herdr::Molecules::HerdrExecutor`: native operations for workspace/tab/pane listing, pane read/send-text/send-keys, pane wait-output, pane split, workspace create, agent send-keys, and focus flags on tab/workspace creation; typed `TabNotFoundError`/`WorkspaceNotFoundError` classification; herdr machine codes are now carried in executor error messages (for example `pane_not_found: ...`); output-wait timeouts raise `WaitTimeoutError`.
17
+ - Gem bootstrap (spec 8wm.t.vs0): push delivery + agent bootstrap for the Herdr runtime, implementing the ace-hitl provider delivery contract (`deliver(ref, answer)` -> `DeliverResult` with `:delivered`/`:retryable`/`:failed`).
18
+ - `Ace::Herdr::Organisms::Deliverer`: idempotent per event id with write-ahead delivery records under `.ace-local/herdr/deliveries/` (0600, atomic writes; the full answer is persisted so a crash never loses it, recoverable via `ace-herdr deliver --resume`), per-event lock serializing concurrent deliveries, duplicate-identical short-circuit, fail-closed content/destination conflicts, bootstrap of missing agents (`herdr agent start` with shell-escaped `HERDR_SESSION`/`HERDR_PANE` exported into the pane shell), a readiness gate before prompting, retry limits with fixed deterministic backoff (covering prompt and probe failures), and terminal-failure reporting — including the ambiguous crash window after a submitted prompt, reported instead of silently resent.
19
+ - `Ace::Herdr::Organisms::Dispatcher`: one-command subagent dispatch — tab + `herdr agent start` + prompt with deterministic defaults (caller's workspace, label = agent/pane name, prompt from file/stdin/`--no-prompt`); workspace and pane identifiers are token-validated and shell-escaped before they reach a pane shell.
20
+ - `ace-herdr` CLI: `deliver`, `dispatch`, `wait`, `close` (JSON output, exception-based exit codes per ADR-023).
21
+ - `Ace::Herdr::Molecules::HerdrExecutor`: the single seam to the herdr CLI (argv arrays per ADR-031) with typed error classification from herdr's machine-readable codes (`agent_blocked`, `agent_prompt_stalled`, `pane_not_found`, `timeout`).
22
+ - Config cascade (ADR-022) defaults: `default_agent_kind`, delivery retry limits/backoff, timeouts, deliveries dir (ADR-029).
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Michal Czyz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ <div align="center">
2
+ <h1> ACE - HERDR </h1>
3
+
4
+ Zero-token push delivery and agent bootstrap for the Herdr runtime, behind the ace-hitl provider delivery contract.
5
+
6
+ <img src="../docs/brand/AgenticCodingEnvironment.Logo.XS.jpg" alt="ACE Logo" width="480">
7
+ <br><br>
8
+
9
+ <a href="https://rubygems.org/gems/ace-herdr"><img alt="Gem Version" src="https://img.shields.io/gem/v/ace-herdr.svg" /></a>
10
+ <a href="https://www.ruby-lang.org"><img alt="Ruby" src="https://img.shields.io/badge/Ruby-3.2+-CC342D?logo=ruby" /></a>
11
+ <a href="https://opensource.org/licenses/MIT"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg" /></a>
12
+
13
+ </div>
14
+
15
+ > Works with: Claude Code, Codex CLI, OpenCode, Gemini CLI, pi-agent, and more.
16
+
17
+ [Usage Guide](docs/usage.md)
18
+
19
+ `ace-herdr` is to herdr what `ace-tmux` is to tmux: a deterministic, zero-token wrapper over the `herdr` CLI. It implements the ace-hitl push-delivery contract — `deliver(ref, answer)` maps to `herdr agent prompt <pane>`, bootstrapping the agent first (`herdr agent start`) when the pane has none, so an answer is never lost — and adds one-command agent dispatch, noiseless monitoring, and closure on top.
20
+
21
+ ## How It Works
22
+
23
+ 1. Every ace-hitl ask captures a reverse address (`HERDR_SESSION` / `HERDR_PANE`, schema `ace.hitl.ref/v1`). `ace-herdr deliver` pushes an answer back to that address.
24
+ 2. Delivery is idempotent per event id: write-ahead delivery records under `.ace-local/herdr/deliveries/` make retries safe — identical content is never delivered twice.
25
+ 3. If the target pane has no agent, `ace-herdr` bootstraps one (`herdr agent start --kind <kind> --pane <pane>`), waits for readiness, and exports `HERDR_SESSION` / `HERDR_PANE` into the agent's environment.
26
+ 4. Retryable failures back off on a fixed deterministic schedule; terminal failures are reported and persisted in the delivery record.
27
+ 5. `ace-herdr dispatch`, `wait`, and `close` give agents one-command subagent lifecycle with sensible defaults (same workspace as the caller, label = task id, prompt from file or stdin).
28
+
29
+ ## Use Cases
30
+
31
+ - Push an HITL answer to the pane that asked, even if its agent was restarted since.
32
+ - Dispatch a subagent in one command with deterministic defaults and no LLM decisions.
33
+ - Wait for agent readiness or completion without scraping pane noise.
34
+ - Close out finished agent panes (rename/close) and return their result.
35
+
36
+ ## Installation
37
+
38
+ Install as part of the ACE toolkit (see the repo README), or add to your Gemfile:
39
+
40
+ ```ruby
41
+ gem "ace-herdr"
42
+ ```
43
+
44
+ ## Testing
45
+
46
+ Fast tests only (no live herdr needed):
47
+
48
+ ```bash
49
+ ace-test ace-herdr
50
+ ```
51
+
52
+ ## License
53
+
54
+ MIT — see [LICENSE](LICENSE).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test" << "lib"
8
+ t.test_files = FileList["test/**/*_test.rb"]
9
+ end
10
+
11
+ task spec: :test
12
+ task default: :test
data/docs/usage.md ADDED
@@ -0,0 +1,306 @@
1
+ ---
2
+ doc-type: user
3
+ title: ace-herdr Usage
4
+ purpose: Full CLI and configuration reference for ace-herdr: push delivery, agent bootstrap, the terminal-control surface (list, send, capture, wait, presets), and tidy cleanup.
5
+ ace-docs:
6
+ last-updated: 2026-09-28
7
+ last-checked: 2026-09-28
8
+ ---
9
+
10
+ # Usage
11
+
12
+ `ace-herdr` is a zero-token wrapper over the `herdr` CLI. It implements the ace-hitl push-delivery contract -- `deliver(ref, answer)` -> `herdr agent prompt <pane>` -- one-command agent dispatch, noiseless waiting and pane closure, the terminal-control intents known from `ace-tmux` (`list`, `send`, `capture`, output waits, preset-driven workspace/tab creation), and dry-run-first cleanup of finished panes and delivery records (`tidy`). No LLM is consulted anywhere in the gem.
13
+
14
+ ## Command Surface
15
+
16
+ - `ace-herdr deliver [OPTIONS]`
17
+ - `ace-herdr dispatch [OPTIONS]`
18
+ - `ace-herdr list [--panes|--tabs|--workspaces] [--workspace ID] [--quiet]`
19
+ - `ace-herdr send [--cmd TEXT] [--msg TEXT...] [--key NAME...] --pane ID [--quiet]`
20
+ - `ace-herdr capture --pane ID [--lines N] [--source visible|recent]`
21
+ - `ace-herdr wait --pane ID (--for output --pattern PATTERN | --for agent [--until ...]) [--timeout S] [--quiet]`
22
+ - `ace-herdr close [OPTIONS]`
23
+ - `ace-herdr tidy [--apply] [--quiet]`
24
+ - `ace-herdr workspace <preset> [--cwd PATH] [--quiet]`
25
+ - `ace-herdr tab <preset> [--workspace ID] [--cwd PATH] [--quiet]`
26
+ - `ace-herdr --list-presets [workspaces|tabs]`
27
+
28
+ ## Output policy
29
+
30
+ Control commands print exactly one deterministic JSON line on stdout; `--quiet` suppresses it. Failures surface herdr's machine error codes in the CLI error message (for example `pane_not_found: pane w9:p1 not found`) and exit non-zero. The one exception is `capture`, which prints raw pane text without any JSON wrapping.
31
+
32
+ herdr *sessions* (server persistence) are intentionally not exposed; the tmux session analogue is the herdr **workspace**.
33
+
34
+ ## tmux-intent ↔ herdr-command parity
35
+
36
+ Every common terminal-control intent available through `ace-tmux` is available through `ace-herdr` (herdr 0.9.1). Parity is of INTENT and FLAG VOCABULARY, not byte-format: ace-herdr keeps one-line JSON where ace-tmux renders human tables.
37
+
38
+ | ace-tmux intent | ace-herdr command | Native herdr call |
39
+ |---|---|---|
40
+ | `list` (panes) | `list` / `list --panes` | `pane list` |
41
+ | `list --windows` | `list --tabs` | `tab list` |
42
+ | `list --sessions` | `list --workspaces` | `workspace list` |
43
+ | `send --cmd` | `send --cmd` | `pane run` (plain) / `agent prompt` (agent pane) |
44
+ | `send --msg` / `--key` | `send --msg` / `--key` | `pane send-text` / `pane send-keys`; `agent prompt` / `agent send-keys` |
45
+ | `capture` | `capture` | `pane read` |
46
+ | `wait --for output` | `wait --for output --pattern` | `pane wait-output` |
47
+ | `wait --for agent` | `wait --until` (agent state) | `agent wait` |
48
+ | `start <preset>` | `workspace <preset>` | `workspace create` + tabs/panes |
49
+ | `window <preset>` | `tab <preset>` | `tab create` + splits |
50
+ | `--list-presets` | `--list-presets` | -- (config cascade) |
51
+ | `attach` / `detach` | ❌ out of scope | human-facing; no automation equivalent |
52
+ | layout strings / `select-layout` | ❌ out of scope | herdr has split/resize only |
53
+
54
+ ## `ace-herdr list`
55
+
56
+ Inspect live state. Panes are the default scope; `--workspace <id>` scopes panes and tabs.
57
+
58
+ ```bash
59
+ ace-herdr list # panes across all workspaces
60
+ ace-herdr list --workspace w1 # panes in one workspace
61
+ ace-herdr list --tabs --workspace w1 # tabs in one workspace
62
+ ace-herdr list --workspaces # workspaces
63
+ ```
64
+
65
+ Output: `{"panes":[{"id":"w1:p1","tab":"w1:t1","workspace":"w1","title":"~","cwd":"/tmp","focused":true,"agent_status":"idle"},...]}` -- `{"tabs":[{id, workspace, title, number, pane_count, focused}...]}` for `--tabs`, `{"workspaces":[{id, title, number, tab_count, pane_count, focused}...]}` for `--workspaces`. Empty results are a success with an empty array, never an error.
66
+
67
+ ## `ace-herdr send`
68
+
69
+ Send a command, raw text, or named keys. Input reaches the pane in **declaration order** (an intentional divergence from ace-tmux's msgs-then-keys; order is expressive here, so it is preserved, not normalized). At least one input token is required -- none is a usage error before any transport call.
70
+
71
+ ```bash
72
+ ace-herdr send --pane p5 --cmd 'bundle exec rake test'
73
+ ace-herdr send --pane p5 --msg 'continue with option 2' --key Enter
74
+ ace-herdr send --pane p5 --key Esc --cmd 'reset' # rejected -- see rules
75
+ ace-herdr send --pane p5 --key enter
76
+ ```
77
+
78
+ ### Plain pane (raw input, declaration order)
79
+
80
+ - `--cmd TEXT` is exactly one `pane run` submission (text + Enter). It must be declared before every `--key`; trailing keys are sent after the submission (post-submission keystrokes such as `y`/`n` confirmations). Leading keys (`--key Esc --cmd run`) are rejected before any transport call.
81
+ - `--msg a --msg b` types raw text without submitting, concatenated in order.
82
+ - `--key K...` sends named keys in order; each `enter` submits pending text.
83
+ - `--cmd` combined with `--msg` is a usage error before any transport call (fail closed, nothing sent).
84
+ - Multi-Enter sequences submit per Enter.
85
+ - Output: `{"pane":"p5","sent":"cmd"}` for command-led sends, `"text"` for message-led sends, `"keys"` for key-only sends.
86
+
87
+ ### Agent pane (prompt semantics)
88
+
89
+ When the target pane hosts a live agent (`herdr agent get`), the same flags route to agent transport -- replacing ace-tmux's INTERACTIVE_CLI_COMMANDS/busy-pattern heuristics with native agent state:
90
+
91
+ - `--cmd T`, or concatenated `--msg` texts (joined with newlines), becomes **one** agent prompt that submits itself -- message-only input submits once on an agent pane (intended divergence from plain panes).
92
+ - At most one trailing `--key Enter` is dropped and reported: `{"pane":"p5","sent":"prompt","dropped_keys":["Enter"]}` -- this guarantees exactly-one submission instead of erroring.
93
+ - `--key`-only sequences go to `agent send-keys` (`esc`, `ctrl+c`, ...); at most one `enter` total.
94
+ - Submission is gated natively: a blocked agent rejects pre-send with a CLI error carrying `agent_blocked` (never silently dropped); a stalled prompt surfaces `agent_prompt_stalled`.
95
+ - Rejected before transport: keys interleaved between or after messages other than the single trailing `enter`; multiple `enter` keys; `--cmd` with non-Enter keys; `--cmd` combined with `--msg`.
96
+
97
+ ## `ace-herdr capture`
98
+
99
+ Print pane content as raw text (no JSON wrapping). Read-only -- agent routing rules do not apply.
100
+
101
+ ```bash
102
+ ace-herdr capture --pane p5 # last 40 lines of history
103
+ ace-herdr capture --pane p5 --lines 40 --source recent
104
+ ace-herdr capture --pane p5 --source visible # the agent screen
105
+ ```
106
+
107
+ ## `ace-herdr wait`
108
+
109
+ Wait for an agent state or matching pane output.
110
+
111
+ ```bash
112
+ ace-herdr wait --pane p5 # agent: idle, done, or blocked
113
+ ace-herdr wait --pane p5 --until done --timeout 120 # agent: one state
114
+ ace-herdr wait --pane p5 --for output --pattern 'tests? OK' --timeout 30
115
+ ```
116
+
117
+ - Agent form (default; unchanged): `--until idle,working,blocked,done,unknown`, output `{"pane":"p5","state":"ready"}`.
118
+ - Output form: `--for output --pattern PATTERN` (literal substring, matching ace-tmux's observable contract). herdr checks existing pane content immediately, then polls; `--timeout` is seconds (gem converts to milliseconds). Output `{"pane":"p5","matched":true}`; a timeout is a CLI error (non-zero exit).
119
+ - The modes are mutually exclusive: `--pattern` requires `--for output`; `--until` requires the agent form.
120
+
121
+ ## `ace-herdr workspace` / `ace-herdr tab` (presets)
122
+
123
+ Declare a workspace or tab layout in YAML and create it in one command -- `workspace <preset>` mirrors `ace-tmux start`, `tab <preset>` mirrors `ace-tmux window`.
124
+
125
+ ```bash
126
+ ace-herdr workspace development
127
+ ace-herdr workspace development --cwd /path/to/project
128
+ ace-herdr tab agent --workspace w1
129
+ ace-herdr --list-presets # merged inventory, both types
130
+ ace-herdr --list-presets workspaces
131
+ ```
132
+
133
+ ### Preset cascade (ADR-022)
134
+
135
+ Presets load nearest-wins and the `--list-presets` inventory reflects the merged set:
136
+
137
+ 1. project `.ace/herdr/{workspaces,tabs}/*.yml` (highest)
138
+ 2. user `~/.ace/herdr/{workspaces,tabs}/*.yml`
139
+ 3. gem `.ace-defaults/herdr/{workspaces,tabs}/*.yml` -- ships `workspaces/development.yml` and `tabs/agent.yml`
140
+
141
+ A project file with the same name overrides a gem/user file; names unique to a level still appear in the merged listing.
142
+
143
+ ### Schema
144
+
145
+ ```yaml
146
+ # .ace/herdr/workspaces/development.yml
147
+ label: development # workspace label (defaults to the preset name)
148
+ cwd: /workspace/project # root cwd (tab/panes inherit; ~ expands)
149
+ focus: true # pass --focus to workspace create
150
+ tabs:
151
+ - preset: agent # a tab may inherit a tab preset (recursive; overlay wins)
152
+ label: work # ...and override any field
153
+ - label: editor
154
+ cwd: /workspace/project # tab cwd overrides the root cwd
155
+ focus: false
156
+ panes:
157
+ - label: shell # the FIRST pane is the tab's root pane
158
+ - label: agent
159
+ cwd: /workspace/project
160
+ agent: # declares an agent pane
161
+ kind: pi # default: config default_agent_kind
162
+ name: development-agent
163
+ prompt: Review the current task.
164
+ splits: # every pane after the first must be placed by a split
165
+ - direction: right # herdr-native right|down; horizontal|vertical accepted
166
+ target: shell # pane label to split from (default: the root pane)
167
+ pane: agent # the declared pane placed by this split
168
+ ratio: 0.5
169
+ ```
170
+
171
+ ```yaml
172
+ # .ace/herdr/tabs/agent.yml -- a tab preset uses the tab-level fields directly
173
+ label: agent
174
+ panes:
175
+ - label: agent
176
+ agent:
177
+ kind: pi
178
+ name: task-agent
179
+ ```
180
+
181
+ Creation is deterministic and ordered: the workspace is created first, then tabs/panes in declared order (root pane from `tab create`, further panes from `pane split`, panes renamed to their labels), then pane `command`s run, then declared agents start readiness-gated (`agent start` blocks until the pane is interactive; vs0 bootstrap order: reverse address exported into the pane shell, agent start, optional prompt). herdr seeds every new workspace with an initial tab; once the preset's declared tabs exist that seeded tab is closed, so only declared tabs remain (a preset with no `tabs:` keeps the seeded one). All layouts are validated before any herdr call -- an unplaced pane, a duplicate split placement, an unknown split target, or an unknown direction fails closed and creates nothing.
182
+
183
+ Output: `{"workspace":"w2","tabs":[{"tab":"w2:t1","panes":["w2:p1","w2:p2"],"commands":1,"agents":["development-agent"]}]}`; the tab command reports the single tab object. `--cwd` overrides the resolved root/tab cwd (CLI > tab > root > pane inheritance).
184
+
185
+ Unknown preset: CLI error listing the available names (ace-tmux `--list-presets` parity), for example `Error: Unknown workspace preset 'nope' (available: development)`.
186
+
187
+ ## `ace-herdr deliver`
188
+
189
+ Push an answer to an agent pane.
190
+
191
+ ```bash
192
+ # Answer from a file, explicit ref
193
+ ace-herdr deliver --session ws-1 --pane p5 --event-id evt-1 --answer-file answer.md
194
+
195
+ # Answer from stdin, ref from the environment (inside the asking pane)
196
+ echo 'the answer' | ace-herdr deliver
197
+
198
+ # Bootstrap a specific agent kind if the pane has none
199
+ ace-herdr deliver --session ws-1 --pane p5 --kind codex --label 8wm.t.vs0 --answer-file a.md
200
+ ```
201
+
202
+ Options: `--session`, `--pane` (default: `HERDR_SESSION` / `HERDR_PANE`), `--event-id` (default: derived from the ref and content digest), `--kind`, `--label`, `--answer-file` (default: stdin), `--resume <event-id>` (re-deliver the stored answer from the record; no ref or answer input needed).
203
+
204
+ Output: one JSON line `{"ref":{...},"state":"delivered|retryable|failed"}`. Exit code is non-zero unless the state is `delivered`.
205
+
206
+ ### The delivery contract
207
+
208
+ `ace-hitl ask` captures the asker's reverse address fail-closed from the environment (`HERDR_SESSION` / `HERDR_PANE`, schema `ace.hitl.ref/v1`). `ace-herdr deliver` pushes an answer back to that address:
209
+
210
+ 1. A write-ahead delivery record carrying the full answer is persisted under `.ace-local/herdr/deliveries/<event-id>.json` (mode 0600, atomic rename) before any herdr contact, so a crash can never lose the answer. A crashed run is recovered with `--resume <event-id>`.
211
+ 2. Delivery is idempotent per event id and serialized by a per-event lock: concurrent deliveries prompt once. Re-delivering identical content after a `delivered` record short-circuits without contacting herdr; different content or a different destination for the same event id fails closed.
212
+ 3. If the target pane has no agent, one is bootstrapped (`herdr agent start`), the reverse address is exported into the pane shell (`export HERDR_SESSION=... HERDR_PANE=...`; values are token-validated and shell-escaped), and delivery waits for the agent to become idle before prompting.
213
+ 4. Transient failures (readiness timeout, `agent_prompt_stalled`, socket/binary unavailability -- at the probe as well as the prompt) persist their history and report `retryable` within `delivery.max_attempts`.
214
+ 5. Terminal failures (`agent_blocked` pre-send rejection, missing pane, agent start failure) report `failed` immediately with the error persisted in the record history.
215
+ 6. An interrupted run whose last recorded event is a prompt submission without an outcome is ambiguous: the answer may already have been delivered. Re-running reports `failed` ("previous run crashed after submitting") instead of silently resending.
216
+
217
+ Result states follow the ace-hitl contract (spec 8wm.t.vrz §1.2): `delivered`, `retryable` (safe to re-push identical content), `failed` (terminal).
218
+
219
+ ## `ace-herdr dispatch`
220
+
221
+ Start an agent in one command: tab + `herdr agent start` + prompt, all with deterministic defaults.
222
+
223
+ ```bash
224
+ ace-herdr dispatch --label 8wm.t.vs0 --kind pi --prompt-file prompt.md
225
+ ace-herdr dispatch --label review --pane p7 --cwd /path/to/project
226
+ ace-herdr dispatch --label 8wm.t.vs0 --no-prompt
227
+ ```
228
+
229
+ Defaults: the caller's herdr workspace (flag `--workspace` > `HERDR_WORKSPACE_ID` > `herdr pane current`), the label as agent and pane name, the prompt from `--prompt-file` or stdin (`--no-prompt` skips submission). The new agent's environment receives `HERDR_SESSION` (workspace id) and `HERDR_PANE` (pane id) so its own `ace-hitl ask` calls carry a working reverse address.
230
+
231
+ Output: one JSON line `{"workspace":...,"pane":...,"agent":...,"kind":...,"tab_created":...,"prompted":...}`.
232
+
233
+ ## `ace-herdr close`
234
+
235
+ Close out a finished agent pane; optionally rename first.
236
+
237
+ ```bash
238
+ ace-herdr close --pane p5 --rename done # rename, then close
239
+ ace-herdr close --pane p5 --keep --rename wip # rename only
240
+ ```
241
+
242
+ ## `ace-herdr tidy`
243
+
244
+ Report cleanable agent panes and delivery records as one deterministic JSON line; **dry run by default** -- nothing is closed, moved, or removed without `--apply`.
245
+
246
+ ```bash
247
+ ace-herdr tidy # dry run: report candidates, zero side effects
248
+ ace-herdr tidy --apply # close eligible panes, archive old delivered records
249
+ ace-herdr tidy --quiet # suppress the report
250
+ ```
251
+
252
+ Output: `{"apply":false,"retention_days":7,"panes":{"candidates":[...],"preserved":[...],"closed":[...],"excluded":[...]},"deliveries":{"candidates":[...],"protected":[...],"archived":[...],"preserved":[...]}}`. Entries are ordered by pane id / event id; every bucket is always present (an explicit empty array means "nothing to do", never an error).
253
+
254
+ ### Pane closure: positive evidence only
255
+
256
+ A pane qualifies as a `candidate` solely on positive completion evidence:
257
+
258
+ - the agent in the pane was **observed** in state `done` (`agent get`), or
259
+ - the pane has **no foreground process** (`pane process-info` reports an empty process list).
260
+
261
+ `unknown` or unreadable evidence is preserve-only: `unknown` status, a missing field, a malformed response, or a probe failure puts the pane in `preserved` with the reason (`active` / `unknown` / `unreadable`) -- no proof is never treated as dead. Panes whose process could not be read are reported, not closed.
262
+
263
+ With `--apply`, every candidate is **re-probed immediately before mutation**: only a second positive reading closes it (rename to `done`, then close -- the `close` semantics). A candidate that revived in between (`idle`/`working`) or whose fresh probe turned uncertain or failed is moved to `excluded` with the reason and never mutated. A pane that vanished in the meantime is `excluded` as `gone` -- there is nothing left to close.
264
+
265
+ ### Delivery record retention
266
+
267
+ Delivered records under `deliveries_dir` whose `updated_at` is **strictly older** than `tidy.delivered_retention_days` (default 7; an exact-threshold record stays) become `candidates`; with `--apply` they are atomically moved to `deliveries_dir/archive/<event-id>.json` (mode 0600 preserved) and reported in `archived` with the archive path. Age parsing is RFC 3339 only; a delivered record with a missing or invalid `updated_at` is `preserved` (`invalid_updated_at`), never guessed.
268
+
269
+ `pending`, `retryable`, and `failed` records are **never touched** (they are audit/recovery state) and are listed in `protected`. Malformed or unreadable record files are `preserved` as `unreadable`, never removed. Lock files, temp files, and the archive directory itself are ignored. Before each archival the record is re-loaded under its per-event lock: a record that changed after discovery (no longer delivered or no longer old enough) is left in place and reported as `preserved` (`changed`); one that turned unreadable is `preserved` (`unreadable`) without aborting the run. Archival never breaks delivery idempotency: `deliver`/`--resume` consult the archive copy, so re-delivering an archived event still short-circuits (identical content) or fails closed (conflicting content) instead of re-prompting.
270
+
271
+ Errors: if the herdr runtime is unreachable, `tidy` fails with an explicit CLI error before reporting anything (no partial report, no mutation). No LLM is consulted anywhere; the report costs zero tokens.
272
+
273
+ ## Configuration
274
+
275
+ Defaults (`.ace-defaults/herdr/config.yml`), overridable in `~/.ace/herdr/config.yml` or `.ace/herdr/config.yml`:
276
+
277
+ ```yaml
278
+ default_agent_kind: pi # herdr agent kind used for bootstrap/dispatch/preset agents
279
+ delivery:
280
+ max_attempts: 3 # prompt attempts per delivery sequence
281
+ backoff_seconds: [1, 2, 4] # fixed deterministic backoff, no jitter
282
+ timeouts:
283
+ agent_start: 60 # seconds to interactive readiness (also preset agents)
284
+ prompt: 30
285
+ wait: 30 # readiness gate / ace-herdr wait (both wait modes)
286
+ deliveries_dir: .ace-local/herdr/deliveries
287
+ tidy:
288
+ delivered_retention_days: 7 # archive delivered records strictly older than this (tidy)
289
+ ```
290
+
291
+ ## Delivery records
292
+
293
+ Records are JSON, one file per event id under `deliveries_dir` (relative to the working directory, mode 0600): event id, reverse address, SHA-256 answer digest, the full answer (so a crash never loses content), state (`pending` / `delivered` / `retryable` / `failed`), attempt count, and an append-only history of bootstrap, readiness, and prompt events with errors. Records are written atomically and guarded by a per-event lock. Re-running `deliver` with the same event id and content resumes or short-circuits; with different content or a different destination it fails closed.
294
+
295
+ ## Error semantics
296
+
297
+ Commands raise a CLI error (non-zero exit) carrying herdr's machine code where one exists:
298
+
299
+ - Unknown pane/tab/workspace: `pane_not_found: ...`, `tab_not_found: ...`, `workspace_not_found: ...`
300
+ - herdr binary or socket unavailable: explicit CLI error, no partial output
301
+ - Blocked agent: `agent_blocked: ...` (terminal -- never silently dropped); stalled prompt: `agent_prompt_stalled: ...` (transient)
302
+ - Output wait timeout: `timeout: ...`
303
+ - Unknown preset: usage error listing the available preset names
304
+ - Invalid send shapes: usage error **before any transport call** (nothing is sent)
305
+
306
+ Exit `0` means success (including an explicit empty `list` result or a satisfied wait).
data/exe/ace-herdr ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "ace/herdr"
6
+
7
+ # No args -> show help
8
+ args = ARGV.empty? ? ["--help"] : ARGV
9
+
10
+ # Start CLI with exception-based exit code handling (per ADR-023)
11
+ begin
12
+ exit_code = Ace::Herdr::CLI.start(args)
13
+ exit(exit_code) if exit_code.is_a?(Integer) && exit_code.nonzero?
14
+ rescue Ace::Support::Cli::Error => e
15
+ warn e.message
16
+ exit(e.exit_code)
17
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Ace
6
+ module Herdr
7
+ module Atoms
8
+ # SHA-256 hex digest of delivery content; pure function
9
+ module AnswerDigest
10
+ module_function
11
+
12
+ def call(content)
13
+ Digest::SHA256.hexdigest(content.to_s)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require "ace/core"
5
+ require_relative "support"
6
+
7
+ module Ace
8
+ module Herdr
9
+ module CLI
10
+ module Commands
11
+ class Capture < Ace::Support::Cli::Command
12
+ include Ace::Support::Cli::Base
13
+ include Runtime
14
+
15
+ desc <<~DESC.strip
16
+ Print recent pane output as raw text (no JSON wrapping)
17
+
18
+ --source visible reads the current screen; recent (default)
19
+ reads terminal history. Read-only: agent routing rules do not
20
+ apply.
21
+ DESC
22
+
23
+ example [
24
+ "--pane p5",
25
+ "--pane p5 --lines 40",
26
+ "--pane p5 --source visible"
27
+ ]
28
+
29
+ option :pane, type: :string, desc: "Target pane id"
30
+ option :lines, type: :integer, desc: "Number of lines (default: 40)"
31
+ option :source, type: :string, desc: "Snapshot source: visible or recent (default: recent)"
32
+
33
+ def initialize(executor: nil)
34
+ @executor = executor
35
+ end
36
+
37
+ def call(**options)
38
+ translate_errors do
39
+ cli_error("--pane is required") if options[:pane].to_s.empty?
40
+ source = options[:source] || "recent"
41
+ unless %w[visible recent].include?(source)
42
+ cli_error("--source must be visible or recent")
43
+ end
44
+ control = Organisms::ControlSurface.new(executor: executor)
45
+ print control.capture(
46
+ pane: options.fetch(:pane), source: source,
47
+ lines: options[:lines] || Organisms::ControlSurface::DEFAULT_LINES
48
+ )
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require "ace/core"
5
+ require_relative "support"
6
+
7
+ module Ace
8
+ module Herdr
9
+ module CLI
10
+ module Commands
11
+ class Close < Ace::Support::Cli::Command
12
+ include Ace::Support::Cli::Base
13
+ include Runtime
14
+
15
+ desc <<~DESC.strip
16
+ Rename and/or close a finished agent pane, returning the result
17
+ DESC
18
+
19
+ example [
20
+ "--pane p5 --rename done",
21
+ "--pane p5",
22
+ "--pane p5 --keep --rename 'review done'"
23
+ ]
24
+
25
+ option :pane, type: :string, desc: "Target pane id"
26
+ option :rename, type: :string, desc: "New pane label applied before closing"
27
+ option :keep, type: :boolean, desc: "Rename only; do not close the pane"
28
+
29
+ def initialize(executor: nil)
30
+ @executor = executor
31
+ end
32
+
33
+ def call(**options)
34
+ translate_errors do
35
+ cli_error("--pane is required") if options[:pane].to_s.empty?
36
+ renamed = false
37
+ closed = false
38
+ if options[:rename]
39
+ executor.pane_rename(options.fetch(:pane), options[:rename])
40
+ renamed = true
41
+ end
42
+ unless options[:keep]
43
+ executor.pane_close(options.fetch(:pane))
44
+ closed = true
45
+ end
46
+ puts JSON.generate(pane: options.fetch(:pane), renamed: renamed, closed: closed)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end