brainiac-basecamp 0.0.15 → 0.0.16
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/basecamp/epic_memory.rb +179 -0
- data/lib/brainiac/plugins/basecamp/hooks.rb +17 -0
- data/lib/brainiac/plugins/basecamp/orchestrator.rb +31 -3
- data/lib/brainiac/plugins/basecamp/review_gate.rb +122 -25
- data/lib/brainiac/plugins/basecamp/version.rb +1 -1
- data/lib/brainiac/plugins/basecamp.rb +3 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4d77f4d0787e023edeec79754c9958f4ee09f036250038ef7cc4ae1dc9961a20
|
|
4
|
+
data.tar.gz: 74d8aba9539c7526eed62e209b8a84cabe9ecf26482f8873f8c1581a2a70a86a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 198ee07060302e70cf71612ede20b9232efda88f91090273dcee7606264364f381ad7646443a8965319a2437cb5a7f46d6e1e1b92e2683aa87fb176028fd97d2
|
|
7
|
+
data.tar.gz: 9194582cc89b6a9cb05721c0369f80762b656776b29e5ad5f97662a6ae460364d486c7d0d15b20bd36e131b9e41c74b9e66d71910155e30b206d0865bd03c967
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brainiac
|
|
4
|
+
module Plugins
|
|
5
|
+
module Basecamp
|
|
6
|
+
# Epic-level shared memory.
|
|
7
|
+
#
|
|
8
|
+
# Unlike per-card memory (agent-specific, gitignored), epic memory is shared
|
|
9
|
+
# across all agents working the epic AND persisted to git. It accumulates
|
|
10
|
+
# architectural decisions, patterns established, gotchas discovered, and
|
|
11
|
+
# cross-task learnings.
|
|
12
|
+
#
|
|
13
|
+
# The epic review agent writes to this file after each task completes.
|
|
14
|
+
# All task agents and gate agents read it as part of their context.
|
|
15
|
+
#
|
|
16
|
+
# Location: ~/.brainiac/brain/knowledge/epics/epic-<todolist-id>.md
|
|
17
|
+
#
|
|
18
|
+
# This is under knowledge/ (not memory/) because:
|
|
19
|
+
# - memory/ is gitignored — per-card, per-agent, ephemeral
|
|
20
|
+
# - knowledge/ is synced to git — shared, permanent, valuable
|
|
21
|
+
# - Epic learnings should persist and be searchable via qmd
|
|
22
|
+
module EpicMemory
|
|
23
|
+
BRAINIAC_DIR = ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac"))
|
|
24
|
+
EPIC_MEMORY_DIR = File.join(BRAINIAC_DIR, "brain", "knowledge", "epics")
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# Path to the epic memory file for a given epic.
|
|
28
|
+
#
|
|
29
|
+
# @param todolist_id [String, Integer] Basecamp todolist ID
|
|
30
|
+
# @return [String] Absolute file path
|
|
31
|
+
def path_for(todolist_id)
|
|
32
|
+
File.join(EPIC_MEMORY_DIR, "epic-#{todolist_id}.md")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Ensure the epic memory directory exists.
|
|
36
|
+
def ensure_directory!
|
|
37
|
+
FileUtils.mkdir_p(EPIC_MEMORY_DIR) unless File.directory?(EPIC_MEMORY_DIR)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Read the epic memory content, if it exists.
|
|
41
|
+
#
|
|
42
|
+
# @param todolist_id [String, Integer] Basecamp todolist ID
|
|
43
|
+
# @return [String, nil] Content or nil if no memory exists
|
|
44
|
+
def read(todolist_id)
|
|
45
|
+
path = path_for(todolist_id)
|
|
46
|
+
return nil unless File.exist?(path)
|
|
47
|
+
|
|
48
|
+
content = File.read(path).strip
|
|
49
|
+
content.empty? ? nil : content
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
LOG.warn "[Basecamp:EpicMemory] Failed to read epic memory: #{e.message}" if defined?(LOG)
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Check if epic memory exists.
|
|
56
|
+
#
|
|
57
|
+
# @param todolist_id [String, Integer] Basecamp todolist ID
|
|
58
|
+
# @return [Boolean]
|
|
59
|
+
def exists?(todolist_id)
|
|
60
|
+
File.exist?(path_for(todolist_id))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Initialize epic memory with the epic title and initial context.
|
|
64
|
+
# Called when an epic starts.
|
|
65
|
+
#
|
|
66
|
+
# @param epic [Hash] Epic state
|
|
67
|
+
def initialize_for(epic)
|
|
68
|
+
ensure_directory!
|
|
69
|
+
path = path_for(epic["basecamp_todolist_id"])
|
|
70
|
+
|
|
71
|
+
# Don't overwrite existing memory (in case of resume)
|
|
72
|
+
return if File.exist?(path) && !File.read(path).strip.empty?
|
|
73
|
+
|
|
74
|
+
initial_content = <<~MARKDOWN
|
|
75
|
+
# Epic Memory: #{epic['title']}
|
|
76
|
+
|
|
77
|
+
Epic started: #{epic['started_at']}
|
|
78
|
+
Orchestrating agent: #{epic['agent']}
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Architectural Decisions
|
|
83
|
+
|
|
84
|
+
(No decisions recorded yet)
|
|
85
|
+
|
|
86
|
+
## Patterns Established
|
|
87
|
+
|
|
88
|
+
(No patterns recorded yet)
|
|
89
|
+
|
|
90
|
+
## Gotchas & Learnings
|
|
91
|
+
|
|
92
|
+
(No learnings recorded yet)
|
|
93
|
+
|
|
94
|
+
## Cross-Task Notes
|
|
95
|
+
|
|
96
|
+
(No notes recorded yet)
|
|
97
|
+
MARKDOWN
|
|
98
|
+
|
|
99
|
+
File.write(path, initial_content)
|
|
100
|
+
LOG.info "[Basecamp:EpicMemory] Initialized memory for epic #{epic['id']}" if defined?(LOG)
|
|
101
|
+
rescue StandardError => e
|
|
102
|
+
LOG.error "[Basecamp:EpicMemory] Failed to initialize: #{e.message}" if defined?(LOG)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Append a section to the epic memory.
|
|
106
|
+
# Used by the epic review agent after each task completes.
|
|
107
|
+
#
|
|
108
|
+
# @param todolist_id [String, Integer] Basecamp todolist ID
|
|
109
|
+
# @param section [String] Section title (e.g., "After Task #1234")
|
|
110
|
+
# @param content [String] Content to add
|
|
111
|
+
def append_section(todolist_id, section:, content:)
|
|
112
|
+
ensure_directory!
|
|
113
|
+
path = path_for(todolist_id)
|
|
114
|
+
|
|
115
|
+
existing = File.exist?(path) ? File.read(path) : ""
|
|
116
|
+
timestamp = Time.now.strftime("%Y-%m-%d %H:%M")
|
|
117
|
+
|
|
118
|
+
new_section = <<~MARKDOWN
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## #{section}
|
|
123
|
+
_Updated: #{timestamp}_
|
|
124
|
+
|
|
125
|
+
#{content.strip}
|
|
126
|
+
MARKDOWN
|
|
127
|
+
|
|
128
|
+
File.write(path, existing + new_section)
|
|
129
|
+
LOG.info "[Basecamp:EpicMemory] Appended section '#{section}' to epic #{todolist_id}" if defined?(LOG)
|
|
130
|
+
rescue StandardError => e
|
|
131
|
+
LOG.error "[Basecamp:EpicMemory] Failed to append: #{e.message}" if defined?(LOG)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Build context injection for an agent prompt.
|
|
135
|
+
# Returns a formatted string with the epic memory, or nil if none exists.
|
|
136
|
+
#
|
|
137
|
+
# @param todolist_id [String, Integer] Basecamp todolist ID
|
|
138
|
+
# @return [String, nil] Formatted context for prompt injection
|
|
139
|
+
def build_context(todolist_id)
|
|
140
|
+
content = read(todolist_id)
|
|
141
|
+
return nil unless content
|
|
142
|
+
|
|
143
|
+
<<~CONTEXT
|
|
144
|
+
## Epic Memory (Shared Knowledge)
|
|
145
|
+
|
|
146
|
+
The following is shared knowledge accumulated across this epic.
|
|
147
|
+
Reference this when making implementation decisions.
|
|
148
|
+
|
|
149
|
+
#{content}
|
|
150
|
+
CONTEXT
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Clean up epic memory after epic completes.
|
|
154
|
+
# Optionally archives to a different location rather than deleting.
|
|
155
|
+
#
|
|
156
|
+
# @param todolist_id [String, Integer] Basecamp todolist ID
|
|
157
|
+
# @param archive [Boolean] Whether to archive rather than delete
|
|
158
|
+
def cleanup(todolist_id, archive: true)
|
|
159
|
+
path = path_for(todolist_id)
|
|
160
|
+
return unless File.exist?(path)
|
|
161
|
+
|
|
162
|
+
if archive
|
|
163
|
+
archive_dir = File.join(EPIC_MEMORY_DIR, "archived")
|
|
164
|
+
FileUtils.mkdir_p(archive_dir)
|
|
165
|
+
archive_path = File.join(archive_dir, "epic-#{todolist_id}-#{Time.now.strftime('%Y%m%d')}.md")
|
|
166
|
+
FileUtils.mv(path, archive_path)
|
|
167
|
+
LOG.info "[Basecamp:EpicMemory] Archived epic memory to #{archive_path}" if defined?(LOG)
|
|
168
|
+
else
|
|
169
|
+
FileUtils.rm(path)
|
|
170
|
+
LOG.info "[Basecamp:EpicMemory] Deleted epic memory for #{todolist_id}" if defined?(LOG)
|
|
171
|
+
end
|
|
172
|
+
rescue StandardError => e
|
|
173
|
+
LOG.warn "[Basecamp:EpicMemory] Cleanup failed: #{e.message}" if defined?(LOG)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -446,6 +446,23 @@ module Brainiac
|
|
|
446
446
|
""
|
|
447
447
|
]
|
|
448
448
|
|
|
449
|
+
# Inject epic memory if it exists (shared knowledge across the epic)
|
|
450
|
+
epic_memory = EpicMemory.read(epic["basecamp_todolist_id"])
|
|
451
|
+
if epic_memory
|
|
452
|
+
context_lines << "### Epic Memory (Shared Knowledge)"
|
|
453
|
+
context_lines << ""
|
|
454
|
+
context_lines << "The following is shared knowledge accumulated from previous tasks in this epic."
|
|
455
|
+
context_lines << "Reference this when making implementation decisions."
|
|
456
|
+
context_lines << ""
|
|
457
|
+
context_lines << "```markdown"
|
|
458
|
+
context_lines << epic_memory.lines.first(50).join # Limit to first 50 lines to avoid context overflow
|
|
459
|
+
if epic_memory.lines.size > 50
|
|
460
|
+
context_lines << "... (truncated — full file at ~/.brainiac/brain/memory/epics/epic-#{epic['basecamp_todolist_id']}.md)"
|
|
461
|
+
end
|
|
462
|
+
context_lines << "```"
|
|
463
|
+
context_lines << ""
|
|
464
|
+
end
|
|
465
|
+
|
|
449
466
|
# List completed tasks with their memory files for reference
|
|
450
467
|
completed_tasks = tasks.select { |t| t["status"] == "complete" }
|
|
451
468
|
if completed_tasks.any?
|
|
@@ -49,6 +49,9 @@ module Brainiac
|
|
|
49
49
|
LOG.info "[Basecamp:Orchestrator] Started epic '#{title}' (todolist #{todolist_id}) " \
|
|
50
50
|
"with agent #{agent}, review_gate: #{review_gate}" if defined?(LOG)
|
|
51
51
|
|
|
52
|
+
# Initialize epic memory for cross-task shared knowledge
|
|
53
|
+
EpicMemory.initialize_for(epic)
|
|
54
|
+
|
|
52
55
|
# FIRST: Populate tasks with project info from Fizzy card tags
|
|
53
56
|
# This must happen BEFORE creating epic branches so we know which repos are involved
|
|
54
57
|
populate_tasks(epic)
|
|
@@ -642,6 +645,18 @@ module Brainiac
|
|
|
642
645
|
"- ##{t['fizzy_card']}: #{t['title']}#{dep_str}"
|
|
643
646
|
end.join("\n")
|
|
644
647
|
|
|
648
|
+
# Include existing epic memory if any
|
|
649
|
+
epic_memory_path = EpicMemory.path_for(epic["basecamp_todolist_id"])
|
|
650
|
+
epic_memory_section = if EpicMemory.exists?(epic["basecamp_todolist_id"])
|
|
651
|
+
<<~MEM
|
|
652
|
+
|
|
653
|
+
### Epic Memory (shared knowledge so far)
|
|
654
|
+
Read the epic memory file at `#{epic_memory_path}` for decisions and patterns established in previous tasks.
|
|
655
|
+
MEM
|
|
656
|
+
else
|
|
657
|
+
""
|
|
658
|
+
end
|
|
659
|
+
|
|
645
660
|
prompt = <<~PROMPT
|
|
646
661
|
## Epic Review: #{epic['title']}
|
|
647
662
|
|
|
@@ -652,7 +667,7 @@ module Brainiac
|
|
|
652
667
|
|
|
653
668
|
### Remaining tasks (with current dependencies):
|
|
654
669
|
#{remaining_summary}
|
|
655
|
-
|
|
670
|
+
#{epic_memory_section}
|
|
656
671
|
### Your job:
|
|
657
672
|
1. Read the memory files for completed tasks to understand what was implemented
|
|
658
673
|
2. Check if remaining tasks still make sense given the implementation decisions
|
|
@@ -665,10 +680,20 @@ module Brainiac
|
|
|
665
680
|
|
|
666
681
|
Memory files are at: `~/.brainiac/brain/memory/#{agent_name&.downcase}/card-<number>.md`
|
|
667
682
|
|
|
668
|
-
|
|
683
|
+
### Update Epic Memory (IMPORTANT)
|
|
684
|
+
After your review, update the epic memory file at `#{epic_memory_path}` with:
|
|
685
|
+
- **Architectural decisions** made during task ##{completed_card_number}
|
|
686
|
+
- **Patterns established** that should be followed in remaining tasks
|
|
687
|
+
- **Gotchas** discovered that future tasks should know about
|
|
688
|
+
- **Cross-task notes** about relationships or dependencies
|
|
689
|
+
|
|
690
|
+
This is shared knowledge — other agents will read it. Be concise but thorough.
|
|
691
|
+
Append a new section, don't replace the existing content.
|
|
692
|
+
|
|
693
|
+
Then post a brief summary comment on the Basecamp todolist:
|
|
669
694
|
`basecamp comments create #{epic['basecamp_todolist_id']} "Epic review after ##{completed_card_number}: <your summary>" --in #{epic['basecamp_project_id']}`
|
|
670
695
|
|
|
671
|
-
Keep
|
|
696
|
+
Keep the basecamp comment concise — this is a checkpoint, not a full analysis.
|
|
672
697
|
PROMPT
|
|
673
698
|
|
|
674
699
|
# Get project config for the agent
|
|
@@ -759,6 +784,9 @@ module Brainiac
|
|
|
759
784
|
|
|
760
785
|
LOG.info "[Basecamp:Orchestrator] Epic '#{epic['title']}' completed!" if defined?(LOG)
|
|
761
786
|
|
|
787
|
+
# Archive epic memory (preserves it for future reference)
|
|
788
|
+
EpicMemory.cleanup(epic["basecamp_todolist_id"], archive: true)
|
|
789
|
+
|
|
762
790
|
# If epic_branch mode, open final PRs to main
|
|
763
791
|
if epic["review_gate"] == "epic_branch" && epic["epic_branches"]&.any?
|
|
764
792
|
open_final_prs(epic)
|
|
@@ -166,15 +166,23 @@ module Brainiac
|
|
|
166
166
|
# @param pr_number [Integer, String] PR number
|
|
167
167
|
# @param repo_name [String] e.g. "stowzilla/brainiac-basecamp"
|
|
168
168
|
# @param repo_path [String] Local repo path
|
|
169
|
+
# @param is_rereview [Boolean] True if this is a re-review after changes
|
|
169
170
|
# @return [Array<String>] Agent names dispatched
|
|
170
|
-
def dispatch_gates(epic:, task:, pr_number:, repo_name:, repo_path:)
|
|
171
|
+
def dispatch_gates(epic:, task:, pr_number:, repo_name:, repo_path:, is_rereview: false)
|
|
171
172
|
dispatched = []
|
|
172
173
|
|
|
174
|
+
# Get current HEAD SHA for tracking incremental reviews
|
|
175
|
+
current_sha = get_pr_head_sha(pr_number: pr_number, repo_path: repo_path)
|
|
176
|
+
|
|
177
|
+
# Determine if this is a re-review and what SHA to diff from
|
|
178
|
+
last_reviewed_sha = task["last_reviewed_sha"]
|
|
179
|
+
diff_from_sha = is_rereview && last_reviewed_sha ? last_reviewed_sha : nil
|
|
180
|
+
|
|
173
181
|
gates.each do |gate|
|
|
174
182
|
agent_name = gate["agent"]
|
|
175
183
|
role = gate["role"] || "review"
|
|
176
184
|
|
|
177
|
-
LOG.info "[Basecamp:ReviewGate] Dispatching #{agent_name} (#{role}) to review PR ##{pr_number}" if defined?(LOG)
|
|
185
|
+
LOG.info "[Basecamp:ReviewGate] Dispatching #{agent_name} (#{role}) to review PR ##{pr_number}#{" (incremental from #{diff_from_sha[0..7]})" if diff_from_sha}" if defined?(LOG)
|
|
178
186
|
|
|
179
187
|
# Dispatch the gate agent via brainiac-github's PR review mechanism.
|
|
180
188
|
# The agent gets the PR diff and reviews it using their bot identity.
|
|
@@ -186,7 +194,8 @@ module Brainiac
|
|
|
186
194
|
repo_name: repo_name,
|
|
187
195
|
repo_path: repo_path,
|
|
188
196
|
card_number: task["fizzy_card"],
|
|
189
|
-
epic: epic
|
|
197
|
+
epic: epic,
|
|
198
|
+
diff_from_sha: diff_from_sha
|
|
190
199
|
)
|
|
191
200
|
rescue StandardError => e
|
|
192
201
|
LOG.error "[Basecamp:ReviewGate] Failed to dispatch #{agent_name}: #{e.message}" if defined?(LOG)
|
|
@@ -200,6 +209,9 @@ module Brainiac
|
|
|
200
209
|
task["gates_dispatched_at"] = Time.now.iso8601
|
|
201
210
|
task["gate_approvals"] ||= []
|
|
202
211
|
|
|
212
|
+
# Track the SHA we're reviewing for incremental diff on next round
|
|
213
|
+
task["last_reviewed_sha"] = current_sha if current_sha
|
|
214
|
+
|
|
203
215
|
dispatched
|
|
204
216
|
end
|
|
205
217
|
|
|
@@ -218,8 +230,9 @@ module Brainiac
|
|
|
218
230
|
# @param pr_number [Integer, String] PR number
|
|
219
231
|
# @param repo_name [String] e.g. "stowzilla/brainiac-basecamp"
|
|
220
232
|
# @param repo_path [String] Local repo path
|
|
233
|
+
# @param is_rereview [Boolean] True if this is a re-review after implementation fixes
|
|
221
234
|
# @return [Array<String>] Agent names dispatched
|
|
222
|
-
def dispatch_missing_gates(epic:, task:, pr_number:, repo_name:, repo_path:)
|
|
235
|
+
def dispatch_missing_gates(epic:, task:, pr_number:, repo_name:, repo_path:, is_rereview: false)
|
|
223
236
|
responded_agents = Set.new
|
|
224
237
|
(task["gate_approvals"] || []).each { |a| responded_agents << a["agent"].downcase }
|
|
225
238
|
(task["changes_requested_by"] || []).each { |a| responded_agents << a.downcase }
|
|
@@ -230,6 +243,10 @@ module Brainiac
|
|
|
230
243
|
# Track per-gate re-dispatch counts to prevent infinite loops
|
|
231
244
|
task["gate_redispatch_counts"] ||= {}
|
|
232
245
|
|
|
246
|
+
# For re-reviews, determine incremental diff range
|
|
247
|
+
diff_from_sha = is_rereview && task["last_reviewed_sha"] ? task["last_reviewed_sha"] : nil
|
|
248
|
+
current_sha = get_pr_head_sha(pr_number: pr_number, repo_path: repo_path)
|
|
249
|
+
|
|
233
250
|
dispatched = []
|
|
234
251
|
|
|
235
252
|
missing_gates.each do |gate|
|
|
@@ -243,7 +260,7 @@ module Brainiac
|
|
|
243
260
|
next
|
|
244
261
|
end
|
|
245
262
|
|
|
246
|
-
LOG.info "[Basecamp:ReviewGate] Re-dispatching missing gate #{agent_name} (#{role}) to review PR ##{pr_number} (retry #{retries + 1}/#{MAX_GATE_REDISPATCH_RETRIES})" if defined?(LOG)
|
|
263
|
+
LOG.info "[Basecamp:ReviewGate] Re-dispatching missing gate #{agent_name} (#{role}) to review PR ##{pr_number} (retry #{retries + 1}/#{MAX_GATE_REDISPATCH_RETRIES})#{' (incremental)' if diff_from_sha}" if defined?(LOG)
|
|
247
264
|
|
|
248
265
|
task["gate_redispatch_counts"][agent_name.downcase] = retries + 1
|
|
249
266
|
|
|
@@ -255,7 +272,8 @@ module Brainiac
|
|
|
255
272
|
repo_name: repo_name,
|
|
256
273
|
repo_path: repo_path,
|
|
257
274
|
card_number: task["fizzy_card"],
|
|
258
|
-
epic: epic
|
|
275
|
+
epic: epic,
|
|
276
|
+
diff_from_sha: diff_from_sha
|
|
259
277
|
)
|
|
260
278
|
rescue StandardError => e
|
|
261
279
|
LOG.error "[Basecamp:ReviewGate] Failed to dispatch #{agent_name}: #{e.message}" if defined?(LOG)
|
|
@@ -268,6 +286,9 @@ module Brainiac
|
|
|
268
286
|
# but do NOT overwrite gates_dispatched_at — that's the original timestamp
|
|
269
287
|
task["last_redispatch_at"] = Time.now.iso8601
|
|
270
288
|
|
|
289
|
+
# Update last_reviewed_sha for next round
|
|
290
|
+
task["last_reviewed_sha"] = current_sha if current_sha
|
|
291
|
+
|
|
271
292
|
dispatched
|
|
272
293
|
end
|
|
273
294
|
|
|
@@ -300,9 +321,33 @@ module Brainiac
|
|
|
300
321
|
|
|
301
322
|
private
|
|
302
323
|
|
|
324
|
+
# Get the HEAD SHA of a PR (for tracking incremental reviews).
|
|
325
|
+
#
|
|
326
|
+
# @param pr_number [Integer, String] PR number
|
|
327
|
+
# @param repo_path [String] Local repo path
|
|
328
|
+
# @return [String, nil] Commit SHA or nil if failed
|
|
329
|
+
def get_pr_head_sha(pr_number:, repo_path:)
|
|
330
|
+
stdout, _, status = Open3.capture3(
|
|
331
|
+
"gh", "pr", "view", pr_number.to_s,
|
|
332
|
+
"--json", "headRefOid",
|
|
333
|
+
"--jq", ".headRefOid",
|
|
334
|
+
chdir: repo_path
|
|
335
|
+
)
|
|
336
|
+
return nil unless status.success?
|
|
337
|
+
|
|
338
|
+
sha = stdout.strip
|
|
339
|
+
sha.empty? ? nil : sha
|
|
340
|
+
rescue StandardError => e
|
|
341
|
+
LOG.warn "[Basecamp:ReviewGate] Failed to get PR head SHA: #{e.message}" if defined?(LOG)
|
|
342
|
+
nil
|
|
343
|
+
end
|
|
344
|
+
|
|
303
345
|
# Dispatch a single gate agent to review a PR.
|
|
304
346
|
# This creates a review prompt and runs the agent in the repo directory.
|
|
305
|
-
|
|
347
|
+
#
|
|
348
|
+
# @param diff_from_sha [String, nil] If present, this is a re-review and agent should
|
|
349
|
+
# focus on changes since this SHA (incremental review)
|
|
350
|
+
def dispatch_agent_for_review(agent_name:, role:, pr_number:, repo_name:, repo_path:, card_number:, epic:, diff_from_sha: nil)
|
|
306
351
|
# Build a review-specific prompt for the gate agent
|
|
307
352
|
prompt = build_gate_review_prompt(
|
|
308
353
|
agent_name: agent_name,
|
|
@@ -310,7 +355,8 @@ module Brainiac
|
|
|
310
355
|
pr_number: pr_number,
|
|
311
356
|
repo_name: repo_name,
|
|
312
357
|
card_number: card_number,
|
|
313
|
-
epic_title: epic["title"]
|
|
358
|
+
epic_title: epic["title"],
|
|
359
|
+
diff_from_sha: diff_from_sha
|
|
314
360
|
)
|
|
315
361
|
|
|
316
362
|
# Resolve the agent's GitHub token for their bot identity
|
|
@@ -359,29 +405,80 @@ module Brainiac
|
|
|
359
405
|
end
|
|
360
406
|
|
|
361
407
|
# Build the prompt for a gate review agent.
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
408
|
+
#
|
|
409
|
+
# @param diff_from_sha [String, nil] If present, this is an incremental re-review
|
|
410
|
+
def build_gate_review_prompt(agent_name:, role:, pr_number:, repo_name:, card_number:, epic_title:, diff_from_sha: nil)
|
|
411
|
+
if diff_from_sha
|
|
412
|
+
# Incremental re-review prompt — focus only on new changes
|
|
413
|
+
<<~PROMPT
|
|
414
|
+
You are RE-REVIEWING PR ##{pr_number} on #{repo_name} as part of epic: "#{epic_title}".
|
|
415
|
+
Your role: **#{role}**
|
|
416
|
+
|
|
417
|
+
This is an INCREMENTAL REVIEW — you already reviewed this PR and requested changes.
|
|
418
|
+
The implementation agent has pushed fixes. Focus on what changed.
|
|
419
|
+
|
|
420
|
+
**Step 1: Read the context to understand WHY changes were made:**
|
|
421
|
+
```
|
|
422
|
+
gh pr view #{pr_number}
|
|
423
|
+
gh pr view #{pr_number} --comments
|
|
424
|
+
git log #{diff_from_sha}..HEAD --oneline
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
- `gh pr view` shows the PR description (implementation rationale)
|
|
428
|
+
- `--comments` shows discussion and review responses
|
|
429
|
+
- `git log` shows commit messages explaining each fix
|
|
430
|
+
|
|
431
|
+
Read these BEFORE looking at the code changes.
|
|
432
|
+
|
|
433
|
+
**Step 2: View the NEW changes since your last review:**
|
|
434
|
+
```
|
|
435
|
+
git fetch origin && git diff #{diff_from_sha}..HEAD
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Or compare in GitHub: `#{repo_name}/compare/#{diff_from_sha[0..7]}...HEAD`
|
|
439
|
+
|
|
440
|
+
If you need the full context, you can still use `gh pr diff #{pr_number}`, but prioritize
|
|
441
|
+
reviewing the incremental changes first.
|
|
442
|
+
|
|
443
|
+
Based on your role (#{role}):
|
|
444
|
+
#{role_instructions(role)}
|
|
445
|
+
|
|
446
|
+
After your review:
|
|
447
|
+
- If the fixes address your concerns: `gh pr review #{pr_number} --approve --body "your summary"`
|
|
448
|
+
- If more changes are needed: `gh pr review #{pr_number} --request-changes --body "what still needs fixing"`
|
|
449
|
+
|
|
450
|
+
Be thorough but pragmatic. This is Fizzy card ##{card_number}.
|
|
451
|
+
|
|
452
|
+
IMPORTANT RESTRICTIONS:
|
|
453
|
+
- Do NOT open new PRs or modify code — you are a reviewer only
|
|
454
|
+
- Do NOT comment on the Fizzy card — your review goes on GitHub only
|
|
455
|
+
- Do NOT use the fizzy CLI at all
|
|
456
|
+
PROMPT
|
|
457
|
+
else
|
|
458
|
+
# Initial full review prompt
|
|
459
|
+
<<~PROMPT
|
|
460
|
+
You are reviewing PR ##{pr_number} on #{repo_name} as part of epic: "#{epic_title}".
|
|
461
|
+
Your role: **#{role}**
|
|
366
462
|
|
|
367
|
-
|
|
463
|
+
This is a review gate — the epic cannot proceed until you approve.
|
|
368
464
|
|
|
369
|
-
|
|
465
|
+
Review the PR changes with `gh pr diff #{pr_number}` and `gh pr view #{pr_number}`.
|
|
370
466
|
|
|
371
|
-
|
|
372
|
-
|
|
467
|
+
Based on your role (#{role}):
|
|
468
|
+
#{role_instructions(role)}
|
|
373
469
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
470
|
+
After your review:
|
|
471
|
+
- If the code meets your standards: `gh pr review #{pr_number} --approve --body "your summary"`
|
|
472
|
+
- If changes are needed: `gh pr review #{pr_number} --request-changes --body "what needs fixing"`
|
|
377
473
|
|
|
378
|
-
|
|
474
|
+
Be thorough but pragmatic. This is Fizzy card ##{card_number}.
|
|
379
475
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
476
|
+
IMPORTANT RESTRICTIONS:
|
|
477
|
+
- Do NOT open new PRs or modify code — you are a reviewer only
|
|
478
|
+
- Do NOT comment on the Fizzy card — your review goes on GitHub only
|
|
479
|
+
- Do NOT use the fizzy CLI at all
|
|
480
|
+
PROMPT
|
|
481
|
+
end
|
|
385
482
|
end
|
|
386
483
|
|
|
387
484
|
# Role-specific review instructions.
|
|
@@ -7,6 +7,7 @@ require_relative "basecamp/config"
|
|
|
7
7
|
require_relative "basecamp/client"
|
|
8
8
|
require_relative "basecamp/epic"
|
|
9
9
|
require_relative "basecamp/epic_branch"
|
|
10
|
+
require_relative "basecamp/epic_memory"
|
|
10
11
|
require_relative "basecamp/review_gate"
|
|
11
12
|
require_relative "basecamp/orchestrator"
|
|
12
13
|
require_relative "basecamp/webhook"
|
|
@@ -206,7 +207,8 @@ module Brainiac
|
|
|
206
207
|
task: task,
|
|
207
208
|
pr_number: effective_pr,
|
|
208
209
|
repo_name: github_repo,
|
|
209
|
-
repo_path: repo_path
|
|
210
|
+
repo_path: repo_path,
|
|
211
|
+
is_rereview: true
|
|
210
212
|
)
|
|
211
213
|
rescue StandardError => e
|
|
212
214
|
LOG.error "[Basecamp:HealthCheck] Gate re-dispatch failed for ##{card_number}: #{e.message}" if defined?(LOG)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: brainiac-basecamp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.16
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Davis
|
|
@@ -93,6 +93,7 @@ files:
|
|
|
93
93
|
- lib/brainiac/plugins/basecamp/config.rb
|
|
94
94
|
- lib/brainiac/plugins/basecamp/epic.rb
|
|
95
95
|
- lib/brainiac/plugins/basecamp/epic_branch.rb
|
|
96
|
+
- lib/brainiac/plugins/basecamp/epic_memory.rb
|
|
96
97
|
- lib/brainiac/plugins/basecamp/hooks.rb
|
|
97
98
|
- lib/brainiac/plugins/basecamp/metadata.rb
|
|
98
99
|
- lib/brainiac/plugins/basecamp/orchestrator.rb
|