brainiac 0.0.2 → 0.0.4
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/README.md +2 -2
- data/bin/brainiac +317 -0
- data/bin/brainiac-completion.bash +178 -0
- data/brainiac.gemspec +0 -4
- data/lib/brainiac/agents.rb +34 -0
- data/lib/brainiac/brain.rb +25 -0
- data/lib/brainiac/config.rb +4 -0
- data/lib/brainiac/cron.rb +9 -6
- data/lib/brainiac/handlers/discord.rb +289 -30
- data/lib/brainiac/handlers/fizzy.rb +129 -86
- data/lib/brainiac/helpers.rb +125 -41
- data/lib/brainiac/prompts.rb +97 -105
- data/lib/brainiac/version.rb +1 -1
- data/receiver.rb +13 -0
- data/templates/cli-providers/grok.json.example +15 -0
- data/templates/cli-providers/kiro.json.example +15 -0
- data/templates/roles/code-reviewer.md.example +28 -0
- data/templates/roles/general-engineer.md.example +36 -0
- data/templates/roles/test-engineer.md.example +33 -0
- metadata +8 -30
- checksums.yaml.gz.sig +0 -0
- data/certs/stowzilla.pem +0 -26
- data.tar.gz.sig +0 -0
- metadata.gz.sig +0 -3
|
@@ -143,6 +143,7 @@ def handle_card_assigned(payload)
|
|
|
143
143
|
branch = "fizzy-#{card_number}-#{slugify(title)}"
|
|
144
144
|
model = detect_model(project_config, tags: tags)
|
|
145
145
|
effort = detect_effort(project_config, tags: tags)
|
|
146
|
+
cli_provider_override = detect_cli_provider(tags: tags)
|
|
146
147
|
|
|
147
148
|
card_key = "card-#{card_number}"
|
|
148
149
|
if session_active?(card_key)
|
|
@@ -274,7 +275,7 @@ def handle_card_assigned(payload)
|
|
|
274
275
|
end
|
|
275
276
|
|
|
276
277
|
pid, log_file = run_agent(prompt, project_config: project_config, chdir: worktree_path, log_name: "assigned-#{card_number}", model: model, effort: effort, agent_name: agent_name,
|
|
277
|
-
card_number: card_number, source: :fizzy, source_context: { card_number: card_number })
|
|
278
|
+
card_number: card_number, source: :fizzy, source_context: { card_number: card_number }, cli_provider: cli_provider_override)
|
|
278
279
|
register_session(card_key, pid, log_file: log_file, supersede_key: card_key, agent_name: assigned_agent)
|
|
279
280
|
|
|
280
281
|
# Move card to Right Now — agent is starting work
|
|
@@ -635,8 +636,13 @@ def handle_comment(payload)
|
|
|
635
636
|
end
|
|
636
637
|
end
|
|
637
638
|
|
|
639
|
+
card_tags = eventable.dig("card", "tags") || []
|
|
638
640
|
model = detect_model(project_config, text: plain_text)
|
|
639
|
-
effort = detect_effort(project_config, tags:
|
|
641
|
+
effort = detect_effort(project_config, tags: card_tags, text: effort_text_for_detection)
|
|
642
|
+
cli_provider_override = detect_cli_provider(text: plain_text, tags: card_tags)
|
|
643
|
+
|
|
644
|
+
# Strip [cli:X] tag from prompt content
|
|
645
|
+
plain_text = plain_text.sub(/\[cli:\w+\]/i, "").strip
|
|
640
646
|
|
|
641
647
|
# Determine which agent should handle this comment.
|
|
642
648
|
#
|
|
@@ -826,7 +832,7 @@ def handle_comment(payload)
|
|
|
826
832
|
pid, log_file = run_agent(prompt, project_config: project_config, chdir: review_worktree_path,
|
|
827
833
|
log_name: "review-#{agent_name.downcase}-#{card_number || card_internal_id}", model: model, effort: effort, agent_name: agent_name,
|
|
828
834
|
card_number: card_number, comment_id: comment_id,
|
|
829
|
-
source: :fizzy, source_context: { card_number: card_number })
|
|
835
|
+
source: :fizzy, source_context: { card_number: card_number }, cli_provider: cli_provider_override)
|
|
830
836
|
register_session(card_key, pid, log_file: log_file, supersede_key: card_key, agent_name: agent_name)
|
|
831
837
|
|
|
832
838
|
return [200, { status: "cross_agent_review", agent: agent_name, card_agent: card_assigned_agent,
|
|
@@ -912,7 +918,7 @@ def handle_comment(payload)
|
|
|
912
918
|
work_dir: work_dir, project_config: project_config, project_key: project_key,
|
|
913
919
|
comment_vars: comment_vars, plain_text: plain_text, model: model,
|
|
914
920
|
agent_name: agent_name, comment_id: comment_id, eventable: eventable,
|
|
915
|
-
deploy_intent: deploy_intent
|
|
921
|
+
deploy_intent: deploy_intent, cli_provider: cli_provider_override
|
|
916
922
|
)
|
|
917
923
|
end
|
|
918
924
|
|
|
@@ -936,7 +942,7 @@ def handle_comment(payload)
|
|
|
936
942
|
work_dir: work_dir, project_config: project_config, project_key: project_key,
|
|
937
943
|
comment_vars: comment_vars, plain_text: plain_text, model: model,
|
|
938
944
|
agent_name: agent_name, comment_id: comment_id, eventable: eventable,
|
|
939
|
-
deploy_intent: deploy_intent
|
|
945
|
+
deploy_intent: deploy_intent, cli_provider: cli_provider_override
|
|
940
946
|
)
|
|
941
947
|
[200, result.to_json]
|
|
942
948
|
else
|
|
@@ -1019,47 +1025,63 @@ def handle_comment(payload)
|
|
|
1019
1025
|
|
|
1020
1026
|
# Detect planning mode
|
|
1021
1027
|
card_tags = eventable.dig("card", "tags") || []
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1028
|
+
|
|
1029
|
+
# Check if we can resume the existing session (lean prompt) vs full prompt
|
|
1030
|
+
resolved = resolve_project_cli_config(project_config, cli_provider_override: cli_provider_override, agent_name: agent_name)
|
|
1031
|
+
should_resume = resolved["resume_flag"]
|
|
1032
|
+
|
|
1033
|
+
if should_resume
|
|
1034
|
+
prompt = render_resume_prompt(
|
|
1035
|
+
comment_body: plain_text,
|
|
1036
|
+
comment_creator: comment_vars["COMMENT_CREATOR"],
|
|
1037
|
+
comment_id: comment_id,
|
|
1038
|
+
card_number: card_number,
|
|
1039
|
+
agent_name: agent_name
|
|
1040
|
+
)
|
|
1041
|
+
LOG.info "[Resume] Using lean prompt for mention on card #{card_number || card_internal_id} (#{resolved["agent_cli"]})"
|
|
1042
|
+
else
|
|
1043
|
+
planning_info = detect_planning_mode(
|
|
1044
|
+
text: plain_text,
|
|
1045
|
+
tags: card_tags,
|
|
1046
|
+
card_internal_id: card_internal_id,
|
|
1047
|
+
card_number: card_number
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
prompt = if planning_info
|
|
1051
|
+
# Planning mode
|
|
1052
|
+
card_id = planning_info[:card_id]
|
|
1053
|
+
LOG.info "[Planning] Planning mode active for mention on card #{card_number || card_internal_id}"
|
|
1054
|
+
|
|
1055
|
+
render_planning_prompt(PROMPT_MENTION,
|
|
1056
|
+
comment_vars.merge(
|
|
1057
|
+
"CARD_INTERNAL_ID" => card_internal_id,
|
|
1058
|
+
"CARD_ID" => card_id,
|
|
1059
|
+
"CARD_NUMBER" => card_number || "N/A",
|
|
1060
|
+
"CARD_NUMBER_TEXT" => card_number ? " (##{card_number})" : "",
|
|
1061
|
+
"BRANCH" => branch
|
|
1062
|
+
),
|
|
1063
|
+
brain_context: build_brain_context(agent_name: agent_name, card_title: card_title, card_number: card_number, project_key: project_key,
|
|
1064
|
+
comment_body: plain_text, source: :fizzy),
|
|
1065
|
+
card_context: prefetch_card_context(card_number, repo_path: worktree_path, agent_name: agent_name),
|
|
1066
|
+
agent_name: agent_name)
|
|
1067
|
+
else
|
|
1068
|
+
render_prompt(PROMPT_MENTION,
|
|
1069
|
+
comment_vars.merge(
|
|
1070
|
+
"CARD_INTERNAL_ID" => card_internal_id,
|
|
1071
|
+
"CARD_ID" => card_number || card_internal_id,
|
|
1072
|
+
"CARD_NUMBER" => card_number || "N/A",
|
|
1073
|
+
"CARD_NUMBER_TEXT" => card_number ? " (##{card_number})" : "",
|
|
1074
|
+
"BRANCH" => branch
|
|
1075
|
+
),
|
|
1076
|
+
brain_context: build_brain_context(agent_name: agent_name, card_title: card_title, card_number: card_number, project_key: project_key,
|
|
1077
|
+
comment_body: plain_text, source: :fizzy),
|
|
1078
|
+
card_context: prefetch_card_context(card_number, repo_path: worktree_path, agent_name: agent_name),
|
|
1079
|
+
agent_name: agent_name)
|
|
1080
|
+
end
|
|
1081
|
+
end
|
|
1060
1082
|
|
|
1061
1083
|
pid, log_file = run_agent(prompt, project_config: project_config, chdir: worktree_path, log_name: "mention-#{card_number || card_internal_id}", model: model, effort: effort, agent_name: agent_name, card_number: card_number, comment_id: comment_id,
|
|
1062
|
-
source: :fizzy, source_context: { card_number: card_number })
|
|
1084
|
+
source: :fizzy, source_context: { card_number: card_number }, cli_provider: cli_provider_override, resume: true)
|
|
1063
1085
|
register_session(card_key, pid, log_file: log_file, supersede_key: card_key, agent_name: agent_name)
|
|
1064
1086
|
return [200,
|
|
1065
1087
|
{ status: "responded", card_internal_id: card_internal_id, card_number: card_number, branch: branch, worktree: worktree_path,
|
|
@@ -1182,7 +1204,7 @@ def handle_comment(payload)
|
|
|
1182
1204
|
end
|
|
1183
1205
|
|
|
1184
1206
|
pid, log_file = run_agent(prompt, project_config: project_config, chdir: worktree_path, log_name: "mention-#{card_number || card_internal_id}", model: model, effort: effort, agent_name: agent_name, card_number: card_number, comment_id: comment_id,
|
|
1185
|
-
source: :fizzy, source_context: { card_number: card_number })
|
|
1207
|
+
source: :fizzy, source_context: { card_number: card_number }, cli_provider: cli_provider_override)
|
|
1186
1208
|
register_session(card_key, pid, log_file: log_file, supersede_key: card_key, agent_name: agent_name)
|
|
1187
1209
|
[200,
|
|
1188
1210
|
{ status: "responded", card_internal_id: card_internal_id, card_number: card_number, branch: branch, worktree: worktree_path,
|
|
@@ -1193,53 +1215,74 @@ end
|
|
|
1193
1215
|
# Dispatch a follow-up comment to the agent. Extracted so it can be called
|
|
1194
1216
|
# both inline (no active session) and from a queued background thread.
|
|
1195
1217
|
def dispatch_followup_comment(card_key:, card_number:, card_internal_id:, work_dir:, project_config:, project_key:, comment_vars:, plain_text:,
|
|
1196
|
-
model:, agent_name:, comment_id:, eventable:, deploy_intent: nil)
|
|
1218
|
+
model:, agent_name:, comment_id:, eventable:, deploy_intent: nil, cli_provider: nil)
|
|
1197
1219
|
card_tags = eventable.dig("card", "tags") || []
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1220
|
+
effort = detect_effort(project_config, tags: card_tags, text: plain_text)
|
|
1221
|
+
|
|
1222
|
+
# Determine if we should resume (worktree + provider supports it)
|
|
1223
|
+
is_worktree = work_dir != project_config["repo_path"]
|
|
1224
|
+
resolved = resolve_project_cli_config(project_config, cli_provider_override: cli_provider, agent_name: agent_name)
|
|
1225
|
+
should_resume = is_worktree && resolved["resume_flag"]
|
|
1226
|
+
|
|
1227
|
+
if should_resume
|
|
1228
|
+
# Lean prompt: only the new comment. The previous session
|
|
1229
|
+
# already has role, persona, knowledge, core instructions, and card history.
|
|
1230
|
+
prompt = render_resume_prompt(
|
|
1231
|
+
comment_body: plain_text,
|
|
1232
|
+
comment_creator: comment_vars["COMMENT_CREATOR"],
|
|
1233
|
+
comment_id: comment_id,
|
|
1234
|
+
card_number: card_number,
|
|
1235
|
+
agent_name: agent_name
|
|
1236
|
+
)
|
|
1237
|
+
LOG.info "[Resume] Using lean prompt for follow-up on card #{card_number || card_internal_id} (#{resolved["agent_cli"]})"
|
|
1238
|
+
else
|
|
1239
|
+
# Full prompt: no session to resume, build everything from scratch
|
|
1240
|
+
planning_info = detect_planning_mode(
|
|
1241
|
+
text: plain_text,
|
|
1242
|
+
tags: card_tags,
|
|
1243
|
+
card_internal_id: card_internal_id,
|
|
1244
|
+
card_number: card_number
|
|
1245
|
+
)
|
|
1204
1246
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1247
|
+
prompt = if planning_info
|
|
1248
|
+
card_id = planning_info[:card_id]
|
|
1249
|
+
LOG.info "[Planning] Planning mode active for card #{card_number || card_internal_id}"
|
|
1250
|
+
|
|
1251
|
+
if work_dir == project_config["repo_path"]
|
|
1252
|
+
render_planning_prompt(PROMPT_FOLLOWUP_NO_WORKTREE,
|
|
1253
|
+
comment_vars.merge("CARD_INTERNAL_ID" => card_internal_id, "CARD_ID" => card_id),
|
|
1254
|
+
brain_context: build_brain_context(agent_name: agent_name, project_key: project_key, comment_body: plain_text,
|
|
1255
|
+
source: :fizzy),
|
|
1256
|
+
card_context: prefetch_card_context(card_number, repo_path: project_config["repo_path"],
|
|
1257
|
+
agent_name: agent_name),
|
|
1258
|
+
agent_name: agent_name)
|
|
1259
|
+
else
|
|
1260
|
+
render_planning_prompt(PROMPT_FOLLOWUP_WORKTREE,
|
|
1261
|
+
comment_vars.merge("CARD_NUMBER" => card_number, "CARD_ID" => card_id),
|
|
1262
|
+
brain_context: build_brain_context(agent_name: agent_name, card_number: card_number, project_key: project_key, comment_body: plain_text,
|
|
1263
|
+
source: :fizzy),
|
|
1264
|
+
card_context: prefetch_card_context(card_number, repo_path: work_dir, agent_name: agent_name),
|
|
1265
|
+
agent_name: agent_name)
|
|
1266
|
+
end
|
|
1267
|
+
elsif work_dir != project_config["repo_path"]
|
|
1268
|
+
render_prompt(PROMPT_FOLLOWUP_WORKTREE,
|
|
1269
|
+
comment_vars.merge("CARD_NUMBER" => card_number, "CARD_ID" => card_number),
|
|
1270
|
+
brain_context: build_brain_context(agent_name: agent_name, card_number: card_number, project_key: project_key, comment_body: plain_text,
|
|
1271
|
+
source: :fizzy),
|
|
1272
|
+
card_context: prefetch_card_context(card_number, repo_path: work_dir, agent_name: agent_name),
|
|
1273
|
+
agent_name: agent_name)
|
|
1217
1274
|
else
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1275
|
+
render_prompt(PROMPT_FOLLOWUP_NO_WORKTREE,
|
|
1276
|
+
comment_vars.merge("CARD_INTERNAL_ID" => card_internal_id, "CARD_ID" => card_internal_id),
|
|
1277
|
+
brain_context: build_brain_context(agent_name: agent_name, project_key: project_key, comment_body: plain_text,
|
|
1278
|
+
source: :fizzy),
|
|
1279
|
+
card_context: prefetch_card_context(card_number, repo_path: project_config["repo_path"], agent_name: agent_name),
|
|
1280
|
+
agent_name: agent_name)
|
|
1224
1281
|
end
|
|
1225
|
-
|
|
1226
|
-
render_prompt(PROMPT_FOLLOWUP_WORKTREE,
|
|
1227
|
-
comment_vars.merge("CARD_NUMBER" => card_number, "CARD_ID" => card_number),
|
|
1228
|
-
brain_context: build_brain_context(agent_name: agent_name, card_number: card_number, project_key: project_key, comment_body: plain_text,
|
|
1229
|
-
source: :fizzy),
|
|
1230
|
-
card_context: prefetch_card_context(card_number, repo_path: work_dir, agent_name: agent_name),
|
|
1231
|
-
agent_name: agent_name)
|
|
1232
|
-
else
|
|
1233
|
-
render_prompt(PROMPT_FOLLOWUP_NO_WORKTREE,
|
|
1234
|
-
comment_vars.merge("CARD_INTERNAL_ID" => card_internal_id, "CARD_ID" => card_internal_id),
|
|
1235
|
-
brain_context: build_brain_context(agent_name: agent_name, project_key: project_key, comment_body: plain_text,
|
|
1236
|
-
source: :fizzy),
|
|
1237
|
-
card_context: prefetch_card_context(card_number, repo_path: project_config["repo_path"], agent_name: agent_name),
|
|
1238
|
-
agent_name: agent_name)
|
|
1239
|
-
end
|
|
1282
|
+
end
|
|
1240
1283
|
|
|
1241
1284
|
pid, log_file = run_agent(prompt, project_config: project_config, chdir: work_dir, log_name: "followup-#{card_number || card_internal_id}", model: model, effort: effort, agent_name: agent_name, card_number: card_number, comment_id: comment_id,
|
|
1242
|
-
source: :fizzy, source_context: { card_number: card_number, card_internal_id: card_internal_id, deploy_intent: deploy_intent })
|
|
1285
|
+
source: :fizzy, source_context: { card_number: card_number, card_internal_id: card_internal_id, deploy_intent: deploy_intent }, cli_provider: cli_provider, resume: is_worktree)
|
|
1243
1286
|
register_session(card_key, pid, log_file: log_file, supersede_key: card_key, agent_name: agent_name)
|
|
1244
1287
|
|
|
1245
1288
|
# Move card to Right Now — agent is actively working again
|
data/lib/brainiac/helpers.rb
CHANGED
|
@@ -7,14 +7,9 @@ CLI_PROVIDERS_DIR = File.join(BRAINIAC_DIR, "cli-providers")
|
|
|
7
7
|
|
|
8
8
|
# --trust-all-tools alone doesn't bypass the non-interactive deny list in kiro-cli 1.29.8+.
|
|
9
9
|
# Adding --trust-tools with explicit tool names ensures write/exec tools are approved.
|
|
10
|
+
# This is now configured in the kiro provider's default_args instead of being hardcoded here.
|
|
10
11
|
TRUSTED_TOOLS = "execute_bash,fs_write,fs_read,code,grep,glob,web_search,web_fetch,use_subagent,use_aws"
|
|
11
12
|
|
|
12
|
-
def add_trust_tools!(cmd, agent_cli_args)
|
|
13
|
-
return if agent_cli_args.include?("--trust-tools")
|
|
14
|
-
|
|
15
|
-
cmd.push("--trust-tools", TRUSTED_TOOLS)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
13
|
# Clean up all worktrees associated with a card: the primary worktree and any
|
|
19
14
|
# cross-agent review worktrees (e.g. glados-fizzy-123-*, threepio-fizzy-123-*).
|
|
20
15
|
# Safe: skips worktrees with uncommitted changes.
|
|
@@ -49,24 +44,86 @@ def cleanup_card_worktrees(card_number, repo_path:, primary_worktree: nil, prima
|
|
|
49
44
|
LOG.info "Card ##{card_number}: cleaned up #{cleaned} worktree(s)" if cleaned.positive?
|
|
50
45
|
end
|
|
51
46
|
|
|
47
|
+
# Load a CLI provider config from ~/.brainiac/cli-providers/<name>.json.
|
|
48
|
+
# Returns a hash with normalized keys, or {} if not found.
|
|
49
|
+
def load_cli_provider(provider_name)
|
|
50
|
+
return {} unless provider_name
|
|
51
|
+
|
|
52
|
+
provider_file = File.join(CLI_PROVIDERS_DIR, "#{provider_name}.json")
|
|
53
|
+
return {} unless File.exist?(provider_file)
|
|
54
|
+
|
|
55
|
+
raw = JSON.parse(File.read(provider_file))
|
|
56
|
+
config = {
|
|
57
|
+
"agent_cli" => raw["binary"],
|
|
58
|
+
"agent_cli_args" => raw["default_args"],
|
|
59
|
+
"agent_model_flag" => raw["model_flag"],
|
|
60
|
+
"agent_effort_flag" => raw["effort_flag"],
|
|
61
|
+
"allowed_models" => raw["models"],
|
|
62
|
+
"allowed_efforts" => raw["efforts"]
|
|
63
|
+
}
|
|
64
|
+
# agent_flag: how the agent identity is passed (default: "--agent").
|
|
65
|
+
# Set to null/false in provider JSON to suppress passing agent name entirely.
|
|
66
|
+
# We must preserve the key even when nil so merges don't lose the "no agent flag" intent.
|
|
67
|
+
config["agent_flag"] = raw.key?("agent_flag") ? raw["agent_flag"] : "--agent"
|
|
68
|
+
# prompt_mode: "stdin" (default) or "flag" — how the prompt is delivered.
|
|
69
|
+
config["prompt_mode"] = raw["prompt_mode"] || "stdin"
|
|
70
|
+
config["prompt_flag"] = raw["prompt_flag"] if raw["prompt_flag"]
|
|
71
|
+
# resume_flag: when set, follow-up dispatches use this flag to continue the
|
|
72
|
+
# most recent session in the working directory (e.g. "-c" or "--continue").
|
|
73
|
+
config["resume_flag"] = raw["resume_flag"] if raw["resume_flag"]
|
|
74
|
+
# Compact nil values except agent_flag (which uses nil to mean "don't pass agent name")
|
|
75
|
+
agent_flag_value = config["agent_flag"]
|
|
76
|
+
config.compact!
|
|
77
|
+
config["agent_flag"] = agent_flag_value if raw.key?("agent_flag")
|
|
78
|
+
config
|
|
79
|
+
rescue JSON::ParserError => e
|
|
80
|
+
LOG.warn "Failed to parse CLI provider '#{provider_name}': #{e.message}"
|
|
81
|
+
{}
|
|
82
|
+
end
|
|
83
|
+
|
|
52
84
|
# Resolve CLI config for a project by merging provider defaults with project overrides.
|
|
53
|
-
# Priority:
|
|
54
|
-
def resolve_project_cli_config(project_config)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
85
|
+
# Priority: cli_provider_override > agent-level cli_provider > project-level cli_provider > DEFAULT_PROJECT
|
|
86
|
+
def resolve_project_cli_config(project_config, cli_provider_override: nil, agent_name: nil)
|
|
87
|
+
# Determine which CLI provider to use (priority: override > agent > project)
|
|
88
|
+
provider_name = cli_provider_override
|
|
89
|
+
provider_name ||= agent_cli_provider_for(agent_name) if agent_name
|
|
90
|
+
provider_name ||= project_config["cli_provider"]
|
|
91
|
+
|
|
92
|
+
provider_config = load_cli_provider(provider_name)
|
|
93
|
+
|
|
94
|
+
DEFAULT_PROJECT.merge(provider_config).merge(project_config).tap do |resolved|
|
|
95
|
+
# If an override or agent-level provider was used, it should win over the
|
|
96
|
+
# project-level cli_provider's config. Re-apply the override provider on top.
|
|
97
|
+
resolved.merge!(provider_config) if provider_name && provider_name != project_config["cli_provider"]
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Get the cli_provider configured at the agent level in agents.json.
|
|
102
|
+
def agent_cli_provider_for(agent_name)
|
|
103
|
+
return nil unless agent_name
|
|
104
|
+
|
|
105
|
+
key = agent_name.downcase.gsub(/[^a-z0-9-]/, "-")
|
|
106
|
+
entry = AGENT_REGISTRY[key]
|
|
107
|
+
return nil unless entry.is_a?(Hash)
|
|
108
|
+
|
|
109
|
+
entry["cli_provider"]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Detect CLI provider override from inline [cli:X] tag or Fizzy card tags.
|
|
113
|
+
# Returns the provider name (e.g. "grok") or nil.
|
|
114
|
+
def detect_cli_provider(text: "", tags: [])
|
|
115
|
+
# Inline tag: [cli:grok]
|
|
116
|
+
if (match = text.match(/\[cli:(\w+)\]/i))
|
|
117
|
+
return match[1].downcase
|
|
67
118
|
end
|
|
68
119
|
|
|
69
|
-
|
|
120
|
+
# Fizzy card tags: cli-grok
|
|
121
|
+
tags.each do |tag|
|
|
122
|
+
name = (tag.is_a?(Hash) ? tag["name"] : tag).to_s.downcase
|
|
123
|
+
return name.sub("cli-", "") if name.start_with?("cli-")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
nil
|
|
70
127
|
end
|
|
71
128
|
|
|
72
129
|
# Copy gitignored files matching .worktreeinclude patterns from repo to worktree.
|
|
@@ -502,13 +559,17 @@ rescue StandardError => e
|
|
|
502
559
|
end
|
|
503
560
|
|
|
504
561
|
def run_agent(prompt, project_config:, chdir: nil, log_name: "agent", model: nil, effort: nil, agent_name: nil, card_number: nil, comment_id: nil,
|
|
505
|
-
source: nil, source_context: {}, skip_column_move: false)
|
|
506
|
-
resolved = resolve_project_cli_config(project_config)
|
|
562
|
+
source: nil, source_context: {}, skip_column_move: false, cli_provider: nil, resume: false)
|
|
563
|
+
resolved = resolve_project_cli_config(project_config, cli_provider_override: cli_provider, agent_name: agent_name)
|
|
507
564
|
chdir ||= resolved["repo_path"]
|
|
508
565
|
model ||= resolved["agent_model"]
|
|
509
566
|
effort ||= resolved["agent_effort"]
|
|
510
567
|
agent_config_name = agent_name&.downcase&.gsub(/[^a-z0-9-]/, "-")
|
|
511
568
|
|
|
569
|
+
# Auto-resume: if the provider supports session resume and we're in a worktree
|
|
570
|
+
# that has had a previous session, resume it. Only applies to follow-ups (not first dispatch).
|
|
571
|
+
should_resume = resume && resolved["resume_flag"]
|
|
572
|
+
|
|
512
573
|
ensure_fizzy_yaml!(chdir, project_config)
|
|
513
574
|
Thread.new { scrub_invalid_attachments!(chdir) }
|
|
514
575
|
|
|
@@ -517,24 +578,22 @@ def run_agent(prompt, project_config:, chdir: nil, log_name: "agent", model: nil
|
|
|
517
578
|
FileUtils.mkdir_p(File.dirname(log_file))
|
|
518
579
|
|
|
519
580
|
prompt_file = write_agent_prompt_file(prompt, log_name, timestamp)
|
|
520
|
-
cmd = build_agent_cmd(resolved, agent_config_name: agent_config_name, model: model, effort: effort)
|
|
581
|
+
cmd = build_agent_cmd(resolved, agent_config_name: agent_config_name, model: model, effort: effort, prompt_file: prompt_file, resume: should_resume)
|
|
582
|
+
prompt_mode = resolved["prompt_mode"] || "stdin"
|
|
583
|
+
|
|
521
584
|
spawn_env = agent_env_for(agent_name)
|
|
522
585
|
|
|
523
586
|
LOG.info "Running #{resolved["agent_cli"]} in #{chdir}, logging to #{log_file}"
|
|
524
587
|
LOG.info "Prompt written to #{prompt_file}"
|
|
525
|
-
LOG.info "Command: #{cmd.join(" ")}"
|
|
588
|
+
LOG.info "Command: #{cmd.join(" ")}#{" (resuming session)" if should_resume}"
|
|
526
589
|
LOG.info "Injecting #{spawn_env.size} env var(s) for agent #{agent_name}: #{spawn_env.keys.join(", ")}" unless spawn_env.empty?
|
|
527
590
|
|
|
528
|
-
head_before = nil
|
|
529
591
|
project_key_for_restart = PROJECTS.find { |_k, v| v == project_config }&.first
|
|
530
|
-
if project_key_for_restart == "brainiac"
|
|
531
|
-
head_before, = Open3.capture2("git", "rev-parse", "HEAD", chdir: chdir)
|
|
532
|
-
head_before = head_before.strip
|
|
533
|
-
end
|
|
592
|
+
head_before, status_before = capture_git_state(chdir) if project_key_for_restart == "brainiac"
|
|
534
593
|
|
|
535
594
|
pid = spawn(spawn_env, *cmd,
|
|
536
595
|
chdir: chdir,
|
|
537
|
-
in: prompt_file,
|
|
596
|
+
**(prompt_mode == "stdin" ? { in: prompt_file } : {}),
|
|
538
597
|
out: [log_file, "w"],
|
|
539
598
|
err: %i[child out])
|
|
540
599
|
|
|
@@ -546,7 +605,8 @@ def run_agent(prompt, project_config:, chdir: nil, log_name: "agent", model: nil
|
|
|
546
605
|
prompt_file: prompt_file, chdir: chdir, source: source,
|
|
547
606
|
source_context: source_context, project_config: project_config,
|
|
548
607
|
card_number: card_number, skip_column_move: skip_column_move,
|
|
549
|
-
head_before: head_before,
|
|
608
|
+
head_before: head_before, status_before: status_before,
|
|
609
|
+
project_key_for_restart: project_key_for_restart
|
|
550
610
|
)
|
|
551
611
|
end
|
|
552
612
|
|
|
@@ -578,13 +638,28 @@ def write_agent_prompt_file(prompt, log_name, timestamp)
|
|
|
578
638
|
end
|
|
579
639
|
|
|
580
640
|
# Build the CLI command array for an agent invocation.
|
|
581
|
-
|
|
641
|
+
# When prompt_file is provided and prompt_mode is "flag", appends the prompt as a CLI argument.
|
|
642
|
+
# When resume is true and the provider has a resume_flag, adds it to continue the last session.
|
|
643
|
+
def build_agent_cmd(resolved, agent_config_name: nil, model: nil, effort: nil, prompt_file: nil, resume: false)
|
|
582
644
|
cmd = [resolved["agent_cli"]]
|
|
583
|
-
|
|
645
|
+
# agent_flag controls how the agent identity is passed. Defaults to "--agent".
|
|
646
|
+
# Provider configs can set it to a different flag or null to suppress entirely.
|
|
647
|
+
agent_flag = resolved.key?("agent_flag") ? resolved["agent_flag"] : "--agent"
|
|
648
|
+
cmd.push(agent_flag, agent_config_name) if agent_flag && agent_config_name
|
|
584
649
|
cmd.concat(resolved["agent_cli_args"].split)
|
|
585
|
-
|
|
586
|
-
|
|
650
|
+
# Only pass --model if the model is a valid ID for this provider.
|
|
651
|
+
# "auto" means "let the CLI choose" — skip passing it unless the provider explicitly maps it.
|
|
652
|
+
if model && resolved["agent_model_flag"] && !resolved["agent_model_flag"].empty?
|
|
653
|
+
allowed = resolved["allowed_models"] || {}
|
|
654
|
+
# Pass the model if it's a mapped value (e.g. "claude-opus-4.6") or the key itself is mapped
|
|
655
|
+
is_known = allowed.value?(model) || allowed.key?(model)
|
|
656
|
+
cmd.push(resolved["agent_model_flag"], model) if is_known
|
|
657
|
+
end
|
|
587
658
|
cmd.push(resolved["agent_effort_flag"], effort) if resolved["agent_effort_flag"] && !resolved["agent_effort_flag"].empty? && effort
|
|
659
|
+
# Resume the most recent session in the working directory (for multi-turn CLIs like grok)
|
|
660
|
+
cmd.push(resolved["resume_flag"]) if resume && resolved["resume_flag"]
|
|
661
|
+
# prompt_mode: "flag" passes the prompt file path via the configured prompt_flag (e.g. --prompt-file).
|
|
662
|
+
cmd.push(resolved["prompt_flag"], prompt_file) if prompt_file && resolved["prompt_mode"] == "flag" && resolved["prompt_flag"]
|
|
588
663
|
cmd
|
|
589
664
|
end
|
|
590
665
|
|
|
@@ -621,7 +696,7 @@ def handle_agent_completion(**ctx)
|
|
|
621
696
|
end
|
|
622
697
|
|
|
623
698
|
brain_push(message: "#{ctx[:agent_config_name] || "agent"}: #{ctx[:log_name]}")
|
|
624
|
-
check_brainiac_restart(ctx[:head_before], ctx[:chdir], ctx[:project_key_for_restart], ctx[:agent_config_name])
|
|
699
|
+
check_brainiac_restart(ctx[:head_before], ctx[:status_before], ctx[:chdir], ctx[:project_key_for_restart], ctx[:agent_config_name])
|
|
625
700
|
end
|
|
626
701
|
|
|
627
702
|
def handle_fizzy_post_session(fizzy_card, exit_status, signaled, agent_name, chdir, source, source_context, project_config, skip_column_move)
|
|
@@ -672,12 +747,21 @@ def handle_plan_finalization(prompt_file, agent_name, project_config)
|
|
|
672
747
|
end
|
|
673
748
|
end
|
|
674
749
|
|
|
675
|
-
|
|
750
|
+
# Capture git HEAD and working tree status for a directory.
|
|
751
|
+
# Returns [head_sha, status_porcelain] or [nil, nil] on failure.
|
|
752
|
+
def capture_git_state(chdir)
|
|
753
|
+
head, = Open3.capture2("git", "rev-parse", "HEAD", chdir: chdir)
|
|
754
|
+
status, = Open3.capture2("git", "status", "--porcelain", chdir: chdir)
|
|
755
|
+
[head.strip, status.strip]
|
|
756
|
+
rescue StandardError
|
|
757
|
+
[nil, nil]
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
def check_brainiac_restart(head_before, status_before, chdir, project_key_for_restart, agent_config_name)
|
|
676
761
|
return unless project_key_for_restart == "brainiac" && head_before
|
|
677
762
|
|
|
678
|
-
head_after, =
|
|
679
|
-
|
|
680
|
-
if head_after.strip != head_before || !git_status.strip.empty?
|
|
763
|
+
head_after, status_after = capture_git_state(chdir)
|
|
764
|
+
if head_after != head_before || status_after != (status_before || "")
|
|
681
765
|
queue_brainiac_restart(agent_config_name || "agent")
|
|
682
766
|
else
|
|
683
767
|
LOG.info "[Brainiac] #{agent_config_name || "agent"} session on brainiac had no changes — skipping restart"
|