brainiac-basecamp 0.0.16 → 0.0.18
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/cli.rb +534 -10
- data/lib/brainiac/plugins/basecamp/comment_responder.rb +380 -0
- data/lib/brainiac/plugins/basecamp/epic_memory.rb +10 -0
- data/lib/brainiac/plugins/basecamp/hooks.rb +94 -270
- data/lib/brainiac/plugins/basecamp/orchestrator.rb +65 -187
- data/lib/brainiac/plugins/basecamp/review_gate.rb +165 -218
- data/lib/brainiac/plugins/basecamp/session_registry.rb +361 -0
- data/lib/brainiac/plugins/basecamp/task_state.rb +86 -0
- data/lib/brainiac/plugins/basecamp/version.rb +1 -1
- data/lib/brainiac/plugins/basecamp/webhook.rb +4 -6
- data/lib/brainiac/plugins/basecamp.rb +128 -492
- metadata +4 -1
|
@@ -49,9 +49,6 @@ 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
|
-
|
|
55
52
|
# FIRST: Populate tasks with project info from Fizzy card tags
|
|
56
53
|
# This must happen BEFORE creating epic branches so we know which repos are involved
|
|
57
54
|
populate_tasks(epic)
|
|
@@ -123,7 +120,7 @@ module Brainiac
|
|
|
123
120
|
|
|
124
121
|
# Mark the task as complete in our state
|
|
125
122
|
if task
|
|
126
|
-
task
|
|
123
|
+
TaskState.transition!(task, :complete, triggered_by: "fizzy_card_completed")
|
|
127
124
|
task["completed_at"] = Time.now.iso8601
|
|
128
125
|
epic["updated_at"] = Time.now.iso8601
|
|
129
126
|
log_event(epic, "task_completed", "Card ##{card_number} completed")
|
|
@@ -192,8 +189,6 @@ module Brainiac
|
|
|
192
189
|
load_epics.find { |e| e["id"] == epic_id }
|
|
193
190
|
end
|
|
194
191
|
|
|
195
|
-
WEBHOOK_CHECK_COOLDOWN = 300 # 5 minutes
|
|
196
|
-
|
|
197
192
|
private
|
|
198
193
|
|
|
199
194
|
# Resolve current todolist state from Basecamp and dispatch unblocked cards.
|
|
@@ -221,7 +216,7 @@ module Brainiac
|
|
|
221
216
|
updated_tasks = new_tasks.map do |task|
|
|
222
217
|
existing = existing_by_card[task.fizzy_card]
|
|
223
218
|
status = if task.completed
|
|
224
|
-
"complete"
|
|
219
|
+
existing&.dig("status") || "complete"
|
|
225
220
|
elsif existing&.dig("status")
|
|
226
221
|
# Preserve existing status (in_flight, in_review, final_decision, etc.)
|
|
227
222
|
existing["status"]
|
|
@@ -249,12 +244,18 @@ module Brainiac
|
|
|
249
244
|
# Preserve PR and gate state from existing task
|
|
250
245
|
if existing
|
|
251
246
|
%w[dispatched_at pr_number pr_repo gates_dispatched_at gate_approvals
|
|
252
|
-
changes_requested_by
|
|
253
|
-
|
|
247
|
+
changes_requested_by gate_redispatch_counts gate_states
|
|
248
|
+
awaiting_final_decision changes_debounce_started
|
|
249
|
+
fizzy_internal_id transitions].each do |key|
|
|
254
250
|
new_task[key] = existing[key] if existing.key?(key)
|
|
255
251
|
end
|
|
256
252
|
end
|
|
257
253
|
|
|
254
|
+
TaskState.migrate!(new_task, triggered_by: "basecamp_sync")
|
|
255
|
+
if task.completed && !TaskState.in?(new_task, :complete)
|
|
256
|
+
TaskState.transition!(new_task, :complete, triggered_by: "basecamp_todo_completed")
|
|
257
|
+
end
|
|
258
|
+
|
|
258
259
|
new_task
|
|
259
260
|
end
|
|
260
261
|
|
|
@@ -268,28 +269,42 @@ module Brainiac
|
|
|
268
269
|
epic["updated_at"] = Time.now.iso8601
|
|
269
270
|
end
|
|
270
271
|
|
|
271
|
-
# Dispatch unblocked tasks that
|
|
272
|
-
# Ensures Fizzy webhooks are active before dispatching (prevents silent failures).
|
|
272
|
+
# Dispatch unblocked tasks that do not have a live implementation session.
|
|
273
273
|
def dispatch_unblocked_tasks(epic)
|
|
274
|
-
ensure_fizzy_webhooks_active
|
|
275
274
|
tasks = epic["tasks"].map do |t|
|
|
275
|
+
# A stale in_flight state is diagnostic state, not liveness evidence.
|
|
276
|
+
# It becomes dispatchable again only after the implementation session has
|
|
277
|
+
# died; in_review/final_decision remain intentionally non-dispatchable.
|
|
278
|
+
# Give a newly assigned implementation agent time to register its
|
|
279
|
+
# session. Without this grace period, a recovery pass that just
|
|
280
|
+
# re-assigned a stale card can immediately assign it a second time
|
|
281
|
+
# before Fizzy's normal spawn hook has run.
|
|
282
|
+
dispatch_status = if TaskState.in?(t, :in_flight) &&
|
|
283
|
+
!SessionRegistry.implementation_alive?(t["fizzy_card"]) &&
|
|
284
|
+
Basecamp.send(:stale_dispatch?, t)
|
|
285
|
+
:pending
|
|
286
|
+
else
|
|
287
|
+
t["status"].to_sym
|
|
288
|
+
end
|
|
276
289
|
Epic::Task.new(
|
|
277
290
|
todo_id: t["todo_id"],
|
|
278
291
|
fizzy_card: t["fizzy_card"],
|
|
279
292
|
title: t["title"],
|
|
280
293
|
depends_on: t["depends_on"] || [],
|
|
281
|
-
status:
|
|
294
|
+
status: dispatch_status,
|
|
282
295
|
completed: t["status"] == "complete",
|
|
283
296
|
project: t["project"]
|
|
284
297
|
)
|
|
285
298
|
end
|
|
286
299
|
|
|
287
|
-
#
|
|
300
|
+
# The registry, rather than Fizzy assignment or a persisted status field,
|
|
301
|
+
# is the authority for whether an implementation dispatch is already live.
|
|
288
302
|
unblocked = Epic.unblocked_tasks(tasks)
|
|
289
|
-
in_flight_cards = epic["tasks"].select { |t| t["status"] == "in_flight" }.map { |t| t["fizzy_card"] }
|
|
290
303
|
complete_cards = epic["tasks"].select { |t| t["status"] == "complete" }.map { |t| t["fizzy_card"] }
|
|
291
304
|
|
|
292
|
-
ready_to_dispatch = unblocked.reject
|
|
305
|
+
ready_to_dispatch = unblocked.reject do |task|
|
|
306
|
+
complete_cards.include?(task.fizzy_card) || SessionRegistry.implementation_alive?(task.fizzy_card)
|
|
307
|
+
end
|
|
293
308
|
|
|
294
309
|
ready_to_dispatch.each do |task|
|
|
295
310
|
dispatch_card(epic, task)
|
|
@@ -307,6 +322,11 @@ module Brainiac
|
|
|
307
322
|
def dispatch_card(epic, task)
|
|
308
323
|
card_number = task.fizzy_card
|
|
309
324
|
|
|
325
|
+
if SessionRegistry.implementation_alive?(card_number)
|
|
326
|
+
LOG.info "[Basecamp:Orchestrator] Skipping Fizzy card ##{card_number} — implementation session is still live" if defined?(LOG)
|
|
327
|
+
return false
|
|
328
|
+
end
|
|
329
|
+
|
|
310
330
|
# Resolve agent from project config — each project can have its own default agent
|
|
311
331
|
project_key = task.is_a?(Hash) ? task["project"] : task.project
|
|
312
332
|
agent = resolve_agent_for_project(project_key) || epic["agent"]
|
|
@@ -316,16 +336,14 @@ module Brainiac
|
|
|
316
336
|
# Mark as in-flight in our state
|
|
317
337
|
epic_task = epic["tasks"].find { |t| t["fizzy_card"] == card_number }
|
|
318
338
|
if epic_task
|
|
319
|
-
epic_task
|
|
339
|
+
TaskState.transition!(epic_task, :dispatch, triggered_by: "orchestrator_dispatch")
|
|
320
340
|
epic_task["dispatched_at"] = Time.now.iso8601
|
|
321
341
|
end
|
|
322
342
|
epic["updated_at"] = Time.now.iso8601
|
|
323
343
|
|
|
324
344
|
log_event(epic, "dispatched", "Card ##{card_number} dispatched to #{agent}")
|
|
325
345
|
|
|
326
|
-
# Assign the card in Fizzy via CLI — this triggers the normal Fizzy webhook flow
|
|
327
|
-
# If the agent is already assigned (stale state from a disabled webhook),
|
|
328
|
-
# the method will cycle the assignment to re-trigger the webhook.
|
|
346
|
+
# Assign the card in Fizzy via CLI — this triggers the normal Fizzy webhook flow
|
|
329
347
|
assign_fizzy_card(card_number, agent)
|
|
330
348
|
|
|
331
349
|
# Post a comment on the Basecamp todo
|
|
@@ -337,6 +355,8 @@ module Brainiac
|
|
|
337
355
|
profile: agent.downcase
|
|
338
356
|
)
|
|
339
357
|
end
|
|
358
|
+
|
|
359
|
+
true
|
|
340
360
|
end
|
|
341
361
|
|
|
342
362
|
# Sync PR state from work_items for cards that already have open PRs.
|
|
@@ -366,7 +386,7 @@ module Brainiac
|
|
|
366
386
|
|
|
367
387
|
task["pr_number"] = pr_number
|
|
368
388
|
task["pr_repo"] = work_item.dig("sources", "github", "repo")
|
|
369
|
-
task
|
|
389
|
+
TaskState.transition!(task, :submit_for_review, triggered_by: "existing_pr_sync")
|
|
370
390
|
end
|
|
371
391
|
rescue StandardError => e
|
|
372
392
|
LOG.warn "[Basecamp:Orchestrator] Error syncing PR state: #{e.message}" if defined?(LOG)
|
|
@@ -387,9 +407,6 @@ module Brainiac
|
|
|
387
407
|
pr_number = task["pr_number"]
|
|
388
408
|
project_key = task["project"]
|
|
389
409
|
|
|
390
|
-
# Skip if gates already dispatched
|
|
391
|
-
next if task["gates_dispatched_at"]
|
|
392
|
-
|
|
393
410
|
# Get repo info
|
|
394
411
|
projects_file = File.join(BRAINIAC_DIR, "projects.json")
|
|
395
412
|
projects = File.exist?(projects_file) ? JSON.parse(File.read(projects_file)) : {}
|
|
@@ -405,7 +422,7 @@ module Brainiac
|
|
|
405
422
|
# Check if all gates already passed
|
|
406
423
|
if ReviewGate.all_gates_passed?(task)
|
|
407
424
|
LOG.info "[Basecamp:Orchestrator] Gates already passed for card ##{card_number} — dispatching final decision" if defined?(LOG)
|
|
408
|
-
task
|
|
425
|
+
TaskState.transition!(task, :approve, triggered_by: "existing_pr_gate_sync", guard: ReviewGate.all_gates_passed?(task))
|
|
409
426
|
task["awaiting_final_decision"] = true
|
|
410
427
|
# Final decision dispatch happens in resume logic
|
|
411
428
|
else
|
|
@@ -425,10 +442,7 @@ module Brainiac
|
|
|
425
442
|
end
|
|
426
443
|
|
|
427
444
|
# Assign a Fizzy card to an agent via Fizzy CLI.
|
|
428
|
-
# If the agent is already assigned,
|
|
429
|
-
# (Fizzy webhooks can silently become disabled, causing the original assignment to be lost).
|
|
430
|
-
#
|
|
431
|
-
# @return [Symbol] :assigned, :reassigned, or :failed
|
|
445
|
+
# If the agent is already assigned, skip — the webhook should have already fired.
|
|
432
446
|
def assign_fizzy_card(card_number, agent)
|
|
433
447
|
agent_config = load_agent_registry[agent.downcase]
|
|
434
448
|
fizzy_name = agent_config&.dig("fizzy_name") || agent
|
|
@@ -437,34 +451,18 @@ module Brainiac
|
|
|
437
451
|
fizzy_user_id = resolve_fizzy_user_id(fizzy_name)
|
|
438
452
|
unless fizzy_user_id
|
|
439
453
|
LOG.error "[Basecamp:Orchestrator] Could not resolve Fizzy user ID for '#{fizzy_name}'" if defined?(LOG)
|
|
440
|
-
return
|
|
454
|
+
return
|
|
441
455
|
end
|
|
442
456
|
|
|
443
|
-
# Check if agent is already assigned
|
|
457
|
+
# Check if agent is already assigned — if so, skip (webhook should have fired)
|
|
444
458
|
stdout, _, status = Open3.capture3("fizzy", "card", "show", card_number.to_s, "--json")
|
|
445
459
|
if status.success?
|
|
446
460
|
card_data = JSON.parse(stdout).dig("data") rescue nil
|
|
447
461
|
if card_data
|
|
448
462
|
current_assignees = (card_data["assignees"] || []).map { |a| a["id"] }
|
|
449
463
|
if current_assignees.include?(fizzy_user_id)
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
LOG.warn "[Basecamp:Orchestrator] Agent already assigned to ##{card_number} — " \
|
|
453
|
-
"cycling assignment to re-trigger webhook" if defined?(LOG)
|
|
454
|
-
|
|
455
|
-
# Fizzy assign TOGGLES, so calling it removes the assignment
|
|
456
|
-
Open3.capture3("fizzy", "card", "assign", card_number.to_s, "--user", fizzy_user_id)
|
|
457
|
-
sleep 1 # Brief pause to ensure the unassign is processed
|
|
458
|
-
|
|
459
|
-
# Now reassign — this should trigger the webhook
|
|
460
|
-
stdout2, stderr2, status2 = Open3.capture3("fizzy", "card", "assign", card_number.to_s, "--user", fizzy_user_id)
|
|
461
|
-
if status2.success?
|
|
462
|
-
LOG.info "[Basecamp:Orchestrator] Reassigned Fizzy ##{card_number} to #{fizzy_name} (webhook re-triggered)" if defined?(LOG)
|
|
463
|
-
return :reassigned
|
|
464
|
-
else
|
|
465
|
-
LOG.error "[Basecamp:Orchestrator] Failed to reassign Fizzy ##{card_number}: #{stderr2.strip}" if defined?(LOG)
|
|
466
|
-
return :failed
|
|
467
|
-
end
|
|
464
|
+
LOG.info "[Basecamp:Orchestrator] Agent already assigned to ##{card_number}, skipping (webhook should have fired)" if defined?(LOG)
|
|
465
|
+
return
|
|
468
466
|
end
|
|
469
467
|
end
|
|
470
468
|
end
|
|
@@ -473,77 +471,11 @@ module Brainiac
|
|
|
473
471
|
|
|
474
472
|
if status.success?
|
|
475
473
|
LOG.info "[Basecamp:Orchestrator] Assigned Fizzy ##{card_number} to #{fizzy_name} (#{fizzy_user_id})" if defined?(LOG)
|
|
476
|
-
:assigned
|
|
477
474
|
else
|
|
478
475
|
LOG.error "[Basecamp:Orchestrator] Failed to assign Fizzy ##{card_number}: #{stderr.strip}" if defined?(LOG)
|
|
479
|
-
:failed
|
|
480
476
|
end
|
|
481
477
|
rescue Errno::ENOENT => e
|
|
482
478
|
LOG.error "[Basecamp:Orchestrator] fizzy CLI not found: #{e.message}" if defined?(LOG)
|
|
483
|
-
:failed
|
|
484
|
-
end
|
|
485
|
-
|
|
486
|
-
# Ensure Fizzy webhooks are active before dispatching cards.
|
|
487
|
-
# Fizzy webhooks can silently become disabled (e.g., after repeated delivery failures),
|
|
488
|
-
# causing assigned cards to never trigger agent work. This pre-flight check reactivates
|
|
489
|
-
# any disabled brainiac webhooks.
|
|
490
|
-
#
|
|
491
|
-
# Uses a cooldown to avoid hammering the API on every dispatch batch.
|
|
492
|
-
def ensure_fizzy_webhooks_active
|
|
493
|
-
now = Time.now
|
|
494
|
-
@last_webhook_check ||= Time.at(0)
|
|
495
|
-
return if (now - @last_webhook_check) < WEBHOOK_CHECK_COOLDOWN
|
|
496
|
-
|
|
497
|
-
@last_webhook_check = now
|
|
498
|
-
|
|
499
|
-
# Read fizzy config to get admin_token and board list
|
|
500
|
-
fizzy_config_file = File.join(BRAINIAC_DIR, "fizzy.json")
|
|
501
|
-
return unless File.exist?(fizzy_config_file)
|
|
502
|
-
|
|
503
|
-
fizzy_config = JSON.parse(File.read(fizzy_config_file))
|
|
504
|
-
admin_token = fizzy_config["admin_token"]
|
|
505
|
-
boards = fizzy_config["boards"] || {}
|
|
506
|
-
|
|
507
|
-
unless admin_token
|
|
508
|
-
LOG.debug "[Basecamp:Orchestrator] Fizzy webhook check skipped (no admin_token in fizzy.json)" if defined?(LOG)
|
|
509
|
-
return
|
|
510
|
-
end
|
|
511
|
-
|
|
512
|
-
env = { "FIZZY_TOKEN" => admin_token }
|
|
513
|
-
reactivated = 0
|
|
514
|
-
|
|
515
|
-
boards.each do |board_key, board_config|
|
|
516
|
-
board_id = board_config["board_id"]
|
|
517
|
-
next unless board_id
|
|
518
|
-
|
|
519
|
-
stdout, _, status = Open3.capture3(env, "fizzy", "webhook", "list",
|
|
520
|
-
"--board", board_id, "--all", "--json",
|
|
521
|
-
chdir: Dir.home)
|
|
522
|
-
next unless status.success?
|
|
523
|
-
|
|
524
|
-
webhooks = JSON.parse(stdout)["data"] || []
|
|
525
|
-
webhooks.each do |webhook|
|
|
526
|
-
next unless webhook["name"]&.start_with?("brainiac")
|
|
527
|
-
next if webhook["active"]
|
|
528
|
-
|
|
529
|
-
wh_name = webhook["name"]
|
|
530
|
-
LOG.warn "[Basecamp:Orchestrator] Fizzy webhook '#{wh_name}' on board '#{board_key}' is INACTIVE — reactivating" if defined?(LOG)
|
|
531
|
-
_, _, reactivate_status = Open3.capture3(env, "fizzy", "webhook", "reactivate", webhook["id"],
|
|
532
|
-
"--board", board_id, chdir: Dir.home)
|
|
533
|
-
if reactivate_status.success?
|
|
534
|
-
reactivated += 1
|
|
535
|
-
LOG.info "[Basecamp:Orchestrator] Reactivated webhook '#{webhook['name']}' on board '#{board_key}'" if defined?(LOG)
|
|
536
|
-
else
|
|
537
|
-
LOG.error "[Basecamp:Orchestrator] Failed to reactivate webhook '#{webhook['name']}' on board '#{board_key}'" if defined?(LOG)
|
|
538
|
-
end
|
|
539
|
-
end
|
|
540
|
-
rescue StandardError => e
|
|
541
|
-
LOG.warn "[Basecamp:Orchestrator] Webhook check failed for board '#{board_key}': #{e.message}" if defined?(LOG)
|
|
542
|
-
end
|
|
543
|
-
|
|
544
|
-
LOG.info "[Basecamp:Orchestrator] Fizzy webhook health check: #{reactivated} reactivated" if reactivated.positive?
|
|
545
|
-
rescue StandardError => e
|
|
546
|
-
LOG.warn "[Basecamp:Orchestrator] Fizzy webhook health check failed: #{e.message}" if defined?(LOG)
|
|
547
479
|
end
|
|
548
480
|
|
|
549
481
|
# Resolve a Fizzy user ID from their display name.
|
|
@@ -636,6 +568,9 @@ module Brainiac
|
|
|
636
568
|
|
|
637
569
|
LOG.info "[Basecamp:Orchestrator] Dispatching epic review after card ##{completed_card_number}" if defined?(LOG)
|
|
638
570
|
|
|
571
|
+
# Ensure epic memory exists (in case this epic started before the feature)
|
|
572
|
+
EpicMemory.ensure_exists_for(epic)
|
|
573
|
+
|
|
639
574
|
# Build the review prompt
|
|
640
575
|
completed_tasks = epic["tasks"].select { |t| t["status"] == "complete" }
|
|
641
576
|
completed_summary = completed_tasks.map { |t| "- ##{t['fizzy_card']}: #{t['title']}" }.join("\n")
|
|
@@ -645,18 +580,6 @@ module Brainiac
|
|
|
645
580
|
"- ##{t['fizzy_card']}: #{t['title']}#{dep_str}"
|
|
646
581
|
end.join("\n")
|
|
647
582
|
|
|
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
|
-
|
|
660
583
|
prompt = <<~PROMPT
|
|
661
584
|
## Epic Review: #{epic['title']}
|
|
662
585
|
|
|
@@ -667,7 +590,7 @@ module Brainiac
|
|
|
667
590
|
|
|
668
591
|
### Remaining tasks (with current dependencies):
|
|
669
592
|
#{remaining_summary}
|
|
670
|
-
|
|
593
|
+
|
|
671
594
|
### Your job:
|
|
672
595
|
1. Read the memory files for completed tasks to understand what was implemented
|
|
673
596
|
2. Check if remaining tasks still make sense given the implementation decisions
|
|
@@ -680,20 +603,10 @@ module Brainiac
|
|
|
680
603
|
|
|
681
604
|
Memory files are at: `~/.brainiac/brain/memory/#{agent_name&.downcase}/card-<number>.md`
|
|
682
605
|
|
|
683
|
-
|
|
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:
|
|
606
|
+
After reviewing, post a brief summary comment on the Basecamp todolist:
|
|
694
607
|
`basecamp comments create #{epic['basecamp_todolist_id']} "Epic review after ##{completed_card_number}: <your summary>" --in #{epic['basecamp_project_id']}`
|
|
695
608
|
|
|
696
|
-
Keep
|
|
609
|
+
Keep it concise — this is a checkpoint, not a full analysis.
|
|
697
610
|
PROMPT
|
|
698
611
|
|
|
699
612
|
# Get project config for the agent
|
|
@@ -720,9 +633,15 @@ module Brainiac
|
|
|
720
633
|
card_number: completed_card_number)
|
|
721
634
|
end
|
|
722
635
|
|
|
723
|
-
# Register session for
|
|
724
|
-
if pid
|
|
725
|
-
|
|
636
|
+
# Register session in SessionRegistry for liveness tracking
|
|
637
|
+
if pid
|
|
638
|
+
SessionRegistry.register_session(
|
|
639
|
+
card_key, pid,
|
|
640
|
+
log_file: log_file,
|
|
641
|
+
agent_name: agent_name,
|
|
642
|
+
epic_id: epic["id"],
|
|
643
|
+
card_number: completed_card_number
|
|
644
|
+
)
|
|
726
645
|
end
|
|
727
646
|
|
|
728
647
|
# Wait for the review to complete
|
|
@@ -747,15 +666,6 @@ module Brainiac
|
|
|
747
666
|
profile: epic["agent"]&.downcase)
|
|
748
667
|
end
|
|
749
668
|
|
|
750
|
-
# Mark a Basecamp todo as incomplete (undo premature completion).
|
|
751
|
-
def uncheck_todo(epic, card_number)
|
|
752
|
-
task = epic["tasks"].find { |t| t["fizzy_card"] == card_number.to_i }
|
|
753
|
-
return unless task && task["todo_id"]
|
|
754
|
-
|
|
755
|
-
Client.run_safe("todos", "uncomplete", task["todo_id"].to_s, "--json",
|
|
756
|
-
profile: epic["agent"]&.downcase)
|
|
757
|
-
end
|
|
758
|
-
|
|
759
669
|
# Post a completion comment on the Basecamp todo.
|
|
760
670
|
def post_completion_comment(epic, card_number)
|
|
761
671
|
task = epic["tasks"].find { |t| t["fizzy_card"] == card_number.to_i }
|
|
@@ -784,9 +694,6 @@ module Brainiac
|
|
|
784
694
|
|
|
785
695
|
LOG.info "[Basecamp:Orchestrator] Epic '#{epic['title']}' completed!" if defined?(LOG)
|
|
786
696
|
|
|
787
|
-
# Archive epic memory (preserves it for future reference)
|
|
788
|
-
EpicMemory.cleanup(epic["basecamp_todolist_id"], archive: true)
|
|
789
|
-
|
|
790
697
|
# If epic_branch mode, open final PRs to main
|
|
791
698
|
if epic["review_gate"] == "epic_branch" && epic["epic_branches"]&.any?
|
|
792
699
|
open_final_prs(epic)
|
|
@@ -892,43 +799,14 @@ module Brainiac
|
|
|
892
799
|
end
|
|
893
800
|
|
|
894
801
|
# Create epic branches for all projects involved in this epic.
|
|
895
|
-
# CRITICAL: If this fails, epic_branches will be empty and agents will
|
|
896
|
-
# open PRs against the default branch instead of the epic branch.
|
|
897
|
-
# We log loudly and retry resolution of project repos if initial attempt fails.
|
|
898
802
|
def create_epic_branches_for(epic)
|
|
899
803
|
project_repos = resolve_project_repos(epic)
|
|
900
|
-
|
|
901
|
-
if project_repos.empty?
|
|
902
|
-
# Fallback: try to resolve from the basecamp project mapping directly
|
|
903
|
-
mapped_project = Config.brainiac_project_for(epic["basecamp_project_id"])
|
|
904
|
-
if mapped_project
|
|
905
|
-
projects_file = File.join(BRAINIAC_DIR, "projects.json")
|
|
906
|
-
all_projects = File.exist?(projects_file) ? JSON.parse(File.read(projects_file)) : {}
|
|
907
|
-
repo = all_projects.dig(mapped_project, "repo_path")
|
|
908
|
-
project_repos = { mapped_project => repo } if repo
|
|
909
|
-
end
|
|
910
|
-
end
|
|
911
|
-
|
|
912
|
-
if project_repos.empty?
|
|
913
|
-
LOG.error "[Basecamp:Orchestrator] Cannot create epic branches — no project repos resolved. " \
|
|
914
|
-
"Tasks have projects: #{(epic['tasks'] || []).map { |t| t['project'] }.compact.uniq.inspect}. " \
|
|
915
|
-
"Epic will proceed WITHOUT epic branches — PRs will target the default branch!" if defined?(LOG)
|
|
916
|
-
log_event(epic, "branches_failed", "No project repos resolved — epic branches not created")
|
|
917
|
-
return
|
|
918
|
-
end
|
|
804
|
+
return if project_repos.empty?
|
|
919
805
|
|
|
920
806
|
epic["epic_branches"] = EpicBranch.create_epic_branches(epic, project_repos)
|
|
921
|
-
|
|
922
|
-
if epic["epic_branches"].empty?
|
|
923
|
-
LOG.error "[Basecamp:Orchestrator] create_epic_branches returned empty — branch creation failed" if defined?(LOG)
|
|
924
|
-
log_event(epic, "branches_failed", "Branch creation returned empty for repos: #{project_repos.keys.join(', ')}")
|
|
925
|
-
else
|
|
926
|
-
log_event(epic, "branches_created", "Epic branches: #{epic['epic_branches'].values.uniq.join(', ')}")
|
|
927
|
-
end
|
|
807
|
+
log_event(epic, "branches_created", "Epic branches: #{epic['epic_branches'].values.uniq.join(', ')}")
|
|
928
808
|
rescue StandardError => e
|
|
929
|
-
LOG.error "[Basecamp:Orchestrator] Failed to create epic branches: #{e.
|
|
930
|
-
"\n#{e.backtrace.first(3).join("\n")}" if defined?(LOG)
|
|
931
|
-
log_event(epic, "branches_failed", "Exception: #{e.message}")
|
|
809
|
+
LOG.error "[Basecamp:Orchestrator] Failed to create epic branches: #{e.message}" if defined?(LOG)
|
|
932
810
|
end
|
|
933
811
|
|
|
934
812
|
# Open final PRs from epic branches to main.
|