brainiac 0.0.6 → 0.0.8

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -2
  3. data/README.md +136 -6
  4. data/bin/brainiac +472 -44
  5. data/bin/brainiac-completion.bash +1 -1
  6. data/lib/brainiac/agents.rb +27 -74
  7. data/lib/brainiac/brain.rb +6 -6
  8. data/lib/brainiac/config.rb +40 -76
  9. data/lib/brainiac/handlers/discord/api.rb +196 -0
  10. data/lib/brainiac/handlers/discord/config.rb +134 -0
  11. data/lib/brainiac/handlers/discord/delivery.rb +196 -0
  12. data/lib/brainiac/handlers/discord/gateway.rb +212 -0
  13. data/lib/brainiac/handlers/discord/message.rb +933 -0
  14. data/lib/brainiac/handlers/discord/reactions.rb +215 -0
  15. data/lib/brainiac/handlers/discord.rb +14 -1892
  16. data/lib/brainiac/handlers/github.rb +134 -317
  17. data/lib/brainiac/handlers/shared/git.rb +190 -0
  18. data/lib/brainiac/handlers/shared/inline_tags.rb +89 -0
  19. data/lib/brainiac/handlers/zoho.rb +103 -153
  20. data/lib/brainiac/helpers.rb +43 -455
  21. data/lib/brainiac/hooks.rb +86 -0
  22. data/lib/brainiac/plugins.rb +154 -0
  23. data/lib/brainiac/prompts.rb +34 -172
  24. data/lib/brainiac/restart.rb +112 -0
  25. data/lib/brainiac/routes/api.rb +411 -0
  26. data/lib/brainiac/users.rb +1 -7
  27. data/lib/brainiac/version.rb +1 -1
  28. data/lib/brainiac/zoho_mail_api.rb +2 -1
  29. data/lib/brainiac.rb +8 -1
  30. data/monitor/daemon.rb +4 -27
  31. data/monitor/shared.rb +247 -0
  32. data/monitor/{waybar-deploy-env.rb → waybar/deploy_env.rb} +17 -89
  33. data/monitor/waybar/setup.rb +232 -0
  34. data/monitor/waybar/status.rb +51 -0
  35. data/monitor/{view-logs-rofi.rb → waybar/view_logs.rb} +21 -88
  36. data/monitor/{deploy-env-macos.rb → xbar/deploy_env.rb} +3 -2
  37. data/monitor/xbar/plugin.rb +149 -0
  38. data/monitor/{setup-menubar.rb → xbar/setup.rb} +14 -30
  39. data/receiver.rb +44 -551
  40. data/templates/agents.json.example +1 -2
  41. data/templates/brainiac.json.example +8 -0
  42. data/templates/cli-providers/kiro.json.example +8 -2
  43. data/templates/plugins.json.example +3 -0
  44. data/templates/users.json.example +0 -3
  45. metadata +25 -23
  46. data/lib/brainiac/card_index.rb +0 -389
  47. data/lib/brainiac/deployments.rb +0 -258
  48. data/lib/brainiac/handlers/fizzy.rb +0 -1292
  49. data/lib/brainiac/planning.rb +0 -237
  50. data/lib/user_registry.rb +0 -159
  51. data/monitor/menubar.rb +0 -295
  52. data/monitor/setup-waybar-deploy-envs.rb +0 -121
  53. data/monitor/setup-waybar-deployments.rb +0 -96
  54. data/monitor/setup-waybar-module.rb +0 -113
  55. data/monitor/setup-xbar-plugin.rb +0 -35
  56. data/monitor/view-logs.rb +0 -119
  57. data/monitor/waybar-config-updater.rb +0 -56
  58. data/monitor/waybar-deployments.rb +0 -239
  59. data/monitor/waybar.rb +0 -146
  60. data/monitor/xbar.3s.rb +0 -179
  61. data/templates/fizzy.json.example +0 -24
  62. /data/monitor/{open-action.sh → xbar/open_action.sh} +0 -0
  63. /data/monitor/{view-logs-macos.rb → xbar/view_logs.rb} +0 -0
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Brainiac hook system.
4
+ #
5
+ # Provides a lightweight pub/sub mechanism for plugins to extend core behavior
6
+ # without core knowing about specific plugins.
7
+ #
8
+ # Core emits events at lifecycle points. Plugins register handlers via Brainiac.on.
9
+ #
10
+ # Events:
11
+ # :agent_completed — After an agent session finishes (success or failure)
12
+ # :pr_merged — After a GitHub PR is merged
13
+ # :pr_opened — After a GitHub PR is opened
14
+ # :pr_reviewed — After a PR review is submitted
15
+ # :build_brain_context — When building brain context (plugins add source-specific queries)
16
+ # :pre_dispatch — Before dispatching an agent (plugins can inject config)
17
+ # :post_comment — After an agent posts a comment/response
18
+ #
19
+ # Usage (in plugin .register):
20
+ # Brainiac.on(:agent_completed) do |ctx|
21
+ # move_card_to_column(ctx[:card_number], "needs_review", ...)
22
+ # end
23
+ #
24
+ # Usage (in core):
25
+ # Brainiac.emit(:agent_completed, card_number: 42, agent_name: "Galen", ...)
26
+
27
+ module Brainiac
28
+ @hooks = Hash.new { |h, k| h[k] = [] }
29
+ @channel_prompts = {}
30
+ @channel_pre_post_checks = {}
31
+
32
+ class << self
33
+ # Register a hook for an event.
34
+ #
35
+ # @param event [Symbol] Event name
36
+ # @param block [Proc] Handler block, receives a context hash
37
+ def on(event, &block)
38
+ @hooks[event] << block
39
+ end
40
+
41
+ # Emit an event, calling all registered handlers.
42
+ # Returns an array of results from each handler (nil results filtered out).
43
+ #
44
+ # @param event [Symbol] Event name
45
+ # @param context [Hash] Context passed to each handler
46
+ # @return [Array] Results from handlers
47
+ def emit(event, **context)
48
+ results = @hooks[event].map do |handler|
49
+ handler.call(context)
50
+ rescue StandardError => e
51
+ LOG.error "[Hooks] Error in #{event} handler: #{e.message}" if defined?(LOG)
52
+ LOG.error "[Hooks] #{e.backtrace.first(3).join("\n ")}" if defined?(LOG)
53
+ nil
54
+ end
55
+ results.compact
56
+ end
57
+
58
+ # Register a channel prompt template for use in render_prompt.
59
+ # Plugins call this to add their channel-specific prompt block.
60
+ #
61
+ # @param channel [Symbol] Channel name (e.g., :fizzy, :discord)
62
+ # @param prompt [String] Channel prompt text
63
+ # @param pre_post_check [String, nil] Optional pre-post comment check instructions
64
+ def register_channel_prompt(channel, prompt, pre_post_check: nil)
65
+ @channel_prompts[channel] = prompt
66
+ @channel_pre_post_checks[channel] = pre_post_check if pre_post_check
67
+ end
68
+
69
+ # Get registered channel prompts (used by render_prompt).
70
+ #
71
+ # @return [Hash<Symbol, String>]
72
+ attr_reader :channel_prompts
73
+
74
+ # Get registered pre-post checks (used by render_prompt).
75
+ #
76
+ # @return [Hash<Symbol, String>]
77
+ attr_reader :channel_pre_post_checks
78
+
79
+ # Clear all hooks (useful for testing).
80
+ def reset_hooks!
81
+ @hooks = Hash.new { |h, k| h[k] = [] }
82
+ @channel_prompts = {}
83
+ @channel_pre_post_checks = {}
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Plugin system for Brainiac.
4
+ #
5
+ # Plugins are distributed as gems named `brainiac-<name>` (e.g. brainiac-whatsapp).
6
+ # Each gem exposes a module at `Brainiac::Plugins::<Name>` that responds to `.register(app)`
7
+ # where `app` is the Sinatra application instance.
8
+ #
9
+ # Installed plugins are tracked in ~/.brainiac/plugins.json so the receiver
10
+ # knows which gems to require at startup.
11
+
12
+ PLUGINS_FILE = File.join(BRAINIAC_DIR, "plugins.json")
13
+
14
+ def load_plugins_config
15
+ return { "plugins" => [] } unless File.exist?(PLUGINS_FILE)
16
+
17
+ JSON.parse(File.read(PLUGINS_FILE))
18
+ rescue JSON::ParserError => e
19
+ LOG.error "Failed to parse plugins.json: #{e.message}"
20
+ { "plugins" => [] }
21
+ end
22
+
23
+ def save_plugins_config(config)
24
+ FileUtils.mkdir_p(BRAINIAC_DIR)
25
+ File.write(PLUGINS_FILE, JSON.pretty_generate(config))
26
+ end
27
+
28
+ PLUGINS_CONFIG = load_plugins_config
29
+
30
+ # Returns the list of installed plugin names (e.g. ["whatsapp", "slack"])
31
+ def installed_plugins
32
+ (PLUGINS_CONFIG["plugins"] || []).map { |p| p.is_a?(Hash) ? p["name"] : p.to_s }
33
+ end
34
+
35
+ # Returns the full plugin entry (Hash) for a given name, or nil.
36
+ def plugin_entry(name)
37
+ (PLUGINS_CONFIG["plugins"] || []).find { |p| (p.is_a?(Hash) ? p["name"] : p.to_s) == name }
38
+ end
39
+
40
+ # Load all installed plugin gems and call their register hooks.
41
+ # Called once during server startup, after core handlers are loaded.
42
+ def load_plugins!(app)
43
+ # rubocop:disable Metrics/BlockLength
44
+ installed_plugins.each do |name|
45
+ gem_name = "brainiac-#{name}"
46
+ entry = plugin_entry(name)
47
+ begin
48
+ # If plugin has a local path, add its lib/ to load path before requiring
49
+ if entry.is_a?(Hash) && entry["path"]
50
+ lib_path = File.join(entry["path"], "lib")
51
+ $LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
52
+ LOG.info "[Plugins] Loading #{gem_name} from local path: #{entry["path"]}"
53
+ end
54
+
55
+ # Try both naming conventions: brainiac-fizzy and brainiac_fizzy
56
+ begin
57
+ require gem_name
58
+ rescue LoadError
59
+ require gem_name.tr("-", "_")
60
+ end
61
+
62
+ plugin_module = resolve_plugin_module(name)
63
+ if plugin_module.respond_to?(:register)
64
+ plugin_module.register(app)
65
+ LOG.info "[Plugins] Loaded #{gem_name}"
66
+ else
67
+ LOG.warn "[Plugins] #{gem_name} loaded but no register method found"
68
+ end
69
+ rescue LoadError => e
70
+ LOG.error "[Plugins] Could not load #{gem_name}: #{e.message}"
71
+ if entry.is_a?(Hash) && entry["path"]
72
+ LOG.error "[Plugins] Local path: #{entry["path"]}"
73
+ else
74
+ LOG.error "[Plugins] Is the gem installed? Run: gem install #{gem_name}"
75
+ end
76
+ rescue StandardError => e
77
+ LOG.error "[Plugins] Error registering #{gem_name}: #{e.message}"
78
+ LOG.error "[Plugins] #{e.backtrace.first(3).join("\n ")}"
79
+ end
80
+ end
81
+ # rubocop:enable Metrics/BlockLength
82
+ end
83
+
84
+ # Resolve the plugin module for a given name.
85
+ # Tries Brainiac::Plugins::Whatsapp, Brainiac::Plugins::WhatsApp, etc.
86
+ def resolve_plugin_module(name)
87
+ return nil unless defined?(Brainiac::Plugins)
88
+
89
+ # Try PascalCase (e.g. "whatsapp" -> "Whatsapp", "test-widget" -> "TestWidget")
90
+ pascal = name.split(/[-_]/).map(&:capitalize).join
91
+ return Brainiac::Plugins.const_get(pascal) if Brainiac::Plugins.const_defined?(pascal)
92
+
93
+ # Try case-insensitive match (e.g. "WhatsApp" if the gem defines it that way)
94
+ Brainiac::Plugins.constants.each do |const|
95
+ return Brainiac::Plugins.const_get(const) if const.to_s.downcase == name.downcase
96
+ end
97
+
98
+ nil
99
+ end
100
+
101
+ # Install a plugin gem and register it in plugins.json.
102
+ # rubocop:disable Naming/PredicateMethod
103
+ def install_plugin(name, version: nil)
104
+ gem_name = "brainiac-#{name}"
105
+
106
+ if installed_plugins.include?(name)
107
+ puts "Plugin '#{name}' is already installed."
108
+ return false
109
+ end
110
+
111
+ puts "Installing #{gem_name}..."
112
+ install_cmd = ["gem", "install", gem_name]
113
+ install_cmd.push("--version", version) if version
114
+
115
+ stdout, stderr, status = Open3.capture3(*install_cmd)
116
+ unless status.success?
117
+ puts "Failed to install #{gem_name}:"
118
+ puts stderr.empty? ? stdout : stderr
119
+ return false
120
+ end
121
+ puts stdout unless stdout.strip.empty?
122
+
123
+ config = load_plugins_config
124
+ config["plugins"] ||= []
125
+ entry = { "name" => name, "gem" => gem_name, "installed_at" => Time.now.iso8601 }
126
+ entry["version"] = version if version
127
+ config["plugins"] << entry
128
+ save_plugins_config(config)
129
+
130
+ puts "✓ Installed plugin '#{name}' (#{gem_name})"
131
+ puts " Restart the server to activate: brainiac restart"
132
+ true
133
+ end
134
+
135
+ # Uninstall a plugin gem and remove from plugins.json
136
+ def uninstall_plugin(name)
137
+ gem_name = "brainiac-#{name}"
138
+
139
+ unless installed_plugins.include?(name)
140
+ puts "Plugin '#{name}' is not installed."
141
+ return false
142
+ end
143
+
144
+ config = load_plugins_config
145
+ config["plugins"].reject! { |p| (p.is_a?(Hash) ? p["name"] : p.to_s) == name }
146
+ save_plugins_config(config)
147
+
148
+ puts "Removed plugin '#{name}' from Brainiac."
149
+ puts " The gem #{gem_name} is still installed system-wide."
150
+ puts " To fully remove: gem uninstall #{gem_name}"
151
+ puts " Restart the server to apply: brainiac restart"
152
+ true
153
+ end
154
+ # rubocop:enable Naming/PredicateMethod
@@ -4,7 +4,6 @@
4
4
  #
5
5
  # Prompts are layered:
6
6
  # PROMPT_CORE — universal (identity, memory, brain, reflection)
7
- # PROMPT_FIZZY_CHANNEL — Fizzy-specific rules (HTML formatting, reactions, screenshots)
8
7
  # PROMPT_DISCORD_CHANNEL — Discord-specific rules (markdown, response file, char limits)
9
8
  # PROMPT_GITHUB_CHANNEL — GitHub-specific rules (GFM, PR conventions)
10
9
  #
@@ -26,7 +25,7 @@ PROMPT_CORE = <<~PROMPT
26
25
  **At the very start of every session:**
27
26
  1. Read `{{MEMORY_DIR}}/card-{{CARD_ID}}.md`. If it contains content, it has context from your previous sessions. If the file is empty (first session on this card), just proceed without prior context.
28
27
 
29
- **Note:** Only the last 15 comments are included in card context (truncated to 500 chars each). Your memory file is the authoritative record of prior discussions. If you need the full text of a truncated comment, run: `fizzy comment show COMMENT_ID --card CARD_NUMBER`
28
+ **Note:** Only the last 15 comments are included in card context (truncated to 500 chars each). Your memory file is the authoritative record of prior discussions.
30
29
 
31
30
  **Before you finish every session (even if you didn't complete the task):**
32
31
  2. Update your memory file at `{{MEMORY_DIR}}/card-{{CARD_ID}}.md`.
@@ -43,11 +42,11 @@ PROMPT_CORE = <<~PROMPT
43
42
  Relevant knowledge is automatically retrieved and included above in this prompt when available.
44
43
  You can also search manually: `qmd search "<query>" -c brainiac-knowledge`
45
44
 
46
- **MANDATORY: Before running any non-standard CLI tool (fizzy, qmd, gh, project scripts) you haven't used in this session, search the brain first:**
45
+ **MANDATORY: Before running any non-standard CLI tool (qmd, gh, project scripts) you haven't used in this session, search the brain first:**
47
46
  ```
48
47
  qmd search "<tool-name>" -c brainiac-knowledge
49
48
  ```
50
- Examples: `qmd search "fizzy" -c brainiac-knowledge`, `qmd search "qmd" -c brainiac-knowledge`
49
+ Examples: `qmd search "qmd" -c brainiac-knowledge`, `qmd search "gh" -c brainiac-knowledge`
51
50
 
52
51
  Standard unix commands (cd, ls, grep, cat, git, curl, etc.) don't need a brain search.
53
52
  But for project-specific tools, do NOT guess at flags or syntax — wrong commands waste time and tokens. Look it up first.
@@ -63,7 +62,7 @@ PROMPT_CORE = <<~PROMPT
63
62
 
64
63
  ### Persona (`{{PERSONA_DIR}}/`) — unique to you
65
64
  Communication style, tone, personality, how to interact with specific people.
66
- **This is for all external communication, such as writing comments on Fizzy cards, Discord chat, and GitHub PRs.**
65
+ **This is for all external communication, such as writing comments on cards, Discord chat, and GitHub PRs.**
67
66
 
68
67
  Do NOT manually read persona files during coding/debugging — the auto-retrieved persona
69
68
  above already shapes your communication style. Focus on implementation during work phases,
@@ -119,7 +118,7 @@ PROMPT_CORE = <<~PROMPT
119
118
  - To discover project locations for cross-repo work, run: `brainiac list`
120
119
 
121
120
  **Limitations:** Subagents don't get your brain context, persona, or memory.
122
- They can read files and run commands, but cannot post to Fizzy, Discord, or GitHub.
121
+ They can read files and run commands, but cannot post to Discord, GitHub, or other channels.
123
122
  They're excellent researchers — use them as such.
124
123
 
125
124
  ## Image Reading Limits
@@ -131,36 +130,8 @@ PROMPT
131
130
  # ---------------------------------------------------------------------------
132
131
  # PROMPT_PRE_POST_CHECK — inserted before PROMPT_REFLECTION so the agent
133
132
  # re-checks for new comments/messages before posting its response.
134
- # Channel-specific: Fizzy and GitHub get re-fetch instructions, Discord skips.
133
+ # Channel-specific: plugins register pre-post checks, Discord skips.
135
134
  # ---------------------------------------------------------------------------
136
- PROMPT_PRE_POST_CHECK_FIZZY = <<~PROMPT
137
- ## Pre-Post Comment Check (MANDATORY — do this BEFORE posting your comment)
138
-
139
- Your session may have been running for a while. Before you post your final comment,
140
- re-fetch the card to see if anything changed while you were working:
141
-
142
- ```bash
143
- fizzy card show {{CARD_NUMBER}}
144
- fizzy comment list --card {{CARD_NUMBER}}
145
- ```
146
-
147
- Compare what you see now against the card context that was provided at the start
148
- of your session. Check for:
149
-
150
- **Card body changes:** If the card description was edited (new acceptance criteria,
151
- clarified scope, updated requirements), adjust your work to match before posting.
152
-
153
- **New comments:** If there are new comments that weren't in your original context:
154
- 1. **Read them carefully** — a human may have added context, changed requirements, or asked you to adjust something
155
- 2. **Decide how to respond:**
156
- - If the new comment changes what you should build or how → adjust your work before posting
157
- - If the new comment adds context that affects your response → incorporate it into your comment
158
- - If the new comment is unrelated or just acknowledgment → proceed as planned, but mention you saw it
159
- 3. **Do NOT ignore new comments** — the whole point is to avoid posting a response that's already outdated
160
-
161
- If nothing changed, proceed normally.
162
-
163
- PROMPT
164
135
 
165
136
  PROMPT_PRE_POST_CHECK_GITHUB = <<~PROMPT
166
137
  ## Pre-Post Comment Check (MANDATORY — do this BEFORE posting your comment)
@@ -215,46 +186,6 @@ PROMPT_REFLECTION = <<~PROMPT
215
186
 
216
187
  PROMPT
217
188
 
218
- # ---------------------------------------------------------------------------
219
- # PROMPT_FIZZY_CHANNEL — Fizzy-specific rules, prepended to Fizzy templates
220
- # ---------------------------------------------------------------------------
221
- PROMPT_FIZZY_CHANNEL = <<~PROMPT
222
- ## Fizzy Channel Rules
223
-
224
- ### Standard Procedure
225
- - If you have questions, ask them in the card's comments.
226
- - Only assign a fizzy card if it is currently unassigned and you are requested to work on it. Otherwise leave it, it will be managed by the users.
227
-
228
- ### Column Transitions
229
- Brainiac handles column moves automatically — do NOT move cards between columns yourself.
230
- Cards move to "Right Now" when you're dispatched and to "Needs Review" when your session ends.
231
-
232
- ### Formatting
233
- **Fizzy comments use HTML, NOT Markdown.** Use `<h2>`/`<h3>` for sections, `<p>` for paragraphs, `<ul><li>` for lists, `<pre data-language="ruby">` for code blocks, `<strong>` for emphasis. Never use markdown syntax (`**bold**`, `- list`, `## heading`) in Fizzy comments — it renders as raw text.
234
-
235
- ### Screenshots (MANDATORY for UI changes)
236
- If you touched any `.js`, `.jsx`, `.css`, or `.html` in a web app directory and `./scripts/screenshot-page.sh` exists in the project, screenshot every affected page. Search the brain for "screenshot" if you need the full workflow.
237
-
238
- **Before uploading, review your own screenshot:**
239
- 1. Read the screenshot image file
240
- 2. Check for: blank/white pages, obvious rendering errors, missing content, broken layouts, error messages, or anything that doesn't match what you expected
241
- 3. If the screenshot looks wrong, fix the underlying issue and retake (max 2 retries)
242
- 4. After 2 retries, upload whatever you have and note the display issue in your comment so the human knows it needs attention
243
-
244
- Upload screenshots and embed them in your comment using `<action-text-attachment>`.
245
-
246
- ### Card Memory Discipline (CRITICAL for long-running cards)
247
- Cards evolve — scope expands, requirements shift, new acceptance criteria appear mid-work.
248
- When writing your memory file for a Fizzy card session, you MUST include:
249
- - The original card scope/requirements (from the card body at time of assignment)
250
- - Any scope changes from comments (e.g. "also handle X while you're in there")
251
- - Any card body edits you detected during pre-post check
252
- - The current scope/focus as of this session
253
- This is the ONLY way future sessions will know the full picture when the card body has changed
254
- or key decisions were made in comments that fell outside the pre-fetched window.
255
-
256
- PROMPT
257
-
258
189
  # ---------------------------------------------------------------------------
259
190
  # PROMPT_DISCORD_CHANNEL — Discord-specific rules, prepended to Discord templates
260
191
  # ---------------------------------------------------------------------------
@@ -284,7 +215,7 @@ PROMPT_DISCORD_CHANNEL = <<~PROMPT
284
215
  per message, though long responses will be split automatically.
285
216
 
286
217
  ### Scope
287
- This is a conversational interaction — no Fizzy card, no PR. You're here to answer questions,
218
+ This is a conversational interaction — no card, no PR. You're here to answer questions,
288
219
  discuss code, share knowledge, or help with whatever the user needs.
289
220
 
290
221
  **Detect user intent:**
@@ -351,81 +282,8 @@ PROMPT_GITHUB_CHANNEL = <<~PROMPT
351
282
 
352
283
  ### Scope
353
284
  You are responding to activity on a GitHub PR. Focus on the code changes and review feedback.
354
- When posting comments, post on the PR unless specifically asked to update the Fizzy card.
355
-
356
- PROMPT
285
+ When posting comments, post on the PR unless specifically asked to update the card.
357
286
 
358
- # ---------------------------------------------------------------------------
359
- # Situation templates — the specific "what happened" for each trigger type
360
- # ---------------------------------------------------------------------------
361
-
362
- PROMPT_CARD_ASSIGNED = <<~'PROMPT'
363
- You have been assigned Fizzy card #{{CARD_NUMBER}}: "{{CARD_TITLE}}".
364
- You are on branch "{{BRANCH}}" in a fresh worktree.
365
- Implement the task, commit, push, and open a PR (link back to Fizzy).
366
- When you're done, post a comment on the card with a concise summary, PR link, and branch name.
367
-
368
- **MANDATORY: Always include the branch name in your comment.** Use this format:
369
- `<p><strong>Branch:</strong> <code>{{BRANCH}}</code></p>`
370
- PROMPT
371
-
372
- PROMPT_FOLLOWUP_WORKTREE = <<~'PROMPT'
373
- There's a new comment on Fizzy card #{{CARD_NUMBER}} that you've already started working on.
374
- You are in the existing worktree for this card.
375
-
376
- The comment that triggered this session is from {{COMMENT_CREATOR}} (comment ID: {{COMMENT_ID}}):
377
- """
378
- {{COMMENT_BODY}}
379
- """
380
-
381
- The card and its full comment history are provided above. Focus your response on the comment above.
382
- If you've already addressed this exact request in a previous session (check your memory file), reply confirming it's done — do NOT redo it.
383
- Otherwise, make the requested changes, commit, push, and update the PR.
384
- PROMPT
385
-
386
- PROMPT_FOLLOWUP_NO_WORKTREE = <<~PROMPT
387
- There's a new comment on a Fizzy card (internal_id: "{{CARD_INTERNAL_ID}}") that you've been involved with.
388
-
389
- The comment that triggered this session is from {{COMMENT_CREATOR}} (comment ID: {{COMMENT_ID}}):
390
- """
391
- {{COMMENT_BODY}}
392
- """
393
-
394
- The card and its full comment history are provided above. Focus your response on the comment above.
395
- If you've already addressed this exact request in a previous session (check your memory file), reply confirming it's done — do NOT redo it.
396
- Otherwise, respond accordingly — that could include doing work on a new or existing branch.
397
- PROMPT
398
-
399
- PROMPT_MENTION = <<~PROMPT
400
- You were mentioned in a comment on a Fizzy card with internal_id "{{CARD_INTERNAL_ID}}"{{CARD_NUMBER_TEXT}}.
401
- You are on branch "{{BRANCH}}" in a dedicated worktree for exploration and investigation.
402
-
403
- Find the card and respond accordingly. You can:
404
- - Investigate the codebase and provide your thoughts
405
- - Make exploratory changes or create test files (they won't pollute the main branch)
406
- - Create a PR if your exploration leads to a concrete solution
407
- PROMPT
408
-
409
- PROMPT_CROSS_AGENT_REVIEW = <<~'PROMPT'
410
- You were tagged in a comment on Fizzy card #{{CARD_NUMBER}} (internal_id: "{{CARD_INTERNAL_ID}}").
411
- This card is being worked on by {{CARD_AGENT}} — you're being brought in for your perspective.
412
-
413
- The comment that tagged you is from {{COMMENT_CREATOR}} (comment ID: {{COMMENT_ID}}):
414
- """
415
- {{COMMENT_BODY}}
416
- """
417
-
418
- The card and its full comment history are provided above. Also check any linked PR to understand the current state.
419
- Then respond to what's being asked of you — that might be a code review, an opinion on
420
- an approach, debugging help, or just a sanity check.
421
-
422
- You are in your own worktree at `{{WORKTREE_PATH}}` on branch `{{BRANCH}}`.
423
- This is separate from {{CARD_AGENT}}'s worktree — you can read code, make changes, and
424
- commit without affecting their work or the main repo.
425
-
426
- **IMPORTANT: Do NOT @mention any other agents in your response.** You were brought in for
427
- a one-shot review. If you think another agent should be involved, say so in plain text
428
- but do NOT use @Agent syntax — tagging agents creates automated dispatches.
429
287
  PROMPT
430
288
 
431
289
  PROMPT_DISCORD = <<~'PROMPT'
@@ -447,7 +305,7 @@ PROMPT_DISCORD = <<~'PROMPT'
447
305
  PROMPT
448
306
 
449
307
  PROMPT_GITHUB_PR_COMMENT = <<~'PROMPT'
450
- There's a new comment from @{{COMMENT_CREATOR}} on your PR #{{PR_NUMBER}} for Fizzy card #{{CARD_NUMBER}}.
308
+ There's a new comment from @{{COMMENT_CREATOR}} on your PR #{{PR_NUMBER}} for card #{{CARD_NUMBER}}.
451
309
 
452
310
  Comment:
453
311
  {{COMMENT_BODY}}
@@ -462,7 +320,7 @@ PROMPT_GITHUB_PR_COMMENT = <<~'PROMPT'
462
320
  PROMPT
463
321
 
464
322
  PROMPT_GITHUB_PR_REVIEW = <<~'PROMPT'
465
- A code review has been submitted on your PR #{{PR_NUMBER}} for Fizzy card #{{CARD_NUMBER}}.
323
+ A code review has been submitted on your PR #{{PR_NUMBER}} for card #{{CARD_NUMBER}}.
466
324
 
467
325
  {{REVIEW_CONTEXT}}
468
326
 
@@ -480,11 +338,11 @@ PROMPT
480
338
  # Channel constant mapping for render_prompt
481
339
  # ---------------------------------------------------------------------------
482
340
  PROMPT_GITHUB_UAT = <<~'PROMPT'
483
- PR #{{PR_NUMBER}} has been merged into main for Fizzy card #{{CARD_NUMBER}}: "{{CARD_TITLE}}"
341
+ PR #{{PR_NUMBER}} has been merged into main for card #{{CARD_NUMBER}}: "{{CARD_TITLE}}"
484
342
 
485
343
  The card has been moved to the UAT column. The changes are now deployed to the UAT environment.
486
344
 
487
- Your job: post a comment on Fizzy card #{{CARD_NUMBER}} with clear, specific steps for how to manually test this feature in UAT. Include:
345
+ Your job: post a comment on card #{{CARD_NUMBER}} with clear, specific steps for how to manually test this feature in UAT. Include:
488
346
  1. What URL(s) or screen(s) to visit
489
347
  2. Step-by-step actions to verify the feature works
490
348
  3. What the expected behavior should be
@@ -497,7 +355,6 @@ PROMPT_GITHUB_UAT = <<~'PROMPT'
497
355
  PROMPT
498
356
 
499
357
  CHANNEL_PROMPTS = {
500
- fizzy: PROMPT_FIZZY_CHANNEL,
501
358
  discord: PROMPT_DISCORD_CHANNEL,
502
359
  github: PROMPT_GITHUB_CHANNEL
503
360
  }.freeze
@@ -505,30 +362,32 @@ CHANNEL_PROMPTS = {
505
362
  # ---------------------------------------------------------------------------
506
363
  # render_prompt — composes PROMPT_CORE + channel rules + situation template
507
364
  #
508
- # channel: :fizzy (default), :discord, or :github
365
+ # channel: :discord, :github, or plugin-registered channels (e.g., :fizzy, :linear)
509
366
  # ---------------------------------------------------------------------------
510
- DEFAULT_COLUMN_IDS = {
511
- "right_now" => "03f5xa5q9fog9592pa1279dts",
512
- "needs_review" => "03f5ykobhpsd78hbuvajtn8g8",
513
- "uat" => "03fsmglsr6az06ppyotawsti8"
514
- }.freeze
515
367
 
516
- def render_prompt(template, vars = {}, brain_context: "", card_context: "", agent_name: AI_AGENT_NAME, channel: :fizzy, board_key: nil)
368
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
369
+ def render_prompt(template, vars = {}, brain_context: "", card_context: "", agent_name: AI_AGENT_NAME, channel: :discord, board_key: nil)
517
370
  result = ""
518
371
  result += "#{brain_context}\n" unless brain_context.empty?
519
372
  result += card_context unless card_context.empty?
520
373
  result += PROMPT_CORE
521
- result += CHANNEL_PROMPTS.fetch(channel, PROMPT_FIZZY_CHANNEL)
374
+
375
+ # Channel prompt: check plugin-registered prompts first, then built-in
376
+ plugin_prompt = Brainiac.channel_prompts[channel]
377
+ result += plugin_prompt || CHANNEL_PROMPTS.fetch(channel, PROMPT_DISCORD_CHANNEL)
378
+
522
379
  result += template
523
380
 
524
- # Pre-post comment check: tell the agent to re-fetch comments before posting.
525
- # Discord skips this — its supersede mechanism handles mid-session updates differently.
526
- case channel
527
- when :fizzy then result += PROMPT_PRE_POST_CHECK_FIZZY
528
- when :github then result += PROMPT_PRE_POST_CHECK_GITHUB
381
+ # Pre-post comment check: plugin-registered or built-in
382
+ plugin_pre_post = Brainiac.channel_pre_post_checks[channel]
383
+ if plugin_pre_post
384
+ result += plugin_pre_post
385
+ elsif channel == :github
386
+ result += PROMPT_PRE_POST_CHECK_GITHUB
529
387
  end
530
388
 
531
- result += PROMPT_REFLECTION
389
+ # Reflection prompt — skip for Discord (causes crashes in post-task phase)
390
+ result += PROMPT_REFLECTION unless channel == :discord
532
391
 
533
392
  vars["KNOWLEDGE_DIR"] ||= KNOWLEDGE_DIR
534
393
  vars["MEMORY_DIR"] ||= memory_dir_for(agent_name)
@@ -537,9 +396,11 @@ def render_prompt(template, vars = {}, brain_context: "", card_context: "", agen
537
396
  vars["AGENT_NAME"] ||= agent_name
538
397
 
539
398
  # Populate column IDs from board config, falling back to defaults
540
- DEFAULT_COLUMN_IDS.each do |col_name, default_id|
541
- var_name = "#{col_name.upcase}_COLUMN_ID"
542
- vars[var_name] ||= (board_key && board_column_id(board_key, col_name)) || default_id
399
+ if defined?(DEFAULT_COLUMN_IDS)
400
+ DEFAULT_COLUMN_IDS.each do |col_name, default_id|
401
+ var_name = "#{col_name.upcase}_COLUMN_ID"
402
+ vars[var_name] ||= (board_key && board_column_id(board_key, col_name)) || default_id
403
+ end
543
404
  end
544
405
 
545
406
  # Touch memory file if CARD_ID is present — ensures file exists before agent tries to read it
@@ -556,6 +417,7 @@ def render_prompt(template, vars = {}, brain_context: "", card_context: "", agen
556
417
  vars.each { |key, val| result.gsub!("{{#{key}}}", val.to_s) }
557
418
  result
558
419
  end
420
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
559
421
 
560
422
  # Lean prompt for resumed sessions. The previous session already has the full context
561
423
  # (role, persona, knowledge, core instructions, channel prompts). We only send the new
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Brainiac self-restart logic.
4
+ #
5
+ # When an agent works on brainiac itself (modifies code), a restart is queued.
6
+ # A background thread checks every 30s and only restarts when no other agents
7
+ # are running, preventing mid-session kills.
8
+ #
9
+ # This is NOT Discord-specific — it was previously in the Discord handler
10
+ # because Discord agents trigger restarts most often, but any source can queue one.
11
+
12
+ BRAINIAC_RESTART_STATE = { queued: false, triggered_by: nil }
13
+ BRAINIAC_RESTART_MUTEX = Mutex.new
14
+
15
+ def queue_brainiac_restart(agent_name)
16
+ BRAINIAC_RESTART_MUTEX.synchronize do
17
+ unless BRAINIAC_RESTART_STATE[:queued]
18
+ BRAINIAC_RESTART_STATE[:queued] = true
19
+ BRAINIAC_RESTART_STATE[:triggered_by] = agent_name
20
+ LOG.info "[Brainiac] #{agent_name} queued a restart — will execute when all agents finish"
21
+ end
22
+ end
23
+ end
24
+
25
+ # Send a Discord notification about brainiac restart/startup using any available bot token.
26
+ def send_restart_notification(message)
27
+ channel_id = DISCORD_CONFIG["notification_channel_id"]
28
+ return unless channel_id
29
+
30
+ tokens = discord_bot_tokens
31
+ triggered_by = BRAINIAC_RESTART_MUTEX.synchronize { BRAINIAC_RESTART_STATE[:triggered_by] }
32
+ token = tokens[triggered_by&.downcase] || tokens.values.first
33
+ return unless token
34
+
35
+ send_discord_message(channel_id, message, token: token)
36
+ rescue StandardError => e
37
+ LOG.warn "[Brainiac] Failed to send restart notification: #{e.message}"
38
+ end
39
+
40
+ def any_agents_running?
41
+ ACTIVE_SESSIONS_MUTEX.synchronize do
42
+ ACTIVE_SESSIONS.any? do |_key, info|
43
+ Process.kill(0, info[:pid])
44
+ true
45
+ rescue Errno::ESRCH, Errno::EPERM
46
+ false
47
+ end
48
+ end
49
+ end
50
+
51
+ def execute_restart
52
+ triggered_by = BRAINIAC_RESTART_MUTEX.synchronize { BRAINIAC_RESTART_STATE[:triggered_by] }
53
+ LOG.info "[Brainiac] All agents finished, executing restart..."
54
+ BRAINIAC_RESTART_MUTEX.synchronize { BRAINIAC_RESTART_STATE[:queued] = false }
55
+
56
+ send_restart_notification("🔄 Restarting brainiac (triggered by #{triggered_by || "unknown"})...")
57
+
58
+ Thread.new do
59
+ sleep 1
60
+ source_dir = defined?(SERVER_ROOT) ? SERVER_ROOT : File.expand_path("../..", __dir__)
61
+ receiver_path = File.join(source_dir, "receiver.rb")
62
+
63
+ # Determine if we're running in foreground mode.
64
+ # If stdin is a TTY or we weren't launched as a daemon, use exec to replace the process.
65
+ # This keeps the server in the foreground terminal.
66
+ foreground = $stdout.tty? || !File.exist?(File.join(BRAINIAC_DIR, "server.pid")) ||
67
+ File.read(File.join(BRAINIAC_DIR, "server.pid")).strip.to_i == Process.pid
68
+
69
+ if foreground
70
+ LOG.info "[Brainiac] Restarting in foreground mode (exec)..."
71
+ # Write PID file for the new process (same PID after exec)
72
+ File.write(File.join(BRAINIAC_DIR, "server.pid"), Process.pid.to_s)
73
+
74
+ # exec replaces this process — the terminal stays attached
75
+ Dir.chdir(source_dir)
76
+ exec("ruby", receiver_path)
77
+ else
78
+ # Daemon mode — spawn a new background process
79
+ log_file = File.join(source_dir, "tmp", "brainiac-server.log")
80
+ FileUtils.mkdir_p(File.dirname(log_file))
81
+
82
+ pid = spawn({ "PATH" => ENV.fetch("PATH", nil) }, "ruby", receiver_path,
83
+ chdir: source_dir, out: [log_file, "a"], err: %i[child out])
84
+ Process.detach(pid)
85
+
86
+ File.write(File.join(BRAINIAC_DIR, "server.pid"), pid.to_s)
87
+
88
+ LOG.info "[Brainiac] Stopping server, new instance started (PID: #{pid}) from #{source_dir}"
89
+ sleep 0.5
90
+ Sinatra::Application.quit!
91
+ sleep 0.5
92
+ exit!
93
+ end
94
+ end
95
+ end
96
+
97
+ def start_brainiac_restart_monitor
98
+ Thread.new do
99
+ LOG.info "[Brainiac] Restart monitor started, checking every 30s"
100
+ loop do
101
+ sleep 30
102
+ restart_needed = BRAINIAC_RESTART_MUTEX.synchronize { BRAINIAC_RESTART_STATE[:queued] }
103
+
104
+ if restart_needed && !any_agents_running?
105
+ execute_restart
106
+ elsif restart_needed
107
+ active_count = ACTIVE_SESSIONS_MUTEX.synchronize { ACTIVE_SESSIONS.size }
108
+ LOG.info "[Brainiac] Restart queued but #{active_count} agent(s) still running, waiting..."
109
+ end
110
+ end
111
+ end
112
+ end