brainiac-github 0.2.3 → 0.2.5

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: 43962acb8ab8344a4e420e841550114d53082045252252ad340944dad73d267f
4
- data.tar.gz: eb8769a6f13771b1d057133fe8c72c3f3482602528ad0494475eb14022ea0d33
3
+ metadata.gz: 4646b834b308b619a34c5b94da48aeb88b80caa3fb5efd107ac40a5dc56da2a0
4
+ data.tar.gz: af0c3f9c0b47e366928760c64168a74eb3651f87c193156cbced75336626bac0
5
5
  SHA512:
6
- metadata.gz: bf3b0e3b90397dfd8232b2bd1fefab1f228dbb2b206d4f9b2766cbd064b6b2c9f4007bd651a7f3ab4ae91bd8934d674e814c931ef4549cf38eaa70b58d74b182
7
- data.tar.gz: b22dae8eacf0a45f9032eaadf63a33e945a17d4647ced3195254eb48d17962c2cf459a52214948d29407066c5217a09f21c093876cdd4dbde5e177e1296cabd2
6
+ metadata.gz: 945f7fad37cd33a37d755b6517faa8638c4a351d74f11edb60bf8299d04933837f38b34e1fa39afaea34af2b2d57c89f5b88d641fb6e6001436622928143d445
7
+ data.tar.gz: 5c2e27ab69d9af292ddbd3c7ac299629f1b97c005b85275bdaaa31d24b010903a75b4973d6d91cf99ab5ea580c53c67c77e341724f46c633a2628d2a78ad6c67
@@ -16,7 +16,8 @@ module Brainiac
16
16
  default_branch = payload.dig("repository", "default_branch") || "main"
17
17
 
18
18
  # Extract card number from branch name (e.g., "fizzy-1182-add-goodbye-method" → 1182)
19
- card_number_from_branch = branch&.match(/^fizzy-(\d+)-/)&.[](1)&.to_i
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
20
21
 
21
22
  # Emit a hook for ALL merges (including epic branches) so consumers can decide
22
23
  if card_number_from_branch
@@ -125,6 +126,24 @@ module Brainiac
125
126
  repo_name = payload.dig("repository", "full_name")
126
127
  review_state = review["state"]
127
128
  reviewer = review.dig("user", "login")
129
+ reviewer_type = review.dig("user", "type")
130
+
131
+ # If the review was submitted by a remote agent bot, ignore — the other machine handles it.
132
+ if reviewer_type == "Bot"
133
+ reviewing_agent = agent_name_from_bot_login(reviewer)
134
+ if reviewing_agent && !local_agent_names.include?(reviewing_agent)
135
+ LOG.info "[GitHub] Ignoring review from remote agent bot #{reviewer} (agent: #{reviewing_agent}) on PR ##{pr_number}"
136
+ return [200, { status: "ignored", reason: "remote agent bot review" }.to_json]
137
+ end
138
+ end
139
+
140
+ # If the PR was opened by a remote agent bot (not local to this machine),
141
+ # skip dispatch — the other machine that owns the agent will handle it.
142
+ unless pr_owned_locally?(pr)
143
+ pr_author = pr.dig("user", "login")
144
+ LOG.info "[GitHub] PR ##{pr_number} owned by remote agent (#{pr_author}) — skipping dispatch"
145
+ return [200, { status: "ignored", reason: "PR owned by remote agent" }.to_json]
146
+ end
128
147
 
129
148
  project_result = identify_project_by_repo(repo_name)
130
149
  return [200, { status: "ignored", reason: "no matching project" }.to_json] unless project_result
@@ -192,6 +211,16 @@ module Brainiac
192
211
  return [200, { status: "ignored", reason: "not a PR comment" }.to_json]
193
212
  end
194
213
 
214
+ # If the PR was opened by a remote agent bot (not local to this machine),
215
+ # skip dispatch — the other machine that owns the agent will handle it.
216
+ pr_author = issue.dig("user", "login")
217
+ pr_author_type = issue.dig("user", "type")
218
+ unless pr_author_owned_locally?(pr_author, pr_author_type)
219
+ pr_number = issue["number"]
220
+ LOG.info "[GitHub] PR ##{pr_number} owned by remote agent (#{pr_author}) — skipping dispatch"
221
+ return [200, { status: "ignored", reason: "PR owned by remote agent" }.to_json]
222
+ end
223
+
195
224
  project_result = identify_project_by_repo(repo_name)
196
225
  unless project_result
197
226
  LOG.info "No project found for GitHub repo #{repo_name}"
@@ -217,12 +246,12 @@ module Brainiac
217
246
  agent_name = mentioned
218
247
  end
219
248
 
220
- # Ignore comments from this project's own bot to prevent self-triggering.
221
- # Cross-agent bot comments (e.g. GLaDOS commenting on Galen's PR) are still processed.
222
- if comment_user_type == "Bot" && own_bot_comment?(comment_user, agent_name)
223
- LOG.info "Ignoring self-triggered bot comment from #{comment_user} (agent: #{agent_name})"
224
- return [200, { status: "ignored", reason: "self-triggered bot comment" }.to_json]
225
- end
249
+ # Ignore comments from agent bots that shouldn't trigger dispatch on this machine:
250
+ # 1. Self-triggered: bot matches the dispatching agent (prevents loops)
251
+ # 2. Remote agent: bot matches a known agent that isn't local (other machine handles it)
252
+ # Cross-agent LOCAL bot comments (e.g. GLaDOS commenting on Galen's PR) are still processed.
253
+ bot_ignore_reason = bot_comment_ignore_reason(comment_user, comment_user_type, agent_name, pr_number)
254
+ return bot_ignore_reason if bot_ignore_reason
226
255
 
