brainiac-basecamp 0.0.11 → 0.0.12
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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1eb44286a14e4c833e7ee753cf3ced44e6579eea0d37ff0c1289db149cc1c97c
|
|
4
|
+
data.tar.gz: b24e2ed8efe6a611f3d3790dfd38a906cd753c8c766743ab50302f365430d8db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 106c01e99ca88a0ee5d2801cb632b17fdcace3bd3ea9fe17278cae2cbfa3560f1669c164bf6a12472fc4451bdc81833889aaad2c273b936df77ead50f1736450
|
|
7
|
+
data.tar.gz: f766419152f38af5404a7640ac87c19540814480e67a1e12ee14621595075d0fc7fc204ed33475022a090c74fbfa97ab8b5a3929c9eedfe3458b2f46345a7fa0
|
|
@@ -123,6 +123,18 @@ module Brainiac
|
|
|
123
123
|
if epic_branches.include?(base_branch)
|
|
124
124
|
LOG.info "[Basecamp:Hooks] PR merged to epic branch #{base_branch} for card ##{card_number} — advancing" if defined?(LOG)
|
|
125
125
|
Orchestrator.on_card_completed(card_number)
|
|
126
|
+
elsif epic_branches.empty?
|
|
127
|
+
# Fallback: epic_branches was never populated (branch creation failed).
|
|
128
|
+
# If the task is in final_decision or in_review, the merge still represents
|
|
129
|
+
# completion — advance the epic regardless of which branch was targeted.
|
|
130
|
+
task = epic["tasks"].find { |t| t["fizzy_card"] == card_number.to_i }
|
|
131
|
+
if task && %w[final_decision in_review in_flight].include?(task["status"])
|
|
132
|
+
if defined?(LOG)
|
|
133
|
+
LOG.warn "[Basecamp:Hooks] epic_branches is empty but PR merged to '#{base_branch}' " \
|
|
134
|
+
"for card ##{card_number} (status: #{task['status']}) — advancing anyway (branch creation likely failed)"
|
|
135
|
+
end
|
|
136
|
+
Orchestrator.on_card_completed(card_number)
|
|
137
|
+
end
|
|
126
138
|
end
|
|
127
139
|
end
|
|
128
140
|
end
|
|
@@ -613,13 +625,32 @@ module Brainiac
|
|
|
613
625
|
github_repo = project_config&.dig("github_repo")
|
|
614
626
|
|
|
615
627
|
# Check if PR is already merged (handles manual merges, webhook misses, etc.)
|
|
616
|
-
if github_repo && pr_number
|
|
628
|
+
if github_repo && pr_number&.to_i&.positive?
|
|
617
629
|
pr_state, = Open3.capture2("gh", "pr", "view", pr_number.to_s, "--repo", github_repo, "--json", "state", "-q", ".state")
|
|
618
630
|
if pr_state.strip == "MERGED"
|
|
619
631
|
LOG.info "[Basecamp:Hooks] PR ##{pr_number} already merged — marking card ##{card_number} complete" if defined?(LOG)
|
|
620
632
|
Orchestrator.on_card_completed(card_number)
|
|
621
633
|
return
|
|
622
634
|
end
|
|
635
|
+
|
|
636
|
+
# SAFEGUARD: Verify PR targets the epic branch (not main/master).
|
|
637
|
+
# If epic_branches is populated and the PR targets the wrong base, retarget it.
|
|
638
|
+
epic_branches = epic["epic_branches"] || {}
|
|
639
|
+
expected_base = epic_branches[project_key] || epic_branches.values.first
|
|
640
|
+
if expected_base
|
|
641
|
+
actual_base, = Open3.capture2("gh", "pr", "view", pr_number.to_s, "--repo", github_repo, "--json", "baseRefName", "-q",
|
|
642
|
+
".baseRefName")
|
|
643
|
+
actual_base = actual_base.strip
|
|
644
|
+
if !actual_base.empty? && actual_base != expected_base
|
|
645
|
+
LOG.warn "[Basecamp:Hooks] PR ##{pr_number} targets '#{actual_base}' but expected '#{expected_base}' — retargeting" if defined?(LOG)
|
|
646
|
+
_, stderr, status = Open3.capture3("gh", "pr", "edit", pr_number.to_s, "--repo", github_repo, "--base", expected_base)
|
|
647
|
+
if status.success?
|
|
648
|
+
LOG.info "[Basecamp:Hooks] Retargeted PR ##{pr_number} to '#{expected_base}'" if defined?(LOG)
|
|
649
|
+
elsif defined?(LOG)
|
|
650
|
+
LOG.error "[Basecamp:Hooks] Failed to retarget PR ##{pr_number}: #{stderr.strip}"
|
|
651
|
+
end
|
|
652
|
+
end
|
|
653
|
+
end
|
|
623
654
|
end
|
|
624
655
|
|
|
625
656
|
# Mark task as awaiting final decision
|
|
@@ -659,6 +690,17 @@ module Brainiac
|
|
|
659
690
|
gate_approvals = task["gate_approvals"] || []
|
|
660
691
|
gate_agents = gate_approvals.map { |a| a["agent"] }.join(", ")
|
|
661
692
|
|
|
693
|
+
# Determine expected target branch for the prompt
|
|
694
|
+
epic_branches = epic["epic_branches"] || {}
|
|
695
|
+
expected_base = epic_branches[project_key] || epic_branches.values.first
|
|
696
|
+
target_note = if expected_base
|
|
697
|
+
"\n**IMPORTANT:** This PR should target `#{expected_base}` (the epic branch). " \
|
|
698
|
+
"If `gh pr view #{pr_number} --json baseRefName` shows a different base, " \
|
|
699
|
+
"do NOT merge — report the mismatch instead.\n"
|
|
700
|
+
else
|
|
701
|
+
""
|
|
702
|
+
end
|
|
703
|
+
|
|
662
704
|
prompt = <<~PROMPT
|
|
663
705
|
## Final Decision Required — Fizzy Card ##{card_number}
|
|
664
706
|
|
|
@@ -670,7 +712,7 @@ module Brainiac
|
|
|
670
712
|
```
|
|
671
713
|
gh pr merge #{pr_number} --squash --delete-branch
|
|
672
714
|
```
|
|
673
|
-
|
|
715
|
+
#{target_note}
|
|
674
716
|
Note: You cannot self-approve PRs you authored. Merge directly since gates have approved.
|
|
675
717
|
|
|
676
718
|
After merging, update the Fizzy card with a brief status comment.
|
|
@@ -741,12 +783,12 @@ module Brainiac
|
|
|
741
783
|
|
|
742
784
|
# Guard: don't spawn a duplicate if a session is already running for this task
|
|
743
785
|
session_alive = if defined?(session_active?)
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
786
|
+
session_active?(card_key)
|
|
787
|
+
elsif Object.respond_to?(:session_active?, true)
|
|
788
|
+
Object.send(:session_active?, card_key)
|
|
789
|
+
else
|
|
790
|
+
false
|
|
791
|
+
end
|
|
750
792
|
|
|
751
793
|
if session_alive
|
|
752
794
|
LOG.info "[Basecamp:Hooks] Session already active for #{card_key} — skipping dispatch" if defined?(LOG)
|
|
@@ -785,11 +827,11 @@ module Brainiac
|
|
|
785
827
|
prompt = <<~PROMPT
|
|
786
828
|
## Changes Requested — Fizzy Card ##{card_number}
|
|
787
829
|
|
|
788
|
-
Review gates have requested changes on your PR#{
|
|
830
|
+
Review gates have requested changes on your PR#{" ##{pr_number}" if pr_number}.
|
|
789
831
|
Reviewers who requested changes: #{changers}
|
|
790
832
|
|
|
791
833
|
Your job:
|
|
792
|
-
1. Read the review feedback: #{pr_number ? "`gh pr view #{pr_number} --comments`" :
|
|
834
|
+
1. Read the review feedback: #{pr_number ? "`gh pr view #{pr_number} --comments`" : 'check the Fizzy card comments'}
|
|
793
835
|
2. Address all requested changes
|
|
794
836
|
3. Commit and push your fixes
|
|
795
837
|
|
|
@@ -764,14 +764,43 @@ module Brainiac
|
|
|
764
764
|
end
|
|
765
765
|
|
|
766
766
|
# Create epic branches for all projects involved in this epic.
|
|
767
|
+
# CRITICAL: If this fails, epic_branches will be empty and agents will
|
|
768
|
+
# open PRs against the default branch instead of the epic branch.
|
|
769
|
+
# We log loudly and retry resolution of project repos if initial attempt fails.
|
|
767
770
|
def create_epic_branches_for(epic)
|
|
768
771
|
project_repos = resolve_project_repos(epic)
|
|
769
|
-
|
|
772
|
+
|
|
773
|
+
if project_repos.empty?
|
|
774
|
+
# Fallback: try to resolve from the basecamp project mapping directly
|
|
775
|
+
mapped_project = Config.brainiac_project_for(epic["basecamp_project_id"])
|
|
776
|
+
if mapped_project
|
|
777
|
+
projects_file = File.join(BRAINIAC_DIR, "projects.json")
|
|
778
|
+
all_projects = File.exist?(projects_file) ? JSON.parse(File.read(projects_file)) : {}
|
|
779
|
+
repo = all_projects.dig(mapped_project, "repo_path")
|
|
780
|
+
project_repos = { mapped_project => repo } if repo
|
|
781
|
+
end
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
if project_repos.empty?
|
|
785
|
+
LOG.error "[Basecamp:Orchestrator] Cannot create epic branches — no project repos resolved. " \
|
|
786
|
+
"Tasks have projects: #{(epic['tasks'] || []).map { |t| t['project'] }.compact.uniq.inspect}. " \
|
|
787
|
+
"Epic will proceed WITHOUT epic branches — PRs will target the default branch!" if defined?(LOG)
|
|
788
|
+
log_event(epic, "branches_failed", "No project repos resolved — epic branches not created")
|
|
789
|
+
return
|
|
790
|
+
end
|
|
770
791
|
|
|
771
792
|
epic["epic_branches"] = EpicBranch.create_epic_branches(epic, project_repos)
|
|
772
|
-
|
|
793
|
+
|
|
794
|
+
if epic["epic_branches"].empty?
|
|
795
|
+
LOG.error "[Basecamp:Orchestrator] create_epic_branches returned empty — branch creation failed" if defined?(LOG)
|
|
796
|
+
log_event(epic, "branches_failed", "Branch creation returned empty for repos: #{project_repos.keys.join(', ')}")
|
|
797
|
+
else
|
|
798
|
+
log_event(epic, "branches_created", "Epic branches: #{epic['epic_branches'].values.uniq.join(', ')}")
|
|
799
|
+
end
|
|
773
800
|
rescue StandardError => e
|
|
774
|
-
LOG.error "[Basecamp:Orchestrator] Failed to create epic branches: #{e.message}"
|
|
801
|
+
LOG.error "[Basecamp:Orchestrator] Failed to create epic branches: #{e.class}: #{e.message}" \
|
|
802
|
+
"\n#{e.backtrace.first(3).join("\n")}" if defined?(LOG)
|
|
803
|
+
log_event(epic, "branches_failed", "Exception: #{e.message}")
|
|
775
804
|
end
|
|
776
805
|
|
|
777
806
|
# Open final PRs from epic branches to main.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "open3"
|
|
3
4
|
require_relative "basecamp/version"
|
|
4
5
|
require_relative "basecamp/metadata"
|
|
5
6
|
require_relative "basecamp/config"
|
|
@@ -99,16 +100,27 @@ module Brainiac
|
|
|
99
100
|
|
|
100
101
|
case status
|
|
101
102
|
when "final_decision"
|
|
102
|
-
# Check if PR is already merged
|
|
103
|
-
|
|
103
|
+
# Check if PR is already merged — resolve pr_number first if missing/zero
|
|
104
|
+
effective_pr = pr_number
|
|
105
|
+
if (effective_pr.nil? || effective_pr.zero?) && project_key
|
|
106
|
+
resolved = resolve_pr_for_task(task)
|
|
107
|
+
if resolved
|
|
108
|
+
task["pr_number"] = resolved[:number]
|
|
109
|
+
task["pr_repo"] = resolved[:repo] if resolved[:repo]
|
|
110
|
+
effective_pr = resolved[:number]
|
|
111
|
+
LOG.info "[Basecamp:HealthCheck] Resolved PR ##{effective_pr} for card ##{card_number}" if defined?(LOG)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
if effective_pr&.positive? && project_key
|
|
104
116
|
projects_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "projects.json")
|
|
105
117
|
projects = File.exist?(projects_file) ? JSON.parse(File.read(projects_file)) : {}
|
|
106
118
|
github_repo = projects.dig(project_key, "github_repo")
|
|
107
119
|
|
|
108
120
|
if github_repo
|
|
109
|
-
pr_state, = Open3.capture2("gh", "pr", "view",
|
|
121
|
+
pr_state, = Open3.capture2("gh", "pr", "view", effective_pr.to_s, "--repo", github_repo, "--json", "state", "-q", ".state")
|
|
110
122
|
if pr_state.strip == "MERGED"
|
|
111
|
-
LOG.info "[Basecamp:HealthCheck] PR ##{
|
|
123
|
+
LOG.info "[Basecamp:HealthCheck] PR ##{effective_pr} merged but task ##{card_number} stuck in final_decision — healing" if defined?(LOG)
|
|
112
124
|
Orchestrator.on_card_completed(card_number)
|
|
113
125
|
healed_any = true
|
|
114
126
|
end
|
|
@@ -388,6 +400,35 @@ module Brainiac
|
|
|
388
400
|
card_number = task["fizzy_card"]
|
|
389
401
|
LOG.info "[Basecamp] Resume: checking final_decision task ##{card_number}, awaiting=#{task['awaiting_final_decision']}" if defined?(LOG)
|
|
390
402
|
|
|
403
|
+
# First, resolve PR number if missing or zero (branch creation may have failed)
|
|
404
|
+
pr_number = task["pr_number"]
|
|
405
|
+
if (pr_number.nil? || pr_number.zero?) && task["project"]
|
|
406
|
+
resolved_pr = resolve_pr_for_task(task)
|
|
407
|
+
if resolved_pr
|
|
408
|
+
task["pr_number"] = resolved_pr[:number]
|
|
409
|
+
task["pr_repo"] = resolved_pr[:repo] if resolved_pr[:repo]
|
|
410
|
+
pr_number = resolved_pr[:number]
|
|
411
|
+
LOG.info "[Basecamp] Resume: resolved PR ##{pr_number} for card ##{card_number}" if defined?(LOG)
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Check if PR is already merged (handles manual merges, webhook misses, agent merges to wrong branch)
|
|
416
|
+
if pr_number&.positive?
|
|
417
|
+
project_key = task["project"]
|
|
418
|
+
projects_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "projects.json")
|
|
419
|
+
projects = File.exist?(projects_file) ? JSON.parse(File.read(projects_file)) : {}
|
|
420
|
+
github_repo = projects.dig(project_key, "github_repo")
|
|
421
|
+
|
|
422
|
+
if github_repo
|
|
423
|
+
pr_state, = Open3.capture2("gh", "pr", "view", pr_number.to_s, "--repo", github_repo, "--json", "state", "-q", ".state")
|
|
424
|
+
if pr_state.strip == "MERGED"
|
|
425
|
+
LOG.info "[Basecamp] Resume: PR ##{pr_number} already merged — marking card ##{card_number} complete" if defined?(LOG)
|
|
426
|
+
Orchestrator.on_card_completed(card_number)
|
|
427
|
+
return
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
391
432
|
# If awaiting_final_decision is set, re-dispatch
|
|
392
433
|
# Also handle the case where it's nil but gates are all approved (stale state)
|
|
393
434
|
if task["awaiting_final_decision"] || ReviewGate.all_gates_passed?(task)
|
|
@@ -400,6 +441,38 @@ module Brainiac
|
|
|
400
441
|
end
|
|
401
442
|
end
|
|
402
443
|
|
|
444
|
+
# Resolve a PR number for a task by searching GitHub for matching branch patterns.
|
|
445
|
+
# Used when pr_number is 0 or nil (e.g., branch creation failed so PR was never tracked).
|
|
446
|
+
def resolve_pr_for_task(task)
|
|
447
|
+
card_number = task["fizzy_card"]
|
|
448
|
+
project_key = task["project"]
|
|
449
|
+
return nil unless project_key
|
|
450
|
+
|
|
451
|
+
projects_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "projects.json")
|
|
452
|
+
projects = File.exist?(projects_file) ? JSON.parse(File.read(projects_file)) : {}
|
|
453
|
+
repo_path = projects.dig(project_key, "repo_path")
|
|
454
|
+
github_repo = projects.dig(project_key, "github_repo")
|
|
455
|
+
return nil unless repo_path
|
|
456
|
+
|
|
457
|
+
# Search for PR by fizzy-NNNN branch pattern (any state)
|
|
458
|
+
stdout, _, status = Open3.capture3(
|
|
459
|
+
"gh", "pr", "list", "--state", "all",
|
|
460
|
+
"--json", "number,headRefName,state",
|
|
461
|
+
"--jq", ".[] | select(.headRefName | startswith(\"fizzy-#{card_number}\")) | [.number, .state] | @tsv",
|
|
462
|
+
chdir: repo_path
|
|
463
|
+
)
|
|
464
|
+
return nil unless status.success? && !stdout.strip.empty?
|
|
465
|
+
|
|
466
|
+
# Take the first match
|
|
467
|
+
number, _state = stdout.strip.split("\n").first.split("\t")
|
|
468
|
+
return nil unless number
|
|
469
|
+
|
|
470
|
+
{ number: number.to_i, repo: github_repo }
|
|
471
|
+
rescue StandardError => e
|
|
472
|
+
LOG.warn "[Basecamp] Error resolving PR for card ##{card_number}: #{e.message}" if defined?(LOG)
|
|
473
|
+
nil
|
|
474
|
+
end
|
|
475
|
+
|
|
403
476
|
def setup_routes(app)
|
|
404
477
|
setup_webhook_route(app)
|
|
405
478
|
setup_api_routes(app)
|