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,596 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mcp"
4
+
5
+ module Superkick
6
+ # MCP stdio server — spawned by the AI CLI as a subprocess.
7
+ # Translates MCP tool calls into Control::Client requests to the server.
8
+ #
9
+ # Reads SUPERKICK_AGENT_ID from the environment (set by PtyProxy when spawning
10
+ # the AI CLI). All tools that need the agent identity use this automatically
11
+ # rather than requiring the AI to pass it as a parameter.
12
+ #
13
+ # Tools exposed:
14
+ # superkick_discover_monitors — list available monitor types and probe-detected configs
15
+ # superkick_discover_goals — list available goal types for worker agents
16
+ # superkick_add_monitor — add a monitor to this agent
17
+ # superkick_remove_monitor — remove a monitor from this agent
18
+ # superkick_signal_goal — signal agent goal status (completed, failed, errored, in_progress)
19
+ # superkick_status — report server/agent status
20
+ class McpServer
21
+ def self.start
22
+ new.run
23
+ end
24
+
25
+ def initialize(control_client: nil)
26
+ @agent_id = ENV["SUPERKICK_AGENT_ID"]
27
+ raise "SUPERKICK_AGENT_ID must be set. The MCP server should be started via `superkick agent`." unless @agent_id
28
+ @control_client = control_client || Control.client_from
29
+ end
30
+
31
+ def run
32
+ server = build_server
33
+ transport = MCP::Server::Transports::StdioTransport.new(server)
34
+ server.transport = transport
35
+ transport.open
36
+ end
37
+
38
+ # Build the MCP::Server with all tools defined. Exposed for integration
39
+ # testing — callers can use server.handle_json(request) directly without
40
+ # starting the stdio transport.
41
+ def build_server
42
+ server = MCP::Server.new(name: "superkick", version: Superkick::VERSION)
43
+ define_tools(server)
44
+ server
45
+ end
46
+
47
+ private
48
+
49
+ def define_tools(server) # rubocop:disable Metrics/MethodLength
50
+ # Capture helpers as lambdas so tool blocks (which run with self = Tool class)
51
+ # can call back into the McpServer instance via closures.
52
+ request = method(:server_request)
53
+ agent_id = @agent_id
54
+ do_discover_monitors = method(:discover_monitors)
55
+ do_discover_goals = method(:discover_goals)
56
+ do_discover_notifiers = method(:discover_notifiers)
57
+ do_symbolize_keys = method(:symbolize_keys)
58
+ do_symbolize_keys_deep = method(:symbolize_keys_deep)
59
+
60
+ # MCP gem 0.7+ expects tool blocks to return MCP::Tool::Response, not plain strings.
61
+ text_response = ->(text) {
62
+ MCP::Tool::Response.new([{type: "text", text: text}])
63
+ }
64
+
65
+ server.define_tool(
66
+ name: "superkick_discover_monitors",
67
+ description: "List all available Superkick monitor types, their descriptions, required configuration, " \
68
+ "and event types. Optionally runs environment probes to detect monitors that can be auto-configured " \
69
+ "for the current working directory (e.g. detecting the GitHub repo and branch from git remotes). " \
70
+ "Use this to discover what monitors you can start before calling superkick_add_monitor.",
71
+ input_schema: {
72
+ type: "object",
73
+ properties: {
74
+ run_probes: {
75
+ type: "boolean",
76
+ description: "Run environment probes to detect auto-configurable monitors (default: true)"
77
+ }
78
+ }
79
+ }
80
+ ) do |run_probes: true, **_|
81
+ text_response.call(do_discover_monitors.call(run_probes:))
82
+ end
83
+
84
+ server.define_tool(
85
+ name: "superkick_discover_notifiers",
86
+ description: "List all available Superkick notifier types, their descriptions, and configuration options. " \
87
+ "Use this to discover what notifier types you can attach to agents or workers " \
88
+ "before calling superkick_add_notifier or superkick_spawn_worker.",
89
+ input_schema: {type: "object", properties: {}}
90
+ ) do |**_|
91
+ text_response.call(do_discover_notifiers.call)
92
+ end
93
+
94
+ server.define_tool(
95
+ name: "superkick_discover_goals",
96
+ description: "List all available Superkick goal types, their descriptions, and required configuration. " \
97
+ "Use this to understand what goal types you can assign to workers when calling superkick_spawn_worker.",
98
+ input_schema: {type: "object", properties: {}}
99
+ ) do |**_|
100
+ text_response.call(do_discover_goals.call)
101
+ end
102
+
103
+ server.define_tool(
104
+ name: "superkick_discover_repositories",
105
+ description: "List all configured repositories with their descriptions, tags, dependencies, " \
106
+ "and paths. Use this to discover which repositories are available for spawning workers " \
107
+ "or understanding the project landscape.",
108
+ input_schema: {type: "object", properties: {}}
109
+ ) do |**_|
110
+ text = begin
111
+ result = request.call("discover_repositories")
112
+ result.success? ? JSON.pretty_generate(result.payload) : "Error: #{result.error_message}"
113
+ rescue Control::Client::ServerUnavailable => e
114
+ "Superkick server unavailable: #{e.message}"
115
+ end
116
+ text_response.call(text)
117
+ end
118
+
119
+ server.define_tool(
120
+ name: "superkick_add_monitor",
121
+ description: "Add a named monitor to this agent.",
122
+ input_schema: {
123
+ type: "object",
124
+ required: %w[monitor_name],
125
+ properties: {
126
+ monitor_name: {type: "string", description: "Unique instance name (e.g. 'github', 'disk_check')"},
127
+ monitor_type: {type: "string", description: "Monitor class type (e.g. 'shell'). Inferred from name if omitted."},
128
+ config: {type: "object", description: "Monitor-specific configuration"}
129
+ }
130
+ }
131
+ ) do |monitor_name:, monitor_type: nil, config: {}, **_|
132
+ text = begin
133
+ config = config.dup
134
+ config[:type] = monitor_type if monitor_type
135
+ result = request.call("add_monitor",
136
+ agent_id:,
137
+ monitor_name:,
138
+ config:)
139
+ result.success? ? "Monitor #{monitor_name} added." : "Error: #{result.error_message}"
140
+ rescue Control::Client::ServerUnavailable => e
141
+ "Superkick server unavailable: #{e.message}"
142
+ end
143
+ text_response.call(text)
144
+ end
145
+
146
+ server.define_tool(
147
+ name: "superkick_remove_monitor",
148
+ description: "Remove a named monitor from this agent.",
149
+ input_schema: {
150
+ type: "object",
151
+ required: %w[monitor_name],
152
+ properties: {
153
+ monitor_name: {type: "string", description: "The monitor instance name to remove"}
154
+ }
155
+ }
156
+ ) do |monitor_name:, **_|
157
+ text = begin
158
+ result = request.call("remove_monitor",
159
+ agent_id:,
160
+ monitor_name:)
161
+ result.success? ? "Monitor removed." : "Error: #{result.error_message}"
162
+ rescue Control::Client::ServerUnavailable => e
163
+ "Superkick server unavailable: #{e.message}"
164
+ end
165
+ text_response.call(text)
166
+ end
167
+
168
+ server.define_tool(
169
+ name: "superkick_add_notifier",
170
+ description: "Add a named notifier to this agent. Per-agent notifiers fire " \
171
+ "alongside global notifiers for this agent's events only.",
172
+ input_schema: {
173
+ type: "object",
174
+ required: %w[notifier_name],
175
+ properties: {
176
+ notifier_name: {type: "string", description: "Unique instance name (e.g. 'slack_ops', 'datadog')"},
177
+ notifier_type: {type: "string", description: "Notifier class type (e.g. 'slack'). Inferred from name if omitted."},
178
+ config: {type: "object", description: "Notifier-specific configuration"}
179
+ }
180
+ }
181
+ ) do |notifier_name:, notifier_type: nil, config: {}, **_|
182
+ text = begin
183
+ config = do_symbolize_keys.call(config.dup)
184
+ config[:type] = notifier_type if notifier_type
185
+ result = request.call("add_notifier",
186
+ agent_id:,
187
+ notifier_name:,
188
+ config:)
189
+ result.success? ? "Notifier #{notifier_name} added." : "Error: #{result.error_message}"
190
+ rescue Control::Client::ServerUnavailable => e
191
+ "Superkick server unavailable: #{e.message}"
192
+ end
193
+ text_response.call(text)
194
+ end
195
+
196
+ server.define_tool(
197
+ name: "superkick_remove_notifier",
198
+ description: "Remove a named notifier from this agent.",
199
+ input_schema: {
200
+ type: "object",
201
+ required: %w[notifier_name],
202
+ properties: {
203
+ notifier_name: {type: "string", description: "The notifier instance name to remove"}
204
+ }
205
+ }
206
+ ) do |notifier_name:, **_|
207
+ text = begin
208
+ result = request.call("remove_notifier",
209
+ agent_id:,
210
+ notifier_name:)
211
+ result.success? ? "Notifier removed." : "Error: #{result.error_message}"
212
+ rescue Control::Client::ServerUnavailable => e
213
+ "Superkick server unavailable: #{e.message}"
214
+ end
215
+ text_response.call(text)
216
+ end
217
+
218
+ server.define_tool(
219
+ name: "superkick_signal_goal",
220
+ description: "Signal the status of this agent's goal. Use this to report progress, " \
221
+ "completion, or failure of your assigned task. Terminal statuses (completed, failed) " \
222
+ "tell Superkick to clean up resources and optionally trigger downstream workflows.",
223
+ input_schema: {
224
+ type: "object",
225
+ required: %w[status],
226
+ properties: {
227
+ status: {
228
+ type: "string",
229
+ enum: %w[completed failed errored in_progress],
230
+ description: "Goal status: 'completed' = task succeeded (terminal), " \
231
+ "'failed' = irrecoverable failure (terminal), " \
232
+ "'errored' = potentially recoverable issue, " \
233
+ "'in_progress' = actively working"
234
+ },
235
+ message: {type: "string", description: "Optional message describing the outcome"},
236
+ summary: {
237
+ type: "string",
238
+ description: "Brief summary of what happened. For failures, explain what went wrong " \
239
+ "and what was tried. This summary is forwarded to retry agents in workflow chains."
240
+ }
241
+ }
242
+ }
243
+ ) do |status:, summary: nil, **_|
244
+ text = begin
245
+ result = request.call("signal_goal",
246
+ agent_id:,
247
+ status:,
248
+ summary:)
249
+ result.success? ? "Signal received: #{status}" : "Error: #{result.error_message}"
250
+ rescue Control::Client::ServerUnavailable => e
251
+ "Superkick server unavailable: #{e.message}"
252
+ end
253
+ text_response.call(text)
254
+ end
255
+
256
+ server.define_tool(
257
+ name: "superkick_report_cost",
258
+ description: "Report token usage and cost for this agent session. " \
259
+ "Call this periodically or at task completion to enable cost tracking. " \
260
+ "Values should be cumulative totals (not incremental deltas).",
261
+ input_schema: {
262
+ type: "object",
263
+ properties: {
264
+ tokens_in: {type: "integer", description: "Total input tokens used so far"},
265
+ tokens_out: {type: "integer", description: "Total output tokens used so far"},
266
+ cost_usd: {type: "number", description: "Total cost in USD so far"}
267
+ }
268
+ }
269
+ ) do |tokens_in: nil, tokens_out: nil, cost_usd: nil, **_|
270
+ text = begin
271
+ result = request.call("report_cost",
272
+ agent_id:,
273
+ tokens_in:,
274
+ tokens_out:,
275
+ cost_usd:,
276
+ source: :mcp_report)
277
+ result.success? ? "Cost recorded." : "Error: #{result.error_message}"
278
+ rescue Control::Client::ServerUnavailable => e
279
+ "Superkick server unavailable: #{e.message}"
280
+ end
281
+ text_response.call(text)
282
+ end
283
+
284
+ server.define_tool(
285
+ name: "superkick_status",
286
+ description: "Report Superkick server status and active agents.",
287
+ input_schema: {type: "object", properties: {}}
288
+ ) do |**_|
289
+ text = begin
290
+ ping = begin
291
+ request.call("ping")
292
+ rescue Control::Client::ServerUnavailable
293
+ nil
294
+ end
295
+ unless ping&.success?
296
+ next text_response.call("Superkick server is not running.")
297
+ end
298
+
299
+ agents_resp = request.call("list_agents")
300
+ agents = agents_resp[:agents] || []
301
+
302
+ if agents.empty?
303
+ "Superkick server running (v#{ping[:version]}). No active agents."
304
+ else
305
+ lines = agents.map do |s|
306
+ buf = s[:has_buffer] ? "pty-connected" : "no-pty"
307
+ line = " agent #{s[:agent_id]}: #{s[:monitor_count]} monitor(s), #{buf}"
308
+ if s[:cost] && s[:cost][:total_cost_usd] > 0
309
+ line += " cost=$#{s[:cost][:total_cost_usd].round(2)}"
310
+ end
311
+ line
312
+ end
313
+ "Superkick server running (v#{ping[:version]}).\n#{lines.join("\n")}"
314
+ end
315
+ rescue Control::Client::ServerUnavailable => e
316
+ "Superkick server unavailable: #{e.message}"
317
+ end
318
+ text_response.call(text)
319
+ end
320
+
321
+ # ── Team tools ────────────────────────────────────────────────────
322
+
323
+ server.define_tool(
324
+ name: "superkick_post_update",
325
+ description: "Post an update or send a message. Without target_agent_id, " \
326
+ "broadcasts to all configured notification channels (Slack, team log, etc.). " \
327
+ "With target_agent_id, sends a direct message injected into that teammate's prompt. " \
328
+ "Use kind to categorize: 'blocker' gets high priority routing.",
329
+ input_schema: {
330
+ type: "object",
331
+ required: %w[message],
332
+ properties: {
333
+ message: {type: "string", description: "The update or message text"},
334
+ kind: {
335
+ type: "string",
336
+ enum: %w[status decision blocker progress completed failed],
337
+ description: "Type of update (for broadcast mode)"
338
+ },
339
+ target_agent_id: {
340
+ type: "string",
341
+ description: "Send a direct message to this teammate instead of broadcasting. " \
342
+ "Both agents must be on the same team."
343
+ }
344
+ }
345
+ }
346
+ ) do |message:, kind: nil, target_agent_id: nil, **_|
347
+ text = begin
348
+ result = request.call("post_update",
349
+ agent_id:,
350
+ message:,
351
+ kind:,
352
+ target_agent_id:)
353
+ result.success? ? "Update posted." : "Error: #{result.error_message}"
354
+ rescue Control::Client::ServerUnavailable => e
355
+ "Superkick server unavailable: #{e.message}"
356
+ end
357
+ text_response.call(text)
358
+ end
359
+
360
+ server.define_tool(
361
+ name: "superkick_team_status",
362
+ description: "Get the current status of your team. Returns each teammate's " \
363
+ "latest update and any unresolved blockers.",
364
+ input_schema: {
365
+ type: "object",
366
+ properties: {
367
+ full_log: {
368
+ type: "boolean",
369
+ description: "If true, returns the full team log instead of just the summary",
370
+ default: false
371
+ }
372
+ }
373
+ }
374
+ ) do |full_log: false, **_|
375
+ text = begin
376
+ result = request.call("team_status",
377
+ agent_id:,
378
+ full_log:)
379
+ result.success? ? JSON.pretty_generate(result.payload) : "Error: #{result.error_message}"
380
+ rescue Control::Client::ServerUnavailable => e
381
+ "Superkick server unavailable: #{e.message}"
382
+ end
383
+ text_response.call(text)
384
+ end
385
+
386
+ server.define_tool(
387
+ name: "superkick_list_teammates",
388
+ description: "List all agents on your team, including their roles, " \
389
+ "current status, and what they're working on.",
390
+ input_schema: {type: "object", properties: {}}
391
+ ) do |**_|
392
+ text = begin
393
+ result = request.call("list_teammates", agent_id:)
394
+ result.success? ? JSON.pretty_generate(result.payload) : "Error: #{result.error_message}"
395
+ rescue Control::Client::ServerUnavailable => e
396
+ "Superkick server unavailable: #{e.message}"
397
+ end
398
+ text_response.call(text)
399
+ end
400
+
401
+ server.define_tool(
402
+ name: "superkick_spawn_worker",
403
+ description: "Spawn a worker agent to work on a specific repository as " \
404
+ "part of your team. The worker will be set up with a git worktree and " \
405
+ "given your instructions as its kickoff prompt.",
406
+ input_schema: {
407
+ type: "object",
408
+ required: %w[repository task agent_suffix],
409
+ properties: {
410
+ repository: {type: "string", description: "Name of the repository from the repository registry"},
411
+ task: {type: "string", description: "Detailed instructions for what the worker should do"},
412
+ agent_suffix: {
413
+ type: "string",
414
+ description: "Short suffix for the agent ID (e.g. 'api', 'web'). " \
415
+ "The full ID will be {team_id}-{suffix}."
416
+ },
417
+ goal: {
418
+ type: "object",
419
+ description: "Goal configuration for the worker. Defaults to agent_signal.",
420
+ properties: {type: {type: "string"}}
421
+ },
422
+ role: {
423
+ type: "string",
424
+ description: "Short label describing the worker's function (e.g. 'API migration', 'Test writer')"
425
+ },
426
+ depends_on: {
427
+ type: "array",
428
+ items: {type: "string"},
429
+ description: "Agent IDs this worker depends on. The worker's " \
430
+ "instructions will note these dependencies."
431
+ },
432
+ monitors: {
433
+ type: "object",
434
+ description: "Named monitors to attach to the worker. " \
435
+ "Each key is a monitor name, each value is its configuration " \
436
+ "(must include 'type' if the name doesn't match a registered monitor type). " \
437
+ "Use superkick_discover_monitors to see available types.",
438
+ additionalProperties: {type: "object"}
439
+ },
440
+ notifiers: {
441
+ type: "object",
442
+ description: "Named notifiers to attach to the worker. " \
443
+ "Each key is a notifier name, each value is its configuration " \
444
+ "(must include 'type'). These fire alongside global notifiers for " \
445
+ "this worker's events only. Use superkick_discover_notifiers to see available types.",
446
+ additionalProperties: {type: "object"}
447
+ }
448
+ }
449
+ }
450
+ ) do |repository:, task:, agent_suffix:, goal: nil, role: nil, depends_on: nil, monitors: nil, notifiers: nil, **_|
451
+ text = begin
452
+ goal = goal.transform_keys(&:to_sym) if goal
453
+ monitors = do_symbolize_keys_deep.call(monitors) if monitors
454
+ notifiers = do_symbolize_keys_deep.call(notifiers) if notifiers
455
+
456
+ result = request.call("spawn_worker",
457
+ agent_id:,
458
+ repository:,
459
+ task:,
460
+ agent_suffix:,
461
+ role:,
462
+ goal:,
463
+ depends_on:,
464
+ monitors:,
465
+ notifiers:)
466
+ result.success? ? "Worker spawned: #{result[:agent_id]}" : "Error: #{result.error_message}"
467
+ rescue Control::Client::ServerUnavailable => e
468
+ "Superkick server unavailable: #{e.message}"
469
+ end
470
+ text_response.call(text)
471
+ end
472
+
473
+ # ── Artifact tools ──────────────────────────────────────────────
474
+
475
+ server.define_tool(
476
+ name: "superkick_publish_artifact",
477
+ description: "Publish a named artifact to your team's shared store. Use this to share " \
478
+ "implementation plans, summaries, API contracts, or any structured data with teammates. " \
479
+ "Artifacts are visible to the entire team. Publishing to an existing name updates it.",
480
+ input_schema: {
481
+ type: "object",
482
+ required: %w[name content],
483
+ properties: {
484
+ name: {type: "string", description: "Artifact name (e.g. 'implementation-plan', 'api-contract')"},
485
+ content: {type: "string", description: "The artifact content (text, JSON, YAML, etc.)"}
486
+ }
487
+ }
488
+ ) do |name:, content:, **_|
489
+ text = begin
490
+ result = request.call("publish_artifact",
491
+ agent_id:,
492
+ name:,
493
+ content:)
494
+ result.success? ? "Artifact '#{name}' published." : "Error: #{result.error_message}"
495
+ rescue Control::Client::ServerUnavailable => e
496
+ "Superkick server unavailable: #{e.message}"
497
+ end
498
+ text_response.call(text)
499
+ end
500
+
501
+ server.define_tool(
502
+ name: "superkick_read_artifact",
503
+ description: "Read an artifact published by a teammate. Use superkick_list_artifacts " \
504
+ "to discover available artifacts first.",
505
+ input_schema: {
506
+ type: "object",
507
+ required: %w[author name],
508
+ properties: {
509
+ author: {type: "string", description: "The agent ID that published the artifact"},
510
+ name: {type: "string", description: "The artifact name"}
511
+ }
512
+ }
513
+ ) do |author:, name:, **_|
514
+ text = begin
515
+ result = request.call("read_artifact",
516
+ agent_id:,
517
+ author:,
518
+ name:)
519
+ result.success? ? JSON.pretty_generate(result.payload) : "Error: #{result.error_message}"
520
+ rescue Control::Client::ServerUnavailable => e
521
+ "Superkick server unavailable: #{e.message}"
522
+ end
523
+ text_response.call(text)
524
+ end
525
+
526
+ server.define_tool(
527
+ name: "superkick_list_artifacts",
528
+ description: "List artifacts published by your team. Returns metadata (name, author, " \
529
+ "timestamps) without content. Optionally filter by author.",
530
+ input_schema: {
531
+ type: "object",
532
+ properties: {
533
+ author: {type: "string", description: "Filter by author agent ID (optional)"}
534
+ }
535
+ }
536
+ ) do |author: nil, **_|
537
+ text = begin
538
+ result = request.call("list_artifacts",
539
+ agent_id:,
540
+ author:)
541
+ result.success? ? JSON.pretty_generate(result.payload) : "Error: #{result.error_message}"
542
+ rescue Control::Client::ServerUnavailable => e
543
+ "Superkick server unavailable: #{e.message}"
544
+ end
545
+ text_response.call(text)
546
+ end
547
+ end
548
+
549
+ def discover_monitors(run_probes:)
550
+ result = server_request("discover_monitors", agent_id: run_probes ? @agent_id : nil)
551
+ if result.success?
552
+ JSON.pretty_generate(result.payload)
553
+ else
554
+ "Error: #{result.error_message}"
555
+ end
556
+ rescue Control::Client::ServerUnavailable => e
557
+ "Superkick server unavailable: #{e.message}"
558
+ end
559
+
560
+ def discover_goals
561
+ result = server_request("discover_goals")
562
+ if result.success?
563
+ JSON.pretty_generate(result.payload)
564
+ else
565
+ "Error: #{result.error_message}"
566
+ end
567
+ rescue Control::Client::ServerUnavailable => e
568
+ "Superkick server unavailable: #{e.message}"
569
+ end
570
+
571
+ def discover_notifiers
572
+ result = server_request("discover_notifiers")
573
+ if result.success?
574
+ JSON.pretty_generate(result.payload)
575
+ else
576
+ "Error: #{result.error_message}"
577
+ end
578
+ rescue Control::Client::ServerUnavailable => e
579
+ "Superkick server unavailable: #{e.message}"
580
+ end
581
+
582
+ def symbolize_keys(hash)
583
+ hash.transform_keys(&:to_sym)
584
+ end
585
+
586
+ def symbolize_keys_deep(hash)
587
+ hash.each_with_object({}) do |(k, v), result|
588
+ result[k.to_sym] = v.is_a?(Hash) ? symbolize_keys_deep(v) : v
589
+ end
590
+ end
591
+
592
+ def server_request(command, **params)
593
+ @control_client.request(command, **params)
594
+ end
595
+ end
596
+ end