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.
@@ -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: tags, text: effort_text_for_detection)
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
- planning_info = detect_planning_mode(
1023
- text: plain_text,
1024
- tags: card_tags,
1025
- card_internal_id: card_internal_id,
1026
- card_number: card_number
1027
- )
1028
-
1029
- prompt = if planning_info
1030
- # Planning mode
1031
- card_id = planning_info[:card_id]
1032
- LOG.info "[Planning] Planning mode active for mention on card #{card_number || card_internal_id}"
1033
-
1034
- render_planning_prompt(PROMPT_MENTION,
1035
- comment_vars.merge(
1036
- "CARD_INTERNAL_ID" => card_internal_id,
1037
- "CARD_ID" => card_id,
1038
- "CARD_NUMBER" => card_number || "N/A",
1039
- "CARD_NUMBER_TEXT" => card_number ? " (##{card_number})" : "",
1040
- "BRANCH" => branch
1041
- ),
1042
- brain_context: build_brain_context(agent_name: agent_name, card_title: card_title, card_number: card_number, project_key: project_key,
1043
- comment_body: plain_text, source: :fizzy),
1044
- card_context: prefetch_card_context(card_number, repo_path: worktree_path, agent_name: agent_name),
1045
- agent_name: agent_name)
1046
- else
1047
- render_prompt(PROMPT_MENTION,
1048
- comment_vars.merge(
1049
- "CARD_INTERNAL_ID" => card_internal_id,
1050
- "CARD_ID" => card_number || card_internal_id,
1051
- "CARD_NUMBER" => card_number || "N/A",
1052
- "CARD_NUMBER_TEXT" => card_number ? " (##{card_number})" : "",
1053
- "BRANCH" => branch
1054
- ),
1055
- brain_context: build_brain_context(agent_name: agent_name, card_title: card_title, card_number: card_number, project_key: project_key,
1056
- comment_body: plain_text, source: :fizzy),
1057
- card_context: prefetch_card_context(card_number, repo_path: worktree_path, agent_name: agent_name),
1058
- agent_name: agent_name)
1059
- end
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
- planning_info = detect_planning_mode(
1199
- text: plain_text,
1200
- tags: card_tags,
1201
- card_internal_id: card_internal_id,
1202
- card_number: card_number
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
- prompt = if planning_info
1206
- card_id = planning_info[:card_id]
1207
- LOG.info "[Planning] Planning mode active for card #{card_number || card_internal_id}"
1208
-
1209
- if work_dir == project_config["repo_path"]
1210
- render_planning_prompt(PROMPT_FOLLOWUP_NO_WORKTREE,
1211
- comment_vars.merge("CARD_INTERNAL_ID" => card_internal_id, "CARD_ID" => card_id),
1212
- brain_context: build_brain_context(agent_name: agent_name, project_key: project_key, comment_body: plain_text,
1213
- source: :fizzy),
1214
- card_context: prefetch_card_context(card_number, repo_path: project_config["repo_path"],
1215
- agent_name: agent_name),
1216
- agent_name: agent_name)
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
- render_planning_prompt(PROMPT_FOLLOWUP_WORKTREE,
1219
- comment_vars.merge("CARD_NUMBER" => card_number, "CARD_ID" => card_id),
1220
- brain_context: build_brain_context(agent_name: agent_name, card_number: card_number, project_key: project_key, comment_body: plain_text,
1221
- source: :fizzy),
1222
- card_context: prefetch_card_context(card_number, repo_path: work_dir, agent_name: agent_name),
1223
- agent_name: agent_name)
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
- elsif work_dir != project_config["repo_path"]
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
@@ -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: project-level keys > provider file > DEFAULT_PROJECT
54
- def resolve_project_cli_config(project_config)
55
- provider_config = {}
56
- if (provider_name = project_config["cli_provider"])
57
- provider_file = File.join(CLI_PROVIDERS_DIR, "#{provider_name}.json")
58
- if File.exist?(provider_file)
59
- raw = JSON.parse(File.read(provider_file))
60
- provider_config = {
61
- "agent_cli" => raw["binary"],
62
- "agent_cli_args" => raw["default_args"],
63
- "agent_model_flag" => raw["model_flag"],
64
- "allowed_models" => raw["models"]
65
- }
66
- end
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
- DEFAULT_PROJECT.merge(provider_config).merge(project_config)
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, project_key_for_restart: project_key_for_restart
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
- def build_agent_cmd(resolved, agent_config_name: nil, model: nil, effort: nil)
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
- cmd.push("--agent", agent_config_name) if agent_config_name
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
- add_trust_tools!(cmd, resolved["agent_cli_args"])
586
- cmd.push(resolved["agent_model_flag"], model) if resolved["agent_model_flag"] && !resolved["agent_model_flag"].empty? && model
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
- def check_brainiac_restart(head_before, chdir, project_key_for_restart, agent_config_name)
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, = Open3.capture2("git", "rev-parse", "HEAD", chdir: chdir)
679
- git_status, = Open3.capture2("git", "status", "--porcelain", chdir: chdir)
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"