@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.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +59 -10
- package/package.json +1 -1
- package/skills/herd/SKILL.md +23 -2
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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 ${
|
|
783
|
+
claudeCmd: `claude --resume ${newSessionId}`
|
|
735
784
|
});
|
|
736
785
|
consola.success(`Forked "${tabName}" → "${newName}" [${newTabId.slice(0, 8)}]`);
|
|
737
|
-
consola.info(`session: ${
|
|
786
|
+
consola.info(`session: ${newSessionId}`);
|
|
738
787
|
}
|
|
739
788
|
});
|
|
740
789
|
//#endregion
|
package/package.json
CHANGED
package/skills/herd/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|