schwarm-cli 0.1.5 → 0.1.7

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: f86723cf9675bb0de813b11efa16be17bd2b8bfa74bd9af1203130cdf7fda0f0
4
- data.tar.gz: 479a9e83426d1fb675b79dd5780b14264d7a6c8454ee30404c104743aed30fdd
3
+ metadata.gz: 601a7ca203a5837a47d4bd0348300054e07fe3577a57c99865a64c282e310d8f
4
+ data.tar.gz: 857487dd8fa4d92e575466ebe1ed63c70fd3b1c744c6b041eb3d4c675c4657fc
5
5
  SHA512:
6
- metadata.gz: 9e473284f4c066af2f5ed3a77cc365448574acd73a55e1f72e5d2eb94d1b11db5a95fc4fdaf23c498ecb721ef0f2f596ff753c1c869726446a66d6ae622cf167
7
- data.tar.gz: b3d2498b0dcea13e9ed75ea1ee865fdf25658a31a5552059cfb617efcead48600f46008fbd4f580766250daa6b99f38619412a4c4cfa32175f6736b7f45e7c3e
6
+ metadata.gz: 899c50f55a6b91d9a8690603de5cbf7dc429b022f18198d205c3679eb6a97911e1fb5afe6bc96cb8ae76f852bb38edf9ca7ef82df245249e5ef1fc5e8188ccc9
7
+ data.tar.gz: 506dd09f7f837606a154cd61f0e2f79e7b231af0daa1e495115ba16b2e7c6fc5290dc101fd0c30ca8370132bc08178e39158295100a68c3a36a0e21e2079ca58
@@ -22,18 +22,23 @@ module SchwarmCli
22
22
  handle_errors do
23
23
  data = client.agents.find(id)
24
24
  output_record(data, fields: {
25
- "ID" => "id", "Name" => "name", "Repository" => "github_repository_id",
26
- "Enabled" => "enabled", "Schedule" => "schedule", "Prompt" => "prompt",
25
+ "ID" => "id", "Name" => "name", "Repository" => "github_repository_name",
26
+ "Shared Agent" => "shared_agent_id", "Enabled" => "enabled",
27
+ "Schedule" => "schedule",
28
+ "Additional Instructions" => "additional_instructions",
27
29
  "Created" => "created_at", "Updated" => "updated_at"
28
30
  })
29
31
  end
30
32
  end
31
33
 
32
- desc "create", "Create a repository agent"
33
- option :name, type: :string, required: true, desc: "Agent name"
34
+ desc "create", "Create a repository agent (subscribe a shared agent to a repo)"
34
35
  option :repo, type: :string, required: true, desc: "Repository ID, owner/repo, or GitHub URL"
35
- option :prompt, type: :string, required: true, desc: "Agent prompt"
36
- option :schedule, type: :string, desc: "Cron schedule"
36
+ option :shared_agent, type: :string, required: true,
37
+ desc: "Shared agent ID to subscribe to this repository"
38
+ option :additional_instructions, type: :string,
39
+ desc: "Per-repository additional instructions appended to the shared prompt"
40
+ option :enabled, type: :boolean, default: true,
41
+ desc: "Whether the agent runs (default: true)"
37
42
  def create
38
43
  handle_errors do
39
44
  data = client.agents.create(**create_attrs)
@@ -42,9 +47,9 @@ module SchwarmCli
42
47
  end
43
48
 
44
49
  desc "update ID", "Update a repository agent"
45
- option :name, type: :string, desc: "Agent name"
46
- option :prompt, type: :string, desc: "Agent prompt"
47
- option :schedule, type: :string, desc: "Cron schedule"
50
+ option :additional_instructions, type: :string,
51
+ desc: "Per-repository additional instructions"
52
+ option :enabled, type: :boolean, desc: "Whether the agent runs"
48
53
  def update(id)
49
54
  handle_errors do
50
55
  data = client.agents.update(id, **update_attrs)
@@ -82,18 +87,18 @@ module SchwarmCli
82
87
 
83
88
  def create_attrs
