-
Notifications
You must be signed in to change notification settings - Fork 219
Add CodeBuddy AI coding agent support with checkpoint preset and hook installation #1600
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| [package] | ||
| name = "git-ai" | ||
| version = "1.5.13" | ||
| version = "1.5.14" | ||
| edition = "2024" | ||
| default-run = "git-ai" | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,213 @@ | ||
| use super::parse; | ||
| use super::{ | ||
| AgentPreset, ParsedHookEvent, PostBashCall, PostFileEdit, PreBashCall, PreFileEdit, | ||
| PresetContext, StreamFormat, StreamSource, | ||
| }; | ||
| use crate::authorship::authorship_log_serialization::generate_session_id; | ||
| use crate::authorship::working_log::AgentId; | ||
| use crate::commands::checkpoint_agent::bash_tool::{self, Agent, ToolClass}; | ||
| use crate::error::GitAiError; | ||
| use std::collections::HashMap; | ||
| use std::path::{Path, PathBuf}; | ||
|
|
||
| pub struct CodeBuddyPreset; | ||
|
|
||
| impl AgentPreset for CodeBuddyPreset { | ||
| fn parse(&self, hook_input: &str, trace_id: &str) -> Result<Vec<ParsedHookEvent>, GitAiError> { | ||
| let data: serde_json::Value = serde_json::from_str(hook_input) | ||
| .map_err(|e| GitAiError::PresetError(format!("Invalid JSON in hook_input: {}", e)))?; | ||
|
|
||
| let cwd = parse::required_str(&data, "cwd")?; | ||
| let transcript_path = parse::required_str(&data, "transcript_path")?; | ||
|
|
||
| let session_id = parse::optional_str(&data, "session_id") | ||
| .map(|s| s.to_string()) | ||
| .unwrap_or_else(|| { | ||
| parse::required_file_stem(&data, "transcript_path") | ||
| .unwrap_or_else(|_| "unknown".to_string()) | ||
| }); | ||
|
|
||
| let tool_name = parse::optional_str_multi(&data, &["tool_name", "toolName"]); | ||
| let hook_event = parse::optional_str_multi(&data, &["hook_event_name", "hookEventName"]); | ||
| let tool_use_id = | ||
| parse::str_or_default_multi(&data, &["tool_use_id", "toolUseId"], "bash"); | ||
|
|
||
| let is_bash = tool_name | ||
| .map(|n| bash_tool::classify_tool(Agent::CodeBuddy, n) == ToolClass::Bash) | ||
| .unwrap_or(false); | ||
|
|
||
| let context = PresetContext { | ||
| agent_id: AgentId { | ||
| tool: "codebuddy".to_string(), | ||
| id: session_id.clone(), | ||
| model: crate::streams::model_extraction::extract_model( | ||
| Path::new(transcript_path), | ||
| crate::streams::sweep::StreamFormat::ClaudeJsonl, | ||
| None, | ||
| ) | ||
| .ok() | ||
| .flatten() | ||
| .unwrap_or_else(|| "unknown".to_string()), | ||
| }, | ||
| external_session_id: session_id.clone(), | ||
| trace_id: trace_id.to_string(), | ||
| cwd: PathBuf::from(cwd), | ||
| metadata: HashMap::from([( | ||
| "transcript_path".to_string(), | ||
| transcript_path.to_string(), | ||
| )]), | ||
| }; | ||
|
|
||
| let transcript_path_buf = PathBuf::from(transcript_path); | ||
| let stream_source = Some(StreamSource { | ||
| path: transcript_path_buf, | ||
| format: StreamFormat::ClaudeJsonl, | ||
| session_id: generate_session_id(&session_id, "codebuddy"), | ||
| external_session_id: session_id.clone(), | ||
| external_parent_session_id: None, | ||
| }); | ||
|
Comment on lines
+62
to
+68
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 CodeBuddy not registered in ALL_AGENT_TYPES — no sweep/discovery support CodeBuddy is not added to Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| let event = match (hook_event, is_bash) { | ||
| (Some("PreToolUse"), true) => ParsedHookEvent::PreBashCall(PreBashCall { | ||
| context, | ||
| tool_use_id: tool_use_id.to_string(), | ||
| }), | ||
| (Some("PreToolUse"), false) => ParsedHookEvent::PreFileEdit(PreFileEdit { | ||
| context, | ||
| file_paths: parse::file_paths_from_tool_input(&data, cwd), | ||
| dirty_files: None, | ||
| tool_use_id: Some(tool_use_id.to_string()), | ||
| }), | ||
| (_, true) => ParsedHookEvent::PostBashCall(PostBashCall { | ||
| context, | ||
| tool_use_id: tool_use_id.to_string(), | ||
| stream_source, | ||
| }), | ||
| (_, false) => ParsedHookEvent::PostFileEdit(PostFileEdit { | ||
| context, | ||
| file_paths: parse::file_paths_from_tool_input(&data, cwd), | ||
| dirty_files: None, | ||
| stream_source, | ||
| tool_use_id: Some(tool_use_id.to_string()), | ||
| }), | ||
| }; | ||
|
|
||
| Ok(vec![event]) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use crate::commands::checkpoint_agent::presets::*; | ||
| use serde_json::json; | ||
|
|
||
| fn make_hook_input(event: &str, tool: &str) -> String { | ||
| json!({ | ||
| "transcript_path": "/home/user/.codebuddy/sessions/abc123.jsonl", | ||
| "cwd": "/home/user/project", | ||
| "hook_event_name": event, | ||
| "tool_name": tool, | ||
| "session_id": "sess-1", | ||
| "tool_use_id": "tu-1", | ||
| "tool_input": {"file_path": "src/main.rs"} | ||
| }) | ||
| .to_string() | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_pre_file_edit() { | ||
| let input = make_hook_input("PreToolUse", "Write"); | ||
| let events = CodeBuddyPreset.parse(&input, "t_test123456789a").unwrap(); | ||
| assert_eq!(events.len(), 1); | ||
| match &events[0] { | ||
| ParsedHookEvent::PreFileEdit(e) => { | ||
| assert_eq!(e.context.agent_id.tool, "codebuddy"); | ||
| assert_eq!(e.context.external_session_id, "sess-1"); | ||
| assert_eq!(e.context.trace_id, "t_test123456789a"); | ||
| assert_eq!(e.context.cwd, PathBuf::from("/home/user/project")); | ||
| assert_eq!( | ||
| e.file_paths, | ||
| vec![PathBuf::from("/home/user/project/src/main.rs")] | ||
| ); | ||
| } | ||
| _ => panic!("Expected PreFileEdit"), | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_post_file_edit() { | ||
| let input = make_hook_input("PostToolUse", "Write"); | ||
| let events = CodeBuddyPreset.parse(&input, "t_test123456789a").unwrap(); | ||
| assert_eq!(events.len(), 1); | ||
| match &events[0] { | ||
| ParsedHookEvent::PostFileEdit(e) => { | ||
| assert_eq!(e.context.agent_id.tool, "codebuddy"); | ||
| assert_eq!( | ||
| e.file_paths, | ||
| vec![PathBuf::from("/home/user/project/src/main.rs")] | ||
| ); | ||
| assert!(e.stream_source.is_some()); | ||
| if let Some(ts) = &e.stream_source { | ||
| assert_eq!(ts.format, StreamFormat::ClaudeJsonl); | ||
| assert_eq!( | ||
| ts.session_id, | ||
| generate_session_id("sess-1", "codebuddy") | ||
| ); | ||
| assert_eq!(ts.external_session_id, "sess-1"); | ||
| } | ||
| } | ||
| _ => panic!("Expected PostFileEdit"), | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_pre_bash_call() { | ||
| let input = make_hook_input("PreToolUse", "Bash"); | ||
| let events = CodeBuddyPreset.parse(&input, "t_test123456789a").unwrap(); | ||
| assert_eq!(events.len(), 1); | ||
| match &events[0] { | ||
| ParsedHookEvent::PreBashCall(e) => { | ||
| assert_eq!(e.context.agent_id.tool, "codebuddy"); | ||
| assert_eq!(e.tool_use_id, "tu-1"); | ||
| } | ||
| _ => panic!("Expected PreBashCall"), | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_post_bash_call() { | ||
| let input = make_hook_input("PostToolUse", "Bash"); | ||
| let events = CodeBuddyPreset.parse(&input, "t_test123456789a").unwrap(); | ||
| assert_eq!(events.len(), 1); | ||
| match &events[0] { | ||
| ParsedHookEvent::PostBashCall(e) => { | ||
| assert_eq!(e.context.agent_id.tool, "codebuddy"); | ||
| assert_eq!(e.tool_use_id, "tu-1"); | ||
| } | ||
| _ => panic!("Expected PostBashCall"), | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_session_id_from_filename() { | ||
| let input = json!({ | ||
| "transcript_path": "/home/user/.codebuddy/sessions/cb947e5b-246e-4253-a953-631f7e464c6b.jsonl", | ||
| "cwd": "/home/user/project", | ||
| "hook_event_name": "PostToolUse", | ||
| "tool_name": "Write", | ||
| "tool_input": {"file_path": "src/main.rs"} | ||
| }) | ||
| .to_string(); | ||
| let events = CodeBuddyPreset.parse(&input, "t_test123456789a").unwrap(); | ||
| match &events[0] { | ||
| ParsedHookEvent::PostFileEdit(e) => { | ||
| assert_eq!( | ||
| e.context.external_session_id, | ||
| "cb947e5b-246e-4253-a953-631f7e464c6b" | ||
| ); | ||
| } | ||
| _ => panic!("Expected PostFileEdit"), | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚩 Asymmetric casing for Bash tool classification vs file-edit tools
The CodeBuddy
classify_toolarm atsrc/commands/checkpoint_agent/bash_tool.rs:365-370includes both PascalCase and snake_case variants for file-edit tools ("Write" | "write_to_file" | "Edit" | "replace_in_file" | "MultiEdit" | "multi_edit" | "NotebookEdit" | "notebook_edit"), but only PascalCase"Bash"for the bash tool — no"bash"lowercase variant. If CodeBuddy can send tool names in snake_case (as suggested bywrite_to_file,replace_in_file,multi_edit,notebook_edit), then a lowercase"bash"would be classified asToolClass::Skipinstead ofToolClass::Bash, causing bash-based file changes to be missed by the stat-diff system. This depends on what tool names CodeBuddy actually emits, which I cannot verify from the codebase alone.Was this helpful? React with 👍 or 👎 to provide feedback.