brainiac 0.0.11 → 0.0.13

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.
@@ -112,7 +112,8 @@ end
112
112
  def load_work_item_map
113
113
  return {} unless File.exist?(WORK_ITEM_MAP_FILE)
114
114
 
115
- JSON.parse(File.read(WORK_ITEM_MAP_FILE))
115
+ raw = JSON.parse(File.read(WORK_ITEM_MAP_FILE))
116
+ migrate_work_item_map(raw)
116
117
  rescue JSON::ParserError
117
118
  {}
118
119
  end
@@ -121,17 +122,164 @@ def save_work_item_map(map)
121
122
  File.write(WORK_ITEM_MAP_FILE, JSON.pretty_generate(map))
122
123
  end
123
124
 
124
- def slugify(title, max_length: 40)
125
- title.downcase.gsub(/[^a-z0-9\s-]/, "").strip.gsub(/\s+/, "-").slice(0, max_length).chomp("-")
125
+ # Migrate old-format work item maps (keyed by Fizzy card internal ID with flat structure)
126
+ # to the new source-agnostic format (keyed by work item ID with sources hash).
127
+ # Old format: { "fizzy-uuid" => { "number" => 42, "branch" => "...", "worktree" => "...", "project" => "...", "agent" => "..." } }
128
+ # New format: { "wi-abc123" => { "id" => "wi-...", "branch" => "...", "worktree" => "...",
129
+ # "project" => "...", "agent" => "...", "sources" => { "fizzy" => { ... } } } }
130
+ def migrate_work_item_map(raw)
131
+ return raw if raw.empty?
132
+
133
+ # Detect: if any entry has a "sources" key, it's already new format (or mixed)
134
+ # If none have "sources", it's entirely old format
135
+ needs_migration = raw.values.any? { |v| v.is_a?(Hash) && !v.key?("sources") }
136
+ return raw unless needs_migration
137
+
138
+ migrated = {}
139
+ raw.each do |key, entry|
140
+ next unless entry.is_a?(Hash)
141
+
142
+ if entry.key?("sources")
143
+ # Already new format
144
+ migrated[key] = entry
145
+ else
146
+ # Old format — migrate. Generate a work item ID from the branch or card number.
147
+ work_item_id = generate_work_item_id(branch: entry["branch"], card_number: entry["number"])
148
+ migrated[work_item_id] = {
149
+ "id" => work_item_id,
150
+ "branch" => entry["branch"],
151
+ "worktree" => entry["worktree"],
152
+ "project" => entry["project"],
153
+ "agent" => entry["agent"],
154
+ "sources" => {
155
+ "fizzy" => {
156
+ "card_internal_id" => key,
157
+ "card_number" => entry["number"]
158
+ }
159
+ }
160
+ }
161
+ # Preserve PR tracking if it exists
162
+ migrated[work_item_id]["sources"]["github"] = { "prs" => entry["prs"] } if entry["prs"]
163
+ end
164
+ end
165
+ migrated
166
+ end
167
+
168
+ # Generate a deterministic work item ID from available identifiers.
169
+ # Priority: branch name (universal join key), then card number fallback.
170
+ def generate_work_item_id(branch: nil, card_number: nil)
171
+ if branch
172
+ "wi-#{Digest::SHA256.hexdigest(branch)[0..7]}"
173
+ elsif card_number
174
+ "wi-card-#{card_number}"
175
+ else
176
+ "wi-#{SecureRandom.hex(4)}"
177
+ end
178
+ end
179
+
180
+ # Find a work item by its branch name. Returns [work_item_id, info] or nil.
181
+ # This is the primary lookup method — branch is the universal join key.
182
+ def find_work_item_by_branch(branch)
183
+ return nil unless branch
184
+
185
+ map = load_work_item_map
186
+ map.each do |work_item_id, info|
187
+ next unless info.is_a?(Hash) && info["branch"] == branch
188
+
189
+ return [work_item_id, info]
190
+ end
191
+ nil
192
+ end
193
+
194
+ # Find a work item by its ID. Returns the info hash or nil.
195
+ def find_work_item_by_id(work_item_id)
196
+ return nil unless work_item_id
197
+
198
+ map = load_work_item_map
199
+ map[work_item_id]
200
+ end
201
+
202
+ # Find a work item by a Fizzy card internal ID (for backward compat with Fizzy plugin).
203
+ # Returns [work_item_id, info] or nil.
204
+ def find_work_item_by_card(card_internal_id)
205
+ return nil unless card_internal_id
206
+
207
+ map = load_work_item_map
208
+ map.each do |work_item_id, info|
209
+ next unless info.is_a?(Hash)
210
+
211
+ fizzy_source = info.dig("sources", "fizzy")
212
+ next unless fizzy_source && fizzy_source["card_internal_id"] == card_internal_id
213
+
214
+ return [work_item_id, info]
215
+ end
216
+ nil
217
+ end
218
+
219
+ # Register a new work item or update an existing one.
220
+ # Returns the work item ID.
221
+ def register_work_item(branch:, worktree: nil, project: nil, agent: nil, source: nil, source_data: {})
222
+ map = load_work_item_map
223
+
224
+ # Check if a work item already exists for this branch
225
+ existing_id = nil
226
+ map.each do |wid, info|
227
+ if info.is_a?(Hash) && info["branch"] == branch
228
+ existing_id = wid
229
+ break
230
+ end
231
+ end
232
+
233
+ work_item_id = existing_id || generate_work_item_id(branch: branch)
234
+
235
+ if existing_id
236
+ # Update existing entry — merge in new source, update worktree/agent if provided
237
+ map[work_item_id]["worktree"] = worktree if worktree
238
+ map[work_item_id]["agent"] = agent if agent
239
+ map[work_item_id]["sources"] ||= {}
240
+ map[work_item_id]["sources"][source] = source_data if source
241
+ else
242
+ # Create new entry
243
+ map[work_item_id] = {
244
+ "id" => work_item_id,
245
+ "branch" => branch,
246
+ "worktree" => worktree,
247
+ "project" => project,
248
+ "agent" => agent,
249
+ "sources" => source ? { source => source_data } : {}
250
+ }
251
+ end
252
+
253
+ save_work_item_map(map)
254
+ work_item_id
255
+ end
256
+
257
+ # Add or update a source on an existing work item.
258
+ # Returns true if the work item was found and updated, false otherwise.
259
+ def register_work_item_source(source:, source_data:, work_item_id: nil, branch: nil) # rubocop:disable Naming/PredicateMethod
260
+ map = load_work_item_map
261
+
262
+ # Find by ID or branch
263
+ target_id = work_item_id
264
+ unless target_id
265
+ map.each do |wid, info|
266
+ if info.is_a?(Hash) && info["branch"] == branch
267
+ target_id = wid
268
+ break
269
+ end
270
+ end
271
+ end
272
+
273
+ return false unless target_id && map[target_id]
274
+
275
+ map[target_id]["sources"] ||= {}
276
+ map[target_id]["sources"][source] = source_data
277
+ save_work_item_map(map)
278
+ true
126
279
  end
