@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.
@@ -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.4";
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: ["dist", ".claude"],
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 (claude --resume <id> --fork-session)",
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 sessionId = findLatestSessionId(sessionLookupDir);
721
- if (!sessionId) {
722
- consola.error(`No Claude session found for ${sessionLookupDir}`);
723
- consola.info(`Looked in ~/.claude/projects/${pathToProjectSlug(sessionLookupDir)}/`);
724
- process.exit(1);
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 ${sessionId} --fork-session`
783
+ claudeCmd: `claude --resume ${newSessionId}`
730
784
  });
731
785
  consola.success(`Forked "${tabName}" → "${newName}" [${newTabId.slice(0, 8)}]`);
732
- consola.info(`session: ${sessionId}`);
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.4",
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