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,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "octokit"
|
|
4
|
+
|
|
5
|
+
module Superkick
|
|
6
|
+
module Integrations
|
|
7
|
+
module GitHub
|
|
8
|
+
# OrganizationRepositorySource — discovers repositories from a GitHub
|
|
9
|
+
# organization or user account using the GitHub API.
|
|
10
|
+
#
|
|
11
|
+
# All metadata (descriptions, topics, clone URLs, READMEs) is fetched via the
|
|
12
|
+
# GitHub API using the provided token — no SSH keys needed.
|
|
13
|
+
#
|
|
14
|
+
# Config:
|
|
15
|
+
# type: github_organization
|
|
16
|
+
# organization: my-org # required — GitHub organization or user name
|
|
17
|
+
# token: ghp_... # optional — falls back to GITHUB_TOKEN env var
|
|
18
|
+
# context_documents: [readme, ...] # optional — documents to fetch (default [])
|
|
19
|
+
# include_archived: false # optional — include archived repositories (default false)
|
|
20
|
+
# include_forks: false # optional — include forked repositories (default false)
|
|
21
|
+
# topic_filter: [backend, api] # optional — only repositories with ALL listed topics
|
|
22
|
+
# exclude: [old-repo, deprecated] # optional — repository names to exclude
|
|
23
|
+
# visibility: public # optional — public, private, or all (default all)
|
|
24
|
+
#
|
|
25
|
+
# Example YAML:
|
|
26
|
+
# repositories:
|
|
27
|
+
# type: github_organization
|
|
28
|
+
# organization: my-company
|
|
29
|
+
# token: <%= env("GITHUB_TOKEN") %>
|
|
30
|
+
# topic_filter:
|
|
31
|
+
# - production
|
|
32
|
+
# exclude:
|
|
33
|
+
# - legacy-app
|
|
34
|
+
class OrganizationRepositorySource < Superkick::RepositorySource
|
|
35
|
+
def self.type = :github_organization
|
|
36
|
+
|
|
37
|
+
def self.setup_label = "GitHub Organization"
|
|
38
|
+
|
|
39
|
+
def self.setup_config
|
|
40
|
+
<<~YAML
|
|
41
|
+
company:
|
|
42
|
+
type: github_organization
|
|
43
|
+
organization: my-company
|
|
44
|
+
token: <%= env("GITHUB_TOKEN") %>
|
|
45
|
+
# include_archived: false # include archived repos (default false)
|
|
46
|
+
# include_forks: false # include forked repos (default false)
|
|
47
|
+
# topic_filter: # only repos with ALL listed topics
|
|
48
|
+
# - production
|
|
49
|
+
# exclude: # repository names to exclude
|
|
50
|
+
# - legacy-app
|
|
51
|
+
# visibility: all # public, private, or all (default)
|
|
52
|
+
YAML
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @param config [Hash] source configuration
|
|
56
|
+
# @param client [Octokit::Client, nil] optional pre-built client (used in tests)
|
|
57
|
+
def initialize(config = {}, client: nil)
|
|
58
|
+
@organization = config[:organization]
|
|
59
|
+
@token = config[:token] || ENV["GITHUB_TOKEN"]
|
|
60
|
+
@include_archived = config.fetch(:include_archived, false)
|
|
61
|
+
@include_forks = config.fetch(:include_forks, false)
|
|
62
|
+
@topic_filter = Array(config[:topic_filter]).map(&:to_s)
|
|
63
|
+
@exclude = Array(config[:exclude]).map(&:to_s).to_set
|
|
64
|
+
@visibility = config.fetch(:visibility, "all").to_s
|
|
65
|
+
@client = client
|
|
66
|
+
@context_document_patterns = merge_context_document_patterns(config[:context_documents])
|
|
67
|
+
|
|
68
|
+
@repositories = fetch_all_repositories.freeze
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
attr_reader :repositories
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def fetch_all_repositories
|
|
76
|
+
return {} unless @organization
|
|
77
|
+
|
|
78
|
+
api_client = @client || build_client
|
|
79
|
+
github_repositories = fetch_from_api(api_client)
|
|
80
|
+
github_repositories = apply_filters(github_repositories)
|
|
81
|
+
|
|
82
|
+
github_repositories.each_with_object({}) do |gh_repository, h|
|
|
83
|
+
name = gh_repository.name.to_sym
|
|
84
|
+
topics = fetch_topics(api_client, gh_repository.full_name)
|
|
85
|
+
|
|
86
|
+
# Apply topic filter — skip repositories that don't have all required topics
|
|
87
|
+
next if @topic_filter.any? && !@topic_filter.all? { topics.include?(it) }
|
|
88
|
+
|
|
89
|
+
h[name] = Repository.new(
|
|
90
|
+
name:,
|
|
91
|
+
url: gh_repository.clone_url,
|
|
92
|
+
version_control: :git
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
fetch_context_documents(api_client, gh_repository.full_name, h[name])
|
|
96
|
+
end
|
|
97
|
+
rescue Octokit::Unauthorized => e
|
|
98
|
+
Superkick.logger.error("github_organization_registry") { "GitHub auth failed: #{e.message}" }
|
|
99
|
+
{}
|
|
100
|
+
rescue Octokit::TooManyRequests => e
|
|
101
|
+
Superkick.logger.error("github_organization_registry") { "GitHub rate limited: #{e.message}" }
|
|
102
|
+
{}
|
|
103
|
+
rescue Faraday::Error => e
|
|
104
|
+
Superkick.logger.error("github_organization_registry") { "GitHub API error: #{e.message}" }
|
|
105
|
+
{}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def fetch_from_api(api_client)
|
|
109
|
+
options = {per_page: 100}
|
|
110
|
+
options[:type] = @visibility unless @visibility == "all"
|
|
111
|
+
|
|
112
|
+
api_client.auto_paginate = true
|
|
113
|
+
|
|
114
|
+
# Try organization repositories first, fall back to user repositories
|
|
115
|
+
begin
|
|
116
|
+
api_client.org_repos(@organization, **options)
|
|
117
|
+
rescue Octokit::NotFound
|
|
118
|
+
api_client.repos(@organization, **options)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def apply_filters(github_repositories)
|
|
123
|
+
github_repositories = github_repositories.reject(&:archived) unless @include_archived
|
|
124
|
+
github_repositories = github_repositories.reject(&:fork) unless @include_forks
|
|
125
|
+
github_repositories.reject { @exclude.include?(it.name) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def fetch_topics(api_client, full_name)
|
|
129
|
+
repository_data = api_client.repo(full_name)
|
|
130
|
+
Array(repository_data.topics).map(&:to_s)
|
|
131
|
+
rescue Octokit::Error
|
|
132
|
+
[]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def fetch_context_documents(api_client, full_name, repository)
|
|
136
|
+
return if @context_document_patterns.empty?
|
|
137
|
+
|
|
138
|
+
@context_document_patterns.each do |name, pattern|
|
|
139
|
+
content = if name == :readme
|
|
140
|
+
# Use the dedicated readme endpoint for the :readme document
|
|
141
|
+
fetch_file_via_readme(api_client, full_name)
|
|
142
|
+
else
|
|
143
|
+
fetch_file_by_pattern(api_client, full_name, pattern)
|
|
144
|
+
end
|
|
145
|
+
repository.set_context_document(name, content) if content
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def fetch_file_via_readme(api_client, full_name)
|
|
150
|
+
readme = api_client.readme(full_name, accept: "application/vnd.github.raw+json")
|
|
151
|
+
readme.is_a?(String) ? readme : readme.content
|
|
152
|
+
rescue Octokit::NotFound
|
|
153
|
+
nil
|
|
154
|
+
rescue Octokit::Error => e
|
|
155
|
+
Superkick.logger.debug("github_organization_registry") { "Could not fetch README for #{full_name}: #{e.message}" }
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def fetch_file_by_pattern(api_client, full_name, pattern)
|
|
160
|
+
# For simple non-glob patterns, fetch directly
|
|
161
|
+
unless pattern.include?("*") || pattern.include?("?")
|
|
162
|
+
return fetch_single_file(api_client, full_name, pattern)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# For glob patterns, list the repo tree and find the first match
|
|
166
|
+
tree = api_client.tree(full_name, "HEAD", recursive: pattern.include?("/"))
|
|
167
|
+
matching = tree.tree.select { |item| item.type == "blob" && File.fnmatch(pattern, item.path) }
|
|
168
|
+
return nil if matching.empty?
|
|
169
|
+
|
|
170
|
+
fetch_single_file(api_client, full_name, matching.first.path)
|
|
171
|
+
rescue Octokit::Error => e
|
|
172
|
+
Superkick.logger.debug("github_organization_registry") { "Could not fetch #{pattern} for #{full_name}: #{e.message}" }
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def fetch_single_file(api_client, full_name, path)
|
|
177
|
+
response = api_client.contents(full_name, path:, accept: "application/vnd.github.raw+json")
|
|
178
|
+
response.is_a?(String) ? response : nil
|
|
179
|
+
rescue Octokit::NotFound
|
|
180
|
+
nil
|
|
181
|
+
rescue Octokit::Error => e
|
|
182
|
+
Superkick.logger.debug("github_organization_registry") { "Could not fetch #{path} for #{full_name}: #{e.message}" }
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def merge_context_document_patterns(per_source_config)
|
|
187
|
+
global = Superkick.config.resolved_context_document_patterns
|
|
188
|
+
local = resolve_context_document_entries(per_source_config)
|
|
189
|
+
global.merge(local)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def resolve_context_document_entries(config)
|
|
193
|
+
return {} unless config.is_a?(Array) && config.any?
|
|
194
|
+
|
|
195
|
+
config.each_with_object({}) do |entry, h|
|
|
196
|
+
if entry.is_a?(Hash) && entry[:name] && entry[:pattern]
|
|
197
|
+
h[entry[:name].to_sym] = entry[:pattern].to_s
|
|
198
|
+
elsif entry.is_a?(String) || entry.is_a?(Symbol)
|
|
199
|
+
name = entry.to_sym
|
|
200
|
+
pattern = Repository::CONTEXT_DOCUMENT_PATTERNS[name]
|
|
201
|
+
h[name] = pattern if pattern
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def build_client
|
|
207
|
+
Octokit::Client.new(
|
|
208
|
+
access_token: @token,
|
|
209
|
+
middleware: faraday_stack
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def faraday_stack
|
|
214
|
+
Faraday::RackBuilder.new do |builder|
|
|
215
|
+
builder.request :retry,
|
|
216
|
+
max: 3,
|
|
217
|
+
interval: 0.5,
|
|
218
|
+
backoff_factor: 2,
|
|
219
|
+
exceptions: Faraday::Retry::Middleware::DEFAULT_EXCEPTIONS + [Faraday::ConnectionFailed]
|
|
220
|
+
builder.adapter Faraday.default_adapter
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
RepositorySource.register(Integrations::GitHub::OrganizationRepositorySource)
|
|
228
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
SUPERKICK [{{ "now" | time }}]: GitHub checks FAILED on {{ branch }} ({{ sha | short_sha }})
|
|
2
|
+
Repository: {{ repo }}
|
|
3
|
+
Commit: {{ commit.message }} by {{ commit.author }}
|
|
4
|
+
{% for check in failed_checks -%}
|
|
5
|
+
- {{ check.name }} ({{ check.app }}): {{ check.conclusion }}{% if check.details_url %} — {{ check.details_url }}{% endif %}
|
|
6
|
+
{% endfor -%}
|
|
7
|
+
{{ failed_checks.size }} of {{ total_checks }} checks failed.
|
|
8
|
+
|
|
9
|
+
Please investigate the failures, fix the underlying issues, write or update
|
|
10
|
+
tests as needed, and push a fix. Check the details URLs above for full logs.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
SUPERKICK [{{ "now" | time }}]: All {{ check_count }} CI checks passed on {{ branch }} ({{ sha | short_sha }}).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
SUPERKICK [{{ "now" | time }}]: GitHub issue assigned — #{{ issue.number }} {{ issue.title }}
|
|
2
|
+
URL: {{ issue.url }}
|
|
3
|
+
Author: @{{ issue.author }}
|
|
4
|
+
{% if issue.labels.size > 0 -%}
|
|
5
|
+
Labels: {{ issue.labels | join: ", " }}
|
|
6
|
+
{% endif -%}
|
|
7
|
+
{% if issue.assignees.size > 0 -%}
|
|
8
|
+
Assignees: {% for a in issue.assignees %}@{{ a }}{% unless forloop.last %}, {% endunless %}{% endfor %}
|
|
9
|
+
{% endif -%}
|
|
10
|
+
{% if issue.milestone -%}
|
|
11
|
+
Milestone: {{ issue.milestone }}
|
|
12
|
+
{% endif -%}
|
|
13
|
+
|
|
14
|
+
## Description
|
|
15
|
+
|
|
16
|
+
{% if issue.body == "" %}(no description){% else %}{{ issue.body }}{% endif %}
|
|
17
|
+
|
|
18
|
+
Please read the issue description carefully. Implement the requested changes,
|
|
19
|
+
write tests, and commit your work. If anything is unclear, check the issue
|
|
20
|
+
comments for additional context.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "github/drops"
|
|
4
|
+
require_relative "github/monitor"
|
|
5
|
+
require_relative "github/probe"
|
|
6
|
+
require_relative "github/goal"
|
|
7
|
+
require_relative "github/issue_goal"
|
|
8
|
+
require_relative "github/issue_spawner"
|
|
9
|
+
require_relative "github/check_failed_spawner"
|
|
10
|
+
require_relative "github/repository_source"
|
|
11
|
+
|
|
12
|
+
module Superkick
|
|
13
|
+
Monitor.register(Integrations::GitHub::Monitor)
|
|
14
|
+
Spawner.register(Integrations::GitHub::IssueSpawner)
|
|
15
|
+
Spawner.register(Integrations::GitHub::CheckFailedSpawner)
|
|
16
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Honeybadger Integration
|
|
2
|
+
|
|
3
|
+
Provides: **notifier**, **spawner**
|
|
4
|
+
|
|
5
|
+
## Notifier (`:honeybadger`)
|
|
6
|
+
|
|
7
|
+
Sends structured events to Honeybadger Insights via the Events API. See the
|
|
8
|
+
[Notifications section](../../../../README.md#notifications) in the main README
|
|
9
|
+
for notifier configuration.
|
|
10
|
+
|
|
11
|
+
## Spawner (`:honeybadger`)
|
|
12
|
+
|
|
13
|
+
Watches Honeybadger for unresolved faults and spawns AI coding agents to fix
|
|
14
|
+
them. Polls the Honeybadger Faults API for faults matching configurable filters
|
|
15
|
+
(environment, fault class, minimum occurrence count).
|
|
16
|
+
|
|
17
|
+
Faults are tracked by ID to prevent re-dispatching. Faults that fail
|
|
18
|
+
client-side filters (e.g. below `minimum_occurrences`) are retried on
|
|
19
|
+
subsequent ticks so they fire once the threshold is crossed.
|
|
20
|
+
|
|
21
|
+
### Configuration
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
spawners:
|
|
25
|
+
honeybadger:
|
|
26
|
+
type: honeybadger
|
|
27
|
+
project_id: "12345"
|
|
28
|
+
token: <%= env("HONEYBADGER_TOKEN") %>
|
|
29
|
+
driver: claude_code
|
|
30
|
+
environment: production
|
|
31
|
+
minimum_occurrences: 5
|
|
32
|
+
fault_classes:
|
|
33
|
+
include:
|
|
34
|
+
- NoMethodError
|
|
35
|
+
- RuntimeError
|
|
36
|
+
exclude:
|
|
37
|
+
- Net::ReadTimeout
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| Key | Required | Default | Description |
|
|
41
|
+
|-----|----------|---------|-------------|
|
|
42
|
+
| `project_id` | yes | — | Honeybadger project ID |
|
|
43
|
+
| `token` | no | `$HONEYBADGER_TOKEN` | Honeybadger API auth token |
|
|
44
|
+
| `environment` | no | `"production"` | Environment name |
|
|
45
|
+
| `minimum_occurrences` | no | `1` | Minimum occurrence count before dispatching |
|
|
46
|
+
| `fault_classes` | no | — | Fault class filter (array or include/exclude hash) |
|
|
47
|
+
|
|
48
|
+
### Fault class filter format
|
|
49
|
+
|
|
50
|
+
The `fault_classes` key accepts two formats:
|
|
51
|
+
|
|
52
|
+
- **Array** — treated as an include list: `["NoMethodError", "TypeError"]`
|
|
53
|
+
- **Hash** — explicit include/exclude: `{ include: ["NoMethodError"], exclude: ["Net::ReadTimeout"] }`
|
|
54
|
+
|
|
55
|
+
Include filters are applied server-side (via the Honeybadger API search query).
|
|
56
|
+
Exclude filters are applied client-side after fetching.
|
|
57
|
+
|
|
58
|
+
### Agent ID format
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
honeybadger-fault-{fault_id}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Where `fault_id` is the Honeybadger fault ID. Dedup is handled by
|
|
65
|
+
`AgentStore` — if an agent with the same ID already exists, the spawn is
|
|
66
|
+
skipped.
|
|
67
|
+
|
|
68
|
+
### Events
|
|
69
|
+
|
|
70
|
+
#### `error_opened`
|
|
71
|
+
|
|
72
|
+
Dispatched when a new unresolved fault is found that passes all filters.
|
|
73
|
+
|
|
74
|
+
Template variables:
|
|
75
|
+
|
|
76
|
+
| Variable | Description |
|
|
77
|
+
|----------|-------------|
|
|
78
|
+
| `fault_id` | Honeybadger fault ID |
|
|
79
|
+
| `error_class` | Exception class name (e.g. `NoMethodError`) |
|
|
80
|
+
| `message` | Error message string |
|
|
81
|
+
| `environment` | Environment name |
|
|
82
|
+
| `events` | Total occurrence count |
|
|
83
|
+
| `users` | Number of impacted users |
|
|
84
|
+
| `first_seen` | Timestamp of first occurrence |
|
|
85
|
+
| `last_seen` | Timestamp of most recent occurrence |
|
|
86
|
+
| `component` | Controller/component name |
|
|
87
|
+
| `action` | Action name |
|
|
88
|
+
| `assignee` | Assigned user (hash or nil) |
|
|
89
|
+
| `url` | Honeybadger dashboard URL for this fault |
|
|
90
|
+
| `project_id` | Honeybadger project ID |
|
|
91
|
+
|
|
92
|
+
### Error handling
|
|
93
|
+
|
|
94
|
+
- **401/403** — raises `FatalError`, stops the spawner (authentication failure)
|
|
95
|
+
- **429** — raises `RateLimited`, backs off per the standard poller backoff
|
|
96
|
+
- **404** — logged as warning, skipped (resource not found)
|
|
97
|
+
- Other HTTP errors — logged as warning, skipped
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{% event_message %}{{ message }}{% endevent_message %}
|
|
2
|
+
{% severity %}{{ event_type | honeybadger_severity }}{% endseverity %}
|
|
3
|
+
{% if monitor %}{% payload_field key: "monitor_type" %}{{ monitor.type }}{% endpayload_field %}
|
|
4
|
+
{% payload_field key: "monitor_name" %}{{ monitor.name }}{% endpayload_field %}{% endif %}
|
|
5
|
+
{% if team %}{% payload_field key: "team_id" %}{{ team.id }}{% endpayload_field %}{% endif %}
|
|
6
|
+
{% if agent.role %}{% payload_field key: "team_role" %}{{ agent.role }}{% endpayload_field %}{% endif %}
|
|
7
|
+
{% if spawner %}{% payload_field key: "spawner_name" %}{{ spawner.name }}{% endpayload_field %}{% endif %}
|
|
8
|
+
{% if agent.cost_usd %}{% payload_field key: "cost_usd" %}{{ agent.cost_usd }}{% endpayload_field %}{% endif %}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Superkick
|
|
7
|
+
module Integrations
|
|
8
|
+
module Honeybadger
|
|
9
|
+
# Sends structured events to Honeybadger Insights via the Events API.
|
|
10
|
+
#
|
|
11
|
+
# Honeybadger Insights is a structured event and log ingestion product.
|
|
12
|
+
# Events appear in the Insights dashboard where they can be searched,
|
|
13
|
+
# filtered, graphed, and used to trigger alerts.
|
|
14
|
+
#
|
|
15
|
+
# Stateful: tracks `spawned_at` per agent to compute duration on terminal
|
|
16
|
+
# events. Duration is included in the event payload.
|
|
17
|
+
#
|
|
18
|
+
# Template support:
|
|
19
|
+
# Event formatting (message, severity, payload fields) is defined via
|
|
20
|
+
# Liquid templates with custom block tags. Users can override per-event-type
|
|
21
|
+
# or the default template.
|
|
22
|
+
#
|
|
23
|
+
# Block tags: {% event_message %}, {% severity %}, {% payload_field key: "name" %}
|
|
24
|
+
# Filters: `honeybadger_severity`
|
|
25
|
+
#
|
|
26
|
+
# Templates are resolved from:
|
|
27
|
+
# 1. ~/.superkick/templates/notifications/honeybadger/<event_type>.liquid
|
|
28
|
+
# 2. <bundled>/notification_templates/<event_type>.liquid
|
|
29
|
+
# 3. ~/.superkick/templates/notifications/honeybadger/default.liquid
|
|
30
|
+
# 4. <bundled>/notification_templates/default.liquid
|
|
31
|
+
#
|
|
32
|
+
# Configuration:
|
|
33
|
+
#
|
|
34
|
+
# notifications:
|
|
35
|
+
# - type: honeybadger
|
|
36
|
+
# api_key: <%= env("HONEYBADGER_API_KEY") %>
|
|
37
|
+
# tags: # optional additional tags
|
|
38
|
+
# - production
|
|
39
|
+
# - team-alpha
|
|
40
|
+
#
|
|
41
|
+
# Each event is sent as newline-delimited JSON to the Honeybadger Events
|
|
42
|
+
# API endpoint. Events include:
|
|
43
|
+
# - event_type: the Superkick event type (e.g. "agent_completed")
|
|
44
|
+
# - agent_id, monitor_type, monitor_name: standard Superkick metadata
|
|
45
|
+
# - message: the human-readable notification message
|
|
46
|
+
# - severity: mapped from event type (error/warning/info)
|
|
47
|
+
# - duration_seconds: time since agent_spawned (on terminal events only)
|
|
48
|
+
# - tags: user-configured tags array
|
|
49
|
+
class Notifier < Superkick::Notifier
|
|
50
|
+
def self.type = :honeybadger
|
|
51
|
+
|
|
52
|
+
def self.templates_dir = File.expand_path("notification_templates", __dir__)
|
|
53
|
+
|
|
54
|
+
def self.setup_label = "Honeybadger"
|
|
55
|
+
|
|
56
|
+
def self.setup_config
|
|
57
|
+
<<~YAML
|
|
58
|
+
- type: honeybadger
|
|
59
|
+
api_key: <%= env("HONEYBADGER_API_KEY") %>
|
|
60
|
+
# tags: # optional tags
|
|
61
|
+
# - production
|
|
62
|
+
YAML
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
API_URL = "https://api.honeybadger.io"
|
|
66
|
+
EVENTS_PATH = "/v1/events"
|
|
67
|
+
TIMEOUT = 10
|
|
68
|
+
|
|
69
|
+
SEVERITY_MAP = {
|
|
70
|
+
"agent_completed" => "info",
|
|
71
|
+
"agent_failed" => "error",
|
|
72
|
+
"agent_timed_out" => "warning",
|
|
73
|
+
"agent_blocked" => "warning",
|
|
74
|
+
"agent_stalled" => "warning",
|
|
75
|
+
"agent_terminated" => "warning",
|
|
76
|
+
"agent_spawned" => "info",
|
|
77
|
+
"agent_claimed" => "info",
|
|
78
|
+
"agent_unclaimed" => "info",
|
|
79
|
+
"agent_pending_approval" => "info",
|
|
80
|
+
"workflow_triggered" => "info",
|
|
81
|
+
"workflow_iterations_exceeded" => "warning",
|
|
82
|
+
"budget_warning" => "warning",
|
|
83
|
+
"budget_exceeded" => "error",
|
|
84
|
+
"team_created" => "info",
|
|
85
|
+
"team_completed" => "info",
|
|
86
|
+
"team_failed" => "error",
|
|
87
|
+
"team_timed_out" => "warning",
|
|
88
|
+
"worker_spawned" => "info",
|
|
89
|
+
"teammate_message" => "info",
|
|
90
|
+
"teammate_blocker" => "warning",
|
|
91
|
+
"attach_promoted" => "info",
|
|
92
|
+
"attach_demoted" => "info",
|
|
93
|
+
"attach_idle_timeout" => "warning",
|
|
94
|
+
"attach_force_takeover" => "warning",
|
|
95
|
+
"agent_update" => "info",
|
|
96
|
+
"artifact_published" => "info"
|
|
97
|
+
}.freeze
|
|
98
|
+
|
|
99
|
+
liquid do
|
|
100
|
+
context do
|
|
101
|
+
attribute :message
|
|
102
|
+
attribute :severity
|
|
103
|
+
attribute :payload, default: -> { {} }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
filter :honeybadger_severity do |event_type|
|
|
107
|
+
Superkick::Integrations::Honeybadger::Notifier::SEVERITY_MAP.fetch(event_type.to_s, "info")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
block :event_message do |ctx, text, _attrs|
|
|
111
|
+
ctx.message = text
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
block :severity do |ctx, text, _attrs|
|
|
115
|
+
ctx.severity = text
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
block :payload_field do |ctx, text, attrs|
|
|
119
|
+
name = attrs["key"]
|
|
120
|
+
ctx.payload[name] = text if name && !text.empty?
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def initialize(api_key: nil, tags: [], connection: nil, **opts)
|
|
125
|
+
super(**opts)
|
|
126
|
+
@api_key = api_key
|
|
127
|
+
@tags = Array(tags)
|
|
128
|
+
@connection = connection
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def stateful? = true
|
|
132
|
+
|
|
133
|
+
def agent_finished(agent_id:)
|
|
134
|
+
@state_store.delete(:honeybadger, agent_id.to_s)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def notify(payload)
|
|
138
|
+
unless @api_key
|
|
139
|
+
Superkick.logger.warn("notifier:honeybadger") { "No api_key configured" }
|
|
140
|
+
return
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
event_type = payload[:event_type]
|
|
144
|
+
agent_id = payload[:agent_id]
|
|
145
|
+
|
|
146
|
+
# Track spawn time for duration computation
|
|
147
|
+
if event_type == "agent_spawned" && agent_id && !agent_id.empty?
|
|
148
|
+
@state_store.put(:honeybadger, agent_id, {spawned_at: Process.clock_gettime(Process::CLOCK_MONOTONIC)}) unless @state_store.get(:honeybadger, agent_id)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
event = build_event(payload)
|
|
152
|
+
post_event(event)
|
|
153
|
+
rescue => e
|
|
154
|
+
Superkick.logger.warn("notifier:honeybadger") { "Failed: #{e.message}" }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
def build_event(payload)
|
|
160
|
+
event_type = payload[:event_type]
|
|
161
|
+
agent_id = payload[:agent_id]
|
|
162
|
+
|
|
163
|
+
template_result = render_notification(payload)
|
|
164
|
+
|
|
165
|
+
if template_result && template_result[:structured]
|
|
166
|
+
structured = template_result[:structured]
|
|
167
|
+
message = structured.message || payload[:message]
|
|
168
|
+
severity = structured.severity || SEVERITY_MAP.fetch(event_type, "info")
|
|
169
|
+
else
|
|
170
|
+
message = payload[:message]
|
|
171
|
+
severity = SEVERITY_MAP.fetch(event_type, "info")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
event = {
|
|
175
|
+
ts: payload[:timestamp] || Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
176
|
+
event_type: "superkick.#{event_type}",
|
|
177
|
+
severity:,
|
|
178
|
+
message:,
|
|
179
|
+
agent_id:,
|
|
180
|
+
source: "superkick"
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# Add template-driven payload fields or fall back to hardcoded fields
|
|
184
|
+
if template_result && template_result[:structured] && !template_result[:structured].payload.empty?
|
|
185
|
+
template_result[:structured].payload.each do |key, value|
|
|
186
|
+
event[key.to_sym] = value
|
|
187
|
+
end
|
|
188
|
+
else
|
|
189
|
+
monitor = payload[:monitor]
|
|
190
|
+
if monitor
|
|
191
|
+
event[:monitor_type] = monitor.type unless monitor.type.to_s.empty?
|
|
192
|
+
event[:monitor_name] = monitor.name unless monitor.name.to_s.empty?
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
ctx = payload[:context] || {}
|
|
196
|
+
team = ctx[:team]
|
|
197
|
+
event[:team_id] = team.id if team&.id
|
|
198
|
+
agent = ctx[:agent]
|
|
199
|
+
event[:team_role] = agent.role if agent&.role
|
|
200
|
+
spawner = ctx[:spawner]
|
|
201
|
+
event[:spawner_name] = spawner.name.to_s if spawner
|
|
202
|
+
agent_drop = ctx[:agent]
|
|
203
|
+
event[:cost_usd] = agent_drop.cost_usd if agent_drop&.cost_usd
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
event[:tags] = @tags unless @tags.empty?
|
|
207
|
+
|
|
208
|
+
# Add duration on terminal events — prefer agent.spawned_at, fall back to local tracking
|
|
209
|
+
if terminal_event?(event_type) && agent_id && !agent_id.empty?
|
|
210
|
+
agent_drop = payload.dig(:context, :agent)
|
|
211
|
+
duration = if agent_drop&.spawned_at
|
|
212
|
+
(Time.now.utc - Time.parse(agent_drop.spawned_at)).round(1)
|
|
213
|
+
else
|
|
214
|
+
state = @state_store.get(:honeybadger, agent_id)
|
|
215
|
+
(Process.clock_gettime(Process::CLOCK_MONOTONIC) - state[:spawned_at]).round(1) if state&.dig(:spawned_at)
|
|
216
|
+
end
|
|
217
|
+
event[:duration_seconds] = duration if duration
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
event
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def post_event(event)
|
|
224
|
+
conn = @connection || build_connection
|
|
225
|
+
response = conn.post(EVENTS_PATH) do |req|
|
|
226
|
+
req.body = JSON.generate(event) + "\n"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
unless (200..299).cover?(response.status)
|
|
230
|
+
Superkick.logger.warn("notifier:honeybadger") { "API returned HTTP #{response.status}" }
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def build_connection
|
|
235
|
+
Faraday.new(url: API_URL) do |f|
|
|
236
|
+
f.headers["Content-Type"] = "application/json"
|
|
237
|
+
f.headers["X-API-Key"] = @api_key
|
|
238
|
+
f.options.timeout = TIMEOUT
|
|
239
|
+
f.options.open_timeout = TIMEOUT
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def terminal_event?(event_type)
|
|
244
|
+
%w[agent_completed agent_failed agent_timed_out agent_terminated
|
|
245
|
+
team_completed team_failed team_timed_out].include?(event_type)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|