schwarm-cli 0.1.0 → 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 +4 -4
- data/lib/schwarm_cli/client/agent_sessions.rb +2 -1
- data/lib/schwarm_cli/client/tasks.rb +3 -2
- data/lib/schwarm_cli/commands/agents.rb +5 -2
- data/lib/schwarm_cli/commands/base.rb +39 -1
- data/lib/schwarm_cli/commands/main.rb +6 -0
- data/lib/schwarm_cli/commands/recurring.rb +5 -2
- data/lib/schwarm_cli/commands/repo_skills.rb +6 -3
- data/lib/schwarm_cli/commands/repos.rb +4 -1
- data/lib/schwarm_cli/commands/secrets.rb +5 -2
- data/lib/schwarm_cli/commands/sessions.rb +9 -2
- data/lib/schwarm_cli/commands/shared_agents.rb +4 -1
- data/lib/schwarm_cli/commands/skill_files.rb +4 -1
- data/lib/schwarm_cli/commands/skills.rb +4 -1
- data/lib/schwarm_cli/commands/tasks.rb +39 -3
- data/lib/schwarm_cli/commands/templates.rb +4 -1
- data/lib/schwarm_cli/formatters/table.rb +14 -2
- data/lib/schwarm_cli/git.rb +18 -0
- data/lib/schwarm_cli/hands_off_task.rb +147 -0
- data/lib/schwarm_cli/version.rb +1 -1
- data/lib/schwarm_cli.rb +2 -0
- data/schwarm-skill.md +226 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 066e7d9d88f9e9d06311933b23308735d9856580de1f2d2dfc320d90f45f6deb
|
|
4
|
+
data.tar.gz: f46de7b54809c3283f14669c0727d308d6e5b8d24066c0e99618c7945b0b044f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 28ae5f67cdd2bbca5a04d1571171252adb36fd7eaccc489f8306038887c52e55c65fea6e348fc5af1ac849f5fd6725be49741d3f68cadfd863897d370fc9ac44
|
|
7
|
+
data.tar.gz: ace4db1afda31bcdc3b4555c7167c3259ec2723ec9a86f6750b9c6eb85ef4dcf26cc71f7e1e636b2882d510cb1e006daf277988851758467412811f445a9765c
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
module SchwarmCli
|
|
4
4
|
class Client
|
|
5
5
|
class AgentSessions < Resource
|
|
6
|
-
def list(source: nil, status: nil, page: nil, per_page: nil)
|
|
6
|
+
def list(source: nil, status: nil, page: nil, per_page: nil, active: nil)
|
|
7
7
|
params = { source:, status:, page:, per_page: }.compact
|
|
8
|
+
params[:active] = active unless active.nil?
|
|
8
9
|
get("/api/v2/agent_sessions", params).body
|
|
9
10
|
end
|
|
10
11
|
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
module SchwarmCli
|
|
4
4
|
class Client
|
|
5
5
|
class Tasks < Resource
|
|
6
|
-
def list(
|
|
7
|
-
params =
|
|
6
|
+
def list(query: nil, **filters)
|
|
7
|
+
params = filters.compact
|
|
8
|
+
params[:q] = query if query
|
|
8
9
|
get("/api/v2/tasks", params).body
|
|
9
10
|
end
|
|
10
11
|
|
|
@@ -6,10 +6,13 @@ module SchwarmCli
|
|
|
6
6
|
desc "list", "List repository agents"
|
|
7
7
|
option :repo, type: :string, desc: "Filter by repository ID"
|
|
8
8
|
option :query, type: :string, desc: "Search by name"
|
|
9
|
+
pagination_options
|
|
9
10
|
def list
|
|
10
11
|
handle_errors do
|
|
11
|
-
data =
|
|
12
|
-
|
|
12
|
+
data = fetch_paged do |page_params|
|
|
13
|
+
client.agents.list(repository_id: options[:repo], query: options[:query], **page_params)
|
|
14
|
+
end
|
|
15
|
+
output_list(data, columns: [%w[ID id], %w[NAME name], %w[REPO github_repository_name],
|
|
13
16
|
%w[ENABLED enabled], %w[SCHEDULE schedule]])
|
|
14
17
|
end
|
|
15
18
|
end
|
|
@@ -9,6 +9,16 @@ module SchwarmCli
|
|
|
9
9
|
class_option :url, type: :string, desc: "Schwarm server URL (overrides config)"
|
|
10
10
|
class_option :token, type: :string, desc: "API key (overrides config)"
|
|
11
11
|
|
|
12
|
+
DEFAULT_LIST_LIMIT = 20
|
|
13
|
+
|
|
14
|
+
# Declares --limit, --page, --all options for the next method.
|
|
15
|
+
def self.pagination_options
|
|
16
|
+
method_option :limit, type: :numeric,
|
|
17
|
+
desc: "Max results per page (default: #{DEFAULT_LIST_LIMIT})"
|
|
18
|
+
method_option :page, type: :numeric, desc: "Page number (1-indexed)"
|
|
19
|
+
method_option :all, type: :boolean, default: false, desc: "Fetch all pages (no cap)"
|
|
20
|
+
end
|
|
21
|
+
|
|
12
22
|
private
|
|
13
23
|
|
|
14
24
|
def client
|
|
@@ -19,8 +29,36 @@ module SchwarmCli
|
|
|
19
29
|
if options[:json]
|
|
20
30
|
Formatters::Json.format(data)
|
|
21
31
|
else
|
|
22
|
-
Formatters::Table.format_list(data["data"], columns:)
|
|
32
|
+
Formatters::Table.format_list(data["data"], columns:, meta: data["meta"])
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Yields pagination params to the block (once per page). With --all, pages
|
|
37
|
+
# through all results; otherwise a single call using --limit/--page (or the
|
|
38
|
+
# CLI default limit). Returns the merged payload.
|
|
39
|
+
def fetch_paged(&block)
|
|
40
|
+
if options[:all]
|
|
41
|
+
fetch_all_pages(&block)
|
|
42
|
+
else
|
|
43
|
+
per_page = options[:limit] || DEFAULT_LIST_LIMIT
|
|
44
|
+
block.call({ page: options[:page], per_page: }.compact)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def fetch_all_pages
|
|
49
|
+
merged_data = []
|
|
50
|
+
page = 1
|
|
51
|
+
per_page = options[:limit] || 200
|
|
52
|
+
meta = nil
|
|
53
|
+
loop do
|
|
54
|
+
response = yield({ page:, per_page: })
|
|
55
|
+
merged_data.concat(response["data"] || [])
|
|
56
|
+
meta = response["meta"]
|
|
57
|
+
break unless meta && page < meta["total_pages"].to_i
|
|
58
|
+
|
|
59
|
+
page += 1
|
|
23
60
|
end
|
|
61
|
+
{ "data" => merged_data, "meta" => meta&.merge("page" => 1, "per_page" => merged_data.size) }
|
|
24
62
|
end
|
|
25
63
|
|
|
26
64
|
def output_record(data, fields:)
|
|
@@ -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
|
|
@@ -6,10 +6,13 @@ module SchwarmCli
|
|
|
6
6
|
desc "list", "List recurring tasks"
|
|
7
7
|
option :repo, type: :string, desc: "Filter by repository ID"
|
|
8
8
|
option :query, type: :string, desc: "Search by name"
|
|
9
|
+
pagination_options
|
|
9
10
|
def list
|
|
10
11
|
handle_errors do
|
|
11
|
-
data =
|
|
12
|
-
|
|
12
|
+
data = fetch_paged do |page_params|
|
|
13
|
+
client.recurring.list(repository_id: options[:repo], query: options[:query], **page_params)
|
|
14
|
+
end
|
|
15
|
+
output_list(data, columns: [%w[ID id], %w[NAME name], %w[REPO github_repository_name],
|
|
13
16
|
%w[ENABLED enabled], %w[SCHEDULE schedule]])
|
|
14
17
|
end
|
|
15
18
|
end
|
|
@@ -6,11 +6,14 @@ module SchwarmCli
|
|
|
6
6
|
desc "list", "List repository-skill associations"
|
|
7
7
|
option :repo, type: :string, desc: "Filter by repository ID"
|
|
8
8
|
option :skill, type: :string, desc: "Filter by skill ID"
|
|
9
|
+
pagination_options
|
|
9
10
|
def list
|
|
10
11
|
handle_errors do
|
|
11
|
-
data =
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
data = fetch_paged do |page_params|
|
|
13
|
+
client.repo_skills.list(repository_id: options[:repo], skill_id: options[:skill], **page_params)
|
|
14
|
+
end
|
|
15
|
+
output_list(data, columns: [%w[ID id], %w[REPO github_repository_name],
|
|
16
|
+
%w[SKILL skill_name]])
|
|
14
17
|
end
|
|
15
18
|
end
|
|
16
19
|
|
|
@@ -6,9 +6,12 @@ module SchwarmCli
|
|
|
6
6
|
desc "list", "List repositories"
|
|
7
7
|
option :status, type: :string, desc: "Filter by status"
|
|
8
8
|
option :query, type: :string, desc: "Search by name"
|
|
9
|
+
pagination_options
|
|
9
10
|
def list
|
|
10
11
|
handle_errors do
|
|
11
|
-
data =
|
|
12
|
+
data = fetch_paged do |page_params|
|
|
13
|
+
client.repositories.list(status: options[:status], query: options[:query], **page_params)
|
|
14
|
+
end
|
|
12
15
|
output_list(data, columns: [%w[ID id], %w[NAME name], %w[STATUS status]])
|
|
13
16
|
end
|
|
14
17
|
end
|
|
@@ -5,10 +5,13 @@ module SchwarmCli
|
|
|
5
5
|
class Secrets < Base
|
|
6
6
|
desc "list", "List secret files"
|
|
7
7
|
option :repo, type: :string, desc: "Filter by repository ID"
|
|
8
|
+
pagination_options
|
|
8
9
|
def list
|
|
9
10
|
handle_errors do
|
|
10
|
-
data =
|
|
11
|
-
|
|
11
|
+
data = fetch_paged do |page_params|
|
|
12
|
+
client.secrets.list(repository_id: options[:repo], **page_params)
|
|
13
|
+
end
|
|
14
|
+
output_list(data, columns: [%w[ID id], %w[NAME name], %w[REPO github_repository_name],
|
|
12
15
|
%w[PATH path]])
|
|
13
16
|
end
|
|
14
17
|
end
|
|
@@ -3,12 +3,19 @@
|
|
|
3
3
|
module SchwarmCli
|
|
4
4
|
module Commands
|
|
5
5
|
class Sessions < Base
|
|
6
|
-
desc "list", "List agent sessions"
|
|
6
|
+
desc "list", "List agent sessions (shows active ones by default)"
|
|
7
7
|
option :source, type: :string, desc: "Filter by source"
|
|
8
8
|
option :status, type: :string, desc: "Filter by status"
|
|
9
|
+
pagination_options
|
|
9
10
|
def list
|
|
10
11
|
handle_errors do
|
|
11
|
-
|
|
12
|
+
active_only = options[:status].nil? && !options[:all]
|
|
13
|
+
data = fetch_paged do |page_params|
|
|
14
|
+
client.sessions.list(
|
|
15
|
+
source: options[:source], status: options[:status],
|
|
16
|
+
active: active_only ? true : nil, **page_params
|
|
17
|
+
)
|
|
18
|
+
end
|
|
12
19
|
output_list(data, columns: [%w[ID id], %w[SOURCE source], %w[STATUS status],
|
|
13
20
|
%w[CREATED created_at]])
|
|
14
21
|
end
|
|
@@ -5,9 +5,12 @@ module SchwarmCli
|
|
|
5
5
|
class SharedAgentsCmd < Base
|
|
6
6
|
desc "list", "List shared agents"
|
|
7
7
|
option :query, type: :string, desc: "Search by name"
|
|
8
|
+
pagination_options
|
|
8
9
|
def list
|
|
9
10
|
handle_errors do
|
|
10
|
-
data =
|
|
11
|
+
data = fetch_paged do |page_params|
|
|
12
|
+
client.shared_agents.list(query: options[:query], **page_params)
|
|
13
|
+
end
|
|
11
14
|
output_list(data, columns: [%w[ID id], %w[NAME name], %w[ENABLED enabled],
|
|
12
15
|
%w[SCHEDULE schedule]])
|
|
13
16
|
end
|
|
@@ -6,9 +6,12 @@ module SchwarmCli
|
|
|
6
6
|
class_option :skill, type: :string, required: true, desc: "Skill ID"
|
|
7
7
|
|
|
8
8
|
desc "list", "List skill files"
|
|
9
|
+
pagination_options
|
|
9
10
|
def list
|
|
10
11
|
handle_errors do
|
|
11
|
-
data =
|
|
12
|
+
data = fetch_paged do |page_params|
|
|
13
|
+
client.skill_files.list(skill_id: options[:skill], **page_params)
|
|
14
|
+
end
|
|
12
15
|
output_list(data, columns: [%w[ID id], %w[PATH path], %w[CREATED created_at]])
|
|
13
16
|
end
|
|
14
17
|
end
|
|
@@ -5,9 +5,12 @@ module SchwarmCli
|
|
|
5
5
|
class SkillsCmd < Base
|
|
6
6
|
desc "list", "List skills"
|
|
7
7
|
option :query, type: :string, desc: "Search by name"
|
|
8
|
+
pagination_options
|
|
8
9
|
def list
|
|
9
10
|
handle_errors do
|
|
10
|
-
data =
|
|
11
|
+
data = fetch_paged do |page_params|
|
|
12
|
+
client.skills.list(query: options[:query], **page_params)
|
|
13
|
+
end
|
|
11
14
|
output_list(data, columns: [%w[ID id], %w[NAME name], %w[GLOBAL global]])
|
|
12
15
|
end
|
|
13
16
|
end
|
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
module SchwarmCli
|
|
4
4
|
module Commands
|
|
5
5
|
class Tasks < Base
|
|
6
|
-
desc "list", "List tasks"
|
|
6
|
+
desc "list", "List tasks (excludes archived by default)"
|
|
7
7
|
option :status, type: :string, desc: "Filter by status"
|
|
8
8
|
option :repo, type: :string, desc: "Filter by repository ID"
|
|
9
9
|
option :query, type: :string, desc: "Search by name"
|
|
10
|
+
pagination_options
|
|
10
11
|
def list
|
|
11
12
|
handle_errors do
|
|
12
|
-
data = client.tasks.list(
|
|
13
|
+
data = fetch_paged { |page_params| client.tasks.list(**list_attrs, **page_params) }
|
|
13
14
|
output_list(data, columns: [%w[ID id], %w[NAME name], %w[STATUS status],
|
|
14
|
-
%w[REPO
|
|
15
|
+
%w[REPO github_repository_name], %w[CREATED created_at]])
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
|
|
@@ -113,8 +114,43 @@ module SchwarmCli
|
|
|
113
114
|
end
|
|
114
115
|
end
|
|
115
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
|
+
|
|
116
134
|
private
|
|
117
135
|
|
|
136
|
+
def list_attrs
|
|
137
|
+
exclude_archived = options[:status].nil? && !options[:all]
|
|
138
|
+
{
|
|
139
|
+
status: options[:status], repository_id: options[:repo], query: options[:query],
|
|
140
|
+
archived: exclude_archived ? false : nil
|
|
141
|
+
}
|
|
142
|
+
end
|
|
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
|
+
|
|
118
154
|
def create_attrs
|
|
119
155
|
{
|
|
120
156
|
name: options[:name], prompt: read_prompt,
|
|
@@ -5,9 +5,12 @@ module SchwarmCli
|
|
|
5
5
|
class Templates < Base
|
|
6
6
|
desc "list", "List task templates"
|
|
7
7
|
option :query, type: :string, desc: "Search by name"
|
|
8
|
+
pagination_options
|
|
8
9
|
def list
|
|
9
10
|
handle_errors do
|
|
10
|
-
data =
|
|
11
|
+
data = fetch_paged do |page_params|
|
|
12
|
+
client.templates.list(query: options[:query], **page_params)
|
|
13
|
+
end
|
|
11
14
|
output_list(data, columns: [%w[ID id], %w[NAME name], %w[CREATED created_at]])
|
|
12
15
|
end
|
|
13
16
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module SchwarmCli
|
|
4
4
|
module Formatters
|
|
5
5
|
module Table
|
|
6
|
-
def self.format_list(records, columns:)
|
|
6
|
+
def self.format_list(records, columns:, meta: nil)
|
|
7
7
|
if records.empty?
|
|
8
8
|
puts "No results found."
|
|
9
9
|
return
|
|
@@ -14,6 +14,18 @@ module SchwarmCli
|
|
|
14
14
|
records.each do |record|
|
|
15
15
|
puts format_row(columns.map { |_, key| record[key].to_s }, columns, widths)
|
|
16
16
|
end
|
|
17
|
+
footer = pagination_footer(records, meta)
|
|
18
|
+
puts "\n#{footer}" if footer
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.pagination_footer(records, meta)
|
|
22
|
+
meta = meta&.transform_keys(&:to_s)
|
|
23
|
+
total = meta && meta["total_count"]
|
|
24
|
+
return nil if total.nil? || total <= records.size
|
|
25
|
+
|
|
26
|
+
parts = ["showing #{records.size} of #{total}"]
|
|
27
|
+
parts << "page #{meta['page']} of #{meta['total_pages']}" if meta["total_pages"].to_i > 1
|
|
28
|
+
"#{parts.join(' · ')} — use --all or --page/--limit to see more"
|
|
17
29
|
end
|
|
18
30
|
|
|
19
31
|
def self.format_record(record, fields:)
|
|
@@ -33,7 +45,7 @@ module SchwarmCli
|
|
|
33
45
|
values.zip(columns).map { |val, (_, key)| val.ljust(widths[key]) }.join(" ")
|
|
34
46
|
end
|
|
35
47
|
|
|
36
|
-
private_class_method :column_widths, :format_row
|
|
48
|
+
private_class_method :column_widths, :format_row, :pagination_footer
|
|
37
49
|
end
|
|
38
50
|
end
|
|
39
51
|
end
|
|
@@ -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
|
data/lib/schwarm_cli/version.rb
CHANGED
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.
|
|
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
|