cdragon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +110 -0
  2. package/bin/cdragon.js +170 -0
  3. package/package.json +31 -0
  4. package/skills/agent-browser/SKILL.md +50 -0
  5. package/skills/grill-me/SKILL.md +7 -0
  6. package/skills/herdr-agent/SKILL.md +142 -0
  7. package/skills/herdr-cli/SKILL.md +388 -0
  8. package/skills/herdr-cli/scripts/herdr-agent-run-and-wait +203 -0
  9. package/skills/herdr-cli/scripts/herdr-agent-wait-complete +168 -0
  10. package/skills/notion-presentation/SKILL.md +170 -0
  11. package/skills/notion-presentation/references/example-redis-deck.md +97 -0
  12. package/skills/setup-matt-pocock-skills/SKILL.md +127 -0
  13. package/skills/setup-matt-pocock-skills/domain.md +51 -0
  14. package/skills/setup-matt-pocock-skills/issue-tracker-github.md +34 -0
  15. package/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md +35 -0
  16. package/skills/setup-matt-pocock-skills/issue-tracker-local.md +19 -0
  17. package/skills/setup-matt-pocock-skills/triage-labels.md +15 -0
  18. package/skills/tdd/SKILL.md +108 -0
  19. package/skills/tdd/mocking.md +59 -0
  20. package/skills/tdd/refactoring.md +10 -0
  21. package/skills/tdd/tests.md +61 -0
  22. package/skills/to-html/SKILL.md +83 -0
  23. package/skills/to-html/designs/INDEX.md +74 -0
  24. package/skills/to-html/designs/airbnb.DESIGN.md +581 -0
  25. package/skills/to-html/designs/airtable.DESIGN.md +275 -0
  26. package/skills/to-html/designs/alipay.DESIGN.md +456 -0
  27. package/skills/to-html/designs/apple.DESIGN.md +566 -0
  28. package/skills/to-html/designs/banksalad.DESIGN.md +621 -0
  29. package/skills/to-html/designs/channeltalk.DESIGN.md +374 -0
  30. package/skills/to-html/designs/clay.DESIGN.md +398 -0
  31. package/skills/to-html/designs/clickhouse.DESIGN.md +374 -0
  32. package/skills/to-html/designs/cohere.DESIGN.md +361 -0
  33. package/skills/to-html/designs/coinone.DESIGN.md +218 -0
  34. package/skills/to-html/designs/coupang.DESIGN.md +502 -0
  35. package/skills/to-html/designs/cursor.DESIGN.md +416 -0
  36. package/skills/to-html/designs/elevenlabs.DESIGN.md +376 -0
  37. package/skills/to-html/designs/expo.DESIGN.md +373 -0
  38. package/skills/to-html/designs/figma.DESIGN.md +490 -0
  39. package/skills/to-html/designs/framer.DESIGN.md +393 -0
  40. package/skills/to-html/designs/freee.DESIGN.md +572 -0
  41. package/skills/to-html/designs/gangnamunni.DESIGN.md +621 -0
  42. package/skills/to-html/designs/gmarket.DESIGN.md +483 -0
  43. package/skills/to-html/designs/gogolook.DESIGN.md +131 -0
  44. package/skills/to-html/designs/hahow.DESIGN.md +158 -0
  45. package/skills/to-html/designs/hashicorp.DESIGN.md +369 -0
  46. package/skills/to-html/designs/hyundaicard.DESIGN.md +177 -0
  47. package/skills/to-html/designs/ibm.DESIGN.md +420 -0
  48. package/skills/to-html/designs/kakaobank.DESIGN.md +548 -0
  49. package/skills/to-html/designs/kakaopay.DESIGN.md +544 -0
  50. package/skills/to-html/designs/karrot.DESIGN.md +445 -0
  51. package/skills/to-html/designs/kdan.DESIGN.md +160 -0
  52. package/skills/to-html/designs/krds.DESIGN.md +997 -0
  53. package/skills/to-html/designs/line.DESIGN.md +431 -0
  54. package/skills/to-html/designs/linear.app.DESIGN.md +548 -0
  55. package/skills/to-html/designs/miro.DESIGN.md +272 -0
  56. package/skills/to-html/designs/mistral.ai.DESIGN.md +353 -0
  57. package/skills/to-html/designs/money-forward.DESIGN.md +401 -0
  58. package/skills/to-html/designs/mongodb.DESIGN.md +357 -0
  59. package/skills/to-html/designs/naver.DESIGN.md +533 -0
  60. package/skills/to-html/designs/nhncloud.DESIGN.md +174 -0
  61. package/skills/to-html/designs/opencode.ai.DESIGN.md +388 -0
  62. package/skills/to-html/designs/pinterest.DESIGN.md +322 -0
  63. package/skills/to-html/designs/posthog.DESIGN.md +430 -0
  64. package/skills/to-html/designs/raycast.DESIGN.md +422 -0
  65. package/skills/to-html/designs/remember.DESIGN.md +460 -0
  66. package/skills/to-html/designs/resend.DESIGN.md +396 -0
  67. package/skills/to-html/designs/sanity.DESIGN.md +449 -0
  68. package/skills/to-html/designs/sendbird.DESIGN.md +285 -0
  69. package/skills/to-html/designs/smarthr.DESIGN.md +404 -0
  70. package/skills/to-html/designs/socar.DESIGN.md +403 -0
  71. package/skills/to-html/designs/spotify.DESIGN.md +265 -0
  72. package/skills/to-html/designs/supabase.DESIGN.md +348 -0
  73. package/skills/to-html/designs/superhuman.DESIGN.md +414 -0
  74. package/skills/to-html/designs/together.ai.DESIGN.md +356 -0
  75. package/skills/to-html/designs/toss.DESIGN.md +655 -0
  76. package/skills/to-html/designs/uber.DESIGN.md +387 -0
  77. package/skills/to-html/designs/upstage.DESIGN.md +232 -0
  78. package/skills/to-html/designs/velog.DESIGN.md +168 -0
  79. package/skills/to-html/designs/vercel.DESIGN.md +479 -0
  80. package/skills/to-html/designs/wanted.DESIGN.md +529 -0
  81. package/skills/to-html/designs/wise.DESIGN.md +276 -0
  82. package/skills/to-html/designs/yanolja.DESIGN.md +463 -0
  83. package/skills/to-html/designs/yeogiotte.DESIGN.md +459 -0
  84. package/skills/to-html/designs/zapier.DESIGN.md +433 -0
  85. package/skills/to-html/designs/zigzag.DESIGN.md +633 -0
  86. package/skills/to-issues/SKILL.md +84 -0
  87. package/skills/to-prd/SKILL.md +75 -0
  88. package/src/colors.js +15 -0
  89. package/src/link.js +47 -0
  90. package/src/prompt.js +137 -0
  91. package/src/skills.js +75 -0
