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
6 changes: 5 additions & 1 deletion herdr/EXAMPLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ Step by step, and *which skill fires when*:

1. **web raises the ask (child `cross-coordinate`, outbound).** web opens a tracking item
(`gh issue` #42 with the contract detail), then injects **one line** into the Coordinator's
pane — never straight to `api`:
pane with `herdr pane run` (the form that supplies the Enter — `send-text` would leave it
sitting unsent in the Coordinator's composer) — never straight to `api`:
> `From web: need GET /users/:id to return avatar_url so the profile page can render it. Needs api. Detail in acme/web#42.`

Then it **stops reaching toward api** and does what web-side work it can while it waits.
Expand Down Expand Up @@ -180,5 +181,8 @@ identical.
out of date with itself. And a reversal goes to *every* pane that got the original.
- **The Coordinator's real job is timing** — protecting a busy pane from interruption and handing
off only when it's safe, escalating to you when it's murky.
- **Delivery is the floor under all of it.** Send with `pane run` (text *plus* Enter) or the
message never submits, and never judge a pane's readiness from its `❯` line — that's usually a
machine-generated ghost suggestion, not something you typed.
- **It scales past two.** Add `project C`, install the child skill with `{AppName}=projC`, and it
joins the same board — no change to the others.
20 changes: 19 additions & 1 deletion herdr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@ instruction is following orders correctly — that's a fan-out failure, not a ju
artifact rule is the backstop for when the fan-out fails anyway, which is why it's the stronger
of the two: it doesn't depend on the Coordinator getting the broadcast right.

### Two delivery mechanics the protocol rests on

The labeling law only works if messages actually arrive. Two things break that silently — both
skills spell them out, and neither is optional reading:

- **`pane run`, always.** `herdr pane run <pane> "<text>"` types the text *and presses Enter*.
`pane send-text` and `agent send` write the characters with **no** Enter, stranding your relay
unsent in the recipient's composer: delivered from your side, invisible from theirs. Most
"the other pane ignored me" incidents are this. (herdr's own `agent --help` says it outright:
*agent send writes literal text; use pane run when you want command text plus Enter*.)
Comment on lines +68 to +72
- **The `❯` composer line lies.** Claude Code auto-fills it with a machine-generated *suggested*
prompt derived from that pane's last turn. A plain-text read can't tell it from real unsent
operator input — only `agent read --ansi` can (ghosts are faint `\x1b[2m` grey; real input is
stark white). Never judge a pane's state, its interruptibility, or the Operator's intent from
that line.

## What's here

| Path | Role | Genericized from | Purpose |
Expand Down Expand Up @@ -100,7 +116,9 @@ Notes:
is a custom skill you may or may not have.
- The platform gotchas the skills call out (Git Bash mangling a leading `/` into a Windows path;
bash command-substituting backticks inside `herdr pane run "..."`) are Windows/Git-Bash
specific — on a pure-POSIX host you can relax them, but they're harmless to keep.
specific — on a pure-POSIX host you can relax them, but they're harmless to keep. The two
delivery mechanics above (`pane run` vs. `send-text`; ghost composer suggestions) are **not**
platform-specific — keep those verbatim.

## Preconditions (all three skills)

Expand Down
97 changes: 89 additions & 8 deletions herdr/child/cross-coordinate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,80 @@ every send.

---

## Sending into another pane — the mechanics that make a message land

Every message in this protocol is **injected input**: you are typing into someone else's live
session. Two failure modes look identical from your side ("I sent it") and from theirs ("nothing
arrived"), so get these right before anything else in this skill.

### 1. Text without an Enter never submits

| Command | What it actually does |
|---------|-----------------------|
| `herdr pane run <pane> "<text>"` | types the text **and presses Enter** — delivered as a turn |
| `herdr pane send-text <pane> "<text>"` | types the text, **no Enter** — it sits unsent in their composer |
| `herdr agent send <target> "<text>"` | same — literal text, **no Enter** |

**Always `pane run`.** A `send-text` relay strands your message on the recipient's `❯` line, where
it looks to them like something they half-typed and to you like a delivered ask. That is the real
cause of most "the Coordinator never answered me" / "the sibling ignored my relay" reports: the
message was pasted, never submitted.

Keep every relay to a **single line** so it submits cleanly. If a send genuinely must span lines,
`pane send-text` the block and then `herdr pane send-keys <pane> Enter` **once** — but prefer one
line plus a pointer to the durable note.
Comment on lines +110 to +112

### 2. Prose is not a send

Your ordinary output reaches the **Operator only**. It never appears in another pane. Writing
"I'll let {SiblingApp} know" is not letting them know — the *only* channel to another pane is an
explicit `pane run`.

### 3. Sends are fire-and-forget

`pane run` prints nothing on success and there is no ack. A response comes back only as a labeled
inbound turn (`From {SiblingApp}:` / `From Coordinator:`), on the recipient's own timing. For a
send that won't draw a reply — a status ping, a close-out — confirm it landed with
`herdr pane read <target> --source recent --lines 20` rather than assuming.

### 4. Two characters that mangle the string

- **Backticks** — bash command-substitutes them inside `herdr pane run "..."`. Write field and
event names as plain text (`user_id` → user_id).
- **A leading `/`** — Git Bash on Windows path-converts it (`/rename` becomes
`C:/Program Files/Git/rename`) and the target receives garbage. Send slash-prefixed payloads via
**PowerShell**, or prefix the bash call with `MSYS_NO_PATHCONV=1`. (Pure-POSIX hosts are fine.)

---

## The `❯` composer line lies — ghost suggestions

Claude Code auto-populates a pane's composer (`❯`) line with a **suggested next prompt** it
generates from that pane's last turn. Nobody typed it. It reads like plausible Operator input
precisely *because* it's derived from the pane's own context — which is what makes it dangerous
whenever you read a pane, including your own.

**The tell is colour, and only `--ansi` shows it:**

- `herdr agent read <pane> --source visible` (default `--format text`) **strips ANSI**, so a ghost
suggestion and real unsent input arrive byte-identical. Blind.
- `herdr agent read <pane> --source visible --ansi` preserves it: a ghost is wrapped in **`\x1b[2m`**
(SGR faint) plus grey `\x1b[38;2;153;153;153m` (`#999`). **Real typed input is stark white** —
normal intensity, no faint.
- Filter on the **`\x1b[2m`** code, not on the `❯` glyph — the glyph decodes to surrogate bytes and
won't match.
Comment on lines +151 to +152

**Rules:**

1. Never read the composer line as pane state or as Operator intent. Judge from the **transcript
above the input box** plus `herdr agent get <pane>` / `herdr agent list` `agent_status`.
2. If you must inspect composer content, read with `--ansi` and **discard any faint line**.
3. **Sending is unaffected** — `pane run` types real characters over whatever placeholder is
showing, so you can never accidentally send a ghost and never need to clear one first. Don't
chase it: Escape-then-run races, and the suggestion regenerates anyway.

---

## Outbound flow — raising an ask

### 1. Back it with a durable note (recommended)
Expand Down Expand Up @@ -120,11 +194,10 @@ turn, the "next convenient interrupt"):
herdr pane run "<coordinator-pane-id>" "From {AppName}: <one-line ask>. Needs <sibling>. Detail in <item-id>."
```

Use `pane run` (text + a real Enter) with a **single-line** message so it submits cleanly. If you
must send multiple lines, `pane send-text` the block then `pane send-keys <pane> Enter` once — but
prefer one line + tracker item. **Never put backtick characters inside the `herdr pane run "..."`
string** — bash command-substitutes them and mangles the send. Write field/event names in plain
text (write `user_id` as user_id, not wrapped in backticks).
`pane run` (not `send-text` / `agent send`) with a **single-line**, backtick-free message — see
*Sending into another pane* above for why each of those matters. It's fire-and-forget: nothing
confirms delivery, so if you want certainty the relay landed, `herdr pane read <coordinator-pane>
--source recent` rather than assuming.

### 4. Then wait — do not touch the sibling

Expand All @@ -142,9 +215,8 @@ your pane clearly labeled — e.g. `From {SiblingApp}:`. That label is how you k
**not** the Operator (unlabeled turns are the Operator; a `From Coordinator:` turn is the
Coordinator).

- Reply into **their** pane, always prefixed `From {AppName}:`. **Never put backtick characters
inside the `herdr pane run "..."` string** — bash command-substitutes them and mangles the send;
write the message in plain text:
- Reply into **their** pane, always prefixed `From {AppName}:`, via `pane run` and in plain text
(no backticks — see *Sending into another pane*):
```bash
herdr pane run "<sibling-pane-id>" "From {AppName}: confirmed - the field ships in the frame. Two questions: ..."
```
Expand Down Expand Up @@ -192,8 +264,17 @@ read.
- Don't open a coordination by messaging a sibling directly — it goes through the Coordinator.
- Don't mistake a `From Coordinator:` turn for the Operator — it's the interrupt-buffer, a distinct
third class.
- Don't deliver a relay with `pane send-text` or `agent send` — they type the text with **no
Enter**, so it sits unsent in the recipient's composer. `pane run` or it didn't happen.
- Don't treat saying something in prose as messaging another pane — your output goes to the
Operator only; the pane hears nothing without an explicit `pane run`.
- Don't assume a no-reply send (status ping, close-out) landed — `pane read --source recent` the
target if it matters.
- Don't put backtick characters inside a `herdr pane run "..."` string — bash will
command-substitute them and mangle the relay.
- Don't read anything into another pane's `❯` composer line — it's usually a machine-generated
ghost suggestion, not typed input. Judge by transcript + `agent_status`; if you must look, read
`--ansi` and discard the faint (`\x1b[2m`) lines.
- Don't send a fat multi-paragraph relay when you have a tracker — the item carries the detail, the
relay is one line + ID.
- Don't hand a sibling a bare tracker ID — it's repo-local; give an absolute-path / `--path`
Expand Down
87 changes: 82 additions & 5 deletions herdr/coordinator/cross-coordinate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,72 @@ Map by **label** (cwd as tiebreak): `Coordinator` (this pane, cwd = `{workspace-
one pane per child app (cwd = `{workspace-root}/<AppName>`). Take each `pane_id` from the JSON
fresh; if labels drifted, re-run rather than reusing an old ID.

## Sending into a child pane — the mechanics that make a message land

Every relay I inject is **typed input into someone else's live session**. As the router I'm the
pane that sends most, so a silent delivery failure here strands a child that's waiting on me.

### 1. Text without an Enter never submits

| Command | What it actually does |
|---------|-----------------------|
| `herdr pane run <pane> "<text>"` | types the text **and presses Enter** — delivered as a turn |
| `herdr pane send-text <pane> "<text>"` | types the text, **no Enter** — it sits unsent in their composer |
| `herdr agent send <target> "<text>"` | same — literal text, **no Enter** |

**Always `pane run`.** A `send-text` green-light strands the whole coordination: the requester
waits for a sibling that never got the ask, the target sees stray text on its `❯` line, and my
tracking item says "relayed." Keep every relay to a **single line** so it submits cleanly; if one
genuinely must span lines, `pane send-text` the block then `herdr pane send-keys <pane> Enter`
**once**.
Comment on lines +56 to +58

### 2. Prose is not a send

My ordinary output reaches the **Operator only** — it never appears in a child's pane. Narrating
"green-lighting {AppB} now" is not green-lighting anyone; only an explicit `pane run` is.

### 3. Sends are fire-and-forget

`pane run` prints nothing on success and there's no ack. A child's answer comes back only as a
`From <App>:` turn on its own timing. That's fine for an ask (the reply *is* the confirmation),
but for a send that draws no reply — a status note, the Step 2c close-out relay — confirm with
`herdr pane read <target> --source recent --lines 20` before recording it as delivered.

### 4. Two characters that mangle the string

- **Backticks** — bash command-substitutes them inside `herdr pane run "..."`. Write field and
event names as plain text (`user_id` → user_id).
- **A leading `/`** — Git Bash on Windows path-converts it (`/rename` becomes
`C:/Program Files/Git/rename`) and the target receives garbage. Send slash-prefixed payloads via
**PowerShell**, or prefix the bash call with `MSYS_NO_PATHCONV=1`. (Pure-POSIX hosts are fine.)

## The `❯` composer line lies — ghost suggestions

Claude Code auto-populates a pane's composer (`❯`) line with a **suggested next prompt** it
generates from that pane's last turn. Nobody typed it. It reads like plausible Operator input
precisely *because* it's derived from the pane's own context. This bites me hardest in Step 2a,
where I'm judging whether a child is safe to interrupt: a ghost makes an idle pane look like it
has operator input pending, and a ghost that regenerates looks like a pane I can't get clean.

**The tell is colour, and only `--ansi` shows it:**

- `herdr agent read <pane> --source visible` (default `--format text`) **strips ANSI**, so a ghost
suggestion and real unsent input arrive byte-identical. Blind.
- `herdr agent read <pane> --source visible --ansi` preserves it: a ghost is wrapped in **`\x1b[2m`**
(SGR faint) plus grey `\x1b[38;2;153;153;153m` (`#999`). **Real typed input is stark white** —
normal intensity, no faint.
- Filter on the **`\x1b[2m`** code, not on the `❯` glyph — the glyph decodes to surrogate bytes and
won't match.
Comment on lines +95 to +96

**Rules:**

1. The composer line is **not** part of the interruptibility test. Classify from the **transcript
above the input box** plus `agent_status` only.
2. If I must inspect composer content, read with `--ansi` and **discard any faint line**.
3. **Sending is unaffected** — `pane run` types real characters over whatever placeholder is
showing, so I can never accidentally send a ghost and never need to clear one first. Don't
chase it: Escape-then-run races, and the suggestion regenerates anyway.

## Step 1 — Receive & parse the relay

From a `From <Origin>:` turn, pull out:
Expand Down Expand Up @@ -83,9 +149,11 @@ herdr agent list # target's agent_status
herdr agent read <target-pane> --source visible --lines ~20
```

Classify what I see:
- **Safe-idle** — `agent_status: idle`, an empty `❯` prompt, no pending question/dialog on
screen → **go now** (Step 2b).
Classify what I see — from the **transcript and `agent_status`**, never from the `❯` composer line
(whatever sits there is almost always a ghost suggestion, not operator input — see *The `❯`
composer line lies*):
- **Safe-idle** — `agent_status: idle`, the last turn visibly finished, no pending
question/dialog on screen → **go now** (Step 2b).
- **Working** — `agent_status: working` / actively mid-task → **don't interrupt.** Either hand to
the Operator (below) or set a light background re-check
(`herdr agent wait <target> --status idle` run in the background, or a short poll loop), then
Expand All @@ -103,8 +171,9 @@ go-signal to run Step 2b for the buffered ask.

### 2b. Release the ask — relay + green-light

Inject one `From Coordinator:` message into the **target's** pane (via `pane run`; send
slash-free text so Git Bash is fine, but PowerShell is always safe). It must carry:
Inject one `From Coordinator:` message into the **target's** pane — one line, via `pane run` so it
actually submits, backtick-free, PowerShell-sent if it carries a leading `/` or a Windows path
(see *Sending into a child pane*). It must carry:

1. **Who's seeking help** — the requesting child's name **and pane_id**.
2. **The request summary** — the one-line ask.
Expand Down Expand Up @@ -220,6 +289,14 @@ the two: it doesn't depend on me getting the fan-out right.
Operator's.
- Don't interrupt a `working` or awaiting-input target on my own judgment — safe-idle only; when
murky, hand the timing to the Operator and wait for "now is a good time."
- Don't judge interruptibility (or Operator intent) from a pane's `❯` composer line — it's
normally a machine-generated ghost suggestion. Transcript + `agent_status` only; if I must look,
read `--ansi` and discard the faint (`\x1b[2m`) lines.
- Don't deliver a relay with `pane send-text` or `agent send` — no Enter means it sits unsent in
the target's composer while I record it as relayed. `pane run` or it didn't happen.
- Don't count narrating a green-light as sending one — prose reaches the Operator, not the pane.
- Don't record a no-reply send (a close-out, a status note) as delivered without a
`pane read --source recent` on the target.
- Don't forget the three reminders in the green-light message: pane of the requester,
`From <App>:` role-play, and check back with me on completion.
- Don't cite a cross-repo tracker item as a bare ID in a relay — the target's tracker is local
Expand Down
20 changes: 16 additions & 4 deletions herdr/coordinator/set-workspace/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,16 @@ herdr pane rename <AppB> "{AppB}"
## Step 3 — Drive each agent's Claude-side setup

Inject these as real typed input via `herdr pane run <pane> "<slash-command>"` — one call per
line, in order. **On Windows, send them through the PowerShell tool** (or Bash prefixed with
`MSYS_NO_PATHCONV=1`): Git Bash mangles a leading `/` into a Windows path
(`/rename` → `C:/Program Files/Git/rename`) and the command arrives as garbage. (Pure-POSIX
hosts don't have this trap.)
line, in order. Two things about that:

- **`pane run` is the only form that submits.** It types the text *and* presses Enter;
`pane send-text` / `agent send` write the characters with **no** Enter, leaving the slash
command sitting unexecuted in the composer. A pane that "ignored" its `/rename` was almost
always sent without an Enter.
Comment on lines +76 to +79
- **On Windows, send them through the PowerShell tool** (or Bash prefixed with
`MSYS_NO_PATHCONV=1`): Git Bash mangles a leading `/` into a Windows path
(`/rename` → `C:/Program Files/Git/rename`) and the command arrives as garbage. (Pure-POSIX
hosts don't have this trap.)

Per child pane, in order — pick a distinct `/color` for each so panes are visually separable:

Expand Down Expand Up @@ -102,6 +108,12 @@ Send each pane's commands sequentially, with a short beat (~0.5s) between them s
`herdr pane send-keys <pane> Escape` so the pane returns to a clean `❯` prompt — do this before
Step 4, since an open RC panel will eat the next injected input.

**A caveat on reading `❯` here:** Claude Code auto-fills the composer with a *suggested* next
prompt derived from that pane's last turn. It is not operator input and does not mean the pane is
dirty or busy — a plain-text read can't distinguish it (only `herdr agent read <pane> --ansi` can;
ghosts are faint `\x1b[2m` grey, real input is stark white). Don't try to clear one before
injecting: `pane run` types over any placeholder, so just send.

Because `/rc` opens the panel, keep it **last** of the three per pane (as ordered above), and
Esc-dismiss it. No verification pass on rename/color — fire and move on.

Expand Down