brainiac 0.0.7 → 0.0.9

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -2
  3. data/bin/brainiac +132 -75
  4. data/lib/brainiac/agents.rb +12 -36
  5. data/lib/brainiac/brain.rb +6 -6
  6. data/lib/brainiac/config.rb +2 -67
  7. data/lib/brainiac/handlers/discord/config.rb +1 -1
  8. data/lib/brainiac/handlers/discord/delivery.rb +1 -1
  9. data/lib/brainiac/handlers/discord/gateway.rb +1 -1
  10. data/lib/brainiac/handlers/discord/message.rb +13 -25
  11. data/lib/brainiac/handlers/discord/reactions.rb +1 -1
  12. data/lib/brainiac/handlers/github.rb +49 -169
  13. data/lib/brainiac/handlers/shared/git.rb +4 -17
  14. data/lib/brainiac/handlers/shared/inline_tags.rb +1 -1
  15. data/lib/brainiac/handlers/zoho.rb +29 -82
  16. data/lib/brainiac/helpers.rb +43 -336
  17. data/lib/brainiac/hooks.rb +86 -0
  18. data/lib/brainiac/plugins.rb +27 -2
  19. data/lib/brainiac/prompts.rb +34 -172
  20. data/lib/brainiac/restart.rb +30 -12
  21. data/lib/brainiac/routes/api.rb +2 -18
  22. data/lib/brainiac/users.rb +1 -7
  23. data/lib/brainiac/version.rb +1 -1
  24. data/lib/brainiac.rb +1 -1
  25. data/monitor/waybar/deploy_env.rb +3 -3
  26. data/monitor/xbar/plugin.rb +11 -17
  27. data/receiver.rb +9 -157
  28. data/templates/agents.json.example +1 -2
  29. data/templates/brainiac.json.example +0 -1
  30. data/templates/users.json.example +0 -3
  31. metadata +2 -10
  32. data/lib/brainiac/handlers/fizzy/assignment.rb +0 -125
  33. data/lib/brainiac/handlers/fizzy/card_index.rb +0 -389
  34. data/lib/brainiac/handlers/fizzy/comments.rb +0 -730
  35. data/lib/brainiac/handlers/fizzy/dedup.rb +0 -74
  36. data/lib/brainiac/handlers/fizzy/deploy.rb +0 -152
  37. data/lib/brainiac/handlers/fizzy/deployments.rb +0 -260
  38. data/lib/brainiac/handlers/fizzy.rb +0 -14
  39. data/lib/brainiac/planning.rb +0 -237
  40. data/templates/fizzy.json.example +0 -24
@@ -11,8 +11,9 @@ require_relative "../shared"
11
11
  SELF_PATH = File.realpath(__FILE__)
12
12
  AGENTS = load_agent_config.freeze
13
13
  CONFIG = load_monitor_config.freeze
14
- FIZZY_ACCOUNT_ID = CONFIG["fizzy_account_id"]
15
- DISCORD_GUILD_ID = CONFIG["discord_guild_id"]
14
+
15
+ # URL patterns for different session types — configured in ~/.brainiac/waybar.json or monitor config
16
+ # Plugins can add their patterns here via the config file.
16
17
 
17
18
  LOG_VIEWER_PATH = File.join(File.dirname(SELF_PATH), "view_logs.rb")
18
19
  DEPLOY_SCRIPT_PATH = File.join(File.dirname(SELF_PATH), "deploy_env.rb")
@@ -30,22 +31,15 @@ def full_log_action(log_file)
30
31
  " | shell=#{OPEN_SCRIPT} param1=#{log_file.shellescape} terminal=false refresh=false"
31
32
  end
32
33
 
