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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "git/version_control"
4
+ require_relative "git/repository_source"
5
+
6
+ module Superkick
7
+ VersionControl.register(Integrations::Git::VersionControl)
8
+ end
@@ -0,0 +1,300 @@
1
+ # GitHub Integration
2
+
3
+ The GitHub integration provides a **monitor**, **spawners**, and **goals** for GitHub repositories.
4
+
5
+ ## Monitor
6
+
7
+ Type: `:github`
8
+
9
+ Polls GitHub CI check runs and PR activity for a registered agent. Injects
10
+ context when CI fails, new PR comments arrive, or reviews are submitted.
11
+
12
+ ## Configuration
13
+
14
+ ```yaml
15
+ monitors:
16
+ github:
17
+ token: <%= env("GITHUB_TOKEN") %>
18
+ ```
19
+
20
+ | Key | Required | Default | Description |
21
+ |-----|----------|---------|-------------|
22
+ | `repo` | yes | auto-detected | GitHub repo in `org/name` format |
23
+ | `branch` | yes | auto-detected | Git branch to watch |
24
+ | `token` | no | `GITHUB_TOKEN` env var | Personal access token for private repos |
25
+
26
+ The probe auto-detects `repo` and `branch` from the local git remote, so
27
+ explicit config is only needed to override or when there's no local checkout.
28
+
29
+ ## Probe
30
+
31
+ `GitHubMonitor::Probe` inspects the current directory's git remote for a
32
+ GitHub URL and reads the current branch via `git symbolic-ref`. Returns a
33
+ config hash keyed as `"github"` with `repo` and `branch` populated.
34
+
35
+ Returns `{}` if the directory isn't a git repo or has no GitHub remote.
36
+
37
+ ## Events
38
+
39
+ ### `ci_failure`
40
+
41
+ Dispatched when all check runs for the HEAD commit complete and at least one
42
+ has a non-success conclusion. Only fires on status change or new SHA.
43
+
44
+ Template variables:
45
+ - `repo` — `org/name`
46
+ - `branch` — branch name
47
+ - `sha` — full commit SHA (`short_sha(sha)` for 7-char form)
48
+ - `failed_checks` — array of failed check names
49
+ - `check_count` — total number of checks
50
+
51
+ ### `ci_success`
52
+
53
+ Dispatched when all check runs pass. Same trigger logic as `ci_failure`.
54
+
55
+ Template variables: same as `ci_failure`.
56
+
57
+ ### `pr_comment`
58
+
59
+ Dispatched for each new issue comment on the open PR for the watched branch.
60
+ Uses a `last_comment_id` watermark to avoid re-reporting.
61
+
62
+ Template variables:
63
+ - `repo`, `branch`
64
+ - `pull_request` — `PullRequestDrop` with `.number`, `.title`
65
+ - `comment` — `CommentDrop` with `.author`, `.body`, `.url`
66
+
67
+ ### `pr_review`
68
+
69
+ Dispatched for each new non-pending review on the open PR. Uses a
70
+ `last_review_id` watermark.
71
+
72
+ Template variables:
73
+ - `repo`, `branch`
74
+ - `pull_request` — `PullRequestDrop` with `.number`, `.title`
75
+ - `review` — `ReviewDrop` with `.author`, `.state`, `.body`, `.url`
76
+
77
+ ## Watermarks
78
+
79
+ The monitor persists these fields on the agent to avoid duplicate events
80
+ across ticks:
81
+
82
+ - `last_ci_sha` / `last_ci_status` — last reported commit + its CI result
83
+ - `last_comment_id` — highest seen PR comment ID
84
+ - `last_review_id` — highest seen PR review ID
85
+
86
+ ## Monitor error handling
87
+
88
+ - `Octokit::TooManyRequests` → `RateLimited` (backs off by `rate_limit_backoff`)
89
+ - `Octokit::Unauthorized` → `FatalError` (stops the monitor thread)
90
+ - `Octokit::NotFound` → logged and skipped (e.g. branch deleted mid-tick)
91
+
92
+ ## Goal
93
+
94
+ Type: `:github_pr_merged`
95
+
96
+ Polls GitHub to check whether a PR has been merged. Intended for spawned agents
97
+ — when the PR is merged, the goal returns `:completed` and the Supervisor
98
+ terminates the agent.
99
+
100
+ ### Configuration
101
+
102
+ ```yaml
103
+ spawners:
104
+ my_spawner:
105
+ goal:
106
+ type: github_pr_merged
107
+ ```
108
+
109
+ All config keys are optional:
110
+
111
+ | Key | Required | Default | Description |
112
+ |-----|----------|---------|-------------|
113
+ | `repo` | no | auto-detected from git remote | GitHub repo in `org/name` format |
114
+ | `branch` | no | auto-detected from git HEAD | Branch to search for PRs on |
115
+ | `pr_number` | no | discovered via branch search | Direct PR number for efficient lookups |
116
+ | `token` | no | `GITHUB_TOKEN` env var | Personal access token for private repos |
117
+ | `working_dir` | no | injected by AgentSpawner | Directory for git auto-detection |
118
+ | `check_interval` | no | `poll_interval` | Seconds between goal checks |
119
+
120
+ The `AgentSpawner` automatically injects `working_dir` into the goal config at
121
+ spawn time, so the server-side goal checker can run git commands against the
122
+ agent's workspace.
123
+
124
+ ### Auto-detection
125
+
126
+ The goal auto-detects `repo` and `branch` from the git working directory using
127
+ the same approach as the monitor probe:
128
+
129
+ - **`repo`** — parsed from `git remote -v` (SSH or HTTPS GitHub URLs). Cached
130
+ after first detection.
131
+ - **`branch`** — read from `git symbolic-ref --short HEAD`. Re-detected on each
132
+ check to handle the spawner flow where the agent starts on `main` and later
133
+ creates a feature branch.
134
+
135
+ Once a PR is found via branch search, its number is cached for efficient direct
136
+ lookups on subsequent checks.
137
+
138
+ ### Status mapping
139
+
140
+ | PR State | Goal Status |
141
+ |----------|-------------|
142
+ | Merged (`merged_at` present) | `:completed` (terminal) |
143
+ | Closed without merge | `:failed` (terminal) |
144
+ | Open | `:in_progress` (non-terminal) |
145
+ | No PR found | `:pending` (non-terminal) |
146
+ | API error / rate limit | `:errored` (non-terminal) |
147
+
148
+ ### Goal error handling
149
+
150
+ - `Octokit::TooManyRequests` → `:errored` (logged, retried next check)
151
+ - `Octokit::Unauthorized` → `:errored` (logged, retried next check)
152
+ - Generic exceptions → `:errored` (logged, retried next check)
153
+
154
+ ## Goal: GitHub Issue Resolved
155
+
156
+ Type: `:github_issue_resolved`
157
+
158
+ Polls GitHub to check whether an issue has been closed. Any close reason
159
+ (completed or not_planned) counts as success. Pairs naturally with the
160
+ `:github_issues` spawner.
161
+
162
+ ### Configuration
163
+
164
+ ```yaml
165
+ spawners:
166
+ github_issues:
167
+ goal:
168
+ type: github_issue_resolved
169
+ # issue context (IssueDrop) is injected from the spawn event by AgentSpawner
170
+ ```
171
+
172
+ | Key | Required | Default | Description |
173
+ |-----|----------|---------|-------------|
174
+ | `repo` | no | auto-detected from git remote | GitHub repo in `org/name` format |
175
+ | `issue` | yes | injected by AgentSpawner | `IssueDrop` from spawn event context (provides `.number`, `.title`, etc.) |
176
+ | `token` | no | `GITHUB_TOKEN` env var | Personal access token for private repos |
177
+ | `working_dir` | no | injected by AgentSpawner | Directory for git auto-detection |
178
+ | `check_interval` | no | `poll_interval` | Seconds between goal checks |
179
+
180
+ ### Status mapping
181
+
182
+ | Issue State | Goal Status |
183
+ |-------------|-------------|
184
+ | Closed (any reason) | `:completed` (terminal) |
185
+ | Open | `:in_progress` (non-terminal) |
186
+ | Missing config | `:pending` (non-terminal) |
187
+ | API error / rate limit | `:errored` (non-terminal) |
188
+
189
+ ## Spawner: GitHub Issues
190
+
191
+ Type: `:github_issues`
192
+
193
+ Watches for newly created or reopened GitHub issues and spawns agents to work
194
+ on them. Uses an `updated_at` watermark so reopened issues are caught
195
+ automatically.
196
+
197
+ ### Configuration
198
+
199
+ ```yaml
200
+ spawners:
201
+ github_issues:
202
+ type: github_issues
203
+ repo: myorg/myrepo
204
+ token: <%= env("GITHUB_TOKEN") %>
205
+ labels:
206
+ - ai-ready
207
+ assignee: "*"
208
+ driver: claude_code
209
+ repository: my-app
210
+ branch_template: "fix-{agent_id}"
211
+ goal:
212
+ type: github_issue_resolved
213
+ max_duration: 7200
214
+ ```
215
+
216
+ | Key | Required | Default | Description |
217
+ |-----|----------|---------|-------------|
218
+ | `repo` | yes | — | GitHub repo in `org/name` format |
219
+ | `token` | no | `GITHUB_TOKEN` env var | Personal access token |
220
+ | `labels` | no | — | Array of label names (AND filter) |
221
+ | `assignee` | no | — | Username, `"*"` (any), or `"none"` |
222
+ | `milestone` | no | — | Milestone number, `"*"`, or `"none"` |
223
+ | `creator` | no | — | Filter by issue creator username |
224
+ | `exclude_pull_requests` | no | `true` | Filter out PRs from the issues endpoint |
225
+
226
+ ### Agent ID format
227
+
228
+ `github-issue-{org}-{repo}-{number}` (e.g. `github-issue-myorg-myrepo-42`)
229
+
230
+ ### Events
231
+
232
+ #### `issue_opened`
233
+
234
+ Template variables:
235
+ - `repo` — `org/name`
236
+ - `issue` — `IssueDrop` with `.number`, `.title`, `.body`, `.url`, `.author`, `.labels`, `.assignees`, `.milestone`, `.created_at`, `.updated_at`
237
+
238
+ ## Spawner: GitHub Check Failed
239
+
240
+ Type: `:github_check_failed`
241
+
242
+ Watches GitHub check suites for failures on configured branches and spawns
243
+ agents to fix them. Works across CI providers since it polls GitHub's check
244
+ runs API.
245
+
246
+ ### Configuration
247
+
248
+ ```yaml
249
+ spawners:
250
+ ci_fixer:
251
+ type: github_check_failed
252
+ repo: myorg/myrepo
253
+ branches:
254
+ - main
255
+ - staging
256
+ token: <%= env("GITHUB_TOKEN") %>
257
+ app_filter:
258
+ - github-actions
259
+ exclude_apps:
260
+ - dependabot
261
+ driver: claude_code
262
+ max_concurrent: 2
263
+ cooldown: 600
264
+ ```
265
+
266
+ | Key | Required | Default | Description |
267
+ |-----|----------|---------|-------------|
268
+ | `repo` | yes | — | GitHub repo in `org/name` format |
269
+ | `branches` | yes | — | Array of branch names to watch |
270
+ | `token` | no | `GITHUB_TOKEN` env var | Personal access token |
271
+ | `app_filter` | no | — | Array of app names/slugs to include |
272
+ | `check_name_filter` | no | — | Array of check run names to include |
273
+ | `exclude_apps` | no | — | Array of app names to ignore |
274
+
275
+ ### Agent ID format
276
+
277
+ `github-check-{org}-{repo}-{branch}-{short_sha}` (e.g. `github-check-myorg-myrepo-main-abc1234`)
278
+
279
+ A new agent is spawned per branch+commit, so a new failure after a push gets
280
+ its own agent.
281
+
282
+ ### Events
283
+
284
+ #### `check_failed`
285
+
286
+ Template variables:
287
+ - `repo` — `org/name`
288
+ - `branch` — branch name
289
+ - `sha` — full commit SHA (`short_sha(sha)` for 7-char form)
290
+ - `failed_checks` — array of `CheckRunDrop` with `.name`, `.conclusion`, `.app`, `.details_url`, `.html_url`
291
+ - `total_checks` — total number of (filtered) checks
292
+ - `commit` — `CommitDrop` with `.message`, `.author`, `.url`
293
+
294
+ ### Behavior
295
+
296
+ - Polls each configured branch's HEAD SHA on every tick
297
+ - Skips branches where the SHA hasn't changed since last check
298
+ - Waits for all (filtered) check runs to reach `completed` status before evaluating
299
+ - Only dispatches when at least one check has a non-success conclusion
300
+ - Does not re-dispatch the same SHA on subsequent ticks
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "octokit"
4
+
5
+ module Superkick
6
+ module Integrations
7
+ module GitHub
8
+ # Watches GitHub check suites for failures on configured branches and
9
+ # spawns agents to fix them.
10
+ #
11
+ # Primary use case: CI broken on main or environment branches. Works
12
+ # across CI providers since it polls GitHub's check runs API.
13
+ #
14
+ # Config keys:
15
+ # repo (required) — owner/name
16
+ # branches (required) — array of branch names to watch
17
+ # token (optional) — falls back to GITHUB_TOKEN
18
+ # app_filter (optional) — array of app names/slugs to include
19
+ # check_name_filter (optional) — array of check run names to include
20
+ # exclude_apps (optional) — array of app names to ignore
21
+ # only_latest_commit (optional, default true) — only check HEAD of each branch
22
+ class CheckFailedSpawner < Superkick::Spawner
23
+ attr_reader :client
24
+
25
+ def initialize(name:, config:, handler:, client: nil)
26
+ super(name:, config:, handler:)
27
+ @client = client
28
+ end
29
+
30
+ def self.type = :github_check_failed
31
+
32
+ def self.description
33
+ "Watches GitHub check suites for failures on configured branches " \
34
+ "and spawns AI coding agents to fix them. Supports filtering by " \
35
+ "app name, check name, and branch."
36
+ end
37
+
38
+ def self.required_config = %i[repo branches]
39
+
40
+ def self.spawn_templates_dir
41
+ File.join(__dir__, "templates")
42
+ end
43
+
44
+ def self.agent_id(event)
45
+ "github-check-#{event[:repo].tr("/", "-")}-#{event[:branch]}-#{event[:sha][0..6]}"
46
+ end
47
+
48
+ def self.setup_label = "GitHub Check Failures"
49
+
50
+ def self.setup_config
51
+ <<~YAML
52
+ github_check_failed:
53
+ type: github_check_failed
54
+ repo: org/repo
55
+ branches:
56
+ - main
57
+ token: <%= env("GITHUB_TOKEN") %>
58
+ # app_filter: # only checks from these CI apps
59
+ # - "GitHub Actions"
60
+ # check_name_filter: # only these check run names
61
+ # - "test"
62
+ # max_duration: 3600
63
+ YAML
64
+ end
65
+
66
+ def tick
67
+ branches = self[:branches]
68
+ return unless branches.is_a?(Array)
69
+
70
+ branches.each { check_branch(it) }
71
+ rescue Octokit::TooManyRequests => e
72
+ raise RateLimited, e.message
73
+ rescue Octokit::Unauthorized => e
74
+ raise FatalError, "GitHub auth failed: #{e.message}"
75
+ end
76
+
77
+ def on_start
78
+ @client ||= build_client
79
+ @last_seen_sha = {}
80
+ @dispatched_shas = {}
81
+ end
82
+
83
+ private
84
+
85
+ def check_branch(branch)
86
+ repo = self[:repo]
87
+
88
+ sha = head_sha(repo, branch)
89
+ return unless sha
90
+
91
+ # Skip if we've already processed this SHA
92
+ return if sha == @last_seen_sha[branch]
93
+
94
+ runs_data = @client.check_runs_for_ref(repo, sha, accept: "application/vnd.github+json")
95
+ checks = runs_data.check_runs
96
+ return if checks.empty?
97
+
98
+ checks = apply_filters(checks)
99
+ return if checks.empty?
100
+
101
+ # Wait for all (filtered) checks to complete
102
+ completed = checks.select { it.status == "completed" }
103
+ return unless completed.size == checks.size
104
+
105
+ @last_seen_sha[branch] = sha
106
+
107
+ failed = completed.reject { it.conclusion == "success" }
108
+ return if failed.empty?
109
+
110
+ # Don't re-dispatch the same SHA
111
+ return if @dispatched_shas[branch] == sha
112
+
113
+ @dispatched_shas[branch] = sha
114
+
115
+ commit_info = fetch_commit_info(repo, sha)
116
+
117
+ dispatch(
118
+ event_type: :check_failed,
119
+ repo:,
120
+ branch:,
121
+ sha:,
122
+ failed_checks: failed.map { CheckRunDrop.new(format_check(it)) },
123
+ total_checks: checks.size,
124
+ commit: CommitDrop.new(commit_info)
125
+ )
126
+ rescue Octokit::NotFound => e
127
+ Superkick.logger.warn(log_tag) { "Branch #{branch} not found: #{e.message}" }
128
+ end
129
+
130
+ def head_sha(repo, branch)
131
+ @client.branch(repo, branch).commit.sha
132
+ rescue Octokit::NotFound
133
+ nil
134
+ end
135
+
136
+ def apply_filters(checks)
137
+ if self[:app_filter]&.any?
138
+ app_names = self[:app_filter].map(&:downcase)
139
+ checks = checks.select { app_names.include?(it.app.slug.downcase) || app_names.include?(it.app.name.downcase) }
140
+ end
141
+
142
+ if self[:exclude_apps]&.any?
143
+ excluded = self[:exclude_apps].map(&:downcase)
144
+ checks = checks.reject { excluded.include?(it.app.slug.downcase) || excluded.include?(it.app.name.downcase) }
145
+ end
146
+
147
+ if self[:check_name_filter]&.any?
148
+ names = self[:check_name_filter].map(&:downcase)
149
+ checks = checks.select { names.include?(it.name.downcase) }
150
+ end
151
+
152
+ checks
153
+ end
154
+
155
+ def format_check(check)
156
+ {
157
+ name: check.name,
158
+ conclusion: check.conclusion,
159
+ app: check.app.name,
160
+ details_url: check.details_url,
161
+ html_url: check.html_url
162
+ }
163
+ end
164
+
165
+ def fetch_commit_info(repo, sha)
166
+ commit = @client.commit(repo, sha)
167
+ {
168
+ message: commit.commit.message.lines.first&.strip.to_s,
169
+ author: commit.commit.author.name,
170
+ url: commit.html_url
171
+ }
172
+ rescue => e
173
+ Superkick.logger.debug(log_tag) { "Could not fetch commit #{sha}: #{e.message}" }
174
+ {message: "", author: "", url: ""}
175
+ end
176
+
177
+ def build_client
178
+ token = self[:token] || ENV["GITHUB_TOKEN"]
179
+
180
+ Octokit::Client.new(
181
+ access_token: token,
182
+ middleware: faraday_stack
183
+ )
184
+ end
185
+
186
+ def faraday_stack
187
+ Faraday::RackBuilder.new do |builder|
188
+ builder.request :retry,
189
+ max: 3,
190
+ interval: 0.5,
191
+ backoff_factor: 2,
192
+ exceptions: Faraday::Retry::Middleware::DEFAULT_EXCEPTIONS + [Faraday::ConnectionFailed]
193
+ builder.adapter Faraday.default_adapter
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superkick
4
+ module Integrations
5
+ module GitHub
6
+ # Liquid Drop for a GitHub issue. Wraps the symbol-keyed hash built
7
+ # by GitHubIssueSpawner so templates can use {{ issue.title }}, etc.
8
+ class IssueDrop < Superkick::Drop
9
+ def self.drop_type = "github_issue"
10
+
11
+ def number = @data[:number]
12
+
13
+ def title = @data[:title]
14
+
15
+ def body = @data[:body]
16
+
17
+ def url = @data[:url]
18
+
19
+ def author = @data[:author]
20
+
21
+ def labels = @data[:labels]
22
+
23
+ def assignees = @data[:assignees]
24
+
25
+ def milestone = @data[:milestone]
26
+
27
+ def created_at = @data[:created_at]
28
+
29
+ def updated_at = @data[:updated_at]
30
+ end
31
+
32
+ # Liquid Drop for a GitHub pull request. Shared across pr_comment
33
+ # and pr_review events so templates can use {{ pull_request.number }}.
34
+ class PullRequestDrop < Superkick::Drop
35
+ def self.drop_type = "github_pull_request"
36
+
37
+ def number = @data[:number]
38
+
39
+ def title = @data[:title]
40
+
41
+ # Formatted reference: #42 "Title…" (title truncated to 50 chars).
42
+ def ref
43
+ title_str = title
44
+ if title_str && !title_str.empty?
45
+ truncated = (title_str.length > 50) ? "#{title_str[0, 49]}…" : title_str
46
+ "##{number} \"#{truncated}\""
47
+ else
48
+ "##{number}"
49
+ end
50
+ end
51
+ end
52
+
53
+ # Liquid Drop for a GitHub PR comment.
54
+ class CommentDrop < Superkick::Drop
55
+ def self.drop_type = "github_comment"
56
+
57
+ def author = @data[:author]
58
+
59
+ def body = @data[:body]
60
+
61
+ def url = @data[:url]
62
+ end
63
+
64
+ # Liquid Drop for a GitHub PR review.
65
+ class ReviewDrop < Superkick::Drop
66
+ def self.drop_type = "github_review"
67
+
68
+ def author = @data[:author]
69
+
70
+ def state = @data[:state]
71
+
72
+ def body = @data[:body]
73
+
74
+ def url = @data[:url]
75
+ end
76
+
77
+ # Liquid Drop for a GitHub commit. Used in check_failed events.
78
+ class CommitDrop < Superkick::Drop
79
+ def self.drop_type = "github_commit"
80
+
81
+ def message = @data[:message]
82
+
83
+ def author = @data[:author]
84
+
85
+ def url = @data[:url]
86
+ end
87
+
88
+ # Liquid Drop for GitHub check run data. Wraps the symbol-keyed
89
+ # hash returned by GitHubCheckFailedSpawner#format_check so
90
+ # Liquid templates can access properties via dot notation
91
+ # (e.g. {{ check.name }}, {{ check.details_url }}).
92
+ class CheckRunDrop < Superkick::Drop
93
+ def self.drop_type = "github_check_run"
94
+
95
+ def name = @data[:name]
96
+
97
+ def conclusion = @data[:conclusion]
98
+
99
+ def app = @data[:app]
100
+
101
+ def details_url = @data[:details_url]
102
+
103
+ def html_url = @data[:html_url]
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ Superkick::Drop.register(Superkick::Integrations::GitHub::IssueDrop)
110
+ Superkick::Drop.register(Superkick::Integrations::GitHub::PullRequestDrop)
111
+ Superkick::Drop.register(Superkick::Integrations::GitHub::CommentDrop)
112
+ Superkick::Drop.register(Superkick::Integrations::GitHub::ReviewDrop)
113
+ Superkick::Drop.register(Superkick::Integrations::GitHub::CommitDrop)
114
+ Superkick::Drop.register(Superkick::Integrations::GitHub::CheckRunDrop)