@generativereality/agentherder 0.1.5 → 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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentherder",
3
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.5",
4
+ "version": "0.1.6",
5
5
  "author": {
6
6
  "name": "generativereality",
7
7
  "url": "https://agentherder.com"
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.5";
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,
@@ -662,6 +662,31 @@ function findLatestSessionId(dir) {
662
662
  }
663
663
  return null;
664
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
+ }
665
690
  //#endregion
666
691
  //#region src/commands/fork.ts
667
692
  /** If dir is inside .claude/worktrees/<name>, return the repo root instead */
@@ -682,7 +707,7 @@ function resolveSessionDir(dir) {
682
707
  }
683
708
  const forkCommand = define({
684
709
  name: "fork",
685
- 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",
686
711
  args: {
687
712
  tab: {
688
713
  type: "positional",
@@ -705,7 +730,7 @@ const forkCommand = define({
705
730
  const { tabsById, tabNames } = await adapter.getAllData();
706
731
  const matches = adapter.resolveTab(sourceQuery, tabsById, tabNames);
707
732
  if (!matches.length) {
708
- 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)`);
709
734
  process.exit(1);
710
735
  }
711
736
  if (matches.length > 1) {
@@ -722,19 +747,43 @@ const forkCommand = define({
722
747
  process.exit(1);
723
748
  }
724
749
  const { sessionLookupDir, openDir } = resolveSessionDir(termBlocks[0].meta?.["cmd:cwd"] ?? process.cwd());
725
- const sessionId = findLatestSessionId(sessionLookupDir);
726
- if (!sessionId) {
727
- consola.error(`No Claude session found for ${sessionLookupDir}`);
728
- consola.info(`Looked in ~/.claude/projects/${pathToProjectSlug(sessionLookupDir)}/`);
729
- 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;
730
779
  }
731
780
  const newTabId = await openSession({
732
781
  tabName: newName,
733
782
  dir: openDir,
734
- claudeCmd: `claude --resume ${sessionId} --fork-session`
783
+ claudeCmd: `claude --resume ${newSessionId}`
735
784
  });
736
785
  consola.success(`Forked "${tabName}" → "${newName}" [${newTabId.slice(0, 8)}]`);
737
- consola.info(`session: ${sessionId}`);
786
+ consola.info(`session: ${newSessionId}`);
738
787
  }
739
788
  });
740
789
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@generativereality/agentherder",
3
- "version": "0.1.5",
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": {
@@ -84,14 +84,20 @@ herd resume api ~/Dev/myapp
84
84
 
85
85
  ## Workflow: Forking a Session
86
86
 
87
- Use `fork` when you want to try an alternative approach without disrupting the original:
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.
88
90
 
89
91
  ```bash
90
92
  herd fork auth # creates "auth-fork" tab
91
93
  herd fork auth -n "auth-v2" # creates "auth-v2" tab
92
94
  ```
93
95
 
94
- The forked session runs `claude --resume <session-id> --fork-session` it shares context from the original but creates an independent new session.
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.
95
101
 
96
102
  ## Workflow: Spawning a Parallel Agent
97
103
 
@@ -99,6 +105,21 @@ As a Claude Code session, you can spawn a sibling session to work on a parallel
99
105
 
100
106
  ```bash
101
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
102
123
  herd send payments --file /tmp/task.txt # send a prompt from a file
103
124
  echo "implement the billing endpoint" | herd send payments # or via stdin
104
125
  herd send payments "yes\n" # or inline for quick replies