33
- def prompt_url(card_key)
34
- return nil unless card_key
35
-
36
- if card_key.start_with?("card-")
37
- card_num = card_key.split("-")[1]
38
- "https://app.fizzy.do/#{FIZZY_ACCOUNT_ID}/cards/#{card_num}" if FIZZY_ACCOUNT_ID && card_num
39
- elsif card_key.start_with?("discord-") && DISCORD_GUILD_ID
40
- parts = card_key.split("-")
41
- channel_id = parts[-2]
42
- message_id = parts[-1]
43
- "https://discord.com/channels/#{DISCORD_GUILD_ID}/#{channel_id}/#{message_id}" if channel_id && message_id
44
- end
34
+ def prompt_url(session)
35
+ # Prefer URL provided directly by the source plugin in session data
36
+ return session["url"] if session.is_a?(Hash) && session["url"]
37
+
38
+ nil
45
39
  end
46
40
 
47
- def prompt_action(card_key)
48
- url = prompt_url(card_key)
41
+ def prompt_action(session)
42
+ url = prompt_url(session)
49
43
  url ? " | shell=#{OPEN_SCRIPT} param1=#{url} terminal=false refresh=false" : ""
50
44
  end
51
45
 
@@ -66,7 +60,7 @@ def render_session_submenu(session)
66
60
  lines << "-- ---" if session["log_file"]
67
61
  lines << "-- Tail Log#{log_action(session["log_file"])}" if session["log_file"]
68
62
  lines << "-- View Full Log#{full_log_action(session["log_file"])}" if session["log_file"]
69
- lines << "-- Open Prompt#{prompt_action(session["card_key"])}" unless prompt_url(session["card_key"]).nil?
63
+ lines << "-- Open Prompt#{prompt_action(session)}" unless prompt_url(session).nil?
70
64
  wt = worktree_path(session["log_file"], session["card_key"])
71
65
  lines << "-- Open Worktree#{worktree_action(session["log_file"], session["card_key"])}" if wt
72
66
  lines
data/receiver.rb CHANGED
@@ -8,7 +8,11 @@
8
8
  require "sinatra"
9
9
  require "json"
10
10
 
11
+ # The directory this server is running from (supports worktrees)
12
+ SERVER_ROOT = File.expand_path(__dir__)
13
+
11
14
  # Load all modules
15
+ require_relative "lib/brainiac/hooks"
12
16
  require_relative "lib/brainiac/config"
13
17
  require_relative "lib/brainiac/users"
14
18
  require_relative "lib/brainiac/agents"
@@ -16,7 +20,6 @@ require_relative "lib/brainiac/brain"
16
20
  require_relative "lib/brainiac/skills"
17
21
  require_relative "lib/brainiac/sessions"
18
22
  require_relative "lib/brainiac/prompts"
19
- require_relative "lib/brainiac/planning"
20
23
  require_relative "lib/brainiac/helpers"
21
24
  require_relative "lib/brainiac/cron"
22
25
  require_relative "lib/brainiac/restart"
@@ -32,13 +35,6 @@ end
32
35
 
33
36
  # --- Conditional handler loading (based on ~/.brainiac/brainiac.json) ---
34
37
 
35
- if handler_enabled?("fizzy")
36
- require_relative "lib/brainiac/handlers/fizzy"
37
- require_relative "lib/brainiac/handlers/fizzy/card_index"
38
- require_relative "lib/brainiac/handlers/fizzy/deployments"
39
- LOG.info "[Handlers] Fizzy handler enabled"
40
- end
41
-
42
38
  if handler_enabled?("github")
43
39
  require_relative "lib/brainiac/handlers/github"
44
40
  LOG.info "[Handlers] GitHub handler enabled"
@@ -86,7 +82,10 @@ if Dir.exist?(CUSTOM_HANDLERS_DIR)
86
82
  end
87
83
 
88
84
  # --- Load gem-based plugins (brainiac-*) ---
89
- load_plugins!(self)
85
+ load_plugins!(Sinatra::Application)
86
+
87
+ # Emit server_started hook — plugins can run startup tasks (backfill, background jobs, etc.)
88
+ Brainiac.emit(:server_started)
90
89
 
91
90
  # --- Sinatra config ---
92
91
  set :host_authorization, { permit_all: true }
@@ -146,90 +145,6 @@ before "/api/*" do
146
145
  authenticate_dashboard!
147
146
  end
148
147
 
