brainiac 0.3.0 → 0.4.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6acdbbb2c88f69d1ca544dca2cb71afb7d4abd3963e4ffb93c1ea90666d4f966
4
- data.tar.gz: 738962e302868862b6be89f4cb65f300b0140dc15bfaf1ff258f286492c9c45e
3
+ metadata.gz: 1e2fc609dfdad411c1262f53783c3d6d88de4dbb75c83ccb9cedf6b935555a42
4
+ data.tar.gz: 6c7f6aed1727d45ba3bd953554598a20222c3ed47a81879b66108fbe2e7abb98
5
5
  SHA512:
6
- metadata.gz: c31b44489df80beab3cb6ed1e5bc7308e1e1926d78793bc73bb9c339ab06a98375ee1b8be1b70e031cdd3c1433c2e9e088976afe96343a08449600868eb206a1
7
- data.tar.gz: dabb28067671721b8890aea7a9b5a67d4a3a4bfc434836fcdd41e3cc13c5f7ee05efc6b93acc43dbf09c426b01749d5f68bcb39bfae451a8605ad9e8a4778703
6
+ metadata.gz: 68db7910a0cca76489408685a40f1fe549eaa6462ddefad4b532160b2ef8bd80c798a79b84c4daaeb538e52cc4df3412fca8f61862c39d94a49e9c080cb83e95
7
+ data.tar.gz: ab36e6f636e4ac76b28bfa55a418d1c00f2251adc36663e9fe21786150d3a27aa7d870bf4c225ffc47303873af38c76ed7601166be9d9ce88a2f30e372770e3b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.2 — 2026-11-02
4
+
5
+ - Add filesystem-search guardrail to PROMPT_CORE — forbids `find /`, `find ~`, `grep -r /`, and `locate`, which hang indefinitely on macOS (autofs, iCloud, sealed sim volumes). Teaches agents to use `gem which` / `Gem.loaded_specs` and scope `find` to known gem/repo roots with a depth limit.
6
+
3
7
  ## 0.9.1 — 2026-04-21
4
8
 
5
9
  - Add subagent delegation section to PROMPT_CORE — teaches agents when/how to use `use_subagent` tool, with `brainiac list` for on-demand project discovery
data/bin/brainiac CHANGED
@@ -15,7 +15,7 @@ BRAINIAC_ROOT = File.expand_path("..", __dir__)
15
15
  require_relative "../lib/brainiac/version"
16
16
  BRAINIAC_VERSION = Brainiac::VERSION
17
17
 
18
- BRAINIAC_DIR = File.join(Dir.home, ".brainiac")
18
+ BRAINIAC_DIR = ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac"))
19
19
  PROJECTS_FILE = File.join(BRAINIAC_DIR, "projects.json")
20
20
  CONFIG_FILE = File.join(BRAINIAC_DIR, "config.json")
21
21
  PID_FILE = File.join(BRAINIAC_DIR, "server.pid")
@@ -193,27 +193,135 @@ rescue StandardError
193
193
  nil
194
194
  end
195
195
 
196
- # Run CLI-time plugin hooks for agent lifecycle events.
197
- # Loads each installed plugin's CLI module and calls the hook method if defined.
198
- # This runs synchronously and can prompt for interactive input.
199
- def notify_plugins_agent_created(agent_key, entry)
196
+ # Installed plugins whose CLI module implements on_agent_created, as
197
+ # [name, module] pairs in plugins.json order.
198
+ def agent_setup_plugins
200
199
  plugins_file = File.join(BRAINIAC_DIR, "plugins.json")
201
- return unless File.exist?(plugins_file)
200
+ return [] unless File.exist?(plugins_file)
202
201
 
203
- plugins_config = JSON.parse(File.read(plugins_file))
204
- (plugins_config["plugins"] || []).each do |pentry|
202
+ (JSON.parse(File.read(plugins_file))["plugins"] || []).filter_map do |pentry|
205
203
  pname = pentry.is_a?(Hash) ? pentry["name"] : pentry.to_s
206
204
  try_require_metadata(pname, pentry)
207
205
  try_require_plugin_cli(pname, pentry)
208
206
  mod = resolve_plugin_module_for(pname)
