brainiac 0.4.0 → 0.4.2
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 +4 -4
- data/bin/brainiac +259 -24
- data/bin/brainiac-completion.bash +1 -1
- data/lib/brainiac/agents.rb +12 -0
- data/lib/brainiac/hooks.rb +1 -0
- data/lib/brainiac/version.rb +1 -1
- data/skills/brainiac-plugins/SKILL.md +10 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ba8a88daeee5e47d89614674cc1855e4a3fb50ca9b7cb8b5f063fa40ef544aeb
|
|
4
|
+
data.tar.gz: e0dcd3bc1916e066f1da0b0827cac3952978e44451df5262308eb9003400f268
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e4a84e1cc4f055ad794ebde722e653af89376ab2f600cd7c35dee4cb5731f99d612b699e6aaf0b6f8e51857ae354ffd3b9627c234d0538c56e3400231b48f0a6
|
|
7
|
+
data.tar.gz: bb1dd4fe2cca8a4c84f73a042b0b7dc8d754d4718281dd849d6d3b966ee70c4a3bacddcabd21b6bfc111bd37c5c70a541142fd0443914f24181b75e3966d35e8
|
data/bin/brainiac
CHANGED
|
@@ -310,6 +310,114 @@ def agent_persona_description(agent_key)
|
|
|
310
310
|
body.empty? ? nil : body
|
|
311
311
|
end
|
|
312
312
|
|
|
313
|
+
# Markdown that already opens with YAML frontmatter is kept as written, so a
|
|
314
|
+
# hand-made style.md or role file isn't wrapped in a second frontmatter block.
|
|
315
|
+
def frontmatter?(text)
|
|
316
|
+
text.to_s.lstrip.start_with?("---\n")
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def available_roles
|
|
320
|
+
roles_dir = File.join(BRAINIAC_DIR, "roles")
|
|
321
|
+
Dir.exist?(roles_dir) ? Dir.glob(File.join(roles_dir, "*.md")).map { |f| File.basename(f, ".md") }.sort : []
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def role_name_arg(value)
|
|
325
|
+
value.to_s.downcase.gsub(/[^a-z0-9-]/, "-")
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Starting point for a new role, shaped like the built-in ones. HTML comment
|
|
329
|
+
# lines are instructions and get stripped by edit_description_in_editor.
|
|
330
|
+
def role_template(role_name, agent_name = nil)
|
|
331
|
+
title = role_name.split("-").map(&:capitalize).join(" ")
|
|
332
|
+
you = agent_name ? "You are #{agent_name}, the #{title.downcase}." : "You are the #{title.downcase}."
|
|
333
|
+
<<~MD
|
|
334
|
+
<!-- Role "#{role_name}": WHAT this agent does. HOW it talks belongs in its persona (style.md). -->
|
|
335
|
+
<!-- Fill in or delete sections. Lines like this one are removed. Save and quit when done; empty = cancel. -->
|
|
336
|
+
# Role: #{title}
|
|
337
|
+
|
|
338
|
+
#{you} Your primary responsibility is ...
|
|
339
|
+
|
|
340
|
+
## Primary Responsibilities
|
|
341
|
+
|
|
342
|
+
- **...**: ...
|
|
343
|
+
|
|
344
|
+
## Checklist
|
|
345
|
+
|
|
346
|
+
When taking on any task:
|
|
347
|
+
|
|
348
|
+
1. ...
|
|
349
|
+
|
|
350
|
+
## What You Don't Do
|
|
351
|
+
|
|
352
|
+
- ...
|
|
353
|
+
|
|
354
|
+
## Looping In Other Agents (opt-in ONLY)
|
|
355
|
+
|
|
356
|
+
Do NOT tag, @mention, or "flag for" other agents by default. Every cross-agent
|
|
357
|
+
mention triggers an automated dispatch that spends the user's credits.
|
|
358
|
+
Only bring another agent in when the user explicitly asks for it. If you think
|
|
359
|
+
another agent should weigh in, say so in plain prose WITHOUT an @mention.
|
|
360
|
+
MD
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Write ~/.brainiac/roles/<name>.md, adding frontmatter unless the text has its own.
|
|
364
|
+
def write_role_file(role_name, text, agent_name = nil)
|
|
365
|
+
roles_dir = File.join(BRAINIAC_DIR, "roles")
|
|
366
|
+
FileUtils.mkdir_p(roles_dir)
|
|
367
|
+
file = File.join(roles_dir, "#{role_name}.md")
|
|
368
|
+
body = text.strip
|
|
369
|
+
unless frontmatter?(body)
|
|
370
|
+
description = agent_name ? "Role definition for #{agent_name}" : "Role definition: #{role_name}"
|
|
371
|
+
body = "---\nname: #{role_name}\ndescription: #{description}\ninclusion: manual\n---\n\n#{body}"
|
|
372
|
+
end
|
|
373
|
+
File.write(file, "#{body}\n")
|
|
374
|
+
file
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Add roles to a registry entry, keeping the string-or-array shape.
|
|
378
|
+
def assign_roles(entry, new_roles)
|
|
379
|
+
roles = (Array(entry["role"]) + new_roles).uniq
|
|
380
|
+
entry["role"] = roles.size == 1 ? roles.first : roles if roles.any?
|
|
381
|
+
roles
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Interactive role step for create/setup: pick existing roles by number or
|
|
385
|
+
# name, write a new one in $EDITOR, or skip. Returns the chosen role names.
|
|
386
|
+
def prompt_for_agent_role(agent_key, display_name)
|
|
387
|
+
existing = available_roles
|
|
388
|
+
puts ""
|
|
389
|
+
puts "Role for #{display_name} (what they do; the persona is how they talk):"
|
|
390
|
+
existing.each_with_index { |r, i| puts " #{i + 1}. #{r}" }
|
|
391
|
+
puts " n. New role (opens #{ENV["VISUAL"] || ENV["EDITOR"] || "vi"})"
|
|
392
|
+
print "Choose #{"numbers/names (comma-separated), " if existing.any?}n, or blank to skip: "
|
|
393
|
+
input = $stdin.gets.to_s.strip
|
|
394
|
+
return [] if input.empty?
|
|
395
|
+
|
|
396
|
+
if input.downcase.start_with?("n") && !existing.include?(input)
|
|
397
|
+
print "Role name [#{agent_key}]: "
|
|
398
|
+
name = role_name_arg($stdin.gets.to_s.strip)
|
|
399
|
+
name = agent_key if name.empty?
|
|
400
|
+
if existing.include?(name)
|
|
401
|
+
puts " Role '#{name}' already exists; assigning it."
|
|
402
|
+
return [name]
|
|
403
|
+
end
|
|
404
|
+
text = edit_description_in_editor(name, role_template(name, display_name))
|
|
405
|
+
unless text
|
|
406
|
+
puts " Empty role, skipped."
|
|
407
|
+
return []
|
|
408
|
+
end
|
|
409
|
+
puts " Created #{write_role_file(name, text, display_name)}"
|
|
410
|
+
return [name]
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
picked = input.split(",").map(&:strip).filter_map do |token|
|
|
414
|
+
token.match?(/\A\d+\z/) ? existing[token.to_i - 1] : token
|
|
415
|
+
end
|
|
416
|
+
unknown = picked - existing
|
|
417
|
+
puts " Unknown role(s) ignored: #{unknown.join(", ")}" if unknown.any?
|
|
418
|
+
picked & existing
|
|
419
|
+
end
|
|
420
|
+
|
|
313
421
|
# Payload handed to plugins' on_agent_created: the registry entry plus the
|
|
314
422
|
# setup inputs plugins need to provision accounts (avatar, description, email).
|
|
315
423
|
# "open_browser" tells plugins whether they may launch a browser for their
|
|
@@ -668,6 +776,9 @@ def generate_plugin_skill_md(plugin_name, pascal_name)
|
|
|
668
776
|
| `:create_work_item` | Create card/issue/ticket |
|
|
669
777
|
| `:detect_cli_provider` | Detect CLI provider |
|
|
670
778
|
| `:detect_effort` | Detect effort level |
|
|
779
|
+
| `:agent_added` | New agent in agents.json (after reload) |
|
|
780
|
+
| `:agent_updated` | Existing agent's entry changed (after reload) |
|
|
781
|
+
| `:agent_removed` | Agent removed from agents.json (after reload) |
|
|
671
782
|
|
|
672
783
|
## Agent Onboarding (`on_agent_created`)
|
|
673
784
|
|
|
@@ -731,6 +842,13 @@ def generate_plugin_skill_md(plugin_name, pascal_name)
|
|
|
731
842
|
asked during create). Core filters on the plugin name before calling you, so
|
|
732
843
|
plugins don't need to handle this.
|
|
733
844
|
|
|
845
|
+
**Going live without a restart.** `agent create` / `agent setup` tell the running
|
|
846
|
+
server to reload `agents.json`. The server may already have seen the agent before
|
|
847
|
+
your hook wrote its token, so a server-side plugin that starts something per agent
|
|
848
|
+
(a gateway connection, a poller) must handle `:agent_updated` as well as
|
|
849
|
+
`:agent_added`: start it when the credential appears or changes, stop it when it's
|
|
850
|
+
removed.
|
|
851
|
+
|
|
734
852
|
## Channel Prompts
|
|
735
853
|
|
|
736
854
|
For communication channel plugins:
|
|
@@ -2430,13 +2548,16 @@ when "agent"
|
|
|
2430
2548
|
puts " --email <address> Email for the agent's accounts (default: agent_email_template)"
|
|
2431
2549
|
puts " --local Mark agent as local (dispatches on this machine)"
|
|
2432
2550
|
puts " --role <role> Assign a role (repeatable)"
|
|
2551
|
+
puts " --role-file <path> Create a role from a markdown file and assign it"
|
|
2552
|
+
puts " (named after the agent, or after a new --role name)"
|
|
2433
2553
|
puts " --cli <provider> Set CLI provider (default: kiro)"
|
|
2434
2554
|
puts " --skip <plugins> Don't set up these plugins for this agent, e.g. --skip basecamp"
|
|
2435
2555
|
puts " --only <plugins> Set up just these plugins, e.g. --only discord,fizzy"
|
|
2436
2556
|
puts " --no-browser Print setup links instead of opening them"
|
|
2437
2557
|
puts ""
|
|
2438
2558
|
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."
|
|
2559
|
+
puts "given on a terminal, $EDITOR opens so you can write a full persona. Without"
|
|
2560
|
+
puts "--role/--role-file you're asked to pick an existing role or write a new one."
|
|
2440
2561
|
puts "After the brainiac files are written, each installed plugin sets up its part"
|
|
2441
2562
|
puts "(Discord bot, Fizzy user, Basecamp person), asking first unless --skip/--only"
|
|
2442
2563
|
puts "is given. Re-run with: brainiac agent setup <name>"
|
|
@@ -2446,6 +2567,8 @@ when "agent"
|
|
|
2446
2567
|
puts " brainiac agent create Quill --local --avatar quill.png --description @quill.md"
|
|
2447
2568
|
puts " brainiac agent create MyAgent # interactive"
|
|
2448
2569
|
puts " brainiac agent create SecurityBot --local --role code-reviewer"
|
|
2570
|
+
puts " brainiac agent create \"Judge Dredd\" --local --avatar dredd.png \\"
|
|
2571
|
+
puts " --description-file dredd-style.md --role-file dredd-role.md"
|
|
2449
2572
|
exit 1
|
|
2450
2573
|
end
|
|
2451
2574
|
|
|
@@ -2460,6 +2583,7 @@ when "agent"
|
|
|
2460
2583
|
|
|
2461
2584
|
local = false
|
|
2462
2585
|
roles = []
|
|
2586
|
+
role_file = nil
|
|
2463
2587
|
cli_provider = nil
|
|
2464
2588
|
persona_text = nil
|
|
2465
2589
|
avatar = nil
|
|
@@ -2477,7 +2601,10 @@ when "agent"
|
|
|
2477
2601
|
local = true
|
|
2478
2602
|
when "--role"
|
|
2479
2603
|
ARGV.shift
|
|
2480
|
-
roles << ARGV.shift
|
|
2604
|
+
roles << role_name_arg(ARGV.shift)
|
|
2605
|
+
when "--role-file"
|
|
2606
|
+
ARGV.shift
|
|
2607
|
+
role_file = File.expand_path(ARGV.shift.to_s)
|
|
2481
2608
|
when "--cli"
|
|
2482
2609
|
ARGV.shift
|
|
2483
2610
|
cli_provider = ARGV.shift
|
|
@@ -2523,14 +2650,6 @@ when "agent"
|
|
|
2523
2650
|
input = $stdin.gets.chomp
|
|
2524
2651
|
cli_provider = input.empty? ? nil : input
|
|
2525
2652
|
|
|
2526
|
-
# Show available roles
|
|
2527
|
-
roles_dir = File.join(BRAINIAC_DIR, "roles")
|
|
2528
|
-
available_roles = Dir.exist?(roles_dir) ? Dir.glob(File.join(roles_dir, "*.md")).map { |f| File.basename(f, ".md") } : []
|
|
2529
|
-
hint = available_roles.any? ? " (#{available_roles.join(", ")})" : ""
|
|
2530
|
-
print "Roles#{hint} (comma-separated) []: "
|
|
2531
|
-
input = $stdin.gets.chomp
|
|
2532
|
-
roles = input.empty? ? [] : input.split(",").map(&:strip)
|
|
2533
|
-
|
|
2534
2653
|
print "Avatar image path (or blank to skip) []: "
|
|
2535
2654
|
input = $stdin.gets.chomp
|
|
2536
2655
|
avatar = input.empty? ? nil : input
|
|
@@ -2547,6 +2666,30 @@ when "agent"
|
|
|
2547
2666
|
persona_text = edit_description_in_editor(name) unless $stdin.gets.to_s.strip.downcase.start_with?("n")
|
|
2548
2667
|
end
|
|
2549
2668
|
|
|
2669
|
+
new_role = nil
|
|
2670
|
+
if role_file && !File.file?(role_file)
|
|
2671
|
+
puts "❌ Role file not found: #{role_file}"
|
|
2672
|
+
exit 1
|
|
2673
|
+
end
|
|
2674
|
+
if role_file
|
|
2675
|
+
# --role-file names its role after a --role that doesn't exist yet, else the agent key.
|
|
2676
|
+
new_role = (roles - available_roles).first || agent_key
|
|
2677
|
+
if available_roles.include?(new_role)
|
|
2678
|
+
puts "❌ Role '#{new_role}' already exists. Pass --role <new-name> to name the role from #{role_file}."
|
|
2679
|
+
exit 1
|
|
2680
|
+
end
|
|
2681
|
+
roles |= [new_role]
|
|
2682
|
+
elsif roles.empty? && $stdin.tty?
|
|
2683
|
+
roles = prompt_for_agent_role(agent_key, name)
|
|
2684
|
+
end
|
|
2685
|
+
|
|
2686
|
+
missing_roles = roles - available_roles - [new_role]
|
|
2687
|
+
if missing_roles.any?
|
|
2688
|
+
puts "❌ Unknown role(s): #{missing_roles.join(", ")}. Available: #{available_roles.join(", ")}"
|
|
2689
|
+
puts " Create one with --role-file, or 'brainiac role create <name>'."
|
|
2690
|
+
exit 1
|
|
2691
|
+
end
|
|
2692
|
+
|
|
2550
2693
|
email ||= default_agent_email(agent_key, name)
|
|
2551
2694
|
|
|
2552
2695
|
entry = { "display_name" => name }
|
|
@@ -2565,6 +2708,8 @@ when "agent"
|
|
|
2565
2708
|
end
|
|
2566
2709
|
end
|
|
2567
2710
|
|
|
2711
|
+
puts "✓ Created role #{write_role_file(new_role, File.read(role_file), name)}" if new_role
|
|
2712
|
+
|
|
2568
2713
|
registry[agent_key] = entry
|
|
2569
2714
|
File.write(agent_registry_file, JSON.pretty_generate(registry))
|
|
2570
2715
|
puts "✓ Created agent '#{name}' (#{agent_key})"
|
|
@@ -2579,7 +2724,12 @@ when "agent"
|
|
|
2579
2724
|
persona_dir = File.join(BRAINIAC_DIR, "brain", "persona", agent_key)
|
|
2580
2725
|
FileUtils.mkdir_p(persona_dir)
|
|
2581
2726
|
persona_file = File.join(persona_dir, "style.md")
|
|
2582
|
-
|
|
2727
|
+
if File.exist?(persona_file)
|
|
2728
|
+
# Already there (e.g. a re-create); leave it alone.
|
|
2729
|
+
elsif frontmatter?(persona_text)
|
|
2730
|
+
File.write(persona_file, "#{persona_text.strip}\n")
|
|
2731
|
+
puts " Persona: #{persona_file}"
|
|
2732
|
+
else
|
|
2583
2733
|
File.write(persona_file, <<~MD)
|
|
2584
2734
|
---
|
|
2585
2735
|
name: #{agent_key}-style
|
|
@@ -2606,8 +2756,12 @@ when "agent"
|
|
|
2606
2756
|
notify_plugins_agent_created(agent_key, agent_hook_payload(agent_key, entry, open_browser: open_browser),
|
|
2607
2757
|
skip: skip_plugins, only: only_plugins, confirm: confirm)
|
|
2608
2758
|
if skip_plugins.any? && entry["skip_plugins"] != skip_plugins.uniq
|
|
2609
|
-
|
|
2610
|
-
|
|
2759
|
+
# Re-read: the plugin hooks above wrote their tokens (DISCORD_BOT_TOKEN,
|
|
2760
|
+
# FIZZY_TOKEN, ...) into agents.json, and the copy we loaded at the start
|
|
2761
|
+
# would clobber them.
|
|
2762
|
+
registry = JSON.parse(File.read(agent_registry_file))
|
|
2763
|
+
registry[agent_key] ||= entry
|
|
2764
|
+
registry[agent_key]["skip_plugins"] = skip_plugins.uniq
|
|
2611
2765
|
File.write(agent_registry_file, JSON.pretty_generate(registry))
|
|
2612
2766
|
puts " Skipping #{skip_plugins.uniq.join(", ")} for #{name} (undo: brainiac agent setup #{agent_key} --only <plugin>)"
|
|
2613
2767
|
end
|
|
@@ -2618,12 +2772,16 @@ when "agent"
|
|
|
2618
2772
|
when "setup"
|
|
2619
2773
|
name = ARGV.shift
|
|
2620
2774
|
unless name
|
|
2621
|
-
puts "Usage: brainiac agent setup <name> [--avatar <path>] [--email <address>] [--
|
|
2775
|
+
puts "Usage: brainiac agent setup <name> [--avatar <path>] [--email <address>] [--role <role>] [--role-file <path>]"
|
|
2776
|
+
puts " [--skip <plugins>] [--only <plugins>] [--no-browser]"
|
|
2622
2777
|
puts ""
|
|
2623
2778
|
puts "Re-runs every plugin's agent setup (Discord bot, Fizzy user, Basecamp person)."
|
|
2624
2779
|
puts "Steps that are already done are skipped, so it's safe to run after an"
|
|
2625
2780
|
puts "interrupted 'agent create' or to set up a newly installed plugin."
|
|
2781
|
+
puts "An agent without a role is asked to pick or write one."
|
|
2626
2782
|
puts ""
|
|
2783
|
+
puts " --role r Assign an existing role"
|
|
2784
|
+
puts " --role-file f Create a role from a markdown file (named after the agent) and assign it"
|
|
2627
2785
|
puts " --skip a,b Never set up these plugins for this agent (saved)"
|
|
2628
2786
|
puts " --only a,b Run just these plugins (also un-skips them)"
|
|
2629
2787
|
puts " --no-browser Print links instead of opening them"
|
|
@@ -2640,8 +2798,18 @@ when "agent"
|
|
|
2640
2798
|
|
|
2641
2799
|
only_plugins = nil
|
|
2642
2800
|
open_browser = true
|
|
2801
|
+
new_roles = []
|
|
2802
|
+
role_file = nil
|
|
2643
2803
|
while ARGV.any?
|
|
2644
2804
|
case ARGV.shift
|
|
2805
|
+
when "--role"
|
|
2806
|
+
new_roles << role_name_arg(ARGV.shift)
|
|
2807
|
+
when "--role-file"
|
|
2808
|
+
role_file = File.expand_path(ARGV.shift.to_s)
|
|
2809
|
+
unless File.file?(role_file)
|
|
2810
|
+
puts "❌ Role file not found: #{role_file}"
|
|
2811
|
+
exit 1
|
|
2812
|
+
end
|
|
2645
2813
|
when "--skip"
|
|
2646
2814
|
entry["skip_plugins"] = ((entry["skip_plugins"] || []) + plugin_list_arg(ARGV.shift)).uniq
|
|
2647
2815
|
when "--only"
|
|
@@ -2667,6 +2835,26 @@ when "agent"
|
|
|
2667
2835
|
# Explicitly running a plugin with --only un-skips it for good.
|
|
2668
2836
|
entry["skip_plugins"] -= only_plugins if only_plugins && entry["skip_plugins"]
|
|
2669
2837
|
entry.delete("skip_plugins") if entry["skip_plugins"] == []
|
|
2838
|
+
|
|
2839
|
+
display = entry["display_name"] || name
|
|
2840
|
+
if role_file
|
|
2841
|
+
role_name = (new_roles - available_roles).first || agent_key
|
|
2842
|
+
if available_roles.include?(role_name)
|
|
2843
|
+
puts "❌ Role '#{role_name}' already exists. Pass --role <new-name> to name the role from #{role_file}."
|
|
2844
|
+
exit 1
|
|
2845
|
+
end
|
|
2846
|
+
puts "✓ Created role #{write_role_file(role_name, File.read(role_file), display)}"
|
|
2847
|
+
new_roles |= [role_name]
|
|
2848
|
+
elsif new_roles.empty? && entry["role"].nil? && $stdin.tty?
|
|
2849
|
+
new_roles = prompt_for_agent_role(agent_key, display)
|
|
2850
|
+
end
|
|
2851
|
+
unknown = new_roles - available_roles
|
|
2852
|
+
if unknown.any?
|
|
2853
|
+
puts "❌ Unknown role(s): #{unknown.join(", ")}. Available: #{available_roles.join(", ")}"
|
|
2854
|
+
exit 1
|
|
2855
|
+
end
|
|
2856
|
+
puts "✓ Role: #{assign_roles(entry, new_roles).join(", ")}" if new_roles.any?
|
|
2857
|
+
|
|
2670
2858
|
registry[agent_key] = entry
|
|
2671
2859
|
File.write(agent_registry_file, JSON.pretty_generate(registry))
|
|
2672
2860
|
|
|
@@ -2937,8 +3125,9 @@ when "role"
|
|
|
2937
3125
|
|
|
2938
3126
|
files.each do |f|
|
|
2939
3127
|
name = File.basename(f, ".md")
|
|
2940
|
-
#
|
|
2941
|
-
|
|
3128
|
+
# First non-empty, non-heading line after any frontmatter is the description
|
|
3129
|
+
body = File.read(f).sub(/\A---\n.*?\n---\n/m, "")
|
|
3130
|
+
desc = body.lines.find { |l| !l.strip.empty? && !l.start_with?("#") }&.strip || ""
|
|
2942
3131
|
agents = agent_roles[name]
|
|
2943
3132
|
agent_str = agents ? " (#{agents.join(", ")})" : ""
|
|
2944
3133
|
puts "#{name}#{agent_str} — #{desc[0..70]}"
|
|
@@ -3035,19 +3224,64 @@ when "role"
|
|
|
3035
3224
|
puts "✓ Removed role '#{role_name}' from #{agent_name}"
|
|
3036
3225
|
|
|
3037
3226
|
when "create", "add"
|
|
3038
|
-
name = ARGV
|
|
3039
|
-
|
|
3040
|
-
puts "Usage: brainiac role create <name>"
|
|
3227
|
+
name = ARGV.shift
|
|
3228
|
+
if name.nil? || name.start_with?("--")
|
|
3229
|
+
puts "Usage: brainiac role create <name> [--from <file>] [--assign <agent>]"
|
|
3230
|
+
puts ""
|
|
3231
|
+
puts "Without --from, $EDITOR opens on a role template (or the template is written"
|
|
3232
|
+
puts "as-is when there's no terminal)."
|
|
3041
3233
|
exit 1
|
|
3042
3234
|
end
|
|
3043
|
-
|
|
3235
|
+
name = role_name_arg(name)
|
|
3236
|
+
from = nil
|
|
3237
|
+
assign_to = nil
|
|
3238
|
+
while ARGV.any?
|
|
3239
|
+
case ARGV.shift
|
|
3240
|
+
when "--from" then from = File.expand_path(ARGV.shift.to_s)
|
|
3241
|
+
when "--assign" then assign_to = ARGV.shift
|
|
3242
|
+
else
|
|
3243
|
+
puts "Unknown option. Run 'brainiac role create' for usage."
|
|
3244
|
+
exit 1
|
|
3245
|
+
end
|
|
3246
|
+
end
|
|
3247
|
+
|
|
3044
3248
|
file = File.join(roles_dir, "#{name}.md")
|
|
3045
3249
|
if File.exist?(file)
|
|
3046
3250
|
puts "Role '#{name}' already exists. Edit #{file} directly."
|
|
3047
3251
|
exit 1
|
|
3048
3252
|
end
|
|
3049
|
-
|
|
3050
|
-
|
|
3253
|
+
if from && !File.file?(from)
|
|
3254
|
+
puts "File not found: #{from}"
|
|
3255
|
+
exit 1
|
|
3256
|
+
end
|
|
3257
|
+
|
|
3258
|
+
agent_registry_file = File.join(BRAINIAC_DIR, "agents.json")
|
|
3259
|
+
registry = File.exist?(agent_registry_file) ? JSON.parse(File.read(agent_registry_file)) : {}
|
|
3260
|
+
agent_key = assign_to && role_name_arg(assign_to)
|
|
3261
|
+
if agent_key && !registry[agent_key]
|
|
3262
|
+
puts "Agent '#{assign_to}' not found."
|
|
3263
|
+
exit 1
|
|
3264
|
+
end
|
|
3265
|
+
agent_display = agent_key && (registry[agent_key]["display_name"] || assign_to)
|
|
3266
|
+
|
|
3267
|
+
text = if from
|
|
3268
|
+
File.read(from)
|
|
3269
|
+
elsif $stdin.tty?
|
|
3270
|
+
edit_description_in_editor(name, role_template(name, agent_display))
|
|
3271
|
+
else
|
|
3272
|
+
role_template(name, agent_display).gsub(/^\s*<!--.*?-->\s*\n?/, "")
|
|
3273
|
+
end
|
|
3274
|
+
unless text
|
|
3275
|
+
puts "Empty role, nothing created."
|
|
3276
|
+
exit 1
|
|
3277
|
+
end
|
|
3278
|
+
puts "✓ Created #{write_role_file(name, text, agent_display)}"
|
|
3279
|
+
|
|
3280
|
+
if agent_key
|
|
3281
|
+
assign_roles(registry[agent_key], [name])
|
|
3282
|
+
File.write(agent_registry_file, JSON.pretty_generate(registry))
|
|
3283
|
+
puts "✓ Assigned role '#{name}' to #{agent_display}"
|
|
3284
|
+
end
|
|
3051
3285
|
|
|
3052
3286
|
else
|
|
3053
3287
|
puts <<~HELP
|
|
@@ -3056,7 +3290,8 @@ when "role"
|
|
|
3056
3290
|
Commands:
|
|
3057
3291
|
list List configured roles
|
|
3058
3292
|
show <name> Show role definition
|
|
3059
|
-
create <name>
|
|
3293
|
+
create <name> [--from <file>] [--assign <agent>]
|
|
3294
|
+
Create a role (opens $EDITOR on a template)
|
|
3060
3295
|
assign <agent> <role> Assign a role to an agent (additive)
|
|
3061
3296
|
unassign <agent> <role> Remove a role from an agent
|
|
3062
3297
|
HELP
|
|
@@ -4378,7 +4613,7 @@ when "help", "--help", "-h", nil
|
|
|
4378
4613
|
Role Commands:
|
|
4379
4614
|
role list List configured roles and assignments
|
|
4380
4615
|
role show <name> Show role definition
|
|
4381
|
-
role create <name>
|
|
4616
|
+
role create <name> [--from f] [--assign a] Create a role (template in $EDITOR)
|
|
4382
4617
|
role assign <agent> <role> Assign a role to an agent (additive)
|
|
4383
4618
|
role unassign <agent> <role> Remove a role from an agent
|
|
4384
4619
|
|
|
@@ -116,7 +116,7 @@ _brainiac() {
|
|
|
116
116
|
case "$agent_name" in
|
|
117
117
|
list|remove|delete|rm|setup) ;;
|
|
118
118
|
create|add)
|
|
119
|
-
COMPREPLY=($(compgen -W "--avatar --description --description-file --email --local --role --cli --skip --only --no-browser" -- "$cur"))
|
|
119
|
+
COMPREPLY=($(compgen -W "--avatar --description --description-file --email --local --role --role-file --cli --skip --only --no-browser" -- "$cur"))
|
|
120
120
|
;;
|
|
121
121
|
*)
|
|
122
122
|
COMPREPLY=($(compgen -W "show env" -- "$cur"))
|
data/lib/brainiac/agents.rb
CHANGED
|
@@ -50,6 +50,7 @@ AGENT_REGISTRY = load_agent_registry
|
|
|
50
50
|
def reload_agent_registry!(force: false)
|
|
51
51
|
return unless file_changed?(AGENT_REGISTRY_FILE, force: force)
|
|
52
52
|
|
|
53
|
+
old_registry = AGENT_REGISTRY.dup
|
|
53
54
|
old_keys = AGENT_REGISTRY.keys.to_set
|
|
54
55
|
AGENT_REGISTRY.replace(load_agent_registry)
|
|
55
56
|
new_keys = AGENT_REGISTRY.keys.to_set
|
|
@@ -70,6 +71,17 @@ def reload_agent_registry!(force: false)
|
|
|
70
71
|
LOG.info "Agent removed: #{key}"
|
|
71
72
|
Brainiac.emit(:agent_removed, agent_key: key)
|
|
72
73
|
end
|
|
74
|
+
|
|
75
|
+
# Agents that already existed but whose entry changed — e.g. `agent create`
|
|
76
|
+
# wrote the entry first and a plugin added its token a few seconds later.
|
|
77
|
+
(old_keys & new_keys).each do |key|
|
|
78
|
+
entry = AGENT_REGISTRY[key]
|
|
79
|
+
next if entry == old_registry[key]
|
|
80
|
+
|
|
81
|
+
display_name = entry.is_a?(Hash) ? (entry["display_name"] || key.capitalize) : key.capitalize
|
|
82
|
+
Brainiac.emit(:agent_updated, agent_key: key, display_name: display_name, entry: entry,
|
|
83
|
+
previous_entry: old_registry[key])
|
|
84
|
+
end
|
|
73
85
|
end
|
|
74
86
|
|
|
75
87
|
# Default environment for all agent processes. Prevents git from hanging
|
data/lib/brainiac/hooks.rb
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
# :agent_completed — After an agent session finishes (success or failure)
|
|
12
12
|
# :agent_added — When a new agent appears in the registry (after reload)
|
|
13
13
|
# :agent_removed — When an agent is removed from the registry (after reload)
|
|
14
|
+
# :agent_updated — When an existing agent's entry changes (after reload; has :previous_entry)
|
|
14
15
|
# :pr_merged — After a GitHub PR is merged
|
|
15
16
|
# :pr_opened — After a GitHub PR is opened
|
|
16
17
|
# :pr_reviewed — After a PR review is submitted
|
data/lib/brainiac/version.rb
CHANGED
|
@@ -109,6 +109,9 @@ Available hooks:
|
|
|
109
109
|
| `:create_work_item` | Create a card/issue/ticket | title, body, project |
|
|
110
110
|
| `:detect_cli_provider` | Detect CLI provider | metadata hash |
|
|
111
111
|
| `:detect_effort` | Detect effort level | metadata hash |
|
|
112
|
+
| `:agent_added` | New key in agents.json (after reload) | agent_key, display_name, entry |
|
|
113
|
+
| `:agent_updated` | Existing agent's entry changed (after reload) | agent_key, display_name, entry, previous_entry |
|
|
114
|
+
| `:agent_removed` | Key gone from agents.json (after reload) | agent_key |
|
|
112
115
|
|
|
113
116
|
## Agent Onboarding (`on_agent_created`)
|
|
114
117
|
|
|
@@ -172,6 +175,13 @@ Users choose which plugins run with `--skip` / `--only` (or by answering "n" whe
|
|
|
172
175
|
asked during create). Core filters on the plugin name before calling you, so
|
|
173
176
|
plugins don't need to handle this.
|
|
174
177
|
|
|
178
|
+
**Going live without a restart.** `agent create` / `agent setup` tell the running
|
|
179
|
+
server to reload `agents.json`. The server may already have seen the agent before
|
|
180
|
+
your hook wrote its token, so a server-side plugin that starts something per agent
|
|
181
|
+
(a gateway connection, a poller) must handle `:agent_updated` as well as
|
|
182
|
+
`:agent_added`: start it when the credential appears or changes, stop it when it's
|
|
183
|
+
removed.
|
|
184
|
+
|
|
175
185
|
## Channel Prompts
|
|
176
186
|
|
|
177
187
|
If your plugin is a communication channel (like Discord, Fizzy), register a prompt:
|