149
- # --- Fizzy webhook routes ---
150
-
151
- post "/fizzy/?:board_key?" do
152
- content_type :json
153
- request.body.rewind
154
- payload_body = request.body.read
155
- board_key = params["board_key"]
156
-
157
- verify_signature!(request, payload_body, board_key: board_key)
158
-
159
- payload = JSON.parse(payload_body)
160
-
161
- event_id = payload["id"]
162
- action = payload["action"]
163
-
164
- LOG.info "[Fizzy] Received event #{event_id}: action=#{action}"
165
-
166
- if already_processed?(event_id)
167
- LOG.info "Skipping duplicate event #{event_id}"
168
- halt 200, { status: "duplicate" }.to_json
169
- end
170
-
171
- reload_projects!
172
- reload_agent_registry!
173
- reload_github_config!
174
-
175
- case action
176
- when "card_assigned"
177
- status_code, body = handle_card_assigned(payload)
178
- LOG.info "[Fizzy] #{action} response: #{status_code} - #{body}"
179
- halt status_code, body
180
- when "comment_created"
181
- status_code, body = handle_comment(payload)
182
- LOG.info "[Fizzy] comment_created response: #{status_code} - #{body}"
183
- halt status_code, body
184
- when "card_published", "card_triaged"
185
- eventable = payload["eventable"] || {}
186
- card_number = eventable["number"]&.to_s
187
-
188
- # card_triaged never dispatches agents — only card_assigned and @mentions do that.
189
- # Guards remain as defense-in-depth for any future column-based routing.
190
- if action == "card_triaged" && card_number
191
- if self_move_recent?(card_number)
192
- LOG.info "[Fizzy] Ignoring card_triaged for ##{card_number} — self-move echo"
193
- halt 200, { status: "ignored", reason: "self_move" }.to_json
194
- end
195
-
196
- if card_merged?(card_number)
197
- LOG.info "[Fizzy] Ignoring card_triaged for ##{card_number} — card already merged"
198
- halt 200, { status: "ignored", reason: "card_merged" }.to_json
199
- end
200
-
201
- card_key = "card-#{card_number}"
202
- if recently_completed?(card_key)
203
- LOG.info "[Fizzy] Ignoring card_triaged for ##{card_number} — recently completed"
204
- halt 200, { status: "ignored", reason: "recently_completed" }.to_json
205
- end
206
- end
207
-
208
- # Only card_published does duplicate detection — card_triaged skips agent dispatch entirely
209
- if action == "card_published"
210
- assignees = eventable["assignees"] || []
211
- if assignees.any? { |a| local_agent_names.include?(a["name"]) }
212
- status_code, body = handle_card_assigned(payload)
213
- LOG.info "[Fizzy] #{action} (with assignee) response: #{status_code} - #{body}"
214
- halt status_code, body
215
- end
216
- end
217
-
218
- status_code, body = handle_card_published(payload)
219
- LOG.info "[Fizzy] #{action} response: #{status_code} - #{body}"
220
- halt status_code, body
221
- else
222
- LOG.info "[Fizzy] Ignoring unknown action: #{action}"
223
- halt 200, { status: "ignored", action: action }.to_json
224
- end
225
- rescue JSON::ParserError => e
226
- LOG.error "Invalid JSON: #{e.message}"
227
- halt 400, { error: "Invalid JSON" }.to_json
228
- rescue StandardError => e
229
- LOG.error "Unhandled error: #{e.message}\n#{e.backtrace.first(5).join("\n")}"
230
- halt 500, { error: e.message }.to_json
231
- end
232
-
233
148
  post "/github" do
234
149
  content_type :json
235
150
  request.body.rewind
@@ -252,7 +167,7 @@ post "/github" do
252
167
  status_code, body = handle_github_pr_merged(payload)
253
168
  halt status_code, body
254
169
  elsif action == "opened"
255
- track_pr_in_card_map(payload)
170
+ track_pr_in_work_items(payload)
256
171
  halt 200, { status: "processed", action: "pr_tracked" }.to_json
257
172
  elsif action == "synchronize"
