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