209
- next unless mod.respond_to?(:on_agent_created)
207
+ [pname, mod] if mod.respond_to?(:on_agent_created)
208
+ rescue StandardError, LoadError => e
209
+ puts " ⚠ Plugin '#{pname}' couldn't load: #{e.message}"
210
+ nil
211
+ end
212
+ rescue JSON::ParserError
213
+ []
214
+ end
215
+
216
+ # Run each plugin's on_agent_created hook (Discord bot, Fizzy user, ...).
217
+ # Runs synchronously and can prompt for interactive input. Plugins in `skip`
218
+ # are left alone; when `only` is given, just those plugins run. With
219
+ # `confirm`, each plugin is asked about first and a "no" is added to `skip`
220
+ # (the caller persists it so `agent setup` won't ask again).
221
+ def notify_plugins_agent_created(agent_key, entry, skip: [], only: nil, confirm: false)
222
+ agent_setup_plugins.each do |pname, mod|
223
+ if only ? !only.include?(pname) : skip.include?(pname)
224
+ puts " [#{pname}] skipped"
225
+ next
226
+ end
227
+
228
+ if confirm
229
+ print "\nSet up #{pname} for #{entry["display_name"] || agent_key}? [Y/n]: "
230
+ if $stdin.gets.to_s.strip.downcase.start_with?("n")
231
+ skip << pname
232
+ next
233
+ end
234
+ end
210
235
 
211
236
  mod.on_agent_created(agent_key, entry)
212
237
  rescue StandardError => e
213
238
  puts " ⚠ Plugin '#{pname}' hook failed: #{e.message}"
214
239
  end
