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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 485f701799de73d084bf43fff3cc0ebf1244a41397775baea03de793b860b4b9
4
- data.tar.gz: 6125bc6e805189c3faa9ec3d33ff6545fd77c4c93ab966ccae142a2fc50102a5
3
+ metadata.gz: 066e7d9d88f9e9d06311933b23308735d9856580de1f2d2dfc320d90f45f6deb
4
+ data.tar.gz: f46de7b54809c3283f14669c0727d308d6e5b8d24066c0e99618c7945b0b044f
5
5
  SHA512:
6
- metadata.gz: 1ef6550c32124fe3a2125dc2e79d10c05ec918bbabafd17918d1c13274954b33fae413cd679f58e5484c4b02b97bda64c6d495a62fb02d38e1afba389de7ac69
7
- data.tar.gz: 2057d8334094ab913ebc115ed3818a54887cafa922688bcc66f54be4035322d51466ccd7610a1aad0b68f59ccb643604551a7b7e791168ba49f3be4a9e83dbf7
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(status: nil, repository_id: nil, page: nil, per_page: nil, query: nil)
7
- params = { status:, repository_id:, page:, per_page:, q: query }.compact
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 = client.agents.list(repository_id: options[:repo], query: options[:query])
12
- output_list(data, columns: [%w[ID id], %w[NAME name], %w[REPO github_repository_id],
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 = client.recurring.list(repository_id: options[:repo], query: options[:query])
12
- output_list(data, columns: [%w[ID id], %w[NAME name], %w[REPO github_repository_id],
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 = client.repo_skills.list(repository_id: options[:repo], skill_id: options[:skill])
12
- output_list(data, columns: [%w[ID id], %w[REPO github_repository_id],
13
- %w[SKILL skill_id]])
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 = client.repositories.list(status: options[:status], query: options[:query])
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 = client.secrets.list(repository_id: options[:repo])
11
- output_list(data, columns: [%w[ID id], %w[NAME name], %w[REPO github_repository_id],
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
- data = client.sessions.list(source: options[:source], status: options[:status])
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 = client.shared_agents.list(query: options[:query])
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 = client.skill_files.list(skill_id: options[:skill])
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 = client.skills.list(query: options[:query])
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(status: options[:status], repository_id: options[:repo], query: options[:query])
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 github_repository_id], %w[CREATED created_at]])
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 = client.templates.list(query: options[:query])
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SchwarmCli
4
- VERSION = "0.1.0"
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.0
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