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