brainiac-basecamp 0.0.9 → 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
|
|
@@ -214,16 +226,12 @@ module Brainiac
|
|
|
214
226
|
# All gates have reviewed — dispatch implementation agent to address ALL feedback
|
|
215
227
|
LOG.info "[Basecamp:Hooks] All gates responded for card ##{card_number} — dispatching fixes" if defined?(LOG)
|
|
216
228
|
task["status"] = "in_flight"
|
|
229
|
+
task["dispatched_at"] = Time.now.iso8601
|
|
217
230
|
task.delete("changes_debounce_started")
|
|
218
231
|
save_epic_state(epic)
|
|
219
232
|
|
|
220
|
-
#
|
|
221
|
-
|
|
222
|
-
fizzy_user_id = Orchestrator.send(:resolve_fizzy_user_id, impl_agent)
|
|
223
|
-
if fizzy_user_id
|
|
224
|
-
LOG.info "[Basecamp:Hooks] Re-assigning card ##{card_number} to #{impl_agent} for fixes" if defined?(LOG)
|
|
225
|
-
safe_assign_card(card_number, fizzy_user_id)
|
|
226
|
-
end
|
|
233
|
+
# Spawn agent directly — safe_assign_card won't work if already assigned
|
|
234
|
+
dispatch_impl_directly(epic, task)
|
|
227
235
|
else
|
|
228
236
|
# Wait for remaining gates to respond
|
|
229
237
|
responded = (task["gate_approvals"]&.size || 0) + (task["changes_requested_by"]&.size || 0)
|
|
@@ -246,11 +254,11 @@ module Brainiac
|
|
|
246
254
|
if task_reloaded && task_reloaded["status"] == "in_review" && task_reloaded["changes_requested_by"]&.any?
|
|
247
255
|
LOG.info "[Basecamp:Hooks] Debounce timeout for card ##{card_number} — forcing dispatch" if defined?(LOG)
|
|
248
256
|
task_reloaded["status"] = "in_flight"
|
|
257
|
+
task_reloaded["dispatched_at"] = Time.now.iso8601
|
|
249
258
|
task_reloaded.delete("changes_debounce_started")
|
|
250
259
|
save_epic_state(epic_reloaded)
|
|
251
|
-
#
|
|
252
|
-
|
|
253
|
-
safe_assign_card(card_number, fizzy_user_id) if fizzy_user_id
|
|
260
|
+
# Spawn agent directly — safe_assign_card won't work if already assigned
|
|
261
|
+
dispatch_impl_directly(epic_reloaded, task_reloaded)
|
|
254
262
|
end
|
|
255
263
|
rescue StandardError => e
|
|
256
264
|
LOG.error "[Basecamp:Hooks] Debounce dispatch failed: #{e.message}" if defined?(LOG)
|
|
@@ -617,13 +625,32 @@ module Brainiac
|
|
|
617
625
|
github_repo = project_config&.dig("github_repo")
|
|
618
626
|
|
|
619
627
|
# Check if PR is already merged (handles manual merges, webhook misses, etc.)
|
|
620
|
-
if github_repo && pr_number
|
|
628
|
+
if github_repo && pr_number&.to_i&.positive?
|
|
621
629
|
pr_state, = Open3.capture2("gh", "pr", "view", pr_number.to_s, "--repo", github_repo, "--json", "state", "-q", ".state")
|
|
622
630
|
if pr_state.strip == "MERGED"
|
|
623
631
|
LOG.info "[Basecamp:Hooks] PR ##{pr_number} already merged — marking card ##{card_number} complete" if defined?(LOG)
|
|
624
632
|
Orchestrator.on_card_completed(card_number)
|
|
625
633
|
return
|
|
626
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
|
|
627
654
|
end
|
|
628
655
|
|
|
629
656
|
# Mark task as awaiting final decision
|
|
@@ -663,6 +690,17 @@ module Brainiac
|
|
|
663
690
|
gate_approvals = task["gate_approvals"] || []
|
|
664
691
|
gate_agents = gate_approvals.map { |a| a["agent"] }.join(", ")
|
|
665
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
|
+
|
|
666
704
|
prompt = <<~PROMPT
|
|
667
705
|
## Final Decision Required — Fizzy Card ##{card_number}
|
|
668
706
|
|
|
@@ -674,7 +712,7 @@ module Brainiac
|
|
|
674
712
|
```
|
|
675
713
|
gh pr merge #{pr_number} --squash --delete-branch
|
|
676
714
|
```
|
|
677
|
-
|
|
715
|
+
#{target_note}
|
|
678
716
|
Note: You cannot self-approve PRs you authored. Merge directly since gates have approved.
|
|
679
717
|
|
|
680
718
|
After merging, update the Fizzy card with a brief status comment.
|
|
@@ -728,14 +766,123 @@ module Brainiac
|
|
|
728
766
|
end
|
|
729
767
|
end
|
|
730
768
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
769
|
+
# Dispatch the implementation agent directly (spawns via run_agent) to address
|
|
770
|
+
# changes_requested feedback. Used by resume and health-check when the agent is
|
|
771
|
+
# no longer running but the Fizzy card is still assigned (so safe_assign_card
|
|
772
|
+
# would bail out with "already assigned, skipping").
|
|
773
|
+
#
|
|
774
|
+
# @param epic [Hash] Epic state
|
|
775
|
+
# @param task [Hash] Task within the epic
|
|
776
|
+
# @return [void]
|
|
777
|
+
def dispatch_impl_directly(epic, task)
|
|
778
|
+
card_number = task["fizzy_card"]
|
|
779
|
+
agent_name = epic["agent"]
|
|
780
|
+
pr_number = task["pr_number"]
|
|
781
|
+
project_key = task["project"]
|
|
782
|
+
card_key = "changes-requested-#{card_number}"
|
|
783
|
+
|
|
784
|
+
# Guard: don't spawn a duplicate if a session is already running for this task
|
|
785
|
+
session_alive = if defined?(session_active?)
|
|
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
|
|
792
|
+
|
|
793
|
+
if session_alive
|
|
794
|
+
LOG.info "[Basecamp:Hooks] Session already active for #{card_key} — skipping dispatch" if defined?(LOG)
|
|
795
|
+
return
|
|
796
|
+
end
|
|
734
797
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
798
|
+
LOG.info "[Basecamp:Hooks] Direct-dispatching #{agent_name} for changes_requested on card ##{card_number}" if defined?(LOG)
|
|
799
|
+
|
|
800
|
+
# Get project config
|
|
801
|
+
projects_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "projects.json")
|
|
802
|
+
projects = File.exist?(projects_file) ? JSON.parse(File.read(projects_file)) : {}
|
|
803
|
+
project_config = projects[project_key]
|
|
804
|
+
|
|
805
|
+
unless project_config
|
|
806
|
+
LOG.error "[Basecamp:Hooks] No project config for '#{project_key}' — cannot direct-dispatch" if defined?(LOG)
|
|
807
|
+
return
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
repo_path = project_config["repo_path"]
|
|
811
|
+
unless repo_path
|
|
812
|
+
LOG.error "[Basecamp:Hooks] No repo_path for project '#{project_key}' — cannot direct-dispatch" if defined?(LOG)
|
|
813
|
+
return
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
# Find the worktree for this card
|
|
817
|
+
work_items_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "work_items.json")
|
|
818
|
+
worktree_path = repo_path # Default to main repo
|
|
819
|
+
if File.exist?(work_items_file)
|
|
820
|
+
work_items = JSON.parse(File.read(work_items_file))
|
|
821
|
+
work_item = work_items.values.find { |wi| wi.dig("sources", "fizzy", "card_number") == card_number }
|
|
822
|
+
worktree_path = work_item["worktree"] if work_item&.dig("worktree")
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
# Build prompt
|
|
826
|
+
changers = (task["changes_requested_by"] || []).join(", ")
|
|
827
|
+
prompt = <<~PROMPT
|
|
828
|
+
## Changes Requested — Fizzy Card ##{card_number}
|
|
829
|
+
|
|
830
|
+
Review gates have requested changes on your PR#{" ##{pr_number}" if pr_number}.
|
|
831
|
+
Reviewers who requested changes: #{changers}
|
|
832
|
+
|
|
833
|
+
Your job:
|
|
834
|
+
1. Read the review feedback: #{pr_number ? "`gh pr view #{pr_number} --comments`" : 'check the Fizzy card comments'}
|
|
835
|
+
2. Address all requested changes
|
|
836
|
+
3. Commit and push your fixes
|
|
837
|
+
|
|
838
|
+
After pushing, update the Fizzy card with a brief status comment noting what you fixed.
|
|
839
|
+
PROMPT
|
|
840
|
+
|
|
841
|
+
# Resolve GitHub App token so agent's `gh` commands run as their bot identity
|
|
842
|
+
github_repo = project_config&.dig("github_repo")
|
|
843
|
+
agent_env = github_repo ? ReviewGate.send(:resolve_agent_github_env, agent_name, github_repo) : {}
|
|
844
|
+
|
|
845
|
+
# Spawn the agent directly
|
|
846
|
+
pid = nil
|
|
847
|
+
log_file = nil
|
|
848
|
+
|
|
849
|
+
begin
|
|
850
|
+
pid, log_file = method(:run_agent).call(
|
|
851
|
+
prompt,
|
|
852
|
+
project_config: project_config,
|
|
853
|
+
chdir: worktree_path,
|
|
854
|
+
log_name: "changes-requested-#{card_number}",
|
|
855
|
+
agent_name: agent_name,
|
|
856
|
+
source: :basecamp,
|
|
857
|
+
card_number: card_number,
|
|
858
|
+
env: agent_env
|
|
859
|
+
)
|
|
860
|
+
rescue NameError
|
|
861
|
+
if Object.respond_to?(:run_agent, true)
|
|
862
|
+
pid, log_file = Object.send(:run_agent,
|
|
863
|
+
prompt,
|
|
864
|
+
project_config: project_config,
|
|
865
|
+
chdir: worktree_path,
|
|
866
|
+
log_name: "changes-requested-#{card_number}",
|
|
867
|
+
agent_name: agent_name,
|
|
868
|
+
source: :basecamp,
|
|
869
|
+
card_number: card_number,
|
|
870
|
+
env: agent_env)
|
|
871
|
+
else
|
|
872
|
+
LOG.warn "[Basecamp:Hooks] run_agent not available — direct impl dispatch skipped" if defined?(LOG)
|
|
873
|
+
return
|
|
874
|
+
end
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
# Register session for waybar visibility
|
|
878
|
+
if pid
|
|
879
|
+
if defined?(register_session)
|
|
880
|
+
register_session(card_key, pid, log_file: log_file, agent_name: agent_name)
|
|
881
|
+
elsif Object.respond_to?(:register_session, true)
|
|
882
|
+
Object.send(:register_session, card_key, pid, log_file: log_file, agent_name: agent_name)
|
|
883
|
+
end
|
|
884
|
+
LOG.info "[Basecamp:Hooks] Spawned #{agent_name} (pid #{pid}) for changes_requested on card ##{card_number}" if defined?(LOG)
|
|
885
|
+
end
|
|
739
886
|
end
|
|
740
887
|
|
|
741
888
|
def auto_merge_and_advance(epic, card_number, ctx)
|
|
@@ -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
|
|
@@ -129,25 +141,21 @@ module Brainiac
|
|
|
129
141
|
impl_agent = epic["agent"]
|
|
130
142
|
LOG.info "[Basecamp:HealthCheck] Task ##{card_number} in_flight for #{elapsed.round}s — re-dispatching #{impl_agent}" if defined?(LOG)
|
|
131
143
|
|
|
132
|
-
#
|
|
144
|
+
# Spawn the agent directly — safe_assign_card won't work if already assigned
|
|
133
145
|
task["dispatched_at"] = Time.now.iso8601
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
Hooks.send(:safe_assign_card, card_number, fizzy_user_id)
|
|
137
|
-
healed_any = true
|
|
138
|
-
end
|
|
146
|
+
Hooks.send(:dispatch_impl_directly, epic, task)
|
|
147
|
+
healed_any = true
|
|
139
148
|
end
|
|
140
149
|
elsif task["changes_requested_by"]&.any?
|
|
141
150
|
# No dispatched_at recorded but changes were requested — legacy state.
|
|
142
151
|
# Always re-dispatch in this case.
|
|
143
152
|
impl_agent = epic["agent"]
|
|
144
153
|
LOG.info "[Basecamp:HealthCheck] Task ##{card_number} in_flight with changes_requested but no dispatched_at — re-dispatching #{impl_agent}" if defined?(LOG)
|
|
154
|
+
|
|
155
|
+
# Spawn the agent directly — safe_assign_card won't work if already assigned
|
|
145
156
|
task["dispatched_at"] = Time.now.iso8601
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
Hooks.send(:safe_assign_card, card_number, fizzy_user_id)
|
|
149
|
-
healed_any = true
|
|
150
|
-
end
|
|
157
|
+
Hooks.send(:dispatch_impl_directly, epic, task)
|
|
158
|
+
healed_any = true
|
|
151
159
|
end
|
|
152
160
|
|
|
153
161
|
when "in_review"
|
|
@@ -329,13 +337,11 @@ module Brainiac
|
|
|
329
337
|
|
|
330
338
|
# Transition to in_flight so the agent gets the right context
|
|
331
339
|
task["status"] = "in_flight"
|
|
340
|
+
task["dispatched_at"] = Time.now.iso8601
|
|
332
341
|
Hooks.send(:save_epic_state, epic)
|
|
333
342
|
|
|
334
|
-
#
|
|
335
|
-
|
|
336
|
-
if fizzy_user_id
|
|
337
|
-
Hooks.send(:safe_assign_card, card_number, fizzy_user_id)
|
|
338
|
-
end
|
|
343
|
+
# Spawn the agent directly — safe_assign_card won't work if already assigned
|
|
344
|
+
Hooks.send(:dispatch_impl_directly, epic, task)
|
|
339
345
|
else
|
|
340
346
|
# No changes requested yet — check if gates need (re-)dispatching
|
|
341
347
|
approvals = task["gate_approvals"]&.size || 0
|
|
@@ -394,6 +400,35 @@ module Brainiac
|
|
|
394
400
|
card_number = task["fizzy_card"]
|
|
395
401
|
LOG.info "[Basecamp] Resume: checking final_decision task ##{card_number}, awaiting=#{task['awaiting_final_decision']}" if defined?(LOG)
|
|
396
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
|
+
|
|
397
432
|
# If awaiting_final_decision is set, re-dispatch
|
|
398
433
|
# Also handle the case where it's nil but gates are all approved (stale state)
|
|
399
434
|
if task["awaiting_final_decision"] || ReviewGate.all_gates_passed?(task)
|
|
@@ -406,6 +441,38 @@ module Brainiac
|
|
|
406
441
|
end
|
|
407
442
|
end
|
|
408
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
|
+
|
|
409
476
|
def setup_routes(app)
|
|
410
477
|
setup_webhook_route(app)
|
|
411
478
|
setup_api_routes(app)
|