227
256
  branch = fetch_pr_branch(repo_name, pr_number, agent_name, project_config)
228
257
  result = find_work_item_by_branch(branch)
@@ -317,6 +346,52 @@ module Brainiac
317
346
  nil
318
347
  end
319
348
 
349
+ # Determine if this machine should handle a PR based on who opened it.
350
+ # If the PR was opened by a bot that maps to a known agent, only the
351
+ # machine where that agent is local should dispatch. Human-opened PRs
352
+ # are always considered local (handled by normal routing logic).
353
+ #
354
+ # This prevents both machines from dispatching when they share the same
355
+ # GitHub webhook — e.g. Adam's Kaylee opens a PR, only Adam's machine handles it.
356
+ def pr_owned_locally?(pull_request)
357
+ pr_author = pull_request.dig("user", "login")
358
+ pr_author_type = pull_request.dig("user", "type")
359
+ pr_author_owned_locally?(pr_author, pr_author_type)
360
+ end
361
+
362
+ # Check PR ownership given the author login and account type.
363
+ # Used by both pull_request_review (has full PR object) and issue_comment
364
+ # (has issue.user which is the PR author on PR-linked issues).
365
+ def pr_author_owned_locally?(pr_author, pr_author_type)
366
+ # Human-opened PRs are always handled (normal routing applies)
367
+ return true unless pr_author_type == "Bot"
368
+
369
+ # Extract agent name from bot login (e.g. "kaylee-brainiac[bot]" → "Kaylee")
370
+ agent_from_bot = agent_name_from_bot_login(pr_author)
371
+ return true unless agent_from_bot
372
+
373
+ # If the bot's agent is local to this machine, we own it
374
+ local_agent_names.include?(agent_from_bot)
375
+ end
376
+
377
+ # Extract a Brainiac agent display name from a GitHub bot login.
378
+ # Bot logins follow the pattern "<name>-brainiac[bot]" or "<name>-brainiac".
379
+ # Returns the matched agent display name, or nil if not a known agent bot.
380
+ def agent_name_from_bot_login(bot_login)
381
+ return nil unless bot_login
382
+
383
+ # Normalize: remove [bot] suffix, lowercase
384
+ normalized = bot_login.to_s.downcase.delete_suffix("[bot]").strip
385
+
386
+ # Match against known agents — bot logins are "<agent>-brainiac"
387
+ all_agent_names.each do |name|
388
+ return name if normalized == "#{name.downcase}-brainiac"
389
+ return name if normalized == name.downcase
390
+ end
391
+
392
+ nil
393
+ end
394
+
320
395
  # Generate a GH_TOKEN env var for the agent process so `gh` CLI
321
396
  # authenticates as the app bot instead of the user's personal account.
322
397
  # Falls back to the shared "brainiac" app if the agent's own app isn't configured.
@@ -429,6 +504,30 @@ module Brainiac
429
504
  nil
430
505
  end
431
506
 
507
+ # Determine if a bot comment should be ignored by this machine.
508
+ # Returns a Rack response array if the comment should be ignored, or nil if it should be processed.
509
+ #
510
+ # Ignores:
511
+ # 1. Self-triggered: bot matches the dispatching agent (prevents loops)
512
+ # 2. Remote agent: bot matches a known agent that isn't local (other machine handles it)
513
+ # Allows: cross-agent LOCAL bot comments (e.g. GLaDOS commenting on Galen's PR)
514
+ def bot_comment_ignore_reason(comment_user, comment_user_type, agent_name, pr_number)
515
+ return nil unless comment_user_type == "Bot"
516
+
517
+ if own_bot_comment?(comment_user, agent_name)
518
+ LOG.info "Ignoring self-triggered bot comment from #{comment_user} (agent: #{agent_name})"
519
+ return [200, { status: "ignored", reason: "self-triggered bot comment" }.to_json]
520
+ end
521
+
522
+ commenting_agent = agent_name_from_bot_login(comment_user)
523
+ if commenting_agent && !local_agent_names.include?(commenting_agent)
524
+ LOG.info "[GitHub] Ignoring comment from remote agent bot #{comment_user} (agent: #{commenting_agent}) on PR ##{pr_number}"
525
+ return [200, { status: "ignored", reason: "remote agent bot comment" }.to_json]
526
+ end
527
+
528
+ nil
529
+ end
530
+
432
531
  # Check if a bot comment is from the same agent that would handle this PR.
433
532
  # e.g. "galen-brainiac[bot]" is a self-trigger for agent "Galen",
434
533
  # but "glados-brainiac[bot]" is a cross-agent request and should be processed.
@@ -3,7 +3,7 @@
3
3
  module Brainiac
4
4
  module Plugins
5
5
  module Github
6
- VERSION = "0.2.3"
6
+ VERSION = "0.2.5"
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.3
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis