brainiac 0.0.6 → 0.0.8
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 +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +136 -6
- data/bin/brainiac +472 -44
- data/bin/brainiac-completion.bash +1 -1
- data/lib/brainiac/agents.rb +27 -74
- data/lib/brainiac/brain.rb +6 -6
- data/lib/brainiac/config.rb +40 -76
- data/lib/brainiac/handlers/discord/api.rb +196 -0
- data/lib/brainiac/handlers/discord/config.rb +134 -0
- data/lib/brainiac/handlers/discord/delivery.rb +196 -0
- data/lib/brainiac/handlers/discord/gateway.rb +212 -0
- data/lib/brainiac/handlers/discord/message.rb +933 -0
- data/lib/brainiac/handlers/discord/reactions.rb +215 -0
- data/lib/brainiac/handlers/discord.rb +14 -1892
- data/lib/brainiac/handlers/github.rb +134 -317
- data/lib/brainiac/handlers/shared/git.rb +190 -0
- data/lib/brainiac/handlers/shared/inline_tags.rb +89 -0
- data/lib/brainiac/handlers/zoho.rb +103 -153
- data/lib/brainiac/helpers.rb +43 -455
- data/lib/brainiac/hooks.rb +86 -0
- data/lib/brainiac/plugins.rb +154 -0
- data/lib/brainiac/prompts.rb +34 -172
- data/lib/brainiac/restart.rb +112 -0
- data/lib/brainiac/routes/api.rb +411 -0
- data/lib/brainiac/users.rb +1 -7
- data/lib/brainiac/version.rb +1 -1
- data/lib/brainiac/zoho_mail_api.rb +2 -1
- data/lib/brainiac.rb +8 -1
- data/monitor/daemon.rb +4 -27
- data/monitor/shared.rb +247 -0
- data/monitor/{waybar-deploy-env.rb → waybar/deploy_env.rb} +17 -89
- data/monitor/waybar/setup.rb +232 -0
- data/monitor/waybar/status.rb +51 -0
- data/monitor/{view-logs-rofi.rb → waybar/view_logs.rb} +21 -88
- data/monitor/{deploy-env-macos.rb → xbar/deploy_env.rb} +3 -2
- data/monitor/xbar/plugin.rb +149 -0
- data/monitor/{setup-menubar.rb → xbar/setup.rb} +14 -30
- data/receiver.rb +44 -551
- data/templates/agents.json.example +1 -2
- data/templates/brainiac.json.example +8 -0
- data/templates/cli-providers/kiro.json.example +8 -2
- data/templates/plugins.json.example +3 -0
- data/templates/users.json.example +0 -3
- metadata +25 -23
- data/lib/brainiac/card_index.rb +0 -389
- data/lib/brainiac/deployments.rb +0 -258
- data/lib/brainiac/handlers/fizzy.rb +0 -1292
- data/lib/brainiac/planning.rb +0 -237
- data/lib/user_registry.rb +0 -159
- data/monitor/menubar.rb +0 -295
- data/monitor/setup-waybar-deploy-envs.rb +0 -121
- data/monitor/setup-waybar-deployments.rb +0 -96
- data/monitor/setup-waybar-module.rb +0 -113
- data/monitor/setup-xbar-plugin.rb +0 -35
- data/monitor/view-logs.rb +0 -119
- data/monitor/waybar-config-updater.rb +0 -56
- data/monitor/waybar-deployments.rb +0 -239
- data/monitor/waybar.rb +0 -146
- data/monitor/xbar.3s.rb +0 -179
- data/templates/fizzy.json.example +0 -24
- /data/monitor/{open-action.sh → xbar/open_action.sh} +0 -0
- /data/monitor/{view-logs-macos.rb → xbar/view_logs.rb} +0 -0
|
@@ -5,14 +5,9 @@
|
|
|
5
5
|
# Fallback column ID for backwards compatibility when no board config exists
|
|
6
6
|
DEFAULT_UAT_COLUMN_ID = "03fsmglsr6az06ppyotawsti8"
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# Find a Fizzy card by matching the PR's head branch to a branch in the card map.
|
|
14
|
-
def find_card_by_branch(branch)
|
|
15
|
-
map = load_card_map
|
|
8
|
+
# Find a work item by matching the PR's head branch to a branch in the card map.
|
|
9
|
+
def find_work_item_by_branch(branch)
|
|
10
|
+
map = load_work_item_map
|
|
16
11
|
map.each do |internal_id, info|
|
|
17
12
|
next unless info["branch"] == branch
|
|
18
13
|
|
|
@@ -21,14 +16,14 @@ def find_card_by_branch(branch)
|
|
|
21
16
|
nil
|
|
22
17
|
end
|
|
23
18
|
|
|
24
|
-
# Track a newly opened PR in the
|
|
25
|
-
def
|
|
19
|
+
# Track a newly opened PR in the work item map by matching its branch.
|
|
20
|
+
def track_pr_in_work_items(payload)
|
|
26
21
|
pr = payload["pull_request"]
|
|
27
22
|
branch = pr.dig("head", "ref")
|
|
28
23
|
pr_number = pr["number"]
|
|
29
24
|
pr_url = pr["html_url"]
|
|
30
25
|
|
|
31
|
-
result =
|
|
26
|
+
result = find_work_item_by_branch(branch)
|
|
32
27
|
unless result
|
|
33
28
|
LOG.info "[PR Track] No card found for branch #{branch}"
|
|
34
29
|
return
|
|
@@ -41,9 +36,9 @@ def track_pr_in_card_map(payload)
|
|
|
41
36
|
prs << { "number" => pr_number, "url" => pr_url }
|
|
42
37
|
card_info["prs"] = prs
|
|
43
38
|
|
|
44
|
-
map =
|
|
39
|
+
map = load_work_item_map
|
|
45
40
|
map[internal_id] = card_info
|
|
46
|
-
|
|
41
|
+
save_work_item_map(map)
|
|
47
42
|
LOG.info "[PR Track] Tracked PR ##{pr_number} on card ##{card_info["number"]} (branch: #{branch})"
|
|
48
43
|
end
|
|
49
44
|
|
|
@@ -57,15 +52,6 @@ rescue StandardError => e
|
|
|
57
52
|
end
|
|
58
53
|
|
|
59
54
|
# Check if a PR link is already present in the card's comments.
|
|
60
|
-
def pr_link_already_commented?(card_number, pr_url, chdir:, env: default_fizzy_env)
|
|
61
|
-
output = run_cmd("fizzy", "comment", "list", "--card", card_number.to_s, chdir: chdir, env: env)
|
|
62
|
-
data = JSON.parse(output)
|
|
63
|
-
comments = data["data"] || []
|
|
64
|
-
comments.any? { |c| (c.dig("body", "plain_text") || "").include?(pr_url) }
|
|
65
|
-
rescue StandardError => e
|
|
66
|
-
LOG.warn "Could not check existing comments for card ##{card_number}: #{e.message}"
|
|
67
|
-
false
|
|
68
|
-
end
|
|
69
55
|
|
|
70
56
|
def handle_github_pr_merged(payload)
|
|
71
57
|
pr = payload["pull_request"]
|
|
@@ -75,14 +61,12 @@ def handle_github_pr_merged(payload)
|
|
|
75
61
|
pr_title = pr["title"]
|
|
76
62
|
repo_full_name = payload.dig("repository", "full_name")
|
|
77
63
|
|
|
78
|
-
# Only act on merges into the repo's default branch
|
|
79
64
|
default_branch = payload.dig("repository", "default_branch") || "main"
|
|
80
65
|
unless base == default_branch
|
|
81
66
|
LOG.info "PR merged into #{base}, not #{default_branch} — ignoring"
|
|
82
67
|
return [200, { status: "ignored", reason: "not merged into #{default_branch}" }.to_json]
|
|
83
68
|
end
|
|
84
69
|
|
|
85
|
-
# Identify project by GitHub repo
|
|
86
70
|
project_result = identify_project_by_repo(repo_full_name)
|
|
87
71
|
unless project_result
|
|
88
72
|
LOG.info "No project found for GitHub repo #{repo_full_name}"
|
|
@@ -92,65 +76,21 @@ def handle_github_pr_merged(payload)
|
|
|
92
76
|
project_key, project_config = project_result
|
|
93
77
|
repo_path = project_config["repo_path"]
|
|
94
78
|
|
|
95
|
-
result =
|
|
79
|
+
result = find_work_item_by_branch(branch)
|
|
96
80
|
unless result
|
|
97
|
-
LOG.info "No
|
|
81
|
+
LOG.info "No card found for branch #{branch}"
|
|
98
82
|
return [200, { status: "ignored", reason: "no matching card" }.to_json]
|
|
99
83
|
end
|
|
100
84
|
|
|
101
85
|
internal_id, card_info = result
|
|
102
86
|
card_number = card_info["number"]
|
|
103
|
-
|
|
104
87
|
unless card_number
|
|
105
88
|
LOG.warn "Card #{internal_id} has no number — can't comment or move"
|
|
106
89
|
return [200, { status: "ignored", reason: "card has no number" }.to_json]
|
|
107
90
|
end
|
|
108
91
|
|
|
109
92
|
LOG.info "PR merged into main for card ##{card_number} (project: #{project_key}): #{pr_url}"
|
|
110
|
-
|
|
111
|
-
# Use the card's assigned agent identity for fizzy interactions
|
|
112
|
-
card_agent = card_info["agent"]
|
|
113
|
-
card_fizzy_env = fizzy_env_for(card_agent)
|
|
114
|
-
|
|
115
|
-
# Comment with the PR link if not already there
|
|
116
|
-
if pr_link_already_commented?(card_number, pr_url, chdir: repo_path, env: card_fizzy_env)
|
|
117
|
-
LOG.info "PR link already on card ##{card_number}, skipping comment"
|
|
118
|
-
else
|
|
119
|
-
comment_body = "<p>PR merged into main: <a href=\"#{pr_url}\">#{pr_title}</a></p><p>Branch: <code>#{branch}</code></p>"
|
|
120
|
-
run_cmd("fizzy", "comment", "create", "--card", card_number.to_s, "--body", comment_body, chdir: repo_path, env: card_fizzy_env)
|
|
121
|
-
LOG.info "Commented PR link on card ##{card_number} as #{card_agent || "default"}"
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# Move card to UAT column — merged into main means it's deployed to UAT
|
|
125
|
-
mark_card_merged(card_number)
|
|
126
|
-
run_cmd("fizzy", "card", "column", card_number.to_s, "--column", uat_column_id(project_config), chdir: repo_path, env: card_fizzy_env)
|
|
127
|
-
record_self_move(card_number)
|
|
128
|
-
LOG.info "Moved card ##{card_number} to UAT column as #{card_agent || "default"}"
|
|
129
|
-
|
|
130
|
-
# Clean up the primary worktree and any cross-agent review worktrees
|
|
131
|
-
cleanup_card_worktrees(card_number, repo_path: repo_path, primary_worktree: card_info["worktree"], primary_branch: branch)
|
|
132
|
-
|
|
133
|
-
# Clear any deployment environments occupied by this card
|
|
134
|
-
clear_deployment_for_card(card_number)
|
|
135
|
-
|
|
136
|
-
# Dispatch agent to comment UAT testing steps on the Fizzy card
|
|
137
|
-
agent_name = card_agent || agent_name_for(project_config)
|
|
138
|
-
card_title = card_info["title"] || pr_title
|
|
139
|
-
|
|
140
|
-
prompt = render_prompt(PROMPT_GITHUB_UAT,
|
|
141
|
-
{ "CARD_NUMBER" => card_number,
|
|
142
|
-
"CARD_TITLE" => card_title,
|
|
143
|
-
"PR_NUMBER" => pr["number"].to_s },
|
|
144
|
-
brain_context: build_brain_context(agent_name: agent_name, card_number: card_number, card_title: card_title,
|
|
145
|
-
project_key: project_key),
|
|
146
|
-
agent_name: agent_name,
|
|
147
|
-
channel: :fizzy,
|
|
148
|
-
board_key: board_key_for_project(project_config))
|
|
149
|
-
|
|
150
|
-
pid, log_file = run_agent(prompt, project_config: project_config, chdir: repo_path, log_name: "uat-#{card_number}", agent_name: agent_name,
|
|
151
|
-
source: :fizzy, source_context: { card_number: card_number }, skip_column_move: true)
|
|
152
|
-
register_session("card-#{card_number}", pid, log_file: log_file, agent_name: agent_name)
|
|
153
|
-
LOG.info "Dispatched #{agent_name} for UAT testing steps on card ##{card_number}"
|
|
93
|
+
process_merged_pr(card_info, card_number, branch, pr, pr_url, pr_title, project_key, project_config, repo_path)
|
|
154
94
|
|
|
155
95
|
[200, { status: "processed", card: card_number, pr: pr_url, action: "merged_to_uat", project: project_key }.to_json]
|
|
156
96
|
rescue StandardError => e
|
|
@@ -158,6 +98,24 @@ rescue StandardError => e
|
|
|
158
98
|
[500, { error: e.message }.to_json]
|
|
159
99
|
end
|
|
160
100
|
|
|
101
|
+
def process_merged_pr(card_info, card_number, branch, pull_request, pr_url, pr_title, project_key, project_config, repo_path)
|
|
102
|
+
mark_work_item_merged(card_number)
|
|
103
|
+
cleanup_work_item_worktrees(card_number, repo_path: repo_path, primary_worktree: card_info["worktree"], primary_branch: branch)
|
|
104
|
+
|
|
105
|
+
# Emit hook — plugins handle their own post-merge actions
|
|
106
|
+
# (e.g., card plugin posts PR link, moves card column, dispatches UAT agent)
|
|
107
|
+
Brainiac.emit(:pr_merged,
|
|
108
|
+
card_number: card_number,
|
|
109
|
+
card_info: card_info,
|
|
110
|
+
branch: branch,
|
|
111
|
+
pull_request: pull_request,
|
|
112
|
+
pr_url: pr_url,
|
|
113
|
+
pr_title: pr_title,
|
|
114
|
+
project_key: project_key,
|
|
115
|
+
project_config: project_config,
|
|
116
|
+
repo_path: repo_path)
|
|
117
|
+
end
|
|
118
|
+
|
|
161
119
|
def handle_github_issue_comment(payload)
|
|
162
120
|
comment = payload["comment"]
|
|
163
121
|
issue = payload["issue"]
|
|
@@ -166,13 +124,11 @@ def handle_github_issue_comment(payload)
|
|
|
166
124
|
comment_user = comment.dig("user", "login")
|
|
167
125
|
repo_name = payload.dig("repository", "full_name")
|
|
168
126
|
|
|
169
|
-
# Only process if this is a PR (issues have pull_request key when they're PRs)
|
|
170
127
|
unless issue["pull_request"]
|
|
171
128
|
LOG.info "Issue comment on non-PR issue ##{issue["number"]}, ignoring"
|
|
172
129
|
return [200, { status: "ignored", reason: "not a PR comment" }.to_json]
|
|
173
130
|
end
|
|
174
131
|
|
|
175
|
-
# Identify project by GitHub repo
|
|
176
132
|
project_result = identify_project_by_repo(repo_name)
|
|
177
133
|
unless project_result
|
|
178
134
|
LOG.info "No project found for GitHub repo #{repo_name}"
|
|
@@ -180,17 +136,15 @@ def handle_github_issue_comment(payload)
|
|
|
180
136
|
end
|
|
181
137
|
|
|
182
138
|
project_key, project_config = project_result
|
|
183
|
-
|
|
184
139
|
pr_number = issue["number"]
|
|
185
|
-
issue["html_url"]
|
|
186
140
|
|
|
187
|
-
|
|
188
|
-
|
|
141
|
+
pr_data = run_cmd("gh", "api", "/repos/#{repo_name}/pulls/#{pr_number}", "--jq", "{branch: .head.ref}",
|
|
142
|
+
chdir: project_config["repo_path"])
|
|
189
143
|
branch = JSON.parse(pr_data)["branch"]
|
|
190
144
|
|
|
191
|
-
result =
|
|
145
|
+
result = find_work_item_by_branch(branch)
|
|
192
146
|
unless result
|
|
193
|
-
LOG.info "No
|
|
147
|
+
LOG.info "No card found for PR ##{pr_number} (branch: #{branch})"
|
|
194
148
|
return [200, { status: "ignored", reason: "no matching card" }.to_json]
|
|
195
149
|
end
|
|
196
150
|
|
|
@@ -198,54 +152,52 @@ def handle_github_issue_comment(payload)
|
|
|
198
152
|
card_number = card_info["number"]
|
|
199
153
|
worktree = card_info["worktree"]
|
|
200
154
|
|
|
201
|
-
# Only process if worktree exists
|
|
202
155
|
unless worktree && File.directory?(worktree)
|
|
203
156
|
LOG.info "No active worktree for PR ##{pr_number}, ignoring comment"
|
|
204
157
|
return [200, { status: "ignored", reason: "no active worktree" }.to_json]
|
|
205
158
|
end
|
|
206
159
|
|
|
207
|
-
LOG.info "PR comment from #{comment_user} on PR ##{pr_number} for card ##{card_number} (project: #{project_key})"
|
|
208
|
-
|
|
209
160
|
card_key = "card-#{card_number}"
|
|
210
161
|
if session_active?(card_key)
|
|
211
162
|
LOG.info "Skipping PR comment on card ##{card_number} — agent session already active"
|
|
212
163
|
return [200, { status: "ignored", reason: "session already active" }.to_json]
|
|
213
164
|
end
|
|
214
165
|
|
|
215
|
-
#
|
|
166
|
+
LOG.info "PR comment from #{comment_user} on PR ##{pr_number} for card ##{card_number} (project: #{project_key})"
|
|
167
|
+
dispatch_pr_comment(card_number, card_key, pr_number, comment_id, comment_user, comment_body,
|
|
168
|
+
repo_name, worktree, project_key, project_config)
|
|
169
|
+
|
|
170
|
+
[200, { status: "processed", card: card_number, pr: pr_number, comment_id: comment_id, project: project_key }.to_json]
|
|
171
|
+
rescue StandardError => e
|
|
172
|
+
LOG.error "Error handling PR comment: #{e.message}"
|
|
173
|
+
[500, { error: e.message }.to_json]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def dispatch_pr_comment(card_number, card_key, pr_number, comment_id, comment_user, comment_body,
|
|
177
|
+
repo_name, worktree, project_key, project_config)
|
|
216
178
|
Thread.new do
|
|
217
|
-
run_cmd("gh", "api", "-X", "POST", "/repos/#{repo_name}/issues/comments/#{comment_id}/reactions",
|
|
218
|
-
"Accept: application/vnd.github+json", chdir: worktree)
|
|
219
|
-
LOG.info "Added 👀 reaction to comment ##{comment_id}"
|
|
179
|
+
run_cmd("gh", "api", "-X", "POST", "/repos/#{repo_name}/issues/comments/#{comment_id}/reactions",
|
|
180
|
+
"-f", "content=eyes", "-H", "Accept: application/vnd.github+json", chdir: worktree)
|
|
220
181
|
rescue StandardError => e
|
|
221
182
|
LOG.warn "Could not add reaction to comment: #{e.message}"
|
|
222
183
|
end
|
|
223
184
|
|
|
224
|
-
# Detect model override
|
|
225
|
-
model = detect_model(project_config, text: comment_body)
|
|
226
|
-
effort = detect_effort(project_config, text: comment_body)
|
|
227
185
|
agent_name = agent_name_for(project_config)
|
|
228
|
-
|
|
229
186
|
prompt = render_prompt(PROMPT_GITHUB_PR_COMMENT,
|
|
230
|
-
{ "CARD_NUMBER" => card_number,
|
|
231
|
-
"
|
|
232
|
-
"
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
187
|
+
{ "CARD_NUMBER" => card_number, "CARD_ID" => card_number,
|
|
188
|
+
"COMMENT_CREATOR" => comment_user, "COMMENT_BODY" => comment_body,
|
|
189
|
+
"PR_NUMBER" => pr_number.to_s, "WORKTREE_PATH" => worktree },
|
|
190
|
+
brain_context: build_brain_context(agent_name: agent_name, card_number: card_number,
|
|
191
|
+
project_key: project_key, comment_body: comment_body),
|
|
192
|
+
agent_name: agent_name, channel: :github)
|
|
193
|
+
|
|
194
|
+
pid, log_file = run_agent(prompt, project_config: project_config, chdir: worktree,
|
|
195
|
+
log_name: "pr-comment-#{pr_number}",
|
|
196
|
+
model: detect_model(project_config, text: comment_body),
|
|
197
|
+
effort: detect_effort(project_config, text: comment_body),
|
|
198
|
+
agent_name: agent_name, source: :github,
|
|
199
|
+
source_context: { pr_number: pr_number, repo_name: repo_name, work_dir: worktree })
|
|
243
200
|
register_session(card_key, pid, log_file: log_file, agent_name: agent_name)
|
|
244
|
-
|
|
245
|
-
[200, { status: "processed", card: card_number, pr: pr_number, comment_id: comment_id, project: project_key }.to_json]
|
|
246
|
-
rescue StandardError => e
|
|
247
|
-
LOG.error "Error handling PR comment: #{e.message}"
|
|
248
|
-
[500, { error: e.message }.to_json]
|
|
249
201
|
end
|
|
250
202
|
|
|
251
203
|
def handle_github_workflow_run(payload)
|
|
@@ -255,93 +207,45 @@ def handle_github_workflow_run(payload)
|
|
|
255
207
|
repo_full_name = payload.dig("repository", "full_name")
|
|
256
208
|
run_url = workflow["html_url"]
|
|
257
209
|
|
|
258
|
-
# Handle Deploy to Production failure
|
|
259
210
|
if workflow_name == "Deploy to Production" && conclusion == "failure"
|
|
260
|
-
|
|
261
|
-
project_key = project_result ? project_result[0] : repo_full_name
|
|
211
|
+
project_key = identify_project_by_repo(repo_full_name)&.first || repo_full_name
|
|
262
212
|
send_workflow_failure_notification(project_key, workflow_name, run_url)
|
|
263
|
-
LOG.info "Deploy to Production failed for #{project_key} — notified Discord"
|
|
264
213
|
return [200, { status: "processed", action: "prod_deploy_failure_notified", project: project_key }.to_json]
|
|
265
214
|
end
|
|
266
215
|
|
|
267
|
-
# Handle Deploy to UAT success
|
|
268
216
|
if workflow_name == "Deploy to UAT" && conclusion == "success"
|
|
269
|
-
|
|
270
|
-
project_key = project_result ? project_result[0] : repo_full_name
|
|
217
|
+
project_key = identify_project_by_repo(repo_full_name)&.first || repo_full_name
|
|
271
218
|
send_uat_deploy_notification(project_key)
|
|
272
|
-
LOG.info "Deploy to UAT succeeded for #{project_key} — notified Discord"
|
|
273
219
|
return [200, { status: "processed", action: "uat_deploy_notified", project: project_key }.to_json]
|
|
274
220
|
end
|
|
275
221
|
|
|
276
|
-
unless conclusion == "success"
|
|
277
|
-
LOG.info "Workflow '#{workflow_name}' concluded with '#{conclusion}' — ignoring"
|
|
278
|
-
return [200, { status: "ignored", reason: "conclusion: #{conclusion}" }.to_json]
|
|
279
|
-
end
|
|
222
|
+
return [200, { status: "ignored", reason: "conclusion: #{conclusion}" }.to_json] unless conclusion == "success"
|
|
280
223
|
|
|
281
|
-
unless workflow_name == "Deploy to Production"
|
|
282
|
-
LOG.info "Workflow '#{workflow_name}' is not a prod deploy — ignoring"
|
|
283
|
-
return [200, { status: "ignored", reason: "workflow: #{workflow_name}" }.to_json]
|
|
284
|
-
end
|
|
224
|
+
return [200, { status: "ignored", reason: "workflow: #{workflow_name}" }.to_json] unless workflow_name == "Deploy to Production"
|
|
285
225
|
|
|
286
226
|
project_result = identify_project_by_repo(repo_full_name)
|
|
287
|
-
unless project_result
|
|
288
|
-
LOG.info "No project found for GitHub repo #{repo_full_name}"
|
|
289
|
-
return [200, { status: "ignored", reason: "no matching project" }.to_json]
|
|
290
|
-
end
|
|
227
|
+
return [200, { status: "ignored", reason: "no matching project" }.to_json] unless project_result
|
|
291
228
|
|
|
292
229
|
project_key, project_config = project_result
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
card_list = cards["data"] || []
|
|
299
|
-
|
|
300
|
-
if card_list.empty?
|
|
301
|
-
LOG.info "No cards in UAT column — nothing to close"
|
|
302
|
-
return [200, { status: "processed", action: "no_uat_cards", project: project_key }.to_json]
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
closed_cards = []
|
|
306
|
-
map = load_card_map
|
|
307
|
-
card_list.each do |card|
|
|
308
|
-
card_number = card["number"]
|
|
309
|
-
next unless card_number
|
|
310
|
-
|
|
311
|
-
# Try to find the card in our map to get the assigned agent
|
|
312
|
-
map_entry = map.values.find { |info| info["number"] == card_number }
|
|
313
|
-
agent_name = map_entry["agent"] if map_entry
|
|
314
|
-
|
|
315
|
-
env = agent_name ? fizzy_env_for(agent_name) : default_fizzy_env
|
|
316
|
-
|
|
317
|
-
comment_body = "<p>✅ Deployed to production. Closing card.</p>"
|
|
318
|
-
run_cmd("fizzy", "comment", "create", "--card", card_number.to_s, "--body", comment_body, chdir: repo_path, env: env)
|
|
319
|
-
run_cmd("fizzy", "card", "close", card_number.to_s, chdir: repo_path, env: env)
|
|
320
|
-
|
|
321
|
-
# Clean up worktrees (primary + cross-agent review)
|
|
322
|
-
primary_worktree = map_entry&.dig("worktree")
|
|
323
|
-
primary_branch = map_entry&.dig("branch")
|
|
324
|
-
cleanup_card_worktrees(card_number, repo_path: repo_path, primary_worktree: primary_worktree, primary_branch: primary_branch)
|
|
230
|
+
close_uat_cards_after_deploy(project_key, project_config)
|
|
231
|
+
rescue StandardError => e
|
|
232
|
+
LOG.error "Error handling workflow run: #{e.message}"
|
|
233
|
+
[500, { error: e.message }.to_json]
|
|
234
|
+
end
|
|
325
235
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
end
|
|
236
|
+
def close_uat_cards_after_deploy(project_key, project_config)
|
|
237
|
+
# Emit hook — work item plugins handle closing cards after production deploy
|
|
238
|
+
results = Brainiac.emit(:production_deployed, project_key: project_key, project_config: project_config)
|
|
239
|
+
closed_cards = results.flatten.compact
|
|
331
240
|
|
|
332
|
-
|
|
333
|
-
|
|
241
|
+
if closed_cards.any?
|
|
242
|
+
send_deploy_notification(project_key, closed_cards)
|
|
243
|
+
LOG.info "Prod deploy complete — closed #{closed_cards.size} cards"
|
|
244
|
+
else
|
|
245
|
+
LOG.info "Prod deploy processed — no cards closed (plugin may not be installed)"
|
|
334
246
|
end
|
|
335
247
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
send_deploy_notification(project_key, closed_cards) if closed_cards.any?
|
|
339
|
-
|
|
340
|
-
LOG.info "Prod deploy complete — closed #{closed_cards.size} UAT cards: #{closed_cards.map { |c| c[:number] }.join(", ")}"
|
|
341
|
-
[200, { status: "processed", action: "prod_deploy_closed_uat", closed_cards: closed_cards.map { |c| c[:number] }, project: project_key }.to_json]
|
|
342
|
-
rescue StandardError => e
|
|
343
|
-
LOG.error "Error handling workflow run: #{e.message}"
|
|
344
|
-
[500, { error: e.message }.to_json]
|
|
248
|
+
[200, { status: "processed", action: "prod_deploy", closed_cards: closed_cards.map { |c| c[:number] }, project: project_key }.to_json]
|
|
345
249
|
end
|
|
346
250
|
|
|
347
251
|
def send_deploy_notification(project_key, closed_cards)
|
|
@@ -402,196 +306,109 @@ def handle_github_pr_review_submitted(payload)
|
|
|
402
306
|
review = payload["review"]
|
|
403
307
|
branch = pr.dig("head", "ref")
|
|
404
308
|
pr_number = pr["number"]
|
|
405
|
-
pr["html_url"]
|
|
406
309
|
repo_name = payload.dig("repository", "full_name")
|
|
407
310
|
review_state = review["state"]
|
|
408
311
|
reviewer = review.dig("user", "login")
|
|
409
312
|
|
|
410
|
-
|
|
411
|
-
unless %w[changes_requested commented].include?(review_state)
|
|
412
|
-
LOG.info "PR review state is '#{review_state}', ignoring"
|
|
413
|
-
return [200, { status: "ignored", reason: "review state: #{review_state}" }.to_json]
|
|
414
|
-
end
|
|
313
|
+
return [200, { status: "ignored", reason: "review state: #{review_state}" }.to_json] unless %w[changes_requested commented].include?(review_state)
|
|
415
314
|
|
|
416
|
-
# Identify project by GitHub repo
|
|
417
315
|
project_result = identify_project_by_repo(repo_name)
|
|
418
|
-
unless project_result
|
|
419
|
-
LOG.info "No project found for GitHub repo #{repo_name}"
|
|
420
|
-
return [200, { status: "ignored", reason: "no matching project" }.to_json]
|
|
421
|
-
end
|
|
316
|
+
return [200, { status: "ignored", reason: "no matching project" }.to_json] unless project_result
|
|
422
317
|
|
|
423
318
|
project_key, project_config = project_result
|
|
424
319
|
repo_path = project_config["repo_path"]
|
|
425
320
|
|
|
426
|
-
result =
|
|
427
|
-
unless result
|
|
428
|
-
LOG.info "No Fizzy card found for PR branch #{branch}"
|
|
429
|
-
return [200, { status: "ignored", reason: "no matching card" }.to_json]
|
|
430
|
-
end
|
|
321
|
+
result = find_work_item_by_branch(branch)
|
|
322
|
+
return [200, { status: "ignored", reason: "no matching card" }.to_json] unless result
|
|
431
323
|
|
|
432
324
|
internal_id, card_info = result
|
|
433
325
|
card_number = card_info["number"]
|
|
434
|
-
worktree = card_info["worktree"]
|
|
435
|
-
|
|
436
326
|
unless card_number
|
|
437
327
|
LOG.warn "Card #{internal_id} has no number — can't comment"
|
|
438
328
|
return [200, { status: "ignored", reason: "card has no number" }.to_json]
|
|
439
329
|
end
|
|
440
330
|
|
|
331
|
+
card_key = "card-#{card_number}"
|
|
332
|
+
return [200, { status: "ignored", reason: "session already active" }.to_json] if session_active?(card_key)
|
|
333
|
+
|
|
441
334
|
LOG.info "PR review submitted by #{reviewer} on PR ##{pr_number} for card ##{card_number} (project: #{project_key})"
|
|
335
|
+
dispatch_pr_review(card_number, card_key, card_info, pr_number, review, reviewer, repo_name, project_key, project_config, repo_path)
|
|
442
336
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
337
|
+
[200, { status: "processed", card: card_number, pr: pr_number, reviewer: reviewer, project: project_key }.to_json]
|
|
338
|
+
rescue StandardError => e
|
|
339
|
+
LOG.error "Error handling PR review: #{e.message}"
|
|
340
|
+
[500, { error: e.message }.to_json]
|
|
341
|
+
end
|
|
448
342
|
|
|
449
|
-
|
|
343
|
+
def dispatch_pr_review(card_number, card_key, card_info, pr_number, review, reviewer, repo_name, project_key, project_config, repo_path)
|
|
450
344
|
review_id = review["id"]
|
|
451
|
-
# React in background — don't block the dispatch path
|
|
452
345
|
Thread.new do
|
|
453
|
-
run_cmd("gh", "api", "-X", "POST", "/repos/#{repo_name}/pulls/reviews/#{review_id}/reactions",
|
|
454
|
-
"Accept: application/vnd.github+json", chdir: repo_path)
|
|
455
|
-
LOG.info "Added 👀 reaction to review ##{review_id}"
|
|
346
|
+
run_cmd("gh", "api", "-X", "POST", "/repos/#{repo_name}/pulls/reviews/#{review_id}/reactions",
|
|
347
|
+
"-f", "content=eyes", "-H", "Accept: application/vnd.github+json", chdir: repo_path)
|
|
456
348
|
rescue StandardError => e
|
|
457
349
|
LOG.warn "Could not add reaction to review: #{e.message}"
|
|
458
350
|
end
|
|
459
351
|
|
|
460
|
-
# Post status to Fizzy in background — don't block the dispatch path
|
|
461
352
|
agent_name = agent_name_for(project_config)
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
LOG.info "Posted status update to card ##{card_number} as #{agent_name}"
|
|
466
|
-
rescue StandardError => e
|
|
467
|
-
LOG.warn "Could not post status update to card ##{card_number}: #{e.message}"
|
|
468
|
-
end
|
|
353
|
+
# Notify work item system that a review was received (plugin posts status comment)
|
|
354
|
+
Brainiac.emit(:pr_review_received, card_number: card_number, reviewer: reviewer,
|
|
355
|
+
agent_name: agent_name, project_config: project_config, repo_path: repo_path)
|
|
469
356
|
|
|
470
|
-
|
|
471
|
-
|
|
357
|
+
review_context = build_review_context(reviewer, review, pr_number, repo_name)
|
|
358
|
+
worktree = card_info["worktree"]
|
|
359
|
+
work_dir = worktree && File.directory?(worktree) ? worktree : repo_path
|
|
360
|
+
|
|
361
|
+
prompt = render_prompt(PROMPT_GITHUB_PR_REVIEW,
|
|
362
|
+
{ "CARD_NUMBER" => card_number, "CARD_ID" => card_number,
|
|
363
|
+
"COMMENT_CREATOR" => reviewer, "REVIEW_CONTEXT" => review_context,
|
|
364
|
+
"PR_NUMBER" => pr_number.to_s, "WORKTREE_PATH" => work_dir },
|
|
365
|
+
brain_context: build_brain_context(agent_name: agent_name, card_number: card_number,
|
|
366
|
+
project_key: project_key),
|
|
367
|
+
agent_name: agent_name, channel: :github)
|
|
368
|
+
|
|
369
|
+
pid, log_file = run_agent(prompt, project_config: project_config, chdir: work_dir,
|
|
370
|
+
log_name: "review-#{card_number}", agent_name: agent_name,
|
|
371
|
+
source: :github,
|
|
372
|
+
source_context: { pr_number: pr_number, repo_name: repo_name, work_dir: work_dir })
|
|
373
|
+
register_session(card_key, pid, log_file: log_file, agent_name: agent_name)
|
|
374
|
+
end
|
|
472
375
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
376
|
+
def build_review_context(reviewer, review, pr_number, repo_name)
|
|
377
|
+
context = "GitHub PR Review from @#{reviewer}:\n\n"
|
|
378
|
+
context += "Review body:\n#{review["body"]}\n\n" if review["body"] && !review["body"].empty?
|
|
476
379
|
|
|
380
|
+
review_comments = fetch_pr_review_comments(pr_number, repo_name)
|
|
477
381
|
if review_comments.any?
|
|
478
|
-
|
|
382
|
+
context += "Line-specific comments:\n"
|
|
479
383
|
review_comments.each do |comment|
|
|
480
|
-
|
|
384
|
+
context += "- #{comment["path"]}:#{comment["line"]} (@#{comment["user"]}): #{comment["body"]}\n"
|
|
481
385
|
end
|
|
482
386
|
end
|
|
483
|
-
|
|
484
|
-
# Determine working directory
|
|
485
|
-
work_dir = worktree && File.directory?(worktree) ? worktree : repo_path
|
|
486
|
-
|
|
487
|
-
prompt = render_prompt(PROMPT_GITHUB_PR_REVIEW,
|
|
488
|
-
{ "CARD_NUMBER" => card_number,
|
|
489
|
-
"CARD_ID" => card_number,
|
|
490
|
-
"COMMENT_CREATOR" => reviewer,
|
|
491
|
-
"REVIEW_CONTEXT" => review_context,
|
|
492
|
-
"PR_NUMBER" => pr_number.to_s,
|
|
493
|
-
"WORKTREE_PATH" => work_dir },
|
|
494
|
-
brain_context: build_brain_context(agent_name: agent_name, card_number: card_number, project_key: project_key),
|
|
495
|
-
agent_name: agent_name,
|
|
496
|
-
channel: :github)
|
|
497
|
-
|
|
498
|
-
pid, log_file = run_agent(prompt, project_config: project_config, chdir: work_dir, log_name: "review-#{card_number}", agent_name: agent_name,
|
|
499
|
-
source: :github, source_context: { pr_number: pr_number, repo_name: repo_name, work_dir: work_dir })
|
|
500
|
-
register_session(card_key, pid, log_file: log_file, agent_name: agent_name)
|
|
501
|
-
|
|
502
|
-
[200, { status: "processed", card: card_number, pr: pr_number, reviewer: reviewer, project: project_key }.to_json]
|
|
503
|
-
rescue StandardError => e
|
|
504
|
-
LOG.error "Error handling PR review: #{e.message}"
|
|
505
|
-
[500, { error: e.message }.to_json]
|
|
387
|
+
context
|
|
506
388
|
end
|
|
507
389
|
|
|
508
|
-
# Auto-deploy when a PR gets new commits (synchronize event) if the card is already on a dev env.
|
|
509
390
|
def handle_github_pr_synchronized(payload)
|
|
510
391
|
pr = payload["pull_request"]
|
|
511
392
|
branch = pr.dig("head", "ref")
|
|
512
|
-
payload.dig("repository", "full_name")
|
|
513
393
|
|
|
514
|
-
result =
|
|
515
|
-
unless result
|
|
516
|
-
LOG.info "[PR Sync] No card found for branch #{branch}"
|
|
517
|
-
return [200, { status: "ignored", reason: "no matching card" }.to_json]
|
|
518
|
-
end
|
|
394
|
+
result = find_work_item_by_branch(branch)
|
|
395
|
+
return [200, { status: "ignored", reason: "no matching card" }.to_json] unless result
|
|
519
396
|
|
|
520
397
|
_internal_id, card_info = result
|
|
521
398
|
card_number = card_info["number"]
|
|
522
399
|
worktree = card_info["worktree"]
|
|
523
400
|
|
|
524
|
-
unless worktree && File.directory?(worktree)
|
|
525
|
-
LOG.info "[PR Sync] No worktree for card ##{card_number}"
|
|
526
|
-
return [200, { status: "ignored", reason: "no worktree" }.to_json]
|
|
527
|
-
end
|
|
401
|
+
return [200, { status: "ignored", reason: "no worktree" }.to_json] unless worktree && File.directory?(worktree)
|
|
528
402
|
|
|
529
|
-
#
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
env_key = state.find { |_k, v| v["card_number"] == card_number && v["status"] == "occupied" }&.first
|
|
403
|
+
# Emit hook — deployment plugin handles sync-deploy logic
|
|
404
|
+
results = Brainiac.emit(:pr_synchronized, card_number: card_number, card_info: card_info,
|
|
405
|
+
worktree: worktree, pull_request: pr, branch: branch)
|
|
533
406
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
# Only deploy if this machine owns the environment
|
|
540
|
-
env_owner = config.dig(env_key, "owner")
|
|
541
|
-
unless env_owner && env_owner.downcase == AI_AGENT_NAME.downcase
|
|
542
|
-
LOG.info "[PR Sync] Skipping #{env_key} — owner is #{env_owner.inspect}, this machine is #{AI_AGENT_NAME}"
|
|
543
|
-
return [200, { status: "ignored", reason: "not env owner" }.to_json]
|
|
544
|
-
end
|
|
545
|
-
|
|
546
|
-
# Cooldown — debounce rapid pushes
|
|
547
|
-
if on_deploy_cooldown?(env_key)
|
|
548
|
-
LOG.info "[PR Sync] Skipping deploy to #{env_key} — within #{DEPLOY_COOLDOWN}s cooldown"
|
|
549
|
-
return [200, { status: "ignored", reason: "deploy cooldown" }.to_json]
|
|
550
|
-
end
|
|
551
|
-
|
|
552
|
-
touch_deploy_cooldown(env_key)
|
|
553
|
-
|
|
554
|
-
# Pull latest commits into the worktree
|
|
555
|
-
system("git", "pull", "--ff-only", chdir: worktree)
|
|
556
|
-
|
|
557
|
-
deploy_script = File.join(worktree, "scripts", "deploy.sh")
|
|
558
|
-
unless File.exist?(deploy_script)
|
|
559
|
-
LOG.warn "[PR Sync] No deploy script at #{deploy_script}"
|
|
560
|
-
return [200, { status: "ignored", reason: "no deploy script" }.to_json]
|
|
561
|
-
end
|
|
562
|
-
|
|
563
|
-
LOG.info "[PR Sync] Auto-deploying card ##{card_number} to #{env_key} (PR updated)"
|
|
564
|
-
mark_deploying(env_key, worktree_path: worktree)
|
|
565
|
-
|
|
566
|
-
Thread.new do
|
|
567
|
-
deploy_env = {}
|
|
568
|
-
aws_profile = config.dig(env_key, "aws_profile")
|
|
569
|
-
deploy_env["AWS_PROFILE"] = aws_profile if aws_profile
|
|
570
|
-
|
|
571
|
-
stdout, stderr, status = Open3.capture3(deploy_env, deploy_script, env_key, chdir: worktree)
|
|
572
|
-
|
|
573
|
-
if status.success?
|
|
574
|
-
deploy_to_environment(env_key, worktree_path: worktree, deployed_by: "pr-sync")
|
|
575
|
-
LOG.info "[PR Sync] Deploy to #{env_key} succeeded for card ##{card_number}"
|
|
576
|
-
elsif terraform_lock_error?(stdout, stderr)
|
|
577
|
-
lock_file = File.join(worktree, "infrastructure/#{env_key}/.terraform.lock.hcl")
|
|
578
|
-
FileUtils.rm_f(lock_file)
|
|
579
|
-
Open3.capture3("terraform", "init", "-upgrade", chdir: File.join(worktree, "infrastructure/#{env_key}"))
|
|
580
|
-
stdout2, stderr2, status2 = Open3.capture3(deploy_env, deploy_script, env_key, chdir: worktree)
|
|
581
|
-
if status2.success?
|
|
582
|
-
deploy_to_environment(env_key, worktree_path: worktree, deployed_by: "pr-sync")
|
|
583
|
-
LOG.info "[PR Sync] Deploy to #{env_key} succeeded (after terraform lock fix) for card ##{card_number}"
|
|
584
|
-
else
|
|
585
|
-
record_deploy_failure(env_key, worktree_path: worktree, stdout: stdout2, stderr: stderr2)
|
|
586
|
-
LOG.error "[PR Sync] Deploy to #{env_key} failed (after retry) for card ##{card_number}"
|
|
587
|
-
end
|
|
588
|
-
else
|
|
589
|
-
record_deploy_failure(env_key, worktree_path: worktree, stdout: stdout, stderr: stderr)
|
|
590
|
-
LOG.error "[PR Sync] Deploy to #{env_key} failed for card ##{card_number}"
|
|
591
|
-
end
|
|
407
|
+
if results.any?
|
|
408
|
+
[200, { status: "processed", action: "pr_sync", card: card_number }.to_json]
|
|
409
|
+
else
|
|
410
|
+
[200, { status: "ignored", reason: "no deployment plugin" }.to_json]
|
|
592
411
|
end
|
|
593
|
-
|
|
594
|
-
[200, { status: "processed", action: "pr_sync_deploy", card: card_number, env: env_key }.to_json]
|
|
595
412
|
rescue StandardError => e
|
|
596
413
|
LOG.error "[PR Sync] Error: #{e.message}"
|
|
597
414
|
[500, { error: e.message }.to_json]
|