Module: Brainiac::Plugins::Basecamp

Defined in:
lib/brainiac/plugins/basecamp.rb,
lib/brainiac/plugins/basecamp/cli.rb,
lib/brainiac/plugins/basecamp/epic.rb,
lib/brainiac/plugins/basecamp/hooks.rb,
lib/brainiac/plugins/basecamp/client.rb,
lib/brainiac/plugins/basecamp/config.rb,
lib/brainiac/plugins/basecamp/prompts.rb,
lib/brainiac/plugins/basecamp/version.rb,
lib/brainiac/plugins/basecamp/webhook.rb,
lib/brainiac/plugins/basecamp/metadata.rb,
lib/brainiac/plugins/basecamp/task_state.rb,
lib/brainiac/plugins/basecamp/epic_branch.rb,
lib/brainiac/plugins/basecamp/epic_memory.rb,
lib/brainiac/plugins/basecamp/review_gate.rb,
lib/brainiac/plugins/basecamp/orchestrator.rb,
lib/brainiac/plugins/basecamp/session_registry.rb,
lib/brainiac/plugins/basecamp/comment_responder.rb

Defined Under Namespace

Modules: Cli, Client, CommentResponder, Config, Epic, EpicBranch, EpicMemory, Hooks, Orchestrator, Prompts, ReviewGate, SessionRegistry, TaskState, Webhook Classes: ClientError

Constant Summary collapse

STALE_DISPATCH_TIMEOUT =

Maximum seconds to wait for a dispatched agent/gate to respond before considering it stale and re-dispatching. Used across resume and health-check.

300
MAX_GATE_REDISPATCH_RETRIES =

Maximum number of times a gate will be re-dispatched for the same review cycle before giving up (prevents infinite re-dispatch loops).

3
VERSION =
"0.0.23"

Class Method Summary collapse

Class Method Details

.cli(args) ⇒ Object



1434
1435
1436
# File 'lib/brainiac/plugins/basecamp/cli.rb', line 1434

def self.cli(args)
  Cli.run(args)
end

.completionsObject



1438
1439
1440
# File 'lib/brainiac/plugins/basecamp/cli.rb', line 1438

def self.completions
  %w[setup config status epics deploy link bot projects set reset scrap webhook]
end

.configured?Boolean

Returns:

  • (Boolean)


10
11
12
13
# File 'lib/brainiac/plugins/basecamp/metadata.rb', line 10

def self.configured?
  config_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "basecamp.json")
  File.exist?(config_file)
end

.help_textObject



15
16
17
# File 'lib/brainiac/plugins/basecamp/metadata.rb', line 15

def self.help_text
  "    brainiac basecamp <command>     Manage Basecamp epic orchestration"
end

.reconcile_active_epics(epics = Orchestrator.active_epics, triggered_by: "recovery") ⇒ Object

The sole restart/periodic reconciliation entry point. It deliberately uses the exact transitions and gate-state operations used by hooks.



95
96
97
98
99
# File 'lib/brainiac/plugins/basecamp.rb', line 95

def reconcile_active_epics(epics = Orchestrator.active_epics, triggered_by: "recovery")
  SessionRegistry.reap_dead!
  SessionRegistry.sweep!
  epics.each { |epic| reconcile_epic(epic, triggered_by: triggered_by) }
end

.reconcile_epic(epic, triggered_by: "recovery") ⇒ Object



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/brainiac/plugins/basecamp.rb', line 101

def reconcile_epic(epic, triggered_by: "recovery")
  tasks = epic["tasks"] || []
  tasks.each { |task| TaskState.migrate!(task, triggered_by: triggered_by) }

  if tasks.any? && tasks.all? { |task| TaskState.in?(task, :complete) }
    tasks.each { |task| Orchestrator.send(:mark_todo_complete, epic, task["fizzy_card"]) }
    Orchestrator.send(:complete_epic, epic)
    Orchestrator.send(:save_epic, epic)
    return true
  end

  # Reconcile every task in this pass. `Enumerable#any?` would stop at
  # the first repaired task and defer later repairs to the next sweep.
  changed = tasks.map do |task|
    reconcile_task(epic, task, triggered_by: triggered_by)
  end.any?

  # Always attempt to dispatch unblocked tasks during recovery. Even if no
  # individual task changed state, there may be pending tasks whose
  # dependencies are satisfied that were never dispatched (e.g., after a
  # cancellation was reversed or an epic was healed manually).
  has_pending = tasks.any? { |t| TaskState.in?(t, :pending) }
  if changed || has_pending
    Orchestrator.send(:dispatch_unblocked_tasks, epic)
    Orchestrator.send(:save_epic, epic)
  end
  changed || has_pending
end

.register(app) ⇒ Object

Called by Brainiac plugin system during server startup.

Parameters:

  • app (Sinatra::Application)

    The running Brainiac server



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/brainiac/plugins/basecamp.rb', line 36

def register(app)
  Config.load!

  # Clear all sessions on startup — PIDs from prior runs are untrustworthy
  SessionRegistry.clear_all!
  SessionRegistry.install_global_registration_hook!

  # Register lifecycle hooks
  Hooks.register_all!

  # Register channel prompt (for when agents need Basecamp awareness)
  Brainiac.register_channel_prompt(:basecamp, Prompts::CHANNEL)

  # Set up routes
  setup_routes(app)

  # Log active epics on startup and resume them
  active = Orchestrator.active_epics
  if active.any?
    LOG.info "[Basecamp] #{active.size} active epic(s) in progress"
    active.each { |e| LOG.info "[Basecamp]   - #{e['title']} (#{e['tasks']&.count { |t| t['status'] == 'complete' }}/#{e['tasks']&.size} complete)" }

    # Reconcile active epics in background after server is ready. The
    # session registry was cleared above, so this treats all inherited
    # agent sessions as dead and safely starts fresh work where needed.
    Thread.new do
      sleep 10 # Wait for server to fully start
      reconcile_active_epics(active, triggered_by: "startup_recovery")
    rescue StandardError => e
      LOG.error "[Basecamp:Recovery] Startup reconciliation failed: #{e.message}" if defined?(LOG)
    end
  end

  # Start the periodic recovery loop for active epics.
  start_epic_health_monitor

  LOG.info "[Basecamp] Plugin registered (webhook: /basecamp, review_gate: #{Config.review_gate})"
end

.start_epic_health_monitorObject

Background thread that periodically runs the same reconciliation used after startup. Recovery owns liveness reaping and state repair; normal hooks own the immediate event path.



78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/brainiac/plugins/basecamp.rb', line 78

def start_epic_health_monitor
  @health_monitor_thread = Thread.new do
    loop do
      sleep 90  # Check every 90 seconds

      begin
        reconcile_active_epics(Orchestrator.active_epics, triggered_by: "periodic_recovery")
      rescue StandardError => e
        LOG.error "[Basecamp:Recovery] Periodic reconciliation failed: #{e.message}" if defined?(LOG)
      end
    end
  end
  @health_monitor_thread.abort_on_exception = false
end