dispatch-agents 0.3.0 → 0.4.1
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/README.md +1 -1
- package/dist/cli.js +140 -40
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/cli.js
CHANGED
|
@@ -77,8 +77,9 @@ function loadConfig(cliOverrides) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// src/commands.ts
|
|
80
|
-
import { existsSync as existsSync2, writeFileSync as writeFileSync2, readdirSync } from "fs";
|
|
80
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, appendFileSync as appendFileSync2, readdirSync } from "fs";
|
|
81
81
|
import { join as join3 } from "path";
|
|
82
|
+
import { homedir as homedir2 } from "os";
|
|
82
83
|
import { execSync as execSync2, spawnSync as spawnSync2 } from "child_process";
|
|
83
84
|
|
|
84
85
|
// src/shell.ts
|
|
@@ -210,6 +211,8 @@ function ensureSession() {
|
|
|
210
211
|
execSync(
|
|
211
212
|
`tmux new-session -d -s "${DISPATCH_SESSION}" -n "dispatch"`
|
|
212
213
|
);
|
|
214
|
+
execSync(`tmux set -t "${DISPATCH_SESSION}" -g mouse on`);
|
|
215
|
+
execSync(`tmux set -t "${DISPATCH_SESSION}" -g history-limit 50000`);
|
|
213
216
|
execSync(
|
|
214
217
|
`tmux send-keys -t "${DISPATCH_SESSION}:dispatch" "# Dispatch control window" Enter`
|
|
215
218
|
);
|
|
@@ -418,6 +421,9 @@ function tailFile(path) {
|
|
|
418
421
|
|
|
419
422
|
// src/commands.ts
|
|
420
423
|
var TICKET_RE = /^[A-Z]+-[0-9]+$/;
|
|
424
|
+
function slugify(text) {
|
|
425
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
|
|
426
|
+
}
|
|
421
427
|
function buildClaudeCmd(prompt, mode, wtPath, config, extraArgs) {
|
|
422
428
|
let cmd = "claude";
|
|
423
429
|
if (mode === "headless") cmd += " -p";
|
|
@@ -432,7 +438,7 @@ function buildClaudeCmd(prompt, mode, wtPath, config, extraArgs) {
|
|
|
432
438
|
if (mode === "headless") {
|
|
433
439
|
const promptFile = join3(wtPath, ".dispatch-prompt.txt");
|
|
434
440
|
writeFileSync2(promptFile, prompt);
|
|
435
|
-
cmd += `
|
|
441
|
+
cmd += ` < '${promptFile}'`;
|
|
436
442
|
}
|
|
437
443
|
return cmd;
|
|
438
444
|
}
|
|
@@ -441,9 +447,9 @@ async function launchAgent(input, headless, extraArgs, skipWorktree, promptFileA
|
|
|
441
447
|
let prompt;
|
|
442
448
|
let branch;
|
|
443
449
|
if (TICKET_RE.test(input)) {
|
|
444
|
-
id = input;
|
|
445
|
-
branch = input.toLowerCase();
|
|
446
450
|
const ticket = await fetchLinearTicket(input);
|
|
451
|
+
id = `${input.toLowerCase()}-${slugify(ticket.title)}`;
|
|
452
|
+
branch = id;
|
|
447
453
|
if (ticket.description) {
|
|
448
454
|
prompt = `Linear ticket ${input}: ${ticket.title}
|
|
449
455
|
|
|
@@ -454,14 +460,13 @@ Work on this ticket. Create commits as you go. When done, push the branch.`;
|
|
|
454
460
|
prompt = `Work on ticket ${input}: ${ticket.title}. Create commits as you go. When done, push the branch.`;
|
|
455
461
|
}
|
|
456
462
|
} else {
|
|
457
|
-
|
|
458
|
-
id = `task-${suffix}`;
|
|
463
|
+
id = slugify(input) || `task-${String(Date.now()).slice(-6)}`;
|
|
459
464
|
branch = id;
|
|
460
465
|
prompt = input;
|
|
461
466
|
}
|
|
462
467
|
if (nameOverride) {
|
|
463
|
-
id = nameOverride;
|
|
464
|
-
branch =
|
|
468
|
+
id = slugify(nameOverride) || nameOverride;
|
|
469
|
+
branch = id;
|
|
465
470
|
}
|
|
466
471
|
if (promptFileArg) {
|
|
467
472
|
if (!existsSync2(promptFileArg)) {
|
|
@@ -471,8 +476,17 @@ Work on this ticket. Create commits as you go. When done, push the branch.`;
|
|
|
471
476
|
if (TICKET_RE.test(input)) {
|
|
472
477
|
log.warn(`Ticket prompt for ${input} overridden by --prompt-file`);
|
|
473
478
|
}
|
|
474
|
-
const { readFileSync:
|
|
475
|
-
prompt =
|
|
479
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
480
|
+
prompt = readFileSync4(promptFileArg, "utf-8");
|
|
481
|
+
if (!nameOverride && !TICKET_RE.test(input)) {
|
|
482
|
+
const firstLine = prompt.split("\n").find((l) => l.trim().length > 0) || "";
|
|
483
|
+
const clean = firstLine.replace(/^#+\s*/, "");
|
|
484
|
+
const derived = slugify(clean);
|
|
485
|
+
if (derived) {
|
|
486
|
+
id = derived;
|
|
487
|
+
branch = id;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
476
490
|
}
|
|
477
491
|
if (windowExists(id)) {
|
|
478
492
|
log.error(`Agent '${id}' is already running. Use 'dispatch stop ${id}' first.`);
|
|
@@ -588,6 +602,9 @@ async function cmdRun(args, config) {
|
|
|
588
602
|
console.log(" dispatch run HEY-837 --base main # branch off main");
|
|
589
603
|
process.exit(1);
|
|
590
604
|
}
|
|
605
|
+
if (inputs.length === 0 && promptFile) {
|
|
606
|
+
inputs.push("prompt-file");
|
|
607
|
+
}
|
|
591
608
|
ensureTmux();
|
|
592
609
|
if (inputs.length > 1) {
|
|
593
610
|
log.info(`Batch launching ${inputs.length} agents...`);
|
|
@@ -803,51 +820,131 @@ function cmdNotifyDone(args) {
|
|
|
803
820
|
notify("Dispatch", `Agent ${agentId} finished`);
|
|
804
821
|
log.ok(`Agent ${agentId} completed`);
|
|
805
822
|
}
|
|
823
|
+
var CLAUDE_MD_SNIPPET = `
|
|
824
|
+
## Dispatch (multi-agent orchestration)
|
|
825
|
+
|
|
826
|
+
Launch Claude Code agents in isolated git worktrees. Each agent gets its own branch, so it can make changes without affecting your working tree or other agents. Agents run inside tmux \u2014 interactive mode to watch/guide, headless for fire-and-forget.
|
|
827
|
+
|
|
828
|
+
**When to use:** Hand off well-defined tasks (Linear tickets, bug fixes, features) to a parallel agent while you keep working. Avoid dispatching two agents to the same files \u2014 they'll create merge conflicts.
|
|
829
|
+
|
|
830
|
+
\`\`\`bash
|
|
831
|
+
# Launch agents
|
|
832
|
+
dispatch run HEY-123 # From Linear ticket (auto-fetches title + description)
|
|
833
|
+
dispatch run "Fix the auth bug" --name HEY-879 # Free text with custom branch name (hey-879)
|
|
834
|
+
dispatch run HEY-123 --headless # Background \u2014 check with: dispatch logs HEY-123
|
|
835
|
+
dispatch run HEY-123 -m sonnet --max-turns 20 # Sonnet, 20 turn limit
|
|
836
|
+
dispatch run HEY-123 HEY-124 HEY-125 # Batch launch in parallel
|
|
837
|
+
|
|
838
|
+
# Monitor and interact
|
|
839
|
+
dispatch list # Status: green=running, yellow=idle, red=exited
|
|
840
|
+
dispatch attach HEY-123 # Jump to agent's terminal (auto-opens tab if no TTY)
|
|
841
|
+
dispatch logs HEY-123 # Tail headless agent output
|
|
842
|
+
|
|
843
|
+
# Lifecycle
|
|
844
|
+
dispatch stop HEY-123 # Interrupt agent (worktree preserved)
|
|
845
|
+
dispatch resume HEY-123 # Pick up where it left off (--continue)
|
|
846
|
+
dispatch cleanup HEY-123 --delete-branch # Remove worktree + branch
|
|
847
|
+
dispatch cleanup --all --delete-branch # Clean up everything
|
|
848
|
+
\`\`\`
|
|
849
|
+
|
|
850
|
+
**Key flags:** \`--name/-n\` sets branch name, \`--model/-m\` picks model, \`--headless/-H\` for background, \`--prompt-file/-f\` for long prompts, \`--base/-b\` to branch off something other than dev.
|
|
851
|
+
|
|
852
|
+
Config: \`~/.dispatch.yml\` (base_branch, model, max_turns, max_budget, worktree_dir, claude_timeout).
|
|
853
|
+
Requires: tmux, claude CLI, git.
|
|
854
|
+
`;
|
|
855
|
+
function cmdSetup() {
|
|
856
|
+
const claudeMdPath = join3(homedir2(), ".claude", "CLAUDE.md");
|
|
857
|
+
if (existsSync2(claudeMdPath)) {
|
|
858
|
+
const content = readFileSync3(claudeMdPath, "utf-8");
|
|
859
|
+
if (content.includes("dispatch run") || content.includes("Dispatch (multi-agent")) {
|
|
860
|
+
log.warn("Dispatch section already exists in ~/.claude/CLAUDE.md");
|
|
861
|
+
log.info("To update it, remove the existing Dispatch section and run setup again.");
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
appendFileSync2(claudeMdPath, "\n" + CLAUDE_MD_SNIPPET);
|
|
865
|
+
log.ok("Added dispatch section to ~/.claude/CLAUDE.md");
|
|
866
|
+
} else {
|
|
867
|
+
const claudeDir = join3(homedir2(), ".claude");
|
|
868
|
+
if (!existsSync2(claudeDir)) {
|
|
869
|
+
spawnSync2("mkdir", ["-p", claudeDir]);
|
|
870
|
+
}
|
|
871
|
+
writeFileSync2(claudeMdPath, CLAUDE_MD_SNIPPET.trimStart());
|
|
872
|
+
log.ok("Created ~/.claude/CLAUDE.md with dispatch section");
|
|
873
|
+
}
|
|
874
|
+
}
|
|
806
875
|
|
|
807
876
|
// src/cli.ts
|
|
808
|
-
var VERSION = "0.
|
|
877
|
+
var VERSION = "0.4.1";
|
|
809
878
|
function help() {
|
|
810
|
-
console.log(`dispatch \u2014
|
|
879
|
+
console.log(`dispatch \u2014 Launch Claude Code agents in isolated git worktrees
|
|
880
|
+
|
|
881
|
+
Each agent gets its own branch and worktree, so it can make changes without
|
|
882
|
+
affecting your working tree or other agents. Agents run inside tmux \u2014 use
|
|
883
|
+
interactive mode to watch and guide them, or headless for fire-and-forget.
|
|
811
884
|
|
|
812
|
-
|
|
813
|
-
dispatch run <ticket|prompt> [
|
|
814
|
-
dispatch list
|
|
815
|
-
dispatch logs <id>
|
|
816
|
-
dispatch stop <id>
|
|
817
|
-
dispatch resume <id> [--headless]
|
|
818
|
-
dispatch cleanup <id>
|
|
819
|
-
dispatch
|
|
885
|
+
Commands:
|
|
886
|
+
dispatch run <ticket|prompt> [options] Launch an agent
|
|
887
|
+
dispatch list Show all running agents with status
|
|
888
|
+
dispatch logs <id> Tail a headless agent's output
|
|
889
|
+
dispatch stop <id> Send Ctrl-C and kill the tmux window
|
|
890
|
+
dispatch resume <id> [--headless] Restart a stopped agent (keeps context)
|
|
891
|
+
dispatch cleanup <id> [--delete-branch] Remove worktree (and optionally branch)
|
|
892
|
+
dispatch cleanup --all [--delete-branch] Remove all worktrees
|
|
893
|
+
dispatch attach [id] Open tmux session (or jump to specific agent)
|
|
894
|
+
dispatch setup Add dispatch docs to ~/.claude/CLAUDE.md
|
|
820
895
|
|
|
821
896
|
Run Options:
|
|
822
|
-
--headless, -H
|
|
823
|
-
--model, -m <model>
|
|
824
|
-
--
|
|
825
|
-
--max-
|
|
826
|
-
--
|
|
827
|
-
--
|
|
828
|
-
--
|
|
829
|
-
--no-worktree
|
|
897
|
+
--headless, -H Fire-and-forget mode (no interactive terminal)
|
|
898
|
+
--model, -m <model> Claude model: sonnet, opus, haiku (default: from config)
|
|
899
|
+
--name, -n <name> Set agent name and branch (default: ticket ID or task-{random})
|
|
900
|
+
--max-turns <n> Limit agentic turns before stopping (headless only)
|
|
901
|
+
--max-budget <usd> Cap spending in USD (headless only)
|
|
902
|
+
--base, -b <branch> Branch to create worktree from (default: dev)
|
|
903
|
+
--prompt-file, -f <file> Load prompt from a file instead of CLI arg
|
|
904
|
+
--no-worktree Run in current directory (no isolation)
|
|
905
|
+
|
|
906
|
+
Lifecycle:
|
|
907
|
+
1. run \u2014 Creates worktree + branch, opens tmux window, starts Claude Code
|
|
908
|
+
2. work \u2014 Agent reads codebase, makes changes, commits, pushes, creates PRs
|
|
909
|
+
3. attach \u2014 View/interact with the agent (auto-opens terminal tab if no TTY)
|
|
910
|
+
4. stop \u2014 Interrupt the agent (worktree and branch preserved)
|
|
911
|
+
5. resume \u2014 Pick up where it left off (Claude --continue)
|
|
912
|
+
6. cleanup \u2014 Remove worktree when done (--delete-branch to also delete the branch)
|
|
913
|
+
|
|
914
|
+
Input Types:
|
|
915
|
+
Linear ticket dispatch run HEY-837 Fetches title + description from Linear
|
|
916
|
+
Free text dispatch run "Fix the auth bug" Uses your prompt directly
|
|
917
|
+
Prompt file dispatch run X -f prompt.txt Loads prompt from file (good for long prompts)
|
|
830
918
|
|
|
831
919
|
Examples:
|
|
832
|
-
dispatch run HEY-837
|
|
833
|
-
dispatch run HEY-837 --headless
|
|
834
|
-
dispatch run HEY-837 HEY-838 HEY-839
|
|
835
|
-
dispatch run "Fix the auth bug"
|
|
836
|
-
dispatch run HEY-837 -m sonnet
|
|
837
|
-
dispatch
|
|
920
|
+
dispatch run HEY-837 # Interactive, from Linear ticket
|
|
921
|
+
dispatch run HEY-837 --headless # Background \u2014 check with: dispatch logs HEY-837
|
|
922
|
+
dispatch run HEY-837 HEY-838 HEY-839 # Batch launch 3 agents in parallel
|
|
923
|
+
dispatch run "Fix the auth bug" --name HEY-879 # Free text with custom branch name
|
|
924
|
+
dispatch run HEY-837 -m sonnet --max-turns 20 # Sonnet model, 20 turn limit
|
|
925
|
+
dispatch attach HEY-837 # Jump to agent's terminal
|
|
926
|
+
dispatch list # See what's running
|
|
927
|
+
dispatch cleanup --all --delete-branch # Clean everything up
|
|
928
|
+
|
|
929
|
+
Tips:
|
|
930
|
+
- Each agent works on its own branch \u2014 avoid dispatching two agents to the same files
|
|
931
|
+
- Use --name to get meaningful branch names (e.g., --name HEY-879 creates branch hey-879)
|
|
932
|
+
- Interactive mode lets you guide the agent; headless is for well-defined tasks
|
|
933
|
+
- Works from inside Claude Code sessions (agents launch in separate terminals)
|
|
934
|
+
- Use dispatch list to check status: green = running, yellow = idle, red = exited
|
|
838
935
|
|
|
839
936
|
Environment:
|
|
840
|
-
LINEAR_API_KEY
|
|
937
|
+
LINEAR_API_KEY Linear API key for auto-fetching ticket details
|
|
841
938
|
DISPATCH_BASE_BRANCH Default base branch (default: dev)
|
|
842
939
|
DISPATCH_MODEL Default model
|
|
843
940
|
DISPATCH_CONFIG Config file path (default: ~/.dispatch.yml)
|
|
844
941
|
|
|
845
942
|
Config (~/.dispatch.yml):
|
|
846
|
-
base_branch: dev
|
|
847
|
-
model: opus
|
|
848
|
-
max_turns: 20
|
|
849
|
-
claude_timeout: 30
|
|
850
|
-
worktree_dir: .worktrees`);
|
|
943
|
+
base_branch: dev # Branch to create worktrees from
|
|
944
|
+
model: opus # Default Claude model
|
|
945
|
+
max_turns: 20 # Default max turns for headless
|
|
946
|
+
claude_timeout: 30 # Seconds to wait for Claude to start
|
|
947
|
+
worktree_dir: .worktrees # Where worktrees are created`);
|
|
851
948
|
}
|
|
852
949
|
async function main() {
|
|
853
950
|
const args = process.argv.slice(2);
|
|
@@ -877,6 +974,9 @@ async function main() {
|
|
|
877
974
|
case "attach":
|
|
878
975
|
cmdAttach(rest);
|
|
879
976
|
break;
|
|
977
|
+
case "setup":
|
|
978
|
+
cmdSetup();
|
|
979
|
+
break;
|
|
880
980
|
case "_notify-done":
|
|
881
981
|
cmdNotifyDone(rest);
|
|
882
982
|
break;
|