84
89
  attrs = {
85
- name: options[:name], github_repository_id: resolve_repo(options[:repo]),
86
- prompt: options[:prompt]
90
+ shared_agent_id: options[:shared_agent],
91
+ github_repository_id: resolve_repo(options[:repo]),
92
+ enabled: options[:enabled]
87
93
  }
88
- attrs[:schedule] = options[:schedule] if options[:schedule]
94
+ attrs[:additional_instructions] = options[:additional_instructions] if options[:additional_instructions]
89
95
  attrs
90
96
  end
91
97
 
92
98
  def update_attrs
93
99
  {}.tap do |attrs|
94
- attrs[:name] = options[:name] if options[:name]
95
- attrs[:prompt] = options[:prompt] if options[:prompt]
96
- attrs[:schedule] = options[:schedule] if options[:schedule]
100
+ attrs[:additional_instructions] = options[:additional_instructions] if options[:additional_instructions]
101
+ attrs[:enabled] = options[:enabled] unless options[:enabled].nil?
97
102
  end
98
103
  end
99
104
  end
@@ -9,6 +9,8 @@ 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
+ def self.exit_on_failure? = true
13
+
12
14
  DEFAULT_LIST_LIMIT = 20
13
15
 
14
16
  # Declares --limit, --page, --all options for the next method.
@@ -32,6 +34,13 @@ module SchwarmCli
32
34
  abort "Error: #{e.message}"
33
35
  end
34
36
 
37
+ # Resolves --template input (ID or exact name) to a schwarm template ID.
38
+ def resolve_template(ref)
39
+ SchwarmCli::TemplateResolver.new(client: client).resolve(ref)
40
+ rescue SchwarmCli::TemplateResolver::ResolutionError => e
41
+ abort "Error: #{e.message}"
42
+ end
43
+
35
44
  def output_list(data, columns:)
36
45
  if options[:json]
37
46
  Formatters::Json.format(data)
@@ -80,12 +89,12 @@ module SchwarmCli
80
89
  yield
81
90
  rescue Faraday::UnauthorizedError
82
91
  abort "Error: Unauthorized. Check your API key or run `schwarm configure`."
83
- rescue Faraday::ResourceNotFound
84
- abort "Error: Not found."
92
+ rescue Faraday::ResourceNotFound => e
93
+ abort "Error: #{parse_error_body(e, fallback: 'Not found.')}"
85
94
  rescue Faraday::UnprocessableEntityError => e
86
- abort "Error: #{parse_error_body(e)}"
95
+ abort "Error: #{parse_error_body(e, fallback: 'Validation failed')}"
87
96
  rescue Faraday::ClientError => e
88
- abort "Error: #{e.message}"
97
+ abort "Error: #{parse_error_body(e, fallback: e.message)}"
89
98
  rescue Faraday::ServerError
90
99
  abort "Error: Server error. Try again later."
91
100
  rescue Faraday::ConnectionFailed
@@ -94,14 +103,14 @@ module SchwarmCli
94
103
  abort "Error: Request timed out."
95
104
  end
96
105
 
97
- def parse_error_body(error)
106
+ def parse_error_body(error, fallback:)
98
107
  body = error.response&.dig(:body)
99
- return "Validation failed" unless body.is_a?(String)
108
+ return fallback unless body.is_a?(String) && !body.empty?
100
109
 
101
110
  parsed = JSON.parse(body)
102
- parsed.dig("error", "message") || "Validation failed"
111
+ parsed.dig("error", "message") || fallback
103
112
  rescue JSON::ParserError
104
- "Validation failed"
113
+ fallback
105
114
  end
106
115
  end
107
116
  end
@@ -17,6 +17,8 @@ require_relative "configure"
17
17
  module SchwarmCli
18
18
  module Commands
19
19
  class Main < Thor
20
+ def self.exit_on_failure? = true
21
+
20
22
  desc "tasks SUBCOMMAND", "Manage tasks"
21
23
  subcommand "tasks", Tasks
22
24
 
@@ -30,13 +32,13 @@ module SchwarmCli
30
32
  subcommand "skills", SkillsCmd
31
33
 
32
34
  desc "skill-files SUBCOMMAND", "Manage skill files"
