brainiac-basecamp 0.0.14 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 14642b9582bda42dff7932fe0b518957bdeb638431de432df6beae906c3206e5
4
- data.tar.gz: 2fac38d27c6c89445d49af1b8e3148bcfa25d8d6632de11984fca1dbd772dcf2
3
+ metadata.gz: 4d77f4d0787e023edeec79754c9958f4ee09f036250038ef7cc4ae1dc9961a20
4
+ data.tar.gz: 74d8aba9539c7526eed62e209b8a84cabe9ecf26482f8873f8c1581a2a70a86a
5
5
  SHA512:
6
- metadata.gz: f1c4b21c180b3a8306770e922c8c4c8bdb65976a1a62f9dbe02541cb4049d635ac4745f469275ad13bd1168811148cb772965e0b64e4d5b0fdac9b0eb4b7c200
7
- data.tar.gz: 7a063d983c097531096ff5d8f954050842f0b775f136f6ec1694111ab0bd9561f061b859e33a9497a7b69b8ed7e3a1aa0725d7c70895556e83789920fedc1f72
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
@@ -16,7 +16,7 @@ module Brainiac
16
16
  # :resolve_base_branch — returns epic branch as worktree base
17
17
  # :resolve_pr_target — returns epic branch as PR target
18
18
  module Hooks
19
- class << self
19
+ class << self # rubocop:disable Metrics/ClassLength
20
20
  def register_all!
21
21
  register_agent_completed
22
22
  register_pr_merged
@@ -380,14 +380,39 @@ module Brainiac
380
380
  next unless epic["review_gate"] == "epic_branch" && ReviewGate.enabled?
381
381
 
382
382
  task = epic["tasks"].find { |t| t["fizzy_card"] == card_number.to_i }
383
- # Re-trigger gates if task is in_flight (agent working/pushed fixes)
384
- # No need to check for prior reviews — if task is in_flight with a PR, gates should review
385
- next unless task && task["status"] == "in_flight" && task["pr_number"]
383
+ next unless task && task["status"] == "in_flight"
384
+
385
+ # Ensure pr_number is set it may be missing if initial gate dispatch
386
+ # failed to persist it or if state was lost during serialization.
387
+ unless task["pr_number"]
388
+ pr_number = ctx.dig(:pull_request, "number") || ctx[:pr_number]
389
+ if pr_number
390
+ task["pr_number"] = pr_number.to_i
391
+ LOG.info "[Basecamp:Hooks] Backfilled pr_number=#{pr_number} for card ##{card_number}" if defined?(LOG)
392
+ else
393
+ # Try to find it from the branch
394
+ project_key = task["project"] || Config.brainiac_project_for(epic["basecamp_project_id"])
395
+ projects_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "projects.json")
396
+ projects = File.exist?(projects_file) ? JSON.parse(File.read(projects_file)) : {}
397
+ repo_path = projects.dig(project_key, "repo_path")
398
+ if repo_path
399
+ branch = ctx[:branch] || "fizzy-#{card_number}-*"
400
+ found_pr = find_pr_number(repo_path: repo_path, branch: branch)
401
+ if found_pr
402
+ task["pr_number"] = found_pr
403
+ LOG.info "[Basecamp:Hooks] Resolved pr_number=#{found_pr} for card ##{card_number} via branch lookup" if defined?(LOG)
404
+ end
405
+ end
406
+ end
407
+ end
408
+
409
+ next unless task["pr_number"]
386
410
 
387
411
  LOG.info "[Basecamp:Hooks] PR updated for card ##{card_number} — re-triggering review gates" if defined?(LOG)
388
412
 
389
413
  # Reset approvals and re-dispatch gates
390
414
  ReviewGate.reset_approvals(task)
415
+ task["changes_requested_by"] = []
391
416
  task["status"] = "in_review"
392
417
  epic["updated_at"] = Time.now.iso8601
393
418
  save_epic_state(epic)
@@ -421,6 +446,23 @@ module Brainiac
421
446
  ""
422
447
  ]
423
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
+
424
466
  # List completed tasks with their memory files for reference
425
467
  completed_tasks = tasks.select { |t| t["status"] == "complete" }
426
468
  if completed_tasks.any?
@@ -821,6 +863,21 @@ module Brainiac
821
863
  return
822
864
  end
823
865
 
866
+ # Ensure pr_number is set — look it up if missing.
867
+ # This is critical for the pr_synchronized hook to re-trigger gates after fixes.
868
+ unless pr_number
869
+ found_pr = find_pr_number(repo_path: repo_path, branch: "fizzy-#{card_number}-*")
870
+ if found_pr
871
+ pr_number = found_pr
872
+ task["pr_number"] = pr_number
873
+ task["pr_repo"] = project_config["github_repo"]
874
+ save_epic_state(epic)
875
+ LOG.info "[Basecamp:Hooks] Backfilled pr_number=#{pr_number} for card ##{card_number} during direct dispatch" if defined?(LOG)
876
+ elsif defined?(LOG)
877
+ LOG.warn "[Basecamp:Hooks] Could not find PR for card ##{card_number} — gates won't re-trigger after fixes"
878
+ end
879
+ end
880
+
824
881
  # Find the worktree for this card
825
882
  work_items_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "work_items.json")
826
883
  worktree_path = repo_path # Default to main repo
@@ -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
- After reviewing, post a brief summary comment on the Basecamp todolist:
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 it concise — this is a checkpoint, not a full analysis.
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
- def dispatch_agent_for_review(agent_name:, role:, pr_number:, repo_name:, repo_path:, card_number:, epic:)
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
- def build_gate_review_prompt(agent_name:, role:, pr_number:, repo_name:, card_number:, epic_title:)
363
- <<~PROMPT
364
- You are reviewing PR ##{pr_number} on #{repo_name} as part of epic: "#{epic_title}".
365
- Your role: **#{role}**
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
- This is a review gate — the epic cannot proceed until you approve.
463
+ This is a review gate — the epic cannot proceed until you approve.
368
464
 
369
- Review the PR changes with `gh pr diff #{pr_number}` and `gh pr view #{pr_number}`.
465
+ Review the PR changes with `gh pr diff #{pr_number}` and `gh pr view #{pr_number}`.
370
466
 
371
- Based on your role (#{role}):
372
- #{role_instructions(role)}
467
+ Based on your role (#{role}):
468
+ #{role_instructions(role)}
373
469
 
374
- After your review:
375
- - If the code meets your standards: `gh pr review #{pr_number} --approve --body "your summary"`
376
- - If changes are needed: `gh pr review #{pr_number} --request-changes --body "what needs fixing"`
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
- Be thorough but pragmatic. This is Fizzy card ##{card_number}.
474
+ Be thorough but pragmatic. This is Fizzy card ##{card_number}.
379
475
 
380
- IMPORTANT RESTRICTIONS:
381
- - Do NOT open new PRs or modify code — you are a reviewer only
382
- - Do NOT comment on the Fizzy card — your review goes on GitHub only
383
- - Do NOT use the fizzy CLI at all
384
- PROMPT
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.
@@ -3,7 +3,7 @@
3
3
  module Brainiac
4
4
  module Plugins
5
5
  module Basecamp
6
- VERSION = "0.0.14"
6
+ VERSION = "0.0.16"
7
7
  end
8
8
  end
9
9
  end
@@ -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.14
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