215
- rescue JSON::ParserError
216
- nil
240
+ end
241
+
242
+ # "--skip basecamp,fizzy" style values → ["basecamp", "fizzy"]
243
+ def plugin_list_arg(value)
244
+ value.to_s.split(",").map { |p| p.strip.downcase.delete_prefix("brainiac-") }.reject(&:empty?)
245
+ end
246
+
247
+ AVATAR_EXTENSIONS = %w[.png .jpg .jpeg .gif .webp].freeze
248
+
249
+ # Resolve a description flag value: "-" reads stdin, "@path" reads a file,
250
+ # anything else is the literal text.
251
+ def read_description_arg(value)
252
+ return $stdin.read.strip if value == "-"
253
+ return File.read(File.expand_path(value[1..])).strip if value.start_with?("@")
254
+
255
+ value
256
+ end
257
+
258
+ # Open $VISUAL/$EDITOR on a scratch file so a multi-paragraph persona can be
259
+ # written comfortably. HTML comment lines are instructions and get stripped.
260
+ # Returns nil when the result is blank.
261
+ def edit_description_in_editor(display_name, initial = nil)
262
+ require "tempfile"
263
+ require "shellwords"
264
+ editor = ENV["VISUAL"] || ENV["EDITOR"] || "vi"
265
+ Tempfile.create(["#{display_name.downcase}-persona", ".md"]) do |f|
266
+ f.write(initial || <<~MD)
267
+ <!-- Describe #{display_name}: personality, tone, voice, what they focus on. -->
268
+ <!-- Paragraphs and markdown are fine. Lines like this one are removed. Save and quit when done. -->
269
+
270
+ MD
271
+ f.flush
272
+ system(*Shellwords.split(editor), f.path)
273
+ text = File.read(f.path).gsub(/^\s*<!--.*?-->\s*\n?/, "").strip
274
+ text.empty? ? nil : text
275
+ end
276
+ end
277
+
278
+ # Default agent email from brainiac.json "agent_email_template",
279
+ # e.g. "agent+{key}@example.com". Returns nil when no template is configured.
280
+ def default_agent_email(agent_key, display_name)
281
+ config_path = File.join(BRAINIAC_DIR, "brainiac.json")
282
+ template = File.exist?(config_path) ? JSON.parse(File.read(config_path))["agent_email_template"] : nil
283
+ return nil if template.nil? || template.strip.empty?
284
+
285
+ template.gsub("{key}", agent_key).gsub("{name}", display_name)
286
+ end
287
+
288
+ # Copy an avatar image into the agent's persona dir (synced with the brain)
289
+ # and return the stored path.
290
+ def store_agent_avatar(agent_key, source)
291
+ path = File.expand_path(source)
292
+ ext = File.extname(path).downcase
293
+ raise ArgumentError, "Avatar not found: #{source}" unless File.file?(path)
294
+ raise ArgumentError, "Avatar must be one of #{AVATAR_EXTENSIONS.join(", ")}" unless AVATAR_EXTENSIONS.include?(ext)
295
+
296
+ persona_dir = File.join(BRAINIAC_DIR, "brain", "persona", agent_key)
297
+ FileUtils.mkdir_p(persona_dir)
298
+ dest = File.join(persona_dir, "avatar#{ext}")
299
+ FileUtils.cp(path, dest) unless File.identical?(path, dest)
300
+ dest
301
+ end
302
+
303
+ # The persona text for an agent, read back from its style.md (frontmatter and
304
+ # the generated "# Name — Persona" heading removed).
305
+ def agent_persona_description(agent_key)
306
+ file = File.join(BRAINIAC_DIR, "brain", "persona", agent_key, "style.md")
307
+ return nil unless File.exist?(file)
308
+
309
+ body = File.read(file).sub(/\A---\n.*?\n---\n/m, "").sub(/\A\s*# .*— Persona\n/, "").strip
310
+ body.empty? ? nil : body
311
+ end
312
+
313
+ # Payload handed to plugins' on_agent_created: the registry entry plus the
314
+ # setup inputs plugins need to provision accounts (avatar, description, email).
315
+ # "open_browser" tells plugins whether they may launch a browser for their
316
+ # manual steps (false with --no-browser, BRAINIAC_NO_BROWSER, or no terminal).
317
+ def agent_hook_payload(agent_key, entry, open_browser: true)
318
+ entry.merge(
319
+ "agent_key" => agent_key,
320
+ "avatar_path" => entry["avatar"],
321
+ "description" => agent_persona_description(agent_key),
322
+ "email" => entry["email"],
323
+ "open_browser" => open_browser && $stdin.tty? && !ENV["BRAINIAC_NO_BROWSER"]
324
+ ).compact
217
325
  end
218
326
 
219
327
  # Run CLI-time plugin hooks for agent removal.
@@ -561,6 +669,68 @@ def generate_plugin_skill_md(plugin_name, pascal_name)
561
669
  | `:detect_cli_provider` | Detect CLI provider |
562
670
  | `:detect_effort` | Detect effort level |
563
671
 
672
+ ## Agent Onboarding (`on_agent_created`)
673
+
674
+ `brainiac agent create` writes the brainiac files (agents.json, persona, brain),
675
+ then asks each plugin to provision the new agent on its service: a Discord bot,
676
+ a Fizzy user, a Basecamp person. `brainiac agent setup <name>` runs the same hook
677
+ again. If your plugin gives agents an identity on some service, implement it.
678
+
679
+ Define it in `cli.rb` (or a file `cli.rb` requires), since it runs in the CLI context:
680
+
681
+ ```ruby
682
+ # cli.rb
683
+ require_relative "agent_setup"
684
+
685
+ module Brainiac
686
+ module Plugins
687
+ module MyService
688
+ # Called by `brainiac agent create` / `brainiac agent setup`.
689
+ def self.on_agent_created(agent_key, entry)
690
+ AgentSetup.run(agent_key, entry)
691
+ end
692
+
693
+ def self.on_agent_removed(agent_key, display_name) # optional: clean up config
694
+ end
695
+ end
696
+ end
697
+ end
698
+ ```
699
+
700
+ `entry` is the agents.json entry plus:
701
+
702
+ | Key | Value |
703
+ |-----|-------|
704
+ | `agent_key` | Registry key, e.g. `quill` |
705
+ | `display_name` | e.g. `Quill` |
706
+ | `avatar_path` | Profile image (png/jpg/gif/webp) or absent |
707
+ | `description` | Persona text (can be several paragraphs) or absent |
708
+ | `email` | Email for the agent's accounts (from `--email` / `agent_email_template`) or absent |
709
+ | `open_browser` | `true` when you may launch a browser for a manual step |
710
+
711
+ Rules:
712
+
713
+ 1. **Idempotent.** `agent setup` re-runs every hook. Check your config first and
714
+ print `✓ <Name> already ... (skipping)` when the work is done.
715
+ 2. **Automate everything the API allows.** Only stop for steps the service truly
716
+ can't do by API (e.g. Discord has no create-application endpoint). Then print
717
+ numbered steps and the URL, and read the result with `$stdin.noecho(&:gets)`
718
+ for secrets.
719
+ 3. **Browser.** If `entry["open_browser"]` is true, open the URL as well as printing
720
+ it (`xdg-open` on Linux, `open` on macOS, spawned and detached). Don't open links
721
+ that must be used while signed in as someone else (e.g. a join link for the bot's
722
+ own account), because they'd land in the user's own session. Say "open in a
723
+ private window" instead.
724
+ 4. **Optional inputs.** Skip quietly if `avatar_path`, `description` or `email` is missing.
725
+ Return without error when `$stdin` isn't a TTY and you need input.
726
+ 5. **Never raise for expected failures.** Print `✗ ... re-run: brainiac agent setup <key>`.
727
+ Uncaught exceptions are reported as "hook failed" and the next plugin runs.
728
+ 6. **Prefix output** with ` [MyService]` so it's clear whose step is running.
729
+
730
+ Users choose which plugins run with `--skip` / `--only` (or by answering "n" when
731
+ asked during create). Core filters on the plugin name before calling you, so
732
+ plugins don't need to handle this.
733
+
564
734
  ## Channel Prompts
565
735
 
566
736
  For communication channel plugins:
@@ -2254,18 +2424,28 @@ when "agent"
2254
2424
  puts "Usage: brainiac agent create <name> [options]"
2255
2425
  puts ""
2256
2426
  puts "Options:"
2257
- puts " --local Mark agent as local (dispatches on this machine)"
2258
- puts " --role <role> Assign a role (repeatable)"
2259
- puts " --cli <provider> Set CLI provider (default: kiro)"
2260
- puts " --persona <text> Create a persona style file with this description"
2427
+ puts " --avatar <path> Profile picture (png/jpg/gif/webp), used by plugins (Discord, Fizzy)"
2428
+ puts " --description <text> Persona description (\"-\" reads stdin, \"@file\" reads a file)"
2429
+ puts " --description-file <path> Read the persona description from a file"
2430
+ puts " --email <address> Email for the agent's accounts (default: agent_email_template)"
2431
+ puts " --local Mark agent as local (dispatches on this machine)"
2432
+ puts " --role <role> Assign a role (repeatable)"
2433
+ puts " --cli <provider> Set CLI provider (default: kiro)"
2434
+ puts " --skip <plugins> Don't set up these plugins for this agent, e.g. --skip basecamp"
2435
+ puts " --only <plugins> Set up just these plugins, e.g. --only discord,fizzy"
2436
+ puts " --no-browser Print setup links instead of opening them"
2261
2437
  puts ""
2262
- puts "If no options are given and stdin is a terminal, interactive mode is used."
2438
+ puts "With no flags and a terminal, interactive mode is used. If no description is"
2439
+ puts "given on a terminal, $EDITOR opens so you can write a full persona."
2440
+ puts "After the brainiac files are written, each installed plugin sets up its part"
2441
+ puts "(Discord bot, Fizzy user, Basecamp person), asking first unless --skip/--only"
2442
+ puts "is given. Re-run with: brainiac agent setup <name>"
2263
2443
  puts ""
2264
2444
  puts "Examples:"
2265
- puts " brainiac agent create MyAgent # interactive"
2445
+ puts " brainiac agent create Quill --local --avatar quill.png # opens $EDITOR for the persona"
2446
+ puts " brainiac agent create Quill --local --avatar quill.png --description @quill.md"
2447
+ puts " brainiac agent create MyAgent # interactive"
2266
2448
  puts " brainiac agent create SecurityBot --local --role code-reviewer"
2267
- puts " brainiac agent create Jane --role general-engineer --cli grok"
2268
- puts " brainiac agent create Merlin --persona \"Wise, enigmatic, helpful\""
2269
2449
  exit 1
2270
2450
  end
2271
2451
 
@@ -2273,7 +2453,8 @@ when "agent"
2273
2453
  registry = File.exist?(agent_registry_file) ? JSON.parse(File.read(agent_registry_file)) : {}
2274
2454
 
2275
2455
  if registry[agent_key]
2276
- puts "Agent '#{name}' already exists. Use 'brainiac agent #{agent_key} show' to view."
2456
+ puts "Agent '#{name}' already exists. Use 'brainiac agent #{agent_key} show' to view,"
2457
+ puts "or 'brainiac agent setup #{agent_key}' to finish plugin setup."
2277
2458
  exit 1
2278
2459
  end
2279
2460
 
@@ -2281,6 +2462,11 @@ when "agent"
2281
2462
  roles = []
2282
2463
  cli_provider = nil
2283
2464
  persona_text = nil
2465
+ avatar = nil
2466
+ email = nil
2467
+ skip_plugins = []
2468
+ only_plugins = nil
2469
+ open_browser = true
2284
2470
 
2285
2471
  if ARGV.any?
2286
2472
  # Flag mode
@@ -2295,9 +2481,27 @@ when "agent"
2295
2481
  when "--cli"
2296
2482
  ARGV.shift
2297
2483
  cli_provider = ARGV.shift
2298
- when "--persona"
2484
+ when "--description", "--persona"
2485
+ ARGV.shift
2486
+ persona_text = read_description_arg(ARGV.shift.to_s)
2487
+ when "--description-file"
2488
+ ARGV.shift
2489
+ persona_text = File.read(File.expand_path(ARGV.shift.to_s)).strip
2490
+ when "--avatar"
2299
2491
  ARGV.shift
2300
- persona_text = ARGV.shift
2492
+ avatar = ARGV.shift
2493
+ when "--email"
2494
+ ARGV.shift
2495
+ email = ARGV.shift
2496
+ when "--skip"
2497
+ ARGV.shift
2498
+ skip_plugins.concat(plugin_list_arg(ARGV.shift))
2499
+ when "--only"
2500
+ ARGV.shift
2501
+ only_plugins = plugin_list_arg(ARGV.shift)
2502
+ when "--no-browser"
2503
+ ARGV.shift
2504
+ open_browser = false
2301
2505
  else
2302
2506
  puts "Unknown option: #{ARGV[0]}"
2303
2507
  exit 1
@@ -2327,15 +2531,39 @@ when "agent"
2327
2531
  input = $stdin.gets.chomp
2328
2532
  roles = input.empty? ? [] : input.split(",").map(&:strip)
2329
2533
 
2330
- print "Persona description (tone/style, or blank to skip) []: "
2534
+ print "Avatar image path (or blank to skip) []: "
2535
+ input = $stdin.gets.chomp
2536
+ avatar = input.empty? ? nil : input
2537
+
2538
+ default_email = default_agent_email(agent_key, name)
2539
+ print "Email for the agent's accounts#{" [#{default_email}]" if default_email}: "
2331
2540
  input = $stdin.gets.chomp
2332
- persona_text = input.empty? ? nil : input
2541
+ email = input.empty? ? default_email : input
2333
2542
  end
2334
2543
 
2544
+ # A persona is usually a paragraph or more — give it a real editor.
2545
+ if persona_text.nil? && $stdin.tty?
2546
+ print "Write #{name}'s persona description in #{ENV["VISUAL"] || ENV["EDITOR"] || "vi"}? [Y/n]: "
2547
+ persona_text = edit_description_in_editor(name) unless $stdin.gets.to_s.strip.downcase.start_with?("n")
2548
+ end
2549
+
2550
+ email ||= default_agent_email(agent_key, name)
2551
+
2335
2552
  entry = { "display_name" => name }
2336
2553
  entry["local"] = true if local
2337
2554
  entry["cli_provider"] = cli_provider if cli_provider
2338
2555
  entry["role"] = roles.size == 1 ? roles.first : roles if roles.any?
2556
+ entry["email"] = email if email
2557
+ entry["skip_plugins"] = skip_plugins.uniq if skip_plugins.any?
2558
+
2559
+ if avatar
2560
+ begin
2561
+ entry["avatar"] = store_agent_avatar(agent_key, avatar)
2562
+ rescue ArgumentError => e
2563
+ puts "❌ #{e.message}"
2564
+ exit 1
2565
+ end
2566
+ end
2339
2567
 
2340
2568
  registry[agent_key] = entry
2341
2569
  File.write(agent_registry_file, JSON.pretty_generate(registry))
@@ -2343,6 +2571,8 @@ when "agent"
2343
2571
  puts " Local: #{local}" if local
2344
2572
  puts " Role: #{roles.join(", ")}" if roles.any?
2345
2573
  puts " CLI: #{cli_provider}" if cli_provider
2574
+ puts " Email: #{email}" if email
2575
+ puts " Avatar: #{entry["avatar"]}" if entry["avatar"]
2346
2576
 
2347
2577
  # Create persona directory and style file if requested
2348
2578
  if persona_text
@@ -2356,6 +2586,7 @@ when "agent"
2356
2586
  description: Persona voice for #{name}.
2357
2587
  ---
2358
2588
  # #{name} — Persona
2589
+
2359
2590
  #{persona_text}
2360
2591
  MD
2361
2592
  puts " Persona: #{persona_file}"
@@ -2368,12 +2599,82 @@ when "agent"
2368
2599
  brain_init(name)
2369
2600
  end
2370
2601
 
2371
- # Let plugins prompt for additional config (e.g. Discord user ID)
2372
- notify_plugins_agent_created(agent_key, entry)
2602
+ # Each plugin provisions its side (Discord bot, Fizzy user, Basecamp person, ...).
2603
+ # Without --skip/--only, a terminal user is asked per plugin; "no" answers
2604
+ # are saved so `agent setup` leaves those plugins alone too.
2605
+ confirm = $stdin.tty? && skip_plugins.empty? && only_plugins.nil?
2606
+ notify_plugins_agent_created(agent_key, agent_hook_payload(agent_key, entry, open_browser: open_browser),
2607
+ skip: skip_plugins, only: only_plugins, confirm: confirm)
2608
+ if skip_plugins.any? && entry["skip_plugins"] != skip_plugins.uniq
2609
+ entry["skip_plugins"] = skip_plugins.uniq
2610
+ registry[agent_key] = entry
2611
+ File.write(agent_registry_file, JSON.pretty_generate(registry))
2612
+ puts " Skipping #{skip_plugins.uniq.join(", ")} for #{name} (undo: brainiac agent setup #{agent_key} --only <plugin>)"
2613
+ end
2373
2614
 
2374
2615
  # Notify running server to reload and trigger :agent_added hook
2375
2616
  notify_server_reload
2376
2617
 
2618
+ when "setup"
2619
+ name = ARGV.shift
2620
+ unless name
2621
+ puts "Usage: brainiac agent setup <name> [--avatar <path>] [--email <address>] [--skip <plugins>] [--only <plugins>] [--no-browser]"
2622
+ puts ""
2623
+ puts "Re-runs every plugin's agent setup (Discord bot, Fizzy user, Basecamp person)."
2624
+ puts "Steps that are already done are skipped, so it's safe to run after an"
2625
+ puts "interrupted 'agent create' or to set up a newly installed plugin."
2626
+ puts ""
2627
+ puts " --skip a,b Never set up these plugins for this agent (saved)"
2628
+ puts " --only a,b Run just these plugins (also un-skips them)"
2629
+ puts " --no-browser Print links instead of opening them"
2630
+ exit 1
2631
+ end
2632
+
2633
+ agent_key = name.downcase.gsub(/[^a-z0-9-]/, "-")
2634
+ registry = File.exist?(agent_registry_file) ? JSON.parse(File.read(agent_registry_file)) : {}
2635
+ entry = registry[agent_key]
2636
+ unless entry
2637
+ puts "Agent '#{name}' not found in registry."
2638
+ exit 1
2639
+ end
2640
+
2641
+ only_plugins = nil
2642
+ open_browser = true
2643
+ while ARGV.any?
2644
+ case ARGV.shift
2645
+ when "--skip"
2646
+ entry["skip_plugins"] = ((entry["skip_plugins"] || []) + plugin_list_arg(ARGV.shift)).uniq
2647
+ when "--only"
2648
+ only_plugins = plugin_list_arg(ARGV.shift)
2649
+ when "--no-browser"
2650
+ open_browser = false
2651
+ when "--avatar"
2652
+ begin
2653
+ entry["avatar"] = store_agent_avatar(agent_key, ARGV.shift.to_s)
2654
+ rescue ArgumentError => e
2655
+ puts "❌ #{e.message}"
2656
+ exit 1
2657
+ end
2658
+ when "--email"
2659
+ entry["email"] = ARGV.shift
2660
+ else
2661
+ puts "Unknown option. Run 'brainiac agent setup' for usage."
2662
+ exit 1
2663
+ end
2664
+ end
2665
+ entry["email"] ||= default_agent_email(agent_key, entry["display_name"] || name)
2666
+ entry.delete("email") unless entry["email"]
2667
+ # Explicitly running a plugin with --only un-skips it for good.
2668
+ entry["skip_plugins"] -= only_plugins if only_plugins && entry["skip_plugins"]
2669
+ entry.delete("skip_plugins") if entry["skip_plugins"] == []
2670
+ registry[agent_key] = entry
2671
+ File.write(agent_registry_file, JSON.pretty_generate(registry))
2672
+
2673
+ puts "Setting up #{entry["display_name"] || name} (#{agent_key})"
2674
+ notify_plugins_agent_created(agent_key, agent_hook_payload(agent_key, entry, open_browser: open_browser),
2675
+ skip: entry["skip_plugins"] || [], only: only_plugins)
2676
+ notify_server_reload
2677
+
2377
2678
  when "remove", "delete", "rm"
2378
2679
  name = ARGV.shift
2379
2680
  unless name
@@ -2407,12 +2708,14 @@ when "agent"
2407
2708
  brainiac agent list
2408
2709
  brainiac agent default [name]
2409
2710
  brainiac agent create <name> [options]
2711
+ brainiac agent setup <name> [--skip|--only <plugins>]
2410
2712
  brainiac agent remove <name>
2411
2713
 
2412
2714
  Commands:
2413
2715
  list List all agents in the registry
2414
2716
  default [name] Show or set the default agent
2415
- create <name> [options] Add a new agent to the registry
2717
+ create <name> [options] Add a new agent and run every plugin's setup
2718
+ setup <name> Re-run plugin setup (skips steps already done)
2416
2719
  remove <name> Remove an agent from the registry
2417
2720
  <name> show Show agent configuration (tokens redacted)
2418
2721
  <name> set <field> <value> Update a config field (local, cli, role, persona, display_name)
@@ -2421,14 +2724,21 @@ when "agent"
2421
2724
  <name> env --delete <KEY> Remove an env var from an agent
2422
2725
 
2423
2726
  Create options (or interactive if no flags given):
2727
+ --avatar <path> Profile picture, used by plugins (Discord, Fizzy)
2728
+ --description <text> Persona description ("-" = stdin, "@file" = file; $EDITOR if omitted)
2729
+ --email <address> Email for the agent's accounts (default: agent_email_template)
2424
2730
  --local Mark agent as local (dispatches on this machine)
2425
2731
  --role <role> Assign a role (repeatable)
2426
2732
  --cli <provider> Set CLI provider (default: kiro)
2427
- --persona <text> Create a persona style file
2733
+ --skip <plugins> Don't set up these plugins for this agent (saved; also for setup)
2734
+ --only <plugins> Set up just these plugins (also for setup)
2735
+ --no-browser Print setup links instead of opening them
2428
2736
 
2429
2737
  Examples:
2430
2738
  brainiac agent list
2431
2739
  brainiac agent default Sherlock
2740
+ brainiac agent create Quill --local --avatar quill.png
2741
+ brainiac agent setup quill
2432
2742
  brainiac agent create SecurityBot --local --role code-reviewer
2433
2743
  brainiac agent create Jane --cli grok
2434
2744
  brainiac agent create MyAgent # interactive mode
@@ -3249,6 +3559,12 @@ when "plugin"
3249
3559
  Cli.run(args)
3250
3560
  end
3251
3561
 
3562
+ # Optional: provision a new agent on #{pascal_name} during
3563
+ # `brainiac agent create` / `brainiac agent setup`. Must be idempotent.
3564
+ # See "Agent Onboarding" in skills/#{plugin_name}/SKILL.md for the payload and rules.
3565
+ # def self.on_agent_created(agent_key, entry)
3566
+ # end
3567
+
3252
3568
  # Subcommand names for bash completion.
3253
3569
  def self.completions
3254
3570
  %w[config setup status]
@@ -109,14 +109,14 @@ _brainiac() {
109
109
  agent)
110
110
  case $cword in
111
111
  2)
112
- COMPREPLY=($(compgen -W "list create remove $(_brainiac_agents)" -- "$cur"))
112
+ COMPREPLY=($(compgen -W "list create setup remove $(_brainiac_agents)" -- "$cur"))
113
113
  ;;
114
114
  3)