33
- subcommand "skill-files", SkillFilesCmd
35
+ subcommand "skill_files", SkillFilesCmd
34
36
 
35
37
  desc "agents SUBCOMMAND", "Manage repository agents"
36
38
  subcommand "agents", Agents
37
39
 
38
40
  desc "shared-agents SUBCOMMAND", "Manage shared agents"
39
- subcommand "shared-agents", SharedAgentsCmd
41
+ subcommand "shared_agents", SharedAgentsCmd
40
42
 
41
43
  desc "recurring SUBCOMMAND", "Manage recurring tasks"
42
44
  subcommand "recurring", Recurring
@@ -48,7 +50,7 @@ module SchwarmCli
48
50
  subcommand "sessions", Sessions
49
51
 
50
52
  desc "repo-skills SUBCOMMAND", "Manage repository-skill associations"
51
- subcommand "repo-skills", RepoSkills
53
+ subcommand "repo_skills", RepoSkills
52
54
 
53
55
  desc "configure", "Set up Schwarm CLI configuration"
54
56
  subcommand "configure", Configure
@@ -13,7 +13,7 @@ module SchwarmCli
13
13
  client.recurring.list(repository_id: resolve_repo(options[:repo]), query: options[:query], **page_params)
14
14
  end
15
15
  output_list(data, columns: [%w[ID id], %w[NAME name], %w[REPO github_repository_name],
16
- %w[ENABLED enabled], %w[SCHEDULE schedule]])
16
+ %w[ENABLED enabled], %w[SCHEDULE cron_expression]])
17
17
  end
18
18
  end
19
19
 
@@ -23,7 +23,7 @@ module SchwarmCli
23
23
  data = client.recurring.find(id)
24
24
  output_record(data, fields: {
25
25
  "ID" => "id", "Name" => "name", "Repository" => "github_repository_id",
26
- "Enabled" => "enabled", "Schedule" => "schedule", "Prompt" => "prompt",
26
+ "Enabled" => "enabled", "Schedule" => "cron_expression", "Prompt" => "prompt",
27
27
  "Created" => "created_at", "Updated" => "updated_at"
28
28
  })
29
29
  end
@@ -38,11 +38,11 @@ module SchwarmCli
38
38
  handle_errors do
39
39
  attrs = {
40
40
  name: options[:name], github_repository_id: resolve_repo(options[:repo]),
41
- prompt: options[:prompt], schedule: options[:schedule]
41
+ prompt: options[:prompt], cron_expression: options[:schedule]
42
42
  }
43
43
 
44
44
  data = client.recurring.create(**attrs)
45
- output_record(data, fields: { "ID" => "id", "Name" => "name", "Schedule" => "schedule" })
45
+ output_record(data, fields: { "ID" => "id", "Name" => "name", "Schedule" => "cron_expression" })
46
46
  end
47
47
  end
48
48
 
@@ -53,7 +53,7 @@ module SchwarmCli
53
53
  def update(id)
54
54
  handle_errors do
55
55
  data = client.recurring.update(id, **update_attrs)
56
- output_record(data, fields: { "ID" => "id", "Name" => "name", "Schedule" => "schedule" })
56
+ output_record(data, fields: { "ID" => "id", "Name" => "name", "Schedule" => "cron_expression" })
57
57
  end
58
58
  end
59
59
 
@@ -80,7 +80,7 @@ module SchwarmCli
80
80
  {}.tap do |attrs|
81
81
  attrs[:name] = options[:name] if options[:name]
82
82
  attrs[:prompt] = options[:prompt] if options[:prompt]
83
- attrs[:schedule] = options[:schedule] if options[:schedule]
83
+ attrs[:cron_expression] = options[:schedule] if options[:schedule]
84
84
  end
85
85
  end
86
86
  end
@@ -64,7 +64,11 @@ module SchwarmCli
64
64
  def pause(id)
65
65
  handle_errors do
66
66
  data = client.repositories.pause(id)
67
- puts "Repository #{id} paused (#{data.dig('data', 'status')})."
67
+ if options[:json]
68
+ output_record(data, fields: {})
69
+ else
70
+ puts "Repository #{id} paused (#{data.dig('data', 'status')})."
71
+ end
68
72
  end