258
173
  status_code, body = handle_github_pr_synchronized(payload)
@@ -511,64 +426,6 @@ else
511
426
  end
512
427
  end
513
428
 
514
- # --- Deployment environment tracking (requires fizzy handler) ---
515
-
516
- if defined?(DEPLOYMENTS_CONFIG)
517
- get "/api/deployments" do
518
- content_type :json
519
- reload_deployments_config!
520
- reload_deployment_state!
521
- { deployments: deployment_status }.to_json
522
- end
523
-
524
- post "/api/deployments/:env" do
525
- content_type :json
526
- env_key = params["env"]
527
- request.body.rewind
528
- payload = JSON.parse(request.body.read)
529
-
530
- result = deploy_to_environment(env_key, worktree_path: payload["worktree"], deployed_by: payload["deployed_by"])
531
- if result[:error]
532
- halt 404, result.to_json
533
- else
534
- { status: "deployed", env: env_key, deployment: result }.to_json
535
- end
536
- rescue JSON::ParserError
537
- halt 400, { error: "Invalid JSON" }.to_json
538
- end
539
-
540
- delete "/api/deployments/:env" do
541
- content_type :json
542
- env_key = params["env"]
543
- state = load_deployment_state
544
- if state.key?(env_key)
545
- state[env_key] = { "status" => "available", "cleared_at" => Time.now.iso8601, "last_card" => state[env_key]["card_number"] }
546
- save_deployment_state(state)
547
- DEPLOYMENT_STATE.replace(state)
548
- LOG.info "[Deploy] Manually cleared #{env_key}"
549
- { status: "cleared", env: env_key }.to_json
550
- else
551
- halt 404, { error: "Unknown environment: #{env_key}" }.to_json
552
- end
553
- end
554
-
555
- post "/api/deployments/:env/deploying" do
556
- content_type :json
557
- env_key = params["env"]
558
- config = DEPLOYMENTS_CONFIG["environments"] || {}
559
- halt 404, { error: "Unknown environment: #{env_key}" }.to_json unless config.key?(env_key)
560
- request.body.rewind
561
- payload = begin
562
- JSON.parse(request.body.read)
563
- rescue StandardError
564
- {}
565
- end
566
- mark_deploying(env_key, worktree_path: payload["worktree"] || "")
567
- LOG.info "[Deploy] #{env_key} marked deploying via API"
568
- { status: "deploying", env: env_key }.to_json
569
- end
570
- end
571
-
572
429
  LOG.info "[Cron] Starting cron thread..."
573
430
  start_cron_thread
574
431
 
@@ -583,11 +440,6 @@ CURATOR_THREAD = Thread.new do
583
440
  end
584
441
  end
585
442
 
586
- if defined?(CARD_INDEX)
587
- LOG.info "[CardIndex] Starting background backfill..."
588
- CARD_INDEX.backfill
589
- end
590
-
591
443
  LOG.info "[Monitor] Starting daemon..."
592
444
  daemon_path = File.join(__dir__, "monitor", "daemon.rb")
593
445
  daemon_pid_file = "/tmp/brainiac-daemon.pid"
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "galen": {
3
- "fizzy_name": "Galen",
3
+ "display_name": "Galen",
4
4
  "local": true,
5
5
  "env": {
6
- "FIZZY_TOKEN": "your-fizzy-token",
7
6
  "DISCORD_BOT_TOKEN": "your-discord-bot-token"
8
7
  }
9
8
  }
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "default_agent": "Galen",
3
3
  "handlers": {
4
- "fizzy": true,
5
4
  "github": true,
6
5
  "discord": true,
7
6
  "zoho": false
@@ -11,9 +11,6 @@
11
11
  "github": {
12
12
  "username": "dalton"
13
13
  },
14
- "fizzy": {
15
- "username": "adam-dalton"
16
- }
17
14
  },
18
15
  "aliases": ["Andy"],
