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,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)
|