69
73
  end
70
74
 
@@ -72,7 +76,11 @@ module SchwarmCli
72
76
  def resume(id)
73
77
  handle_errors do
74
78
  data = client.repositories.resume(id)
75
- puts "Repository #{id} resumed (#{data.dig('data', 'status')})."
79
+ if options[:json]
80
+ output_record(data, fields: {})
81
+ else
82
+ puts "Repository #{id} resumed (#{data.dig('data', 'status')})."
83
+ end
76
84
  end
77
85
  end
78
86
 
@@ -11,50 +11,46 @@ module SchwarmCli
11
11
  data = fetch_paged do |page_params|
12
12
  client.secrets.list(repository_id: resolve_repo(options[:repo]), **page_params)
13
13
  end
14
- output_list(data, columns: [%w[ID id], %w[NAME name], %w[REPO github_repository_name],
15
- %w[PATH path]])
14
+ output_list(data, columns: [%w[ID id], %w[REPO github_repository_name], %w[PATH path]])
16
15
  end
17
16
  end
18
17
 
19
- desc "show ID", "Show secret file details"
18
+ desc "show ID", "Show secret file details (metadata only — content is never returned)"
20
19
  def show(id)
21
20
  handle_errors do
22
21
  data = client.secrets.find(id)
23
22
  output_record(data, fields: {
24
- "ID" => "id", "Name" => "name", "Repository" => "github_repository_id",
23
+ "ID" => "id", "Repository" => "github_repository_name",
25
24
  "Path" => "path", "Created" => "created_at", "Updated" => "updated_at"
26
25
  })
27
26
  end
28
27
  end
29
28
 
30
29
  desc "create", "Create a secret file"
31
- option :name, type: :string, required: true, desc: "Secret name"
32
30
  option :repo, type: :string, required: true, desc: "Repository ID, owner/repo, or GitHub URL"
33
31
  option :path, type: :string, required: true, desc: "File path in workspace"
34
- option :content, type: :string, desc: "Secret content"
35
- option :file, type: :string, desc: "Read content from file"
32
+ option :content, type: :string, desc: "Secret content (mutually exclusive with --file)"
33
+ option :file, type: :string, desc: "Read content from file (mutually exclusive with --content)"
36
34
  def create
37
35
  handle_errors do
38
- content = read_content
39
36
  attrs = {
40
- name: options[:name], github_repository_id: resolve_repo(options[:repo]),
41
- path: options[:path], content:
37
+ github_repository_id: resolve_repo(options[:repo]),
38
+ path: options[:path], content: read_content(required: true)
42
39
  }
43
40
 
44
41
  data = client.secrets.create(**attrs)
45
- output_record(data, fields: { "ID" => "id", "Name" => "name", "Path" => "path" })
42
+ output_record(data, fields: { "ID" => "id", "Path" => "path" })
46
43
  end
47
44
  end
48
45
 
49
46
  desc "update ID", "Update a secret file"
50
- option :name, type: :string, desc: "Secret name"
51
47
  option :path, type: :string, desc: "File path in workspace"
52
- option :content, type: :string, desc: "Secret content"
53
- option :file, type: :string, desc: "Read content from file"
48
+ option :content, type: :string, desc: "Secret content (mutually exclusive with --file)"
49
+ option :file, type: :string, desc: "Read content from file (mutually exclusive with --content)"
54
50
  def update(id)
55
51
  handle_errors do
56
52
  data = client.secrets.update(id, **update_attrs)
57
- output_record(data, fields: { "ID" => "id", "Name" => "name", "Path" => "path" })
53
+ output_record(data, fields: { "ID" => "id", "Path" => "path" })
58
54
  end
59
55
  end
60
56
 
@@ -69,19 +65,24 @@ module SchwarmCli
69
65
  private
70
66
 
71
67
  def update_attrs
72
- { name: options[:name], path: options[:path] }.compact.tap do |attrs|
73
- attrs[:content] = read_content if options[:content] || options[:file]
68
+ { path: options[:path] }.compact.tap do |attrs|
69
+ content = read_content(required: false)
70
+ attrs[:content] = content if content
74
71
  end
