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,75 @@
1
+ # CircleCI Monitor
2
+
3
+ Type: `:circleci`
4
+
5
+ Polls CircleCI pipelines and workflows for a project and branch. Injects
6
+ context when workflows fail or all pass.
7
+
8
+ ## Configuration
9
+
10
+ ```yaml
11
+ monitors:
12
+ circleci:
13
+ token: <%= env("CIRCLECI_TOKEN") %>
14
+ ```
15
+
16
+ | Key | Required | Default | Description |
17
+ |-----|----------|---------|-------------|
18
+ | `project_slug` | yes | auto-detected | CircleCI project slug (e.g. `gh/org/repo`) |
19
+ | `branch` | yes | auto-detected | Git branch to watch |
20
+ | `token` | no | `CIRCLECI_TOKEN` env var | CircleCI API token |
21
+
22
+ The probe auto-detects `project_slug` and `branch` from `.circleci/config.yml`
23
+ and git remote, so explicit config is only needed to override or when there's
24
+ no local checkout.
25
+
26
+ ## Probe
27
+
28
+ `CircleCIMonitor::Probe` checks for the presence of `.circleci/config.yml` and
29
+ then reads the git remote URL and current branch. Supports GitHub (`gh/`) and
30
+ Bitbucket (`bb/`) project slugs via both SSH and HTTPS remote formats.
31
+
32
+ Returns `{ "circleci" => { "type" => "circleci", "project_slug" => "...", "branch" => "..." } }`
33
+ on match, or `{}` if no CircleCI config is found.
34
+
35
+ ## Events
36
+
37
+ ### `ci_failure`
38
+
39
+ Dispatched when all workflows in the latest pipeline complete and at least one
40
+ has a non-success status. Only fires on status change or new pipeline.
41
+
42
+ Template variables:
43
+ - `project_slug` — CircleCI project slug
44
+ - `branch` — branch name
45
+ - `pipeline_id` — pipeline UUID
46
+ - `pipeline_number` — pipeline number
47
+ - `failed_workflows` — array of failed workflow names
48
+ - `failed_jobs` — array of failed job names across all failed workflows
49
+ - `workflow_count` — total number of workflows
50
+
51
+ ### `ci_success`
52
+
53
+ Dispatched when all workflows in the latest pipeline pass. Same trigger logic
54
+ as `ci_failure`.
55
+
56
+ Template variables: same as `ci_failure`.
57
+
58
+ ## Watermarks
59
+
60
+ The monitor persists these fields on the agent to avoid duplicate events
61
+ across ticks:
62
+
63
+ - `last_pipeline_id` — last reported pipeline UUID
64
+ - `last_ci_status` — last reported aggregate status (`"success"` or `"failure"`)
65
+
66
+ ## API usage
67
+
68
+ The monitor uses the [CircleCI API v2](https://circleci.com/docs/api/v2/) via
69
+ Faraday with the `Circle-Token` header for authentication.
70
+
71
+ ## Error handling
72
+
73
+ - HTTP 401 → `FatalError` (bad token — stops the monitor)
74
+ - HTTP 429 → `RateLimited` (backs off by `rate_limit_backoff`)
75
+ - HTTP 404 → logged and skipped
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+
5
+ module Superkick
6
+ module Integrations
7
+ module CircleCI
8
+ # Polls CircleCI pipelines and workflows for a project+branch.
9
+ #
10
+ # Required per-agent config keys: project_slug, branch
11
+ # Optional config key: token (or CIRCLECI_TOKEN env var)
12
+ #
13
+ # Events emitted: ci_failure, ci_success
14
+ # Watermarks persisted: last_pipeline_id, last_ci_status
15
+ class Monitor < Superkick::Monitor
16
+ TERMINAL_STATUSES = %w[success failed error canceled unauthorized].freeze
17
+
18
+ GITHUB_SSH_RE = %r{github\.com[:/](?<org>[^/]+)/(?<repo>[^\s.]+)}
19
+ GITHUB_HTTPS_RE = %r{github\.com/(?<org>[^/\s]+)/(?<repo>[^\s.]+)}
20
+ BITBUCKET_SSH_RE = %r{bitbucket\.org[:/](?<org>[^/]+)/(?<repo>[^\s.]+)}
21
+ BITBUCKET_HTTPS_RE = %r{bitbucket\.org/(?<org>[^/\s]+)/(?<repo>[^\s.]+)}
22
+
23
+ attr_reader :conn
24
+
25
+ def self.type = :circleci
26
+
27
+ def self.description
28
+ "Monitors CircleCI pipelines for workflow status changes. " \
29
+ "Injects context when workflows fail or all pass on a branch. " \
30
+ "Requires a project_slug (e.g. gh/org/repo) and branch. " \
31
+ "Optionally accepts a token (or set CIRCLECI_TOKEN env var)."
32
+ end
33
+
34
+ def self.required_config = %i[project_slug branch]
35
+
36
+ def self.setup_label = "CircleCI"
37
+
38
+ def self.setup_config
39
+ <<~YAML
40
+ circleci:
41
+ token: <%= env("CIRCLECI_TOKEN") %>
42
+ # project_slug and branch are auto-detected from git remote.
43
+ # Uncomment to override:
44
+ # project_slug: gh/org/repo
45
+ # branch: main
46
+ YAML
47
+ end
48
+
49
+ def self.templates_dir
50
+ File.join(__dir__, "templates")
51
+ end
52
+
53
+ # Fill in missing project_slug and branch from the agent's environment snapshot.
54
+ def self.resolve_config(config, environment: {})
55
+ config[:project_slug] ||= parse_project_slug(environment[:git_remotes])
56
+ config[:branch] ||= environment[:git_branch]
57
+ config
58
+ end
59
+
60
+ # Extract CircleCI project slug from git remote URLs.
61
+ def self.parse_project_slug(remotes)
62
+ return nil unless remotes
63
+
64
+ remotes.each do |remote|
65
+ url = remote[:url].to_s
66
+ slug = slug_from_url(url)
67
+ return slug if slug
68
+ end
69
+
70
+ nil
71
+ end
72
+
73
+ private_class_method def self.slug_from_url(url)
74
+ if (m = GITHUB_SSH_RE.match(url) || GITHUB_HTTPS_RE.match(url))
75
+ "gh/#{m[:org]}/#{m[:repo].delete_suffix(".git")}"
76
+ elsif (m = BITBUCKET_SSH_RE.match(url) || BITBUCKET_HTTPS_RE.match(url))
77
+ "bb/#{m[:org]}/#{m[:repo].delete_suffix(".git")}"
78
+ end
79
+ end
80
+
81
+ def initialize(name:, config:, handler:, agent: nil, server_context: {}, connection: nil)
82
+ super(name:, config:, handler:, agent:, server_context:)
83
+ @conn = connection
84
+ end
85
+
86
+ def tick
87
+ project_slug = self[:project_slug]
88
+ branch = self[:branch]
89
+
90
+ check_ci(project_slug, branch)
91
+ end
92
+
93
+ def on_start
94
+ @conn ||= build_connection
95
+ end
96
+
97
+ private
98
+
99
+ def check_ci(project_slug, branch)
100
+ pipeline = latest_pipeline(project_slug, branch)
101
+ return unless pipeline
102
+
103
+ pipeline_id = pipeline["id"]
104
+ pipeline_number = pipeline["number"]
105
+
106
+ last_id = self[:last_pipeline_id]
107
+ last_status = self[:last_ci_status]
108
+
109
+ workflows = fetch_workflows(pipeline_id)
110
+ return if workflows.empty?
111
+
112
+ completed = workflows.select { TERMINAL_STATUSES.include?(it["status"]) }
113
+ return unless completed.size == workflows.size
114
+
115
+ current_status = (completed.all? { it["status"] == "success" }) ? "success" : "failure"
116
+
117
+ return if pipeline_id == last_id && current_status == last_status
118
+
119
+ @agent.set_monitor_field(@name, :last_pipeline_id, pipeline_id)
120
+ @agent.set_monitor_field(@name, :last_ci_status, current_status)
121
+ @config[:last_pipeline_id] = pipeline_id
122
+ @config[:last_ci_status] = current_status
123
+
124
+ failed_workflows = completed.reject { it["status"] == "success" }
125
+ failed_jobs = failed_workflows.flat_map { fetch_failed_jobs(it["id"]) }
126
+ event_type = (current_status == "success") ? :ci_success : :ci_failure
127
+
128
+ dispatch(
129
+ event_type:,
130
+ project_slug:,
131
+ branch:,
132
+ pipeline_id:,
133
+ pipeline_number:,
134
+ failed_workflows: failed_workflows.map { |w| w["name"] },
135
+ failed_jobs:,
136
+ workflow_count: workflows.size,
137
+ injection_supersede_key: "ci_status",
138
+ **((event_type == :ci_success) ? {injection_ttl: 60} : {})
139
+ )
140
+ end
141
+
142
+ def latest_pipeline(project_slug, branch)
143
+ resp = @conn.get("/api/v2/project/#{project_slug}/pipeline", branch: branch)
144
+ handle_response!(resp)
145
+ resp.body.dig("items")&.first
146
+ end
147
+
148
+ def fetch_workflows(pipeline_id)
149
+ resp = @conn.get("/api/v2/pipeline/#{pipeline_id}/workflow")
150
+ handle_response!(resp)
151
+ resp.body.fetch("items", [])
152
+ end
153
+
154
+ def fetch_failed_jobs(workflow_id)
155
+ resp = @conn.get("/api/v2/workflow/#{workflow_id}/job")
156
+ handle_response!(resp)
157
+ jobs = resp.body.fetch("items", [])
158
+ jobs.reject { |j| j["status"] == "success" }.map { |j| j["name"] }
159
+ end
160
+
161
+ def handle_response!(resp)
162
+ case resp.status
163
+ when 200..299
164
+ # ok
165
+ when 401
166
+ raise FatalError, "CircleCI auth failed (HTTP 401)"
167
+ when 429
168
+ raise RateLimited, "CircleCI rate limited (HTTP 429)"
169
+ when 404
170
+ Superkick.logger.warn(log_tag) { "CircleCI resource not found (HTTP 404)" }
171
+ end
172
+ end
173
+
174
+ def build_connection
175
+ token = self[:token] || ENV["CIRCLECI_TOKEN"]
176
+
177
+ Faraday.new(url: "https://circleci.com") do |f|
178
+ f.headers["Circle-Token"] = token if token
179
+ f.response :json
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superkick
4
+ module Integrations
5
+ module CircleCI
6
+ # Detects a CircleCI project from the agent's environment snapshot.
7
+ # Detection requires a .circleci/config.yml file to be present.
8
+ class Monitor::Probe < Superkick::Monitor::Probe
9
+ def self.type = :circleci
10
+
11
+ def self.description
12
+ "Detects CircleCI projects from .circleci/config.yml and infers project slug from git remote."
13
+ end
14
+
15
+ def self.environment_actions
16
+ [
17
+ {action: :git_branch},
18
+ {action: :git_remotes},
19
+ {action: :file_exists, paths: [".circleci/config.yml"]}
20
+ ]
21
+ end
22
+
23
+ # @param environment [Hash] environment data from the agent
24
+ # @return [Hash] { circleci: { type: "circleci", ... } } or {}
25
+ def self.detect(environment:)
26
+ return {} unless environment.dig(:file_exists, ".circleci/config.yml")
27
+
28
+ config = Monitor.resolve_config({}, environment:)
29
+ return {} unless config[:project_slug] && config[:branch]
30
+
31
+ {circleci: config.merge(type: "circleci")}
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,8 @@
1
+ SUPERKICK [{{ "now" | time }}]: CircleCI FAILED on {{ branch }} (pipeline #{{ pipeline_number }})
2
+ {% if failed_workflows.size > 0 -%}
3
+ Failed workflows: {{ failed_workflows | join: ", " }}
4
+ {% endif -%}
5
+ {% if failed_jobs.size > 0 -%}
6
+ Failed jobs: {{ failed_jobs | join: ", " }}
7
+ {% endif -%}
8
+ Please review the CircleCI output and fix the failures before continuing.
@@ -0,0 +1 @@
1
+ SUPERKICK [{{ "now" | time }}]: All {{ workflow_count }} CircleCI workflows passed on {{ branch }} (pipeline #{{ pipeline_number }}).
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "circleci/monitor"
4
+ require_relative "circleci/probe"
5
+
6
+ module Superkick
7
+ Monitor.register(Integrations::CircleCI::Monitor)
8
+ end
@@ -0,0 +1,253 @@
1
+ # Datadog Integration
2
+
3
+ Provides: **notifier**, **spawner** (error tracking + alerts), **monitor**, **goal**
4
+
5
+ ## Notifier (`:datadog`)
6
+
7
+ Sends events and metrics to Datadog via DogStatsD (UDP). See the
8
+ [Notifications section](../../../../README.md#notifications) in the main README
9
+ for notifier configuration.
10
+
11
+ ## Spawner (`:datadog`)
12
+
13
+ Watches Datadog Error Tracking for open error groups and spawns AI coding
14
+ agents to fix them. Polls the Datadog Error Tracking API for error groups
15
+ matching configurable filters (service, environment, source, minimum event
16
+ count).
17
+
18
+ Error groups are tracked by ID to prevent re-dispatching. Groups that fail
19
+ client-side filters (e.g. below `minimum_events`) are retried on subsequent
20
+ ticks so they fire once the threshold is crossed.
21
+
22
+ ### Configuration
23
+
24
+ ```yaml
25
+ spawners:
26
+ datadog:
27
+ type: datadog
28
+ site: datadoghq.com
29
+ api_key: <%= env("DD_API_KEY") %>
30
+ application_key: <%= env("DD_APP_KEY") %>
31
+ driver: claude_code
32
+ service: api
33
+ environment: production
34
+ source: ruby
35
+ minimum_events: 5
36
+ query: "version:2.0.0"
37
+ ```
38
+
39
+ | Key | Required | Default | Description |
40
+ |-----|----------|---------|-------------|
41
+ | `site` | no | `"datadoghq.com"` | Datadog site (e.g. `"datadoghq.eu"` for EU) |
42
+ | `api_key` | no | `$DD_API_KEY` | Datadog API key |
43
+ | `application_key` | no | `$DD_APP_KEY` | Datadog Application key |
44
+ | `service` | no | — | Filter to a specific service name |
45
+ | `environment` | no | `"production"` | Filter to an environment |
46
+ | `source` | no | — | Error source filter (e.g. `"ruby"`, `"python"`) |
47
+ | `minimum_events` | no | `1` | Minimum event count before dispatching |
48
+ | `query` | no | — | Raw Datadog search query string (appended to filters) |
49
+
50
+ ### Agent ID format
51
+
52
+ ```
53
+ datadog-error-{group_id}
54
+ ```
55
+
56
+ Where `group_id` is the Datadog error group ID. Dedup is handled by
57
+ `AgentStore` — if an agent with the same ID already exists, the spawn is
58
+ skipped.
59
+
60
+ ### Events
61
+
62
+ #### `error_opened`
63
+
64
+ Dispatched when a new open error group is found that passes all filters.
65
+
66
+ Template variables:
67
+
68
+ | Variable | Description |
69
+ |----------|-------------|
70
+ | `group_id` | Datadog error group ID |
71
+ | `error_class` | Error class name (e.g. `NoMethodError`) |
72
+ | `message` | Error message string |
73
+ | `status` | Error group status (e.g. `open`) |
74
+ | `service` | Service name |
75
+ | `environment` | Environment name |
76
+ | `first_seen` | Timestamp of first occurrence |
77
+ | `last_seen` | Timestamp of most recent occurrence |
78
+ | `events` | Total event count |
79
+ | `users` | Number of impacted users |
80
+ | `source` | Error source (e.g. `ruby`) |
81
+ | `url` | Datadog dashboard URL for this error group |
82
+
83
+ ### Error handling
84
+
85
+ - **401/403** — raises `FatalError`, stops the spawner (authentication failure)
86
+ - **429** — raises `RateLimited`, backs off per the standard poller backoff
87
+ - **404** — logged as warning, skipped (resource not found)
88
+ - Other HTTP errors — logged as warning, skipped
89
+
90
+ ## Alert Spawner (`:datadog_alerts`)
91
+
92
+ Watches Datadog monitors for triggered alerts and spawns AI coding agents to
93
+ triage them. Polls the Datadog Monitors Search API (`GET /api/v1/monitor/search`)
94
+ for monitors in an alerting state.
95
+
96
+ Monitors that recover (return to OK) are cleared from the seen set, so if they
97
+ re-alert later a new agent is spawned.
98
+
99
+ Pairs naturally with the `:datadog_alert_resolved` goal and `:datadog_alert`
100
+ monitor for a full triage lifecycle.
101
+
102
+ ### Configuration
103
+
104
+ ```yaml
105
+ spawners:
106
+ datadog_alerts:
107
+ type: datadog_alerts
108
+ site: datadoghq.com
109
+ api_key: <%= env("DD_API_KEY") %>
110
+ application_key: <%= env("DD_APP_KEY") %>
111
+ driver: claude_code
112
+ goal:
113
+ type: datadog_alert_resolved
114
+ max_duration: 3600
115
+ statuses:
116
+ - Alert
117
+ - Warn
118
+ tags:
119
+ - "team:backend"
120
+ - "env:production"
121
+ monitor_types:
122
+ - metric
123
+ - log
124
+ priority:
125
+ - 1
126
+ - 2
127
+ query: "scope:host:web-01"
128
+ ```
129
+
130
+ | Key | Required | Default | Description |
131
+ |-----|----------|---------|-------------|
132
+ | `site` | no | `"datadoghq.com"` | Datadog site (e.g. `"datadoghq.eu"` for EU) |
133
+ | `api_key` | no | `$DD_API_KEY` | Datadog API key |
134
+ | `application_key` | no | `$DD_APP_KEY` | Datadog Application key |
135
+ | `statuses` | no | `["Alert"]` | Array of monitor statuses to match |
136
+ | `tags` | no | — | Array of tags to filter (AND logic) |
137
+ | `monitor_types` | no | — | Array of monitor types to include (e.g. `metric`, `log`) |
138
+ | `priority` | no | — | Array of priority levels (1-5) |
139
+ | `query` | no | — | Raw Datadog search query string (appended to filters) |
140
+
141
+ ### Agent ID format
142
+
143
+ ```
144
+ datadog-alert-{monitor_id}
145
+ ```
146
+
147
+ ### Events
148
+
149
+ #### `alert_triggered`
150
+
151
+ Dispatched when a Datadog monitor enters an alerting state matching the
152
+ configured filters.
153
+
154
+ Template variables:
155
+
156
+ | Variable | Description |
157
+ |----------|-------------|
158
+ | `monitor_id` | Datadog monitor ID |
159
+ | `name` | Monitor name |
160
+ | `status` | Current status (e.g. `Alert`, `Warn`) |
161
+ | `alert_type` | Monitor type (e.g. `metric alert`, `log alert`) |
162
+ | `query` | Monitor query being evaluated |
163
+ | `message` | Notification message configured on the monitor |
164
+ | `tags` | Array of monitor tags |
165
+ | `priority` | Monitor priority (1-5, or nil) |
166
+ | `creator` | Name or email of the monitor creator |
167
+ | `url` | URL to the monitor in Datadog |
168
+
169
+ ## Alert Monitor (`:datadog_alert`)
170
+
171
+ Polls a specific Datadog monitor for status changes and injects events when the
172
+ alert state transitions. Records a baseline on the first tick and only dispatches
173
+ on subsequent state changes.
174
+
175
+ Typically auto-configured when using the alert spawner — the `monitor_id` is
176
+ passed through the spawn event context.
177
+
178
+ ### Configuration
179
+
180
+ ```yaml
181
+ monitors:
182
+ datadog_alert:
183
+ type: datadog_alert
184
+ monitor_id: 12345
185
+ site: datadoghq.com
186
+ api_key: <%= env("DD_API_KEY") %>
187
+ application_key: <%= env("DD_APP_KEY") %>
188
+ ```
189
+
190
+ | Key | Required | Default | Description |
191
+ |-----|----------|---------|-------------|
192
+ | `monitor_id` | yes | — | Datadog monitor ID to watch |
193
+ | `site` | no | `"datadoghq.com"` | Datadog site |
194
+ | `api_key` | no | `$DD_API_KEY` | Datadog API key |
195
+ | `application_key` | no | `$DD_APP_KEY` | Datadog Application key |
196
+
197
+ ### Events
198
+
199
+ #### `alert_recovered`
200
+
201
+ Dispatched when the monitor transitions to OK status.
202
+
203
+ #### `alert_escalated`
204
+
205
+ Dispatched when the monitor transitions from Warn to Alert.
206
+
207
+ #### `alert_changed`
208
+
209
+ Dispatched for any other status transition (e.g. OK → Warn, Alert → No Data).
210
+
211
+ All monitor events include these template variables:
212
+
213
+ | Variable | Description |
214
+ |----------|-------------|
215
+ | `monitor_id` | Datadog monitor ID |
216
+ | `name` | Monitor name |
217
+ | `status` | Current status after transition |
218
+ | `previous_status` | Status before transition |
219
+ | `alert_type` | Monitor type |
220
+ | `query` | Monitor query |
221
+ | `tags` | Array of monitor tags |
222
+ | `url` | URL to the monitor in Datadog |
223
+
224
+ ## Alert Goal (`:datadog_alert_resolved`)
225
+
226
+ Polls a Datadog monitor and completes when it returns to OK status. The
227
+ `monitor_id` is injected from the spawn event context via `AgentSpawner`.
228
+
229
+ ### Configuration
230
+
231
+ ```yaml
232
+ spawners:
233
+ datadog_alerts:
234
+ type: datadog_alerts
235
+ goal:
236
+ type: datadog_alert_resolved
237
+ ```
238
+
239
+ | Key | Required | Default | Description |
240
+ |-----|----------|---------|-------------|
241
+ | `monitor_id` | no | — | Injected from spawn event context |
242
+ | `site` | no | `"datadoghq.com"` | Datadog site |
243
+ | `api_key` | no | `$DD_API_KEY` | Datadog API key |
244
+ | `application_key` | no | `$DD_APP_KEY` | Datadog Application key |
245
+
246
+ ### Status mapping
247
+
248
+ | Monitor state | Goal status |
249
+ |---------------|-------------|
250
+ | OK | `:completed` |
251
+ | Alert, Warn | `:in_progress` |
252
+ | No Data | `:pending` |
253
+ | API error | `:errored` |
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+
5
+ module Superkick
6
+ module Integrations
7
+ module Datadog
8
+ # Goal that checks whether a Datadog monitor has recovered (returned to OK).
9
+ #
10
+ # Polls the Datadog Monitor API for the specified monitor's status.
11
+ # Completes when the monitor reaches OK status; stays in_progress while
12
+ # it remains in an alerting state.
13
+ #
14
+ # Configuration:
15
+ # goal:
16
+ # type: datadog_alert_resolved
17
+ # monitor_id: 12345 # injected from spawn event context
18
+ # site: datadoghq.com # optional
19
+ # api_key: xxx # optional, falls back to DD_API_KEY
20
+ # application_key: xxx # optional, falls back to DD_APP_KEY
21
+ #
22
+ # The monitor_id is injected from the spawn event context via AgentSpawner.
23
+ class AlertResolvedGoal < Superkick::Goal
24
+ DEFAULT_SITE = "datadoghq.com"
25
+
26
+ def self.type = :datadog_alert_resolved
27
+
28
+ def self.description
29
+ "Polls a Datadog monitor and completes when it returns to OK status. " \
30
+ "The monitor_id is injected from the spawn event context."
31
+ end
32
+
33
+ def self.required_config = %i[]
34
+
35
+ def check
36
+ monitor_id = config[:monitor_id]
37
+ return :pending unless monitor_id
38
+
39
+ resp = connection.get("/api/v1/monitor/#{monitor_id}")
40
+
41
+ case resp.status
42
+ when 200..299
43
+ status = resp.body["overall_state"].to_s
44
+ case status
45
+ when "OK" then :completed
46
+ when "Alert", "Warn" then :in_progress
47
+ when "No Data" then :pending
48
+ else :in_progress
49
+ end
50
+ when 401, 403
51
+ Superkick.logger.error("goal:datadog_alert_resolved") { "Auth failed (HTTP #{resp.status}) for #{agent_id}" }
52
+ :errored
53
+ when 429
54
+ Superkick.logger.warn("goal:datadog_alert_resolved") { "Rate limited for #{agent_id}" }
55
+ :errored
56
+ when 404
57
+ Superkick.logger.warn("goal:datadog_alert_resolved") { "Monitor #{monitor_id} not found for #{agent_id}" }
58
+ :errored
59
+ else
60
+ Superkick.logger.warn("goal:datadog_alert_resolved") { "HTTP #{resp.status} for #{agent_id}" }
61
+ :errored
62
+ end
63
+ rescue Faraday::Error => e
64
+ Superkick.logger.error("goal:datadog_alert_resolved") { "Check failed for #{agent_id}: #{e.message}" }
65
+ :errored
66
+ end
67
+
68
+ def teardown
69
+ @connection = nil
70
+ end
71
+
72
+ private
73
+
74
+ def connection
75
+ @connection ||= build_connection
76
+ end
77
+
78
+ def build_connection
79
+ site = config[:site] || DEFAULT_SITE
80
+ api_key = config[:api_key] || ENV["DD_API_KEY"]
81
+ app_key = config[:application_key] || ENV["DD_APP_KEY"]
82
+
83
+ Faraday.new(url: "https://api.#{site}") do |f|
84
+ f.headers["DD-API-KEY"] = api_key if api_key
85
+ f.headers["DD-APPLICATION-KEY"] = app_key if app_key
86
+ f.response :json
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ Superkick::Goal.register(Superkick::Integrations::Datadog::AlertResolvedGoal)