schwarm-cli 0.1.1 → 0.1.2

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: 066e7d9d88f9e9d06311933b23308735d9856580de1f2d2dfc320d90f45f6deb
4
+ data.tar.gz: f46de7b54809c3283f14669c0727d308d6e5b8d24066c0e99618c7945b0b044f
5
5
  SHA512:
6
- metadata.gz: d3841fa2204bac7b1ee9debab270e7509925388563d49a72dcfbbfb61d8c7ab593ccc14a6c6ec978925c43c0a83f46603e0607a28a4e5d4696f50b87f1f7e8c4
7
- data.tar.gz: cffebca5659d080e7b05ddd386e254acc3870b40678a59ee3e76acf5e5ee82119643ecd7390df00b7102768b5f8e11703900927a4ba035e211b0314e33a3fb5b
6
+ metadata.gz: 28ae5f67cdd2bbca5a04d1571171252adb36fd7eaccc489f8306038887c52e55c65fea6e348fc5af1ac849f5fd6725be49741d3f68cadfd863897d370fc9ac44
7
+ data.tar.gz: ace4db1afda31bcdc3b4555c7167c3259ec2723ec9a86f6750b9c6eb85ef4dcf26cc71f7e1e636b2882d510cb1e006daf277988851758467412811f445a9765c
@@ -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.2"
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,226 @@
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:
101
+ ```bash
102
+ schwarm tasks list --status claimed
103
+ schwarm tasks list --status ready
104
+ schwarm tasks list --status error
105
+ ```
106
+ 2. Check a specific task:
107
+ ```bash
108
+ schwarm tasks show <TASK_ID>
109
+ ```
110
+ Look at Status, Branch, and Error fields.
111
+ 3. View agent session logs for debugging:
112
+ ```bash
113
+ schwarm sessions list --json
114
+ schwarm sessions show <SESSION_ID>
115
+ ```
116
+
117
+ **Watch for:** A task in `claimed` status means a node is actively working on
118
+ it. `ready` means it's waiting for a node to pick it up.
119
+
120
+ ### 4. Send a message to a running task
121
+
122
+ **When:** A task is in `claimed` status and you want to give it additional
123
+ instructions or course corrections.
124
+
125
+ **Steps:**
126
+ 1. Send the message:
127
+ ```bash
128
+ schwarm tasks message <TASK_ID> --content "Also fix the validation for the password field"
129
+ ```
130
+
131
+ **Watch for:** Messages are delivered asynchronously via the node's polling loop.
132
+ The agent will see the message on its next check. Only works for `claimed` tasks.
133
+
134
+ ### 5. Handle failures
135
+
136
+ **When:** A task has moved to `error` status.
137
+
138
+ **Steps:**
139
+ 1. Check what went wrong:
140
+ ```bash
141
+ schwarm tasks show <TASK_ID>
142
+ ```
143
+ The Error field shows the failure reason.
144
+ 2. Decide: retry or reset.
145
+ - **Retry** — re-runs the task from where it left off (same worktree):
146
+ ```bash
147
+ schwarm tasks retry <TASK_ID>
148
+ ```
149
+ - **Reset** — moves the task back to `draft` for a fresh start:
150
+ ```bash
151
+ schwarm tasks reset <TASK_ID>
152
+ schwarm tasks start <TASK_ID>
153
+ ```
154
+
155
+ **Watch for:** Use `retry` for transient failures (node crash, timeout). Use
156
+ `reset` when you need to change the prompt or start clean. After reset, you
157
+ must `start` again.
158
+
159
+ ### 6. Discover repos and templates
160
+
161
+ **When:** Before creating tasks, you need to know what's available.
162
+
163
+ **Steps:**
164
+ 1. List repositories:
165
+ ```bash
166
+ schwarm repos list
167
+ ```
168
+ 2. List task templates (pre-made prompts):
169
+ ```bash
170
+ schwarm templates list
171
+ schwarm templates show <TEMPLATE_ID>
172
+ ```
173
+ 3. Create a task from a template:
174
+ ```bash
175
+ schwarm tasks create --repo <REPO_ID> --name "Apply template" --template <TEMPLATE_ID> --status ready
176
+ ```
177
+
178
+ ### 7. Manage recurring tasks
179
+
180
+ **When:** You want a task to run on a schedule (e.g., nightly tests).
181
+
182
+ **Steps:**
183
+ 1. Create a recurring task:
184
+ ```bash
185
+ schwarm recurring create --repo <REPO_ID> --name "Nightly tests" --schedule "0 0 * * *" --prompt "Run the full test suite and fix any failures"
186
+ ```
187
+ 2. Toggle on/off:
188
+ ```bash
189
+ schwarm recurring toggle <ID>
190
+ ```
191
+ 3. List existing recurring tasks:
192
+ ```bash
193
+ schwarm recurring list --repo <REPO_ID>
194
+ ```
195
+
196
+ ### 8. Configure skills and agents
197
+
198
+ **When:** You want to customize what skills or agent instructions are available
199
+ for tasks in a repository.
200
+
201
+ **Steps:**
202
+ 1. List available skills:
203
+ ```bash
204
+ schwarm skills list
205
+ ```
206
+ 2. Attach a skill to a repository:
207
+ ```bash
208
+ schwarm repo-skills create --repo <REPO_ID> --skill <SKILL_ID>
209
+ ```
210
+ 3. Create a repository agent (custom instructions for all tasks in a repo):
211
+ ```bash
212
+ schwarm agents create --repo <REPO_ID> --name "Code style" --prompt "Always follow the project's eslint config"
213
+ ```
214
+ 4. List agents for a repo:
215
+ ```bash
216
+ schwarm agents list --repo <REPO_ID>
217
+ ```
218
+
219
+ ## Tips
220
+
221
+ - Use `--json` when parsing output programmatically.
222
+ - Check task status before taking actions (e.g., only `retry` works on `error`
223
+ tasks, only `start` works on `draft` tasks).
224
+ - When building a DAG, create all tasks as drafts first, then start them all.
225
+ - For long prompts, write them to a file and use `--prompt-file`.
226
+ - Run `schwarm <command> help` for full flag documentation on any command.
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.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vincent Garrigues
@@ -119,7 +119,10 @@ files:
119
119
  - lib/schwarm_cli/config.rb
120
120
  - lib/schwarm_cli/formatters/json.rb
121
121
  - lib/schwarm_cli/formatters/table.rb
122
+ - lib/schwarm_cli/git.rb
123
+ - lib/schwarm_cli/hands_off_task.rb
122
124
  - lib/schwarm_cli/version.rb
125
+ - schwarm-skill.md
123
126
  homepage: https://github.com/getdexter/schwarm
124
127
  licenses:
125
128
  - MIT