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 e52ce7a..104e1fe 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. --- @@ -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 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 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 @@ -144,18 +166,32 @@ 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) 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 ` / `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. @@ -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 `` 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}:`) @@ -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 (`:`) — 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. @@ -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 diff --git a/herdr/coordinator/cross-coordinate/SKILL.md b/herdr/coordinator/cross-coordinate/SKILL.md index 79cd609..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 @@ -53,9 +74,23 @@ 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**. + +**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 @@ -88,18 +123,32 @@ 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) 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. 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. @@ -179,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 @@ -291,7 +358,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. 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