brainiac 0.3.0 → 0.4.1
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/CHANGELOG.md +4 -0
- data/bin/brainiac +358 -28
- data/bin/brainiac-completion.bash +3 -3
- data/lib/brainiac/agents.rb +12 -0
- data/lib/brainiac/hooks.rb +1 -0
- data/lib/brainiac/prompts.rb +12 -0
- data/lib/brainiac/version.rb +1 -1
- data/skills/brainiac-plugins/SKILL.md +72 -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: 1edc9fb64e331daacfa390596ce230ee37f6739e91efc72438434110e88d24de
|
|
4
|
+
data.tar.gz: 0fdf11e2eace81f2ce56f6f562e675d7c67598232a23b020fcc982f7ac20db58
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca3e17f678f0207947da8fda7af7711788cacdf377e2496b04c82a1aed303faa3cc57e6c3b32be3227a98bc12b86bb28b25d523ab817514b396a249955cc226a
|
|
7
|
+
data.tar.gz: 2cab777438aa1b245b037a46f6fdf9dcfe16120eca888528dd045f7e70fed699c850bfe76b858601f9f7708a61c213f3be96cb4173a3f2fae81d83f7f4fcc36b
|
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
|
-
#
|
|
197
|
-
#
|
|
198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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.
|
|
@@ -560,6 +668,78 @@ def generate_plugin_skill_md(plugin_name, pascal_name)
|
|
|
560
668
|
| `:create_work_item` | Create card/issue/ticket |
|
|
561
669
|
| `:detect_cli_provider` | Detect CLI provider |
|
|
562
670
|
| `:detect_effort` | Detect effort level |
|
|
671
|
+
| `:agent_added` | New agent in agents.json (after reload) |
|
|
672
|
+
| `:agent_updated` | Existing agent's entry changed (after reload) |
|
|
673
|
+
| `:agent_removed` | Agent removed from agents.json (after reload) |
|
|
674
|
+
|
|
675
|
+
## Agent Onboarding (`on_agent_created`)
|
|
676
|
+
|
|
677
|
+
`brainiac agent create` writes the brainiac files (agents.json, persona, brain),
|
|
678
|
+
then asks each plugin to provision the new agent on its service: a Discord bot,
|
|
679
|
+
a Fizzy user, a Basecamp person. `brainiac agent setup <name>` runs the same hook
|
|
680
|
+
again. If your plugin gives agents an identity on some service, implement it.
|
|
681
|
+
|
|
682
|
+
Define it in `cli.rb` (or a file `cli.rb` requires), since it runs in the CLI context:
|
|
683
|
+
|
|
684
|
+
```ruby
|
|
685
|
+
# cli.rb
|
|
686
|
+
require_relative "agent_setup"
|
|
687
|
+
|
|
688
|
+
module Brainiac
|
|
689
|
+
module Plugins
|
|
690
|
+
module MyService
|
|
691
|
+
# Called by `brainiac agent create` / `brainiac agent setup`.
|
|
692
|
+
def self.on_agent_created(agent_key, entry)
|
|
693
|
+
AgentSetup.run(agent_key, entry)
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
def self.on_agent_removed(agent_key, display_name) # optional: clean up config
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
`entry` is the agents.json entry plus:
|
|
704
|
+
|
|
705
|
+
| Key | Value |
|
|
706
|
+
|-----|-------|
|
|
707
|
+
| `agent_key` | Registry key, e.g. `quill` |
|
|
708
|
+
| `display_name` | e.g. `Quill` |
|
|
709
|
+
| `avatar_path` | Profile image (png/jpg/gif/webp) or absent |
|
|
710
|
+
| `description` | Persona text (can be several paragraphs) or absent |
|
|
711
|
+
| `email` | Email for the agent's accounts (from `--email` / `agent_email_template`) or absent |
|
|
712
|
+
| `open_browser` | `true` when you may launch a browser for a manual step |
|
|
713
|
+
|
|
714
|
+
Rules:
|
|
715
|
+
|
|
716
|
+
1. **Idempotent.** `agent setup` re-runs every hook. Check your config first and
|
|
717
|
+
print `✓ <Name> already ... (skipping)` when the work is done.
|
|
718
|
+
2. **Automate everything the API allows.** Only stop for steps the service truly
|
|
719
|
+
can't do by API (e.g. Discord has no create-application endpoint). Then print
|
|
720
|
+
numbered steps and the URL, and read the result with `$stdin.noecho(&:gets)`
|
|
721
|
+
for secrets.
|
|
722
|
+
3. **Browser.** If `entry["open_browser"]` is true, open the URL as well as printing
|
|
723
|
+
it (`xdg-open` on Linux, `open` on macOS, spawned and detached). Don't open links
|
|
724
|
+
that must be used while signed in as someone else (e.g. a join link for the bot's
|
|
725
|
+
own account), because they'd land in the user's own session. Say "open in a
|
|
726
|
+
private window" instead.
|
|
727
|
+
4. **Optional inputs.** Skip quietly if `avatar_path`, `description` or `email` is missing.
|
|
728
|
+
Return without error when `$stdin` isn't a TTY and you need input.
|
|
729
|
+
5. **Never raise for expected failures.** Print `✗ ... re-run: brainiac agent setup <key>`.
|
|
730
|
+
Uncaught exceptions are reported as "hook failed" and the next plugin runs.
|
|
731
|
+
6. **Prefix output** with ` [MyService]` so it's clear whose step is running.
|
|
732
|
+
|
|
733
|
+
Users choose which plugins run with `--skip` / `--only` (or by answering "n" when
|
|
734
|
+
asked during create). Core filters on the plugin name before calling you, so
|
|
735
|
+
plugins don't need to handle this.
|
|
736
|
+
|
|
737
|
+
**Going live without a restart.** `agent create` / `agent setup` tell the running
|
|
738
|
+
server to reload `agents.json`. The server may already have seen the agent before
|
|
739
|
+
your hook wrote its token, so a server-side plugin that starts something per agent
|
|
740
|
+
(a gateway connection, a poller) must handle `:agent_updated` as well as
|
|
741
|
+
`:agent_added`: start it when the credential appears or changes, stop it when it's
|
|
742
|
+
removed.
|
|
563
743
|
|
|
564
744
|
## Channel Prompts
|
|
565
745
|
|
|
@@ -2254,18 +2434,28 @@ when "agent"
|
|
|
2254
2434
|
puts "Usage: brainiac agent create <name> [options]"
|
|
2255
2435
|
puts ""
|
|
2256
2436
|
puts "Options:"
|
|
2257
|
-
puts " --
|
|
2258
|
-
puts " --
|
|
2259
|
-
puts " --
|
|
2260
|
-
puts " --
|
|
2437
|
+
puts " --avatar <path> Profile picture (png/jpg/gif/webp), used by plugins (Discord, Fizzy)"
|
|
2438
|
+
puts " --description <text> Persona description (\"-\" reads stdin, \"@file\" reads a file)"
|
|
2439
|
+
puts " --description-file <path> Read the persona description from a file"
|
|
2440
|
+
puts " --email <address> Email for the agent's accounts (default: agent_email_template)"
|
|
2441
|
+
puts " --local Mark agent as local (dispatches on this machine)"
|
|
2442
|
+
puts " --role <role> Assign a role (repeatable)"
|
|
2443
|
+
puts " --cli <provider> Set CLI provider (default: kiro)"
|
|
2444
|
+
puts " --skip <plugins> Don't set up these plugins for this agent, e.g. --skip basecamp"
|
|
2445
|
+
puts " --only <plugins> Set up just these plugins, e.g. --only discord,fizzy"
|
|
2446
|
+
puts " --no-browser Print setup links instead of opening them"
|
|
2261
2447
|
puts ""
|
|
2262
|
-
puts "
|
|
2448
|
+
puts "With no flags and a terminal, interactive mode is used. If no description is"
|
|
2449
|
+
puts "given on a terminal, $EDITOR opens so you can write a full persona."
|
|
2450
|
+
puts "After the brainiac files are written, each installed plugin sets up its part"
|
|
2451
|
+
puts "(Discord bot, Fizzy user, Basecamp person), asking first unless --skip/--only"
|
|
2452
|
+
puts "is given. Re-run with: brainiac agent setup <name>"
|
|
2263
2453
|
puts ""
|
|
2264
2454
|
puts "Examples:"
|
|
2265
|
-
puts " brainiac agent create
|
|
2455
|
+
puts " brainiac agent create Quill --local --avatar quill.png # opens $EDITOR for the persona"
|
|
2456
|
+
puts " brainiac agent create Quill --local --avatar quill.png --description @quill.md"
|
|
2457
|
+
puts " brainiac agent create MyAgent # interactive"
|
|
2266
2458
|
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
2459
|
exit 1
|
|
2270
2460
|
end
|
|
2271
2461
|
|
|
@@ -2273,7 +2463,8 @@ when "agent"
|
|
|
2273
2463
|
registry = File.exist?(agent_registry_file) ? JSON.parse(File.read(agent_registry_file)) : {}
|
|
2274
2464
|
|
|
2275
2465
|
if registry[agent_key]
|
|
2276
|
-
puts "Agent '#{name}' already exists. Use 'brainiac agent #{agent_key} show' to view
|
|
2466
|
+
puts "Agent '#{name}' already exists. Use 'brainiac agent #{agent_key} show' to view,"
|
|
2467
|
+
puts "or 'brainiac agent setup #{agent_key}' to finish plugin setup."
|
|
2277
2468
|
exit 1
|
|
2278
2469
|
end
|
|
2279
2470
|
|
|
@@ -2281,6 +2472,11 @@ when "agent"
|
|
|
2281
2472
|
roles = []
|
|
2282
2473
|
cli_provider = nil
|
|
2283
2474
|
persona_text = nil
|
|
2475
|
+
avatar = nil
|
|
2476
|
+
email = nil
|
|
2477
|
+
skip_plugins = []
|
|
2478
|
+
only_plugins = nil
|
|
2479
|
+
open_browser = true
|
|
2284
2480
|
|
|
2285
2481
|
if ARGV.any?
|
|
2286
2482
|
# Flag mode
|
|
@@ -2295,9 +2491,27 @@ when "agent"
|
|
|
2295
2491
|
when "--cli"
|
|
2296
2492
|
ARGV.shift
|
|
2297
2493
|
cli_provider = ARGV.shift
|
|
2298
|
-
when "--persona"
|
|
2494
|
+
when "--description", "--persona"
|
|
2495
|
+
ARGV.shift
|
|
2496
|
+
persona_text = read_description_arg(ARGV.shift.to_s)
|
|
2497
|
+
when "--description-file"
|
|
2498
|
+
ARGV.shift
|
|
2499
|
+
persona_text = File.read(File.expand_path(ARGV.shift.to_s)).strip
|
|
2500
|
+
when "--avatar"
|
|
2299
2501
|
ARGV.shift
|
|
2300
|
-
|
|
2502
|
+
avatar = ARGV.shift
|
|
2503
|
+
when "--email"
|
|
2504
|
+
ARGV.shift
|
|
2505
|
+
email = ARGV.shift
|
|
2506
|
+
when "--skip"
|
|
2507
|
+
ARGV.shift
|
|
2508
|
+
skip_plugins.concat(plugin_list_arg(ARGV.shift))
|
|
2509
|
+
when "--only"
|
|
2510
|
+
ARGV.shift
|
|
2511
|
+
only_plugins = plugin_list_arg(ARGV.shift)
|
|
2512
|
+
when "--no-browser"
|
|
2513
|
+
ARGV.shift
|
|
2514
|
+
open_browser = false
|
|
2301
2515
|
else
|
|
2302
2516
|
puts "Unknown option: #{ARGV[0]}"
|
|
2303
2517
|
exit 1
|
|
@@ -2327,15 +2541,39 @@ when "agent"
|
|
|
2327
2541
|
input = $stdin.gets.chomp
|
|
2328
2542
|
roles = input.empty? ? [] : input.split(",").map(&:strip)
|
|
2329
2543
|
|
|
2330
|
-
print "
|
|
2544
|
+
print "Avatar image path (or blank to skip) []: "
|
|
2545
|
+
input = $stdin.gets.chomp
|
|
2546
|
+
avatar = input.empty? ? nil : input
|
|
2547
|
+
|
|
2548
|
+
default_email = default_agent_email(agent_key, name)
|
|
2549
|
+
print "Email for the agent's accounts#{" [#{default_email}]" if default_email}: "
|
|
2331
2550
|
input = $stdin.gets.chomp
|
|
2332
|
-
|
|
2551
|
+
email = input.empty? ? default_email : input
|
|
2333
2552
|
end
|
|
2334
2553
|
|
|
2554
|
+
# A persona is usually a paragraph or more — give it a real editor.
|
|
2555
|
+
if persona_text.nil? && $stdin.tty?
|
|
2556
|
+
print "Write #{name}'s persona description in #{ENV["VISUAL"] || ENV["EDITOR"] || "vi"}? [Y/n]: "
|
|
2557
|
+
persona_text = edit_description_in_editor(name) unless $stdin.gets.to_s.strip.downcase.start_with?("n")
|
|
2558
|
+
end
|
|
2559
|
+
|
|
2560
|
+
email ||= default_agent_email(agent_key, name)
|
|
2561
|
+
|
|
2335
2562
|
entry = { "display_name" => name }
|
|
2336
2563
|
entry["local"] = true if local
|
|
2337
2564
|
entry["cli_provider"] = cli_provider if cli_provider
|
|
2338
2565
|
entry["role"] = roles.size == 1 ? roles.first : roles if roles.any?
|
|
2566
|
+
entry["email"] = email if email
|
|
2567
|
+
entry["skip_plugins"] = skip_plugins.uniq if skip_plugins.any?
|
|
2568
|
+
|
|
2569
|
+
if avatar
|
|
2570
|
+
begin
|
|
2571
|
+
entry["avatar"] = store_agent_avatar(agent_key, avatar)
|
|
2572
|
+
rescue ArgumentError => e
|
|
2573
|
+
puts "❌ #{e.message}"
|
|
2574
|
+
exit 1
|
|
2575
|
+
end
|
|
2576
|
+
end
|
|
2339
2577
|
|
|
2340
2578
|
registry[agent_key] = entry
|
|
2341
2579
|
File.write(agent_registry_file, JSON.pretty_generate(registry))
|
|
@@ -2343,6 +2581,8 @@ when "agent"
|
|
|
2343
2581
|
puts " Local: #{local}" if local
|
|
2344
2582
|
puts " Role: #{roles.join(", ")}" if roles.any?
|
|
2345
2583
|
puts " CLI: #{cli_provider}" if cli_provider
|
|
2584
|
+
puts " Email: #{email}" if email
|
|
2585
|
+
puts " Avatar: #{entry["avatar"]}" if entry["avatar"]
|
|
2346
2586
|
|
|
2347
2587
|
# Create persona directory and style file if requested
|
|
2348
2588
|
if persona_text
|
|
@@ -2356,6 +2596,7 @@ when "agent"
|
|
|
2356
2596
|
description: Persona voice for #{name}.
|
|
2357
2597
|
---
|
|
2358
2598
|
# #{name} — Persona
|
|
2599
|
+
|
|
2359
2600
|
#{persona_text}
|
|
2360
2601
|
MD
|
|
2361
2602
|
puts " Persona: #{persona_file}"
|
|
@@ -2368,12 +2609,86 @@ when "agent"
|
|
|
2368
2609
|
brain_init(name)
|
|
2369
2610
|
end
|
|
2370
2611
|
|
|
2371
|
-
#
|
|
2372
|
-
|
|
2612
|
+
# Each plugin provisions its side (Discord bot, Fizzy user, Basecamp person, ...).
|
|
2613
|
+
# Without --skip/--only, a terminal user is asked per plugin; "no" answers
|
|
2614
|
+
# are saved so `agent setup` leaves those plugins alone too.
|
|
2615
|
+
confirm = $stdin.tty? && skip_plugins.empty? && only_plugins.nil?
|
|
2616
|
+
notify_plugins_agent_created(agent_key, agent_hook_payload(agent_key, entry, open_browser: open_browser),
|
|
2617
|
+
skip: skip_plugins, only: only_plugins, confirm: confirm)
|
|
2618
|
+
if skip_plugins.any? && entry["skip_plugins"] != skip_plugins.uniq
|
|
2619
|
+
# Re-read: the plugin hooks above wrote their tokens (DISCORD_BOT_TOKEN,
|
|
2620
|
+
# FIZZY_TOKEN, ...) into agents.json, and the copy we loaded at the start
|
|
2621
|
+
# would clobber them.
|
|
2622
|
+
registry = JSON.parse(File.read(agent_registry_file))
|
|
2623
|
+
registry[agent_key] ||= entry
|
|
2624
|
+
registry[agent_key]["skip_plugins"] = skip_plugins.uniq
|
|
2625
|
+
File.write(agent_registry_file, JSON.pretty_generate(registry))
|
|
2626
|
+
puts " Skipping #{skip_plugins.uniq.join(", ")} for #{name} (undo: brainiac agent setup #{agent_key} --only <plugin>)"
|
|
2627
|
+
end
|
|
2373
2628
|
|
|
2374
2629
|
# Notify running server to reload and trigger :agent_added hook
|
|
2375
2630
|
notify_server_reload
|
|
2376
2631
|
|
|
2632
|
+
when "setup"
|
|
2633
|
+
name = ARGV.shift
|
|
2634
|
+
unless name
|
|
2635
|
+
puts "Usage: brainiac agent setup <name> [--avatar <path>] [--email <address>] [--skip <plugins>] [--only <plugins>] [--no-browser]"
|
|
2636
|
+
puts ""
|
|
2637
|
+
puts "Re-runs every plugin's agent setup (Discord bot, Fizzy user, Basecamp person)."
|
|
2638
|
+
puts "Steps that are already done are skipped, so it's safe to run after an"
|
|
2639
|
+
puts "interrupted 'agent create' or to set up a newly installed plugin."
|
|
2640
|
+
puts ""
|
|
2641
|
+
puts " --skip a,b Never set up these plugins for this agent (saved)"
|
|
2642
|
+
puts " --only a,b Run just these plugins (also un-skips them)"
|
|
2643
|
+
puts " --no-browser Print links instead of opening them"
|
|
2644
|
+
exit 1
|
|
2645
|
+
end
|
|
2646
|
+
|
|
2647
|
+
agent_key = name.downcase.gsub(/[^a-z0-9-]/, "-")
|
|
2648
|
+
registry = File.exist?(agent_registry_file) ? JSON.parse(File.read(agent_registry_file)) : {}
|
|
2649
|
+
entry = registry[agent_key]
|
|
2650
|
+
unless entry
|
|
2651
|
+
puts "Agent '#{name}' not found in registry."
|
|
2652
|
+
exit 1
|
|
2653
|
+
end
|
|
2654
|
+
|
|
2655
|
+
only_plugins = nil
|
|
2656
|
+
open_browser = true
|
|
2657
|
+
while ARGV.any?
|
|
2658
|
+
case ARGV.shift
|
|
2659
|
+
when "--skip"
|
|
2660
|
+
entry["skip_plugins"] = ((entry["skip_plugins"] || []) + plugin_list_arg(ARGV.shift)).uniq
|
|
2661
|
+
when "--only"
|
|
2662
|
+
only_plugins = plugin_list_arg(ARGV.shift)
|
|
2663
|
+
when "--no-browser"
|
|
2664
|
+
open_browser = false
|
|
2665
|
+
when "--avatar"
|
|
2666
|
+
begin
|
|
2667
|
+
entry["avatar"] = store_agent_avatar(agent_key, ARGV.shift.to_s)
|
|
2668
|
+
rescue ArgumentError => e
|
|
2669
|
+
puts "❌ #{e.message}"
|
|
2670
|
+
exit 1
|
|
2671
|
+
end
|
|
2672
|
+
when "--email"
|
|
2673
|
+
entry["email"] = ARGV.shift
|
|
2674
|
+
else
|
|
2675
|
+
puts "Unknown option. Run 'brainiac agent setup' for usage."
|
|
2676
|
+
exit 1
|
|
2677
|
+
end
|
|
2678
|
+
end
|
|
2679
|
+
entry["email"] ||= default_agent_email(agent_key, entry["display_name"] || name)
|
|
2680
|
+
entry.delete("email") unless entry["email"]
|
|
2681
|
+
# Explicitly running a plugin with --only un-skips it for good.
|
|
2682
|
+
entry["skip_plugins"] -= only_plugins if only_plugins && entry["skip_plugins"]
|
|
2683
|
+
entry.delete("skip_plugins") if entry["skip_plugins"] == []
|
|
2684
|
+
registry[agent_key] = entry
|
|
2685
|
+
File.write(agent_registry_file, JSON.pretty_generate(registry))
|
|
2686
|
+
|
|
2687
|
+
puts "Setting up #{entry["display_name"] || name} (#{agent_key})"
|
|
2688
|
+
notify_plugins_agent_created(agent_key, agent_hook_payload(agent_key, entry, open_browser: open_browser),
|
|
2689
|
+
skip: entry["skip_plugins"] || [], only: only_plugins)
|
|
2690
|
+
notify_server_reload
|
|
2691
|
+
|
|
2377
2692
|
when "remove", "delete", "rm"
|
|
2378
2693
|
name = ARGV.shift
|
|
2379
2694
|
unless name
|
|
@@ -2407,12 +2722,14 @@ when "agent"
|
|
|
2407
2722
|
brainiac agent list
|
|
2408
2723
|
brainiac agent default [name]
|
|
2409
2724
|
brainiac agent create <name> [options]
|
|
2725
|
+
brainiac agent setup <name> [--skip|--only <plugins>]
|
|
2410
2726
|
brainiac agent remove <name>
|
|
2411
2727
|
|
|
2412
2728
|
Commands:
|
|
2413
2729
|
list List all agents in the registry
|
|
2414
2730
|
default [name] Show or set the default agent
|
|
2415
|
-
create <name> [options] Add a new agent
|
|
2731
|
+
create <name> [options] Add a new agent and run every plugin's setup
|
|
2732
|
+
setup <name> Re-run plugin setup (skips steps already done)
|
|
2416
2733
|
remove <name> Remove an agent from the registry
|
|
2417
2734
|
<name> show Show agent configuration (tokens redacted)
|
|
2418
2735
|
<name> set <field> <value> Update a config field (local, cli, role, persona, display_name)
|
|
@@ -2421,14 +2738,21 @@ when "agent"
|
|
|
2421
2738
|
<name> env --delete <KEY> Remove an env var from an agent
|
|
2422
2739
|
|
|
2423
2740
|
Create options (or interactive if no flags given):
|
|
2741
|
+
--avatar <path> Profile picture, used by plugins (Discord, Fizzy)
|
|
2742
|
+
--description <text> Persona description ("-" = stdin, "@file" = file; $EDITOR if omitted)
|
|
2743
|
+
--email <address> Email for the agent's accounts (default: agent_email_template)
|
|
2424
2744
|
--local Mark agent as local (dispatches on this machine)
|
|
2425
2745
|
--role <role> Assign a role (repeatable)
|
|
2426
2746
|
--cli <provider> Set CLI provider (default: kiro)
|
|
2427
|
-
--
|
|
2747
|
+
--skip <plugins> Don't set up these plugins for this agent (saved; also for setup)
|
|
2748
|
+
--only <plugins> Set up just these plugins (also for setup)
|
|
2749
|
+
--no-browser Print setup links instead of opening them
|
|
2428
2750
|
|
|
2429
2751
|
Examples:
|
|
2430
2752
|
brainiac agent list
|
|
2431
2753
|
brainiac agent default Sherlock
|
|
2754
|
+
brainiac agent create Quill --local --avatar quill.png
|
|
2755
|
+
brainiac agent setup quill
|
|
2432
2756
|
brainiac agent create SecurityBot --local --role code-reviewer
|
|
2433
2757
|
brainiac agent create Jane --cli grok
|
|
2434
2758
|
brainiac agent create MyAgent # interactive mode
|
|
@@ -3249,6 +3573,12 @@ when "plugin"
|
|
|
3249
3573
|
Cli.run(args)
|
|
3250
3574
|
end
|
|
3251
3575
|
|
|
3576
|
+
# Optional: provision a new agent on #{pascal_name} during
|
|
3577
|
+
# `brainiac agent create` / `brainiac agent setup`. Must be idempotent.
|
|
3578
|
+
# See "Agent Onboarding" in skills/#{plugin_name}/SKILL.md for the payload and rules.
|
|
3579
|
+
# def self.on_agent_created(agent_key, entry)
|
|
3580
|
+
# end
|
|
3581
|
+
|
|
3252
3582
|
# Subcommand names for bash completion.
|
|
3253
3583
|
def self.completions
|
|
3254
3584
|
%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 --
|
|
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"))
|
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/prompts.rb
CHANGED
|
@@ -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.
|
data/lib/brainiac/version.rb
CHANGED
|
@@ -109,6 +109,78 @@ 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 |
|
|
115
|
+
|
|
116
|
+
## Agent Onboarding (`on_agent_created`)
|
|
117
|
+
|
|
118
|
+
`brainiac agent create` writes the brainiac files (agents.json, persona, brain),
|
|
119
|
+
then asks each plugin to provision the new agent on its service: a Discord bot,
|
|
120
|
+
a Fizzy user, a Basecamp person. `brainiac agent setup <name>` runs the same hook
|
|
121
|
+
again. If your plugin gives agents an identity on some service, implement it.
|
|
122
|
+
|
|
123
|
+
Define it in `cli.rb` (or a file `cli.rb` requires), since it runs in the CLI context:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
# cli.rb
|
|
127
|
+
require_relative "agent_setup"
|
|
128
|
+
|
|
129
|
+
module Brainiac
|
|
130
|
+
module Plugins
|
|
131
|
+
module MyService
|
|
132
|
+
# Called by `brainiac agent create` / `brainiac agent setup`.
|
|
133
|
+
def self.on_agent_created(agent_key, entry)
|
|
134
|
+
AgentSetup.run(agent_key, entry)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def self.on_agent_removed(agent_key, display_name) # optional: clean up config
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`entry` is the agents.json entry plus:
|
|
145
|
+
|
|
146
|
+
| Key | Value |
|
|
147
|
+
|-----|-------|
|
|
148
|
+
| `agent_key` | Registry key, e.g. `quill` |
|
|
149
|
+
| `display_name` | e.g. `Quill` |
|
|
150
|
+
| `avatar_path` | Profile image (png/jpg/gif/webp) or absent |
|
|
151
|
+
| `description` | Persona text (can be several paragraphs) or absent |
|
|
152
|
+
| `email` | Email for the agent's accounts (from `--email` / `agent_email_template`) or absent |
|
|
153
|
+
| `open_browser` | `true` when you may launch a browser for a manual step |
|
|
154
|
+
|
|
155
|
+
Rules:
|
|
156
|
+
|
|
157
|
+
1. **Idempotent.** `agent setup` re-runs every hook. Check your config first and
|
|
158
|
+
print `✓ <Name> already ... (skipping)` when the work is done.
|
|
159
|
+
2. **Automate everything the API allows.** Only stop for steps the service truly
|
|
160
|
+
can't do by API (e.g. Discord has no create-application endpoint). Then print
|
|
161
|
+
numbered steps and the URL, and read the result with `$stdin.noecho(&:gets)`
|
|
162
|
+
for secrets.
|
|
163
|
+
3. **Browser.** If `entry["open_browser"]` is true, open the URL as well as printing
|
|
164
|
+
it (`xdg-open` on Linux, `open` on macOS, spawned and detached). Don't open links
|
|
165
|
+
that must be used while signed in as someone else (e.g. a join link for the bot's
|
|
166
|
+
own account), because they'd land in the user's own session. Say "open in a
|
|
167
|
+
private window" instead.
|
|
168
|
+
4. **Optional inputs.** Skip quietly if `avatar_path`, `description` or `email` is missing.
|
|
169
|
+
Return without error when `$stdin` isn't a TTY and you need input.
|
|
170
|
+
5. **Never raise for expected failures.** Print `✗ ... re-run: brainiac agent setup <key>`.
|
|
171
|
+
Uncaught exceptions are reported as "hook failed" and the next plugin runs.
|
|
172
|
+
6. **Prefix output** with ` [MyService]` so it's clear whose step is running.
|
|
173
|
+
|
|
174
|
+
Users choose which plugins run with `--skip` / `--only` (or by answering "n" when
|
|
175
|
+
asked during create). Core filters on the plugin name before calling you, so
|
|
176
|
+
plugins don't need to handle this.
|
|
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.
|
|
112
184
|
|
|
113
185
|
## Channel Prompts
|
|
114
186
|
|