127
280
 
128
- def verify_github_signature!(request, payload_body)
129
- signature = request.env["HTTP_X_HUB_SIGNATURE_256"]
130
- halt 403, { error: "Missing GitHub signature" }.to_json unless signature
131
- secret = github_webhook_secret
132
- halt 500, { error: "GitHub webhook secret not configured" }.to_json unless secret
133
- computed = "sha256=#{OpenSSL::HMAC.hexdigest("sha256", secret, payload_body)}"
134
- halt 403, { error: "Invalid GitHub signature" }.to_json unless Rack::Utils.secure_compare(signature, computed)
281
+ def slugify(title, max_length: 40)
282
+ title.downcase.gsub(/[^a-z0-9\s-]/, "").strip.gsub(/\s+/, "-").slice(0, max_length).chomp("-")
135
283
  end
136
284
 
137
285
  def run_cmd(*cmd, chdir:, env: {})
@@ -184,7 +332,6 @@ end
184
332
  def notify_agent_crash(exit_status:, log_file:, agent_name:, source:, source_context:, project_config:)
185
333
  agent_display = agent_name || "Agent"
186
334
  snippet = extract_crash_snippet(log_file)
187
- snippet_block = snippet ? "\n```\n#{snippet[-1500..]}\n```" : ""
188
335
 
189
336
  # Emit to plugins — they handle their own channel-specific delivery
