superkick 0.1.0

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.
Files changed (199) hide show
  1. checksums.yaml +7 -0
  2. data/CLA.md +91 -0
  3. data/CLAUDE.md +2226 -0
  4. data/CONTRIBUTING.md +104 -0
  5. data/LICENSE +108 -0
  6. data/LICENSE-COMMERCIAL.md +39 -0
  7. data/PLAN.md +161 -0
  8. data/README.md +1155 -0
  9. data/exe/superkick +6 -0
  10. data/lib/superkick/agent/runtime.rb +82 -0
  11. data/lib/superkick/agent/runtimes/local.rb +74 -0
  12. data/lib/superkick/agent/runtimes.rb +4 -0
  13. data/lib/superkick/agent.rb +209 -0
  14. data/lib/superkick/agent_store.rb +85 -0
  15. data/lib/superkick/attach/client.rb +245 -0
  16. data/lib/superkick/attach/protocol.rb +71 -0
  17. data/lib/superkick/attach/server.rb +371 -0
  18. data/lib/superkick/budget_checker.rb +120 -0
  19. data/lib/superkick/buffer/client.rb +91 -0
  20. data/lib/superkick/buffer/server.rb +127 -0
  21. data/lib/superkick/cli/agent.rb +524 -0
  22. data/lib/superkick/cli/completion.rb +591 -0
  23. data/lib/superkick/cli/goal.rb +71 -0
  24. data/lib/superkick/cli/mcp.rb +34 -0
  25. data/lib/superkick/cli/monitor.rb +47 -0
  26. data/lib/superkick/cli/notifier.rb +39 -0
  27. data/lib/superkick/cli/repository.rb +46 -0
  28. data/lib/superkick/cli/server.rb +106 -0
  29. data/lib/superkick/cli/setup.rb +166 -0
  30. data/lib/superkick/cli/spawner.rb +85 -0
  31. data/lib/superkick/cli/team.rb +407 -0
  32. data/lib/superkick/cli.rb +175 -0
  33. data/lib/superkick/client_registry.rb +30 -0
  34. data/lib/superkick/configuration.rb +178 -0
  35. data/lib/superkick/connection.rb +56 -0
  36. data/lib/superkick/control/client.rb +78 -0
  37. data/lib/superkick/control/reply.rb +43 -0
  38. data/lib/superkick/control/server.rb +1271 -0
  39. data/lib/superkick/cost_accumulator.rb +53 -0
  40. data/lib/superkick/cost_extractor.rb +65 -0
  41. data/lib/superkick/cost_poller.rb +70 -0
  42. data/lib/superkick/driver/profile_source.rb +134 -0
  43. data/lib/superkick/driver.rb +179 -0
  44. data/lib/superkick/drivers/claude_code.rb +110 -0
  45. data/lib/superkick/drivers/codex.rb +57 -0
  46. data/lib/superkick/drivers/copilot.rb +75 -0
  47. data/lib/superkick/drivers/gemini.rb +86 -0
  48. data/lib/superkick/drivers/goose.rb +74 -0
  49. data/lib/superkick/drivers.rb +16 -0
  50. data/lib/superkick/drop.rb +80 -0
  51. data/lib/superkick/drops.rb +76 -0
  52. data/lib/superkick/environment_executor.rb +90 -0
  53. data/lib/superkick/goal.rb +95 -0
  54. data/lib/superkick/goals/agent_exit.rb +41 -0
  55. data/lib/superkick/goals/agent_signal.rb +42 -0
  56. data/lib/superkick/goals/command.rb +103 -0
  57. data/lib/superkick/history_buffer.rb +38 -0
  58. data/lib/superkick/hosted/attach/bridge.rb +52 -0
  59. data/lib/superkick/hosted/attach/client.rb +208 -0
  60. data/lib/superkick/hosted/attach/relay.rb +313 -0
  61. data/lib/superkick/hosted/attach/relay_store.rb +48 -0
  62. data/lib/superkick/hosted/bridge.rb +263 -0
  63. data/lib/superkick/hosted/buffer/bridge.rb +42 -0
  64. data/lib/superkick/hosted/buffer/client.rb +63 -0
  65. data/lib/superkick/hosted/buffer/relay.rb +126 -0
  66. data/lib/superkick/hosted/buffer/relay_store.rb +42 -0
  67. data/lib/superkick/hosted/control/client.rb +84 -0
  68. data/lib/superkick/hosted/mcp_proxy.rb +144 -0
  69. data/lib/superkick/inject_handler.rb +24 -0
  70. data/lib/superkick/injection_guard.rb +26 -0
  71. data/lib/superkick/injection_queue.rb +177 -0
  72. data/lib/superkick/injector.rb +65 -0
  73. data/lib/superkick/input_buffer.rb +171 -0
  74. data/lib/superkick/integrations/bugsnag/README.md +98 -0
  75. data/lib/superkick/integrations/bugsnag/spawner.rb +307 -0
  76. data/lib/superkick/integrations/bugsnag/templates/error_opened.liquid +17 -0
  77. data/lib/superkick/integrations/bugsnag.rb +7 -0
  78. data/lib/superkick/integrations/circleci/README.md +75 -0
  79. data/lib/superkick/integrations/circleci/monitor.rb +185 -0
  80. data/lib/superkick/integrations/circleci/probe.rb +36 -0
  81. data/lib/superkick/integrations/circleci/templates/ci_failure.liquid +8 -0
  82. data/lib/superkick/integrations/circleci/templates/ci_success.liquid +1 -0
  83. data/lib/superkick/integrations/circleci.rb +8 -0
  84. data/lib/superkick/integrations/datadog/README.md +253 -0
  85. data/lib/superkick/integrations/datadog/alert_goal.rb +94 -0
  86. data/lib/superkick/integrations/datadog/alert_monitor.rb +163 -0
  87. data/lib/superkick/integrations/datadog/alert_spawner.rb +201 -0
  88. data/lib/superkick/integrations/datadog/notification_templates/default.liquid +10 -0
  89. data/lib/superkick/integrations/datadog/notifier.rb +294 -0
  90. data/lib/superkick/integrations/datadog/spawner.rb +201 -0
  91. data/lib/superkick/integrations/datadog/templates/alert_changed.liquid +8 -0
  92. data/lib/superkick/integrations/datadog/templates/alert_escalated.liquid +8 -0
  93. data/lib/superkick/integrations/datadog/templates/alert_recovered.liquid +14 -0
  94. data/lib/superkick/integrations/datadog/templates/alert_triggered.liquid +29 -0
  95. data/lib/superkick/integrations/datadog/templates/error_opened.liquid +15 -0
  96. data/lib/superkick/integrations/datadog.rb +14 -0
  97. data/lib/superkick/integrations/docker/README.md +256 -0
  98. data/lib/superkick/integrations/docker/client.rb +295 -0
  99. data/lib/superkick/integrations/docker/runtime.rb +218 -0
  100. data/lib/superkick/integrations/docker.rb +4 -0
  101. data/lib/superkick/integrations/git/repository_source.rb +66 -0
  102. data/lib/superkick/integrations/git/version_control.rb +119 -0
  103. data/lib/superkick/integrations/git.rb +8 -0
  104. data/lib/superkick/integrations/github/README.md +300 -0
  105. data/lib/superkick/integrations/github/check_failed_spawner.rb +199 -0
  106. data/lib/superkick/integrations/github/drops.rb +114 -0
  107. data/lib/superkick/integrations/github/goal.rb +135 -0
  108. data/lib/superkick/integrations/github/issue_goal.rb +104 -0
  109. data/lib/superkick/integrations/github/issue_spawner.rb +160 -0
  110. data/lib/superkick/integrations/github/monitor.rb +251 -0
  111. data/lib/superkick/integrations/github/probe.rb +30 -0
  112. data/lib/superkick/integrations/github/repository_source.rb +228 -0
  113. data/lib/superkick/integrations/github/templates/check_failed.liquid +10 -0
  114. data/lib/superkick/integrations/github/templates/ci_failure.liquid +5 -0
  115. data/lib/superkick/integrations/github/templates/ci_success.liquid +1 -0
  116. data/lib/superkick/integrations/github/templates/issue_opened.liquid +20 -0
  117. data/lib/superkick/integrations/github/templates/pr_comment.liquid +2 -0
  118. data/lib/superkick/integrations/github/templates/pr_review.liquid +4 -0
  119. data/lib/superkick/integrations/github.rb +16 -0
  120. data/lib/superkick/integrations/honeybadger/README.md +97 -0
  121. data/lib/superkick/integrations/honeybadger/notification_templates/default.liquid +8 -0
  122. data/lib/superkick/integrations/honeybadger/notifier.rb +250 -0
  123. data/lib/superkick/integrations/honeybadger/spawner.rb +214 -0
  124. data/lib/superkick/integrations/honeybadger/templates/error_opened.liquid +17 -0
  125. data/lib/superkick/integrations/honeybadger.rb +9 -0
  126. data/lib/superkick/integrations/shell/README.md +83 -0
  127. data/lib/superkick/integrations/shell/monitor.rb +87 -0
  128. data/lib/superkick/integrations/shell/templates/shell_alert.liquid +6 -0
  129. data/lib/superkick/integrations/shell/templates/shell_success.liquid +6 -0
  130. data/lib/superkick/integrations/shell.rb +7 -0
  131. data/lib/superkick/integrations/shortcut/README.md +193 -0
  132. data/lib/superkick/integrations/shortcut/drops.rb +91 -0
  133. data/lib/superkick/integrations/shortcut/monitor.rb +582 -0
  134. data/lib/superkick/integrations/shortcut/probe.rb +34 -0
  135. data/lib/superkick/integrations/shortcut/spawner.rb +264 -0
  136. data/lib/superkick/integrations/shortcut/templates/related_story_changed.liquid +6 -0
  137. data/lib/superkick/integrations/shortcut/templates/story_blocker.liquid +8 -0
  138. data/lib/superkick/integrations/shortcut/templates/story_comment.liquid +5 -0
  139. data/lib/superkick/integrations/shortcut/templates/story_description_changed.liquid +19 -0
  140. data/lib/superkick/integrations/shortcut/templates/story_owner_changed.liquid +10 -0
  141. data/lib/superkick/integrations/shortcut/templates/story_ready.liquid +41 -0
  142. data/lib/superkick/integrations/shortcut/templates/story_state_changed.liquid +9 -0
  143. data/lib/superkick/integrations/shortcut/templates/story_unblocked.liquid +5 -0
  144. data/lib/superkick/integrations/shortcut.rb +11 -0
  145. data/lib/superkick/integrations/slack/README.md +297 -0
  146. data/lib/superkick/integrations/slack/drops.rb +70 -0
  147. data/lib/superkick/integrations/slack/notifier.rb +426 -0
  148. data/lib/superkick/integrations/slack/spawner.rb +251 -0
  149. data/lib/superkick/integrations/slack/templates/default.liquid +17 -0
  150. data/lib/superkick/integrations/slack/templates/slack_reply.liquid +3 -0
  151. data/lib/superkick/integrations/slack/templates/spawn/slack_message.liquid +10 -0
  152. data/lib/superkick/integrations/slack/thread_monitor.rb +161 -0
  153. data/lib/superkick/integrations/slack.rb +12 -0
  154. data/lib/superkick/liquid.rb +129 -0
  155. data/lib/superkick/local/repository_source.rb +148 -0
  156. data/lib/superkick/mcp_server.rb +596 -0
  157. data/lib/superkick/monitor.rb +215 -0
  158. data/lib/superkick/notification_dispatcher.rb +280 -0
  159. data/lib/superkick/notifier.rb +173 -0
  160. data/lib/superkick/notifier_state_store.rb +55 -0
  161. data/lib/superkick/notifier_template.rb +121 -0
  162. data/lib/superkick/notifiers/command.rb +124 -0
  163. data/lib/superkick/notifiers/terminal_bell.rb +41 -0
  164. data/lib/superkick/output_logger.rb +54 -0
  165. data/lib/superkick/poller.rb +126 -0
  166. data/lib/superkick/process_runner.rb +87 -0
  167. data/lib/superkick/pty_proxy.rb +403 -0
  168. data/lib/superkick/registry.rb +75 -0
  169. data/lib/superkick/repository_source.rb +195 -0
  170. data/lib/superkick/server.rb +211 -0
  171. data/lib/superkick/session_recorder.rb +154 -0
  172. data/lib/superkick/setup.rb +160 -0
  173. data/lib/superkick/spawn/agent_spawner.rb +311 -0
  174. data/lib/superkick/spawn/approval_store.rb +113 -0
  175. data/lib/superkick/spawn/handler.rb +144 -0
  176. data/lib/superkick/spawn/injector.rb +119 -0
  177. data/lib/superkick/spawn/workflow_executor.rb +196 -0
  178. data/lib/superkick/spawn/workflow_validator.rb +77 -0
  179. data/lib/superkick/spawner.rb +67 -0
  180. data/lib/superkick/supervisor.rb +516 -0
  181. data/lib/superkick/team/artifact_store.rb +92 -0
  182. data/lib/superkick/team/log.rb +140 -0
  183. data/lib/superkick/team/log_entry_drop.rb +34 -0
  184. data/lib/superkick/team/log_monitor.rb +84 -0
  185. data/lib/superkick/team/log_notifier.rb +96 -0
  186. data/lib/superkick/team/log_store.rb +40 -0
  187. data/lib/superkick/template_filters.rb +24 -0
  188. data/lib/superkick/template_renderer.rb +223 -0
  189. data/lib/superkick/templates/team_log/planning_agent.liquid +38 -0
  190. data/lib/superkick/templates/team_log/team_digest.liquid +45 -0
  191. data/lib/superkick/templates/team_log/teammate_message.liquid +7 -0
  192. data/lib/superkick/templates/team_log/worker_kickoff.liquid +37 -0
  193. data/lib/superkick/templates/workflow/workflow_triggered.liquid +22 -0
  194. data/lib/superkick/version.rb +5 -0
  195. data/lib/superkick/version_control.rb +135 -0
  196. data/lib/superkick/yaml_config.rb +302 -0
  197. data/lib/superkick.rb +198 -0
  198. data/plan.md +267 -0
  199. metadata +404 -0