115
115
  local agent_name="${words[2]}"
116
116
  case "$agent_name" in
117
- list|remove|delete|rm) ;;
117
+ list|remove|delete|rm|setup) ;;
118
118
  create|add)
119
- COMPREPLY=($(compgen -W "--local --role --cli --persona" -- "$cur"))
119
+ COMPREPLY=($(compgen -W "--avatar --description --description-file --email --local --role --cli --skip --only --no-browser" -- "$cur"))
120
120
  ;;
121
121
  *)
122
122
  COMPREPLY=($(compgen -W "show env" -- "$cur"))
@@ -58,6 +58,18 @@ PROMPT_CORE = <<~PROMPT
58
58
  Standard unix commands (cd, ls, grep, cat, git, curl, etc.) don't need a brain search.
59
59
  But for project-specific tools, do NOT guess at flags or syntax — wrong commands waste time and tokens. Look it up first.
60
60
 
61
+ **NEVER run a whole-filesystem search.** Do not run `find /`, `find ~`, `grep -r /`, `locate`, or any
62
+ command that walks the entire disk from root or home. On macOS these effectively never return — they
63
+ block on autofs automounts, iCloud Drive cloud-only files, sealed CoreSimulator volumes, and Spotlight
64
+ stores — and they pin a CPU for hours. To locate an installed gem or plugin, scope the search:
65
+ ```
66
+ gem which <gem-name> # exact file for a loaded gem
67
+ ruby -e 'puts Gem.loaded_specs.keys.grep(/<pattern>/)' # list matching loaded gems
68
+ # Only if those fail, scope find to known roots with a timeout and head:
69
+ find "$(gem env gemdir)" ~/.asdf/installs ./ -maxdepth 6 -path '*<pattern>*' -name '*.rb' 2>/dev/null | head
70
+ ```
71
+ Always point `find`/`grep -r` at a specific repo, gem root, or directory — never at `/` or `~`.
72
+
61
73
  **When to save knowledge:** Be selective — only save significant architecture decisions,
