Skip to content

Commit 176a063

Browse files
Alex Holmbergclaude
authored andcommitted
fix(agent): use monorepo analyzer to detect ALL projects instead of flat analysis
BREAKING CHANGE: Agent now correctly identifies all projects in monorepos ## Problem The agent's `analyze_project` tool was calling `analyze_project()` which returns a flat `ProjectAnalysis` structure with only ~5 detected services. Meanwhile, the CLI's `sync-ctl analyze .` was calling `analyze_monorepo()` which correctly returns `MonorepoAnalysis` with all 42 projects. This caused the agent to miss the vast majority of projects when analyzing monorepo codebases, leading to incomplete and misleading analysis results. ## Solution Changed `AnalyzeTool` to call `analyze_monorepo()` instead of `analyze_project()`. This returns `MonorepoAnalysis` which includes: - `is_monorepo`: Boolean flag for monorepo detection - `projects`: Array of ALL detected projects with full analysis - `technology_summary`: Aggregated languages, frameworks, databases - `metadata`: Analysis timing and confidence scores Each project in the array contains: - `name`: Project name (from package.json, Cargo.toml, etc.) - `path`: Relative path from monorepo root - `project_category`: Frontend/Backend/Api/Service/Library/etc. - `analysis`: Full `ProjectAnalysis` for that specific project ## Additional Improvements ### Smart Compression (compression.rs) - Added `failures` field to `extract_issues()` for lint tools (kubelint, hadolint, dclint, helmlint) - Updated `compress_analysis_output()` to handle both output types: - MonorepoAnalysis: Extracts project_names, languages, frameworks - ProjectAnalysis: Extracts from flat structure (languages at top level) - Added `services_detected` array with service names (not just count) ### Smart Retrieval (output_store.rs) - Added `OutputType` enum for type detection: - MonorepoAnalysis, ProjectAnalysis, LintResult, OptimizationResult, Generic - Added `detect_output_type()` function for routing retrieval - Added section-based queries for analyze_project outputs: - `section:summary` - Project overview - `section:projects` - List all projects with basic info - `section:languages` - All detected languages - `section:frameworks` - All detected frameworks - `section:services` - All detected services - `project:<name>` - Get specific project details - `language:<name>` - Language details with file counts - `framework:<name>` - Framework/technology details - `compact:true` - Compacted output (default) - File arrays are replaced with file_count to reduce context size ### Retrieval Tool (retrieve.rs) - Updated tool description to document all query options - Added examples for both lint tools and analyze_project outputs ### Error Display (streaming.rs) - Fixed nested error message display (ToolCallError: ToolCallError: ...) - Cleans up error messages for better readability ## Files Changed - src/agent/tools/analyze.rs - Use analyze_monorepo() (1 line) - src/agent/tools/compression.rs - Handle both output types - src/agent/tools/output_store.rs - Smart retrieval with section queries - src/agent/tools/retrieve.rs - Document query options - src/agent/ui/streaming.rs - Clean error messages ## Testing - All 884 tests pass - Build succeeds with only minor warnings (unrelated dead code) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6dd8e23 commit 176a063

5 files changed

Lines changed: 924 additions & 97 deletions

File tree

src/agent/tools/analyze.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ impl Tool for AnalyzeTool {
6161
self.project_path.clone()
6262
};
6363

