@generativereality/agentherder 0.1.4 → 0.1.6
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.
- package/.claude-plugin/plugin.json +12 -0
- package/dist/index.js +65 -11
- package/package.json +4 -2
- package/skills/herd/SKILL.md +202 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentherder",
|
|
3
|
+
"description": "Run a fleet of Claude Code sessions. Terminal tabs as the UI, no tmux. Claude can orchestrate its own sibling sessions.",
|
|
4
|
+
"version": "0.1.6",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "generativereality",
|
|
7
|
+
"url": "https://agentherder.com"
|
|
8
|
+
},
|
|
9
|
+
"repository": "https://github.com/generativereality/agentherder",
|
|
10
|
+
"homepage": "https://agentherder.com",
|
|
11
|
+
"license": "MIT"
|
|
12
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { consola } from "consola";
|
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
11
11
|
//#region package.json
|
|
12
12
|
var name = "@generativereality/agentherder";
|
|
13
|
-
var version = "0.1.
|
|
13
|
+
var version = "0.1.6";
|
|
14
14
|
var description = "Agent session manager for AI coding tools. Terminal tabs as the UI, no tmux.";
|
|
15
15
|
var package_default = {
|
|
16
16
|
name,
|
|
@@ -18,7 +18,12 @@ var package_default = {
|
|
|
18
18
|
description,
|
|
19
19
|
type: "module",
|
|
20
20
|
bin: { "herd": "dist/index.js" },
|
|
21
|
-
files: [
|
|
21
|
+
files: [
|
|
22
|
+
"dist",
|
|
23
|
+
".claude",
|
|
24
|
+
".claude-plugin",
|
|
25
|
+
"skills"
|
|
26
|
+
],
|
|
22
27
|
scripts: {
|
|
23
28
|
"dev": "bun run ./src/index.ts",
|
|
24
29
|
"build": "tsdown",
|
|
@@ -657,6 +662,31 @@ function findLatestSessionId(dir) {
|
|
|
657
662
|
}
|
|
658
663
|
return null;
|
|
659
664
|
}
|
|
665
|
+
/**
|
|
666
|
+
* Find the most recently created session ID after a given timestamp.
|
|
667
|
+
* Used by `herd fork` to detect the session Claude created in response to /branch.
|
|
668
|
+
*/
|
|
669
|
+
function findNewestSessionIdSince(dir, sinceMs) {
|
|
670
|
+
const projectsRoot = join(homedir(), ".claude", "projects");
|
|
671
|
+
const candidates = [];
|
|
672
|
+
function scanProjectDir(projectDir) {
|
|
673
|
+
if (!existsSync(projectDir)) return;
|
|
674
|
+
for (const f of readdirSync(projectDir)) {
|
|
675
|
+
if (extname(f) !== ".jsonl") continue;
|
|
676
|
+
const mtime = statSync(join(projectDir, f)).mtimeMs;
|
|
677
|
+
if (mtime > sinceMs) candidates.push({
|
|
678
|
+
id: basename(f, ".jsonl"),
|
|
679
|
+
mtime
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
scanProjectDir(join(projectsRoot, pathToProjectSlug(dir)));
|
|
684
|
+
const worktreesDir = join(dir, ".claude", "worktrees");
|
|
685
|
+
if (existsSync(worktreesDir)) for (const entry of readdirSync(worktreesDir)) scanProjectDir(join(projectsRoot, pathToProjectSlug(join(worktreesDir, entry))));
|
|
686
|
+
if (!candidates.length) return null;
|
|
687
|
+
candidates.sort((a, b) => b.mtime - a.mtime);
|
|
688
|
+
return candidates[0].id;
|
|
689
|
+
}
|
|
660
690
|
//#endregion
|
|
661
691
|
//#region src/commands/fork.ts
|
|
662
692
|
/** If dir is inside .claude/worktrees/<name>, return the repo root instead */
|
|
@@ -677,7 +707,7 @@ function resolveSessionDir(dir) {
|
|
|
677
707
|
}
|
|
678
708
|
const forkCommand = define({
|
|
679
709
|
name: "fork",
|
|
680
|
-
description: "Fork a session into a new tab
|
|
710
|
+
description: "Fork a session into a new tab by sending /branch to the source tab",
|
|
681
711
|
args: {
|
|
682
712
|
tab: {
|
|
683
713
|
type: "positional",
|
|
@@ -700,7 +730,7 @@ const forkCommand = define({
|
|
|
700
730
|
const { tabsById, tabNames } = await adapter.getAllData();
|
|
701
731
|
const matches = adapter.resolveTab(sourceQuery, tabsById, tabNames);
|
|
702
732
|
if (!matches.length) {
|
|
703
|
-
consola.error(`No tab matching '${sourceQuery}'`);
|
|
733
|
+
consola.error(`No tab matching '${sourceQuery}' (tabs in workspaces with no open window are not visible — open that workspace first)`);
|
|
704
734
|
process.exit(1);
|
|
705
735
|
}
|
|
706
736
|
if (matches.length > 1) {
|
|
@@ -717,19 +747,43 @@ const forkCommand = define({
|
|
|
717
747
|
process.exit(1);
|
|
718
748
|
}
|
|
719
749
|
const { sessionLookupDir, openDir } = resolveSessionDir(termBlocks[0].meta?.["cmd:cwd"] ?? process.cwd());
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
750
|
+
const sourceBlockId = termBlocks[0].blockid;
|
|
751
|
+
consola.info(`Sending /branch to "${tabName}"…`);
|
|
752
|
+
const before = Date.now();
|
|
753
|
+
await adapter.sendInput(sourceBlockId, "/branch\n");
|
|
754
|
+
adapter.closeSocket();
|
|
755
|
+
const POLL_INTERVAL = 500;
|
|
756
|
+
const TIMEOUT = 1e4;
|
|
757
|
+
let newSessionId = null;
|
|
758
|
+
const deadline = Date.now() + TIMEOUT;
|
|
759
|
+
while (Date.now() < deadline) {
|
|
760
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
761
|
+
newSessionId = findNewestSessionIdSince(sessionLookupDir, before);
|
|
762
|
+
if (newSessionId) break;
|
|
763
|
+
}
|
|
764
|
+
if (!newSessionId) {
|
|
765
|
+
consola.warn("No new session detected after /branch — falling back to --fork-session");
|
|
766
|
+
const fallbackId = findLatestSessionId(sessionLookupDir);
|
|
767
|
+
if (!fallbackId) {
|
|
768
|
+
consola.error(`No Claude session found for ${sessionLookupDir}`);
|
|
769
|
+
consola.info(`Looked in ~/.claude/projects/${pathToProjectSlug(sessionLookupDir)}/`);
|
|
770
|
+
process.exit(1);
|
|
771
|
+
}
|
|
772
|
+
const newTabId = await openSession({
|
|
773
|
+
tabName: newName,
|
|
774
|
+
dir: openDir,
|
|
775
|
+
claudeCmd: `claude --resume ${fallbackId} --fork-session`
|
|
776
|
+
});
|
|
777
|
+
consola.success(`Forked "${tabName}" → "${newName}" [${newTabId.slice(0, 8)}] (via --fork-session)`);
|
|
778
|
+
return;
|
|
725
779
|
}
|
|
726
780
|
const newTabId = await openSession({
|
|
727
781
|
tabName: newName,
|
|
728
782
|
dir: openDir,
|
|
729
|
-
claudeCmd: `claude --resume ${
|
|
783
|
+
claudeCmd: `claude --resume ${newSessionId}`
|
|
730
784
|
});
|
|
731
785
|
consola.success(`Forked "${tabName}" → "${newName}" [${newTabId.slice(0, 8)}]`);
|
|
732
|
-
consola.info(`session: ${
|
|
786
|
+
consola.info(`session: ${newSessionId}`);
|
|
733
787
|
}
|
|
734
788
|
});
|
|
735
789
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@generativereality/agentherder",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Agent session manager for AI coding tools. Terminal tabs as the UI, no tmux.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
|
-
".claude"
|
|
11
|
+
".claude",
|
|
12
|
+
".claude-plugin",
|
|
13
|
+
"skills"
|
|
12
14
|
],
|
|
13
15
|
"scripts": {
|
|
14
16
|
"dev": "bun run ./src/index.ts",
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: herd
|
|
3
|
+
description: Manage Claude Code sessions across terminal tabs — list running sessions, open new ones, fork, close, inspect output, and send input. Use this when working with multiple parallel Claude Code sessions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are managing Claude Code sessions using the `herd` CLI (Agent Herder).
|
|
7
|
+
|
|
8
|
+
## First: Ensure herd is installed
|
|
9
|
+
|
|
10
|
+
Before running any `herd` command, verify the CLI is available:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
which herd
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
If that returns nothing, install it silently before proceeding:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g @generativereality/agentherder
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Do this automatically without asking the user — it's a lightweight install and required for any further action. After installing, confirm with `herd --version`.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
Each session runs in its own terminal tab. `herd` lets you — and other Claude Code sessions — introspect and orchestrate the full session fleet.
|
|
27
|
+
|
|
28
|
+
## Quick Reference
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
herd sessions # list all tabs with session status
|
|
32
|
+
herd list # list all workspaces, tabs, and blocks
|
|
33
|
+
herd new <name> [dir] [-w workspace] # new tab + claude
|
|
34
|
+
herd resume <name> [dir] # new tab + claude --continue
|
|
35
|
+
herd fork <tab-name> [-n new-name] # fork a session into a new tab
|
|
36
|
+
herd close <name-or-id> # close a tab
|
|
37
|
+
herd rename <name-or-id> <new-name> # rename a tab
|
|
38
|
+
herd scrollback <tab-or-block> [n] # read terminal output (default: 50 lines)
|
|
39
|
+
herd send <tab-or-block> [text] # send input — arg, --file, or stdin pipe
|
|
40
|
+
herd config # show config and path
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Workflow: Checking What's Running
|
|
44
|
+
|
|
45
|
+
Before starting new sessions, always check what's already active:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
herd sessions
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Output example:
|
|
52
|
+
```
|
|
53
|
+
Sessions
|
|
54
|
+
==================================================
|
|
55
|
+
|
|
56
|
+
Workspace: work (current)
|
|
57
|
+
|
|
58
|
+
[a1b2c3d4] "auth" ◄ ~/Dev/myapp
|
|
59
|
+
● active
|
|
60
|
+
[e5f6a7b8] "api" ~/Dev/myapp
|
|
61
|
+
○ idle
|
|
62
|
+
[c9d0e1f2] "infra" ~/Dev/myapp
|
|
63
|
+
terminal
|
|
64
|
+
last: $ git status
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Workflow: Opening a Session Batch
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
herd new auth ~/Dev/myapp
|
|
71
|
+
herd new api ~/Dev/myapp
|
|
72
|
+
herd new infra ~/Dev/myapp
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Each tab is automatically named and the claude session name is synced to the tab title.
|
|
76
|
+
|
|
77
|
+
## Workflow: Resuming After Restart
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
herd sessions # identify which tabs need resuming
|
|
81
|
+
herd resume auth ~/Dev/myapp
|
|
82
|
+
herd resume api ~/Dev/myapp
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Workflow: Forking a Session
|
|
86
|
+
|
|
87
|
+
Use `fork` when you want to explore an alternative approach without disrupting the original.
|
|
88
|
+
`herd fork` sends `/branch` to the source tab (Claude's built-in conversation fork command),
|
|
89
|
+
waits for the new session to be written, then opens it in a new tab.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
herd fork auth # creates "auth-fork" tab
|
|
93
|
+
herd fork auth -n "auth-v2" # creates "auth-v2" tab
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The forked session shares full conversation history up to the branch point, then diverges independently.
|
|
97
|
+
If Claude does not respond to `/branch` in time, herd falls back to `claude --resume <id> --fork-session`.
|
|
98
|
+
|
|
99
|
+
**In-session equivalent**: typing `/branch` (alias `/fork`) directly in Claude produces the same fork —
|
|
100
|
+
use `herd resume <name> <dir>` afterwards to open the resulting session in a new tab.
|
|
101
|
+
|
|
102
|
+
## Workflow: Spawning a Parallel Agent
|
|
103
|
+
|
|
104
|
+
As a Claude Code session, you can spawn a sibling session to work on a parallel task:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
herd new payments ~/Dev/myapp
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**CRITICAL: Wait for Claude to be ready before sending tasks.** After `herd new` returns, Claude is still starting up (loading MCP servers, showing the initial prompt). Sending immediately causes the task to arrive as raw shell commands, not as a Claude prompt.
|
|
111
|
+
|
|
112
|
+
Poll `herd scrollback` until you see the Claude prompt (the `❯` line with no pending output):
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# Poll until Claude prompt appears (look for the ❯ prompt line)
|
|
116
|
+
herd scrollback payments 5
|
|
117
|
+
# Repeat every few seconds until you see Claude's prompt — typically 10-15s
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Once Claude is ready, send the task:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
herd send payments --file /tmp/task.txt # send a prompt from a file
|
|
124
|
+
echo "implement the billing endpoint" | herd send payments # or via stdin
|
|
125
|
+
herd send payments "yes\n" # or inline for quick replies
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Workflow: Monitoring Another Session
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
herd scrollback auth # last 50 lines
|
|
132
|
+
herd scrollback auth 200 # last 200 lines
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Workflow: Sending Input to a Session
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
herd send auth "yes\n" # approve a tool call
|
|
139
|
+
herd send auth "\n" # press enter (confirm a prompt)
|
|
140
|
+
herd send auth "/clear\n" # send a slash command
|
|
141
|
+
herd send auth --file ~/prompts/task.txt # send a full prompt from file
|
|
142
|
+
echo "do the thing" | herd send auth # pipe via stdin
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Workflow: Worktrees
|
|
146
|
+
|
|
147
|
+
**Always point tabs at the repo root — never at a manually-created worktree directory.** Claude Code manages worktrees itself via `claude --worktree <name>`, which creates `.claude/worktrees/<name>/` inside the repo and handles branch creation and cleanup automatically.
|
|
148
|
+
|
|
149
|
+
### New isolated session (new branch, Claude manages everything)
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
herd new feature-name ~/Dev/myapp --worktree
|
|
153
|
+
# Equivalent to: cd ~/Dev/myapp && claude --worktree "feature-name" --name "feature-name"
|
|
154
|
+
# Claude creates: ~/Dev/myapp/.claude/worktrees/feature-name/
|
|
155
|
+
# Claude creates branch: worktree-feature-name
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Existing branch — ask Claude to enter the worktree mid-session
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
herd new hiring ~/Dev/myapp # open tab at repo root
|
|
162
|
+
herd send hiring "Enter a worktree for branch z.old/new-hire-ad and ..."
|
|
163
|
+
# Claude will use EnterWorktree tool to set up isolation
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Do NOT manage git worktrees manually
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# ❌ WRONG — do not create worktree dirs yourself and pass them to herd new
|
|
170
|
+
git worktree add ~/Dev/myapp-feature branch
|
|
171
|
+
herd new feature ~/Dev/myapp-feature
|
|
172
|
+
|
|
173
|
+
# ✅ RIGHT — always use repo root; let Claude Code manage the worktree
|
|
174
|
+
herd new feature ~/Dev/myapp --worktree
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Why:** Manually created worktree dirs placed outside the repo confuse Claude Code's session tracking, project memory lookup (`.claude/` is in the main repo), and CLAUDE.md resolution. Claude Code's built-in worktree support keeps everything co-located under `.claude/worktrees/` and handles cleanup on session exit.
|
|
178
|
+
|
|
179
|
+
## Workflow: Cleanup
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
herd sessions # find idle/terminal tabs
|
|
183
|
+
herd close old-feature # close by name (prefix match)
|
|
184
|
+
herd close e5f6a7b8 # close by block ID prefix
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Tab Naming Conventions
|
|
188
|
+
|
|
189
|
+
Name tabs after the **project or task**:
|
|
190
|
+
- `auth` — authentication work
|
|
191
|
+
- `api` — API service
|
|
192
|
+
- `infra` — infrastructure
|
|
193
|
+
- `pr-1234` — specific PR work
|
|
194
|
+
- `auth-v2` — forked attempt
|
|
195
|
+
|
|
196
|
+
## Notes
|
|
197
|
+
|
|
198
|
+
- Tab names are matched by exact name or prefix (case-insensitive)
|
|
199
|
+
- Block IDs can be abbreviated to the first 8 characters
|
|
200
|
+
- `herd new` and `herd resume` automatically pass `--name <tab-name>` to claude, syncing the session display name with the tab title
|
|
201
|
+
- Configured `claude.flags` in `~/.config/herd/config.toml` are applied to every session
|
|
202
|
+
- `herd send` resolves tab names to their terminal block automatically
|