@@ -0,0 +1,388 @@
1
+ ---
2
+ name: herdr-cli
3
+ description: "Control herdr from inside it. Manage workspaces and tabs, split panes, spawn agents, read output, and wait for state changes — all via CLI commands that talk to the running herdr instance over a local unix socket. Use when running inside herdr (HERDR_ENV=1)."
4
+ ---
5
+
6
+ # herdr — agent skill
7
+
8
+ before using this skill, check that `HERDR_ENV=1`. if it is not set to `1`, say you are not running inside a herdr-managed pane and stop. do not inspect or control the focused herdr pane from outside herdr.
9
+
10
+ you are running inside herdr, a terminal-native agent multiplexer. herdr gives you workspaces, tabs, and panes — each pane is a real terminal with its own shell, agent, server, or log stream — and you can control all of it from the cli.
11
+
12
+ this means you can:
13
+
14
+ - see what other panes and agents are doing
15
+ - create tabs for separate subcontexts inside one workspace
16
+ - split panes and run commands in them
17
+ - start servers, watch logs, and run tests in sibling panes
18
+ - wait for specific output before continuing
19
+ - wait for another agent to finish
20
+ - spawn more agent instances
21
+
22
+ the `herdr` binary is available in your PATH. its workspace, tab, pane, and wait commands talk to the running herdr instance over a local unix socket.
23
+
24
+ if you need the raw protocol or full api reference, read the [socket api docs](https://herdr.dev/docs/socket-api/).
25
+
26
+ ## concepts
27
+
28
+ **workspaces** are project contexts. each workspace has one or more tabs. unless manually renamed, a workspace's label follows the first tab's root pane — usually the repo name, otherwise the root pane's current folder name.
29
+
30
+ **tabs** are subcontexts inside a workspace. each tab has one or more panes.
31
+
32
+ **panes** are terminal splits inside a tab. each pane runs its own process — a shell, an agent, a server, anything.
33
+
34
+ **agent status** is detected automatically by herdr. the api exposes one public field for it:
35
+
36
+ - `agent_status` — `idle`, `working`, `blocked`, `done`, `unknown`
37
+
38
+ `done` means the agent finished, but you have not looked at that finished pane yet.
39
+
40
+ plain shells still exist as panes, but herdr's sidebar agent section intentionally focuses on detected agents rather than listing every shell.
41
+
42
+ **ids** — workspace ids look like `1`, `2`. tab ids look like `1:1`, `1:2`, `2:1`. pane ids look like `1-1`, `1-2`, `2-1`. these are compact public ids for the current live session.
43
+
44
+ important: ids can compact when tabs, panes, or workspaces are closed. do not treat them as durable ids. re-read ids from `workspace list`, `tab list`, `pane list`, or create/split responses when you need a current id. do not guess that an older `1-3` is still the same pane later.
45
+
46
+ ## discover yourself
47
+
48
+ **you are the pane named by `$HERDR_PANE_ID`.** herdr injects this env var into every pane's shell, and your cli commands inherit it. resolve it to the current public ids — pane, tab, and workspace — with one call:
49
+
50
+ ```bash
51
+ herdr pane get "$HERDR_PANE_ID"
52
+ ```
53
+
54
+ the response holds your `pane_id`, `tab_id`, and `workspace_id`. "current workspace" and "current tab" mean _these_ — the ones your agent pane lives in. when the task says to test in the current workspace/tab, split or create from these ids, not from whatever else is on screen.
55
+
56
+ do **not** use `focused` to find yourself. `focused:true` is whichever pane the user's herdr ui is looking at right now — often a different agent's pane entirely. when several agents run at once, multiple panes show `agent_status: working` and your own pane is usually `focused:false`. the only reliable self-signal is `$HERDR_PANE_ID`.
57
+
58
+ if `$HERDR_PANE_ID` is somehow unset, fall back to matching: compare each candidate pane's `cwd` to your working directory, and if still ambiguous, `pane read` each one and look for _this_ conversation's output on screen.
59
+
60
+ see every pane and its neighbors:
61
+
62
+ ```bash
63
+ herdr pane list
64
+ ```
65
+
66
+ list workspaces:
67
+
68
+ ```bash
69
+ herdr workspace list
70
+ ```
71
+
72
+ ## tab management
73
+
74
+ list tabs in the current workspace:
75
+
76
+ ```bash
77
+ herdr tab list --workspace 1
78
+ ```
79
+
80
+ create a new tab:
81
+
82
+ ```bash
83
+ herdr tab create --workspace 1
84
+ ```
85
+
86
+ without `--label`, the new tab keeps the default numbered tab name.
87
+
88
+ create and name it in one step:
89
+
90
+ ```bash
91
+ herdr tab create --workspace 1 --label "logs"
92
+ ```
93
+
94
+ rename it:
95
+
96
+ ```bash
97
+ herdr tab rename 1:2 "logs"
98
+ ```
99
+
100
+ focus it:
101
+
102
+ ```bash
103
+ herdr tab focus 1:2
104
+ ```
105
+
106
+ close it:
107
+
108
+ ```bash
109
+ herdr tab close 1:2
110
+ ```
111
+
112
+ ## read another pane
113
+
114
+ see what is on another pane's screen:
115
+
116
+ ```bash
117
+ herdr pane read 1-1 --source recent --lines 50
118
+ ```
119
+
120
+ - `--source visible` = current viewport
121
+ - `--source recent` = recent scrollback as rendered in the pane
122
+ - `--source recent-unwrapped` = recent terminal text with soft wraps joined back together
123
+
124
+ ## split a pane and run a command
125
+
126
+ split your pane to the right and keep focus on your current pane:
127
+
128
+ ```bash
129
+ herdr pane split 1-2 --direction right --no-focus
130
+ ```
131
+
132
+ that prints json with the new pane nested at `result.pane.pane_id`. parse that value, then run a command in that pane:
133
+
134
+ ```bash
135
+ NEW_PANE=$(herdr pane split 1-2 --direction right --no-focus | python3 -c 'import sys,json; print(json.load(sys.stdin)["result"]["pane"]["pane_id"])')
136
+ herdr pane run "$NEW_PANE" "npm run dev"
137
+ ```
138
+
139
+ split downward instead:
140
+
141
+ ```bash
142
+ herdr pane split 1-2 --direction down --no-focus
143
+ ```
144
+
145
+ ## wait for output
146
+
147
+ block until specific text appears in a pane. useful for waiting on servers, builds, and tests.
148
+
149
+ for `--source recent`, matching uses unwrapped recent terminal text, so pane width and soft wrapping do not break matches. `pane read --source recent` still shows the pane as rendered. if you want to inspect the same transcript that the waiter matches, use `pane read --source recent-unwrapped`.
150
+
151
+ ```bash
152
+ herdr wait output 1-3 --match "ready on port 3000" --timeout 30000
153
+ ```
154
+
155
+ with regex:
156
+
157
+ ```bash
158
+ herdr wait output 1-3 --match "server.*ready" --regex --timeout 30000
159
+ ```
160
+
161
+ if it times out, exit code is `1`.
162
+
163
+ ## wait for an agent status
164
+
165
+ block until another agent reaches a specific status:
166
+
167
+ ```bash
168
+ herdr wait agent-status 1-1 --status done --timeout 60000
169
+ ```
170
+
171
+ use this when you want the same `done` / `idle` distinction the UI shows.
172
+
173
+ ## wait for an agent task to complete
174
+
175
+ `wait agent-status` is level-triggered: if the pane is already in the requested status, it returns immediately. so a stale `idle` or `done` from a previous task looks identical to a fresh completion. there is no native OR between `idle` and `done`, and no `--wait done` shortcut.
176
+
177
+ two helper scripts wrap this safely. resolve them relative to this `SKILL.md`:
178
+
179
+ ```bash
180
+ HERDR_CLI_SKILL_DIR=<directory containing this SKILL.md>
181
+ RUN_WAIT="$HERDR_CLI_SKILL_DIR/scripts/herdr-agent-run-and-wait"
182
+ WAIT_COMPLETE="$HERDR_CLI_SKILL_DIR/scripts/herdr-agent-wait-complete"
183
+ ```
184
+
185
+ both treat `idle` and `done` as completion, and `blocked` as needs-attention.
186
+
187
+ ### send a new task and wait — `herdr-agent-run-and-wait`
188
+
189
+ use this whenever you are about to send a prompt. it records the pane's baseline status, sends the prompt, then waits for `working`, `idle`, `done`, or `blocked`:
190
+
191
+ ```bash
192
+ "$RUN_WAIT" 1-3 "review the test coverage in src/api/" --timeout 120000
193
+ herdr pane read 1-3 --source recent-unwrapped --lines 120
194
+ ```
195
+
196
+ recording the baseline before sending is what makes it safe: it only counts a _new_ terminal status as completion, not a leftover one. if the task is so fast it returns to its previous status without the helper ever seeing `working`, the helper times out instead of guessing — read the pane and verify manually in that case.
197
+
198
+ ### wait on an already-running task — `herdr-agent-wait-complete`
199
+
200
+ use this only when the task is already in flight and you did not start it through `run-and-wait`. it first waits briefly for `working` (so a pre-task `idle`/`done` is not mistaken for completion), then races `idle` / `done` / `blocked`:
201
+
202
+ ```bash
203
+ "$WAIT_COMPLETE" 1-3 --timeout 120000
204
+ herdr pane read 1-3 --source recent-unwrapped --lines 120
205
+ ```
206
+
207
+ if the task already finished before this helper starts, it can fail because it never sees `working`. it can run for up to `--start-timeout + --timeout` wall-clock time.
208
+
209
+ if you are sure the task is running and only want to race the terminal statuses, skip the working check:
210
+
211
+ ```bash
212
+ "$WAIT_COMPLETE" 1-3 --no-wait-working --timeout 120000
213
+ ```
214
+
215
+ `--no-wait-working` is unsafe for fresh tasks: it can treat an existing `idle` or `done` as completion. prefer `run-and-wait` for new prompts.
216
+
217
+ ### exit codes (both helpers)
218
+
219
+ - `0` — completed as `idle` or `done`.
220
+ - `1` — failed or timed out.
221
+ - `2` — reached `blocked`; read the pane and respond instead of waiting longer.
222
+
223
+ on timeout, inspect in this order:
224
+
225
+ ```bash
226
+ herdr pane get 1-3
227
+ herdr pane read 1-3 --source recent-unwrapped --lines 120
228
+ herdr pane list
229
+ ```
230
+
231
+ run one task at a time per agent pane; queued tasks make status attribution ambiguous. for deterministic shell commands, prefer `wait output` on the command's own output over these agent-status helpers.
232
+
233
+ ## send text or keys to a pane
234
+
235
+ send text without pressing Enter:
236
+
237
+ ```bash
238
+ herdr pane send-text 1-1 "hello from claude"
239
+ ```
240
+
241
+ press Enter or other keys:
242
+
243
+ ```bash
244
+ herdr pane send-keys 1-1 Enter
245
+ ```
246
+
247
+ `send-keys` accepts only these named keys:
248
+
249
+ ```
250
+ Enter Tab Esc Backspace Up Down Left Right C-c ctrl+c
251
+ ```
252
+
253
+ Lowercase spellings also work for the basic named keys. Single-character keys also
254
+ work. For keys not on the named-key list — notably **Shift+Tab / BackTab** (e.g.
255
+ to cycle Claude's permission mode) — send the raw escape with `send-text`:
256
+
257
+ ```bash
258
+ herdr pane send-text 1-1 $'\e[Z' # Shift+Tab (BackTab)
259
+ ```
260
+
261
+ `pane run` sends the text and then a real `Enter` key in one request:
262
+
263
+ ```bash
264
+ herdr pane run 1-1 "echo hello"
265
+ ```
266
+
267
+ ## workspace management
268
+
269
+ create a new workspace:
270
+
271
+ ```bash
272
+ herdr workspace create --cwd /path/to/project
273
+ ```
274
+
275
+ without `--label`, the new workspace keeps the default cwd-based name.
276
+
277
+ create and name one in one step:
278
+
279
+ ```bash
280
+ herdr workspace create --cwd /path/to/project --label "api server"
281
+ ```
282
+
283
+ create one without focusing it:
284
+
285
+ ```bash
286
+ herdr workspace create --no-focus
287
+ ```
288
+
289
+ focus a workspace:
290
+
291
+ ```bash
292
+ herdr workspace focus 2
293
+ ```
294
+
295
+ rename:
296
+
297
+ ```bash
298
+ herdr workspace rename 1 "api server"
299
+ ```
300
+
301
+ close:
302
+
303
+ ```bash
304
+ herdr workspace close 2
305
+ ```
306
+
307
+ ## close a pane
308
+
309
+ ```bash
310
+ herdr pane close 1-3
311
+ ```
312
+
313
+ ## recipes
314
+
315
+ ### run a server and wait until it is ready
316
+
317
+ ```bash
318
+ NEW_PANE=$(herdr pane split 1-2 --direction right --no-focus | python3 -c 'import sys,json; print(json.load(sys.stdin)["result"]["pane"]["pane_id"])')
319
+ herdr pane run "$NEW_PANE" "npm run dev"
320
+ herdr wait output "$NEW_PANE" --match "ready" --timeout 30000
321
+ herdr pane read "$NEW_PANE" --source recent --lines 20
322
+ ```
323
+
324
+ ### run tests in a separate pane and inspect the result
325
+
326
+ ```bash
327
+ herdr pane split 1-2 --direction down --no-focus
328
+ herdr pane run 1-3 "cargo test"
329
+ herdr wait output 1-3 --match "test result" --timeout 60000
330
+ herdr pane read 1-3 --source recent --lines 30
331
+ ```
332
+
333
+ ### check what another agent is working on
334
+
335
+ ```bash
336
+ herdr pane list
337
+ herdr pane read 1-1 --source recent --lines 80
338
+ ```
339
+
340
+ ### watch another pane robustly
341
+
342
+ use this pattern when you need to coordinate with a sibling pane:
343
+
344
+ ```bash
345
+ # inspect what is already there
346
+ herdr pane read 1-3 --source recent --lines 40
347
+
348
+ # wait only for the next output you expect
349
+ herdr wait output 1-3 --match "ready" --timeout 30000
350
+
351
+ # if you need to inspect the same transcript the waiter matched,
352
+ # read the unwrapped recent text directly
353
+ herdr pane read 1-3 --source recent-unwrapped --lines 40
354
+ ```
355
+
356
+ ### spawn a new agent and give it a task
357
+
358
+ ```bash
359
+ herdr pane split 1-2 --direction right --no-focus
360
+ herdr pane run 1-3 "claude"
361
+ herdr wait output 1-3 --match ">" --timeout 15000
362
+ "$RUN_WAIT" 1-3 "review the test coverage in src/api/" --timeout 120000
363
+ herdr pane read 1-3 --source recent-unwrapped --lines 120
364
+ ```
365
+
366
+ see [wait for an agent task to complete](./scripts/herdr-agent-wait-complete) for how `$RUN_WAIT` is resolved and why it is safer than a bare `wait agent-status`.
367
+
368
+ ### coordinate with another agent
369
+
370
+ ```bash
371
+ herdr wait agent-status 1-1 --status done --timeout 120000
372
+ herdr pane read 1-1 --source recent --lines 100
373
+ ```
374
+
375
+ ## notes
376
+
377
+ - `workspace list`, `workspace create`, `tab list`, `tab create`, `tab get`, `tab focus`, `tab rename`, `tab close`, `pane list`, `pane get`, `pane split`, `wait output`, and `wait agent-status` print json on success.
378
+ - `pane read` prints text, not json.
379
+ - `pane read --format ansi` or `pane read --ansi` returns a rendered ANSI snapshot for TUI feedback loops.
380
+ - `pane read --source recent-unwrapped` is useful when you want to inspect the same unwrapped transcript that `wait output --source recent` matches against.
381
+ - `pane send-text`, `pane send-keys`, and `pane run` print nothing on success.
382
+ - the `scripts/herdr-agent-run-and-wait` and `scripts/herdr-agent-wait-complete` helpers (resolved relative to this `SKILL.md`) print json and wrap `wait agent-status` so a stale `idle` / `done` is not mistaken for a fresh completion. use `run-and-wait` when sending a new task, `wait-complete` only for a task already in flight.
383
+ - parse ids from `workspace create`, `tab create`, and `pane split` responses when you need new ids. `workspace create` returns `result.workspace`, `result.tab`, and `result.root_pane`. `tab create` returns `result.tab` and `result.root_pane`. for `pane split`, the new pane id is at `result.pane.pane_id`.
384
+ - use `pane read` for current output that already exists. use `wait output` for future output you expect next.
385
+ - `--no-focus` on split, tab create, and workspace create keeps your current terminal context focused.
386
+ - without `--label`, workspace create keeps cwd-based naming and tab create keeps numbered naming.
387
+ - `--label` on tab create and workspace create applies the custom name immediately.
388
+ - if you are running inside herdr, the `HERDR_ENV` environment variable is set to `1`.
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import json
4
+ import subprocess
5
+ import sys
6
+ import time
7
+
8
+
9
+ COMPLETION_STATUSES = ("idle", "done")
10
+ ATTENTION_STATUSES = ("blocked",)
11
+
12
+
13
+ def run_json(args):
14
+ result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
15
+ if result.returncode != 0:
16
+ return None, result
17
+ try:
18
+ return json.loads(result.stdout), result
19
+ except json.JSONDecodeError:
20
+ return None, result
21
+
22
+
23
+ def pane_status(pane_id):
24
+ data, result = run_json(["herdr", "pane", "get", pane_id])
25
+ if data is None:
26
+ return None, result
27
+ return data["result"]["pane"].get("agent_status"), result
28
+
29
+
30
+ def popen(args):
31
+ return subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
32
+
33
+
34
+ def wait_status(pane_id, status, timeout_ms):
35
+ return popen(["herdr", "wait", "agent-status", pane_id, "--status", status, "--timeout", str(timeout_ms)])
36
+
37
+
38
+ def finish_process(proc):
39
+ stdout, stderr = proc.communicate()
40
+ return stdout.strip(), stderr.strip()
41
+
42
+
43
+ def stop_processes(procs):
44
+ for proc in procs:
45
+ if proc.poll() is None:
46
+ proc.terminate()
47
+ try:
48
+ proc.wait(timeout=2)
49
+ except subprocess.TimeoutExpired:
50
+ proc.kill()
51
+
52
+
53
+ def parse_event(stdout):
54
+ if not stdout:
55
+ return None
56
+ try:
57
+ return json.loads(stdout)
58
+ except json.JSONDecodeError:
59
+ return stdout
60
+
61
+
62
+ def emit(payload, stream=sys.stdout):
63
+ print(json.dumps(payload), file=stream)
64
+
65
+
66
+ def remaining_timeout_ms(deadline):
67
+ return max(1, int((deadline - time.time()) * 1000))
68
+
69
+
70
+ def completion_statuses_to_watch(baseline_status, saw_working):
71
+ if saw_working:
72
+ return COMPLETION_STATUSES
73
+ return tuple(status for status in COMPLETION_STATUSES if status != baseline_status)
74
+
75
+
76
+ def build_waiters(pane_id, timeout_ms, baseline_status, saw_working):
77
+ statuses = []
78
+ if not saw_working:
79
+ statuses.append("working")
80
+ statuses.extend(completion_statuses_to_watch(baseline_status, saw_working))
81
+ statuses.extend(ATTENTION_STATUSES)
82
+ return {status: wait_status(pane_id, status, timeout_ms) for status in statuses}
83
+
84
+
85
+ def add_completion_waiters(waiters, pane_id, deadline):
86
+ timeout_ms = remaining_timeout_ms(deadline)
87
+ for status in COMPLETION_STATUSES:
88
+ if status not in waiters:
89
+ waiters[status] = wait_status(pane_id, status, timeout_ms)
90
+
91
+
92
+ def main():
93
+ parser = argparse.ArgumentParser(
94
+ description="Run a prompt in a herdr agent pane and wait for completion."
95
+ )
96
+ parser.add_argument("pane_id")
97
+ parser.add_argument("prompt")
98
+ parser.add_argument("--timeout", type=int, default=120000, help="total wait timeout in ms")
99
+ args = parser.parse_args()
100
+
101
+ baseline_status, baseline_result = pane_status(args.pane_id)
102
+ if baseline_status is None:
103
+ emit(
104
+ {
105
+ "error": "could not read baseline pane status",
106
+ "pane_id": args.pane_id,
107
+ "pane_get_stderr": baseline_result.stderr.strip() if baseline_result else None,
108
+ },
109
+ stream=sys.stderr,
110
+ )
111
+ return 1
112
+
113
+ deadline = time.time() + (args.timeout / 1000)
114
+ saw_working = baseline_status == "working"
115
+ waiters = build_waiters(args.pane_id, args.timeout, baseline_status, saw_working)
116
+
117
+ run_result = subprocess.run(
118
+ ["herdr", "pane", "run", args.pane_id, args.prompt],
119
+ stdout=subprocess.PIPE,
120
+ stderr=subprocess.PIPE,
121
+ text=True,
122
+ )
123
+ if run_result.returncode != 0:
124
+ stop_processes(list(waiters.values()))
125
+ emit(
126
+ {
127
+ "error": "pane run failed",
128
+ "pane_id": args.pane_id,
129
+ "baseline_status": baseline_status,
130
+ "pane_run_stderr": run_result.stderr.strip(),
131
+ "pane_run_stdout": run_result.stdout.strip(),
132
+ },
133
+ stream=sys.stderr,
134
+ )
135
+ return 1
136
+
137
+ while time.time() < deadline:
138
+ for status, proc in list(waiters.items()):
139
+ if proc.poll() is None:
140
+ continue
141
+ stdout, stderr = finish_process(proc)
142
+ if proc.returncode != 0:
143
+ waiters.pop(status, None)
144
+ continue
145
+ event = parse_event(stdout)
146
+
147
+ if status == "working":
148
+ saw_working = True
149
+ waiters.pop(status)
150
+ add_completion_waiters(waiters, args.pane_id, deadline)
151
+ break
152
+
153
+ if status in ATTENTION_STATUSES:
154
+ stop_processes(list(waiters.values()))
155
+ emit(
156
+ {
157
+ "completed_status": None,
158
+ "status": status,
159
+ "phase": "attention",
160
+ "pane_id": args.pane_id,
161
+ "baseline_status": baseline_status,
162
+ "saw_working": saw_working,
163
+ "event": event,
164
+ }
165
+ )
166
+ return 2
167
+
168
+ if saw_working or status != baseline_status:
169
+ stop_processes(list(waiters.values()))
170
+ emit(
171
+ {
172
+ "completed_status": status,
173
+ "status": status,
174
+ "phase": "completion",
175
+ "pane_id": args.pane_id,
176
+ "baseline_status": baseline_status,
177
+ "saw_working": saw_working,
178
+ "evidence": "working_then_terminal" if saw_working else "terminal_status_changed",
179
+ "event": event,
180
+ }
181
+ )
182
+ return 0
183
+
184
+ time.sleep(0.05)
185
+
186
+ stop_processes(list(waiters.values()))
187
+ status, result = pane_status(args.pane_id)
188
+ emit(
189
+ {
190
+ "error": "timed out waiting for a new completion signal",
191
+ "pane_id": args.pane_id,
192
+ "baseline_status": baseline_status,
193
+ "last_status": status,
194
+ "saw_working": saw_working,
195
+ "pane_get_stderr": result.stderr.strip() if result else None,
196
+ },
197
+ stream=sys.stderr,
198
+ )
199
+ return 1
200
+
201
+
202
+ if __name__ == "__main__":
203
+ raise SystemExit(main())