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
7 changes: 4 additions & 3 deletions herdr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,10 @@ skills spell them out, and neither is optional reading:
*agent send writes literal text; use pane run when you want command text plus Enter*.)
- **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.
operator input — only `agent read --ansi` can, and only in two steps: locate the composer by its
`❯` glyph (decoding UTF-8), *then* faint-test that one line (`\x1b[2m` = ghost). Faint-testing a
whole read is a false-positive machine — `\x1b[2m` is also ordinary transcript chrome. Never
judge a pane's state, its interruptibility, or the Operator's intent from that line.

## What's here

Expand Down
102 changes: 77 additions & 25 deletions herdr/child/cross-coordinate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,27 +64,36 @@ herdr pane IDs compact when panes close — re-read them every time:
herdr pane list
```

Label→pane_id map, to eyeball all panes at once (avoids fragile `grep`/`tr` parsing):
Label→pane_id map, to eyeball all panes at once (avoids fragile `grep`/`tr` parsing).
Panes without a `label` key appear as `(unlabeled)` so they do not silently disappear or
raise `KeyError`:

```bash
herdr pane list | python -c "import sys,json; [print(p['label'],'->',p['pane_id']) for p in json.load(sys.stdin)['result']['panes']]"
herdr pane list | python -c "import sys,json; [print(p.get('label') or '(unlabeled)','->',p['pane_id']) for p in json.load(sys.stdin)['result']['panes']]"
```

Match by **label** (and cwd as tiebreak): **Coordinator** → label `Coordinator`, cwd =
`{workspace-root}`; each sibling → label `{SiblingApp}`, cwd = `{workspace-root}/{SiblingApp}`.

To **script a send**, don't eyeball the map — resolve one label to its `pane_id`
deterministically (empty string if the pane is gone, so a closed pane yields nothing instead of
raising):
deterministically. The result is an empty string when no pane carries that label. Read
that as "the pane with that label is not currently resolvable in this workspace" —
**not** as "a pane that existed has been closed". Those are different failure modes with
different recoveries:

```bash
# → the pane_id for a given label, or nothing if the pane is gone
herdr pane list | python -c "import sys,json; ps=json.load(sys.stdin)['result']['panes']; print(next((p['pane_id'] for p in ps if p['label']=='Coordinator'), ''))"
# → the pane_id for a given label, or empty string if no pane carries that label.
# .get('label') prevents a KeyError when the workspace contains unlabeled panes.
herdr pane list | python -c "import sys,json; ps=json.load(sys.stdin)['result']['panes']; print(next((p['pane_id'] for p in ps if p.get('label')=='Coordinator'), ''))"
```

Swap `Coordinator` for a sibling label as needed. An empty result means the pane closed — re-run
`herdr pane list` rather than reusing an old ID, and confirm the target pane still exists before
every send.
Swap `Coordinator` for the actual label of the pane you want to reach — the string must
match exactly, including case. When the result is empty, three possibilities are in
play: the pane was closed, the pane exists but has no label, or the label does not match
the current workspace's panes. Distinguish them by re-running `herdr pane list` and
inspecting the live pane set, then either re-resolve with the right label or handle the
missing pane as a separate failure mode. Do not treat "empty result" as proof that a
pane closed.

---

Expand All @@ -105,12 +114,25 @@ arrived"), so get these right before anything else in this skill.
**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.
message was pasted, never submitted. That the CLI carries a *separate*
`pane send-keys <pane> Enter` is the corroboration: submitting is its own act.

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.

**A relay also has a length ceiling, and it reports the wrong cause.** Past roughly 700
characters `pane run` can die with `Error: Os { code: 232, kind: BrokenPipe }`. The error names
a *pipe*, not a size, so it reads as a transient transport glitch worth retrying verbatim — and
the identical retry fails identically. Measured on one target moments apart: ~1050 chars failed,
~700 went through, while a ~700-char send to a different pane in the same call succeeded. Treat
`BrokenPipe` as **"too long"** first.

**Then re-read the shortened retry against what you meant to send.** Trimming to fit is a *scope
edit*, not a formatting one: what you cut is by definition what you judged least important, which
is not the same as unimportant. A relay trimmed to clear the ceiling and never re-checked will
quietly hand the recipient a shorter list of work than you actually found.

### 2. Prose is not a send

Your ordinary output reaches the **Operator only**. It never appears in another pane. Writing
Expand Down Expand Up @@ -144,18 +166,32 @@ 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.
suggestion and real unsent input arrive byte-identical. Blind. `--ansi` separates them — but only
in two steps, and skipping the first is the trap.
- **Step 1 — locate the composer: the LAST `❯` line (U+276F, decoded as UTF-8) that sits between
the input box's final two `─` rules.** Take **both** conditions; each alone has a live failure
mode. The glyph alone hits transcript content — a pane displaying a doc *about* this rule had 5
glyph matches, 4 of them prose, the first faint from line-number chrome, so step 1 + step 2 would
call a diff line a ghost. The rules alone fabricate a composer on a pane that has none — 2 of 8
panes swept (non-Claude agents) had zero `❯` lines while a last-two-rules span happily bracketed
ordinary transcript. **No `❯` line means no visible composer: infer nothing.** Across the 6 panes
that had one it was always the last glyph match. It usually lands within the last two or three
lines — but don't implement a fixed offset; the anchor is the rule. (cp1252 decoding
yields mojibake, which is where the folk advice "don't match the glyph" came from.)
- **Step 2 — on that line only**, test for faint: text wrapped in **`\x1b[2m`** is a ghost; text
with no faint is real typed input.
- **Never faint-test the whole read.** `\x1b[2m` is ordinary transcript chrome — bullet glyphs,
line numbers, tree characters, truncated JSON. A six-pane sweep found faint on 67 lines, **none**
of them a composer ghost. Faint is necessary, not sufficient; position is what decides.
- **The grey `\x1b[38;2;153;153;153m` (`#999`) is the `❯` marker's own colour**, not a ghost tell —
it appears on empty composers with no ghost at all. Don't test for it in either direction.

**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**.
2. If you must inspect composer content: find the `❯` line (UTF-8), then faint-test **that line** —
not the transcript around it.
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.
Expand All @@ -173,9 +209,19 @@ exposing (field names, event names, result keys), **your side** (what you ship),
**checklist** of the cross-app steps for both sides to tick off as the contract firms up.

**A bare tracker ID is repo-local**: a sibling in another repo can't read your `<id>` from its own
cwd. If a sibling will need to read the item itself, hand them a *resolvable* pointer — an absolute
path or a `--path`-style form. (No tracker? Then carry the detail inline in the relay — it just
makes the relay longer.)
cwd. If a sibling will need to read the item itself, hand them a *resolvable* pointer. (No tracker?
Then carry the detail inline in the relay — it just makes the relay longer.)

**The pointer's form is load-bearing, and the wrong form fails only for the recipient:** prefer
a **relative** path and state the cwd it resolves from; if it must be absolute use **forward
slashes** (`C:/path/to/tracker`, never `C:\path\to\tracker` — the backslash form is mangled to
`C:pathtotracker` by the bash layer inside `herdr pane run`); and **never `~`**, because most
tracker CLIs do not expand it and PowerShell does not expand it for a native executable's
argument, so it resolves only for an interactive bash user who leaves it unquoted — the person
who spot-checks it, never the person who receives it.

Verify a pointer by **running it** in the shell the recipient will use, quoted the way they will
quote it. Reading it, or running it in your own shell, proves less than it appears.

### 2. Draft the relay (labeled `From {AppName}:`)

Expand Down Expand Up @@ -260,7 +306,8 @@ read.

## Don't

- Don't hardcode pane IDs (`w5:p3`) — re-discover by label every time.
- Don't hardcode pane IDs (`<workspace>:<pane>`) — re-discover by label every time, and read the
label with `.get`, since a pane may not have one.
- 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.
Expand All @@ -273,12 +320,17 @@ read.
- 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.
ghost suggestion, not typed input. Judge by transcript + `agent_status`; if you must look, locate
the composer line first, then faint-test **that line only**.
- Don't faint-test a whole read to find a ghost — `\x1b[2m` is common transcript chrome and will
flag dozens of innocent lines. Position first, faint second.
- 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`
pointer if they need to read it.
- Don't hand a sibling a bare tracker ID — it's repo-local; give a resolvable `--path` pointer,
relative with the cwd stated, or forward-slash absolute. **Never `~`** — it resolves only for
an unquoted interactive bash user, i.e. the author and not the recipient.
- Don't record a relay as delivered on a clean exit code — confirm it, and treat `BrokenPipe`
as "too long" rather than a transient glitch.
- Don't mark a coordination item completed while its checklist still has unchecked items.
- Don't take another pane's word for the state of a shared artifact when it conflicts with what
I'm seeing — read the file. It cannot be out of date with itself, and two honest panes can be
Expand Down
Loading