superkick 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CLA.md +91 -0
- data/CLAUDE.md +2226 -0
- data/CONTRIBUTING.md +104 -0
- data/LICENSE +108 -0
- data/LICENSE-COMMERCIAL.md +39 -0
- data/PLAN.md +161 -0
- data/README.md +1155 -0
- data/exe/superkick +6 -0
- data/lib/superkick/agent/runtime.rb +82 -0
- data/lib/superkick/agent/runtimes/local.rb +74 -0
- data/lib/superkick/agent/runtimes.rb +4 -0
- data/lib/superkick/agent.rb +209 -0
- data/lib/superkick/agent_store.rb +85 -0
- data/lib/superkick/attach/client.rb +245 -0
- data/lib/superkick/attach/protocol.rb +71 -0
- data/lib/superkick/attach/server.rb +371 -0
- data/lib/superkick/budget_checker.rb +120 -0
- data/lib/superkick/buffer/client.rb +91 -0
- data/lib/superkick/buffer/server.rb +127 -0
- data/lib/superkick/cli/agent.rb +524 -0
- data/lib/superkick/cli/completion.rb +591 -0
- data/lib/superkick/cli/goal.rb +71 -0
- data/lib/superkick/cli/mcp.rb +34 -0
- data/lib/superkick/cli/monitor.rb +47 -0
- data/lib/superkick/cli/notifier.rb +39 -0
- data/lib/superkick/cli/repository.rb +46 -0
- data/lib/superkick/cli/server.rb +106 -0
- data/lib/superkick/cli/setup.rb +166 -0
- data/lib/superkick/cli/spawner.rb +85 -0
- data/lib/superkick/cli/team.rb +407 -0
- data/lib/superkick/cli.rb +175 -0
- data/lib/superkick/client_registry.rb +30 -0
- data/lib/superkick/configuration.rb +178 -0
- data/lib/superkick/connection.rb +56 -0
- data/lib/superkick/control/client.rb +78 -0
- data/lib/superkick/control/reply.rb +43 -0
- data/lib/superkick/control/server.rb +1271 -0
- data/lib/superkick/cost_accumulator.rb +53 -0
- data/lib/superkick/cost_extractor.rb +65 -0
- data/lib/superkick/cost_poller.rb +70 -0
- data/lib/superkick/driver/profile_source.rb +134 -0
- data/lib/superkick/driver.rb +179 -0
- data/lib/superkick/drivers/claude_code.rb +110 -0
- data/lib/superkick/drivers/codex.rb +57 -0
- data/lib/superkick/drivers/copilot.rb +75 -0
- data/lib/superkick/drivers/gemini.rb +86 -0
- data/lib/superkick/drivers/goose.rb +74 -0
- data/lib/superkick/drivers.rb +16 -0
- data/lib/superkick/drop.rb +80 -0
- data/lib/superkick/drops.rb +76 -0
- data/lib/superkick/environment_executor.rb +90 -0
- data/lib/superkick/goal.rb +95 -0
- data/lib/superkick/goals/agent_exit.rb +41 -0
- data/lib/superkick/goals/agent_signal.rb +42 -0
- data/lib/superkick/goals/command.rb +103 -0
- data/lib/superkick/history_buffer.rb +38 -0
- data/lib/superkick/hosted/attach/bridge.rb +52 -0
- data/lib/superkick/hosted/attach/client.rb +208 -0
- data/lib/superkick/hosted/attach/relay.rb +313 -0
- data/lib/superkick/hosted/attach/relay_store.rb +48 -0
- data/lib/superkick/hosted/bridge.rb +263 -0
- data/lib/superkick/hosted/buffer/bridge.rb +42 -0
- data/lib/superkick/hosted/buffer/client.rb +63 -0
- data/lib/superkick/hosted/buffer/relay.rb +126 -0
- data/lib/superkick/hosted/buffer/relay_store.rb +42 -0
- data/lib/superkick/hosted/control/client.rb +84 -0
- data/lib/superkick/hosted/mcp_proxy.rb +144 -0
- data/lib/superkick/inject_handler.rb +24 -0
- data/lib/superkick/injection_guard.rb +26 -0
- data/lib/superkick/injection_queue.rb +177 -0
- data/lib/superkick/injector.rb +65 -0
- data/lib/superkick/input_buffer.rb +171 -0
- data/lib/superkick/integrations/bugsnag/README.md +98 -0
- data/lib/superkick/integrations/bugsnag/spawner.rb +307 -0
- data/lib/superkick/integrations/bugsnag/templates/error_opened.liquid +17 -0
- data/lib/superkick/integrations/bugsnag.rb +7 -0
- data/lib/superkick/integrations/circleci/README.md +75 -0
- data/lib/superkick/integrations/circleci/monitor.rb +185 -0
- data/lib/superkick/integrations/circleci/probe.rb +36 -0
- data/lib/superkick/integrations/circleci/templates/ci_failure.liquid +8 -0
- data/lib/superkick/integrations/circleci/templates/ci_success.liquid +1 -0
- data/lib/superkick/integrations/circleci.rb +8 -0
- data/lib/superkick/integrations/datadog/README.md +253 -0
- data/lib/superkick/integrations/datadog/alert_goal.rb +94 -0
- data/lib/superkick/integrations/datadog/alert_monitor.rb +163 -0
- data/lib/superkick/integrations/datadog/alert_spawner.rb +201 -0
- data/lib/superkick/integrations/datadog/notification_templates/default.liquid +10 -0
- data/lib/superkick/integrations/datadog/notifier.rb +294 -0
- data/lib/superkick/integrations/datadog/spawner.rb +201 -0
- data/lib/superkick/integrations/datadog/templates/alert_changed.liquid +8 -0
- data/lib/superkick/integrations/datadog/templates/alert_escalated.liquid +8 -0
- data/lib/superkick/integrations/datadog/templates/alert_recovered.liquid +14 -0
- data/lib/superkick/integrations/datadog/templates/alert_triggered.liquid +29 -0
- data/lib/superkick/integrations/datadog/templates/error_opened.liquid +15 -0
- data/lib/superkick/integrations/datadog.rb +14 -0
- data/lib/superkick/integrations/docker/README.md +256 -0
- data/lib/superkick/integrations/docker/client.rb +295 -0
- data/lib/superkick/integrations/docker/runtime.rb +218 -0
- data/lib/superkick/integrations/docker.rb +4 -0
- data/lib/superkick/integrations/git/repository_source.rb +66 -0
- data/lib/superkick/integrations/git/version_control.rb +119 -0
- data/lib/superkick/integrations/git.rb +8 -0
- data/lib/superkick/integrations/github/README.md +300 -0
- data/lib/superkick/integrations/github/check_failed_spawner.rb +199 -0
- data/lib/superkick/integrations/github/drops.rb +114 -0
- data/lib/superkick/integrations/github/goal.rb +135 -0
- data/lib/superkick/integrations/github/issue_goal.rb +104 -0
- data/lib/superkick/integrations/github/issue_spawner.rb +160 -0
- data/lib/superkick/integrations/github/monitor.rb +251 -0
- data/lib/superkick/integrations/github/probe.rb +30 -0
- data/lib/superkick/integrations/github/repository_source.rb +228 -0
- data/lib/superkick/integrations/github/templates/check_failed.liquid +10 -0
- data/lib/superkick/integrations/github/templates/ci_failure.liquid +5 -0
- data/lib/superkick/integrations/github/templates/ci_success.liquid +1 -0
- data/lib/superkick/integrations/github/templates/issue_opened.liquid +20 -0
- data/lib/superkick/integrations/github/templates/pr_comment.liquid +2 -0
- data/lib/superkick/integrations/github/templates/pr_review.liquid +4 -0
- data/lib/superkick/integrations/github.rb +16 -0
- data/lib/superkick/integrations/honeybadger/README.md +97 -0
- data/lib/superkick/integrations/honeybadger/notification_templates/default.liquid +8 -0
- data/lib/superkick/integrations/honeybadger/notifier.rb +250 -0
- data/lib/superkick/integrations/honeybadger/spawner.rb +214 -0
- data/lib/superkick/integrations/honeybadger/templates/error_opened.liquid +17 -0
- data/lib/superkick/integrations/honeybadger.rb +9 -0
- data/lib/superkick/integrations/shell/README.md +83 -0
- data/lib/superkick/integrations/shell/monitor.rb +87 -0
- data/lib/superkick/integrations/shell/templates/shell_alert.liquid +6 -0
- data/lib/superkick/integrations/shell/templates/shell_success.liquid +6 -0
- data/lib/superkick/integrations/shell.rb +7 -0
- data/lib/superkick/integrations/shortcut/README.md +193 -0
- data/lib/superkick/integrations/shortcut/drops.rb +91 -0
- data/lib/superkick/integrations/shortcut/monitor.rb +582 -0
- data/lib/superkick/integrations/shortcut/probe.rb +34 -0
- data/lib/superkick/integrations/shortcut/spawner.rb +264 -0
- data/lib/superkick/integrations/shortcut/templates/related_story_changed.liquid +6 -0
- data/lib/superkick/integrations/shortcut/templates/story_blocker.liquid +8 -0
- data/lib/superkick/integrations/shortcut/templates/story_comment.liquid +5 -0
- data/lib/superkick/integrations/shortcut/templates/story_description_changed.liquid +19 -0
- data/lib/superkick/integrations/shortcut/templates/story_owner_changed.liquid +10 -0
- data/lib/superkick/integrations/shortcut/templates/story_ready.liquid +41 -0
- data/lib/superkick/integrations/shortcut/templates/story_state_changed.liquid +9 -0
- data/lib/superkick/integrations/shortcut/templates/story_unblocked.liquid +5 -0
- data/lib/superkick/integrations/shortcut.rb +11 -0
- data/lib/superkick/integrations/slack/README.md +297 -0
- data/lib/superkick/integrations/slack/drops.rb +70 -0
- data/lib/superkick/integrations/slack/notifier.rb +426 -0
- data/lib/superkick/integrations/slack/spawner.rb +251 -0
- data/lib/superkick/integrations/slack/templates/default.liquid +17 -0
- data/lib/superkick/integrations/slack/templates/slack_reply.liquid +3 -0
- data/lib/superkick/integrations/slack/templates/spawn/slack_message.liquid +10 -0
- data/lib/superkick/integrations/slack/thread_monitor.rb +161 -0
- data/lib/superkick/integrations/slack.rb +12 -0
- data/lib/superkick/liquid.rb +129 -0
- data/lib/superkick/local/repository_source.rb +148 -0
- data/lib/superkick/mcp_server.rb +596 -0
- data/lib/superkick/monitor.rb +215 -0
- data/lib/superkick/notification_dispatcher.rb +280 -0
- data/lib/superkick/notifier.rb +173 -0
- data/lib/superkick/notifier_state_store.rb +55 -0
- data/lib/superkick/notifier_template.rb +121 -0
- data/lib/superkick/notifiers/command.rb +124 -0
- data/lib/superkick/notifiers/terminal_bell.rb +41 -0
- data/lib/superkick/output_logger.rb +54 -0
- data/lib/superkick/poller.rb +126 -0
- data/lib/superkick/process_runner.rb +87 -0
- data/lib/superkick/pty_proxy.rb +403 -0
- data/lib/superkick/registry.rb +75 -0
- data/lib/superkick/repository_source.rb +195 -0
- data/lib/superkick/server.rb +211 -0
- data/lib/superkick/session_recorder.rb +154 -0
- data/lib/superkick/setup.rb +160 -0
- data/lib/superkick/spawn/agent_spawner.rb +311 -0
- data/lib/superkick/spawn/approval_store.rb +113 -0
- data/lib/superkick/spawn/handler.rb +144 -0
- data/lib/superkick/spawn/injector.rb +119 -0
- data/lib/superkick/spawn/workflow_executor.rb +196 -0
- data/lib/superkick/spawn/workflow_validator.rb +77 -0
- data/lib/superkick/spawner.rb +67 -0
- data/lib/superkick/supervisor.rb +516 -0
- data/lib/superkick/team/artifact_store.rb +92 -0
- data/lib/superkick/team/log.rb +140 -0
- data/lib/superkick/team/log_entry_drop.rb +34 -0
- data/lib/superkick/team/log_monitor.rb +84 -0
- data/lib/superkick/team/log_notifier.rb +96 -0
- data/lib/superkick/team/log_store.rb +40 -0
- data/lib/superkick/template_filters.rb +24 -0
- data/lib/superkick/template_renderer.rb +223 -0
- data/lib/superkick/templates/team_log/planning_agent.liquid +38 -0
- data/lib/superkick/templates/team_log/team_digest.liquid +45 -0
- data/lib/superkick/templates/team_log/teammate_message.liquid +7 -0
- data/lib/superkick/templates/team_log/worker_kickoff.liquid +37 -0
- data/lib/superkick/templates/workflow/workflow_triggered.liquid +22 -0
- data/lib/superkick/version.rb +5 -0
- data/lib/superkick/version_control.rb +135 -0
- data/lib/superkick/yaml_config.rb +302 -0
- data/lib/superkick.rb +198 -0
- data/plan.md +267 -0
- metadata +404 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superkick
|
|
4
|
+
module Team
|
|
5
|
+
# Log — append-only shared log for a team of agents.
|
|
6
|
+
#
|
|
7
|
+
# Each team has its own log stored at ~/.superkick/teams/<team_id>.log as
|
|
8
|
+
# newline-delimited JSON (same framing pattern as IPC). Teammates append
|
|
9
|
+
# status updates, decisions, blockers, messages, lifecycle events, and
|
|
10
|
+
# artifact publications. Any teammate can read the log.
|
|
11
|
+
#
|
|
12
|
+
# Entry schema:
|
|
13
|
+
# timestamp — ISO 8601 UTC with microseconds
|
|
14
|
+
# category — :update, :message, :lifecycle, :system, :artifact
|
|
15
|
+
# agent_id — the agent that created this entry
|
|
16
|
+
# agent_role — team role of the agent (lead, worker, member)
|
|
17
|
+
# role — human-readable role label (optional)
|
|
18
|
+
# message — free-text content
|
|
19
|
+
# kind — sub-type within the category (e.g. :blocker, :decision, :spawned)
|
|
20
|
+
# target_agent_id — recipient agent for messages (optional)
|
|
21
|
+
# artifact_name — name of published artifact (optional)
|
|
22
|
+
class Log
|
|
23
|
+
Entry = Data.define(
|
|
24
|
+
:timestamp, :category, :agent_id, :agent_role,
|
|
25
|
+
:role, :message, :kind, :target_agent_id, :artifact_name
|
|
26
|
+
) do
|
|
27
|
+
# Build an Entry from a parsed hash, filling nil defaults for optional
|
|
28
|
+
# fields that were compacted away during persistence.
|
|
29
|
+
def self.from_hash(raw)
|
|
30
|
+
raw[:category] = raw[:category].to_sym if raw[:category]
|
|
31
|
+
raw[:kind] = raw[:kind].to_sym if raw[:kind]
|
|
32
|
+
defaults = members.each_with_object({}) { |m, h| h[m] = nil }
|
|
33
|
+
new(**defaults.merge(raw))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
CATEGORIES = %i[update message lifecycle system artifact].freeze
|
|
38
|
+
|
|
39
|
+
attr_reader :team_id
|
|
40
|
+
|
|
41
|
+
def initialize(team_id, teams_dir: Superkick.config.teams_dir)
|
|
42
|
+
@team_id = team_id
|
|
43
|
+
@entries = []
|
|
44
|
+
@mutex = Mutex.new
|
|
45
|
+
@file_path = File.join(teams_dir, "#{team_id}.log")
|
|
46
|
+
FileUtils.mkdir_p(File.dirname(@file_path))
|
|
47
|
+
load_from_disk
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def append(agent_id:, agent_role:, category:, message:,
|
|
51
|
+
role: nil, kind: nil, target_agent_id: nil, artifact_name: nil)
|
|
52
|
+
cat = category.to_sym
|
|
53
|
+
unless CATEGORIES.include?(cat)
|
|
54
|
+
raise ArgumentError, "Invalid category: #{category}. Must be one of: #{CATEGORIES.join(", ")}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
entry = Entry.new(
|
|
58
|
+
timestamp: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%6NZ"),
|
|
59
|
+
category: cat,
|
|
60
|
+
agent_id: agent_id.to_s,
|
|
61
|
+
agent_role: agent_role.to_s,
|
|
62
|
+
role: role,
|
|
63
|
+
message: message.to_s,
|
|
64
|
+
kind: kind&.to_sym,
|
|
65
|
+
target_agent_id: target_agent_id,
|
|
66
|
+
artifact_name: artifact_name
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@mutex.synchronize do
|
|
70
|
+
@entries << entry
|
|
71
|
+
persist_entry(entry)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
entry
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def entries(since: nil, category: nil)
|
|
78
|
+
@mutex.synchronize do
|
|
79
|
+
result = @entries.dup
|
|
80
|
+
result = result.select { it.timestamp > since } if since
|
|
81
|
+
result = result.select { it.category == category.to_sym } if category
|
|
82
|
+
result
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Condensed view: latest entry per agent + any unresolved blockers.
|
|
87
|
+
def summary
|
|
88
|
+
@mutex.synchronize do
|
|
89
|
+
latest_by_agent = {}
|
|
90
|
+
blockers = []
|
|
91
|
+
|
|
92
|
+
@entries.each do |entry|
|
|
93
|
+
latest_by_agent[entry.agent_id] = entry
|
|
94
|
+
blockers << entry if entry.category == :update && entry.kind == :blocker
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Remove resolved blockers (if same agent posted a non-blocker after)
|
|
98
|
+
unresolved = blockers.select do |b|
|
|
99
|
+
latest = latest_by_agent[b.agent_id]
|
|
100
|
+
latest == b || (latest.category == :update && latest.kind == :blocker)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
{latest_by_agent: latest_by_agent.values, unresolved_blockers: unresolved}
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def size
|
|
108
|
+
@mutex.synchronize { @entries.size }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def persist_entry(entry)
|
|
114
|
+
# Only persist non-nil fields to keep the NDJSON compact
|
|
115
|
+
h = entry.to_h.compact
|
|
116
|
+
File.open(@file_path, "a") do |f|
|
|
117
|
+
f.puts(JSON.generate(h))
|
|
118
|
+
f.flush
|
|
119
|
+
end
|
|
120
|
+
rescue => e
|
|
121
|
+
Superkick.logger.warn("team_log") { "Failed to persist entry: #{e.message}" }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def load_from_disk
|
|
125
|
+
return unless File.exist?(@file_path)
|
|
126
|
+
|
|
127
|
+
File.readlines(@file_path).each do |line|
|
|
128
|
+
raw = JSON.parse(line.strip, symbolize_names: true)
|
|
129
|
+
@entries << Entry.from_hash(raw)
|
|
130
|
+
rescue JSON::ParserError, ArgumentError
|
|
131
|
+
next
|
|
132
|
+
end
|
|
133
|
+
rescue => e
|
|
134
|
+
Superkick.logger.warn("team_log") { "Failed to load team log: #{e.message}" }
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Backwards-compatible alias
|
|
140
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superkick
|
|
4
|
+
module Team
|
|
5
|
+
# Liquid Drop for team log entries. Wraps the symbol-keyed hash
|
|
6
|
+
# from Team::Log::Entry#to_h so the team_digest template can access
|
|
7
|
+
# properties via dot notation (e.g. {{ e.agent_id }}, {{ e.kind }}).
|
|
8
|
+
class LogEntryDrop < Superkick::Drop
|
|
9
|
+
def self.drop_type = "team_log_entry"
|
|
10
|
+
|
|
11
|
+
def timestamp = @data[:timestamp]
|
|
12
|
+
|
|
13
|
+
def category = @data[:category].to_s
|
|
14
|
+
|
|
15
|
+
def agent_id = @data[:agent_id]
|
|
16
|
+
|
|
17
|
+
def agent_role = @data[:agent_role]
|
|
18
|
+
|
|
19
|
+
def role = @data[:role]
|
|
20
|
+
|
|
21
|
+
def message = @data[:message]
|
|
22
|
+
|
|
23
|
+
def kind = @data[:kind].to_s
|
|
24
|
+
|
|
25
|
+
def target_agent_id = @data[:target_agent_id]
|
|
26
|
+
|
|
27
|
+
def artifact_name = @data[:artifact_name]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Backwards-compatible alias
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Superkick::Drop.register(Superkick::Team::LogEntryDrop)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superkick
|
|
4
|
+
module Team
|
|
5
|
+
# LogMonitor — polls the team log and injects digest events.
|
|
6
|
+
#
|
|
7
|
+
# Registered as `:team_log` in the Monitor registry. Auto-added by the
|
|
8
|
+
# server for team leads and human team members (`:member`). Workers can
|
|
9
|
+
# opt in via superkick_add_monitor.
|
|
10
|
+
#
|
|
11
|
+
# On each tick, reads team log entries since the last injection, filters
|
|
12
|
+
# out self-entries, and dispatches a single `team_digest` event with
|
|
13
|
+
# batched entries grouped by category.
|
|
14
|
+
#
|
|
15
|
+
# Config:
|
|
16
|
+
# team_id — (required) the team to watch
|
|
17
|
+
class LogMonitor < Monitor
|
|
18
|
+
class << self
|
|
19
|
+
def type = :team_log
|
|
20
|
+
|
|
21
|
+
def description
|
|
22
|
+
"Polls the team log and injects digest summaries of teammate activity."
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def required_config = %i[team_id]
|
|
26
|
+
|
|
27
|
+
def event_types = %w[team_digest]
|
|
28
|
+
|
|
29
|
+
def templates_dir
|
|
30
|
+
File.join(__dir__, "..", "templates", "team_log")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def on_start
|
|
35
|
+
@last_check_at = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%6NZ")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def tick
|
|
39
|
+
team_id = self[:team_id]
|
|
40
|
+
return unless team_id
|
|
41
|
+
|
|
42
|
+
log = team_log_store&.get(team_id)
|
|
43
|
+
return unless log
|
|
44
|
+
|
|
45
|
+
entries = log.entries(since: @last_check_at)
|
|
46
|
+
|
|
47
|
+
# Filter out entries from this agent (no self-echo)
|
|
48
|
+
agent_id = @agent&.id
|
|
49
|
+
entries = entries.reject { it.agent_id == agent_id }
|
|
50
|
+
|
|
51
|
+
return if entries.empty?
|
|
52
|
+
|
|
53
|
+
@last_check_at = entries.last.timestamp
|
|
54
|
+
|
|
55
|
+
# Group entries by category for the template
|
|
56
|
+
grouped = entries.group_by(&:category)
|
|
57
|
+
|
|
58
|
+
entry_drops = entries.map { Team::LogEntryDrop.new(it.to_h) }
|
|
59
|
+
grouped_drops = grouped.transform_keys(&:to_s).transform_values { |v| v.map { Team::LogEntryDrop.new(it.to_h) } }
|
|
60
|
+
|
|
61
|
+
has_blockers = entries.any? { it.category == :update && it.kind == :blocker }
|
|
62
|
+
|
|
63
|
+
dispatch(
|
|
64
|
+
event_type: :team_digest,
|
|
65
|
+
entries: entry_drops,
|
|
66
|
+
grouped: grouped_drops,
|
|
67
|
+
entry_count: entries.size,
|
|
68
|
+
has_blockers:,
|
|
69
|
+
agent_role: @agent&.team_role&.to_s || "unknown",
|
|
70
|
+
injection_ttl: 120,
|
|
71
|
+
**(has_blockers ? {injection_priority: :high} : {})
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def team_log_store
|
|
78
|
+
server_context[:team_log_store]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
Monitor.register(Team::LogMonitor)
|
|
84
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superkick
|
|
4
|
+
module Team
|
|
5
|
+
# Writes team log entries as a notification side-effect.
|
|
6
|
+
#
|
|
7
|
+
# This notifier is automatically wired into the NotificationDispatcher
|
|
8
|
+
# when a team_log_store is available. It converts notification payloads
|
|
9
|
+
# into Team::Log entries for any agent that belongs to a team.
|
|
10
|
+
#
|
|
11
|
+
# Not user-configurable — it's an internal notifier.
|
|
12
|
+
class LogNotifier < Superkick::Notifier
|
|
13
|
+
def self.type = :team_log
|
|
14
|
+
|
|
15
|
+
# Event type → { category:, kind: } mapping.
|
|
16
|
+
# Events not listed here are ignored (no team log entry).
|
|
17
|
+
EVENT_MAP = {
|
|
18
|
+
"agent_update" => {category: :update},
|
|
19
|
+
"teammate_message" => {category: :message},
|
|
20
|
+
"teammate_blocker" => {category: :update, kind: :blocker},
|
|
21
|
+
"agent_spawned" => {category: :lifecycle},
|
|
22
|
+
"worker_spawned" => {category: :lifecycle, kind: :spawned},
|
|
23
|
+
"agent_completed" => {category: :lifecycle, kind: :completed},
|
|
24
|
+
"agent_failed" => {category: :lifecycle, kind: :failed},
|
|
25
|
+
"agent_timed_out" => {category: :lifecycle, kind: :timed_out},
|
|
26
|
+
"agent_blocked" => {category: :lifecycle, kind: :blocked},
|
|
27
|
+
"agent_terminated" => {category: :lifecycle, kind: :terminated},
|
|
28
|
+
"agent_claimed" => {category: :lifecycle, kind: :claimed},
|
|
29
|
+
"agent_unclaimed" => {category: :lifecycle, kind: :unclaimed},
|
|
30
|
+
"artifact_published" => {category: :artifact}
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
def initialize(team_log_store:, **kwargs)
|
|
34
|
+
super(**kwargs)
|
|
35
|
+
@team_log_store = team_log_store
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def stateful? = true
|
|
39
|
+
|
|
40
|
+
def notify(payload)
|
|
41
|
+
team_id = payload.dig(:context, :team)&.id
|
|
42
|
+
return unless team_id
|
|
43
|
+
|
|
44
|
+
mapping = EVENT_MAP[payload[:event_type]]
|
|
45
|
+
return unless mapping
|
|
46
|
+
|
|
47
|
+
entry = build_entry(payload, mapping)
|
|
48
|
+
log = @team_log_store.get_or_create(team_id)
|
|
49
|
+
log.append(**entry)
|
|
50
|
+
rescue => e
|
|
51
|
+
Superkick.logger.warn("notifier:team_log") { "Failed to write team log: #{e.message}" }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def agent_finished(agent_id:)
|
|
55
|
+
# No cleanup needed — team log entries are persistent
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def build_entry(payload, mapping)
|
|
61
|
+
agent = payload.dig(:context, :agent)
|
|
62
|
+
event_kind = payload.dig(:context, :kind)
|
|
63
|
+
|
|
64
|
+
entry = {
|
|
65
|
+
agent_id: payload[:agent_id],
|
|
66
|
+
agent_role: agent&.team_role || "unknown",
|
|
67
|
+
role: agent&.role,
|
|
68
|
+
category: mapping[:category],
|
|
69
|
+
message: payload[:message]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Kind: use mapping default, but allow event-level override (e.g. agent_update kinds)
|
|
73
|
+
# For agent_spawned, infer kind from team_role (member → joined, others → spawned)
|
|
74
|
+
kind = event_kind || mapping[:kind]
|
|
75
|
+
if payload[:event_type] == "agent_spawned" && !kind
|
|
76
|
+
kind = (agent&.team_role == "member") ? :joined : :spawned
|
|
77
|
+
end
|
|
78
|
+
entry[:kind] = kind
|
|
79
|
+
|
|
80
|
+
# Directed messages include target
|
|
81
|
+
if payload[:event_type] == "teammate_message"
|
|
82
|
+
target = payload.dig(:context, :target_agent_id) ||
|
|
83
|
+
payload[:monitor]&.name # fallback
|
|
84
|
+
entry[:target_agent_id] = target if target
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Artifact entries include the artifact name
|
|
88
|
+
if payload[:event_type] == "artifact_published"
|
|
89
|
+
entry[:artifact_name] = payload.dig(:context, :artifact_name)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
entry
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superkick
|
|
4
|
+
module Team
|
|
5
|
+
# LogStore — server-level registry of active team logs.
|
|
6
|
+
#
|
|
7
|
+
# Thread-safe. Lazily creates Team::Log instances on first access.
|
|
8
|
+
class LogStore
|
|
9
|
+
def initialize(teams_dir: Superkick.config.teams_dir)
|
|
10
|
+
@teams_dir = teams_dir
|
|
11
|
+
@logs = {}
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Get or create the team log for the given team_id.
|
|
16
|
+
def get_or_create(team_id)
|
|
17
|
+
@mutex.synchronize do
|
|
18
|
+
@logs[team_id.to_s] ||= Team::Log.new(team_id, teams_dir: @teams_dir)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Get the team log if it exists, nil otherwise.
|
|
23
|
+
def get(team_id)
|
|
24
|
+
@mutex.synchronize { @logs[team_id.to_s] }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# List all active team IDs.
|
|
28
|
+
def team_ids
|
|
29
|
+
@mutex.synchronize { @logs.keys.dup }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Remove the team log entry (does not delete the file).
|
|
33
|
+
def cleanup(team_id)
|
|
34
|
+
@mutex.synchronize { @logs.delete(team_id.to_s) }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Backwards-compatible alias
|
|
40
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superkick
|
|
4
|
+
# Core Liquid filters available in all templates.
|
|
5
|
+
#
|
|
6
|
+
# These are registered globally on every Liquid::Environment.
|
|
7
|
+
# Integration-specific filters are declared inline via the `liquid do`
|
|
8
|
+
# DSL on each monitor/spawner class.
|
|
9
|
+
module TemplateFilters
|
|
10
|
+
def time(input = nil)
|
|
11
|
+
t = input.is_a?(Time) ? input : Time.now
|
|
12
|
+
t.strftime("%H:%M")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def short_sha(sha)
|
|
16
|
+
sha.to_s[0, 7]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def truncate(str, max = 72)
|
|
20
|
+
str = str.to_s
|
|
21
|
+
(str.length <= max) ? str : "#{str[0, max - 1]}…"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "liquid"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Superkick
|
|
7
|
+
# Resolves and renders Liquid templates for injected events.
|
|
8
|
+
#
|
|
9
|
+
# Resolution order:
|
|
10
|
+
# 1. ~/.superkick/templates/<monitor_name>/<event_type>.liquid (user per-instance)
|
|
11
|
+
# 2. ~/.superkick/templates/<monitor_type>/<event_type>.liquid (user per-type)
|
|
12
|
+
# 3. <Monitor.templates_dir>/<event_type>.liquid (monitor bundled)
|
|
13
|
+
#
|
|
14
|
+
# Templates are rendered with Liquid. All event keys are available as
|
|
15
|
+
# template variables. Built-in filters: time, truncate, short_sha.
|
|
16
|
+
# Integration-specific filters are resolved via the monitor/spawner class.
|
|
17
|
+
module TemplateRenderer
|
|
18
|
+
FALLBACK_TEMPLATE = "SUPERKICK [{{ \"now\" | time }}]: {{ event_type }} — {{ monitor_name | truncate: 40 }}"
|
|
19
|
+
|
|
20
|
+
# Render an event hash to a string.
|
|
21
|
+
# @param event [Hash] must include :monitor_type and :event_type
|
|
22
|
+
# @return [String]
|
|
23
|
+
def self.render(event)
|
|
24
|
+
template_path = resolve(event)
|
|
25
|
+
source = template_path ? File.read(template_path, encoding: "utf-8") : FALLBACK_TEMPLATE
|
|
26
|
+
render_source(source, event, monitor_type: event[:monitor_type])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Render a Liquid source string against an event hash.
|
|
30
|
+
# Useful for testing and for callers that have already resolved a template.
|
|
31
|
+
# @param source [String] Liquid template source
|
|
32
|
+
# @param event [Hash] event data; all keys become template variables
|
|
33
|
+
# @param monitor_type [String, nil] monitor type key; used to look up template filters
|
|
34
|
+
# @return [String]
|
|
35
|
+
def self.render_source(source, event, monitor_type: nil)
|
|
36
|
+
env = environment_for_type(monitor_type)
|
|
37
|
+
template = ::Liquid::Template.parse(source, environment: env)
|
|
38
|
+
assigns = build_assigns(event)
|
|
39
|
+
template.render(assigns).strip
|
|
40
|
+
rescue => e
|
|
41
|
+
Superkick.logger.error("renderer") { "Template render error: #{e.message}\n#{e.backtrace.first(5).join("\n")}" }
|
|
42
|
+
"SUPERKICK: #{event[:event_type]} in #{event[:repo]}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Copy all built-in monitor templates to ~/.superkick/templates/ for user customisation.
|
|
46
|
+
# Iterates every registered monitor and copies its bundled templates,
|
|
47
|
+
# namespaced under <type>/ within the user templates directory.
|
|
48
|
+
# @param force [Boolean] overwrite existing files
|
|
49
|
+
def self.install_defaults(force: false)
|
|
50
|
+
Monitor.registered.each do |type, monitor_class|
|
|
51
|
+
next unless (templates_dir = monitor_class.templates_dir) && File.directory?(templates_dir)
|
|
52
|
+
|
|
53
|
+
install_from(templates_dir, File.join(Superkick.config.templates_dir, type.to_s), force:)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Copy all built-in spawner templates to ~/.superkick/templates/spawners/ for
|
|
58
|
+
# user customisation. Iterates every registered Spawner and copies
|
|
59
|
+
# its bundled spawn templates, namespaced under spawners/<type>/.
|
|
60
|
+
# @param force [Boolean] overwrite existing files
|
|
61
|
+
def self.install_spawner_defaults(force: false)
|
|
62
|
+
Spawner.registered.each do |type, spawner_class|
|
|
63
|
+
next unless (templates_dir = spawner_class.spawn_templates_dir) && File.directory?(templates_dir)
|
|
64
|
+
|
|
65
|
+
install_from(templates_dir, File.join(Superkick.config.templates_dir, "spawners", type.to_s), force:)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.install_from(source_dir, dest_dir, force: false)
|
|
70
|
+
Dir.glob("#{source_dir}/**/*.liquid").each do |source_path|
|
|
71
|
+
rel = source_path.sub("#{source_dir}/", "")
|
|
72
|
+
dest = File.join(dest_dir, rel)
|
|
73
|
+
|
|
74
|
+
if File.exist?(dest) && !force
|
|
75
|
+
Superkick.logger.info("installer") { "Skipping (exists): #{dest}" }
|
|
76
|
+
next
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
80
|
+
FileUtils.cp(source_path, dest)
|
|
81
|
+
Superkick.logger.info("installer") { "Installed: #{dest}" }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
private_class_method :install_from
|
|
85
|
+
|
|
86
|
+
# Build a Liquid::Environment for a class that includes Superkick::Liquid.
|
|
87
|
+
# Combines global TemplateFilters + inline filters and block tags from the
|
|
88
|
+
# liquid_config DSL. Cached per class.
|
|
89
|
+
# @param klass [Class] monitor or spawner class
|
|
90
|
+
# @return [Liquid::Environment]
|
|
91
|
+
def self.environment_for(klass)
|
|
92
|
+
@environment_cache ||= {}
|
|
93
|
+
@environment_cache[klass] ||= build_environment(klass)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Look up the monitor/spawner class for a type key and return its environment.
|
|
97
|
+
# Falls back to a global-filters-only environment for unknown types.
|
|
98
|
+
private_class_method def self.environment_for_type(monitor_type)
|
|
99
|
+
return global_environment unless monitor_type
|
|
100
|
+
|
|
101
|
+
key = monitor_type.to_sym
|
|
102
|
+
klass = Monitor.registered[key] || Spawner.registered[key]
|
|
103
|
+
return global_environment unless klass
|
|
104
|
+
|
|
105
|
+
environment_for(klass)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Environment with only global TemplateFilters — used when no class-specific
|
|
109
|
+
# customizations are needed.
|
|
110
|
+
private_class_method def self.global_environment
|
|
111
|
+
@global_environment ||= ::Liquid::Environment.build do |env|
|
|
112
|
+
env.register_filter(TemplateFilters)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private_class_method def self.build_environment(klass)
|
|
117
|
+
config = klass.respond_to?(:liquid_config) ? klass.liquid_config : nil
|
|
118
|
+
|
|
119
|
+
::Liquid::Environment.build do |env|
|
|
120
|
+
# Global filters — always present
|
|
121
|
+
env.register_filter(TemplateFilters)
|
|
122
|
+
|
|
123
|
+
# Inline filters from liquid_config DSL
|
|
124
|
+
if config && !config.filters.empty?
|
|
125
|
+
mod = Module.new
|
|
126
|
+
config.filters.each do |name, handler|
|
|
127
|
+
mod.define_method(name, &handler)
|
|
128
|
+
end
|
|
129
|
+
env.register_filter(mod)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Block tags from liquid_config DSL
|
|
133
|
+
if config
|
|
134
|
+
config.blocks.each do |name, handler|
|
|
135
|
+
env.register_tag(name.to_s, NotifierTemplate.create_block_class(handler))
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Bodyless tags from liquid_config DSL
|
|
139
|
+
config.tags.each do |name, handler|
|
|
140
|
+
env.register_tag(name.to_s, NotifierTemplate.create_tag_class(handler))
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
private_class_method :build_environment
|
|
146
|
+
|
|
147
|
+
# Clear cached environments. Called from tests via reset_config!.
|
|
148
|
+
def self.reset_environments!
|
|
149
|
+
@environment_cache = nil
|
|
150
|
+
@global_environment = nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Build Liquid assigns from event hash.
|
|
154
|
+
# Liquid requires string keys.
|
|
155
|
+
private_class_method def self.build_assigns(event)
|
|
156
|
+
event.transform_keys(&:to_s)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Resolve a notification template for a notifier class and event type.
|
|
160
|
+
# Returns the template source string, or nil if no template is found.
|
|
161
|
+
#
|
|
162
|
+
# Resolution order:
|
|
163
|
+
# 1. ~/.superkick/templates/notifications/<type>/<event_type>.liquid (user override)
|
|
164
|
+
# 2. <Notifier.templates_dir>/<event_type>.liquid (bundled per-event)
|
|
165
|
+
# 3. ~/.superkick/templates/notifications/<type>/default.liquid (user catch-all)
|
|
166
|
+
# 4. <Notifier.templates_dir>/default.liquid (bundled catch-all)
|
|
167
|
+
def self.resolve_notification(notifier_class, event_type)
|
|
168
|
+
type = notifier_class.type.to_s
|
|
169
|
+
event_type = event_type.to_s
|
|
170
|
+
user_dir = File.join(Superkick.config.templates_dir, "notifications", type)
|
|
171
|
+
bundled_dir = notifier_class.templates_dir
|
|
172
|
+
|
|
173
|
+
# 1. User per-event override
|
|
174
|
+
user_event = File.join(user_dir, "#{event_type}.liquid")
|
|
175
|
+
return File.read(user_event, encoding: "utf-8") if File.exist?(user_event)
|
|
176
|
+
|
|
177
|
+
# 2. Bundled per-event
|
|
178
|
+
if bundled_dir
|
|
179
|
+
bundled_event = File.join(bundled_dir, "#{event_type}.liquid")
|
|
180
|
+
return File.read(bundled_event, encoding: "utf-8") if File.exist?(bundled_event)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# 3. User default catch-all
|
|
184
|
+
user_default = File.join(user_dir, "default.liquid")
|
|
185
|
+
return File.read(user_default, encoding: "utf-8") if File.exist?(user_default)
|
|
186
|
+
|
|
187
|
+
# 4. Bundled default catch-all
|
|
188
|
+
if bundled_dir
|
|
189
|
+
bundled_default = File.join(bundled_dir, "default.liquid")
|
|
190
|
+
return File.read(bundled_default, encoding: "utf-8") if File.exist?(bundled_default)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
nil
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
private_class_method def self.resolve(event)
|
|
197
|
+
monitor_type = event[:monitor_type]&.to_s
|
|
198
|
+
monitor_name = event[:monitor_name]&.to_s
|
|
199
|
+
event_type = event[:event_type]&.to_s # Symbol from dispatch, needs String for path
|
|
200
|
+
|
|
201
|
+
return nil unless monitor_type && event_type
|
|
202
|
+
|
|
203
|
+
# 1. User per-instance override (by monitor name)
|
|
204
|
+
if monitor_name && monitor_name != monitor_type
|
|
205
|
+
name_path = File.join(Superkick.config.templates_dir, monitor_name, "#{event_type}.liquid")
|
|
206
|
+
return name_path if File.exist?(name_path)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# 2. User per-type override
|
|
210
|
+
user_path = File.join(Superkick.config.templates_dir, monitor_type, "#{event_type}.liquid")
|
|
211
|
+
return user_path if File.exist?(user_path)
|
|
212
|
+
|
|
213
|
+
# 3. Monitor bundled templates
|
|
214
|
+
monitor_class = Monitor.registered[monitor_type.to_sym]
|
|
215
|
+
if monitor_class&.templates_dir
|
|
216
|
+
bundled = File.join(monitor_class.templates_dir, "#{event_type}.liquid")
|
|
217
|
+
return bundled if File.exist?(bundled)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
nil
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
You are a planning agent coordinating a team of AI coding agents.
|
|
2
|
+
|
|
3
|
+
## Task
|
|
4
|
+
{{ event_type }}: {{ title }}
|
|
5
|
+
{{ description }}
|
|
6
|
+
|
|
7
|
+
## Available repositories
|
|
8
|
+
{{ repository_context }}
|
|
9
|
+
|
|
10
|
+
## Your responsibilities
|
|
11
|
+
1. Analyze the task and determine which repositories need changes.
|
|
12
|
+
2. For each affected repository, define a clear sub-task with specific instructions.
|
|
13
|
+
3. Spawn a worker agent for each repository using the superkick_spawn_worker tool.
|
|
14
|
+
4. Monitor team progress using superkick_team_status.
|
|
15
|
+
5. Coordinate between agents if one depends on another's work.
|
|
16
|
+
6. Use superkick_post_update to share your planning decisions with the team.
|
|
17
|
+
7. Read worker artifacts (implementation plans, summaries) to track detailed progress.
|
|
18
|
+
|
|
19
|
+
## Available tools
|
|
20
|
+
- superkick_spawn_worker: Create a worker agent for a specific repository
|
|
21
|
+
- superkick_discover_repositories: List available repositories with descriptions and tags
|
|
22
|
+
- superkick_team_status: Check what all teammates are doing
|
|
23
|
+
- superkick_post_update: Broadcast a status update to the team
|
|
24
|
+
- superkick_post_update (with target_agent_id): Send a message to a specific teammate
|
|
25
|
+
- superkick_list_teammates: List all agents on your team
|
|
26
|
+
- superkick_publish_artifact: Share structured data (plans, contracts) with the team
|
|
27
|
+
- superkick_read_artifact: Read an artifact published by a teammate
|
|
28
|
+
- superkick_list_artifacts: List all published artifacts
|
|
29
|
+
- superkick_signal_goal: Signal when the team's work is complete
|
|
30
|
+
|
|
31
|
+
## Guidelines
|
|
32
|
+
- Spawn workers in parallel when their work is independent.
|
|
33
|
+
- If repository B depends on repository A's changes, note this in repository B's
|
|
34
|
+
instructions and tell its worker to check team status before starting dependent work.
|
|
35
|
+
- Keep sub-tasks focused — each worker should have a clear, achievable goal.
|
|
36
|
+
- Give each worker a descriptive role label (e.g. "API migration", "Test writer").
|
|
37
|
+
- Use superkick_post_update with category "decision" to explain your decomposition.
|
|
38
|
+
- Read worker artifacts periodically to review their implementation plans.
|