19
16
  "relationships": {
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brainiac
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 0.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis
@@ -149,19 +149,12 @@ files:
149
149
  - lib/brainiac/handlers/discord/gateway.rb
150
150
  - lib/brainiac/handlers/discord/message.rb
151
151
  - lib/brainiac/handlers/discord/reactions.rb
152
- - lib/brainiac/handlers/fizzy.rb
153
- - lib/brainiac/handlers/fizzy/assignment.rb
154
- - lib/brainiac/handlers/fizzy/card_index.rb
155
- - lib/brainiac/handlers/fizzy/comments.rb
156
- - lib/brainiac/handlers/fizzy/dedup.rb
157
- - lib/brainiac/handlers/fizzy/deploy.rb
158
- - lib/brainiac/handlers/fizzy/deployments.rb
159
152
  - lib/brainiac/handlers/github.rb
160
153
  - lib/brainiac/handlers/shared/git.rb
161
154
  - lib/brainiac/handlers/shared/inline_tags.rb
162
155
  - lib/brainiac/handlers/zoho.rb
163
156
  - lib/brainiac/helpers.rb
164
- - lib/brainiac/planning.rb
157
+ - lib/brainiac/hooks.rb
165
158
  - lib/brainiac/plugins.rb
166
159
  - lib/brainiac/prompts.rb
167
160
  - lib/brainiac/restart.rb
@@ -188,7 +181,6 @@ files:
188
181
  - templates/cli-providers/grok.json.example
189
182
  - templates/cli-providers/kiro.json.example
190
183
  - templates/discord.json.example
191
- - templates/fizzy.json.example
192
184
  - templates/github.json.example
193
185
  - templates/plugins.json.example
194
186
  - templates/roles/code-reviewer.md.example
@@ -1,125 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Fizzy card assignment handler.
4
- #
5
- # When a card is assigned to a local agent, creates a worktree, builds the prompt,
6
- # and dispatches the agent to begin work.
7
-
8
- def handle_card_assigned(payload)
9
- eventable = payload["eventable"] || {}
10
- assignees = eventable["assignees"] || []
11
-
12
- local_names = local_agent_names
13
- assigned_agent = assignees.map { |a| a["name"] }.find { |name| local_names.include?(name) }
14
-
15
- assignee_names = assignees.map { |a| a["name"] }.join(", ")
16
- LOG.info "[Fizzy] Card assigned to: [#{assignee_names}], local agents: [#{local_names.join(", ")}]"
17
-
18
- unless assigned_agent
19
- LOG.info "[Fizzy] No local agent matched. Assignees: [#{assignee_names}], Local: [#{local_names.join(", ")}]"
20
- return [200, { status: "ignored", reason: "wrong assignee" }.to_json]
21
- end
22
-
23
- unless authorized?(payload)
24
- creator_name = payload.dig("creator", "name") || "Unknown"
25
- notify_unauthorized("card_assigned", creator_name, "card ##{eventable["number"]}")
26
- return [200, { status: "ignored", reason: "unauthorized" }.to_json]
27
- end
28
-
29
- card_number = eventable["number"]
30
- card_internal_id = eventable["id"]
31
- title = eventable["title"] || "untitled"
32
- tags = eventable["tags"] || []
33
-
34
- project_result = identify_project_by_tags(tags)
35
- unless project_result
36
- tag_names = tags.map { |t| t.is_a?(Hash) ? t["name"] : t }.join(", ")
37
- LOG.warn "No project found for card ##{card_number} with tags: #{tag_names}"
38
- return [200, { status: "ignored", reason: "no matching project" }.to_json]
39
- end
40
-
41
- project_key, project_config = project_result
42
- repo_path = project_config["repo_path"]
43
- branch = "fizzy-#{card_number}-#{slugify(title)}"
44
-
45
- card_key = "card-#{card_number}"
46
- if session_active?(card_key)
47
- LOG.info "Skipping card ##{card_number} — agent session already active"
48
- return [200, { status: "ignored", reason: "session already active" }.to_json]
49
- end
50
-
51
- LOG.info "Card ##{card_number} assigned to #{assigned_agent} for project '#{project_key}', " \
52
- "creating worktree: #{branch} (model: #{detect_model(project_config, tags: tags) || "default"})"
53
-
54
- react_to_assignment(card_number, repo_path, assigned_agent)
55
- worktree_path = setup_assigned_worktree(repo_path, branch, card_internal_id, card_number, project_key, assigned_agent)
56
-
57
- dispatch_assigned_card(
58
- card_number: card_number, card_internal_id: card_internal_id, title: title, tags: tags,
59
- branch: branch, worktree_path: worktree_path, project_config: project_config, project_key: project_key,
60
- agent_name: assigned_agent, model: detect_model(project_config, tags: tags),
61
- effort: detect_effort(project_config, tags: tags), cli_provider_override: detect_cli_provider(tags: tags)
62
- )
63
- end
64
-
65
- def react_to_assignment(card_number, repo_path, agent_name)
66
- Thread.new do
67
- run_cmd("fizzy", "reaction", "create", "--card", card_number.to_s,
68
- "--content", "👍", chdir: repo_path, env: fizzy_env_for(agent_name))
69
- rescue StandardError => e
70
- LOG.warn "Could not add reaction to card: #{e.message}"
71
- end
72
- end
73
-
74
- def setup_assigned_worktree(repo_path, branch, card_internal_id, card_number, project_key, agent_name)
75
- debounced_repo_fetch(repo_path)
76
- worktree_path = File.join(File.dirname(repo_path), "#{File.basename(repo_path)}--#{branch}")
77
- worktree_path = create_or_reuse_worktree(repo_path: repo_path, branch: branch, worktree_path: worktree_path)
78
-
79
- map = load_card_map
80
- map[card_internal_id] = {
81
- "number" => card_number, "branch" => branch, "worktree" => worktree_path,
82
- "project" => project_key, "agent" => agent_name
83
- }
84
- save_card_map(map)
85
- worktree_path
86
- end
87
-
88
- def dispatch_assigned_card(card_number:, card_internal_id:, title:, tags:, branch:, worktree_path:,
89
- project_config:, project_key:, agent_name:, model:, effort:, cli_provider_override:)
90
- card_context = prefetch_card_context(card_number, repo_path: project_config["repo_path"], agent_name: agent_name)
91
- planning_info = detect_planning_mode(text: title, tags: tags, card_internal_id: card_internal_id, card_number: card_number)
92
-
93
- template_vars = {
94
- "CARD_NUMBER" => card_number, "CARD_TITLE" => title,
95
- "BRANCH" => branch, "COMMENT_CREATOR" => agent_name
96
- }
97
- brain_ctx = build_brain_context(
98
- agent_name: agent_name, card_title: title,
99
- card_number: card_number, project_key: project_key, source: :fizzy
100
- )
101
-
102
- prompt = if planning_info
103
- LOG.info "[Planning] Planning mode active for card ##{card_number}"
104
- template_vars["CARD_ID"] = planning_info[:card_id]
105
- render_planning_prompt(PROMPT_CARD_ASSIGNED, template_vars,
106
- brain_context: brain_ctx, card_context: card_context, agent_name: agent_name)
107
- else
108
- template_vars["CARD_ID"] = card_number
109
- render_prompt(PROMPT_CARD_ASSIGNED, template_vars,
110
- brain_context: brain_ctx, card_context: card_context, agent_name: agent_name)
111
- end
112
-
113
- card_key = "card-#{card_number}"
114
- pid, log_file = run_agent(prompt,
115
- project_config: project_config, chdir: worktree_path,
116
- log_name: "assigned-#{card_number}", model: model, effort: effort,
117
- agent_name: agent_name, card_number: card_number, source: :fizzy,
118
- source_context: { card_number: card_number },
119
- cli_provider: cli_provider_override)
120
- register_session(card_key, pid, log_file: log_file, supersede_key: card_key, agent_name: agent_name)
121
-
122
- Thread.new { move_card_to_column(card_number, "right_now", project_config: project_config, agent_name: agent_name) }
123
-
124
- [200, { status: "processed", card: card_number, branch: branch, project: project_key, agent: agent_name }.to_json]
125
- end