64-
match crate::analyzer::analyze_project(&path) {
64+
// Use monorepo analyzer to detect ALL projects in monorepos
65+
// This returns MonorepoAnalysis with full project list instead of flat ProjectAnalysis
66+
match crate::analyzer::analyze_monorepo(&path) {
6567
Ok(analysis) => {
6668
let json_value = serde_json::to_value(&analysis)
6769
.map_err(|e| AnalyzeError(format!("Failed to serialize: {}", e)))?;

src/agent/tools/compression.rs

Lines changed: 135 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ fn extract_issues(output: &Value) -> Vec<Value> {
249249
"recommendations",
250250
"results",
251251
"diagnostics",
252+
"failures", // LintResult from kubelint, hadolint, dclint, helmlint
252253
];
253254

254255
for field in &issue_fields {
@@ -465,127 +466,170 @@ fn deduplicate_to_patterns(
465466
}
466467

467468
/// Compress analyze_project output specifically
469+
///
470+
/// Handles both:
471+
/// - MonorepoAnalysis: has "projects" array, "is_monorepo", "root_path"
472+
/// - ProjectAnalysis: flat structure with "languages", "technologies" at top level
473+
///
474+
/// For large analysis, returns a minimal summary and stores full data for retrieval.
468475
pub fn compress_analysis_output(output: &Value, config: &CompressionConfig) -> String {
469476
let raw_str = serde_json::to_string(output).unwrap_or_default();
470477
if raw_str.len() <= config.target_size_bytes {
471478
return raw_str;
472479
}
473480

474-
// Store full output
475-
let ref_id = output_store::store_output(output, "analyze");
481+
// Store full output for later retrieval
482+
let ref_id = output_store::store_output(output, "analyze_project");
476483

477-
// Extract key summary fields
478-
let mut compressed = json!({
484+
// Build a MINIMAL summary - just enough to understand the project
485+
let mut summary = json!({
479486
"tool": "analyze_project",
480-
"full_data_ref": ref_id,
481-
"retrieval_hint": format!("Use retrieve_output('{}') for full analysis", ref_id)
487+
"status": "ANALYSIS_COMPLETE",
488+
"full_data_ref": ref_id.clone()
482489
});
483490

484-
if let Some(obj) = output.as_object() {
485-
let compressed_obj = compressed.as_object_mut().unwrap();
486-
487-
// Always include these summary fields
488-
let summary_fields = [
489-
"name",
490-
"languages",
491-
"frameworks",
492-
"build_tools",
493-
"package_managers",
494-
"ci_cd",
495-
"containerization",
496-
"cloud_providers",
497-
"databases",
498-
];
491+
let summary_obj = summary.as_object_mut().unwrap();
499492

500-
for field in &summary_fields {
501-
if let Some(v) = obj.get(*field) {
502-
// For arrays, include count + first few items
503-
if let Some(arr) = v.as_array() {
504-
if arr.len() > 5 {
505-
let truncated: Vec<Value> = arr.iter().take(5).cloned().collect();
506-
compressed_obj.insert(
507-
field.to_string(),
508-
json!({
509-
"items": truncated,
510-
"total": arr.len(),
511-
"note": format!("+{} more (use retrieve_output)", arr.len() - 5)
512-
}),
513-
);
514-
} else {
515-
compressed_obj.insert(field.to_string(), v.clone());
516-
}
517-
} else {
518-
compressed_obj.insert(field.to_string(), v.clone());
519-
}
520-
}
493+
// Detect output type and extract accordingly
494+
let is_monorepo = output.get("projects").is_some() || output.get("is_monorepo").is_some();
495+
let is_project_analysis = output.get("languages").is_some() && output.get("analysis_metadata").is_some();
496+
497+
if is_monorepo {
498+
// MonorepoAnalysis structure
499+
if let Some(mono) = output.get("is_monorepo").and_then(|v| v.as_bool()) {
500+
summary_obj.insert("is_monorepo".to_string(), json!(mono));
521501
}
502+
if let Some(root) = output.get("root_path").and_then(|v| v.as_str()) {
503+
summary_obj.insert("root_path".to_string(), json!(root));
504+
}
505+
506+
if let Some(projects) = output.get("projects").and_then(|v| v.as_array()) {
507+
summary_obj.insert("project_count".to_string(), json!(projects.len()));
508+
509+
let mut all_languages: Vec<String> = Vec::new();
510+
let mut all_frameworks: Vec<String> = Vec::new();
511+
let mut project_names: Vec<String> = Vec::new();
522512

523-
// Handle dependencies specially - just counts
524-
if let Some(deps) = obj.get("dependencies") {
525-
if let Some(deps_obj) = deps.as_object() {
526-
let mut dep_summary = json!({});
527-
for (lang, dep_list) in deps_obj {
528-
if let Some(arr) = dep_list.as_array() {
529-
dep_summary[lang] = json!({ "count": arr.len() });
513+
for project in projects.iter().take(20) {
514+
if let Some(name) = project.get("name").and_then(|v| v.as_str()) {
515+
project_names.push(name.to_string());
516+
}
517+
if let Some(analysis) = project.get("analysis") {
518+
if let Some(langs) = analysis.get("languages").and_then(|v| v.as_array()) {
519+
for lang in langs {
520+
if let Some(name) = lang.get("name").and_then(|v| v.as_str()) {
521+
if !all_languages.contains(&name.to_string()) {
522+
all_languages.push(name.to_string());
523+
}
524+
}
525+
}
526+
}
527+
if let Some(fws) = analysis.get("frameworks").and_then(|v| v.as_array()) {
528+
for fw in fws {
529+
if let Some(name) = fw.get("name").and_then(|v| v.as_str()) {
530+
if !all_frameworks.contains(&name.to_string()) {
531+
all_frameworks.push(name.to_string());
532+
}
533+
}
534+
}
530535
}
531536
}
532-
compressed_obj.insert("dependencies_summary".to_string(), dep_summary);
533537
}
538+
539+
summary_obj.insert("project_names".to_string(), json!(project_names));
540+
summary_obj.insert("languages_detected".to_string(), json!(all_languages));
541+
summary_obj.insert("frameworks_detected".to_string(), json!(all_frameworks));
542+
}
543+
} else if is_project_analysis {
544+
// ProjectAnalysis flat structure - languages/technologies at top level
545+
if let Some(root) = output.get("project_root").and_then(|v| v.as_str()) {
546+
summary_obj.insert("project_root".to_string(), json!(root));
547+
}
548+
if let Some(arch) = output.get("architecture_type").and_then(|v| v.as_str()) {
549+
summary_obj.insert("architecture_type".to_string(), json!(arch));
550+
}
551+
if let Some(proj_type) = output.get("project_type").and_then(|v| v.as_str()) {
552+
summary_obj.insert("project_type".to_string(), json!(proj_type));
534553
}
535554

536-
// Handle file structure - depth-limited
537-
if let Some(structure) = obj.get("structure") {
538-
compressed_obj.insert(
539-
"structure_note".to_string(),
540-
json!("Full structure available via retrieve_output"),
541-
);
542-
// Include just top-level directories
543-
if let Some(dirs) = structure.get("directories").and_then(|v| v.as_array()) {
544-
let top_dirs: Vec<&str> = dirs
545-
.iter()
546-
.filter_map(|v| v.as_str())
547-
.filter(|s| !s.contains('/') || s.matches('/').count() == 1)
548-
.take(10)
549-
.collect();
550-
compressed_obj.insert("top_directories".to_string(), json!(top_dirs));
555+
// Extract languages (at top level)
556+
if let Some(langs) = output.get("languages").and_then(|v| v.as_array()) {
557+
let names: Vec<&str> = langs
558+
.iter()
559+
.filter_map(|l| l.get("name").and_then(|n| n.as_str()))
560+
.collect();
561+
summary_obj.insert("languages_detected".to_string(), json!(names));
562+
}
563+
564+
// Extract technologies (at top level)
565+
if let Some(techs) = output.get("technologies").and_then(|v| v.as_array()) {
566+
let names: Vec<&str> = techs
567+
.iter()
568+
.filter_map(|t| t.get("name").and_then(|n| n.as_str()))
569+
.collect();
570+
summary_obj.insert("technologies_detected".to_string(), json!(names));
571+
}
572+
573+
// Extract services (include names, not just count)
574+
if let Some(services) = output.get("services").and_then(|v| v.as_array()) {
575+
summary_obj.insert("services_count".to_string(), json!(services.len()));
576+
// Include service names so agent knows what microservices exist
577+
let service_names: Vec<&str> = services
578+
.iter()
579+
.filter_map(|s| s.get("name").and_then(|n| n.as_str()))
580+
.collect();
581+
if !service_names.is_empty() {
582+
summary_obj.insert("services_detected".to_string(), json!(service_names));
551583
}
552584
}
553585
}
554586

555-
// Build summary for session registry
556-
let summary_parts: Vec<String> = output
557-
.as_object()
558-
.map(|obj| {
559-
let mut parts = Vec::new();
560-
if let Some(langs) = obj.get("languages").and_then(|v| v.as_array()) {
561-
parts.push(format!("{} languages", langs.len()));
562-
}
563-
if let Some(fws) = obj.get("frameworks").and_then(|v| v.as_array()) {
564-
parts.push(format!("{} frameworks", fws.len()));
565-
}
566-
parts
567-
})
568-
.unwrap_or_default();
569-
let summary_str = if summary_parts.is_empty() {
570-
"Project structure and dependencies".to_string()
571-
} else {
572-
summary_parts.join(", ")
573-
};
587+
// CRITICAL: Include retrieval instructions prominently
588+
summary_obj.insert(
589+
"retrieval_instructions".to_string(),
590+
json!({
591+
"message": "Full analysis stored. Use retrieve_output with queries to get specific sections.",
592+
"ref_id": ref_id,
593+
"available_queries": [
594+
"section:summary - Project overview",
595+
"section:languages - All detected languages",
596+
"section:frameworks - All detected frameworks/technologies",
597+
"section:services - All detected services",
598+
"language:<name> - Details for specific language (e.g., language:Rust)",
599+
"framework:<name> - Details for specific framework"
600+
],
601+
"example": format!("retrieve_output('{}', 'section:summary')", ref_id)
602+
}),
603+
);
604+
605+
// Build session summary
606+
let project_count = output
607+
.get("projects")
608+
.and_then(|v| v.as_array())
609+
.map(|a| a.len())
610+
.unwrap_or(1);
611+
let summary_str = format!(
612+
"{} project(s), {} bytes stored",
613+
project_count,
614+
raw_str.len()
615+
);
574616

575617
// Register in session registry
576618
output_store::register_session_ref(
577619
&ref_id,
578-
"analyze",
579-
"Project analysis (languages, frameworks, dependencies, structure)",
620+
"analyze_project",
621+
"Full project analysis (use section queries to retrieve specific data)",
580622
&summary_str,
581623
raw_str.len(),
582624
);
583625

584-
let mut result = serde_json::to_string_pretty(&compressed).unwrap_or(raw_str);
585-
586-
// Append ALL session refs so agent always knows what's available
587-
result.push_str(&output_store::format_session_refs_for_agent());
588-
result
626+
// Return minimal JSON
627+
serde_json::to_string_pretty(&summary).unwrap_or_else(|_| {
628+
format!(
629+
r#"{{"tool":"analyze_project","status":"STORED","full_data_ref":"{}","message":"Analysis complete. Use retrieve_output('{}', 'section:summary') to view."}}"#,
630+
ref_id, ref_id
631+
)
632+
})
589633
}
590634

591635
#[cfg(test)]

0 commit comments

Comments
 (0)