schwarm-cli 0.1.1 → 0.1.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 407d8cf7c33a697ed71a0bd8b3345d278fa34705ec04fdc2ee358d55c7c549c2
4
- data.tar.gz: 2949bbaf855be804caec35e0a25df038850c2bd4e7885421651f3cc48c4cbbe3
3
+ metadata.gz: 5ad711fe3997e1d3b3c18cee54f6621ecc3d895f01f7c09916e5bb1f217ea6c4
4
+ data.tar.gz: 3c0b5c575b1ee49423a547eeadc5a7310109e5ff294f718ee0e3cbd016f25dda
5
5
  SHA512:
6
- metadata.gz: d3841fa2204bac7b1ee9debab270e7509925388563d49a72dcfbbfb61d8c7ab593ccc14a6c6ec978925c43c0a83f46603e0607a28a4e5d4696f50b87f1f7e8c4
7
- data.tar.gz: cffebca5659d080e7b05ddd386e254acc3870b40678a59ee3e76acf5e5ee82119643ecd7390df00b7102768b5f8e11703900927a4ba035e211b0314e33a3fb5b
6
+ metadata.gz: a1650f63cca311d44de2f6c6b351a4bb1f81958ec5ce3093ac0a6745f5f1104ae632bcb38386bac177dd03e1274052bb47dd7967f2a0a689de1b9e28dd8227eb
7
+ data.tar.gz: b5696799424c27eb8a7808ff225a461e1573e28ffb4cb2261ecd541687344293e5f115fa743605273b612db9639c61ba1eb1ee70113deaca8f9e5dd26b70dc5d
data/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # schwarm-cli
2
+
3
+ Command-line interface for [Schwarm](https://github.com/getdexter/schwarm) — a
4
+ distributed system that orchestrates Claude Code agents working on tasks in
5
+ parallel. Wraps the Schwarm Public API v2 to manage tasks, repositories,
6
+ skills, agents, and more.
7
+
8
+ ## Installation
9
+
10
+ Requires Ruby >= 4.0.
11
+
12
+ ```bash
13
+ gem install schwarm-cli
14
+ ```
15
+
16
+ This installs the `schwarm` executable.
17
+
18
+ ## Configuration
19
+
20
+ Run the interactive configuration once to point the CLI at your Schwarm
21
+ server and save an API key:
22
+
23
+ ```bash
24
+ schwarm configure
25
+ ```
26
+
27
+ You'll be prompted for:
28
+
29
+ - **Server URL** (defaults to `https://schwarm.getdexter.net`)
30
+ - **API Key** — create one from your Schwarm server's API Keys page
31
+
32
+ The credentials are saved to `~/.schwarm/config/<env>.json` and the connection
33
+ is tested before saving.
34
+
35
+ ## Usage
36
+
37
+ List top-level commands:
38
+
39
+ ```bash
40
+ schwarm help
41
+ schwarm tree # full command tree
42
+ schwarm version
43
+ ```
44
+
45
+ Common workflows:
46
+
47
+ ```bash
48
+ schwarm repos list
49
+ schwarm tasks list
50
+ schwarm tasks create --repo <REPO_ID> --name "Fix bug" --prompt "..." --status ready
51
+ schwarm tasks show <TASK_ID>
52
+ schwarm tasks handoff --repo <REPO_ID> --name "Continue WIP"
53
+ ```
54
+
55
+ For the full set of agent-oriented workflows (dispatching tasks, DAGs,
56
+ handoffs, monitoring, messaging), print the bundled skill:
57
+
58
+ ```bash
59
+ schwarm skill
60
+ ```
61
+
62
+ This is the same document AI agents read to learn how to drive the CLI, and
63
+ it's kept in sync with the gem at [`cli/schwarm-skill.md`](schwarm-skill.md).
64
+
65
+ ## Links
66
+
67
+ - [Schwarm repository](https://github.com/getdexter/schwarm)
68
+ - [Source](https://github.com/getdexter/schwarm/tree/main/cli)
@@ -4,7 +4,7 @@ module SchwarmCli
4
4
  class Client
5
5
  class UserMessages < Resource
6
6
  def create(task_id:, content:)
7
- post("/api/v2/tasks/#{task_id}/messages", { user_message: { content: } }).body
7
+ post("/api/v2/tasks/#{task_id}/messages", { message: { content: } }).body
8
8
  end
9
9
  end
10
10
  end
@@ -59,6 +59,12 @@ module SchwarmCli
59
59
  end
60
60
 
61
61
  map %w[-v --version] => :version
62
+
63
+ desc "skill", "Print the agent skill documenting how AI agents use the Schwarm CLI"
64
+ def skill
65
+ path = File.expand_path("../../../schwarm-skill.md", __dir__)
66
+ puts File.read(path)
67
+ end
62
68
  end
63
69
  end
64
70
  end
@@ -114,6 +114,23 @@ module SchwarmCli
114
114
  end
115
115
  end
116
116
 
117
+ desc "handoff", "Hand off the current local branch to schwarm as a ready task"
118
+ option :prompt, type: :string, desc: "Task prompt"
119
+ option :prompt_file, type: :string, desc: "Read prompt from file"
120
+ option :repo, type: :string, desc: "Repository ID (skips origin auto-detect)"
121
+ option :name, type: :string, desc: "Task name (defaults to the branch name)"
122
+ def handoff
123
+ prompt = read_prompt
124
+ abort "Error: --prompt or --prompt-file is required." if prompt.nil? || prompt.strip.empty?
125
+
126
+ handle_errors do
127
+ result = SchwarmCli::HandsOffTask.new(client: client).call(
128
+ prompt: prompt, repo_override: options[:repo], name_override: options[:name]
129
+ )
130
+ print_handoff_result(result)
131
+ end
132
+ end
133
+
117
134
  private
118
135
 
119
136
  def list_attrs
@@ -124,6 +141,16 @@ module SchwarmCli
124
141
  }
125
142
  end
126
143
 
144
+ def print_handoff_result(result)
145
+ if result.success?
146
+ puts "Pushed #{result.branch_name} to origin."
147
+ puts "Task #{result.task_id} created (status: #{result.status})."
148
+ puts " #{result.task_url}"
149
+ else
150
+ abort "Error: #{result.error_message}"
151
+ end
152
+ end
153
+
127
154
  def create_attrs
128
155
  {
129
156
  name: options[:name], prompt: read_prompt,
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module SchwarmCli
6
+ # Minimal git shell-out wrapper.
7
+ #
8
+ # Returns a Result struct for every invocation so callers can pattern-match on
9
+ # +success?+ and surface +stderr+ verbatim when a command fails.
10
+ module Git
11
+ Result = Struct.new(:stdout, :stderr, :success?, keyword_init: true)
12
+
13
+ def self.run(*, chdir: Dir.pwd)
14
+ stdout, stderr, status = Open3.capture3("git", *, chdir: chdir)
15
+ Result.new(stdout: stdout, stderr: stderr, success?: status.success?)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SchwarmCli
4
+ # Runs preflight checks, pushes the current branch, and creates a schwarm
5
+ # task pointing at it. Returns a Result the CLI command prints.
6
+ class HandsOffTask
7
+ Result = Struct.new(:success?, :task_id, :task_url, :branch_name, :status, :error_message,
8
+ keyword_init: true)
9
+
10
+ def initialize(git: SchwarmCli::Git, client: SchwarmCli::Client.new)
11
+ @git = git
12
+ @client = client
13
+ end
14
+
15
+ def call(prompt:, repo_override:, name_override:)
16
+ repo, branch, error = run_preflight(repo_override)
17
+ return error if error
18
+
19
+ push_result = @git.run("push", "-u", "origin", "HEAD")
20
+ return failure(push_result.stderr.strip) unless push_result.success?
21
+
22
+ create_task(repo: repo, branch: branch, prompt: prompt, name_override: name_override)
23
+ rescue Faraday::Error => e
24
+ failure(parse_api_error(e))
25
+ end
26
+
27
+ private
28
+
29
+ def run_preflight(repo_override) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
30
+ return [nil, nil, failure("not in a git repository")] unless inside_work_tree?
31
+
32
+ origin = origin_url
33
+ unless github_url?(origin)
34
+ return [nil, nil, failure("handoff requires a GitHub `origin` remote (got `#{origin}`)")]
35
+ end
36
+
37
+ repo = resolve_repository(origin: origin, override: repo_override)
38
+ return [nil, nil, failure(missing_repo_message(origin, repo_override))] if repo.nil?
39
+
40
+ branch = current_branch
41
+ if branch.empty?
42
+ return [nil, nil, failure("not on a branch (detached HEAD). Check out a branch before handing off.")]
43
+ end
44
+
45
+ base_branch = repo["base_branch"]
46
+ if branch == base_branch
47
+ return [nil, nil,
48
+ failure("refusing to hand off the base branch `#{base_branch}`; create a feature branch first.")]
49
+ end
50
+
51
+ dirty = status_porcelain
52
+ return [nil, nil, failure("commit or stash your changes before handing off:\n#{dirty}")] unless dirty.empty?
53
+
54
+ [repo, branch, nil]
55
+ end
56
+
57
+ def missing_repo_message(origin, repo_override)
58
+ if repo_override
59
+ "no schwarm repository with id `#{repo_override}`."
60
+ else
61
+ "no schwarm repository matches origin `#{origin}`. Pass --repo <id> to override."
62
+ end
63
+ end
64
+
65
+ def inside_work_tree?
66
+ @git.run("rev-parse", "--is-inside-work-tree").success?
67
+ end
68
+
69
+ def origin_url
70
+ result = @git.run("remote", "get-url", "origin")
71
+ result.success? ? result.stdout.strip : ""
72
+ end
73
+
74
+ def github_url?(url)
75
+ url.include?("github.com")
76
+ end
77
+
78
+ def current_branch
79
+ @git.run("symbolic-ref", "--short", "HEAD").stdout.strip
80
+ end
81
+
82
+ def status_porcelain
83
+ @git.run("status", "--porcelain").stdout
84
+ end
85
+
86
+ def resolve_repository(origin:, override:)
87
+ if override
88
+ begin
89
+ return @client.repositories.find(override)["data"]
90
+ rescue Faraday::ResourceNotFound
91
+ return nil
92
+ end
93
+ end
94
+
95
+ slug = origin_slug(origin)
96
+ response = @client.repositories.list(query: slug)
97
+ Array(response["data"]).find { |r| normalize_url(r["github_url"]) == normalize_url(origin) }
98
+ end
99
+
100
+ def origin_slug(url)
101
+ # Accept both https://github.com/org/repo[.git] and git@github.com:org/repo.git
102
+ url.sub(%r{\Ahttps?://github\.com/}, "").sub(/\Agit@github\.com:/, "").sub(/\.git\z/, "")
103
+ end
104
+
105
+ def normalize_url(url)
106
+ return nil if url.nil?
107
+
108
+ url.sub(/\.git\z/, "").sub(/\Agit@github\.com:/, "https://github.com/")
109
+ end
110
+
111
+ def create_task(repo:, branch:, prompt:, name_override:)
112
+ response = @client.tasks.create(
113
+ name: name_override || branch,
114
+ branch_name: branch,
115
+ prompt: prompt,
116
+ status: "ready",
117
+ github_repository_id: repo["id"]
118
+ )
119
+ data = response["data"]
120
+ Result.new(
121
+ success?: true,
122
+ task_id: data["id"],
123
+ task_url: task_url_for(data["id"]),
124
+ branch_name: data["branch_name"],
125
+ status: data["status"]
126
+ )
127
+ end
128
+
129
+ def task_url_for(task_id)
130
+ base = ENV["SCHWARM_URL"] || SchwarmCli::Config.new.url
131
+ "#{base.chomp('/')}/tasks/#{task_id}"
132
+ end
133
+
134
+ def parse_api_error(error)
135
+ body = error.response&.dig(:body)
136
+ return error.message unless body.is_a?(String)
137
+
138
+ JSON.parse(body).dig("error", "message") || error.message
139
+ rescue JSON::ParserError
140
+ error.message
141
+ end
142
+
143
+ def failure(message)
144
+ Result.new(success?: false, error_message: message)
145
+ end
146
+ end
147
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SchwarmCli
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
data/lib/schwarm_cli.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  require_relative "schwarm_cli/version"
4
4
  require_relative "schwarm_cli/config"
5
5
  require_relative "schwarm_cli/client"
6
+ require_relative "schwarm_cli/git"
7
+ require_relative "schwarm_cli/hands_off_task"
6
8
  require_relative "schwarm_cli/formatters/json"
7
9
  require_relative "schwarm_cli/formatters/table"
8
10
  require_relative "schwarm_cli/commands/main"
data/schwarm-skill.md ADDED
@@ -0,0 +1,235 @@
1
+ # Schwarm
2
+
3
+ Schwarm is a distributed system that orchestrates Claude Code agents working on
4
+ tasks in parallel. Use the `schwarm` CLI to dispatch work to Schwarm from your
5
+ local machine.
6
+
7
+ ## Prerequisites
8
+
9
+ The `schwarm` CLI is installed and configured (`schwarm configure`).
10
+
11
+ ## Key Concepts
12
+
13
+ - **Repository** — a GitHub repo linked to Schwarm. Tasks run against a repo.
14
+ - **Task** — a unit of work. Schwarm creates an isolated git worktree and runs
15
+ Claude Code on it. Tasks have a lifecycle: draft -> ready -> claimed -> archived.
16
+ - **Dependencies** — tasks can depend on other tasks, forming a DAG. A task with
17
+ unresolved dependencies stays in `waiting` until they're all archived.
18
+ - **Node** — a worker machine that picks up `ready` tasks and runs Claude Code.
19
+
20
+ ## Workflows
21
+
22
+ ### 1. Dispatch a single task
23
+
24
+ **When:** You have a self-contained piece of work to send to Schwarm.
25
+
26
+ **Steps:**
27
+ 1. Find the repository ID:
28
+ ```bash
29
+ schwarm repos list
30
+ ```
31
+ 2. Create the task and start it immediately:
32
+ ```bash
33
+ schwarm tasks create --repo <REPO_ID> --name "Fix login bug" --prompt "The login form crashes when email is empty. Fix the validation in app/models/user.rb" --status ready
34
+ ```
35
+ Use `--prompt-file ./prompt.md` for long prompts.
36
+ 3. Check the created task:
37
+ ```bash
38
+ schwarm tasks show <TASK_ID>
39
+ ```
40
+
41
+ **Watch for:** If you set `--status ready`, the task starts immediately. Omit
42
+ `--status` (defaults to draft) if you want to review before starting.
43
+
44
+ ### 1b. Hand off a WIP branch to schwarm
45
+
46
+ **When:** You have work in progress on a local branch and want schwarm to
47
+ continue from where you left off. Schwarm picks up the same branch and
48
+ continues committing to it.
49
+
50
+ **Steps:**
51
+ 1. Make sure your working tree is clean (commit or stash local changes).
52
+ 2. Hand off from the current branch:
53
+ ```bash
54
+ schwarm tasks handoff --prompt "continue the validation logic, add tests"
55
+ ```
56
+ Use `--prompt-file ./handoff.md` for long prompts.
57
+ 3. Take note of the task ID printed on success; you can monitor it like any
58
+ other task.
59
+
60
+ **Watch for:**
61
+ - The command refuses to hand off the base branch (`main`). Create a feature
62
+ branch first.
63
+ - It refuses if the working tree is dirty — commit or stash first.
64
+ - If another non-archived task already owns this branch name, the command
65
+ will print the conflicting task ID. Archive or reset it first.
66
+ - The repository is auto-detected from `git remote get-url origin`. Use
67
+ `--repo <id>` if you need to override it (e.g. in a fork).
68
+
69
+ ### 2. Dispatch a DAG of tasks
70
+
71
+ **When:** You need to break work into multiple steps with ordering constraints.
72
+
73
+ **Steps:**
74
+ 1. Create all tasks as drafts first:
75
+ ```bash
76
+ schwarm tasks create --repo <REPO_ID> --name "Step 1: Refactor models" --prompt "..."
77
+ schwarm tasks create --repo <REPO_ID> --name "Step 2: Add tests" --prompt "..." --depends-on <STEP1_ID>
78
+ schwarm tasks create --repo <REPO_ID> --name "Step 3: Update docs" --prompt "..." --depends-on <STEP1_ID>
79
+ schwarm tasks create --repo <REPO_ID> --name "Step 4: Integration tests" --prompt "..." --depends-on <STEP2_ID> <STEP3_ID>
80
+ ```
81
+ 2. Start all tasks — Schwarm cascades automatically:
82
+ ```bash
83
+ schwarm tasks start <STEP1_ID>
84
+ schwarm tasks start <STEP2_ID>
85
+ schwarm tasks start <STEP3_ID>
86
+ schwarm tasks start <STEP4_ID>
87
+ ```
88
+ Tasks with unresolved dependencies move to `waiting` and become `ready`
89
+ automatically when their dependencies are archived.
90
+
91
+ **Watch for:** You must start all tasks, not just the root. Starting moves
92
+ drafts to `ready` or `waiting` (depending on whether dependencies are resolved).
93
+ Tasks left as `draft` will never run.
94
+
95
+ ### 3. Monitor task progress
96
+
97
+ **When:** You've dispatched tasks and want to check on them.
98
+
99
+ **Steps:**
100
+ 1. List active tasks (archived excluded by default):
101
+ ```bash
102
+ schwarm tasks list
103
+ schwarm tasks list --status claimed
104
+ schwarm tasks list --status ready
105
+ schwarm tasks list --status error
106
+ ```
107
+ 2. Check a specific task:
108
+ ```bash
109
+ schwarm tasks show <TASK_ID>
110
+ ```
111
+ Look at Status, Branch, and Error fields.
112
+ 3. View agent session logs for debugging:
113
+ ```bash
114
+ schwarm sessions list --json
115
+ schwarm sessions show <SESSION_ID>
116
+ ```
117
+
118
+ **Watch for:** A task in `claimed` status means a node is actively working on
119
+ it. `ready` means it's waiting for a node to pick it up.
120
+
121
+ ### 4. Send a message to a running task
122
+
123
+ **When:** A task is in `claimed` status and you want to give it additional
124
+ instructions or course corrections.
125
+
126
+ **Steps:**
127
+ 1. Send the message:
128
+ ```bash
129
+ schwarm tasks message <TASK_ID> --content "Also fix the validation for the password field"
130
+ ```
131
+
132
+ **Watch for:** Messages are delivered asynchronously via the node's polling loop.
133
+ The agent will see the message on its next check. Only works for `claimed` tasks.
134
+
135
+ ### 5. Handle failures
136
+
137
+ **When:** A task has moved to `error` status.
138
+
139
+ **Steps:**
140
+ 1. Check what went wrong:
141
+ ```bash
142
+ schwarm tasks show <TASK_ID>
143
+ ```
144
+ The Error field shows the failure reason.
145
+ 2. Decide: retry or reset.
146
+ - **Retry** — re-runs the task from where it left off (same worktree):
147
+ ```bash
148
+ schwarm tasks retry <TASK_ID>
149
+ ```
150
+ - **Reset** — moves the task back to `draft` for a fresh start:
151
+ ```bash
152
+ schwarm tasks reset <TASK_ID>
153
+ schwarm tasks start <TASK_ID>
154
+ ```
155
+
156
+ **Watch for:** Use `retry` for transient failures (node crash, timeout). Use
157
+ `reset` when you need to change the prompt or start clean. After reset, you
158
+ must `start` again.
159
+
160
+ ### 6. Discover repos and templates
161
+
162
+ **When:** Before creating tasks, you need to know what's available.
163
+
164
+ **Steps:**
165
+ 1. List repositories:
166
+ ```bash
167
+ schwarm repos list
168
+ ```
169
+ 2. List task templates (pre-made prompts):
170
+ ```bash
171
+ schwarm templates list
172
+ schwarm templates show <TEMPLATE_ID>
173
+ ```
174
+ 3. Create a task from a template:
175
+ ```bash
176
+ schwarm tasks create --repo <REPO_ID> --name "Apply template" --template <TEMPLATE_ID> --status ready
177
+ ```
178
+
179
+ ### 7. Manage recurring tasks
180
+
181
+ **When:** You want a task to run on a schedule (e.g., nightly tests).
182
+
183
+ **Steps:**
184
+ 1. Create a recurring task:
185
+ ```bash
186
+ schwarm recurring create --repo <REPO_ID> --name "Nightly tests" --schedule "0 0 * * *" --prompt "Run the full test suite and fix any failures"
187
+ ```
188
+ 2. Toggle on/off:
189
+ ```bash
190
+ schwarm recurring toggle <ID>
191
+ ```
192
+ 3. List existing recurring tasks:
193
+ ```bash
194
+ schwarm recurring list --repo <REPO_ID>
195
+ ```
196
+
197
+ ### 8. Configure skills and agents
198
+
199
+ **When:** You want to customize what skills or agent instructions are available
200
+ for tasks in a repository.
201
+
202
+ **Steps:**
203
+ 1. List available skills:
204
+ ```bash
205
+ schwarm skills list
206
+ ```
207
+ 2. Attach a skill to a repository:
208
+ ```bash
209
+ schwarm repo-skills create --repo <REPO_ID> --skill <SKILL_ID>
210
+ ```
211
+ 3. Create a repository agent (custom instructions for all tasks in a repo):
212
+ ```bash
213
+ schwarm agents create --repo <REPO_ID> --name "Code style" --prompt "Always follow the project's eslint config"
214
+ ```
215
+ 4. List agents for a repo:
216
+ ```bash
217
+ schwarm agents list --repo <REPO_ID>
218
+ ```
219
+
220
+ ## Tips
221
+
222
+ - Use `--json` when parsing output programmatically.
223
+ - Check task status before taking actions (e.g., only `retry` works on `error`
224
+ tasks, only `start` works on `draft` tasks).
225
+ - When building a DAG, create all tasks as drafts first, then start them all.
226
+ - For long prompts, write them to a file and use `--prompt-file`.
227
+ - Run `schwarm <command> help` for full flag documentation on any command.
228
+ - `schwarm tasks list` excludes archived tasks by default. Use `--all` to
229
+ include them or `--status archived` to see only archived.
230
+ - `schwarm sessions list` shows only active sessions (pending/running) by
231
+ default. Use `--all` to include terminal states.
232
+ - All list commands support `--limit N` and `--page N` for pagination. The
233
+ footer shows "showing N of M · page X of Y".
234
+ - Run `schwarm skill` to print this document — useful for piping into context
235
+ or refreshing your knowledge of the available workflows.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: schwarm-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vincent Garrigues
@@ -86,6 +86,7 @@ executables:
86
86
  extensions: []
87
87
  extra_rdoc_files: []
88
88
  files:
89
+ - README.md
89
90
  - bin/schwarm
90
91
  - lib/schwarm_cli.rb
91
92
  - lib/schwarm_cli/client.rb
@@ -119,7 +120,10 @@ files:
119
120
  - lib/schwarm_cli/config.rb
120
121
  - lib/schwarm_cli/formatters/json.rb
121
122
  - lib/schwarm_cli/formatters/table.rb
123
+ - lib/schwarm_cli/git.rb
124
+ - lib/schwarm_cli/hands_off_task.rb
122
125
  - lib/schwarm_cli/version.rb
126
+ - schwarm-skill.md
123
127
  homepage: https://github.com/getdexter/schwarm
124
128
  licenses:
125
129
  - MIT