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,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,5 @@
1
+ SUPERKICK [{{ "now" | time }}]: CI checks FAILED on {{ branch }} ({{ sha | short_sha }})
2
+ {% if failed_checks.size > 0 -%}
3
+ Failed: {{ failed_checks | join: ", " }}
4
+ {% endif -%}
5
+ Please review the CI output and fix the failures before continuing.
@@ -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,2 @@
1
+ SUPERKICK [{{ "now" | time }}]: New PR comment on {{ pull_request.ref }} from {{ comment.author }}:
2
+ "{{ comment.body | truncate: 200 }}"
@@ -0,0 +1,4 @@
1
+ SUPERKICK [{{ "now" | time }}]: PR review {{ review.state }} on {{ pull_request.ref }} by {{ review.author }}.
2
+ {% unless review.body == "" -%}
3
+ "{{ review.body | truncate: 200 }}"
4
+ {% endunless -%}
@@ -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