62
74
  non-obvious gotchas, major workflow changes, or things the user explicitly asks you to remember.
63
75
  Routine card work and things already documented in the codebase don't need brain entries.
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Brainiac
4
4
  # @return [String] the current gem version
5
- VERSION = "0.3.0"
5
+ VERSION = "0.4.0"
6
6
  end
@@ -110,6 +110,68 @@ Available hooks:
110
110
  | `:detect_cli_provider` | Detect CLI provider | metadata hash |
111
111
  | `:detect_effort` | Detect effort level | metadata hash |
112
112
 
113
+ ## Agent Onboarding (`on_agent_created`)
114
+
115
+ `brainiac agent create` writes the brainiac files (agents.json, persona, brain),
116
+ then asks each plugin to provision the new agent on its service: a Discord bot,
117
+ a Fizzy user, a Basecamp person. `brainiac agent setup <name>` runs the same hook
118
+ again. If your plugin gives agents an identity on some service, implement it.
119
+
120
+ Define it in `cli.rb` (or a file `cli.rb` requires), since it runs in the CLI context:
121
+
122
+ ```ruby
123
+ # cli.rb
124
+ require_relative "agent_setup"
125
+
126
+ module Brainiac
127
+ module Plugins
128
+ module MyService
129
+ # Called by `brainiac agent create` / `brainiac agent setup`.
130
+ def self.on_agent_created(agent_key, entry)
131
+ AgentSetup.run(agent_key, entry)
132
+ end
133
+
134
+ def self.on_agent_removed(agent_key, display_name) # optional: clean up config
135
+ end
136
+ end
137
+ end
138
+ end
139
+ ```
140
+
141
+ `entry` is the agents.json entry plus:
142
+
143
+ | Key | Value |
144
+ |-----|-------|
145
+ | `agent_key` | Registry key, e.g. `quill` |
146
+ | `display_name` | e.g. `Quill` |
147
+ | `avatar_path` | Profile image (png/jpg/gif/webp) or absent |
148
+ | `description` | Persona text (can be several paragraphs) or absent |
149
+ | `email` | Email for the agent's accounts (from `--email` / `agent_email_template`) or absent |
150
+ | `open_browser` | `true` when you may launch a browser for a manual step |
151
+
152
+ Rules:
153
+
154
+ 1. **Idempotent.** `agent setup` re-runs every hook. Check your config first and
155
+ print `✓ <Name> already ... (skipping)` when the work is done.
156
+ 2. **Automate everything the API allows.** Only stop for steps the service truly
157
+ can't do by API (e.g. Discord has no create-application endpoint). Then print
158
+ numbered steps and the URL, and read the result with `$stdin.noecho(&:gets)`
159
+ for secrets.
160
+ 3. **Browser.** If `entry["open_browser"]` is true, open the URL as well as printing
161
+ it (`xdg-open` on Linux, `open` on macOS, spawned and detached). Don't open links
162
+ that must be used while signed in as someone else (e.g. a join link for the bot's
163
+ own account), because they'd land in the user's own session. Say "open in a
164
+ private window" instead.
165
+ 4. **Optional inputs.** Skip quietly if `avatar_path`, `description` or `email` is missing.
166
+ Return without error when `$stdin` isn't a TTY and you need input.
167
+ 5. **Never raise for expected failures.** Print `✗ ... re-run: brainiac agent setup <key>`.
168
+ Uncaught exceptions are reported as "hook failed" and the next plugin runs.
169
+ 6. **Prefix output** with ` [MyService]` so it's clear whose step is running.
170
+
171
+ Users choose which plugins run with `--skip` / `--only` (or by answering "n" when
172
+ asked during create). Core filters on the plugin name before calling you, so
173
+ plugins don't need to handle this.
174
+
113
175
  ## Channel Prompts
114
176
 
115
177
  If your plugin is a communication channel (like Discord, Fizzy), register a prompt:
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brainiac
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis