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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +140 -40
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -29,7 +29,7 @@ Each agent gets:
29
29
  ## Install
30
30
 
31
31
  ```bash
32
- npm install -g dispatch-agent
32
+ npm install -g dispatch-agents
33
33
  ```
34
34
 
35
35
  Or from source:
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 += ` "$(cat '${promptFile}')"`;
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
- const suffix = String(Date.now()).slice(-6);
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 = nameOverride.toLowerCase();
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: readFileSync3 } = await import("fs");
475
- prompt = readFileSync3(promptFileArg, "utf-8");
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.3.0";
877
+ var VERSION = "0.4.1";
809
878
  function help() {
810
- console.log(`dispatch \u2014 Orchestrate Claude Code agents in git worktrees
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
- Usage:
813
- dispatch run <ticket|prompt> [more...] [options] Launch agent(s)
814
- dispatch list Show running agents
815
- dispatch logs <id> Tail agent output
816
- dispatch stop <id> Stop an agent
817
- dispatch resume <id> [--headless] Resume a stopped agent
818
- dispatch cleanup <id> | --all [--delete-branch] Remove worktree(s)
819
- dispatch attach Attach to tmux session
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 Run in background (no interactive tab)
823
- --model, -m <model> Claude model (sonnet, opus, etc.)
824
- --max-turns <n> Limit agent turns (headless only)
825
- --max-budget <usd> Cap spending (headless only)
826
- --base, -b <branch> Base branch for worktree (default: dev)
827
- --prompt-file, -f <file> Load prompt from file
828
- --name, -n <name> Override agent name and branch (e.g., HEY-879)
829
- --no-worktree Run in current directory (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 Interactive, from Linear ticket
833
- dispatch run HEY-837 --headless Background mode
834
- dispatch run HEY-837 HEY-838 HEY-839 Batch launch (multiple agents)
835
- dispatch run "Fix the auth bug" Free text prompt
836
- dispatch run HEY-837 -m sonnet Use Sonnet model
837
- dispatch run HEY-837 --max-turns 10 Limit to 10 turns
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 Linear API key for ticket fetching
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dispatch-agents",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Orchestrate Claude Code agents in git worktrees",
5
5
  "type": "module",
6
6
  "bin": {