From 1c39c9f905e662847c6620d9e51dfbcdd77234b9 Mon Sep 17 00:00:00 2001 From: iRonin Date: Fri, 7 Aug 2026 09:55:33 -0500 Subject: [PATCH 1/6] herdr skills: send-text joins the no-Enter list; faint is the only ghost invariant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two corrections from the panes that adopted this, both verified against the live CLI: - `pane send-text` omits the Enter exactly as `agent send` does. The CLI's own help only names `agent send`, so the omission read as "send-text is fine" — it isn't. That herdr carries a separate `pane send-keys Enter` is the corroboration: submitting is its own act. - A ghost composer suggestion is not reliably grey. One read off a live pane carried `\x1b[2m` with no `38;2;153;153;153` at all, so a detector requiring faint AND grey would have passed it through as real operator input. Faint is the invariant; the grey is incidental. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01U5Rxy1AE7FV6RwmMFMbotY --- herdr/child/cross-coordinate/SKILL.md | 8 +++++--- herdr/coordinator/cross-coordinate/SKILL.md | 13 ++++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/herdr/child/cross-coordinate/SKILL.md b/herdr/child/cross-coordinate/SKILL.md index e52ce7a..67ad388 100644 --- a/herdr/child/cross-coordinate/SKILL.md +++ b/herdr/child/cross-coordinate/SKILL.md @@ -105,7 +105,8 @@ 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 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 Enter` **once** — but prefer one @@ -146,8 +147,9 @@ whenever you read a pane, including your own. - `herdr agent read --source visible` (default `--format text`) **strips ANSI**, so a ghost suggestion and real unsent input arrive byte-identical. Blind. - `herdr agent read --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. + (SGR faint), usually — but **not always** — also grey `\x1b[38;2;153;153;153m` (`#999`). **Real + typed input is stark white** — normal intensity, no faint. The faint code is the invariant; a + filter requiring faint *and* grey will wave some ghosts through as real input. - Filter on the **`\x1b[2m`** code, not on the `❯` glyph — the glyph decodes to surrogate bytes and won't match. diff --git a/herdr/coordinator/cross-coordinate/SKILL.md b/herdr/coordinator/cross-coordinate/SKILL.md index 79cd609..13d12cb 100644 --- a/herdr/coordinator/cross-coordinate/SKILL.md +++ b/herdr/coordinator/cross-coordinate/SKILL.md @@ -53,9 +53,11 @@ pane that sends most, so a silent delivery failure here strands a child that's w **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 Enter` -**once**. +tracking item says "relayed." That the CLI carries a *separate* +`pane send-keys Enter` is the corroboration: submitting is its own act. + +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 Enter` **once**. ### 2. Prose is not a send @@ -90,8 +92,9 @@ has operator input pending, and a ghost that regenerates looks like a pane I can - `herdr agent read --source visible` (default `--format text`) **strips ANSI**, so a ghost suggestion and real unsent input arrive byte-identical. Blind. - `herdr agent read --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. + (SGR faint), usually — but **not always** — also grey `\x1b[38;2;153;153;153m` (`#999`). **Real + typed input is stark white** — normal intensity, no faint. The faint code is the invariant; a + filter requiring faint *and* grey will wave some ghosts through as real input. - Filter on the **`\x1b[2m`** code, not on the `❯` glyph — the glyph decodes to surrogate bytes and won't match. From 840e42303efc2328f86192f867af4f0cfb5283e3 Mon Sep 17 00:00:00 2001 From: iRonin Date: Fri, 7 Aug 2026 10:03:35 -0500 Subject: [PATCH 2/6] herdr skills: the ghost tell needs a position anchor, not just a colour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PersonaForge's review of the previous commit found the detection rule was a false-positive machine, and it was right. Two fixes, both verified against live panes rather than reasoned about: - **Faint is not sufficient.** A six-pane --ansi sweep found `\x1b[2m` on 67 lines across three panes and NOT ONE was a composer ghost — they were line numbers, tool-output tree glyphs, and truncated JSON. "Discard any faint line" over a whole visible read discards most of the transcript. The test needs a position anchor: faint ON THE COMPOSER LINE. - **The composer line IS locatable, contra the previous text.** The claim that `❯` "decodes to surrogate bytes, match on \x1b[2m instead" was a decoding bug in the reader, not a property of herdr's output: decode the same pane as cp1252 and you get mojibake surrogates, decode it as UTF-8 and U+276F is right there. Verified both ways on one pane. So step 1 is find `❯` (as UTF-8), step 2 is faint-test that line — which also closes the hole where the old text told you not to match the glyph while giving you no other way to find the line. Also: the grey `#999` is the `❯` prompt marker's own colour, appearing on empty composers with no ghost at all — it was never a ghost tell in either direction, which is why the earlier "faint plus grey" phrasing kept half-matching reality. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01U5Rxy1AE7FV6RwmMFMbotY --- herdr/README.md | 7 ++++--- herdr/child/cross-coordinate/SKILL.md | 23 ++++++++++++++------- herdr/coordinator/cross-coordinate/SKILL.md | 23 ++++++++++++++------- herdr/coordinator/set-workspace/SKILL.md | 3 ++- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/herdr/README.md b/herdr/README.md index 60d6638..1780db4 100644 --- a/herdr/README.md +++ b/herdr/README.md @@ -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 diff --git a/herdr/child/cross-coordinate/SKILL.md b/herdr/child/cross-coordinate/SKILL.md index 67ad388..31e5f77 100644 --- a/herdr/child/cross-coordinate/SKILL.md +++ b/herdr/child/cross-coordinate/SKILL.md @@ -145,19 +145,26 @@ whenever you read a pane, including your own. **The tell is colour, and only `--ansi` shows it:** - `herdr agent read --source visible` (default `--format text`) **strips ANSI**, so a ghost - suggestion and real unsent input arrive byte-identical. Blind. -- `herdr agent read --source visible --ansi` preserves it: a ghost is wrapped in **`\x1b[2m`** - (SGR faint), usually — but **not always** — also grey `\x1b[38;2;153;153;153m` (`#999`). **Real - typed input is stark white** — normal intensity, no faint. The faint code is the invariant; a - filter requiring faint *and* grey will wave some ghosts through as real input. -- 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 line by its `❯` glyph (U+276F)**, decoding the read as **UTF-8**; + the glyph *is* matchable. (Decode as cp1252 and you get mojibake surrogates — that reader bug is + where the folk advice "don't match the glyph" came from.) Fallback anchor: the composer sits + between the input box's last two `─` rules. +- **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 ` / `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. diff --git a/herdr/coordinator/cross-coordinate/SKILL.md b/herdr/coordinator/cross-coordinate/SKILL.md index 13d12cb..e2b642c 100644 --- a/herdr/coordinator/cross-coordinate/SKILL.md +++ b/herdr/coordinator/cross-coordinate/SKILL.md @@ -90,19 +90,26 @@ has operator input pending, and a ghost that regenerates looks like a pane I can **The tell is colour, and only `--ansi` shows it:** - `herdr agent read --source visible` (default `--format text`) **strips ANSI**, so a ghost - suggestion and real unsent input arrive byte-identical. Blind. -- `herdr agent read --source visible --ansi` preserves it: a ghost is wrapped in **`\x1b[2m`** - (SGR faint), usually — but **not always** — also grey `\x1b[38;2;153;153;153m` (`#999`). **Real - typed input is stark white** — normal intensity, no faint. The faint code is the invariant; a - filter requiring faint *and* grey will wave some ghosts through as real input. -- 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 line by its `❯` glyph (U+276F)**, decoding the read as **UTF-8**; + the glyph *is* matchable. (Decode as cp1252 and you get mojibake surrogates — that reader bug is + where the folk advice "don't match the glyph" came from.) Fallback anchor: the composer sits + between the input box's last two `─` rules. +- **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. 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**. +2. If I 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 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. diff --git a/herdr/coordinator/set-workspace/SKILL.md b/herdr/coordinator/set-workspace/SKILL.md index a4b8a02..89ebf53 100644 --- a/herdr/coordinator/set-workspace/SKILL.md +++ b/herdr/coordinator/set-workspace/SKILL.md @@ -111,7 +111,8 @@ Send each pane's commands sequentially, with a short beat (~0.5s) between them s **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 --ansi` can; -ghosts are faint `\x1b[2m` grey, real input is stark white). Don't try to clear one before +locate the `❯` line, then faint-test *that line only* — `\x1b[2m` elsewhere in a read is +ordinary transcript chrome). 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 From 00e5f39576f97231bc4afe2628df50bafdd8aa16 Mon Sep 17 00:00:00 2001 From: iRonin Date: Fri, 7 Aug 2026 10:11:26 -0500 Subject: [PATCH 3/6] herdr skills: anchor the composer by position AND glyph, not either alone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both adopting panes independently hit the same gap in the previous commit, from opposite sides, and each proposed the anchor the other's data breaks. An eight-pane sweep settles it: take both conditions. - Glyph alone hits transcript. A pane displaying a diff OF THIS DOCUMENT had 5 U+276F matches in 108 lines — 4 were prose, and the first carried faint from line-number chrome, so "first glyph + faint test" returns ghost-detected on an innocent diff line. (Found by MindHive and PersonaForge independently; pleasingly recursive.) - Box-rules alone fabricate a composer where none exists. 2 of 8 panes swept were non-Claude agents with ZERO composer lines, yet a last-two-rules span happily bracketed ordinary transcript. No `❯` line means no visible composer — infer nothing. The composer is the LAST glyph match that also sits between the final two rules. On all 6 panes that had one, it was the last match and exactly 2 lines from the end. Also: the previous commit updated the body but left the retired "discard any faint line" rule verbatim in every Don't list, so the skills contradicted themselves. Caught by MindHive. Fixed here and in the two anti-pattern bullets it should have been paired with all along. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01U5Rxy1AE7FV6RwmMFMbotY --- herdr/child/cross-coordinate/SKILL.md | 19 +++++++++++++------ herdr/coordinator/cross-coordinate/SKILL.md | 17 ++++++++++++----- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/herdr/child/cross-coordinate/SKILL.md b/herdr/child/cross-coordinate/SKILL.md index 31e5f77..18cb4ca 100644 --- a/herdr/child/cross-coordinate/SKILL.md +++ b/herdr/child/cross-coordinate/SKILL.md @@ -147,10 +147,15 @@ whenever you read a pane, including your own. - `herdr agent read --source visible` (default `--format text`) **strips ANSI**, so a ghost 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 line by its `❯` glyph (U+276F)**, decoding the read as **UTF-8**; - the glyph *is* matchable. (Decode as cp1252 and you get mojibake surrogates — that reader bug is - where the folk advice "don't match the glyph" came from.) Fallback anchor: the composer sits - between the input box's last two `─` rules. +- **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, always 2 lines from the end. (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, @@ -282,8 +287,10 @@ 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` diff --git a/herdr/coordinator/cross-coordinate/SKILL.md b/herdr/coordinator/cross-coordinate/SKILL.md index e2b642c..1467922 100644 --- a/herdr/coordinator/cross-coordinate/SKILL.md +++ b/herdr/coordinator/cross-coordinate/SKILL.md @@ -92,10 +92,15 @@ has operator input pending, and a ghost that regenerates looks like a pane I can - `herdr agent read --source visible` (default `--format text`) **strips ANSI**, so a ghost 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 line by its `❯` glyph (U+276F)**, decoding the read as **UTF-8**; - the glyph *is* matchable. (Decode as cp1252 and you get mojibake surrogates — that reader bug is - where the folk advice "don't match the glyph" came from.) Fallback anchor: the composer sits - between the input box's last two `─` rules. +- **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, always 2 lines from the end. (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, @@ -301,7 +306,9 @@ the two: it doesn't depend on me getting the fan-out right. 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. + 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 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. From 2ba32ac031e4c18fd9bf6bb063e36e84b91960b5 Mon Sep 17 00:00:00 2001 From: iRonin Date: Fri, 7 Aug 2026 10:14:25 -0500 Subject: [PATCH 4/6] =?UTF-8?q?herdr=20skills:=20soften=20the=20composer?= =?UTF-8?q?=20offset=20=E2=80=94=20the=20anchor=20is=20the=20rule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PersonaForge's validation run caught an over-generalisation in the last commit. "Always exactly 2 lines from the end" held on all six panes of the first sweep and got written down as a law; their sweep found a pane at 3. Nothing depends on the number — the conjunction anchor does the work — but stating it flatly invites someone to implement a fixed offset, which is precisely the shortcut the rest of the paragraph exists to prevent. Softened to "usually within the last two or three lines" with an explicit don't-use- an-offset note. Their run also measured the failure the earlier text only predicted: against the four reference captures, the two-step rule classifies all four correctly while the original faint-AND-grey rule calls BOTH live ghosts real operator input. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01U5Rxy1AE7FV6RwmMFMbotY --- herdr/child/cross-coordinate/SKILL.md | 3 ++- herdr/coordinator/cross-coordinate/SKILL.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/herdr/child/cross-coordinate/SKILL.md b/herdr/child/cross-coordinate/SKILL.md index 18cb4ca..f44034b 100644 --- a/herdr/child/cross-coordinate/SKILL.md +++ b/herdr/child/cross-coordinate/SKILL.md @@ -154,7 +154,8 @@ whenever you read a pane, including your own. 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, always 2 lines from the end. (cp1252 decoding + 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. diff --git a/herdr/coordinator/cross-coordinate/SKILL.md b/herdr/coordinator/cross-coordinate/SKILL.md index 1467922..3f62f60 100644 --- a/herdr/coordinator/cross-coordinate/SKILL.md +++ b/herdr/coordinator/cross-coordinate/SKILL.md @@ -99,7 +99,8 @@ has operator input pending, and a ghost that regenerates looks like a pane I can 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, always 2 lines from the end. (cp1252 decoding + 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. From 1b4bc083c1c9302c66161e69807ab2d102f3f4c2 Mon Sep 17 00:00:00 2001 From: iRonin Date: Sat, 15 Aug 2026 13:25:24 -0500 Subject: [PATCH 5/6] =?UTF-8?q?herdr=20skills:=20fix=20cross-coordinate=20?= =?UTF-8?q?KeyError=20on=20unlabeled=20panes;=20clarify=20no-match=20?= =?UTF-8?q?=E2=89=A0=20closed-pane?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The L70 eyeball map and L82 script-a-send snippets both subscripted p['label'] directly. Any pane without a label key (e.g., a sibling session opened without a label) raised KeyError and crashed the consumer, including when only eyeballing the map. Three stacked defects: 1. p['label'] raises KeyError on unlabeled panes. 2. next(..., '') collapses three failure modes (pane closed, pane unlabeled, label mismatch) into one empty string. 3. The framing 'empty result means the pane closed' pre-loaded a false conclusion; the failure-mode recovery is different for each of the three. Fix: use .get('label') in both snippets. The eyeball map renders unlabeled panes as (unlabeled) so they remain visible. The script-a-send comment now says 'no pane carries that label' rather than 'pane is gone'. Prose rewritten to enumerate the three possibilities and direct the reader to re-inspect the live pane set. No workspace-specific labels, pane IDs, or project names are introduced; the seed remains generic per the publication rule. --- herdr/child/cross-coordinate/SKILL.md | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/herdr/child/cross-coordinate/SKILL.md b/herdr/child/cross-coordinate/SKILL.md index f44034b..3c9a5a5 100644 --- a/herdr/child/cross-coordinate/SKILL.md +++ b/herdr/child/cross-coordinate/SKILL.md @@ -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. --- From 89dd9dd439a7b3b4f93a57ef631198a279ed6dc6 Mon Sep 17 00:00:00 2001 From: iRonin Date: Sat, 15 Aug 2026 20:05:28 -0500 Subject: [PATCH 6/6] herdr skills: propagate relay-delivery and pointer-form fixes to both seeds The deployed copies were repaired in a live workspace; these are the seeds that regenerate them, so the fixes belong here or they resurface in the next workspace. coordinator seed - was missing the pane-discovery repair entirely. It ships no lookup code, so a subscript scan came back clean; absence of the bug is not presence of the fix, and its prose still tells a reader to match on `label`, which is what mints the KeyError. Adds `.get('label')`, keeps unlabeled panes visible in a listing, and reframes an empty result as "not currently resolvable" rather than "the pane closed" - plus the standing warning not to repair a failed exact match with a substring match, which resolves to whichever pane is listed first. both seeds - relay length ceiling. Past roughly 700 characters `pane run` can die with BrokenPipe; the error names a pipe rather than a size, so it reads as a transient glitch worth retrying verbatim. Also records that trimming a relay to clear the ceiling is a scope edit, not a formatting one. both seeds - pointer form. "an absolute path" was left unqualified, which mints unrunnable pointers: `~` is expanded by neither the tracker CLI nor PowerShell, so it resolves only for an unquoted interactive bash user - the author, never the recipient - and a backslash absolute is mangled by the bash layer inside `pane run`. Prefer relative with a stated cwd; forward slashes when absolute. Also genericises a hardcoded pane id in an example. Co-Authored-By: Claude Opus 5 (1M context) --- herdr/child/cross-coordinate/SKILL.md | 38 +++++++++++--- herdr/coordinator/cross-coordinate/SKILL.md | 55 ++++++++++++++++++++- 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/herdr/child/cross-coordinate/SKILL.md b/herdr/child/cross-coordinate/SKILL.md index 3c9a5a5..104e1fe 100644 --- a/herdr/child/cross-coordinate/SKILL.md +++ b/herdr/child/cross-coordinate/SKILL.md @@ -121,6 +121,18 @@ Keep every relay to a **single line** so it submits cleanly. If a send genuinely `pane send-text` the block and then `herdr pane send-keys 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 @@ -197,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 `` 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}:`) @@ -284,7 +306,8 @@ read. ## Don't -- Don't hardcode pane IDs (`w5:p3`) — re-discover by label every time. +- Don't hardcode pane IDs (`:`) — 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. @@ -303,8 +326,11 @@ read. 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 diff --git a/herdr/coordinator/cross-coordinate/SKILL.md b/herdr/coordinator/cross-coordinate/SKILL.md index 3f62f60..d8f33a1 100644 --- a/herdr/coordinator/cross-coordinate/SKILL.md +++ b/herdr/coordinator/cross-coordinate/SKILL.md @@ -38,6 +38,27 @@ Map by **label** (cwd as tiebreak): `Coordinator` (this pane, cwd = `{workspace- one pane per child app (cwd = `{workspace-root}/`). Take each `pane_id` from the JSON fresh; if labels drifted, re-run rather than reusing an old ID. +**⚠ `label` is an OPTIONAL key — read it with `.get`, never `[...]`.** Panes can exist with no +`label` key at all, so `p['label'] == 'Coordinator'` raises `KeyError` on the **first unlabeled +pane in the list** and never reaches the one you were looking for. Use `p.get('label')`, and +render unlabeled panes explicitly so they cannot silently vanish from a listing: + +```bash +# label→pane_id map; unlabeled panes stay visible instead of disappearing +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']]" +``` + +**A no-match is not a closed pane.** An empty lookup means only *no pane currently carries that +label in this workspace*. That also happens when the label was renamed, when the workspace uses +a longer form (`Foo Coordinator` rather than `Coordinator`), or when two panes' labels both +contain the string you searched for. Read an empty result as **"not currently resolvable"**, not +as "the pane closed" — treat a genuinely missing pane as a separate failure mode, and re-run +`herdr pane list` to see the labels this workspace actually uses before concluding anything. + +**Never repair a failed exact match by switching to a substring match.** If two panes' labels +share a substring, that silently resolves to whichever the JSON lists first — a message delivered +to the wrong pane, accepted and plausible. Fix the label you are matching, not the operator. + ## 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 @@ -59,6 +80,18 @@ tracking item says "relayed." That the CLI carries a *separate* 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 Enter` **once**. +**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 that was trimmed to clear the ceiling +and never re-checked will quietly hand the recipient a shorter list of work than you found. + ### 2. Prose is not a send My ordinary output reaches the **Operator only** — it never appears in a child's pane. Narrating @@ -195,8 +228,26 @@ actually submits, backtick-free, PowerShell-sent if it carries a leading `/` or 2. **The request summary** — the one-line ask. 3. **Related tracker items — as a *resolvable* pointer, never a bare ID.** If tracker IDs are repo-local (each app's tracker is its own), a bare ID isn't readable from the target's repo. - Hand the target a command it can run as-is (an absolute path or a `--path`-style pointer to - the requester's tracker) so it can read the full detail itself. + Hand the target a command it can run as-is, so it can read the full detail itself. + + **The pointer's *form* is load-bearing, and the wrong form fails only for the recipient:** + + - **Prefer relative, and state the cwd it resolves from.** A relative path with no stated cwd + is just a different trap. Sibling panes usually share a parent, which makes one relative + form correct for all of them. + - **If it must be absolute, use forward slashes** — `C:/path/to/tracker`, not + `C:\path\to\tracker`. A backslash path survives a native shell but is mangled by the bash + layer inside `herdr pane run` (`C:pathtotracker`). + - **Never `~`.** Most tracker CLIs do not expand it, and PowerShell does not expand it for a + native executable's argument — so `~/...` resolves *only* for an interactive bash user who + leaves it unquoted. That is precisely the person who spot-checks it and never the person + who receives it. It self-validates for its author and breaks downstream. + - **Never write "or an absolute path" as an alternative.** It reads as flexibility and behaves + as a time bomb: it mints one fresh dead pointer per use, and the advice that generates them + lives where no repo sweep looks. + + 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. 4. **The green light** — they're cleared to work **directly** with the requester now. 5. **The role-play reminder** — label every message they send `From :`. 6. **The check-back reminder** — report back to me (Coordinator) once the coordination is