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,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Superkick
|
|
7
|
+
module Drivers
|
|
8
|
+
class Copilot < Driver
|
|
9
|
+
DEFAULT_CONFIG_DIR = File.join(Dir.home, ".copilot").freeze
|
|
10
|
+
|
|
11
|
+
def self.driver_name = :copilot
|
|
12
|
+
|
|
13
|
+
def initialize(cli_command: "copilot", config_dir: DEFAULT_CONFIG_DIR, **)
|
|
14
|
+
@cli_command = cli_command
|
|
15
|
+
@config_dir = config_dir
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :cli_command
|
|
19
|
+
|
|
20
|
+
def driver_name = :copilot
|
|
21
|
+
|
|
22
|
+
def prompt_patterns
|
|
23
|
+
[/^\s*@\s*$/]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def apply_config_dir(config_dir, env:, args:)
|
|
27
|
+
env["COPILOT_HOME"] = config_dir
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def install_mcp(exe_path:)
|
|
31
|
+
config_path = File.join(@config_dir, "mcp-config.json")
|
|
32
|
+
config = load_config(config_path)
|
|
33
|
+
return print_manual_instructions(exe_path) unless config
|
|
34
|
+
|
|
35
|
+
servers = config["servers"] || []
|
|
36
|
+
existing = servers.find { |s| s["name"] == "superkick" }
|
|
37
|
+
|
|
38
|
+
if existing && existing["command"] == exe_path
|
|
39
|
+
$stdout.puts "Superkick already configured in #{config_path}."
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if existing
|
|
44
|
+
existing["command"] = exe_path
|
|
45
|
+
else
|
|
46
|
+
servers << {"name" => "superkick", "type" => "local",
|
|
47
|
+
"command" => exe_path, "args" => ["mcp"]}
|
|
48
|
+
config["servers"] = servers
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
write_config(config_path, config)
|
|
52
|
+
verb = existing ? "Updated" : "Installed"
|
|
53
|
+
$stdout.puts "#{verb} superkick MCP server in #{config_path}."
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def load_config(path)
|
|
59
|
+
return {} unless File.exist?(path)
|
|
60
|
+
|
|
61
|
+
JSON.parse(File.read(path))
|
|
62
|
+
rescue JSON::ParserError => e
|
|
63
|
+
$stdout.puts "Parse error in #{path}: #{e.message}"
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def write_config(path, config)
|
|
68
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
69
|
+
File.write(path, JSON.pretty_generate(config))
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
Superkick::Driver.register(Superkick::Drivers::Copilot)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Superkick
|
|
7
|
+
module Drivers
|
|
8
|
+
class Gemini < Driver
|
|
9
|
+
DEFAULT_CONFIG_DIR = File.join(Dir.home, ".gemini").freeze
|
|
10
|
+
|
|
11
|
+
def self.driver_name = :gemini
|
|
12
|
+
|
|
13
|
+
def initialize(cli_command: "gemini", config_dir: DEFAULT_CONFIG_DIR, **)
|
|
14
|
+
@cli_command = cli_command
|
|
15
|
+
@config_dir = config_dir
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :cli_command
|
|
19
|
+
|
|
20
|
+
def driver_name = :gemini
|
|
21
|
+
|
|
22
|
+
def prompt_patterns
|
|
23
|
+
[/gemini>\s*$/, /^\s*>\s*$/]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def cost_patterns
|
|
27
|
+
[
|
|
28
|
+
CostPattern.new(
|
|
29
|
+
pattern: /Estimated cost:\s*\$([0-9]+\.?[0-9]*)/,
|
|
30
|
+
extractor: ->(m) { {cost_usd: m[1].to_f} }
|
|
31
|
+
),
|
|
32
|
+
CostPattern.new(
|
|
33
|
+
pattern: /Tokens used:\s*([\d,]+)\s*input,\s*([\d,]+)\s*output/,
|
|
34
|
+
extractor: ->(m) {
|
|
35
|
+
{tokens_in: m[1].delete(",").to_i,
|
|
36
|
+
tokens_out: m[2].delete(",").to_i}
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def cost_command = "/stats"
|
|
43
|
+
|
|
44
|
+
def apply_config_dir(config_dir, env:, args:)
|
|
45
|
+
env["GEMINI_CLI_HOME"] = config_dir
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def install_mcp(exe_path:)
|
|
49
|
+
config_path = File.join(@config_dir, "settings.json")
|
|
50
|
+
config = load_config(config_path)
|
|
51
|
+
return print_manual_instructions(exe_path) unless config
|
|
52
|
+
|
|
53
|
+
existing = config.dig("mcpServers", "superkick", "command")
|
|
54
|
+
|
|
55
|
+
if existing == exe_path
|
|
56
|
+
$stdout.puts "Superkick already configured in #{config_path}."
|
|
57
|
+
return
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
config["mcpServers"] ||= {}
|
|
61
|
+
config["mcpServers"]["superkick"] = {"command" => exe_path, "args" => ["mcp"]}
|
|
62
|
+
write_config(config_path, config)
|
|
63
|
+
verb = existing ? "Updated" : "Installed"
|
|
64
|
+
$stdout.puts "#{verb} superkick MCP server in #{config_path}."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def load_config(path)
|
|
70
|
+
return {} unless File.exist?(path)
|
|
71
|
+
|
|
72
|
+
JSON.parse(File.read(path))
|
|
73
|
+
rescue JSON::ParserError => e
|
|
74
|
+
$stdout.puts "Parse error in #{path}: #{e.message}"
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def write_config(path, config)
|
|
79
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
80
|
+
File.write(path, JSON.pretty_generate(config))
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
Superkick::Driver.register(Superkick::Drivers::Gemini)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Superkick
|
|
7
|
+
module Drivers
|
|
8
|
+
class Goose < Driver
|
|
9
|
+
DEFAULT_CONFIG_DIR = File.join(Dir.home, ".config", "goose").freeze
|
|
10
|
+
|
|
11
|
+
def self.driver_name = :goose
|
|
12
|
+
|
|
13
|
+
def initialize(cli_command: "goose", config_dir: DEFAULT_CONFIG_DIR, **)
|
|
14
|
+
@cli_command = cli_command
|
|
15
|
+
@config_dir = config_dir
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :cli_command
|
|
19
|
+
|
|
20
|
+
def driver_name = :goose
|
|
21
|
+
|
|
22
|
+
def prompt_patterns
|
|
23
|
+
[/goose>\s*$/, /^\s*>\s*$/]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def apply_config_dir(config_dir, env:, args:)
|
|
27
|
+
env["GOOSE_CONFIG_DIR"] = config_dir
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def install_mcp(exe_path:)
|
|
31
|
+
config_path = File.join(@config_dir, "config.yaml")
|
|
32
|
+
config = load_config(config_path)
|
|
33
|
+
return print_manual_instructions(exe_path) unless config
|
|
34
|
+
|
|
35
|
+
extensions = config["extensions"] || {}
|
|
36
|
+
existing = extensions.dig("superkick", "command")
|
|
37
|
+
|
|
38
|
+
if existing == exe_path
|
|
39
|
+
$stdout.puts "Superkick already configured in #{config_path} (path matches)."
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
extensions["superkick"] = {
|
|
44
|
+
"command" => exe_path,
|
|
45
|
+
"args" => ["mcp"],
|
|
46
|
+
"type" => "stdio",
|
|
47
|
+
"timeout" => 300
|
|
48
|
+
}
|
|
49
|
+
config["extensions"] = extensions
|
|
50
|
+
write_config(config_path, config)
|
|
51
|
+
verb = existing ? "Updated" : "Installed"
|
|
52
|
+
$stdout.puts "#{verb} superkick MCP server in #{config_path}."
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def load_config(path)
|
|
58
|
+
return {} unless File.exist?(path)
|
|
59
|
+
|
|
60
|
+
YAML.safe_load_file(path) || {}
|
|
61
|
+
rescue Psych::SyntaxError => e
|
|
62
|
+
$stdout.puts "Parse error in #{path}: #{e.message}"
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def write_config(path, config)
|
|
67
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
68
|
+
File.write(path, YAML.dump(config))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
Superkick::Driver.register(Superkick::Drivers::Goose)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Namespace for driver subclasses. Each driver lives in its own file under
|
|
4
|
+
# lib/superkick/drivers/<name>.rb and subclasses Superkick::Driver.
|
|
5
|
+
module Superkick
|
|
6
|
+
module Drivers
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Load the base class first, then all built-in drivers.
|
|
11
|
+
require_relative "driver"
|
|
12
|
+
require_relative "drivers/claude_code"
|
|
13
|
+
require_relative "drivers/copilot"
|
|
14
|
+
require_relative "drivers/codex"
|
|
15
|
+
require_relative "drivers/gemini"
|
|
16
|
+
require_relative "drivers/goose"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "liquid"
|
|
4
|
+
|
|
5
|
+
module Superkick
|
|
6
|
+
# Base class for all Superkick Liquid Drops. Provides serialization,
|
|
7
|
+
# deserialization, and the shared `initialize(data)` / `@data` pattern
|
|
8
|
+
# that every Drop follows.
|
|
9
|
+
#
|
|
10
|
+
# Subclasses must implement:
|
|
11
|
+
# - `self.drop_type` → unique String identifier (e.g. "github_issue")
|
|
12
|
+
#
|
|
13
|
+
# Register subclasses with `Superkick::Drop.register(MyDrop)`.
|
|
14
|
+
#
|
|
15
|
+
# Serialization stamps `_drop_type` markers on hashes so Drops can be
|
|
16
|
+
# reconstructed from JSON-persisted data (e.g. spawn_info[:context]).
|
|
17
|
+
class Drop < ::Liquid::Drop
|
|
18
|
+
@registry = {}
|
|
19
|
+
|
|
20
|
+
def initialize(data)
|
|
21
|
+
super()
|
|
22
|
+
@data = data
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.drop_type
|
|
26
|
+
raise NotImplementedError, "#{name} must implement self.drop_type"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
include Superkick::Registry
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.register(klass)
|
|
34
|
+
@registry[klass.drop_type] = klass
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.lookup(type)
|
|
38
|
+
@registry[type.to_s]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.registered
|
|
42
|
+
@registry.dup.freeze
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Recursively serialize Drops into plain hashes with _drop_type markers.
|
|
46
|
+
def self.serialize(value)
|
|
47
|
+
case value
|
|
48
|
+
when Superkick::Drop then value.to_h
|
|
49
|
+
when Array then value.map { serialize(it) }
|
|
50
|
+
when Hash then value.transform_values { serialize(it) }
|
|
51
|
+
else value
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Recursively reconstruct Drops from _drop_type-marked hashes.
|
|
56
|
+
def self.rehydrate(value)
|
|
57
|
+
case value
|
|
58
|
+
when Hash
|
|
59
|
+
if (type = value[:_drop_type])
|
|
60
|
+
klass = lookup(type)
|
|
61
|
+
return value unless klass # unknown type, pass through
|
|
62
|
+
|
|
63
|
+
data = value.except(:_drop_type)
|
|
64
|
+
.transform_values { rehydrate(it) }
|
|
65
|
+
klass.new(data)
|
|
66
|
+
else
|
|
67
|
+
value.transform_values { rehydrate(it) }
|
|
68
|
+
end
|
|
69
|
+
when Array then value.map { rehydrate(it) }
|
|
70
|
+
else value
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Serialize this Drop to a JSON-safe hash with a _drop_type marker.
|
|
75
|
+
def to_h
|
|
76
|
+
@data.transform_values { Superkick::Drop.serialize(it) }
|
|
77
|
+
.merge(_drop_type: self.class.drop_type)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superkick
|
|
4
|
+
# Liquid Drops for core Superkick objects used in notification templates,
|
|
5
|
+
# team digest templates, and workflow templates.
|
|
6
|
+
|
|
7
|
+
# Wraps team information for template access.
|
|
8
|
+
#
|
|
9
|
+
# Properties: id, members
|
|
10
|
+
class TeamDrop < Superkick::Drop
|
|
11
|
+
def self.drop_type = "team"
|
|
12
|
+
|
|
13
|
+
def id = @data[:id]
|
|
14
|
+
|
|
15
|
+
def members = @data[:members]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Wraps spawner information for template access.
|
|
19
|
+
#
|
|
20
|
+
# Properties: name
|
|
21
|
+
class SpawnerDrop < Superkick::Drop
|
|
22
|
+
def self.drop_type = "spawner"
|
|
23
|
+
|
|
24
|
+
def name = @data[:name]&.to_s
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Wraps monitor information for template access.
|
|
28
|
+
#
|
|
29
|
+
# Properties: name, type
|
|
30
|
+
class MonitorDrop < Superkick::Drop
|
|
31
|
+
def self.drop_type = "monitor"
|
|
32
|
+
|
|
33
|
+
def name = @data[:name]&.to_s
|
|
34
|
+
|
|
35
|
+
def type = @data[:type]&.to_s
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Wraps goal state for template access in notifications.
|
|
39
|
+
# Nested under AgentDrop — accessed as {{ agent.goal.status }}.
|
|
40
|
+
#
|
|
41
|
+
# Properties: status, summary
|
|
42
|
+
class GoalDrop < Superkick::Drop
|
|
43
|
+
def self.drop_type = "goal"
|
|
44
|
+
|
|
45
|
+
def status = @data[:status]&.to_s
|
|
46
|
+
|
|
47
|
+
def summary = @data[:summary]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Wraps agent metadata for template access in notifications.
|
|
51
|
+
#
|
|
52
|
+
# Properties: id, role, team_role, spawned_at, cost_usd, claimed, goal
|
|
53
|
+
class AgentDrop < Superkick::Drop
|
|
54
|
+
def self.drop_type = "agent"
|
|
55
|
+
|
|
56
|
+
def id = @data[:id]
|
|
57
|
+
|
|
58
|
+
def role = @data[:role]&.to_s
|
|
59
|
+
|
|
60
|
+
def team_role = @data[:team_role]&.to_s
|
|
61
|
+
|
|
62
|
+
def spawned_at = @data[:spawned_at]
|
|
63
|
+
|
|
64
|
+
def cost_usd = @data[:cost_usd]
|
|
65
|
+
|
|
66
|
+
def claimed = @data[:claimed]
|
|
67
|
+
|
|
68
|
+
def goal = @data[:goal]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
Drop.register(TeamDrop)
|
|
72
|
+
Drop.register(SpawnerDrop)
|
|
73
|
+
Drop.register(MonitorDrop)
|
|
74
|
+
Drop.register(GoalDrop)
|
|
75
|
+
Drop.register(AgentDrop)
|
|
76
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superkick
|
|
4
|
+
# EnvironmentExecutor — agent-side action executor for server-driven probes.
|
|
5
|
+
#
|
|
6
|
+
# The server sends an array of action hashes describing what environment data
|
|
7
|
+
# it needs (git branch, git remotes, file existence checks). The executor
|
|
8
|
+
# runs each action in the agent's working directory and returns the results.
|
|
9
|
+
#
|
|
10
|
+
# The executor has no knowledge of monitors or probes — it is a simple,
|
|
11
|
+
# self-contained action runner.
|
|
12
|
+
#
|
|
13
|
+
# Supported actions:
|
|
14
|
+
# { action: :git_branch }
|
|
15
|
+
# → String or nil (current branch name)
|
|
16
|
+
#
|
|
17
|
+
# { action: :git_remotes }
|
|
18
|
+
# → Array of { name: String, url: String } or nil
|
|
19
|
+
#
|
|
20
|
+
# { action: :file_exists, paths: [String, ...] }
|
|
21
|
+
# → Hash of { path => Boolean }
|
|
22
|
+
class EnvironmentExecutor
|
|
23
|
+
def initialize(working_dir:)
|
|
24
|
+
@working_dir = working_dir
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Execute a list of action hashes and return the merged results.
|
|
28
|
+
#
|
|
29
|
+
# @param actions [Array<Hash>] each with an :action key
|
|
30
|
+
# @return [Hash] merged results keyed by action name
|
|
31
|
+
def execute(actions)
|
|
32
|
+
actions.each_with_object({}) do |action, result|
|
|
33
|
+
key = action[:action]&.to_sym
|
|
34
|
+
next unless key
|
|
35
|
+
|
|
36
|
+
case key
|
|
37
|
+
when :git_branch
|
|
38
|
+
result[:git_branch] ||= execute_git_branch
|
|
39
|
+
when :git_remotes
|
|
40
|
+
result[:git_remotes] ||= execute_git_remotes
|
|
41
|
+
when :file_exists
|
|
42
|
+
existing = result[:file_exists] || {}
|
|
43
|
+
existing.merge!(execute_file_exists(action[:paths] || []))
|
|
44
|
+
result[:file_exists] = existing
|
|
45
|
+
else
|
|
46
|
+
Superkick.logger.debug("environment_executor") { "Unknown action: #{key}" }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def execute_git_branch
|
|
54
|
+
out = run_git("symbolic-ref", "--short", "HEAD")&.strip
|
|
55
|
+
out unless out&.empty?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def execute_git_remotes
|
|
59
|
+
out = run_git("remote", "-v")
|
|
60
|
+
return nil unless out
|
|
61
|
+
|
|
62
|
+
remotes = {}
|
|
63
|
+
out.each_line do |line|
|
|
64
|
+
parts = line.split
|
|
65
|
+
next unless parts.size >= 2
|
|
66
|
+
|
|
67
|
+
name = parts[0]
|
|
68
|
+
url = parts[1]
|
|
69
|
+
# git remote -v shows each remote twice (fetch + push); deduplicate by name
|
|
70
|
+
remotes[name] ||= {name:, url:}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
remotes.values
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def execute_file_exists(paths)
|
|
77
|
+
paths.each_with_object({}) do |path, result|
|
|
78
|
+
full_path = File.join(@working_dir, path)
|
|
79
|
+
result[path] = File.exist?(full_path)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def run_git(*args)
|
|
84
|
+
IO.popen(["git", "-C", @working_dir, *args], err: File::NULL) { |io| io.read }
|
|
85
|
+
rescue SystemCallError, IOError => e
|
|
86
|
+
Superkick.logger.debug("environment_executor") { "git command failed: #{e.message}" }
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superkick
|
|
4
|
+
# Goal — base class for spawned agent goal checks.
|
|
5
|
+
#
|
|
6
|
+
# A goal defines what "finished" means for a spawned agent. The Supervisor
|
|
7
|
+
# runs a periodic check thread per spawned agent; on each tick it calls
|
|
8
|
+
# `check` and acts on the result.
|
|
9
|
+
#
|
|
10
|
+
# Subclass contract:
|
|
11
|
+
# self.type → unique Symbol (e.g. :command, :agent_exit, :agent_signal)
|
|
12
|
+
# check → one of STATUSES
|
|
13
|
+
# teardown → optional cleanup (default no-op)
|
|
14
|
+
#
|
|
15
|
+
# Status vocabulary:
|
|
16
|
+
# :pending — not started / waiting (non-terminal, default)
|
|
17
|
+
# :in_progress — actively working (non-terminal, stored for observability)
|
|
18
|
+
# :errored — potentially recoverable issue (non-terminal, stored)
|
|
19
|
+
# :completed — succeeded (terminal → terminates agent)
|
|
20
|
+
# :failed — irrecoverable (terminal → terminates agent)
|
|
21
|
+
# :timed_out — max_duration exceeded (terminal, set by Supervisor)
|
|
22
|
+
#
|
|
23
|
+
# Subclasses register themselves:
|
|
24
|
+
# Superkick::Goal.register(MyGoal)
|
|
25
|
+
#
|
|
26
|
+
# Configuration (inside a spawner's goal: block in config.yml):
|
|
27
|
+
#
|
|
28
|
+
# spawners:
|
|
29
|
+
# my-spawner:
|
|
30
|
+
# goal:
|
|
31
|
+
# type: command
|
|
32
|
+
# run: "gh pr list --head $BRANCH --json number -q '.[0].number'"
|
|
33
|
+
class Goal
|
|
34
|
+
STATUSES = %i[pending in_progress errored completed failed timed_out].freeze
|
|
35
|
+
TERMINAL_STATUSES = %i[completed failed timed_out].freeze
|
|
36
|
+
|
|
37
|
+
attr_reader :config, :agent_id
|
|
38
|
+
|
|
39
|
+
def initialize(config:, agent_id:)
|
|
40
|
+
@config = config
|
|
41
|
+
@agent_id = agent_id
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# ── Registry (stores classes, keyed by type) ─────────────────────────
|
|
45
|
+
@registry = {}
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
include Superkick::Registry
|
|
49
|
+
|
|
50
|
+
def register(goal_class)
|
|
51
|
+
key = goal_class.type
|
|
52
|
+
raise ArgumentError, "Goal :#{key} already registered" if @registry.key?(key)
|
|
53
|
+
@registry[key] = goal_class
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def lookup(name)
|
|
57
|
+
@registry[name.to_sym] or raise ArgumentError, "Unknown goal type: #{name.inspect}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def registered
|
|
61
|
+
@registry.dup.freeze
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Build a goal instance from a spawner's goal config hash.
|
|
65
|
+
def build(config, agent_id:)
|
|
66
|
+
type_name = config[:type]
|
|
67
|
+
raise ArgumentError, "Goal config must include :type" unless type_name
|
|
68
|
+
klass = lookup(type_name.to_s.to_sym)
|
|
69
|
+
klass.new(config:, agent_id:)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# ── Instance interface ───────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
def self.type
|
|
76
|
+
raise NotImplementedError, "#{self}.type not implemented"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Human-readable description for discovery. Override in subclasses.
|
|
80
|
+
def self.description = nil
|
|
81
|
+
|
|
82
|
+
# Required config keys. Override in subclasses that need specific config.
|
|
83
|
+
def self.required_config = []
|
|
84
|
+
|
|
85
|
+
# Check whether the goal has been reached.
|
|
86
|
+
# @return [Symbol] one of STATUSES
|
|
87
|
+
def check
|
|
88
|
+
raise NotImplementedError, "#{self.class}#check not implemented"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def teardown
|
|
92
|
+
# Default no-op — goals may override to clean up resources.
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superkick
|
|
4
|
+
module Goals
|
|
5
|
+
# Goal that completes when the AI CLI process exits.
|
|
6
|
+
#
|
|
7
|
+
# This doesn't actively check anything — the Supervisor calls
|
|
8
|
+
# `signal!` when the agent process ends (via the unregister flow).
|
|
9
|
+
# Clean exit (0) → :completed, non-zero → :failed.
|
|
10
|
+
#
|
|
11
|
+
# Configuration:
|
|
12
|
+
# goal:
|
|
13
|
+
# type: agent_exit
|
|
14
|
+
class AgentExit < Goal
|
|
15
|
+
def self.type = :agent_exit
|
|
16
|
+
|
|
17
|
+
def self.description
|
|
18
|
+
"Completes when the AI CLI process exits. Clean exit (0) maps to " \
|
|
19
|
+
"completed, non-zero maps to failed."
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(config:, agent_id:)
|
|
23
|
+
super
|
|
24
|
+
@status = :pending
|
|
25
|
+
@mutex = Mutex.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Called by the Supervisor when the agent process exits.
|
|
29
|
+
# @param status [Symbol] :completed or :failed
|
|
30
|
+
def signal!(status)
|
|
31
|
+
@mutex.synchronize { @status = status.to_sym }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def check
|
|
35
|
+
@mutex.synchronize { @status }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
Superkick::Goal.register(Superkick::Goals::AgentExit)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superkick
|
|
4
|
+
module Goals
|
|
5
|
+
# Goal that completes when the AI CLI calls the `superkick_signal_goal` MCP tool.
|
|
6
|
+
#
|
|
7
|
+
# The signal is received via IPC → stored on the Agent's goal_status →
|
|
8
|
+
# and `check` reads it. This goal is purely reactive; it never polls.
|
|
9
|
+
#
|
|
10
|
+
# This is the default goal type when no goal is specified in spawner config.
|
|
11
|
+
#
|
|
12
|
+
# Configuration:
|
|
13
|
+
# goal:
|
|
14
|
+
# type: agent_signal
|
|
15
|
+
class AgentSignal < Goal
|
|
16
|
+
def self.type = :agent_signal
|
|
17
|
+
|
|
18
|
+
def self.description
|
|
19
|
+
"Completes when the agent calls the superkick_signal_goal MCP tool. " \
|
|
20
|
+
"Use this when the agent should decide when it is done. " \
|
|
21
|
+
"This is the default goal type."
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(config:, agent_id:)
|
|
25
|
+
super
|
|
26
|
+
@status = :pending
|
|
27
|
+
@mutex = Mutex.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Called by the IPC server when a superkick_signal_goal arrives.
|
|
31
|
+
def signal!(status)
|
|
32
|
+
@mutex.synchronize { @status = status.to_sym }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def check
|
|
36
|
+
@mutex.synchronize { @status }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
Superkick::Goal.register(Superkick::Goals::AgentSignal)
|