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,45 @@
1
+ SUPERKICK: Team update — {{ entry_count }} new entries from teammates.
2
+ {% if has_blockers %}
3
+ ⚠ BLOCKERS DETECTED — review and take action.
4
+ {% endif %}
5
+ {% if grouped.update -%}
6
+
7
+ ## Updates
8
+ {% for e in grouped.update -%}
9
+ [{{ e.agent_id }}{% if e.role %} ({{ e.role }}){% endif %}] [{{ e.kind }}] {{ e.message }}
10
+ {% endfor -%}
11
+ {% endif -%}
12
+ {% if grouped.message -%}
13
+
14
+ ## Messages
15
+ {% for e in grouped.message -%}
16
+ [{{ e.agent_id }}{% if e.role %} ({{ e.role }}){% endif %} → {{ e.target_agent_id }}] {{ e.message }}
17
+ {% endfor -%}
18
+ {% endif -%}
19
+ {% if grouped.lifecycle -%}
20
+
21
+ ## Lifecycle
22
+ {% for e in grouped.lifecycle -%}
23
+ * {{ e.agent_id }} {{ e.kind }}{% if e.message %}: {{ e.message }}{% endif %}
24
+ {% endfor -%}
25
+ {% endif -%}
26
+ {% if grouped.artifact -%}
27
+
28
+ ## Artifacts
29
+ {% for e in grouped.artifact -%}
30
+ [{{ e.agent_id }}] published "{{ e.artifact_name }}"
31
+ {% endfor -%}
32
+ {% endif -%}
33
+ {% if agent_role == "lead" %}
34
+
35
+ As team lead, review this digest and take action:
36
+ - superkick_team_status for full team state
37
+ - superkick_read_artifact to review worker plans
38
+ - superkick_post_update (with target_agent_id) for urgent coordination
39
+ - superkick_signal_goal when all work is complete
40
+ {% else %}
41
+
42
+ Review this digest and adjust your work accordingly.
43
+ - superkick_post_update to share your progress
44
+ - superkick_team_status for full team state
45
+ {% endif -%}
@@ -0,0 +1,7 @@
1
+ SUPERKICK [{{ "now" | time }}]: A teammate has sent you a message.
2
+
3
+ From: {{ sender_agent_id }} ({{ sender_role }})
4
+ Message: {{ message }}
5
+
6
+ This message is also recorded in the team log. The sender believes this
7
+ needs your attention. Acknowledge and adjust your work accordingly.
@@ -0,0 +1,37 @@
1
+ You are a worker agent on a team coordinated by a planning agent.
2
+
3
+ ## Your task
4
+ {{ task }}
5
+
6
+ ## Repository: {{ repository_name }}
7
+
8
+ ## Team context
9
+ Team ID: {{ team_id }}
10
+ Team lead: {{ lead_agent_id }}
11
+ {% if depends_on.size > 0 -%}
12
+
13
+ ## Dependencies
14
+ You depend on work from these teammates:
15
+ {% for dep in depends_on -%}
16
+ - {{ dep }}
17
+ {% endfor -%}
18
+ Check superkick_team_status before starting work that depends on their output.
19
+ {% endif -%}
20
+
21
+ {% if monitor_names.size > 0 -%}
22
+ ## Active monitors
23
+ The following monitors have been configured for you:
24
+ {% for name in monitor_names -%}
25
+ - {{ name }}
26
+ {% endfor -%}
27
+ {% endif -%}
28
+
29
+ ## Communication
30
+ - Publish your implementation plan as an artifact using superkick_publish_artifact
31
+ (name: "implementation-plan") as your first action.
32
+ - Use superkick_post_update regularly to share your progress.
33
+ - Use superkick_team_status to check on teammates you depend on.
34
+ - Use superkick_read_artifact to read teammates' implementation plans.
35
+ - Use superkick_post_update with target_agent_id only for urgent coordination needs.
36
+ - Signal superkick_signal_goal with status "completed" when your task is done,
37
+ or "failed" if you cannot complete it.
@@ -0,0 +1,22 @@
1
+ You are a workflow agent, spawned automatically after agent "{{ parent_agent_id }}"
2
+ {% if parent_goal_status == "completed" %}completed successfully{% else %}failed{% endif %}.
3
+
4
+ {% if issue -%}
5
+ Original issue: #{{ issue.number }} {{ issue.title }}{% if issue.url %} ({{ issue.url }}){% endif %}
6
+ {% endif -%}
7
+ {% if story -%}
8
+ Original story: {{ story.ref }}{% if story.url %} ({{ story.url }}){% endif %}
9
+ {% endif -%}
10
+ {% if repo -%}
11
+ Repository: {{ repo }}
12
+ {% endif -%}
13
+ {% if branch -%}
14
+ Branch: {{ branch }}
15
+ {% endif -%}
16
+
17
+ {% if parent_goal_summary -%}
18
+ Summary from previous agent:
19
+ {{ parent_goal_summary }}
20
+
21
+ {% endif -%}
22
+ Review the work done by the previous agent and continue from where it left off.
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superkick
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superkick
4
+ # VersionControl — abstract base class for VCS adapters.
5
+ #
6
+ # A version control adapter knows how to acquire an isolated working copy
7
+ # of a repository and clean it up afterwards. The adapter is selected
8
+ # automatically based on the Repository's `version_control` field.
9
+ #
10
+ # Subclass contract:
11
+ # self.type → unique Symbol (e.g. :git, :mercurial, :subversion)
12
+ # acquire(source:, destination:, → nil (create an isolated working copy)
13
+ # branch:, base_branch:)
14
+ # teardown(destination:) → nil (remove the working copy)
15
+ #
16
+ # Subclasses register themselves:
17
+ # Superkick::VersionControl.register(MyAdapter)
18
+ class VersionControl
19
+ # ── Probe — abstract base for VCS detection probes ──────────────────
20
+ #
21
+ # Probes detect whether a local directory is managed by a particular VCS
22
+ # by inspecting the filesystem directly (e.g. checking for `.git`).
23
+ #
24
+ # Subclass contract:
25
+ # self.type → unique Symbol (matches the adapter type)
26
+ # self.detect_at(path:) → { type: :git } or nil
27
+ class Probe
28
+ @registry = {}
29
+
30
+ class << self
31
+ include Superkick::Registry
32
+
33
+ def register(klass)
34
+ raise ArgumentError, "#{klass} must define self.type" unless klass.respond_to?(:type)
35
+
36
+ key = klass.type.to_sym
37
+ raise ArgumentError, "VersionControl::Probe type :#{key} already registered" if @registry.key?(key)
38
+
39
+ @registry[key] = klass
40
+ end
41
+
42
+ def registered
43
+ @registry.dup.freeze
44
+ end
45
+
46
+ # Run all registered probes against a local directory path.
47
+ # Returns the first match (e.g. { type: :git }) or nil.
48
+ def detect_from_path(path:)
49
+ @registry.each_value do |klass|
50
+ result = klass.detect_at(path:)
51
+ return result if result
52
+ end
53
+ nil
54
+ end
55
+
56
+ def type
57
+ raise NotImplementedError, "#{self}.type not defined"
58
+ end
59
+
60
+ # Subclasses must override.
61
+ # @param path [String] absolute path to a directory
62
+ # @return [Hash, nil] e.g. { type: :git } or nil
63
+ def detect_at(path:)
64
+ raise NotImplementedError, "#{self}.detect_at not implemented"
65
+ end
66
+ end
67
+ end
68
+
69
+ # ── Registry (stores classes, keyed by type) ─────────────────────────
70
+ @registry = {}
71
+
72
+ class << self
73
+ include Superkick::Registry
74
+
75
+ def register(adapter_class)
76
+ key = adapter_class.type
77
+ raise ArgumentError, "VersionControl :#{key} already registered" if @registry.key?(key)
78
+ @registry[key] = adapter_class
79
+ Probe.register(adapter_class.probe_class) if adapter_class.probe_class
80
+ end
81
+
82
+ def lookup(name)
83
+ @registry[name.to_sym] or raise ArgumentError, "Unknown version control: #{name.inspect}"
84
+ end
85
+
86
+ def registered
87
+ @registry.dup.freeze
88
+ end
89
+
90
+ # Convenience alias — delegates to Probe.detect_from_path.
91
+ def detect_from_path(path:)
92
+ Probe.detect_from_path(path:)
93
+ end
94
+
95
+ # Returns the Probe class nested inside this adapter, or nil.
96
+ def probe_class
97
+ const_defined?(:Probe, false) ? const_get(:Probe) : nil
98
+ end
99
+
100
+ # Build an adapter for a given Repository.
101
+ #
102
+ # @param repository [Repository] the repository to work with
103
+ # @return [VersionControl] an adapter instance
104
+ def for_repository(repository)
105
+ vcs = repository.version_control
106
+ raise ArgumentError, "Repository #{repository.name} has no version_control set" unless vcs
107
+ klass = lookup(vcs)
108
+ klass.new
109
+ end
110
+ end
111
+
112
+ # ── Instance interface ───────────────────────────────────────────────
113
+
114
+ def self.type
115
+ raise NotImplementedError, "#{self}.type not implemented"
116
+ end
117
+
118
+ # Acquire an isolated working copy of the repository.
119
+ #
120
+ # @param source [Repository] the repository to work with
121
+ # @param destination [String] absolute path where the working copy should be created
122
+ # @param branch [String] the branch to create or check out
123
+ # @param base_branch [String] the branch to base the new branch on (default: "main")
124
+ def acquire(source:, destination:, branch:, base_branch: "main")
125
+ raise NotImplementedError, "#{self.class}#acquire not implemented"
126
+ end
127
+
128
+ # Remove the isolated working copy.
129
+ #
130
+ # @param destination [String] absolute path of the working copy to remove
131
+ def teardown(destination:)
132
+ raise NotImplementedError, "#{self.class}#teardown not implemented"
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,302 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "erb"
5
+
6
+ module Superkick
7
+ # Loads ~/.superkick/config.yml through ERB, then YAML.
8
+ #
9
+ # Load order:
10
+ # 1. plugins — require each listed gem/file
11
+ # 2. driver — call Superkick.use(:name, **driver_opts)
12
+ # 3. superkick: — apply tunable keys to Superkick.config
13
+ # 4. monitors: — named monitor configs stored on Superkick.config.monitors
14
+ #
15
+ # Driver-specific options come from the `drivers:` top-level key:
16
+ #
17
+ # driver: claude_code
18
+ # drivers:
19
+ # claude_code:
20
+ # cli_command: /opt/bin/claude
21
+ # config_dir: /custom/path/.claude
22
+ #
23
+ # Unknown keys under `superkick:` are warned.
24
+ # Unknown top-level keys are warned.
25
+ # Ruby config.rb is loaded after this and always wins.
26
+ module YamlConfig
27
+ SUPERKICK_KEYS = %i[
28
+ poll_interval idle_threshold inject_clear_delay
29
+ rate_limit_backoff error_backoff log_level
30
+ attach_history_size attach_escape_key attach_rw_idle_timeout
31
+ attach_replay_buffer_size attach_max_connections
32
+ cost_poll_interval cost_stale_after
33
+ max_workers_per_team
34
+ ].freeze
35
+
36
+ NUMERIC_KEYS = %i[poll_interval idle_threshold inject_clear_delay
37
+ rate_limit_backoff error_backoff attach_history_size attach_rw_idle_timeout
38
+ attach_replay_buffer_size attach_max_connections
39
+ cost_poll_interval cost_stale_after max_workers_per_team].freeze
40
+
41
+ RESERVED_KEYS = %i[plugins driver drivers superkick monitors notifications spawners budget repositories context_documents server runtime attach session_recording].freeze
42
+
43
+ # @param path [String] absolute path to config.yml
44
+ def self.load!(path)
45
+ return unless File.exist?(path)
46
+
47
+ raw = File.read(path)
48
+ source = ERB.new(raw).result(env_binding)
49
+ data = YAML.safe_load(source, permitted_classes: [], aliases: true, symbolize_names: true) || {}
50
+
51
+ apply_plugins(data[:plugins] || [])
52
+ apply_driver(data[:driver], data[:drivers])
53
+ apply_superkick_section(data[:superkick] || {})
54
+ apply_monitors_section(data[:monitors] || {})
55
+ apply_notifications_section(data[:notifications]) if data.key?(:notifications)
56
+ apply_spawners_section(data[:spawners] || {})
57
+ apply_budget_section(data[:budget]) if data.key?(:budget)
58
+ apply_repositories_section(data[:repositories]) if data.key?(:repositories)
59
+ apply_context_documents_section(data[:context_documents]) if data.key?(:context_documents)
60
+ apply_server_section(data[:server]) if data.key?(:server)
61
+ apply_runtime_section(data[:runtime]) if data.key?(:runtime)
62
+ apply_attach_section(data[:attach]) if data.key?(:attach)
63
+ apply_session_recording_section(data[:session_recording]) if data.key?(:session_recording)
64
+ warn_unknown_keys(data)
65
+ rescue KeyError, Psych::SyntaxError, Psych::DisallowedClass, SyntaxError => e
66
+ Superkick.logger.error("yaml_config") { "Failed to load config.yml: #{e.message}" }
67
+ end
68
+
69
+ private_class_method def self.env_binding
70
+ # Expose an env() helper in ERB templates that raises on missing vars.
71
+ env_mod = Module.new do
72
+ define_method(:env) do |key|
73
+ ENV.fetch(key) do
74
+ raise KeyError, "Required environment variable #{key.inspect} is not set"
75
+ end
76
+ end
77
+ end
78
+ Object.new.tap { |o| o.extend(env_mod) }.instance_eval { binding }
79
+ end
80
+
81
+ private_class_method def self.apply_plugins(plugins)
82
+ Array(plugins).each do |plugin|
83
+ name = plugin.is_a?(Hash) ? plugin[:name] : plugin.to_s
84
+ begin
85
+ require name
86
+ Superkick.logger.info("yaml_config") { "Loaded plugin: #{name}" }
87
+ rescue LoadError => e
88
+ Superkick.logger.warn("yaml_config") { "Could not load plugin #{name.inspect}: #{e.message}" }
89
+ end
90
+ end
91
+ end
92
+
93
+ private_class_method def self.apply_driver(driver_name, drivers_section)
94
+ drivers_section ||= {}
95
+
96
+ # Extract profiles from the drivers section
97
+ apply_driver_profiles(drivers_section[:profiles]) if drivers_section[:profiles]
98
+
99
+ return unless driver_name
100
+
101
+ options = drivers_section[driver_name.to_sym] || {}
102
+ Superkick.use(driver_name.to_sym, **options)
103
+ rescue ArgumentError => e
104
+ Superkick.logger.warn("yaml_config") { "Unknown driver #{driver_name.inspect}: #{e.message}" }
105
+ end
106
+
107
+ private_class_method def self.apply_driver_profiles(profiles)
108
+ unless profiles.is_a?(Hash)
109
+ Superkick.logger.warn("yaml_config") { "drivers.profiles: expected a hash, got #{profiles.class}" }
110
+ return
111
+ end
112
+ Superkick.config.driver_profiles = profiles
113
+ end
114
+
115
+ private_class_method def self.apply_superkick_section(section)
116
+ configuration = Superkick.config
117
+
118
+ section.each do |key, value|
119
+ if SUPERKICK_KEYS.include?(key)
120
+ setter = "#{key}="
121
+ value = value.to_f if NUMERIC_KEYS.include?(key)
122
+ value = value.to_sym if key == :log_level
123
+ value = parse_escape_key(value) if key == :attach_escape_key
124
+ configuration.public_send(setter, value)
125
+ else
126
+ Superkick.logger.warn("yaml_config") { "Unknown superkick config key: #{key.inspect}" }
127
+ end
128
+ end
129
+ end
130
+
131
+ # Parse a human-readable key name like "ctrl-b" into a single control byte.
132
+ # Accepts: "ctrl-a" through "ctrl-z", or a raw single-byte string.
133
+ private_class_method def self.parse_escape_key(value)
134
+ str = value.to_s.strip.downcase
135
+ if str.match?(/\Actrl-[a-z]\z/)
136
+ letter = str[-1]
137
+ (letter.ord - "a".ord + 1).chr
138
+ else
139
+ Superkick.logger.warn("yaml_config") { "Invalid attach_escape_key #{value.inspect} — use 'ctrl-a' through 'ctrl-z'" }
140
+ "\x01" # fall back to default
141
+ end
142
+ end
143
+
144
+ private_class_method def self.apply_monitors_section(monitors)
145
+ if monitors.key?(:privileged_types)
146
+ apply_privileged_types(monitors[:privileged_types])
147
+ end
148
+
149
+ monitors.each do |name, config|
150
+ next if name == :privileged_types
151
+ next unless config.is_a?(Hash)
152
+ Superkick.config.monitors[name] = config
153
+ Superkick.logger.info("yaml_config") { "Configured monitor: #{name}" }
154
+ end
155
+ end
156
+
157
+ private_class_method def self.apply_privileged_types(types)
158
+ unless types.is_a?(Array)
159
+ Superkick.logger.warn("yaml_config") { "privileged_types: expected an array, got #{types.class}" }
160
+ return
161
+ end
162
+ Superkick.config.privileged_types = types.map(&:to_sym)
163
+ end
164
+
165
+ private_class_method def self.apply_notifications_section(notifications)
166
+ if notifications.is_a?(Hash)
167
+ # Hash form with privileged_types key
168
+ apply_notification_privileged_types(notifications[:privileged_types]) if notifications[:privileged_types]
169
+ items = notifications[:items]
170
+ if items.is_a?(Array)
171
+ Superkick.config.notifications = items.select { |n| n.is_a?(Hash) }
172
+ end
173
+ return
174
+ end
175
+
176
+ unless notifications.is_a?(Array)
177
+ Superkick.logger.warn("yaml_config") { "notifications: expected an array or hash, got #{notifications.class}" }
178
+ return
179
+ end
180
+
181
+ Superkick.config.notifications = notifications.select { |n| n.is_a?(Hash) }
182
+ end
183
+
184
+ private_class_method def self.apply_notification_privileged_types(types)
185
+ unless types.is_a?(Array)
186
+ Superkick.logger.warn("yaml_config") { "notification privileged_types: expected an array, got #{types.class}" }
187
+ return
188
+ end
189
+ Superkick.config.notification_privileged_types = types.map(&:to_sym)
190
+ end
191
+
192
+ private_class_method def self.apply_spawners_section(spawners)
193
+ spawners.each do |name, config|
194
+ next unless config.is_a?(Hash)
195
+ config[:name] = name
196
+ Superkick.config.spawners[name] = config
197
+ Superkick.logger.info("yaml_config") { "Configured spawner: #{name}" }
198
+ end
199
+ end
200
+
201
+ private_class_method def self.apply_budget_section(budget)
202
+ unless budget.is_a?(Hash)
203
+ Superkick.logger.warn("yaml_config") { "budget: expected a hash, got #{budget.class}" }
204
+ return
205
+ end
206
+ Superkick.config.budget = budget
207
+ end
208
+
209
+ private_class_method def self.apply_repositories_section(repositories)
210
+ unless repositories.is_a?(Hash)
211
+ Superkick.logger.warn("yaml_config") { "repositories: expected a hash, got #{repositories.class}" }
212
+ return
213
+ end
214
+ Superkick.config.repositories = repositories
215
+ end
216
+
217
+ private_class_method def self.apply_context_documents_section(context_documents)
218
+ unless context_documents.is_a?(Array)
219
+ Superkick.logger.warn("yaml_config") { "context_documents: expected an array, got #{context_documents.class}" }
220
+ return
221
+ end
222
+ Superkick.config.context_documents = context_documents
223
+ end
224
+
225
+ private_class_method def self.apply_server_section(server)
226
+ unless server.is_a?(Hash)
227
+ Superkick.logger.warn("yaml_config") { "server: expected a hash, got #{server.class}" }
228
+ return
229
+ end
230
+ Superkick.config.server = server
231
+ end
232
+
233
+ private_class_method def self.apply_runtime_section(runtime)
234
+ unless runtime.is_a?(Hash)
235
+ Superkick.logger.warn("yaml_config") { "runtime: expected a hash, got #{runtime.class}" }
236
+ return
237
+ end
238
+ Superkick.config.runtime = runtime
239
+ end
240
+
241
+ # Maps attach: YAML keys to their config accessor names.
242
+ ATTACH_KEY_MAP = {
243
+ history_size: :attach_history_size,
244
+ escape_key: :attach_escape_key,
245
+ rw_idle_timeout: :attach_rw_idle_timeout,
246
+ replay_buffer_size: :attach_replay_buffer_size,
247
+ max_connections: :attach_max_connections
248
+ }.freeze
249
+
250
+ private_class_method def self.apply_attach_section(attach)
251
+ unless attach.is_a?(Hash)
252
+ Superkick.logger.warn("yaml_config") { "attach: expected a hash, got #{attach.class}" }
253
+ return
254
+ end
255
+
256
+ configuration = Superkick.config
257
+ attach.each do |key, value|
258
+ config_key = ATTACH_KEY_MAP[key]
259
+ unless config_key
260
+ Superkick.logger.warn("yaml_config") { "Unknown attach config key: #{key.inspect}" }
261
+ next
262
+ end
263
+
264
+ value = value.to_f if NUMERIC_KEYS.include?(config_key)
265
+ value = parse_escape_key(value) if config_key == :attach_escape_key
266
+ configuration.public_send("#{config_key}=", value)
267
+ end
268
+ end
269
+
270
+ SESSION_RECORDING_KEY_MAP = {
271
+ enabled: :session_recording_enabled,
272
+ max_size: :session_recording_max_size
273
+ }.freeze
274
+
275
+ private_class_method def self.apply_session_recording_section(section)
276
+ unless section.is_a?(Hash)
277
+ Superkick.logger.warn("yaml_config") { "session_recording: expected a hash, got #{section.class}" }
278
+ return
279
+ end
280
+
281
+ configuration = Superkick.config
282
+ section.each do |key, value|
283
+ config_key = SESSION_RECORDING_KEY_MAP[key]
284
+ unless config_key
285
+ Superkick.logger.warn("yaml_config") { "Unknown session_recording config key: #{key.inspect}" }
286
+ next
287
+ end
288
+
289
+ value = value.to_f if config_key == :session_recording_max_size
290
+ configuration.public_send("#{config_key}=", value)
291
+ end
292
+ end
293
+
294
+ private_class_method def self.warn_unknown_keys(data)
295
+ data.each_key do |key|
296
+ next if RESERVED_KEYS.include?(key)
297
+ next unless data[key].is_a?(Hash)
298
+ Superkick.logger.warn("yaml_config") { "Unknown top-level config key: #{key.inspect}" }
299
+ end
300
+ end
301
+ end
302
+ end