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.
- checksums.yaml +7 -0
- data/CLA.md +91 -0
- data/CLAUDE.md +2226 -0
- data/CONTRIBUTING.md +104 -0
- data/LICENSE +108 -0
- data/LICENSE-COMMERCIAL.md +39 -0
- data/PLAN.md +161 -0
- data/README.md +1155 -0
- data/exe/superkick +6 -0
- data/lib/superkick/agent/runtime.rb +82 -0
- data/lib/superkick/agent/runtimes/local.rb +74 -0
- data/lib/superkick/agent/runtimes.rb +4 -0
- data/lib/superkick/agent.rb +209 -0
- data/lib/superkick/agent_store.rb +85 -0
- data/lib/superkick/attach/client.rb +245 -0
- data/lib/superkick/attach/protocol.rb +71 -0
- data/lib/superkick/attach/server.rb +371 -0
- data/lib/superkick/budget_checker.rb +120 -0
- data/lib/superkick/buffer/client.rb +91 -0
- data/lib/superkick/buffer/server.rb +127 -0
- data/lib/superkick/cli/agent.rb +524 -0
- data/lib/superkick/cli/completion.rb +591 -0
- data/lib/superkick/cli/goal.rb +71 -0
- data/lib/superkick/cli/mcp.rb +34 -0
- data/lib/superkick/cli/monitor.rb +47 -0
- data/lib/superkick/cli/notifier.rb +39 -0
- data/lib/superkick/cli/repository.rb +46 -0
- data/lib/superkick/cli/server.rb +106 -0
- data/lib/superkick/cli/setup.rb +166 -0
- data/lib/superkick/cli/spawner.rb +85 -0
- data/lib/superkick/cli/team.rb +407 -0
- data/lib/superkick/cli.rb +175 -0
- data/lib/superkick/client_registry.rb +30 -0
- data/lib/superkick/configuration.rb +178 -0
- data/lib/superkick/connection.rb +56 -0
- data/lib/superkick/control/client.rb +78 -0
- data/lib/superkick/control/reply.rb +43 -0
- data/lib/superkick/control/server.rb +1271 -0
- data/lib/superkick/cost_accumulator.rb +53 -0
- data/lib/superkick/cost_extractor.rb +65 -0
- data/lib/superkick/cost_poller.rb +70 -0
- data/lib/superkick/driver/profile_source.rb +134 -0
- data/lib/superkick/driver.rb +179 -0
- data/lib/superkick/drivers/claude_code.rb +110 -0
- data/lib/superkick/drivers/codex.rb +57 -0
- data/lib/superkick/drivers/copilot.rb +75 -0
- data/lib/superkick/drivers/gemini.rb +86 -0
- data/lib/superkick/drivers/goose.rb +74 -0
- data/lib/superkick/drivers.rb +16 -0
- data/lib/superkick/drop.rb +80 -0
- data/lib/superkick/drops.rb +76 -0
- data/lib/superkick/environment_executor.rb +90 -0
- data/lib/superkick/goal.rb +95 -0
- data/lib/superkick/goals/agent_exit.rb +41 -0
- data/lib/superkick/goals/agent_signal.rb +42 -0
- data/lib/superkick/goals/command.rb +103 -0
- data/lib/superkick/history_buffer.rb +38 -0
- data/lib/superkick/hosted/attach/bridge.rb +52 -0
- data/lib/superkick/hosted/attach/client.rb +208 -0
- data/lib/superkick/hosted/attach/relay.rb +313 -0
- data/lib/superkick/hosted/attach/relay_store.rb +48 -0
- data/lib/superkick/hosted/bridge.rb +263 -0
- data/lib/superkick/hosted/buffer/bridge.rb +42 -0
- data/lib/superkick/hosted/buffer/client.rb +63 -0
- data/lib/superkick/hosted/buffer/relay.rb +126 -0
- data/lib/superkick/hosted/buffer/relay_store.rb +42 -0
- data/lib/superkick/hosted/control/client.rb +84 -0
- data/lib/superkick/hosted/mcp_proxy.rb +144 -0
- data/lib/superkick/inject_handler.rb +24 -0
- data/lib/superkick/injection_guard.rb +26 -0
- data/lib/superkick/injection_queue.rb +177 -0
- data/lib/superkick/injector.rb +65 -0
- data/lib/superkick/input_buffer.rb +171 -0
- data/lib/superkick/integrations/bugsnag/README.md +98 -0
- data/lib/superkick/integrations/bugsnag/spawner.rb +307 -0
- data/lib/superkick/integrations/bugsnag/templates/error_opened.liquid +17 -0
- data/lib/superkick/integrations/bugsnag.rb +7 -0
- data/lib/superkick/integrations/circleci/README.md +75 -0
- data/lib/superkick/integrations/circleci/monitor.rb +185 -0
- data/lib/superkick/integrations/circleci/probe.rb +36 -0
- data/lib/superkick/integrations/circleci/templates/ci_failure.liquid +8 -0
- data/lib/superkick/integrations/circleci/templates/ci_success.liquid +1 -0
- data/lib/superkick/integrations/circleci.rb +8 -0
- data/lib/superkick/integrations/datadog/README.md +253 -0
- data/lib/superkick/integrations/datadog/alert_goal.rb +94 -0
- data/lib/superkick/integrations/datadog/alert_monitor.rb +163 -0
- data/lib/superkick/integrations/datadog/alert_spawner.rb +201 -0
- data/lib/superkick/integrations/datadog/notification_templates/default.liquid +10 -0
- data/lib/superkick/integrations/datadog/notifier.rb +294 -0
- data/lib/superkick/integrations/datadog/spawner.rb +201 -0
- data/lib/superkick/integrations/datadog/templates/alert_changed.liquid +8 -0
- data/lib/superkick/integrations/datadog/templates/alert_escalated.liquid +8 -0
- data/lib/superkick/integrations/datadog/templates/alert_recovered.liquid +14 -0
- data/lib/superkick/integrations/datadog/templates/alert_triggered.liquid +29 -0
- data/lib/superkick/integrations/datadog/templates/error_opened.liquid +15 -0
- data/lib/superkick/integrations/datadog.rb +14 -0
- data/lib/superkick/integrations/docker/README.md +256 -0
- data/lib/superkick/integrations/docker/client.rb +295 -0
- data/lib/superkick/integrations/docker/runtime.rb +218 -0
- data/lib/superkick/integrations/docker.rb +4 -0
- data/lib/superkick/integrations/git/repository_source.rb +66 -0
- data/lib/superkick/integrations/git/version_control.rb +119 -0
- data/lib/superkick/integrations/git.rb +8 -0
- data/lib/superkick/integrations/github/README.md +300 -0
- data/lib/superkick/integrations/github/check_failed_spawner.rb +199 -0
- data/lib/superkick/integrations/github/drops.rb +114 -0
- data/lib/superkick/integrations/github/goal.rb +135 -0
- data/lib/superkick/integrations/github/issue_goal.rb +104 -0
- data/lib/superkick/integrations/github/issue_spawner.rb +160 -0
- data/lib/superkick/integrations/github/monitor.rb +251 -0
- data/lib/superkick/integrations/github/probe.rb +30 -0
- data/lib/superkick/integrations/github/repository_source.rb +228 -0
- data/lib/superkick/integrations/github/templates/check_failed.liquid +10 -0
- data/lib/superkick/integrations/github/templates/ci_failure.liquid +5 -0
- data/lib/superkick/integrations/github/templates/ci_success.liquid +1 -0
- data/lib/superkick/integrations/github/templates/issue_opened.liquid +20 -0
- data/lib/superkick/integrations/github/templates/pr_comment.liquid +2 -0
- data/lib/superkick/integrations/github/templates/pr_review.liquid +4 -0
- data/lib/superkick/integrations/github.rb +16 -0
- data/lib/superkick/integrations/honeybadger/README.md +97 -0
- data/lib/superkick/integrations/honeybadger/notification_templates/default.liquid +8 -0
- data/lib/superkick/integrations/honeybadger/notifier.rb +250 -0
- data/lib/superkick/integrations/honeybadger/spawner.rb +214 -0
- data/lib/superkick/integrations/honeybadger/templates/error_opened.liquid +17 -0
- data/lib/superkick/integrations/honeybadger.rb +9 -0
- data/lib/superkick/integrations/shell/README.md +83 -0
- data/lib/superkick/integrations/shell/monitor.rb +87 -0
- data/lib/superkick/integrations/shell/templates/shell_alert.liquid +6 -0
- data/lib/superkick/integrations/shell/templates/shell_success.liquid +6 -0
- data/lib/superkick/integrations/shell.rb +7 -0
- data/lib/superkick/integrations/shortcut/README.md +193 -0
- data/lib/superkick/integrations/shortcut/drops.rb +91 -0
- data/lib/superkick/integrations/shortcut/monitor.rb +582 -0
- data/lib/superkick/integrations/shortcut/probe.rb +34 -0
- data/lib/superkick/integrations/shortcut/spawner.rb +264 -0
- data/lib/superkick/integrations/shortcut/templates/related_story_changed.liquid +6 -0
- data/lib/superkick/integrations/shortcut/templates/story_blocker.liquid +8 -0
- data/lib/superkick/integrations/shortcut/templates/story_comment.liquid +5 -0
- data/lib/superkick/integrations/shortcut/templates/story_description_changed.liquid +19 -0
- data/lib/superkick/integrations/shortcut/templates/story_owner_changed.liquid +10 -0
- data/lib/superkick/integrations/shortcut/templates/story_ready.liquid +41 -0
- data/lib/superkick/integrations/shortcut/templates/story_state_changed.liquid +9 -0
- data/lib/superkick/integrations/shortcut/templates/story_unblocked.liquid +5 -0
- data/lib/superkick/integrations/shortcut.rb +11 -0
- data/lib/superkick/integrations/slack/README.md +297 -0
- data/lib/superkick/integrations/slack/drops.rb +70 -0
- data/lib/superkick/integrations/slack/notifier.rb +426 -0
- data/lib/superkick/integrations/slack/spawner.rb +251 -0
- data/lib/superkick/integrations/slack/templates/default.liquid +17 -0
- data/lib/superkick/integrations/slack/templates/slack_reply.liquid +3 -0
- data/lib/superkick/integrations/slack/templates/spawn/slack_message.liquid +10 -0
- data/lib/superkick/integrations/slack/thread_monitor.rb +161 -0
- data/lib/superkick/integrations/slack.rb +12 -0
- data/lib/superkick/liquid.rb +129 -0
- data/lib/superkick/local/repository_source.rb +148 -0
- data/lib/superkick/mcp_server.rb +596 -0
- data/lib/superkick/monitor.rb +215 -0
- data/lib/superkick/notification_dispatcher.rb +280 -0
- data/lib/superkick/notifier.rb +173 -0
- data/lib/superkick/notifier_state_store.rb +55 -0
- data/lib/superkick/notifier_template.rb +121 -0
- data/lib/superkick/notifiers/command.rb +124 -0
- data/lib/superkick/notifiers/terminal_bell.rb +41 -0
- data/lib/superkick/output_logger.rb +54 -0
- data/lib/superkick/poller.rb +126 -0
- data/lib/superkick/process_runner.rb +87 -0
- data/lib/superkick/pty_proxy.rb +403 -0
- data/lib/superkick/registry.rb +75 -0
- data/lib/superkick/repository_source.rb +195 -0
- data/lib/superkick/server.rb +211 -0
- data/lib/superkick/session_recorder.rb +154 -0
- data/lib/superkick/setup.rb +160 -0
- data/lib/superkick/spawn/agent_spawner.rb +311 -0
- data/lib/superkick/spawn/approval_store.rb +113 -0
- data/lib/superkick/spawn/handler.rb +144 -0
- data/lib/superkick/spawn/injector.rb +119 -0
- data/lib/superkick/spawn/workflow_executor.rb +196 -0
- data/lib/superkick/spawn/workflow_validator.rb +77 -0
- data/lib/superkick/spawner.rb +67 -0
- data/lib/superkick/supervisor.rb +516 -0
- data/lib/superkick/team/artifact_store.rb +92 -0
- data/lib/superkick/team/log.rb +140 -0
- data/lib/superkick/team/log_entry_drop.rb +34 -0
- data/lib/superkick/team/log_monitor.rb +84 -0
- data/lib/superkick/team/log_notifier.rb +96 -0
- data/lib/superkick/team/log_store.rb +40 -0
- data/lib/superkick/template_filters.rb +24 -0
- data/lib/superkick/template_renderer.rb +223 -0
- data/lib/superkick/templates/team_log/planning_agent.liquid +38 -0
- data/lib/superkick/templates/team_log/team_digest.liquid +45 -0
- data/lib/superkick/templates/team_log/teammate_message.liquid +7 -0
- data/lib/superkick/templates/team_log/worker_kickoff.liquid +37 -0
- data/lib/superkick/templates/workflow/workflow_triggered.liquid +22 -0
- data/lib/superkick/version.rb +5 -0
- data/lib/superkick/version_control.rb +135 -0
- data/lib/superkick/yaml_config.rb +302 -0
- data/lib/superkick.rb +198 -0
- data/plan.md +267 -0
- 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
|