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,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superkick
4
+ module Spawn
5
+ # WorkflowExecutor — resolves workflow config, builds enriched events, handles
6
+ # VCS ownership transfer, enforces iteration limits, and dispatches
7
+ # workflow spawns.
8
+ #
9
+ # Called by Supervisor when a spawned agent's goal reaches a terminal status
10
+ # and the spawner config has `on_complete:` or `on_fail:`.
11
+ class WorkflowExecutor
12
+ def initialize(store:, agent_spawner:, notification_dispatcher:, config: Superkick.config)
13
+ @store = store
14
+ @agent_spawner = agent_spawner
15
+ @notification_dispatcher = notification_dispatcher
16
+ @config = config
17
+ end
18
+
19
+ # Fire a workflow spawn if configured.
20
+ # @param agent_id [String] the parent agent that just finished
21
+ # @param goal_status [Symbol] :completed or :failed
22
+ # @param spawner_config [Hash] the parent spawner's config
23
+ def fire(agent_id:, goal_status:, spawner_config:)
24
+ hook_key = case goal_status
25
+ when :completed then :on_complete
26
+ when :failed then :on_fail
27
+ else return
28
+ end
29
+
30
+ hook_config = spawner_config[hook_key]
31
+ return unless hook_config
32
+
33
+ agent = @store.get(agent_id)
34
+ current_depth = agent&.spawn_info&.dig(:workflow_depth) || 0
35
+ parent_iterations = agent&.spawn_info&.dig(:workflow_iterations) || {}
36
+
37
+ resolved = resolve_workflow_config(hook_config, spawner_config)
38
+ return unless resolved
39
+
40
+ # Check iteration limit for the target spawner
41
+ target_name = resolved[:name]&.to_sym
42
+ if target_name
43
+ current_count = parent_iterations[target_name] || 0
44
+ max = resolved[:max_iterations]
45
+ if max && current_count >= max
46
+ Superkick.logger.warn("workflow_executor") {
47
+ "Max iterations reached for #{target_name} (#{current_count}/#{max}) in chain for #{agent_id} — not spawning"
48
+ }
49
+ dispatch_notification(
50
+ event: {event_type: :workflow_iterations_exceeded, monitor_type: :spawner,
51
+ monitor_name: spawner_config[:name] || :system,
52
+ goal_status:},
53
+ agent_id:,
54
+ message: "Max iterations reached for #{target_name} (#{current_count}/#{max}) in chain for #{agent_id}"
55
+ )
56
+ return
57
+ end
58
+ end
59
+
60
+ workflow_event = build_workflow_event(
61
+ agent_id:, goal_status:, spawner_config:,
62
+ workflow_config: resolved, depth: current_depth,
63
+ parent_iterations:
64
+ )
65
+
66
+ # VCS ownership transfer for inherit case
67
+ if hook_config[:repository].nil?
68
+ vcs_state = @agent_spawner.take_vcs_state(agent_id)
69
+
70
+ if vcs_state
71
+ workflow_event[:inherited_vcs_state] = vcs_state
72
+ workflow_event[:working_dir] = vcs_state[:destination] ||
73
+ agent&.spawn_info&.dig(:working_dir)
74
+ else
75
+ # No VCS state to transfer — pass parent working dir
76
+ workflow_event[:working_dir] = agent&.spawn_info&.dig(:working_dir)
77
+ end
78
+ end
79
+
80
+ # Fire notification
81
+ dispatch_notification(
82
+ event: {event_type: :workflow_triggered, monitor_type: :spawner,
83
+ monitor_name: spawner_config[:name] || :system,
84
+ goal_status:},
85
+ agent_id:,
86
+ message: "Workflow triggered for #{agent_id} (#{goal_status} → #{resolved[:name]})"
87
+ )
88
+
89
+ # Spawn in background thread
90
+ Thread.new do
91
+ @agent_spawner.spawn(event: workflow_event, spawner_config: resolved)
92
+ rescue => e
93
+ Superkick.logger.error("workflow_executor") {
94
+ "Workflow spawn failed for #{agent_id}: #{e.message}"
95
+ }
96
+ # Reclaim VCS state if spawn fails to avoid orphaned workspaces
97
+ reclaim_vcs_state(workflow_event)
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ def resolve_workflow_config(workflow_config, parent_config)
104
+ if workflow_config[:spawner]
105
+ # Reference to a named spawner
106
+ name = workflow_config[:spawner].to_sym
107
+ ref = @config.spawners[name]
108
+ unless ref
109
+ Superkick.logger.warn("workflow_executor") { "Workflow references unknown spawner: #{name}" }
110
+ return nil
111
+ end
112
+ ref.merge(name:, workflow_source: parent_config[:name])
113
+ else
114
+ # Inline config — fill in defaults from parent, merging driver configs
115
+ effective_driver = if workflow_config[:driver]
116
+ Driver.merge_driver(parent_config[:driver], workflow_config[:driver])
117
+ else
118
+ parent_config[:driver]
119
+ end
120
+
121
+ {
122
+ name: "#{parent_config[:name]}-workflow",
123
+ driver: effective_driver,
124
+ goal: workflow_config[:goal],
125
+ repository: workflow_config[:repository],
126
+ max_duration: workflow_config[:max_duration] || parent_config[:max_duration],
127
+ max_concurrent: workflow_config[:max_concurrent],
128
+ approval_required: workflow_config[:approval_required] || false,
129
+ prompt_template: workflow_config[:prompt_template],
130
+ workflow_source: parent_config[:name]
131
+ }.compact
132
+ end
133
+ end
134
+
135
+ def build_workflow_event(agent_id:, goal_status:, spawner_config:, workflow_config:, depth:, parent_iterations:)
136
+ agent = @store.get(agent_id)
137
+ original_event = agent&.spawn_info || {}
138
+
139
+ # Determine event_type for template resolution
140
+ event_type = workflow_config[:prompt_template] || :workflow_triggered
141
+
142
+ # Determine agent_id for the workflow spawn
143
+ stage = workflow_config[:name] || ((goal_status == :completed) ? "on_complete" : "on_fail")
144
+ workflow_agent_id = "#{agent_id}-workflow-#{stage}"
145
+
146
+ # Increment iteration count for the target spawner
147
+ target_name = workflow_config[:name]&.to_sym
148
+ workflow_iterations = parent_iterations.dup
149
+ workflow_iterations[target_name] = (workflow_iterations[target_name] || 0) + 1 if target_name
150
+
151
+ event = {
152
+ event_type:,
153
+ agent_id: workflow_agent_id,
154
+ monitor_type: :workflow,
155
+ monitor_name: spawner_config[:name] || :system,
156
+ parent_agent_id: agent_id,
157
+ parent_goal_status: goal_status,
158
+ parent_spawner_name: spawner_config[:name],
159
+ workflow_depth: depth + 1,
160
+ workflow_iterations:
161
+ }
162
+
163
+ # Forward serialized context from parent's spawn_info wholesale.
164
+ # This replaces the old FORWARDED_FIELDS approach — context carries
165
+ # all integration-specific data (Drops, scalars) without enumeration.
166
+ original_event[:context]&.each do |key, value|
167
+ event[key] = value
168
+ end
169
+
170
+ # Include failure retrospective when available
171
+ event[:parent_goal_summary] = agent.goal_summary if agent&.goal_summary
172
+
173
+ # Forward per-agent notifier configs so workflow children inherit them
174
+ event[:_spawn_notifiers] = agent.notifiers if agent&.notifiers&.any?
175
+
176
+ event
177
+ end
178
+
179
+ # Reclaim VCS resources from a failed workflow event to avoid orphaning.
180
+ def reclaim_vcs_state(workflow_event)
181
+ vcs_state = workflow_event.delete(:inherited_vcs_state)
182
+ vcs_state[:adapter].teardown(destination: vcs_state[:destination]) if vcs_state
183
+ rescue => e
184
+ Superkick.logger.error("workflow_executor") {
185
+ "VCS reclaim teardown failed: #{e.message}"
186
+ }
187
+ end
188
+
189
+ def dispatch_notification(...)
190
+ @notification_dispatcher.dispatch(...)
191
+ end
192
+ end
193
+ end
194
+
195
+ # Backwards-compatible alias
196
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superkick
4
+ module Spawn
5
+ # WorkflowValidator — static validation for spawner workflow configs.
6
+ #
7
+ # Two checks, both called at server startup:
8
+ #
9
+ # 1. **Cycle detection** — walks `on_complete:` / `on_fail:` spawner
10
+ # reference chains looking for cycles via visited-set tracking.
11
+ #
12
+ # 2. **Cyclic iteration requirement** — spawners with `allow_cycles: true`
13
+ # must have an explicit `max_iterations` to prevent infinite recursion.
14
+ #
15
+ # Only `:spawner` references create chain links — inline configs are leaf
16
+ # nodes and cannot form cycles.
17
+ class WorkflowValidator
18
+ # Validate all spawner configs for cycles.
19
+ # @param spawners [Hash] the spawners config hash (name => config)
20
+ # @return [Array<Array<Symbol>>] list of cycles found (empty = valid)
21
+ def self.validate(spawners)
22
+ cycles = []
23
+ spawners.each_key do |name|
24
+ walk_chain(name.to_sym, spawners, {}, [], cycles)
25
+ end
26
+ cycles
27
+ end
28
+
29
+ # Find cyclic spawners that lack a max_iterations limit.
30
+ #
31
+ # Spawners with `allow_cycles: true` need an explicit iteration limit
32
+ # to prevent infinite recursion.
33
+ #
34
+ # @param spawners [Hash] the spawners config hash
35
+ # @return [Array<Symbol>] names of spawners missing an iteration limit
36
+ def self.cyclic_spawners_without_iteration_limit(spawners)
37
+ spawners.each_with_object([]) do |(name, config), missing|
38
+ next unless config[:allow_cycles]
39
+
40
+ missing << name.to_sym unless config[:max_iterations]
41
+ end
42
+ end
43
+
44
+ # Return the spawner names reachable from a config's on_complete/on_fail hooks.
45
+ def self.successors(config)
46
+ %i[on_complete on_fail].filter_map do |hook|
47
+ config[hook]&.dig(:spawner)&.to_sym
48
+ end
49
+ end
50
+ private_class_method :successors
51
+
52
+ def self.walk_chain(spawner, spawners, visited, path, cycles)
53
+ return if visited[spawner] == :done
54
+
55
+ if visited[spawner] == :in_progress
56
+ cycle_start = path.index(spawner)
57
+ cycles << path[cycle_start..] + [spawner]
58
+ return
59
+ end
60
+
61
+ visited[spawner] = :in_progress
62
+ path.push(spawner)
63
+
64
+ config = spawners[spawner]
65
+ successors(config || {}).each do |target|
66
+ walk_chain(target, spawners, visited, path, cycles)
67
+ end
68
+
69
+ path.pop
70
+ visited[spawner] = :done
71
+ end
72
+ private_class_method :walk_chain
73
+ end
74
+ end
75
+
76
+ # Backwards-compatible alias
77
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superkick
4
+ # Spawner — server-level poller base class for spawning agents.
5
+ #
6
+ # Inherits the run loop, error handling, backoff, and config validation
7
+ # from Poller. Differences from agent-bound monitors:
8
+ #
9
+ # 1. No @agent — spawners are not bound to any agent
10
+ # 2. Separate class-level registry — types don't collide with Monitor types
11
+ # 3. dispatch stamps an agent_id on the event
12
+ # 4. log_tag doesn't reference an agent ID
13
+ #
14
+ # Subclass contract (in addition to Poller's):
15
+ # self.agent_id(event) → String — deterministic agent ID for dedup
16
+ # self.spawn_templates_dir → path to kickoff ERB templates (optional)
17
+ class Spawner < Poller
18
+ @registry = {}
19
+
20
+ class << self
21
+ include Superkick::Registry
22
+
23
+ def register(klass)
24
+ raise ArgumentError, "#{klass} must define self.type" unless klass.respond_to?(:type)
25
+ key = klass.type.to_sym
26
+ raise ArgumentError, "Spawner type :#{key} already registered" if @registry.key?(key)
27
+ @registry[key] = klass
28
+ end
29
+
30
+ def deregister(type)
31
+ @registry.delete(type.to_sym)
32
+ end
33
+
34
+ def registered = @registry.dup.freeze
35
+
36
+ def lookup(type)
37
+ @registry[type.to_sym] or raise ArgumentError, "Unknown spawner type: #{type}"
38
+ end
39
+ end
40
+
41
+ # Subclasses must implement: given an event hash, return a string that
42
+ # uniquely identifies the work unit and serves as the agent ID.
43
+ def self.agent_id(event)
44
+ raise NotImplementedError, "#{self}.agent_id not implemented"
45
+ end
46
+
47
+ # Override to provide spawn-specific template directory.
48
+ def self.spawn_templates_dir
49
+ nil
50
+ end
51
+
52
+ # Override dispatch to stamp agent_id from the spawner class.
53
+ def dispatch(event)
54
+ full_event = event.merge(
55
+ monitor_type: self.class.type.to_s,
56
+ monitor_name: @name,
57
+ agent_id: self.class.agent_id(event)
58
+ )
59
+ @handler.handle(event: full_event)
60
+ end
61
+
62
+ # No agent — use spawner-specific tag.
63
+ def log_tag
64
+ "spawner:#{self.class.type}(#{@name})"
65
+ end
66
+ end
67
+ end