brainiac-github 0.2.2 → 0.2.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5ae816a199e6894108759603b7d806d34000a3da5c44637456cbeb270ed4e8c
4
- data.tar.gz: 06c7c6bb1bf211992d40ba6cf7f50df203300319dea3d34bc4f3630990311edf
3
+ metadata.gz: 4cb6c5132e15d851261b150a636b895a0d8d8f6487b51a5049290a1ce2f12274
4
+ data.tar.gz: c6bf867e0bb5dbca4008e0971dc0568fb95755c2b296b7865e889e2d09645120
5
5
  SHA512:
6
- metadata.gz: 7491a2210feb364892a300ea122f07694d08a82a8f46563e45a36e425346480779ebe82bbb6c1269dbc3ea8bb2805a6fe505b6fa8ee4d957177318c63daece08
7
- data.tar.gz: b99ada00a52071cda049b98e4baa6f68358827dba5dced00bfb2cf1421e9ca2095d756d2364b1c176f54e68209b038ee9922502b6bd6ecb866bea126b200cd04
6
+ metadata.gz: ed2481547416f7bd7d0194aff3ec96da00811c6fe2a4ee8478b4f90c63820fde0318e7ea2427ba07646bd5755abcba3ac2a3c32af78170abb6eebb461f54a4dc
7
+ data.tar.gz: 3df84f31506b7fbeeaf7636f84b8d8602a9e6fee9d6d8a6a4b18b4f5b76a9de666df79e5c2dccb9c81d1f90727bc8ffd60cc8b784776ea8f55ef0385e3234791
@@ -14,6 +14,22 @@ module Brainiac
14
14
  repo_full_name = payload.dig("repository", "full_name")
15
15
 
16
16
  default_branch = payload.dig("repository", "default_branch") || "main"
17
+
18
+ # Extract card number from branch name (e.g., "fizzy-1182-add-goodbye-method" → 1182)
19
+ card_number_from_branch = branch&.match(/^fizzy-(\d+)-/)
20
+ card_number_from_branch = card_number_from_branch[1].to_i if card_number_from_branch
21
+
22
+ # Emit a hook for ALL merges (including epic branches) so consumers can decide
23
+ if card_number_from_branch
24
+ Brainiac.emit(:pr_merged_to_branch,
25
+ card_number: card_number_from_branch,
26
+ branch: branch,
27
+ base_branch: base,
28
+ pr_url: pr_url,
29
+ pr_title: pr_title,
30
+ repo_full_name: repo_full_name)
31
+ end
32
+
17
33
  unless base == default_branch
18
34
  LOG.info "PR merged into #{base}, not #{default_branch} — ignoring"
19
35
  return [200, { status: "ignored", reason: "not merged into #{default_branch}" }.to_json]
@@ -111,8 +127,12 @@ module Brainiac
111
127
  review_state = review["state"]
112
128
  reviewer = review.dig("user", "login")
113
129
 
114
- unless %w[changes_requested commented].include?(review_state)
115
- return [200, { status: "ignored", reason: "review state: #{review_state}" }.to_json]
130
+ # If the PR was opened by a remote agent bot (not local to this machine),
131
+ # skip dispatch the other machine that owns the agent will handle it.
132
+ unless pr_owned_locally?(pr)
133
+ pr_author = pr.dig("user", "login")
134
+ LOG.info "[GitHub] PR ##{pr_number} owned by remote agent (#{pr_author}) — skipping dispatch"
135
+ return [200, { status: "ignored", reason: "PR owned by remote agent" }.to_json]
116
136
  end
117
137
 
118
138
  project_result = identify_project_by_repo(repo_name)
@@ -134,6 +154,24 @@ module Brainiac
134
154
  card_info = {}
135
155
  card_number = nil
136
156
  end
157
+
158
+ # Always emit the hook so gate tracking works for approvals too
159
+ Brainiac.emit(:pr_review_received,
160
+ card_number: card_number,
161
+ reviewer: reviewer,
162
+ review_state: review_state,
163
+ pr_number: pr_number,
164
+ repo_name: repo_name,
165
+ project_config: project_config,
166
+ repo_path: repo_path)
167
+
168
+ # Only dispatch the implementation agent for changes_requested or commented reviews
169
+ # Approvals don't need the agent to do anything (the gate hook handles the flow)
170
+ unless %w[changes_requested commented].include?(review_state)
171
+ LOG.info "PR review (#{review_state}) by #{reviewer} on PR ##{pr_number} — hook emitted, no agent dispatch"
172
+ return [200, { status: "processed", card: card_number, pr: pr_number, reviewer: reviewer, action: "hook_only" }.to_json]
173
+ end
174
+
137
175
  card_key = "pr-review-#{repo_name.tr("/", "-")}-#{pr_number}"