75
72
  end
76
73
 
77
- def read_content
78
- if options[:file]
79
- file_path = File.expand_path(options[:file])
80
- abort "Error: File not found: #{file_path}" unless File.exist?(file_path)
81
- File.read(file_path)
82
- else
83
- options[:content]
84
- end
74
+ def read_content(required:)
75
+ abort "Error: --content and --file are mutually exclusive." if options[:content] && options[:file]
76
+ return options[:content] if options[:content]
77
+ return read_file(options[:file]) if options[:file]
78
+
79
+ abort "Error: one of --content or --file is required." if required
80
+ end
81
+
82
+ def read_file(path)
83
+ expanded = File.expand_path(path)
84
+ abort "Error: File not found: #{expanded}" unless File.exist?(expanded)
85
+ File.read(expanded)
85
86
  end
86
87
  end
87
88
  end
@@ -27,7 +27,8 @@ module SchwarmCli
27
27
  data = client.sessions.find(id)
28
28
  output_record(data, fields: {
29
29
  "ID" => "id", "Source" => "source", "Status" => "status",
30
- "Prompt" => "prompt", "Result" => "result",
30
+ "External ID" => "external_id", "Model" => "model_id",
31
+ "Error" => "error_message",
31
32
  "Created" => "created_at", "Updated" => "updated_at"
32
33
  })
33
34
  end
@@ -21,19 +21,24 @@ module SchwarmCli
21
21
  data = client.skills.find(id)
22
22
  output_record(data, fields: {
23
23
  "ID" => "id", "Name" => "name", "Description" => "description",
24
- "Global" => "global", "Created" => "created_at"
24
+ "Instructions" => "instructions", "Global" => "global",
25
+ "Created" => "created_at"
25
26
  })
26
27
  end
27
28
  end
28
29
 
29
30
  desc "create", "Create a skill"
30
31
  option :name, type: :string, required: true, desc: "Skill name"
31
- option :description, type: :string, desc: "Skill description"
32
+ option :description, type: :string, required: true, desc: "Skill description"
33
+ option :instructions, type: :string, desc: "Skill instructions (inline)"
34
+ option :instructions_file, type: :string, desc: "Read instructions from file"
32
35
  option :global, type: :boolean, default: false, desc: "Make skill global"
33
36
  def create
34
37
  handle_errors do
35
- attrs = { name: options[:name], global: options[:global] }
36
- attrs[:description] = options[:description] if options[:description]
38
+ attrs = {
39
+ name: options[:name], description: options[:description],
40
+ instructions: read_instructions, global: options[:global]
41
+ }
37
42
 
38
43
  data = client.skills.create(**attrs)
39
44
  output_record(data, fields: { "ID" => "id", "Name" => "name", "Global" => "global" })
@@ -43,6 +48,8 @@ module SchwarmCli
43
48
  desc "update ID", "Update a skill"
44
49
  option :name, type: :string, desc: "Skill name"
45
50
  option :description, type: :string, desc: "Skill description"
51
+ option :instructions, type: :string, desc: "Skill instructions (inline)"
52
+ option :instructions_file, type: :string, desc: "Read instructions from file"
46
53
  option :global, type: :boolean, desc: "Make skill global"
47
54
  def update(id)
48
55
  handle_errors do
@@ -62,10 +69,24 @@ module SchwarmCli
62
69
  private
63
70
 
64
71
  def update_attrs
72
+ instructions = read_instructions
65
73
  { name: options[:name], description: options[:description] }.compact.tap do |attrs|
74
+ attrs[:instructions] = instructions if instructions
66
75
  attrs[:global] = options[:global] unless options[:global].nil?
67
76
  end
68
77
  end
78
+
79
+ def read_instructions
80
+ if options[:instructions] && options[:instructions_file]
81
+ abort "Error: --instructions and --instructions-file are mutually exclusive."
82
+ elsif options[:instructions_file]
83
+ file_path = File.expand_path(options[:instructions_file])
84
+ abort "Error: File not found: #{file_path}" unless File.exist?(file_path)
85
+ File.read(file_path)
86
+ else
87
+ options[:instructions]
88
+ end
89
+ end
69
90
  end
70
91
  end
71
92
  end
@@ -34,7 +34,7 @@ module SchwarmCli
34
34
  option :prompt, type: :string, desc: "Task prompt"
35
35
  option :repo, type: :string, desc: "Repository ID, owner/repo, or GitHub URL"
36
36
  option :status, type: :string, desc: "Initial status (draft/waiting)"
37
- option :template, type: :string, desc: "Template ID"
37
+ option :template, type: :string, desc: "Template ID or name"
38
38
  option :depends_on, type: :array, desc: "Dependency task IDs"
39
39
  def create
40
40
  handle_errors do
@@ -116,14 +116,13 @@ module SchwarmCli
116
116
  option :prompt, type: :string, desc: "Task prompt"
117
117
  option :repo, type: :string, desc: "Repository ID, owner/repo, or GitHub URL (skips origin auto-detect)"
118
118
  option :name, type: :string, desc: "Task name (defaults to the branch name)"
119
+ option :template, type: :string, desc: "Task template ID or name"
119
120
  def handoff
120
121
  prompt = options[:prompt]
121
122
  abort "Error: --prompt is required." if prompt.nil? || prompt.strip.empty?
122
123
 
123
124
  handle_errors do
124
- result = SchwarmCli::HandsOffTask.new(client: client).call(
125
- prompt: prompt, repo_override: resolve_repo(options[:repo]), name_override: options[:name]
126
- )
125
+ result = SchwarmCli::HandsOffTask.new(client: client).call(**handoff_attrs(prompt))
127
126
  print_handoff_result(result)
128
127
  end
129
128
  end
@@ -148,11 +147,18 @@ module SchwarmCli
148
147
  end
149
148
  end
150
149
 
150
+ def handoff_attrs(prompt)
151
+ {
152
+ prompt: prompt, repo_override: resolve_repo(options[:repo]),
153
+ name_override: options[:name], template_override: options[:template]
154
+ }
155
+ end
156
+
151
157
  def create_attrs
152
158
  {
153
159
  name: options[:name], prompt: options[:prompt],
154
160
  github_repository_id: resolve_repo(options[:repo]), status: options[:status],
155
- task_template_id: options[:template], dependency_ids: options[:depends_on]
161
+ task_template_id: resolve_template(options[:template]), dependency_ids: options[:depends_on]
156
162
  }.compact
157
163
  end
158
164
 
@@ -20,7 +20,8 @@ module SchwarmCli
20
20
  handle_errors do
21
21
  data = client.templates.find(id)
22
22
  output_record(data, fields: {
23
- "ID" => "id", "Name" => "name", "Prompt" => "prompt", "Created" => "created_at"
23
+ "ID" => "id", "Name" => "name", "Description" => "description",
24
+ "Prompt" => "prompt_prefix", "Created" => "created_at"
24
25
  })
25
26
  end
26
27
  end
@@ -28,9 +29,13 @@ module SchwarmCli
28
29
  desc "create", "Create a task template"
29
30
  option :name, type: :string, required: true, desc: "Template name"
30
31
  option :prompt, type: :string, required: true, desc: "Template prompt"
32
+ option :description, type: :string, desc: "Template description"
31
33
  def create
32
34
  handle_errors do
33
- data = client.templates.create(name: options[:name], prompt: options[:prompt])
35
+ attrs = { name: options[:name], prompt_prefix: options[:prompt] }
36
+ attrs[:description] = options[:description] if options[:description]
37
+
38
+ data = client.templates.create(**attrs)
34
39
  output_record(data, fields: { "ID" => "id", "Name" => "name" })
35
40
  end
36
41
  end
@@ -38,13 +43,10 @@ module SchwarmCli
38
43
  desc "update ID", "Update a task template"
39
44
  option :name, type: :string, desc: "Template name"
40
45
  option :prompt, type: :string, desc: "Template prompt"
46
+ option :description, type: :string, desc: "Template description"
41
47
  def update(id)
42
48
  handle_errors do