@@ -0,0 +1,407 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superkick
4
+ class CLI < Thor
5
+ class Team < Thor
6
+ package_name "superkick team"
7
+
8
+ desc "list", "List active teams"
9
+ option :json, type: :boolean, default: false, desc: "Output as JSON"
10
+ def list
11
+ client = Control.client_from
12
+
13
+ unless client.alive?
14
+ $stdout.puts "Superkick server is not running."
15
+ return
16
+ end
17
+
18
+ resp = client.request("list_teams")
19
+ teams = resp[:teams] || []
20
+
21
+ if options[:json]
22
+ $stdout.puts JSON.pretty_generate(teams)
23
+ return
24
+ end
25
+
26
+ if teams.empty?
27
+ $stdout.puts "No active teams."
28
+ return
29
+ end
30
+
31
+ teams.each do |t|
32
+ $stdout.puts "Team: #{t[:team_id]} (#{t[:agent_count]} agents)"
33
+ # Fetch members for this team
34
+ members_resp = client.request("list_agents", team_id: t[:team_id])
35
+ (members_resp[:agents] || []).each do |m|
36
+ team_role = m[:team_role] || "unknown"
37
+ status = m[:goal_status] || "active"
38
+ label = m[:role] ? "#{team_role}/#{m[:role]}" : team_role.to_s
39
+ line = " #{m[:agent_id].ljust(40)} #{label.ljust(20)} [#{status}]"
40
+ $stdout.puts line
41
+ end
42
+ $stdout.puts ""
43
+ end
44
+ rescue Control::Client::ServerUnavailable
45
+ $stdout.puts "Superkick server is not running."
46
+ end
47
+
48
+ desc "status TEAM_ID", "Show team log summary"
49
+ option :full, type: :boolean, default: false, desc: "Show full team log"
50
+ option :json, type: :boolean, default: false, desc: "Output as JSON"
51
+ def status(team_id)
52
+ client = Control.client_from
53
+ result = client.request("team_status", team_id:, full_log: options[:full])
54
+
55
+ if options[:json]
56
+ $stdout.puts JSON.pretty_generate(result.payload)
57
+ return
58
+ end
59
+
60
+ payload = result.payload
61
+ $stdout.puts "Team: #{payload[:team_id]} (#{payload[:teammates]&.size || 0} agents)"
62
+ $stdout.puts ""
63
+
64
+ (payload[:teammates] || []).each do |t|
65
+ team_role = t[:team_role] || "unknown"
66
+ status = t[:goal_status] || "active"
67
+ label = t[:role] ? "#{team_role}/#{t[:role]}" : team_role.to_s
68
+ $stdout.puts " #{t[:agent_id]} (#{label}) [#{status}]"
69
+ end
70
+
71
+ latest = payload[:latest] || []
72
+ if latest.any?
73
+ $stdout.puts ""
74
+ $stdout.puts "Latest updates:"
75
+ latest.each do |entry|
76
+ $stdout.puts " [#{entry[:agent_id]}] [#{entry[:category]}] #{entry[:message]}"
77
+ end
78
+ end
79
+
80
+ blockers = payload[:unresolved_blockers] || []
81
+ if blockers.any?
82
+ $stdout.puts ""
83
+ $stdout.puts "Unresolved blockers:"
84
+ blockers.each do |b|
85
+ $stdout.puts " [#{b[:agent_id]}] #{b[:message]}"
86
+ end
87
+ end
88
+
89
+ entries = payload[:entries] || []
90
+ if entries.any?
91
+ $stdout.puts ""
92
+ $stdout.puts "Full log:"
93
+ entries.each do |e|
94
+ $stdout.puts " #{e[:timestamp]} [#{e[:agent_id]}] [#{e[:category]}] #{e[:message]}"
95
+ end
96
+ end
97
+ rescue Control::Client::ServerUnavailable
98
+ warn "Superkick server is not running."
99
+ exit(1)
100
+ end
101
+
102
+ desc "watch TEAM_ID", "Watch team activity in real time"
103
+ option :no_color, type: :boolean, default: false, desc: "Disable colored output"
104
+ def watch(team_id)
105
+ client = Control.client_from
106
+
107
+ unless client.alive?
108
+ warn "Superkick server is not running."
109
+ exit(1)
110
+ end
111
+
112
+ use_color = !options[:no_color] && $stdout.tty? && !ENV["NO_COLOR"]
113
+ since = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%6NZ")
114
+
115
+ $stdout.puts "Watching team #{team_id}... (Ctrl-C to stop)"
116
+ $stdout.puts ""
117
+
118
+ loop do
119
+ result = client.request("team_status", team_id:, full_log: true, since:)
120
+ entries = result.payload[:entries] || []
121
+
122
+ entries.each do |e|
123
+ since = e[:timestamp]
124
+ $stdout.puts format_watch_entry(e, use_color:)
125
+ end
126
+
127
+ sleep 2
128
+ rescue Interrupt
129
+ break
130
+ end
131
+
132
+ $stdout.puts "\nDone."
133
+ rescue Control::Client::ServerUnavailable
134
+ warn "Superkick server is not running."
135
+ exit(1)
136
+ end
137
+
138
+ desc "stop TEAM_ID", "Terminate all agents in a team"
139
+ def stop(team_id)
140
+ client = Control.client_from
141
+
142
+ resp = client.request("list_agents", team_id:)
143
+ agents = resp[:agents] || []
144
+
145
+ if agents.empty?
146
+ warn "No agents found for team: #{team_id}"
147
+ exit(1)
148
+ end
149
+
150
+ terminated = 0
151
+ agents.each do |a|
152
+ next unless a[:spawn_info] # Only terminate spawned agents
153
+ begin
154
+ client.request("terminate_agent", agent_id: a[:agent_id])
155
+ terminated += 1
156
+ rescue => e
157
+ warn "Failed to terminate #{a[:agent_id]}: #{e.message}"
158
+ end
159
+ end
160
+
161
+ $stdout.puts "Terminated #{terminated} agent(s) in team #{team_id}."
162
+ rescue Control::Client::ServerUnavailable
163
+ warn "Superkick server is not running."
164
+ exit(1)
165
+ end
166
+
167
+ desc "artifacts TEAM_ID", "List artifacts for a team"
168
+ option :json, type: :boolean, default: false, desc: "Output as JSON"
169
+ option :author, type: :string, desc: "Filter by author agent ID"
170
+ def artifacts(team_id)
171
+ client = Control.client_from
172
+ result = client.request("list_team_artifacts", team_id:, author: options[:author])
173
+
174
+ if result.error?
175
+ warn "Error: #{result.error_message}"
176
+ exit(1)
177
+ end
178
+
179
+ artifacts = result[:artifacts] || []
180
+
181
+ if options[:json]
182
+ $stdout.puts JSON.pretty_generate(artifacts)
183
+ return
184
+ end
185
+
186
+ if artifacts.empty?
187
+ $stdout.puts "No artifacts for team #{team_id}."
188
+ return
189
+ end
190
+
191
+ artifacts.each do |a|
192
+ $stdout.puts "#{a[:author]}/#{a[:name]}"
193
+ $stdout.puts " Created: #{a[:created_at]}"
194
+ $stdout.puts " Updated: #{a[:updated_at]}"
195
+ $stdout.puts ""
196
+ end
197
+ rescue Control::Client::ServerUnavailable
198
+ warn "Superkick server is not running."
199
+ exit(1)
200
+ end
201
+
202
+ desc "artifact TEAM_ID AUTHOR NAME", "Read a team artifact"
203
+ option :json, type: :boolean, default: false, desc: "Output as JSON"
204
+ def artifact(team_id, author, name)
205
+ client = Control.client_from
206
+ result = client.request("read_team_artifact", team_id:, author:, name:)
207
+
208
+ if result.error?
209
+ warn "Error: #{result.error_message}"
210
+ exit(1)
211
+ end
212
+
213
+ if options[:json]
214
+ $stdout.puts JSON.pretty_generate(result.payload)
215
+ return
216
+ end
217
+
218
+ $stdout.puts "#{result[:author]}/#{result[:name]}"
219
+ $stdout.puts "Created: #{result[:created_at]}"
220
+ $stdout.puts "Updated: #{result[:updated_at]}"
221
+ $stdout.puts ""
222
+ $stdout.puts result[:content]
223
+ rescue Control::Client::ServerUnavailable
224
+ warn "Superkick server is not running."
225
+ exit(1)
226
+ end
227
+
228
+ desc "message TEAM_ID MESSAGE", "Send a message to all agents on a team"
229
+ long_desc <<~DESC
230
+ Broadcast a high-priority message to every agent on a team. The message
231
+ is injected into each agent's prompt and recorded in the team log.
232
+
233
+ Useful for operator directives like "shift priority to the API migration"
234
+ or "stop working on feature X".
235
+ DESC
236
+ def message(team_id, *message_parts)
237
+ message = message_parts.join(" ")
238
+ if message.empty?
239
+ warn "Usage: superkick team message TEAM_ID MESSAGE"
240
+ exit(1)
241
+ end
242
+
243
+ client = Control.client_from
244
+ result = client.request("post_team_message", team_id:, message:)
245
+
246
+ if result.success?
247
+ $stdout.puts "Message delivered to #{result[:delivered_to]} agent(s)."
248
+ else
249
+ warn "Error: #{result.error_message}"
250
+ exit(1)
251
+ end
252
+ rescue Control::Client::ServerUnavailable
253
+ warn "Superkick server is not running."
254
+ exit(1)
255
+ end
256
+
257
+ desc "spawn-worker TEAM_ID", "Spawn a worker agent into an existing team"
258
+ long_desc <<~DESC
259
+ Spawn a new worker agent into an existing team. The worker gets its own
260
+ working copy of the specified repository and receives the task as its
261
+ kickoff prompt. The agent ID is auto-generated from the team ID and role.
262
+
263
+ The --task text is injected as the agent's initial prompt, so write it as
264
+ instructions you'd give to a developer. The --role label appears in team
265
+ status and helps teammates understand each other's responsibilities.
266
+
267
+ Workers use agent_signal as the default goal type, meaning they signal
268
+ completion by calling the superkick_signal_goal MCP tool. Use --goal-type
269
+ to override (e.g. "command" for a shell-based goal check).
270
+
271
+ The --depends-on flag is informational only — it records which agents this
272
+ worker depends on but does not enforce ordering.
273
+
274
+ Examples:
275
+
276
+ superkick team spawn-worker my-team -r api \\
277
+ --task "Add rate limiting to the /users endpoint" \\
278
+ --role "Rate limiter"
279
+
280
+ superkick team spawn-worker my-team -r web \\
281
+ --task "Write integration tests for checkout" \\
282
+ --goal-type command --depends-on agent-a agent-b
283
+ DESC
284
+ option :repository, type: :string, aliases: "-r", required: true,
285
+ desc: "Repository name from the registry"
286
+ option :task, type: :string, required: true,
287
+ desc: "Detailed instructions for the worker (kickoff prompt)"
288
+ option :role, type: :string,
289
+ desc: "Human-readable role label (e.g. 'Rate limiter')"
290
+ option :goal_type, type: :string,
291
+ desc: "Goal type for the worker (default: agent_signal)"
292
+ option :depends_on, type: :array,
293
+ desc: "Agent IDs this worker depends on (informational)"
294
+ option :monitors, type: :string,
295
+ desc: "JSON/YAML config string or @filepath for monitors to pre-attach"
296
+ option :notifiers, type: :string,
297
+ desc: "JSON/YAML config string or @filepath for notifiers to pre-attach"
298
+ def spawn_worker(team_id)
299
+ client = Control.client_from
300
+
301
+ params = {
302
+ team_id:,
303
+ repository: options[:repository],
304
+ task: options[:task]
305
+ }
306
+ params[:role] = options[:role] if options[:role]
307
+ params[:goal] = {type: options[:goal_type]} if options[:goal_type]
308
+ params[:depends_on] = options[:depends_on] if options[:depends_on]
309
+ params[:monitors] = parse_config(options[:monitors]) if options[:monitors]
310
+ params[:notifiers] = parse_config(options[:notifiers]) if options[:notifiers]
311
+
312
+ result = client.request("spawn_worker_by_team", **params)
313
+
314
+ if result.success?
315
+ $stdout.puts "Worker spawned: #{result[:agent_id]}"
316
+ else
317
+ warn "Error: #{result.error_message}"
318
+ exit(1)
319
+ end
320
+ rescue Control::Client::ServerUnavailable
321
+ warn "Superkick server is not running."
322
+ exit(1)
323
+ end
324
+
325
+ default_command :list
326
+
327
+ private
328
+
329
+ def parse_config(value)
330
+ return {} unless value
331
+
332
+ raw = if value.start_with?("@")
333
+ path = File.expand_path(value[1..])
334
+ unless File.exist?(path)
335
+ warn "Config file not found: #{path}"
336
+ exit(1)
337
+ end
338
+ File.read(path)
339
+ else
340
+ value
341
+ end
342
+
343
+ parsed = YAML.safe_load(raw, symbolize_names: true)
344
+ unless parsed.is_a?(Hash)
345
+ warn "Config must be a YAML/JSON object, got: #{parsed.class}"
346
+ exit(1)
347
+ end
348
+ parsed
349
+ rescue Psych::SyntaxError => e
350
+ warn "Invalid YAML/JSON config: #{e.message}"
351
+ exit(1)
352
+ end
353
+
354
+ def format_watch_entry(entry, use_color:)
355
+ ts = entry[:timestamp]&.then { |t| t[11, 8] } || "??:??:??"
356
+ agent = entry[:agent_id] || "unknown"
357
+ category = entry[:category]&.to_sym
358
+ kind = entry[:kind]
359
+ message = entry[:message] || ""
360
+ role_label = entry[:role] ? " (#{entry[:role]})" : ""
361
+
362
+ line = case category
363
+ when :update
364
+ kind_str = kind ? "[#{kind}]" : ""
365
+ "[#{ts}] #{agent}#{role_label} #{kind_str} #{message}"
366
+ when :message
367
+ target = entry[:target_agent_id] || "?"
368
+ "[#{ts}] #{agent}#{role_label} -> #{target}: #{message}"
369
+ when :lifecycle
370
+ "[#{ts}] * #{agent} #{kind}#{": #{message}" unless message.empty?}"
371
+ when :artifact
372
+ artifact_name = entry[:artifact_name] || "?"
373
+ "[#{ts}] #{agent} published \"#{artifact_name}\""
374
+ else
375
+ "[#{ts}] [#{category}] #{agent}: #{message}"
376
+ end
377
+
378
+ return line unless use_color
379
+
380
+ colorize_entry(line, category, kind)
381
+ end
382
+
383
+ def colorize_entry(line, category, kind)
384
+ case category
385
+ when :update
386
+ case kind&.to_sym
387
+ when :blocker then "\e[31m#{line}\e[0m"
388
+ when :completed then "\e[32m#{line}\e[0m"
389
+ when :decision then "\e[36m#{line}\e[0m"
390
+ when :progress then "\e[33m#{line}\e[0m"
391
+ else line
392
+ end
393
+ when :lifecycle
394
+ case kind&.to_sym
395
+ when :completed, :joined then "\e[32m#{line}\e[0m"
396
+ when :failed, :timed_out then "\e[31m#{line}\e[0m"
397
+ when :blocked then "\e[33m#{line}\e[0m"
398
+ else "\e[2m#{line}\e[0m"
399
+ end
400
+ when :message then "\e[36m#{line}\e[0m"
401
+ when :artifact then "\e[35m#{line}\e[0m"
402
+ else line
403
+ end
404
+ end
405
+ end
406
+ end
407
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require_relative "cli/server"
5
+ require_relative "cli/agent"
6
+ require_relative "cli/monitor"
7
+ require_relative "cli/spawner"
8
+ require_relative "cli/mcp"
9
+ require_relative "cli/team"
10
+ require_relative "cli/goal"
11
+ require_relative "cli/notifier"
12
+ require_relative "cli/repository"
13
+ require_relative "cli/completion"
14
+ require_relative "cli/setup"
15
+
16
+ module Superkick
17
+ class CLI < Thor
18
+ package_name "superkick"
19
+
20
+ class_option :dir, type: :string, aliases: "-C",
21
+ desc: "Superkick base directory (default: ~/.superkick)"
22
+
23
+ def initialize(*args)
24
+ super
25
+ Superkick.configure { |c| c.base_dir = File.expand_path(options[:dir]) } if options[:dir]
26
+ end
27
+
28
+ register Server, "server", "server SUBCOMMAND", "Manage the Superkick server"
29
+ register Agent, "agent", "agent SUBCOMMAND", "Manage Superkick agents"
30
+ register MonitorCmd, "monitor", "monitor SUBCOMMAND", "Manage monitors"
31
+ register Spawner, "spawner", "spawner SUBCOMMAND", "Manage spawner monitors"
32
+ register Mcp, "mcp", "mcp SUBCOMMAND", "Manage MCP server"
33
+ register Team, "team", "team SUBCOMMAND", "Manage agent teams"
34
+ register GoalCmd, "goal", "goal SUBCOMMAND", "Manage goal types"
35
+ register NotifierCmd, "notifier", "notifier SUBCOMMAND", "Manage notifier types"
36
+ register RepositoryCmd, "repository", "repository SUBCOMMAND", "Manage repositories"
37
+ register Completion, "completion", "completion SUBCOMMAND", "Shell completion scripts and helpers"
38
+ register Setup, "setup", "setup", "Interactive first-time setup"
39
+
40
+ # ── Approval gates ──────────────────────────────────────────────────
41
+
42
+ desc "approve ID", "Approve a pending spawn"
43
+ def approve(approval_id)
44
+ client = Control.client_from
45
+ reply = client.request("approve", approval_id:)
46
+
47
+ if reply.success?
48
+ $stdout.puts "Approved. Agent #{reply[:agent_id] || approval_id} spawning."
49
+ else
50
+ warn "Error: #{reply.error_message}"
51
+ exit(1)
52
+ end
53
+ rescue Control::Client::ServerUnavailable
54
+ warn "Superkick server is not running."
55
+ exit(1)
56
+ end
57
+
58
+ desc "reject ID", "Reject a pending spawn"
59
+ option :reason, type: :string, aliases: "-r",
60
+ desc: "Reason for rejection"
61
+ option :clear, type: :boolean, default: false,
62
+ desc: "Clear a specific rejection to allow re-dispatch"
63
+ option :clear_all, type: :boolean, default: false,
64
+ desc: "Clear all rejections"
65
+ def reject(approval_id = nil)
66
+ client = Control.client_from
67
+
68
+ if options[:clear_all]
69
+ reply = client.request("clear_all_rejections")
70
+ if reply.success?
71
+ $stdout.puts "Cleared all rejections."
72
+ else
73
+ warn "Error: #{reply.error_message}"
74
+ exit(1)
75
+ end
76
+ return
77
+ end
78
+
79
+ unless approval_id
80
+ warn "Usage: superkick reject ID [--reason REASON]"
81
+ exit(1)
82
+ end
83
+
84
+ if options[:clear]
85
+ reply = client.request("clear_rejection", approval_id:)
86
+ if reply.success?
87
+ $stdout.puts "Cleared rejection for #{approval_id}."
88
+ else
89
+ warn "Error: #{reply.error_message}"
90
+ exit(1)
91
+ end
92
+ return
93
+ end
94
+
95
+ reply = client.request("reject", approval_id:, reason: options[:reason])
96
+
97
+ if reply.success?
98
+ $stdout.puts "Rejected approval #{approval_id}."
99
+ else
100
+ warn "Error: #{reply.error_message}"
101
+ exit(1)
102
+ end
103
+ rescue Control::Client::ServerUnavailable
104
+ warn "Superkick server is not running."
105
+ exit(1)
106
+ end
107
+
108
+ desc "approvals", "List pending spawn approvals and rejections"
109
+ option :json, type: :boolean, default: false, desc: "Output as JSON"
110
+ option :rejections, type: :boolean, default: false, desc: "Show rejections instead"
111
+ def approvals
112
+ client = Control.client_from
113
+
114
+ unless client.alive?
115
+ $stdout.puts "Superkick server is not running."
116
+ return
117
+ end
118
+
119
+ if options[:rejections]
120
+ show_rejections(client)
121
+ else
122
+ show_approvals(client)
123
+ end
124
+ rescue Control::Client::ServerUnavailable
125
+ $stdout.puts "Superkick server is not running."
126
+ end
127
+
128
+ private
129
+
130
+ def show_approvals(client)
131
+ result = client.request("list_approvals")
132
+ approvals = result[:approvals] || []
133
+
134
+ if options[:json]
135
+ $stdout.puts JSON.pretty_generate(approvals)
136
+ return
137
+ end
138
+
139
+ if approvals.empty?
140
+ $stdout.puts "No pending approvals."
141
+ return
142
+ end
143
+
144
+ approvals.each do |a|
145
+ $stdout.puts a[:id].to_s
146
+ $stdout.puts " Spawner: #{a[:spawner_name]}"
147
+ $stdout.puts " Event: #{a[:event_type]}"
148
+ $stdout.puts " Created at: #{a[:created_at]}"
149
+ $stdout.puts ""
150
+ end
151
+ end
152
+
153
+ def show_rejections(client)
154
+ result = client.request("list_rejections")
155
+ rejections = result[:rejections] || []
156
+
157
+ if options[:json]
158
+ $stdout.puts JSON.pretty_generate(rejections)
159
+ return
160
+ end
161
+
162
+ if rejections.empty?
163
+ $stdout.puts "No rejections."
164
+ return
165
+ end
166
+
167
+ rejections.each do |r|
168
+ $stdout.puts r[:id].to_s
169
+ $stdout.puts " Rejected at: #{r[:rejected_at]}"
170
+ $stdout.puts " Reason: #{r[:reason]}" if r[:reason]
171
+ $stdout.puts ""
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superkick
4
+ # Shared registry for client factory methods.
5
+ #
6
+ # Each namespace module (Control, Buffer, Attach) extends this to get
7
+ # `register_client` and `client_from`. The base Client class in each
8
+ # namespace defines a `from` class method that knows how to build
9
+ # itself from config.
10
+ #
11
+ # Example:
12
+ # Superkick::Control.register_client(:hosted, Superkick::Hosted::Control::Client)
13
+ # client = Superkick::Control.client_from(config:)
14
+ module ClientRegistry
15
+ def register_client(type, klass)
16
+ @clients[type] = klass
17
+ end
18
+
19
+ def client_from(config: Superkick.config, **kwargs)
20
+ type = config.server_type
21
+ if type == :local
22
+ self::Client.from(config:, **kwargs)
23
+ elsif (klass = @clients[type])
24
+ klass.from(config:, **kwargs)
25
+ else
26
+ raise ArgumentError, "Unknown server type: #{type}"
27
+ end
28
+ end
29
+ end
30
+ end