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
data/CLAUDE.md ADDED
@@ -0,0 +1,2226 @@
1
+ # CLAUDE.md — Superkick
2
+
3
+ This file exists so you don't have to rediscover Superkick's architecture every session. Read it before touching anything.
4
+
5
+ ## What Superkick is
6
+
7
+ Superkick is a **Ruby gem** that transparently wraps AI coding CLIs (Claude Code, GitHub Copilot, OpenAI Codex, Google Gemini, Goose) in a PTY proxy and **injects external context** — GitHub CI results, PR reviews, PR comments — directly into the CLI at idle moments via a text prompt.
8
+
9
+ It has two runtime processes:
10
+ - **`superkick agent`** — the PTY proxy process, one per AI CLI agent
11
+ - **`superkick server`** — the background server that runs monitors and orchestrates injection
12
+
13
+ They communicate over a Unix socket (`~/.superkick/run/server.sock`) using a simple JSON control protocol.
14
+
15
+ ## Architecture at a glance
16
+
17
+ ```
18
+ superkick agent (agent process)
19
+ ┌──────────────────────────────────────┐
20
+ │ PtyProxy │ superkick server process
21
+ │ │ │ Control ┌───────────────────────────────────┐
22
+ │ ├─owns─► Buffer::Server │◄───────►│ Control::Server (server.sock) │
23
+ │ │ (buffer-<id>.sock) │ │ │
24
+ │ ├─owns─► [Hosted::Buffer::Bridge] │ │ AgentStore │
25
+ │ │ (hosted mode only) │ │ └─ Agent │
26
+ │ ├─owns─► [Hosted::Attach::Bridge] │ │ ├─ monitors: { … } │
27
+ │ │ (hosted mode only) │ │ ├─ buffer_socket_path ◄──────┼─┐
28
+ │ ├─owns─► OutputLogger │ │ └─ output_log_path │ │
29
+ │ ├─owns─► SessionRecorder │ │ └─ recording_path │ │
30
+ │ │ (logs/<id>.log) │ │ (transient links) │ │
31
+ │ └─owns─► InputBuffer │ │ │ │
32
+ │ (partial input + guards) │ │ Buffer::Client ──────────────────►│ │
33
+ │ │ │ ├─ (default: Unix socket) ───────┼─┘
34
+ │ proxy_stdin ──► InputBuffer ──►PTY │ │ └─ Hosted::Buffer::Client ──────►│
35
+ │ proxy_output ──► guard detect ──►out│ │ └─► Hosted::Buffer::RelayStore│
36
+ │ └──► OutputLogger │ │ └─► Hosted::Buffer::Relay┼─┐
37
+ │ drain_inject ──────────────────►PTY │ │ ▲ WebSocket │ │
38
+ └──────────────────────────────────────┘ │ │ │
39
+ │ PTY │ Hosted::Attach::RelayStore │ │
40
+ ▼ │ └─► Hosted::Attach::Relay ──────►│ │
41
+ ┌──────────────────────────────────────┐ │ (RW exclusivity, history) │ │
42
+ │ AI CLI (claude / copilot / …) │─MCP─► │ │ │
43
+ └──────────────────────────────────────┘ │ │ Supervisor │ │
44
+ │ │ └─ owns monitor threads │ │
45
+ Hosted::Bridge subclasses ── WebSocket ──┼────┼───────────────────────────────────┼─┘
46
+ (Buffer + Attach, in agent process) │ │ Injector (uses Buffer::Client) │
47
+ │ │ │
48
+ └───►│ McpServer (local mode) │
49
+ └───────────────────────────────────┘
50
+
51
+ In hosted mode, MCP flows through a transparent stdio proxy:
52
+ ```
53
+ AI CLI → stdio → Hosted::McpProxy → POST /api/v1/mcp → Hosted Server
54
+ ```
55
+ ```
56
+
57
+ The agent registration sequence (multi-step with environment probing):
58
+ 1. `PtyProxy` sends `register` to the server (no monitors — just agent metadata and `working_dir`)
59
+ 2. Server responds with `environment_request` containing an array of action hashes (collected from all registered monitor probes via `Monitor.all_environment_actions`)
60
+ 3. `PtyProxy` executes the actions via `EnvironmentExecutor` in the agent's working directory
61
+ 4. `PtyProxy` sends `register_environment` with the environment snapshot
62
+ 5. Server runs probes server-side using the snapshot, merges with YAML-configured monitors, and starts monitor threads
63
+
64
+ The injection sequence when a monitor emits an event:
65
+ 1. `Monitor#dispatch` calls `Injector#inject`
66
+ 2. `TemplateRenderer` renders the Liquid template for the event type
67
+ 3. Injector sends `enqueue_injection` to the agent's `Buffer::Server` via `Buffer::Client` (fire-and-forget)
68
+ 4. Agent's `InjectionQueue` checks preconditions locally (idle state, guards)
69
+ 5. `InjectionQueue` writes: **C-c → blank Enter → rendered prompt + Enter → restore partial input**
70
+ 6. `InjectionQueue` reports result back to server via `injection_result` IPC
71
+ 7. `NotificationDispatcher` fires both global notifiers and per-agent notifiers (injected via `_spawn_notifiers`) in a background thread to alert the user. Stateful notifiers (e.g. Slack in API mode) can correlate events for the same agent (thread replies, email threading, etc.).
72
+
73
+ ## Directory structure
74
+
75
+ ```
76
+ superkick/
77
+ ├── exe/superkick # CLI entry point (requires lib/superkick/cli)
78
+ ├── lib/
79
+ │ └── superkick/
80
+ │ ├── cli.rb # Thor CLI — registers subcommands (server, agent, monitor, spawner, mcp, team, goal, notifier, repository, setup, approve, reject, approvals)
81
+ │ ├── cli/
82
+ │ │ ├── server.rb # superkick server: start, stop, status, log
83
+ │ │ ├── setup.rb # superkick setup: start (interactive first-time setup wizard)
84
+ │ │ ├── agent.rb # superkick agent: start, list, log, stop, attach, claim, unclaim, cost, report-cost, add-monitor, remove-monitor, add-notifier, remove-notifier
85
+ │ │ ├── monitor.rb # superkick monitor: list, install-templates
86
+ │ │ ├── spawner.rb # superkick spawner: list, start, stop, install-templates
87
+ │ │ ├── team.rb # superkick team: list, status, watch, stop, artifacts, artifact, message, spawn-worker
88
+ │ │ ├── mcp.rb # superkick mcp: start, configure
89
+ │ │ ├── goal.rb # superkick goal: list, signal
90
+ │ │ ├── notifier.rb # superkick notifier: list
91
+ │ │ ├── repository.rb # superkick repository: list
92
+ │ │ └── completion.rb # superkick completion: install, hidden helpers for dynamic shell completions
93
+ │ ├── server.rb # Server lifecycle (startup, signal handling, shutdown)
94
+ │ ├── mcp_server.rb # MCP stdio server exposing tools to the AI CLI
95
+ │ ├── registry.rb # Superkick::Registry — shared test isolation for class-level registries
96
+ │ ├── configuration.rb # Superkick::Configuration — all tuneable settings
97
+ │ ├── yaml_config.rb # YAML+ERB config loader
98
+ │ ├── pty_proxy.rb # 3-thread PTY wrapper (stdin / output / inject queue)
99
+ │ ├── environment_executor.rb # Agent-side action executor for server-driven probes
100
+ │ ├── input_buffer.rb # Tracks partial keystrokes + injection guard state
101
+ │ ├── connection.rb # Connection — newline-delimited JSON framing (shared by control + data plane)
102
+ │ ├── client_registry.rb # ClientRegistry — shared module for client factory methods (used by Control, Buffer, Attach)
103
+ │ ├── buffer/
104
+ │ │ ├── server.rb # Buffer::Server — per-agent Unix socket (get/guards_active/inject/idle_state)
105
+ │ │ └── client.rb # Buffer::Client — Unix socket implementation + .from factory
106
+ │ ├── hosted/
107
+ │ │ ├── bridge.rb # Hosted::Bridge — abstract base class for agent-side WebSocket bridges (raw WebSocket, not ActionCable)
108
+ │ │ ├── mcp_proxy.rb # Hosted::McpProxy — transparent stdio-to-HTTP MCP proxy for hosted mode
109
+ │ │ ├── buffer/
110
+ │ │ │ ├── client.rb # Hosted::Buffer::Client — routes commands through WebSocket relays
111
+ │ │ │ ├── bridge.rb # Hosted::Buffer::Bridge — agent-side bridge to hosted server (subclass of Hosted::Bridge)
112
+ │ │ │ ├── relay.rb # Hosted::Buffer::Relay — server-side per-agent WebSocket relay
113
+ │ │ │ └── relay_store.rb # Hosted::Buffer::RelayStore — thread-safe relay registry
114
+ │ │ ├── control/
115
+ │ │ │ └── client.rb # Hosted::Control::Client — HTTPS control client for remote servers
116
+ │ │ └── attach/
117
+ │ │ ├── client.rb # Hosted::Attach::Client — WebSocket attach subclass
118
+ │ │ ├── bridge.rb # Hosted::Attach::Bridge — agent-side attach bridge (subclass of Hosted::Bridge)
119
+ │ │ ├── relay.rb # Hosted::Attach::Relay — server-side attach relay/mux (RW exclusivity, history, idle timeout)
120
+ │ │ └── relay_store.rb # Hosted::Attach::RelayStore — thread-safe attach relay registry
121
+ │ ├── attach/
122
+ │ │ ├── protocol.rb # Attach::Protocol — binary frame protocol (OUTPUT, INPUT, META, RESIZE, HISTORY, ERROR, HELLO, COMMAND, NOTIFY)
123
+ │ │ ├── server.rb # Attach::Server — per-agent attach endpoint with RW exclusivity, force-takeover, idle timeout, broadcast callbacks
124
+ │ │ └── client.rb # Attach::Client — Unix socket implementation + .from factory, session logic
125
+ │ ├── history_buffer.rb # Ring buffer for PTY output replay on attach connect
126
+ │ ├── output_logger.rb # Tees PTY output to per-agent log files with rotation
127
+ │ ├── session_recorder.rb # Records timestamped sessions in asciicast v2 (input + output + resize)
128
+ │ ├── setup.rb # Setup — interactive config.yml generator with integration discovery
129
+ │ ├── injection_guard.rb # Named guards that block injection (cleared on submit)
130
+ │ ├── injection_queue.rb # Agent-side injection queue with TTL, priority, supersede-by-key
131
+ │ ├── process_runner.rb # Unified subprocess execution with reliable timeouts
132
+ │ ├── injector.rb # Orchestrates a single injection attempt
133
+ │ ├── template_renderer.rb # Liquid rendering with template filters + notification template resolution
134
+ │ ├── template_filters.rb # Core Liquid filters (time, short_sha, truncate)
135
+ │ ├── notifier.rb # Abstract Notifier base class + registry + lifecycle events
136
+ │ ├── notifier_state_store.rb # NotifierStateStore — injectable key-value state for stateful notifiers
137
+ │ ├── notification_dispatcher.rb # Stateful notification dispatcher — long-lived notifier instances
138
+ │ ├── drop.rb # Superkick::Drop — base class for Liquid Drops with serialize/rehydrate
139
+ │ ├── drops.rb # Core Drops: TeamDrop, SpawnerDrop, MonitorDrop, AgentDrop
140
+ │ ├── notifiers/
141
+ │ │ ├── terminal_bell.rb # Writes BEL (\a) to stderr — zero-config default
142
+ │ │ └── command.rb # Runs a shell command with SUPERKICK_* env vars
143
+ │ ├── poller.rb # Abstract Poller base class — shared run loop for monitors & spawners
144
+ │ ├── monitor.rb # Abstract Monitor base class (extends Poller) + Monitor::Probe + registries
145
+ │ ├── spawner.rb # Abstract Spawner base class (extends Poller) — server-level agent spawners
146
+ │ ├── repository_source.rb # Repository, RepositorySource base + CompositeRepositorySource
147
+ │ ├── local/
148
+ │ │ └── repository_source.rb # Local::RepositorySource — single-repo or directory-scan source
149
+ │ ├── agent.rb # Live agent object (id, monitors, notifiers, buffer_socket_path, cost, team, role, goal_summary, working_dir, environment)
150
+ │ ├── agent/
151
+ │ │ ├── runtime.rb # Agent::Runtime — abstract base class + registry for agent process runtimes
152
+ │ │ ├── runtimes.rb # Requires all built-in runtimes
153
+ │ │ └── runtimes/
154
+ │ │ └── local.rb # Agent::Runtime::Local — spawns agent processes via Process.spawn (default)
155
+ │ ├── agent_store.rb # Thread-safe agent registry with JSON persistence + dedup
156
+ │ ├── team/
157
+ │ │ ├── log.rb # Team::Log — append-only ndjson team communication log
158
+ │ │ ├── log_store.rb # Team::LogStore — server-level lazy registry of Team::Log instances
159
+ │ │ ├── artifact_store.rb # Team::ArtifactStore — per-team persistent artifact storage
160
+ │ │ ├── log_entry_drop.rb # Liquid Drop for Team::Log entries in team digest templates
161
+ │ │ ├── log_monitor.rb # Team::LogMonitor — polls team log, injects digest events
162
+ │ │ └── log_notifier.rb # Team::LogNotifier — internal notifier that writes team log entries
163
+ │ ├── spawn/
164
+ │ │ ├── agent_spawner.rb # Spawn::AgentSpawner — manages repository acquisition and agent subprocess lifecycle
165
+ │ │ ├── handler.rb # Spawn::Handler — event handler for spawners (dedup, approval gates)
166
+ │ │ ├── injector.rb # Spawn::Injector — kickoff prompt injection into newly spawned agents
167
+ │ │ ├── approval_store.rb # Spawn::ApprovalStore — pending approval + rejection tracking for spawn gates
168
+ │ │ ├── workflow_validator.rb # Spawn::WorkflowValidator — static cycle detection for spawner workflows
169
+ │ │ └── workflow_executor.rb # Spawn::WorkflowExecutor — resolves workflow config, enriches events, transfers VCS state
170
+ │ ├── supervisor.rb # Supervisor — starts/stops monitor + spawner threads
171
+ │ ├── control/
172
+ │ │ ├── client.rb # Control::Client — Unix socket implementation + .from factory
173
+ │ │ ├── server.rb # Control::Server — Unix socket control server
174
+ │ │ └── reply.rb # Control::Reply — typed response wrapper
175
+ │ ├── goal.rb # Superkick::Goal base class + registry — spawned agent goal checks
176
+ │ ├── goals/
177
+ │ │ ├── agent_exit.rb # Completes when the AI CLI process exits
178
+ │ │ ├── command.rb # Runs a shell command periodically; success on exit 0
179
+ │ │ └── agent_signal.rb # Completes when AI CLI calls superkick_signal_goal MCP tool (default goal type)
180
+ │ ├── version_control.rb # Superkick::VersionControl base class + registry + VersionControl::Probe base
181
+ │ ├── cost_accumulator.rb # Thread-safe per-agent cost tracking
182
+ │ ├── cost_extractor.rb # Line-buffered PTY output cost parsing
183
+ │ ├── cost_poller.rb # Periodic cost command injection for stale agents
184
+ │ ├── budget_checker.rb # Multi-level budget evaluation (agent/spawner/global)
185
+ │ ├── driver.rb # Superkick::Driver base class + registry + CostPattern + normalize/merge helpers
186
+ │ ├── driver/
187
+ │ │ └── profile_source.rb # Driver::ProfileSource base + StaticProfileSource — named driver configs
188
+ │ ├── drivers.rb # Requires driver.rb + all built-in drivers
189
+ │ ├── drivers/
190
+ │ │ ├── claude_code.rb
191
+ │ │ ├── copilot.rb
192
+ │ │ ├── codex.rb
193
+ │ │ ├── gemini.rb
194
+ │ │ └── goose.rb
195
+ │ ├── templates/
196
+ │ │ ├── workflow/ # Default Liquid templates for workflow spawner events
197
+ │ │ └── team_log/ # Default Liquid templates for team agent events
198
+ │ └── integrations/
199
+ │ ├── docker/
200
+ │ │ ├── runtime.rb # Integrations::Docker::Runtime — provisions agents in Docker containers
201
+ │ │ ├── client.rb # Integrations::Docker::Client — Faraday-based Docker Engine API client
202
+ │ │ └── test/ # Docker runtime + client tests
203
+ │ ├── git/
204
+ │ │ ├── version_control.rb # Integrations::Git::VersionControl — worktree/clone adapter + Git::Probe
205
+ │ │ ├── repository_source.rb # Integrations::Git::RepositorySource — single-repo source for URL-based git repos
206
+ │ │ └── test/ # Git version control + repository source tests
207
+ │ ├── github/
208
+ │ │ ├── monitor.rb # Polls CI checks, PR comments, PR reviews via Octokit
209
+ │ │ ├── probe.rb # Auto-detects repo/branch from git remote
210
+ │ │ ├── goal.rb # Integrations::GitHub::PrMergedGoal — completes when a PR is merged
211
+ │ │ ├── issue_goal.rb # Integrations::GitHub::IssueResolvedGoal — completes when an issue is closed
212
+ │ │ ├── issue_spawner.rb # Spawns agents for new/reopened GitHub issues
213
+ │ │ ├── check_failed_spawner.rb # Spawns agents when GitHub checks fail on watched branches
214
+ │ │ ├── repository_source.rb # Integrations::GitHub::OrganizationRepositorySource — fetches repositories from a GitHub organization
215
+ │ │ ├── drops.rb # Liquid Drops (IssueDrop, PullRequestDrop, CommentDrop, ReviewDrop, CommitDrop, CheckRunDrop) for type-safe template access
216
+ │ │ ├── templates/ # Liquid templates for GitHub events + spawn templates
217
+ │ │ └── test/ # GitHub monitor, spawners, goals, drops tests
218
+ │ ├── circleci/
219
+ │ │ ├── monitor.rb # Polls CircleCI pipelines/workflows via Faraday
220
+ │ │ ├── probe.rb # Auto-detects from .circleci/config.yml + git remote
221
+ │ │ ├── templates/ # Liquid templates for CircleCI events
222
+ │ │ └── test/ # CircleCI monitor + probe tests
223
+ │ ├── shortcut/
224
+ │ │ ├── monitor.rb # Polls Shortcut stories + related stories via Faraday
225
+ │ │ ├── probe.rb # Auto-detects story ID from sc-XXXXX branch pattern
226
+ │ │ ├── spawner.rb # Polls Shortcut search for new stories to spawn agents
227
+ │ │ ├── drops.rb # Liquid Drops (MemberDrop, TaskDrop, CommentDrop) for type-safe template access
228
+ │ │ ├── templates/ # Liquid templates for Shortcut events + spawn templates
229
+ │ │ └── test/ # Shortcut monitor, probe, spawner tests
230
+ │ ├── bugsnag/
231
+ │ │ ├── spawner.rb # Polls Bugsnag errors API for open errors to spawn agents
232
+ │ │ ├── templates/ # Liquid templates for Bugsnag spawn events
233
+ │ │ └── test/ # Bugsnag spawner tests
234
+ │ ├── shell/
235
+ │ │ ├── monitor.rb # Runs a shell command each tick, dispatches on exit status
236
+ │ │ ├── templates/ # Liquid templates for shell events
237
+ │ │ └── test/ # Shell monitor tests
238
+ │ ├── slack/
239
+ │ │ ├── drops.rb # Liquid Drops (MessageDrop, UserDrop, ChannelDrop) for Slack events
240
+ │ │ ├── notifier.rb # Posts Block Kit messages to Slack (webhook or API)
241
+ │ │ ├── spawner.rb # Polls Slack channel for new messages to spawn agents
242
+ │ │ ├── thread_monitor.rb # Polls Slack thread for replies, injects as high-priority events
243
+ │ │ ├── templates/ # Liquid templates for Slack spawn + thread events
244
+ │ │ └── test/ # Slack notifier, spawner, thread monitor, drops tests
245
+ │ ├── datadog/
246
+ │ │ ├── notifier.rb # Sends events + metrics to Datadog via DogStatsD (UDP)
247
+ │ │ ├── spawner.rb # Polls Datadog Error Tracking API for open errors to spawn agents
248
+ │ │ ├── alert_spawner.rb # Polls Datadog Monitors API for triggered alerts to spawn agents
249
+ │ │ ├── alert_monitor.rb # Polls a specific Datadog monitor for status changes
250
+ │ │ ├── alert_goal.rb # Goal that completes when a Datadog monitor recovers to OK
251
+ │ │ ├── templates/ # Liquid templates for Datadog spawn + monitor events
252
+ │ │ └── test/ # Datadog notifier, spawner, alert spawner, alert monitor, alert goal tests
253
+ │ └── honeybadger/
254
+ │ ├── notifier.rb # Sends structured events to Honeybadger Insights (HTTP)
255
+ │ ├── spawner.rb # Polls Honeybadger Faults API for unresolved faults to spawn agents
256
+ │ ├── templates/ # Liquid templates for Honeybadger spawn events
257
+ │ └── test/ # Honeybadger notifier + spawner tests
258
+ ├── test/
259
+ │ ├── test_helper.rb # Minitest setup, TEST_HOME tmpdir, silenced logger
260
+ │ ├── buffer/
261
+ │ │ └── client_test.rb # Buffer::Client transport selection + Unix socket tests
262
+ │ ├── hosted/
263
+ │ │ ├── bridge_test.rb # Hosted::Bridge base class tests
264
+ │ │ ├── mcp_proxy_test.rb # Hosted::McpProxy stdio-to-HTTP proxy tests
265
+ │ │ ├── buffer/
266
+ │ │ │ ├── client_test.rb # Hosted::Buffer::Client relay routing tests
267
+ │ │ │ ├── relay_test.rb # Hosted::Buffer::Relay WebSocket relay tests
268
+ │ │ │ └── relay_store_test.rb # Hosted::Buffer::RelayStore registry tests
269
+ │ │ ├── attach/
270
+ │ │ │ ├── relay_test.rb # Hosted::Attach::Relay relay/mux tests
271
+ │ │ │ └── relay_store_test.rb # Hosted::Attach::RelayStore registry tests
272
+ │ │ └── control/
273
+ │ │ └── client_test.rb # Hosted::Control::Client HTTPS tests
274
+ │ ├── control/
275
+ │ │ └── client_test.rb # Control::Client transport selection + Unix socket tests
276
+ │ ├── agent/
277
+ │ │ └── runtimes/
278
+ │ │ └── local_test.rb # Agent::Runtime::Local tests
279
+ │ ├── integration/ # Cross-component integration tests
280
+ │ │ ├── test_helper.rb # Integration test helper (RecordingRuntime, Thor parser, etc.)
281
+ │ │ ├── spawn_pipeline_test.rb # Spawn command construction ↔ CLI parsing ↔ PtyProxy kwargs
282
+ │ │ ├── server_agent_lifecycle_test.rb # Server ↔ agent IPC lifecycle (register, unregister, monitors)
283
+ │ │ ├── injection_flow_test.rb # Injector → Buffer::Client → Buffer::Server → InjectionQueue
284
+ │ │ └── harness/ # Test harness scripts for subprocess tests
285
+ │ │ └── fake_agent.rb # Dual-mode: records ARGV/env (basic) or full IPC lifecycle (with signal file)
286
+ │ ├── environment_executor_test.rb # EnvironmentExecutor tests
287
+ │ ├── setup_test.rb # Setup core logic tests
288
+ │ ├── setup_config_test.rb # Setup config generation tests
289
+ │ └── *_test.rb # One file per core lib class
290
+ ├── docs/
291
+ │ ├── README.md # Documentation index (Diataxis framework)
292
+ │ ├── tutorials/ # Step-by-step learning guides
293
+ │ │ ├── getting-started.md # First injection experience
294
+ │ │ ├── first-spawner-pipeline.md # Issue-to-PR automation
295
+ │ │ └── team-workflow.md # Multi-repo agent teams
296
+ │ ├── how-to/ # Task-oriented recipes
297
+ │ │ ├── customize-templates.md # Template override system
298
+ │ │ ├── configure-notifications.md # Notification recipes
299
+ │ │ └── troubleshooting.md # Diagnosing common issues
300
+ │ ├── explanation/ # Conceptual discussions
301
+ │ │ ├── injection-model.md # How the PTY proxy and injection work
302
+ │ │ ├── spawner-workflows.md # Workflow design, composition, VCS inheritance
303
+ │ │ └── agent-teams.md # Team coordination model
304
+ │ ├── reference/ # Lookup information
305
+ │ │ ├── templates.md # Template variables per event type
306
+ │ │ └── event-types.md # All event types and payloads
307
+ │ └── planning/ # Internal planning docs (not published)
308
+ ├── skills/
309
+ │ ├── superkick-new-driver/ # Step-by-step guide for adding a new AI CLI driver
310
+ │ ├── superkick-new-goal/ # Step-by-step guide for adding a new goal type
311
+ │ ├── superkick-new-monitor/ # Step-by-step guide for adding a new monitor
312
+ │ ├── superkick-new-notifier/ # Step-by-step guide for adding a new notifier
313
+ │ ├── superkick-new-spawner/ # Step-by-step guide for adding a new spawner
314
+ │ └── superkick-update-docs/ # Guide for updating documentation after implementation changes
315
+ ├── .github/workflows/ci.yml # GitHub Actions: test matrix + lint
316
+ ├── Rakefile # rake test, rake standard, rake standard:fix
317
+ └── superkick.gemspec
318
+ ```
319
+
320
+ ## Key classes
321
+
322
+ ### `Superkick::EnvironmentExecutor` (`environment_executor.rb`)
323
+ Agent-side action executor for server-driven probes. The server sends an array of action hashes describing what environment data it needs, and the executor runs each action in the agent's working directory.
324
+
325
+ Supported actions:
326
+ - `:git_branch` → current git branch name (String or nil)
327
+ - `:git_remotes` → parsed git remotes (Array of `{name:, url:}` or nil)
328
+ - `:file_exists` → file existence checks (Hash of `{path => Boolean}`)
329
+
330
+ Constructor: `initialize(working_dir:)`.
331
+ Key method: `execute(actions)` → merged results Hash keyed by action name.
332
+
333
+ ### `Superkick::PtyProxy` (`pty_proxy.rb`)
334
+ Wraps the AI CLI in a PTY. Three threads:
335
+ - `proxy_stdin` — reads raw keystrokes from `$stdin`, feeds `InputBuffer`, forwards to pty master
336
+ - `proxy_output` — reads CLI output, writes to `$stdout`, tees to `OutputLogger` and `SessionRecorder`, updates `@last_output_at` and `@at_prompt`, calls `update_guards`, feeds `CostExtractor` for cost data extraction
337
+ - `drain_inject_queue` — blocks on `@inject_queue.pop`, writes bytes to pty master
338
+
339
+ On startup it creates a `Buffer::Server`, an `OutputLogger`, and (when `session_recording_enabled`) a `SessionRecorder`. Registration is multi-step: first sends `register` to the server (agent metadata and `working_dir: Dir.pwd`, no monitors), receives an `environment_request` containing action hashes, executes them via `EnvironmentExecutor`, then sends `register_environment` with the results so the server can run probes and start monitors. When `server_type` is `:hosted`, it also starts a `Hosted::Buffer::Bridge` and a `Hosted::Attach::Bridge` that maintain persistent WebSocket connections to the hosted server, bridging remote buffer commands and attach sessions respectively. In hosted mode, the `SessionRecorder` is wired with a `Store::Http` that uploads the completed recording on close. Accepts optional `agent_id:`, `headless:`, `team_id:`, `role:`, and `team_log:` parameters. The `agent_id:` and `headless:` flags are set by `Spawn::AgentSpawner` via hidden CLI options. On exit it closes the recorder and logger and unregisters.
340
+
341
+ When spawning the AI CLI process, PtyProxy sets `SUPERKICK_AGENT_ID` in the child process environment so the MCP server subprocess can identify the agent without requiring the AI to pass its own ID.
342
+
343
+ Cost extraction: the `proxy_output` thread feeds ANSI-stripped output text to a lazily-initialized `CostExtractor` (only if the driver has `cost_patterns`). Extracted cost deltas are reported to the server via `report_cost` IPC requests (best-effort, failures are silently logged).
344
+
345
+ ### `Superkick::OutputLogger` (`output_logger.rb`)
346
+ Manages writing PTY output to a per-agent log file at `~/.superkick/logs/<agent-id>.log`. Raw bytes (including ANSI escape codes) are written in append mode with sync flushing so `tail -f` works in real time. Simple size-based rotation: one rotated file (`*.1`). When the log exceeds `MAX_SIZE` (10 MB), the current file becomes `.1` and a new file starts.
347
+
348
+ ### `Superkick::SessionRecorder` (`session_recorder.rb`)
349
+ Records complete, timestamped agent sessions (input + output) in the asciicast v2 format at `~/.superkick/recordings/<agent-id>.cast`. Thread-safe via Mutex (three PtyProxy threads write concurrently). Uses monotonic clock for elapsed time computation. Simple size-based rotation (same pattern as `OutputLogger`, `.1` suffix). On `close`, calls `@store.upload` if a store is present (hosted mode). Lifecycle follows `OutputLogger`: create → start → record_* → close.
350
+
351
+ Inner classes:
352
+ - `SessionRecorder::Store` — abstract upload interface for completed recordings
353
+ - `SessionRecorder::Store::Noop` — local mode, recordings stay on disk
354
+ - `SessionRecorder::Store::Http` — hosted mode, uploads `.cast` file to hosted server via `POST /api/v1/agents/{agent_id}/recording`. Best-effort — failures are logged as warnings
355
+
356
+ ### `Superkick::Buffer::Server` (`buffer/server.rb`)
357
+ Per-agent Unix socket server running inside the `superkick agent` process. Server-side components send buffer commands here (in local mode directly, in hosted mode via `Hosted::Buffer::Relay` → `Hosted::Buffer::Bridge`). Accepts six commands:
358
+ - `get` — returns current partial input from `InputBuffer`
359
+ - `guards_active` — returns whether any injection guards are active
360
+ - `inject` — enqueues base64-encoded bytes into `PtyProxy#inject_queue`
361
+ - `idle_state` — returns `{ seconds_idle:, at_prompt: }` from `PtyProxy`
362
+ - `enqueue_injection` — enqueues a rendered prompt into the agent-side `InjectionQueue` with priority, TTL, and supersede key
363
+ - `ping` — health check, returns `{ ok: true }`
364
+
365
+ ### `Superkick::Buffer::Client` (`buffer/client.rb`)
366
+ Default Unix socket implementation of the buffer client. All server-side components (`Injector`, `Spawn::Injector`, `CostPoller`, `Supervisor`, `Control::Server`) use this class via the `buffer_client:` constructor parameter.
367
+
368
+ The `Buffer` module extends `ClientRegistry` and provides `Buffer.client_from(store:, config:, relay_store:)` — returns a `Buffer::Client` (Unix socket) for `:local` server type, or the registered hosted client for `:hosted`. Hosted subclasses register via `Buffer.register_client(:hosted, klass)`. The `relay_store:` is only needed in hosted mode.
369
+
370
+ Each client class defines a `from` class method that knows how to construct itself from config kwargs.
371
+
372
+ The base class is the Unix socket implementation. Each request opens a fresh `UNIXSocket`, sends one JSON message via `Connection`, reads one response, and closes. Resolves the socket path from the `AgentStore`.
373
+
374
+ Constructor: `initialize(store:)`.
375
+
376
+ Defines a shared exception class:
377
+ - `Buffer::Client::AgentUnreachable` — raised when the agent's buffer endpoint is unreachable.
378
+
379
+ Key methods:
380
+ - `send_command(agent_id, command, **params)` — fire-and-forget (logs and swallows `AgentUnreachable`)
381
+ - `probe(agent_id)` — ping the agent to verify it's alive
382
+ - `request(agent_id, command, **params)` — send a command and return the parsed response
383
+ - `reachable?(agent_id)` — check if the agent is reachable via the buffer channel
384
+
385
+ ### `Superkick::Hosted::Buffer::Client` (`hosted/buffer/client.rb`)
386
+ WebSocket relay subclass of `Buffer::Client`. Routes buffer commands to remote agents through their `Hosted::Buffer::Relay` WebSocket connections. Overrides `send_command` to use the relay's fire-and-forget method instead of the request/response path. Self-registers as the `:hosted` client on `Buffer`.
387
+
388
+ Constructor: `initialize(relay_store:)`.
389
+
390
+ ### `Superkick::Hosted::Buffer::Relay` (`hosted/buffer/relay.rb`)
391
+ Server-side relay that bridges server components to a remote agent over a persistent WebSocket connection. In hosted mode, the agent opens a WebSocket to the server, and the server creates a `Relay` for each connected agent. Server-side components send buffer commands through the `Relay`, which forwards them over the WebSocket and waits for the reply (with a 5-second timeout).
392
+
393
+ Key methods:
394
+ - `attach(websocket)` — called when an agent connects via WebSocket
395
+ - `detach` — called when the WebSocket connection closes; wakes pending waiters
396
+ - `connected?` — returns whether a WebSocket is currently attached
397
+ - `request(command, **params)` — send a command and wait for the response (request/response pattern with `request_id` correlation)
398
+ - `send_command(command, **params)` — fire-and-forget
399
+ - `handle_message(message)` — routes incoming messages to pending request waiters or handles unsolicited agent events (e.g. `injection_result`)
400
+
401
+ ### `Superkick::Hosted::Buffer::RelayStore` (`hosted/buffer/relay_store.rb`)
402
+ Thread-safe registry of `Hosted::Buffer::Relay` instances, one per connected agent. Created at server startup in hosted mode and passed to `Hosted::Buffer::Client`.
403
+
404
+ Key methods:
405
+ - `get_or_create(agent_id)` — get or create a relay for the given agent
406
+ - `get(agent_id)` — get an existing relay (nil if none)
407
+ - `remove(agent_id)` — remove a relay when the agent disconnects permanently
408
+
409
+ ### `Superkick::Hosted::Bridge` (`hosted/bridge.rb`)
410
+ Abstract base class for agent-side WebSocket bridges that connect local agent components to the hosted server over a persistent WebSocket. Provides shared WebSocket lifecycle: TCP/TLS connect, auth handshake, read loop, and reconnection with exponential backoff.
411
+
412
+ Uses a **raw WebSocket protocol** (NOT ActionCable):
413
+ 1. Client connects to a WebSocket endpoint
414
+ 2. Client sends auth JSON text frame: `{"type":"auth","agent_id":"...","api_key":"..."}`
415
+ 3. Server responds: `{"type":"welcome"}` or `{"type":"error","message":"..."}`
416
+ 4. After auth, data frames flow (text or binary depending on channel)
417
+
418
+ Uses the `websocket-driver` gem for WebSocket framing. Constants: `RECONNECT_DELAYS = [1, 2, 4, 8, 16, 30]`, `PING_INTERVAL = 30`.
419
+
420
+ Constructor: `initialize(agent_id:, server_url:, api_key:)`.
421
+
422
+ Key methods:
423
+ - `start` — start the bridge in a background thread
424
+ - `stop` — cleanly close the connection and stop reconnecting
425
+
426
+ Subclass hooks (override as needed):
427
+ - `websocket_path` → URL path (e.g. `"/api/v1/agents/#{@agent_id}/buffer/ws"`)
428
+ - `channel_name` → log tag (e.g. `"buffer:ws"`)
429
+ - `handle_server_message(message)` → process parsed JSON text messages
430
+ - `handle_binary_message(data)` → process binary messages (optional)
431
+ - `on_authenticated` → called after welcome (optional)
432
+
433
+ Sending helpers for subclasses: `send_text(data)`, `send_binary(data)`.
434
+
435
+ Contains `SocketWrapper` inner class that `websocket-driver` requires (responds to `#url` and `#write`).
436
+
437
+ ### `Superkick::Hosted::Buffer::Bridge` (`hosted/buffer/bridge.rb`)
438
+ Subclass of `Superkick::Hosted::Bridge`. Agent-side bridge that connects the local `Buffer::Server` to the hosted server. Commands from the server arrive as JSON text frames and are dispatched to a `command_handler` (which delegates to the local `Buffer::Server` command handlers). Responses flow back the same way.
439
+
440
+ Constructor: `initialize(agent_id:, server_url:, api_key:, command_handler:)`.
441
+
442
+ ### `Superkick::Attach::Protocol` (`attach/protocol.rb`)
443
+ Binary frame protocol for the attach system. Defines frame types: OUTPUT, INPUT, META, RESIZE, HISTORY, ERROR, HELLO, COMMAND, NOTIFY.
444
+
445
+ ### `Superkick::Attach::Server` (`attach/server.rb`)
446
+ Per-agent attach endpoint with RW exclusivity, force-takeover, idle timeout, and broadcast callbacks.
447
+
448
+ ### `Superkick::Attach::Client` (`attach/client.rb`)
449
+ Default Unix socket implementation for CLI-side attach sessions. Contains session logic (escape detection, raw terminal mode, output streaming, keystroke forwarding). Wraps a `UNIXSocket` with `Attach::Protocol` binary framing and connects eagerly in the constructor.
450
+
451
+ The `Attach` module extends `ClientRegistry` and provides `Attach.client_from(agent_id:, config:, mode:, escape_key:, force:)` — returns an `Attach::Client` (Unix socket) for `:local` server type, or the registered hosted client for `:hosted`. Hosted subclasses register via `Attach.register_client(:hosted, klass)`.
452
+
453
+ Constructor: `initialize(socket_path:, mode:, escape_key:, force:)` — stores session parameters and connects to the Unix socket.
454
+
455
+ Key methods:
456
+ - `run` — blocking session loop. Returns exit code (0 on clean detach, 1 on error).
457
+ - `read_frame` — read one `Attach::Protocol` frame
458
+ - `write_frame(type, data)` — write one `Attach::Protocol` frame
459
+ - `write_json_frame(type, hash)` — write a JSON-encoded frame
460
+ - `close` — close the underlying connection
461
+
462
+ ### `Superkick::Hosted::Attach::Client` (`hosted/attach/client.rb`)
463
+ WebSocket subclass of `Attach::Client`. Connects to the hosted server, authenticates via the raw WebSocket auth handshake (same as `Hosted::Bridge`), then streams `Attach::Protocol` binary frames over WebSocket messages. Uses a background reader thread and a `Queue` for frame buffering. Self-registers as the `:hosted` client on `Attach`.
464
+
465
+ Constructor: `initialize(server_url:, api_key:, agent_id:, **session_args)`.
466
+
467
+ Contains `SocketWrapper` inner class that `websocket-driver` requires.
468
+
469
+ ### `Superkick::Hosted::Attach::Relay` (`hosted/attach/relay.rb`)
470
+ Server-side relay/mux for remote attach sessions over raw WebSocket. Manages one agent WebSocket and multiple user WebSockets, routing `Attach::Protocol` binary frames between them. The relay enforces RW exclusivity, buffers output for replay on new connections, and handles idle timeout auto-demotion.
471
+
472
+ WebSocket objects must respond to `#send_binary(data)` and `#close`. The relay is transport-agnostic — the hosted server wraps its WebSocket library to provide this interface.
473
+
474
+ Constructor: `initialize(agent_id:, config: {})`. Config keys: `replay_buffer_size` (default 65536), `max_connections` (default 10), `rw_idle_timeout` (default 300).
475
+
476
+ Key methods:
477
+ - `attach_agent(websocket)` — called when the agent connects via WebSocket
478
+ - `detach_agent` — called when the agent WebSocket closes; notifies all users with an ERROR frame
479
+ - `agent_connected?` — returns whether an agent WebSocket is attached
480
+ - `add_user(websocket, mode:, force: false)` — add a user connection; enforces RW exclusivity and max connections; sends META frame and replays history
481
+ - `remove_user(websocket)` — remove a user connection
482
+ - `handle_agent_frame(data)` — route a binary frame from the agent (buffers OUTPUT in history, broadcasts to all users)
483
+ - `handle_user_frame(websocket, data)` — route a binary frame from a user (INPUT/RESIZE forwarded to agent for RW user only; COMMAND handled locally for mode switching)
484
+ - `stop` — kill the idle check thread
485
+ - `user_count` — number of connected users
486
+ - `rw_user` — the current RW user websocket (or nil)
487
+
488
+ COMMAND handling: `promote_rw`, `force_promote_rw`, `demote_ro` are handled locally within the relay. `claim` and `unclaim` are forwarded to the agent.
489
+
490
+ ### `Superkick::Hosted::Attach::RelayStore` (`hosted/attach/relay_store.rb`)
491
+ Thread-safe registry of `Hosted::Attach::Relay` instances, one per agent. Created at server startup and passed to `Control::Server`. The hosted server wires WebSocket connections to relays via `get_or_create` on agent connect and `remove` on permanent disconnect.
492
+
493
+ Constructor: `initialize(config: {})`. Config is passed through to each relay.
494
+
495
+ Key methods:
496
+ - `get_or_create(agent_id)` — get or create a relay for the given agent
497
+ - `get(agent_id)` — get an existing relay (nil if none)
498
+ - `remove(agent_id)` — remove and stop a relay when the agent disconnects permanently
499
+ - `each(&block)` — iterate over all relays
500
+ - `size` — number of relays
501
+
502
+ ### `Superkick::Hosted::Attach::Bridge` (`hosted/attach/bridge.rb`)
503
+ Subclass of `Superkick::Hosted::Bridge`. Agent-side bridge that connects the local `Attach::Server` to the hosted server over a persistent WebSocket for streaming PTY output to remote attach users.
504
+
505
+ Output forwarding: registers a broadcast callback on `Attach::Server` via `on_authenticated` so that PTY output is forwarded to the relay as OUTPUT frames.
506
+
507
+ Remote input: binary WebSocket messages from the relay (INPUT/RESIZE frames from remote users) are dispatched to `Attach::Server#handle_remote_input`.
508
+
509
+ Constructor: `initialize(agent_id:, server_url:, api_key:, attach_server:)`.
510
+
511
+ WebSocket path: `/api/v1/agents/#{agent_id}/attach/agent/ws`.
512
+
513
+ ### `Superkick::Hosted::McpProxy` (`hosted/mcp_proxy.rb`)
514
+ Transparent stdio-to-HTTP proxy for hosted mode. In hosted mode, `superkick mcp start` launches this instead of `McpServer`. Reads JSON-RPC lines from stdin and POSTs them to `{server_url}/api/v1/mcp` with `Authorization: Bearer {api_key}` and `X-Superkick-Agent-Id: {agent_id}` headers. Writes responses back to stdout.
515
+
516
+ The AI CLI sees a normal stdio MCP server — it has no idea it's talking to a remote endpoint. This avoids teaching every driver about remote MCP URL configuration.
517
+
518
+ Handles three HTTP response types:
519
+ - `application/json` — single JSON-RPC response, written as one line to stdout
520
+ - `text/event-stream` — SSE stream, each `message` event data written as a line
521
+ - `202 Accepted` — notification acknowledged, nothing written
522
+
523
+ Error handling: on HTTP errors, connection failures, or timeouts, returns a JSON-RPC error response (code `-32603`) for requests (messages with `id`). Notifications (no `id`) are silently dropped on error.
524
+
525
+ Tracks `Mcp-Session-Id` from server responses and sends it back on subsequent requests.
526
+
527
+ Constructor: `initialize(connection: nil)`. Reads `SUPERKICK_AGENT_ID` from the environment (same as `McpServer`). Reads server URL and API key from `Superkick.config.server`. The `connection:` parameter accepts a pre-built Faraday connection for test injection.
528
+
529
+ ### `Superkick::InputBuffer` (`input_buffer.rb`)
530
+ Tracks the user's partial input (characters typed since the last Enter) and manages named injection guards. Guards are set by `update_guards` when output matches a guard pattern; they block injection until cleared (either on submit or manually).
531
+
532
+ ### `Superkick::InjectionQueue` (`injection_queue.rb`)
533
+ Agent-side queue that manages injection precondition checks and PTY writes locally, eliminating network round-trips between server and agent. Entries have TTL, supersede-by-key, and priority semantics.
534
+
535
+ Three priority levels:
536
+ - `:high` — spawn kickoff prompts (processed first)
537
+ - `:normal` — monitor events (default)
538
+ - `:low` — cost polling injections
539
+
540
+ Constructor requires `pty_proxy:`, `idle_threshold:`, and `inject_clear_delay:` — config values are injected by `PtyProxy` at construction time (no direct `Superkick.config` access).
541
+
542
+ The drain loop runs agent-side, checking idle state and guards locally with zero network latency. When an injection succeeds or is skipped/expired, the queue reports the result back to the server via `injection_result` IPC so the server can fire notifications.
543
+
544
+ Constants: `MAX_SIZE = 50`, `DEFAULT_TTL = 300` seconds. Entries that exceed TTL are silently expired. When the queue is full, lowest-priority entries are evicted.
545
+
546
+ ### `Superkick::Injector` (`injector.rb`)
547
+ Orchestrates one injection attempt:
548
+ 1. Checks agent is reachable via `Buffer::Client`
549
+ 2. Renders Liquid template via `TemplateRenderer`
550
+ 3. Sends `enqueue_injection` to the agent via `Buffer::Client` (fire-and-forget)
551
+ 4. Returns `:enqueued` or `:skipped` (no longer checks gates or writes to PTY directly)
552
+
553
+ Accepts a `buffer_client:` constructor parameter for transport-agnostic buffer communication. The actual precondition checks (idle state, guards) and PTY writes are handled agent-side by `InjectionQueue`. Notification dispatch happens asynchronously when the agent reports injection results back via IPC.
554
+
555
+ **Event-level injection hints:** Monitors can include optional keys in the event hash to control queue behavior:
556
+ - `injection_priority` — `:high`, `:normal` (default), or `:low`
557
+ - `injection_ttl` — seconds before the entry expires (overrides the per-monitor `injection_ttl` config)
558
+ - `injection_supersede_key` — custom key for supersede semantics (defaults to `monitor_name`)
559
+
560
+ Built-in events with non-default hints:
561
+
562
+ | Event | Priority | TTL | Supersede key |
563
+ |-------|----------|-----|---------------|
564
+ | `ci_success` (GitHub, CircleCI) | `:normal` | 60s | `"ci_status"` |
565
+ | `ci_failure` (GitHub, CircleCI) | `:normal` | 300s | `"ci_status"` |
566
+ | `pr_review` (CHANGES_REQUESTED) | `:high` | 300s | default |
567
+ | `story_blocker` | `:high` | 300s | `"story_blocker_status"` |
568
+ | `story_unblocked` | `:normal` | 120s | `"story_blocker_status"` |
569
+ | `shell_success` | `:normal` | 60s | default |
570
+ | `team_digest` | `:normal` | 120s | default |
571
+ | `team_digest` (with blockers) | `:high` | 120s | default |
572
+ | `teammate_message` | `:high` | 900s | default |
573
+ | `slack_reply` | `:high` | 900s | default |
574
+
575
+ ### `Superkick::Notifier` (`notifier.rb`)
576
+ Abstract base class for notification channels. Notifications fire on two kinds of events:
577
+ 1. **Monitor injections** — from `Injector` after a successful injection
578
+ 2. **Agent lifecycle events** — from `Supervisor`/`Spawn::AgentSpawner` when spawned agents change state
579
+
580
+ Each notifier is isolated — one failure doesn't prevent others from firing.
581
+
582
+ Subclasses implement:
583
+ - `self.type` → unique Symbol (e.g. `:terminal_bell`, `:command`)
584
+ - `self.templates_dir` → (optional) path to bundled Liquid templates directory (default `nil`)
585
+ - `notify(payload)` → called with a Hash containing `event_type`, `monitor_type`, `monitor_name`, `agent_id`, `message`, and `rendered_prompt`
586
+ - `stateful?` → (optional) return `true` if this notifier tracks state across events. Stateful notifiers persist for the server's lifetime and receive `agent_finished` callbacks.
587
+ - `agent_finished(agent_id:)` → (optional) called when an agent's event stream is definitively over. Stateful notifiers use this to clean up per-agent state.
588
+ - `self.setup_label` → (optional) short name for the `superkick setup` wizard
589
+ - `self.setup_config` → (optional) YAML snippet string for config generation
590
+
591
+ **Template support:** Notifiers that include the `Superkick::Liquid` DSL can use structured templates with block tags, bodyless tags, and accumulators. Two instance methods are provided:
592
+ - `render_notification(payload)` → resolves a template via `TemplateRenderer.resolve_notification`, renders it with the notifier's `Liquid::Environment` (including any `liquid do` filters/blocks/tags), and returns a `NotifierTemplate::Result`. Returns `nil` if no template is found. Context fields from the payload are promoted to top-level template assigns, so templates use `{{ team.id }}` rather than `{{ context.team.id }}`.
593
+ - `build_accumulator` → constructs the accumulator for block tag rendering. Returns a `context_class` instance if defined via `liquid do`, otherwise an empty Hash.
594
+
595
+ The `liquid do` DSL supports three tag primitives:
596
+ - `block :name do |ctx, text, attrs| ... end` — declares a Liquid block tag (`{% name %}...{% endname %}`). The handler receives the accumulator, rendered body text, and parsed attributes.
597
+ - `tag :name do |ctx, attrs| ... end` — declares a bodyless Liquid tag (`{% name %}`). The handler receives the accumulator and parsed attributes (no body, no end tag).
598
+ - `filter :name do |input, ...| ... end` — declares a Liquid filter.
599
+
600
+ Attribute values are resolved as Liquid expressions at render time (matching Liquid's built-in `{% render %}` tag): quoted values (`key: "literal"`) stay as literal strings, bare identifiers (`key: my_url`) are resolved from the template context, and dotted paths (`key: issue.url`) traverse nested objects and Drops. Undefined variables are omitted from the attributes hash.
601
+
602
+ Registry follows the same pattern as Driver and Monitor:
603
+ ```ruby
604
+ Superkick::Notifier.register(MyNotifier)
605
+ Superkick::Notifier.lookup(:my_notifier)
606
+ Superkick::Notifier.registered
607
+ ```
608
+
609
+ **Lifecycle event types** (`Notifier::LIFECYCLE_EVENTS`):
610
+ - `agent_spawned` — a new agent was started by a spawner
611
+ - `agent_completed` — a spawned agent reached its goal
612
+ - `agent_failed` — a spawned agent's goal failed
613
+ - `agent_timed_out` — a spawned agent hit `max_duration`
614
+ - `agent_blocked` — the AI signaled it needs help (errored status)
615
+ - `agent_stalled` — a spawned agent has been idle past the stall threshold
616
+ - `agent_terminated` — a spawned agent was manually stopped
617
+ - `agent_claimed` — a user claimed a spawned agent for manual intervention
618
+ - `agent_unclaimed` — a user released a claimed agent back to autonomous operation
619
+ - `agent_pending_approval` — a spawn requires approval before proceeding
620
+ - `workflow_triggered` — a workflow spawn was fired after a goal reached a terminal status
621
+ - `workflow_iterations_exceeded` — a workflow spawn was blocked because the target spawner reached its `max_iterations` limit
622
+ - `budget_warning` — an agent's cost is approaching its budget limit
623
+ - `budget_exceeded` — an agent's cost has exceeded its budget limit
624
+ - `team_created` — a new team was created (fires when a team lead agent is spawned)
625
+ - `team_completed` — a team's lead agent completed its goal (all team members terminated)
626
+ - `team_failed` — a team's lead agent failed (all team members terminated)
627
+ - `team_timed_out` — a team's lead agent exceeded `max_duration` (all team members terminated)
628
+ - `worker_spawned` — a worker agent was spawned by a team lead via `superkick_spawn_worker`
629
+ - `teammate_message` — a team member sent a direct message to another teammate
630
+ - `teammate_blocker` — a team member reported a blocker via `superkick_post_update`
631
+ - `attach_promoted` — an attach client was promoted to read-write mode
632
+ - `attach_demoted` — an attach client was demoted to read-only mode
633
+ - `attach_idle_timeout` — an attach client was demoted due to idle timeout
634
+ - `attach_force_takeover` — an attach client's read-write was taken over by another client
635
+ - `agent_update` — a team member posted a status update via `superkick_post_update`
636
+ - `artifact_published` — a team member published an artifact via `superkick_publish_artifact`
637
+
638
+ Built-in notifiers:
639
+ - **`TerminalBell`** (`:terminal_bell`) — writes BEL (`\a`) to stderr. Zero-config default. Most terminals respond with a visual or audible alert.
640
+ - **`Command`** (`:command`) — runs a shell command via `ProcessRunner.spawn`. Event details are passed as `SUPERKICK_*` environment variables (`SUPERKICK_EVENT_TYPE`, `SUPERKICK_MONITOR_TYPE`, `SUPERKICK_MONITOR_NAME`, `SUPERKICK_AGENT_ID`, `SUPERKICK_MESSAGE`). Supports notification templates with an `{% env key: "name" %}` block tag that populates additional `SUPERKICK_<NAME>` env vars (uppercased, auto-prefixed). Text outside `{% env %}` blocks becomes `SUPERKICK_BODY`. When no template exists, `SUPERKICK_BODY` is not set. No bundled templates — users create per-event templates at `~/.superkick/templates/notifications/command/<event_type>.liquid`. Commands are killed after `timeout_seconds` (default 10).
641
+ - **`Integrations::Slack::Notifier`** (`:slack`) — posts a rich Block Kit message to Slack. Supports two authentication modes: **Incoming Webhook** (`webhook_url:`) for single-channel zero-token setup, and **Web API** (`token:` + `channel:`) for dynamic channel targeting via `chat.postMessage`. Block Kit structure is defined via Liquid templates with custom block tags and bodyless tags covering all Block Kit block types. **Block tags** (have body content): `{% header %}` (plain text, emoji-enabled), `{% section %}` (mrkdwn), `{% context %}` (mrkdwn metadata), `{% image url: "..." alt_text: "..." %}` (body becomes optional title), `{% button url: "..." style: "primary" %}` (auto-groups consecutive buttons into an actions block), `{% fields %}` (each non-empty line becomes a mrkdwn field in a section), `{% rich_text %}` (body becomes text in a rich_text_section), `{% video url: "..." thumbnail_url: "..." alt_text: "..." %}` (body becomes title), `{% input action_id: "..." type: "plain_text_input" placeholder: "..." %}` (body becomes label, supports `dispatch_action: "true"`). **Bodyless tags** (self-closing, no end tag): `{% divider %}` (horizontal rule), `{% file external_id: "..." %}` (source defaults to `"remote"`). **Filters:** `event_emoji`, `format_title`, `join_present`. The `event_emoji` filter resolves emoji in three tiers: explicit `EMOJI_MAP` lookup, then keyword substring matching (e.g. `deployment_completed` matches `completed` → `:white_check_mark:`), then `:bell:` default. The bundled `default.liquid` template produces a header with event emoji, a section with the message body, and a context block with agent/monitor/team metadata. Users can override per-event or globally at `~/.superkick/templates/notifications/slack/`. **Stateful in API mode** — tracks `thread_ts` per thread key. When an agent has a `team_id` in its context, messages are threaded by team (all agents in the same team share a thread). Otherwise, messages are threaded by agent. Webhook mode is stateless (Slack webhooks don't return a `ts`). Accepts an optional `thread_ts:` constructor parameter (`@initial_thread_ts`) that pre-seeds thread correlation — when no `thread_ts` exists in the state store and no `seed_thread_ts` from payload context, falls back to `@initial_thread_ts`. This is used by the `_spawn_notifiers` mechanism to create per-agent Slack notifiers that thread under a specific message. Uses Faraday for HTTP (already a dependency). Config: `webhook_url` or `token` + `channel`.
642
+ - **`Integrations::Datadog::Notifier`** (`:datadog`) — sends events and metrics to Datadog via DogStatsD (UDP). Events appear in the Event Explorer; metrics (`<prefix>.event` counter, `<prefix>.agent.duration` gauge, `<prefix>.agent.cost_usd` gauge) appear in Metrics Explorer. **Tag cardinality is managed**: events include all tags (`agent_id`, `team_id`, `event_type`, `monitor_type`, `monitor_name`, `team_role`, `spawner_name`), while metrics use only low-cardinality tags (`event_type`, `monitor_type`, `monitor_name`, `team_role`, `spawner_name`) to avoid billing explosions from unbounded dimensions. **Stateful** — tracks `spawned_at` per agent to compute duration on terminal events (also uses `spawned_at` from context when available). Alert type mappings: terminal events → `"error"`, warning events → `"warning"`, informational events (`agent_update`, `artifact_published`, `agent_spawned`, `worker_spawned`, etc.) → `"info"`. No gem dependency — uses raw UDP datagrams in the standard DogStatsD protocol. Config: `statsd_host` (default `"localhost"`), `statsd_port` (default `8125`), `prefix` (default `"superkick"`), `tags` (array of global tags).
643
+ - **`Integrations::Honeybadger::Notifier`** (`:honeybadger`) — sends structured events to Honeybadger Insights via the Events API (HTTP). Events include `event_type`, `agent_id`, `severity` (mapped from event type), `message`, monitor metadata, `duration_seconds` on terminal events, plus context fields (`team_id`, `team_role`, `spawner_name`, `cost_usd`) when available. Uses the payload `timestamp` as the event timestamp. Severity mappings: terminal failure events → `"error"`, warning events → `"warning"`, informational events (`agent_update`, `artifact_published`, `agent_spawned`, `worker_spawned`, etc.) → `"info"`. **Stateful** — tracks `spawned_at` per agent to compute duration (also uses `spawned_at` from context when available). Uses Faraday for HTTP (already a dependency). Config: `api_key` (required), `tags` (array of optional tags).
644
+
645
+ Configuration (in `config.yml`):
646
+ ```yaml
647
+ notifications:
648
+ - type: terminal_bell
649
+ - type: command
650
+ run: "notify-send 'Superkick' '$SUPERKICK_MESSAGE'"
651
+ events: # optional — restricts which events fire
652
+ - agent_blocked
653
+ - agent_completed
654
+ - type: slack
655
+ webhook_url: <%= env("SLACK_WEBHOOK_URL") %>
656
+ - type: slack
657
+ token: <%= env("SLACK_BOT_TOKEN") %>
658
+ channel: "#superkick-notifications"
659
+ events:
660
+ - agent_completed
661
+ - agent_failed
662
+ - agent_blocked
663
+ - type: datadog
664
+ statsd_host: localhost
665
+ statsd_port: 8125
666
+ prefix: superkick
667
+ tags:
668
+ - "env:production"
669
+ - type: honeybadger
670
+ api_key: <%= env("HONEYBADGER_API_KEY") %>
671
+ tags:
672
+ - production
673
+ ```
674
+
675
+ When `notifications` is empty or absent, defaults to `[{ type: terminal_bell }]`. When `events:` is absent on a notifier, it receives all events.
676
+
677
+ ### `Superkick::NotifierStateStore` (`notifier_state_store.rb`)
678
+ Injectable key-value state storage for stateful notifiers. Stateful notifiers (Slack, Datadog, Honeybadger) track per-agent state across events (e.g. Slack `thread_ts` for threading, `spawned_at` for duration computation). This class provides an explicit interface for that state, enabling the hosted Rails app to provide a database-backed implementation.
679
+
680
+ Interface:
681
+ - `get(notifier_type, key)` → Hash or nil
682
+ - `put(notifier_type, key, data)` → void (upsert)
683
+ - `delete(notifier_type, key)` → void
684
+
685
+ Built-in implementation:
686
+ - **`NotifierStateStore::Memory`** — thread-safe in-memory storage (Mutex-protected nested Hash). Default for local server mode. State is lost on server restart. Created at server startup and passed through `NotificationDispatcher` → each notifier's `state_store:` constructor parameter.
687
+
688
+ The `Notifier` base class requires `state_store:` in its constructor (no default) and stores it as `@state_store`. The server creates a `NotifierStateStore::Memory` at startup and passes it through; tests must pass it explicitly. Stateful notifiers use `@state_store` instead of managing their own mutex-protected hashes.
689
+
690
+ ### `Superkick::NotificationDispatcher` (`notification_dispatcher.rb`)
691
+ Holds long-lived notifier instances so stateful notifiers can track per-agent state across events (e.g. Slack `thread_ts` for threading). Created once at server startup and passed to all server-side components via constructor injection.
692
+
693
+ Accepts an optional `store:` keyword (AgentStore) to enrich payloads with agent context. Requires a `state_store:` keyword (`NotifierStateStore`) that is passed through to each notifier instance. Accepts an optional `notifiers:` keyword for test injection of pre-built `[notifier, events_filter]` pairs (bypasses config-based construction). Accepts an optional `internal_notifiers:` keyword — an array of notifier instances that always fire on every event (no event filter). Internal notifiers are used for side-effect notifiers like `Team::LogNotifier` that must see all events regardless of user configuration. When a store is provided, `build_payload` looks up the agent and populates a `context` Hash with team info, spawn info, cost data, and rehydrated event context (Drops like `issue`, `story`, plus scalars like `repo`, `branch`).
694
+
695
+ Key methods:
696
+ - `dispatch(event:, agent_id:, rendered_prompt:, message:)` → fires both global notifiers and per-agent notifiers in a background thread. After terminal events, automatically calls `agent_finished` on stateful notifiers and cleans up per-agent notifier instances.
697
+ - `agent_finished(agent_id)` → notifies stateful notifiers that an agent's event stream is over, and removes per-agent notifier instances for the agent. Called automatically after terminal events, but can also be called explicitly.
698
+ - `register_agent_notifiers(agent_id)` → builds notifier instances from the agent's notifier configs (stored on the `Agent` object via `_spawn_notifiers`). Per-agent notifiers are stored in `@agent_notifiers` as `{ agent_id => { name => [notifier, events_filter] } }` and fired alongside global notifiers on every dispatch for that agent. Thread-safe via Mutex.
699
+ - `add_agent_notifier(agent_id, name)` → builds and registers a single per-agent notifier by name. Called by `Control::Server` when `superkick_add_notifier` is invoked on a running agent. Looks up the notifier config from the agent's stored configs.
700
+ - `remove_agent_notifier(agent_id, name)` → removes a single per-agent notifier by name. Calls `agent_finished` on stateful notifiers for proper cleanup. Called by `Control::Server` when `superkick_remove_notifier` is invoked.
701
+
702
+ Terminal event types (`TERMINAL_EVENT_TYPES`): `agent_completed`, `agent_failed`, `agent_timed_out`, `agent_terminated`, `team_completed`, `team_failed`, `team_timed_out`.
703
+
704
+ Payload structure passed to notifiers:
705
+ ```ruby
706
+ {
707
+ event_type: String, # e.g. "agent_completed"
708
+ monitor: MonitorDrop, # Drop with .name and .type (e.g. "github_issues", "spawner")
709
+ agent_id: String, # the affected agent's ID
710
+ message: String, # human-readable summary
711
+ rendered_prompt: String, # (nil for lifecycle events)
712
+ timestamp: String, # ISO 8601 UTC timestamp
713
+ context: { # enriched from agent store + event
714
+ agent: AgentDrop, # .id, .role, .team_role, .spawned_at, .cost_usd, .claimed, .goal
715
+ # goal: GoalDrop, # .status, .summary (nested under agent)
716
+ team: TeamDrop, # .id, .members (if any)
717
+ spawner: SpawnerDrop, # .name (spawner that created the agent)
718
+ parent_agent_id: String, # parent in workflow chain
719
+ workflow_depth: Integer, # depth in workflow chain
720
+ repo: String, # repository from spawn event context
721
+ branch: String, # branch from spawn event context
722
+ issue: IssueDrop, # GitHub issue (from spawn event context, rehydrated)
723
+ story: StoryDrop, # Shortcut story (from spawn event context, rehydrated)
724
+ budget: Float, # (budget events only)
725
+ spent: Float, # (budget events only)
726
+ approval_id: String, # (pending_approval events only)
727
+ repository_name: String, # (worker_spawned events only)
728
+ artifact_name: String, # (artifact_published events only)
729
+ kind: String, # (agent_update events only — status/decision/blocker/progress/completed/failed)
730
+ target_agent_id: String, # (teammate_message events only — directed message recipient)
731
+ slack_thread_ts: String # (slack spawner events only — Slack thread timestamp)
732
+ # ... plus other rehydrated context fields from spawn event
733
+ }
734
+ }
735
+ ```
736
+
737
+ All context fields are optional — only present when the data is available. Agent-level fields are populated from the store lookup; event-level fields are forwarded from the dispatch event.
738
+
739
+ The dispatcher is passed via constructor injection (as a required keyword argument) to `Injector`, `Supervisor`, `Control::Server`, `Spawn::AgentSpawner`, `Spawn::Handler`, and `Spawn::WorkflowExecutor`. All these classes have a `dispatch_notification(...)` private helper that delegates to the dispatcher.
740
+
741
+ ### `Superkick::Spawn::ApprovalStore` (`spawn/approval_store.rb`)
742
+ Thread-safe in-memory store for pending spawn approvals and rejection records. When a spawner has `approval_required: true`, the `Spawn::Handler` queues events here instead of spawning immediately. Rejections are persistent (within a server lifetime) to prevent re-dispatching of rejected events.
743
+
744
+ Key methods:
745
+ - `add(event:, spawner_config:)` → returns approval ID (skips if rejected)
746
+ - `take(id)` / `get(id)` → retrieve (destructive/non-destructive)
747
+ - `reject(id, reason:)` → move from pending to rejected
748
+ - `has_agent?(agent_id)` → true if pending or rejected
749
+ - `clear_rejection(id)` / `clear_all_rejections` → allow re-dispatch
750
+
751
+ ### `Superkick::Team::Log` (`team/log.rb`)
752
+ Append-only shared log for a team of agents. Stored at `~/.superkick/teams/<team_id>.log` as newline-delimited JSON. Thread-safe via Mutex.
753
+
754
+ Entry schema (`Team::Log::Entry` — `Data.define`):
755
+ - `timestamp` — ISO 8601 UTC with microseconds
756
+ - `category` — one of `CATEGORIES`: `:update`, `:message`, `:lifecycle`, `:system`, `:artifact`
757
+ - `agent_id` — the agent that created this entry
758
+ - `agent_role` — team role of the agent (lead, worker, member)
759
+ - `role` — human-readable role label (optional)
760
+ - `message` — free-text content
761
+ - `kind` — sub-type within the category (e.g. `:blocker`, `:decision`, `:spawned`)
762
+ - `target_agent_id` — recipient agent for messages (optional)
763
+ - `artifact_name` — name of published artifact (optional)
764
+
765
+ `Entry.from_hash(raw)` builds an Entry from a parsed hash, filling nil defaults for optional fields that were compacted away during persistence and symbolizing `category` and `kind`.
766
+
767
+ Key methods:
768
+ - `append(agent_id:, agent_role:, category:, message:, **opts)` — validates category, creates entry, persists to disk
769
+ - `entries(since: nil, category: nil)` — filtered entry retrieval
770
+ - `summary` — returns `{ latest_by_agent:, unresolved_blockers: }`
771
+
772
+ ### `Superkick::Team::ArtifactStore` (`team/artifact_store.rb`)
773
+ Per-team persistent artifact storage for sharing data between agents. Artifacts are JSON files at `~/.superkick/teams/<team_id>/artifacts/<author>--<name>.json`. Thread-safe via Mutex.
774
+
775
+ `Artifact = Data.define(:name, :author, :content, :created_at, :updated_at)`
776
+
777
+ Key methods:
778
+ - `publish(team_id:, author:, name:, content:)` → creates or updates, returns `Artifact`
779
+ - `get(team_id:, author:, name:)` → returns `Artifact` or `nil`
780
+ - `list(team_id:, author: nil)` → metadata array (no content)
781
+ - `delete(team_id:, author:, name:)` → returns boolean
782
+
783
+ ### `Superkick::Team::LogMonitor` (`team/log_monitor.rb`)
784
+ Registered monitor (`:team_log`) that polls the team log and injects digest summaries of teammate activity. Auto-added for team leads (via `_spawn_monitors` in `Spawn::Handler#enrich_for_team`), workers (via `_spawn_monitors` in `Control::Server#cmd_spawn_worker`), and human team members (inline in `Control::Server#cmd_register`).
785
+
786
+ On each tick:
787
+ 1. Reads team log entries since last check
788
+ 2. Filters out self-entries (no self-echo)
789
+ 3. Groups remaining entries by category
790
+ 4. Dispatches `team_digest` event with batched entries and blocker flag
791
+
792
+ Config: `team_id` (required), `team_log_store` (injected by server).
793
+
794
+ ### `Superkick::Team::LogNotifier` (`team/log_notifier.rb`)
795
+ Internal notifier (type `:team_log`) that writes team log entries as a notification side-effect. Wired as an internal notifier via the `internal_notifiers:` parameter on `NotificationDispatcher`, so it fires on every event regardless of user-configured event filters. When the dispatched event's agent belongs to a team, the notifier writes a log entry to the team's `Team::Log`. Handles lifecycle events (e.g. `agent_spawned`, `agent_completed`), update events (`agent_update`), message events (`teammate_message`), and artifact events (`artifact_published`) by mapping them to the appropriate team log categories and kinds. This replaces the ad-hoc team log writes that were previously scattered across `Control::Server` and `Spawn::AgentSpawner`.
796
+
797
+ ### `Superkick::Spawn::WorkflowValidator` (`spawn/workflow_validator.rb`)
798
+ Static validation for spawner workflow configs. Performs two checks at server startup:
799
+
800
+ 1. **Cycle detection** — walks `on_complete:` and `on_fail:` spawner reference chains looking for cycles via visited-set tracking. Only `:spawner` references create chain links — inline configs are leaf nodes.
801
+ 2. **Cyclic iteration requirement** — checks that spawners with `allow_cycles: true` have `max_iterations` set. Without an iteration limit, cyclic workflows would run forever. Spawners that fail this check are disabled with an error at startup.
802
+
803
+ Class methods:
804
+ - `Spawn::WorkflowValidator.validate(spawners)` → returns list of cycles (empty = valid)
805
+ - `Spawn::WorkflowValidator.cyclic_spawners_without_iteration_limit(spawners)` → returns list of spawner names (Symbols) that have `allow_cycles: true` but no `max_iterations`
806
+
807
+ ### `Superkick::Spawn::WorkflowExecutor` (`spawn/workflow_executor.rb`)
808
+ Resolves workflow config, builds enriched events, handles VCS ownership transfer, enforces depth limits, and dispatches workflow spawns. Called by Supervisor when a spawned agent's goal reaches a terminal status.
809
+
810
+ Key method:
811
+ - `fire(agent_id:, goal_status:, spawner_config:)` — triggers workflow spawn if `on_complete:` or `on_fail:` is configured
812
+
813
+ Workflow config supports two forms:
814
+ 1. **Spawner reference** — `{ spawner: :review_pr }` — resolves from `Superkick.config.spawners`
815
+ 2. **Inline config** — `{ goal: { type: :agent_signal }, driver: "claude_code" }` — fills in defaults from parent
816
+
817
+ **Execution sequence:**
818
+ 1. Determine hook key from goal status (`:completed` → `:on_complete`, `:failed` → `:on_fail`; `:timed_out` triggers neither)
819
+ 2. Resolve config — spawner references are looked up from `Superkick.config.spawners`; inline configs inherit `driver` and `max_duration` from parent
820
+ 3. Check iteration limit — `workflow_iterations[target_name]` from parent's spawn_info vs target spawner's `max_iterations`
821
+ 4. Build enriched event — forward serialized context from parent's `spawn_info[:context]` wholesale (replaces the old `FORWARDED_FIELDS` approach), stamp `workflow_depth`, `workflow_iterations`, `parent_agent_id`, `parent_goal_status`, and `parent_goal_summary` (from the parent agent's `goal_summary`, when available — provides failure retrospective context for retry workflows). Also forwards parent's per-agent notifier configs as `_spawn_notifiers` when the parent agent has notifier configs, so workflow children inherit per-agent notifiers.
822
+ 5. Transfer VCS state — when no `repository:` key, call `agent_spawner.take_vcs_state(agent_id)` to hand the parent's VCS state (adapter + destination) to the child.
823
+ 6. Fire `workflow_triggered` notification
824
+ 7. Spawn in a background thread via `agent_spawner.spawn`
825
+ 9. On spawn failure, reclaim inherited VCS state (teardown) to avoid orphaned workspaces
826
+
827
+ **Event enrichment — context forwarding:**
828
+ The parent agent's `spawn_info[:context]` is forwarded wholesale to the child workflow event. This carries all integration-specific data (Drops like `issue`, `story`, plus scalars like `repo`, `branch`) without enumerating field names. Context is serialized with `_drop_type` markers so Drops survive the round-trip and are available in workflow templates as `{{ issue.title }}`, `{{ story.ref }}`, etc. Per-agent notifier configs (from `_spawn_notifiers`) are also forwarded when present on the parent agent, ensuring workflow children inherit the same per-agent notification channels.
829
+
830
+ This ensures workflow templates have access to the full context chain — a review agent knows which issue it's reviewing, which branch to check, etc.
831
+
832
+ **VCS inheritance modes:**
833
+ - **Inherit (default)** — no `repository:` key → parent's VCS state is transferred via `take_vcs_state`. Parent skips teardown; child cleans up on exit.
834
+ - **New repository** — explicit `repository:` key → child gets its own VCS setup. Parent tears down normally.
835
+
836
+ Configuration (in spawner config):
837
+ ```yaml
838
+ spawners:
839
+ implement:
840
+ type: github_issues
841
+ on_complete:
842
+ spawner: review_pr # reference to another spawner
843
+ on_fail:
844
+ goal: # inline config (agent_signal is the default, shown here for clarity)
845
+ type: agent_signal
846
+ prompt_template: fix_and_retry
847
+ max_iterations: 3 # max times this spawner can fire per chain (required for allow_cycles)
848
+ ```
849
+
850
+ ### `Superkick::CostAccumulator` (`cost_accumulator.rb`)
851
+ Thread-safe per-agent cost tracking data model. Stored on each `Agent` object as `agent.cost`. Records token counts (in/out), USD cost, and a capped sample history (max 100 samples).
852
+
853
+ Key methods:
854
+ - `record(tokens_in:, tokens_out:, cost_usd:, source:)` — add a cost sample (thread-safe via Mutex)
855
+ - `to_h` — returns `{ total_tokens_in:, total_tokens_out:, total_cost_usd:, sample_count: }`
856
+ - `last_sample_at` — timestamp of most recent sample (for staleness checks)
857
+ - `samples` — array of all recorded samples (newest last)
858
+
859
+ ### `Superkick::CostExtractor` (`cost_extractor.rb`)
860
+ Line-buffered PTY output parser that matches complete lines against driver-specific `CostPattern` arrays. Handles cumulative-to-delta conversion — AI CLIs typically report cumulative totals (e.g. "Total cost: $1.50"), so the extractor tracks previous values and computes incremental deltas to avoid double-counting on status-bar refreshes.
861
+
862
+ Key methods:
863
+ - `initialize(patterns:)` — takes an Array of `CostPattern` objects from the driver
864
+ - `feed(text)` — buffer text, process complete lines, return Array of cost delta Hashes
865
+
866
+ The internal line buffer is capped at `MAX_BUFFER` (1024 bytes) to prevent unbounded memory growth from binary output.
867
+
868
+ ### `Superkick::CostPattern` (defined in `driver.rb`)
869
+ `Data.define(:pattern, :extractor)` — pairs a Regexp with a lambda that extracts cost data from a MatchData. Drivers return arrays of these from `cost_patterns`.
870
+
871
+ ```ruby
872
+ CostPattern.new(
873
+ pattern: /Total cost:\s*\$([0-9]+\.?[0-9]*)/,
874
+ extractor: ->(m) { { cost_usd: m[1].to_f } }
875
+ )
876
+ ```
877
+
878
+ ### Driver config helpers (class methods on `Superkick::Driver`)
879
+
880
+ Three class methods on `Driver` handle driver config normalization, profile resolution, and merging across the spawner/team/workflow cascade:
881
+
882
+ - `Driver.normalize_driver(driver, profile_source: nil)` — normalizes a polymorphic `driver` value to a Hash. Strings/Symbols become `{ type: :name }`. Hashes are normalized (`:type` symbolized). When the Hash contains a `:profile` key and a `profile_source` is provided, the named profile is resolved from the source and used as the base, with any remaining inline keys merged on top.
883
+
884
+ - `Driver.merge_driver(base, override)` — deep-merges two driver config values. When the override is a String/Symbol, it completely replaces the base (no merge). When both are Hashes: scalar keys (`type`, `config_dir`, `command`) — override wins; `args` — arrays are concatenated (base first); `env` — hashes are merged (override keys win).
885
+
886
+ - `driver.apply_config_dir(config_dir, env:, args:)` — instance method. Applies a `config_dir` override to the spawned process environment and args. Each driver knows how its CLI accepts a settings directory override. Built-in implementations: ClaudeCode sets `CLAUDE_CONFIG_DIR`, Copilot sets `COPILOT_HOME`, Codex sets `CODEX_HOME`, Gemini sets `GEMINI_CLI_HOME`, Goose sets `GOOSE_CONFIG_DIR`.
887
+
888
+ ### `Superkick::Driver::ProfileSource` (`driver/profile_source.rb`)
889
+ Abstract base class for named driver profile sources. A profile is a named, reusable set of driver configuration options (type, config_dir, command, args, env) that can be referenced by name from spawner configs via `driver: { profile: :review }`.
890
+
891
+ Follows the same registry + build pattern as `RepositorySource`:
892
+ - `ProfileSource.build(config)` — builds a source from a profiles config hash. Currently always builds a `StaticProfileSource`.
893
+ - `ProfileSource.register(source_class)` / `ProfileSource.lookup(name)` / `ProfileSource.registered` — standard registry pattern.
894
+
895
+ Subclass contract:
896
+ - `self.type` → unique Symbol
897
+ - `get(name)` → profile Hash or nil
898
+ - `list` → Hash of `{ name_sym => profile Hash }`
899
+
900
+ ### `Superkick::Driver::StaticProfileSource` (`driver/profile_source.rb`)
901
+ Profiles defined inline in the YAML config under `drivers.profiles`. Each key is a profile name, each value is a driver config hash.
902
+
903
+ Profile Hash structure:
904
+ ```ruby
905
+ { name: :review, type: :claude_code,
906
+ config_dir: "~/.superkick/driver-configs/review/.claude",
907
+ command: "/opt/bin/claude",
908
+ args: ["--verbose"],
909
+ env: { "ANTHROPIC_API_KEY" => "sk-review" } }
910
+ ```
911
+
912
+ Normalizes on construction: expands `config_dir` paths, symbolizes `type`, wraps `args` into an array.
913
+
914
+ ### `Superkick::BudgetChecker` (`budget_checker.rb`)
915
+ Centralized multi-level budget evaluation. Checks three levels of budget constraints and returns an array of violation Hashes.
916
+
917
+ Budget levels (all three support `warning_threshold_percentage`, default 80):
918
+ 1. **Agent-level** — per-spawner `budget.per_agent.max_cost_usd`. Warns at threshold percentage. Exceeded when agent cost > max.
919
+ 2. **Spawner-level** — `budget.max_cost_usd`. Sums cost across all agents from the same spawner within `budget.window_hours`. Warns at threshold percentage.
920
+ 3. **Global** — top-level `budget.max_cost_usd`. Sums cost across all agents within `budget.window_hours`. Warns at threshold percentage.
921
+
922
+ Violation Hash format: `{ level: :agent|:spawner|:global, budget:, spent:, action: :warning|:exceeded }`
923
+
924
+ Only checks spawned agents (agents with `spawn_info`). Interactive agents are always exempt.
925
+
926
+ ### `Superkick::CostPoller` (`cost_poller.rb`)
927
+ Background thread that periodically injects the driver's cost command (e.g. `/cost`, `/stats`) into idle headless agents when their cost data goes stale. Only runs for spawned agents — interactive agents have a human who can type the command themselves.
928
+
929
+ Configuration:
930
+ - `interval` — check frequency (default 300s / 5 min, configurable via `cost_poll_interval`)
931
+ - `stale_after` — cost data staleness threshold (default 600s / 10 min, configurable via `cost_stale_after`)
932
+
933
+ Uses the `enqueue_injection` buffer command with `:low` priority to submit cost commands to the agent-side `InjectionQueue`. Precondition checks (guards, idle state) are handled agent-side by the queue.
934
+
935
+ ### `Superkick::ProcessRunner` (`process_runner.rb`)
936
+ Unified subprocess execution with reliable timeouts. Avoids Ruby's `Timeout.timeout` (which cannot reliably interrupt C-level blocking calls). Used by the `Command` notifier and the `Integrations::Shell::Monitor`.
937
+
938
+ Two entry points:
939
+ - `.run(cmd, timeout:, env:, chdir:)` — captures stdout+stderr, returns `{ output:, status:, timed_out: }`
940
+ - `.spawn(cmd, timeout:, env:)` — fire-and-forget (no output capture), returns `{ status:, timed_out: }`
941
+
942
+ Both methods SIGTERM the child on timeout and reap it to prevent zombies. Timeouts use `CLOCK_MONOTONIC`.
943
+
944
+ ### `Superkick::Poller` (`poller.rb`)
945
+ Abstract base class for all polling event sources. Provides the shared run loop, error handling, config validation, and backoff logic used by both `Monitor` and `Spawner`.
946
+
947
+ Subclasses implement:
948
+ - `self.type` → Symbol (unique, e.g. `:github`)
949
+ - `self.required_config` → Array of required config key Symbols (e.g. `%i[branch repo]`)
950
+ - `tick` → called each `poll_interval`
951
+ - `self.setup_label` → String or nil (short name for setup wizard, optional)
952
+ - `self.setup_config` → String or nil (YAML snippet for config generation, optional)
953
+
954
+ The `run` loop handles `RateLimited` (backs off), `FatalError` (stops the thread), and generic errors (logs + backs off by `error_backoff`).
955
+
956
+ Exception classes: `Poller::RateLimited`, `Poller::FatalError`.
957
+
958
+ ### `Superkick::Monitor` (`monitor.rb`)
959
+ Extends `Poller` for agent-bound monitors. Subclasses also implement:
960
+ - `self.templates_dir` → path to Liquid templates (or nil)
961
+ - `self.resolve_config(config, environment: {})` → fills in missing config keys from the environment snapshot (default: returns config unchanged)
962
+ - `tick` → called each `poll_interval`; call `dispatch(event_hash)` to emit
963
+
964
+ Constructor: `initialize(name:, config:, handler:, agent: nil, server_context: {})`. The `server_context` Hash carries server-side dependencies (like `team_log_store`) that are not serializable config — they are injected by the server when building monitors and kept separate from the persisted `config` hash.
965
+
966
+ Each instance has a **name** (`@name`, set at construction) that is the unique per-agent key, and a **type** (from the class) that identifies the monitor class. Multiple instances of the same type can coexist under different names (e.g. two `:shell` monitors named `:disk_check` and `:mem_check`).
967
+
968
+ Type inference: when a monitor config hash has no `:type` key, the name is used to look up the class. So `github: { ... }` infers type `:github`.
969
+
970
+ `dispatch(event)` stamps both `monitor_type` and `monitor_name` on the event before passing it to the injector. Events are enqueued on the agent-side `InjectionQueue` via `Buffer::Client`; pending event management is handled agent-side with TTL and priority semantics. Monitors can include `injection_priority`, `injection_ttl`, and `injection_supersede_key` in the event hash to control queue behavior per-event (see Injector docs).
971
+
972
+ Optionally define a nested `Probe` class (see below) for auto-detection of monitor config from the local environment. `Monitor.register` automatically picks up `MyMonitor::Probe` if it exists.
973
+
974
+ ### `Superkick::Spawner` (`spawner.rb`)
975
+ Extends `Poller` for server-level spawners that create new agents in response to external events. Separate registry from Monitor (they don't collide). Subclasses implement:
976
+ - `self.type` → Symbol (unique, e.g. `:shortcut`)
977
+ - `self.required_config` → Array of required config key Symbols
978
+ - `self.agent_id(event)` → deterministic agent ID from event data (e.g. `"shortcut-story-42"`)
979
+ - `self.spawn_templates_dir` → path to spawn Liquid templates (or nil)
980
+ - `tick` → called each `poll_interval`; call `dispatch(event_hash)` to emit
981
+
982
+ `dispatch(event)` stamps `monitor_type`, `monitor_name`, and `agent_id` (via `self.class.agent_id(event)`) on the event before passing it to the `Spawn::Handler`.
983
+
984
+ Dedup is handled by `AgentStore` — if an agent with the same ID already exists, the spawn is skipped.
985
+
986
+ **`_spawn_monitors` mechanism:** Spawners can include a `_spawn_monitors` key in the dispatched event hash. This is a Hash of `{ monitor_name: config_hash }` that `Spawn::AgentSpawner` auto-attaches to the spawned agent at startup, alongside any monitors from the spawner config or probe detection. The key is listed in `Spawn::AgentSpawner::INTERNAL_EVENT_KEYS` and is stripped from the event before template rendering. This mechanism allows spawners to dynamically attach monitors based on event context — for example, the Slack spawner uses it to attach a `:slack_thread` monitor, and `Spawn::Handler#enrich_for_team` uses it to attach the `:team_log` monitor to team leads.
987
+
988
+ **`_spawn_notifiers` mechanism:** Parallel to `_spawn_monitors`, spawners can include a `_spawn_notifiers` key in the dispatched event hash. This is a Hash of `{ notifier_name: config_hash }` that `Spawn::AgentSpawner` stores on the agent's `notifiers` hash and registers with the `NotificationDispatcher` via `register_agent_notifiers(agent_id)`. Per-agent notifiers fire alongside global notifiers for that agent's events only, and are cleaned up when the agent finishes. The key is listed in `Spawn::AgentSpawner::INTERNAL_EVENT_KEYS` and is stripped from the event before template rendering. `attach_spawn_notifiers` is called after `attach_spawn_monitors` in the spawn flow. The Slack spawner uses this to attach a per-agent Slack notifier with a pre-seeded `thread_ts` for thread correlation. Workflow children inherit per-agent notifiers from their parent via `WorkflowExecutor`.
989
+
990
+ **`working_dir` on spawned agents:** After spawning an agent, `Spawn::AgentSpawner` calls `agent.set_working_dir(working_dir)` to record the agent's working directory on the `Agent` object. This is the directory passed to the agent process (e.g. a VCS-acquired worktree). The Supervisor uses the agent's stored environment snapshot when starting monitor threads to call `MonitorClass.resolve_config` for config inference.
991
+
992
+ ### `Superkick::Goal` (`goal.rb`)
993
+ Base class for spawned agent goal checks. A goal defines what "finished" means for a spawned agent. The Supervisor runs a periodic check thread per spawned agent; on each tick it calls `check` and acts on the result.
994
+
995
+ Subclass contract:
996
+ - `self.type` → unique Symbol (e.g. `:command`, `:agent_exit`, `:agent_signal`)
997
+ - `self.description` → human-readable description (for `superkick_discover_goals` MCP tool)
998
+ - `self.required_config` → Array of required config key Symbols (default `[]`)
999
+ - `check` → one of `STATUSES`
1000
+ - `teardown` → optional cleanup (default no-op)
1001
+
1002
+ Status vocabulary:
1003
+ - `:pending` — not started / waiting (non-terminal, default)
1004
+ - `:in_progress` — actively working (non-terminal, stored for observability)
1005
+ - `:errored` — potentially recoverable issue (non-terminal, stored)
1006
+ - `:completed` — succeeded (terminal → terminates agent)
1007
+ - `:failed` — irrecoverable (terminal → terminates agent)
1008
+ - `:timed_out` — max_duration exceeded (terminal, set by Supervisor)
1009
+
1010
+ `TERMINAL_STATUSES = %i[completed failed timed_out]` — the Supervisor terminates the agent when `check` returns one of these. Non-terminal statuses (`:in_progress`, `:errored`) are stored on the agent for observability but the goal checker keeps running.
1011
+
1012
+ Registry follows the standard pattern:
1013
+ ```ruby
1014
+ Superkick::Goal.register(MyGoal)
1015
+ Superkick::Goal.lookup(:my_goal)
1016
+ Superkick::Goal.registered
1017
+ Superkick::Goal.build(goal_config, agent_id:) # → instance from :type key
1018
+ ```
1019
+
1020
+ Built-in goals:
1021
+ - **`AgentExit`** (`:agent_exit`) — signals when the AI CLI process exits. The Supervisor calls `signal!(status)` when the agent process ends (clean exit → `:completed`, non-zero → `:failed`). Passive — doesn't actively poll.
1022
+ - **`Command`** (`:command`) — runs a shell command periodically; exit code determines status. Config: `run` (required), `timeout` (default 30). Passes `SUPERKICK_AGENT_ID` and `SUPERKICK_GOAL_*` env vars (use `exit $SUPERKICK_GOAL_IN_PROGRESS` instead of magic numbers). Exit code convention: `0` → `:completed`, `1` → `:pending`, `2` → `:in_progress`, `3` → `:errored`, `4` → `:failed`, `5-125` → `:errored` (reserved), `126+` → `:failed` (shell/OS errors).
1023
+ - **`AgentSignal`** (`:agent_signal`) — the **default goal type**. Signals when the AI CLI calls the `superkick_signal_goal` MCP tool. Purely reactive; the signal arrives via IPC → `Supervisor#signal_goal` → `goal.signal!(status)`. Accepts any status (`:completed`, `:failed`, `:errored`, `:in_progress`). Spawners do not need to explicitly specify this goal type — it is used automatically when no `goal:` config is provided.
1024
+ - **`Integrations::GitHub::PrMergedGoal`** (`:github_pr_merged`) — polls GitHub via Octokit to check whether a PR has been merged. All config is optional: `repo` and `branch` are auto-detected from the git working directory (the `Spawn::AgentSpawner` injects `working_dir` into the goal config at spawn time). Once a PR is found via branch search, its number is cached for efficient direct lookups. Status mapping: merged → `:completed`, closed without merge → `:failed`, open → `:in_progress`, no PR found → `:pending`, API error → `:errored`. Token falls back to `GITHUB_TOKEN` env var.
1025
+ - **`Integrations::GitHub::IssueResolvedGoal`** (`:github_issue_resolved`) — polls GitHub via Octokit to check whether an issue has been closed. Any close reason (completed or not_planned) counts as success. Config: `issue` (required, `IssueDrop` injected from spawn event context by `Spawn::AgentSpawner` — provides `.number`), `repo` (optional, auto-detected from git remote), `token` (optional, falls back to `GITHUB_TOKEN`). Status mapping: closed → `:completed`, open → `:in_progress`, missing config → `:pending`, API error → `:errored`.
1026
+ - **`Integrations::Datadog::AlertResolvedGoal`** (`:datadog_alert_resolved`) — polls the Datadog Monitors API to check whether a monitor has recovered (returned to OK status). Config: `monitor_id` (injected from spawn event context), `site` (optional, default `"datadoghq.com"`), `api_key` (optional, falls back to `DD_API_KEY`), `application_key` (optional, falls back to `DD_APP_KEY`). Status mapping: OK → `:completed`, Alert/Warn → `:in_progress`, No Data → `:pending`, API error → `:errored`.
1027
+
1028
+ Configuration (in spawner config):
1029
+ ```yaml
1030
+ spawners:
1031
+ my_spawner:
1032
+ goal:
1033
+ type: agent_signal # this is the default, can be omitted
1034
+ max_duration: 3600 # hard timeout in seconds
1035
+ cooldown: 300 # minimum seconds between spawns
1036
+ approval_required: true # require human approval before spawning
1037
+ stall_threshold: 300 # seconds idle before agent_stalled notification
1038
+ repository: api # auto-acquire working copy from repositories config
1039
+ branch_template: "fix/{{ issue.number }}" # Liquid template for branch name
1040
+ base_branch: main # branch to base the working copy on
1041
+ budget: # cost budget enforcement
1042
+ max_cost_usd: 50.0 # aggregate cap across all agents from this spawner
1043
+ window_hours: 24 # time window for spawner aggregate
1044
+ warning_threshold_percentage: 80 # fires warning at this % of max
1045
+ enforce: notify # "notify" (default) or "hard" (terminates agent)
1046
+ per_agent:
1047
+ max_cost_usd: 5.0 # per-agent cost cap
1048
+ warning_threshold_percentage: 80
1049
+ ```
1050
+
1051
+ The Supervisor runs a goal checker thread per spawned agent. On each tick it checks `max_duration` (terminates if exceeded, sets status to `:timed_out`) and `goal.check`. Terminal statuses (`:completed`, `:failed`) trigger agent termination. Non-terminal statuses (`:in_progress`, `:errored`) are stored on the agent but the checker keeps running. The check interval defaults to `poll_interval` but can be overridden with `check_interval` in the goal config.
1052
+
1053
+ When `approval_required: true` is set, spawn events are held in the `Spawn::ApprovalStore` instead of spawning immediately. An `agent_pending_approval` notification is fired. The user approves via `superkick approve <id>` or rejects via `superkick reject <id> [--reason REASON]`. Rejections are persistent (within a server lifetime) — the spawner won't re-dispatch a rejected agent ID until the rejection is cleared.
1054
+
1055
+ ### `Superkick::VersionControl` (`version_control.rb`)
1056
+ Base class for version control adapters. A version control adapter knows how to acquire a working copy of a repository (via worktree, clone, etc.) and tear it down when no longer needed. Used by `Spawn::AgentSpawner` when a spawner config specifies `repository:` to automatically set up a working copy for a spawned agent.
1057
+
1058
+ Subclasses implement:
1059
+ - `self.type` → unique Symbol (e.g. `:git`)
1060
+ - `acquire(source:, destination:, branch:, base_branch:)` → create a working copy at `destination` from `source` (a `Repository` object). `branch` is the branch name to create/checkout; `base_branch` is the branch to base it on.
1061
+ - `teardown(destination:)` → remove the working copy at `destination`
1062
+
1063
+ Registry follows the standard pattern:
1064
+ ```ruby
1065
+ Superkick::VersionControl.register(MyAdapter)
1066
+ Superkick::VersionControl.lookup(:my_vcs)
1067
+ Superkick::VersionControl.registered
1068
+ Superkick::VersionControl.for_repository(repository) # → instance for the repository's VCS type
1069
+ Superkick::VersionControl.detect_from_path(path:) # → convenience alias for VersionControl::Probe.detect_from_path
1070
+ ```
1071
+
1072
+ Auto-registration: `VersionControl.register(adapter)` automatically registers `adapter::Probe` if the adapter defines a nested `Probe` class (same pattern as `Monitor.register`).
1073
+
1074
+ ### `Superkick::VersionControl::Probe` (nested in `version_control.rb`)
1075
+ Abstract base class for VCS detection probes. Each VCS adapter can define a nested `Probe` class that detects its presence by inspecting a local directory path directly.
1076
+
1077
+ Subclasses implement:
1078
+ - `self.type` → Symbol (e.g. `:git`)
1079
+ - `self.detect_at(path:)` → `{ type: :git }` or `nil`
1080
+
1081
+ Key class methods:
1082
+ - `VersionControl::Probe.detect_from_path(path:)` — runs all registered probes against a local directory path, returns first match (e.g. `{ type: :git }`) or `nil`
1083
+ - `VersionControl::Probe.register(probe_class)` / `VersionControl::Probe.registered` — standard registry pattern
1084
+
1085
+ `VersionControl.detect_from_path(path:)` is a convenience alias for `VersionControl::Probe.detect_from_path(path:)`. Used by `Local::RepositorySource` to detect VCS type without hardcoding specific VCS checks. `Repository` is a pure value object and does not perform VCS detection — the source that constructs it is responsible for detecting and passing the `version_control:` value.
1086
+
1087
+ Built-in adapters:
1088
+ - **`Superkick::Integrations::Git::VersionControl`** (`:git`) — uses `git worktree add` when the repository has a local path, falls back to `git clone` when URL-only. Teardown removes the worktree (or deletes the clone directory). Located at `lib/superkick/integrations/git/version_control.rb`. Defines a nested **`VersionControl::Probe`** that checks for `.git` (directory or file for worktrees) via `File.exist?` and returns `{ type: :git }` or `nil`.
1089
+
1090
+ The `Spawn::AgentSpawner` integration:
1091
+ 1. Spawner config specifies `repository: api` (name from `repositories:` config)
1092
+ 2. `Spawn::AgentSpawner` looks up the `Repository` from `RepositorySource`
1093
+ 3. Creates a `VersionControl` adapter via `VersionControl.for_repository(repository)`
1094
+ 4. Calls `acquire(source:, destination:, branch:, base_branch:)` where destination is `workspaces_dir/<agent_id>`
1095
+ 5. VCS state (adapter + destination) is tracked per agent and cleaned up when the agent ends
1096
+ 6. `Spawn::WorkflowExecutor` transfers VCS state alongside pipeline state to child agents
1097
+
1098
+ ### `Superkick::RepositorySource` (`repository_source.rb`)
1099
+ Abstract base class for repository sources used by team planning agents. Provides the source pattern (register/lookup/build) and shared query methods.
1100
+
1101
+ Subclass contract:
1102
+ - `self.type` → unique Symbol (e.g. `:local`, `:git`, `:github_organization`)
1103
+ - `repositories` → Hash of `{ name_sym => Repository }`
1104
+ - Inherits: `find_by_name(n)`, `to_prompt_context`, `empty?`, `size`
1105
+ - `self.setup_label` → (optional) short name for the `superkick setup` wizard
1106
+ - `self.setup_config` → (optional) YAML snippet string for config generation
1107
+
1108
+ `RepositorySource.build(config)` accepts a Hash where each key is a user-chosen name and each value is either:
1109
+ - **No `:type` key, has `:path` key** → a `Local::RepositorySource` with `depth: 0` (single repo)
1110
+ - **No `:type` key, no `:path` key** → silently skipped
1111
+ - **Has `:type` key** → a typed source config, built as its own typed source
1112
+
1113
+ When the result is a single source, it's returned directly. When there are multiple, they're composited via `CompositeRepositorySource`. Empty config returns an empty `Local::RepositorySource`.
1114
+
1115
+ Built-in implementations:
1116
+
1117
+ **`Local::RepositorySource`** (`:local`) — filesystem-based repository source. Auto-detects `version_control` via `VersionControl::Probe.detect_from_path`. Discovers context documents via `Dir.glob` against configured patterns. Behavior depends on `depth:`:
1118
+ - `depth: 0` (default) — treats `path:` as a single repository. Accepts `name:` override (defaults to directory basename).
1119
+ - `depth: 1+` — scans subdirectories of `path:` for repositories.
1120
+
1121
+ Located at `lib/superkick/local/repository_source.rb`.
1122
+ ```yaml
1123
+ # Single repository (depth: 0 is default)
1124
+ repositories:
1125
+ api:
1126
+ path: ~/projects/api
1127
+ dependencies: [shared]
1128
+ web:
1129
+ path: ~/projects/web
1130
+ dependencies: [api]
1131
+
1132
+ # Directory scan
1133
+ repositories:
1134
+ local:
1135
+ type: local
1136
+ path: ~/projects
1137
+ context_documents: [readme, claude_md] # documents to discover (merged with global context_documents)
1138
+ depth: 1 # scan subdirectories
1139
+ exclude: [node_modules] # directory names to skip
1140
+ ```
1141
+
1142
+ **`Integrations::Git::RepositorySource`** (`:git`) — single-repo source for URL-based git repositories. Config: `url:` (required), `name:` (optional override, defaults to repo name parsed from URL). Located at `lib/superkick/integrations/git/repository_source.rb`.
1143
+ ```yaml
1144
+ repositories:
1145
+ upstream:
1146
+ type: git
1147
+ url: https://github.com/org/repo.git
1148
+ name: custom_name # optional — overrides the default name parsed from URL
1149
+ ```
1150
+
1151
+ **`Integrations::GitHub::OrganizationRepositorySource`** (`:github_organization`) — fetches repositories from a GitHub organization or user account via the API. All metadata (descriptions, topics, clone URLs) is fetched using the GitHub API token — no SSH keys needed. Context documents are fetched via the GitHub API: `:readme` uses Octokit's `readme` endpoint; other documents are fetched via the `contents` API (direct path) or `tree` + `contents` (glob patterns). Located at `lib/superkick/integrations/github/repository_source.rb`.
1152
+ ```yaml
1153
+ repositories:
1154
+ company:
1155
+ type: github_organization
1156
+ organization: my-company
1157
+ token: <%= env("GITHUB_TOKEN") %>
1158
+ include_archived: false # include archived repositories (default false)
1159
+ include_forks: false # include forked repositories (default false)
1160
+ topic_filter: [production] # only repositories with ALL listed topics
1161
+ exclude: [legacy-app] # repository names to exclude
1162
+ context_documents: [readme, claude_md] # documents to fetch (merged with global context_documents)
1163
+ visibility: all # public, private, or all (default all)
1164
+ ```
1165
+
1166
+ **`CompositeRepositorySource`** (`:composite`) — unions multiple sources. Built automatically when `repositories:` contains multiple sources. First-found precedence on name collisions. Exposes `children` for introspection.
1167
+ ```yaml
1168
+ # Mix of local repos, git URL, directory scan, and GitHub org:
1169
+ repositories:
1170
+ api:
1171
+ path: ~/projects/api
1172
+ upstream:
1173
+ type: git
1174
+ url: https://github.com/org/upstream.git
1175
+ company:
1176
+ type: github_organization
1177
+ organization: my-company
1178
+ token: <%= env("GITHUB_TOKEN") %>
1179
+ local:
1180
+ type: local
1181
+ path: ~/projects/local
1182
+ depth: 1
1183
+ ```
1184
+
1185
+ `Superkick::Repository` — pure value object for a single repository. Fields: `name` (Symbol), `path`, `url`, `dependencies` (Array of Strings), `context_documents` (Hash of `{ name_sym => content_string }`, default `{}`), `version_control` (Symbol or nil, e.g. `:git`). `Repository` does not perform filesystem access or VCS detection — the source that constructs it (e.g. `Local::RepositorySource`, `Integrations::Git::RepositorySource`) is responsible for detecting and passing the `version_control:` value.
1186
+
1187
+ Convenience accessors for the `:readme` document:
1188
+ - `readme_content` → `context_documents[:readme]`
1189
+ - `readme_content=` → sets `context_documents[:readme]`
1190
+
1191
+ Additional methods:
1192
+ - `set_context_document(name, content)` — sets a named document in `context_documents`
1193
+ - `to_h` — includes a `context_documents` key (array of document names) when documents are present
1194
+
1195
+ `CONTEXT_DOCUMENT_PATTERNS` constant maps well-known document names to glob patterns used for auto-discovery:
1196
+ - `:readme` → `"README*"`, `:claude_md` → `"CLAUDE.md"`, `:agents_md` → `"AGENTS.md"`, `:contributing` → `"CONTRIBUTING*"`, `:conventions` → `"CONVENTIONS*"`, `:cursorrules` → `".cursorrules"`
1197
+
1198
+ ### `Superkick::Monitor::Probe` (nested in `monitor.rb`)
1199
+ Abstract base class for server-side environment probes. Probes run on the server using an environment snapshot collected from the agent via `EnvironmentExecutor`, rather than having direct filesystem access. Subclasses implement:
1200
+ - `self.type` → Symbol
1201
+ - `self.environment_actions` → Array of action hashes describing what environment data the probe needs (e.g. `[{ action: :git_branch }, { action: :git_remotes }]`)
1202
+ - `self.detect(environment:)` → `{}` or `{ monitor_name: { type: "…", config… } }` hash (symbol keys)
1203
+
1204
+ The returned hash keys are monitor **names** as Symbols (unique per agent). Each config hash should include a `:type` key identifying the monitor class. When the name matches a registered monitor type, `:type` may be omitted.
1205
+
1206
+ Key class methods:
1207
+ - `Monitor::Probe.detect_all(environment:)` (also accessible as `Monitor.detect_all(environment:)`) — runs all registered probes against an environment snapshot to auto-configure monitors. Called server-side during `register_environment` processing, where the returned hash is merged with YAML-configured monitors (YAML is the base, probes overlay).
1208
+ - `Monitor::Probe.all_environment_actions` (also accessible as `Monitor.all_environment_actions`) — collects and deduplicates action hashes from all registered probes. Called during `register` to build the `environment_request` response.
1209
+
1210
+ Probes are defined as nested classes inside their monitor (e.g. `Integrations::GitHub::Monitor::Probe < Monitor::Probe`) and are automatically registered when the monitor is registered.
1211
+
1212
+ ### `Superkick::Agent::Runtime` (`agent/runtime.rb`)
1213
+ Abstract base class for agent process runtimes. A runtime knows how to provision a compute environment for an agent process, terminate it, and check whether it's still alive. The `Spawn::AgentSpawner` delegates to a `Runtime` instead of calling `Process.spawn` directly.
1214
+
1215
+ Subclass contract:
1216
+ - `self.type` → unique Symbol (e.g. `:local`, `:docker`)
1217
+ - `provision(agent_id:, config:)` → Handle (opaque lifecycle handle)
1218
+ - `terminate(handle:)` → nil
1219
+ - `alive?(handle:)` → Boolean
1220
+ - `metadata(handle:)` → Hash (optional, default `{}`)
1221
+
1222
+ The `config` Hash passed to `provision` contains: `{ env: Hash, command: Array, working_dir: String }`.
1223
+
1224
+ Each runtime defines its own Handle as a `Data.define` with the fields it needs. Handles are ephemeral — they are stored in-memory on `Spawn::AgentSpawner` and do not survive server restarts.
1225
+
1226
+ Registry follows the standard pattern:
1227
+ ```ruby
1228
+ Superkick::Agent::Runtime.register(MyRuntime)
1229
+ Superkick::Agent::Runtime.lookup(:my_runtime)
1230
+ Superkick::Agent::Runtime.registered
1231
+ ```
1232
+
1233
+ Built-in runtimes:
1234
+ - **`Agent::Runtime::Local`** (`:local`) — spawns agent processes on the same machine via `Process.spawn`. Sends SIGTERM on terminate, SIGKILL after 10 seconds if still alive. Uses `Process.kill(0, pid)` for liveness checks. `Handle = Data.define(:pid)`. This is the default runtime.
1235
+ - **`Integrations::Docker::Runtime`** (`:docker`) — provisions agents in Docker containers via the Docker Engine API. Lives in `integrations/docker/`. Supports Unix socket and TCP+TLS connections. Image pulling follows a configurable policy (`:always`, `:missing`, `:never`). Private registry auth via Base64-encoded credentials. `Handle = Data.define(:container_id, :container_name)`. Config: `image` (required), `host`, `tls`, `pull_policy`, `registry`, `memory`, `cpu`, `network`, `stop_timeout`, `auto_remove`, `env`, `volumes`, `labels`, `privileged`, `cap_add`, `security_opt`, `container_runtime`.
1236
+
1237
+ ### `Superkick::Integrations::Docker::Runtime` (`integrations/docker/runtime.rb`)
1238
+ Provisions spawned agents in Docker containers via the Docker Engine API. Containers are created with the agent's environment, command, and working directory, plus optional resource limits, networking, and volume mounts. Uses `Integrations::Docker::Client` for all API communication. Lives in `integrations/docker/` to signal that it follows the standard `Agent::Runtime` extension point and can be replicated by third-party gems.
1239
+
1240
+ Constructor: `initialize(image:, host: "unix:///var/run/docker.sock", tls: nil, pull_policy: :missing, registry: nil, memory: nil, cpu: nil, network: nil, stop_timeout: 30, auto_remove: true, env: nil, volumes: nil, labels: nil, privileged: false, cap_add: nil, security_opt: nil, container_runtime: nil, server: {}, connection: nil)`. The `connection:` parameter accepts a pre-built Faraday connection for test injection (passed through to `Integrations::Docker::Client`).
1241
+
1242
+ **Local connectivity auto-injection:** When the `server:` context has `type: :local`, the runtime automatically adds a bind mount for `{base_dir}/run` → `/superkick/run` and sets `SUPERKICK_DIR=/superkick` in the container environment. This allows containerized agents to communicate with the local server via Unix sockets without manual configuration. `Configuration#build_agent_runtime` injects the `server:` context automatically for all runtimes. User-configured `SUPERKICK_DIR` env values take precedence over the auto-injected default.
1243
+
1244
+ Key methods:
1245
+ - `provision(agent_id:, config:)` — pulls the image (per `pull_policy`), creates and starts a container, returns a `Handle`
1246
+ - `terminate(handle:)` — stops the container (with `stop_timeout`) and removes it
1247
+ - `alive?(handle:)` — inspects the container and checks if it is running
1248
+ - `metadata(handle:)` — returns `{ container_id:, container_name: }`
1249
+
1250
+ `Handle = Data.define(:container_id, :container_name)`.
1251
+
1252
+ ### `Superkick::Integrations::Docker::Client` (`integrations/docker/client.rb`)
1253
+ Faraday-based client for the Docker Engine API. Supports two connection modes: Unix socket (via a custom `UnixSocketAdapter` Faraday adapter) and TCP+TLS (standard Faraday with optional TLS client certificates). Accepts `connection:` for test injection.
1254
+
1255
+ Constructor: `initialize(host: nil, tls: nil, connection: nil)`. When no `host:` is given, defaults to `unix:///var/run/docker.sock`. When `host:` starts with `unix://`, uses the `UnixSocketAdapter`. Otherwise, uses standard Faraday with optional TLS configuration from the `tls:` hash (`ca`, `cert`, `key` paths).
1256
+
1257
+ Key methods:
1258
+ - `create_container(name:, body:)` — `POST /containers/create`
1259
+ - `start_container(id)` — `POST /containers/{id}/start`
1260
+ - `stop_container(id, timeout:)` — `POST /containers/{id}/stop`
1261
+ - `remove_container(id, force:)` — `DELETE /containers/{id}`
1262
+ - `inspect_container(id)` — `GET /containers/{id}/json`
1263
+ - `pull_image(image, tag:, auth:)` — `POST /images/create`
1264
+ - `inspect_image(image_ref)` — `GET /images/{image_ref}/json`
1265
+
1266
+ Configuration (in `config.yml`):
1267
+ ```yaml
1268
+ runtime:
1269
+ type: local # default, can be omitted
1270
+ ```
1271
+
1272
+ ### `Superkick::Supervisor` (`supervisor.rb`)
1273
+ Server-side supervisor. When an agent registers, it starts one thread per monitor. Before constructing each monitor instance, `start_agent_monitor` calls `MonitorClass.resolve_config(config, environment: agent.environment)` when the agent has an environment snapshot, allowing monitors to fill in missing config from the environment data. Threads run `monitor.run` (the blocking loop). Also manages spawner threads (server-level pollers), goal checker threads (per spawned agent), and cost poller threads (per spawned agent with budget config). On agent unregister or server shutdown, threads are killed. Has an `agent_spawner=` setter that also wires up the `Spawn::WorkflowExecutor` (constructed with the spawner, store, and notification dispatcher).
1274
+
1275
+ The goal checker fires lifecycle notifications when spawned agents reach terminal states:
1276
+ - `:completed` → `agent_completed` notification
1277
+ - `:failed` → `agent_failed` notification
1278
+ - `:timed_out` → `agent_timed_out` notification
1279
+ - `:errored` → `agent_blocked` notification (non-terminal, checker continues)
1280
+
1281
+ The Supervisor also supports claim/unclaim for spawned agents:
1282
+ - `pause_goal_checker(agent_id)` — pauses the goal checker thread (skips checks while paused). Max duration is frozen.
1283
+ - `resume_goal_checker(agent_id)` — resumes the goal checker. Max duration budget is preserved from when it was paused.
1284
+ - `goal_paused?(agent_id)` — returns whether the goal checker is paused.
1285
+
1286
+ Stall detection is configured per-spawner with `stall_threshold:` (seconds). When a spawned agent is idle past this threshold, an `agent_stalled` lifecycle notification fires (once, until the agent becomes active again).
1287
+
1288
+ Cost poller management:
1289
+ - `start_cost_poller(agent_id:, spawner_config:)` — starts a `CostPoller` thread for the agent
1290
+ - `stop_cost_poller(agent_id:)` — kills the cost poller thread (also called automatically by `stop_goal_checker`)
1291
+
1292
+ ### `Superkick::Connection` (`connection.rb`)
1293
+ Newline-delimited JSON framing over raw sockets. Wraps a socket and provides `send_message`, `reply`, `error`, `receive_message`, and `close`. Used by the control plane (`Control::Client`, `Control::Server`) and the data plane (`Buffer::Server`, `Buffer::Client`, etc.). A shared primitive, not specific to control messages.
1294
+
1295
+ ### `Superkick::Control::Client` (`control/client.rb`)
1296
+ Default Unix socket implementation of the control client. All call sites (CLI commands, MCP server, PtyProxy, Attach::Server) use `Control.client_from` to construct the appropriate instance.
1297
+
1298
+ The `Control` module extends `ClientRegistry` and provides `Control.client_from(config:, connection:)` — returns a `Control::Client` (Unix socket) for `:local` server type, or the registered hosted client for `:hosted`. The `connection:` parameter is for test injection of a Faraday connection (hosted mode only). Hosted subclasses register via `Control.register_client(:hosted, klass)`.
1299
+
1300
+ The base class is the Unix socket implementation. Each `request` opens a fresh `UNIXSocket`, sends one JSON message via `Connection`, reads one response, and closes. When no `socket_path:` is given, uses `Superkick.config.socket_path`.
1301
+
1302
+ Defines two shared exception classes:
1303
+ - `Control::Client::ServerUnavailable` — raised when the server is unreachable.
1304
+ - `Control::Client::AuthenticationError` — subclass of `ServerUnavailable`, raised by the hosted client when the server rejects the API key (HTTP 401).
1305
+
1306
+ Key methods:
1307
+ - `request(command, **params)` — send a command, return a `Control::Reply`
1308
+ - `alive?` — check if the server is reachable
1309
+ - `close` — close the underlying connection
1310
+
1311
+ Raises `Control::Client::ServerUnavailable` when the socket is missing or the server closes without responding.
1312
+
1313
+ ### `Superkick::Hosted::Control::Client` (`hosted/control/client.rb`)
1314
+ HTTPS subclass of `Control::Client`. All control commands go through `POST /api/v1/control` with the command name in the JSON body. Uses Faraday for HTTP with built-in middleware: `f.request :authorization` for Bearer token auth, `f.request :json` for automatic JSON serialization, and `f.response :json` for automatic JSON deserialization with `symbolize_names: true`. Maps HTTP errors to `ServerUnavailable` (connection/timeout errors, non-200 status) or `AuthenticationError` (401). Accepts an optional `connection:` constructor parameter for test injection. Self-registers as the `:hosted` client on `Control`.
1315
+
1316
+ ### `Superkick::Control::Server` (`control/server.rb`)
1317
+ Unix socket server — the control plane's main endpoint. One thread per connection (connections are short-lived request/response). Commands are dispatched dynamically to `cmd_<command>` private methods. Uses `Connection` for JSON message framing.
1318
+
1319
+ Key commands related to the environment-based registration flow:
1320
+ - `register` — registers a new agent (no monitors). Returns `environment_request` containing action hashes collected from all registered monitor probes via `Monitor.all_environment_actions`.
1321
+ - `register_environment` — receives an environment snapshot from the agent, runs server-side probes (`Monitor.detect_all(environment:)`), merges with YAML-configured monitors, stores the environment on the agent, and starts monitor threads.
1322
+ - `list_agents` — returns all agents. Accepts optional `team_id:` (filter by team) and `spawned_only:` (only spawned agents) parameters for server-side filtering.
1323
+ - `list_teams` — returns unique teams with summary metadata (`team_id`, `agent_count`, `lead_agent_id`).
1324
+ - `team_status` — accepts `team_id:` directly (preferred) or `agent_id:` (legacy, derives team from agent).
1325
+ - `discover_monitors` — returns available monitor types, metadata, and probe-detected configs from the agent's stored environment snapshot.
1326
+ - `discover_goals` — returns available goal types.
1327
+ - `discover_notifiers` — returns available notifier types.
1328
+
1329
+ ### `Superkick::Control::Reply` (`control/reply.rb`)
1330
+ Wraps a parsed control response hash. Provides `success?`, `error?`, `error_message`, `[]`, and `payload` methods. Returned by `Control::Client#request`.
1331
+
1332
+ ### `Superkick::McpServer` (`mcp_server.rb`)
1333
+ **Local mode only.** Runs as `superkick mcp start` — a stdio MCP server the AI CLI connects to. Each tool proxies to the server via `Control::Client`. In hosted mode, `superkick mcp start` launches `Hosted::McpProxy` instead (see above). Reads `SUPERKICK_AGENT_ID` from the environment at startup (set by `PtyProxy` when spawning the AI CLI) — all tools that need the agent's identity use this automatically rather than requiring the AI to pass it as a parameter. Aborts if the env var is missing.
1334
+
1335
+ Core tools: `superkick_discover_monitors` (thin proxy to the control server — no local registry iteration or probe execution), `superkick_discover_goals` (thin proxy to the control server), `superkick_add_monitor`, `superkick_remove_monitor`, `superkick_signal_goal`, `superkick_report_cost`, `superkick_status`. Notifier tools: `superkick_discover_notifiers` (thin proxy to the control server), `superkick_add_notifier`, `superkick_remove_notifier`. Team tools: `superkick_spawn_worker` (with optional `role`, `monitors`, and `notifiers`), `superkick_team_status`, `superkick_post_update`, `superkick_list_teammates`, `superkick_discover_repositories`, `superkick_publish_artifact`, `superkick_read_artifact`, `superkick_list_artifacts`. None of these tools require `agent_id` as a parameter — it is always read from the environment.
1336
+
1337
+ The `superkick_signal_goal` tool lets the AI CLI signal goal status (`completed`, `failed`, `errored`, `in_progress`) for spawned agents, with an optional `summary` parameter that provides a brief description of the outcome (forwarded as `parent_goal_summary` in workflow chains). The `superkick_post_update` tool replaces the former `superkick_team_update` and `superkick_send_message` tools with a unified interface. Params: `message` (required), `kind` (optional — one of `status`, `decision`, `blocker`, `progress`, `completed`, `failed`), `target_agent_id` (optional — when present, sends a directed message to a specific teammate instead of a broadcast update). The `superkick_discover_repositories` tool returns structured repository data (name, dependencies, path, URL) from the configured repository sources. The `superkick_discover_notifiers` tool returns a list of available notifier types the agent can use. The `superkick_add_notifier` tool adds a per-agent notifier by name, type, and optional config. The `superkick_remove_notifier` tool removes a per-agent notifier by name. Both add/remove tools respect privileged notifier restrictions. The `superkick_spawn_worker` tool accepts optional `monitors` (Hash of name → config) and `notifiers` (Hash of name → config) to pre-configure workers at spawn time. The `team_log` monitor is always auto-attached to workers; explicit monitors merge on top. The artifact tools allow agents to publish, read, and list persistent team artifacts stored at `~/.superkick/teams/<team_id>/artifacts/`. Agent registration, unregistration, and monitor restarts are handled internally by the PTY proxy and server — they are not exposed as MCP tools.
1338
+
1339
+ ### `Superkick::Setup` (`setup.rb`)
1340
+ Generates a well-documented `config.yml` from user selections. Each integration provides a `self.setup_config` class method (raw YAML string with comments) and a `self.setup_label` method (short name for the setup menu). The setup command concatenates selected snippets into a final config.
1341
+
1342
+ Key methods:
1343
+ - `detect_drivers` — returns registered drivers with installation status
1344
+ - `available_monitors` / `available_spawners` / `available_notifiers` / `available_repository_sources` — returns integrations with `setup_label`
1345
+ - `generate_config(driver:, monitors:, spawners:, notifiers:, repository_sources:)` — assembles the config.yml string
1346
+
1347
+ ### `Superkick::CLI` (`cli.rb`)
1348
+ Thor-based CLI organized into subcommands. Global option `--dir` / `-C` overrides `SUPERKICK_DIR`. Each subcommand has a `default_command` so bare invocation works (e.g. `superkick server` runs `superkick server start`).
1349
+
1350
+ Subcommands:
1351
+ - **`superkick server`** — `start` (default), `stop`, `status`, `log`
1352
+ - **`superkick agent`** — `start` (default, wraps AI CLI in PTY; `--team TEAM_ID`, `--role LABEL`, `--no-feed`; hidden flags: `--agent-id ID`, `--headless` — used by `Spawn::AgentSpawner`), `list`, `log <id>`, `stop <id>`, `attach <id>` (`-i` for RW, `-f` / `--force` for force-takeover), `claim <id>`, `unclaim <id>`, `cost [id]`, `report-cost <id>` (`--tokens_in`, `--tokens_out`, `--cost_usd`), `add-monitor <id> <name>` (`--type`, `--config`), `remove-monitor <id> <name>`, `add-notifier <id> <name>` (`--type`, `--config`), `remove-notifier <id> <name>`
1353
+ - **`superkick monitor`** — `list` (default), `install-templates`
1354
+ - **`superkick spawner`** — `list` (default), `start <name>`, `stop <name>`, `install-templates`
1355
+ - **`superkick team`** — `list` (default), `status <id>`, `watch <id>`, `stop <id>`, `artifacts <id>` (`--author`, `--json`), `artifact <id> <author> <name>` (`--json`), `message <id> <text>` (broadcast to all agents), `spawn-worker <team_id>` (`--repository`, `--task`, `--role`, `--goal-type`, `--depends-on`, `--monitors`, `--notifiers`)
1356
+ - **`superkick mcp`** — `start` (default, stdio MCP server), `configure`
1357
+ - **`superkick goal`** — `list` (default, discover goal types), `signal <agent_id> <status>` (`--summary`)
1358
+ - **`superkick notifier`** — `list` (default, discover notifier types)
1359
+ - **`superkick repository`** — `list` (default, list configured repositories; `--json`)
1360
+ - **`superkick setup`** — `start` (default, interactive first-time setup; `--driver`, `--force`, `--skip-mcp`, `--skip-completions`)
1361
+
1362
+ Top-level commands:
1363
+ - **`superkick approve <id>`** — approve a pending spawn
1364
+ - **`superkick reject <id>`** — reject a pending spawn (with optional `--reason`)
1365
+ - **`superkick reject --clear <id>`** — clear a rejection to allow re-dispatch
1366
+ - **`superkick reject --clear-all`** — clear all rejections
1367
+ - **`superkick approvals`** — list pending approvals (with `--rejections` flag for rejections)
1368
+ - **`superkick completion`** — `install` (default, prints shell completion script for `bash` or `zsh`). Hidden helper subcommands provide dynamic completions: `agents` (`--spawned`, `--team`), `team-ids`, `spawner-names`, `approval-ids`, `monitor-types`, `monitors-for <id>`, `notifier-types`, `notifiers-for <id>`, `goal-types`, `goal-statuses`, `driver-names`, `repository-names`, `artifact-authors <team>`, `artifact-names <team> <author>`. All helpers silently return nothing when the server is unavailable.
1369
+
1370
+ ## Plugin registries
1371
+
1372
+ Nine independent registries, all using the same pattern:
1373
+
1374
+ ```ruby
1375
+ # Drivers
1376
+ Superkick::Driver.register(MyDriver) # keyed by MyDriver.driver_name
1377
+ Superkick::Driver.lookup(:my_cli) # → class
1378
+ Superkick::Driver.registered # → frozen hash of all registered classes
1379
+
1380
+ # Monitors (+ their probes)
1381
+ Superkick::Monitor.register(MyMonitor) # keyed by MyMonitor.type; auto-registers MyMonitor::Probe
1382
+ Superkick::Monitor::Probe.register(MyProbe) # direct probe registration (rarely needed)
1383
+
1384
+ # Look up
1385
+ Superkick::Monitor.lookup(:github) # → class
1386
+ Superkick::Monitor.registered # → frozen hash of all registered monitor classes
1387
+ Superkick::Monitor::Probe.registered # → frozen hash of all registered probe classes
1388
+ Superkick::Monitor.detect_all(environment: {}) # → merged config from all registered probes
1389
+ Superkick::Monitor.all_environment_actions # → deduplicated actions from all registered probes
1390
+
1391
+ # Notifiers
1392
+ Superkick::Notifier.register(MyNotifier) # keyed by MyNotifier.type
1393
+ Superkick::Notifier.lookup(:my_notifier) # → class
1394
+ Superkick::Notifier.registered # → frozen hash of all registered notifier classes
1395
+
1396
+ # Spawners (server-level pollers that spawn agents)
1397
+ Superkick::Spawner.register(MySpawner) # keyed by MySpawner.type
1398
+ Superkick::Spawner.lookup(:my_service) # → class
1399
+ Superkick::Spawner.registered # → frozen hash of all registered classes
1400
+
1401
+ # Version control adapters (VCS operations for spawned agent workspaces)
1402
+ Superkick::VersionControl.register(MyAdapter) # keyed by MyAdapter.type; auto-registers MyAdapter::Probe
1403
+ Superkick::VersionControl::Probe.register(MyProbe) # direct probe registration (rarely needed)
1404
+ Superkick::VersionControl.lookup(:my_vcs) # → class
1405
+ Superkick::VersionControl.registered # → frozen hash of all registered classes
1406
+ Superkick::VersionControl.for_repository(repository) # → instance for the repository's VCS type
1407
+ Superkick::VersionControl::Probe.registered # → frozen hash of all registered probe classes
1408
+ Superkick::VersionControl.detect_from_path(path: ".") # → first matching probe result, or nil
1409
+
1410
+ # Goals (spawned agent completion checks)
1411
+ Superkick::Goal.register(MyGoal) # keyed by MyGoal.type
1412
+ Superkick::Goal.lookup(:my_goal) # → class
1413
+ Superkick::Goal.registered # → frozen hash of all registered classes
1414
+ Superkick::Goal.build(goal_config, agent_id:) # → instance from :type key
1415
+
1416
+ # Repository sources (catalog of repositories for team planning)
1417
+ Superkick::RepositorySource.register(MySource) # keyed by MySource.type
1418
+ Superkick::RepositorySource.lookup(:my_type) # → class
1419
+ Superkick::RepositorySource.registered # → frozen hash of all registered classes
1420
+ Superkick::RepositorySource.build(config) # → partitions by :type presence, composites if needed
1421
+
1422
+ # Agent runtimes (process management backends for spawned agents)
1423
+ Superkick::Agent::Runtime.register(MyRuntime) # keyed by MyRuntime.type
1424
+ Superkick::Agent::Runtime.lookup(:my_runtime) # → class
1425
+ Superkick::Agent::Runtime.registered # → frozen hash of all registered classes
1426
+
1427
+ # Driver profile sources (named driver configurations for spawners)
1428
+ Superkick::Driver::ProfileSource.register(MySource) # keyed by MySource.type
1429
+ Superkick::Driver::ProfileSource.lookup(:my_type) # → class
1430
+ Superkick::Driver::ProfileSource.registered # → frozen hash of all registered classes
1431
+ Superkick::Driver::ProfileSource.build(config) # → StaticProfileSource from hash
1432
+ ```
1433
+
1434
+ A single `Monitor.register` call wires up the monitor and its probe (if one is defined as a nested `Probe` class). Similarly, a single `VersionControl.register` call wires up the adapter and its probe (if one is defined as a nested `Probe` class).
1435
+
1436
+ **Important**: The monitor class registry is keyed by **type** (Symbol). The per-agent instance registry (`Agent#monitors`) is keyed by **name** (Symbol). Multiple instances of the same type can coexist under different names. The `:type` key in monitor config is reserved.
1437
+
1438
+ ### Registry test isolation (`Superkick::Registry`)
1439
+
1440
+ All registry classes include the `Superkick::Registry` module (on their singleton class), which provides test isolation methods. These are used to snapshot and restore registries in tests, replacing ad-hoc `deregister`/`register` patterns.
1441
+
1442
+ **Block-scoped** (preferred for focused tests):
1443
+ ```ruby
1444
+ Superkick::Monitor.with_registry do
1445
+ Superkick::Monitor.register(StubMonitor)
1446
+ # StubMonitor visible here, cleaned up automatically
1447
+ end
1448
+
1449
+ # Pre-seed with specific entries:
1450
+ Superkick::Goal.with_registry(stub: StubGoal) do
1451
+ assert_equal StubGoal, Superkick::Goal.lookup(:stub)
1452
+ end
1453
+ ```
1454
+
1455
+ **Paired save/restore** (for Minitest `before`/`after` hooks):
1456
+ ```ruby
1457
+ before do
1458
+ @saved_registry = Superkick::Monitor.snapshot_registry
1459
+ Superkick::Monitor.register(StubMonitor)
1460
+ end
1461
+
1462
+ after do
1463
+ Superkick::Monitor.restore_registry(@saved_registry)
1464
+ end
1465
+ ```
1466
+
1467
+ **Note**: Registry snapshot/restore replaces the `@registry` hash reference. This is NOT thread-safe across parallel tests when background threads perform registry lookups (e.g. Supervisor starting monitor threads). Tests that spawn background threads performing registry lookups should NOT use `parallelize_me!` even with snapshot/restore.
1468
+
1469
+ ## Configuration system
1470
+
1471
+ Config is loaded from `~/.superkick/` (override via `SUPERKICK_DIR` env var) in two passes:
1472
+
1473
+ 1. `config.yml` — YAML processed through ERB first. Use `<%= env("VAR") %>` for secrets.
1474
+ 2. `config.rb` — Ruby file, loaded after YAML, always wins.
1475
+
1476
+ `Superkick.config` returns the `Superkick::Configuration` singleton. Key settings:
1477
+
1478
+ | Setting | Default | Description |
1479
+ |---------|---------|-------------|
1480
+ | `poll_interval` | `30` | Seconds between monitor ticks |
1481
+ | `idle_threshold` | `5.0` | Seconds CLI must be idle before injection |
1482
+ | `inject_clear_delay` | `0.15` | Sleep between PTY write chunks |
1483
+ | `rate_limit_backoff` | `60` | Backoff on API rate limit |
1484
+ | `error_backoff` | `10` | Backoff on general errors |
1485
+ | `log_level` | `:info` | Logger level |
1486
+ | `monitors` | `{}` | Named monitor configs from YAML `monitors:` section |
1487
+ | `privileged_types` | `[]` | Monitor types the AI cannot add/remove |
1488
+ | `notifications` | `[]` | Array of notifier configs from YAML `notifications:` section (defaults to terminal bell when empty) |
1489
+ | `notification_privileged_types` | `[]` | Notifier types the AI cannot add/remove (analogous to `privileged_types` for monitors) |
1490
+ | `attach_escape_key` | `"\x01"` (Ctrl-A) | Escape prefix byte for attach sessions. Configurable via `ctrl-a` through `ctrl-z` in YAML |
1491
+ | `attach_rw_idle_timeout` | `300` | Seconds of no input before an RW attach client is auto-demoted to read-only. Set to `nil` or `0` to disable |
1492
+ | `attach_replay_buffer_size` | `65_536` | Size in bytes of the output replay buffer for attach relay (used in hosted mode) |
1493
+ | `attach_max_connections` | `10` | Maximum number of concurrent user connections per attach relay (used in hosted mode) |
1494
+ | `budget` | `{}` | Global budget config hash (`max_cost_usd`, `window_hours`) |
1495
+ | `cost_poll_interval` | `nil` | CostPoller check frequency in seconds (nil = 300s default) |
1496
+ | `cost_stale_after` | `nil` | Seconds before cost data is considered stale (nil = 600s default) |
1497
+ | `repositories` | `{}` | Repository source config from YAML `repositories:` section |
1498
+ | `context_documents` | `[]` | Global list of context document names to discover (e.g. `[readme, claude_md]`). Merged with per-source `context_documents`. Resolved to glob patterns via `CONTEXT_DOCUMENT_PATTERNS` |
1499
+ | `driver_profiles` | `{}` | Named driver profile configs from YAML `drivers.profiles` section |
1500
+ | `server` | `{}` | Server connection config from YAML `server:` section (url, api_key) |
1501
+ | `runtime` | `{}` | Agent runtime config from YAML `runtime:` section (type, docker options) |
1502
+ | `session_recording_enabled` | `true` | Record timestamped sessions in asciicast v2 format |
1503
+ | `session_recording_max_size` | `104857600` | Max recording file size in bytes (100 MB) before rotation |
1504
+
1505
+ ### YAML `drivers:` section
1506
+
1507
+ The `drivers:` section configures driver-specific options and named profiles:
1508
+
1509
+ ```yaml
1510
+ drivers:
1511
+ # Per-driver-type options (used by interactive agents)
1512
+ claude_code:
1513
+ cli_command: /opt/bin/claude
1514
+ config_dir: /custom/path/.claude
1515
+
1516
+ # Named profiles (used by spawners via profile: key)
1517
+ profiles:
1518
+ review:
1519
+ type: claude_code
1520
+ config_dir: ~/.superkick/driver-configs/review/.claude
1521
+ env:
1522
+ ANTHROPIC_API_KEY: <%= env("REVIEW_KEY") %>
1523
+ worker:
1524
+ type: claude_code
1525
+ config_dir: ~/.superkick/driver-configs/worker/.claude
1526
+ args: ["--verbose"]
1527
+ ```
1528
+
1529
+ Spawners reference profiles via `driver: { profile: :review }`. Inline overrides can be deep-merged on top of the profile:
1530
+
1531
+ ```yaml
1532
+ spawners:
1533
+ my_spawner:
1534
+ driver:
1535
+ profile: review
1536
+ env:
1537
+ EXTRA_VAR: value # merged on top of profile env
1538
+ ```
1539
+
1540
+ The `driver_profile_source` lazy accessor on Configuration builds a `Driver::ProfileSource` from the `driver_profiles` config, analogous to `repository_source`.
1541
+
1542
+ ### YAML `server:` section
1543
+
1544
+ The `server:` section configures how the agent connects to the Superkick control server. When absent or empty, agents connect to the local Unix socket (default). When `url:` is set to an HTTPS endpoint, agents use the hosted transport instead.
1545
+
1546
+ ```yaml
1547
+ # Hosted mode
1548
+ server:
1549
+ url: https://superkick.example.com
1550
+ api_key: <%= env("SUPERKICK_API_KEY") %>
1551
+ ```
1552
+
1553
+ The `server_type` is derived automatically: `:local` when no `url` is configured, `:hosted` when a `url` is present.
1554
+
1555
+ ### YAML `session_recording:` section
1556
+
1557
+ The `session_recording:` section configures timestamped session recording. When absent, session recording is enabled by default.
1558
+
1559
+ ```yaml
1560
+ session_recording:
1561
+ enabled: true # default: true
1562
+ max_size: 104857600 # 100 MB default, rotate if exceeded
1563
+ ```
1564
+
1565
+ ### YAML `runtime:` section
1566
+
1567
+ The `runtime:` section configures how spawned agents are provisioned. When absent or empty, agents are spawned as local processes (default). Other runtime types (e.g. `docker`) provision agents in isolated compute environments.
1568
+
1569
+ ```yaml
1570
+ # Local mode (default, can be omitted)
1571
+ runtime:
1572
+ type: local
1573
+
1574
+ # Docker mode
1575
+ runtime:
1576
+ type: docker
1577
+ docker:
1578
+ host: unix:///var/run/docker.sock
1579
+ tls:
1580
+ ca: /path/to/ca.pem
1581
+ cert: /path/to/cert.pem
1582
+ key: /path/to/key.pem
1583
+ image: superkick/agent:latest
1584
+ pull_policy: missing
1585
+ registry:
1586
+ username: <%= env("DOCKER_REGISTRY_USER") %>
1587
+ password: <%= env("DOCKER_REGISTRY_PASSWORD") %>
1588
+ server: ghcr.io
1589
+ memory: 4G
1590
+ cpu: 2
1591
+ network: superkick-net
1592
+ stop_timeout: 30
1593
+ auto_remove: true
1594
+ env:
1595
+ GITHUB_TOKEN: <%= env("GITHUB_TOKEN") %>
1596
+ volumes:
1597
+ - /home/user/.ssh:/root/.ssh:ro
1598
+ - /home/user/.gitconfig:/root/.gitconfig:ro
1599
+ labels:
1600
+ app: superkick
1601
+ environment: production
1602
+ privileged: false # run in privileged mode (for DinD)
1603
+ cap_add: # Linux capabilities to add
1604
+ - SYS_ADMIN
1605
+ security_opt: # security options
1606
+ - seccomp:unconfined
1607
+ container_runtime: sysbox-runc # OCI runtime (for unprivileged DinD)
1608
+ ```
1609
+
1610
+ The `agent_runtime` lazy accessor on Configuration builds the runtime instance from this config. The runtime type's nested config hash (e.g. `docker:` for `type: docker`) is passed as constructor kwargs via `klass.new(**runtime_config)`. A `server:` context hash is always injected, containing `{ type: :local, base_dir: "..." }` or `{ type: :hosted }` depending on `server_type`. Runtimes use this to set up server connectivity (e.g. Docker auto-mounts the `run/` directory when type is `:local`).
1611
+
1612
+ ### YAML `context_documents:` section
1613
+
1614
+ The `context_documents:` section specifies which context documents should be discovered across all repository sources. Values are well-known document names from `Repository::CONTEXT_DOCUMENT_PATTERNS`. Per-source `context_documents` configs are merged with this global list.
1615
+
1616
+ ```yaml
1617
+ context_documents:
1618
+ - readme
1619
+ - claude_md
1620
+ - agents_md
1621
+ - contributing
1622
+ ```
1623
+
1624
+ Recognized names and their glob patterns: `readme` → `README*`, `claude_md` → `CLAUDE.md`, `agents_md` → `AGENTS.md`, `contributing` → `CONTRIBUTING*`, `conventions` → `CONVENTIONS*`, `cursorrules` → `.cursorrules`.
1625
+
1626
+ `Configuration#resolved_context_document_patterns` resolves the `context_documents` config array into a `{ name_sym => glob_pattern }` hash by looking up each name in `Repository::CONTEXT_DOCUMENT_PATTERNS`. Repository sources merge this global hash with their own per-source `context_documents` list before running discovery.
1627
+
1628
+ When present on a repository, each context document is rendered in `to_prompt_context` as `"Doc name (excerpt):"` with the first 30 lines.
1629
+
1630
+ ### YAML `monitors:` section
1631
+
1632
+ Named monitor instances are configured under the `monitors:` key in `config.yml`. Each key is the monitor **name**; the config hash may include a `:type` key to specify the monitor class. When `:type` is omitted, the name is used as the type. After YAML loading, all keys are symbolized via `YAML.safe_load(…, symbolize_names: true)`.
1633
+
1634
+ Use **snake_case** for monitor and spawner names so they work naturally as Ruby symbols (e.g. `:disk_check` not `:"disk-check"`). The shell probe auto-converts hyphens in filenames to underscores.
1635
+
1636
+ ```yaml
1637
+ monitors:
1638
+ github: # name=github, type inferred as "github"
1639
+ token: <%= env("GITHUB_TOKEN") %>
1640
+ disk_check: # name=disk_check, type=shell
1641
+ type: shell
1642
+ command: ./scripts/check_disk.sh
1643
+ mem_check: # name=mem_check, type=shell
1644
+ type: shell
1645
+ command: ./scripts/check_mem.sh
1646
+ report_success: true
1647
+ ```
1648
+
1649
+ YAML-configured monitors serve as base defaults. At agent registration time, probe-detected monitors are merged on top (probes win on key conflict within a name).
1650
+
1651
+ ### Privileged monitors
1652
+
1653
+ Privileged monitors cannot be added or removed by the AI coding agent via MCP tools (`superkick_add_monitor`, `superkick_remove_monitor`). They are also hidden from AI-facing responses (`list_monitors`, `list_agents`, `discover_monitors`).
1654
+
1655
+ Two levels of protection:
1656
+
1657
+ 1. **By name** — set `privileged: true` on a monitor config:
1658
+ ```yaml
1659
+ monitors:
1660
+ github:
1661
+ privileged: true
1662
+ token: <%= env("GITHUB_TOKEN") %>
1663
+ ```
1664
+
1665
+ 2. **By type** — use the `privileged_types` array under `monitors:` to protect all instances of a monitor type:
1666
+ ```yaml
1667
+ monitors:
1668
+ privileged_types:
1669
+ - shell
1670
+ ```
1671
+
1672
+ Enforcement happens at the server's control layer (`Control::Server`). Registration and restart are not affected — those are internal operations.
1673
+
1674
+ ### Privileged notifiers
1675
+
1676
+ Privileged notifiers follow the same pattern as privileged monitors. They cannot be added or removed by the AI coding agent via MCP tools (`superkick_add_notifier`, `superkick_remove_notifier`). They are also hidden from AI-facing responses (`list_notifiers`, `discover_notifiers`).
1677
+
1678
+ Two levels of protection:
1679
+
1680
+ 1. **By name** — set `name:` and `privileged: true` on a notification config:
1681
+ ```yaml
1682
+ notifications:
1683
+ - type: slack
1684
+ name: audit_channel
1685
+ privileged: true
1686
+ token: <%= env("SLACK_BOT_TOKEN") %>
1687
+ channel: "#audit"
1688
+ ```
1689
+
1690
+ 2. **By type** — use the hash form of `notifications:` with `privileged_types`:
1691
+ ```yaml
1692
+ notifications:
1693
+ privileged_types:
1694
+ - datadog
1695
+ items:
1696
+ - type: datadog
1697
+ statsd_host: localhost
1698
+ - type: terminal_bell
1699
+ ```
1700
+
1701
+ The `notifications:` section supports both array form (existing) and hash form with `privileged_types` and `items` keys. Enforcement happens at the server's control layer (`Control::Server`) via `notifier_hidden?` and `raise_if_notifier_privileged!` helper methods, mirroring the monitor equivalents. Configuration provides `notifier_privileged?(name)` and `notifier_type_privileged?(type)` query methods.
1702
+
1703
+ Driver-related state (`cli_command`, `prompt_patterns`, `injection_guards`) lives on the driver instance, not on Configuration. Access via `Superkick.driver`.
1704
+
1705
+ `Superkick.reset_config!` rebuilds the configuration and clears the active driver — used in tests.
1706
+
1707
+ ### Template resolution
1708
+
1709
+ Templates are resolved in this order:
1710
+ 1. `~/.superkick/templates/<monitor_name>/<event_type>.liquid` — user per-instance override
1711
+ 2. `~/.superkick/templates/<monitor_type>/<event_type>.liquid` — user per-type override
1712
+ 3. `<Monitor.templates_dir>/<event_type>.liquid` — bundled per-type default
1713
+
1714
+ This lets named instances (e.g. `disk-check`) have their own templates while sharing bundled defaults from the type (`shell/`).
1715
+
1716
+ **Notification templates** are resolved separately via `TemplateRenderer.resolve_notification(notifier_class, event_type)`:
1717
+ 1. `~/.superkick/templates/notifications/<type>/<event_type>.liquid` — user per-event override
1718
+ 2. `<Notifier.templates_dir>/<event_type>.liquid` — bundled per-event
1719
+ 3. `~/.superkick/templates/notifications/<type>/default.liquid` — user default catch-all
1720
+ 4. `<Notifier.templates_dir>/default.liquid` — bundled default catch-all
1721
+
1722
+ ### Liquid Drops
1723
+
1724
+ Monitors and spawners that dispatch **nested objects** (e.g. members, tasks, comments, check runs) wrap them in `Liquid::Drop` subclasses before dispatching. This is an implementation detail of the monitor/spawner — there is no shared interface or registry for Drops.
1725
+
1726
+ **Why Drops are needed:** `TemplateRenderer.build_assigns` stringifies only top-level event hash keys. Nested hashes retain symbol keys, but Liquid resolves `{{ obj.prop }}` as `obj["prop"]` (string key lookup), so symbol-keyed nested hashes silently return `nil`. Drops provide method-based access that Liquid calls correctly.
1727
+
1728
+ **When to use Drops:** Wrap nested objects in Drops when templates need dot-notation access on them (`{{ member.display_name }}`, `{{ check.name }}`). Scalar values at the top level of the event hash don't need Drops — `build_assigns` handles them.
1729
+
1730
+ **Drops vs Filters:** Property access and formatting on individual objects → Drop methods (e.g. `{{ pull_request.ref }}`, `{{ story.ref }}`, `{{ member.tag }}`). Array operations that can be expressed in pure Liquid (`{{ owners | map: "tag" | join: ", " }}`) should use built-in Liquid filters rather than custom ones. Custom filters should only be added when they provide something impossible or cumbersome to express in pure Liquid — if a built-in filter chain works, prefer that over a new custom filter.
1731
+
1732
+ Current Drops:
1733
+
1734
+ Core Drops (used in notification payloads):
1735
+ - `Superkick::MonitorDrop` — `name`, `type` (wraps monitor identity in payloads)
1736
+ - `Superkick::TeamDrop` — `id`, `members` (wraps team context in payloads)
1737
+ - `Superkick::SpawnerDrop` — `name` (wraps spawner identity in payloads)
1738
+ - `Superkick::AgentDrop` — `id`, `role`, `team_role`, `spawned_at`, `cost_usd`, `claimed`, `goal` (nested GoalDrop)
1739
+ - `Superkick::GoalDrop` — `status`, `summary` (nested under AgentDrop)
1740
+
1741
+ Integration Drops:
1742
+ - `Superkick::Integrations::GitHub::IssueDrop` — `number`, `title`, `body`, `url`, `author`, `labels`, `assignees`, `milestone`, `created_at`, `updated_at`
1743
+ - `Superkick::Integrations::GitHub::PullRequestDrop` — `number`, `title`, `ref` (formatted: `#42 "Title…"`)
1744
+ - `Superkick::Integrations::GitHub::CommentDrop` — `author`, `body`, `url`
1745
+ - `Superkick::Integrations::GitHub::ReviewDrop` — `author`, `state`, `body`, `url`
1746
+ - `Superkick::Integrations::GitHub::CommitDrop` — `message`, `author`, `url`
1747
+ - `Superkick::Integrations::GitHub::CheckRunDrop` — `name`, `conclusion`, `app`, `details_url`, `html_url`
1748
+ - `Superkick::Integrations::Shortcut::StoryDrop` — `id`, `name`, `url`, `ref`, `description`, `type`, `workflow_state`, `labels`, `owners`, `epic_name`, `tasks`, `comments`
1749
+ - `Superkick::Integrations::Shortcut::MemberDrop` — `display_name`, `mention_name`, `tag`
1750
+ - `Superkick::Integrations::Shortcut::TaskDrop` — `description`, `complete`
1751
+ - `Superkick::Integrations::Shortcut::CommentDrop` — `author` (returns MemberDrop), `body`, `created_at`
1752
+ - `Superkick::Integrations::Slack::MessageDrop` — `text`, `sender` (nested UserDrop), `channel` (nested ChannelDrop), `message_ts`, `thread_ts`, `ref` (formatted: `@Alice in #engineering`)
1753
+ - `Superkick::Integrations::Slack::UserDrop` — `id`, `name`, `tag` (formatted: `Display Name (<@U123>)`)
1754
+ - `Superkick::Integrations::Slack::ChannelDrop` — `id`, `name`, `ref` (formatted: `#channel-name`)
1755
+ - `Superkick::Team::LogEntryDrop` — `timestamp`, `category`, `agent_id`, `agent_role`, `role`, `message`, `kind`, `target_agent_id`, `artifact_name`
1756
+
1757
+ ## Development workflow
1758
+
1759
+ ### Claude Code sandbox bootstrap
1760
+
1761
+ When running in the hosted Claude Code sandbox (`/home/user` base, rbenv at
1762
+ `/opt/rbenv`), Ruby 3.4.x is **not pre-installed** — only older versions
1763
+ (3.1–3.3) ship in the image. You must compile Ruby 3.4 from source via
1764
+ `rbenv install`, which takes several minutes. **Start this in the background
1765
+ immediately** so it finishes while you read the codebase.
1766
+
1767
+ ```bash
1768
+ # 1. Kick off Ruby 3.4 compilation in the background (takes ~2-4 min).
1769
+ export PATH="/opt/rbenv/bin:/opt/rbenv/shims:$PATH"
1770
+ rbenv install 3.4.4 & # runs in background
1771
+
1772
+ # 2. Once the install finishes, activate it and install gems.
1773
+ export RBENV_VERSION=3.4.4
1774
+ export PATH="/opt/rbenv/shims:$PATH"
1775
+
1776
+ # IMPORTANT: Bundler 4.0.6 (shipped in Gemfile.lock) has a known bug
1777
+ # with Ruby 3.4 — `uninitialized class variable @@accept_charset in CGI`.
1778
+ # Work around it by installing and pinning Bundler 2.6.x:
1779
+ gem install bundler -v 2.6.7
1780
+ bundle _2.6.7_ install
1781
+
1782
+ # Now bundle exec works normally.
1783
+ bundle exec rake test
1784
+ ```
1785
+
1786
+ Skip this section on developer machines where rbenv is properly initialised.
1787
+
1788
+ ### Commands
1789
+
1790
+ ```bash
1791
+ # Install dependencies
1792
+ bundle install
1793
+
1794
+ # Run the full test suite (core + per-integration, excludes integration/e2e)
1795
+ bundle exec rake test
1796
+
1797
+ # Run core unit tests only
1798
+ bundle exec rake test:core
1799
+
1800
+ # Run cross-component integration tests
1801
+ bundle exec rake test:integration
1802
+
1803
+ # Run a single test file
1804
+ bundle exec rake test TEST=test/injector_test.rb
1805
+
1806
+ # Run a specific test by name
1807
+ bundle exec rake test TEST=test/injector_test.rb TESTOPTS="-n test_returns_skipped_when_gate_refuses"
1808
+
1809
+ # Run all tests matching a pattern
1810
+ bundle exec rake test TESTOPTS="-n /injection/"
1811
+
1812
+ # Lint with Standard.rb
1813
+ bundle exec rake standard
1814
+
1815
+ # Auto-fix lint violations
1816
+ bundle exec rake standard:fix
1817
+
1818
+ # Run tests + lint (the default)
1819
+ bundle exec rake
1820
+ ```
1821
+
1822
+ ## Test conventions
1823
+
1824
+ - Framework: **Minitest::Spec** (`minitest/autorun`) — uses the Spec DSL for structure with assertion syntax for verification
1825
+ - Core tests live in `test/`, one file per class, named `<class>_test.rb`. Per-integration tests are colocated with their integration code at `lib/superkick/integrations/<name>/test/` (excluded from the gem via gemspec)
1826
+ - Cross-component integration tests live in `test/integration/`. These test the seams between components (spawn pipeline, server-agent IPC, injection flow) rather than individual component logic. Run via `bundle exec rake test:integration`
1827
+ - `test_helper.rb` creates a `TEST_HOME` tmpdir and sets `SUPERKICK_DIR` to it, so tests never touch `~/.superkick`
1828
+ - `Superkick.logger` is silenced (`Logger.new(File::NULL)`) in tests
1829
+ - `Superkick.reset_config!` is called in `test_helper.rb` and in any test that touches configuration
1830
+ - HTTP calls are mocked with **Faraday's test adapter** (`Faraday::Adapter::Test`). All HTTP-making classes (monitors, spawners, notifiers, `Hosted::Control::Client`) accept a `connection:` (or `client:` for Octokit-based classes) constructor parameter. Tests create a `Faraday::Adapter::Test::Stubs` instance, build a Faraday connection with the test adapter, and inject it via the constructor. Use `strict_mode: false` on stubs when the same stub should be reusable across multiple calls. No WebMock — the `webmock` gem is not a dependency
1831
+ - Tests that need a real Unix socket server typically spin up a thread in `before` and tear it down in `after`
1832
+ - **Parallel execution:** Test files that have no global state mutations use `parallelize_me!` at the top of the outermost `describe` block. A file is NOT safe to parallelize if it: mutates `Superkick.config`, calls `Superkick.reset_config!`, uses class-level `SomeClass.stub(:method, ...)` (replaces the method for ALL threads, not just the calling thread), writes to shared filesystem paths under `TEST_HOME`, swaps global objects like `$stderr`, or spawns background threads that perform registry lookups (e.g. Supervisor tests). Registry mutations alone are no longer a blocker — use `snapshot_registry`/`restore_registry` to isolate them (see "Registry test isolation" under Plugin registries)
1833
+
1834
+ ### E2E testing philosophy
1835
+
1836
+ E2E tests (`test/e2e/`) exercise the production workflow as a **black box**. They are the highest-value tests in the suite and must follow these principles:
1837
+
1838
+ 1. **Maximize production fidelity.** E2E tests should exercise as much of the real production code path as possible. Use the same configuration options a normal user would (`config.yml` settings, CLI flags, MCP tool calls). If a feature is user-configurable, configure it the way a user would — don't bypass configuration by manually wiring internal components.
1839
+
1840
+ 2. **Prefer new test tooling over stubs.** When an E2E test needs to simulate an external system (e.g. a fake agent process, a fake PTY, a mock HTTP endpoint), build reusable test infrastructure (like `FakeAgent` in `test/e2e/support/`) rather than stubbing individual methods or manually constructing internal objects. It is ALWAYS preferable to introduce new tooling or behavior in the test harness to enable a more black-box test than to stub or manually configure individual components that aren't manually configurable by an end user.
1841
+
1842
+ 3. **Assert on observable behavior, not internals.** Check what the user would see or what the system produces — injected prompt text, IPC messages, file contents, MCP tool responses. Never inspect instance variables, private state, or intermediate data structures. When asserting on rendered output, verify the actual content (e.g. the message text), not just structural markers (e.g. a header keyword). Fuzzy assertions hide bugs — the `grouped` symbol-key bug went undetected because the test only checked for `"BLOCKERS"` instead of the actual blocker message.
1843
+
1844
+ 4. **Fix production bugs immediately.** When an E2E test reveals a bug in production code, fix it right then. Do not work around it in the test, ignore it, or defer it unless it requires substantial architectural changes — in that case, add it to the icebox with a clear description. The default is always to fix it.
1845
+
1846
+ 5. **Test the full injection pipeline.** For injection-related tests, verify the complete path: monitor tick → event dispatch → template rendering → buffer enqueue → injection queue drain → PTY write. The `FakeAgent` infrastructure enables this by providing a real `Buffer::Server`, `InjectionQueue`, and injectable PTY sink.
1847
+
1848
+ ### Minitest::Spec style
1849
+
1850
+ All tests use the **Spec DSL for structure** (`describe`/`it`/`before`/`after`) but keep **assertion syntax everywhere** (`assert_*`/`refute_*`). No expectation-style syntax (`_()`, `must_*`, `wont_*`). No `let` — use instance variables in `before` blocks.
1851
+
1852
+ | Element | Syntax |
1853
+ |---------|--------|
1854
+ | Test class | `describe Superkick::Foo do` |
1855
+ | Setup | `before do` |
1856
+ | Teardown | `after do` |
1857
+ | Test method | `it "does something" do` |
1858
+ | Test grouping | Nested `describe "group name" do` blocks |
1859
+ | Assertions | `assert_*` / `refute_*` (never expectations) |
1860
+ | Test state | Instance variables in `before` (never `let`) |
1861
+ | Helper methods | Regular `def` methods inside `describe` blocks |
1862
+ | Inner stub classes | Class definitions inside `describe` blocks |
1863
+ | Parallel execution | `parallelize_me!` inside `describe` block |
1864
+
1865
+ Example:
1866
+ ```ruby
1867
+ describe Superkick::Foo do
1868
+ before do
1869
+ @foo = Superkick::Foo.new
1870
+ end
1871
+
1872
+ describe "some feature" do
1873
+ it "does the thing" do
1874
+ assert_equal :expected, @foo.do_thing
1875
+ end
1876
+ end
1877
+ end
1878
+ ```
1879
+
1880
+ ### Stubbing and test doubles
1881
+
1882
+ - **Use Minitest's `stub` method** for replacing methods during tests: `SomeClass.stub(:method_name, return_value) { ... }`. This is block-scoped and automatically restores the original method.
1883
+ - **Never use `define_singleton_method`** to stub methods on classes or instances. It pollutes state and persists beyond the test — there is no automatic cleanup like `stub` provides. Always use Minitest's `stub` method instead, which is block-scoped and restores the original automatically. The only acceptable use of `define_singleton_method` is inside a `Class.new` block to define methods on a fresh anonymous class that needs closure access (e.g. capturing a local variable for `templates_dir`). Even then, prefer `def self.method_name` when no closure is needed.
1884
+ - **Do not introspect private APIs or state** in tests. Do not use `instance_variable_set` or `instance_variable_get` to reach into objects. Either test behavior through existing public interfaces, or expose what you need through new public methods.
1885
+ - **Use `capture_io`** (from Minitest) to capture stdout/stderr. Do not write custom `capture_stdout` or `capture_stderr` helpers — Minitest provides `capture_io { ... }` which returns `[stdout, stderr]`.
1886
+ - **Use `snapshot_registry`/`restore_registry`** to isolate registry mutations in tests. Never use `deregister` for test cleanup — snapshot/restore is cleaner and handles all entries atomically. See "Registry test isolation" under Plugin registries.
1887
+
1888
+ ## Extending Superkick
1889
+
1890
+ ### Adding a new driver
1891
+
1892
+ A driver describes how to drive a specific AI CLI. See `skills/superkick-new-driver/SKILL.md` for the full walkthrough. In brief:
1893
+
1894
+ 1. Create `lib/superkick/drivers/my_cli.rb` — subclass `Superkick::Driver`
1895
+ 2. Implement `self.driver_name`, `initialize(**opts)`, and instance methods (`cli_command`, `prompt_patterns`, etc.)
1896
+ 3. Optionally implement `cost_patterns` → Array of `CostPattern` for PTY cost extraction, and `cost_command` → String (e.g. `/cost`) for the CostPoller to inject
1897
+ 4. Implement `apply_config_dir(config_dir, env:, args:)` to set the CLI's config directory env var or flag
1898
+ 5. Override `install_mcp(exe_path:)` to configure the CLI's MCP settings
1899
+ 5. Register: `Superkick::Driver.register(Superkick::Drivers::MyCli)`
1900
+ 6. Require in `lib/superkick/drivers.rb`
1901
+
1902
+ ### Adding a new monitor
1903
+
1904
+ See `skills/superkick-new-monitor/SKILL.md` for the full walkthrough. In brief:
1905
+
1906
+ 1. Create `lib/superkick/integrations/<name>/monitor.rb` — subclass `Superkick::Monitor`, implement `self.type`, `self.required_config`, `self.templates_dir`, `tick`
1907
+ 2. Optionally create `lib/superkick/integrations/<name>/probe.rb` — define `MyMonitor::Probe < Monitor::Probe`, implement `self.type`, `self.environment_actions`, `self.detect(environment:)`. Probes return `{ monitor_name: { type: "…", config } }` (symbol keys). Probes run server-side using an environment snapshot, not with direct filesystem access.
1908
+ 3. Create `lib/superkick/integrations/<name>.rb` — requires both files, calls `Superkick::Monitor.register(MyMonitor)`
1909
+ 4. Add Liquid templates in `lib/superkick/integrations/<name>/templates/`
1910
+ 5. Require the entry point in `lib/superkick.rb`
1911
+ 6. Optionally implement `self.setup_label` and `self.setup_config` to appear in `superkick setup`
1912
+ 7. Optionally package as a gem and load via `plugins:` in `config.yml`
1913
+
1914
+ ### Adding a new notifier
1915
+
1916
+ A notifier alerts the user after a successful injection. See `skills/superkick-new-notifier/SKILL.md` for the full walkthrough. In brief:
1917
+
1918
+ 1. Create `lib/superkick/notifiers/my_notifier.rb` — subclass `Superkick::Notifier`
1919
+ 2. Implement `self.type` and `notify(payload)`
1920
+ 3. Register: `Superkick::Notifier.register(Superkick::Notifiers::MyNotifier)`
1921
+ 4. Require in `lib/superkick.rb` under the notifiers section
1922
+ 5. Optionally implement `self.setup_label` and `self.setup_config` to appear in `superkick setup`
1923
+
1924
+ ### Adding a new spawner
1925
+
1926
+ A spawner is a server-level poller that spawns new agents in response to external events. See `skills/superkick-new-spawner/SKILL.md` for the full walkthrough. In brief:
1927
+
1928
+ 1. Create `lib/superkick/integrations/<name>/spawner.rb` — subclass `Superkick::Spawner`, implement `self.type`, `self.required_config`, `self.agent_id(event)`, `tick`
1929
+ 2. Optionally create spawn templates in `lib/superkick/integrations/<name>/templates/`
1930
+ 3. Create the entry point and register: `Superkick::Spawner.register(MySpawner)`
1931
+ 4. Require in `lib/superkick.rb` under the integrations section
1932
+ 5. Optionally implement `self.setup_label` and `self.setup_config` to appear in `superkick setup`
1933
+
1934
+ ### Adding a new goal
1935
+
1936
+ A goal defines what "finished" means for a spawned agent. See `skills/superkick-new-goal/SKILL.md` for the full walkthrough. In brief:
1937
+
1938
+ 1. Create `lib/superkick/goals/my_goal.rb` — subclass `Superkick::Goal`
1939
+ 2. Implement `self.type` and `check` (return one of `STATUSES`)
1940
+ 3. Optionally implement `teardown` for cleanup
1941
+ 4. Self-register at the bottom of the file: `Superkick::Goal.register(Superkick::Goals::MyGoal)`
1942
+ 5. Require in `lib/superkick.rb` under the goals section
1943
+
1944
+ ### Adding a new version control adapter
1945
+
1946
+ A version control adapter knows how to acquire an isolated working copy of a repository and clean it up afterwards. See `skills/superkick-new-version-control/SKILL.md` for the full walkthrough. In brief:
1947
+
1948
+ 1. Create `lib/superkick/integrations/<name>/version_control.rb` — subclass `Superkick::VersionControl`
1949
+ 2. Implement `self.type`, `acquire(source:, destination:, branch:, base_branch:)`, and `teardown(destination:)`
1950
+ 3. Self-register at the bottom of the file: `Superkick::VersionControl.register(Superkick::Integrations::MyVcs::VersionControl)`
1951
+ 4. Require in `lib/superkick.rb` under the integrations section
1952
+
1953
+ ### Adding a new repository source
1954
+
1955
+ A repository source discovers and describes repositories for team planning and spawned agent workspaces. See `skills/superkick-new-repository-source/SKILL.md` for the full walkthrough. In brief:
1956
+
1957
+ 1. Create `lib/superkick/repository_sources/my_source.rb` (or in an integration directory) — subclass `Superkick::RepositorySource`
1958
+ 2. Implement `self.type` and `repositories` (returns `{ name_sym => Repository }`)
1959
+ 3. Self-register at the bottom of the file: `Superkick::RepositorySource.register(MySource)`
1960
+ 4. Require in `lib/superkick.rb`
1961
+
1962
+ ### Built-in monitors
1963
+
1964
+ - **`Integrations::GitHub::Monitor`** (`:github`) — polls CI checks, PR comments, PR reviews via Octokit. `resolve_config` fills in missing `repo` and `branch` from the environment snapshot (parsed via `parse_github_repo` from git remote data). Probe auto-detects repo/branch from the environment's git remote and branch data.
1965
+ - **`Integrations::CircleCI::Monitor`** (`:circleci`) — polls CircleCI pipelines and workflows for status changes via Faraday. Config: `project_slug` (required), `branch` (required), `token`. `resolve_config` fills in missing `project_slug` and `branch` from the environment snapshot (parsed via `parse_project_slug` from git remote data, plus `.circleci/config.yml` existence check). Probe auto-detects from the environment's file existence and git remote data.
1966
+ - **`Integrations::Shortcut::Monitor`** (`:shortcut`) — polls Shortcut stories for state changes, comments, blockers, owner changes, and description updates via Faraday. Also watches linked stories and epic siblings for state changes. Config: `token`, `workspace_slug`, `member`, `ignore_self`. `resolve_config` fills in missing `story_id` from the `sc-XXXXX` branch pattern in the environment snapshot's git branch data. Probe auto-detects `story_id` from the environment's git branch name.
1967
+ - **`Integrations::Shell::Monitor`** (`:shell`) — runs a shell command each tick, dispatches `shell_alert` on non-zero exit or `shell_success` when `report_success` is enabled. Config: `command` (required), `working_dir`, `timeout`, `report_success`. No probe — shell monitors must be explicitly configured. `resolve_config` resolves bare command names (without path separators) relative to `.superkick/shell/` in the agent's working directory. Supports multiple instances under different names.
1968
+ - **`Integrations::Datadog::AlertMonitor`** (`:datadog_alert`) — polls a specific Datadog monitor for status changes and injects events when the alert state transitions. Dispatches `alert_recovered` (→ OK), `alert_escalated` (Warn → Alert), or `alert_changed` (any other transition). Config: `monitor_id` (required), `site` (optional), `api_key` (optional, falls back to `DD_API_KEY`), `application_key` (optional, falls back to `DD_APP_KEY`). Records baseline on first tick and only dispatches on state changes.
1969
+ - **`Integrations::Slack::ThreadMonitor`** (`:slack_thread`) — polls Slack `conversations.replies` for new messages in a specific thread. Designed to be auto-attached to agents spawned by the Slack spawner via the `_spawn_monitors` mechanism. Dispatches `slack_reply` events with `injection_priority: :high, injection_ttl: 900`. Config: `channel_id` (required), `thread_ts` (required), `token`. Uses Faraday for HTTP.
1970
+
1971
+ ### Built-in spawners
1972
+
1973
+ - **`Integrations::Shortcut::Spawner`** (`:shortcut`) — watches Shortcut for stories matching a search query and spawns agents. Config: `query` (required), `token`, `workspace_slug`, `owner`, `label`.
1974
+ - **`Integrations::GitHub::IssueSpawner`** (`:github_issues`) — watches GitHub for newly created or reopened issues and spawns agents to work on them. Uses an `updated_at` watermark so reopened issues are caught automatically. Config: `repo` (required), `token`, `labels` (array, AND filter), `assignee` (username, `"*"`, or `"none"`), `milestone`, `creator`, `exclude_pull_requests` (default true). Agent ID format: `github-issue-{org}-{repo}-{number}`. Pairs naturally with the `:github_issue_resolved` goal.
1975
+ - **`Integrations::GitHub::CheckFailedSpawner`** (`:github_check_failed`) — watches GitHub check suites for failures on configured branches and spawns agents to fix them. Works across CI providers since it polls GitHub's check runs API. Config: `repo` (required), `branches` (required, array of branch names), `token`, `app_filter` (array of app names/slugs to include), `check_name_filter` (array of check run names to include), `exclude_apps` (array of app names to ignore). Agent ID format: `github-check-{org}-{repo}-{branch}-{short_sha}` — unique per branch+commit so a new failure after a push gets a new agent.
1976
+ - **`Integrations::Bugsnag::Spawner`** (`:bugsnag`) — watches Bugsnag for open errors and spawns agents to fix them. Polls the Bugsnag Data Access API with configurable filters. Config: `project_id` (required), `token` (optional, falls back to `BUGSNAG_TOKEN`), `severity` (array, default `["error"]`), `release_stage` (string, default `"production"`), `minimum_events` (integer, default 1), `error_classes` (hash with `include`/`exclude` arrays for server-side and client-side filtering), `versions` (array, supports `"latest"` which resolves via the releases API), `filters` (hash of additional Bugsnag API filter fields as passthrough). Agent ID format: `bugsnag-error-{error_id}`.
1977
+ - **`Integrations::Datadog::Spawner`** (`:datadog`) — watches Datadog Error Tracking for open error groups and spawns agents to fix them. Polls the Datadog Error Tracking API with configurable filters. Config: `site` (optional, default `"datadoghq.com"`), `api_key` (optional, falls back to `DD_API_KEY`), `application_key` (optional, falls back to `DD_APP_KEY`), `service` (optional, filter by service name), `environment` (optional, default `"production"`), `source` (optional, e.g. `"ruby"`), `minimum_events` (integer, default 1), `query` (optional, raw Datadog search query). Agent ID format: `datadog-error-{group_id}`.
1978
+ - **`Integrations::Datadog::AlertSpawner`** (`:datadog_alerts`) — watches Datadog monitors for triggered alerts and spawns agents to triage them. Polls the Datadog Monitors Search API for monitors in an alerting state. Monitors that recover and re-alert are re-dispatched as new agents. Config: `site` (optional, default `"datadoghq.com"`), `api_key` (optional, falls back to `DD_API_KEY`), `application_key` (optional, falls back to `DD_APP_KEY`), `statuses` (array, default `["Alert"]`), `tags` (array, AND filter), `monitor_types` (array, e.g. `["metric", "log"]`), `priority` (array of priority levels 1-5), `query` (optional, raw search query). Agent ID format: `datadog-alert-{monitor_id}`. Pairs naturally with the `:datadog_alert_resolved` goal and `:datadog_alert` monitor.
1979
+ - **`Integrations::Honeybadger::Spawner`** (`:honeybadger`) — watches Honeybadger for unresolved faults and spawns agents to fix them. Polls the Honeybadger Faults API with configurable filters. Config: `project_id` (required), `token` (optional, falls back to `HONEYBADGER_TOKEN`), `environment` (optional, default `"production"`), `minimum_occurrences` (integer, default 1), `fault_classes` (array or include/exclude hash for server-side and client-side filtering). Agent ID format: `honeybadger-fault-{fault_id}`.
1980
+ - **`Integrations::Slack::Spawner`** (`:slack`) — watches a Slack channel for new messages and spawns agents to handle them. Polls Slack `conversations.history` via Faraday. Auto-attaches a `:slack_thread` monitor to each spawned agent via the `_spawn_monitors` mechanism, so the agent receives high-priority injections when replies arrive in the originating thread. Also auto-attaches a per-agent Slack notifier via the `_spawn_notifiers` mechanism with a pre-seeded `thread_ts` pointing to the originating message, so lifecycle notifications for the agent are threaded under the original Slack message. Both the thread monitor and per-agent notifier are skipped when `monitor_thread: false`. Config: `channel` (required — channel ID), `token`, `channel_name`, `filter_pattern` (regex to filter messages), `ignore_bots` (default true), `ignore_threads` (default true — only top-level messages), `monitor_thread` (default true — auto-attach thread monitor and per-agent notifier). Agent ID format: `slack-message-{channel_id}-{ts}`.
1981
+
1982
+ ## Hash key conventions
1983
+
1984
+ All internal hashes use **Symbol keys**. This is enforced at deserialization boundaries and must be maintained when writing new code.
1985
+
1986
+ ### Rules
1987
+
1988
+ 1. **Internal hashes always use Symbol keys.** Config hashes, event hashes, monitor hashes, agent data — all Symbol-keyed. Use `%i[...]` for symbol arrays (e.g. `required_config`), not `%w[...]`.
1989
+
1990
+ 2. **Symbolize at deserialization boundaries:**
1991
+ - **JSON** — use `JSON.parse(str, symbolize_names: true)` when parsing internal data (IPC messages, agent files, config persistence).
1992
+ - **YAML** — use `YAML.safe_load(str, symbolize_names: true)`. The `YamlConfig` loader does this at the YAML boundary.
1993
+ - **Control messages** — `Connection` already uses `symbolize_names: true`, so all received messages have symbol keys.
1994
+
1995
+ 3. **External API responses keep string keys.** Data from third-party APIs (GitHub/Octokit, Shortcut, CircleCI) comes through `JSON.parse` without `symbolize_names` and stays as string-keyed hashes (e.g. `story["name"]`, `pipeline["id"]`). Do NOT symbolize these — they're consumed locally within the monitor and never stored or passed to internal interfaces.
1996
+
1997
+ 4. **No defensive dual-key access.** Never write `cfg["type"] || cfg[:type]` or `cfg.fetch("key") rescue cfg.fetch(:key)`. Pick one (Symbol) and trust it.
1998
+
1999
+ 5. **No unnecessary key conversions.** Do not sprinkle `transform_keys(&:to_s)`, `transform_keys(&:to_sym)`, or `.to_s` / `.to_sym` throughout the code. If you find yourself needing a conversion, the data was constructed wrong upstream — fix it there instead.
2000
+
2001
+ 6. **Config accessor uses Symbol lookup.** The `Monitor#[]` accessor converts to symbol: `@config[key.to_sym]`. Callers should use `self[:key]` (symbol), not `self["key"]` (string).
2002
+
2003
+ 7. **Probes return Symbol-keyed hashes.** e.g. `{ github: { type: "github", branch: branch, repo: repo } }`.
2004
+
2005
+ ### Quick reference
2006
+
2007
+ ```ruby
2008
+ # Good
2009
+ config = { branch: "main", repo: "org/repo" }
2010
+ JSON.parse(data, symbolize_names: true)
2011
+ self[:token]
2012
+ required_config = %i[branch repo]
2013
+
2014
+ # Bad
2015
+ config = { "branch" => "main", "repo" => "org/repo" }
2016
+ JSON.parse(data) # for internal data — missing symbolize_names
2017
+ self["token"]
2018
+ required_config = %w[branch repo]
2019
+ config.transform_keys(&:to_sym) # shouldn't be needed
2020
+ ```
2021
+
2022
+ ## Code style conventions
2023
+
2024
+ Superkick targets **Ruby >= 3.4** and uses modern Ruby idioms throughout. Follow these when writing new code.
2025
+
2026
+ ### No abbreviated names in public APIs
2027
+
2028
+ **Do NOT abbreviate names**, especially in public APIs, config keys, method names, class names, and MCP tool parameters. Always use the full word.
2029
+
2030
+ ```ruby
2031
+ # Good
2032
+ repository_source
2033
+ repositories
2034
+ Integrations::GitHub::OrganizationRepositorySource
2035
+ :github_organization
2036
+ :version_control
2037
+ repository_name
2038
+ repository_context
2039
+
2040
+ # Bad — abbreviated
2041
+ repo_source
2042
+ repos
2043
+ GithubOrgRepositorySource
2044
+ :github_org
2045
+ :ver_ctrl
2046
+ repo_name
2047
+ repo_context
2048
+ ```
2049
+
2050
+ This applies to all identifiers: class names, method names, variable names, config keys, YAML keys, MCP tool parameter names, event hash keys, and template variable names. Internal local variables in small scopes may use shorter names when clarity is obvious, but when in doubt, spell it out.
2051
+
2052
+ ### Implicit `it` block parameter (Ruby 3.4)
2053
+
2054
+ Use `it` instead of a named block parameter in single-parameter blocks where the body is short (one expression or a simple one-liner):
2055
+
2056
+ ```ruby
2057
+ # Good — single-param block, simple body
2058
+ threads.each_value { it.each_value(&:kill) }
2059
+ agents.each { yield it }
2060
+ guards.transform_values { it[:reason] }
2061
+ patterns.any? { it.match?(stripped_text) }
2062
+ Thread.new(connection) { handle(it) }
2063
+ monitors.each_key { start_agent_monitor(agent_id, it) }
2064
+ workflows.select { TERMINAL_STATUSES.include?(it["status"]) }
2065
+
2066
+ # Bad — unnecessary named parameter
2067
+ threads.each_value { |t| t.each_value(&:kill) }
2068
+ agents.each { |agent| yield agent }
2069
+ Thread.new(connection) { |conn| handle(conn) }
2070
+ ```
2071
+
2072
+ Keep named parameters when:
2073
+ - The block has **multiple parameters** (e.g. `each_with_object`, `reject! { |_, v| ... }`)
2074
+ - The block body is **multi-line** and a name aids readability (e.g. long `do...end` blocks)
2075
+ - The parameter is **destructured** (e.g. `|key, value|`)
2076
+
2077
+ ### Keyword argument shorthand (value punning)
2078
+
2079
+ When a keyword argument name matches the local variable being passed, use the shorthand:
2080
+
2081
+ ```ruby
2082
+ # Good
2083
+ dispatch(event_type:, project_slug:, branch:)
2084
+ Injector.new(store:)
2085
+ Agent.new(id:, registered_at:)
2086
+
2087
+ # Bad
2088
+ dispatch(event_type: event_type, project_slug: project_slug, branch: branch)
2089
+ Injector.new(store: store)
2090
+ ```
2091
+
2092
+ ### Hash literal shorthand
2093
+
2094
+ Same rule applies to hash literals when the key matches the variable name:
2095
+
2096
+ ```ruby
2097
+ # Good
2098
+ { seconds_idle:, at_prompt: }
2099
+ { ok: true, seconds_idle:, at_prompt: }
2100
+
2101
+ # Bad
2102
+ { seconds_idle: seconds_idle, at_prompt: at_prompt }
2103
+ ```
2104
+
2105
+ ### `attr_reader` over trivial getters
2106
+
2107
+ Use `attr_reader` instead of defining trivial reader methods:
2108
+
2109
+ ```ruby
2110
+ # Good
2111
+ attr_reader :idle_state
2112
+
2113
+ # Bad
2114
+ def idle_state = @idle_state
2115
+ def idle_state; @idle_state; end
2116
+ ```
2117
+
2118
+ ### Nil-safe guards
2119
+
2120
+ Prefer `&.` (safe navigation) and guard clauses over verbose nil checks:
2121
+
2122
+ ```ruby
2123
+ # Good
2124
+ @worker&.join(5)
2125
+ return unless agent
2126
+
2127
+ # Bad
2128
+ @worker.join(5) if @worker
2129
+ ```
2130
+
2131
+ ### Empty-line between method definitions
2132
+
2133
+ StandardRB requires a blank line between method definitions, even in compact test stubs:
2134
+
2135
+ ```ruby
2136
+ # Good
2137
+ def initialize
2138
+ @dispatched = []
2139
+ end
2140
+
2141
+ def inject(agent_id:, event:)
2142
+ @dispatched << event
2143
+ end
2144
+
2145
+ # Bad — missing blank line
2146
+ def initialize
2147
+ @dispatched = []
2148
+ end
2149
+ def inject(agent_id:, event:)
2150
+ @dispatched << event
2151
+ end
2152
+ ```
2153
+
2154
+ ## Documentation requirements
2155
+
2156
+ **ALWAYS review and update documentation after every implementation change.** This is not optional — treat documentation updates as part of the implementation, not a follow-up task. Incomplete documentation is a bug.
2157
+
2158
+ For the full documentation update procedure, see the **`superkick-update-docs`** skill at `skills/superkick-update-docs/SKILL.md`. It contains detailed mapping of which implementation changes require which documentation updates. The summary below covers the essentials.
2159
+
2160
+ ### What to review after every change
2161
+
2162
+ 1. **`README.md`** — the root-level README is the public-facing documentation. When adding or modifying integrations, spawners, monitors, goals, drivers, notifiers, CLI commands, MCP tools, or configuration settings, update the corresponding section. New integrations MUST get an entry under "Built-in integrations" following the existing pattern.
2163
+
2164
+ 2. **`CLAUDE.md`** (this file) — update the architecture docs, directory structure, key classes, configuration tables, and extension guides to reflect any structural or behavioral changes. New files should appear in the directory structure listing. New classes should get a "Key classes" entry if they are architecturally significant.
2165
+
2166
+ 3. **Integration-level READMEs** (e.g. `lib/superkick/integrations/github/README.md`) — when adding a new integration, create a README. When updating an existing integration, review and update its README.
2167
+
2168
+ 4. **Skills** (`skills/superkick-new-<type>/SKILL.md`) — each plugin registry (Driver, Monitor, Spawner, Goal, Notifier, VersionControl) has a matching skill that walks through adding a new implementation. When you introduce a new extension point or significantly change the contract for an existing one, create or update the corresponding skill.
2169
+
2170
+ 5. **`CONTRIBUTING.md`** — update if contribution workflows, development setup, or testing patterns change.
2171
+
2172
+ 6. **User-facing docs** (`docs/`) — the `docs/` directory contains user-facing documentation organized by the Diataxis framework. Planning documents live in `docs/planning/` and are not published. Specific documents to evaluate:
2173
+
2174
+ - **Tutorials** (`docs/tutorials/`) — update when the user experience changes:
2175
+ - `getting-started.md` — installation, basic config, first-run experience
2176
+ - `first-spawner-pipeline.md` — spawner config, goal config, repository acquisition, workflow syntax
2177
+ - `team-workflow.md` — team mechanics, repository source config, MCP team tools
2178
+
2179
+ - **How-to guides** (`docs/how-to/`) — update when task-oriented procedures change:
2180
+ - `customize-templates.md` — template resolution, Liquid filters, template directory structure
2181
+ - `configure-notifications.md` — notifier types, event filtering, SUPERKICK_* env vars
2182
+ - `troubleshooting.md` — new failure modes, changed error messages, new diagnostic commands
2183
+
2184
+ - **Explanations** (`docs/explanation/`) — update when system behavior or design changes:
2185
+ - `injection-model.md` — PTY proxy, idle detection, guards, injection sequence
2186
+ - `spawner-workflows.md` — goal statuses, workflow chaining, VCS inheritance
2187
+ - `agent-teams.md` — planning agent, team log, team config, claim/unclaim
2188
+
2189
+ - **Reference** (`docs/reference/`) — update when APIs, events, or template variables change:
2190
+ - `templates.md` — template variables per event type (update whenever a `dispatch()` call changes)
2191
+ - `event-types.md` — all event types and payloads (update whenever events are added/removed/renamed)
2192
+
2193
+ ### Checklist
2194
+
2195
+ Before considering any implementation task complete, verify:
2196
+ - [ ] `README.md` reflects the change (new features, integrations, commands, config options)
2197
+ - [ ] `CLAUDE.md` reflects the change (architecture, classes, directory structure)
2198
+ - [ ] Integration READMEs are created or updated as needed
2199
+ - [ ] `docs/reference/templates.md` updated if template variables changed
2200
+ - [ ] `docs/reference/event-types.md` updated if event types changed
2201
+ - [ ] `docs/tutorials/` updated if user experience or config syntax changed
2202
+ - [ ] `docs/how-to/` updated if procedures, notifications, or troubleshooting patterns changed
2203
+ - [ ] `docs/explanation/` updated if system behavior or design changed
2204
+ - [ ] Skills are created or updated for new/changed extension points
2205
+ - [ ] Configuration tables are updated if settings were added or changed
2206
+ - [ ] New documentation evaluated — explicitly state whether new tutorials, how-to guides, explanations, or reference pages are recommended for the change (see `skills/superkick-update-docs/SKILL.md` for evaluation criteria)
2207
+ - [ ] `docs/planning/README.md` (roadmap) updated if a roadmap item was completed or priorities changed
2208
+
2209
+ ## Design principles
2210
+
2211
+ - **Prefer the right solution over the least invasive one.** When choosing between approaches, lean towards the architecturally correct design rather than the quickest path, always considering the future product vision and roadmap. Superkick is headed towards a SaaS architecture with multiple server instances — designs that rely on global state, singletons, or class-level variables will break at scale. Constructor injection, instance-level state, and explicit dependency passing are preferred over hidden global state, even when they touch more files.
2212
+
2213
+ ## Things that trip you up
2214
+
2215
+ - **Monitor name vs type.** The `:type` key in monitor config is reserved — it identifies the monitor class. Monitor names are the per-agent unique keys (Symbols). When `:type` is absent, the name is used as the type.
2216
+ - **`SUPERKICK_DIR` is everything.** The base dir path is baked into `Configuration` at init time. If you change it mid-process you must call `Superkick.reset_config!`.
2217
+ - **The server must be running for injection to work.** `superkick agent` still works without the server — it just prints a log line and continues.
2218
+ - **Buffer sockets are transient.** They exist only while `superkick agent` is active. The server stores the path in the `Agent` object; it's nil until the agent registers. Runtime sockets live in `~/.superkick/run/`. In hosted mode, the buffer channel uses a WebSocket instead of a Unix socket — the `Hosted::Buffer::Bridge` in the agent connects to the server's `Hosted::Buffer::Relay`.
2219
+ - **Client factories live on the namespace modules.** `Buffer.client_from`, `Control.client_from`, and `Attach.client_from` select the right client class based on `config.server_type` (`:local` or `:hosted`). Each namespace module extends `ClientRegistry`, which provides `register_client` and `client_from`. Hosted subclasses self-register (e.g. `Hosted::Control::Client` registers as `:hosted` on `Control`). Never construct hosted subclasses directly outside of tests — always use `client_from`. Server-side components receive the constructed client via constructor injection (e.g. `buffer_client:`).
2220
+ - **Protocol framing.** `Connection` uses newline-delimited JSON. Don't hand-roll the protocol — it's shared by both control plane and data plane components.
2221
+ - **Frozen string literals everywhere.** All lib files have `# frozen_string_literal: true`. Don't use `<<` for string mutation; use `+` or `dup`.
2222
+ - **Standard.rb for linting.** Zero-config. Run `bundle exec rake standard:fix` to auto-fix, then review the diff. Don't add a `.rubocop.yml` — StandardRB manages that internally.
2223
+ - **Privileged monitors and notifiers are enforced at the server.** The MCP process does not load `config.yml`, so privilege checks live in `Control::Server`, not `McpServer`. For monitors: the `:privileged` key on a monitor config and the `privileged_types` list on Configuration are the sources of truth. For notifiers: the `:name` + `:privileged` keys on a notification config and `notification_privileged_types` on Configuration are the sources of truth.
2224
+ - **No backwards compatibility concerns.** This is a new tool with a single user. Feel free to rename, restructure, change config formats, or break APIs without deprecation shims. No need for migration paths, backwards-compatible aliases, or re-exported symbols. Just make the change directly. This policy will be explicitly revisited when the user base grows.
2225
+ - **Monitor probes use environment snapshots, not filesystem access.** Monitor probes run server-side using an environment snapshot (collected from the agent via `EnvironmentExecutor`). They take `environment:` not `dir:`. The agent collects environment data in response to action hashes sent by the server during registration. If you need new environment data for a monitor probe, add an action type to `EnvironmentExecutor` and declare it in `self.environment_actions`. VCS probes are different — they detect VCS type by inspecting the local filesystem directly via `detect_at(path:)` and are used by repository sources, not during agent registration.
2226
+ - **No abbreviated names.** Always use full words in identifiers — `repository` not `repo`, `organization` not `org`, `repositories` not `repos`. See the "No abbreviated names in public APIs" code style rule. This applies everywhere: class names, method names, config keys, MCP parameters, event keys, templates.