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,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
|
|
5
|
+
module Superkick
|
|
6
|
+
module Integrations
|
|
7
|
+
module Honeybadger
|
|
8
|
+
# Watches Honeybadger for unresolved faults and spawns AI coding agents to
|
|
9
|
+
# fix them.
|
|
10
|
+
#
|
|
11
|
+
# Polls the Honeybadger Faults API for faults matching configurable filters
|
|
12
|
+
# (environment, fault class, minimum occurrence count). Tracks dispatched
|
|
13
|
+
# fault IDs in-memory to avoid re-dispatching. Faults that fail client-side
|
|
14
|
+
# filters (e.g. below minimum_occurrences) are retried on subsequent ticks
|
|
15
|
+
# so they fire once the threshold is crossed.
|
|
16
|
+
#
|
|
17
|
+
# Config keys:
|
|
18
|
+
# project_id (required) — Honeybadger project ID
|
|
19
|
+
# token (optional) — API auth token, falls back to $HONEYBADGER_TOKEN
|
|
20
|
+
# environment (optional) — environment name, default "production"
|
|
21
|
+
# minimum_occurrences (optional) — minimum occurrence count to dispatch, default 1
|
|
22
|
+
# fault_classes (optional) — array or include/exclude hash
|
|
23
|
+
class Spawner < Superkick::Spawner
|
|
24
|
+
attr_reader :conn
|
|
25
|
+
|
|
26
|
+
API_BASE = "https://app.honeybadger.io"
|
|
27
|
+
DEFAULT_ENVIRONMENT = "production"
|
|
28
|
+
DEFAULT_MINIMUM_OCCURRENCES = 1
|
|
29
|
+
PER_PAGE = 25
|
|
30
|
+
|
|
31
|
+
def self.type = :honeybadger
|
|
32
|
+
|
|
33
|
+
def self.description
|
|
34
|
+
"Watches Honeybadger for unresolved faults and spawns AI coding " \
|
|
35
|
+
"agents to fix them. Supports filtering by environment, fault " \
|
|
36
|
+
"class, and minimum occurrence count."
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.required_config = %i[project_id]
|
|
40
|
+
|
|
41
|
+
def self.spawn_templates_dir
|
|
42
|
+
File.join(__dir__, "templates")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.agent_id(event)
|
|
46
|
+
"honeybadger-fault-#{event[:fault_id]}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.setup_label = "Honeybadger"
|
|
50
|
+
|
|
51
|
+
def self.setup_config
|
|
52
|
+
<<~YAML
|
|
53
|
+
honeybadger:
|
|
54
|
+
type: honeybadger
|
|
55
|
+
project_id: 12345
|
|
56
|
+
token: <%= env("HONEYBADGER_TOKEN") %>
|
|
57
|
+
# environment: production # default
|
|
58
|
+
# minimum_occurrences: 1 # minimum occurrences before spawning
|
|
59
|
+
# max_duration: 3600
|
|
60
|
+
YAML
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def initialize(name:, config:, handler:, connection: nil)
|
|
64
|
+
super(name:, config:, handler:)
|
|
65
|
+
@conn = connection
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def tick
|
|
69
|
+
faults = fetch_faults
|
|
70
|
+
faults.each do |fault|
|
|
71
|
+
next if filtered_by_fault_class?(fault)
|
|
72
|
+
next if filtered_by_minimum_occurrences?(fault)
|
|
73
|
+
|
|
74
|
+
@seen_fault_ids.add(fault["id"])
|
|
75
|
+
|
|
76
|
+
dispatch(
|
|
77
|
+
event_type: :error_opened,
|
|
78
|
+
fault_id: fault["id"],
|
|
79
|
+
error_class: fault["klass"].to_s,
|
|
80
|
+
message: fault["message"].to_s,
|
|
81
|
+
environment: fault["environment"].to_s,
|
|
82
|
+
events: fault["notices_count"] || 0,
|
|
83
|
+
users: fault["impacted_users_count"],
|
|
84
|
+
first_seen: fault["created_at"].to_s,
|
|
85
|
+
last_seen: fault["last_notice_at"].to_s,
|
|
86
|
+
component: fault["component"].to_s,
|
|
87
|
+
action: fault["action"].to_s,
|
|
88
|
+
assignee: fault["assignee"],
|
|
89
|
+
url: fault_url(fault["id"]),
|
|
90
|
+
project_id: self[:project_id]
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def on_start
|
|
96
|
+
@conn ||= build_connection
|
|
97
|
+
@seen_fault_ids = Set.new
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
# -- Honeybadger Faults API -----------------------------------------------
|
|
103
|
+
|
|
104
|
+
def fetch_faults
|
|
105
|
+
project_id = self[:project_id]
|
|
106
|
+
params = build_params
|
|
107
|
+
|
|
108
|
+
path = "/v2/projects/#{project_id}/faults"
|
|
109
|
+
Superkick.logger.debug(log_tag) { "Fetching faults: #{path} #{params.inspect}" }
|
|
110
|
+
|
|
111
|
+
resp = get(path, params)
|
|
112
|
+
return [] unless resp
|
|
113
|
+
|
|
114
|
+
body = resp.body
|
|
115
|
+
faults = body["results"]
|
|
116
|
+
return [] unless faults.is_a?(Array)
|
|
117
|
+
|
|
118
|
+
new_faults = faults.reject { @seen_fault_ids.include?(it["id"]) }
|
|
119
|
+
|
|
120
|
+
Superkick.logger.info(log_tag) { "Found #{faults.size} faults, #{new_faults.size} new" }
|
|
121
|
+
new_faults
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def build_params
|
|
125
|
+
params = {
|
|
126
|
+
"order" => "recent",
|
|
127
|
+
"resolved" => "false",
|
|
128
|
+
"ignored" => "false",
|
|
129
|
+
"per_page" => PER_PAGE.to_s
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
env = self[:environment]
|
|
133
|
+
env = DEFAULT_ENVIRONMENT if env.nil?
|
|
134
|
+
params["environment"] = env if env && !env.to_s.empty?
|
|
135
|
+
|
|
136
|
+
# Server-side class filter (include only)
|
|
137
|
+
fc = normalize_filter(self[:fault_classes])
|
|
138
|
+
if fc[:include].any?
|
|
139
|
+
params["q"] = fc[:include].join(" ")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
params
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# -- Client-side filters --------------------------------------------------
|
|
146
|
+
|
|
147
|
+
def filtered_by_fault_class?(fault)
|
|
148
|
+
excludes = normalize_filter(self[:fault_classes])[:exclude]
|
|
149
|
+
return false unless excludes.any?
|
|
150
|
+
|
|
151
|
+
excludes.include?(fault["klass"].to_s)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def filtered_by_minimum_occurrences?(fault)
|
|
155
|
+
min = self[:minimum_occurrences] || DEFAULT_MINIMUM_OCCURRENCES
|
|
156
|
+
count = fault["notices_count"] || 0
|
|
157
|
+
count < min
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# -- Filter normalization -------------------------------------------------
|
|
161
|
+
|
|
162
|
+
def normalize_filter(value, default_include: [])
|
|
163
|
+
case value
|
|
164
|
+
when Array
|
|
165
|
+
{include: value, exclude: []}
|
|
166
|
+
when Hash
|
|
167
|
+
{include: Array(value[:include]), exclude: Array(value[:exclude])}
|
|
168
|
+
else
|
|
169
|
+
{include: default_include, exclude: []}
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# -- URL helpers ----------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
def fault_url(fault_id)
|
|
176
|
+
"https://app.honeybadger.io/projects/#{self[:project_id]}/faults/#{fault_id}"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# -- HTTP -----------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
def get(path, params = {})
|
|
182
|
+
resp = @conn.get(path) do |req|
|
|
183
|
+
params.each { |k, v| req.params[k] = v }
|
|
184
|
+
end
|
|
185
|
+
return nil unless handle_response!(resp)
|
|
186
|
+
resp
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def handle_response!(resp)
|
|
190
|
+
case resp.status
|
|
191
|
+
when 200..299 then true
|
|
192
|
+
when 401, 403 then raise FatalError, "Honeybadger auth failed (HTTP #{resp.status})"
|
|
193
|
+
when 429 then raise RateLimited, "Honeybadger rate limited"
|
|
194
|
+
when 404
|
|
195
|
+
Superkick.logger.warn(log_tag) { "Honeybadger 404: resource not found" }
|
|
196
|
+
false
|
|
197
|
+
else
|
|
198
|
+
Superkick.logger.warn(log_tag) { "Honeybadger HTTP #{resp.status}" }
|
|
199
|
+
false
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def build_connection
|
|
204
|
+
token = self[:token] || ENV["HONEYBADGER_TOKEN"]
|
|
205
|
+
|
|
206
|
+
Faraday.new(url: API_BASE) do |f|
|
|
207
|
+
f.request :authorization, :basic, token, "" if token
|
|
208
|
+
f.response :json
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
SUPERKICK [{{ "now" | time }}]: Honeybadger fault — {{ error_class }}
|
|
2
|
+
{% if url -%}
|
|
3
|
+
URL: {{ url }}
|
|
4
|
+
{% endif -%}
|
|
5
|
+
{% unless environment == "" %}Env: {{ environment }} | {% endunless %}Occurrences: {{ events }}{% if users %} | Users: {{ users }}{% endif %}
|
|
6
|
+
{% unless component == "" and action == "" -%}
|
|
7
|
+
Component: {{ component }}{% unless action == "" %}#{{ action }}{% endunless %}
|
|
8
|
+
{% endunless -%}
|
|
9
|
+
First seen: {{ first_seen }} | Last seen: {{ last_seen }}
|
|
10
|
+
|
|
11
|
+
## Error
|
|
12
|
+
|
|
13
|
+
**{{ error_class }}**: {% if message == "" %}(no message){% else %}{{ message }}{% endif %}
|
|
14
|
+
|
|
15
|
+
Please investigate this fault, identify the root cause, implement a fix, and
|
|
16
|
+
write tests to prevent regression. Check the Honeybadger dashboard for full
|
|
17
|
+
stack traces and contextual data.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Shell Monitor
|
|
2
|
+
|
|
3
|
+
Type: `:shell`
|
|
4
|
+
|
|
5
|
+
Runs a shell command on each poll interval and dispatches events based on exit
|
|
6
|
+
status. Designed for custom health checks, linters, or any script the user
|
|
7
|
+
wants Superkick to watch.
|
|
8
|
+
|
|
9
|
+
Supports multiple named instances — each with its own command, working
|
|
10
|
+
directory, and timeout.
|
|
11
|
+
|
|
12
|
+
## Configuration
|
|
13
|
+
|
|
14
|
+
```yaml
|
|
15
|
+
monitors:
|
|
16
|
+
disk-check:
|
|
17
|
+
type: shell
|
|
18
|
+
command: ./scripts/check-disk.sh
|
|
19
|
+
timeout: 10
|
|
20
|
+
lint:
|
|
21
|
+
type: shell
|
|
22
|
+
command: bundle exec standardrb --no-fix
|
|
23
|
+
working_dir: /home/user/myproject
|
|
24
|
+
report_success: true
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
| Key | Required | Default | Description |
|
|
28
|
+
|-----|----------|---------|-------------|
|
|
29
|
+
| `command` | yes | — | Shell command string to execute |
|
|
30
|
+
| `working_dir` | no | inherited | Working directory for the command |
|
|
31
|
+
| `timeout` | no | `30` | Seconds before the command is killed with SIGTERM |
|
|
32
|
+
| `report_success` | no | `false` | Dispatch `shell_success` on zero exit (by default only failures are reported) |
|
|
33
|
+
|
|
34
|
+
The `type: shell` key is required when the monitor name doesn't match the
|
|
35
|
+
type (i.e. for all instances except one literally named `"shell"`).
|
|
36
|
+
|
|
37
|
+
## Probe
|
|
38
|
+
|
|
39
|
+
`ShellMonitor::Probe` scans `.superkick/shell/` in the working directory for
|
|
40
|
+
executable files. Each script gets a monitor named `shell-<basename>` (without
|
|
41
|
+
extension).
|
|
42
|
+
|
|
43
|
+
Example layout:
|
|
44
|
+
```
|
|
45
|
+
.superkick/shell/
|
|
46
|
+
check-disk.sh (executable)
|
|
47
|
+
lint.sh (executable)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Produces:
|
|
51
|
+
```ruby
|
|
52
|
+
{
|
|
53
|
+
"shell-check-disk" => { "type" => "shell", "command" => "/abs/path/.superkick/shell/check-disk.sh" },
|
|
54
|
+
"shell-lint" => { "type" => "shell", "command" => "/abs/path/.superkick/shell/lint.sh" }
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Returns `{}` if the directory doesn't exist or contains no executable files.
|
|
59
|
+
|
|
60
|
+
## Events
|
|
61
|
+
|
|
62
|
+
### `shell_alert`
|
|
63
|
+
|
|
64
|
+
Dispatched when the command exits with a non-zero status.
|
|
65
|
+
|
|
66
|
+
Template variables:
|
|
67
|
+
- `command` — the command string that was run
|
|
68
|
+
- `output` — combined stdout + stderr (captured, may be empty)
|
|
69
|
+
- `exit_code` — integer exit code
|
|
70
|
+
|
|
71
|
+
### `shell_success`
|
|
72
|
+
|
|
73
|
+
Dispatched when the command exits with status 0 **and** `report_success` is
|
|
74
|
+
enabled. Disabled by default to avoid noise.
|
|
75
|
+
|
|
76
|
+
Template variables: same as `shell_alert`.
|
|
77
|
+
|
|
78
|
+
## Error handling
|
|
79
|
+
|
|
80
|
+
- Command timeout → SIGTERM sent, output captured up to that point, logged as
|
|
81
|
+
warning. The tick continues (no event dispatched for the timeout itself).
|
|
82
|
+
- `SystemCallError` / `IOError` → logged, error message returned as output
|
|
83
|
+
with `nil` status.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superkick
|
|
4
|
+
module Integrations
|
|
5
|
+
module Shell
|
|
6
|
+
# Runs a shell command each tick and dispatches events based on exit status.
|
|
7
|
+
#
|
|
8
|
+
# Required config keys: command
|
|
9
|
+
# Optional config keys: working_dir, timeout, report_success
|
|
10
|
+
#
|
|
11
|
+
# Events emitted: shell_alert (non-zero exit), shell_success (zero exit)
|
|
12
|
+
class Monitor < Superkick::Monitor
|
|
13
|
+
def self.type = :shell
|
|
14
|
+
|
|
15
|
+
def self.description
|
|
16
|
+
"Runs a shell command on each poll interval and dispatches events based on exit status. " \
|
|
17
|
+
"Alerts on non-zero exit codes; optionally reports successes. " \
|
|
18
|
+
"Requires a command string. Optional: working_dir, timeout (default 30s), report_success (bool)."
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.required_config = %i[command]
|
|
22
|
+
|
|
23
|
+
def self.setup_label = "Shell"
|
|
24
|
+
|
|
25
|
+
def self.setup_config
|
|
26
|
+
<<~YAML
|
|
27
|
+
my_check:
|
|
28
|
+
type: shell
|
|
29
|
+
command: .superkick/shell/my-check
|
|
30
|
+
# timeout: 30 # seconds before command is killed
|
|
31
|
+
# report_success: false # also dispatch on exit code 0
|
|
32
|
+
# working_dir: /path/to/dir # working directory for command
|
|
33
|
+
YAML
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
SHELL_DIR = ".superkick/shell"
|
|
37
|
+
|
|
38
|
+
# Resolve bare command names relative to .superkick/shell/.
|
|
39
|
+
# Absolute paths and explicit relative paths (starting with ./ or ../)
|
|
40
|
+
# are left unchanged.
|
|
41
|
+
def self.resolve_config(config, environment: {})
|
|
42
|
+
command = config[:command]
|
|
43
|
+
if command && !command.include?("/")
|
|
44
|
+
config[:command] = File.join(SHELL_DIR, command)
|
|
45
|
+
end
|
|
46
|
+
config
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.templates_dir
|
|
50
|
+
File.join(__dir__, "templates")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def tick
|
|
54
|
+
output, status = run_command
|
|
55
|
+
return if output.nil? && status.nil?
|
|
56
|
+
|
|
57
|
+
success = status&.success?
|
|
58
|
+
return if success && !self[:report_success]
|
|
59
|
+
|
|
60
|
+
dispatch(
|
|
61
|
+
event_type: success ? :shell_success : :shell_alert,
|
|
62
|
+
command: self[:command],
|
|
63
|
+
output: output.to_s.strip,
|
|
64
|
+
exit_code: status&.exitstatus,
|
|
65
|
+
**(success ? {injection_ttl: 60} : {})
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def run_command
|
|
72
|
+
cmd = self[:command]
|
|
73
|
+
dir = self[:working_dir]
|
|
74
|
+
timeout = self[:timeout]&.to_i || 30
|
|
75
|
+
|
|
76
|
+
result = ProcessRunner.run(cmd, timeout: timeout, chdir: dir)
|
|
77
|
+
|
|
78
|
+
if result[:timed_out]
|
|
79
|
+
Superkick.logger.warn(log_tag) { "Command timed out after #{timeout}s: #{cmd}" }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
[result[:output], result[:status]]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Shortcut Monitor
|
|
2
|
+
|
|
3
|
+
Type: `:shortcut`
|
|
4
|
+
|
|
5
|
+
Polls [Shortcut](https://shortcut.com) (formerly Clubhouse) for story-level
|
|
6
|
+
changes. Watches a primary story plus its linked stories and epic siblings for
|
|
7
|
+
state changes, new comments, blockers, owner changes, and description updates.
|
|
8
|
+
|
|
9
|
+
## Configuration
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
monitors:
|
|
13
|
+
shortcut:
|
|
14
|
+
token: <%= env("SHORTCUT_API_TOKEN") %>
|
|
15
|
+
workspace_slug: my-company
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| Key | Required | Default | Description |
|
|
19
|
+
|-----|----------|---------|-------------|
|
|
20
|
+
| `token` | no | `SHORTCUT_API_TOKEN` env var | API token for authentication |
|
|
21
|
+
| `workspace_slug` | no | — | Workspace slug, used to build web URLs in templates |
|
|
22
|
+
| `member` | no | auto-detected | UUID, email, or mention name to identify the current user for self-action filtering |
|
|
23
|
+
| `ignore_self` | no | `true` | Skip events caused by the current user (the API token owner) |
|
|
24
|
+
|
|
25
|
+
The `story_id` is auto-detected by the probe from the current git branch's
|
|
26
|
+
`sc-XXXXX` pattern — it is not a YAML config key.
|
|
27
|
+
|
|
28
|
+
## Probe
|
|
29
|
+
|
|
30
|
+
`ShortcutMonitor::Probe` reads the current branch via `git symbolic-ref` and
|
|
31
|
+
matches against the regex `/sc-(\d+)/`. This covers common Shortcut branch
|
|
32
|
+
patterns:
|
|
33
|
+
|
|
34
|
+
- `user/sc-12345/my-feature`
|
|
35
|
+
- `sc-12345/fix-bug`
|
|
36
|
+
- `sc-12345`
|
|
37
|
+
|
|
38
|
+
Returns `{ "shortcut" => { "type" => "shortcut", "story_id" => "12345" } }` on
|
|
39
|
+
match, or `{}` if the branch doesn't contain an `sc-XXXXX` pattern.
|
|
40
|
+
|
|
41
|
+
The probe cannot detect the API token — that must come from YAML config or the
|
|
42
|
+
`SHORTCUT_API_TOKEN` environment variable.
|
|
43
|
+
|
|
44
|
+
## Events
|
|
45
|
+
|
|
46
|
+
### `story_state_changed`
|
|
47
|
+
|
|
48
|
+
Dispatched when the story's `workflow_state_id` changes between ticks. Resolves
|
|
49
|
+
state IDs to human-readable names via the cached workflow map.
|
|
50
|
+
|
|
51
|
+
Template variables:
|
|
52
|
+
- `story` — `StoryDrop` with `.id`, `.name`, `.url`, `.ref` (formatted as `sc-123 "title"`)
|
|
53
|
+
- `old_state` — previous workflow state name
|
|
54
|
+
- `new_state` — current workflow state name
|
|
55
|
+
- `new_state_type` — state category (`"backlog"`, `"unstarted"`, `"started"`, `"done"`)
|
|
56
|
+
- `actor` — `MemberDrop` or nil with `.display_name`, `.mention_name`, `.tag`
|
|
57
|
+
|
|
58
|
+
### `story_comment`
|
|
59
|
+
|
|
60
|
+
Dispatched for each new comment on the tracked story. Uses a `last_comment_id`
|
|
61
|
+
watermark to avoid re-reporting.
|
|
62
|
+
|
|
63
|
+
Template variables:
|
|
64
|
+
- `story` — `StoryDrop`
|
|
65
|
+
- `author` — `MemberDrop` for the comment author
|
|
66
|
+
- `body` — comment text
|
|
67
|
+
|
|
68
|
+
### `story_blocker`
|
|
69
|
+
|
|
70
|
+
Dispatched when the story's `blocker` field changes from `false` to `true`.
|
|
71
|
+
|
|
72
|
+
Template variables:
|
|
73
|
+
- `story` — `StoryDrop`
|
|
74
|
+
- `blocker_reason` — text of the latest blocker comment (if any)
|
|
75
|
+
- `actor` — `MemberDrop` or nil
|
|
76
|
+
|
|
77
|
+
### `story_unblocked`
|
|
78
|
+
|
|
79
|
+
Dispatched when the story's `blocker` field changes from `true` to `false`.
|
|
80
|
+
|
|
81
|
+
Template variables:
|
|
82
|
+
- `story` — `StoryDrop`
|
|
83
|
+
- `actor` — `MemberDrop` or nil
|
|
84
|
+
|
|
85
|
+
### `story_owner_changed`
|
|
86
|
+
|
|
87
|
+
Dispatched when the story's `owner_ids` array changes. Resolves member UUIDs
|
|
88
|
+
to display names via the cached member map.
|
|
89
|
+
|
|
90
|
+
Template variables:
|
|
91
|
+
- `story` — `StoryDrop`
|
|
92
|
+
- `added_owners` — array of `MemberDrop` for newly added owners
|
|
93
|
+
- `removed_owners` — array of `MemberDrop` for removed owners
|
|
94
|
+
- `actor` — `MemberDrop` or nil
|
|
95
|
+
|
|
96
|
+
### `story_description_changed`
|
|
97
|
+
|
|
98
|
+
Dispatched when the SHA-256 hash of `name + description` changes, indicating
|
|
99
|
+
the story title or description was edited. Includes the full before and after
|
|
100
|
+
text so the AI agent can identify exactly what changed.
|
|
101
|
+
|
|
102
|
+
Template variables:
|
|
103
|
+
- `story` — `StoryDrop`
|
|
104
|
+
- `old_name` — previous story title
|
|
105
|
+
- `new_name` — current story title
|
|
106
|
+
- `old_description` — previous story description body
|
|
107
|
+
- `new_description` — current story description body
|
|
108
|
+
- `actor` — `MemberDrop` or nil
|
|
109
|
+
|
|
110
|
+
### `related_story_changed`
|
|
111
|
+
|
|
112
|
+
Dispatched when a linked story or an epic sibling story changes workflow state.
|
|
113
|
+
Related stories are discovered from the primary story's `story_links` and
|
|
114
|
+
`epic_id` fields, capped at 10 to manage API budget.
|
|
115
|
+
|
|
116
|
+
Template variables:
|
|
117
|
+
- `story` — `StoryDrop` for the related story
|
|
118
|
+
- `old_state`, `new_state` — workflow state transition
|
|
119
|
+
- `primary_story` — `StoryDrop` for the primary tracked story
|
|
120
|
+
- `relationship` — array of directed verbs (e.g., `["blocks"]`, `["is_blocked_by"]`)
|
|
121
|
+
- `epic_sibling` — boolean
|
|
122
|
+
|
|
123
|
+
## Self-action filtering
|
|
124
|
+
|
|
125
|
+
By default (`ignore_self: true`), the monitor skips events caused by the
|
|
126
|
+
current user — the person driving the AI agent. This prevents
|
|
127
|
+
redundant injections when you move your own story through workflow states,
|
|
128
|
+
post comments, or toggle blockers.
|
|
129
|
+
|
|
130
|
+
**How it works:**
|
|
131
|
+
|
|
132
|
+
1. On startup, the monitor calls `GET /api/v3/member` to discover the
|
|
133
|
+
authenticated user's member ID. This can be overridden with the `member`
|
|
134
|
+
config key — accepts a UUID, email address, or mention name (useful when
|
|
135
|
+
using a shared/service API token).
|
|
136
|
+
|
|
137
|
+
2. For **comments**, the monitor compares `comment.author_id` against the
|
|
138
|
+
current member ID. Self-authored comments are silently dropped.
|
|
139
|
+
|
|
140
|
+
3. For **state changes, blockers, owner changes, and description changes**,
|
|
141
|
+
the monitor fetches `GET /api/v3/stories/{id}/history` when a change is
|
|
142
|
+
detected and checks the `member_id` on the most recent history entry. If
|
|
143
|
+
the actor is the current user, the event is skipped.
|
|
144
|
+
|
|
145
|
+
4. **Watermarks are always advanced** regardless of filtering — a skipped
|
|
146
|
+
self-action won't be re-detected on the next tick.
|
|
147
|
+
|
|
148
|
+
5. If the history endpoint is unavailable or returns an error, the monitor
|
|
149
|
+
falls back to dispatching the event (fail-open).
|
|
150
|
+
|
|
151
|
+
Set `ignore_self: false` to disable this filtering entirely.
|
|
152
|
+
|
|
153
|
+
## Watermarks
|
|
154
|
+
|
|
155
|
+
The monitor persists these fields on the agent to avoid duplicate events
|
|
156
|
+
across ticks:
|
|
157
|
+
|
|
158
|
+
- `last_state_id` — last seen `workflow_state_id`
|
|
159
|
+
- `last_comment_id` — highest seen comment ID
|
|
160
|
+
- `last_blocker` — last seen `blocker` boolean
|
|
161
|
+
- `last_owner_ids` — JSON-encoded array of last seen `owner_ids`
|
|
162
|
+
- `last_description_hash` — SHA-256 of `name + description`
|
|
163
|
+
|
|
164
|
+
Related story states are tracked in memory (not persisted) — they reset when
|
|
165
|
+
the monitor restarts.
|
|
166
|
+
|
|
167
|
+
## Caching
|
|
168
|
+
|
|
169
|
+
The monitor caches two reference datasets in `on_start`:
|
|
170
|
+
|
|
171
|
+
- **Workflow states** — fetched from `GET /api/v3/workflows`, maps state IDs to
|
|
172
|
+
names and types. Refreshed on cache miss.
|
|
173
|
+
- **Members** — fetched from `GET /api/v3/members`, maps member UUIDs to mention
|
|
174
|
+
names. Refreshed on cache miss.
|
|
175
|
+
|
|
176
|
+
## API usage
|
|
177
|
+
|
|
178
|
+
The monitor uses the [Shortcut REST API v3](https://developer.shortcut.com/api/rest/v3)
|
|
179
|
+
via Faraday with the `Shortcut-Token` header for authentication. Rate limit is
|
|
180
|
+
200 requests/minute.
|
|
181
|
+
|
|
182
|
+
Per-tick API budget:
|
|
183
|
+
- 1 request for the primary story
|
|
184
|
+
- 1 request per related story being tracked (up to 10)
|
|
185
|
+
- 1 request for `GET /api/v3/member` on startup (once)
|
|
186
|
+
- 1 request for story history per detected change (only when `ignore_self` is
|
|
187
|
+
enabled and a change is found — typically 0 per tick)
|
|
188
|
+
|
|
189
|
+
## Error handling
|
|
190
|
+
|
|
191
|
+
- HTTP 401/403 → `FatalError` (bad token or no access — stops the monitor)
|
|
192
|
+
- HTTP 429 → `RateLimited` (backs off by `rate_limit_backoff`)
|
|
193
|
+
- HTTP 404 → logged and skipped (story may have been deleted or archived)
|