43
- attrs = {}
44
- attrs[:name] = options[:name] if options[:name]
45
- attrs[:prompt] = options[:prompt] if options[:prompt]
46
-
47
- data = client.templates.update(id, **attrs)
49
+ data = client.templates.update(id, **update_attrs)
48
50
  output_record(data, fields: { "ID" => "id", "Name" => "name" })
49
51
  end
50
52
  end
@@ -56,6 +58,16 @@ module SchwarmCli
56
58
  puts "Template #{id} deleted."
57
59
  end
58
60
  end
61
+
62
+ private
63
+
64
+ def update_attrs
65
+ {}.tap do |attrs|
66
+ attrs[:name] = options[:name] if options[:name]
67
+ attrs[:prompt_prefix] = options[:prompt] if options[:prompt]
68
+ attrs[:description] = options[:description] if options[:description]
69
+ end
70
+ end
59
71
  end
60
72
  end
61
73
  end
@@ -12,14 +12,18 @@ module SchwarmCli
12
12
  @client = client
13
13
  end
14
14
 
15
- def call(prompt:, repo_override:, name_override:)
15
+ def call(prompt:, repo_override:, name_override:, template_override: nil)
16
16
  repo, branch, error = run_preflight(repo_override)
17
17
  return error if error
18
18
 
19
+ template_id, template_error = resolve_template_id(template_override)
20
+ return template_error if template_error
21
+
19
22
  push_result = @git.run("push", "-u", "origin", "HEAD")
20
23
  return failure(push_result.stderr.strip) unless push_result.success?
21
24
 
22
- create_task(repo: repo, branch: branch, prompt: prompt, name_override: name_override)
25
+ create_task(repo: repo, branch: branch, prompt: prompt, name_override: name_override,
26
+ template_id: template_id)
23
27
  rescue Faraday::Error => e
24
28
  failure(parse_api_error(e))
25
29
  end
@@ -97,13 +101,22 @@ module SchwarmCli
97
101
  nil
98
102
  end
99
103
 
100
- def create_task(repo:, branch:, prompt:, name_override:)
104
+ def resolve_template_id(ref)
105
+ [TemplateResolver.new(client: @client).resolve(ref), nil]
106
+ rescue TemplateResolver::ResolutionError => e
107
+ [nil, failure(e.message)]
108
+ end
109
+
110
+ def create_task(repo:, branch:, prompt:, name_override:, template_id:)
101
111
  response = @client.tasks.create(
102
- name: name_override || branch,
103
- branch_name: branch,
104
- prompt: prompt,
105
- status: "ready",
106
- github_repository_id: repo["id"]
112
+ **{
113
+ name: name_override || branch,
114
+ branch_name: branch,
115
+ prompt: prompt,
116
+ status: "ready",
117
+ github_repository_id: repo["id"],
118
+ task_template_id: template_id
119
+ }.compact
107
120
  )
108
121
  data = response["data"]
