brainiac-github 0.3.2 → 0.3.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 +4 -4
- data/lib/brainiac/plugins/github/handler.rb +106 -27
- data/lib/brainiac/plugins/github/version.rb +1 -1
- data/lib/brainiac/plugins/github.rb +2 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 49d9d73cefe9f24ff840f683ea53a4ff2276cbd89495037765dcc65fb3370fae
|
|
4
|
+
data.tar.gz: 9de4f939b32f3f7c6f802d64e5a5280cc2de108fc771212b865bb17b8a12d21e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f3b54d02be22dbc9072aa3170a7073ef912b40f9555b5fdfde9a0cea3c4212fcf84c5e797026cdf094b1ef4e220804784a30361e64377e00470a785e6481d2e6
|
|
7
|
+
data.tar.gz: 6cc367893d43b6276e141902e21b17e48c416a84c76e989bc0d34620223ee47b52fd02ec01650684e3adde7410d39ac42e9de8eeece11a895c43aea2e7d1b414
|
|
@@ -4,6 +4,17 @@ module Brainiac
|
|
|
4
4
|
module Plugins
|
|
5
5
|
module Github
|
|
6
6
|
module Handler
|
|
7
|
+
# Marker that starts every crash-notification comment (see register_crash_handler!).
|
|
8
|
+
CRASH_COMMENT_PREFIX = "💥"
|
|
9
|
+
|
|
10
|
+
# Circuit breaker: max comment dispatches per (repo, PR, agent) within the window.
|
|
11
|
+
# Stops any comment -> dispatch -> crash -> comment feedback loop, regardless of
|
|
12
|
+
# who posted the comment or what it says.
|
|
13
|
+
COMMENT_DISPATCH_LIMIT = 4
|
|
14
|
+
COMMENT_DISPATCH_WINDOW = 120 # seconds
|
|
15
|
+
COMMENT_DISPATCH_LOCK = Mutex.new
|
|
16
|
+
COMMENT_DISPATCH_TIMES = Hash.new { |h, k| h[k] = [] }
|
|
17
|
+
|
|
7
18
|
class << self
|
|
8
19
|
def handle_pr_merged(payload)
|
|
9
20
|
pr = payload["pull_request"]
|
|
@@ -181,13 +192,14 @@ module Brainiac
|
|
|
181
192
|
result = find_work_item_by_branch(branch)
|
|
182
193
|
|
|
183
194
|
if result
|
|
184
|
-
|
|
195
|
+
work_item_id, card_info = result
|
|
185
196
|
card_number = extract_card_number(card_info)
|
|
186
197
|
unless card_number
|
|
187
198
|
LOG.warn "Card has no number — can't dispatch review"
|
|
188
199
|
return [200, { status: "ignored", reason: "card has no number" }.to_json]
|
|
189
200
|
end
|
|
190
201
|
else
|
|
202
|
+
work_item_id = nil
|
|
191
203
|
card_info = {}
|
|
192
204
|
card_number = nil
|
|
193
205
|
end
|
|
@@ -236,7 +248,7 @@ module Brainiac
|
|
|
236
248
|
card_context = card_number ? " for card ##{card_number}" : ""
|
|
237
249
|
LOG.info "PR review submitted by #{reviewer} on PR ##{pr_number}#{card_context} (project: #{project_key})"
|
|
238
250
|
dispatch_pr_review(card_number, card_key, card_info, pr_number, review, reviewer,
|
|
239
|
-
repo_name, project_key, project_config, repo_path)
|
|
251
|
+
repo_name, project_key, project_config, repo_path, work_item_id: work_item_id)
|
|
240
252
|
|
|
241
253
|
[200, { status: "processed", card: card_number, pr: pr_number, reviewer: reviewer, project: project_key }.to_json]
|
|
242
254
|
rescue StandardError => e
|
|
@@ -253,20 +265,8 @@ module Brainiac
|
|
|
253
265
|
comment_user_type = comment.dig("user", "type")
|
|
254
266
|
repo_name = payload.dig("repository", "full_name")
|
|
255
267
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
return [200, { status: "ignored", reason: "not a PR comment" }.to_json]
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
# If the PR was opened by a remote agent bot (not local to this machine),
|
|
262
|
-
# skip dispatch — the other machine that owns the agent will handle it.
|
|
263
|
-
pr_author = issue.dig("user", "login")
|
|
264
|
-
pr_author_type = issue.dig("user", "type")
|
|
265
|
-
unless pr_author_owned_locally?(pr_author, pr_author_type)
|
|
266
|
-
pr_number = issue["number"]
|
|
267
|
-
LOG.info "[GitHub] PR ##{pr_number} owned by remote agent (#{pr_author}) — skipping dispatch"
|
|
268
|
-
return [200, { status: "ignored", reason: "PR owned by remote agent" }.to_json]
|
|
269
|
-
end
|
|
268
|
+
rejection = issue_comment_rejection(issue, comment_body)
|
|
269
|
+
return rejection if rejection
|
|
270
270
|
|
|
271
271
|
project_result = identify_project_by_repo(repo_name)
|
|
272
272
|
unless project_result
|
|
@@ -300,6 +300,12 @@ module Brainiac
|
|
|
300
300
|
bot_ignore_reason = bot_comment_ignore_reason(comment_user, comment_user_type, agent_name, pr_number)
|
|
301
301
|
return bot_ignore_reason if bot_ignore_reason
|
|
302
302
|
|
|
303
|
+
if comment_dispatch_throttled?(repo_name, pr_number, agent_name)
|
|
304
|
+
LOG.warn "[GitHub] PR ##{pr_number}: >#{COMMENT_DISPATCH_LIMIT} dispatches for #{agent_name} in " \
|
|
305
|
+
"#{COMMENT_DISPATCH_WINDOW}s — possible loop, ignoring comment"
|
|
306
|
+
return [200, { status: "ignored", reason: "dispatch rate limit" }.to_json]
|
|
307
|
+
end
|
|
308
|
+
|
|
303
309
|
branch = fetch_pr_branch(repo_name, pr_number, agent_name, project_config)
|
|
304
310
|
result = find_work_item_by_branch(branch)
|
|
305
311
|
|
|
@@ -420,6 +426,42 @@ module Brainiac
|
|
|
420
426
|
pr_author_owned_locally?(pr_author, pr_author_type)
|
|
421
427
|
end
|
|
422
428
|
|
|
429
|
+
# Early-exit guards for issue_comment events. Returns a Rack-style response
|
|
430
|
+
# array when the comment should be ignored, or nil to continue dispatch.
|
|
431
|
+
def issue_comment_rejection(issue, comment_body)
|
|
432
|
+
unless issue["pull_request"]
|
|
433
|
+
LOG.info "Issue comment on non-PR issue ##{issue["number"]}, ignoring"
|
|
434
|
+
return [200, { status: "ignored", reason: "not a PR comment" }.to_json]
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# If the PR was opened by a remote agent bot (not local to this machine),
|
|
438
|
+
# skip dispatch — the other machine that owns the agent will handle it.
|
|
439
|
+
pr_author = issue.dig("user", "login")
|
|
440
|
+
pr_author_type = issue.dig("user", "type")
|
|
441
|
+
unless pr_author_owned_locally?(pr_author, pr_author_type)
|
|
442
|
+
LOG.info "[GitHub] PR ##{issue["number"]} owned by remote agent (#{pr_author}) — skipping dispatch"
|
|
443
|
+
return [200, { status: "ignored", reason: "PR owned by remote agent" }.to_json]
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Crash notifications are posted via `gh` as the human's account when the app
|
|
447
|
+
# client isn't configured, so the bot-login checks below can't catch them.
|
|
448
|
+
# Without this guard each crash comment re-dispatches the agent, which crashes
|
|
449
|
+
# again, forever.
|
|
450
|
+
if crash_notification_comment?(comment_body)
|
|
451
|
+
LOG.info "[GitHub] Ignoring crash notification comment on PR ##{issue["number"]}"
|
|
452
|
+
return [200, { status: "ignored", reason: "crash notification" }.to_json]
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
nil
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# True when a comment is a crash notification posted by the crash handler.
|
|
459
|
+
# These are posted via `gh` as the human account when no app client is
|
|
460
|
+
# configured, so the bot-login checks can't catch them — match on the prefix.
|
|
461
|
+
def crash_notification_comment?(comment_body)
|
|
462
|
+
comment_body.to_s.lstrip.start_with?(CRASH_COMMENT_PREFIX)
|
|
463
|
+
end
|
|
464
|
+
|
|
423
465
|
# Check PR ownership given the author login and account type.
|
|
424
466
|
# Used by both pull_request_review (has full PR object) and issue_comment
|
|
425
467
|
# (has issue.user which is the PR author on PR-linked issues).
|
|
@@ -630,6 +672,20 @@ module Brainiac
|
|
|
630
672
|
# 1. Self-triggered: bot matches the dispatching agent (prevents loops)
|
|
631
673
|
# 2. Remote agent: bot matches a known agent that isn't local (other machine handles it)
|
|
632
674
|
# Allows: cross-agent LOCAL bot comments (e.g. GLaDOS commenting on Galen's PR)
|
|
675
|
+
# Records a dispatch attempt; true when the (repo, PR, agent) is over the limit.
|
|
676
|
+
def comment_dispatch_throttled?(repo_name, pr_number, agent_name)
|
|
677
|
+
key = [repo_name, pr_number, agent_name.to_s.downcase]
|
|
678
|
+
now = Time.now.to_f
|
|
679
|
+
COMMENT_DISPATCH_LOCK.synchronize do
|
|
680
|
+
times = COMMENT_DISPATCH_TIMES[key]
|
|
681
|
+
times.reject! { |t| now - t > COMMENT_DISPATCH_WINDOW }
|
|
682
|
+
return true if times.size >= COMMENT_DISPATCH_LIMIT
|
|
683
|
+
|
|
684
|
+
times << now
|
|
685
|
+
false
|
|
686
|
+
end
|
|
687
|
+
end
|
|
688
|
+
|
|
633
689
|
def bot_comment_ignore_reason(comment_user, comment_user_type, agent_name, pr_number)
|
|
634
690
|
return nil unless comment_user_type == "Bot"
|
|
635
691
|
|
|
@@ -742,23 +798,15 @@ module Brainiac
|
|
|
742
798
|
|
|
743
799
|
intent_ctx = fetch_pr_intent_context(pr_number, repo_name, agent_name)
|
|
744
800
|
agent_env = github_agent_env(agent_name, repo_name)
|
|
745
|
-
# detect_model_explicit is newer core; fall back to detect_model on older builds.
|
|
746
|
-
if respond_to?(:detect_model_explicit)
|
|
747
|
-
comment_model, comment_model_explicit = detect_model_explicit(project_config, text: comment_body)
|
|
748
|
-
else
|
|
749
|
-
comment_model = detect_model(project_config, text: comment_body)
|
|
750
|
-
comment_model_explicit = nil
|
|
751
|
-
end
|
|
752
801
|
run_agent_opts = { project_config: project_config, chdir: worktree,
|
|
753
802
|
log_name: "pr-comment-#{pr_number}",
|
|
754
|
-
model: comment_model,
|
|
755
|
-
effort: detect_effort(project_config, text: comment_body),
|
|
756
803
|
agent_name: agent_name, source: :github,
|
|
757
804
|
source_context: { pr_number: pr_number, repo_name: repo_name, work_dir: worktree },
|
|
758
805
|
message: comment_body, channel: "GitHub PR comment",
|
|
759
806
|
context: intent_ctx }
|
|
807
|
+
work_item_id = find_work_item_by_worktree(worktree)&.first if respond_to?(:find_work_item_by_worktree, true)
|
|
808
|
+
run_agent_opts.merge!(dispatch_override_opts(project_config, comment_body, agent_name, work_item_id))
|
|
760
809
|
run_agent_opts[:env] = agent_env if method(:run_agent).parameters.flatten.include?(:env)
|
|
761
|
-
run_agent_opts[:explicit_model] = comment_model_explicit if method(:run_agent).parameters.flatten.include?(:explicit_model)
|
|
762
810
|
pid, log_file = run_agent(prompt, **run_agent_opts)
|
|
763
811
|
return unless pid
|
|
764
812
|
|
|
@@ -766,7 +814,7 @@ module Brainiac
|
|
|
766
814
|
end
|
|
767
815
|
|
|
768
816
|
def dispatch_pr_review(card_number, card_key, card_info, pr_number, review, reviewer,
|
|
769
|
-
repo_name, project_key, project_config, repo_path)
|
|
817
|
+
repo_name, project_key, project_config, repo_path, work_item_id: nil)
|
|
770
818
|
review_id = review["id"]
|
|
771
819
|
agent_name = resolve_work_item_agent(card_info, project_config)
|
|
772
820
|
|
|
@@ -809,6 +857,7 @@ module Brainiac
|
|
|
809
857
|
source_context: { pr_number: pr_number, repo_name: repo_name, work_dir: work_dir },
|
|
810
858
|
message: review["body"], channel: "GitHub PR review",
|
|
811
859
|
context: fetch_pr_intent_context(pr_number, repo_name, agent_name) }
|
|
860
|
+
run_agent_opts.merge!(dispatch_override_opts(project_config, review["body"].to_s, agent_name, work_item_id))
|
|
812
861
|
run_agent_opts[:env] = agent_env if method(:run_agent).parameters.flatten.include?(:env)
|
|
813
862
|
pid, log_file = run_agent(prompt, **run_agent_opts)
|
|
814
863
|
return unless pid
|
|
@@ -816,6 +865,36 @@ module Brainiac
|
|
|
816
865
|
register_session(card_key, pid, log_file: log_file, agent_name: agent_name)
|
|
817
866
|
end
|
|
818
867
|
|
|
868
|
+
# run_agent options for the CLI provider, model, effort and profile of a dispatch.
|
|
869
|
+
# Core's resolve_dispatch_overrides parses the [cli:X]/[model]/[effort:X]/[p:X] tags,
|
|
870
|
+
# falls back to the work item's stored overrides (so a card switched to claude in
|
|
871
|
+
# Fizzy stays on claude for PR comments), resolves the model through the provider
|
|
872
|
+
# that will actually run, and persists new tags. Older core builds lack it, so fall
|
|
873
|
+
# back to the per-tag detectors there.
|
|
874
|
+
def dispatch_override_opts(project_config, text, agent_name, work_item_id)
|
|
875
|
+
supported = method(:run_agent).parameters.flatten
|
|
876
|
+
opts = if respond_to?(:resolve_dispatch_overrides, true)
|
|
877
|
+
overrides = resolve_dispatch_overrides(project_config, text: text, agent_name: agent_name,
|
|
878
|
+
work_item_id: work_item_id)
|
|
879
|
+
{ cli_provider: overrides[:cli_provider], model: overrides[:model], effort: overrides[:effort],
|
|
880
|
+
profile: overrides[:profile], explicit_model: overrides[:explicit_model] }
|
|
881
|
+
else
|
|
882
|
+
legacy_override_opts(project_config, text)
|
|
883
|
+
end
|
|
884
|
+
opts.compact.slice(*supported)
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
def legacy_override_opts(project_config, text)
|
|
888
|
+
cli_provider = respond_to?(:detect_cli_provider, true) ? detect_cli_provider(text: text) : nil
|
|
889
|
+
if respond_to?(:detect_model_explicit, true)
|
|
890
|
+
model, explicit = detect_model_explicit(project_config, text: text, cli_provider_override: cli_provider)
|
|
891
|
+
else
|
|
892
|
+
model = detect_model(project_config, text: text)
|
|
893
|
+
end
|
|
894
|
+
{ cli_provider: cli_provider, model: model, explicit_model: explicit,
|
|
895
|
+
effort: detect_effort(project_config, text: text) }
|
|
896
|
+
end
|
|
897
|
+
|
|
819
898
|
def build_review_context(reviewer, review, pr_number, repo_name, agent_key = nil)
|
|
820
899
|
context = "GitHub PR Review from @#{reviewer}:\n\n"
|
|
821
900
|
context += "Review body:\n#{review["body"]}\n\n" if review["body"] && !review["body"].empty?
|
|
@@ -67,7 +67,8 @@ module Brainiac
|
|
|
67
67
|
agent_key = ctx[:agent_key]
|
|
68
68
|
snippet = ctx[:snippet]
|
|
69
69
|
snippet_block = snippet ? "\n```\n#{snippet[-1500..]}\n```" : ""
|
|
70
|
-
comment_body = "
|
|
70
|
+
comment_body = "#{Handler::CRASH_COMMENT_PREFIX} **#{agent_display} crashed** " \
|
|
71
|
+
"(exit code #{ctx[:exit_status]})\n\nLog: `#{ctx[:log_file]}`#{snippet_block}"
|
|
71
72
|
|
|
72
73
|
begin
|
|
73
74
|
if AppClient.configured?(agent_key)
|