190
337
  handled = Brainiac.emit(:agent_crashed,
@@ -192,28 +339,66 @@ def notify_agent_crash(exit_status:, log_file:, agent_name:, source:, source_con
192
339
  source: source, source_context: source_context, project_config: project_config,
193
340
  snippet: snippet)
194
341
 
195
- # If a plugin handled it, we're done
196
- return if handled.any?
197
-
198
- # Built-in: GitHub crash comment (doesn't need a plugin)
199
- if source == :github
200
- pr_number = source_context[:pr_number]
201
- repo_name = source_context[:repo_name]
202
- return unless pr_number && repo_name
203
-
204
- work_dir = source_context[:work_dir] || Dir.pwd
205
- comment_body = "💥 **#{agent_display} crashed** (exit code #{exit_status})\n\nLog: `#{log_file}`#{snippet_block}"
206
- begin
207
- run_cmd("gh", "pr", "comment", pr_number.to_s, "--repo", repo_name, "--body", comment_body, chdir: work_dir)
208
- LOG.info "[CrashNotify] Posted crash comment on GitHub PR ##{pr_number}"
209
- rescue StandardError => e
210
- LOG.error "[CrashNotify] Failed to post GitHub crash comment: #{e.message}"
211
- end
212
- end
342
+ # If no plugin handled it, log a warning
343
+ LOG.warn "[CrashNotify] Agent crashed but no plugin handled notification (source: #{source})" unless handled.any?
213
344
  rescue StandardError => e
214
345
  LOG.error "[CrashNotify] Unexpected error: #{e.message}"
215
346
  end
216
347
 
348
+ # Check if a prior CLI session exists in the given directory for the specified CLI binary.
349
+ # This prevents resume attempts when the CLI provider changed (e.g., [cli:grok] in a thread
350
+ # started by kiro-cli) or when the session was started on a different machine.
351
+ def prior_session_exists?(chdir, agent_cli)
352
+ return false unless chdir && agent_cli
353
+
354
+ cli_name = File.basename(agent_cli)
355
+
356
+ # Check for CLI-specific session markers:
357
+ # - grok uses .grok/ directory for session state
358
+ # - kiro-cli uses .kiro-cli/ or similar
359
+ # - Generic fallback: check tmp/ for agent logs from this CLI
360
+ session_dir = File.join(chdir, ".#{cli_name}")
361
+ return true if File.directory?(session_dir)
362
+
363
+ # Fallback: look for recent session logs in tmp/ that suggest this CLI ran here before.
364
+ # This covers CLIs that don't leave a dotdir but do leave logs via brainiac.
365
+ tmp_dir = File.join(chdir, "tmp")
366
+ return false unless File.directory?(tmp_dir)
367
+
368
+ Dir.glob(File.join(tmp_dir, "agent-*.log")).any? do |log|
369
+ # Only count logs from the last 24 hours as valid "resumable" sessions
370
+ File.mtime(log) > Time.now - 86_400
371
+ end
372
+ rescue StandardError
373
+ false
374
+ end
375
+
376
+ # Public helper: check if resume is viable for a given project + CLI provider combo.
377
+ # Plugins should call this BEFORE building the prompt to decide between
378
+ # render_resume_prompt (lean) and render_prompt (full context).
379
+ #
380
+ # Returns true if the CLI supports resume AND a prior session exists in the working directory.
381
+ # When this returns false, plugins should use render_prompt with thread history as card_context
382
+ # so the agent gets full context even though this is a follow-up message.
383
+ def resume_viable?(project_config:, cli_provider: nil, agent_name: nil, chdir: nil)
384
+ resolved = resolve_project_cli_config(project_config, cli_provider_override: cli_provider, agent_name: agent_name)
385
+ chdir ||= resolved["repo_path"]
386
+ return false unless resolved["resume_flag"]
387
+
388
+ prior_session_exists?(chdir, resolved["agent_cli"])
389
+ end
390
+
391
+ # Determine whether a session resume should actually happen.
392
+ # Returns truthy (the resume flag string) if viable, false otherwise.
393
+ # Logs a message when resume was requested but isn't possible.
394
+ def resolve_resume(resume, resolved, chdir)
395
+ return false unless resume && resolved["resume_flag"]
396
+ return resolved["resume_flag"] if prior_session_exists?(chdir, resolved["agent_cli"])
397
+
398
+ LOG.info "[Dispatch] Resume requested but not viable for #{resolved["agent_cli"]} in #{chdir} — starting fresh session"
399
+ false
400
+ end
401
+
217
402
  def run_agent(prompt, project_config:, chdir: nil, log_name: "agent", model: nil, effort: nil, agent_name: nil, card_number: nil, comment_id: nil,
218
403
  source: nil, source_context: {}, skip_column_move: false, cli_provider: nil, resume: false)
219
404
  resolved = resolve_project_cli_config(project_config, cli_provider_override: cli_provider, agent_name: agent_name)
@@ -222,9 +407,8 @@ def run_agent(prompt, project_config:, chdir: nil, log_name: "agent", model: nil
222
407
  effort ||= resolved["agent_effort"]
223
408
  agent_config_name = agent_name&.downcase&.gsub(/[^a-z0-9-]/, "-")
224
409
 
225
- # Auto-resume: if the provider supports session resume and we're in a worktree
226
- # that has had a previous session, resume it. Only applies to follow-ups (not first dispatch).
227
- should_resume = resume && resolved["resume_flag"]
410
+ # Auto-resume: only if the provider supports it AND a prior session exists for this CLI here.
411
+ should_resume = resolve_resume(resume, resolved, chdir)
228
412
 
229
413
  # Pre-dispatch hook — plugins can prep the working directory (e.g., copy config files, clean up)
230
414
  Brainiac.emit(:pre_dispatch, chdir: chdir, project_config: project_config, agent_name: agent_name)
@@ -22,7 +22,7 @@
22
22
  # end
23
23
  #
24
24
  # Usage (in core):
25
- # Brainiac.emit(:agent_completed, card_number: 42, agent_name: "Galen", ...)
25
+ # Brainiac.emit(:agent_completed, card_number: 42, agent_name: "Sherlock", ...)
26
26
 
27
27
  module Brainiac
28
28
  @hooks = Hash.new { |h, k| h[k] = [] }
@@ -77,8 +77,8 @@ end
77
77
  # Convenience: send a notification for cron job output.
78
78
  def notify_cron_output(job, message, agent_name: nil)
79
79
  send_notification(:cron, message,
80
- target: job[:notify_target] || job[:discord_channel_id],
81
- channel: job[:notify_channel] || (job[:discord_channel_id] ? :discord : nil),
80
+ target: job[:notify_target],
81
+ channel: job[:notify_channel],
82
82
  agent: agent_name || job[:agent],
83
83
  job_id: job[:id],
84
84
  forum_title: job[:forum_title],
@@ -4,7 +4,7 @@
4
4
  #
5
5
  # Prompts are layered:
6
6
  # PROMPT_CORE — universal (identity, memory, brain, reflection)
7
- # PROMPT_GITHUB_CHANNEL — GitHub-specific rules (GFM, PR conventions)
7
+ # (GitHub prompts extracted to brainiac-github plugin)
8
8
  #
9
9
  # Each handler composes: PROMPT_CORE + channel rules + situation template.
10
10
  # Channel-specific prompts (Discord, Fizzy, etc.) are registered by plugins.
@@ -112,7 +112,7 @@ PROMPT_CORE = <<~PROMPT
112
112
  - Be specific in your query — tell the subagent exactly what to find and where to look
113
113
  - Include relevant file paths and repo locations in the query
114
114
  - Use `relevant_context` to pass information the subagent needs
115
- - You can specify `agent_name` to use a specialized agent (e.g., "sheogorath" for Android research)
115
+ - You can specify `agent_name` to use a specialized agent (e.g., "robin" for Android research)
116
116
  - Run `ListAgents` first if you want to see available specialized agents
117
117
  - Up to 4 subagents can run in parallel
118
118
  - To discover project locations for cross-repo work, run: `brainiac list`
@@ -122,37 +122,11 @@ PROMPT_CORE = <<~PROMPT
122
122
  They're excellent researchers — use them as such.
123
123
 
124
124
  ## Image Reading Limits
125
- Read at most 45 images per tool call. Summarize what you saw before reading more.
125
+ Read at most 4-5 images per tool call. Summarize what you saw before reading more.
126
126
  Loading too many images at once can exceed the API request size limit and crash your session.
127
127
 
128
128
  PROMPT
129
129
 
130
- # ---------------------------------------------------------------------------
131
- # PROMPT_PRE_POST_CHECK — inserted before PROMPT_REFLECTION so the agent
132
- # re-checks for new comments/messages before posting its response.
133
- # Channel-specific: plugins register pre-post checks, Discord skips.
134
- # ---------------------------------------------------------------------------
135
-
136
- PROMPT_PRE_POST_CHECK_GITHUB = <<~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-check the PR for new comments that arrived while you were working:
141
-
142
- ```bash
143
- gh pr view {{PR_NUMBER}} --comments --json comments
144
- ```
145
-
146
- If there are **new comments** that weren't in your original context:
147
-
148
- 1. **Read them carefully** — a reviewer may have added feedback or changed direction
149
- 2. **Adjust your work or response** to account for the new information
150
- 3. **Do NOT ignore new comments** — avoid posting a response that's already outdated
151
-
152
- If no new comments appeared, proceed normally.
153
-
154
- PROMPT
155
-
156
130
  # ---------------------------------------------------------------------------
157
131
  # PROMPT_REFLECTION — appended AFTER the situation template so the agent
158
132
  # sees its task first and reflects only after completing it.
@@ -186,78 +160,13 @@ PROMPT_REFLECTION = <<~PROMPT
186
160
 
187
161
  PROMPT
188
162
 
189
- # ---------------------------------------------------------------------------
190
- # PROMPT_GITHUB_CHANNEL — GitHub-specific rules, prepended to GitHub templates
191
- # ---------------------------------------------------------------------------
192
- PROMPT_GITHUB_CHANNEL = <<~PROMPT
193
- ## GitHub Channel Rules
194
-
195
- ### Formatting
196
- Use GitHub-Flavored Markdown for all comments:
197
- - `## Heading` for sections
198
- - `**bold**` for emphasis
199
- - ``` ```language ``` for code blocks
200
- - `- item` for lists
201
-
202
- ### Scope
203
- You are responding to activity on a GitHub PR. Focus on the code changes and review feedback.
204
- When posting comments, post on the PR unless specifically asked to update the card.
205
-
206
- PROMPT
207
-
208
- PROMPT_GITHUB_PR_COMMENT = <<~'PROMPT'
209
- There's a new comment from @{{COMMENT_CREATOR}} on your PR #{{PR_NUMBER}} for card #{{CARD_NUMBER}}.
210
-
211
- Comment:
212
- {{COMMENT_BODY}}
213
-
214
- Please:
215
- 1. Read the comment and understand what's being requested
216
- 2. Make any necessary changes
217
- 3. Commit and push your updates
218
- 4. Reply on the PR summarizing what you changed
219
-
220
- You are in the worktree at {{WORKTREE_PATH}}.
221
- PROMPT
222
-
223
- PROMPT_GITHUB_PR_REVIEW = <<~'PROMPT'
224
- A code review has been submitted on your PR #{{PR_NUMBER}} for card #{{CARD_NUMBER}}.
225
-
226
- {{REVIEW_CONTEXT}}
227
-
228
- Please:
229
- 1. Read the review comments carefully
230
- 2. Address each piece of feedback
231
- 3. Make the necessary code changes
232
- 4. Commit and push your updates
233
- 5. Post a comment on the PR summarizing the changes
234
-
235
- You are in the worktree at {{WORKTREE_PATH}}.
236
- PROMPT
237
-
238
163
  # ---------------------------------------------------------------------------
239
164
  # Channel constant mapping for render_prompt
165
+ # NOTE: GitHub prompts have been extracted to brainiac-github plugin.
166
+ # The plugin registers via Brainiac.register_channel_prompt(:github, ...)
240
167
  # ---------------------------------------------------------------------------
241
- PROMPT_GITHUB_UAT = <<~'PROMPT'
242
- PR #{{PR_NUMBER}} has been merged into main for card #{{CARD_NUMBER}}: "{{CARD_TITLE}}"
243
168
 
244
- The card has been moved to the UAT column. The changes are now deployed to the UAT environment.
245
-
246
- Your job: post a comment on card #{{CARD_NUMBER}} with clear, specific steps for how to manually test this feature in UAT. Include:
247
- 1. What URL(s) or screen(s) to visit
248
- 2. Step-by-step actions to verify the feature works
249
- 3. What the expected behavior should be
250
- 4. Any edge cases worth checking
251
- 5. Links to relevant pages if applicable (use the UAT/staging URL, not localhost)
252
-
253
- Base your testing steps on the card title, the PR diff, and any card context provided. Be specific — "verify it works" is not a testing step.
254
-
255
- Do NOT make any code changes. This is a read-only review task.
256
- PROMPT
257
-
258
- CHANNEL_PROMPTS = {
259
- github: PROMPT_GITHUB_CHANNEL
260
- }.freeze
169
+ CHANNEL_PROMPTS = {}.freeze
261
170
 
262
171
  # ---------------------------------------------------------------------------
263
172
  # render_prompt — composes PROMPT_CORE + channel rules + situation template
@@ -266,7 +175,7 @@ CHANNEL_PROMPTS = {
266
175
  # ---------------------------------------------------------------------------
267
176
 
268
177
  # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
269
- def render_prompt(template, vars = {}, brain_context: "", card_context: "", agent_name: AI_AGENT_NAME, channel: :discord, board_key: nil)
178
+ def render_prompt(template, vars = {}, brain_context: "", card_context: "", agent_name: AI_AGENT_NAME, channel: nil, board_key: nil)
270
179
  result = ""
271
180
  result += "#{brain_context}\n" unless brain_context.empty?
272
181
  result += card_context unless card_context.empty?
@@ -278,16 +187,12 @@ def render_prompt(template, vars = {}, brain_context: "", card_context: "", agen
278
187
 
279
188
  result += template
280
189
 
281
- # Pre-post comment check: plugin-registered or built-in
190
+ # Pre-post comment check: plugin-registered
282
191
  plugin_pre_post = Brainiac.channel_pre_post_checks[channel]
283
- if plugin_pre_post
284
- result += plugin_pre_post
285
- elsif channel == :github
286
- result += PROMPT_PRE_POST_CHECK_GITHUB
287
- end
192
+ result += plugin_pre_post if plugin_pre_post
288
193
 
289
- # Reflection prompt — skip for Discord (causes crashes in post-task phase)
290
- result += PROMPT_REFLECTION unless channel == :discord
194
+ # Reflection prompt — skip for now
195
+ # result += PROMPT_REFLECTION
291
196
 
292
197
  vars["KNOWLEDGE_DIR"] ||= KNOWLEDGE_DIR
293
198
  vars["MEMORY_DIR"] ||= memory_dir_for(agent_name)
@@ -345,30 +250,3 @@ def render_resume_prompt(comment_body:, comment_creator:, comment_id:, card_numb
345
250
 
346
251
  lines.join("\n")
347
252
  end
348
-
349
- # Lean resume prompt for Discord threads. The previous session has full context
350
- # (role, persona, knowledge, instructions). We only send the new message + channel history.
351
- def render_discord_resume_prompt(message_body:, discord_user:, response_file:, agent_name: AI_AGENT_NAME, card_id: nil)
352
- memory_dir = memory_dir_for(agent_name)
353
- if card_id
354
- memory_file = File.join(memory_dir, "card-#{card_id}.md")
355
- FileUtils.mkdir_p(memory_dir)
356
- FileUtils.touch(memory_file)
357
- end
358
-
359
- lines = []
360
- lines << "## Resumed Session — New Discord Message"
361
- lines << ""
362
- lines << "This is a continuation of your previous session in this thread."
363
- lines << "All prior context, instructions, and your previous work are still in this conversation."
364
- lines << ""
365
- lines << "### New Message from #{discord_user}"
366
- lines << ""
367
- lines << message_body
368
- lines << ""
369
- lines << "---"
370
- lines << "**IMPORTANT: Write your response to `#{response_file}`. Do NOT reply via stdout.**"
371
- lines << "All your previous instructions still apply (memory, persona, one message per session, etc.)."
372
-
373
- lines.join("\n")
374
- end
@@ -5,9 +5,6 @@
5
5
  # When an agent works on brainiac itself (modifies code), a restart is queued.
6
6
  # A background thread checks every 30s and only restarts when no other agents
7
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
8
 
12
9
  BRAINIAC_RESTART_STATE = { queued: false, triggered_by: nil }
13
10
  BRAINIAC_RESTART_MUTEX = Mutex.new
@@ -30,16 +30,10 @@ def format_active_sessions
30
30
  end
31
31
  end
32
32
 
33
- def resolve_session_agent_name(card_key, info)
33
+ def resolve_session_agent_name(_card_key, info)
34
34
  return info[:agent_name] if info[:agent_name]
35
35
 
36
- parts = card_key.split("-")
37
- agent_key = if parts[0] == "discord" && parts.size >= 4
38
- parts[1]
39
- else
40
- "Unknown"
41
- end
42
- agent_display_name(agent_key)
36
+ agent_display_name("Unknown")
43
37
  end
44
38
 
45
39
  def format_recent_sessions
@@ -70,20 +64,6 @@ rescue Errno::EPERM
70
64
  halt 403, { error: "permission denied" }.to_json
71
65
  end
72
66
 
73
- def search_giphy(query, api_key)
74
- uri = URI("https://api.giphy.com/v1/gifs/search")
75
- uri.query = URI.encode_www_form(api_key: api_key, q: query, limit: 5, rating: "pg-13")
76
- response = Net::HTTP.get_response(uri)
77
-
78
- if response.code.to_i == 200
79
- results = JSON.parse(response.body)["data"] || []
80
- results.map { |g| { url: g.dig("images", "original", "url") || g["url"], title: g["title"] } }
81
- else
82
- LOG.warn "[GIF] Giphy API returned #{response.code}: #{response.body[0..200]}"
83
- nil
84
- end
85
- end
86
-
87
67
  # --- Projects ---
88
68
 
89
69
  get "/api/projects" do
@@ -108,7 +88,6 @@ post "/api/reload" do
108
88
  reload_projects!(force: true)
109
89
  reload_agent_registry!(force: true)
110
90
  reload_user_registry!(force: true)
111
- reload_github_config!(force: true)
112
91
  ReloadHooks.run_all!
113
92
  { status: "reloaded", projects: PROJECTS.keys, agents: all_agent_names.to_a, registry: AGENT_REGISTRY.keys,
114
93
  users: USER_REGISTRY["users"].size }.to_json
@@ -283,13 +262,6 @@ get "/api/logs" do
283
262
  all_lines.join.gsub(/\e\[[\d;]*[a-zA-Z]/, "").gsub(/\e\[\?[\d;]*[a-zA-Z]/, "")
284
263
  end
285
264
 
286
- # --- GIF (handled by discord plugin when installed) ---
287
-
288
- get "/api/gif" do
289
- content_type :json
290
- halt 503, { error: "brainiac-discord plugin not installed (provides GIF API)" }.to_json
291
- end
292
-
293
265
  # --- Cron ---
294
266
 
295
267
  get "/api/cron/script" do
@@ -332,7 +304,8 @@ post "/api/cron/add" do
332
304
  script: payload["script"],
333
305
  model: payload["model"],
334
306
  effort: payload["effort"],
335
- discord_channel_id: payload["discord_channel_id"],
307
+ notify_channel: payload["notify_channel"],
308
+ notify_target: payload["notify_target"],
336
309
  forum_title: payload["forum_title"],
337
310
  forum_reply_to_latest: payload["forum_reply_to_latest"] || false,
338
311
  repeat_count: payload["repeat_count"]
@@ -367,7 +340,8 @@ post "/api/cron/update" do
367
340
  result = update_cron_job(
368
341
  payload["id"],
369
342
  schedule: payload["schedule"],
370
- discord_channel_id: payload["discord_channel_id"],
343
+ notify_target: payload["notify_target"],
344
+ notify_channel: payload["notify_channel"],
371
345
  forum_title: payload["forum_title"],
372
346
  forum_reply_to_latest: payload["forum_reply_to_latest"]
373
347
  )
@@ -179,8 +179,7 @@ def register_session(card_key, pid, log_file: nil, message_id: nil, channel_id:
179
179
  end
180
180
  end
181
181
 
182
- # --- Session supersede (Discord follow-up within window kills previous run) ---
183
-
182
+ # --- Session supersede (Channel follow-up within window kills previous run) ---
184
183
  SUPERSEDE_WINDOW = 60 # seconds
185
184
 
186
185
  # Find an active session for the same supersede key (agent+channel) started within the window.
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Brainiac
4
4
  # @return [String] the current gem version
5
- VERSION = "0.0.11"
5
+ VERSION = "0.0.13"
6
6
  end
data/monitor/shared.rb CHANGED
@@ -21,7 +21,7 @@ BRAINIAC_DIR = File.expand_path("~/.brainiac")
21
21
  # --- Agent Config ---
22
22
 
23
23
  # Load agent configuration from ~/.brainiac/waybar.json.
24
- # Returns { "galen" => { emoji: "🤖", color: "blue" }, ... }
24
+ # Returns { "sherlock" => { emoji: "🤖", color: "blue" }, ... }
25
25
  def load_agent_config
26
26
  config = JSON.parse(File.read(CONFIG_PATH, encoding: "utf-8"))
27
27
  agents = {}