brainiac 0.1.0 → 0.1.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/Gemfile.lock +2 -2
- data/lib/brainiac/handlers/shared/inline_tags.rb +10 -1
- data/lib/brainiac/helpers.rb +5 -2
- data/lib/brainiac/profiles.rb +116 -0
- data/lib/brainiac/routes/api.rb +2 -1
- data/lib/brainiac/version.rb +1 -1
- data/lib/brainiac.rb +1 -0
- data/receiver.rb +1 -1
- data/templates/profiles.json.example +13 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af2bf14f62db5780228f9c8f175abe703dcf0d654c1a35534446117bfced7034
|
|
4
|
+
data.tar.gz: 431b72de572f425911109cc0ced248859456ee7564dd028e5cb502654baf0d7b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 87da5739c961603be5884be7dfae1e02c312b8a5649ce612185b19149e0d1ffecb40a97ab0619787ff08c98003d333024565d4e4c7cfa4158631d5db0ea00b00
|
|
7
|
+
data.tar.gz: 70ce18db1d7520f129d286425f07af9a5e58b7f796a462d3af932bf636793db5054a1b2aac90e6b6565189d8b7e83e5f04ee0c7e1d5a56a33b36f9ec5dcde391
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
brainiac (0.1.
|
|
4
|
+
brainiac (0.1.1)
|
|
5
5
|
puma (~> 7.2)
|
|
6
6
|
rackup (~> 2.3)
|
|
7
7
|
sinatra (~> 4.1)
|
|
@@ -84,7 +84,7 @@ DEPENDENCIES
|
|
|
84
84
|
CHECKSUMS
|
|
85
85
|
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
|
|
86
86
|
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
87
|
-
brainiac (0.1.
|
|
87
|
+
brainiac (0.1.1)
|
|
88
88
|
json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
|
|
89
89
|
language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0
|
|
90
90
|
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
#
|
|
5
5
|
# Messages from any channel can contain inline tags like:
|
|
6
6
|
# [project:my-project], [opus], [effort:high], [cli:grok], [chat], [plan],
|
|
7
|
-
# [fresh], [branch:feature-xyz], [workitem:wi-abc123]
|
|
7
|
+
# [fresh], [branch:feature-xyz], [workitem:wi-abc123], [profile:k+] ([p:k+])
|
|
8
8
|
#
|
|
9
9
|
# This module provides a single parser that extracts all tags and returns
|
|
10
10
|
# a structured result with the cleaned text.
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
# model_tag: "opus" or nil (raw tag, not resolved model ID),
|
|
17
17
|
# effort: "high" or nil,
|
|
18
18
|
# cli_provider: "grok" or nil,
|
|
19
|
+
# profile: "k+" or nil,
|
|
19
20
|
# chat_mode: true/false,
|
|
20
21
|
# planning: true/false,
|
|
21
22
|
# fresh: true/false,
|
|
@@ -29,6 +30,7 @@ def parse_inline_tags(text)
|
|
|
29
30
|
model_tag: nil,
|
|
30
31
|
effort: nil,
|
|
31
32
|
cli_provider: nil,
|
|
33
|
+
profile: nil,
|
|
32
34
|
chat_mode: false,
|
|
33
35
|
planning: false,
|
|
34
36
|
fresh: false,
|
|
@@ -67,6 +69,13 @@ def parse_value_tags(result)
|
|
|
67
69
|
result[:clean_text].sub!(match[0], "")
|
|
68
70
|
end
|
|
69
71
|
|
|
72
|
+
# [profile:k+] or short alias [p:k+] — a named env bundle (e.g. an alternate
|
|
73
|
+
# kiro-cli account via XDG_DATA_HOME). Values allow non-word chars like '+'.
|
|
74
|
+
if (match = result[:clean_text].match(/\[(?:profile|p):([^\]]+)\]/i))
|
|
75
|
+
result[:profile] = match[1].strip.downcase
|
|
76
|
+
result[:clean_text].sub!(match[0], "")
|
|
77
|
+
end
|
|
78
|
+
|
|
70
79
|
# [deploy] or [deploy:dev01]
|
|
71
80
|
if (match = result[:clean_text].match(/\[deploy(?::([^\]]+))?\]/i))
|
|
72
81
|
result[:deploy_intent] = match[1]&.strip&.downcase || :auto
|
data/lib/brainiac/helpers.rb
CHANGED
|
@@ -774,7 +774,7 @@ end
|
|
|
774
774
|
# Plugins should NOT build their own resume logic; pass `resume: true` and let core handle it.
|
|
775
775
|
def run_agent(prompt, project_config:, chdir: nil, log_name: "agent", model: nil, effort: nil, agent_name: nil, card_number: nil, comment_id: nil,
|
|
776
776
|
source: nil, source_context: {}, skip_column_move: false, cli_provider: nil, resume: false,
|
|
777
|
-
message: nil, channel: nil, context: nil, env: {})
|
|
777
|
+
message: nil, channel: nil, context: nil, env: {}, profile: nil)
|
|
778
778
|
# Intent gate: if a raw message is provided, check whether the agent should respond.
|
|
779
779
|
return nil if intent_skip?(message, agent_name: agent_name, source: source, channel: channel, context: context)
|
|
780
780
|
|
|
@@ -805,7 +805,10 @@ def run_agent(prompt, project_config:, chdir: nil, log_name: "agent", model: nil
|
|
|
805
805
|
prompt_file: prompt_file, resume: should_resume,
|
|
806
806
|
output_file: output_file, chdir: chdir, title: work_item_id,
|
|
807
807
|
new_session_id: minted_session_id)
|
|
808
|
-
|
|
808
|
+
# Profile env (named bundle, e.g. an alternate kiro-cli account) layers between
|
|
809
|
+
# agent env and the explicit `env:` passed in. An explicitly-requested profile
|
|
810
|
+
# wins over agent env; the default profile is only a fallback. See profiles.rb.
|
|
811
|
+
spawn_env = profile_spawn_env(agent_env_for(agent_name), profile).merge(env)
|
|
809
812
|
|
|
810
813
|
log_agent_launch(resolved: resolved, chdir: chdir, log_file: log_file, prompt_file: prompt_file,
|
|
811
814
|
output_file: output_file, cmd: cmd, should_resume: should_resume,
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Profiles: named bundles of environment variables that can be injected into an
|
|
4
|
+
# agent dispatch without standing up a separate agent.
|
|
5
|
+
#
|
|
6
|
+
# The canonical use case is kiro-cli multi-account: each AWS account gets its own
|
|
7
|
+
# XDG_DATA_HOME (separate auth store), and a profile lets you flip which account a
|
|
8
|
+
# dispatch runs against via an inline tag ([profile:X] / [p:X]) — same CLI, same
|
|
9
|
+
# agent, different credentials.
|
|
10
|
+
#
|
|
11
|
+
# Config lives at ~/.brainiac/profiles.json:
|
|
12
|
+
#
|
|
13
|
+
# {
|
|
14
|
+
# "q": { "env": { "XDG_DATA_HOME": "/home/andy/.local/share" } },
|
|
15
|
+
# "k+": { "env": { "XDG_DATA_HOME": "/home/andy/.brainiac/kiro-accounts/kiro-pro" }, "default": true }
|
|
16
|
+
# }
|
|
17
|
+
#
|
|
18
|
+
# A profile may also optionally pin a model and/or cli_provider:
|
|
19
|
+
#
|
|
20
|
+
# "k+": { "env": {...}, "model": "opus", "cli_provider": "kiro-pro" }
|
|
21
|
+
#
|
|
22
|
+
# Exactly one profile may be marked "default": true — it applies when no [profile:X]
|
|
23
|
+
# tag is present. Because it's only a fallback, an agent's own env (from agents.json)
|
|
24
|
+
# takes precedence over the default profile. An *explicitly requested* profile, by
|
|
25
|
+
# contrast, takes precedence over agent env — that's the whole point of asking for it.
|
|
26
|
+
#
|
|
27
|
+
# Precedence at the spawn point (highest wins):
|
|
28
|
+
# explicit env passed to run_agent
|
|
29
|
+
# > explicitly-requested profile env
|
|
30
|
+
# > agent env (agents.json)
|
|
31
|
+
# > default profile env
|
|
32
|
+
# > DEFAULT_AGENT_ENV
|
|
33
|
+
|
|
34
|
+
PROFILES_FILE = File.join(BRAINIAC_DIR, "profiles.json")
|
|
35
|
+
|
|
36
|
+
def load_profiles_config
|
|
37
|
+
return {} unless File.exist?(PROFILES_FILE)
|
|
38
|
+
|
|
39
|
+
raw = JSON.parse(File.read(PROFILES_FILE))
|
|
40
|
+
LOG.info "Loaded #{raw.size} profile(s) from #{PROFILES_FILE}" if defined?(LOG)
|
|
41
|
+
|
|
42
|
+
# Normalize keys to lowercase for case-insensitive lookup, but preserve the
|
|
43
|
+
# profile body as-is. Profile keys can contain '+' etc. (e.g. "k+").
|
|
44
|
+
normalized = {}
|
|
45
|
+
raw.each { |key, entry| normalized[key.to_s.downcase] = entry }
|
|
46
|
+
normalized
|
|
47
|
+
rescue JSON::ParserError => e
|
|
48
|
+
LOG.error "Failed to parse profiles.json: #{e.message}" if defined?(LOG)
|
|
49
|
+
{}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
PROFILES = load_profiles_config
|
|
53
|
+
|
|
54
|
+
def reload_profiles!(force: false)
|
|
55
|
+
return unless file_changed?(PROFILES_FILE, force: force)
|
|
56
|
+
|
|
57
|
+
PROFILES.replace(load_profiles_config)
|
|
58
|
+
LOG.info "Reloaded profiles: #{PROFILES.keys.join(", ")}" if defined?(LOG)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Look up a profile entry by name (case-insensitive). Returns the raw hash or nil.
|
|
62
|
+
def profile_entry(profile_name)
|
|
63
|
+
return nil unless profile_name
|
|
64
|
+
|
|
65
|
+
PROFILES[profile_name.to_s.downcase]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Find the profile marked "default": true. Returns [name, entry] or nil.
|
|
69
|
+
def default_profile
|
|
70
|
+
PROFILES.find { |_name, entry| entry.is_a?(Hash) && entry["default"] }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Resolve the env hash contributed by a profile.
|
|
74
|
+
#
|
|
75
|
+
# When profile_name is given and matches a profile, returns that profile's env.
|
|
76
|
+
# When profile_name is nil, returns the default profile's env (or {}).
|
|
77
|
+
# When profile_name is given but unknown, returns {} (never silently falls back
|
|
78
|
+
# to the default — a typo shouldn't route a dispatch to the wrong account).
|
|
79
|
+
#
|
|
80
|
+
# The caller decides precedence relative to agent env — see profile_spawn_env.
|
|
81
|
+
def profile_env(profile_name)
|
|
82
|
+
entry = profile_name ? profile_entry(profile_name) : default_profile&.last
|
|
83
|
+
return {} unless entry.is_a?(Hash)
|
|
84
|
+
|
|
85
|
+
(entry["env"] || {}).each_with_object({}) { |(k, v), h| h[k.to_s] = v.to_s }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Compute the profile contribution to a spawn env, honoring precedence relative
|
|
89
|
+
# to agent env.
|
|
90
|
+
#
|
|
91
|
+
# - An explicitly-requested profile (profile_name present and known) layers ON TOP
|
|
92
|
+
# of agent env — the request wins, so you can flip accounts for one dispatch.
|
|
93
|
+
# - The default profile (no profile_name given) layers UNDER agent env — it's only
|
|
94
|
+
# a fallback for dispatches that didn't ask for anything.
|
|
95
|
+
# - An unknown profile name is ignored (agent env only) and logged, so a typo never
|
|
96
|
+
# silently swaps accounts.
|
|
97
|
+
#
|
|
98
|
+
# Returns a merged env hash ready to be used as the base for further merges
|
|
99
|
+
# (e.g. run_agent's explicit `env:` still wins by merging afterward).
|
|
100
|
+
def profile_spawn_env(agent_env, profile_name)
|
|
101
|
+
if profile_name
|
|
102
|
+
if profile_entry(profile_name)
|
|
103
|
+
# Explicit request: profile wins over agent env.
|
|
104
|
+
agent_env.merge(profile_env(profile_name))
|
|
105
|
+
else
|
|
106
|
+
LOG.warn "Unknown profile '#{profile_name}' — ignoring (using agent env)" if defined?(LOG)
|
|
107
|
+
agent_env
|
|
108
|
+
end
|
|
109
|
+
else
|
|
110
|
+
# No profile requested: default profile is a baseline agent env overrides.
|
|
111
|
+
default_env = profile_env(nil)
|
|
112
|
+
return agent_env if default_env.empty?
|
|
113
|
+
|
|
114
|
+
default_env.merge(agent_env)
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/brainiac/routes/api.rb
CHANGED
|
@@ -137,9 +137,10 @@ post "/api/reload" do
|
|
|
137
137
|
reload_projects!(force: true)
|
|
138
138
|
reload_agent_registry!(force: true)
|
|
139
139
|
reload_user_registry!(force: true)
|
|
140
|
+
reload_profiles!(force: true)
|
|
140
141
|
ReloadHooks.run_all!
|
|
141
142
|
{ status: "reloaded", projects: PROJECTS.keys, agents: all_agent_names.to_a, registry: AGENT_REGISTRY.keys,
|
|
142
|
-
users: USER_REGISTRY["users"].size }.to_json
|
|
143
|
+
users: USER_REGISTRY["users"].size, profiles: PROFILES.keys }.to_json
|
|
143
144
|
end
|
|
144
145
|
|
|
145
146
|
# --- Agents & Roles ---
|
data/lib/brainiac/version.rb
CHANGED
data/lib/brainiac.rb
CHANGED
data/receiver.rb
CHANGED
|
@@ -84,7 +84,7 @@ set :quiet, true
|
|
|
84
84
|
set :server_settings, { Silent: true }
|
|
85
85
|
|
|
86
86
|
# Custom logger that filters polling endpoints unless LOG_LEVEL=debug
|
|
87
|
-
SILENT_POLL_PATHS = %w[/api/status /api/deployments].freeze
|
|
87
|
+
SILENT_POLL_PATHS = %w[/api/status /api/deployments /api/sessions/history].freeze
|
|
88
88
|
|
|
89
89
|
class SelectiveLogger < Rack::CommonLogger
|
|
90
90
|
def call(env)
|
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.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Davis
|
|
@@ -139,6 +139,7 @@ files:
|
|
|
139
139
|
- lib/brainiac/model_parser.rb
|
|
140
140
|
- lib/brainiac/notifications.rb
|
|
141
141
|
- lib/brainiac/plugins.rb
|
|
142
|
+
- lib/brainiac/profiles.rb
|
|
142
143
|
- lib/brainiac/prompts.rb
|
|
143
144
|
- lib/brainiac/restart.rb
|
|
144
145
|
- lib/brainiac/routes/api.rb
|
|
@@ -170,6 +171,7 @@ files:
|
|
|
170
171
|
- templates/cli-providers/opencode.json.example
|
|
171
172
|
- templates/hooks/pre-commit
|
|
172
173
|
- templates/plugins.json.example
|
|
174
|
+
- templates/profiles.json.example
|
|
173
175
|
- templates/roles/code-reviewer.md.example
|
|
174
176
|
- templates/roles/general-engineer.md.example
|
|
175
177
|
- templates/roles/test-engineer.md.example
|