brainiac 0.0.7 → 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 (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 +10 -10
  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
@@ -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
@@ -32,13 +32,33 @@ def installed_plugins
32
32
  (PLUGINS_CONFIG["plugins"] || []).map { |p| p.is_a?(Hash) ? p["name"] : p.to_s }
33
33
  end
34
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
+
35
40
  # Load all installed plugin gems and call their register hooks.
36
41
  # Called once during server startup, after core handlers are loaded.
37
42
  def load_plugins!(app)
43
+ # rubocop:disable Metrics/BlockLength
38
44
  installed_plugins.each do |name|
39
45
  gem_name = "brainiac-#{name}"
46
+ entry = plugin_entry(name)
40
47
  begin
41
- require gem_name
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
+
42
62
  plugin_module = resolve_plugin_module(name)
43
63
  if plugin_module.respond_to?(:register)
44
64
  plugin_module.register(app)
@@ -48,12 +68,17 @@ def load_plugins!(app)
48
68
  end
49
69
  rescue LoadError => e
50
70
  LOG.error "[Plugins] Could not load #{gem_name}: #{e.message}"
51
- LOG.error "[Plugins] Is the gem installed? Run: gem install #{gem_name}"
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
52
76
  rescue StandardError => e
53
77
  LOG.error "[Plugins] Error registering #{gem_name}: #{e.message}"
54
78
  LOG.error "[Plugins] #{e.backtrace.first(3).join("\n ")}"
55
79
  end
56
80
  end
81
+ # rubocop:enable Metrics/BlockLength
57
82
  end
58
83
 
59
84
  # Resolve the plugin module for a given name.
@@ -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
@@ -57,22 +57,40 @@ def execute_restart
57
57
 
58
58
  Thread.new do
59
59
  sleep 1
60
- source_dir = File.expand_path("../../..", __dir__)
60
+ source_dir = defined?(SERVER_ROOT) ? SERVER_ROOT : File.expand_path("../..", __dir__)
61
61
  receiver_path = File.join(source_dir, "receiver.rb")
62
- log_file = File.join(source_dir, "tmp", "brainiac-server.log")
63
- FileUtils.mkdir_p(File.dirname(log_file))
64
62
 
65
- pid = spawn({ "PATH" => ENV.fetch("PATH", nil) }, "ruby", receiver_path,
66
- chdir: source_dir, out: [log_file, "a"], err: %i[child out])
67
- Process.detach(pid)
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
68
 
69
- File.write(File.join(BRAINIAC_DIR, "brainiac-server.pid"), pid.to_s)
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)
70
73
 
71
- sleep 1
72
- LOG.info "[Brainiac] Stopping server, new instance started (PID: #{pid}) from #{source_dir}"
73
- Sinatra::Application.quit!
74
- sleep 0.5
75
- exit!
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
76
94
  end
77
95
  end
78
96
 
@@ -39,7 +39,7 @@ def resolve_session_agent_name(card_key, info)
39
39
  else
40
40
  "Unknown"
41
41
  end
42
- fizzy_display_name(agent_key)
42
+ agent_display_name(agent_key)
43
43
  end
44
44
 
45
45
  def format_recent_sessions
@@ -109,7 +109,6 @@ post "/api/reload" do
109
109
  reload_agent_registry!(force: true)
110
110
  reload_user_registry!(force: true)
111
111
  reload_github_config!(force: true)
112
- reload_deployments_config!(force: true) if defined?(DEPLOYMENTS_CONFIG)
113
112
  ReloadHooks.run_all!
114
113
  { status: "reloaded", projects: PROJECTS.keys, agents: all_agent_names.to_a, registry: AGENT_REGISTRY.keys,
115
114
  users: USER_REGISTRY["users"].size }.to_json
@@ -128,7 +127,7 @@ get "/api/roles" do
128
127
  if Dir.exist?(ROLES_DIR)
129
128
  Dir.glob(File.join(ROLES_DIR, "*.md")).each do |f|
130
129
  name = File.basename(f, ".md")
131
- agents = AGENT_REGISTRY.select { |_, e| e.is_a?(Hash) && Array(e["role"]).include?(name) }.map { |k, e| e["fizzy_name"] || k.capitalize }
130
+ agents = AGENT_REGISTRY.select { |_, e| e.is_a?(Hash) && Array(e["role"]).include?(name) }.map { |k, _e| agent_display_name(k) }
132
131
  roles << { name: name, agents: agents }
133
132
  end
134
133
  end
@@ -210,21 +209,6 @@ post "/api/skills/curate" do
210
209
  result.to_json
211
210
  end
212
211
 
213
- # --- Card Index ---
214
-
215
- get "/api/card-index" do
216
- content_type :json
217
- halt 404, { error: "Card index not enabled (fizzy handler disabled)" }.to_json unless defined?(CARD_INDEX)
218
-
219
- query = params["q"]
220
- if query && !query.empty?
221
- similar = CARD_INDEX.find_similar_cards(query)
222
- { query: query, matches: similar, total_indexed: CARD_INDEX.size }.to_json
223
- else
224
- { total: CARD_INDEX.size, cards: CARD_INDEX }.to_json
225
- end
226
- end
227
-
228
212
  # --- Dispatch Depth ---
229
213
 
230
214
  get "/api/dispatch-depth" do
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # User identity registry - resolves identities across platforms
4
- # (Discord, GitHub, Fizzy)
4
+ # (Discord, GitHub)
5
5
 
6
6
  USERS_FILE = File.join(BRAINIAC_DIR, "users.json")
7
7
 
@@ -40,11 +40,6 @@ def find_user_by_github_username(username)
40
40
  USER_REGISTRY["users"].find { |u| u.dig("identities", "github", "username") == username.to_s }
41
41
  end
42
42
 
43
- # Find user by Fizzy username
44
- def find_user_by_fizzy_username(username)
45
- USER_REGISTRY["users"].find { |u| u.dig("identities", "fizzy", "username") == username.to_s }
46
- end
47
-
48
43
  # Find user by canonical name
49
44
  def find_user_by_canonical_name(name)
50
45
  USER_REGISTRY["users"].find { |u| u["canonical_name"].downcase == name.downcase }
@@ -55,7 +50,6 @@ def find_user(identifier)
55
50
  find_user_by_discord_id(identifier) ||
56
51
  find_user_by_discord_username(identifier) ||
57
52
  find_user_by_github_username(identifier) ||
58
- find_user_by_fizzy_username(identifier) ||
59
53
  find_user_by_canonical_name(identifier)
60
54
  end
61
55
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Brainiac
4
4
  # @return [String] the current gem version
5
- VERSION = "0.0.7"
5
+ VERSION = "0.0.8"
6
6
  end
data/lib/brainiac.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require_relative "brainiac/hooks"
1
2
  require_relative "brainiac/config"
2
3
  require_relative "brainiac/users"
3
4
  require_relative "brainiac/agents"
@@ -5,7 +6,6 @@ require_relative "brainiac/brain"
5
6
  require_relative "brainiac/skills"
6
7
  require_relative "brainiac/sessions"
7
8
  require_relative "brainiac/prompts"
8
- require_relative "brainiac/planning"
9
9
  require_relative "brainiac/helpers"
10
10
  require_relative "brainiac/cron"
11
11
  require_relative "brainiac/plugins"
@@ -48,9 +48,9 @@ def handle_deploy(env_key, deployment)
48
48
  return unless deployment
49
49
 
50
50
  prefill = deployment["status"] == "occupied" && deployment["card_number"] ? deployment["card_number"].to_s : ""
51
- card_number = `timeout 60 zenity --entry --title="Deploy to #{env_key}" --text="Fizzy card number:"#{unless prefill.empty?
52
- " --entry-text=#{Shellwords.escape(prefill)}"
53
- end} 2>/dev/null`.strip
51
+ card_number = `timeout 60 zenity --entry --title="Deploy to #{env_key}" --text="Card number:"#{unless prefill.empty?
52
+ " --entry-text=#{Shellwords.escape(prefill)}"
53
+ end} 2>/dev/null`.strip
54
54
  return if card_number.empty?
55
55
 
56
56
  worktree = resolve_worktree(card_number)