109
122
  Result.new(
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SchwarmCli
4
+ # Turns a user-provided `--template` reference into a task template ID.
5
+ #
6
+ # Accepted inputs:
7
+ # - a numeric template ID (looked up directly)
8
+ # - an exact template name (resolved via search + exact match)
9
+ class TemplateResolver
10
+ class ResolutionError < StandardError; end
11
+
12
+ def initialize(client:)
13
+ @client = client
14
+ end
15
+
16
+ def resolve(ref)
17
+ return nil if blank?(ref)
18
+
19
+ ref = ref.to_s.strip
20
+ template = ref.match?(/\A\d+\z/) ? find_by_id(ref) : find_by_name(ref)
21
+ raise ResolutionError, "no schwarm task template matches `#{ref}`." if template.nil?
22
+
23
+ template["id"]
24
+ end
25
+
26
+ private
27
+
28
+ def blank?(ref)
29
+ ref.nil? || ref.to_s.strip.empty?
30
+ end
31
+
32
+ def find_by_id(id)
33
+ @client.templates.find(id)["data"]
34
+ rescue Faraday::ResourceNotFound
35
+ nil
36
+ end
37
+
38
+ def find_by_name(name)
39
+ response = @client.templates.list(query: name)
40
+ Array(response["data"]).find { |t| t["name"] == name }
41
+ end
42
+ end
43
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SchwarmCli
4
- VERSION = "0.1.5"
4
+ VERSION = "0.1.7"
5
5
  end
data/lib/schwarm_cli.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "schwarm_cli/config"
5
5
  require_relative "schwarm_cli/client"
6
6
  require_relative "schwarm_cli/git"
7
7
  require_relative "schwarm_cli/repository_resolver"
8
+ require_relative "schwarm_cli/template_resolver"
8
9
  require_relative "schwarm_cli/hands_off_task"
9
10
  require_relative "schwarm_cli/formatters/json"
10
11
  require_relative "schwarm_cli/formatters/table"
data/schwarm-skill.md CHANGED
@@ -86,6 +86,42 @@ EOF
86
86
  will print the conflicting task ID. Archive or reset it first.
87
87
  - The repository is auto-detected from `git remote get-url origin`. Use
88
88
  `--repo <id>` if you need to override it (e.g. in a fork).
89
+ - Pass `--template <id-or-name>` to apply a task template to the handoff. The
90
+ value can be a template ID or its exact name.
91
+
92
+ ### 1c. Hand off a WIP branch with follow-up tasks
93
+
94
+ **When:** You're handing off a WIP branch and want other tasks to run after it
95
+ on the same repo (e.g. add docs, cut a release PR, update a dashboard). The
96
+ handoff task starts immediately; the follow-ups wait until it's archived.
97
+
98
+ **Steps:**
99
+ 1. Hand off the current branch first and note the task ID it prints:
100
+ ```bash
101
+ schwarm tasks handoff --prompt "continue per HANDOFF.md"
102
+ # => Task a1b2c3 created (status: ready).
103
+ ```
104
+ 2. Create follow-up tasks as drafts depending on the handoff task ID:
105
+ ```bash
106
+ schwarm tasks create --repo <REPO_ID> --name "Update docs" \
107
+ --prompt "Update README and CHANGELOG once the branch lands" --depends-on a1b2c3
108
+ schwarm tasks create --repo <REPO_ID> --name "Announce in #releases" \
109
+ --prompt "Post the summary to Slack" --depends-on a1b2c3
110
+ ```
111
+ 3. Start each follow-up — it will move to `waiting` until the handoff archives:
112
+ ```bash
113
+ schwarm tasks start <FOLLOWUP_ID>
114
+ ```
115
+
116
+ **Watch for:**
117
+ - The handoff task is created with `status: ready` and runs immediately. You
118
+ don't need to start it.
119
+ - Follow-ups run on fresh worktrees off the repo's base branch by default, not
120
+ off the handoff branch. `--depends-on` keeps them in `waiting` until the
121
+ handoff task is archived, so by the time they start the handoff's changes are
122
+ already on the base branch.
123
+ - Pass `--repo <id>` on `tasks create` (handoff auto-detects it from `origin`,
124
+ but `create` does not).
89
125
 
90
126
  ### 2. Dispatch a DAG of tasks
91
127
 
@@ -192,9 +228,10 @@ must `start` again.
192
228
  schwarm templates list
193
229
  schwarm templates show <TEMPLATE_ID>
194
230
  ```
195
- 3. Create a task from a template:
231
+ 3. Create a task from a template. `--template` accepts either the template ID or
232
+ its exact name:
196
233
  ```bash
197
- schwarm tasks create --repo <REPO_ID> --name "Apply template" --template <TEMPLATE_ID> --status ready
234
+ schwarm tasks create --repo <REPO_ID> --name "Apply template" --template <TEMPLATE_ID_OR_NAME> --status ready
198
235
  ```
199
236
 
200
237
  ### 7. Manage recurring tasks
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.5
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vincent Garrigues
@@ -123,6 +123,7 @@ files:
123
123
  - lib/schwarm_cli/git.rb
124
124
  - lib/schwarm_cli/hands_off_task.rb
125
125
  - lib/schwarm_cli/repository_resolver.rb
126
+ - lib/schwarm_cli/template_resolver.rb
126
127
  - lib/schwarm_cli/version.rb
127
128
  - schwarm-skill.md
128
129
  homepage: https://github.com/getdexter/schwarm