138
176
 
139
177
  return [200, { status: "ignored", reason: "session already active" }.to_json] if session_active?(card_key)
@@ -163,6 +201,16 @@ module Brainiac
163
201
  return [200, { status: "ignored", reason: "not a PR comment" }.to_json]
164
202
  end
165
203
 
204
+ # If the PR was opened by a remote agent bot (not local to this machine),
205
+ # skip dispatch — the other machine that owns the agent will handle it.
206
+ pr_author = issue.dig("user", "login")
207
+ pr_author_type = issue.dig("user", "type")
208
+ unless pr_author_owned_locally?(pr_author, pr_author_type)
209
+ pr_number = issue["number"]
210
+ LOG.info "[GitHub] PR ##{pr_number} owned by remote agent (#{pr_author}) — skipping dispatch"
211
+ return [200, { status: "ignored", reason: "PR owned by remote agent" }.to_json]
212
+ end
213
+
166
214
  project_result = identify_project_by_repo(repo_name)
167
215
  unless project_result
168
216
  LOG.info "No project found for GitHub repo #{repo_name}"
@@ -288,6 +336,52 @@ module Brainiac
288
336
  nil
289
337
  end
290
338
 
339
+ # Determine if this machine should handle a PR based on who opened it.
340
+ # If the PR was opened by a bot that maps to a known agent, only the
341
+ # machine where that agent is local should dispatch. Human-opened PRs
342
+ # are always considered local (handled by normal routing logic).
343
+ #
344
+ # This prevents both machines from dispatching when they share the same
345
+ # GitHub webhook — e.g. Adam's Kaylee opens a PR, only Adam's machine handles it.
346
+ def pr_owned_locally?(pull_request)
347
+ pr_author = pull_request.dig("user", "login")
348
+ pr_author_type = pull_request.dig("user", "type")
349
+ pr_author_owned_locally?(pr_author, pr_author_type)
350
+ end
351
+
352
+ # Check PR ownership given the author login and account type.
353
+ # Used by both pull_request_review (has full PR object) and issue_comment
354
+ # (has issue.user which is the PR author on PR-linked issues).
355
+ def pr_author_owned_locally?(pr_author, pr_author_type)
356
+ # Human-opened PRs are always handled (normal routing applies)
357
+ return true unless pr_author_type == "Bot"
358
+
359
+ # Extract agent name from bot login (e.g. "kaylee-brainiac[bot]" → "Kaylee")
360
+ agent_from_bot = agent_name_from_bot_login(pr_author)
361
+ return true unless agent_from_bot
362
+
363
+ # If the bot's agent is local to this machine, we own it
364
+ local_agent_names.include?(agent_from_bot)
365
+ end
366
+
367
+ # Extract a Brainiac agent display name from a GitHub bot login.
368
+ # Bot logins follow the pattern "<name>-brainiac[bot]" or "<name>-brainiac".
369
+ # Returns the matched agent display name, or nil if not a known agent bot.
370
+ def agent_name_from_bot_login(bot_login)
371
+ return nil unless bot_login
372
+
373
+ # Normalize: remove [bot] suffix, lowercase
374
+ normalized = bot_login.to_s.downcase.delete_suffix("[bot]").strip
375
+
376
+ # Match against known agents — bot logins are "<agent>-brainiac"
377
+ all_agent_names.each do |name|
378
+ return name if normalized == "#{name.downcase}-brainiac"
379
+ return name if normalized == name.downcase
380
+ end
381
+
382
+ nil
383
+ end
384
+
291
385
  # Generate a GH_TOKEN env var for the agent process so `gh` CLI
292
386
  # authenticates as the app bot instead of the user's personal account.
293
387
  # Falls back to the shared "brainiac" app if the agent's own app isn't configured.
@@ -3,7 +3,7 @@
3
3
  module Brainiac
4
4
  module Plugins
5
5
  module Github
6
- VERSION = "0.2.2"
6
+ VERSION = "0.2.4"
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brainiac-github
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis